Fix the position of one bubble in d3 pack layout - javascript

I'm building a bubble chart using D3 js (v5), to show some results of a research.
Basically I have a JSON with the total as the root and the children as each result.
One of the results must be highlighted and be positioned at the top center of the circle.
I managed to show this result there, but the other bubbles are overlapping (probably because I changed the position provided by the D3 hierarchy algorithm).
Is it possible to programmatically fix the position of only one bubble (the blue bubble in my example) and reposition the other bubbles, in order to not let them overlap each other?
JFiddle
const data = {
name: 'Total',
size: 1999999,
children: [{
name: 'Result A',
size: 69936,
},
{
name: 'Result b',
size: 45000,
},
{
name: 'Result C',
size: 250000,
},
{
name: 'Result D',
size: 426791,
},
{
name: 'Result E',
size: 56000,
},
{
name: 'Result F',
size: 61050,
},
{
name: 'Result G',
size: 30000,
},
],
};
// Fix this bubble at the top
const FIXED_BUBBLE_NAME = 'Result b';
const GREEN = '#90E0C2';
const BLUE = '#73A1FC';
const GRAPH_DIMENSIONS = {
WIDTH: 234,
HEIGHT: 234,
PADDING: 10,
};
const buildDataTree = () => {
const packLayout = d3
.pack()
.size([
GRAPH_DIMENSIONS.WIDTH,
GRAPH_DIMENSIONS.HEIGHT,
])
.padding(GRAPH_DIMENSIONS.PADDING);
const rootNode = d3
.hierarchy(data)
.sum((d) => d.size)
.sort((a, b) => {
return b.value - a.value;
})
return packLayout(rootNode);
};
const getSvgRoot = () => {
return d3
.select('#graph-container')
.append('svg')
.attr('id', 'graph-container-svg')
.attr('width', GRAPH_DIMENSIONS.WIDTH + GRAPH_DIMENSIONS.PADDING)
.attr('height', GRAPH_DIMENSIONS.HEIGHT + GRAPH_DIMENSIONS.PADDING)
.style('overflow', 'visible');
};
const rootNodeDataTree = buildDataTree();
const svgRoot = getSvgRoot();
const node = svgRoot
.selectAll('g')
.data(
d3
.nest()
.key((d) => d.id)
.entries(rootNodeDataTree.descendants()),
)
.join('g')
.selectAll('g')
.data(d => d.values)
.join('g');
node
.append('circle')
.attr('r', (d) => d.r)
.attr('cx', (d) => {
// if it is the selected bubble it must be at the center
if (d.data.name === FIXED_BUBBLE_NAME) {
return GRAPH_DIMENSIONS.WIDTH / 2 + d.r / 2;
}
return d.x + GRAPH_DIMENSIONS.PADDING;
})
.attr('cy', (d) => {
// if it is the selected bubble it must be at the center
if (d.data.name === FIXED_BUBBLE_NAME) {
return d.r + GRAPH_DIMENSIONS.PADDING * 2;
}
return d.y + GRAPH_DIMENSIONS.PADDING;
})
.attr('cursor', 'pointer')
.attr('stroke', (d) => (d.children ? GREEN : ''))
.attr('stroke-width', (d) => (d.children ? 2 : 0))
.attr('fill-opacity', (d) => (d.children ? 0.24 : 1))
.attr('fill', (d) => {
if (d.data.name === FIXED_BUBBLE_NAME) {
return BLUE;
} else {
return GREEN;
}
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="graph-container">
</div>

Here is a fiddle with my suggestion: https://jsfiddle.net/9r8kt1e0/1/
The force simulation code is:
const simulation = d3
.forceSimulation(allNodes.filter( (d) => d.depth !== 0))
.force("collide",d3.forceCollide(d => { return d.r;}))
.force("charge", d3.forceManyBody().strength(0))
.force("center", d3.forceCenter(GRAPH_DIMENSIONS.WIDTH/2+10, GRAPH_DIMENSIONS.HEIGHT/2))
.force("bound", (alpha) => {
for (let i = 0; i < allNodes.length; ++i) {
n = allNodes[i];
n.x = pythag(n.r, n.y, n.x);
n.y = pythag(n.r, n.x, n.y);
}
})
.stop();
for (var i = 0; i < 100; ++i) {
simulation.tick();
}
Notice that we are passing a custom force called bound, this one should take care of keeping the nodes inside the parent circle.
That solves the overlapping, you would have to play a little with the forces according to your needs.
As per the documentation, a force is a function that modifies the nodes' position, you can have multiple forces, so hopefully, playing a little bit with the forces should solve your problem.
I hope it helps.

Related

Links in d3.js tree are not being removed within Angular, but in vanilla JavaScript

I'm creating horizontal tree from nested array. After tree is created, the links from one node to another are regularly being updated, but the removal functionality is not working.
import { Component, OnInit } from '#angular/core';
import * as d3 from 'd3';
#Component({
selector: 'app-rtm-graph',
templateUrl: './rtm-graph.component.html',
styleUrls: ['./rtm-graph.component.css']
})
export class RtmGraphComponent implements OnInit {
private rtmData: {id:number, name: string, children: any[] } =
{ id: 0, name: 'RTM', children: [
{ id: 0, name: 'RTM', children: [
{ id: 0, name: 'RTM', children: []},
{ id: 0, name: 'RTM', children: []},
{ id: 0, name: 'RTM', children: []},
]},
{ id: 0, name: 'RTM', children: []},
] };
private svg: any;
private margin = 50;
private width = 750 - (this.margin * 2);
private height = 750 - (this.margin * 2);
private root: any;
private treemap = d3.tree().size([this.height, this.width])
private duration = 1124;
private i = 0;
constructor(public _rs:RootService) { }
ngOnInit(): void {
this.createSvg();
this.createTree()
}
private callSvg() {
return d3.select("figure#rtm-graph");
}
private createTree(): void {
this.root = d3.hierarchy(this.rtmData, (d) => {
return d.children;
})
this.root.x0 = this.height / 2;
this.root.y0 = 0;
this.update(this.root);
}
private createSvg(): void {
this.svg = this.callSvg()
.append("svg")
.attr("width", this.width + (this.margin * 2))
.attr("height", this.height + (this.margin * 2))
.append("g")
.attr("transform", `translate(${this.margin},${this.margin})`);
}
private update(source): void {
let treedata = this.treemap(this.root);
let nodes = treedata.descendants();
nodes.forEach(d => {
d.y = d.depth * this.width / 5;
});
let node = this.svg.selectAll("g.node").data(nodes, (d) => d.id || (d.id = ++this.i));
// links
let links = treedata.descendants().slice(1);
let link = this.svg.selectAll('path.link').data(links, (d) => {
return d.id;
})
let linkEnter = link
.enter()
.insert('path', 'g')
.attr('class', 'links')
.attr('d', (d) => {
let o = { x: source.x0, y: source.y0 + 40 }
return this.diagonal(o, o)
})
let linkUpdate = linkEnter.merge(link);
linkUpdate
.transition()
.duration(this.duration)
.attr("d", (d) => {
return this.diagonal(d, d.parent);
});
link
.exit()
.transition()
.attr('d', (d: any) => {
console.log("Inside link exit")
let o = { x: source.x0, y: source.y0 }
return this.diagonal(o, o);
})
.remove();
let nodeEnter = node
.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => {
return `translate(${source.y0 + 20},${source.x0})`
})
.on("click", this.clicked.bind(this))
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 0)
.style('fill', d => {
return d._children ? "red" : "white";
})
let nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition()
.duration(this.duration)
.attr("transform", d => `translate(${d.y + 20},${d.x})`)
.attr("opacity", 1)
nodeUpdate.select("circle.node")
.attr('r', 10)
.style("fill", d => d._children ? "red" : "black")
.attr("cursor", "pointer");
nodeUpdate.append('rect')
.attr('x', 0)
.attr('y', -20)
.attr('rx', 5)
.attr('ry', 5)
.attr('width', 80)
.attr('height', 40)
.attr('fill', 'grey')
.exit();
nodeUpdate.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('dx', 10)
.text(d => {
console.log(d.data.name)
return d.data.name;
});
let nodeExit = node.exit()
.transition()
.duration(this.duration)
.attr("transform", function () { return `translate(${source.y + 20},${source.x})` })
.attr("opacity", 0.5)
.remove();
// collapsing of the nodes
nodes.forEach(d => {
d.x0 = d.x;
d.y0 = d.y;
})
}
diagonal(s, d) {
let path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x}
${(s.y + d.y) / 2} ${d.x}
${d.y} ${d.x}`;
return path;
}
clicked(event, d) {
console.log('ddddd:::',d);
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
this.update(d);
}
}
If I add console.log("Inside link exit") to the code, then no log is written to the console. But if I run same code in vanilla JavaScript, then I get the expected output from this console.log.
For the first time, the graph is generated correctly.
but after collapsing nodes, the output is as follows:
How to remove this residual link?

D3 circle pack layout mouseover event is getting triggered multiple times

I have this circle pack layout using D3:
I have assigned mouseover and mouseout event on the circles in the screenshot, but am not able to figure out why mouseover event is being triggered multiple times for inner circle (for example A1, B1, etc..) ?
const data = {
name: "root",
children: [
{
name: "A",
children: [
{name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
]
},
{
name: "B",
children: [
{name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
]
},
{
name: "C",
value: 10
},
{
name: "D",
value: 10
},
{
name: "E",
value: 10
}
],
links: [{from: "A5", to: "B3"}, {from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
};
const cloneObj = item => {
if (!item) { return item; } // null, undefined values check
let types = [ Number, String, Boolean ],
result;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (item instanceof type) {
result = type( item );
}
});
if (typeof result == "undefined") {
if (Object.prototype.toString.call( item ) === "[object Array]") {
result = [];
item.forEach(function(child, index, array) {
result[index] = cloneObj( child );
});
} else if (typeof item == "object") {
// testing that this is DOM
if (item.nodeType && typeof item.cloneNode == "function") {
result = item.cloneNode( true );
} else if (!item.prototype) { // check that this is a literal
if (item instanceof Date) {
result = new Date(item);
} else {
// it is an object literal
result = {};
for (let i in item) {
result[i] = cloneObj( item[i] );
}
}
} else {
// depending what you would like here,
// just keep the reference, or create new object
if (false && item.constructor) {
// would not advice to do that, reason? Read below
result = new item.constructor();
} else {
result = item;
}
}
} else {
result = item;
}
}
return result;
}
const findNode = (parent, name) => {
if (parent.name === name)
return parent;
if (parent.children) {
for (let child of parent.children) {
const found = findNode(child, name);
if (found) {
return found;
}
}
}
return null;
}
const findNodeAncestors = (parent, name) => {
if (parent.name === name)
return [parent];
const children = parent.children || parent._children;
if (children) {
for (let child of children) {
const found = findNodeAncestors(child, name);
//console.log('FOUND: ', found);
if (found) {
return [...found, parent];
}
}
}
return null;
}
const svg = d3.select("svg");
// This is for tooltip
const Tooltip = d3.select("body").append("div")
.attr("class", "tooltip-menu")
.style("opacity", 0);
const onMouseover = (e,d )=> {
console.log('d -->>', d);
e.stopPropagation();
Tooltip.style("opacity", 1);
let html = `<span>
Hi
</span>`;
Tooltip.html(html)
.style("left", (e.pageX + 10) + "px")
.style("top", (e.pageY - 15) + "px");
}
const onMouseout = (e,d ) => {
Tooltip.style("opacity", 0)
}
const container = svg.append('g')
.attr('transform', 'translate(0,0)')
const onClickNode = (e, d) => {
e.stopPropagation();
e.preventDefault();
const node = findNode(data, d.data.name);
if(node.children && !node._children) {
/*node._children = node.children;*/
node._children = cloneObj(node.children);
node.children = undefined;
node.value = 20;
updateGraph(data);
} else {
if (node._children && !node.children) {
//node.children = node._children;
node.children = cloneObj(node._children);
node._children = undefined;
node.value = undefined;
updateGraph(data);
}
}
}
const updateGraph = graphData => {
const pack = data => d3.pack()
.size([600, 600])
.padding(0)
(d3.hierarchy(data)
.sum(d => d.value * 3.5)
.sort((a, b) => b.value - a.value));
const root = pack(graphData);
const nodes = root.descendants().slice(1);
const nodeElements = container
.selectAll("g.node")
.data(nodes, d => d.data.name);
const addedNodes = nodeElements.enter()
.append("g")
.classed('node', true)
.style('cursor', 'pointer')
.on('click', (e, d) => onClickNode(e, d))
.on('mouseover',(e, d) => onMouseover(e, d))
.on('mouseout', (e, d) => onMouseout(e, d));
addedNodes.append('circle')
.attr('stroke', 'black')
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.style('visibility', 'hidden')
.style('fill', 'black');
const mergedNodes = addedNodes.merge(nodeElements);
mergedNodes
.transition()
.duration(500)
.attr('transform', d => `translate(${d.x},${d.y})`);
mergedNodes.select('circle')
.attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
.transition()
.duration(1000)
.attr('r', d => d.value)
mergedNodes.select('text')
.attr('dy', d => d.children ? d.value + 10 : 0)
.transition()
.delay(1000)
.style('visibility', 'visible');
const exitedNodes = nodeElements.exit()
exitedNodes.select('circle')
.transition()
.duration(500)
.attr('r', 1);
exitedNodes.select('text')
.remove();
exitedNodes
.transition()
.duration(750)
.remove();
const linkPath = d => {
let length = Math.hypot(d.from.x - d.to.x, d.from.y - d.to.y);
if(length == 0 ) {
return ''; // This means its a connection inside collapsed node
}
const fd = d.from.value / length;
const fx = d.from.x + (d.to.x - d.from.x) * fd;
const fy = d.from.y + (d.to.y - d.from.y) * fd;
const td = d.to.value / length;
const tx = d.to.x + (d.from.x - d.to.x) * td;
const ty = d.to.y + (d.from.y - d.to.y) * td;
return `M ${fx},${fy} L ${tx},${ty}`;
};
const links = data.links.map(link => {
let from = nodes.find(n => n.data.name === link.from);
if (!from) {
const ancestors = findNodeAncestors(data, link.from);
for (let index = 1; !from && index < ancestors.length -1; index++) {
from = nodes.find(n => n.data.name === ancestors[index].name)
}
}
let to = nodes.find(n => n.data.name === link.to);
if (!to) {
const ancestors = findNodeAncestors(data, link.to);
for (let index = 1; !to && index < ancestors.length -1; index++) {
to = nodes.find(n => n.data.name === ancestors[index].name)
}
}
return {from, to};
});
const linkElements = container.selectAll('path.link')
.data(links.filter(l => l.from && l.to));
const addedLinks = linkElements.enter()
.append('path')
.classed('link', true)
.attr('marker-end', 'url(#arrowhead-to)')
.attr('marker-start', 'url(#arrowhead-from)');
addedLinks.merge(linkElements)
.style('visibility', 'hidden')
.transition()
.delay(750)
.attr('d', linkPath)
.style('visibility', 'visible')
linkElements.exit().remove();
}
updateGraph(data);
text {
font-family: "Ubuntu";
font-size: 12px;
}
.link {
stroke: blue;
fill: none;
}
div.tooltip-menu {
position: absolute;
text-align: center;
padding: .5rem;
background: #FFFFFF;
color: #313639;
border: 1px solid #313639;
border-radius: 8px;
pointer-events: none;
font-size: 1.3rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<svg width="600" height="600">
<defs>
<marker id="arrowhead-to" markerWidth="10" markerHeight="7"
refX="10" refY="3.5" orient="auto">
<polygon fill="blue" points="0 0, 10 3.5, 0 7" />
</marker>
<marker id="arrowhead-from" markerWidth="10" markerHeight="7"
refX="0" refY="3.5" orient="auto">
<polygon fill="blue" points="10 0, 0 3.5, 10 7" />
</marker>
</defs>
</svg>
Each inner circle is a <g> with a <circle> and <text> within it. Even though you attach the mouse events to the <g>, the mouseover event fires as you pass over the <text> element in the middle of the circle, causing the tooltip to move as your mouse around.
You can add .attr('pointer-events', 'none') to the <text> elements to prevent this:
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('pointer-events', 'none') // <---- HERE
.style('visibility', 'hidden')
.style('fill', 'black');
Example below:
const data = {
name: "root",
children: [
{
name: "A",
children: [
{name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
]
},
{
name: "B",
children: [
{name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
]
},
{
name: "C",
value: 10
},
{
name: "D",
value: 10
},
{
name: "E",
value: 10
}
],
links: [{from: "A5", to: "B3"}, {from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
};
const cloneObj = item => {
if (!item) { return item; } // null, undefined values check
let types = [ Number, String, Boolean ],
result;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (item instanceof type) {
result = type( item );
}
});
if (typeof result == "undefined") {
if (Object.prototype.toString.call( item ) === "[object Array]") {
result = [];
item.forEach(function(child, index, array) {
result[index] = cloneObj( child );
});
} else if (typeof item == "object") {
// testing that this is DOM
if (item.nodeType && typeof item.cloneNode == "function") {
result = item.cloneNode( true );
} else if (!item.prototype) { // check that this is a literal
if (item instanceof Date) {
result = new Date(item);
} else {
// it is an object literal
result = {};
for (let i in item) {
result[i] = cloneObj( item[i] );
}
}
} else {
// depending what you would like here,
// just keep the reference, or create new object
if (false && item.constructor) {
// would not advice to do that, reason? Read below
result = new item.constructor();
} else {
result = item;
}
}
} else {
result = item;
}
}
return result;
}
const findNode = (parent, name) => {
if (parent.name === name)
return parent;
if (parent.children) {
for (let child of parent.children) {
const found = findNode(child, name);
if (found) {
return found;
}
}
}
return null;
}
const findNodeAncestors = (parent, name) => {
if (parent.name === name)
return [parent];
const children = parent.children || parent._children;
if (children) {
for (let child of children) {
const found = findNodeAncestors(child, name);
//console.log('FOUND: ', found);
if (found) {
return [...found, parent];
}
}
}
return null;
}
const svg = d3.select("svg");
// This is for tooltip
const Tooltip = d3.select("body").append("div")
.attr("class", "tooltip-menu")
.style("opacity", 0);
const onMouseover = (e,d )=> {
console.log('d -->>', d);
e.stopPropagation();
Tooltip.style("opacity", 1);
let html = `<span>
Hi ${d.data.name}
</span>`;
Tooltip.html(html)
.style("left", (e.pageX + 10) + "px")
.style("top", (e.pageY - 15) + "px");
}
const onMouseout = (e,d ) => {
Tooltip.style("opacity", 0)
}
const container = svg.append('g')
.attr('transform', 'translate(0,0)')
const onClickNode = (e, d) => {
e.stopPropagation();
e.preventDefault();
const node = findNode(data, d.data.name);
if(node.children && !node._children) {
/*node._children = node.children;*/
node._children = cloneObj(node.children);
node.children = undefined;
node.value = 20;
updateGraph(data);
} else {
if (node._children && !node.children) {
//node.children = node._children;
node.children = cloneObj(node._children);
node._children = undefined;
node.value = undefined;
updateGraph(data);
}
}
}
const updateGraph = graphData => {
const pack = data => d3.pack()
.size([600, 600])
.padding(0)
(d3.hierarchy(data)
.sum(d => d.value * 3.5)
.sort((a, b) => b.value - a.value));
const root = pack(graphData);
const nodes = root.descendants().slice(1);
const nodeElements = container
.selectAll("g.node")
.data(nodes, d => d.data.name);
const addedNodes = nodeElements.enter()
.append("g")
.classed('node', true)
.style('cursor', 'pointer')
.on('click', (e, d) => onClickNode(e, d))
.on('mouseover',(e, d) => onMouseover(e, d))
.on('mouseout', (e, d) => onMouseout(e, d));
addedNodes.append('circle')
.attr('stroke', 'black')
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('pointer-events', 'none')
.style('visibility', 'hidden')
.style('fill', 'black');
const mergedNodes = addedNodes.merge(nodeElements);
mergedNodes
.transition()
.duration(500)
.attr('transform', d => `translate(${d.x},${d.y})`);
mergedNodes.select('circle')
.attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
.transition()
.duration(1000)
.attr('r', d => d.value)
mergedNodes.select('text')
.attr('dy', d => d.children ? d.value + 10 : 0)
.transition()
.delay(1000)
.style('visibility', 'visible');
const exitedNodes = nodeElements.exit()
exitedNodes.select('circle')
.transition()
.duration(500)
.attr('r', 1);
exitedNodes.select('text')
.remove();
exitedNodes
.transition()
.duration(750)
.remove();
const linkPath = d => {
let length = Math.hypot(d.from.x - d.to.x, d.from.y - d.to.y);
if(length == 0 ) {
return ''; // This means its a connection inside collapsed node
}
const fd = d.from.value / length;
const fx = d.from.x + (d.to.x - d.from.x) * fd;
const fy = d.from.y + (d.to.y - d.from.y) * fd;
const td = d.to.value / length;
const tx = d.to.x + (d.from.x - d.to.x) * td;
const ty = d.to.y + (d.from.y - d.to.y) * td;
return `M ${fx},${fy} L ${tx},${ty}`;
};
const links = data.links.map(link => {
let from = nodes.find(n => n.data.name === link.from);
if (!from) {
const ancestors = findNodeAncestors(data, link.from);
for (let index = 1; !from && index < ancestors.length -1; index++) {
from = nodes.find(n => n.data.name === ancestors[index].name)
}
}
let to = nodes.find(n => n.data.name === link.to);
if (!to) {
const ancestors = findNodeAncestors(data, link.to);
for (let index = 1; !to && index < ancestors.length -1; index++) {
to = nodes.find(n => n.data.name === ancestors[index].name)
}
}
return {from, to};
});
const linkElements = container.selectAll('path.link')
.data(links.filter(l => l.from && l.to));
const addedLinks = linkElements.enter()
.append('path')
.classed('link', true)
.attr('marker-end', 'url(#arrowhead-to)')
.attr('marker-start', 'url(#arrowhead-from)');
addedLinks.merge(linkElements)
.style('visibility', 'hidden')
.transition()
.delay(750)
.attr('d', linkPath)
.style('visibility', 'visible')
linkElements.exit().remove();
}
updateGraph(data);
text {
font-family: "Ubuntu";
font-size: 12px;
}
.link {
stroke: blue;
fill: none;
}
div.tooltip-menu {
position: absolute;
text-align: center;
padding: .5rem;
background: #FFFFFF;
color: #313639;
border: 1px solid #313639;
border-radius: 8px;
pointer-events: none;
font-size: 1.3rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<svg width="600" height="600">
<defs>
<marker id="arrowhead-to" markerWidth="10" markerHeight="7"
refX="10" refY="3.5" orient="auto">
<polygon fill="blue" points="0 0, 10 3.5, 0 7" />
</marker>
<marker id="arrowhead-from" markerWidth="10" markerHeight="7"
refX="0" refY="3.5" orient="auto">
<polygon fill="blue" points="10 0, 0 3.5, 10 7" />
</marker>
</defs>
</svg>

Updating Pie Chart with Specific colors not working as expected

Looking at this fiddle (http://jsfiddle.net/gs6rehnx/2106/), there are four arcs with four different colors. I expect the Pie Chart to have three colors after clicking the update button. However, there are still four arcs. I assue, the mapping of the specific colors to the values is not working properly? Or is something else not working?
const chart = {};
const duration = 750;
const width = 160;
const height = 160;
const min = Math.min(width, height);
const oRadius = min / 2 * 0.9;
const iRadius = min / 2.5 * 0.85;
const pie = d3
.pie()
.value(function(d) {
return d.value;
})
.sort(null);
const arc = d3
.arc()
.outerRadius(oRadius)
.innerRadius(iRadius);
function arcTween(a) {
const i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
};
const labels = ['1', '2', '3', '4'];
const color = ["rgba(126,211,33,1)", "rgba(39,173,232,1)", "rgba(229,5,1,1)", "rgba(245,166,35,1)"];
const scale = d3.scaleOrdinal()
.domain(labels)
.range(color);
const create = function(data) {
const svg = d3
.select('.foo')
.append('svg')
.attr('class', 'pie')
.attr('width', width)
.attr('height', height)
.attr('id', 'svgClass');
svg
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
.attr('id', 'bar');
draw(data);
}
const draw = function(data) {
const path = d3.select('#bar')
.selectAll('path')
.data(pie(data))
path
.enter()
.append('g')
.append('path')
.attr('d', arc)
.attr('fill', (d, i) => {
return scale(d.data.name)
});
path
.transition()
.duration(duration)
.attrTween('d', function(d) {
const interpolate = d3.interpolate({
startAngle: 0,
endAngle: 0
}, d);
return function(t) {
return arc(interpolate(t));
};
});
};
const data = [{
"name": "1",
"value": 2
}, {
"name": "2",
"value": 1
}, {
"name": "3",
"value": 2
}, {
"name": "4",
"value": 1
}];
const newData = [{
"name": "1",
"value": 2
}, {
"name": "2",
"value": 1
}, {
"name": "3",
"value": 2
}];
function createPie() {
create(data)
}
function updatePie() {
draw(newData)
}
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<button type="button" onclick="createPie()">Click Me First!</button>
<button type="button" onclick="updatePie()">Update Diagram!</button>
<div class='foo'></div>
The reason this isn't working is because d3 has three main parts to a selection:
enter
update
exit
In your example above you're dealing with the enter, the update isn't quite right and the exit is completely missing. This is a really good article from the author discussing data joins.
Enter
The following grabs all the new data elements, and adds a new g element for each one.
path.enter().append('g')
Exit
The following which you're missing will take all the items in the DOM that are no longer represented in the data, and remove them.
path.exit().remove();
const chart = {};
const duration = 750;
const width = 160;
const height = 160;
const min = Math.min(width, height);
const oRadius = min / 2 * 0.9;
const iRadius = min / 2.5 * 0.85;
const pie = d3
.pie()
.value(function(d) {
return d.value;
})
.sort(null);
const arc = d3
.arc()
.outerRadius(oRadius)
.innerRadius(iRadius);
function arcTween(a) {
const i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
};
const labels = ['1', '2', '3', '4'];
const color = ["rgba(126,211,33,1)", "rgba(39,173,232,1)", "rgba(229,5,1,1)", "rgba(245,166,35,1)"];
const scale = d3.scaleOrdinal()
.domain(labels)
.range(color);
const create = function(data) {
const svg = d3
.select('.foo')
.append('svg')
.attr('class', 'pie')
.attr('width', width)
.attr('height', height)
.attr('id', 'svgClass');
svg
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
.attr('id', 'bar');
draw(data);
}
const draw = function(data) {
const path = d3.select('#bar')
.selectAll('path')
.data(pie(data))
path
.enter()
.append('g')
.append('path')
.attr('d', arc)
.attr('fill', (d, i) => {
return scale(d.data.name)
});
path.exit().remove();
path
.transition()
.duration(duration)
.attrTween('d', function(d) {
const interpolate = d3.interpolate({
startAngle: 0,
endAngle: 0
}, d);
return function(t) {
return arc(interpolate(t));
};
});
};
const data = [{
"name": "1",
"value": 2
}, {
"name": "2",
"value": 1
}, {
"name": "3",
"value": 2
}, {
"name": "4",
"value": 1
}];
const newData = [{
"name": "1",
"value": 2
}, {
"name": "2",
"value": 1
}, {
"name": "3",
"value": 2
}];
function createPie() {
create(data)
}
function updatePie() {
draw(newData)
}
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<button type="button" onclick="createPie()">Click Me First!</button>
<button type="button" onclick="updatePie()">Update Diagram!</button>
<div class='foo'></div>

How to use D3 from external file

I am trying to create a simple tree map with D3.js but want to include the d3 code from an external file. I have 2 files, treemap.js and treemap.html. However, the treemap is not displaying and there is no errors popping up in the console. Am I referencing the d3 file correctly? Thank you.
This is the code inside the treemap.js
var tree = {
name: "tree",
children: [
{ name: "Word-wrapping comes for free in HTML", size: 16000 },
{ name: "animate makes things fun", size: 8000 },
{ name: "data data everywhere...", size: 5220 },
{ name: "display something beautiful", size: 3623 },
{ name: "flex your muscles", size: 984 },
{ name: "physics is religion", size: 6410 },
{ name: "query and you get the answer", size: 2124 }
]
};
var width = innerWidth-40,
height = innerHeight-40,
color = d3.scale.category20c(),
div = d3.select("#treemap").append("div")
.style("position", "relative");
var treemap = d3.layout.treemap()
.size([width, height])
.sticky(true)
.value(function(d) { return d.size; });
var node = div.datum(tree).selectAll(".node")
.data(treemap.nodes)
.enter().append("div")
.attr("class", "node")
.call(position)
.style("background-color", function(d) {
return d.name == 'tree' ? '#fff' : color(d.name); })
.append('div')
.style("font-size", function(d) {
// compute font size based on sqrt(area)
return Math.max(20, 0.18*Math.sqrt(d.area))+'px'; })
.text(function(d) { return d.children ? null : d.name; });
function position() {
this.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
.style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
}
And this is the code inside the body tag inside the treemap.html
<body>
<div id="treemap"></div>
<script src="d3.v3.js"></script>
<script src="treemap.js"></script>
</body>

d3 transition with percentages using styleTween

I have a treemap rendered with d3. Since I want to be responsive and economical (not running js if I do not really have to) I am using percentages for the divs. But the transitions are some kind of wired using percentages. After reading this issue I have tried several styleTweens but I do not have any luck ...
How can I use transitions for percentage values in d3?
Here is a fiddle of the below code: http://jsfiddle.net/0z7p68wb/ (just click somewhere on the treemap to start the animation)
var target = d3.select("#target")
render = function(data, oldData) {
// our custom d3 code
console.log("render!", data, oldData);
// draw rectangles
var margin = {margin: 0.2, padding: 2},
width = 100 - margin.margin * 2,
height = 100 - margin.margin * 2;
var treemap = d3.layout.treemap()
.size([100, 100])
//.sticky(true)
.value(function(d) { return d.size; });
// bind data
var nodes = target.datum(data)
.selectAll(".node")
.data(treemap.nodes);
// transform existing nodes
if (data !== oldData)
nodes.transition()
.duration(1500)
.call(position);
// append new nodes
nodes.enter().append("div")
.attr("class", "node")
.style("position", "absolute")
.style("display", function(d,i) { return i==0 ? "none" : "block"})
.style("background-color", "silver")
.call(position)
;
// remove obsolete nodes
nodes.exit().remove();
// set position of nodes
function position() {
this.style("left", function(d) { return d.x + "%"; })
.style("top", function(d) { return d.y + "%"; })
.style("width", function(d) { return Math.max(0, d.dx) + "%"; })
.style("height", function(d) { return Math.max(0, d.dy) + "%"; })
}
}
tree1 = {
name: "tree",
children: [
{ name: "Word-wrapping comes for free in HTML", size: 16000 },
{ name: "animate makes things fun", size: 8000 },
{ name: "data data everywhere...", size: 5220 },
{ name: "display something beautiful", size: 3623 },
{ name: "flex your muscles", size: 984 },
{ name: "physics is religion", size: 6410 },
{ name: "query and you get the answer", size: 2124 }
]
};
tree2 = {
name: "tree",
children: [
{ name: "Word-wrapping comes for free in HTML", size: 8000 },
{ name: "animate makes things fun", size: 10000 },
{ name: "data data everywhere...", size: 2220 },
{ name: "display something beautiful", size: 6623 },
{ name: "flex your muscles", size: 1984 },
{ name: "physics is religion", size: 3410 },
{ name: "query and you get the answer", size: 2124 }
]
};
tree = tree1;
render(tree, tree);
d3.select("#target").on("click", function(){
console.log("click");
tree = tree == tree1 ? tree2 : tree1;
render(tree, {});
});
Got it!
// transform existing nodes
if (data !== oldData)
nodes.transition()
.duration(1500)
.call(position)
.styleTween('left', function(d,i,a){
return d3.interpolateString(this.style.left, d.x + "%")
})
.styleTween('top', function(d,i,a){;
return d3.interpolateString(this.style.top, d.y + "%")
})
.styleTween('width', function(d,i,a){;
return d3.interpolateString(this.style.width, Math.max(0, d.dx) + "%")
})
.styleTween('height', function(d,i,a){;
return d3.interpolateString(this.style.height, Math.max(0, d.dy) + "%")
})
;

Categories