D3 pack layout make multiple nodes value consistent - d3.js

I'm using d3.pack and hierarchy to create circle layout. However, since I want to have a rectangle container rather than circular. I thought I split my data into half and position them side by side. In doing so, I realized that the radius of each array does not reflect one another as it scales dynamically. Is there a way to ensure the data of each array to be consistent and relative?
From the code below, the circle "Apple 5" has the same value with "Apple 1" but get bigger radius from the hierarchy. Please support if you have experience this. Thanks!
const data = {
children: [
{ label: 'Apple 1', count: 100 },
{ label: 'Apple 2', count: 70 },
{ label: 'Apple 3', count: 10 },
{ label: 'Apple 4', count: 50 },
{ label: 'Apple 5', count: 100 },
{ label: 'Apple 6', count: 30 },
],
};
let arrays = [];
if (data.children.length > 3) {
const index = Math.ceil(data.children.length / 2);
arrays[0] = { ...data, children: data.children.slice(0, index) };
arrays[1] = { ...data, children: data.children.slice(index) };
}
var bubble = d3.pack().size([600, 600]).padding(5);
arrays.forEach((item) => {
var nodes = d3.hierarchy(item).sum(function (d) {
return d.count;
});
var node = svg.selectAll("circle")
.data(bubble(nodes).descendants())
.enter()
.append('circle')
.attr('r', function (d) {
return d.r;
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
})

Related

failed to use d3 to update legend data, entering is empty for changed data

I have an "add dataset" button, and once I click it the legend should show the changed dataset, this is all good. I also have a click event with the legend, so when I click the legend, the dataset get deleted, and I put a line through to the style, and here is the problem, when I use the add button to add another dataset, the deleted dataset legend should be changed to the new dataset number. The console shows the data is correct, but the legend is not changing. I have followed some tutorial and I completely followed it, but still get this, I have been working on this for several hours, I would like some help, thank you
let jsonObj = [{
category: "Jan",
values: [{
value: 9,
source: "dataset1",
},
{
value: 8,
source: "dataset2",
},
],
},
{
category: "Feb",
values: [{
value: 15,
source: "dataset1",
},
{
value: 21,
source: "dataset2",
},
],
},
];
// function for adding data
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 15 + 1);
const add = (arr) => {
counter++;
arr.map((i) =>
i.values.push({
value: ran(),
source: `dataset${counter}`
})
);
};
add(arr);
};
//initial variables
let svg, totalWidth, totalHeight, legend;
const setup = () => {
totalWidth = 100;
totalHeight = 100;
svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
};
const draw = (data) => {
//No.6 set lengend
legend = svg
.selectAll(".legend")
.data(data[0].values.map((d) => d.source));
console.log(data[0].values.map((d) => d.source));
let entering = legend.enter();
let newLegend = entering
.append("g")
.attr("class", "legend")
.attr("transform", (d, i) => "translate(0," + i * 20 + ")")
.append("text")
.attr("x", totalWidth)
.attr("y", 9)
.style("text-anchor", "end")
.text((d) => d);
entering.text((d) => d);
let exiting = legend.exit();
exiting.remove();
newLegend.on("click", function(e, d) {
jsonObj.forEach((i) => {
i.values = i.values.filter((s) => s.source != d);
});
newLegend
.filter((x) => x === d)
.style("text-decoration", "line-through");
});
};
setup();
draw(jsonObj);
const update = () => {
add_set(jsonObj);
draw(jsonObj);
};
<script src="https://d3js.org/d3.v6.min.js"></script>
<button onclick="update()">Add dataset</button>
The following solution is exactly what you asked for.
I don't remove any nodes, because you always re-use them.
There is also a difference between the entering nodes and the newLegend nodes - one has actual content, the other is just a sort of empty array.
let jsonObj = [{
category: "Jan",
values: [{
value: 9,
source: "dataset1",
},
{
value: 8,
source: "dataset2",
},
],
},
{
category: "Feb",
values: [{
value: 15,
source: "dataset1",
},
{
value: 21,
source: "dataset2",
},
],
},
];
// function for adding data
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 15 + 1);
const add = (arr) => {
counter++;
arr.map((i) =>
i.values.push({
value: ran(),
source: `dataset${counter}`
})
);
};
add(arr);
};
//initial variables
let svg, totalWidth, totalHeight, legend;
const setup = () => {
totalWidth = 100;
totalHeight = 100;
svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
};
const draw = (data) => {
//No.6 set lengend
legend = svg
.selectAll(".legend")
.data(data[0].values.map((d) => d.source));
console.log(data[0].values.map((d) => d.source));
legend.exit().remove();
let newLegend = legend.enter()
.append("g")
.attr("class", "legend")
.attr("transform", (d, i) => "translate(0," + i * 20 + ")");
newLegend
.append("text")
.datum((d) => d)
.attr("x", totalWidth)
.attr("y", 9)
.style("text-anchor", "end")
.on("click", function(e, d) {
jsonObj.forEach((i) => {
i.values = i.values.filter((s) => s.source != d);
});
newLegend
.filter((x) => x === d)
.style("text-decoration", "line-through");
});
legend = newLegend.merge(legend);
legend
.style("text-decoration", null)
.select("text")
.text((d) => d);
};
setup();
draw(jsonObj);
const update = () => {
add_set(jsonObj);
draw(jsonObj);
};
<script src="https://d3js.org/d3.v6.min.js"></script>
<button onclick="update()">Add dataset</button>

