I am following the video tutorial (https://channel9.msdn.com/Blogs/Azure/Building-Power-BI-custom-visuals-that-meet-your-app-needs) to create a bar chart (custom visual) within Power BI.
Code snippet (in the video at 13.40):
let bars = this.barContainer.selectAll('.bar').data(viewModel.dataPoints);
bars.enter().append('rect').classed('bar', true);
bars
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category));
bars.exit().remove();
The above code is written in update method of visual.ts file.
As per my understanding, the enter() method will create rect elements with class bar based on the viewModel.dataPoints data.
The problem is I am not able to get it to work upon initial load, when the visual is loaded, this works only when the visual is resized. I cannot see any attributes getting applied on the rect elements upon load, but its getting applied when a resize is made on the visual.
If I changed the code to:
bars.enter().append('rect').classed('bar', true)
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category));
bars.transition()
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category));
bars.exit().remove();
The above code works fine, The attributes are getting applied for both initial load and resize.
Additional Info (Constructor code)
let svg = this.svg = d3.select(options.element).append('svg').classed('barChart', true);
this.barContainer = svg.append('g').classed('barContainer', true);
this.xAxis = svg.append('g').classed('xAxis', true);
Is this the way its supposed to work in the latest version (D3 Js 5.0)? or I am doing anything wrong?
The .merge() function does the trick.
The solution is:
let bars = this.barContainer.selectAll('.bar').data(viewModel.dataPoints);
const a = bars.enter().append('rect').merge(<any>bars); // merge before attr()
a.classed('bar', true);
a
.transition().duration(50)
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category)); // This code now works for initial
// load and update.
bars.exit().remove();
This should help talk you through merge. There is a link at the bottom if you would prefer to use the new join method in d3.js v5.
https://www.codementor.io/#milesbryony/d3-js-merge-in-depth-jm1pwhurw
Or for more complex use see Mike Bostock's
https://observablehq.com/#d3/selection-join
Related
So, I am faced with a situation where I have little to no experience coding in typescript and I still have to create a custom visual for PowerBI.
I am slowly slowly getting there through online tutorials and scavenging code...
I do not however understand the difference between update's handling of the barchart boxes and the labels in my code.
I tried implementing them identically, but while the chart itself updates fine, the old labels remain.
I have tried googling but it is a classic case of simply not knowing the term to google for so I am at a loss on how to proceed.
Here is the (hopefully) relevant part of my code. Any help is much appreciated:
const bars = this.barContainer
.selectAll('.bar')
.data(extractedData); /*select all bar elements and then passes data using "data" command and stores it in "bars" variable */
bars.enter()
.append('rect')
.classed('bar', true)
.attr('height', y.bandwidth())
.attr('width', dataPoint => width - cm - x(dataPoint.value))
.attr('x', dataPoint => (x(dataPoint.value) + cm) / 2)
.attr('y', dataPoint => y(dataPoint.category))
.attr('fill', dataPoint => dataPoint.color);
bars
.attr('height', y.bandwidth())
.attr('width', dataPoint => width - cm - x(dataPoint.value))
.attr('x', dataPoint => (x(dataPoint.value) + cm) / 2)
.attr('y', dataPoint => y(dataPoint.category));
const mylabels = this.textLabel
.selectAll('.textlabel')
.data(extractedData);
mylabels.enter()
.append('text')
.classed('textlabel', true)
.attr("class", "label")
.attr('x', dataPoint => (x(dataPoint.value) + cm) / 2)
.attr('y', dataPoint => y(dataPoint.category))
.attr("dy", ".75em")
.text(function (d) { return d.value; });
mylabels
.attr('x', dataPoint => (x(dataPoint.value) + cm) / 2)
.attr('y', dataPoint => y(dataPoint.category))
bars.exit().remove();
mylabels.exit().remove();
I am making a stacked bar chart in d3.js and have the following code which currently works for what I need:
const stackedData = d3.stack().keys(keys)
const layers = stackedData(this.data)
let time = this.chart.selectAll('.time').data(layers);
// removes time from the DOM
time.exit().remove();
// adds time to the DOM
this.chart.append("g")
.selectAll("g")
.data(layers)
.join("g")
.style("fill", (d, i) => colors[i])
.selectAll('.time')
.data(d => d)
.enter().append('rect')
.attr('class', 'time')
.attr('x', d => this.xScale(d.data.date))
.attr('y', d => this.yScale(0))
.attr("width", this.xScale.bandwidth())
.attr('height', 0)
.transition()
.delay((d, i) => i * 10)
.attr('y', d => this.yScale(d[1]))
.attr('height', d => this.yScale(d[0]) - this.yScale(d[1]))
However, I am unsure on what the general update pattern should be to update the shapes already present in the DOM on refresh. for a standard (non-stacked) chart, it would be as follows, however these does not work for a stacked chart:
// update existing time
this.chart.selectAll('.time').transition()
.attr('x', d => this.xScale(d.date))
.attr('y', d => this.yScale(d.revenue))
.attr('width', this.xScale.bandwidth())
.attr('height', d => this.graphHeight - this.yScale(d.revenue))
.attr("fill", "#007bff")
Could anyone please help me with the update pattern? For now this is what I get when I give my graph React component new data... the old one stays...
This is where I have my code https://github.com/danieltkach/react-d3-integration
I've been reading about it an I understand I have to do "stuff" in here... replace enter with join() ? And then do the 3 update functions inside? ...
// SVGs creation ---
const linkSvg = svgRef.current
.selectAll('path')
.data(LINKS, d=>d.agentName)
// .enter()
.join(
enter => enter.append('path')
.attr('stroke', d => !d.weight ? "gray" : linkStyles.onlineColor)
.attr('stroke-width', d => linkWeightScale(d.weight))
.attr('stroke-dasharray', d => {
if (!d.weight) return linkDashedScale(d.weight)
}),
update => update,
exit => exit.call(exit=>exit).remove()
)
I think your problem is here on GraphGenerator.jsx:
svgRef.current = d3.select('#viz')
.attr('width', svgWidth)
.attr('height', svgHeight)
.append('g') // a new group is appended
You're appending a new g element each time. That new element has no data, so nothing is removed or updated in the join. If you remove the .append('g') it should work.
EDIT:
Furthur down in your node creation you use:
const circles = svgRef.current
.selectAll()
.data(NODES)
.join("g");
You aren't selecting anything, so nothing can be updated. Use this there:
const circles = svgRef.current
.selectAll('.node')
.data(NODES)
.join("g")
.classed('node',true)
I am doing a number of transitions when my bar chart renders.
After these transitions have completed, I would like the values to render.
I am trying to use d3 transition.end but it looks like the code has changed from previous versions - I am using v6.
The code below runs without any delay - it doesn't wait for the transition to complete before invoking the function.
I have also tried .end(renderValuesInBars(data, metric, countryID, measurements) ) but the same thing happens.
Where am I going wrong?
function renderVerticalBars(data, measurements, metric, countryID) {
let selectDataForBarCharts = d3.select("svg")
.selectAll("rect")
.data(data, d => d[countryID])
selectDataForBarCharts
.enter()
.append("rect")
.attr('width', measurements.xScale.bandwidth())
.attr("height", 0)
.attr('y', d => measurements.yScale(0))
.merge(selectDataForBarCharts)
.transition().delay(500)
.attr("transform", `translate(0, ${measurements.margin.top})`)
.attr('width', measurements.xScale.bandwidth())
.attr('x', (d) => measurements.xScale(d[countryID]))
.transition()
.ease(d3.easeLinear)
.duration(setSpeed())
.attr("height", d => measurements.innerHeight - measurements.yScale(d[metric]))
.attr("y", (d) => measurements.yScale(d[metric]))
.attr("fill", d => setBarColor(d))
.on("end", renderValuesInBars(data, metric, countryID, measurements) )
selectDataForBarCharts.exit()
.transition().duration(500).attr("height", 0).attr("y", d => measurements.yScale(0)).remove()
}
Note that the .on("end", ...) method takes a callback for the second argument, which is executed when the transition ends. The code you posted is not passing a callback, but already evaluating the renderValuesInBars function at the moment of declaration. Instead, you want to pass a callback that tells d3 that the evaluation should occur at a later time (in that case, after the transition)
Instead of:
.on("end", renderValuesInBars(data, metric, countryID, measurements))
You can pass a callback that evaluates the function:
on("end", ( ) => renderValuesInBars(data, metric, countryID, measurements))
That way you're passing a callback that says "at the end of the transition, evaluate renderValuesInBars"
Has been changed from version 4.0
Previously it was:
element.each("end", callback)
Now it is:
element.on("end", callback)
I am having a stacked bar chart and i would like to animate the stacked bar chart. I am using d3js v5.
my project is available at https://stackblitz.com/edit/angular-2xgkwr
https://angular-2xgkwr.stackblitz.io
the animation i like is bars growing up as show here https://bl.ocks.org/guilhermesimoes/8913c15adf7dd2cab53a
really appreciate if you can help
I just updated the code and it starts to work
g.append('g')
.selectAll('g')
.data(d3.stack().keys(keys)(data))
.enter().append('g')
// tslint:disable-next-line:only-arrow-functions
.attr('class', d => z(d.key))
.selectAll('rect')
.data(d => d )
.enter().append('rect')
.attr('x', d =>
x(new Date(d.data.dateRange))
)
.attr("height", 0)
.attr('height', function(d) { return y(d[0]) - y(d[1]); })
.transition().duration(750).delay(calculateDelayFn)
.attr('y', function(d) {
return y(d[1]); }
)