How to wire path/chart redraw after y domain changes - d3.js

I want to draw multiple barcharts into the same visualization.
When the first barchart is drawn it ḱnows nothing about the domain of the 2nd, 3rd, etc. At the time the 2nd barchart is drawn the domain of the y scale changes. 'Consequently the first barchart needs to be redrawn. The question is what is a good way to wire up the redraw since the scale/domain has no change notification mechanism.
http://bl.ocks.org/markfink/4d8f1c183e6cd9d6ea07

Here's one implementation: http://jsfiddle.net/tBHyD/2/
I only tried to address the setup in the question, not the full implementation noted in your comment. There are lots of ways to accomplish this; this one uses an event-driven model, using d3.dispatch:
var evt = d3.dispatch("change");
The key here is to update the scale extents globally, then fire an event if they've changed. Here I use a function, updateExtent, for this purpose:
var x0 = Infinity,
x1 = -Infinity,
y0 = Infinity,
y1 = -Infinity;
function updateExtent(data) {
var extx = d3.extent(data, function(d) { return d[0]; }),
exty = d3.extent(data, function(d) { return d[1]; }),
changed;
// update
if (extx[0] < x0) { x0 = extx[0]; changed = true; }
if (extx[1] > x1) { x1 = extx[1]; changed = true; }
if (exty[0] < y0) { y0 = exty[0]; changed = true; }
if (exty[1] > y1) { y1 = exty[1]; changed = true; }
// if changed, update scales and fire event
if (changed) {
// update scales
x.domain([x0, x1]);
y.domain([y1, y0]);
// update axes
vis.select(".x.axis").call(xAxis);
vis.select(".y.axis").call(yAxis);
// fire event
evt.change();
}
}
Then the redraw function sets a listener:
function redraw(selection, data, style) {
var bar = selection.selectAll(".bar")
.data(data);
// enter
bar.enter().append("rect")
.attr('class', "bar")
.attr("width", 5)
.style(style);
function updateBar() {
// update
bar
.attr("x", function(d) { return x(d[0]) - .5; })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return height - y(d[1]); });
}
// initial call
updateBar();
// handler call
evt.on("change", updateBar);
};
Note that now you don't need to set the extent explicitly:
var data1 = [[2,0.5], [4,0.8], [6,0.6], [8,0.7], [12,0.8]];
updateExtent(data1);

Related

d3.js click and apply zoom and pan to distribute points located inside a targeted division to the triggered subdivisions

