D3 v4 (Tree Diagram) - Dynamically calculate the height of the SVG given dynamic number of nodes - d3.js

I dynamically am changing the data for a Tree Diagram, and don't know what the height of the SVG should be. How can I calculate the height of the Tree Diagram to update the SVG dimensions?
Fiddle example: https://jsfiddle.net/intelligence_ai/guesukv6/
Please view the code on the fiddle, SO requires that I post some code with fiddle links:
var treemap = d3.tree().nodeSize([40, 40]);
root = d3.hierarchy(data, function(d) { return d.children; });
root.x0 = 100;
root.y0 = 0;
function update(source) {
// Assigns the x and y position for the nodes
var data = treemap(root);
// Compute the new tree layout.
var nodes = data.descendants(),
links = data.descendants().slice(1);
// Normalize for fixed-depth.
let connectorLength = 200; // the length of the lines in pixels
nodes.forEach(function(d){ d.y = d.depth * connectorLength});
nodes = nodes.filter(function(d){
return d.depth != 0;
})
...

Since you specified that the nodeSize is [40,40] and you have 129 nodes, the height should be:
var height = 40 * 129;
Since you want to calculate it dynamically, it should be:
var height = treemap.nodeSize()[1] * nodes.length;
Also, when specifying tree.nodeSize the root node will be positioned at 0,0. Therefore, you have to translate the main group:
.attr("transform", "translate("+ margin.left + "," + (height/2) + ")");
Here is the updated fiddle: https://jsfiddle.net/jxw0Ld3c/

Related

d3.js - Dendrogram display adjusted to the tree diagram

With d3.js I have created d3 dendrograms to visualize hierachicals relations between objects. Dimensions and margins of the graph are defined with fixed height and width values.
var width = 1000,
height = 800,
boxWidth = 150,
boxHeight = 35,
gap = {
width: 50,
height: 12
},
margin = {
top: 16,
right: 16,
bottom: 16,
left: 16
},
svg;
With a few relations, display is ok but with many relations it's doesn't fit, graph is 'cut' and I can't see the entire graph. How to set this width and height properties dynamically and adjusted to the size of the graph ?
An example with a correct display : Codepen
An example with an incorrect display : Codepen
Let's work this out, you need to know the bounding box of your content first and then adjust the svg size. To do that, in this particular case, you only have to look at the boxes or nodes and can ignore the links.
With that in mind you can do the following after populating the Nodes in your renderRelationshipGraph function and return the calculated value:
function renderRelationshipGraph(data) {
// ...
var bbox = Nodes.reduce(function (max, d)
{
var w = d.x + boxWidth;
var h = d.y + boxHeight;
if (w > max[0]) {max[0] = w}
if (h > max[1]) {max[1] = h}
return max
}, [0,0])
return bbox
}
then on the main code change use it to update height and width of the svg:
svg = d3.select("#tree").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g");
var bbox = renderRelationshipGraph(data);
svg.attr("width", bbox[0])
.attr("height", bbox[1]);
You can add a transition and limit the height but this does what you requested with a really large end result.

D3.js: How to Center svg map after Drag, Zoom, or MouseMove Events

My map projection and display goes awry when users drag, move the map (see: https://realtimeceap.brc.tamus.edu).
For example (duplicate event):
1. Select a field condition from dropdownlist.
2. Select a State from dropdownlist.
3. Select a County from dropdownlist.
4. Drag the map or move the mousewheel.
5. Then, select another State from dropdownlist. The map is not centered at the middle of the svg element and the scale is off, instead of scale 1.
Appreciate any help.
I reset the map on selecting a state as follows:
function resetMap() {
svg = d3.select("#svgMap2");
var w = 728;
var h = 500;
var project = d3.geoAlbersUsa()
.scale(1000)
.translate([w / 2, h / 2]);
var t = project.translate(); // the projection's default translation
var scale = project.scale;
//reset all features to original scale
d3.select("#svgMap2").select("#counties").selectAll(".county")
.transition()
.duration(750)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + scale + ")");
}
Been working on this for a while and found the solution is so simple. I needed to reset the g element's transform attribute to null to redefine it. The id of my svg = svgMap2 and the id of my g element = counties. BTW, I'm using D3.js version 4.
function resetMap() {
var k = 1;
var t = [0, 0];
svg = d3.select("#svgMap2")
d3.select("#svgMap2").select("#counties").selectAll(".county").classed("active", false);
d3.select("#counties").attr("transform", null);
svg.select("#counties").selectAll(".county") //must use svg or it disables pan, zoom
.attr("width", width)
.attr("height", height)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + k + ")");
}

D3js projection issues when fitting to BBox

(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');

D3: What projection am I using? / How to simplify with a null projection?

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);
});

how do you draw linear line in scatter plot with d3.js

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
}
}
}

Resources