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>
I am new to D3.js, pardon me if my understanding is wrong.
I have an equation for a straight line in a log-log plot, Log(Y)=Log(C) + Log(X), C is constant and user defined.
Is there a way to draw the straight line in D3 purely from the equation?
Thank you.
No this isn't possible exactly as you'd like in D3. D3 is less about mathmatical calculation & visualization compared to other tools (R, MatLab) and is more about binding data sets to DOM and handling animation between data sets.
That being said, if you calculate the X and Y values for the equation then you can plot those values easily. I've seen D3 used like this, with input boxes for C and then plotting across a range.
Following your comment here's an example:
const C = 1;
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 1000]); // pixels
const yScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 1000]);
const line = d3.line()
.x(d => xScale(d))
.y(d => yScale(Math.log(C) + Math.log(d)));
const values = [0, 50, 100];
d3.selectAll("path")
.datum(values)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("d", line);
Note that the key to pumping in the equation is defining how to generate the y value given the x in the line generator, covered by this line:
.y(d => yScale(Math.log(C) + Math.log(d)))
In the d3.v4 documentation the following is stated:
To generate ticks every fifteen minutes with a time scale, say:
axis.tickArguments([d3.timeMinute.every(15)]);
Is there a similar approach that can be used with values other than time? I am plotting sine and cosine curves, so I'd like the ticks to begin at -2*Math.PI, end at 2*Math.PI, and between these values I'd like a tick to occur every Math.PI/2. I could, of course, explicitly compute the tick values and supply them to the tickValue method; however, if there is a simpler way to accomplish this, as in the time-related example quoted above, I'd prefer to use that.
Setting the end ticks and specifying the precise space of the ticks in a linear scale is a pain in the neck. The reason is that D3 axis generator was created in such a way that the ticks are automatically generated and spaced. So, what is handy for someone who doesn't care too much for customisation can be a nuisance for those that want a precise customisation.
My solution here is a hack: create two scales, one linear scale that you'll use to plot your data, and a second scale, that you'll use only to make the axis and whose values you can set at your will. Here, I choose a scalePoint() for the ordinal scale.
Something like this:
var realScale = d3.scaleLinear()
.range([10,width-10])
.domain([-2*Math.PI, 2*Math.PI]);
var axisScale = d3.scalePoint()
.range([10,width-10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0",
"0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
Don't mind the \u03c0, that's just π (pi) in Unicode.
Check this demo, hover over the circles to see their positions:
var width = 500,
height = 150;
var data = [-2, -1, 0, 0.5, 1.5];
var realScale = d3.scaleLinear()
.range([10, width - 10])
.domain([-2 * Math.PI, 2 * Math.PI]);
var axisScale = d3.scalePoint()
.range([10, width - 10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0", "0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var circles = svg.selectAll("circle").data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("fill", "teal")
.attr("cy", 50)
.attr("cx", function(d) {
return realScale(d * Math.PI)
})
.append("title")
.text(function(d) {
return "this circle is at " + d + " \u03c0"
});
var axis = d3.axisBottom(axisScale);
var gX = svg.append("g")
.attr("transform", "translate(0,100)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I was able to implement an x axis in units of PI/2, under program control (not manually laid out), by targetting the D3 tickValues and tickFormat methods. The call to tickValues sets the ticks at intervals of PI/2. The call to tickFormat generates appropriate tick labels. You can view the complete code on GitHub:
https://github.com/quantbo/sine_cosine
My solution is to customise tickValues and tickFormat. Only 1 scale is needed, and delegate d3.ticks function to give me the new tickValues that are proportional to Math.PI.
const piChar = String.fromCharCode(960);
const tickFormat = val => {
const piVal = val / Math.PI;
return piVal + piChar;
};
const convertSIToTrig = siDomain => {
const trigMin = siDomain[0] / Math.PI;
const trigMax = siDomain[1] / Math.PI;
return d3.ticks(trigMin, trigMax, 10).map(v => v * Math.PI);
};
const xScale = d3.scaleLinear().domain([-Math.PI * 2, Math.PI * 2]).range([0, 600]);
const xAxis = d3.axisBottom(xScale)
.tickValues(convertSIToTrig(xScale.domain()))
.tickFormat(tickFormat);
This way if your xScale's domain were changed via zoom/pan, the new tickValues are nicely generated with smaller/bigger interval
I am attempting to simplify a d3 map on zoom, and I am using this example as a starting point. However, when I replace the json file in the example with my own (http://weather-bell.com/res/nws_regions.topojson), I get a tiny upside-down little map.
Here is my jsfiddle: http://jsfiddle.net/8ejmH
code:
var width = 900,
height = 500;
var chesapeake = [-75.959, 38.250];
var scale,
translate,
visibleArea, // minimum area threshold for points inside viewport
invisibleArea; // minimum area threshold for points outside viewport
var simplify = d3.geo.transform({
point: function (x, y, z) {
if (z < visibleArea) return;
x = x * scale + translate[0];
y = y * scale + translate[1];
if (x >= 0 && x <= width && y >= 0 && y <= height || z >= invisibleArea) this.stream.point(x, y);
}
});
var zoom = d3.behavior.zoom()
.size([width, height])
.on("zoom", zoomed);
// This projection is baked into the TopoJSON file,
// but is used here to compute the desired zoom translate.
var projection = d3.geo.mercator().translate([0, 0])
var canvas = d3.select("#map").append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(simplify)
.context(context);
d3.json("http://weather-bell.com/res/nws_regions.topojson", function (error, json) {
canvas.datum(topojson.mesh(topojson.presimplify(json)))
.call(zoomTo(chesapeake, 0.05).event)
.transition()
.duration(5000)
.each(jump);
});
function zoomTo(location, scale) {
var point = projection(location);
return zoom.translate([width / 2 - point[0] * scale, height / 2 - point[1] * scale])
.scale(scale);
}
function zoomed(d) {
translate = zoom.translate();
scale = zoom.scale();
visibleArea = 1 / scale / scale;
invisibleArea = 200 * visibleArea;
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
}
function jump() {
var t = d3.select(this);
(function repeat() {
t = t.transition()
.call(zoomTo(chesapeake, 100).event)
.transition()
.call(zoomTo(chesapeake, 0.05).event)
.each("end", repeat);
})();
}
My guess is that the topojson file I am using already has the projection built in, so I should be using a null projection in d3.
The map renders properly if I do not use a projection at all: (http://jsfiddle.net/KQfrK/1/) - but then I cannot simplify on zoom.
I feel like I am missing something basic... perhaps I just need to somehow rotate and zoom into the map in my first fiddle.
Either way, I'd appreciate some help. Been struggling with this one.
Edit: I used QGIS to save the geojson file with a "EPSG:3857 - WGS 84 / Pseudo Mercator" projection.
However, when I convert this to topojson with the topojson command-line utility and then display it with D3 using the same code as above I get a blank screen.
Should I specify the projection within the topojson command-line utility? I tried to do that but I got an error message:
topojson --projection EPSG:3857 E:\gitstore\public\res\nws.geojson -o E:\gitstore\public\res\nws.topojson --id-property NAME
[SyntaxError: Unexpected token :]
The TopoJSON file doesn't have a projection built-in, you're simply using the default projection when you don't specify one (which is albersUsa, see the documentation). You can retrieve this projection by calling d3.geo.projection() without an argument. Then you can modify this projection in the usual way for zoom etc.
I set up this fiddle using the Mercator projection and I took a different approach to zooming in and out based on this block, which to me was a simpler approach. I have a feeling that there was an issue in the zoomTo function in the translate bit, but I could exactly what it was. So I replaced with the code below and included a recursive call:
function clicked(k) {
if (typeof k === 'undefined') k = 8;
g.transition()
.duration(5000)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -projection(chesapeake)[0] + "," + -projection(chesapeake)[1] + ")")
.each("end", function () {
(k === 8) ? k = 1 : k = 8;
clicked(k);
});
I am looking to implement ggplot2 type of graphs using d3.js library for interactivey purpose. I love ggplot2 but users are interested in interactive graphs. I've been exploring d3.js library and there seems to be lots of different graph capability but I really did not see any statistical graphs like linear line, forecast etc. Given a scatter plot, is it possible to also add linear line to the graph.
I have this sample script that draws scatter plot. How would I add linear line to this graph in d3.js?
// data that you want to plot, I've used separate arrays for x and y values
var xdata = [5, 10, 15, 20],
ydata = [3, 17, 4, 6];
// size and margins for the chart
var margin = {top: 20, right: 15, bottom: 60, left: 60}
, width = 960 - margin.left - margin.right
, height = 500 - margin.top - margin.bottom;
// x and y scales, I've used linear here but there are other options
// the scales translate data values to pixel values for you
var x = d3.scale.linear()
.domain([0, d3.max(xdata)]) // the range of the values to plot
.range([ 0, width ]); // the pixel range of the x-axis
var y = d3.scale.linear()
.domain([0, d3.max(ydata)])
.range([ height, 0 ]);
// the chart object, includes all margins
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
// the main object where the chart and axis will be drawn
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
// draw the graph object
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(ydata) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d); } ) // translate y value to a pixel
.attr("cx", function (d,i) { return x(xdata[i]); } ) // translate x value
.attr("r", 10) // radius of circle
.style("opacity", 0.6); // opacity of circle
To add a line to your plot, all that you need to do is to append some line SVGs to your main SVG (chart) or to the group that contains your SVG elements (main).
Your code would look something like the following:
chart.append('line')
.attr('x1',x(10))
.attr('x2',x(20))
.attr('y1',y(5))
.attr('y2',y(10))
This would draw a line from (10,5) to (20,10). You could similarly create a data set for your lines and append a whole bunch of them.
One thing you might be interested in is the SVG path element. This is more common for lines than drawing one straight segment at a time. The documentation is here.
On another note you may find it easier to work with data in d3 if you create it all as one object. For example, if your data was in the following form:
data = [{x: 5, y:3}, {x: 10, y:17}, {x: 15, y:4}, {x: 20, y:6}]
You could use:
g.selectAll("scatter-dots")
.data(ydata) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d.y); } ) //set y
.attr("cx", function (d,i) { return x(d.x); } ) //set x
This would eliminate potentially messy indexing if your data gets more complex.
UPDATE: Here is the relevant block: https://bl.ocks.org/HarryStevens/be559bed98d662f69e68fc8a7e0ad097
I wrote this function to calculate a linear regression from data, formatted as JSON.
It takes 5 parameters:
1) Your data
2) The column name of the data plotted on your x-axis
3) The column name of the data plotted on your y-axis
4) The minimum value of your x-axis
5) The minimum value of your y-axis
I got the formula for calculating a linear regression from http://classroom.synonym.com/calculate-trendline-2709.html
function calcLinear(data, x, y, minX, minY){
/////////
//SLOPE//
/////////
// Let n = the number of data points
var n = data.length;
var pts = [];
data.forEach(function(d,i){
var obj = {};
obj.x = d[x];
obj.y = d[y];
obj.mult = obj.x*obj.y;
pts.push(obj);
});
// Let a equal n times the summation of all x-values multiplied by their corresponding y-values
// Let b equal the sum of all x-values times the sum of all y-values
// Let c equal n times the sum of all squared x-values
// Let d equal the squared sum of all x-values
var sum = 0;
var xSum = 0;
var ySum = 0;
var sumSq = 0;
pts.forEach(function(pt){
sum = sum + pt.mult;
xSum = xSum + pt.x;
ySum = ySum + pt.y;
sumSq = sumSq + (pt.x * pt.x);
});
var a = sum * n;
var b = xSum * ySum;
var c = sumSq * n;
var d = xSum * xSum;
// Plug the values that you calculated for a, b, c, and d into the following equation to calculate the slope
// m = (a - b) / (c - d)
var m = (a - b) / (c - d);
/////////////
//INTERCEPT//
/////////////
// Let e equal the sum of all y-values
var e = ySum;
// Let f equal the slope times the sum of all x-values
var f = m * xSum;
// Plug the values you have calculated for e and f into the following equation for the y-intercept
// y-intercept = b = (e - f) / n = (14.5 - 10.5) / 3 = 1.3
var b = (e - f) / n;
// return an object of two points
// each point is an object with an x and y coordinate
return {
ptA : {
x: minX,
y: m * minX + b
},
ptB : {
y: minY,
x: (minY - b) / m
}
}
}