Based on the response and example made by Andrew Reid, I produced this
pen code here points_in_subdivisons: on clicking on areas(Germany) on the screen
We want to offer a smooth animation from one close-up on the map to another
by using ZOOM OUT, PAN, ZOOM IN.
I have many divisions(countries) on Country level and then many sub-divisions(regions) inside each country .
Many points scattered across all divisions (countries) on my example mainly above Germany.
when I have to click on a targeted division(country) I must get only the points which correspond to this targeted division(country) that I have just clicked on
That means when the zoom of the subdivision(regions) is triggered(when the click is
made),
the code should take all the points that exist already only inside the
contours of the targeted divison(country) (that have just been clicked on) and points
enclosed-in should scatter in their corresponding subdivisions(regions).
To achieve this functionality and
based on Michael Rovinsky comment:
in the function manipulate(), the code is able to filter and extract only points that are embedded inside the targeted and triggered subdivisions(regions) and exclude markers those that are outside.
Inside function redraw() the enter exit pattern works well .
var svg = d3.select("svg");
width = 960;
height = 500;
var dataArray = [];
var mydataArray= [];
var projection = d3.geoMercator();
var baseProjection = d3.geoMercator();
var path = d3.geoPath().projection(projection);
var gBackground = svg.append("g"); // appended first
var gProvince = svg.append("g");
var gDataPoints = svg.append("g"); // appended second
var ttooltip = d3.select("body").append("div")
.attr("class", "ttooltip");
var csvPath="https://dl.dropbox.com/s/rb9trt4zy87ezi3/lonlat.csv?dl=0";
d3.csv(csvPath, function(error, data) {
if (error) throw error;
d3.json("https://gist.githubusercontent.com/rveciana/5919944/raw/2fef6be25d39ebeb3bead3933b2c9380497ddff4/nuts0.json", function(error, nuts0) {
if (error) throw error;
d3.json("https://gist.githubusercontent.com/rveciana/5919944/raw/2fef6be25d39ebeb3bead3933b2c9380497ddff4/nuts2.json", function(error, nuts2) {
if (error) throw error;
// convert topojson back to geojson
var countries = topojson.feature(nuts0, nuts0.objects.nuts0);
var regions = topojson.feature(nuts2, nuts2.objects.nuts2);
baseProjection.fitSize([width,height],regions);
projection.fitSize([width,height],regions);
var color = d3.scaleLinear().range(["steelblue","darkblue"]).domain([0,countries.features.length]);
var regionColor = d3.scaleLinear().range(["orange","red"]);
baseProjection.fitSize([width,height],countries);
projection.fitSize([width,height],countries);
var featureCollectionCountries = { "type":"FeatureCollection", "features": countries.features };
gBackground
.attr("class", "country")
.selectAll("path")
.data(countries.features)
.enter()
.append("path")
.attr("fill",function(d,i) { return color(i); })
.attr("opacity",0.7)
.attr("d", path)
.style("stroke","black")
.style("stroke-width",0)
.on("mouseover", function() {
d3.select(this)
.style("stroke-width",1)
.raise();
})
.on("mouseout", function(d,i) {
d3.select(this)
.style("stroke-width", 0 );
})
///// now zoom in when clicked and show subdivisions:
.on("click", function(d) {
// remove all other subdivisions:
d3.selectAll(".region")
.remove();
// add new features:
var features = regions.features.filter(function(feature) { return feature.properties.nuts_id.substring(0,2) == d.properties.nuts_id; });
regionColor.domain([0,features.length])
gProvince.selectAll(null)
.data(features)
.enter()
.append("path")
.attr("class","region")
.attr("fill", function(d,i) { return regionColor(i) })
.attr("d", path)
.style("stroke","black")
.style("stroke-width",0)
.on("click", function() {
zoom(projection,baseProjection);
d3.selectAll(".subdivision")
.remove();
})
.on("mouseover", function() {
d3.select(this)
.style("stroke-width",1)
.raise();
})
.on("mouseout", function(d,i) {
d3.select(this)
.style("stroke-width", 0 );
})
.raise()
// zoom to selected features:
var featureCollection = { "type":"FeatureCollection", "features": features }
manipulate(data,features);
redraw(featureCollection);
var endProjection = d3.geoMercator();
zoom(projection,endProjection.fitExtent([[50,50],[width-50,height-50]],featureCollection));
});
dataArray = data;
redraw(featureCollectionCountries);
});
});
});
function zoom(startProjection,endProjection,middleProjection) {
if(!middleProjection) {
d3.selectAll("path")
.transition()
.attrTween("d", function(d) {
var s = d3.interpolate(startProjection.scale(), endProjection.scale());
var x = d3.interpolate(startProjection.translate()[0], endProjection.translate()[0]);
var y = d3.interpolate(startProjection.translate()[1], endProjection.translate()[1]);
return function(t) {
projection
.scale(s(t))
.translate([x(t),y(t)])
path.projection(projection);
return path(d);
}
})
.duration(1000);
}
else {
d3.selectAll("path")
.transition()
.attrTween("d", function(d) {
var s1 = d3.interpolate(startProjection.scale(),middleProjection.scale());
var s2 = d3.interpolate(middleProjection.scale(),endProjection.scale());
var x = d3.interpolate(startProjection.translate()[0], endProjection.translate()[0]);
var y = d3.interpolate(startProjection.translate()[1], endProjection.translate()[1]);
function s(t) {
if (t < 0.5) return s1; return s2;
}
return function(t) {
projection
.translate([x(t),y(t)])
.scale(s(t)(t))
path.projection(projection);
return path(d);
}
})
.duration(1500);
}
}
function redraw(featureCollection,type) {
var mapG = d3.select('svg g.country');
d3.selectAll('circle')
.remove();
let grp = gDataPoints
.attr("class", "circle")
.selectAll("circle")
.data(dataArray,function(d) { return d.NOM; })
let grpEnter = grp.enter()
let group = grpEnter
group.append("circle")
.attr('fill', 'rgba(135, 5, 151, 125)')
.attr('stroke', 'black')
.each(function(d) {
if (d.lon === null ) return;
if (isNaN(d.lon ))return;
if (d.lat === null) return;
if (isNaN(d.lat ))return;
var pos = projection([parseFloat(d.lon), parseFloat(d.lat)]);
d.cx = pos[0];
d.cy = pos[1];
})
.attr("cx", function(d) {
return d.cx;
})
.attr("cy", function(d) {
return d.cy;
})
.attr("r",0.5)
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip)
.on('mousemove', function(d) {
var xPos = d3.mouse(this)[0] - 15;
var yPos = d3.mouse(this)[1] - 55;
ttooltip.attr('transform', 'translate(' + xPos + ',' + yPos + ')');
ttooltip.style('opacity', 1);
var html = "<span>" + d.lon+ "</span>, <span>" + d.lat + "</span>";
ttooltip.html(html);
});
// Setup each circle with a transition, each transition working on transform attribute,
// and using the translateFn
group
.transition()
.duration(2000)
.attrTween("transform",function(d) {
return mapG._groups[0][0] != null ? recenter(featureCollection): null;
});
group.exit().remove() // exit > remove > g
}
function recenter(featureCollection) {
console.log('recentering');
};
function manipulate(data,features){
dataArray= [];
mydataArray =[];
data.forEach(function(ddd)
{
features.forEach(function(feature)
{
var polygoneOriginal =feature;
var points = [parseFloat(ddd.lon), parseFloat(ddd.lat)];
var isIn = d3.geoContains(polygoneOriginal, points);
if(isIn)
{
var element = ddd;
mydataArray.pushIfNotExist(element, function(e) {
return e.lat === element.lat && e.lon === element.lon ;
});
}
});
});
if(mydataArray.length>0)
{
var columnsArray= ["lon","lat"];
dataArray=mydataArray;
dataArray.columns = columnsArray;
}
}
function showTooltip(d) {
var html = "<span>" + d.lon+ "</span>, <span>" + d.lat + "</span>";
ttooltip.html(html);
ttooltip
.style("left", window.pageXOffset + d3.event.x + 12 + "px")
.style("top", window.pageYOffset + d3.event.y + 12 + "px")
.transition()
.style("opacity", 1);
return d3.select(this).attr('fill', 'rgba(103, 65, 114, 0.8)');
}
function hideTooltip() {
ttooltip
.transition()
.style("opacity", 0);
return d3.select(this).attr('fill', 'rgba(103, 65, 114, 0.5)');
}
// check if an element exists in array using a comparer function
// comparer : function(currentElement)
Array.prototype.inArray = function(comparer) {
for(var i=0; i < this.length; i++) {
if(comparer(this[i])) return true;
}
return false;
};
// adds an element to the array if it does not already exist using a comparer
// function
Array.prototype.pushIfNotExist = function(element, comparer) {
if (!this.inArray(comparer)) {
this.push(element);
}
};
My Question is the following : How to make the Zooming (for points circle) to work adequately:
right now, on a map upon click the x y points not scale.
They are rendered as circles in background and I would like them to move with the map.
That means How to apply the same animation zoom (when subdivisions are triggered by click on a division) in order to those points inside the targeted subdivision follow in transition and move with the map and we could see circles points clearly distributed adequately in each correct corresponding subdivisions?
update
Andrew Reid described here How To accomplish a smooth zoom using d3.js
so following his hints.
I added the following instructions in redraw() function
var mapG = d3.select('svg g.country');
group
.transition()
.duration(2000)
.attrTween("transform",function(d) {
return mapG._groups[0][0] != null ? recenter(): null;
});
AND then we should add the code to the The function that should actually do the moving recenter(featureCollection) function to
function recenter(featureCollection) {
// TO ADD CODE TO BE IMPLEMENTED HERE
};
Thank You very much for your cooperation,participation and help !
1- To generate first iteration click on Region equal country
//GENERATE FIRST MAP
dataArray = data;
redraw();
2- To generate counties for example on click on region, we should first set startprojection and endprojection in zoom function and then trigger redraw of circles
//zoom to selected provinces features:
var countiesFeatureCollection = { "type":"FeatureCollection", "features": countiesFeatures }
//manipulate counties And Redraw
manipulateCounties(data,countiesFeatures);
baseProjection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection);
projection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection);
redraw(countiesFeatureCollection,"counties");
if ( projection.translate().toString() === baseProjection.translate().toString() && projection.scale() === baseProjection.scale() )
{
zoom(baseProjection,projection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection));
}
else
{
var endProjection = d3.geoMercator();
zoom(projection,endProjection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection));
}
3-the same thing should be applied to communities
var endProjection = d3.geoMercator();
endProjection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection);
projection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection);
redraw(communesfeatureCollection,"communes");
if ( projection.translate().toString() === projectioncommune.translate().toString() && projection.scale() === projectioncommune.scale()){
zoom(projectioncommune,projection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection));
}
else {
var endProjection = d3.geoMercator();
zoom(projection,endProjection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection));
}
4- Then reinitialise to go to first step 1 by
// start rendering points again
baseProjection.fitSize([width,height],regions);
projection.fitSize([width,height],regions);
//GENERATE AGAIN THE FIRST MAP
dataArray = data;
redraw();
zoom(projection,baseProjection);
ATTACHED WORKING PEN

