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
}
}
}
Related
I'm trying to draw LineStrings that follow various latitude segments, however the built-in geodesic arc interpolation doesn't seem to be drawing arcs that follow latitude. My question is: why not and how do I achieve this?
Here is my result:
And my code:
const width = 500;
const height = 500;
const scale = 200;
const svg = d3.select('svg').attr("viewBox", [0, 0, width, height]);
const projection = d3.geoStereographic().rotate([0, -90]).precision(0.1).clipAngle(90.01).scale(scale).translate([width / 2, height / 2]);
const path = d3.geoPath(projection);
const graticule = d3.geoGraticule().stepMajor([15, 15]).stepMinor([0, 0])();
svg
.append("path")
.datum(graticule)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", '#000000')
.attr("stroke-width", 0.3)
.attr("stroke-opacity", 1);
let curve = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[-180, 15],
[-90, 15]
]
}
}
svg
.append("path")
.datum(curve)
.attr("d", path)
.attr('fill-opacity', 0)
.attr('stroke', 'red')
.attr("stroke-width", 1)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
My fiddle: https://jsfiddle.net/jovrtn/komfxycz/
D3 is fairly unique when it comes to geographic data: it uses spherical math (which despite many benefits, does lead to some challenges). d3.geoPath samples a line segment between two points so that the path follows a great circle (the shortest path between two points on a globe). Parallels do not follow great circle distances, so your path does not follow the parallel.
The behavior you are looking for requires us to draw a line between two points of latitude longitude as though they were Carteisan, even though they are not, and then preserve the points along that line when applying the stereographic projection.
When using an cylindrical projection the solution is easy enough, don't sample between points on a line. This answer contains such a solution.
This doesn't help with a stereographic projection - the linked approach would just result in a straight line between the first point and end point instead of a curved line along the parallel.
A solution is to manually sample points between start and end as though the data were Cartesian, then treat them as 3D in order to project them with a stereographic projection. This results in a path that follows parallels where start and end have the same north/south value. How frequently you sample reduces/eliminates the effect of great circle distances when using d3.geoPath.
In my solution I'm going to use two d3 helper functions:
d3.geoDistance which measures the distance between two lat long pairs in radians.
d3.interpolate which creates an interpolation function between two values.
let sample = function(line) {
let a = line.geometry.coordinates[0]; // first point
let b = line.geometry.coordinates[1]; // end point
let distance = d3.geoDistance(a, b); // in radians
let precision = 1*Math.PI/180; // sample every degree.
let n = Math.ceil(distance/precision); // number of sample points
let interpolate = d3.interpolate(a,b) // create an interpolator
let points = []; // sampled points.
for(var i = 0; i <= n; i++) { // sample n+1 times
points.push([...interpolate(i/n)]); // interpolate a point
}
line.geometry.coordinates = points; // replace the points in the feature
}
The above assumes a line with two points/one segment, naturally if your lines are more complex than that you'll need to adjust this. It's intended just as a starting point.
And in action:
const width = 500;
const height = 500;
const scale = 200;
const svg = d3.select('svg').attr("viewBox", [0, 0, width, height]);
const projection = d3.geoStereographic().rotate([0, -90]).precision(0.1).clipAngle(90.01).scale(scale).translate([width / 2, height / 2]);
const path = d3.geoPath(projection);
const graticule = d3.geoGraticule().stepMajor([15, 15]).stepMinor([0, 0])();
svg
.append("path")
.datum(graticule)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", '#000000')
.attr("stroke-width", 0.3)
.attr("stroke-opacity", 1);
let curve = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[-180, 15],
[-90, 15]
]
}
}
svg
.append("path")
.datum(curve)
.attr("d", path)
.attr('fill-opacity', 0)
.attr('stroke', 'red')
.attr("stroke-width", 1)
let sample = function(line) {
let a = line.geometry.coordinates[0];
let b = line.geometry.coordinates[1];
let distance = d3.geoDistance(a, b); // in radians
let precision = 5*Math.PI/180;
let n = Math.ceil(distance/precision);
let interpolate = d3.interpolate(a,b)
let points = [];
for(var i = 0; i <= n; i++) {
points.push([...interpolate(i/n)]);
}
line.geometry.coordinates = points;
}
sample(curve);
svg
.append("path")
.datum(curve)
.attr("d", path)
.attr('fill-opacity', 0)
.attr('stroke', 'blue')
.attr("stroke-width", 1)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
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
(My code is at the end)
My goal is to display a country map (provided in a topojson file) which automatically scale and translate to fit into an area and then display few dots on it, representing some cities (given their lat/long coordinates).
First part was easy. I found (don't remember if it was on SO or on bl.ocks.org) that we can use bounds to compute scale and translate. That works perfectly and my country adapt to its parent area.
First Question: Why the country doesn't behave the same if I scale/translate it with its transform attribute or with projection.scale().translate() ? I mean, when I use transform attribute the country adapts perfectly whereas projection.scale().translate() displays a small country in a corner.
Second part is displaying some cities on my map. My cities has coordinates (which are real ones) :
var cities = {
features: [
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [2.351828, 48.856578] // Longitude, Latitude
},
'properties':{}
},
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [5.726945, 45.187778] // Longitude, Latitude
},
'properties':{}
},
};
When I try to apply scale and translate parameters (to adapt with my country which has been scaled and translated) either with projection.scale().translate() or with transform attribute my cities are far far away from where they should be.
Second Question: Why I cannot use same scale/translate parameters on country and cities ? How can I properly display my cities where they should be ?
function computeAutoFitParameters(bounds, width, height) {
var dx = bounds[1][0] - bounds[0][0];
var dy = bounds[1][1] - bounds[0][1];
var x = (bounds[0][0] + bounds[1][0]) / 2;
var y = (bounds[0][1] + bounds[1][1]) / 2;
var scale = 0.9 / Math.max(dx / width, dy / height);
var translate = [width / 2 - scale * x, height / 2 - scale * y];
return {
scale : scale,
translate: translate
};
}
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var scale = params.scale;
var translate = params.translate;
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + translate + ')scale(' + scale + ')');
svg.selectAll('circle')
.data(cities.features) // city is defined in the code above
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
)
.attr('r', '6px');
EDIT: I had removed too much code to simplify it. It's fixed now. The difference is that I have an array of cities to display rather than just one.
Thanks in advance.
I found out that I had to add null parameters to my projection. To sum up :
Create a minimal projection (and a path)
Apply null scale and translate parameters to the projection : projection.scale(1).translate([0, 0])
Compute real scale and translate parameters according to the bounding box
Display the country's map as before (no changes here)
Set computed scale and translate parameters to the projection : projection.scale(params.scale).translate(params.translate);
Draw the cities dots.
`
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
projection.scale(1).translate([0, 0]) // This is new
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + params.translate + ')scale(' + params.scale + ')');
projection.scale(params.scale).translate(params.translate); // This is new
svg.selectAll('circle')
.data(cities.features)
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
})
.attr('r', '6px')
.attr('fill', 'red');
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 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);
});