D3 - Add dots in middle of lines between two points - javascript

I am drawing lines with D3 between two points which is working fine but want to add dots / points in the middle of the lines.
I'm following this example in the D3 docs but can't get it to work with my own code.
Here's the code that draws the lines:
export default function drawLines(): DrawLines {
const getSVG = (
ref: SVGSVGElement | null,
viewBoxHeight: number,
): d3Selection | undefined => {
if (ref) {
const svg = d3
.select(ref)
.attr('viewBox', `0 0 350 ${viewBoxHeight}`);
return svg;
}
return undefined;
};
const getLine = (idx: number, arr: Array<NodePositions>): d3Line => {
const line = d3
.line<Number>()
.x((value, index) => arr[idx].x[index])
.y((value) => Number(value));
return line;
};
const drawLine = (nodes: Array<NodePositions> | undefined, svg: d3Selection): void => {
if (nodes) {
nodes.forEach((el, idx, arr) => {
if (svg) {
svg
.selectAll(null)
.data([el.y])
.join('path')
.attr('d', (value: Number[] | Iterable<Number>) => getLine(idx, arr)(value));
}
});
}
};
return { getSVG, drawLine };
}
How can I add points / dots or shapes in the middle of the lines?

Try this:
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => (d.x[0] + d.x[1]) / 2)
.attr('cy', d => (d.y[0] + d.y[1]) / 2)
.attr('r', 3)
.style('fill', 'red');

Related

d3.js transform.rescaleX() returning big and negative values