possible to tween d3.geo.circle().angle()

I'm trying to recreate the "pulse" effect from this example:
https://anthonyskelton.com/2016/d3-js-earthquake-visualizations/
on a rotating globe... so I have to use d3.geo.circle() to generate paths (rather than svg circles) to manage the clipping properly.
I can transition() other attributes but am guessing I'll need to tween the path for each circle... I just don't know where to start and can't find any examples... there are very few examples using d3.geo.circle() and they are all static.
Thanks for any pointers!
The solution to this question came by way of pursuing a related question:
D3: Accessing bound data after using .datum()
The first step was understanding d3.geo.path().pointRadius() and creating a function to pass into .attr('d', f(d){})
The parameter i is unused but serves as a placeholder so that the radius r can be passed.
The pointPath() function is used elsewhere in update() and reDraw() functions, so it looks for a radius attribute that may already be present in bound data.
geoPath = d3.geo.path().projection(projection);
// var pointPath = function(d, i, data, r) { // d3v4 adds extra param
var pointPath = function(d, i, r) {
if (d.properties && d.properties.radius != undefined) {
r = r || d.properties.radius;
}
r = r || 1.5;
var coords = [d.geometry.coordinates[0], d.geometry.coordinates[1]];
var pr = geoPath.pointRadius(globe.scale() / 100 * r);
return pr({ type: "Point", coordinates: coords })
}
The animation was then possible using an .attrTween()
pulse = function() {
surface.selectAll('.pulse-circle')
.attr("d", function(d) { return pointPath(d, 0, 0); })
.style("fill-opacity", 1 )
.transition()
.delay(function(d, i) { return i * 200; })
.duration(3000)
.style("fill-opacity", 0 )
.attrTween("d", function(d) {
rinterp = d3.interpolate(0, 10);
var fn = function(t) {
d.r = rinterp(t);
return pointPath(d, 0, d.r) || 'M0,0';
};
return fn;
});
}
Because this occurs on a rotating globe I had to add return 'M0,0' if pointPath() returned undefined... to avoid console errors.

