D3 v4 graph not updating bars - but appending new axis data - javascript

I have seen this type of question asked a lot but no answers i found solved it for me.
I created a fiddle here of a stripped down simplified version of my code
https://jsfiddle.net/00m2cv84/4/
here is a snippet of the ganttish object - best to check out the fiddle though for context
const ganttish = function (data) {
this.dataSet = data
this.fullWidth = +document.getElementById('chart').clientWidth
this.fullHeight = 700
this.pad = { top: 50, right: 50, bottom: 50, left: 50 }
this.h = this.fullHeight - this.pad.top - this.pad.bottom
this.w = this.fullWidth - this.pad.left - this.pad.right
this.svg = d3.select('#chart')
.append('svg')
.attr('width', this.fullWidth)
.attr('height', this.fullHeight)
}
ganttish.prototype = {
redraw (newDataSet) {
this.dataSet = newDataSet // this should overwrite the old data
this.draw()
},
draw () {
let y = d3.scaleBand()
.padding(0.1)
.range([0, this.h])
y.domain(this.dataSet.map(d => d.idea_id))
let x = d3.scaleTime().range([this.pad.left, this.w + this.pad.left])
x.domain([
d3.min(this.dataSet, d => new Date(Date.parse(d.start_date))),
d3.max(this.dataSet, d => new Date(Date.parse(d.end_date)))
])
let xAxis = d3.axisBottom(x).tickSize(-(this.h), 0, 0)
let yAxis = d3.axisLeft(y).tickSize(-(this.w), 0, 0)
let xA = this.svg.append('g')
.attr('transform', 'translate(0,' + this.h + ')')
.attr('class', 'x axis')
.call(xAxis)
let yA = this.svg.append('g')
.attr('transform', 'translate(' + this.pad.left + ', 0)')
.attr('class', 'y axis')
.call(yAxis)
let timeBlocks = this.svg.selectAll('.timeBlock')
.data(this.dataSet)
let tbGroup = timeBlocks
.enter()
.append('g')
tbGroup
.append('rect')
.attr('class', 'timeBlock')
.attr('rx', 5)
.attr('ry', 5)
.attr('x', d => x(new Date(Date.parse(d.start_date))))
.attr('y', d => y(d.idea_id))
.attr('width', d => parseInt(x(new Date(Date.parse(d.end_date))) - x(new Date(Date.parse(d.start_date)))))
.attr('height', d => y.bandwidth())
.style('fill', (d) => {
return d.color ? d.color : 'rgba(123, 173, 252, 0.7)'
})
.style('stroke', 'black')
.style('stroke-width', 1)
tbGroup
.append('text')
.attr('class', 'timeBlockText')
.attr('x', d => x(new Date(Date.parse(d.start_date))) + 10)
.attr('y', d => y(d.idea_id) + (y.bandwidth() / 2))
.attr('alignment-baseline', 'middle')
.attr('font-size', '1em')
.attr('color', 'black')
.text(d => d.name)
/**
I have literally tried exit().remove() on pretty much everything i could think of :(
this.svg.exit().remove()
timeBlocks.exit().remove()
this.svg.selectAll('.timeBlock').exit().remove()
this.svg.selectAll('.timeBlockText').exit().remove()
this.svg.select('.x').exit().remove()
this.svg.select('.y').exit().remove()
*/
}
Sidenote:
I have a Vue js application and i'm implementing a Gantt(ish) style horizontal bar chart. In the fiddle i have simulated the Vue part by creating an Object which then you call the redraw method on it, this pattern simulates a watcher in the component updating the dataset when the parent data changes. The issue i face is the same.
issue:
When i change the data to the chart it does not update my chart rects or text. It does however append the new axis' on top of the old ones.
i understand that i should be calling .exit().remove() on anything .enter() 'd when finished in order to clear them for the next data push, but everywhere i try this it fails. I can get it to work by creating a fresh svg on every draw, but i understand i won't be able to do any transitions - and it seems a very bad approach :)
What is twisting my noodle is that if i push extra data to the new data object, it appends it fine, once - then does not do it again. It seems once element X in the data array has been added it will not update again.
https://jsfiddle.net/00m2cv84/5/
I know that the this.dataSet is updating, it just does not seem to be accepted by D3
Any help would be greatly appreciated for a D3 noob :)

