I have the following function to draw different SVG shapes with D3.
updateChart(props){
var svg = d3.select(this.refs.svg);
let prefix = "XX";
// Render SVG Tags with ID and without attributes first
let objectsRender = svg.selectAll("line")
.data(props.data)
.enter()
.append(function(d) {
return document.createElementNS(
"http://www.w3.org/2000/svg",
d.objType);
})
.attr("id", function(d){ return prefix +d._id;})
.attr("class", "no-attr")
.on("click",(d)=>{this.modeClick(d);});
// Set Attributes for all classes with no attributes set so far
let objectsAttributes= svg.selectAll(".no-attr").each(function(){
let id= d3.select(this).attr("id");
let data = null;
data = props.data[0];
for(i = 0;i < props.data.length; i++){
if(id == prefix +props.data[i]._id){
data = props.data[i];
break;
}
}
if( data !== null){
d3.select(this)
.attr(data.coordinates)
.attr(data.style)
.attr("class", null);
}
}
);
}
props.data contains all the objects with the relevant svg data.
The problem I have right now, is that sometimes when this method is invoked,
D3 starts duplicating already existing SVG elements, even though the amount of data in props.data has not changed.
Can anyoe help me with that?
Related
Issue: I want to update the bars in my graph so that when the "Dreamworks" button is clicked, it appends new bars and gets rid of the old ones. I know it is an enter(), exit() issue, but I do not know exactly how to implement it.
Context: When my button is clicked, it activates a function that extracts the inner HTML of my button and uses it to filter my data so only observations from a company remain. The code works, but instead of getting rid of old bars, it appends the new bars on top of the old ones. When you look in the console, a new "g" element appears (which contains the new "rects") every time the button is clicked. I lowered the opacity of the "rects" to show what is going on. I removed all exit() and remove() attempts from my code because nothing was working.
HTML Code:
<div class= "button-holder">
<button class= "button button-dreamworks">DreamWorks</button>
<button class= "button button-disney">Disney</button>
<button class= "button button-pixar">Pixar</button>
</div>
<div class = "chart chart1"></div>
JS Code:
async function drawBar() {
// 2. Create Chart Dimensions
const width = 600
let dimensions = {
width,
height: width*0.6,
margin: {
top: 30,
right: 10,
bottom: 50,
left: 50
}
}
dimensions.boundedWidth = dimensions.width
-dimensions.margin.right -dimensions.margin.left
dimensions.boundedHeight = dimensions.height
-dimensions.margin.top -dimensions.margin.left
// 3. Draw Canvas
const wrapper = d3.select(".chart1")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)
// 4. Load Data
const raw_data = await d3.csv("./data/all_movie_data.csv")
const drawBarChart = function(company_name) {
const dataset = raw_data.filter(function(d){ return d["company"] == company_name })
const xAccessor = d => d["name"]
const yAccessor = d => parseFloat(d.budget)
let bounds = wrapper
.append("g")
.attr("class", "bounds")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
// 5. Create scales
const xScale = d3.scaleBand()
.domain(dataset.map(xAccessor))
.range([0,dimensions.boundedWidth])
.padding(0.4);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset,yAccessor))
.range([dimensions.boundedHeight, 0])
// 6. Draw Data
bounds.selectAll("rect")
.data(dataset)
.join("rect")
.attr("x", (d) => xScale(xAccessor(d)))
.attr("y", (d) => yScale(yAccessor(d)))
.attr("width", xScale.bandwidth())
.attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d)))
.attr("fill", "blue");
}
//6. Interactions
drawBarChart("Pixar");
const button1 = d3.select(".button-dreamworks")
.node()
.addEventListener("click", onClick1)
function onClick1() {
const company = document.querySelector(".button-dreamworks").innerHTML;
drawBarChart(company);
}
}
drawBar();
You can find a version of my code in this code pen: https://codepen.io/larylc/pen/XWzbQGy
Everything is the same except for the data, which I just made up to show the issue.
Answer: I understand the enter(), exit() structure now.
By setting the "rects" as a variable (which I called bars) I can now manipulate them ( this gave me a selection object that was different from before).
I can now add exit() and remove() to the bars variable. This was not possible before.
I moved the bounds variable to the section where my canvas was, which completed the exit() and enter() pattern. Every time a button was clicked, the SVG elements in the DOM (the bars) would match the number of data elements that were supposed to be added. So if I had 22 bars in the DOM, it would update to 34(or whatever the new dataset was). I tried this before and it worked but my bars were not updating correctly, which brings me to my last point.
The last problem was the bars would be added or removed to match the number of new data points without changing existing ones. This meant that the DOM would not update the bars to match the actual data. So if I had 13 existing bars and the new data was 22, it would just keep the 13 and add the last 9. This did not necessarily match the data. So I needed to add the merge(bars) statement to ensure that all my bars (including the existing ones) would update.
My New JS Code (with all of the buttons working)
async function drawBar() {
// Create Chart Dimensions
const width = 600
let dimensions = {
width,
height: width*0.6,
margin: {
top: 30,
right: 10,
bottom: 50,
left: 50
}
}
dimensions.boundedWidth = dimensions.width
-dimensions.margin.right -dimensions.margin.left
dimensions.boundedHeight = dimensions.height
-dimensions.margin.top -dimensions.margin.left
// Draw Canvas
const wrapper = d3.select(".chart1")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)
let bounds = wrapper
.append("g")
.attr("class", "bounds")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
// Load Data
const raw_data = await d3.csv("./data/all_movie_data.csv")
// Function that draws data
const drawBarChart = function(company_name) {
const dataset = raw_data.filter(function(d){ return d["company"] == company_name })
const xAccessor = d => d["name"]
const yAccessor = d => parseFloat(d.budget)
// Create scales
const xScale = d3.scaleBand()
.domain(dataset.map(xAccessor))
.range([0,dimensions.boundedWidth])
.padding(0.4);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset,yAccessor))
.range([dimensions.boundedHeight, 0])
// Draw Data
const bars = bounds.selectAll("rect")
.data(dataset)
bars.join("rect").merge(bars)
.attr("x", (d) => xScale(xAccessor(d)))
.attr("y", (d) => yScale(yAccessor(d)))
.attr("width", xScale.bandwidth())
.attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d)))
.attr("fill", "blue")
.attr("opacity", 0.4)
bars.exit().remove();
}
// Interactions
drawBarChart("Pixar");
// All Buttons and functions that triggers data change
const button1 = d3.select(".button-dreamworks")
.node()
.addEventListener("click", onClick1)
function onClick1() {
const company = document.querySelector(".button-dreamworks").innerHTML;
drawBarChart(company);
}
const button2 = d3.select(".button-disney")
.node()
.addEventListener("click", onClick2)
function onClick2() {
const company2 = document.querySelector(".button-disney").innerHTML;
drawBarChart(company2);
}
const button3 = d3.select(".button-pixar")
.node()
.addEventListener("click", onClick3)
function onClick3() {
const company3 = document.querySelector(".button-pixar").innerHTML;
drawBarChart(company3);
}
}
drawBar();
How can I access/modify an data related SVG element in place? I got an interval function, which executes every 3 seconds to control the data.
window.setInterval(function() {
for (var i = 0; i < data.length; i++) {
if(data[i].active) {
//THIS SVG node.. .style("fill", "green")
} else {
//THIS SVG node.. .style("fill", "red")
}
}
}, 3000)
Or do I need to re-initialize the whole D3.js graph each time?
I have a problem with a panel development. The panel generates a bar graph for the current reading and draws lines for the lower and upper limits. This works great. Now I want to draw a sparkline to it. I first draw this sparkline in a separate div element below the actual graph.
In the graph I have two rect elements, one for the background and one for the actual bar. My goal now is to determine the size of the background rect element, then assign this size to my div for the sparkline and then place the sparkline div over the graph.
But the problem is that I can't access the rect element (it's just not found).
Hopefully my question is understandable. What am I doing wrong?
Here is the code snippet:
...
var panelID = "dd-multistat-panel-" + id;
var tooltipDivID = "dd-multistat-panel-tooltip-" + id;
var RectBackID = "dd-multistat-panel-back-" + id;
var RectBackClass = "dd-multistat-panel-back-" + id;
var RectBarID = "dd-multistat-panel-bar-" + id;
...
// draw the background
svg
.append("g")
//.attr("id", RectBackID)
//.attr("class", RectBackClass)
.selectAll("rect")
.data(stripedata)
.enter()
.append("rect")
.attr("id", RectBackID)
.attr("class", RectBackClass)
.attr("width", w)
.attr("height", stripeScale.step())
.attr("x", left)
.attr("y", function (d) {
return stripeScale(d);
})
.attr("fill", "rgba(0,0,0,0)")
.attr("stroke", OutlineColor)
.on("mouseover", function (d) {
if (showTooltips || Links.length /* && i < data.length*/)
tooltipShow(d);
})
.on("mouseleave", function () {
if (!isInTooltip) {
tooltipHide(false);
}
});
...
//my seach code
var BarClassID = "." + panelID;
var PanelBackID = "#" + RectBackID;
var PanelBackClass = "." + RectBackClass;
console.log("var findBar = d3.select(" + BarClassID + ")");
var findBar = d3.select(BarClassID);
console.log(findBar.nodes()); // --> finds 1 svg
console.log("var findRect = findBar.selectAll(" + PanelBackID + ")");
var findRect = findBar.selectAll(PanelBackID);
console.log(findRect.nodes()); // --> finds 0 elements
console.log("var findRect2 = d3.selectAll(" + PanelBackID + ")");
var findRect2 = d3.selectAll(PanelBackID);
console.log(findRect2.nodes()); // --> finds 0 elements
console.log("var findRect3 = d3.selectAll(" + PanelBackClass + ")");
var findRect3 = d3.selectAll(PanelBackClass);
console.log(findRect3.nodes()); // --> finds 0 elements
console.log("var findRect4 = d3.selectAll(svg)");
var findRect4 = d3.selectAll("svg");
console.log(findRect4.nodes()); // --> finds 55 svg
console.log("var findRect5 = d3.selectAll(g)");
var findRect5 = d3.selectAll("g");
console.log(findRect5.nodes()); // --> finds 0 elements
console.log("var findRect6 = d3.selectAll(rect)");
var findRect6 = d3.selectAll("rect");
console.log(findRect6.nodes()); // --> finds 0 elements
Instead of creating multiple svgs, you should create one svg and append your bar graph and spakrline to that.
In case you stick to same logic, try using d3-selection to select your rect.
import * as d3Select from 'd3-selection';
d3Select.select('#your-rect-id');
Here are some helpful links -
ngx-charts
Simple Bar Graph
Graph Gallery
i'm using a rowchart to show the total of sales by item of a salesman.
Already tried a composite chart unsuccessfully like many posts from the google, but none of the examples uses a rowchart.
I need to do like the image, creating the red lines to represent the sale value target for each item, but i dont know how, can you guys help me? Thanks!
Actually this is my code to plot the rowchart
spenderRowChart = dc.rowChart("#chart-row-spenders");
spenderRowChart
.width(450).height(200)
.dimension(itemDim)
.group(totalItemGroup)
.elasticX(true);
Obviously you need a source for the target data, which could be a global map, or a field in your data.
I've created an example which pulls the data from a global, but it would also take from the data if your group reduction provides a field called target.
Then, it adds a new path element to each row. Conveniently the rows are already SVG g group elements, so anything put in there will already be offset to the top left corner of the row rect.
The only coordinate we are missing is the height of the rect, which we can get by reading it from one of the existing bars:
var height = chart.select('g.row rect').attr('height');
Then we select the gs and use the general update pattern to add a path.target to each one if it doesn't have one. We'll make it red, make it visible only if we have data for that row, and start it at X 0 so that it will animate from the left like the row rects do:
var target = chart.selectAll('g.row')
.selectAll('path.target').data(function(d) { return [d]; });
target = target.enter().append('path')
.attr('class', 'target')
.attr('stroke', 'red')
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M0,0 v' + height;
}).merge(target);
The final .merge(target) merges this selection into the main selection.
Now we can now animate all target lines into position:
target.transition().duration(chart.transitionDuration())
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M' + (chart.x()(d.value.target || _targets[d.key] || 0)+0.5) + ',0 v' + height;
});
The example doesn't show it, but this will also allow the targets to move dynamically if they change or the scale changes. Likewise targets may also become visible or invisible if data is added/removed.
thank you, due the long time to have an answer i've developed a solution already, but, really thank you and its so nice beacause its pretty much the same ideia, so i think its nice to share the code here too.
The difference its in my code i use other logic to clear the strokes and use the filter value of some other chart to make it dynamic.
.renderlet(function(chart) {
dc.events.trigger(function() {
filter1 = yearRingChart.filters();
filter2 = spenderRowChart.filters();
});
})
.on('pretransition', function(chart) {
if (aux_path.length > 0){
for (i = 0; i < aux_path.length; i++){
aux_path[i].remove();
}
};
aux_data = JSON.parse(JSON.stringify(data2));
aux_data = aux_data.filter(venda => filter1.indexOf(venda.Nome) > -1);
meta_subgrupo = [];
aux_data.forEach(function(o) {
var existing = meta_subgrupo.filter(function(i) { return i.SubGrupo === o.SubGrupo })[0];
if (!existing)
meta_subgrupo.push(o);
else
existing.Meta += o.Meta;
});
if (filter1.length > 0) {
for (i = 0; (i < Object.keys(subGrupos).length); i++){
var x_vert = meta_subgrupo[i].Meta;
var extra_data = [
{x: chart.x()(x_vert), y: 0},
{x: chart.x()(x_vert), y: chart.effectiveHeight()}
];
var line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveLinear);
var chartBody = chart.select('g');
var path = chartBody.selectAll('path.extra').data([extra_data]);
path = path.enter()
.append('path')
.attr('class', 'oeExtra')
.attr('stroke', subGruposColors[i].Color)
.attr('id', 'ids')
.attr("stroke-width", 2)
.style("stroke-dasharray", ("10,3"))
.merge(path)
path.attr('d', line);
aux_path.push(path);
}
}
})
And that's how it looks
While I'm on my way into learning the secrets of the beautiful world of Data Visualization, I'm encountering some difficulties with D3.js and the loading process of external data from a csv or a json file.
I'm kinda a newbie with JS so I'd like some help from experts.
Below my code:
var w = 500;
var h = 500;
// I'm setting up an empty array
var csvData = [];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Get the data
d3.csv("csv/cities.csv", function(dataset) {
for(var i = 0; i < dataset.length; i++) {
csvData.push(dataset[i]);
}
});
// Draw data
svg.select("body").selectAll("p")
.data(csvData)
.enter()
.append("p")
.text(function(d) { return d });
Well, I'm not sure I did understand well the correct way to load data and process these values. Can someone be so kind to give me an hint? I have access to the csvData array (using the console from the developers tools) but I can't see any data returned with the // Draw data section.
This is the csv file:
csv/cities.csv
city,state,population,land area
seattle,WA,652405,83.9
new york,NY,8405837,302.6
boston,MA,645966,48.3
kansas city,MO,467007,315.0
Thanks
The snippet below is an ajax call which loads csv/cities.csv asynchronously:
// Get the data
d3.csv("csv/cities.csv", function(dataset) {
for(var i = 0; i < dataset.length; i++) {
csvData.push(dataset[i]);
}
});
Thus the draw data section should have been like shown below:
// Get the data
d3.csv("csv/cities.csv", function(dataset) {
for(var i = 0; i < dataset.length; i++) {
csvData.push(dataset[i]);
}
// Draw data
svg.select("body").selectAll("p")
.data(csvData)
.enter()
.append("p")
.text(function(d) { return d });
});
Next Mistake:
You cannot add a p DOM inside SVG, that is the reason why you don't see the p DOM elements. I have appended the p DOM element into the body DOM that fixed the problem.
Working fiddle here