How to create curved lines connecting nodes in D3

I would like to create a graphic in D3 that consists of nodes connected to each other with curved lines. The lines should be curved differently depending on how far apart the start and end point of the line are.
For example (A) is a longer connection and therefore is less curved than (C).
Which D3 function is best used for this calculation and how is it output as SVG path
A code example (for example on observablehq.com) would help me a lot.
Here is a code example in obserbavlehq.com
https://observablehq.com/#garciaguillermoa/circles-and-links
I will try to explain it, let me know if there is something I am not clear enough:
Lets start with our circles, we use d3.pie() to position this circles, passing the data defined above, it will return us some arcs, but as we want circles instead of arcs, we use arc.centroid to get the coordinates of our circles
Value is required for the spacing in the pie layout that we use to calculate the position, if you want more circles, you will need to reduce the value, here is the related code:
pie = d3
.pie()
.sort(null)
.value((d) => {
return d.value;
});
arc = d3.arc().outerRadius(300).innerRadius(50);
data = [
{ id: 0, value: 10 },
{ id: 1, value: 10 },
{ id: 2, value: 10 },
{ id: 3, value: 10 },
{ id: 4, value: 10 },
{ id: 5, value: 10 },
{ id: 6, value: 10 },
{ id: 7, value: 10 },
{ id: 8, value: 10 },
{ id: 9, value: 10 },
];
const circles = [];
for(let item of pieData) {
const [x, y] = arc.centroid(item);
circles.push({x, y});
}
Now we can render the circles:
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const mainGroup = svg
.append("g")
.attr("id", "main")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Insert lines and circles groups, lines first so they are behind circles
const linesGroup = mainGroup.append("g").attr("id", "lines");
const circlesGroup = mainGroup.append("g").attr("id", "circles");
circlesGroup
.selectAll("circle")
.data(circles, (_, index) => index)
.join((enter) => {
enter
.append("circle")
.attr("id", (_, index) => {
return `circle-${index}`;
})
.attr("r", 20)
.attr("cx", (d) => {
return d.x;
})
.attr("cy", (d) => {
return d.y;
})
.style("stroke-width", "2px")
.style("stroke", "#000")
.style("fill", "#963cff");
});
Now we need to declare the links, we could do this with an array specifying the id of the source and destination (from and to). we use this to search each circle, get its coordinates (the source and destination of our links) and then create the links, in order to create them, we can use a path and the d3 method quadraticCurveTo, this function requires four parameters, the first two are "the control point" which defines our curve, we use 0, 0 as it is the center of our viz (it is the center because we used a translate in the parent group).
lines = [
{
from: 1,
to: 3,
},
{
from: 8,
to: 4,
},
];
for (let line of lines) {
const fromCircle = circles[line.from];
const toCircle = circles[line.to];
const fromP = { x: fromCircle.x, y: fromCircle.y };
const toP = { x: toCircle.x, y: toCircle.y };
const path = d3.path();
path.moveTo(fromP.x, fromP.y);
path.quadraticCurveTo(0, 0, toP.x, toP.y);
linesGroup
.append("path")
.style("fill", "none")
.style("stroke-width", "2px")
.style("stroke-dasharray", "10 10")
.style("stroke", "#000")
.attr("d", path);
}

