I have an example of a chart on jsFiddle which has multiple groups of multiple lines. It draws successfully, but I would like to be able to transition to new sets of data.
The example should update with new data after 4 seconds. Although the chart gets called (and outputs something in the console), the lines aren't updated.
I've tried lots of variations based on existing, simpler examples, with no luck. I suspect its the nested data based on this SO answer that's confusing me even more than usual.
SO insists I have some code in the answer, so here's where I assume I need to add/change something:
svg.transition().attr({ width: width, height: height });
g.attr('transform', 'translate(' + margin.left +','+ margin.right + ')');
var lines = g.selectAll("g.lines").data(function(d) { return d; });
lines.enter().append("g").attr("class", "lines");
lines.selectAll("path.line.imports")
.data(function(d) { return [d.values]; })
.enter().append("path").attr('class', 'line imports');
lines.selectAll('path.line.imports')
.data(function(d) { return [d.values]; })
.transition()
.attr("d", function(d) { return imports_line(d); });
// repeated for path.line.exports with exports_line(d).
The problem is the way you're determining the top-level g element that everything is appended to:
var g = svg.enter().append('svg').append('g');
This will only be set if there is no SVG already, as you're only handling the enter selection. If it exists, g will be empty and therefore nothing will happen. To fix, select the g explicitly afterwards:
var g = svg.enter().append('svg').append('g').attr("class", "main");
g = svg.select("g.main");
Complete demo here.
Related
I'm pretty new to d3js and trying to understand the difference between using data and datum to attach data to elements. I've done a fair bit of reading the material online and I think I theoretically understand what's going on but I still lack an intuitive understanding. Specifically, I have a case where I'm creating a map using topojson. I'm using d3js v7.
In the first instance, I have the following code to create the map within a div (assume height, width, projection etc. setup correctly):
var svg = d3.select("div#map").append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath()
.projection(projection);
var mapGroup = svg.append("g");
d3.json("json/world-110m.json").then(function(world){
console.log(topojson.feature(world, world.objects.land))
mapGroup.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
});
The console log for the topojson feature looks like this:
And the map comes out fine (with styling specified in a css file):
But if I change datum to data, the map disappears. I'm trying to improve my understanding of how this is working and I'm struggling a little bit after having read what I can find online. Can someone explain the difference between data and datum as used in this case and why one works and the other doesn't?
Thanks for your help!
There are several differences between data() and datum(), but for the scope of your question the main difference is that data() accepts only 3 things:
An array;
A function;
Nothing (in that case, it's a getter);
As you can see, topojson.feature(world, world.objects.land) is an object. Thus, all you'd need to use data() here (again, not the idiomatic D3, I'm just addressing your specific question) is wrapping it with an array:
.data([topojson.feature(world, world.objects.land)])
Here is your code using data():
var svg = d3.select("div#map").append("svg")
.attr("width", 500)
.attr("height", 300)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath();
var mapGroup = svg.append("g");
d3.json("https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json").then(function(world) {
const projection = d3.geoEqualEarth()
.fitExtent([
[0, 0],
[500, 300]
], topojson.feature(world, world.objects.land));
path.projection(projection);
mapGroup.append("path")
.data([topojson.feature(world, world.objects.land)])
.attr("class", "land")
.attr("d", path);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson#3"></script>
<div id="map"></div>
I have a stacked bar chart that can be updated and filtered using a dropdown menu. When the chart first renders I don't have issues but when I uncheck an option, the rect does not get removed.
I checked the array after the removal and it does get removed from the array. The issue here is that the rect gets shifted to the far left. The x-axis does get updated and the text associated with that objects gets removed.
My feeling is that I have a problem with the selection, but I am confused because I have rect and g that represents the series for each object. I'm not sure if I am selecting the right object when I am removing elements.
My code: Stacked bar chart with filter
Here is how the chart looks like after removing one item:
As you can see the rect is still there, but shifted to the left.
I have included the important part of my code. Any feedback even about how to make my code work on Plunker is appreciated
d3.selectAll("input").on("change", function() {
var paragraphID = d3.select(this).attr("id");
paragraphID = String(paragraphID);
if (this.checked) {
if (paragraphID === 'A') {
data.push({
production_company: "A",
Pass: 50,
Fail: 65,
total: 11
});
}
} else {
data = data.filter(d => d.production_company !== paragraphID);
}
xScale_production.domain(data.map(function(d) {
return d.production_company;
}));
yScale_production.domain([0, d3.max(series_production, d => d3.max(d, d => d[1]))]);
var group = svg_stack_production.selectAll("g").data(series_production);
var mbars = group.selectAll("rect").data(d => d);
mbars.enter()
.append("rect")
.attr("x", width)
.attr("y", d => yScale_production(d[1]))
.attr("width", xScale_production.bandwidth())
.merge(mbars)
.attr("x", (d, i) => xScale_production(d.data.production_company))
.attr("y", d => yScale_production(d[1]))
.attr("width", xScale_production.bandwidth())
.attr("height", d => yScale_production(d[0]) - yScale_production(d[1]));
group.selectAll("g")
.exit()
.remove();
mbars.selectAll("rect").exit().remove()
});
No need to select elements before calling exit, they are already in selection. Just call:
group.exit().remove();
mbars.exit().remove();
instead of:
group.selectAll("g").exit().remove();
mbars.selectAll("rect").exit().remove();
I solved the issue, there were two issues here. First, I needed to clone the original data and then apply the filtering on it as #Michael Rovinsky suggested.
Second, series_production needed to be updated after filtering the data.
here is the link for the working code.
Working code
I have a simple (o.k. not so simple) barchart which shows the electric power consumption of one consumer (C1). I add the consumption of another consumer (C2) as line. The max consumption of C2 if higher then the max consumption of C1 so I have to rescale. I have solved this problem but not as beautiful I wanted to.
I calculate the new yMax, set the domain, rescale the axis (beautiful) remove all 'rect' and redraw (not beautiful). Is there a possibility to say: hey bars, I have a new scale, go down with a beautiful animation :)
Here the rescale method:
var rescale = function () {
//in this function the new _maxYValue is set
renderLineView();
var data = _data;
y.domain([_minYValue, _maxYValue]);
_svg.select(".y.axis")
.transition().duration(1500).ease("sin-in-out")
.call(yAxis());
_svg.selectAll("rect").remove();
var barWidth = getBarWidth(data.length);
var bars = d3.select("#layer_1").selectAll(".bar").data(data, function (d) {
return d.xValue;
});
bars.enter().append("rect")
.attr("class", "daybarincomplete")
.attr("x", function (d, i) {
return x(d.xValue) + 4;
})
.attr("width", barWidth)
.attr("y", function (d) {
return Math.min(y(0), y(d.value));
})
.attr("height", function (d) {
return Math.abs(y(d.value) - y(0));
});
}
Here is the jsfiddle: http://jsfiddle.net/axman/v4qc7/5/
thx in advance
©a-x-i
Use the .transition() call on bars, to determine the behaviour you want when the data changes (e.g. Bar heights change). You'd chain the .attr() function after it to set bar height etc.
To deal with data points that disappear between refreshes (e.g. You had 10 bars originally but now only have 9), chain the .exit().remove() functions to bars.
With both of the above, you can additionally chain something like .duration(200).ease('linear') to make it look all pretty.
pretty much what #ninjaPixel said. There's a easy to follow example here
http://examples.oreilly.com/0636920026938/chapter_09/05_transition.html
I have two g elements each containing circles. Circles are organized using force.layout. The g elements are transitioning.
You can see here: demo. Reduced code:
var dots = svg.selectAll(".dots")
.data(data_groups)
.enter().append("g")
.attr("class", "dots")
.attr("id", function (d) {
return d.name;
})
...
.each(addCircles);
dots.transition()
.duration(30000)
.ease("linear")
.attr("transform", function (d, i) {
return "translate(" + (150 + i * 100) + ", " + 450 + ")";
});
function addCircles(d) {
d3.select(this).selectAll('circle')
.data(data_circles.filter(function (D) {
return D.name == d.name
}))
.enter()
.append('circle')
.attr("class", "dot")
.attr("id", function (d) {
return d.id;
})
...
.call(forcing);
}
function forcing(E) {
function move_towards(alpha) {
...
}
var force = d3.layout.force()
.nodes(E.data())
.gravity(-0.01)
.charge(-1.9)
.friction(0.9)
.on("tick", function (e) {
...
});
force.start();
}
I need to move circle (for example id=1) from the first g element to the second one using transition.
Any suggestions are appreciated.
It can be done.
What I did was:
1) Use jquery to append the point to the target group
2) Use a transformation (no transition) to move the point back to its original location
3) Transition the point to its new location
The jQuery was used for the appendTo method. It can be removed and replaced with some pure Javascript stuff, but it's quite convenient.
I've got a partially working fiddle here. The green points work right, but something is going wrong with the blue ones. Not sure why.
In my view, transitions work on a single element. If an element changes its position in the DOM tree, from below one g to another, I can't think of a way to make that as one smooth transition because it's basically a binary split: Now there's an element under one g, now it's gone but there's another one somewhere else.
What I'd do in order to achieve what I think you want to do: Group everything under the same ´g´, assign color and translation individually, then change color and translation for that single element you want to change.
But don't take that as a reliable statement that you can't do it the way you originally wanted.
I have implemented this d3 visualization http://bl.ocks.org/4745936 , to be loaded with dynamic data instead of a .tsv
in my case, once my server passes new information to the selector, a second chart gets rendered under the first one, instead of modifying the contents of the existing graph.
I believe it has to do with this append method.
var svg = d3.select(selector).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
so I tried adding other exit().remove() methods to legend and cities variables right after they append('g'); but my javascript console says the exit() method does not exist at that point.
I feel I have the completely wrong approach, how do I update an existing graph like this? Having a second and third graph get generated alongside the previous ones is not the outcome I wanted at all
You're right the append method is adding a new svg element every time. To prevent the duplicate charts you need to check if the svg element exists already. So try something like this at the begining:
var svg = d3.select("#mycontainer > svg")
if (svg.empty())
svg = d3.select(selector).append("svg");
...
As stated in the exit() docs, This method is only defined on a selection returned by the data operator. So make sure that you're calling exit on a selection returned from .data(..).
scott's answer is one way of ensuring that the initialization happens only once.
However, I prefer a more d3-ic way of handling this:
var svg = d3.select(selector)
.selectAll('svg')
.data( [ dataFromTSV ] ); // 1 element array -> 1 svg element
// This will be empty if the `svg` element already exists.
var gEnter = svg.enter()
.append('svg')
.append('g');
gEnter.append( ... ); // Other elements to be appended only once like axis
svg.attr('width', ...)
.attr('height', ...);
// Finally, working with the elements which are surely in the DOM.
var g = svg.select("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.selectAll(...).attr(...);
This approach is exemplified in the reusable charts example's source code.
I prefer this approach because it keeps the code very declarative and true to the visualisation by hiding away the logic of initialisation and updates.
I would modify the original example: http://jsfiddle.net/8Axn7/5/ to http://jsfiddle.net/3Ztt8/
Both the legend and the graph are defined from svgElem with one single element of data:
var svgElem = d3.select("#multiLinegraph").selectAll('svg')
.data([cities]);
// ...
var svg = svgElem.select('g');
// ...
var city = svg.selectAll(".city")
.data(
function (d) { return d; },
function (d) { return d.name; } // Object consistency
);
// ...
var legend = svg.selectAll('g.legend')
.data(
function(d) { return d; },
function (d) { return d.name; } // Object consistency
);
Also, the static properties are set only once when the element is entered (or exited), while the update properties are set (transitioned) with each update:
gEnter.append("g")
.attr("class", "y multiLineaxis")
.append('text')
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Requests (#)");
svg.select('g.y.multiLineaxis').transition().call(yAxis);
The code, in my opinion, follows the cycle of enter-update-exit cleanly.
I was able to solve this problem with some jQuery and CSS voodoo
basically since my d3 graph adds an svg element to an existing selector (a div in my case), I was able to check for the name of this dynamically
var svgtest = d3.select(selector+" > svg"); getting the svg subchild element of that div. then I could use jquery to remove that element from the dom completely, and then let d3 continue running and append svg's all it wants!
var svgtest = d3.select(selector+" > svg");
if(!svgtest.empty())
{
$(selector+" > svg").remove();
}
First of all you should remove old svg, after then you can add updated charts.
For that you should add only one line before you append svg.
And its working.
var flag=d3.select("selector svg").remove();
//----your old code would be start here-------
var svg = d3.select(selector).append("svg")