Related
I've been trying to learn how to create a Norse Mythology family tree using D3 and was able to find some code that someone wrote on here that had the perfect layout of the Norse gods -> (found here)
I really like the way that this looks, but the links between parents and children get a little muddled. Is there a way to have the color of the links have an ordinal color scale and have the links be at a right angle?
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var levels = [
[{ id: 'Ymir' },
{id: 'Auðumbla'}
],
[
{ id: 'Fárbauti ',parents:['Ymir'], info:"pronounce like this" },
{ id: 'Laufey',parents:['Ymir']},
{ id: 'Ægir', parents:['Ymir']},
{ id: 'Rán', parents:['Ymir']},
{id:'Búri', parents:['Auðumbla']}
],
[
{ id: 'Loki', parents:['Fárbauti ', 'Laufey']},
{id: 'A Horse'},
{ id: 'Bára', parents:['Ægir','Rán']},
{ id: 'Blóðughadda', parents:['Ægir','Rán']},
{ id: 'Bylgja', parents:['Ægir','Rán']},
{ id: 'Dúfa', parents:['Ægir','Rán']},
{ id: 'Hefring', parents:['Ægir','Rán']},
{ id: 'Himinglæva', parents:['Ægir','Rán']},
{ id: 'Angrboða', parents: ['Ymir'] },
{ id: 'Hrǫnn', parents:['Ægir','Rán']},
{ id: 'Kólga', parents:['Ægir','Rán']},
{ id: 'Unnr', parents:['Ægir','Rán']},
{id: 'Bestla'},
{id:'Burr', parents:['Búri']},
{id:'Fjörgyn'},
],
[
{ id: 'Fenrir', parents:['Angrboða','Loki']},
{ id:'Jörmungandr', parents:['Angrboða','Loki']},
{ id:'Hel', parents:['Angrboða','Loki']},
{ id:'Sleipnir', parents:['Loki','A Horse']},
{id:'Jörd'},
{id: 'Heimdall', parents:['Bára','Blóðughadda','Bylgja','Dúfa','Hefring','Himinglæva','Hrǫnn','Kólga','Unnr']},
{id:'Hœnir', parents:['Bestla','Burr']},
{id:'Odin', parents:['Bestla','Burr']},
{id:'Vili', parents:['Bestla','Burr']},
{id:'Vé', parents:['Bestla','Burr']},
{id:'Frigg', parents:['Fjörgyn']}
],
[
{id: 'Njörds sister'},
{id:'Njörd'},
{id:'Járnsaxa'},
{id:'Sif'},
{id:'Thor', parents:['Odin','Jörd']},
{id:'Höðr', parents:['Odin', 'Frigg']},
{id:'Bragi', parents:['Odin']},
{id:'Baldur',parents:['Odin','Frigg']},
{id:'Nanna'}
],
[
{id:'Freyr', parents:['Njörd', 'Njörds sister']},
{id:'Freya', parents:['Njörd', 'Njörds sister']},
{id:'Magni', parents:['Thor','Járnsaxa']},
{id:'Thrúd', parents:['Thor','Sif']},
{id:'Módi', parents:['Thor','Sif']},
{id:'Ullr', parents:['Sif','Odin']},
{id:'Forseti', parents:['Baldur', 'Nanna']}
]
]
// precompute level depth
levels.forEach((l, i) => l.forEach(n => n.level = i));
var nodes = levels.reduce(((a, x) => a.concat(x)), []);
var nodes_index = {};
nodes.forEach(d => nodes_index[d.id] = d);
// objectification
nodes.forEach(d => {
d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
})
// precompute bundles
levels.forEach((l, i) => {
var index = {}
l.forEach(n => {
if (n.parents.length == 0) {
return
}
var id = n.parents.map(d => d.id).sort().join('--')
if (id in index) {
index[id].parents = index[id].parents.concat(n.parents)
} else {
index[id] = {
id: id,
parents: n.parents.slice(),
level: i
}
}
n.bundle = index[id]
})
l.bundles = Object.keys(index).map(k => index[k])
l.bundles.forEach((b, i) => b.i = i)
})
var links = []
nodes.forEach(d => {
d.parents.forEach(p => links.push({
source: d,
bundle: d.bundle,
target: p
}))
})
var bundles = levels.reduce(((a, x) => a.concat(x.bundles)), [])
// reverse pointer from parent to bundles
bundles.forEach(b => b.parents.forEach(p => {
if (p.bundles_index === undefined) {
p.bundles_index = {}
}
if (!(b.id in p.bundles_index)) {
p.bundles_index[b.id] = []
}
p.bundles_index[b.id].push(b)
}))
nodes.forEach(n => {
if (n.bundles_index !== undefined) {
n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
} else {
n.bundles_index = {}
n.bundles = []
}
n.bundles.forEach((b, i) => b.i = i)
})
links.forEach(l => {
if (l.bundle.links === undefined) {
l.bundle.links = []
}
l.bundle.links.push(l)
})
// layout
const padding = 8
const node_height = 22
const node_width = 70
const bundle_width = 14
const level_y_padding = 16
const metro_d = 4
const c = 16
const min_family_height = 16
nodes.forEach(n => n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)
var x_offset = padding
var y_offset = padding
levels.forEach(l => {
x_offset += l.bundles.length * bundle_width
y_offset += level_y_padding
l.forEach((n, i) => {
n.x = n.level * node_width + x_offset
n.y = node_height + y_offset + n.height / 2
y_offset += node_height + n.height
})
})
var i = 0
levels.forEach(l => {
l.bundles.forEach(b => {
b.x = b.parents[0].x + node_width + (l.bundles.length - 1 - b.i) * bundle_width
b.y = i * node_height
})
i += l.length
})
links.forEach(l => {
l.xt = l.target.x
l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
l.xb = l.bundle.x
l.xs = l.source.x
l.ys = l.source.y
})
// compress vertical space
var y_negative_offset = 0
levels.forEach(l => {
y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys - c) - (link.yt + c))) || 0
l.forEach(n => n.y -= y_negative_offset)
})
// very ugly, I know
links.forEach(l => {
l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
l.ys = l.source.y
l.c1 = l.source.level - l.target.level > 1 ? node_width + c : c
l.c2 = c
})
const cluster = d3.cluster()
.size([width, height]);
const root = d3.hierarchy(links);
cluster(root);
let oValues = Object.values(root)[0];
let linkks = oValues.map(x => x.bundle.links);
linkks.forEach((linkk) => {
let nodeG1 = svg.append("g")
.selectAll("circle")
.data(linkk)
.join("circle")
.attr("cx", d => d.target.x)
.attr("cy", d => d.target.y)
.attr("fill", "none")
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 1)))).toString(16);
})
.attr("r", 6);
let nodeG11 = svg.append("g")
.selectAll("circle")
.data(linkk)
.join("circle")
.attr("cx", d => d.source.x)
.attr("cy", d => d.source.y)
.attr("fill", "none")
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
})
.attr("r", 6);
let nodeG2 = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 14)
.selectAll("text")
.data(linkk)
.join("text")
.attr("class", "text")
.attr("x", d => d.target.x + padding)
.attr("y", d => d.target.y)
.text(d => d.target.id )
.attr("fill", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 2)))).toString(16);
});
let nodeG22 = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 14)
.selectAll("text")
.data(linkk)
.join("text")
.attr("class", "text")
.attr("x", d => d.source.x + padding)
.attr("y", d => d.source.y)
.text(d => d.source.id )
.attr("fill", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
});
let nodeG = svg.append('g')
.attr('class', 'node')
.selectAll("path")
.data(linkk)
.join('path')
.attr("class", "link")
.attr("d", d3.linkHorizontal()
.source(d => [d.xs, d.ys])
.target(d => [d.xt, d.yt]))
.attr("fill", "none")
.attr("stroke-opacity", 0.325)
.attr("stroke-width", 0.75)
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (4 * parseInt(d.source.level)))).toString(16);
});
});
path {
display: block;
z-index: 0;
}
text,
circle {
display: block;
z-index: 1000;
}
<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="main.css"/>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
<div id ="Norse"></div>
<!-- Version 7 is the latest version of D3. -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<!-- Our visualization data will be in this '.js' file -->
<script src="main_viz.js"></script>
</div>
</body>
</html>
This is the example that I'm basing the preferred design off of:
My d3 graph is looking great - it's beautiful apart from one thing - the x position of my circles does not seem to correspond very closely with the x values given in my array.
I have an array of objects which are like this:
{
x: 2020,
cx: 0,
colour: "#F25F5C",
y1: 0,
y2: 200,
rad: 10,
amt: 5000
},
I have two sets of circles. The first is positioned really close to the xaxis and indicates an exact date, and their colours match those of larger circles, whose radii vary across a larger scale and I apply d3.force to these to position them more effectively.
The trouble is that the larger circles should be positioned with their centres close to values in the array given by (x, y2). They are not though, and my forceX doesn't seem to be working.
Here is my complete code:
import React, {useState, useEffect} from 'react';
import * as d3 from 'd3';
import './App.css';
function App() {
const [maxYear, setMaxYear] = useState();
const [data, setData] = useState(
[{
x: 2020,
colour: "#69306D",
y1: 0,
y2: 50,
rad: 10,
amt: 2000
},
{
x: 2020,
colour: "#247BA0",
y1: 0,
y2: 100,
rad: 10,
amt: 5000
},
{
x: 2020,
colour: "#3F762C",
y1: 0,
y2: 150,
rad: 10,
amt: 7500
},
{
x: 2020,
colour: "#F25F5C",
y1: 0,
y2: 200,
rad: 10,
amt: 5000
},
{
x: 2022,
colour: "#0C3957",
y1: 0,
y2: 250,
rad: 10,
amt: 9000
},
{
x: 2055,
colour: "#BF802F",
y1: 0,
y2: 300,
rad: 10,
amt: 25000
}
]
);
const thisYear = new Date().getFullYear()
const initialiseData = () => {
const svg = d3.select( "svg" );
const pxX = svg.attr( "width" );
const pxY = svg.attr( "height" );
let tickLabelOffset = 170;
let minDotX = Math.min.apply(Math, data.map(function(o) { return o.y1; }))
if (minDotX < -20) {
tickLabelOffset += minDotX + 20;
}
const makeScale = ( arr, accessor, range ) => {
// console.log("RANGE", accessor, range)
return d3.scaleLinear()
.domain( d3.extent( arr, accessor ) )
.range( range )
.nice()
}
//SCALES//
//-------------------------------------------------------------------------------------------------//
const scX = makeScale( data, d => d.x, [0, pxX - 200]);
const scY = d3.scaleLinear()
.domain([0, 100])
.range( [0, 100] );
const rad = d3.scaleLinear() //scale for radius of small dots on xaxis
.domain(d3.extent(data, d => d.rad))
.range([3, 10]);
const amt = d3.scaleLinear() //scale for radius of large circle .domain(d3.extent(data, d => d.amt))
.domain(d3.extent(data, d => d.amt))
.range([20, 150]);
//-------------------------------------------------------------------------------------------------//
//END OF SCALES//
//stacks small dots on x axis
for (let dotindex=0; dotindex<data.length; dotindex++) {
if (data[dotindex - 1]) {
if (data[dotindex - 1].x === data[dotindex].x) {
data[dotindex].y1 = data[dotindex -1].y1 -20
}
}
}
//creates array of multiples of ten for x axis labels
let tickTens = [];
for (let i=thisYear; i<maxYear; i++) {
if (i % 10 === 0) {
tickTens.push(i)
}
}
//maps array of multiples of ten to values at ticks on x axis
const g = d3.axisBottom( scX ).tickValues(
tickTens.map((tickVal) => {
return tickVal
})
)
//Groups data into arrays by goal year
const _data = data.reduce(
(r, v, _, __, k = v["x"]) => ((r[k] || (r[k] = [])).push(v), r),
[]
)
//CREATE X AXIS//
//---------------------------------------------------------------------------//
svg.append( "g" )
.attr( "transform", "translate(" + 50 + "," + (pxY - 200) + ")")
.call( g )
.selectAll(".tick text")
.attr("fill", "#7A7A7A")
svg.selectAll(".domain")
.attr("stroke", "#BDBDBD")
.attr("stroke-width", "2px")
.attr( "transform", "translate(" + 50 + "," + 150 + ")")
svg.selectAll(".tick line")
.attr("stroke", "#BDBDBD")
.attr("stroke-width", "4px")
.attr( "transform", "translate(" + 50 + "," + 150 + ")")
svg.selectAll( ".tick text")
.attr("font-size", 20)
.attr( "transform", "translate(" + 50 + "," + tickLabelOffset + ")")
.attr("font-weight", "bold")
.attr("dy", "0.5em")
svg.selectAll("circle")
.data(data)
.enter()
.append("g")
.attr("class", "circles")
.append("circle")
.attr( "transform", "translate(" + 100 + "," + 650 + ")")
.attr("fill", "white")
.attr("stroke", d => d.colour)
.attr("stroke-width", "2px")
.attr("cx", d => scX(d.x))
.attr("cy", d => scY(d.y1))
.attr("r", d => rad(d.rad));
//END OF CREATE X AXIS//
//---------------------------------------------------------------------------//
//CREATE LARGE CIRCLES//
//---------------------------------------------------------------------------//
const ticked = () => {
goalAmounts
.attr("cx", (d => (2 * d.x)))
.attr("cy", d => d.y2);
}
let i = 0;
const goalAmounts = svg.selectAll("circle .circles")
.append( "g" )
.attr("class", "goalAmounts")
.data(data.sort(function(a, b) {
return a.amt - b.amt;
}).reverse())
.enter()
.append("circle")
.attr( "transform", "translate(" + 650 + "," + 0 + ")")
.attr("fill", d => {
return d.colour}
)
.attr("cx", d => (d.x))
.attr("cy", (d, index) => {
let _y = scY(d.y2)
if (_data[d.x].length > 1 && _data[i-1]) {
i++
_y = _data[d.x][i -1].y2
} else {
i = 0
}
return _y
})
.attr("r", d => amt(d.amt));
d3.forceSimulation(data)
// .force('charge', d3.forceManyBody().strength(-200))
.force('x', scX(d3.forceX().x(function(d) {
return d.x
})))
.force("y", d3.forceY(d => (d.y2 * Math.random())))
.force('collision', d3.forceCollide().radius(d => amt(d.amt/2)))
.on("tick", ticked);
//END OF CREATE LARGE CIRCLES//
//---------------------------------------------------------------------------//
}
useEffect(() => {
if (data) {
setMaxYear(Math.max.apply(Math, data.map(function(o) { return o.x; })))
}
if (maxYear) {
initialiseData();
}
}, [data,maxYear])
return (
<div className="App">
<svg id="demo1" width="1200" height="700" style={{background: "white"}}/>
</div>
);
}
export default App;
Please see how the code works so far with this sandbox: d3Sandbox
Here is the code for a working version I think?
import React, {useState, useEffect} from 'react';
import * as d3 from 'd3';
import './App.css';
function App() {
const [maxYear, setMaxYear] = useState();
const [data, setData] = useState(
[{
x1: 2020,
colour: "#69306D",
y1: 0,
y2: 50,
rad: 10,
amt: 2000
},
{
x1: 2021,
colour: "#247BA0",
y1: 0,
y2: 100,
rad: 10,
amt: 5000
},
{
x1: 2030,
colour: "#3F762C",
y1: 0,
y2: 150,
rad: 10,
amt: 7500
},
{
x1: 2020,
colour: "#F25F5C",
y1: 0,
y2: 200,
rad: 10,
amt: 5000
},
{
x1: 2022,
colour: "#0C3957",
y1: 0,
y2: 250,
rad: 10,
amt: 9000
},
{
x1: 2055,
colour: "#BF802F",
y1: 0,
y2: 300,
rad: 10,
amt: 25000
}
]
);
const thisYear = new Date().getFullYear()
const initialiseData = () => {
const svg = d3.select( "svg" );
const pxX = svg.attr( "width" );
const pxY = svg.attr( "height" );
let tickLabelOffset = 170;
let minDotX = Math.min.apply(Math, data.map(function(o) { return o.y1; }))
if (minDotX < -20) {
tickLabelOffset += minDotX + 20;
}
const makeScale = ( arr, accessor, range ) => {
// console.log("RANGE", accessor, range)
return d3.scaleLinear()
.domain( d3.extent( arr, accessor ) )
.range( range )
.nice()
}
//SCALES//
//-------------------------------------------------------------------------------------------------//
const scX = makeScale( data, d => d.x1, [0, pxX - 200]);
const scX2 = d3.scaleLinear()
.domain([2020, 2080])
.range( [0, pxX - 200] );
const scY = d3.scaleLinear()
.domain([0, 100])
.range( [0, 100] );
const rad = d3.scaleLinear() //scale for radius of small dots on xaxis
.domain(d3.extent(data, d => d.rad))
.range([3, 10]);
const amt = d3.scaleLinear() //scale for radius of large circle .domain(d3.extent(data, d => d.amt))
.domain(d3.extent(data, d => d.amt))
.range([20, 150]);
//-------------------------------------------------------------------------------------------------//
//END OF SCALES//
//stacks small dots on x axis
let _newData = data;
for (let dotindex=0; dotindex<data.length; dotindex++) {
if (_newData[dotindex - 1]) {
if (_newData[dotindex - 1].x1 === _newData[dotindex].x1) {
_newData[dotindex].y1 = _newData[dotindex -1].y1 -20
}
}
}
setData(_newData)
console.log("DATA: ", data)
//creates array of multiples of ten for x axis labels
let tickTens = [];
for (let i=thisYear; i<maxYear; i++) {
if (i % 10 === 0) {
tickTens.push(i)
}
}
//maps array of multiples of ten to values at ticks on x axis
const g = d3.axisBottom( scX ).tickValues(
tickTens.map((tickVal) => {
return tickVal
})
)
//Groups data into arrays by goal year
const _data = data.reduce(
(r, v, _, __, k = v["x"]) => ((r[k] || (r[k] = [])).push(v), r),
[]
)
//CREATE X AXIS//
//---------------------------------------------------------------------------//
svg.append( "g" )
.attr( "transform", "translate(" + 50 + "," + (pxY - 200) + ")")
.call( g )
.selectAll(".tick text")
.attr("fill", "#7A7A7A")
svg.selectAll(".domain")
.attr("stroke", "#BDBDBD")
.attr("stroke-width", "2px")
.attr( "transform", "translate(" + 50 + "," + 150 + ")")
svg.selectAll(".tick line")
.attr("stroke", "#BDBDBD")
.attr("stroke-width", "4px")
.attr( "transform", "translate(" + 50 + "," + 150 + ")")
svg.selectAll( ".tick text")
.attr("font-size", 20)
.attr( "transform", "translate(" + 50 + "," + tickLabelOffset + ")")
.attr("font-weight", "bold")
.attr("dy", "0.5em")
svg.selectAll("circle")
.data(data)
.enter()
.append("g")
.attr("class", "xDots")
.append("circle")
.attr( "transform", "translate(" + 100 + "," + 650 + ")")
.attr("fill", "white")
.attr("stroke", d => d.colour)
.attr("stroke-width", "2px")
.attr("cx", d => scX(d.x1))
.attr("cy", d => scY(d.y1))
.attr("r", d => rad(d.rad));
//END OF CREATE X AXIS//
//---------------------------------------------------------------------------//
//CREATE LARGE CIRCLES//
//---------------------------------------------------------------------------//
const ticked = () => {
goalAmounts
.attr("cx", (d => d.x))
.attr("cy", d => d.y);
}
let i = 0;
const goalAmounts = svg.selectAll()
.data(data.sort(function(a, b) {
return a.amt - b.amt;
}).reverse())
.enter()
.append("g")
.attr("class", "goals")
.append("circle")
.attr( "transform", "translate(" + 200 + "," + 100 + ")")
.attr("fill", d => {
return d.colour}
)
.attr("cx", d => {
// console.log("DX1, ", d.x1, (d.x1)
return d.x1
})
.attr("cy", (d, index) => {
let _y = scY(d.y2)
}
)
.attr("r", d => amt(d.amt));
d3.forceSimulation(data)
// .force('charge', d3.forceManyBody().strength(-20))
.force('x', (d3.forceX().x(function(d) {
return scX2(d.x1)
})))
.force("y", d3.forceY(d => (d.y2 * Math.random())))
.force('collision', d3.forceCollide().radius(d => amt(d.amt) + 10))
.on("tick", ticked);
//END OF CREATE LARGE CIRCLES//
//---------------------------------------------------------------------------//
}
useEffect(() => {
if (data) {
console.log("DATA: ", data)
setMaxYear(Math.max.apply(Math, data.map(function(o) { return o.x1; })))
}
if (maxYear) {
console.log("MAXYEAR", maxYear)
initialiseData();
}
}, [data, maxYear])
return (
<div className="App">
<svg id="demo1" width="1200" height="700" style={{background: "white"}}/>
</div>
);
}
export default App;
I have created 2 level sunburst chart on http://test.smokethis.com/wheel/ which is working perfectly.
But i want to validate my chart to show only 10 child of a particular parent and an additional node called "More child".
The behaviour on "More child" will be same as on click on parent. Which reveal all child of parent.
Image link of current situation
Image link of Resultant behaviour.Which i want
var d3Data = JSON.parse(jQuery('body .Jsondata').text())[0];
const m0 = {
id: "123",
variables: [
{
name: "chart",
inputs: ["partition","data","d3","DOM","width","color","arc","format","radius"],
value: (function(partition,data,d3,DOM,width,color,arc,format,radius)
{
var formatNumber = d3.format(",d");
var b = {
w: 140, h: 30, s: 3, t: 10
};
const root = partition(data);
root.each(d => d.current = d);
const svg = d3.select(DOM.svg(width, width))
.style("width", "100%")
.style("height", "auto")
.style("font", "20px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`);
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => { while (d.depth > 1) d = d.parent; console.log('d value--',d); return (typeof d.data.color != 'undefined' && d.data.color != '') ? d.data.color : color(d.data.name); })
.attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0)
.attr("d", d => arc(d.current));
path.filter(d => d.children)
.style("cursor", "pointer")
.on("click", clicked);
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.text(d => d.data.name);
const parent = g.append("circle")
.datum(root)
.attr("r", radius)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("click", clicked);
initializeBreadcrumbTrail();
return svg.node();
function clicked(p) {
updateBreadcrumbs(getParents(p),p.value);
var level = p.ancestors().map(p => p.data.name).reverse();
// var str = '';
// if(level.length > 1){
// for(var i=0;i<level.length;i++){
// str += '<span class="str_att show-pointer" style="background-color: #8cb125;margin-right:10px;border-radius: 5px;padding: 5px 10px;">'+level[i]+'</span>';
// }
// }
//jQuery('.breadCrumb').html(str);
console.log('level-=-',level);
parent.datum(p.parent || root);
root.each(d => d.target = {
x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
y0: Math.max(0, d.y0 - p.depth),
y1: Math.max(0, d.y1 - p.depth)
});
const t = g.transition().duration(750);
//console.log('p',p);
// Transition the data on all arcs, even the ones that aren’t visible,
// so that if this transition is interrupted, entering arcs will start
// the next transition from the desired position.
path.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => d.current = i(t);
})
.filter(function(d) {
return +this.getAttribute("fill-opacity") || arcVisible(d.target);
})
.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
.attrTween("d", d => () => arc(d.current));
label.filter(function(d) {
return +this.getAttribute("fill-opacity") || labelVisible(d.target);
}).transition(t)
.attr("fill-opacity", d => +labelVisible(d.target))
.attrTween("transform", d => () => labelTransform(d.current));
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select(".breadCrumb").append("svg:svg")
.attr("width", width)
.attr("height", width/10)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g").on("click", clicked)
.data(nodeArray, function(x) { return percentageString + x.data.name + x.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", breadcrumbPoints)
.style("fill", function(x) { return color(x.data.name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(x) { return x.data.name; });
entering.attr("transform", function(x, i) { return "translate(" + i* (b.w + b.s) + ", 0)"; });
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
function breadcrumbPoints(x, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
function arcVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
}
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
function getParents(a){
var nodeArray = [a];
while(a.parent){
nodeArray.push(a.parent);
a = a.parent
}
return nodeArray.reverse();
}
}
)
},
{
name: "data",
value: (function(){
return d3Data;
})
},
{
name: "partition",
inputs: ["d3"],
value: (function(d3){
// console.log('partition',d3);
return(
data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
console.log('data',d3.partition().size([2 * Math.PI, root.height+1])(root));
return d3.partition()
.size([2 * Math.PI, root.height+1])
(root);
}
)
})
},
{
name: "color",
inputs: ["d3","data"],
value: (function(d3,data){return (
d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
)})
},
{
name: "format",
inputs: ["d3"],
value: (function(d3){return(
d3.format(",d")
)})
},
{
name: "width",
value: (function(){return(
932
)})
},
{
name: "radius",
inputs: ["width"],
value: (function(width){return(
width / 6
)})
},
{
name: "arc",
inputs: ["d3","radius"],
value: (function(d3,radius){return(
d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius(d => d.y0 * radius)
.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1))
)})
},
{
name: "d3",
inputs: ["require"],
value: (function(require){return(
require("https://d3js.org/d3.v5.min.js")
)})
}
]
};
const notebook = {
id: "123",
modules: [m0]
};
function loadData(){
$.ajax({
type:'GET',
url:'http://shopnz.in/wp-admin/admin-ajax.php?action=wp_ajax_list_items&sunburst=sunburstcat',
success: function(response){
console.log('response',response[0]);
console.log('data-=-=',data);
data = response[0];
}
});
}
export default notebook;
[{"name":"Balance","term_id":"588","slug":"balance","parent":"0","has_children":true,"color":"#aac62d","children":[{"name":"Aroma","term_id":"589","slug":"aroma","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Earth","term_id":"593","slug":"earth","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Fruit","term_id":"594","slug":"fruit","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Herb","term_id":"595","slug":"herb","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Pungent","term_id":"596","slug":"pungent","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Spice","term_id":"597","slug":"spice","parent":"589","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Health","term_id":"590","slug":"health","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Body","term_id":"598","slug":"body","parent":"590","has_children":true,"color":"#aac62d","children":[{"name":"Fatigue","term_id":"613","slug":"fatigue","parent":"598","has_children":true,"color":"#aac62d","children":[{"name":"Sleep","term_id":"616","slug":"sleep","parent":"613","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"PainRelief","term_id":"614","slug":"painrelief","parent":"598","has_children":true,"color":"#aac62d","children":[{"name":"Spasms","term_id":"617","slug":"spasms","parent":"614","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Relaxed","term_id":"618","slug":"relaxed","parent":"614","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Digestion","term_id":"615","slug":"digestion","parent":"598","has_children":true,"color":"#aac62d","children":[{"name":"Appetite","term_id":"619","slug":"appetite","parent":"615","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Nausea","term_id":"620","slug":"nausea","parent":"615","has_children":false,"size":"1000","color":"#aac62d"}]}]},{"name":"Mind","term_id":"599","slug":"mind","parent":"590","has_children":true,"color":"#aac62d","children":[{"name":"Behavior","term_id":"621","slug":"behavior","parent":"599","has_children":true,"color":"#aac62d","children":[{"name":"Sedative","term_id":"623","slug":"sedative","parent":"621","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Energetic","term_id":"624","slug":"energetic","parent":"621","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Focus","term_id":"625","slug":"focus","parent":"621","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Mood","term_id":"622","slug":"mood","parent":"599","has_children":true,"color":"#aac62d","children":[{"name":"Creative","term_id":"626","slug":"creative","parent":"622","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Uplifted","term_id":"627","slug":"uplifted","parent":"622","has_children":false,"size":"1000","color":"#aac62d"}]}]}]},{"name":"Medical","term_id":"591","slug":"medical","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Condition","term_id":"600","slug":"condition","parent":"591","has_children":true,"color":"#aac62d","children":[{"name":"Cancer","term_id":"602","slug":"cancer","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"PMS","term_id":"603","slug":"pms","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Arthritis","term_id":"628","slug":"arthritis","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Asthma","term_id":"629","slug":"asthma","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Cachexia","term_id":"630","slug":"cachexia","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Crohn\u2019s Disease","term_id":"631","slug":"crohns-disease","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Depression (currently a 'symptom')","term_id":"632","slug":"depression-currently-a-symptom","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Epilepsy","term_id":"633","slug":"epilepsy","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Gastrointestinal Disorder","term_id":"634","slug":"gastrointestinal-disorder","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Hypertension","term_id":"635","slug":"hypertension","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Parkinson\u2019s","term_id":"636","slug":"parkinsons","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Phantom Limb Pain","term_id":"637","slug":"phantom-limb-pain","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Spinal Cord Injury","term_id":"638","slug":"spinal-cord-injury","parent":"600","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Symptom","term_id":"601","slug":"symptom","parent":"591","has_children":true,"color":"#aac62d","children":[{"name":"Anxiety","term_id":"604","slug":"anxiety","parent":"601","has_children":true,"color":"#aac62d","children":[{"name":"ACDC","term_id":"612","slug":"acdc","parent":"604","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Depresssion","term_id":"605","slug":"depresssion","parent":"601","has_children":false,"size":"1000","color":"#aac62d"}]}]},{"name":"Terpenes","term_id":"592","slug":"terpenes","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Humulene","term_id":"606","slug":"humulene","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Limonene","term_id":"607","slug":"limonene","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Linalool","term_id":"608","slug":"linalool","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Myrcene","term_id":"609","slug":"myrcene","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Pinene","term_id":"610","slug":"pinene","parent":"592","has_children":true,"color":"#aac62d","children":[{"name":"Romulan","term_id":"611","slug":"romulan","parent":"610","has_children":false,"size":"1000","color":"#aac62d"}]}]}]}]
This is my data.
I am trying to color the connections in my hierarchical edge bundling visualization based on the groups they are connecting to. An example of this can be seen here.
Here is my current mouseover function:
function mouseover(d) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes("target", true));
}
And here is the mouseover function from the example I've posted:
function mouseovered(d)
{
// Handle tooltip
// Tooltips should avoid crossing into the center circle
d3.selectAll("#tooltip").remove();
d3.selectAll("#vis")
.append("xhtml:div")
.attr("id", "tooltip")
.style("opacity", 0)
.html(d.title);
var mouseloc = d3.mouse(d3.select("#vis")[0][0]),
my = ((rotateit(d.x) > 90) && (rotateit(d.x) < 270)) ? mouseloc[1] + 10 : mouseloc[1] - 35,
mx = (rotateit(d.x) < 180) ? (mouseloc[0] + 10) : Math.max(130, (mouseloc[0] - 10 - document.getElementById("tooltip").offsetWidth));
d3.selectAll("#tooltip").style({"top" : my + "px", "left": mx + "px"});
d3.selectAll("#tooltip")
.transition()
.duration(500)
.style("opacity", 1);
node.each(function(n) { n.target = n.source = false; });
currnode = d3.select(this)[0][0].__data__;
link.classed("link--target", function(l) {
if (l.target === d)
{
return l.source.source = true;
}
if (l.source === d)
{
return l.target.target = true;
}
})
.filter(function(l) { return l.target === d || l.source === d; })
.attr("stroke", function(d){
if (d[0].name == currnode.name)
{
return color(d[2].cat);
}
return color(d[0].cat);
})
.each(function() { this.parentNode.appendChild(this); });
d3.selectAll(".link--clicked").each(function() { this.parentNode.appendChild(this); });
node.classed("node--target", function(n) {
return (n.target || n.source);
});
}
I am somewhat new to D3, but I am assuming what I'll need to do is check the group based on the key and then match it to the same color as that group.
My full code is here:
<script type="text/javascript">
color = d3.scale.category10();
var w = 840,
h = 800,
rx = w / 2,
ry = h / 2,
m0,
rotate = 0
pi = Math.PI;
var splines = [];
var cluster = d3.layout.cluster()
.size([360, ry - 180])
.sort(function(a, b) {
return d3.ascending(a.key, b.key);
});
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.5)
.radius(function(d) {
return d.y;
})
.angle(function(d) {
return d.x / 180 * Math.PI;
});
// Chrome 15 bug: <http://code.google.com/p/chromium/issues/detail?id=98951>
var div = d3.select("#bundle")
.style("width", w + "px")
.style("height", w + "px")
.style("position", "absolute");
var svg = div.append("svg:svg")
.attr("width", w)
.attr("height", w)
.append("svg:g")
.attr("transform", "translate(" + rx + "," + ry + ")");
svg.append("svg:path")
.attr("class", "arc")
.attr("d", d3.svg.arc().outerRadius(ry - 180).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
.on("mousedown", mousedown);
d3.json("TASKS AND PHASES.json", function(classes) {
var nodes = cluster.nodes(packages.root(classes)),
links = packages.imports(nodes),
splines = bundle(links);
var path = svg.selectAll("path.link")
.data(links)
.enter().append("svg:path")
.attr("class", function(d) {
return "link source-" + d.source.key + " target-" + d.target.key;
})
.attr("d", function(d, i) {
return line(splines[i]);
});
var groupData = svg.selectAll("g.group")
.data(nodes.filter(function(d) {
return (d.key == 'Department' || d.key == 'Software' || d.key == 'Tasks' || d.key == 'Phases') && d.children;
}))
.enter().append("group")
.attr("class", "group");
var groupArc = d3.svg.arc()
.innerRadius(ry - 177)
.outerRadius(ry - 157)
.startAngle(function(d) {
return (findStartAngle(d.__data__.children) - 2) * pi / 180;
})
.endAngle(function(d) {
return (findEndAngle(d.__data__.children) + 2) * pi / 180
});
svg.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.attr("id", function(d, i) {console.log(d.__data__.key); return d.__data__.key;})
.style("fill", function(d, i) {return color(i);})
.style("fill-opacity", 0.5)
.each(function(d,i) {
var firstArcSection = /(^.+?)L/;
var newArc = firstArcSection.exec( d3.select(this).attr("d") )[1];
newArc = newArc.replace(/,/g , " ");
svg.append("path")
.attr("class", "hiddenArcs")
.attr("id", "hidden"+d.__data__.key)
.attr("d", newArc)
.style("fill", "none");
});
svg.selectAll(".arcText")
.data(groupData[0])
.enter().append("text")
.attr("class", "arcText")
.attr("dy", 15)
.append("textPath")
.attr("startOffset","50%")
.style("text-anchor","middle")
.attr("xlink:href",function(d,i){return "#hidden" + d.__data__.key;})
.text(function(d){return d.__data__.key;});
svg.selectAll("g.node")
.data(nodes.filter(function(n) {
return !n.children;
}))
.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) {
return "node-" + d.key;
})
.attr("transform", function(d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
})
.append("svg:text")
.attr("dx", function(d) {
return d.x < 180 ? 25 : -25;
})
.attr("dy", ".31em")
.attr("text-anchor", function(d) {
return d.x < 180 ? "start" : "end";
})
.attr("transform", function(d) {
return d.x < 180 ? null : "rotate(180)";
})
.text(function(d) {
return d.key.replace(/_/g, ' ');
})
.on("mouseover", mouseover)
.on("mouseout", mouseout);
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
path.attr("d", function(d, i) {
return line(splines[i]);
});
});
});
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
function mouse(e) {
return [e.pageX - rx, e.pageY - ry];
}
function mousedown() {
m0 = mouse(d3.event);
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
div.style("-webkit-transform", "translate3d(0," + (ry - rx) + "px,0)rotate3d(0,0,0," + dm + "deg)translate3d(0," + (rx - ry) + "px,0)");
}
}
function mouseup() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
rotate += dm;
if (rotate > 360) rotate -= 360;
else if (rotate < 0) rotate += 360;
m0 = null;
div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");
svg.attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
.selectAll("g.node text")
.attr("dx", function(d) {
return (d.x + rotate) % 360 < 180 ? 25 : -25;
})
.attr("text-anchor", function(d) {
return (d.x + rotate) % 360 < 180 ? "start" : "end";
})
.attr("transform", function(d) {
return (d.x + rotate) % 360 < 180 ? null : "rotate(180)";
});
}
}
function mouseover(d) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes("target", true));
}
function mouseout(d) {
svg.selectAll("path.link.source-" + d.key)
.classed("source", false)
.each(updateNodes("target", false));
svg.selectAll("path.link.target-" + d.key)
.classed("target", false)
.each(updateNodes("source", false));
}
function updateNodes(name, value) {
return function(d) {
if (value) this.parentNode.appendChild(this);
svg.select("#node-" + d[name].key).classed(name, value);
};
}
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
function findStartAngle(children) {
var min = children[0].x;
children.forEach(function(d) {
if (d.x < min)
min = d.x;
});
return min;
}
function findEndAngle(children) {
var max = children[0].x;
children.forEach(function(d) {
if (d.x > max)
max = d.x;
});
return max;
}
</script>
Here's an example solution in D3 v6 adapting the Observable example plus my answer to this other question. Basic points:
You will to add the 'group' into the input data - for the data you mention in the comments I've defined group as the 2nd element (per dot separation) of the name. The hierarchy function in the Observable appears to strip this.
It's probably fortunate that all the name values are e.g. root.parent.child - this makes the leafGroups work quite well for your data (but might not for asymmetric hierarchies).
Define a colour range e.g. const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10); which you can use for arcs, label text (nodes), paths (links)
I've avoided using the mix-blend-mode styling with the example as it doesn't look good to me.
I'm applying the styles in overed and outed - see below for the logic.
See the comments in overed for styling logic on mouseover:
function overed(event, d) {
//link.style("mix-blend-mode", null);
d3.select(this)
// set dark/ bold on hovered node
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.incoming.map(d => d.path))
// each link has data with source and target so you can get group
// and therefore group color; 0 for incoming and 1 for outgoing
.attr("stroke", d => colors(d[0].data.group))
// increase stroke width for emphasis
.attr("stroke-width", 4)
.raise();
d3.selectAll(d.outgoing.map(d => d.path))
// each link has data with source and target so you can get group
// and therefore group color; 0 for incoming and 1 for outgoing
.attr("stroke", d => colors(d[1].data.group))
// increase stroke width for emphasis
.attr("stroke-width", 4)
.raise()
d3.selectAll(d.incoming.map(([d]) => d.text))
// source and target nodes to go dark and bold
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.outgoing.map(([, d]) => d.text))
// source and target nodes to go dark and bold
.style("fill", colordark)
.attr("font-weight", "bold");
}
See the comments in outed for styling logic on mouseout:
function outed(event, d) {
//link.style("mix-blend-mode", "multiply");
d3.select(this)
// hovered node to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.incoming.map(d => d.path))
// incoming links to revert to 'colornone' and width 1 on mouseout
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.outgoing.map(d => d.path))
// incoming links to revert to 'colornone' and width 1 on mouseout
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.incoming.map(([d]) => d.text))
// incoming nodes to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.outgoing.map(([, d]) => d.text))
// incoming nodes to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
}
Working example with the data you mentioned in the comments:
const url = "https://gist.githubusercontent.com/robinmackenzie/5c5d2af4e3db47d9150a2c4ba55b7bcd/raw/9f9c6b92d24bd9f9077b7fc6c4bfc5aebd2787d5/harvard_vis.json";
const colornone = "#ccc";
const colordark = "#222";
const width = 600;
const radius = width / 2;
d3.json(url).then(json => {
// hack in the group name to each object
json.forEach(o => o.group = o.name.split(".")[1]);
// then render
render(json);
});
function render(data) {
const line = d3.lineRadial()
.curve(d3.curveBundle.beta(0.85))
.radius(d => d.y)
.angle(d => d.x);
const tree = d3.cluster()
.size([2 * Math.PI, radius - 100]);
const root = tree(bilink(d3.hierarchy(hierarchy(data))
.sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.name, b.data.name))));
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", width)
.append("g")
.attr("transform", `translate(${radius},${radius})`);
const arcInnerRadius = radius - 100;
const arcWidth = 20;
const arcOuterRadius = arcInnerRadius + arcWidth;
const arc = d3
.arc()
.innerRadius(arcInnerRadius)
.outerRadius(arcOuterRadius)
.startAngle((d) => d.start)
.endAngle((d) => d.end);
const leafGroups = d3.groups(root.leaves(), d => d.parent.data.name);
const arcAngles = leafGroups.map(g => ({
name: g[0],
start: d3.min(g[1], d => d.x),
end: d3.max(g[1], d => d.x)
}));
const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10);
svg
.selectAll(".arc")
.data(arcAngles)
.enter()
.append("path")
.attr("id", (d, i) => `arc_${i}`)
.attr("d", (d) => arc({start: d.start, end: d.end}))
.attr("fill", d => colors(d.name))
svg
.selectAll(".arcLabel")
.data(arcAngles)
.enter()
.append("text")
.attr("x", 5)
.attr("dy", (d) => ((arcOuterRadius - arcInnerRadius) * 0.8))
.append("textPath")
.attr("class", "arcLabel")
.attr("xlink:href", (d, i) => `#arc_${i}`)
.text((d, i) => ((d.end - d.start) < (6 * Math.PI / 180)) ? "" : d.name);
// add nodes
const node = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y}, 0)`)
.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI ? (arcWidth + 5) : (arcWidth + 5) * -1)
.attr("text-anchor", d => d.x < Math.PI ? "start" : "end")
.attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
.text(d => d.data.name)
.style("fill", d => colors(d.data.group))
.each(function(d) { d.text = this; })
.on("mouseover", overed)
.on("mouseout", outed)
.call(text => text.append("title").text(d => `${id(d)} ${d.outgoing.length} outgoing ${d.incoming.length} incoming`));
// add edges
const link = svg.append("g")
.attr("stroke", colornone)
.attr("fill", "none")
.selectAll("path")
.data(root.leaves().flatMap(leaf => leaf.outgoing))
.join("path")
//.style("mix-blend-mode", "multiply")
.attr("d", ([i, o]) => line(i.path(o)))
.each(function(d) { d.path = this; });
function overed(event, d) {
//link.style("mix-blend-mode", null);
d3.select(this)
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.incoming.map(d => d.path))
.attr("stroke", d => colors(d[0].data.group))
.attr("stroke-width", 4)
.raise();
d3.selectAll(d.outgoing.map(d => d.path))
.attr("stroke", d => colors(d[1].data.group))
.attr("stroke-width", 4)
.raise()
d3.selectAll(d.incoming.map(([d]) => d.text))
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.outgoing.map(([, d]) => d.text))
.style("fill", colordark)
.attr("font-weight", "bold");
}
function outed(event, d) {
//link.style("mix-blend-mode", "multiply");
d3.select(this)
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.incoming.map(d => d.path))
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.outgoing.map(d => d.path))
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.incoming.map(([d]) => d.text))
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.outgoing.map(([, d]) => d.text))
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
}
function id(node) {
return `${node.parent ? id(node.parent) + "." : ""}${node.data.name}`;
}
function bilink(root) {
const map = new Map(root.leaves().map(d => [id(d), d]));
for (const d of root.leaves()) d.incoming = [], d.outgoing = d.data.imports.map(i => [d, map.get(i)]);
for (const d of root.leaves()) for (const o of d.outgoing) o[1].incoming.push(o);
return root;
}
function hierarchy(data, delimiter = ".") {
let root;
const map = new Map;
data.forEach(function find(data) {
const {name} = data;
if (map.has(name)) return map.get(name);
const i = name.lastIndexOf(delimiter);
map.set(name, data);
if (i >= 0) {
find({name: name.substring(0, i), children: []}).children.push(data);
data.name = name.substring(i + 1);
} else {
root = data;
}
return data;
});
return root;
}
}
.node {
font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
.arcLabel {
font: 300 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
I am trying to recreate this chart design. With a dougnut chart, surrounded by curved data labels and an inside curved bar chart.
//starting demo
http://jsfiddle.net/NYEaX/1753/
//LATEST demo with inner bars//
http://jsfiddle.net/NYEaX/1761/
I've tried to map out the outer labels using a path, but the labels are not showing up correctly?
var labelRadius = (radius * 0.95);
var numBars = data.length;
// Labels
var labels = svg.append('g')
.classed('labels', true);
labels.append('def')
.append('path')
.attr('id', 'label-path')
.attr('d', 'm0 ' + -labelRadius + ' a' + labelRadius + ' ' + labelRadius + ' 0 1,1 -0.01 0');
labels.selectAll('text')
.data(data)
.enter()
.append('text')
.style('text-anchor', 'middle')
.append('textPath')
.attr('xlink:href', '#label-path')
.attr('startOffset', function(d, i) {
return i * 100 / numBars + 50 / numBars + '%';
})
.text(function(d) {
return d.group;
});
Live Demo:
http://jsfiddle.net/zu8m9ckd/8/
var $this = $("#progress");
var data = [{
"group": "Chinese",
"value": 22.6,
"children": [{
"growth": 1277.4
}]
}, {
"group": "Portuguese",
"value": 4.2,
"children": [{
"growth": 989.6
}]
}, {
"group": "Spanish",
"value": 7.8,
"children": [{
"growth": 743.2
}]
}, {
"group": "Rest",
"value": 17.8,
"children": [{
"growth": 588.5
}]
}, {
"group": "French",
"value": 3.0,
"children": [{
"growth": 398.2
}]
}, {
"group": "English",
"value": 27.3,
"children": [{
"growth": 281.2
}]
}, {
"group": "German",
"value": 3.8,
"children": [{
"growth": 173.1
}]
}, {
"group": "Japanese",
"value": 5.0,
"children": [{
"growth": 110.6
}]
}, {
"group": "Korean",
"value": 2.0,
"children": [{
"growth": 107.1
}]
}, {
"group": "Arabic",
"value": 3.3,
"children": [{
"growth": 2501.2
}]
}, {
"group": "Russian",
"value": 3.0,
"children": [{
"growth": 1825.8
}]
}];
var w = 500;
var h = w;
var radius = Math.min(w, h) / 2 - 50;
var svg = d3.select($this[0])
.append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
svg.append("g")
.attr("class", "innerslices");
svg.append("g")
.attr("class", "slices");
svg.append("g")
.attr("class", "labels");
svg.append("g")
.attr("class", "labelsvals");
var pie = d3.layout.pie()
.sort(null)
.value(function (d) {
return d.value;
});
const outerRadius=0.85;
const innerRadius=0.75;
const earthRadius=0.05;
const arc = d3.svg.arc()
.outerRadius(radius * outerRadius)
.innerRadius(radius * innerRadius);
const outerArc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius - 20);
const innerArc = d3.svg.arc()
.innerRadius(radius - 55)
.outerRadius(radius - 55);
svg.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
function colores_google(n) {
var colores_g = ["#e9168a", "#f8dd2f", "#448875", "#c3bd75", "#2b2d39", "#311854", "#553814", "#f7b363", "#89191d", "#c12f34", "#2b2a2c", "#c5b8a6", "#57585b"];
return colores_g[n % colores_g.length];
}
var totalsArray = [];
$.each(data, function (index, value) {
value["groupid"] = index;
var total = 0;
$.each(value.children, function (i, v) {
v["groupid"] = index;
total += v.growth;
});
value["total"] = total;
totalsArray.push(total);
});
var maxTotal = Math.max.apply(Math, totalsArray);
//slices
var slice = svg.select(".slices").selectAll("path.slice")
.data(pie(data))
slice.enter()
.insert("path")
.style("fill", function (d) {
return "#3b453f"; //colores_google(d.data.groupid);
})
.attr("class", "slice");
slice
.transition().duration(1000)
.attr("d", function (d) {
return arc(d);
})
slice.exit()
.remove();
//slices
//innerslices
var innerslice = svg.select(".innerslices").selectAll("path.innerslice")
.data(pie(data));
innerslice.enter()
.insert("path")
.style("fill", function (d) {
return "#8fdfff"; //colores_google(d.data.groupid);
})
.attr("class", "innerslice");
innerslice
.transition().duration(1000)
.attr("d", function (d) {
var arc3 = d3.svg.arc()
.outerRadius(radius * innerRadius)
.innerRadius(radius * (innerRadius-(innerRadius-earthRadius) * (d.data.children[0].growth / maxTotal)));
return arc3(d);
})
innerslice.exit()
.remove();
//innerslice
var pieData = pie(data);
var pieAngle = pieData.map(function (p) {
return (p.startAngle + p.endAngle) / 2 / Math.PI * 180;
});
const labels = svg.append('g')
.classed('labels', true);
//base on angle to change `text-anchor` and `transform(rotate)` to make the position of text correct
labels.selectAll('text')
.data(data)
.enter()
.append('text')
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "start"
}
return "end"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${outerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.group;
});
//similar with outer black text
labels.selectAll('text.inner')
.data(data)
.enter()
.append('text')
.attr("class","inner")
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "end"
}
return "start"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${innerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.children[0].growth+"%";
});
const labelFontSize = 10;
const labelValRadius = (radius * 0.80 - labelFontSize * 0.35); //calculate correct radius
const labelValRadius1 = (radius * 0.80 + labelFontSize * 0.35); //why 0.35? I don't know. Try to google it.
const labelsVals = svg.append('g')
.classed('labelsvals', true);
//define two paths to make the direction of labels correct
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-1')
.attr('d', `m0 ${-labelValRadius} a${labelValRadius} ${labelValRadius} 0 1,1 -0.01 0`);
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-2')
.attr('d', `m0 ${-labelValRadius1} a${labelValRadius1} ${labelValRadius1} 0 1,0 0.01 0`);
labelsVals.selectAll('text')
.data(data)
.enter()
.append('text')
.style('font-size', labelFontSize)
.style('font-weight', "bold")
.style('text-anchor', 'middle')
.append('textPath')
.attr('href', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 90 && angle <= 270) { //based on angle to choose the path
return '#label-path-2';
} else {
return '#label-path-1';
}
})
.attr('startOffset', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
let percent = (p.startAngle + p.endAngle) / 2 / 2 / Math.PI * 100;
if (angle > 90 && angle <= 270) { //calculate the correct percent for each path respectively
return 100 - percent + "%";
}
return percent + "%";
})
.text(function (d) {
if (d.value > 2) {//according to the simple image, the percent less than 3% should only show int part
return d.value.toFixed(1) + "%";
} else {
return d.value.toFixed(0) + "%";
}
});
body {
background: #eeeeee;
}
path {
stroke-width: 1px;
stroke: #eeeeee;
}
.small {
fill: steelblue;
}
.big {
stroke: #666;
fill: #ddd;
}
.small:hover {
stroke: steelblue;
fill: lightsteelblue;
}
.test {
padding: 30px
}
#progress {
position: relative;
margin-top: 20px
}
.progresschart {
background: white;
border-radius: 100px;
width: 100px;
height: 100px;
overflow: hidden;
border: 1px solid grey;
margin-top: 5px;
}
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.progresslabels {
position: absolute;
top: 0px;
left: 0;
}
.labelsvals {
fill: #ffffff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="progress"></div>
There are many parts in your code should be corrected. I choose two main parts to explain:
White Label:
//base on angle to change `text-anchor` and `transform(rotate)` to make the position of text correct
labels.selectAll('text')
.data(data)
.enter()
.append('text')
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "start"
}
return "end"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${outerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.group;
});
Outer Black Label:
const labelFontSize = 10;
const labelValRadius = (radius * 0.80 - labelFontSize * 0.35); //calculate correct radius
const labelValRadius1 = (radius * 0.80 + labelFontSize * 0.35); //why 0.35? I don't know. Try to google it.
const labelsVals = svg.append('g')
.classed('labelsvals', true);
//define two paths to make the direction of labels correct
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-1')
.attr('d', `m0 ${-labelValRadius} a${labelValRadius} ${labelValRadius} 0 1,1 -0.01 0`);
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-2')
.attr('d', `m0 ${-labelValRadius1} a${labelValRadius1} ${labelValRadius1} 0 1,0 0.01 0`);
labelsVals.selectAll('text')
.data(data)
.enter()
.append('text')
.style('font-size', labelFontSize)
.style('font-weight', "bold")
.style('text-anchor', 'middle')
.append('textPath')
.attr('href', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 90 && angle <= 270) { //based on angle to choose the path
return '#label-path-2';
} else {
return '#label-path-1';
}
})
.attr('startOffset', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
let percent = (p.startAngle + p.endAngle) / 2 / 2 / Math.PI * 100;
if (angle > 90 && angle <= 270) { //calculate the correct percent for each path respectively
return 100 - percent + "%";
}
return percent + "%";
})
.text(function (d) {
if (d.value > 2) {//according to the simple image, the percent less than 3% should only show int part
return d.value.toFixed(1) + "%";
} else {
return d.value.toFixed(0) + "%";
}
});
Inner Black Label
//similar with outer black label
labels.selectAll('text.inner')
.data(data)
.enter()
.append('text')
.attr("class","inner")
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "end"
}
return "start"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${innerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.children[0].growth+"%";
});
In this fiddle I've managed to get the doughnut ring, with the inner bars - and some labels.
But I need to get the labels in the correct orientations, colors, scales reflecting more accurately.
//latest code
http://jsfiddle.net/NYEaX/1761/
//innerslices
var arc2 = d3.svg.arc()
.outerRadius(radius * 0.75)
.innerRadius(radius * 0.25);
var innerslice = svg.select(".innerslices").selectAll("path.innerslice")
.data(pie(data));
innerslice.enter()
.insert("path")
.style("fill", function(d) {
return "#8fdfff";//colores_google(d.data.groupid);
})
.attr("class", "innerslice");
innerslice
.transition().duration(1000)
.attrTween("d", function(d) {
var arc3 = d3.svg.arc()
.outerRadius(radius * 0.75)
.innerRadius(radius * 0.25 * (d.data.children[0].growth / maxTotal));
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc3(interpolate(t));
};
})
innerslice.exit()
.remove();
//innerslice