I want to do an animation on the lines, but the second line will draw from two parts, one from begining, and the other from close to the second last point and disappear, so I got a result like this
I was following others'code
const pathLength = path.node().getTotalLength();
const transitionPath = d3.transition().ease(d3.easeQuad).duration(3000);
path
.attrs({
"stroke-dashoffset": pathLength,
"stroke-dasharray": pathLength,
})
.transition(transitionPath)
.attr("stroke-dashoffset", 0);
if you need all the code, I can paste, but it is really just this part that works with the animation, thank you!
I think you're accidentally using the same path length twice - namely that of the first path. path.node() returns the first node, even if there are multiple nodes in the selection.
const data = [{
category: "series_1",
values: [{
name: "A",
value: 10
},
{
name: "B",
value: 21
},
{
name: "C",
value: 19
},
{
name: "D",
value: 23
},
{
name: "E",
value: 20
},
],
}, ];
let counter = 1;
const add_set = (arr) => {
let copy = JSON.parse(JSON.stringify(arr[0]));
const random = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
copy.values.map((i) => (i.value = random()));
copy.category = `series_${counter}`;
arr.push(copy);
};
add(arr);
};
add_set(data);
//No.1 define the svg
let graphWidth = 600,
graphHeight = 300;
let margin = {
top: 60,
right: 10,
bottom: 30,
left: 45
};
let totalWidth = graphWidth + margin.left + margin.right,
totalHeight = graphHeight + margin.top + margin.bottom;
let svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
//No.2 define mainGraph
let mainGraph = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//No.3 define axises
let categoriesNames = data[0].values.map((d) => d.name);
let xScale = d3
.scalePoint()
.domain(categoriesNames)
.range([0, graphWidth]); // scalepoint make the axis starts with value compared with scaleBand
let colorScale = d3.scaleOrdinal(d3.schemeCategory10);
colorScale.domain(data.map((d) => d.category));
let yScale = d3
.scaleLinear()
.range([graphHeight, 0])
.domain([
d3.min(data, (i) => d3.min(i.values, (x) => x.value)),
d3.max(data, (i) => d3.max(i.values, (x) => x.value)),
]); //* If an arrow function is simply returning a single line of code, you can omit the statement brackets and the return keyword
//No.4 set axises
mainGraph
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(d3.axisBottom(xScale));
mainGraph.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
//No.5 make lines
let lineGenerator = d3
.line()
.x((d) => xScale(d.name))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX);
var lines = mainGraph
.selectAll(".path")
.data(data.map((i) => i.values))
.enter()
.append("path")
.attr("d", lineGenerator)
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("stroke", (d, i) => colorScale(i));
//No.6 append circles
let circleData = data.map((i) => i.values);
mainGraph
.selectAll(".circle-container")
.data(circleData)
.enter()
.append("g")
.attr("class", "circle-container")
.attr("fill", (d, i) => console.log(d) || colorScale(i))
.selectAll("circle")
.data((d) => d)
.enter()
.append("circle")
.attrs({
cx: (d) => xScale(d.name),
cy: (d) => yScale(d.value),
r: 3,
opacity: 1,
});
// HERE we let the lines grow
lines
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `0 ${pathLength}`
})
.transition()
.duration(2500)
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `${pathLength} ${pathLength}`
});
.line {
stroke: blue;
fill: none;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
Related
I am trying to graph this set of data (showing a snippet of a larger set) link to photo of chart shown below
month_started Member_casual count_of_rides
7 casual 435,289
8 casual 407,009
9 member 385,843
8 member 385,305
7 member 373,710
As you can see I have a month_started that maps to a member or casual attribute. I want to make a staked bar chart of the month_started as an independent variable (x) and the count_of_rides as a dependent (y) with the casual value stacked on top of the member per month.
I currently have a d3 bar chart that overlays both month_started = 7 on top of each other instead of stacked. I'm not sure how I can get d3 to recognize that it needs to read off of the member_casual variable to separate the two values.
I have additional code but here is the relevant sections I believe
Also, the .data(bike_trips) I believe should be .data(stackedData) but for some reason doesn't show up any bars, which makes me think my error could be with the stakedData variable.
Could anyone point me in the right direction please
bike_trip_chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg.append("g")
.attr("fill-opacity", .8)
.selectAll("rect")
.data(bike_trips)
.join("rect")
.attr("fill", (d) => colorScale(d.key))
.attr("x", d => x(d.month_started))
.attr("width", x.bandwidth())
.attr("y", d => y1(d.count_of_rides))
.attr("height", d => y1(0) - y1(d.count_of_rides));
svg.append("path")
.attr("fill", "none")
.attr("stroke", "#274e13")
.attr("stroke-miterlimit", 4)
.attr("stroke-width", 4)
.attr("d", line(chicago_weather));
svg.append("g")
.attr("fill", "none")
.attr("pointer-events", "all")
.selectAll("rect")
.data(bike_trips)
.join("rect")
.attr("x", d => x(d.month_started))
.attr("width", x.bandwidth())
.attr("y", 0)
.attr("height", height);
svg.append("g")
.call(xAxis);
svg.append("g")
.call(y1Axis);
svg.append("g")
.call(y2Axis);
return svg.node();
}
stackedData = d3.stack()
.keys(["member","casual"])
(bike_trips)
colorScale = d3.scaleOrdinal()
.domain(["member","casual"])
.range(["#E4BA14","#45818e"]);
Before you pass your data to d3.stack(), you want it to look like this:
[
{month: 7, casual: 435289, member: 373710},
{month: 8, casual: 407009, member: 385305},
{month: 9, member: 385843}
]
Here's a full example:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// set up
const margin = { top: 25, right: 25, bottom: 50, left: 50 };
const width = 500 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.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})`);
// data
const data = [
{ month_started: 7, member_casual: "casual", count_of_rides: 435289 },
{ month_started: 8, member_casual: "casual", count_of_rides: 407009 },
{ month_started: 9, member_casual: "member", count_of_rides: 385843 },
{ month_started: 8, member_casual: "member", count_of_rides: 385305 },
{ month_started: 7, member_casual: "member", count_of_rides: 373710 },
];
const keys = ["member", "casual"];
const months = Array.from(new Set(data.map(d => d.month_started))).sort(d3.ascending);
// get a map from the month_started to the member_casual to the count_of_rides
const monthToTypeToCount = d3.rollup(
data,
// g is an array that contains a single element
// get the count for this element
g => g[0].count_of_rides,
// group by month first
d => d.month_started,
// then group by member of casual
d => d.member_casual
);
// put the data in the format mentioned above
const countsByMonth = Array.from(monthToTypeToCount, ([month, counts]) => {
// counts is a map from member_casual to count_of_rides
counts.set("month", month);
counts.set("total", d3.sum(counts.values()));
// turn the map into an object
return Object.fromEntries(counts);
});
const stackedData = d3.stack()
.keys(keys)
// return 0 if a month doesn't have a count for member/casual
.value((d, key) => d[key] ?? 0)
(countsByMonth);
// scales
const x = d3.scaleBand()
.domain(months)
.range([0, width])
.padding(0.25);
const y = d3.scaleLinear()
.domain([0, d3.max(countsByMonth, d => d.total)])
.range([height, 0]);
const color = d3.scaleOrdinal()
.domain(keys)
.range(["#E4BA14","#45818e"]);
// axes
const xAxis = d3.axisBottom(x);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
const yAxis = d3.axisLeft(y);
svg.append('g')
.call(yAxis);
// draw bars
const groups = svg.append('g')
.selectAll('g')
.data(stackedData)
.join('g')
.attr('fill', d => color(d.key));
groups.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => x(d.data.month))
.attr('y', d => y(d[1]))
.attr('width', x.bandwidth())
.attr('height', d => y(d[0]) - y(d[1]));
// title
svg.append('g')
.attr('transform', `translate(${width / 2},${-10})`)
.attr('font-family', 'sans-serif')
.append('text')
.attr('text-anchor', 'middle')
.call(
text => text.append('tspan')
.attr('fill', color('member'))
.text('member')
)
.call(
text => text.append('tspan')
.attr('fill', 'black')
.text(' vs. ')
)
.call(
text => text.append('tspan')
.attr('fill', color('casual'))
.text('casual')
)
</script>
</body>
</html>
This question already has answers here:
Why my D3 line graphs shows black areas for each entity?
(1 answer)
Unclosed SVG path appears to be closed
(2 answers)
D3 line acting as a closed path
(2 answers)
Closed 2 years ago.
I would like to know what causes this, and I also want to insert another line to the chart, what is the right way to do that? I know how to update data, but don't know how to make multiple lines,
any help is appreciated, thank you!
D3.js is a JavaScript library for producing dynamic, interactive data visualizations in web browsers. It makes use of Scalable Vector Graphics, HTML5, and Cascading Style Sheets standards. It is the successor to the earlier Protovis framework.
const data = [{
name: "A",
x: 10,
},
{
name: "B",
x: 22,
},
{
name: "C",
x: 33,
},
{
name: "D",
x: 20,
},
{
name: "E",
x: 21,
},
];
//No.1 define the svg
let graphWidth = 600,
graphHeight = 450;
let margin = {
top: 30,
right: 10,
bottom: 30,
left: 85
};
let totalWidth = graphWidth + margin.left + margin.right,
totalHeight = graphHeight + margin.top + margin.bottom;
let svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
//No.2 define mainGraph
let mainGraph = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//No.3 define axises
let categoriesNames = data.map((d) => d.name);
let xScale = d3
.scalePoint()
.domain(categoriesNames)
.range([0, graphWidth]); // scalepoint make the axis starts with value compared with scaleBand
var yScale = d3
.scaleLinear()
.range([graphHeight, 0])
.domain([0, d3.max(data, (data) => data.x)]); //* If an arrow function is simply returning a single line of code, you can omit the statement brackets and the return keyword
//No.4 set axises
mainGraph
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(d3.axisBottom(xScale));
mainGraph.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
//No.5 make lines
var line = d3
.line()
.x(function(d) {
return xScale(d.name);
}) // set the x values for the line generator
.y(function(d) {
return yScale(d.x);
}) // set the y values for the line generator
.curve(d3.curveMonotoneX); // apply smoothing to the line
mainGraph
.append("path")
.datum(data) // 10. Binds data to the line
.attr("class", "line") // Assign a class for styling
.attr("d", line); // 11. Calls the line generator
<script src="https://d3js.org/d3.v6.min.js"></script>
You need to set fill: none; and stroke: <your line colour here> for the path. Otherwise, it thinks it's a closed shape and tries to fill it in.
That is because normally, paths are used to draw two-dimensional shapes. Only lines are assumed not to have two dimensions. See also the MDN docs
const data = [{
name: "A",
x: 10,
},
{
name: "B",
x: 22,
},
{
name: "C",
x: 33,
},
{
name: "D",
x: 20,
},
{
name: "E",
x: 21,
},
];
//No.1 define the svg
let graphWidth = 600,
graphHeight = 450;
let margin = {
top: 30,
right: 10,
bottom: 30,
left: 85
};
let totalWidth = graphWidth + margin.left + margin.right,
totalHeight = graphHeight + margin.top + margin.bottom;
let svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
//No.2 define mainGraph
let mainGraph = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//No.3 define axises
let categoriesNames = data.map((d) => d.name);
let xScale = d3
.scalePoint()
.domain(categoriesNames)
.range([0, graphWidth]); // scalepoint make the axis starts with value compared with scaleBand
var yScale = d3
.scaleLinear()
.range([graphHeight, 0])
.domain([0, d3.max(data, (data) => data.x)]); //* If an arrow function is simply returning a single line of code, you can omit the statement brackets and the return keyword
//No.4 set axises
mainGraph
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(d3.axisBottom(xScale));
mainGraph.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
//No.5 make lines
var line = d3
.line()
.x(function(d) {
return xScale(d.name);
}) // set the x values for the line generator
.y(function(d) {
return yScale(d.x);
}) // set the y values for the line generator
.curve(d3.curveMonotoneX); // apply smoothing to the line
mainGraph
.append("path")
.datum(data) // 10. Binds data to the line
.attr("class", "line") // Assign a class for styling
.attr("d", line); // 11. Calls the line generator
.line {
stroke: blue;
fill: none;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
I have created a D3 chart which I would like to update after the user clicks on one of the radio buttons. The value of the button isn't part of the data but an update of the time range along the X axis which should update as well as Y axis data based on the newly selected timeRange value (starting point is 36 hours from "now"). I can tell from console.log statements that the timeRange is updating when the user clicks on a radio button but can't figure out what I need to do to get that chart updated (the change() function is what I am trying to use to make this happen.
Here is a fiddle: https://jsfiddle.net/dtepdc/L1qf0bvk/
Here is my code:
const dataset = [
{
start_date: "2019-01-27T14:30:40",
end_date: "2019-01-27T16:32:25",
elapsed_time: 130,
coNum:"CO19044"
},
{
start_date: "2019-01-27T03:05:40",
end_date: "2019-01-27T03:32:25",
elapsed_date: 189,
coNum:"CO12904"
},
{
start_date: "2019-01-26T22:15:40",
end_date: "2019-01-26T23:32:25",
elapsed_time: 89,
coNum:"CO18345"
},
{
start_date: "2019-01-26T07:00:40",
end_date: "2019-01-26T07:40:25",
elapsed_time: 89,
coNum:"CO12005"
}
];
const coNumW = window.innerWidth,
coNumH = window.innerHeight,
margin = {top: coNumH * 0.15, right: coNumW * 0.05, bottom: coNumH * 0.12, left: coNumW * 0.12},
w = coNumW - margin.left - margin.right,
h = coNumH - margin.top - margin.bottom;
const xSc = d3.scaleTime().range([0, w]),
ySc = d3.scaleBand().range([h, 0]),
xAxis = d3.axisBottom(xSc),
yAxis = d3.axisLeft(ySc),
filtered = [],
dateFormat = d3.timeFormat("%Y-%m-%d %I:%M %p");
const svg = d3.select("body")
.append("svg")
.attr("width", coNumW)
.attr("height", coNumH)
.append("g").classed("no-select", true)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
let radio = d3.select('input[name="options"]:checked').property("value");
let timeRange = radio;
let start = moment().subtract(timeRange, 'hours').format('LLL');
const end = moment().format('LLL');
timeRange = this.value;
dataset.forEach(function(d, i) {
console.log('forEach timeRange: ', timeRange);
d.start_date = new Date(d.start_date);
d.end_date = new Date(d.end_date);
if (d.start_date >= new Date(start) && d.end_date <= new Date(end)) {
filtered.push(d);
}
});
xSc.domain([new Date(end), new Date(start)])
.range([0, w]);
ySc.domain(filtered.map(d => d.coNum)).padding(0.1);
console.log('xSc & ySc timeRange: ', timeRange)
svg.append("g")
.attr("class", "x Axis")
.attr("transform", "translate(0, " + h + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y Axis")
.call(yAxis);
const tasks = svg.append("g").attr("class", "dataCont")
.selectAll("g")
.data(filtered)
.enter()
.append("g")
.on("mouseenter", showData);
tasks.append("rect")
.attr("x", function(d) {
return xSc(d.start_date) + 2; // + 2 is for padding
})
.attr("y", function(d) {
return ySc(d.coNum);
})
.attr("width", function(d) {
return xSc(d.start_date) - xSc(d.end_date) - 2;
})
.attr("height", function(d) {
return ySc.bandwidth();
})
.attr("fill", "green");
d3.selectAll("input")
.on("change", change);
function change() {
timeRange = this.value;
dataset.forEach(function(d, i) {
console.log('forEach timeRange: ', timeRange);
d.start_date = new Date(d.start_date);
d.end_date = new Date(d.end_date);
if (d.start_date >= new Date(start) && d.end_date <= new Date(end)) {
filtered.push(d);
}
});
xSc.domain([new Date(end), new Date(start)])
.range([0, w]);
ySc.domain(filtered.map(d => d.coNum)).padding(0.1);
console.log('xSc & ySc timeRange: ', timeRange)
svg.append("g")
.attr("class", "x Axis")
.attr("transform", "translate(0, " + h + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y Axis")
.call(yAxis);
const tasks = svg.append("g").attr("class", "dataCont")
.selectAll("g")
.data(filtered)
.enter()
.append("g")
.on("mouseenter", showData);
tasks.append("rect")
.attr("x", function(d) {
return xSc(d.start_date) + 2; // + 2 is for padding
})
.attr("y", function(d) {
return ySc(d.coNum);
})
.attr("width", function(d) {
return xSc(d.start_date) - xSc(d.end_date) - 2;
})
.attr("height", function(d) {
return ySc.bandwidth();
})
.attr("fill", "green");
}
function showData(d) {
const dur = (d.end_date - d.start_date)/3600000;
console.log("-" + d.coNum + "- start_date: " + dateFormat(d.start_date) + " || end_date: " + dateFormat(d.end_date))
}
I've done something similar in a project. I think the main thing you are missing is that when you change the data you need to remove the current chart and draw a new one. You may be able to make Angular do this for you, but I'm not sure how.
Personally, I would refactor the logic into something like drawChart(filteredData) and filterData(unfilteredData, HOW TO FILTER). The initial draw on page load would be a call to drawChart with whatever data you want (it could go through filterData(). Your change() function would take the HOW TO FILTER from the radio button, pass it to filterData(), then remove the chart, then call drawChart with the filtered data. This architecture makes every draw of the chart the same and if you need changes to how the chart it drawn you can do it one place.
Example of new change function
change(howToFilter){
var filteredData = filterData(dataset, howToFilter);
CODE TO REMOVE THE CHART
drawChart(filteredData);
}
In the example below, I'm trying to animate new items appearance.
As you can see, they animate from the bottom of the chart to their position.
However, existing items ("second" in this example) jump, instead of smoothly transitioning to their new position.
I thought it is because the new band suddenly appears, without a transition. So, I tried to add a transition:
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate)
.transition(t)
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
But, I'm getting:
Uncaught TypeError: band.selectAll(...).data is not a function
Could you explain the error please, and suggest a way to avoid the undesired jump?
Bonus: How could I animate the y axis labels?
Playground
const width = 300;
const height = 200;
const margin = { top: 30, right: 30, bottom: 30, left: 50 };
let data = {};
const main = d3.select('.chart')
.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})`);
const xScale = d3.scaleLinear().domain([0, 16]).range([0, width]);
const xAxis = d3.axisBottom(xScale);
main.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
const yScale = d3.scaleBand().domain([]).range([0, height]);
const yAxis = d3.axisLeft(yScale);
const yAxisG = main.append('g').call(yAxis);
const bandG = main.append('g');
function update() {
const t = d3.transition().duration(500);
const ids = Object.keys(data);
yScale.domain(ids);
yAxisG.call(yAxis);
const bandHeight = yScale.bandwidth();
const bandUpdate = bandG.selectAll('.band').data(ids, id => id);
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate)
// .transition(t) // Throws: Uncaught TypeError: band.selectAll(...).data is not a function
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
bandUpdate.exit().remove();
const itemUpdate = band.selectAll('.item')
.data(id => data[id], item => item.value);
const itemG = itemUpdate.enter().append('g').attr('class', 'item');
const rectHeight = 4;
itemG
.append('rect')
.attr('class', (_, i) => `item-${i}`)
.attr('x', d => xScale(d.value))
.attr('width', d => width - xScale(d.value))
.attr('height', rectHeight)
.attr('y', height)
.transition(t)
.attr('y', bandHeight / 2 - rectHeight / 2);
itemG
.append('circle')
.attr('class', (_, i) => `item-${i}`)
.attr('cx', d => xScale(d.value))
.attr('r', 6)
.attr('cy', height)
.transition(t)
.attr('cy', bandHeight / 2);
itemUpdate
.select('rect')
.attr('x', d => xScale(d.value))
.attr('width', d => width - xScale(d.value))
.transition(t)
.attr('y', bandHeight / 2 - rectHeight / 2);
itemUpdate
.select('circle')
.attr('cx', d => xScale(d.value))
.transition(t)
.attr('cy', bandHeight / 2);
itemUpdate.exit().remove();
}
update();
setTimeout(() => {
data['first'] = [
{
value: 7
},
{
value: 10
}
];
update();
}, 1000);
setTimeout(() => {
data['second'] = [
{
value: 1
}
];
update();
}, 2000);
setTimeout(() => {
data['third'] = [
{
value: 13
}
];
update();
}, 3000);
svg {
margin: 0 30px 30px 30px;
}
.item-0 {
fill: red;
}
.item-1 {
fill: green;
}
<div class="chart"></div>
<script src="https://unpkg.com/d3#4.4.1/build/d3.js"></script>
Just break your band constant:
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate);
band.transition(t)
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
Here is the updated CodePen: http://codepen.io/anon/pen/oBWJdp?editors=0010
Explanation:
According to the documentation, selection.transition([name]):
Returns a new transition on the given selection with the specified name.
So, when you later in the code do this:
const itemUpdate = band.selectAll('.item')
.data(id => data[id], item => item.value);
You're selecting a new transition, and that's giving you the error (you cannot bind data to a transition).
Breaking the band constant makes itemUpdate a selection based in the band selection, not in the following transition.
I’m starting with d3.js and built a simple stacked chart.
Now I want to be able to update the chart with new dataset on click.
I followed tutorials and especially the Thinking with Joins article, the General Update Pattern example and this stackoverflow question but I didn’t manage to apply the enter/update/exit logic to my example.
As you can see in this fiddle, the updated axis are placed on top of the previous one and the chart doesn’t update with the new data.
var data1 = [
{month: 'Jan', A: 20, B: 5, C: 10},
{month: 'Feb', A: 30, B: 10, C: 20}
];
var data2 = [
{month: 'Mar', A: 10, B: 55, C: 100},
{month: 'Apr', A: 10, B: 70, C: 2}
];
var xData = ["A", "B", "C"];
var margin = {top: 20, right: 50, bottom: 30, left: 50},
width = 400 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], 0.35);
var y = d3.scale.linear()
.rangeRound([height, 0]);
var color = d3.scale.category20();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").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 + ")");
function draw(data) {
var dataIntermediate = xData.map(function (c) {
return data.map(function (d) {
return {x: d.month, y: d[c]};
});
});
var dataStackLayout = d3.layout.stack()(dataIntermediate);
x.domain(dataStackLayout[0].map(function (d) {
return d.x;
}));
y.domain([0,
d3.max(dataStackLayout[dataStackLayout.length - 1],
function (d) { return d.y0 + d.y;})
])
.nice();
var layer = svg.selectAll(".stack")
.data(dataStackLayout);
layer.exit().remove(); // has no effect
layer.enter().append("g")
.attr("class", "stack")
.style("fill", function (d, i) {
return color(i);
});
var rect = layer.selectAll("rect")
.data(function (d) {
return d;
});
rect.exit().remove(); // has no effect
rect.enter().append("rect")
.attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) {
return y(d.y + d.y0);
})
.attr("height", function (d) {
return y(d.y0) - y(d.y + d.y0);
})
.attr("width", x.rangeBand());
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "axis")
.call(yAxis);
}
function updateData() {
draw(data2);
}
d3.select('#update')
.on("click", updateData);
draw(data1);
Could you please explain where to insert the exit there and why?
Many thanks
To summarize the enter/update/exit logic;
When you first create your svg, the enter part takes the new nodes and appends them to the svg.
When you update your svg, the select part updates your data and styles. And the exit part removes the data as you know.
So according to this your pattern is almost correct but when you select your new data(update), you are not updating your styles.
Here's the part you need to change:
var rect = layer.selectAll("rect")
.data(function (d) {
return d;
}).attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) {
return y(d.y + d.y0);
})
.attr("height", function (d) {
return y(d.y0) - y(d.y + d.y0);
})
.attr("width", x.rangeBand());
And here's the updated fiddle: https://jsfiddle.net/8gp8x89c/2/
Note that the axis' are still present so you either remove and re-append them or apply the update pattern to them also. I leave that part to you.