I'm hand cranking a network diagram from D3 (I didn't like the output of Force Directed). To do this, I've generated an array of nodes, each with an x/y co-ordinate.
{
"nodes" : [
{
"name" : "foo",
"to" : ["bar"]
},
{
"name" : "bar",
"to" : ["baz"]
},
{
"name" : "baz"
}
]
}
I then generate an svg, with a parent svg:g, and bind this data to a series of svg:g elements hanging off the parent.
addSvg = function () {
// add the parent svg element
return d3.select('#visualisation')
.append('svg')
.attr('width', width)
.attr('height', height);
};
addSvgGroup = function (p) {
// add a containing svg:g
return p.append('svg:g').
attr('transform', 'translate(0,0)');
};
addSvgNodes = function(p, nodes) {
// attach all nodes to the parent p data
// and render to svg:g
return p.selectAll('g')
.data(nodes)
.enter()
.append('svg:g')
.attr('class', 'node');
};
Then I manually position the nodes (this will be dynamic later, I'm just getting my feet)
transformNodes = function (nodes) {
// position nodes manually
// deprecate later for column concept
nodes.attr('transform', function (o, i) {
offset = (i + 1) * options.nodeOffset;
// options.nodeOffset = 150
o.x = offset;
o.y = offset / 2;
return 'translate(' + offset + ',' + offset / 2 + ')';
});
};
Then I attach these items to the parent svg:g, and hang some text off them.
This results in a staircase of text descending left to right within the svg. So far, so good.
Next, I want to generate some links, so I use a method to determine if the current node has a relationship, and then get that nodes location. Finally, I generate a series of links using d3.svg.diagonal and set their source/target to the appropriate nodes. Written longhand for clarity.
getLinkGenerator = function (o) {
return d3.svg.diagonal()
.source(o.source)
.target(o.target)
.projection(function (d) {
console.log('projection', d);
return [d.x, d.y]
});
};
Now, so far, so good - except the control handles for the bezier are not where I would like them to be. For example from node A to node B the path d attribute is thus:
<path d="M150,75C150,112.5 300,112.5 300,150" style="fill: none" stroke="#000"></path>
But I'd like it to alter the orientation of the control handles - i.e
<path d="M150,75C200,75 250,150 300,150" style="fill: none" stroke="#000"></path>
This would make it look more like a dendrograph from the page of examples. What I noticed in the collapsible dendrograph example is that it returns an inversion of the axes:
return [d.y, d.x]
But if I do this, while the control points are oriented as I would like, the location of the points is out of whack (i.e their x/y co-ordinates are also reversed, effectively translating them.
Has anyone else encountered an issue like this or have an idea of how to fix it?
OK, so I took a look at this and figured out a solution. It appears that some of the layouts (dendrogram, collapsed tree) are inverting the co-ordinates of source/target in the path links so that when they hit the projection call, they get reversed back into their correct location, but with the orientation of their bezier points rotated.
So, if you're hand cranking a custom layout and you want to orient the bezier controls horizontally (like the examples), that's what you need to do.
Related
I have a basic map here, with dummy data. Basically a bubble map.
The problem is I have multiple dots (ex:20) with exact same GPS coordinates.
The following image is my csv with dummy data, color blue highlight overlapping dots in this basic example. Thats because many compagny have the same city gps coordinates.
Here is a fiddle with the code I'm working on :
https://jsfiddle.net/MathiasLauber/bckg8es4/45/
Many research later, I found that d3.js add this force simulation fonction, that avoid dots from colliding.
// Avoiding bubbles overlapping
var simulationforce = d3.forceSimulation(data)
.force('x', d3.forceX().x(d => xScale(d.longitude)))
.force('y', d3.forceY().y(d => yScale(d.latitude)))
.force('collide', d3.forceCollide().radius(function(d) {
return d.radius + 10
}))
simulationforce
.nodes(cities)
.on("tick", function(d){
node
.attr("cx", function(d) { return projection.latLngToLayerPoint([d.latitude, d.longitude]).x; })
.attr("cy", function(d) {return projection.latLngToLayerPoint([d.latitude, d.longitude]).y; })
});
The problem is I can't make force layout work and my dots are still on top of each other. (lines: 188-200 in the fiddle).
If you have any tips, suggestions, or if you notice basic errors in my code, just let me know =D
Bunch of code close to what i'm trying to achieve
https://d3-graph-gallery.com/graph/circularpacking_group.html
https://jsbin.com/taqewaw/edit?html,output
There are 3 problems:
For positioning the circles near their original position, the x and y initial positions need to be specified in the data passed to simulation.nodes() call.
When doing a force simulation, you need to provide the selection to be simulated in the on tick callback (see node in the on('tick') callback function).
The simulation needs to use the previous d.x and d.y values as calculated by the simulation
Relevant code snippets below
// 1. Add x and y (cx, cy) to each row (circle) in data
const citiesWithCenter = cities.map(c => ({
...c,
x: projection.latLngToLayerPoint([c.latitude, c.longitude]).x,
y: projection.latLngToLayerPoint([c.latitude, c.longitude]).y,
}))
// citiesWithCenter will be passed to selectAll('circle').data()
// 2. node selection you forgot
const node = selection
.selectAll('circle')
.data(citiesWithcenter)
.enter()
.append('circle')
...
// let used in simulation
simulationforce.nodes(citiesWithcenter).on('tick', function (d) {
node
.attr('cx', function (d) {
// 3. use previously computed x value
// on the first tick run, the values in citiesWithCenter is used
return d.x
})
.attr('cy', function (d) {
// 3. use previously computed y value
// on the first tick run, the values in citiesWithCenter is used
return d.y
})
})
Full working demo here: https://jsfiddle.net/b2Lhfuw5/
I have a line chart with current year and comparison year / previous period. I would like to show a tooltip on hover, so it will display the current tick's value for both current's years and the comparing year. However, I've noticed that the total value returned doesn't match the amount on the y-axis (please see screenshot below).
As per image above the value should be between 30M - 40M but its showing as 56150248.2.
CodeSandbox
// tooltip on x axis
d3.selectAll(".xAxis > .tick")
.style("color", "#65757E")
.on("mouseover", function (d) {
console.log("data");
const mousePosition = d3.pointer(d);
const data = yScale.invert(mousePosition[0]);
console.log("pos", mousePosition);
console.log("data", data);
})
.on("mouseout", (d) => {
console.log("test");
});
What happens is the following:
your selection for the mouseover is d3.selectAll(".xAxis > .tick")
on the line: .on("mouseover", function (d) {
the argument d refers to the event beeing passed. I recommend
calling it event instead of d otherwise it confusing
The mousePosition is initialized by an array with weird coordinates, namely coordinates relative to the selection (which are the ticks)
See: d3.pointer(event[, target])
Returns a two-element array of numbers [x, y] representing the
coordinates of the specified event relative to the specified target.
event can be a MouseEvent, a PointerEvent, a Touch, or a custom event
holding a UIEvent as event.sourceEvent.
If target is not specified, it defaults to the source event’s
currentTarget property, if available. If the target is an SVG element,
the event’s coordinates are transformed using the inverse of the
screen coordinate transformation matrix. If the target is an HTML
element, the event’s coordinates are translated relative to the
top-left corner of the target’s bounding client rectangle. (As such,
the coordinate system can only be translated relative to the client
coordinates. See also GeometryUtils.) Otherwise, [event.pageX,
event.pageY] is returned.
With const data = yScale.invert(mousePosition[0]); you refer to the x position with mousePosition[0], you probably meant mousePosition[1]
To use coordinates which you invert with the yscale you want the coordinates to be relative to the translated g directly under the svg. You have the selection of that element already referenced by the variable svg.
// tooltip on the g child of the svg
svg
.style("color", "#65757E")
.on("mouseover", function (event) {
console.log("data");
const mousePosition = d3.pointer(event);
const data = yScale.invert(mousePosition[1]);
console.log("pos", mousePosition);
console.log("data", data);
})
.on("mouseout", (event) => {
console.log("test");
});
Update 06/13/2021 based on the comments below:
You may want to do something like this (not sure if I reference the right data, but you can work from this:
d3.selectAll(".xAxis > .tick")
.style("color", "#65757E")
.on("mousemove", function (event) {
const mousePosition = d3.pointer(event, svg.node()); // gets [x,y]
console.log("mouse position:", mousePosition);
const currentDate = xScale.invert(mousePosition[0]); // converts x to date
console.log("date at position:", currentDate);
const bisect = d3.bisector(d => new Date(d.date)) // create a bisector
const index_currentData = bisect.right(lineChartData.values[0].dataset, currentDate);
if (index_currentData < lineChartData.values[0].dataset.length) {
console.log(`Current data at index[${index_currentData}]: ${lineChartData.values[0].dataset[index_currentData].value}`);
}
const index_comparisonData = bisect.right(comparisonData.values[0].dataset, currentDate);
if (index_comparisonData < comparisonData.values[0].dataset.length) {
console.log(`Comparison data at index[${index_comparisonData}]: ${comparisonData.values[0].dataset[index_comparisonData].value}`);
}
})
Say, I am having a 2d arr = [[1,2,3],[100,200],['A','B'...'Z']]. As you can see, arr[0].length!=arr[1].length!=arr[2].length.
I want to respresent them as text-tags within an svg.
Moreover, I want to be flexible as in where each subarray starts in terms of (x,y) and how wide the spaces are between each element of a subarray (x, y).
d3.select('svg').selectAll('g').data(arr).enter().append('g').selectAll('text').data((d)=>d).enter().append('text').text(d=>d)
But this way I am losing information within each g. I tried setting .attr('x', (d,i) => (i+1)*20) before .selectAll('text'), but it only adds the 'x'-attr to g and has no effect on the text (i.e. elements of the array) displayed on the page.
The thing is that I put all them into the DOM. But then how can I adjust their .attr('x'), .attr('y') in a group (without hardcoding) like in lines, but each line can have its own spacing between elements?
You can try to use "getBBox" or getBoundingClientRect to get node width, then make a herizontal layout.
var arr = [[1,2,3],[100,200],['A','B', 'C', 'D', 'E', 'Z']]
var container = d3.select("#box")
.append("g")
.attr("transform", "translate(10, 50)");
container
.selectAll("text")
.data(arr)
.enter()
.append("text")
.text(d => d);
/*
Using "getBBox" or "getBoundingClientRect" method to get each text node's width,
then move them one another.
You must make sure the svg element and its parent elements are visible,
which means "display: none" is should not applied on them.
*/
var xInit = 0, padding = 15;
container
.selectAll("text")
.each(function (d, i) {
d3.select(this).attr("x", xInit);
xInit += this.getBBox().width + padding;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg id="box" width="500" height="300"></svg>
I think I have got it. Not sure if that's a clear-cut solution but it works.
First, adding ids after creating gs. Then writing functions for x and y coordinates based on the ids (i.e. rows).
function x(d,i,j){
let node_id = j[0].parentNode.id; // allows to dynamically access the ids of every text element
//calculate x_value here
return x_value;
}
And that function can be passed into the original chain.
d3.select('svg').selectAll('g').data(arr).enter().append('g')
.attr('id', (d,i)=>i) // adding ids to every g based on the array index
.selectAll('text').data((d)=>d).enter().append('text').text(d=>d)
.attr('x', x) // here
If anyone knows a leaner solution, it would be much appreciated as I am new to Javascript and d3.
I am using d3 to make a line chart that has to support up to 100 points on it, making it very crowded. The problem is that some of the labels overlap.
The method I was trying involved drawing all the points, then separately drawing all the labels and running a force collision on the labels to stop them overlapping, then after the force collision drawing a line between each of the labels and their associated point.
I can't make the forces work, let alone the drawing of lines after.
Any suggestions for a better way to do this are heartily welcomed also.
Here is my code:
$.each(data.responseJSON.responsedata, function(k, v) {
var thispoint = svg.append("g").attr("transform", "translate("+pointx+","+pointy+")");
thispoint.append("circle").attr("r", 10).style("fill","darkBlue").style("stroke","black");
var label = svg.append("text").text(v.conceptName).style("text-anchor", "end").attr("font-family", "Calibri");
label.attr("transform", "translate("+(pointx)+","+(pointy-12)+") rotate(90)")
});
nodes = d3.selectAll("text")
simulation = d3.forceSimulation(nodes)
.force("x", d3.forceX().strength(10))
.force("y", d3.forceY().strength(10))
.force("collide",d3.forceCollide(20).strength(5))
.velocityDecay(0.15);
ticks = 0;
simulation.nodes(data)
.on("tick", d => {
ticks = ticks + 1;
d3.select(this).attr("x", function(d) { return d.x }).attr("y", function(d) { return d.x });
console.log("updated" + this)
});
Force layout is a relatively expensive way of moving labels to avoid collision. It is iteratively and computationally intensive.
More efficient algorithms add the labels one at a time, determining the best position for each. For example a 'greedy' strategy adds each label in sequence, selecting the position where the label has the lowest overlap with already added labels.
I've created a D3 components, d3fc-label-layout, that implements a number of label layout strategies:
https://github.com/d3fc/d3fc-label-layout
Here's an example of how to use it:
// Use the text label component for each datapoint. This component renders both
// a text label and a circle at the data-point origin. For this reason, we don't
// need to use a scatter / point series.
const labelPadding = 2;
const textLabel = fc.layoutTextLabel()
.padding(2)
.value(d => d.language);
// a strategy that combines simulated annealing with removal
// of overlapping labels
const strategy = fc.layoutRemoveOverlaps(fc.layoutGreedy());
// create the layout that positions the labels
const labels = fc.layoutLabel(strategy)
.size((d, i, g) => {
// measure the label and add the required padding
const textSize = g[i].getElementsByTagName('text')[0].getBBox();
return [
textSize.width,
textSize.height
];
})
.position((d) => {
return [
d.users,
d.orgs
]
})
.component(textLabel);
https://bl.ocks.org/ColinEberhardt/27508a7c0832d6e8132a9d1d8aaf231c
I have an example of a chart on jsFiddle which has multiple groups of multiple lines. It draws successfully, but I would like to be able to transition to new sets of data.
The example should update with new data after 4 seconds. Although the chart gets called (and outputs something in the console), the lines aren't updated.
I've tried lots of variations based on existing, simpler examples, with no luck. I suspect its the nested data based on this SO answer that's confusing me even more than usual.
SO insists I have some code in the answer, so here's where I assume I need to add/change something:
svg.transition().attr({ width: width, height: height });
g.attr('transform', 'translate(' + margin.left +','+ margin.right + ')');
var lines = g.selectAll("g.lines").data(function(d) { return d; });
lines.enter().append("g").attr("class", "lines");
lines.selectAll("path.line.imports")
.data(function(d) { return [d.values]; })
.enter().append("path").attr('class', 'line imports');
lines.selectAll('path.line.imports')
.data(function(d) { return [d.values]; })
.transition()
.attr("d", function(d) { return imports_line(d); });
// repeated for path.line.exports with exports_line(d).
The problem is the way you're determining the top-level g element that everything is appended to:
var g = svg.enter().append('svg').append('g');
This will only be set if there is no SVG already, as you're only handling the enter selection. If it exists, g will be empty and therefore nothing will happen. To fix, select the g explicitly afterwards:
var g = svg.enter().append('svg').append('g').attr("class", "main");
g = svg.select("g.main");
Complete demo here.