I am using a nested dataset and the following code to draw circles in d3 v5:
const scatterGroup = svg.selectAll(".scatterGroup").data(data);
scatterGroup.exit().remove();
scatterGroup
.enter()
.append("g")
.attr("class", "scatterGroup")
.attr("fill", (d, i) => color[i])
.attr("stroke", (d, i) => color[i])
.append("g")
.attr("class", "scatterPoints");
const scatterPoints = scatterGroup
.selectAll(".scatterPoints")
.data((d) => d);
scatterPoints
.enter()
.append("circle")
.attr("class", "scatterPoints")
.attr("cx", (d, i) => xScale(d.x))
.attr("cy", (d, i) => yScale(d.y))
.attr("r", 5);
scatterPoints.exit().remove();
const scatterUpdate = scatterGroup
.transition()
.duration(500)
.attr("fill", (d, i) => color[i])
.attr("stroke", (d, i) => color[i]);
scatterPoints
.transition()
.duration(500)
.attr("cx", (d, i) => xScale(d.x))
.attr("cy", (d, i) => yScale(d.y));
Nothing happens in the first run of providing the data. The control doesn't reach the append circle in the first load. When the data is loaded the second time, d3 appends the circles. Can anyone let me know on how to make them appear when the data is first provided and why this is happening?
It's happening because the data is nested, so you need to .merge() scatterGroup or re-select it before creating scatterPoints. Otherwise, scatterGroup is still empty, while scatterGroup.enter() holds all the points.
I also removed .append(g).attr('class', 'scatterPoints') from your code, since it uses a g instead of a circle and it doesn't need to be there
const svg = d3.select('svg');
const color = ['red', 'blue'];
const data = [
[{
x: 10,
y: 10
}, {
x: 40,
y: 100
}],
[{
x: 25,
y: 50
}]
];
const newData = [
[{
x: 10,
y: 20
}, {
x: 50,
y: 100
}],
[{
x: 25,
y: 40
}]
];
function draw(data) {
const scatterGroup = svg.selectAll(".scatterGroup").data(data);
scatterGroup.exit().remove();
const scatterGroupNew = scatterGroup
.enter()
.append("g")
.attr("class", "scatterGroup")
.attr("fill", (d, i) => color[i])
.attr("stroke", (d, i) => color[i]);
// Equivalent:
//const scatterPoints = svg.selectAll(".scatterGroup")
// .selectAll(".scatterPoint")
// .data((d) => d);
const scatterPoints = scatterGroup
.merge(scatterGroupNew)
.selectAll(".scatterPoint")
.data((d) => d);
scatterPoints.exit().remove();
const scatterPointsNew = scatterPoints
.enter()
.append("circle")
.attr("class", "scatterPoint")
.attr("r", 5);
scatterPoints.merge(scatterPointsNew)
.attr("cx", (d, i) => d.x)
.attr("cy", (d, i) => d.y);
}
draw(data);
setTimeout(() => draw(newData), 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
</svg>
Related
I am trying to append another shape when one of the values satisfy the condition as follows:
const keys = ["A", "B", "C", "D", "E", "E", "G"]
const colors = ['#1e90ff', '#008000', '#d3d3d3', '#d3d3d3', '#fe0000', '#ffa500', '#800080']
var triangle = d3.symbol().type(d3.symbolTriangle).size(500);
const colorOrdinalScale = d3
.scaleOrdinal()
.domain(keys)
.range(colors);
const svg = d3
.select("#container")
.append("svg")
.attr("class", "svg-area")
.attr("width", 600)
.attr("height", 500)
const legend = svg
.append('g')
.attr('id', 'legend')
.selectAll("symbols")
.data(keys)
.enter()
legend
.each(d => {
if (d === "D") {
console.log("triangle", d)
legend
.append("circle")
.attr("r", 15)
.style("fill", (d) => colorOrdinalScale(d))
.attr("transform", (d, i) => `translate(${100 + i * 70}, 20)`)
}
else {
console.log("circle", d)
legend
.append("path")
.attr("d", triangle)
.attr("fill", (d) => colorOrdinalScale(d))
.attr("transform", (d, i) => `translate(${100 + i * 70}, 20)`);
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="container"></div>
What is supposed to happen is that when the key is equal to D it should print out a triangle and the rest should print out a circle.
As far as I understand, the logic seems to be working. But the result is that both a triangle and a circle are printed for each datum. However, the log shows that the appropriate block of code runs.
Basically, this is the result that I am looking forward to:
Since you are already using d3.symbol, you could just use symbolCircle for the circle, which simplifies the code a lot:
.attr("d", d => symbol.type(d === "D" ? d3.symbolTriangle : d3.symbolCircle)())
Also, your each makes little sense, you're appending several elements on top of each other (you might think you have 7 elements, but have a look at your SVG, you have 49).
This is your code with those changes:
const keys = ["A", "B", "C", "D", "E", "E", "G"]
const colors = ['#1e90ff', '#008000', '#d3d3d3', '#d3d3d3', '#fe0000', '#ffa500', '#800080']
var symbol = d3.symbol().size(500);
const colorOrdinalScale = d3.scaleOrdinal()
.domain(keys)
.range(colors);
const svg = d3
.select("#container")
.append("svg")
.attr("class", "svg-area")
.attr("width", 600)
.attr("height", 500)
const legend = svg
.append('g')
.attr('id', 'legend')
.selectAll("symbols")
.data(keys)
.enter()
.append("path")
.attr("d", d => symbol.type(d === "D" ? d3.symbolTriangle : d3.symbolCircle)())
.attr("fill", d => colorOrdinalScale(d))
.attr("transform", (d, i) => `translate(${100 + i * 70}, 20)`);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="container"></div>
You are doing a very basic mistake,
when you are writing a statement legend.append("circle) you are appending circles and triangles at each key and its appending circle or triangle at each point. I have edited your code to filter legend based on index which is being processed and transform based on that index.
const keys = ["A", "B", "C", "D", "E", "E", "G"]
const colors = ['#1e90ff', '#008000', '#d3d3d3', '#d3d3d3', '#fe0000', '#ffa500', '#800080']
var triangle = d3.symbol().type(d3.symbolTriangle).size(500);
const colorOrdinalScale = d3
.scaleOrdinal()
.domain(keys)
.range(colors);
const svg = d3
.select("#container")
.append("svg")
.attr("class", "svg-area")
.attr("width", 600)
.attr("height", 500)
const legend = svg
.append('g')
.attr('id', 'legend')
.selectAll("symbols")
.data(keys)
.enter()
legend
.each((d,index) => {
if (d === "D") {
console.log("triangle", index)
legend
.filter(function (d, i) { return i == index;})
.append("circle")
.attr("r", 15)
.style("fill", (d) => colorOrdinalScale(d))
.attr("transform", (d, i) => `translate(${100 + index * 70}, 20)`)
}
else {
console.log("circle", index)
legend
.filter(function (d, i) { return i == index;})
.append("path")
.attr("d", triangle)
.attr("fill", (d) => colorOrdinalScale(d))
.attr("transform", (d, i) => `translate(${100 + index * 70}, 20)`);
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="container"></div>
I would like to know how to create etc groups in d3.js donut chart.
data = {{result|safe}}
var text = "";
var width = 1450;
var height = 500;
var thickness = 40;
var duration = 750;
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory20);
var svg = d3.select("#donutchart")
.append('svg')
.attr('class', 'pie')
.attr('width', width)
.attr('height', height);
var g = svg.append('g')
.attr('transform', 'translate(' + (width/2) + ',' + (height/2) + ')');
var arc = d3.arc()
.innerRadius(radius - thickness)
.outerRadius(radius);
var pie = d3.pie()
.value(function(d) { return d.count; })
.sort(null);
var path = g.selectAll('path')
.data(pie(data))
.enter()
.append("g")
.on("mouseover", function(d) {
let g = d3.select(this)
.style("cursor", "pointer")
.style("fill", "black")
.append("g")
.attr("class", "text-group");
g.append("text")
.attr("class", "name-text")
.text(`${d.data.word}`)
.attr('text-anchor', 'middle')
.attr('dy', '-1.2em');
g.append("text")
.attr("class", "value-text")
.text(`${d.data.count}`)
.attr('text-anchor', 'middle')
.attr('dy', '.6em');
})
.on("mouseout", function(d) {
d3.select(this)
.style("cursor", "none")
.style("fill", color(this._current))
.select(".text-group").remove();
})
.append('path')
.attr('d', arc)
.attr('fill', (d,i) => color(i))
.on("mouseover", function(d) {
d3.select(this)
.style("cursor", "pointer")
.style("fill", "black");
})
.on("mouseout", function(d) {
d3.select(this)
.style("cursor", "none")
.style("fill", color(this._current));
})
.each(function(d, i) { this._current = i; });
g.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.text(text);
When the number of word counts of data is less than 100, is there a way to show the chart by making it into an etc group? Does it have to be handled by the server?
Or is there a way to show only the top 8 data and make it into an etc group?
Let's pretend the data is an array of objects with "name" and "value":
[
{
"name": "a",
"value": 198
},
{
"name": "b",
"value": 100
},
{
"name": "c",
"value": 50
},
// ...
]
We'll also assume it's sorted (you can do this like so):
var data = data.sort((a, b) => d3.descending(a.value, b.value))
Let's take the 8 biggest values, and put all the rest in an "etc" label:
let newData = data.slice(0, 8)
const etcSliceAmount = data.slice(8, data.length-1)
.map(d => d.value)
.reduce((acc, x) => acc + x)
newData.push({
name: "etc",
value: etcSliceAmount,
})
In etcSliceAmount, we first get all the items except the first eight. We use reduce to add them up to the sum. Finally, we can append this new partition at the end of our data. Since there's no sorting, disabled in your code with sort(null), d3 will place the slices in order.
// random array of 100 values from [0-200], sorted
const data = d3.range(100).map(() => ({
name: "a",
value: Math.floor(d3.randomUniform(0, 200)())
})).sort((a, b) => d3.descending(a.value, b.value))
let newData = data.slice(0, 8)
const etcSliceAmount = data.slice(8, data.length-1).map(d => d.value).reduce((acc, x) => acc + x)
newData.push({
name: "etc",
value: etcSliceAmount,
})
console.log(newData)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I am drawing circles with every update using a smooth transition which goes from one location to another.
Here is the code I am using...
function drawChart(newData)
{
var circles = slider.selectAll(".dot")
.data(newData);
circles.enter()
.append("circle")
.merge(circles)
.transition() // and apply changes to all of them
.duration(1000)
.ease(d3.easeLinear)
.attr("class", "dot")
.attr("r", 10.5)
.attr("cx", function(d,i) {
return Math.pow(d.open,i); })
.attr("cy", function(d,i) { return Math.pow(i,5)+d.close; })
.style("fill", function(d) { return color(d.class); });
circles.exit()
.remove();
}
This is how data is updated using the filterData function.
function filterData(dd){
var newData = dataset.filter(function(d) {
return d.date.getDate() == dd.getDate() && d.date.getMonth() == dd.getMonth();
})
drawChart(newData)
}
This code shows the simple circle and transition, whereas I want to have the transition in a way circles are leaving trails while moving as in this picture. .
Is there any way to do this? Any help would be appreciated.
I made your starting positions a little easier to mock, the true calculations are in the .tween function. Note that I execute the function only a few times, otherwise you get a continuous flow of circles.
You can often find solutions like this by looking at similar problems. In this case, I based it on this answer, which led me to tween.
var svg = d3.select('svg');
var color = (v) => v;
var nTrails = 20;
function createTraceBall(x, y) {
svg.append('circle')
.classed('shadow', true)
.attr('cx', x)
.attr('cy', y)
.attr('r', 10)
.style('fill', 'grey')
.style('opacity', 0.5)
.transition()
.duration(500)
.ease(d3.easeLinear)
.style('fill', 'lightgrey')
.style('opacity', 0.1)
.attr('r', 3)
.remove();
}
function drawChart(newData) {
var circles = svg.selectAll(".dot")
.data(newData);
circles.enter()
.append("circle")
.attr("cx", (d) => d.open.x)
.attr("cy", (d) => d.open.y)
.merge(circles)
.transition() // and apply changes to all of them
.duration(1000)
.ease(d3.easeLinear)
.tween("shadow", function(d) {
var xRange = d.close.x - d.open.x;
var yRange = d.close.y - d.open.y;
var nextT = 0;
return function(t) {
// t is in [0, 1), and we only want to execute it nTrails times
if(t > nextT) {
nextT += 1 / nTrails;
createTraceBall(
d.open.x + xRange * t,
d.open.y + yRange * t
);
}
};
})
.attr("class", "dot")
.attr("r", 10.5)
.attr("cx", (d) => d.close.x)
.attr("cy", (d) => d.close.y)
.style("fill", function(d) { return color(d.class); });
circles.exit()
.remove();
}
drawChart([
{open: {x: 20, y: 20}, close: {x: 150, y: 150}, class: 'red'},
{open: {x: 150, y: 20}, close: {x: 20, y: 150}, class: 'blue'},
{open: {x: 20, y: 20}, close: {x: 150, y: 20}, class: 'green'}
]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
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);
}
};
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>