Unexpected d3 v4 tree behaviour

The following d3.js (v4) interactive tree layout I've put together as a proof of concept for a user interface project is not behaving as expected. This is my first d3.js visualisation and I'm still getting my head around all the concepts.
Essentially, clicking any yellow node should generate two yellow child nodes (& links). This works fine when following a left to right, top to bottom click sequence, otherwise it displays unexpected behaviour.
It's probably easiest to run you through an example, so here's a snippet:
var data = {
source: {
type: 'dataSource',
name: 'Data Source',
silos: [
{ name: 'Silo 1', selected: true },
{ name: 'Silo 2', selected: false },
{ name: 'Silo 3', selected: false }
],
union: {
type: 'union',
name: 'Union',
count: null,
cardinalities: [
{ type: 'cardinality', positive: false, name: 'Falsey', count: 40, cardinalities: [] },
{ type: 'cardinality', positive: true, name: 'Truthy', count: 60, cardinalities: [] }
]
}
}
}
// global variables
var containerPadding = 20;
var container = d3.select('#container').style('padding', containerPadding + 'px'); // contains the structured search svg
var svg = container.select('svg'); // the canvas that displays the structured search
var group = svg.append('g'); // contains the tree elements (nodes & links)
var nodeWidth = 40, nodeHeight = 30, nodeCornerRadius = 3, verticalNodeSeparation = 150, transitionDuration = 600;
var tree = d3.tree().nodeSize([nodeWidth, nodeHeight]);
var source;
function nodeClicked(d) {
source = d;
switch (d.data.type) {
case 'dataSource':
// todo: show the data source popup and update the selected values
d.data.silos[0].selected = !d.data.silos[0].selected;
break;
default:
// todo: show the operation popup and update the selected values
if (d.data.cardinalities && d.data.cardinalities.length) {
d.data.cardinalities.splice(-2, 2);
}
else {
d.data.cardinalities.push({ type: 'cardinality', positive: false, name: 'F ' + (new Date()).getSeconds(), count: 40, cardinalities: [] });
d.data.cardinalities.push({ type: 'cardinality', positive: true, name: 'T ' + (new Date()).getSeconds(), count: 60, cardinalities: [] });
}
break;
}
render();
}
function renderLink(source, destination) {
var x = destination.x + nodeWidth / 2;
var y = destination.y;
var px = source.x + nodeWidth / 2;
var py = source.y + nodeHeight;
return 'M' + x + ',' + y
+ 'C' + x + ',' + (y + py) / 2
+ ' ' + x + ',' + (y + py) / 2
+ ' ' + px + ',' + py;
}
function render() {
// map the data source to a heirarchy that d3.tree requires
// d3.tree instance needs the data structured in a specific way to generate the required layout of nodes & links (lines)
var hierarchy = d3.hierarchy(data.source, function (d) {
switch (d.type) {
case 'dataSource':
return d.silos.some(function (e) { return e.selected; }) ? [d.union] : undefined;
default:
return d.cardinalities;
}
});
// set the layout parameters (all required for resizing)
var containerBoundingRect = container.node().getBoundingClientRect();
var width = containerBoundingRect.width - containerPadding * 2;
var height = verticalNodeSeparation * hierarchy.height;
svg.transition().duration(transitionDuration).attr('width', width).attr('height', height + nodeHeight);
tree.size([width - nodeWidth, height]);
// tree() assigns the (x, y) coords, depth, etc, to the nodes in the hierarchy
tree(hierarchy);
// get the descendants
var descendants = hierarchy.descendants();
// store previous position for transitioning
descendants.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// ensure source is set when rendering for the first time (hierarch is the root, same as descendants[0])
source = source || hierarchy;
// render nodes
var nodesUpdate = group.selectAll('.node').data(descendants);
var nodesEnter = nodesUpdate.enter()
.append('g')
.attr('class', 'node')
.attr('transform', 'translate(' + source.x0 + ',' + source.y0 + ')')
.style('opacity', 0)
.on('click', nodeClicked);
nodesEnter.append('rect')
.attr('rx', nodeCornerRadius)
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('class', function (d) { return 'box ' + d.data.type; });
nodesEnter.append('text')
.attr('dx', nodeWidth / 2 + 5)
.attr('dy', function (d) { return d.parent ? -5 : nodeHeight + 15; })
.text(function (d) { return d.data.name; });
nodesUpdate
.merge(nodesEnter)
.transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.style('opacity', 1);
nodesUpdate.exit().transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + source.x + ',' + source.y + ')'; })
.style('opacity', 0)
.remove();
// render links
var linksUpdate = group.selectAll('.link').data(descendants.slice(1));
var linksEnter = linksUpdate.enter()
.append('path')
.attr('class', 'link')
.classed('falsey', function (d) { return d.data.positive === false })
.classed('truthy', function (d) { return d.data.positive === true })
.attr('d', function (d) { var o = { x: source.x0, y: source.y0 }; return renderLink(o, o); })
.style('opacity', 0);
linksUpdate
.merge(linksEnter)
.transition().duration(transitionDuration)
.attr('d', function (d) { return renderLink({ x: d.parent.x, y: d.parent.y }, d); })
.style('opacity', 1);
linksUpdate.exit()
.transition().duration(transitionDuration)
.attr('d', function (d) { var o = { x: source.x, y: source.y }; return renderLink(o, o); })
.style('opacity', 0)
.remove();
}
window.addEventListener('resize', render); // todo: use requestAnimationFrame (RAF) for this
render();
.link {
fill:none;
stroke:#555;
stroke-opacity:0.4;
stroke-width:1.5px
}
.truthy {
stroke:green
}
.falsey {
stroke:red
}
.box {
stroke:black;
stroke-width:1;
cursor:pointer
}
.dataSource {
fill:blue
}
.union {
fill:orange
}
.cardinality {
fill:yellow
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="container" style="background-color:gray">
<svg style="background-color:#fff" width="0" height="0"></svg>
</div>
If you click on the Falsey node then the Truthy node, you'll see two child nodes appear beneath each, as expected. However, if you click on the Truthy node first, when you then click the Falsey node, you'll see that the Truthy child nodes move under Falsey, and the Falsey child nodes move under Truthy. Plus, the child nodes beneath Falsey and Truthy are actually the same two nodes, even though the underlying data is different.
I've confirmed that the data object is correctly structured after creating the children. From what I can see, the d3.hierarchy() and d3.tree() methods are working correctly, so I'm assuming that there's an issue with the way I'm constructing the selections.
Hopefully someone can spot the problem.
A second issue that may be related to the first is: Clicking Falsey or Truthy a second time should cause the child nodes (& links) to transition back to the parent node, but it does not track the parent's position. Hopefully someone can spot the issue here too.
Thanks!
It seems to me that you need a key function when you join your data:
If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on. A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index.
So, this should be your data binding selection:
var nodesUpdate = group.selectAll('.node')
.data(descendants, function(d){ return d.data.name});
Check the snippet:
var data = {
source: {
type: 'dataSource',
name: 'Data Source',
silos: [
{ name: 'Silo 1', selected: true },
{ name: 'Silo 2', selected: false },
{ name: 'Silo 3', selected: false }
],
union: {
type: 'union',
name: 'Union',
count: null,
cardinalities: [
{ type: 'cardinality', positive: false, name: 'Falsey', count: 40, cardinalities: [] },
{ type: 'cardinality', positive: true, name: 'Truthy', count: 60, cardinalities: [] }
]
}
}
}
// global variables
var containerPadding = 20;
var container = d3.select('#container').style('padding', containerPadding + 'px'); // contains the structured search svg
var svg = container.select('svg'); // the canvas that displays the structured search
var group = svg.append('g'); // contains the tree elements (nodes & links)
var nodeWidth = 40, nodeHeight = 30, nodeCornerRadius = 3, verticalNodeSeparation = 150, transitionDuration = 600;
var tree = d3.tree().nodeSize([nodeWidth, nodeHeight]);
var source;
function nodeClicked(d) {
source = d;
switch (d.data.type) {
case 'dataSource':
// todo: show the data source popup and update the selected values
d.data.silos[0].selected = !d.data.silos[0].selected;
break;
default:
// todo: show the operation popup and update the selected values
if (d.data.cardinalities && d.data.cardinalities.length) {
d.data.cardinalities.splice(-2, 2);
}
else {
d.data.cardinalities.push({ type: 'cardinality', positive: false, name: 'F ' + (new Date()).getSeconds(), count: 40, cardinalities: [] });
d.data.cardinalities.push({ type: 'cardinality', positive: true, name: 'T ' + (new Date()).getSeconds(), count: 60, cardinalities: [] });
}
break;
}
render();
}
function renderLink(source, destination) {
var x = destination.x + nodeWidth / 2;
var y = destination.y;
var px = source.x + nodeWidth / 2;
var py = source.y + nodeHeight;
return 'M' + x + ',' + y
+ 'C' + x + ',' + (y + py) / 2
+ ' ' + x + ',' + (y + py) / 2
+ ' ' + px + ',' + py;
}
function render() {
// map the data source to a heirarchy that d3.tree requires
// d3.tree instance needs the data structured in a specific way to generate the required layout of nodes & links (lines)
var hierarchy = d3.hierarchy(data.source, function (d) {
switch (d.type) {
case 'dataSource':
return d.silos.some(function (e) { return e.selected; }) ? [d.union] : undefined;
default:
return d.cardinalities;
}
});
// set the layout parameters (all required for resizing)
var containerBoundingRect = container.node().getBoundingClientRect();
var width = containerBoundingRect.width - containerPadding * 2;
var height = verticalNodeSeparation * hierarchy.height;
svg.transition().duration(transitionDuration).attr('width', width).attr('height', height + nodeHeight);
tree.size([width - nodeWidth, height]);
// tree() assigns the (x, y) coords, depth, etc, to the nodes in the hierarchy
tree(hierarchy);
// get the descendants
var descendants = hierarchy.descendants();
// store previous position for transitioning
descendants.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// ensure source is set when rendering for the first time (hierarch is the root, same as descendants[0])
source = source || hierarchy;
// render nodes
var nodesUpdate = group.selectAll('.node').data(descendants, function(d){ return d.data.name});
var nodesEnter = nodesUpdate.enter()
.append('g')
.attr('class', 'node')
.attr('transform', 'translate(' + source.x0 + ',' + source.y0 + ')')
.style('opacity', 0)
.on('click', nodeClicked);
nodesEnter.append('rect')
.attr('rx', nodeCornerRadius)
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('class', function (d) { return 'box ' + d.data.type; });
nodesEnter.append('text')
.attr('dx', nodeWidth / 2 + 5)
.attr('dy', function (d) { return d.parent ? -5 : nodeHeight + 15; })
.text(function (d) { return d.data.name; });
nodesUpdate
.merge(nodesEnter)
.transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.style('opacity', 1);
nodesUpdate.exit().transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + source.x + ',' + source.y + ')'; })
.style('opacity', 0)
.remove();
// render links
var linksUpdate = group.selectAll('.link').data(descendants.slice(1));
var linksEnter = linksUpdate.enter()
.append('path')
.attr('class', 'link')
.classed('falsey', function (d) { return d.data.positive === false })
.classed('truthy', function (d) { return d.data.positive === true })
.attr('d', function (d) { var o = { x: source.x0, y: source.y0 }; return renderLink(o, o); })
.style('opacity', 0);
linksUpdate
.merge(linksEnter)
.transition().duration(transitionDuration)
.attr('d', function (d) { return renderLink({ x: d.parent.x, y: d.parent.y }, d); })
.style('opacity', 1);
linksUpdate.exit()
.transition().duration(transitionDuration)
.attr('d', function (d) { var o = { x: source.x, y: source.y }; return renderLink(o, o); })
.style('opacity', 0)
.remove();
}
window.addEventListener('resize', render); // todo: use requestAnimationFrame (RAF) for this
render();
.link {
fill:none;
stroke:#555;
stroke-opacity:0.4;
stroke-width:1.5px
}
.truthy {
stroke:green
}
.falsey {
stroke:red
}
.box {
stroke:black;
stroke-width:1;
cursor:pointer
}
.dataSource {
fill:blue
}
.union {
fill:orange
}
.cardinality {
fill:yellow
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="container" style="background-color:gray">
<svg style="background-color:#fff" width="0" height="0"></svg>
</div>

d3.js: legends with dynamic element width

I am trying to replace a d3 legend drawing function which used manual creation and placement of the legend elements to one using .data().
I was able to get everything working, except one thing: with manually looping through the data array, I was able to ensure equal spacing between the legend elements by using a currentX tracker variable. With .data() however, I can't reference the other elements, and have to use fixed-width boxes, which the designer hates (and me too).
Given the code below, how do I make the new code (top row) behave exactly like the old code (bottom row)?
(Please do not suggest that I use a legend library, there is lots of event-handling and more stuff in the "real" code upon which this testcase was extracted)
var config = {
circleYCenter: 170,
circleXCenter: 150,
legendMarginTop: 52,
legendMarginInner: 18,
legendDotHeight: 8,
legendDotWidth: 16,
};
var colors = {
'lightyellow': '#FEE284',
'darkblue': '#2872A3',
'dirtyorange': '#E68406',
};
function drawLegend(container, map) {
var legendGroup = container.append("g").attr({
"transform": "translate(0,50)",
"class": "legendGroup"
});
//New code
var elGroup = legendGroup.selectAll("g").data(map).enter().append("g").attr({
transform: function(d, i) {
return "translate(" + (i * 100) + ",0)"
}
});
elGroup.append("text").text(function(d) {
return d.label;
}).attr({
x: config.legendDotWidth + config.legendMarginInner
});
elGroup.append("rect").attr({
x: 0,
width: config.legendDotWidth,
y: -5,
height: config.legendDotHeight,
"fill": function(d) {
return d.color;
}
});
//Old code
var currentX = 0;
map.forEach(function(el, key) {
var elGroup = legendGroup.append("g").attr("transform", "translate(" + currentX + ",100)");
elGroup.append("rect").attr({
x: 0,
width: config.legendDotWidth,
y: -5,
height: config.legendDotHeight,
"fill": el.color
});
elGroup.append("text").attr({
"x": (config.legendDotWidth + 10),
"y": 0,
"alignment-baseline": "middle"
}).text(el.label);
currentX += elGroup.node().getBBox().width + config.legendMarginInner;
});
}
drawLegend(d3.select("svg"), [{
label: "foo",
color: colors.dirtyorange
}, {
label: "Banana",
color: colors.darkblue
}, {
label: "baz",
color: colors.lightyellow
}]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width=600 height=300></svg>
First make the group:
var elGroup = legendGroup.selectAll("g").data(map).enter().append("g");
Append the text and rect DOM as you are doing:
elGroup.append("text").text(function(d) {
return d.label;
}).attr({
x: config.legendDotWidth + config.legendMarginInner
});
elGroup.append("rect").attr({
x: 0,
width: config.legendDotWidth,
y: -5,
height: config.legendDotHeight,
"fill": function(d) {
return d.color;
}
});
Now add the translate to the group using the bbox of the group.
var currentX = 0;
elGroup.attr({
transform: function(d, i) {
var ret = "translate(" + currentX + ",0)"
currentX += this.getBBox().width + config.legendMarginInner;
return ret;
}
});
working code here

d3.js gauge-like arc initialization and transition

I'd like your assistance in building a kind of 'gauge', an 'arc' based circle that would populate a value from 0 to 100 dynamically (8 gauges).
I was able to get this working (based on Arc Clock gist https://gist.github.com/mbostock/1098617), but now I'm trying to force a transition on every update and on the start.
I'm trying to implement the following flow:
1. arch loaded - goes from 0 to 100 -> from 100 to initial value
2. arch updated - goes from previous value to 0 -> from 0 to new value
Can't seem to find the right way to implement this...
The values are currently being inserted at random (10 increments)
var w = 1500,
h = 300,
x = d3.scale.ordinal().domain(d3.range(8)).rangePoints([0, w], 2);
var fields = [
{ name: "A", value: 100, previous: 0, size: 100 },
{ name: "B", value: 100, previous: 0, size: 100 },
{ name: "C", value: 100, previous: 0, size: 100 },
{ name: "D", value: 100, previous: 0, size: 100 },
{ name: "E", value: 100, previous: 0, size: 100 },
{ name: "F", value: 100, previous: 0, size: 100 },
{ name: "G", value: 100, previous: 0, size: 100 },
{ name: "H", value: 100, previous: 0, size: 100 }
];
var arc = d3.svg.arc()
.innerRadius(40)
.outerRadius(60)
.startAngle(0)
.endAngle(function (d) { return (d.value / d.size) * 2 * Math.PI; });
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(0," + (h / 2) + ")");
var path = svg.selectAll("path")
.data(fields.filter(function (d) { return d.value; }), function (d) { return d.name; })
.enter().append("svg:path")
.attr("transform", function (d, i) { return "translate(" + x(i) + ",0)"; })
.transition()
.ease("liniar")
.duration(750)
.attrTween("d", arcTween);
setTimeout(function () { services() }, 750);
setInterval(function () { services(); }, 5000);
function services() {
for (var i = 0; i < fields.length; i++) {
fields[i].previous = fields[i].value;
fields[i].value = Math.floor((Math.random() * 100) + 1);
}
path = svg.selectAll("path").data(fields.filter(function (d) { return d.value; }), function (d) { return d.name; });
path.transition()
.ease("linear")
.duration(1600)
.attrTween("d", arcTweenReversed);
}
function arcTween(b) {
var i = d3.interpolate({ value: b.previous }, b);
return function (t) {
return arc(i(t));
};
}
Here is JSFiddle to see it live: http://jsfiddle.net/p5xWZ/2/
Thanks in advance!
Something like the following chain transition could complete the arc and then go back to the next value:
path.transition()
.ease("linear")
.duration(function(d, i) { return 1600 * ((d.size-d.value)/d.size); })
.delay(function(d, i) { return i * 10; })
.attrTween("d", completeArc)
.transition()
.ease("linear")
.duration(function(d, i) { return 1600 * (d.value/d.size); })
.attrTween("d", resetArc)
.style("fill", function (d) { if (d.value < 100) { return "green"; } else { return "red" } });
Where completing the arc goes to 100, and resetting the arc goes from 0 to the next value:
function completeArc(b) {
// clone the data for the purposes of interpolation
var newb = $.extend({}, b);
// Set to 100
newb.value = newb.size;
var i = d3.interpolate({value: newb.previous}, newb);
return function(t) {
return arc(i(t));
};
}
function resetArc(b) {
var i = d3.interpolate({value: 0}, b);
return function(t) {
return arc(i(t));
};
}
Fiddle here also with fill color added.

Resources