D3 - Change stacked bar chart from clicking on legend - d3.js

I have a plunker here - https://plnkr.co/edit/7Eg34HyhXY6M3UJccAFq?p=preview
Its a simple stacked bar chart
I wanted to be able to click on a color in the legend and update the chart to show just that bar only
Ive found a few examples online but they are a bit too complex - just needed a simple method if thats possible
let legendItems = legend.selectAll('li')
.data(legendKeys)
.enter()
.append('li');
legendItems.append('span')
.attr('class', 'rect')
.style('background-color', (d, i) =>{
return colors[i];
});
legendItems.append('span')
.attr('class', 'label')
.html((d) => {
return d
});

You should bind click event handler this way:
let legendItems = legend.selectAll('li')
.data(legendKeys)
.enter()
.append('li')
.on('click', handleLegendItemClick);
Handler-function should looks like this (pay attention on the comments):
function handleLegendItemClick(d, i) {
// change opacity to show active legend item
legendItems.filter(legendItem => legendItem !== d).style('opacity', 0.5);
legendItems.filter(legendItem => legendItem === d).style('opacity', 1);
// update domain of y scale and update tick on y axis
y.domain([0, stackedSeries[i][0].data[d]]);
yAxis.transition().call(d3.axisLeft(y));
// bind new data
let enter = rects.data([stackedSeries[i]])
.enter()
.append('rect');
// remove old rects
rects.exit();
// draw new rect
rects.merge(enter)
.attr('height', (d) => {
return height - y(d[0].data[d.key]);
})
.attr('y', (d) => {
return 0;
})
.style('fill', (d, i) => {
return colorScale(d);
});
}
Check this fork of your plnkr. (I also add color scale - colorScale for applying a color by legend key name, not by index as you did.)
const colorScale = d3.scaleOrdinal(colors);
colorScale.domain(legendKeys);

Related

d3 General Update Pattern

I just mimic the code d3 update pattern trying to render some rect with updated data
here is my code.
function update(data){
var r = g.selectAll("rect").data(data,function(d){return (d)});
r.exit().attr("class","exit").remove()
r
.attr("class","update")
.attr("x",(d, i) =>{return i* (50+interval)})
.attr("y", (d)=>{ return y(d)})
.attr("width", "20px")
.transition(t)
.attr("height",( d => { return height-padding.top-padding.bottom-y(d);}))
r.enter()
.append("rect")
.attr("width", "20px")
.attr("class","new")
.attr("x",(d, i) =>{ return i * (50+interval)})
.attr("y", (d)=>{return y(d)})
.attr("height",( d => { return height-padding.top-padding.bottom-y(d);}))
}
then I call the update function twice
update([3,2,1,5,4,10,9,7,8,6])
setTimeout(()=>{update([2,3,1,5,4,10,9,7,8,6])},1000)
Expected: only the first and second rect will be rerendered and set class "new", but in fact, all the rect will be set class "new" .
Codepen
The enter/exit pattern works when the data is an array of identified objects.
Replace this code:
var r = g.selectAll("rect").data(data,function(d){return (d)});
with:
const _data = data.map((v,i) => ({id: i, value: v}));
const r = g.selectAll("rect").data(_data,d => d.id);
The D3 will identify each object and update it accordingly instead of replacing with a new one.
See it's working in a pen
UPD:
If you want to highlight the items whose values have been changed, you can save the current value in an attribute of a newly added item:
r.enter()
.append("rect")
.attr('cur-value', d => d.value)
...
then, on update, query the value and compare with the one in datum:
r.attr("class","update")
...
.each(function(d) {
const rect = d3.select(this);
const prevValue = parseInt(rect.attr('cur-value'));
rect.attr('cur-value', d.value);
rect.style('fill', prevValue === d.value ? 'black' : 'red')
});
You can see it's working in the updated pen.

Why d3.event in dragging callback is somehow related to data coordinate

