According the multiBarChart doc it should be possible to set yScale on multiBarChart as yScale should be inherited method.
I'd like to scale it to logarithmic scale. I got this working with lineChart.yScale(d3.scale.log()), but for multiBarChart, calling multiBarChart.yScale(d3.scale.log()), results in yScale is not a function.
Is there any other way to log scale multiBarChart's y axis?
Thanks
I solved this whit following approach, it's controlled by shouldLogScale var:
nv.addGraph(() => {
this.chart = nv.models.multiBarChart()
...
.y(d => {
const y = parseFloat(d.y);
return this.shouldLogScale ? Math.log10(y <= 0 ? 1 : y) : y;
});
if(this.shouldLogScale) {
this.logScale();
} else {
this.chart.yAxis.tickFormat(d3.format(`,.${this.precision}f`));
}
...
});
When initialising the chart: I modify y values, force y (chart.forceY) start and end values (map them to their log10 range), analogically I set chart.yAxis.tickValues. Lastly I map ticks (chart.yAxis.tickValues) to their original values. Just make sure to comply domain of the log function.
logScale method:
private logScale() {
this.chart.forceY([1, 180]
.map(v => Math.log10(v)));
this.chart.yAxis.tickValues([1, 3, 5, 10, 20, 50, 100, 180]
.map(v => Math.log10(v)));
this.chart.yAxis.tickFormat(d => Math.pow(10, d).toFixed(this.precision));
}
Maybe I could use a map of y domain and range values to avoid using log and pow functions in logScale method.
Related
I've got this kind of non uniforme datas :
[{'time':0,'sum':0},{'time':600,'sum':2},{'time':700,'sum':4},{'time':1200,'sum':1},{'time':1300,'sum':3},{'time':1600,'sum':1},{'time':2000,'sum':0}];
"time" is on x axis and "sum" on y axis. If I make an area, I've got these shapes (curved in red, not curved in white) :
https://codepen.io/kilden/pen/podadRW
But the meaning of this is wrong. I have to interpret the "missing" datas. A bit like the "kernel density estimation" charts (example here :https://bl.ocks.org/mbostock/4341954) where values are at zero when there is no data, but there is a "fall off" around the point with data. (a gaussian curve)
It's hard to explain with words (and English is not my mother tongue). So I did this second codepen to show the idea of the shape. The area in red is the shape I want (White one is the reference of the first codepen) :
https://codepen.io/kilden/pen/VwrQrbo
I wonder if there is a way to make this kind of cumulative gaussian curves with a (hidden?) d3 function or a trick function ?
A. Your cheating yourself when you use the Epanechnikov kernel, evaluate these on a rather coarse grid and make a smooth line interpolation so that it looks gaussian. Just take a gaussian kernel to start with.
B. You're comparing apples and oranges. A kernel density estimate is an estimate of a probability density that cannot be compared to the count of observations. The integral of the kernel density estimate is always equal to 1. You can scale the estimate by the total count of observations, but even then your curve would not "stick" to the point, since the kernel spreads the observation away from the point.
C. What comes close to what you want to achieve is implemented below. Use a gaussian curve which is 1 at 0, i. e. without the normalizing factor and the rescaling by the bandwidth. The bandwidth now scales only the width of the curve but not its height. Then use your original data array and add up all these curves with the weight sum from your data array.
This will match your data points when there are no clustered observations. Naturally, when two observations are close to each other, their individual gaussian curves can add up to something bigger than each observation.
DISCLAIMER: As I already pointed out in the comments, this just produces a pretty chart and is mathematical nonsense. I strongly recommend working out the mathematics behind what it is you really want to achieve. Only then you should make a chart of your data.
const WIDTH = 600;
const HEIGHT = 150;
const BANDWIDTH = 25;
let data = [
{time: 0, sum: 0},
{time: 200, sum: 4},
{time: 250, sum: 2},
{time: 500, sum: 1},
{time: 600, sum: 2},
{time: 1500, sum: 5},
{time: 1600, sum: 4},
{time: 1800, sum: 3},
{time: 2000, sum: 0},
];
// svg
const svg = d3.select("body")
.append("svg")
.attr("width", WIDTH)
.attr("height", HEIGHT)
.style("background-color", "grey");
// scales
const x_scale = d3.scaleLinear()
.domain([0, 2000])
.range([0, WIDTH]);
const y_scale = d3.scaleLinear()
.range([HEIGHT, 0]);
// curve interpolator
const line = d3.line()
.x(d => x_scale(d.time))
.y(d => y_scale(d.sum))
.curve(d3.curveMonotoneX);
const grid = [...Array(2001).keys()];
svg.append("path")
.style("fill", "rgba(255,255,255,0.4");
// gaussian "kernel"
const gaussian = k => x => Math.exp(-0.5 * x / k * x / k);
// similar to kernel density estimate
function estimate(kernel, grid) {
return obs => grid.map(x => ({time: x, sum: d3.sum(obs, d => d.sum * kernel(x - d.time))}));
}
function render(data) {
data = data.sort((a, b) => a.time - b.time);
// make curve estimate with these kernels
const curve_estimate = estimate(gaussian(BANDWIDTH), grid)(data);
// set endpoints to zero for area plot
curve_estimate[0].sum = 0;
curve_estimate[curve_estimate.length-1].sum = 0;
y_scale.domain([0, 1.5 * Math.max(d3.max(data, d => d.sum), d3.max(curve_estimate, d => d.sum))]);
svg.select("path")
.attr("d", line(curve_estimate))
const circles = svg.selectAll("circle")
.data(data, d => d.time)
.join(
enter => enter.append("circle")
.attr("fill", "red"),
update => update.attr("fill", "white")
)
.attr("r", 2)
.attr("cx", d => x_scale(d.time))
.attr("cy", d => y_scale(d.sum));
}
render(data);
function randomData() {
data = [];
for (let i = 0; i < 10; i++) {
data.push({
time: Math.round(2000 * Math.random()),
sum: Math.round(10 * Math.random()) + 1,
});
}
render(data);
}
function addData() {
data.push({
time: Math.round(2000 * Math.random()),
sum: Math.round(10 * Math.random()) + 1,
});
render(data);
}
d3.select("#random_data").on("click", randomData);
d3.select("#add_data").on("click", addData);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<button id="random_data">
Random Data
</button>
<button id="add_data">
Add data point
</button>
Here is a jsbin of what I have so far.
My sine wave is not going to the y value of 1 or -1, i.e the amplitude.
My yScale is defined like this:
const yScaleAxis = d3.scale.linear()
.domain([-1, 1])
.range([radius, -radius]);
And I am creating the values like this:
const xValues = [0, 1.57, 3.14, 4.71, 6.28]; // 0 to 2PI
const sineData = xValues.map((x) => {
console.log(Math.sin(x));
return {x: x, y: Math.sin(x)};
});
The values for y are logged as:
0
0.9999996829318346
0.0015926529164868282
-0.999997146387718
-0.0031853017931379904
I then use the scale to set the values:
const sine = d3.svg.line()
.interpolate('basis')
.x( (d) => {return xScaleAxis(d.x);})
.y( (d) => {return yScaleAxis(d.y);});
circleGroup.append('path')
.datum(sineData)
.attr('class', 'sine-curve')
.attr('d', sine);
But as you can see in the jsbin the amplitude of the sine wave is not reaching 1 or -1 and I am not sure why.
Change the line interpolation method to monotone, basis corresponds to a B-spline
More info about the interpolation options provided by d3
I'm hoping to do a filter of polygons smaller than a certain area. can this be achieved using rethinkdb without calculating the area beforehand?
I got this working with some of my own data, but a small variation of this should work. Here I'm providing a function to filter which calculates the area and returns true or false depending on it being greated than REQ_AREA. You could also create an index that preforms this calculation automatically by passing a anonymous function to .createIndex('area', function(doc) { ... } and then doing a getAll using that index.
The .slice and prepend|appending is just rotating the x and y coords for simpler multiplication mapping.
Here's out the ReQL area calculation looks like though:
r.db('geography').table('area_polygons').filter((doc) => {
// Retrieve just the points of the polygon
var coords = doc('polygon').toGeojson()('coordinates').nth(0)
var x_coords = coords.map((point) => {return point.nth(0)}).coerceTo('array');
var y_coords = coords.map((point) => {return point.nth(1)}).coerceTo('array');
// Move item from beginning to end
y_coords = y_coords.append(y_coords.slice(0,1).nth(0)).deleteAt(0);
var x = r.map(x_coords, y_coords, (l, r) => { return l.mul(r) }).sum();
// Reset y and now move first x item to end
y_coords = y_coords.prepend(y_coords.slice(-1).nth(0)).deleteAt(-1);
x_coords = x_coords.append(x_coords.slice(0,1).nth(0)).deleteAt(0);
var y = r.map(x_coords, y_coords, (l, r) => { return l.mul(r) }).sum();
// Return area
return x.sub(y).div(2) > REQ_SIZE ? true : false;
})
What's the best way of drawing multiple horizontal lines and labels for a simple line graph in either ChartJS or D3? I know that I could draw these as individual lines and then do a text overlay but I'm wondering if there is a simpler solution. Ideally I'd be able to create each of the labels below as one unit and move it anywhere.
If this is simpler in another JS graph library then feel free suggest.
Example below
To do it with Chart.js you have to extend the line chart
Chart.types.Line.extend({
name: "LineAlt",
initialize: function (data) {
// it's easier to programmatically update if you store the raw data in the object (vs. storing the geometric data)
this.marks = data.marks;
this.marks.xStart = Number(data.labels[0]);
this.marks.xStep = data.labels[1] - data.labels[0];
// make sure all our x labels are uniformly apart
if (!data.labels.every(function (e, i, arr) { return !i || ((e - arr[i - 1]) === this.marks.xStep); }, this))
throw "labels must be uniformly spaced";
Chart.types.Line.prototype.initialize.apply(this, arguments);
},
draw: function () {
Chart.types.Line.prototype.draw.apply(this, arguments);
// save existing context properties
var self = this;
var ctx = self.chart.ctx;
var scale = self.scale;
ctx.save();
// line properties
ctx.lineWidth = 1;
ctx.fillStyle = "#666";
ctx.strokeStyle = "#666";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.font = scale.font;
// draw marks
self.marks.forEach(function (mark) {
// assuming that the marks are always within the data range
var y = scale.calculateY(mark.y);
var x1 = scale.calculateX((mark.x1 - self.marks.xStart) / self.marks.xStep);
var x2 = scale.calculateX((mark.x2 - self.marks.xStart) / self.marks.xStep);
// draw line
ctx.beginPath();
ctx.moveTo(x1, y);
ctx.lineTo(x2, y);
// draw edges
ctx.moveTo(x1, y + 10);
ctx.lineTo(x1, y - 10);
ctx.moveTo(x2, y + 10);
ctx.lineTo(x2, y - 10);
ctx.stroke();
// draw text
ctx.fillText(mark.label, (x1 + x2) / 2, y + scale.fontSize * 1.5);
})
ctx.restore();
},
});
You pass in the data for drawing the lines like so
var data = {
...
marks: [
{
x1: 1.5,
x2: 3.5,
y: 50,
label: 'Label1'
},
{
x1: 5,
x2: 7,
y: 60,
label: 'Label2'
}
]
};
and you create the chart using this extended chart type
var myLineChart = new Chart(ctx).LineAlt(data);
You can update the lines like this
myLineChart.marks[0].y = 80;
myLineChart.marks[0].x1 = 9;
myLineChart.marks[0].x2 = 10;
and then call
myLineChart.update();
to reflect those changes on the canvas
Caveats
The (x axis) labels should be numeric and uniformly spaced.
The lines should be within the scale range of the y axis (alternatively you can do a scaleOverride to set the scale parameters so that the lines are within the y scale range)
Fiddle - http://jsfiddle.net/en92k763/2/
I am new to d3, learning a lot. I have an issue I cannot find an example for:
I have two y axes with positive and negative values with vastly different domains, one being large dollar amounts the other being percentages.
The resulting graph from cobbling together examples looks really awesome with one slight detail, the zero line for each y axis is in a slightly different position. Does anyone know of a way in d3 to get the zero line to be at the same x position?
I would like these two yScales/axes to share the same zero line
// define yScale
var yScale = d3.scale.linear()
.range([height, 0])
.domain(d3.extent(dataset, function(d) { return d.value_di1; }))
;
// define y2 scale
var yScale2 = d3.scale.linear()
.range([height, 0])
.domain(d3.extent(dataset, function(d) { return d.calc_di1_di2_percent; }))
;
Here is a link to a jsfiddle with sample data:
http://jsfiddle.net/jglover/XvBs3/1/
(the x-axis ticks look horrible in the jsfiddle example)
In general, there's unfortunately no way to do this neatly. D3 doesn't really have a concept of several things lining up and therefore no means of accomplishing it.
In your particular case however, you can fix it quite easily by tweaking the domain of the second y axis:
.domain([d3.min(dataset, function(d) { return d.calc_di1_di2_percent; }), 0.7])
Complete example here.
To make the 0 level the same position, a strategy is to equalize the length/proportion of the y axes.
Here are the concepts to the solution below:
The alignment of baseline depends on the length of the y axes.
To let all value shown in the bar, we need to extend the shorter side of the dimension, which compares to the other, to make the proportion of the two axes equal.
example:
// dummy data
const y1List = [-1000, 120, -130, 1400],
y2List = [-0.1, 0.2, 0.3, -0.4];
// get proportion of the two y axes
const totalY1Length = Math.abs(d3.min(y1List)) + Math.abs(d3.max(y1List)),
totalY2Length = Math.abs(d3.min(y2List)) + Math.abs(d3.max(y2List)),
maxY1ToY2 = totalY2Length * d3.max(y1List) / totalY1Length,
minY1ToY2 = totalY2Length * d3.min(y1List) / totalY1Length,
maxY2ToY1 = totalY1Length * d3.max(y2List) / totalY2Length,
minY2ToY1 = totalY1Length * d3.min(y2List) / totalY2Length;
// extend the shorter side of the upper dimension with corresponding value
let maxY1Domain = d3.max(y1List),
maxY2Domain = d3.max(y2List);
if (maxY1ToY2 > d3.max(y2List)) {
maxY2Domain = d3.max(y2List) + maxY1ToY2 - d3.max(y2List);
} else {
maxY1Domain = d3.max(y1List) + maxY2ToY1 - d3.max(y1List);
}
// extend the shorter side of the lower dimension with corresponding value
let minY1Domain = d3.min(y1List),
minY2Domain = d3.min(y2List);
if (minY1ToY2 < d3.min(y2List)) {
minY2Domain = d3.min(y2List) + minY1ToY2 - d3.min(y2List);
} else {
minY1Domain = d3.min(y1List) + minY2ToY1 - d3.min(y1List);
}
// finally, we get the domains for our two y axes
const y1Domain = [minY1Domain, maxY1Domain],
y2Domain = [minY2Domain, maxY2Domain];