Using General update pattern in line graph - javascript

I have a demo here
Its a line bar chart using D3 in an Angular app.
I want the chart to be responsive so when the page is resized the chart width will increase and the height will be stay the same.
I'm doing this by capturing the window resize and then calling the function that draws the chart.
This works for the axis but I cant get the line and points to redraw.
I think it's to do with the way I'm trying to us the update pattern
How can I use the update pattern to redraw this line graph
const that = this;
const valueline = d3.line()
.x(function (d, i) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.y(function (d) {
return that.y(d.value);
});
this.x.domain(data.map((d: any) => d.date));
this.y.domain(d3.extent(data, function (d) {
return d.value
}));
const thisLine = this.chart.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
const totalLength = thisLine.node().getTotalLength();
thisLine.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength);
thisLine.transition()
.duration(1500)
.attr("stroke-dashoffset", 0)
let circle = this.chart.selectAll("line-circle")
.data(data);
circle = circle
.enter()
.append("circle")
.attr("class", "line-circle")
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.exit()
.remove()

You have problems in both circles' selection and the line selection.
The circles' selection:
You're selecting "line-circle". Instead of that, you have to select by class: ".line-circle";
You're reassigning the circle selection:
circle = circle.enter()//etc...
Don't do that, otherwise circle will point to the enter selection, not to the update selection anymore. Just do:
circle.enter()//etc...
The path:
You're appending a new path every time you call the function. Don't do that. Instead, select the existing path and change its d attribute, or append a new path if there is none. Both behaviours can be achieved with this code:
let thisLine = this.chart.selectAll(".line")
.data([data]);
thisLine = thisLine.enter()
.append("path")
.attr("class", "line")
.merge(thisLine)
.attr("d", valueline);
Here is your forked code: https://stackblitz.com/edit/basic-scatter-mt-vvdxqr?file=src/app/bar-chart.ts

Related

dot (symbol) color on d3.js multiline chart

I am trying to replicate this example of a multiline chart with dots. My data is basically the same, where I have an object with name and values in the first level, and then a couple of values in the second level inside values. For the most part, my code works, but for some reason, the j index in the anonymous function for the fill returns an array of repeated circle instead of returning the parent of the current element. I believe this may have something to do with the way I created the svg and selected the elements, but I can't figure it out. Below is an excerpt of my code that shows how I created the svg, the line path and the circles.
var svgb = d3.select("body")
.append("svg")
.attr("id","svg-b")
.attr("width", width)
.attr("height", height)
var gameb = svgb.selectAll(".gameb")
.data(games)
.enter()
.append("g")
.attr("class", "gameb");
gameb.append("path")
.attr("class", "line")
.attr("d", function(d) {return line_count(d.values); })
.style("stroke", function(d) { return color(d.name); })
.style("fill", "none");
gameb.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3)
.style("fill", function(d,i,j) {console.log(j)
return color(games[j].name);});
j (or more accurately, the third parameter) will always be the nodes in the selection (the array of circles here), not the parent. If you want the parent datum you can use:
.attr("fill", function() {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
return color(datum.name);
})
Note that using ()=> instead of function() will change the this context and the above will not work.
However, rather than coloring each circle independently, you could use a or the parent g to color the circles too:
gameb.append("g")
.style("fill", function(d) { return color(d.name); })
.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3);
Here we add an intermediate g (though we could use the original parent with a few additional modifications), apply a fill color to it, and then the parent g will color the children circles for us. The datum is passed on to this new g behind the scenes.

Change symbol for graph

I want to change the symbol type from circle to triangle, square, other symbols.
svg.selectAll().
data(data).enter()
.append("circle")
.attr("class", "dot")
.attr("cx", function(d, i) { return timeScale(d.year); })
.attr("cy", function(d, i) { return yScale(d.sale) })
.style("fill", "#FFC300")
.attr("r", function(d) {return est_size(d.est)})
If I change .append("circle") to .append("triangle"), the chart does not show the symbol. How can I show a triangle instead of a circle?
SVG doesn't have an element type for a triangle - the most basic shapes are rect and circle (there are also paths, polygons, ellipses, etc, but no triangle). However, we have a few options open to us, we can use a d3-symbol (available symbols listed here), or we can create our own symbol and use that.
For using d3-symbol we can do the following:
var width = 500;
var height = 300;
var data = d3.range(10)
.map(function(d) { return { x: Math.random()*width, y: Math.random()*height }; })
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
svg.selectAll(".symbol")
.data(data)
.enter()
.append("path")
.attr("d", d3.symbol().type(d3.symbolTriangle).size(50))
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" })
.attr("class","symbol");
// For demonstrating that the triangles are centered:
svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 3)
.attr("fill","orange")
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
symbol.size corresponds to shape area, not an edge length
Alternatively, we can create a function that returns a basic triangle polygon ourselves, and use it with selection.append():
var width = 500;
var height = 300;
var data = d3.range(10)
.map(function(d) { return { x: Math.random()*width, y: Math.random()*height }; })
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
var symbol = function() {
// Hand drawn triangle:
return d3.create('svg:path').attr("d","M0,8L-5,-3L5,-3Z").node()
}
svg.selectAll(".symbol")
.data(data)
.enter()
.append(symbol) // append can accept a function.
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" })
.attr("class","symbol");
// For demonstrating that the triangles are centered:
svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 3)
.attr("fill","orange")
.attr("transform",function(d) { return "translate("+[d.x,d.y]+")" });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
We could also take a few other approaches, such as using svg symbol elements, but the above two methods should be sufficient.