I'm playing with d3.js and I noticed a weird behavior with dragging.
If I add a circle using data and scaleX/scaleY, when I drag it the d3.event.y value is relative to the xy coordinates inside the "data" array, instead of the top left corner...
Here is the code and a jsFiddle: if you open the console, if you drag the first circle the reference is the top left corner, if you drag the second circle the reference is something related to {x: 3, y: 2}
var circle2 = svgGraph
.selectAll('.circle2')
.data([{ x: 3, y: 2 }])
.enter()
.append('circle') // Uses the enter().append() method
.attr('cx', scaleX(100))
.attr('cy', scaleY(100))
.attr('r', 15)
.call(d3.drag()
.on('start', (d) => { })
.on('drag', onHandlerDrag())
.on('end', (d) => { }));
function onHandlerDrag() {
return (d) => {
console.log(d3.event.y);
}
}
Is the behavior intended? Do you have any reference about it?
Thanks
Yes, this is the intended behaviour. In fact, we normally use those x and y properties in a drag event to avoid the element jumping around the container (when one is lazy enough to specify the correct subject).
Since you asked for a reference, have a look at drag.subject:
The subject of a drag gesture represents the thing being dragged. It is computed when an initiating input event is received, such as a mousedown or touchstart, immediately before the drag gesture starts.
Then, in the next paragraph, the most important information to you:
The default subject is the datum of the element in the originating selection (see drag) that received the initiating input event; if this datum is undefined, an object representing the coordinates of the pointer is created. When dragging circle elements in SVG, the default subject is thus the datum of the circle being dragged. (emphasis mine)
And finally:
The returned subject should be an object that exposes x and y properties, so that the relative position of the subject and the pointer can be preserved during the drag gesture.
So, if you don't want the x and y in the datum being used by drag, just set a different object in the subject:
.subject(function() {
return {
foo: d3.event.x,
bar: d3.event.y
};
})
Here is your code with that change:
function onHandlerDrag() {
return (d) => {
console.log(d3.event.y);
}
}
var scaleX = d3
.scaleLinear()
.domain([0, 500])
.range([0, 500]);
var scaleY = d3
.scaleLinear()
.domain([0, 500])
.range([0, 500]);
var svgGraph = d3
.select('.area')
.attr('width', 500)
.attr('height', 500);
var circle1 = svgGraph
.append('circle')
.attr('cx', scaleX(20))
.attr('cy', scaleY(20))
.attr('r', 15)
.call(d3.drag()
.on('start', (d) => {})
.on('drag', onHandlerDrag())
.on('end', (d) => {}));
var circle2 = svgGraph
.selectAll('.circle2')
.data([{
x: 3,
y: 2
}])
.enter()
.append('circle') // Uses the enter().append() method
.attr('cx', scaleX(100))
.attr('cy', scaleY(100))
.attr('r', 15)
.call(d3.drag()
.subject(function() {
return {
foo: d3.event.x,
bar: d3.event.y
};
})
.on('start', (d) => {})
.on('drag', onHandlerDrag())
.on('end', (d) => {}));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg class='area'></svg>

Positioning and grouping SVG D3/SVG/ionic

