I have a data structure like this (assume that the data structure is non-negotiable):
data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]};
Using the d3.js javascript library, I'd like to draw four rectangles, one for each color in both colors arrays. Information from each entry in the segments array is used to draw the rectangles corresponding to each color in its color array. E.g., The red and green rectangles will have a width and height of 10. The resulting html should look like this:
<div id="container">
<svg width="200" height="200">
<g>
<rect x="20" y="20" width="10" height="10" fill="#ff0000"></rect>
<rect x="30" y="30" width="10" height="10" fill="#00ff00"></rect>
</g>
<g>
<rect x="40" y="40" width="20" height="20" fill="#0000ff"></rect>
<rect x="60" y="60" width="20" height="20" fill="#000000"></rect>
</g>
</svg>
</div>
I've come up with some code that accomplishes this, but I found the part about using data from two different levels of nesting in data to be confusing, and I feel that there might be a more idiomatic way to accomplish the same with d3.js. Here's the code (full example at http://jsbin.com/welcome/39650/edit):
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
// add the top-level svg element and size it
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter()
.append('g');
// Add a rectangle for each color
nested
.selectAll('rect')
.data(function(d) {
// **** ATTENTION ****
// Is there a more idiomatic, d3-ish way to approach this?
var expanded = [];
for(var i = 0; i < d.colors.length; i++) {
expanded.push({
color : d.colors[i],
x : d.x
size : d.size });
}
return expanded;
})
.enter()
.append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
Is there a better and/or more idiomatic way to access data from two different levels of nesting in a data structure using d3.js?
Edit
Here's the solution I came up with, thanks to meetamit's answer for the closure idea, and using more idiomatic d3.js indentation thanks to nautat's answer:
$(function() {
var
vis = null,
width = 200,
height = 200,
data = {
segments : [
{x : 20, y : 0, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, y : 0, size : 20, colors : ['#0000ff','#000000']}
]
};
// set the color
function f(d,i) {return d;}
// set the position
function pos(segment) {
return function(d,i) {
return segment.x + (i * segment.size);
};
}
// set the size
function size(segment) {
return function() {
return segment.size;
};
}
// add the top-level svg element and size it
vis = d3.select('#container').append('svg')
.attr('width',width)
.attr('height',height);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter().append('g');
// Add a rectangle for each color. Size of rectangles is determined
// by the "parent" data object.
nested
.each(function(segment, i) {
var
ps = pos(segment),
sz = size(segment);
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors)
.enter().append('rect')
.attr('x', ps)
.attr('y',ps)
.attr('width', sz)
.attr('height',sz)
.attr('fill', f);
});
});
Here's the full working example: http://jsbin.com/welcome/42885/edit
You can use closures
var nested = vis
.selectAll('g')
.data(data.segments);
nested.enter()
.append('g')
.each(function(segment, i) {
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors);
colors.enter()
.append('rect')
.attr('x', function(color, j) { return pos(segment, j); })
// OR: .attr('x', function(color, j) { return segment.x + (j * segment.size); })
.attr('width', function(color, j) { return size(segment); })
.attr('fill', String);
});
You could do something like the following to restructure your data:
newdata = data.segments.map(function(s) {
return s.colors.map(function(d) {
var o = this; // clone 'this' in some manner, for example:
o = ["x", "size"].reduce(function(obj, k) { return(obj[k] = o[k], obj); }, {});
return (o.color = d, o);
}, s);
});
This will transform your input data into:
// newdata:
[
[
{"size":10,"x":20,"color":"#ff0000"},
{"size":10,"x":20,"color":"#00ff00"}],
[
{"size":20,"x":40,"color":"#0000ff"},
{"size":20,"x":40,"color":"#000000"}
]
]
which then can be used in the standard nested data selection pattern:
var nested = vis.selectAll('g')
.data(newdata)
.enter().append('g');
nested.selectAll('rect')
.data(function(d) { return d; })
.enter().append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
BTW, if you'd like to be more d3-idiomatic, I would change the indentation style a bit for the chained methods. Mike proposed to use half indentation every time the selection changes. This helps to make it very clear what selection you are working on. For example in the last code; the variable nested refers to the enter() selection. See the 'selections' chapter in: http://bost.ocks.org/mike/d3/workshop/
I would try to flatten the colors before you actually start creating the elements. If changes to the data occur I would then update this flattened data structure and redraw. The flattened data needs to be stored somewhere to make real d3 transitions possible.
Here is a longer example that worked for me. Yon can see it in action here.
Here is the code:
var data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]
};
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
function flatten(data) {
// converts the .colors to a ._colors list
data.segments.forEach( function(s,i) {
var list = s._colors = s._colors || [];
s.colors.forEach( function(c,j) {
var obj = list[j] = list[j] || {}
obj.color = c
obj.x = s.x
obj.size = s.size
});
});
}
function changeRect(chain) {
return chain
.transition()
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f)
.style('fill-opacity', 0.5)
}
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the top-level svg element and size it
function update(){
flatten(data);
// add the nested svg elements
var all = vis.selectAll('g')
.data(data.segments)
all.enter().append('g');
all.exit().remove();
// Add a rectangle for each color
var rect = all.selectAll('rect')
.data(function (d) { return d._colors; }, function(d){return d.color;})
changeRect( rect.enter().append('rect') )
changeRect( rect )
rect.exit().remove()
}
function changeLater(time) {
setTimeout(function(){
var ds = data.segments
ds[0].x = 10 + Math.random() * 100;
ds[0].size = 10 + Math.random() * 100;
ds[1].x = 10 + Math.random() * 100;
ds[1].size = 10 + Math.random() * 100;
if(time == 500) ds[0].colors.push("orange")
if(time == 1000) ds[1].colors.push("purple")
if(time == 1500) ds[1].colors.push("yellow")
update()
}, time)
}
update()
changeLater(500)
changeLater(1000)
changeLater(1500)
Important here is the flatten function which does the data conversion and stores/reuses the result as _colors property in the parent data element. Another important line is;
.data(function (d) { return d._colors; }, function(d){return d.color;})
which specifies where to get the data (first parameter) AND what the unique id for each data element is (second parameter). This helps identifying existing colors for transitions, etc.
Related
I have a tree visualisation in which I am trying to display paths between nodes that represent a distribution with multiple classes. I want to split the path lengthwise into multiple colours to represent the frequency of each distribution.
For example: say we have Class A (red) and Class B (black), that each have a frequency of 50. Then I would like a path that is half red and half black between the nodes. The idea is to represent the relative frequencies of the classes, so the frequencies would be normalised.
My current (naive) attempt is to create a separate path for each class and then use an x-offset. It looks like this.
However, as shown in the image, the lines do not maintain an equal distance for the duration of the path.
The relevant segment of code:
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "red")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + -2.5 + "," + 0.0 + ")"; });
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "black")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + 2.5 + "," + 0.0 + ")"; });
It would be great if anyone has some advice.
Thanks!
A possible solution is to calculate the individual paths and fill with the required color.
Using the library svg-path-properties from geoexamples.com you can calculate properties (x,y,tangent) of a path without creating it first like it is done in this SO answer (this does not calculate the tangent).
The code snippet does it for 2 colors but it can be easy generalized for more.
You specify the colors, percentage and width of the stroke with a dictionary
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
percent is the amount color[0] takes from the stroke width.
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
The pathPoints parameters
path that needs to be stroked, can be generated by d3.line path example from SO answer
var lineGenerator = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveNatural);
var curvePoints = [[0,0],[0,10],[20,30]];
var duoPath = pathPoints(lineGenerator(curvePoints), 10, duoProp);
path length interval at which to sample (unit pixels). Every 10 pixels gives a good approximation
dictionary with the percent and width of the stroke
It returns an array with the paths to be filled, 1 for each color.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/svg-path-properties#0.4.4/build/path-properties.min.js"></script>
</head>
<body>
<svg id="chart" width="350" height="350"></svg>
<script>
var svg = d3.select("#chart");
function pathPoints(path, stepLength, duoProp) {
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(d => props.getPropertiesAtLength(d));
var pFactor = percent => (percent - 0.5) * duoProp.width;
tProps.forEach(p => {
p.x0 = p.x - pFactor(0) * p.tangentY;
p.y0 = p.y + pFactor(0) * p.tangentX;
p.xP = p.x - pFactor(duoProp.percent) * p.tangentY;
p.yP = p.y + pFactor(duoProp.percent) * p.tangentX;
p.x1 = p.x - pFactor(1) * p.tangentY;
p.y1 = p.y + pFactor(1) * p.tangentX;
});
var format1d = d3.format(".1f");
var createPath = (forward, backward) => {
var fp = tProps.map(p => forward(p));
var bp = tProps.map(p => backward(p));
bp.reverse();
return 'M' + fp.concat(bp).map(p => `${format1d(p[0])},${format1d(p[1])}`).join(' ') + 'z';
}
return [createPath(p => [p.x0, p.y0], p => [p.xP, p.yP]), createPath(p => [p.xP, p.yP], p => [p.x1, p.y1])]
}
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
</script>
</body>
</html>
As a quick follow-up to rioV8's excellent answer, I was able to get their code working but needed to generalise it to work with more than two colours. In case someone else has a similar requirement, here is the code:
function pathPoints(path, stepLength, duoProp) {
// get the properties of the path
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
// build a list of segments to use as approximation points
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(function (d) {
return props.getPropertiesAtLength(d);
});
// incorporate the percentage
var pFactor = function pFactor(percent) {
return (percent - 0.5) * duoProp.width;
};
// for each path segment, calculate offset points
tProps.forEach(function (p) {
// create array to store modified points
p.x_arr = [];
p.y_arr = [];
// calculate offset at 0%
p.x_arr.push(p.x - pFactor(0) * p.tangentY);
p.y_arr.push(p.y + pFactor(0) * p.tangentX);
// calculate offset at each specified percent
duoProp.percents.forEach(function(perc) {
p.x_arr.push(p.x - pFactor(perc) * p.tangentY);
p.y_arr.push(p.y + pFactor(perc) * p.tangentX);
});
// calculate offset at 100%
p.x_arr.push(p.x - pFactor(1) * p.tangentY);
p.y_arr.push(p.y + pFactor(1) * p.tangentX);
});
var format1d = d3.format(".1f");
var createPath = function createPath(forward, backward) {
var fp = tProps.map(function (p) {
return forward(p);
});
var bp = tProps.map(function (p) {
return backward(p);
});
bp.reverse();
return 'M' + fp.concat(bp).map(function (p) {
return format1d(p[0]) + "," + format1d(p[1]);
}).join(' ') + 'z';
};
// create a path for each projected point
var paths = [];
for(var i=0; i <= duoProp.percents.length; i++) {
paths.push(createPath(function (p) { return [p.x_arr[i], p.y_arr[i]]; }, function (p) { return [p.x_arr[i+1], p.y_arr[i+1]]; }));
}
return paths;
}
// generate the line
var duoProp = { color: ["red", "blue", "green"], percents: [0.5, 0.7], width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
Note that the percents array specifies the cumulative percentage of the stroke, not the individual percentages of the width. E.g. in the example above, the red stroke will span 0% to 50% width, the blue stroke 50% to 70% width and the green stroke 70% to 100% width.
I'm experimenting with D3 version 4 force directed graphs and have looked at Jim Vallandingham's tutorial and code as a starting point.
http://vallandingham.me/bubble_chart_v4/
and am attempting to produce an animation similar to the example here from Nathan Yau
https://flowingdata.com/2016/08/23/make-a-moving-bubbles-chart-to-show-clustering-and-distributions/
I've stripped the bubble chart from Jim Vallandingham's code to what I think I need and can display the individual states by changing the index value, but for some reason the code does not want to animate between the different states. I assume the redraw function isn't working. It may be an obvious error or one made through complete ignorance, but if you can help it would be great.
Here's my code:
function bubbleChart() {
var width = 940;
var height = 600;
var center = { x: width / 2, y: height / 3 };
var years = ["0","2008", "2009", "2010"];
var yearCenters = {
2008: { x: width / 3, y: 2 * height / 3 },
2009: { x: width / 2, y: 2 * height / 3 },
2010: { x: 2 * width / 3, y: 2 * height / 3 }
};
// #v4 strength to apply to the position forces
var forceStrength = 0.03;
// These will be set in create_nodes and create_vis
var svg = null;
var bubbles = null;
var nodes = [];
var index= 0;
function charge(d) {
return -Math.pow(d.radius, 2.3) * forceStrength;
}
// Here we create a force layout
var simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(forceStrength).x(center.x))
.force('y', d3.forceY().strength(forceStrength).y(center.y))
.force('charge', d3.forceManyBody().strength(charge))
.on('tick', ticked);
// #v4 Force starts up automatically, which we don't want as there aren't any nodes yet.
simulation.stop();
// Nice looking colors
var fillColor = d3.scaleOrdinal()
.domain(['low', 'medium', 'high'])
.range(['#d84b2a', '#beccae', '#7aa25c']);
function createNodes(rawData) {
var myNodes = rawData.map(function (d) {
return {
id: d.id,
radius: 5,
value: +d.total_amount,
name: d.grant_title,
org: d.organization,
group: d.group,
year: d.start_year,
x: Math.random() * 900,
y: Math.random() * 800
};
});
// sort them to prevent occlusion of smaller nodes.
myNodes.sort(function (a, b) { return b.value - a.value; });
return myNodes;
}
/*
* Main entry point to the bubble chart.
*/
var chart = function chart(selector, rawData) {
// convert raw data into nodes data
nodes = createNodes(rawData);
// Create a SVG element inside the provided selector
// with desired size.
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function (d) { return d.id; });
// Create new circle elements each with class `bubble`.
// There will be one circle.bubble for each object in the nodes array.
// Initially, their radius (r attribute) will be 0.
// #v4 Selections are immutable, so lets capture the
// enter selection to apply our transtition to below.
var bubblesE = bubbles.enter().append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('fill', function (d) { return fillColor(d.group); })
.attr('stroke', function (d) { return d3.rgb(fillColor(d.group)).darker(); })
.attr('stroke-width', 2)
// #v4 Merge the original empty selection and the enter selection
bubbles = bubbles.merge(bubblesE);
// Fancy transition to make bubbles appear, ending with the
// correct radius
bubbles.transition()
.duration(2000)
.attr('r', function (d) { return d.radius; });
// Set the simulation's nodes to our newly created nodes array.
// #v4 Once we set the nodes, the simulation will start running automatically!
simulation.nodes(nodes);
chart.redraw();
};
// Callback function that is called after every tick of the force simulation.
// These x and y values are modified by the force simulation.
function ticked() {
bubbles
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
}
chart.redraw = function (index){
simulation.force('x', d3.forceX().strength(forceStrength).x(nodePosX));
simulation.force('y', d3.forceY().strength(forceStrength).y(nodePosY));
simulation.alpha(1).restart();
}
function nodePosX(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].x;
} else {
return center.x;
}
}
function nodePosY(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].y;
} else {
return center.y;
}
}
// return the chart function from closure.
return chart;
}
var myBubbleChart = bubbleChart();
myBubbleChart('#vis', data);
for (i=0;i<4;i++){
setInterval(function(){myBubbleChart.redraw(i);}, 100);
}
I misunderstood how to use setInterval to redraw the chart, so it should be as follows:
var i = 0;
setInterval(function(){myBubbleChart.redraw(i++);}, 1000);
I have a dataset that looks like this:
var shapes = [
{
type: 'rect', // which shape to draw
size: [ 1, 100 ], // width and height (for rect) or single number for lines, triangles or squares
color: color[0],
orientation: 0 // in degrees in interval [0, 360[
},
{
type: 'triangle',
size: 55,
color: color[1],
orientation: 0
},
{
type: 'triangle',
size: 96,
color: color[0],
orientation: 0
}
// etc …
]
What I want to do is draw all of the shapes in the dataset, which is of variable length and randomly generated, as defined by the various properties in the different objects defining the shapes. The shapes should be equally distributed and not overlap each other.
The data is bound to a surrounding g-element like this:
var viewport = d3.select('body').append('svg').selectAll('g').data(shapes)
var group = viewport.append('g')
How do I approach this the d3 way? I have tried shapes.filter(shape => shape.type === 'rect').forEach(/* ... */) but it feels like I'm not doing it the d3 way. Thanks for any clues on how to approach this!
I'd go with paths, and a function that'd return the path regarding the d.type attribute.
Edit : something a little bit like that, although you'll have to specify somehow the way you want the symbols to be positioned because with this example, they'll just be drawn on top of each other.
var drawers = {
rect: function(d) {
return 'M 0 0 l '+ d.size[0] + ' 0 l 0 ' + d.size[1] + ' l -' + d.size[0] + ' 0 l 0 -' + d.size[1];
},
triangle: function(d) {},
};
var g = d3.select('#mySvg').append('g');
var symbols = g.selectAll('.symbol')
.data(shapes);
symbols.enter()
.append('path')
.classed('symbol', true)
.attr({
d: function(d) {return drawers[d.type](d);}
});
The final solution was to use the d3.svg.symbol() constructor (given that shapes is the array as described in the introductory post, with the slight difference of type being either triangle-up, circle or square:
const center = [ w / 2, h / 2 ]
const vis = d3.select(container)
.append('svg')
.attr('width', w)
.attr('height', h)
const symbols = vis.selectAll('.symbol').data(shapes)
const getPathForShape = d => d3.svg.symbol().type(d.type).size(d.size)()
const paths = symbols.enter()
.append('path')
.classed('symbol', true)
.attr('d', getPathForShape)
.attr('fill', d => d.color)
.attr('x', center[0])
.attr('y', center[1])
they were then distributed by using a force directed graph:
const force = d3.layout.force()
.size([w, h])
.charge(-100)
.nodes(shapes)
.gravity(0.1)
.on('tick', _ => paths.attr('transform', d =>
'translate(' + d.x + ',' + d.y + ')'))
// simulate a static graph:
force.start()
for (var i = 0; i < 100; i++) force.tick()
force.stop()
I'm trying to get d3 to draw a single stacked bar chart, like so:
<svg width = '500' height= '100'>
<rect x='0' y='0' width='224' height='30' fill='green'/>
<rect x='224' y='0' width='84' height='30' fill='blue'/>
<rect x='308' y='0' width='29' height='30' fill='darkgray'/>
<rect x='337' y='0' width='3' height='30' fill='#606060'/>
</svg>
As you can see, the x position starts at zero, then each subsequent x position is equal to the sum of the preceding widths.
So I'm trying to get d3 to draw something like that from an array called datavars:
var datavars = [224, 84, 29, 3];
... and to ensure that d3 assigns the correct width-value to the correct x-value, I created these variables:
var prev_width0 = 0;
var prev_width1 = datavars[0];
var prev_width2 = datavars[0] + datavars[1];
var prev_width3 = datavars[0] + datavars[1] + datavars[2];
... and define the x value like so:
//create SVG element
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
svg.selectAll('rect')
.data(datavars)
.enter()
.append('rect')
.attr('width', function(d){
return d;})
.attr('x',function(d, i){
return 'prev_width'+i; })
.attr('y',0)
.attr('height', 30);
As you might have guessed, the strongly-indented function (based on the sum of the preceding widths, and defined in the prev_width variables) returns a string for each iteration (prev_width0, prev_width1, etc.) and not the values I thought I defined when I created the prev_width variables.
I'm obviously defining the variables incorrectly. Any idea how I can do this properly?
JavaScript does not interpret the string "datavars1" as the variable datavars1. To interpret a string as a JavaScript variable you can use eval(). So change: return 'prev_width'+i; }) to return eval('prev_width'+i); }). Although a better idea might be to use an array instead such as:
var prev_width = [0,
datavars[0],
datavars[0] + datavars[1],
datavars[0] + datavars[1] + datavars[2]
];
...
.attr('x',function(d, i){
return prev_width[i]; })
Fiddle Example
You can do a similar thing with the colors:
var colors = ['green','blue','darkgray','#606060'];
...
.attr('fill', function(d, i){ return colors[i]; })
Bar chart with color
You can also create a single array of objects to store the color, width, etc.
A more scalable idea would be to get rid of the array prev_width and have a function that can take the sum up to a point. Such as:
function sum(array, start, end) {
var total = 0;
for(var i=start; i<end; i++) total += array[i];
return total;
}
Then you can do:
...
.attr('x',function(d, i){
return sum(datavars, 0, i); })
Example using summation
here is a snippet (approached a little differently by me)
//this function deals with pairing finding x for each width in the array and add them to a new array called x_and_width_values
//example :[[0,224],[84,224],[29,308],[3,337]]
// ^ ^ ^ ^ ^ ^ ^ ^
// x width x width .............
function combinations(prev,current,index,arr){
// This method focus on accummulating results by reference previous and current element
//for the first and second element we need specifically add this
if(index==1){
x_and_width_values.push([arr[index-1],0]);
x_and_width_values.push([arr[index],prev])
}
//other elements we use index as follows;
else{
x_and_width_values.push([arr[index],prev])
}
return prev+current
}
//after creating an array with all [x,width] combination, we map it to colors to get out final array
//example :[[[0,224],'green'],[[84,224],'blue'],[29,308],[[3,337],'darkgray']
function colors_with_combination(e, i) {
return [x_and_width_values[i], colors[i]];
}
//*******************acutal beef og code***************
var x_and_width_values=[];
//this link is needed to creation of svg elements
var link="http://www.w3.org/2000/svg"
//all your widths
var all_widths=[224,84,29,3];
//all your colors
var colors=['green','blue','darkgray','#606060'];
//sort your width array to be on the safe side
all_widths.sort(function(a,b){return b-a});
//find all width and x combination
all_widths.reduce(combinations);
//map width and x values to color values
var all = x_and_width_values.map(colors_with_combination);
//create a loop for object creation
for(var i=0;i<all.length;++i){
var rect=document.createElementNS(link,'rect');
rect.setAttributeNS(null,'x',all[i][0][1])
rect.setAttributeNS(null,'width',all[i][0][0]);
rect.setAttributeNS(null,'fill',all[i][1]);
rect.setAttributeNS(null,'height','30');
rect.setAttributeNS(null,'y','0');
document.getElementById('svg').appendChild(rect);;
}
<svg id='svg' width = '500' height= '100'>
</svg>
I'm using d3 and I'd like to append a group with basic shapes attached to it, like the following:
startEvent (a circle)
task (a recangle)
endEvent (two circles)
since I'm new to d3 I'd like to know how to append each group dynamically depending on the 'shape type' and avoid to append each shape one by one using a foreach.
this is the code:
var shapes ={
startEvent:function(id,x,y,params){
var radius = 18,
cy = Math.floor(Number(y) + radius),
cx = Math.floor(Number(x) + radius),
g = d3.select('g');
var circle = g.append('circle')
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radius)
.attr('id', id);
if(params.label!==undefined){
var txt = g.append('text')
.attr('y',y).text(params.label);
txt.attr('x',Number(x));
txt.attr('y',Number(y));
}
return g;
},
endEvent:function(id,x,y, params){
// something similar to startEvent, but with two circles instead of one
},
task:function(id,x,y, params){
// something similar but with a rectangle
}
};
passing the data and rendering the elements:
svg.selectAll('g')
.data(data)
.enter()
.append(function(d){
params={label: d.meta.name};
return shapes[d.type](d.id,d.x,d.y,params);
});
but I'm getting
Error: Failed to execute 'appendChild' on 'Node': The new child
element is null.
I guess that's because I'm returning the selector, any ideas?
based on this and this answers I got to the following point, it seems like you need to create an instance manually under the d3 namespace, once you got that you can use a d3 selector over it and return the node() of the element which return the actual DOM code.
this is the code:
var shapes ={
startEvent:function(id,x,y,params){
var radius = 18,
cy = Math.floor(Number(y) + radius),
cx = Math.floor(Number(x) + radius),
e = document.createElementNS(d3.ns.prefix.svg,'g'),
g = d3.select(e).attr('id', id).
attr('class','node');
var circle = g.append('circle')
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radius)
.attr('class','circle');
if(params.label!==undefined){
var txt = g.append('text')
.attr('y',y).text(params.label);
txt.attr('x',Number(x));
txt.attr('y',Number(y));
}
return g;
},
endEvent:function(id,x,y, params){
// something similar to startEvent, but with two circles instead of one
},
task:function(id,x,y, params){
// something similar but with a rectangle
}
};
and then return the node
svg.selectAll('g')
.data(data)
.enter()
.append(function(d){
params={label: d.meta.name};
var v = shapes[d.type](d.id,d.x,d.y,params);
return v.node();
});