Say I have the following JSON array:
var json = [
[1,2,3],
[1,2,3],
[1,2,3]
];
How can I render a grid of SVG rect nodes without performing 2 selectAll calls? In this fiddle I was able to make the grid, but I had to render each row inside of an intermediate g node. Is there a way to write it without the first selection? If I try:
svg.data(json)
.selectAll('rect')
.data(function(data) { return data; })
.enter()
.append('rect')
.attr('x', function(data, x, y) {
return (x * size) + (x * spacing);
})
.attr('y', function(data, x, y) {
return (y * size) + (y * spacing);
})
.attr('width', size)
.attr('height', size);
it only renders the first row of the 2D array.
Do you mean that you'd rather not have the g nodes in the hierarchy, and instead, you want to make all the rects direct siblings?
This is doable, but you have to first flatten the json into a 1-dimensional array with 9 elements (not shown here; can use Array.reduce()). Then you would only select and bind once:
var flatJson = [1,2,3,1,2,3,1,2,3]
svg
.selectAll('rect')
.data(flatJson)`
and position based on i:
.enter()
.append('rect')
.attr('x', function(d, i) {
return (i % numColumns) * (size + spacing);
})
.attr('y', function(d, i) {
return Math.floor(i/numColumns) * (size + spacing);
})
.attr('width', size)
.attr('height', size);
The trade off is that there needs to be a sense of the number of columns numColumns = 3.
The flattened json could be turned into an array of objects, and each object could have a column and row. For eample:
var flatJson = [
{ row:0, col:0, value: 1},
{ row:0, col:1, value: 2},
{ row:0, col:2, value: 3},
{ row:1, col:0, value: 1},
...
]
Related
I'm new to d3 and using it for creating a simple chart using array of numbers where the value '16' appears twice in it.
It generate the chart with one 'missing' 'rect' element for the 2nd '16' value, when I check the html I see that both '16' rect has same 'y' value of 72.
Please tell me what I'm doing wrong, thanks
code:
var data = [4, 8, 15, 16, 23, 16];
var chart = d3.select("body").append("svg")
.attr("class", "chart")
.attr("width", 420)
.attr("height", 20 * data.length);
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420])
var y = d3.scale.ordinal()
.domain(data)
.rangeBands([0, 120]);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", y)
.attr("width", x)
.attr("height", y.rangeBand());
The problem with your code is that you are trying to use the values from your data array to create range bands on an ordinal scale. Since the same input value will always be mapped to the same output value that means that both inputs 16 get mapped to the same range band 72.
If you want each input value to be mapped to its own "bar" then you need to use array indices instead of array values.
First you prepare the indices
var indices = d3.range(0, data.length); // [0, 1, 2, ..., data.length-1]
Then you use them to define the y scale domain
var y = d3.scale.ordinal()
.domain(indices)
// use rangeRoundBands instead of rangeBands when your output units
// are pixels (or integers) if you want to avoid empty line artifacts
.rangeRoundBands([0, chartHeight]);
Finally, instead of using array values as inputs use array indices when mapping to y
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (value, index) {
// use the index as input instead of value; y(index) instead of y(value)
return y(index);
})
.attr("width", x)
.attr("height", y.rangeBand());
As an added bonus this code will automatically rescale the chart if the amount of data changes or if you decide to change the chart width or height.
Here's a jsFiddle demo: http://jsfiddle.net/q8SBN/1/
Complete code:
var data = [4, 8, 15, 16, 23, 16];
var indices = d3.range(0, data.length);
var chartWidth = 420;
var chartHeight = 120;
var chart = d3.select("body").append("svg")
.attr("class", "chart")
.attr("width", chartWidth)
.attr("height", chartHeight);
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, chartWidth])
var y = d3.scale.ordinal()
.domain(indices)
.rangeRoundBands([0, chartHeight]);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (value, index) { return y(index); })
.attr("width", x)
.attr("height", y.rangeBand());
The way you are setting the y attribute of the rectangles will utilize the same value for all duplicate elements. You can use some offsetting like so:
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function (d, i) {
return (i * y.rangeBand()) + y.rangeBand();})
.attr("width", x)
.attr("height", y.rangeBand());
Also you might have to adjust the height of your overall chart to see all the bands.
The description of the selection.data function includes an example with multiple groups (link) where a two-dimensional array is turned into an HTML table.
In d3.js v3, for lower dimensions, the accessor functions included a third argument which was the index of the parent group's datum:
td.text(function(d,i,j) {
return "Row: " + j;
});
In v4, this j argument has been replaced by the selection's NodeList. How do I access the parent group's datum index now?
Well, sometimes an answer doesn't provide a solution, because the solution may not exist. This seems to be the case.
According to Bostock:
I’ve merged the new bilevel selection implementation into master and also simplified how parents are tracked by using a parallel parents array.
A nice property of this new approach is that selection.data can
evaluate the values function in exactly the same manner as other
selection functions: the values function gets passed {d, i, nodes}
where this is the parent node, d is the parent datum, i is the parent
(group) index, and nodes is the array of parent nodes (one per group).
Also, the parents array can be reused by subselections that do not
regroup the selection, such as selection.select, since the parents
array is immutable.
This change restricts functionality—in the sense that you cannot
access the parent node from within a selection function, nor the
parent data, nor the group index — but I believe this is ultimately A
Good Thing because it encourages simpler code.
(emphasis mine)
Here's the link: https://github.com/d3/d3-selection/issues/47
So, it's not possible to get the index of the parent's group using selection (the parent's group index can be retrieved using selection.data, as this snippet bellow shows).
var testData = [
[
{x: 1, y: 40},
{x: 2, y: 43},
{x: 3, y: 12},
{x: 6, y: 23}
], [
{x: 1, y: 12},
{x: 4, y: 18},
{x: 5, y: 73},
{x: 6, y: 27}
], [
{x: 1, y: 60},
{x: 2, y: 49},
{x: 3, y: 16},
{x: 6, y: 20}
]
];
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
var g = svg.selectAll(".groups")
.data(testData)
.enter()
.append("g");
var rects = g.selectAll("rect")
.data(function(d, i , j) { console.log("Data: " + JSON.stringify(d), "\nIndex: " + JSON.stringify(i), "\nNode: " + JSON.stringify(j)); return d})
.enter()
.append("rect");
<script src="https://d3js.org/d3.v4.min.js"></script>
My workaround is somewhat similar to Dinesh Rajan's, assuming the parent index is needed for attribute someAttr of g.nestedElt:
v3:
svg.selectAll(".someClass")
.data(nestedData)
.enter()
.append("g")
.attr("class", "someClass")
.selectAll(".nestedElt")
.data(Object)
.enter()
.append("g")
.attr("class", "nestedElt")
.attr("someAttr", function(d, i, j) {
});
v4:
svg.selectAll(".someClass")
.data(nestedData)
.enter()
.append("g")
.attr("class", "someClass")
.attr("data-index", function(d, i) { return i; }) // make parent index available from DOM
.selectAll(".nestedElt")
.data(Object)
.enter()
.append("g")
.attr("class", "nestedElt")
.attr("someAttr", function(d, i) {
var j = +this.parentNode.getAttribute("data-index");
});
I ended up defining an external variable "j" and then increment it whenever "i" is 0
example V3 snippet below.
rowcols.enter().append("rect")
.attr("x", function (d, i, j) { return CalcXPos(d, j); })
.attr("fill", function (d, i, j) { return GetColor(d, j); })
and in V4, code converted as below.
var j = -1;
rowcols.enter().append("rect")
.attr("x", function (d, i) { if (i == 0) { j++ }; return CalcXPos(d, j); })
.attr("fill", function (d, i) { return GetColor(d, j); })
If j is the nodeList...
j[i] is the current node (eg. the td element),
j[i].parentNode is the level-1 parent (eg. the row element),
j[i].parentNode.parentNode is the level-2 parent (eg. the table element),
j[i].parentNode.parentNode.childNodes is the array of level-1 parents (eg. array of row elements) including the original parent.
So the question is, what is the index of the parent (the row) with respect to it's parent (the table)?
We can find this using Array.prototype.indexOf like so...
k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode);
You can see in the snippet below that the row is printed in each td cell when k is returned.
var testData = [
[
{x: 1, y: 1},
{x: 1, y: 2},
{x: 1, y: 3},
{x: 1, y: 4}
], [
{x: 2, y: 1},
{x: 2, y: 2},
{x: 2, y: 3},
{x: 2, y: 4}
], [
{x: 3, y: 4},
{x: 3, y: 4},
{x: 3, y: 4},
{x: 3, y: 4}
]
];
var tableData =
d3.select('body').selectAll('table')
.data([testData]);
var tables =
tableData.enter()
.append('table');
var rowData =
tables.selectAll('table')
.data(function(d,i,j){
return d;
});
var rows =
rowData.enter()
.append('tr');
var eleData =
rows.selectAll('tr')
.data(function(d,i,j){
return d;
});
var ele =
eleData.enter()
.append('td')
.text(function(d,i,j){
var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode);
return k;
});
<script src="https://d3js.org/d3.v4.min.js"></script>
Reservations
This approach is using DOM order as a proxy for data index. In many cases, I think this is a viable band-aid solution if this is no longer possible in D3 (as reported in this answer).
Some extra effort in manipulating the DOM selection to match data might be needed. As an example, filtering j[i].parentNode.parentNode.childNodes for <tr> elements only in order to determine the row -- generally speaking the childNodes array may not match the selection and could contain extra elements/junk.
While this is not a cure-all, I think it should work or could be made to work in most cases, presuming there is some logical connection between DOM and data that can be leveraged which allows you to use DOM child index as a proxy for data index.
Here's an example of how to use the selection.each() method. I don't think it's messy, but it did slow down the render on a large matrix. Note the following code assumes an existing table selection and a call to update().
update(matrix) {
var self = this;
var tr = table.selectAll("tr").data(matrix);
tr.exit().remove();
tr.enter().append("tr");
tr.each(addCells);
function addCells(data, rowIndex) {
var td = d3.select(this).selectAll("td")
.data(function (d) {
return d;
});
td.exit().remove();
td.enter().append("td");
td.attr("class", function (d) {
return d === 0 ? "dead" : "alive";
});
td.on("click", function(d,i){
matrix[rowIndex][i] = d === 1 ? 0 : 1; // rowIndex now available for use in callback.
});
}
setTimeout(function() {
update(getNewMatrix(matrix))
}, 1000);
},
Assume you want to do a nested selectiom, and your
data is some array where each element in turn
contains an array, let's say "values". Then you
have probably some code like this:
var aInnerSelection = oSelection.selectAll(".someClass") //
.data(d.values) //
...
You can replace the array with the values by a new array, where
you cache the indices within the group.
var aInnerSelection = oSelection.selectAll(".someClass") //
.data(function (d, i) {
var aData = d.values.map(function mapValuesToIndexedValues(elem, index) {
return {
outerIndex: i,
innerIndex: index,
datum: elem
};
})
return aData;
}, function (d, i) {
return d.innerIndex;
}) //
...
Assume your outer array looks like this:
[{name "X", values: ["A", "B"]}, {name "y", values: ["C", "D"]}
With the first approach, the nested selection brings you from here
d i
------------------------------------------------------------------
root dummy X {name "X", values: ["A", "B"]} 0
dummy Y {name "Y", values: ["C", "D"]} 1
to here.
d i
------------------------------------------------------------------
root X A "A" 0
B "B" 1
Y C "C" 2
D "D" 3
With the augmented array, you end up here instead:
d i
------------------------------------------------------------------
root X A {datum: "A", outerIndex: 0, innerIndex: 0} 0
B {datum: "B", outerIndex: 0, innerIndex: 1} 1
Y C {datum: "C", outerIndex: 1, innerIndex: 0} 2
D {datum: "D", outerIndex: 1, innerIndex: 1} 3
So you have within the nested selections, in any function(d,i), all
information you need.
Here's a snippet I crafter after re-remembering this usage of .each for nesting, I thought it may be useful to others who end up here. This examples creates two layers of circles, and the parent group index is used to determine the color of the circles - white for the circles in the first layer, and black for the circles in the top layer (only two layers in this case).
const nested = nest().key(layerValue).entries(data);
let layerGroups = g.selectAll('g.layer').data(nested);
layerGroups = layerGroups.enter().append('g').attr('class', 'layer')
.merge(layerGroups);
layerGroups.each(function(layerEntry, j) {
const circles = select(this)
.selectAll('circle').data(layerEntry.values);
circles.enter().append('circle')
.merge(circles)
.attr('cx', d => xScale(xValue(d)))
.attr('cy', d => yScale(yValue(d)))
.attr('r', d => radiusScale(radiusValue(d)))
.attr('fill', j === 0 ? 'white' : 'black'); // <---- Access parent index.
});
My solution was to embed this information in the data provided to d3js
data = [[1,2,3],[4,5,6],[7,8,9]]
flattened_data = data.reduce((acc, v, i) => {
v.forEach((d, j) => {
data_item = { i, j, d };
acc.push(data_item);
});
return acc;
}, []);
Then you can access i, j and d from the data arg of the function
td.text(function(d) {
// Can access i, j and original data here
return "Row: " + d.j;
});
I'm attempting to make a visualization with a multistage animation. Here's a contrived fiddle illustrating my problem (code below).
In this visualization the boxes in each row should turn green when the entire group has finished moving to the right column. IOW, when the first row (containing 3 boxes) is entirely in the right column, all the boxes should turn from black to green, but the second row, having only partially moved to the right column at this point, would remain black until it, too, is completely in the right column.
I'm having a hard time designing this transition.
Basic chaining without a delay immediately turns each box green once its finished moving (this is how it's working currently). Not good enough.
On the other hand creating a delay for the chain is difficult, since the effective delay per group is based on the number of boxes it has and I don't think this count is available to me.
It's like I need the transition to happen at mixed levels of granularity.
How should I go about doing this?
The fiddle (code below)
var data = [
["x", "y", "z"],
["a", "b", "c", "d", "e"]
];
var svg = d3.select("svg");
var group = svg.selectAll("g").data(data)
.enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0, " + (40 * i) + ")";
});
var box = group.selectAll("rect")
.data(function(d) { return d; });
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.transition()
.attr("style", "fill:green");
// I probably need a delay here but it'd be based off the
// number of elements in the nested data and I don't know
// how to get that count
.attr("style", "fill:green");
I manage to get the effect you want, it's a little tricky though. You can customize the behavior of a transition at the begining and end of a transition. If you add a function to the end of the transition that detects if the transitioned element is the last in the group, you select all the rectangles in the group and apply the change to them.
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.each('end', function(d, i) {
var g = d3.select(d3.select(this).node().parentNode),
n = g.selectAll('rect')[0].length;
if (i === n - 1) {
g.selectAll('rect').attr('fill', 'green');
}
});
More details in the transitions here, a working fiddle here.
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
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.