i'm pretty new to d3. Could you help me, please, why doesn't my graph redraw after I generate new data on button press? It draws once, but after I push button it only logs new data array to console. Do I remove old data wrong?
Just in case I import d3 functions separately this is why it's written select
instead of d3.select
. Everything works besides updating graph.
Thank you in advance. There is a similar question here, and I tried to do it in a similar way, but I don't get what am I doing wrong.
const randomValue = randomInt(0, 100);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sen', 'Oct', 'Nov', 'Dec'];
let data = months.map(m => ({
label: m,
value: randomValue()
}))
let values = data.map(m => m.value)
const yScale = scaleLinear().domain([0, 100]).range([0, height]);
const colorScale = scaleLinear().domain([0, 100]).range(['#d22626', '#086cdc']);
const $chartContainer = $root.append('g')
.classed('.mz-chart__container', true)
.attr('transform', `translate(${padding}, ${100})`);
const xScale = scaleBand().domain(months).range([0, width]).paddingInner(0.2);
const hAxis = axisBottom().scale(xScale);
$root.append("g")
.attr('transform', `translate(${padding}, ${height 100})`)
.call(hAxis);
const vAxisScale = scaleLinear().domain([0, 100]).range([height, 0]);
const vAxis = axisLeft()
.scale(vAxisScale)
.tickSize(-width, 0, 0)
.tickFormat((y, x) => y);
$root.append("g")
.attr("transform", "translate(40, 100)")
.call(vAxis);
function updateChart() {
data = months.map(m => ({
label: m,
value: randomValue()
}))
console.log(data);
let chartbars = $chartContainer.selectAll('.mz-chart__bar').data(data)
chartbars
.exit()
.remove()
chartbars
.enter()
.append('g')
.classed('mz-chart__bar', true)
.append('rect')
.attr('x', (d, i) => xScale(d.label))
.attr('y', (d, i) => height - yScale(d.value))
.attr('height', (d, i) => yScale(d.value))
.attr('width', (d, i) => xScale.bandwidth())
.attr('fill', d => colorScale(d.value));
}
select(".mz-button").on("click", updateChart)
updateChart()
CodePudding user response:
You have an enter selection and an exit selection, but you aren't using the udpate selection.
selection.enter() only creates elements if there are fewer selected elements than there are items in the data array: it is used to create all your elements to start with because none exist. The enter selection does not contain pre-existing elements however, these are in the update selection (chartbars
).
We can force existing and new bars to have the appropriate attributes by merging them and then styling them:
let newbars = chartbars
.enter()
.append('g')
.classed('mz-chart__bar', true)
.append('rect');
chartbars.select('rect').merge(newbars)
.attr('x', (d, i) => xScale(d.label))
.attr('y', (d, i) => height - yScale(d.value))
.attr('height', (d, i) => yScale(d.value))
.attr('width', (d, i) => xScale.bandwidth())
.attr('fill', d => colorScale(d.value));
eg:
const randomValue = Math.random;
const height = 300;
const width = 500;
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sen', 'Oct', 'Nov', 'Dec'];
const $root = d3.select("body").append("svg").attr("width",width).attr("height",height);
const padding = {left: 50, right: 10, top: 40, bottom: 40}
let data = months.map(m => ({
label: m,
value: randomValue()
}))
let values = data.map(m => m.value)
const $chartContainer = $root.append("g")
.attr("transform", `translate(${padding.left},${padding.top} )`)
.classed('.mz-chart__container', true)
const plotWidth = width - padding.left - padding.right;
const plotHeight = height - padding.top - padding.bottom;
const yScale = d3.scaleLinear().domain([0, 100]).range([plotHeight,0]);
const xScale = d3.scaleBand().domain(months).range([0,plotWidth]).paddingInner(0.2);
const colorScale = d3.scaleLinear().domain([0, 100]).range(['#d22626', '#086cdc']);
const hAxis = d3.axisBottom().scale(xScale);
const vAxis = d3.axisLeft()
.scale(yScale)
.tickSize(-plotWidth, 0, 0)
.tickFormat((y, x) => y);
$chartContainer.append("g")
.attr('transform', `translate(0, ${plotHeight})`)
.call(hAxis);
$chartContainer.append("g")
.call(vAxis);
function updateChart() {
data = months.map(m => ({
label: m,
value: randomValue() * 100
}))
// console.log(data);
let chartbars = $chartContainer.selectAll('.mz-chart__bar').data(data)
chartbars
.exit()
.remove()
let newbars = chartbars
.enter()
.append('g')
.classed('mz-chart__bar', true)
.append('rect');
chartbars.select('rect').merge(newbars) // merge the update and enter selections
.attr('x', (d, i) => xScale(d.label))
.attr('y', (d, i) => plotHeight - yScale(d.value))
.attr('height', (d, i) => yScale(d.value))
.attr('width', (d, i) => xScale.bandwidth())
.attr('fill', d => colorScale(d.value));
}
d3.select("body").on("click", updateChart)
updateChart()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Because we have a nested element added with enter().append() but we are selecting the parent g
elements, we need to select a child rectangle for each element in the update selection with chartbars.select('rect') and then merge it with newbars (which is a selection of newly added rectangles).
I've reworked your code with respect to spacing and sizing - I also am not sure, but I believe you wanted to have red bars be larger (which required switching the values in the y scale range). Lastly, I removed the redundant y scale.