Update D3 chart dynamically

The idea is to have a d3 vertical bar-chart that will be given live data.
I simulate the live data with a setInterval function that updates the the values of the elements in my dataset:
var updateData = function(){
a = parseInt(Math.random() * 100),
b = parseInt(Math.random() * 100),
c = parseInt(Math.random() * 100),
d = parseInt(Math.random() * 100);
dataset = [a, b, c, d];
console.log(dataset);
};
// simullate live data input
var update = setInterval(updateData, 1000);
I want to update the chart every 2 seconds.
For that I need a update function that gets the new dataset and then animates a transition to show the new results.
Like that:
var updateVis = function(){
..........
};
var updateLoop = setInterval(drawVis,2000);
I don't want to simply remove the chart and draw again. I want to animate the transition between the new and old bar height for each bar.
Checkout the fiddle
Since your not changing the number of bars, this can be as simple as:
var updateVis = function(){
svg.selectAll(".input")
.data(dataset)
.transition()
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
};
Updated fiddle.
But your next question becomes, what if I need a different number of bars? This is where you need to handle enter, update, exit a little better. You you can write one function for initial draw or updating.
function drawVis(){
// update selection
var uSel = svg.selectAll(".input")
.data(dataset);
// those exiting
uSel.exit().remove();
// new bars
uSel
.enter()
.append("rect")
.attr("class", "input")
.attr("fill", "rgb(250, 128, 114)");
// update all
uSel
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5/100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
}
New fiddle.
That's the way to go.
Just think what you've done to get the initial chart:
1) Get data
2) Bind it to element (.enter())
3) Set element attributes to be function of the data.
Well, you do this again:
In the function updateData you get a new dataset that's the first step.
Then, rebind it:
d3.selectAll("rect").data(dataset);
And finally update the attributes:
d3.selectAll("rect").attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
(Want transitions? Go for it. It is easy to add in your code but you better read this tuto if you want to deeply understand it)
Check it on fiddle

D3.js Donut transition