I'm making a semantic zoom on xAxis using the helper function transform.rescaleX() inside zoomed function
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension, sillons: Circulation[]): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis, sillons, dimension, this.drawSillonService));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed(
{ xAxis, xAxisBottom, yAxis }: AllAxis,
sillons: Circulation[],
dimension: Dimension,
drawSillonService: DrawSillonService,
{ transform }: any
) {
xAxis.axisContainer.call(xAxis.axis.scale(transform.rescaleX(xAxis.scale)) as any);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
svgs.sillons
.selectAll('path')
.nodes()
.forEach((path, j) => {
const pathSelect = d3.select(path);
const pathData = JSON.parse(pathSelect.attr('data'));
const line = d3
.line()
.x((d) => {
const transformScaled = transform.rescaleX(xAxis.scale);
const value = transformScaled(d[0]);
return value;
})
.y((d) => d[1]);
pathSelect.attr('d', line(pathData));
});
...
Here's the values of transform:
k:1.6817928305074288
x:-278.99232789420023
y:
-200.42112372679287
when d[0] = 0 the transformScaled(d[0]) return -133335,..
I don't know why I get these values. When I calculate transformation manually using x * transform.k + transform.x I get the right values (-79.5), but I would use rescaleX function.

D3 only render to certain depth

For d3, given an array of nested objects.
Is it possible to only render a certain amount of depth?
I am basing off of some sunburst examples online like :
https://github.com/Nikhilkoneru/react-d3-zoomable-sunburst/blob/master/src/index.js
Using the .selectAll method from d3, can I only limit the sunburst to render 'X' depths, instead of rendering the entire array of nested objects?
I'm trying to render a really large array, and it causes a really laggy experience.
This is the selectAll that I'm using from the github example.
svg.selectAll('path')
.data(partition(root)
.descendants())
.enter()
.append('path')
.style('fill', (d) => {
let hue;
const current = d;
if (current.depth === 0) {
return '#33cccc';
}
if (current.depth <= 1) {
hue = hueDXScale(d.x0);
current.fill = d3.hsl(hue, 0.5, 0.6);
return current.fill;
}
if(current.depth <= 2){
console.log("depth > 2: ", d)
current.fill = current.parent.fill.brighter(0.5);
const hsl = d3.hsl(current.fill);
hue = hueDXScale(current.x0);
const colorshift = hsl.h + (hue / 4);
return d3.hsl(colorshift, hsl.s, hsl.l);
}
})
.attr('stroke', '#fff')
.attr('stroke-width', '1')
.on('click', d => click(d, node, svg, x, y, radius, arc))
.on('mouseover', function (d) {
if (props.tooltip) {
d3.select(this).style('cursor', 'pointer');
tooltip.html(() => {
const name = utils.formatNameTooltip(d);
return name;
});
return tooltip.transition().duration(50).style('opacity', 1);
}
return null;
})
.on('mousemove', () => {
if (props.tooltip) {
tooltip
.style('top', `${d3.event.pageY - 50}px`)
.style('left', `${props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`);
}
return null;
})
.on('mouseout', function () {
if (props.tooltip) {
d3.select(this).style('cursor', 'default');
tooltip.transition().duration(50).style('opacity', 0);
}
return null;
});
As you can see, from the current.depth, i'm able to filter out what depth is more than 2.
Is it possible to add display: none to anything more than depth of 2, or is there a better way to not render anything more than depth of 2 from current

d3js prevent forceSimulation from recalculating position all the time

I'm trying to build a force layout that will allow me to visualize the flow of objects in a system.
I want to show how many objects are on a specific state and when a state change I want to update my graph.
I've built a prototype, but I've noticed that D3.js is recalculating transform of each node even when they don't need to move:
can this be fixed? maybe there is an option to add minimum value for an update?
I've declared force layout this way:
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
After changing alphaTarget to alpha recalculation stopped, but I got another bug:
I've added drag functionality and it stopped working with above changes.
Here is the version with fixed recalculation but with drag problem.
The culprit is in your restart() function:
force.alphaTarget(0.3).restart();
The way you are reheating your simulation by setting .alphaTarget(0.3) is not correct. alphaTarget is a configuration parameter which controls the way alpha decreases. Figuratively speaking, alpha—for as long as it is greater than alphaMin— is headed towards alphaTarget. The heat in the system is measured by alpha which can be thought of as dynamic data; alphaTarget, on the other hand, resembles more or less static data.
Furthermore, it is important to have alphaTarget set to a value less than alphaMin or else your simulation is going to run indefinitely because alpha, while on its way towards alphaTarget so to speak, is never going to be less than alphaMin.
Thus, if you want to reheat your system, you have to manipulate alpha instead of alphaTarget. Changing above mentioned line to the following is all it takes to get the desired effect.
force.alpha(0.3).restart();
Have a look at the following snippet, which is basically a fork of your JSFiddle, to see it in action.
document.getElementById("a").addEventListener("click", function() {
AddNewLink(null, 1);
});
document.getElementById("b").addEventListener("click", function() {
AddNewLink(1, 2);
});
document.getElementById("c").addEventListener("click", function() {
AddNewLink(2, 3);
});
document.getElementById("d").addEventListener("click", function() {
AddNewLink(1, 3);
});
document.getElementById("e").addEventListener("click", function() {
AddNewLink(3, 4);
});
document.getElementById("f").addEventListener("click", function() {
AddNewLink(4, 5);
});
function AddNewLink(from, to) {
var startNode;
var start = availableNodes.find(x => x.id === from);
if (start !== undefined) {
//check if this node is already added
var foundStart = nodes.find(x => x.id == start.id);
if (foundStart === undefined) {
nodes.push(start);
startNode = start;
} else {
foundStart.value--;
if (foundStart.value < 0) foundStart.value = 0;
startNode = foundStart;
}
}
var endNode;
var end = availableNodes.find(x => x.id === to);
if (end !== undefined) {
//check if this node is already added
var foundEnd = nodes.find(x => x.id == end.id);
if (foundEnd === undefined) {
nodes.push(end);
endNode = end;
end.value++;
} else {
foundEnd.value++;
endNode = foundEnd;
}
}
//console.log(startNode, endNode);
if (startNode !== undefined && endNode !== undefined) {
links.push({
source: startNode,
target: endNode
});
}
restart();
}
// set up SVG for D3
const width = 400;
const height = 400;
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select('svg')
.on('contextmenu', () => {
d3.event.preventDefault();
})
.attr('width', width)
.attr('height', height);
var availableNodes = [{
id: 1,
name: "Start",
value: 0,
reflexive: false
}, {
id: 2,
name: "Node 1",
value: 0,
reflexive: false
}, {
id: 3,
name: "Node 2",
value: 0,
reflexive: false
}, {
id: 4,
name: "Node 3",
value: 0,
reflexive: false
}, {
id: 5,
name: "Finish",
value: 0,
reflexive: false
}];
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black circle).
// - links are always source < target; edge directions are set by 'left' and 'right'.
let nodes = [
availableNodes[0], availableNodes[1], availableNodes[2]
];
let links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[1],
target: nodes[2]
}
];
// init D3 force layout
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// handles to link and node element groups
let path = svg.append('svg:g').attr('id', 'lines').selectAll('path');
let circle = svg.append('svg:g').attr('id', 'circles').selectAll('g');
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.attr('d', (d) => {
const deltaX = d.target.x - d.source.x;
const deltaY = d.target.y - d.source.y;
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const normX = deltaX / dist;
const normY = deltaY / dist;
const sourcePadding = d.left ? 17 : 12;
const targetPadding = d.right ? 17 : 12;
const sourceX = d.source.x + (sourcePadding * normX);
const sourceY = d.source.y + (sourcePadding * normY);
const targetX = d.target.x - (targetPadding * normX);
const targetY = d.target.y - (targetPadding * normY);
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
});
circle.attr('transform', (d) => `translate(${d.x},${d.y})`);
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// remove old links
path.exit().remove();
// add new links
path = path.enter().append('svg:path')
.attr('class', 'link')
.style('marker-end', 'url(#end-arrow)')
.merge(path);
// circle (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
circle = circle.data(nodes, (d) => d.id);
// update existing nodes (reflexive & selected visual states)
circle.selectAll('circle')
.style('fill', (d) => colors(d.id))
.classed('reflexive', (d) => d.reflexive);
circle.selectAll('text.value').text((d) => d.value);
// remove old nodes
circle.exit().remove();
// add new nodes
const g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.style('fill', (d) => colors(d.id))
.style('stroke', (d) => d3.rgb(colors(d.id)).darker().toString())
.classed('reflexive', (d) => d.reflexive)
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'value')
.text((d) => d.value);
g.append('svg:text')
.attr('x', 20)
.attr('y', 4)
.attr('class', 'name')
.text((d) => d.name);
circle = g.merge(circle);
// set the graph in motion
force
.nodes(nodes)
.force('link').links(links);
force.alpha(0.3).restart();
}
restart();
svg {
background-color: #FFF;
cursor: default;
user-select: none;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 3px;
cursor: default;
}
path.link.selected {
stroke-dasharray: 10, 2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
circle.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.value {
text-anchor: middle;
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.js"></script>
<button id='a'>1</button>
<button id='b'>1>2</button>
<button id='c'>2>3</button>
<button id='d'>1>3</button>
<button id='e'>3>4</button>
<button id='f'>4>5</button>
<svg width="400" height="400"></svg>

Change the collision behavior of many nodes stored in an array

when using the force layout in d3.js it is possible to push nodes away with the help of the collision force by increasing an imaginary radius that surrounds the nodes.
I created a seperate button named button and i want to use .data() (to select a whole array) to increase the collide radius into 40 of many nodes when click on that button. For example when a filtered number of nodes is stored in an array called abc, i tried this code:
var node =......
.on("click", function(d, i) {
abc = start && start.path(d) || [];
node.style("fill", function(n) {
if (n == start ) {
return "yellow";
} else if ( n == d) {
return "green"
} else if (abc.includes(n)) {
return "red"
} else {
return "lightgrey"
}
.....
}});
button.on("click", function(d) {
d3.selectAll("circle").data().forEach(d => d.r = 6);
d3.select(abc).data().r = 40;
simulation.nodes(data);
simulation.alpha(0.8).restart();
})
I am able to click on 2 nodes and store these 2 nodes and all the nodes between them in the arrayabc. This is possible with the help of the d3.js function path()which returns the shortest way between 2 nodes.
But unfortunally it does not work. Maybe there is someone who can help me with the problem. The basic idea of pushing nodes away is already discussed in here: Using the force-layout physics for seperated elements
Thanks so much!
After several comments I finally have an idea of how you are filtering the node selection.
In the following demo, the circles have 4 different colours:
var colours = ["blue", "red", "green", "yellow"];
node.attr("fill", (d, i) => colours[i%4]);
So, when you click the button, we simply filter the nodes with "red" colour and increase their r property, making the collide radius increase, using each:
node.filter(function() {
return d3.select(this).attr("fill") === "red"
}).each(d => d.r = 40);
If you want use data as a getter, you can do it with a forEach:
node.filter(function() {
return d3.select(this).attr("fill") === "red"
}).data().forEach(d => d.r = 40);
Which has the same result.
Here is a demo, all red nodes will push away the other nodes after the click, with a collide radius of 40:
var svg = d3.select("svg");
var colours = ["blue", "red", "green", "yellow"];
var data = d3.range(30).map(d => ({
r: 6
}));
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(150).strength(0.05))
.force("y", d3.forceY(75).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return d.r + 1;
}));
var node = svg.selectAll(".circles")
.data(data)
.enter()
.append("circle")
.attr("r", d => d.r)
.attr("fill", (d, i) => colours[i%4]);
d3.select("button").on("click", function(d) {
node.filter(function(){
return d3.select(this).attr("fill") === "red"
}).each(d=>d.r = 40);
simulation.nodes(data);
simulation.alpha(0.8).restart();
})
simulation.nodes(data)
.on("tick", d => {
node.attr("cx", d => d.x).attr("cy", d => d.y);
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<button>Click me</button>
<br>
<svg></svg>

D3 path tween changes side

I'm trying to do a path tween like this one: https://bl.ocks.org/mbostock/3916621
Problem: The path is changing the side. The gray path is changing from left to right and the white path is changing from bottom to top. This isn't the expected transition!
Edit: My expected transition should be a simple grow transition. So the small one should grow to the bigger one.
Example: https://jsfiddle.net/wdv3rufs/
const PATHS = {
FULL: {
GRAY: 'M1035,429l-4.6-73.7L1092,223l-66,1l-66.3-36.4l-102.5,67.6L623.8,0L467.4,302.1l-218.7-82.9L77.6,317.4L0,214.5V429H1035z',
WHITE: 'M0,429V292l249.4-72.9l135.4,56.6L623.8,0L824,232.5l135.7-44.9l26.7,190.5l29.3,16.8l19.3,34H0z'
},
SMALL: {
GRAY: 'M0,429h834l-134.2-34.6l-37,23.2l-130.6-35.2l-112.7,33.5l-144.2-67l-96.7,65.2L43.5,377.5L0,391.4V429z',
WHITE: 'M0,429h834l-134.2-34.6l-83.5,29l-84.1-41l-126.9,31.2l-130-64.7l-144.8,58.6l-87-30L0,386.1V429z'
}
};
const pathTween = (d1, precision) => {
return function() {
const path0 = this;
const path1 = path0.cloneNode();
const n0 = path0.getTotalLength();
const n1 = (path1.setAttribute('d', d1), path1).getTotalLength();
const distances = [0];
const dt = precision / Math.max(n0, n1);
let i = 0;
while ((i += dt) < 1) distances.push(i);
distances.push(1);
const points = distances.map(t => {
const p0 = path0.getPointAtLength(t * n0);
const p1 = path1.getPointAtLength(t * n1);
return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
});
return t => {
return t < 1 ? 'M' + points.map(p => p(t)).join('L') : d1;
};
};
};
const pathTransition = (path, d1) => {
path.transition()
.duration(10000)
.attrTween('d', pathTween(d1, 4));
}
var svg = d3.select('svg');
svg.append('path')
.attr('class', 'white-mountain')
.attr('d', PATHS.SMALL.WHITE)
.call(pathTransition, PATHS.FULL.WHITE);
svg.append('path')
.attr('class', 'gray-mountain')
.attr('d', PATHS.SMALL.GRAY)
.call(pathTransition, PATHS.FULL.GRAY);
How can I get this working?

Categories