You're problem is that you're not handling your data join properly. I'd highly recommend reading some of Mike Bostock's examples probably starting with Three Little Circles. To summarize though:
D3 joins created by calling .data() contain 3 selections:
Enter (DOM elements that need to be created as they exist in the data but don't yet exist in the DOM)
Exit (DOM elements that need to be removed, as they're not longer represented in the data)
Update (DOM elements that already exist and still exist in the data)
Within your code you're handling the enter() selection with this bit of code:
let tbGroup = timeBlocks
.enter()
.append('g')
The problem is you're not handling any of the other selections. The way I'd go about doing this is:
let join = this.svg.selectAll('.timeBlock').data(this.dataSet);
// Get rid of any old data
join.exit().remove();
// Create the container groups. Note that merge takes the "new" selection
// and merges it with the "update" selection. We'll see why in a minute
const newGroups = join.enter()
.append('g')
.merge(join);
// Create all the new DOM elements that we need
newGroups.append("rect").attr("class", "timeBlock");
newGroups.append('text').attr('class', 'timeBlockText');
// We need to set values for all the "new" items, but we also want to
// reflect any changes that have happened in the data. Because we used
// `merge()` previously, we can access all the "new" and "update" items
// and set their values together.
join.select(".timeBlock")
.attr('rx', 5)
.attr('ry', 5)
.attr('x', d => x(new Date(Date.parse(d.start_date))))
.attr('y', d => y(d.idea_id))
...
join.select(".timeBlockText")
.attr('x', d => x(new Date(Date.parse(d.start_date))) + 10)
.attr('y', d => y(d.idea_id) + (y.bandwidth() / 2))
.attr('alignment-baseline', 'middle')
.attr('font-size', '1em')
.attr('color', 'black')
.text(d => d.name)

Related

How to add popup on D3.js visualization (tree nodes)

We are using d3.js tree (d3.tree()) to render org chart visualization. It is very similar to
https://observablehq.com/#julienreszka/d3-v5-org-chart
I want to display small popup over the node on click/mouseover of some button. e.g. clicking on person image display small popup with some more actions. So user can click on any of the link in popup.
Is there any recommended approach to achieve it in D3js?
You can attach the event listener for click or mouseover when you update Nodes. For instance, the example you mentioned has this piece of code:
// Updating nodes
const nodesSelection = centerG.selectAll('g.node')
.data(nodes, d => d.id)
// Enter any new nodes at the parent's previous position.
var nodeEnter = nodesSelection.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.attr('cursor', 'pointer')
.on('click', function(d) {
if ([...d3.event.srcElement.classList].includes('node-button-circle')) {
return;
}
attrs.onNodeClick(d.data.nodeId);
})
If you check the onclick here, it is calling the NodeClick method; you will need to change NodeClick or, if you want a mouseover method, add an .on('mouseover') event. If you want to target the image in node, then add the event at this place:
nodeUpdate.selectAll('.node-image-group')
.attr('transform', d => {
let x = -d.imageWidth / 2 - d.width / 2;
let y = -d.imageHeight / 2 - d.height / 2;
return `translate(${x},${y})`
})
nodeUpdate.select('.node-image-rect')
.attr('fill', d => `url(#${d.id})`)
.attr('width', d => d.imageWidth)
.attr('height', d => d.imageHeight)
.attr('stroke', d => d.imageBorderColor)
.attr('stroke-width', d => d.imageBorderWidth)
.attr('rx', d => d.imageRx)
.attr('y', d => d.imageCenterTopDistance)
.attr('x', d => d.imageCenterLeftDistance)
.attr('filter', d => d.dropShadowId)

Dotplot distribution (frequency count)

I am trying to replicate a histogram that is made up of dots. Here is a good reference:
My data has the following format:
{'name':'Company1', 'aum':42, 'growth':16},
{'name':'Company2', 'aum':36, 'growth':24},
{'name':'Company3', 'aum':34, 'growth':19},
...
In my case I'm binning by aum and color coding by growth. Everything seemed fine up until I went to set the cy attribute.
graphGroup.selectAll('circ')
.data(data)
.enter()
.append('circ')
.attr('r',8)
.attr('cx', function(d) { return xScale(d.aum);})
.attr('cy', ??????)
.style('fill', function(d) { return colorScale(d.growth);});
The only way I can think of to handle this is to re-engineer the data to give it an explicit y index value or something. However, this is not supported in my statistics suite and the n is too large for me to manually do it.
Question
Given my current data structure, can d3 help me with anything ad hoc to get the correct y value? I was thinking a counter might work, but I was unable to devise a bin-specific counter. Maybe d3 has a more elegant solution than a counter.
...can d3 help me with anything ad hoc to get the correct y value?
No, there is no native method for this. But here is where most people get D3 wrong: D3 is not a charting tool (except for the axis module, D3 doesn't paint anything!), it's just a collection of JavaScript methods. This fact simultaneously gives D3 its weakness (steep learning curve) and its advantage (you can do almost anything with D3).
In your case, for instance, all I'd do is separate the data in bins (using a histogram generator, for instance, or just manipulating the data directly) and then appending the circles in each bin/group using their indices.
It can be just something like this:
circle.attr("cy", function(_, i) {
return maxValue - circleRadius * i;
});
This will append the first circle of each bin (group) at the base of the SVG, and then each subsequent one in a smaller y coordinate, according to its index.
Check this basic demo:
const data = d3.range(100).map(() => Math.random());
const svg = d3.select("svg");
const xScale = d3.scalePoint(d3.range(10).map(d => d / 10), [20, 480])
.padding(0.5);
const binData = d3.histogram()
.domain([0, 1])(data);
const g = svg.selectAll(null)
.data(binData)
.enter()
.append("g")
.attr("transform", d => "translate(" + xScale(~~(100 * d.x0) / 100) + ",0)");
g.selectAll(null)
.data(d => d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cy", function(_, i) {
return 220 - 13 * i;
})
.style("fill", "gray")
.style("stroke", "black");
const axisGroup = svg.append("g")
.attr("transform", "translate(0,230)")
.call(d3.axisBottom(xScale));
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="250"></svg>

Appended bars wont draw on svg element

I am trying to write a transitioning bar graph that uses two CVS files. I know that both of the files are loading properly because it shows in the console that the first one loads with the page and the second one loads when you click the update button.
The only thing that I have really thought of trying was changing the svg select to group instead of selecting all rectangles incase there was something screwed up there.
This block is creating the svg element, bringing in the first CSV file, and appending the rectangles onto the chart. My only thought for what the problem could be is that it is inside a function, but if I take it out of the function how do I bind the data to them?
//Creating SVG Element
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3.select('#chart')
.append('svg')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand().range([chart_pad_x, chart_w -
chart_pad_x]).padding(0.2);
var y_scale = d3.scaleLinear().range([chart_pad_y, chart_h -
chart_pad_y]);
//Data-------------------------------------------------------------------
d3.csv('data.csv').then(function(data){
console.log(data);
generate(data); });
function generate(data){
//Scale domains
x_scale.domain(d3.extent(data, function(d){ return d }));
y_scale.domain([0, d3.max(data, function(d){ return d })]);
//Create Bars
svg.select('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i){
return x_scale(i);
})
.attr('y', function(d){
return y_scale(d);
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d){
return y_scale(d);
})
.attr('transform',
"translate(0,0)")
.attr('fill', '#03658C')
'''
The results I have experienced is a blank window with just the update button. As previously stated I know that the data is being generated because I can see it in the console.
Try using the following:
svg.selectAll('rect')
.data(data)
If you use svg.select this will only make the data binding with the first element found.
d3.select(selector): Selects the first element that matches the specified selector string. If no elements match the selector, returns an empty selection. If multiple elements match the selector, only the first matching element (in document order) will be selected. For example, to select the first anchor element:
This should be clear if you inspect the DOM nodes.
To fix the issue lets change some things in your code:
Lets create a dummy fetch function:
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
You are also using a scaleBand with an incomplete domain by using the extent function:
d3.extent(): Returns the minimum and maximum value in the given iterable using natural order. If the iterable contains no comparable values, returns [undefined, undefined]. An optional accessor function may be specified, which is equivalent to calling Array.from before computing the extent.
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
console.log(x_scale.domain()) // [min, max]
The scaleBand needs the whole domain to be mapped
Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
If we continue using that scale we will be only to get two values for our x scale. Lets fix that with the correct domain:
x_scale.domain(data);
Lastly use the selectAll to create the data bind:
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
This should do the trick.
Complete code:
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3
.select('#chart')
.append('svg')
.style('background', '#f9f9f9')
.style('border', '1px solid #cacaca')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand()
.range([chart_pad_x, chart_w - chart_pad_x])
.padding(0.2);
var y_scale = d3.scaleLinear()
.range([chart_pad_y, chart_h - chart_pad_y]);
//Data-------------------------------------------------------------------
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
function generate(data) {
console.log(d3.extent(data, function(d) { return d }));
//Scale domains
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
// Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
x_scale.domain(data);
y_scale.domain([0, d3.max(data, function(d) {
return d
})]);
//Create Bars
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
}
Working jsfiddle here

D3JS passing in a JSON object returns an error

This is my code:
d3.json('../../dev/json/ajson.php', function (error, json) {
if (error) return console.warn(error);
megavalues = ["Meetings", "Development", "Management"];
megadata = [213, 891, 121];
var categoryGraph = d3.select('#graph-container').append('svg')
.attr('width', graphWidth)
.attr('height', graphHeight)
.data(megadata)
.append('svg:g')
.attr('transform', 'translate(' + graphWidth / 2.085 + ',' + graphHeight / 2.085 + ')');
var arc = d3.svg.arc()
.outerRadius(graphRadius - 10)
.innerRadius(graphRadius - 130);
var pie = d3.layout.pie()
.value(function(d){ return d });
var categoryDataGraph = categoryGraph.selectAll('g.slice')
.data(pie(megadata))
.enter().append('svg:g')
.attr('class', 'slice');
categoryDataGraph.append('svg:path')
.attr('fill', function(d, i){ return color(i); })
.attr('d', arc);
categoryDataGraph.append('text')
.attr('transform', function(d) { return 'translate(' + arc.centroid(d) + ')' })
.attr('dy', '.35em')
.attr('text-anchor', 'middle')
.text(function(d, i) { return megavalues[i]; });
This is the result:
http://cloud.erikhellman.com/eQHJ/5TmsqKWu
This is my problem:
I am currently drawing the graphs using 2 arrays (megavalues & megadata), but what I WANT to do is to draw it onto categoryGraph using this json object:
json.projectData.timeDistribution
which looks like this:
Object {Meetings: 12, Planning: 1, Development: 21}
However, when I pass the object as the data the graph isn't drawn and/or returns this error:
Uncaught TypeError: Object #<Object> has no method 'map'
I have spent the past 5 hours reading through the documentation and googling for help. This is the first time I am using D3.js and this far surpasses my JavaScript knowledge I think. On all the examples I have found there has either been arrays passed that only consisted of numbers or if there has been text included, the JSON has looked different. But I am sure it has to work even with my JSON object looking like this, right?
This is my first question here, if I can improve the question or improve anything here in any way, please tell me. :)
The "secret sauce" here is how you access the values in what's passed in. The difference between the two structures that you've tried is that one is an array (and works), whereas the other one is an object (and doesn't). This is because D3 won't iterate over an object (as suggested by the error message).
The easiest way to fix this is to transform your object into an array using d3.entries and changing the accessor function accordingly. That is, your code would look something like
var pie = d3.layout.pie()
.value(function(d) { return d.value; });
var categoryDataGraph = categoryGraph.selectAll('g.slice')
.data(pie(d3.entries(json.projectData.timeDistribution))
.enter().append('svg:g')
.attr('class', 'slice');
You then also need to change the way the text is created:
categoryDataGraph.append('text')
.attr('transform', function(d) { return 'translate(' + arc.centroid(d) + ')' })
.attr('dy', '.35em')
.attr('text-anchor', 'middle')
.text(function(d, i) { return d3.keys(json.projectData.timeDistribution)[i]; });

D3 - how to deal with JSON data structures?

I'm new to D3, and spent already a few hours to find out anything about dealing with structured data, but without positive result.
I want to create a bar chart using data structure below.
Bars are drawn (horizontally), but only for user "jim".
var data = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
var chart = d3.select("div#charts").append("svg")
.data(data)
.attr("class","chart")
.attr("width",800)
.attr("height",350);
chart.selectAll("rect")
.data(function(d){return d3.values(d.scores);})
.enter().append("rect")
.attr("y", function(d,i){return i * 20;})
.attr("width",function(d){return d;})
.attr("height", 20);
Could anyone point what I did wrong?
When you join data to a selection via selection.data, the number of elements in your data array should match the number of elements in the selection. Your data array has two elements (for Jim and Ray), but the selection you are binding it to only has one SVG element. Are you trying to create multiple SVG elements, or put the score rects for both Jim and Ray in the same SVG element?
If you want to bind both data elements to the singular SVG element, you can wrap the data in another array:
var chart = d3.select("#charts").append("svg")
.data([data])
.attr("class", "chart")
…
Alternatively, use selection.datum, which binds data directly without computing a join:
var chart = d3.select("#charts").append("svg")
.datum(data)
.attr("class", "chart")
…
If you want to create multiple SVG elements for each person, then you'll need a data-join:
var chart = d3.select("#charts").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "chart")
…
A second problem is that you shouldn't use d3.values with an array; that function is for extracting the values of an object. Assuming you wanted one SVG element per person (so, two in this example), then the data for the rect is simply that person's associated scores:
var rect = chart.selectAll("rect")
.data(function(d) { return d.scores; })
.enter().append("rect")
…
If you haven't already, I recommend reading these tutorials:
Thinking with Joins
Nested Selections
This may clarify the nested aspect, in addition to mbostock's fine answer.
Your data has 2 degrees of nesting. You have an array of 2 objects, each has an array of ints. If you want your final image to reflect these differences, you need to do a join for each.
Here's one solution: Each user is represented by a group g element, with each score represented by a rect. You can do this a couple of ways: Either use datum on the svg, then an identity function on each g, or you can directly join the data on the g. Using data on the g is more typical, but here are both ways:
Using datum on the svg:
var chart = d3.select('body').append('svg')
.datum(data) // <---- datum
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(function(d){ return d; }) // <----- identity function
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Using data on the group (g) element:
var chart = d3.select('body').append('svg')
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(data) // <--- attach directly to the g
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Again, you don't have to create these g elements, but by doing so I can now represent the user scores differently (they have different y from the transform) and I can also give them different styles, like this:
.jim {
fill: red;
}
.ray {
fill: blue;
}

Categories

Resources