A D3 bubblechart. Group and position svg:circles and svg:text elements
the function render() creates an svg element, circle and text as usual. This function includes .exit.remove update patterns.
runSimulation() is executed after page opening and a createChart() function.
click on a circle executes runSimulation() again, removing the circle
with .exit().remove() etc.
Simplified code:
fundtion render (){
const nodeEnter = this.nodeElements
.enter()
.append('svg:svg');
nodeEnter
.append('circle')
.on('click',runSimulation);
const textEnter = this.textElements
.enter()
.append('svg:text');
}
this.runSimulation(){
this.render();
function ticked(){
this.nodeElements
.attr('cx', cxValue)
.attr('cy', cyValue):
}
this.simulation.nodes.on.('tick',ticked);
}
On the first run the cx and cy attributes are appended to the svg:svg while the circles do not have the attributes and everything is rendered in the top left corner ( also with using svg:g)
on the click action the runSimulation is executed a second time; now the circle gets the cx and cy attributes attached and all elements move into the expected position.
-I am looking for a way to get the cx cy attributes to the circle on the first rendering so that the parent elements do not cluster at x=0 y =0, or to get x and y to svg:svg; the shown pattern is not working and I appreciate your help.
this.nodeElements = this.nodeGroup.selectAll('g')
.data(this.data, node => node.nodeName);
this.nodeElements.exit().remove();
const nodeEnter = this.nodeElements
.enter()
.append('g');
nodeEnter
.append('circle')
.attr('fill', node => this.color(node.familyType))
.attr('r', function (node) {
return (node.nodeName.length > 10) ? "75" : node.nodeName.length*6;
})
.on('click', this.runSimulation);
let wrap = (text) => {
text.each(function() {
let text = d3.select(this),
const textnode = nodeEnter
.append('text')
.attr('id', 'text')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('dy', '0.1em')
.attr('font-size', '1.2em');
this.settings.getValue('option1').then((option) => {
if (option === true) {
let type = node => node.nodeName;
textnode
.text(type);
}
if (option === false) {
let type = node => node.otherName;
textnode
.text(type);
}
})
Thank you for your answer, Steven. runSimulation executes a tick function that does add the proper d.x and d.y now. I am not sure why it did not work - ionic lifecycle events ?

Is it possible to add labeled points to a dc.js line chart?

I like dcjs, http://bl.ocks.org/d3noob/6584483 but the problem is I see no labels anywhere for the line chart (Events Per Hour). Is it possible to add a label that shows up just above the data point, or even better, within a circular dot at the tip of each data point?
I attempted to apply the concepts in the pull request and came up with:
function getLayers(chart){
var chartBody = chart.chartBodyG();
var layersList = chartBody.selectAll('g.label-list');
if (layersList.empty()) {
layersList = chartBody.append('g').attr('class', 'label-list');
}
var layers = layersList.data(chart.data());
return layers;
}
function addDataLabelToLineChart(chart){
var LABEL_FONTSIZE = 50;
var LABEL_PADDING = -19;
var layers = getLayers(chart);
layers.each(function (d, layerIndex) {
var layer = d3.select(this);
var labels = layer.selectAll('text.lineLabel')
.data(d.values, dc.pluck('x'));
labels.enter()
.append('text')
.attr('class', 'lineLabel')
.attr('text-anchor', 'middle')
.attr('x', function (d) {
return dc.utils.safeNumber(chart.x()(d.x));
})
.attr('y', function (d) {
var y = chart.y()(d.y + d.y0) - LABEL_PADDING;
return dc.utils.safeNumber(y);
})
.attr('fill', 'white')
.style('font-size', LABEL_FONTSIZE + "px")
.text(function (d) {
return chart.label()(d);
});
dc.transition(labels.exit(), chart.transitionDuration())
.attr('height', 0)
.remove();
});
}
I changed the "layers" to be a new group rather than using the existing "stack-list" group so that it would be added after the data points and therefore render on top of them.
Here is a fiddle of this hack: https://jsfiddle.net/bsx0vmok/

Access x value on axis d3

I think this is a simple question but I'm not getting it. I have the following code for the x-axis of my bar chart, and I'm trying to access the x-value when the corresponding bar is clicked. I've tried selecting xAxis, x, and .domain, but I'm getting null values.
var x = d3.scale.ordinal()
.domain(['191','192','255','902'])
.rangeRoundBands([margin,w-margin], .1)
var xAxis = d3.svg.axis().scale(x).orient("bottom").tickSize(3, 0)
svg.selectAll(".series")
.data(ratiodata)
.enter()
.append("g")
.classed("series",true)
.style("fill","url(#gradient)")
.selectAll("rect").data(Object)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x",function(d,i) { return x(x.domain()[i])})
.attr("y",function(d) { return y(d.y0)})
.attr("height",function(d) { return y(0)-y(d.size)})
.attr("width",x.rangeBand())
.on("click", function(d,i) {
//Clicking on the bar currently displays elements from another dataset.//
//Ratiodata is only used for displaying the bars//
});
You can get the value in the same way in that you're setting it to start with:
.on("click", function(d, i) {
console.log(x.domain()[i]);
});

Resources