I am having trouble using d3's symbol mechanism to specify a unique symbol for each set of data. The data's like this:
[[{x: 1, y:1},{x: 2, y:2},{x: 3, y:3}], [{x: 1, y:1},{x: 2, y:4},{x: 3, y:9}], etc.]
The part of the code that writes out the symbols looks like this:
I create a series group for each vector of points. Then:
series.selectAll("g.points")
//this selects all <g> elements with class points (there aren't any yet)
.data(Object) //drill down into the nested Data
.enter()
.append("g") //create groups then move them to the data location
.attr("transform", function(d, i) {
return "translate(" + xScale(d.x) + "," + yScale(d.y) + ")";
})
.append("path")
.attr("d", function(d,i,j){
return (d3.svg.symbol().type(d3.svg.symbolTypes[j]));
}
);
Or at least that's how I'd like it to work. The trouble is that I can't return the function d3.svg.symbol() from the other function. If I try to just put the function in the "type" argument, then data is no longer scoped correctly to know what j is (the index of the series).
right, but I don't want a unique symbol for each datapoint, I want a unique symbol for each series. The data consists of multiple arrays (series), each of which can have an arbitrary number of points (x,y). I'd like a different symbol for each array, and that's what j should give me. I associate the data (in the example, two arrays shown, so i is 0 then 1 for that) with the series selection. Then I associate the data Object with the points selection, so i becomes the index for the points in each array, and j becomes the index of the original arrays/series of data. I actually copied this syntax from somewhere else, and it works ok for other instances (coloring series of bars in a grouped bar chart for example), but I couldn't tell you exactly why it works...
Any guidance would be appreciated.
Thanks!
What is the question exactly? The code that you give answers your question. My bad, j does return a reference to the series. Simpler example.
var data = [
{id: 1, pts: [{x:50, y:10},{x:50, y:30},{x:50, y:20},{x:50, y:30},{x:50, y:40}]},
{id: 2, pts: [{x:10, y:10},{x:10, y:30},{x:40, y:20},{x:30, y:30},{x:10, y:30}]}
];
var vis = d3.select("svg");
var series = vis.selectAll("g.series")
.data(data, function(d, i) { return d.id; })
.enter()
.append("svg:g")
.classed("series", true);
series.selectAll("g.point")
.data(function(d, i) { return d.pts })
.enter()
.append("svg:path")
.attr("transform", function(d, i) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("d", function(d,i, j) { return d3.svg.symbol().type(d3.svg.symbolTypes[j])(); })
The only difference is that I added parenthesis after d3.svg.symbol().type(currentType)() to return the value rather than the function. D3js uses chaining, jquery style. This let you use symbol().type('circle') to set a value and symbol().type() to get it. Whenever accessors are used, what is returned is a reference to a function that has methods and attributes. Keep in mind that, in Javascript functions are first class objects - What is meant by 'first class object'?. In libraries that use that approach, often, there is an obvious getter for retrieving meaningful data. With symbol, you have to use symbol()().
The code beyond the symbol functionality can be seen at: https://github.com/mbostock/d3/blob/master/src/svg/symbol.js
d3.svg.symbol = function() {
var type = d3_svg_symbolType,
size = d3_svg_symbolSize;
function symbol(d, i) {
return (d3_svg_symbols.get(type.call(this, d, i))
|| d3_svg_symbolCircle)
(size.call(this, d, i));
}
...
symbol.type = function(x) {
if (!arguments.length) return type;
type = d3_functor(x);
return symbol;
};
return symbol;
};
Just in case you haven't. Have you tried?
.append("svg:path")
.attr("d", d3.svg.symbol())
as per https://github.com/mbostock/d3/wiki/SVG-Shapes.
Related
I am using the D3 Force Diagram and trying to find a way to make a network more "linear" and avoid "tangles" when the network generates, especially on long networks.
Example of Tangles
No Tangles, Better but not Ideal
Ideal result, network in a left to right line
Solutions that won't work:
increasing charge
reducing gravity
The example file I built on VizHub is simple, and although increasing charge may seem to work for small networks, 20-50 items will get tangled even with a very strong charge.
I have attempted to seed the location of the nodes starting locations based on the known "rough" rank order of the network. However, this gets disregarded and overwritten immediately by the on(tick) action. I may likely be doing something wrong and may need to do the seeding at a different point in the code but I was not able to find good examples or documentation on this.
I also attempted anchoring some points (less than ideal) but the links disregarded the seeded nodes locations. Again, quite possible that I didn't do it right and that may be my problem, but I was unable to find good examples of anchoring as well.
node.attr("transform", function (d) {
return "translate(" + d.x_seed + "," + d.y_seed + ")";})
Seeding when on("tick",...) disabled
I am constrained to using D3 version 3.5.17 at the latest so I cannot use some of the more modern force tools (like forceY) that would help "flatten" the network.
Any ideas on a good way to get the "ideal" result? Seeding, anchoring, something else?
I've partially answered my question. These have been integrated into the VizHub example now.
How to seed the locations on load.
for (var i = 0; i < nodesData.length; i++){
var t = nodesData[i];
var x_seed = t.rank*width/totalNodes
t.x_seed = x_seed;
t.x = x_seed;
var y_seed = var y_seed = height/2 + Math.random()*height/20 - height/40; // randomize y starting location a little to prevent nodes getting stuck in line
t.y_seed = y_seed;
t.y = y_seed;
}
Anchoring the first, last, and middle nodes:
force.on("tick", function () {
link
.attr("x1", function (d) {
return d.source.x;})
.attr("y1", function (d) {
return d.source.y;})
.attr("x2", function (d) {
return d.target.x;})
.attr("y2", function (d) {
return d.target.y;});
node
.attr("transform", function (d) {
if(d.rank == totalNodes || d.rank == 1 || d.rank == Math.floor(totalNodes/2)){
d.x = d.x_seed
d.y = d.y_seed
}
return "translate(" + d.x + "," + d.y + ")";
});
});
I want to create a kind of infographic where I can represent percentages intuitively using a kind of fill logic.
Example
For the sake of simplicity let's just assume intervals of 25%. For the task of 75% of households, there would be four houses in total and 3 of them would be filled in. The remaining house would remain fill:'none'.
I had something in mind like:
It would be in SVG form.
The only way I can think of to achieve this is pre-draw the houses as a collective image and link the file like:
var fileMap = { 50:'fifty.svg', 75:'seventy-five.svg'};
But this doesn't seem to be very modular, and it doesn't utilize d3 hardly.
Question: Is it possible/feasible to create a simple 25% interval conditional fill using d3 compatible logic? What would my .data() call expect? It has to be an array, maybe a binary:
var data = [1,1,1,0] //75%;
Maybe there's a better way altogether, but that's the best I have got.
"I want to create a kind of infographic where I can represent percentages intuitively using a kind of fill logic"... The technical name for this is pictogram.
For creating a pictogram you don't need anything special, you can use a common enter selection. So, given your data...
var data = [1,1,1,0]
... we will create one house for each array element...
var house = svg.selectAll(null)
.data(data)
.enter()
.append("path")
... and fill them according to the datum:
.style("fill", function(d){
return d ? "blue" : "white"
})
Here is a basic demo:
var d = "m787.67 1599.58l148.83 157.74 124.02-131.45v630.95h396.87 198.44 396.87v-630.95l124.02 131.45 148.83-157.74-768.94-814.97-768.94 814.97m1066.6-709.82v78.868l198.44 210.32v-289.18h-198.44z";
var svg = d3.select("svg");
var data = [1, 1, 1, 0];
var house = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.attr("d", d)
.attr("transform", function(_, i) {
return "translate(" + (i * 70) + ",100) matrix(.04 0 0 .03-4.159-50.852)"
})
.style("stroke", "black")
.style("stroke-width", "50px")
.style("fill", function(d) {
return d ? "blue" : "white"
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
I have confusion about this tutorial of D3. On this page there is some example code:
var myData = [
[15, 20],
[40, 10],
[30, 17]
]
var svg = d3.select("div.output svg")
var selA = svg.selectAll("g").data(myData)
selA.enter().append("g")
selA.attr("transform", function(d,i) { // I'm confused!
return 'translate(70,' + (i*100+50) + ')'
})
selA.exit().remove()
var selB = selA.selectAll('circle')
.data(function(d) { return d })
selB.enter().append('circle')
selB
.attr("cx", function(d,i) { return i*80 }) // I'm confused!
.attr("r", function(d,i) { return d })
selB.exit().remove()
My confusion is about the two function(d,i) functions. Judging from the code output i means different things in the two functions. In the first function, i seems to be the index for the [15,20], [40,10], [30,17] entries. Therefore the indexes are 0, 1, 2. In the second function i seems to be the second dimension index. So the indexes are 0, 1, 0, 1, 0, 1.
I think this has something to do with
var selB = selA.selectAll('circle')
.data(function(d) { return d })
but I can't really think through. Could anyone explain to me why i meant different indexes in the two functions? Thanks!
In your first selection you are binding the data ([[],[],[]]) and creating a group for each element in the data, so the function in selA.attr(..., function(d, i) {}) gets called onces for each element in the outer array (indices 0,1,2).
For the second part, each group in selA got bounded to one of the inner arrays, so selB.enter gets called 3 times (once for each group), each time with the data that was bounded to the group (each of the inner arrays), so each function in selB.attr(...) gets passed each element in each of the inner arrays, hence indices 0,1 three times.
Hope this makes sense :)
Take a look at this example:
http://jsfiddle.net/jaimedp/heEyn/
Let's say you've got a selection with some data bound to it and you use the typical inline anonymous function to access that data:
d3.select("#whatever").each(function(d,i,q) {console.log(d,i,q)})
We all know the first variable is the data and the second is the array position. But what does the third variable (q in this case) represent? So far it's always come back zero in everything I've tested.
The secret third argument is only of use when you have nested selections. In these cases, it holds the index of the parent data element. Consider for example this code.
var sel = d3.selectAll("foo")
.data(data)
.enter()
.append("foo");
var subsel = sel.selectAll("bar")
.data(function(d) { return d; })
.enter()
.append("bar");
Assuming that data is a nested structure, you can now do this.
subsel.attr("foobar", function(d, i) { console.log(d, i); });
This, unsurprisingly, will log the data item inside the nesting and its index. But you can also do this.
subsel.attr("foobar", function(d, i, j) { console.log(d, i, j); });
Here d and i still refer to the same things, but j refers to the index of the parent data element, i.e. the index of the foo element.
A note on Lars's reply, which is correct but I found one more feature that is helpful.
The j element gives the index of the element without regard to the nesting of the parent elements. In other words, if you are appending and logging as follows, the final circles are treated as a flat array, not as a group of nested arrays. So your indexes will be scaled from 0 to the number of circle elements you have, without regard to the data structure of your nesting.
var categorygroups = chart.selectAll('g.categorygroups')
.data(data)
.enter()
.append('g').attr('class','categorygroups');
var valuesgroups = categorygroups.selectAll('g.valuesgroups')
.data(function(d) {return d.values; }).enter().append('g').attr('class','valuesgroups');
valuesgroups.append('text').text(function(d) {
return d.category
}).attr('y',function(d,i) { return (i + 1) * 100 }).attr('x',0);
var circlesgroups = valuesgroups.selectAll('g.circlesgroups')
.data(function(d) {return d.values; }).enter().append('g').attr('class','circlesgroups');
circlesgroups.append('circle').style('fill','#666')
.attr('cy',function(d,i,j) { console.log(j); return (j + 1) * 100 })
.attr('cx',function(d,i) { return (i + 1) * 40 });
I've created a line chart based on the example found here:
http://bl.ocks.org/mbostock/3884955
However, with my data the line labels (cities) end up overlapping because the final values on the y-axis for different lines are frequently close together. I know that I need to compare the last value for each line and move the label up or down when the values differ by 12 units or less. My thought is to look at the text labels that are written by this bit of code
city.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
If the y(d.value.temperature) values differ by 12 or less, move the values apart until they have at least 12 units between them. Any thoughts on how to get this done? This is my first d3 project and the syntax is still giving me fits!
You're probably better off passing in all the labels at once -- this is also more in line with the general d3 idea. You could then have code something like this:
svg.selectAll("text.label").data(data)
.enter()
.append("text")
.attr("transform", function(d, i) {
var currenty = y(d.value.temperature);
if(i > 0) {
var previousy = y(data[i-1].value.temperature),
if(currenty - previousy < 12) { currenty = previousy + 12; }
}
return "translate(" + x(d.value.date) + "," + currenty + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
This does not account for the fact that the previous label may have been moved. You could get the position of the previous label explicitly and move the current one depending on that. The code would be almost the same except that you would need to save a reference to the current element (this) such that it can be accessed later.
All of this will not prevent the labels from being potentially quite far apart from the lines they are labelling in the end. If you need to move every label, the last one will be pretty far away. A better course of action may be to create a legend separately where you can space labels and lines as necessary.
Consider using a D3 force layout to place the labels. See an example here: https://bl.ocks.org/wdickerson/bd654e61f536dcef3736f41e0ad87786
Assuming you have a data array containing objects with a value property, and a scale y:
// Create some nodes
const labels = data.map(d => {
return {
fx: 0,
targetY: y(d.value)
};
});
// Set up the force simulation
const force = d3.forceSimulation()
.nodes(labels)
.force('collide', d3.forceCollide(10))
.force('y', d3.forceY(d => d.targetY).strength(1))
.stop();
// Execute thte simulation
for (let i = 0; i < 300; i++) force.tick();
// Assign values to the appropriate marker
labels.sort((a, b) => a.y - b.y);
data.sort((a, b) => b.value - a.value);
data.forEach((d, i) => d.y = labels[i].y);
Now your data array will have a y property representing its optimal position.
Example uses D3 4.0, read more here: https://github.com/d3/d3-force