i want to update the graph but it does not work. I want to update the line and the circles. I tried to add
.exit().remove()
to update the circle and
g.selectAll("path").attr("d", line);
to update the paths.
However it does not work.
Updating the outside group with exit().remove() works fine. (Checkbox in this example).
Updating only the path and circle does not work. (Update Button in this example)
I dont want to remove all lines in the graph and append it again, because i want to add transition when data changes.
Here is a JS Fiddle: LINK
Here is my code:
var data = [
[{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}],
[{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 200
}
},
{
point: {
x: 50,
y: 300
}
},
]
];
var svg = d3.select("svg");
var line = d3.line()
.x((d) => d.point.x)
.y((d) => d.point.y);
function updateGraph() {
console.log(data)
var allGroup = svg.selectAll(".pathGroup").data(data);
var g = allGroup.enter()
.append("g")
.attr("class", "pathGroup")
allGroup.exit().remove()
g.append("path")
.attr("class", "line")
.attr("stroke", "red")
.attr("stroke-width", "1px")
.attr("d", line);
g.selectAll("path").attr("d", line);
g.selectAll(null)
.data(d => d)
.enter()
.append("circle")
.attr("r", 4)
.attr("fill", "teal")
.attr("cx", d => d.point.x)
.attr("cy", d => d.point.y)
.exit().remove()
}
updateGraph()
document.getElementById('update').onclick = function(e) {
data = [
[{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}],
[{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 300
}
},
]
];
updateGraph()
}
$('#cb1').click(function() {
if ($(this).is(':checked')) {
data = [
[{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}],
[{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 200
}
},
{
point: {
x: 50,
y: 300
}
},
]
];
} else {
data = [
[{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
];
}
updateGraph()
});
Problem
The reason why allGroup.exit().remove() does nothing is that the updated dataset still has the same number of items as the original one. The exit selection is therefore empty.
The variable data contains lines, not points. The one defined at page load, and the one inside update listener contain two lines, only the number of points in them differs.
You can check this by putting a console.log(data.length) inside function updateGraph.
Solution 1
Change your data structure. You can assign an id property to each line, and use .data's, key function. cf. d3-selection documentation.
Updated jsFiddle implementing solution 1: see here.
This solution requires less changes.
Solution 2
In case you have no control over the data structure, you can transition the line drawing inside the update selection, rather than the exit one.
Related
Given the following code which calls the update function which creates 4 nodes with a circle and text element nested in a g element, waits 500ms, then calls the function again with updated data:
var data1 = [
{ x: 10, y: 10, text: "A" },
{ x: 30, y: 30, text: "B" },
{ x: 50, y: 50, text: "C" },
{ x: 70, y: 70, text: "D" }
];
var data2 = [
{ x: 30, y: 10, text: "X" },
{ x: 50, y: 30, text: "Y" },
{ x: 70, y: 50, text: "Z" },
{ x: 90, y: 70, text: "W" }
];
var svg = d3.select("body").append("svg");
update(data1);
setTimeout(function() { update(data2); }, 500);
function update(data) {
var nodes = svg.selectAll(".node")
.data(data);
var nodesUpdate = nodes
.attr("class", "node update")
var nodesEnter = nodes.enter();
var node = nodesEnter.append("g")
.attr("class", "node enter")
node
.attr("transform", function(d) { return "translate("+d.x+","+d.y+")"; });
node.append("circle")
.attr("r", 10)
.style("opacity", 0.2);
node.append("text")
.text(function(d) { return d.text; });
}
With the code as it is the second call has no effect, because everything is set in the enter selection. I'm trying to make it so I can call update with new data, and change properties on both the enter and update selections, without duplicating code. I can achieve this for top-level elements (ie the g elements) using merge, by making this change:
node
.merge(nodesUpdate)
.attr("transform", function(d) { return "translate("+d.x+","+d.y+")"; });
Now the nodes update their position after 500ms. However, I haven't been able to figure out how to update the text element. If I do nodes.selectAll("text") I end up with nested data, which doesn't work.
I've scoured the following docs to try and figure this out:
https://bl.ocks.org/mbostock/3808218
https://github.com/d3/d3-selection
https://bost.ocks.org/mike/nest/
It should just be nodes.select when dealing with a subselection.
Here's a quick refactor with comments and clearer variable names:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var data1 = [{
x: 10,
y: 10,
text: "A"
}, {
x: 30,
y: 30,
text: "B"
}, {
x: 50,
y: 50,
text: "C"
}, {
x: 70,
y: 70,
text: "D"
}];
var data2 = [{
x: 30,
y: 10,
text: "X"
}, {
x: 50,
y: 30,
text: "Y"
}, {
x: 70,
y: 50,
text: "Z"
}, {
x: 90,
y: 70,
text: "W"
}];
var svg = d3.select("body").append("svg");
update(data1);
setTimeout(function() {
update(data2);
}, 500);
function update(data) {
var nodesUpdate = svg.selectAll(".node")
.data(data); // UPDATE SELECTION
var nodesEnter = nodesUpdate.enter()
.append("g")
.attr("class", "node"); // ENTER THE Gs
nodesEnter.append("text"); // APPEND THE TEXT
nodesEnter.append("circle") // APPEND THE CIRCLE
.attr("r", 10)
.style("opacity", 0.2);
var nodesEnterUpdate = nodesEnter.merge(nodesUpdate); // UPDATE + ENTER
nodesEnterUpdate // MOVE POSITION
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodesEnterUpdate.select("text") // SUB-SELECT THE TEXT
.text(function(d) {
return d.text;
});
}
</script>
</body>
</html>
Without refactoring a lot of your code, the simplest solution is using a key in the data function, followed by an "exit" selection:
var nodes = svg.selectAll(".node")
.data(data, d=> d.text);
nodes.exit().remove();
Here is the demo:
var data1 = [{
x: 10,
y: 10,
text: "A"
}, {
x: 30,
y: 30,
text: "B"
}, {
x: 50,
y: 50,
text: "C"
}, {
x: 70,
y: 70,
text: "D"
}];
var data2 = [{
x: 30,
y: 10,
text: "X"
}, {
x: 50,
y: 30,
text: "Y"
}, {
x: 70,
y: 50,
text: "Z"
}, {
x: 90,
y: 70,
text: "W"
}];
var svg = d3.select("body").append("svg");
update(data1);
setTimeout(function() {
update(data2);
}, 500);
function update(data) {
var nodes = svg.selectAll(".node")
.data(data, d => d.text);
nodes.exit().remove();
var nodesUpdate = nodes
.attr("class", "node update")
var nodesEnter = nodes.enter();
var node = nodesEnter.append("g")
.attr("class", "node enter")
node
.merge(nodesUpdate)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.append("circle")
.attr("r", 10)
.style("opacity", 0.2);
node.append("text")
.text(function(d) {
return d.text;
});
}
<script src="https://d3js.org/d3.v4.min.js"></script>
This will create a different "enter" selection. If, on the other hand, you want to get the data bound to the "update" selection, you'll have to refactor your code.
This question builds on this question.
Using d3.js/dc.js, I have three (or more) charts. All have the same x-axis (a date series), so the nth datapoint on any chart will correspond exactly to the nth datapoint on the x-axis of the other charts.
When the user clicks on a dot point in one chart, I need to get the "y" data from the same point on the other 2+ charts and return an array or object or string with the chartID/y-datum from the other charts, something like this:
{"chart1":"30","chart2":"50","chart3":"10"}
Here is an example borrowed from Gerardo Furtado's answer to the above-referenced question. How would I modify Gerardo's example to return the datapoints from each chart?
var data = [{x:20, y:30},
{x:30, y:60},
{x:40, y:40},
{x:50, y:90},
{x:60, y:20},
{x:70, y:90},
{x:80, y:90},
{x:90, y:10}];
draw("#svg1");
draw("#svg2");
draw("#svg3");
function draw(selector){
var width = 250,
height = 250;
var svg = d3.select(selector)
.append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scaleLinear()
.domain([0, 100])
.range([30, width - 10]);
var yScale = d3.scaleLinear()
.domain([0,100])
.range([height - 30, 10]);
var circles = svg.selectAll("foo")
.data(data)
.enter()
.append("circle");
circles.attr("r", 10)
.attr("fill", "teal")
.attr("cx", d=>xScale(d.x))
.attr("cy", d=>yScale(d.y));
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
svg.append("g").attr("transform", "translate(0,220)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(30,0)")
.attr("class", "yAxis")
.call(yAxis);
}
d3.selectAll("circle").on("mouseover", function(){
var thisDatum = d3.select(this).datum();
d3.selectAll("circle").filter(d=>d.x == thisDatum.x && d.y == thisDatum.y).attr("fill", "firebrick");
}).on("mouseout", function(){
d3.selectAll("circle").attr("fill", "teal")
})
#svg1 {
float: left;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="svg1"></div>
<div id="svg2"></div>
<div id="svg3"></div>
As you have several different data sets, I'll modify the answer I wrote in your previous question so we can have different y values.
First, let't put all data in an object. That way, we can access the different data sets later:
var dataObject = {
data1: [{
x: 10,
y: 30
}, ...
}],
data2: [{
x: 10,
y: 70
}, ...
}],
data3: [{
x: 10,
y: 10
}, ...
}]
};
Then, we call the draw function:
draw("#svg1", dataObject.data1);
draw("#svg2", dataObject.data2);
draw("#svg3", dataObject.data3);
So, to get what you want, in the mouseover...
d3.selectAll("circle").on("mouseover", function() {
var thisDatum = d3.select(this).datum();
findPoints(thisDatum);
})
We call this function:
function findPoints(datum) {
var myObject = {};
for (var i = 1; i < 4; i++) {
myObject["chart" + i] = dataObject["data" + i].filter(e => e.x === datum.x)[0].y;
}
console.log(myObject)//use return instead of console.log
}
Here is the demo:
var dataObject = {
data1: [{
x: 10,
y: 30
}, {
x: 20,
y: 60
}, {
x: 30,
y: 40
}, {
x: 40,
y: 90
}, {
x: 50,
y: 20
}, {
x: 60,
y: 90
}, {
x: 70,
y: 90
}, {
x: 80,
y: 10
}],
data2: [{
x: 10,
y: 70
}, {
x: 20,
y: 60
}, {
x: 30,
y: 80
}, {
x: 40,
y: 10
}, {
x: 50,
y: 10
}, {
x: 60,
y: 20
}, {
x: 70,
y: 10
}, {
x: 80,
y: 90
}],
data3: [{
x: 10,
y: 10
}, {
x: 20,
y: 20
}, {
x: 30,
y: 40
}, {
x: 40,
y: 90
}, {
x: 50,
y: 80
}, {
x: 60,
y: 70
}, {
x: 70,
y: 50
}, {
x: 80,
y: 50
}]
};
draw("#svg1", dataObject.data1);
draw("#svg2", dataObject.data2);
draw("#svg3", dataObject.data3);
function draw(selector, data) {
var width = 200,
height = 100;
var svg = d3.select(selector)
.append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scaleLinear()
.domain([0, 100])
.range([30, width - 10]);
var yScale = d3.scaleLinear()
.domain([0, 100])
.range([height - 30, 10]);
var circles = svg.selectAll("foo")
.data(data)
.enter()
.append("circle");
circles.attr("r", 5)
.attr("fill", "palegreen")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y));
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale).ticks(2);
svg.append("g").attr("transform", "translate(0,70)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(30,0)")
.attr("class", "yAxis")
.call(yAxis);
}
d3.selectAll("circle").on("mouseover", function() {
var thisDatum = d3.select(this).datum();
findPoints(thisDatum);
d3.selectAll("circle").filter(d => d.x == thisDatum.x).attr("fill", "firebrick");
}).on("mouseout", function() {
d3.selectAll("circle").attr("fill", "palegreen")
})
function findPoints(datum) {
var myObject = {};
for (var i = 1; i < 4; i++) {
myObject["chart" + i] = dataObject["data" + i].filter(e => e.x === datum.x)[0].y;
}
console.log(JSON.stringify(myObject))
}
#svg1, #svg2 {
float: left;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="svg1"></div>
<div id="svg2"></div>
<div id="svg3"></div>
I am trying to create stacked bar chart using plottable.js from the same dataset. Is it possible?
Here is my sample dataset
var data = [{ x: 0, y: 1, m: 10 },
{ x: 1, y: 1, m: 9 },
{ x: 2, y: 3, m: 5 },
{ x: 3, y: 2, m: 5 },
{ x: 4, y: 4, m: 8 },
{ x: 5, y: 3, m: 7 },
{ x: 6, y: 5, m: 5 }];
In the example posted on http://plottablejs.org/components/plots/stacked-bar/, it used two dataset.
var plot = new Plottable.Plots.StackedBar()
.addDataset(new Plottable.Dataset(primaryData).metadata(5))
.addDataset(new Plottable.Dataset(secondaryData).metadata(3))
.x(function(d) { return d.x; }, xScale)
.y(function(d) { return d.y; }, yScale)
.attr("fill", function(d, i, dataset) { return dataset.metadata(); }, colorScale)
.renderTo("svg#example");
My question is, since I am using the same dataset and I need to change the 'y' function into two distinct functions to something like this:
.y(function(d) { return d.y; }, yScale)
.y(function(d) { return d.m; }, yScale)
Is it possible? If yes, how?
Thanks...
You can create two different Plottable.Datasets based on the same data.
So start like this:
var ds1 = new Plottable.Dataset(data);
var ds2 = new Plottable.Dataset(data);
However, later you'll need to distinguish the two datasets to know which attribute to use, so you should add metadata
var ds1 = new Plottable.Dataset(data, {name: "ds1"});
var ds2 = new Plottable.Dataset(data, {name: "ds2"});
now later, you can reference the name of the dataset to know which value to return in the .y() call
plot.y(function(d, i, ds){
if(ds.metadata().name == "ds1"){
return d.y;
}
else{
return d.m;
}
}, yScale);
Because there's very little NVD3 documentation, I'm unsure about how to modify the code to create pulsing bubbles in a scatter chart. I'd like to be able to be able to iterate through an array of data for each bubble, and animate the size of the bubble based on that data, in a continuous loop. At the moment my code is pasting all the data points on top of each other as soon as the chart is called.
I've found various examples of doing something similar using D3, but nothing exactly the same. And I'm still not sure how I would apply this to NVD3 as it doesn't seem to use the same enter/exit way of doing things as D3. I've provided an example of my current code and data below. I've also created a JSFiddle so you can see it (not) working: http://jsfiddle.net/9oyaypz0/. Any help would be very greatly appreciated.
function makeScatter(data, div, showLabels,title) {
nv.addGraph(function() {
var width = 450,
height = 275;
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.transitionDuration(350)
.sizeRange([0, 10000])
.margin({bottom:75})
.color(d3.scale.category10().range());
chart.tooltipContent(function(key, x, y, obj) {
return '<h4>' + key + '</h4><h5 style="clear:both;line-height:0.1rem;text-align:center;">' + obj.point.size + '</h5>';
});
chart.xAxis.tickFormat(function (d) { return ''; });
chart.yAxis.tickFormat(function (d) { return ''; });
chart.forceX([0]);
chart.forceX([5]);
chart.forceY([0]);
chart.forceY([6]);
chart.scatter.onlyCircles(false);
if(title) {
d3.select('title')
.append("text")
.attr("x", (width / 2))
.attr("y", 100)
.style("font-size", "16px")
.attr("text-anchor", "middle")
.text(title);
$(div).append("<h2>"+title+"</h2>");
}
d3.select(div)
.datum(data)
.transition().duration(1200)
.attr('width', width)
.attr('height', height)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
};
And the data is like so:
var data = [{ key: "Test Bubble 1",
values: [{ series: 0,
shape: "circle",
size: 1592,
x: 5,
y: 4 },
{ series: 1,
shape: "circle",
size: 1560,
x: 5,
y: 4 },
{ series: 2,
shape: "circle",
size: 1512,
x: 5,
y: 4 },
{ series: 3,
shape: "circle",
size: 1487,
x: 5,
y: 4 }]
},
{ key: "Test Bubble 2",
values: [{ series: 0,
shape: "circle",
size: 592,
x: 5,
y: 4 },
{ series: 1,
shape: "circle",
size: 560,
x: 5,
y: 4 },
{ series: 2,
shape: "circle",
size: 512,
x: 5,
y: 4 },
{ series: 3,
shape: "circle",
size: 487,
x: 5,
y: 4 }]
}]
I am charting different data with RickshawJS. But I need a way to update the chart when a user clicks the #search button. Right now it just creates a new chart below the old one, and that is pretty messy.
The user enters the page and enters some details and clicks the button to chart it. So ideally I'd like to start with an empty chart that isn't shown, but I can't really figure out how to remove the data from the chart and axes and then update it.
I could call $('#chart svg').remove(); on the chart and axes but it seems messy.
$('#search').click(function(event){
event.preventDefault();
var data = utils.malletData();
var graph = new Rickshaw.Graph( {
element: document.querySelector("#chart"),
width: 800,
height: 250,
series: [ {
name: data['name'],
color: 'steelblue',
data: data['series']
} ]
} );
graph.render();
var hoverDetail = new Rickshaw.Graph.HoverDetail( {
graph: graph,
xFormatter: function(x) {
var date = new Date(x).getTime();
return moment(x).format('MMMM Do YYYY, h:mm:ss a');
},
yFormatter: function(y) { return Math.floor(y) + " users" }
} );
var xAxis = new Rickshaw.Graph.Axis.X( {
graph: graph,
orientation: 'bottom',
element: document.getElementById('x_axis'),
tickFormat: function(x) { return moment(x).fromNow(); },
ticks: 7,
tickSize: 1,
} );
xAxis.render();
var ticksTreatment = 'glow';
var yAxis = new Rickshaw.Graph.Axis.Y( {
graph: graph,
orientation: 'left',
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
ticksTreatment: ticksTreatment,
element: document.getElementById('y_axis'),
} );
yAxis.render();
});
There's no official way to do so. However, you could leverage the fact that arrays in javascript are passed by reference and then update the graph.
Have a look at this demo on fiddle
var data = [
{
data: [ { x: 0, y: 120 }, { x: 1, y: 890 }, { x: 2, y: 38 }, { x: 3, y: 70 }, { x: 4, y: 32 } ],
color: "#c05020"
}, {
data: [ { x: 0, y: 80 }, { x: 1, y: 200 }, { x: 2, y: 100 }, { x: 3, y: 520 }, { x: 4, y: 133 } ],
color: "#30c020"
}
];
var graph = new Rickshaw.Graph( {
element: document.getElementById("chart"),
renderer: 'line',
height: 300,
width: 800,
series: data
} );
var y_ticks = new Rickshaw.Graph.Axis.Y( {
graph: graph,
orientation: 'left',
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
element: document.getElementById('y_axis'),
} );
graph.render();
$('button#add').click(function() {
data.push({
data: [ { x: 0, y: 200 }, { x: 1, y: 390 }, { x: 2, y: 1000 }, { x: 3, y: 200 }, { x: 4, y: 230 } ],
color: "#6060c0"
});
graph.update();
});