In the following code I tried to create a visualization for a market on which one buys per hour. I tried to follow v5s update pattern but it won't let me join two text different <text> elements. The last added join overwrites the first so 8
I've looked around but I can not find anything related to an update pattern for two of the same elements.
https://jsfiddle.net/itsahoax/gd2uew73/7/
const updateCircles = () => {
const circles = d3.select('svg')
.selectAll('circle');
circles
.data(dataPoints)
.join('circle')
.attr('cx', xPosition)
.attr('cy', canvasHeight)
.attr('r', circleRadius)
.attr('id', (d) => d.uniqueid)
.attr('fill', (d) => d.color);
const text = d3.select('svg')
.selectAll('text')
.data(dataPoints);
text
.join()
.attr('x', xPosition)
.attr('y', canvasHeight)
.attr('id', (d) => d.uniqueid)
.text((d) => d.description);
text
.join()
.attr('x', xPosition)
.attr('y', canvasHeight + 15)
.attr('id', (d) => d.uniqueid)
.text((d) => `${d.value} KwH`);
};
if (update === true) {
updateCircles();
} else {
const circles = selection.selectAll('circle')
.data(dataPoints, (d) => d.id);
const text = selection.selectAll('text')
.data(dataPoints);
circles
.enter().append('circle')
.attr('cx', xPosition)
.attr('cy', canvasHeight)
.attr('r', circleRadius)
.attr('id', (d) => d.uniqueid)
.attr('fill', (d) => d.color)
.merge(circles);
text
.enter().append('text')
.attr('x', xPosition)
.attr('y', canvasHeight)
.attr('id', (d) => d.uniqueid)
.merge(text)
.text((d) => d.description);
text
.enter().append('text')
.attr('x', xPosition)
.attr('y', canvasHeight + 15)
.attr('id', (d) => d.uniqueid)
.merge(text)
.text((d) => `${d.value} KwH`);
}
};
Do not use an element selector if you have multiple elements with different content with the same selector (e.g <text>). Add them class and use .selectAll('.className')
There is a working example using selection.join JSFiddle.
More information about selection.join here.
// render code
const circles = (selection, dataPoints, isUpdate) => {
const xPosition = (d, i) => +i * 180 + 100;
const updateCircles = (data) => {
const circles = d3.select('svg').selectAll('.circle-area').data(data);
circles
.join((enter) => {
enter
.append('circle')
.attr('class', 'circle-area')
.attr('cx', xPosition)
.attr('cy', canvasHeight)
.attr('r', circleRadius)
.attr('id', (d) => d.uniqueid)
.attr('fill', (d) => d.color);
}, (update) => {
update.attr('fill', (d) => d.color);
}, (exit) => {
exit.remove();
});
const descriptionText = d3.select('svg').selectAll('.kwh-description').data(data);
descriptionText
.join((enter) => {
enter
.append('text')
.attr('class', 'kwh-description')
.attr('x', xPosition)
.attr('y', canvasHeight)
.attr('id', (d) => `description-${d.uniqueid}`)
.text((d) => d.description);
}, (update) => {
update.text((d) => d.description);
}, (exit) => {
exit.remove();
});
const valueText = d3.select('svg').selectAll('.kwh-value').data(data);
valueText
.join((enter) => {
enter
.append('text')
.attr('class', 'kwh-value')
.attr('x', xPosition)
.attr('y', canvasHeight + 15)
.attr('id', (d) => `value-${d.uniqueid}`)
.text((d) => `${d.value} KwH`);
}, (update) => {
update.text((d) => `${d.value} KwH`);
}, (exit) => {
exit.remove();
});
};
if (isUpdate) {
console.log(dataPoints)
updateCircles(dataPoints);
}
};
Related
I have a hierarchy layout as shown in the image below where the 3rd layer of nodes contains words that are quite long so I want to give these nodes more space to their left to make it easier to read. But I am not sure what to change to do so.
Code for the node, path and the text:
const root = hierarchy(investmentByLocationData).sort(
(a, b) =>
descending(a.height, b.height) || ascending(a.data.name, b.data.name)
);
root.dx = 12;
root.dy = width / (root.height + 1);
cluster().nodeSize([root.dx, root.dy])(root);
const linkGenerator = linkHorizontal()
.x((node) => node.y)
.y((node) => node.x);
svg
.selectAll(".node")
.data(root.descendants())
.join((enter) =>
enter
.append("circle")
.attr("fill", (d) => (d.children ? "#555" : "#999"))
)
.attr("r", 2.5)
.attr("class", "node")
.attr("cx", (node) => node.y)
.attr("cy", (node) => node.x)
.attr("r", 2.5)
.transition()
.duration(500)
.delay((node) => node.depth * 300)
.attr("opacity", 1);
// links
const enteringAndUpdatingLinks = svg
.selectAll(".path")
.data(root.links())
.join("path")
.attr("class", "link")
.attr("d", linkGenerator)
.attr("stroke-dasharray", function() {
const length = this.getTotalLength();
return `${length} ${length}`;
})
.attr("stroke", "black")
.attr("fill", "none")
.attr("stroke-opacity", 0.5)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 0.4);
if (data !== previouslyRenderedData) {
enteringAndUpdatingLinks
.attr("stroke-dashoffset", function() {
return this.getTotalLength();
})
.transition()
.duration(500)
.delay((link) => link.source.depth * 500)
.attr("stroke-dashoffset", 5);
}
//labels
svg
.selectAll(".text")
.data(root.descendants())
.join("text")
.attr("x", (d) => d.y)
.attr("y", (d) => d.x)
.attr("dy", "0.31em")
.attr("dx", (d) => (d.children ? -6 : 6))
.text((d) => (d.children ? d.data.name : d.data.funding))
.attr("text-anchor", (d) => (d.children ? "end" : "start"))
.attr("font-size", (d) => (d.children ? 15 : 14));
After tinkering around with it I managed to solve it by altering root.dx and root.dy.
root.dx = 18;
root.dy = width / (root.height * 0.6);
The text for the cells in my d3 treemap don't wrap and overflow the other cells. This my project
I want the text to look like this project. I've looked at their code (and many others) but I can't get it to work in my project.
The problem area is:
svg.append('text')
.selectAll('tspan')
.data(root.leaves())
.enter()
.append('tspan')
.attr("x", (d) => d.x0 + 5)
.attr("y", (d) => d.y0 + 20)
.text( (d) => d.data.name) //.html( (d) => d.data.name.replace(/\s/g, "<br>"))
.attr("font-size", "0.6em")
.attr("fill", "white");
I tried using .html rather than .text as in the comment. In Safari and Chrome the text still overflowed the cells. In Firefox only the first word of the movie name was displayed.
We have two options to display the text ina similar way to the example you provide.
The first and easisest approach is to keep your code structure and make a similar procedure to split the text as the example provided:
d.data.name.split(/(?=[A-Z][^A-Z])/g)
So lets change your code a little bit:
svg.selectAll('text')
.data(root.leaves())
.enter()
.append('text')
.selectAll('tspan')
.data(d => {
return d.data.name.split(/(?=[A-Z][^A-Z])/g) // split the name of movie
.map(v => {
return {
text: v,
x0: d.x0, // keep x0 reference
y0: d.y0 // keep y0 reference
}
});
})
.enter()
.append('tspan')
.attr("x", (d) => d.x0 + 5)
.attr("y", (d, i) => d.y0 + 15 + (i * 10)) // offset by index
.text((d) => d.text)
.attr("font-size", "0.6em")
.attr("fill", "white");
This should accomplish the desired display. We have to take into account that labels are a very difficult to position and display in way which avoids overlapping since it would require a little more computation at build time.
The second approach is to change a little the code structure and create cells, pretty much like the example provided:
const cell = svg.selectAll('g')
.data(root.leaves())
.enter()
.append('g') // create a group for each cell / movie
.attr('transform', d => `translate(${d.x0},${d.y0})`) // let the group element handle the general positioning
.on('mousemove', d => {
//...
})
.on('mouseout', d => {
//...
});
cell.append('rect') // append rect for each cell / movie
.attr('id', d => d.data.id)
.attr('class', 'tile')
.attr('data-name', d => d.data.name)
.attr('data-value', d => d.data.value)
.attr('data-category', d => d.data.category)
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => color(d.data.category));
cell.append('text') // append text node for each cell / movie
.selectAll('tspan')
.data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g)) // split the name and use that as data to create indiviual tspan elements
.enter()
.append('tspan') // append tspan node for each element of the string which got split
.attr('font-size', '8px')
.attr('x', 4)
.attr('y', (d, i) => 13 + 10 * i) // offset the y positioning with the index of the data
.text(d => d);
CodePen for approach 1
CodePen for approach 2
Full code for approach 1:
// !! IMPORTANT README:
// You may add additional external JS and CSS as needed to complete the project, however the current external resource MUST remain in place for the tests to work. BABEL must also be left in place.
const w = 960;
const h = 600;
const padding = 60;
const svg = d3.select("#container").append("svg")
.attr("width", w).attr("height", h);
const legendsvg = d3.select("#legend").append("svg")
.attr("width", 960).attr("height", 50);
const legendPadding = 10;
d3.json("https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json")
.then(function(data) {
var root = d3.hierarchy(data).sum(function(d){ return d.value});
var treeMap = d3.treemap()
.size([w, h])
.paddingInner(1);
treeMap(root);
const toolTip = d3
.select("#container")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var color = d3.scaleOrdinal()
.domain(["Action", "Drama", "Adventure", "Family", "Animation", "Comedy", "Biography"])
.range(["#db8a00", "#75b0ff", "#13ad37", "#5d6d00", "#757582", "#d37cff", "#f96868"])
svg.selectAll("rect")
.data(root.leaves())
.enter().append("rect")
.attr("class", "tile")
.attr("data-name", (d) => d.data.name)
.attr("data-category", (d) => d.data.category)
.attr("data-value", (d) => d.data.value)
.attr('x', (d) => d.x0)
.attr('y', (d) => d.y0)
.attr('width', (d) => d.x1 - d.x0)
.attr('height', (d) => d.y1 - d.y0)
.style("stroke", "black")
.style("fill", (d) => color(d.parent.data.name))
.on("mouseover", (d, i) => {
toolTip
.transition()
.duration(0)
.style("opacity", 0.8);
toolTip
.attr("id", "tooltip")
.html(function() {
return "<span>" + "Name: " + d.data.name + "<br />" + "Category: " + d.data.category + "<br />" + "Value: " + d.data.value + "</span>";
})
.style("left", d3.event.pageX - 87.5 + "px") // -87.5 is half width of tooltip in css
.style("top", d3.event.pageY - 75 + "px")
.attr("data-value", d.data.value);
})
.on("mouseout", function(d) {
toolTip
.transition()
.duration(0)
.style("opacity", 0);
});
svg.selectAll('text')
.data(root.leaves())
.enter()
.append('text')
.selectAll('tspan')
.data(d => {
return d.data.name.split(/(?=[A-Z][^A-Z])/g) // split the name of movie
.map(v => {
return {
text: v,
x0: d.x0, // keep x0 reference
y0: d.y0 // keep y0 reference
}
});
})
.enter()
.append('tspan')
.attr("x", (d) => d.x0 + 5)
.attr("y", (d, i) => d.y0 + 15 + (i * 10)) // offset by index
.text((d) => d.text)
.attr("font-size", "0.6em")
.attr("fill", "white");
console.log(root.leaves());
/*svg.selectAll("text")
.data(root.leaves())
.enter()
.append("text")
.attr("x", function(d){ return d.x0+5})
.attr("y", function(d){ return d.y0+20})
.text(function(d){ return d.data.name })
.attr("font-size", "0.6em")
.attr("fill", "white")*/
legendsvg.selectAll('rect')
.data(root.children)
.enter()
.append('rect')
.attr('class', 'legend-item')
.style('stroke', 'white')
.attr('x', (d,i) => i*140 )
.attr('width', 130)
.attr('height', 20)
.style('fill', d => color(d.data.name))
legendsvg.selectAll('text')
.data(root.children)
.enter()
.append('text')
.attr('x', (d,i) => i*140)
.attr('y', 40)
.text(d => d.data.name);
//had to change the legend below because it wouldn't pass fcc test
/*legendsvg.append("g").classed("legend", true).classed("legend-item", true);
const legend = d3.legendColor().shape("rect")
.shapeWidth(90).cells(7).orient("horizontal").scale(color);
legendsvg.select(".legend").call(legend);*/
});
Full code for approach 2:
// !! IMPORTANT README:
// You may add additional external JS and CSS as needed to complete the project, however the current external resource MUST remain in place for the tests to work. BABEL must also be left in place.
const w = 960;
const h = 600;
const padding = 60;
const svg = d3.select("#container").append("svg")
.attr("width", w).attr("height", h);
const legendsvg = d3.select("#legend").append("svg")
.attr("width", 960).attr("height", 50);
const legendPadding = 10;
d3.json("https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json")
.then(function(data) {
var root = d3.hierarchy(data).sum(function(d){ return d.value});
var treeMap = d3.treemap()
.size([w, h])
.paddingInner(1);
treeMap(root);
const toolTip = d3
.select("#container")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var color = d3.scaleOrdinal()
.domain(["Action", "Drama", "Adventure", "Family", "Animation", "Comedy", "Biography"])
.range(["#db8a00", "#75b0ff", "#13ad37", "#5d6d00", "#757582", "#d37cff", "#f96868"])
const cell = svg.selectAll('g')
.data(root.leaves())
.enter()
.append('g')
.attr('transform', d => `translate(${d.x0},${d.y0})`)
.on('mousemove', d => {
toolTip.transition()
.duration(200)
.style('opacity', 0.75);
toolTip.attr('data-value', d.data.value);
toolTip.html(
'Name: ' + d.data.name + '<br>' +
'Category: ' + d.data.category + '<br>' +
'Value: ' + d.data.value
)
.style('top', `${d3.event.pageY + 10}px`)
.style('left', `${d3.event.pageX + 8}px`);
})
.on('mouseout', d => {
toolTip.transition()
.duration(200)
.style('opacity', 0);
});
cell.append('rect')
.attr('id', d => d.data.id)
.attr('class', 'tile')
.attr('data-name', d => d.data.name)
.attr('data-value', d => d.data.value)
.attr('data-category', d => d.data.category)
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => color(d.data.category));
cell.append('text')
.selectAll('tspan')
.data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))
.enter()
.append('tspan')
.attr('font-size', '8px')
.attr('x', 4)
.attr('y', (d, i) => 13 + 10*i)
.text(d => d);
legendsvg.selectAll('rect')
.data(root.children)
.enter()
.append('rect')
.attr('class', 'legend-item')
.style('stroke', 'white')
.attr('x', (d,i) => i*140 )
.attr('width', 130)
.attr('height', 20)
.style('fill', d => color(d.data.name))
legendsvg.selectAll('text')
.data(root.children)
.enter()
.append('text')
.attr('x', (d,i) => i*140)
.attr('y', 40)
.text(d => d.data.name);
//had to change the legend below because it wouldn't pass fcc test
/*legendsvg.append("g").classed("legend", true).classed("legend-item", true);
const legend = d3.legendColor().shape("rect")
.shapeWidth(90).cells(7).orient("horizontal").scale(color);
legendsvg.select(".legend").call(legend);*/
});
The other tutorials/answers online are about D3.js v3.x or specific positions on draggable elements.
I looked at the documentation and I don't fully understand how to do it:
I'm trying to prevent the red rectangles to overlap with the circles without changing the rectangles positions.
I specified fx and fy and still no success.
const nodes = d3.range(100).map(d => ({radius: 5, type: "circle"}));
const walls = [{}, {}, {}, {}].map((_, index) => ({
fx: 200 * index,
fy: 100,
type: "wall"
}));
const circleCenters = [100, 300, 500];
d3.forceSimulation(nodes.concat(walls))
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX().x(function (d, i) {
if (d.type === "circle")
return circleCenters[i % 3];
else
return d.fx;
}))
.force('y', d3.forceY().y(100))
.force('collision', d3.forceCollide().radius(d => d.radius))
.on('tick', ticked);
function ticked() {
d3.select('svg')
.selectAll('rect')
.data(walls)
.enter()
.append('rect')
.attr('width', 100)
.attr('height', 10)
.attr('fill', 'red')
.attr('x', d => d.x)
.attr('y', d => d.y);
const u = d3.select('svg')
.selectAll('circle')
.data(nodes);
u.enter()
.append('circle')
.merge(u)
.attr('fill', 'blue')
.attr('r', d => d.radius)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<svg width="90vw" height="90vh">
</svg>
you need to fill the whole rect with guard nodes of the correct size.
If you want to see the guard circles un-comment part of the code.
There are 2 g elements, one containing the guard nodes and the other the blue nodes.
Edit
If you make the X-force a bit stronger the nodes get closer to the actual centers.
.force('x', d3.forceX()
.x( (d, i) => (d.type === "circle") ? circleCenters[i % 3] : d.fx )
.strength(0.3))
const nodes = d3.range(100).map(d => ({radius: 5, type: "circle"}));
const walls = [{}, {}, {}, {}].map((_, index) => ({
fx: 200 * index,
fy: 100,
width: 100,
height: 10,
radius: 5,
type: "wall"
}));
const circleCenters = [100, 300, 500];
// construct "invisible" circles covering the rects
var invCircles = [];
walls.forEach(e => {
d3.range(e.fx+3, e.fx+e.width-3, 3).forEach(cx => {
invCircles.push({
fx: cx,
fy: e.fy + e.radius,
radius: e.radius,
type: e.type
});
});
});
d3.forceSimulation(nodes.concat(invCircles))
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX().x( (d, i) => (d.type === "circle") ? circleCenters[i % 3] : d.fx ).strength(0.3))
.force('y', d3.forceY().y(100))
.force('collision', d3.forceCollide().radius(d => d.radius))
.on('tick', ticked);
var wallGeom = d3.select('svg').append('g').attr('class', 'wall');
var circlesGeom = d3.select('svg').append('g').attr('class', 'circles');
wallGeom.selectAll('rect')
.data(walls)
.enter()
.append('rect')
.attr('width', d => d.width )
.attr('height', d => d.height )
.attr('fill', 'red')
.attr('x', d => d.fx)
.attr('y', d => d.fy);
// wallGeom.selectAll('circle')
// .data(invCircles)
// .enter()
// .append('circle')
// .attr('fill', 'yellow')
// .attr('r', d => d.radius)
// .attr('cx', d => d.fx)
// .attr('cy', d => d.fy);
function ticked() {
const u = d3.select('svg')
.select('.circles')
.selectAll('circle')
.data(nodes);
u.enter()
.append('circle')
.attr('fill', 'blue')
.attr('r', d => d.radius)
.merge(u)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<svg width="90vw" height="90vh"></svg>
I have a stacked bar chart made in D3 v5.
https://codepen.io/bental/pen/oMbrjL
I haven't quite got my head around the update pattern, the bars overwrite.
I think I have to grab the inner rects on the update, but I cant quite make this work. I'm sure my misunderstanding and error lies in the plotArea() function
function plotArea() {
const stackedData = d3.stack().keys(keys)(data);
const layer = svg.append('g')
.selectAll('g')
.data(stackedData);
layer
.transition()
.attr('x', (d) => graphAxes.x(d.data.interval))
.attr('y', (d) => graphAxes.y(d[1]))
.attr('height', (d) => graphAxes.y(d[0]) - graphAxes.y(d[1]))
.attr('width', graphAxes.x.bandwidth());
layer
.enter().append('g')
.attr('class', d => 'data-path type-' + d.key)
.selectAll('rect')
.data(d => d)
.enter().append('rect')
.attr('x', (d) => {
return graphAxes.x(d.data.interval)
})
.attr('y', (d) => graphAxes.y(d[1]))
.attr('width', graphAxes.x.bandwidth())
.attr('height', (d) => graphAxes.y(d[0]) - graphAxes.y(d[1]));
layer.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.attr('y', graphAxes.y(0))
.attr('height', graphDimensions.height - graphAxes.y(0))
.remove();
}
Any help appreciated
I have the following enter / update / exit phases defined.
// this.x = my x time scale
// this.y = my y scale
// this.c = a color scale with 2 colors (red,blue)
// this.chart = D3.select() element
let series = D3.stack().keys(['point', 'topPoint'])(<any[]>this.barData);
this.chart
.append('g')
.selectAll('g')
.data(series)
.enter().append('g')
.attr('class', (d) => {return d.key + ' layer';})
.attr('fill', (d) => {return this.c(d.key);})
.selectAll('.bar')
.data((d) => {return d;})
.enter()
.append('rect')
.attr('class', 'bar');
// Update Phase
this.chart.selectAll('.bar').transition()
.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
// Exit phase
this.chart.selectAll('.point.layer').selectAll('.bar').exit().remove();
this.chart.selectAll('.topPoint.layer').selectAll('.bar').exit().remove();
When the data changes, the new bars are drawn, but they are drawn over the old bars.
if you use d3 v4 try this:
let series = D3.stack().keys(['point', 'topPoint'])(<any[]>this.barData);
const elements = this.chart
.append('g')
.selectAll('g')
.data(series);
elements.enter().append('g')
.attr('class', (d) => {return d.key + ' layer';})
.attr('fill', (d) => {return this.c(d.key);})
.each(function(d){
d3.select(this)
.append('rect')
.attr('class', 'bar');
})
.merge(elements) // updatePhase
.each(function(d){
d3.select(this).select(".bar")
.transition()
.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
}
// Exit phase
elements.exit().remove();
So the problem was my selecting of the elements I wish to bind and unbind.
this.chart
.selectAll('.layer')
.data(series)
.enter()
.append('g')
.attr('class', (d) => {return d.key + ' layer';});
// Set the enter phase for the bars within the groups, with the data derived from the layer data binding
this.chart.selectAll('.layer')
.selectAll('.bar')
.data((d) => {return d;})
.enter()
.append('rect')
.attr('class', 'bar');
// Set the update phase for the layers to fill the groups with the relevant color
let layers = this.chart.selectAll('.layer').attr('fill', (d) => {return this.c(d.key);});
// Update Phase
let bars;
if(this.animate) {
// Set the update phase of the bar data based on the data derived from the layer update phase
bars = layers.selectAll('.bar').data((d) => {return d;}).transition();
} else {
bars = layers.selectAll('.bar').data((d) => {return d;});
}
// Set the update phase of the bar data based on the data derived from the layer update phase
bars.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
// Exit phase
this.chart.selectAll('.layer').data(series).exit().remove();