D3.js adding draggable circles not working when adding single

I am following this example:
https://bl.ocks.org/mbostock/6123708
the thing I can't understand is how I can possibly add new circles when a button is clicked, for example:
d3.tsv("dots.tsv", dottype, function (error, dots) {
container.append("g")
.attr("class", "dot")
.selectAll("circle")
.data(dots)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.call(drag);
});
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
return d;
}
self.addNode = function () {
container.append('g')
.attr('class', 'dot')
.append('circle')
.attr('r', 35)
.attr('cx', (i * 100) + cx)
.attr('cy', (i * 100) + cy)
//.style('fill', 'purple')
.call(drag);
i++;
};
The first part is the same as the example, I then created a function to add a single circle inside the container, the problem is that when I drag the new added circle I can move only the external G element, thus moving every other circle together.
I can't understand why, as the functions are the same (I removed even the style 'fill' to be sure)
You are giving your layout a data in .data(dots) but when you are adding a node in your addNode function, the layout is unaware of this new data. What you want is to add/push the new node data to your data array(dots) and recall the drawing function.
Therefore, you should cut the code under d3.tsv into a function to call it again when you update the data.

d3.js doughnut chart :: Additional paths are rendered and not updated

I'm having problems rendering doughnut charts in d3. I have 2 doughnuts, that I'm creating side-by-side inside of a for each function::
data.forEach(function(d,i){...}
The charts render fine. When I go to update the chart, paths are redrawn. Not sure why this happening because I'm using .enter()
Any advise?
var singleDonutData = [1];
var donutSections=singleDonut.selectAll(".donutSections")
.data(singleDonutData)
.enter()
.append("g")
.attr("class","donutSections");
var pathGroup = svg.select("." + donutName).select(".donutSections").selectAll("path.arc")
.data(pie(d));
var path = pathGroup
.enter()
.append('path')
.style('fill', function(d){ //give arc a color in SVG Scale
return color(d.data.label);
})
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial angles;
You need to add corresponding class name on the generated path, for example:
var pathGroup = svg.select("." + donutName).select(".donutSections").selectAll("path.arc")
.data(pie(d));
var path = pathGroup
.enter()
.append('path')
.style('fill', function(d){
return color(d.data.label);
})
.attr("class", "arc") // corresponding to selectAll("path.arc")
.attr("d", arc)
.each(function(d) { this._current = d; }); angles;
So that when you update the chart, d3 can correctly select these already rendered path.
After this update, you also need to add code to handle the update selection. Something like this:
pathGroup.transition().duration(1000)
.attrTween("d", function(d){
// some transition animation code here if you need it.
})

Plotting svg circles based off csv data

I'm trying to plot circles from data in my csv file, but the circles are not appearing on the svg canvas. I believe the problem stems from how I load in the data (it gets loaded as an array of objects), but I'm not quite sure how to figure out what to do next.
Based off this tutorial: https://www.dashingd3js.com/svg-text-element
D3.js code:
var circleData = d3.csv("files/data.csv", function (error, data) {
data.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(data);
});
var svg = d3.select("body").append("svg")
.attr("width", 480)
.attr("height", 480);
var circles = svg.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.KCComment; })
.attr("cy", function (d) { return d.pscoreResult; })
.attr("r", function (d) { return d.r; })
.style("fill", "green");
var text = svg.selectAll("text")
.data(circleData)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.KCComment; })
.attr("y", function(d) { return d.pscoreResult; })
.text(function (d) { return "( " + d.KCComment + ", " + d.pscoreResult + " )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
What the CSV looks like:
fmname, fmtype, KCComment, pscoreResult, r
test1, type1, 7.1, 8, 39
test2, type2, 1.2, 3, 12
You should have the circle-drawing code within the d3.csv function's callback, so it's only processed when the data is available.
d3.csv("data.csv", function (error, circleData) {
circleData.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(circleData);
// Do the SVG drawing stuff
...
// Finished
});
Also note that instead of setting var circleData = d3.csv(... you should just define it in the callback function.
Here's a plunker with the working code: http://embed.plnkr.co/fzBX0o/preview
You'll be able to see a number of further issues now: both circles are overlapping and only one quarter is visible. That's because your KCComment and pscoreResult values used to define the circles' cx and cy are too small. Try multiplying them up so that the circles move right and down and are a bit more visible! Same is true of the text locations, but I'll leave those problems for you to solve

Categories

Resources