I am working on a d3 donut and am stuck on how to update the donut value where the value will flow back if for instance you change the value from 42 to 17.
I can remove the svg, but then it rewrites the new value (17) from the zero position.
I would like it to flow backwards from 42 say to 17.
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial values
here is a link to my jsfidle http://jsfiddle.net/yr595n96/
and I would love any help you could offer.
Thanks
Heres you're new button click :
$("#next").click(function () {
percent = 17;
var progress = 0;
var timeout = setTimeout(function () {
clearTimeout(timeout);
var randNumber = Math.random() * (100 - 0 + 1) + 0;
//path = path.data(pie(calcPercent(17))); // update the data << change this
path = path.data(pie(calcPercent(randNumber)));
path.transition().duration(duration).attrTween("d", function (a) {
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, randNumber)
this._current = i(0);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
}); // redraw the arcs
}, 100);
});
Notice on line '8':
path = path.data(pie(calcPercent(randNumber)));
You need to pass new data for it to transition to. I have used a random number here just to show you any number works. I made a variable for this and passed it to both the 'path' and the text : 'i2' > d3.interpolate.
You can always just change it to 17 to suit what you asked for :)
Updated fiddle : http://jsfiddle.net/yr595n96/3/

How do I adjust zoom size for a point in D3?

This could be a classic case of "you're doing it wrong", but all of my searching to date hasn't warranted any help.
Here's my scenario:
I'm using an albersUSA map projection in conjunction with the national and county GeoJson files to draw everything.
I also have a self created "cities" file that contains major cities for each state. The coordinates are accurate and everything looks good.
When a user clicks on a given state, I hide all state shapes and then calculate the transform needed to get the county shapes for that state to fit within my viewport. I then apply that transform to all the necessary county shapes in order to get the "zoomed" view. My code is as follows:
function CalculateTransform(objectPath)
{
var results = '';
// Define bounds/points of viewport
var mapDimensions = getMapViewportDimensions();
var baseWidth = mapDimensions[0];
var baseHeight = mapDimensions[1];
var centerX = baseWidth / 2;
var centerY = baseHeight / 2;
// Get bounding box of object path and calculate centroid and zoom factor
// based on viewport.
var bbox = objectPath.getBBox();
var centroid = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
var zoomScaleFactor = baseHeight / bbox.height;
var zoomX = -centroid[0];
var zoomY = -centroid[1];
// If the width of the state is greater than the height, scale by
// that property instead so that state will still fit in viewport.
if (bbox.width > bbox.height) {
zoomScaleFactor = baseHeight / bbox.width;
}
// Calculate how far to move the object path from it's current position to
// the center of the viewport.
var augmentX = -(centroid[0] - centerX);
var augmentY = -(centroid[1] - centerY);
// Our transform logic consists of:
// 1. Move the state to the center of the screen.
// 2. Move the state based on our anticipated scale.
// 3. Scale the state.
// 4. Move the state back to accomodate for the scaling.
var transform = "translate(" + (augmentX) + "," + (augmentY) + ")" +
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + zoomScaleFactor + ")" +
"translate(" + (zoomX) + "," + (zoomY) + ")";
return results;
}
...and the binding function
// Load county data for the state specified.
d3.json(jsonUrl, function (json) {
if (json === undefined || json == null || json.features.length == 0)
{
logging.error("Failed to retrieve county structure data.");
showMapErrorMessage("Unable to retrieve county structure data.");
return false;
}
else
{
counties.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("id", function (d, i) {
return "county_" + d.properties.GEO_ID
})
.attr("data-id", function (d, i) { return d.properties.GEO_ID })
.attr("data-name", function (d, i) { return countyLookup[d.properties.GEO_ID] })
.attr("data-stateid", function (d, i) { return d.properties.STATE })
.attr("d", path);
// Show all counties for state specified and apply zoom transform.
d3.selectAll(countySelector).attr("visibility", "visible");
d3.selectAll(countySelector).attr("transform", stateTransform);
// Show all cities for the state specified and apply zoom transform
d3.selectAll(citySelector).attr("visibility", "visible");
d3.selectAll(citySelector).attr("transform", stateTransform);
}
});
This works fine here, except for really small states, the zoom factor is much larger, and the circles get distored.
Is there a way to force the size of the points to be a fixed size (say a 15px radius) even after the transform occurs?
For things you don't want to scale, just make them divided by 'scale' . In my case,
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll(".mapmarker")
.attr("r",6/d3.event.scale)
.attr("stroke-width",1/d3.event.scale);
});
This is happening because you are setting a scale transform instead of scaling the positions. You can see the difference here Basically it is the difference between:
// Thick lines because they are scaled too
var bottom = svg.append('g').attr('transform', 'scale('+scale+','+scale+')');
bottom.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
and
// line thicknesses are nice and thin
var top = svg.append('g');
top.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x * scale; })
.attr('cy', function(d) { return d.y * scale; });
With mapping probably you best solution is to compute your offset and scale as you do and then add them into your projection function - you want to directly modify the post-projection x and y values. If you update your projection function properly you should not have to do anything else to apply the appropriate zoom to your map.

Resources