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>
Related
I have three array of objects.
I want to create a chart with 3 lines, the Dates are the same for the three arrays. The Y for peakUsage and avgUsage are the same values so they are horizontal straight lines
This code show the Y and X Axises but failing with drawing the actual lines.
data: { x: Date; y: number }[],
peakUsage: { x: Date; y: number }[],
avgUsage: { x: Date; y: number }[]
const axisThickness = 60;
const width = (params.width || 500) - axisThickness;
const height = (params.height || 500) - axisThickness;
const { document } = new JSDOM('').window;
// Create D3 container with SVG element
const div = d3.select(document.body).append('div');
const svg = div
.attr('class', 'container')
.append('svg')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr('width', width + axisThickness)
.attr('height', height + axisThickness);
const yScale = d3.scaleLinear().range([height, 0]);
yScale.domain([0, Math.max(...data.map((d) => d.y))]);
const xScale = d3
.scaleTime()
.range([0, width])
.domain(
d3.extent(data, function (d) {
return new Date(d.x);
}) as Date[]
);
const g = svg.append('g').attr('transform', `translate(${axisThickness},${axisThickness / 2})`);
g.append('g').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale));
g.append('g')
.call(
d3
.axisLeft(yScale)
.tickFormat(function (d) {
return `${d}`; // Label text
})
.ticks(8)
)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '-5.1em')
.attr('text-anchor', 'end')
.attr('fill', 'black')
.text('Est. kWh');
g.selectAll('.line')
.enter()
.append('path')
.data(data)
.attr('fill', 'none')
.attr('stroke', 'green')
.attr('stroke-width', 1.5)
.attr('d', function (d) {
return d3
.line()
.x(function (d: any) {
return xScale(d.x);
})
.y(function (d: any) {
return yScale(d.y);
}) as any;
});
g.selectAll('.line')
.enter()
.append('path')
.data(peakUsage)
.attr('fill', 'none')
.attr('stroke', 'Orange')
.attr('stroke-width', 1.5)
.attr(
'd',
d3
.line()
.x(function (d: any) {
return xScale(d.x);
})
.y(function (d: any) {
return yScale(d.y);
}) as any
);
g.selectAll('.line')
.enter()
.append('path')
.data(avgUsage)
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('stroke-width', 1.5)
.attr(
'd',
d3
.line()
.x(function (d: any) {
return xScale(d.x);
})
.y(function (d: any) {
return yScale(d.y);
}) as any
);
What I am doing wrong?
This bit is in the wrong order:
g.selectAll('.line')
.enter()
.append('path')
.data(data)
It should be:
g.selectAll('.line')
.data(data)
.enter()
.append('path')
But that does not fix your issue. Since you are using 3 different arrays for each line individually (a better approach is using just one array with 3 inner arrays, but that's another issue...), you can simply declare the line generator...
const lineGenerator = d3.line()
.x(function (d: any) {
return xScale(d.x);
})
.y(function (d: any) {
return yScale(d.y);
})
... and then, for each path, do:
g.append('path')
.attr('fill', 'foo')
.attr('stroke', 'bar')
.attr('stroke-width', 'baz')
.attr('d', lineGenerator(pathDataHere))
//the data for each line -----^
I'm trying the force-directed example in d3.js(v7).
In this sample, when I drag a node, the links and other nodes move in tandem.
I want all nodes to move randomly at all times, and I want other links and nodes to move in tandem with them, just as if I were dragging them.
The code is below. The json file is the same as the sample. When I run this code, the nodes move, but the links don't follow the movement and remain stationary.
function ForceGraph({
nodes, // an iterable of node objects (typically [{id}, …])
links, // an iterable of link objects (typically [{source, target}, …])
}, {
nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
nodeGroup, // given d in nodes, returns an (ordinal) value for color
nodeGroups, // an array of ordinal values representing the node groups
nodeStrength,
linkSource = ({source}) => source, // given d in links, returns a node identifier string
linkTarget = ({target}) => target, // given d in links, returns a node identifier string
linkStrokeWidth = 10, // given d in links, returns a stroke width in pixels
linkStrength = 0.55,
colors = d3.schemeTableau10, // an array of color strings, for the node groups
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
invalidation // when this promise resolves, stop the simulation
} = {}) {
// Compute values.
const N = d3.map(nodes, nodeId).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({id: N[i], type: NODETYPES[i], tag: parsed_NODETAGS[i], texts: X[i]}));
links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));
// Compute default domains.
if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
// Construct the scales.
const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
// Construct the forces.
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);
const zoom = d3.zoom()
.scaleExtent([1, 40])
.on("zoom", zoomed);
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2.5, width, height])
.on("click", reset)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.call(zoom);
const g = svg.append("g");
const link = g.append("g")
.selectAll("line")
.data(links)
.join("line");
const simulation = d3.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter())
.on("tick", ticked);
const node = g.append("g")
.attr("class", "nodes")
.style("opacity", 1.0)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.call(drag(simulation));
if (W) link.attr("stroke-width", ({index: i}) => W[i]);
if (G) node.attr("fill", ({index: i}) => color(G[i]));
function random(){
node
.transition()
.duration(2000)
.attr("cx", function(d){
return d.x + Math.random()*80 - 40;
})
.attr("cy", function(d){
return d.y + Math.random()*80 - 40;
});
}
setInterval(random, 800);
if (invalidation != null) invalidation.then(() => simulation.stop());
function intern(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
}
function ticked() {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
function zoomed({transform}) {
g.attr("transform", transform);
}
return Object.assign(svg.node(), {});
}
In your function random(), you don't change the underlying data, you only change how it is represented. Each circle holds a reference to an element in the nodes array, but you set cx and cy within random(), you don't update the underlying data d.x and d.y. And even then, the values the circle has for cx and cy are not reactive. That is, they are not re-evaluated when d.x or d.y changes.
So I would split your code. Have a function random() that is called every 800ms and shuffles the nodes around a bit by changing d.x and d.y. And then the simulation is in charge of actually drawing the circles and the links - the way it already seems to be doing.
const size = 500;
const nodes = [{
id: 'A',
x: 150,
y: 150
},
{
id: 'B',
x: 250,
y: 250
},
{
id: 'C',
x: 350,
y: 350
}
];
const links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[0],
target: nodes[2]
}
];
const svg = d3.select('body')
.append('svg')
.attr('width', size)
.attr('height', size)
.attr('border', 'solid 1px red')
const g = svg.append('g');
const node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5);
const link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.join("line");
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).strength(2))
.force("charge", d3.forceManyBody().strength(2))
.force("center", d3.forceCenter(size / 2, size / 2).strength(0.05))
.on("tick", ticked);
function ticked() {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function random() {
simulation.stop();
nodes.forEach(d => {
d.x += Math.random() * 80 - 40;
d.y += Math.random() * 80 - 40;
});
node
.transition(1000)
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.transition(1000)
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)
.on('end', () => { simulation.restart(); });
}
setInterval(random, 2000);
.links>line {
stroke: black;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js"></script>
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>
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);
}
};
D3 noob question: I have a horizontal bar graph that looks like this:
Here's the code that produces it:
function barChartInner(selection) {
selection.each(function (data) {
const colorScale = d3.scale.quantize()
.domain([0, data.length])
.range(colors);
const maxVal = d3.max(data, d => +d.value);
const widthScale = d3.scale.linear()
.domain([0, maxVal])
.range([0, width]);
const svg = d3.select(this).selectAll('svg').data([data]);
svg.enter().append('svg');
svg.attr('width', width)
.attr('height', height);
const groups = svg.selectAll('g').data(data);
const groupEnter = groups.enter().append('g');
groupEnter.append('rect')
.attr('width', 0)
.attr('y', 0);
groupEnter.append('text');
groupEnter.append('text').classed(`${c.baseClass} user-answer`, true);
groupEnter.append('text').classed(`${c.baseClass} count`, true);
groups.style('transform', (d, i) => `translateY(${i * (barHeight + barMargin * 6)}px)`);
const rects = svg.selectAll('g rect');
const labelWidth = 0;
const valueMargin = 4;
rects
.attr('x', 100)
.attr('height', barHeight)
.attr('fill', (d, i) => (
userAnswers && userAnswers.indexOf(i) > -1
? c.colors.userColor
: colorScale(i)
));
rects.transition()
.duration(1000)
.attr('width', d => widthScale(+d.value));
svg.selectAll('g text')
.attr('fill', c.colors.black)
.style('font-family', c.fontStack)
.attr('dy', '.35em')
.attr('x', 90)
.attr('y', barHeight / 2)
.attr('text-anchor', 'end')
.text(d => d.name);
if (userAnswers) {
console.log('userAnswers');
console.log(userAnswers);
svg.selectAll('g text.user-answer')
.attr('fill', c.colors.black)
.style('font-family', c.fontStack)
.attr('dy', '.35em')
.attr('x', d => widthScale(+d.value))
.attr('y', barHeight + 10)
.attr('text-anchor', 'start')
.text((d, i) => (userAnswers.indexOf(i) > -1 ? 'Your answer' : ''));
}
});
}
I'm trying to add a value in the data to the end of each bar on the right side. Here's the block:
svg.selectAll('g text.count')
.attr('fill', c.colors.black)
.style('font-family', c.fontStack)
.attr('dx', -valueMargin + labelWidth) // margin right
.attr('dy', '.35em')
.attr('x', 725)
.attr('y', barHeight / 2)
.attr('text-anchor', 'start')
.text(d => d.value);
This block comes after the block that handles the data on the left side of the graph. But it doesn't render out in the graph. I'm sure the data is valid because if I sub out the earlier block ('g text') with d.value, I see the values on the left hand side. So why isn't this block rendering out?