I am following the tutorial in https://observablehq.com/#d3/bar-chart-race-explained.
I am using chrome with version 89.0.4389.90 to test the code.
I mostly copied the code and downloaded the data csv file, and I am trying to run the code locally - here it is:
<!DOCTYPE html>
<html>
<head>
<title>Assignment1</title>
<script src="d3.v6.min.js"></script>
</head>
<body>
<!--<svg width="1600" height="800" id="mainsvg" class="svgs"></svg>-->
<script>
let margin = { top: 16, right: 6, bottom: 6, left: 0 };
let barSize = 48;
let n = 12;
let width = 1600;
let duration = 250;
let height = margin.top + barSize * n + margin.bottom;
d3.csv("category-brands.csv").then((data) => {
const x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
const y = d3
.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1);
const names = new Set(data.map((d) => d.name));
console.log(names);
let datevalues = Array.from(
d3.rollup(
data,
([d]) => +d.value,
(d) => d.date,
(d) => d.name
)
);
console.log(datevalues);
datevalues = datevalues
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b));
console.log("datavalues:", datevalues);
function rank(value) {
const data = Array.from(names, (name) => ({
name,
value: value(name),
}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
console.log(
"rank",
rank((name) => datevalues[0][1].get(name))
);
const k = 10;
const keyframes = (function () {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank(
(name) =>
(a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t
),
]);
}
}
keyframes.push([new Date(kb), rank((name) => b.get(name) || 0)]);
return keyframes;
})();
console.log("total frames:", keyframes.length);
console.log("total frames:", keyframes);
let nameframes = d3.groups(
keyframes.flatMap(([, data]) => data),
(d) => d.name
);
console.log("name frames number:", nameframes.length);
console.log("name frames:", nameframes);
let prev = new Map(
nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a]))
);
let next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));
console.log("pref:", prev);
console.log("next:", next);
function bars(svg) {
let bar = svg.append("g").attr("fill-opacity", 0.6).selectAll("rect");
return ([date, data], transition) =>
(bar = bar
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append("rect")
.attr("fill", color)
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", (d) => y((prev.get(d) || d).rank))
.attr("width", (d) => x((prev.get(d) || d).value) - x(0)),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr("y", (d) => y((next.get(d) || d).rank))
.attr("width", (d) => x((next.get(d) || d).value) - x(0))
)
.call((bar) =>
bar
.transition(transition)
.attr("y", (d) => y(d.rank))
.attr("width", (d) => x(d.value) - x(0))
));
}
function labels(svg) {
let label = svg
.append("g")
.style("font", "bold 12px var(--sans-serif)")
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.selectAll("text");
return ([date, data], transition) =>
(label = label
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append("text")
.attr(
"transform",
(d) =>
`translate(${x((prev.get(d) || d).value)},${y(
(prev.get(d) || d).rank
)})`
)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.attr("dy", "-0.25em")
.text((d) => d.name)
.call((text) =>
text
.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "1.15em")
),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr(
"transform",
(d) =>
`translate(${x((next.get(d) || d).value)},${y(
(next.get(d) || d).rank
)})`
)
.call((g) =>
g
.select("tspan")
.tween("text", (d) =>
textTween(d.value, (next.get(d) || d).value)
)
)
)
.call((bar) =>
bar
.transition(transition)
.attr(
"transform",
(d) => `translate(${x(d.value)},${y(d.rank)})`
)
.call((g) =>
g
.select("tspan")
.tween("text", (d) =>
textTween((prev.get(d) || d).value, d.value)
)
)
));
}
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function (t) {
this.textContent = formatNumber(i(t));
};
}
formatNumber = d3.format(",d");
function axis(svg) {
const g = svg
.append("g")
.attr("transform", `translate(0,${margin.top})`);
const axis = d3
.axisTop(x)
.ticks(width / 160)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));
return (_, transition) => {
g.transition(transition).call(axis);
g.select(".tick:first-of-type text").remove();
g.selectAll(".tick:not(:first-of-type) line").attr(
"stroke",
"white"
);
g.select(".domain").remove();
};
}
function ticker(svg) {
const now = svg
.append("text")
.style("font", `bold ${barSize}px var(--sans-serif)`)
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.attr("x", width - 6)
.attr("y", margin.top + barSize * (n - 0.45))
.attr("dy", "0.32em")
.text(formatDate(keyframes[0][0]));
return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
let formatDate = d3.utcFormat("%Y");
let color = (function () {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some((d) => d.category !== undefined)) {
const categoryByName = new Map(
data.map((d) => [d.name, d.category])
);
scale.domain(categoryByName.values());
return (d) => scale(categoryByName.get(d.name));
}
return (d) => scale(d.name);
})();
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);
const start = async function () {
for (const keyframe of keyframes) {
const transition = svg
.transition()
.duration(duration)
.ease(d3.easeLinear);
console.log("iteration..");
// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);
updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);
await transition.end();
}
};
start();
});
</script>
</body>
</html>
I have altered the code to run without observablehq's environment, but, after running the code, nothing was showing on the web page.
The console log shows that the data processing logic is normal, but for the rending part, it does nothing except printing iteration.
What is the problem with my code ?
You need to attach the svg you just created to some element in the HTML to see the output.
d3.create just creates a detached element. Since you are not attaching it anywhere, we are not seeing the output in the screen.
We can use d3.select to select where we want to place this chart and use then .append.
d3.select('#chart').append('svg')
const fileUrl =
'https://static.observableusercontent.com/files/aec3792837253d4c6168f9bbecdf495140a5f9bb1cdb12c7c8113cec26332634a71ad29b446a1e8236e0a45732ea5d0b4e86d9d1568ff5791412f093ec06f4f1?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27category-brands.csv';
let margin = {
top: 16,
right: 6,
bottom: 6,
left: 0,
};
let barSize = 48;
let n = 12;
let width = 1600;
let duration = 250;
let height = margin.top + barSize * n + margin.bottom;
d3.csv(fileUrl).then((data) => {
const x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
const y = d3
.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1);
const names = new Set(data.map((d) => d.name));
let datevalues = Array.from(
d3.rollup(
data,
([d]) => +d.value,
(d) => d.date,
(d) => d.name
)
);
datevalues = datevalues
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b));
function rank(value) {
const data = Array.from(names, (name) => ({
name,
value: value(name),
}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
const k = 10;
const keyframes = (function () {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank((name) => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t),
]);
}
}
keyframes.push([new Date(kb), rank((name) => b.get(name) || 0)]);
return keyframes;
})();
console.log('total frames:', keyframes.length);
console.log('total frames:', keyframes);
let nameframes = d3.groups(
keyframes.flatMap(([, data]) => data),
(d) => d.name
);
console.log('name frames number:', nameframes.length);
console.log('name frames:', nameframes);
let prev = new Map(
nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a]))
);
let next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));
console.log('pref:', prev);
console.log('next:', next);
function bars(svg) {
let bar = svg.append('g').attr('fill-opacity', 0.6).selectAll('rect');
return ([date, data], transition) =>
(bar = bar
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append('rect')
.attr('fill', color)
.attr('height', y.bandwidth())
.attr('x', x(0))
.attr('y', (d) => y((prev.get(d) || d).rank))
.attr('width', (d) => x((prev.get(d) || d).value) - x(0)),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr('y', (d) => y((next.get(d) || d).rank))
.attr('width', (d) => x((next.get(d) || d).value) - x(0))
)
.call((bar) =>
bar
.transition(transition)
.attr('y', (d) => y(d.rank))
.attr('width', (d) => x(d.value) - x(0))
));
}
function labels(svg) {
let label = svg
.append('g')
.style('font', 'bold 12px var(--sans-serif)')
.style('font-variant-numeric', 'tabular-nums')
.attr('text-anchor', 'end')
.selectAll('text');
return ([date, data], transition) =>
(label = label
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append('text')
.attr(
'transform',
(d) =>
`translate(${x((prev.get(d) || d).value)},${y(
(prev.get(d) || d).rank
)})`
)
.attr('y', y.bandwidth() / 2)
.attr('x', -6)
.attr('dy', '-0.25em')
.text((d) => d.name)
.call((text) =>
text
.append('tspan')
.attr('fill-opacity', 0.7)
.attr('font-weight', 'normal')
.attr('x', -6)
.attr('dy', '1.15em')
),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr(
'transform',
(d) =>
`translate(${x((next.get(d) || d).value)},${y(
(next.get(d) || d).rank
)})`
)
.call((g) =>
g
.select('tspan')
.tween('text', (d) =>
textTween(d.value, (next.get(d) || d).value)
)
)
)
.call((bar) =>
bar
.transition(transition)
.attr('transform', (d) => `translate(${x(d.value)},${y(d.rank)})`)
.call((g) =>
g
.select('tspan')
.tween('text', (d) =>
textTween((prev.get(d) || d).value, d.value)
)
)
));
}
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function (t) {
this.textContent = formatNumber(i(t));
};
}
formatNumber = d3.format(',d');
function axis(svg) {
const g = svg.append('g').attr('transform', `translate(0,${margin.top})`);
const axis = d3
.axisTop(x)
.ticks(width / 160)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));
return (_, transition) => {
g.transition(transition).call(axis);
g.select('.tick:first-of-type text').remove();
g.selectAll('.tick:not(:first-of-type) line').attr('stroke', 'white');
g.select('.domain').remove();
};
}
function ticker(svg) {
const now = svg
.append('text')
.style('font', `bold ${barSize}px var(--sans-serif)`)
.style('font-variant-numeric', 'tabular-nums')
.attr('text-anchor', 'end')
.attr('x', width - 6)
.attr('y', margin.top + barSize * (n - 0.45))
.attr('dy', '0.32em')
.text(formatDate(keyframes[0][0]));
return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
let formatDate = d3.utcFormat('%Y');
let color = (function () {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some((d) => d.category !== undefined)) {
const categoryByName = new Map(data.map((d) => [d.name, d.category]));
scale.domain(categoryByName.values());
return (d) => scale(categoryByName.get(d.name));
}
return (d) => scale(d.name);
})();
const svg = d3.select('#chart').append('svg').attr('viewBox', [0, 0, width, height]);
const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);
const start = async function () {
for (const keyframe of keyframes) {
const transition = svg
.transition()
.duration(duration)
.ease(d3.easeLinear);
// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);
updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);
await transition.end();
}
};
start();
});
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="chart"></div>
Related
I'm trying to add two d3 visuals to my index.html page, but only one of the visuals displays at a time. The two visuals use the same code and the only difference is the data used. They work well individually, but I am unable to get them to appear on the same page together.
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 800;
var width = 500;
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: 'Gaia'},
{id:'Ouranos', parents:['Gaia']}
],
[
{id:'Aphrodite', parents:['Ouranos']},
{id: 'Themis', parents: ['Gaia', 'Ouranos']},
{id: 'Mnemosyne', parents: ['Gaia', 'Ouranos']},
{id: 'Hyperion', parents: ['Gaia', 'Ouranos']},
{id: 'Thea', parents: ['Gaia', 'Ouranos']},
{id: 'Crius', parents: ['Gaia', 'Ouranos']},
{id: 'Oceanus', parents: ['Gaia', 'Ouranos']},
{id: 'Tethys', parents: ['Gaia', 'Ouranos']},
{id: 'Iapetus', parents: ['Gaia', 'Ouranos']},
{id: 'Coeus', parents: ['Gaia', 'Ouranos']},
{id: 'Phoebe', parents: ['Gaia','Ouranos']},
{id: 'Kronos', parents: ['Gaia', 'Ouranos']},
{id: 'Rhea', parents: ['Gaia', 'Ouranos']},
],
[
{id: 'Pleione', parents:['Oceanus','Tethys']},
{id:'Atlas',parents:['Iapetus']},
{id:'Semele'},
{id:'Maia', parents:['Pleione','Atlas']},
{id:'Leto',parents:['Coeus','Phoebe']},
{id:'Zeus',parents:['Kronos','Rhea']},
{id:'Hera',parents:['Kronos','Rhea']},
{id:'Poseidon',parents:['Kronos','Rhea']},
{id:'Amphitrite'},
{id:'Hestia',parents:['Kronos','Rhea']},
{id:'Hades',parents:['Kronos','Rhea']},
{id:'Demeter',parents:['Kronos','Rhea']},
],
[
{id:'Dionysus',parents:['Semele','Zeus']},
{id:'Hermes',parents:['Maia','Zeus']},
{id:'Apollo',parents:['Leto','Zeus']},
{id:'Artemis',parents:['Leto','Zeus']},
{id:'Athena',parents:['Zeus']},
{id:'Ares',parents:['Zeus','Hera']},
{id:'Hephaistos',parents:['Zeus','Hera']},
{id:'Hebe',parents:['Zeus','Hera']},
{id:'Triton',parents:['Poseidon','Amphitrite']},
{id:'Benthesikyme',parents:['Poseidon','Amphitrite']},
{id:'Rhodos',parents:['Poseidon','Amphitrite']},
{id:'Persephone',parents:['Zeus','Demeter']},
{id:'Zagreus', parents:['Hades','Persephone']},
{id:'Macaria', parents:['Hades','Persephone']}
]
]
// 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 = 20
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) => {
//nodeG1 are nodes that have children
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", "lightblue")
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 1)))).toString(16);
})
.attr("r", 6);
//nodeG11 are nodes that have parents
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", "gray")
.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", "papyrus")
.attr("font-size", 16)
.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 '#' + 0;
});
let nodeG22 = svg.append("g")
.attr("font-family", "papyrus")
.attr("font-size", 16)
.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 '#' + 0;
});
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", 10)
.attr("stroke-width", .75)
.attr("stroke", (d) => {
return '#' + Math.floor(16776960 * 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="greek.css"/>
<link rel="stylesheet" type="text/css" href="main.css"/>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Here</title>
</head>
<body style="background-color: antiquewhite;">
<center>
<h style="font-size:50px">Greek and Norse Gods: </h>
</center>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="greek.js"></script>
<script src="norse.js"></script>
</body>
</html>
Is there a way to get the two visuals to appear side-by-side?
Looks like there's only one svg element. I think you're appending both tree structures to the same svg and element and overwriting the first one with the second. Try to create two svg elements and seperately append the greek and norse nodes. Didn't have a chance to test it, however, maybe it helps.
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:
i'm using d3 library alongside with react to create family tree layout and i did, but the problem is i want to flip it to make the root lie down and also want make the size look good on different devices just like this tree Family Tree because the tree could become bigger and bigger and contains more than 500 names and also how can i control the text orientation to make it look like the attached image
note:- i'm using ResizeObserver API and i think this what makes the tree looks squashed on mobile devices but when i try to remove it, the problem still exist and i don't know what to do
import {useEffect, useRef} from 'react'
import {select,hierarchy, stratify, tree,linkVertical, selectAll} from 'd3'
import { useHistory} from 'react-router'
import useResizeObserver from './useResizeObserver'
const Tree = ({familyData,isProfile}) => {
const wrapperRef = useRef()
const dimension = useResizeObserver(wrapperRef)
const history = useHistory()
const genColors = [
'#D92027','#DEEEEA', '#BF1363',
'#FFF5B7','#94D0CC','#7B6079',
'#FF8882','#45526C','#FFC93C',
'#FFB037','#EFF7E1','#839B97',
'#CEE397','#F5A25D','#625261',
'#87556F','#E5EDB7','#231E23',
'#6F0000','#FFF0F5','#FFEBD9',
'#BEEBE9','#B0A160','#E4F9FF',
]
useEffect(() => {
if(!dimension) return;
wrapperRef.current.innerHTML = '';
const dataStructure = stratify().id(id => id._id).parentId(id => id.parentId)(familyData)
const root = hierarchy(dataStructure)
const rootTree = tree().size([dimension.width, dimension.height])
rootTree(root)
const linkGenerator = linkVertical().x(node => node.x).y(node => node.y)
const svg = select(wrapperRef.current).append('svg').attr('width','4000').attr('height','650')
svg.selectAll('.node')
.data(root.descendants())
.join('circle')
.attr('r', 5)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('fill', (d) => {
return genColors[d.depth]
})
.attr('opacity', 0)
.transition()
.duration(1500)
.delay(d => d.data.depth * 500)
.attr('opacity', 1)
svg.selectAll('.leaf')
.data(root.descendants())
.join('image')
.attr('href','/image/leaf.png')
.attr('width', '1.5rem')
.style('display', (d) => {
if(!d.children) return 'inline'
else return 'none'
})
.attr('x', d => d.x - 5)
.attr('y', d => d.y + 15)
.attr('opacity', 0)
.transition()
.duration(1500)
.delay(d => d.data.depth * 500)
.attr('opacity', 1)
svg.selectAll('.link')
.data(root.links())
.join('path')
.attr('fill', 'none')
.attr('d', linkGenerator)
.attr('stroke', '#555')
.attr('stroke-width', '2')
.attr('opacity', '0.5')
.attr('id', d => 'link_' + d.target.data.data._id)
.attr('stroke-dasharray', function(){
const length = this.getTotalLength()
return `${length} ${length}`
})
.attr('stroke-dashoffset', function(){
const length = this.getTotalLength()
return length
})
.transition()
.duration(1500)
.delay(d => d.source.depth * 500)
.attr('stroke-dashoffset', 0)
svg.selectAll('.name')
.data(root.descendants())
.join('text')
.text(d => d.data.data.firstName)
.attr('x', d => d.x + 10)
.attr('y', d => d.y + 15)
.attr('fill', '#009688')
.attr('id', d => 'name_' + d.data.data._id)
.style('cursor', 'pointer')
.style('font-size', '1.2rem')
.style(' z-index', '9999999')
.on('mouseover', (e, d) => {
selectAll('path').style('stroke', '#2c3e50')
selectAll('text').style('fill', '#2c3e50')
selectAll('circle').style('fill', '#2c3e50')
selectAll('image').style('opacity', '0.1')
while(d){
if(!d.data.parentId ) {
select(`#name_${d.data.data._id}`).style('fill','#f39c12')
}
if(d.data.parentId !== null){
select(`#link_${d.data.data._id}`).style('stroke','#e74c3c');
select(`#name_${d.data.data._id}`).style('fill','#f39c12')
.style('font-size', '1.2rem')
.transition()
.duration(500)
.style('font-size', '2.5rem');
}
d = d.parent
}
})
.on('mouseout', (e, d) => {
selectAll('path').style('stroke', '#555')
selectAll('text').style('fill', '#009688').style('font-size', '1.2rem')
selectAll('image').style('opacity', '1')
selectAll('circle').style('fill', (d) => {
return genColors[d.depth]
})
})
.on('click', (e, d) => {
if(!isProfile){
history.push(`/info/${d.data.data._id}`)
}
})
.attr('opacity', 0)
.transition()
.duration(1500)
.delay(d => d.data.depth * 500)
.attr('opacity', 1)
},[familyData, dimension, history, genColors,isProfile])
return (
<div ref={wrapperRef} className='tree__wrapper'></div>
)
}
export default Tree
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
and also this is my useResizeObserver Hook
import { useState, useEffect } from "react"
const useResizeObserver = ref => {
const [dimension, setDimension] = useState(null)
useEffect(() => {
const targetElement = ref.current
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
setDimension(entry.contentRect)
})
})
observer.observe(targetElement)
return () => {
observer.unobserve(targetElement)
}
},[ref])
return dimension
}
export default useResizeObserver
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Basically what you would have to do is invert the y values so that they decrease from the bottom of your SVG instead of increasing from the top. You should do this by using a height variable for consistency. I haven't tested the following code as I don't have the data, but if it doesn't work it should give you the general idea:
import {useEffect, useRef} from 'react'
import {select,hierarchy, stratify, tree,linkVertical, selectAll} from 'd3'
import { useHistory} from 'react-router'
import useResizeObserver from './useResizeObserver'
const Tree = ({familyData,isProfile}) => {
const height = 650 // Based on what you choose, could be a prop
const width = 4000
const wrapperRef = useRef()
const dimension = useResizeObserver(wrapperRef)
const history = useHistory()
const genColors = [
'#D92027','#DEEEEA', '#BF1363',
'#FFF5B7','#94D0CC','#7B6079',
'#FF8882','#45526C','#FFC93C',
'#FFB037','#EFF7E1','#839B97',
'#CEE397','#F5A25D','#625261',
'#87556F','#E5EDB7','#231E23',
'#6F0000','#FFF0F5','#FFEBD9',
'#BEEBE9','#B0A160','#E4F9FF',
]
useEffect(() => {
if(!dimension) return;
wrapperRef.current.innerHTML = '';
const dataStructure = stratify().id(id => id._id).parentId(id => id.parentId)(familyData)
const root = hierarchy(dataStructure)
const rootTree = tree().size([dimension.width, dimension.height])
rootTree(root)
const linkGenerator = linkVertical()
.x(node => node.x) // X coord doesn't change
.y(node => height - node.y) // Y coordinate decreases instead of increasing
const svg = select(wrapperRef.current)
.append('svg')
.attr('width', width) // Use variable for width (optional, but good practice)
.attr('height', height) // Use your variable for height
svg.selectAll('.node')
.data(root.descendants())
.join('circle')
.attr('r', 5)
.attr('cx', d => d.x)
.attr('cy', d => height - d.y) // subtract the y coord from the height
.attr('fill', (d) => {
return genColors[d.depth]
})
.attr('opacity', 0)
.transition()
.duration(1500)
.delay(d => d.data.depth * 500)
.attr('opacity', 1)
svg.selectAll('.leaf')
.data(root.descendants())
.join('image')
.attr('href','/image/leaf.png')
.attr('width', '1.5rem')
.style('display', (d) => {
if(!d.children) return 'inline'
else return 'none'
})
.attr('x', d => d.x - 5)
.attr('y', d => (height - d.y) + 15) // Again here subtract
.attr('opacity', 0)
.transition()
.duration(1500)
.delay(d => d.data.depth * 500)
.attr('opacity', 1)
svg.selectAll('.link')
.data(root.links())
.join('path')
.attr('fill', 'none')
.attr('d', linkGenerator)
.attr('stroke', '#555')
.attr('stroke-width', '2')
.attr('opacity', '0.5')
.attr('id', d => 'link_' + d.target.data.data._id)
.attr('stroke-dasharray', function(){
const length = this.getTotalLength()
return `${length} ${length}`
})
.attr('stroke-dashoffset', function(){
const length = this.getTotalLength()
return length
})
.transition()
.duration(1500)
.delay(d => d.source.depth * 500)
.attr('stroke-dashoffset', 0)
svg.selectAll('.name')
.data(root.descendants())
.join('text')
.text(d => d.data.data.firstName)
.attr('x', d => d.x + 10)
.attr('y', d => (height - d.y) + 15) // And again
.attr('fill', '#009688')
.attr('id', d => 'name_' + d.data.data._id)
.style('cursor', 'pointer')
.style('font-size', '1.2rem')
.style(' z-index', '9999999')
.on('mouseover', (e, d) => {
selectAll('path').style('stroke', '#2c3e50')
selectAll('text').style('fill', '#2c3e50')
selectAll('circle').style('fill', '#2c3e50')
selectAll('image').style('opacity', '0.1')
while(d){
if(!d.data.parentId ) {
select(`#name_${d.data.data._id}`).style('fill','#f39c12')
}
if(d.data.parentId !== null){
select(`#link_${d.data.data._id}`).style('stroke','#e74c3c');
select(`#name_${d.data.data._id}`).style('fill','#f39c12')
.style('font-size', '1.2rem')
.transition()
.duration(500)
.style('font-size', '2.5rem');
}
d = d.parent
}
})
.on('mouseout', (e, d) => {
selectAll('path').style('stroke', '#555')
selectAll('text').style('fill', '#009688').style('font-size', '1.2rem')
selectAll('image').style('opacity', '1')
selectAll('circle').style('fill', (d) => {
return genColors[d.depth]
})
})
.on('click', (e, d) => {
if(!isProfile){
history.push(`/info/${d.data.data._id}`)
}
})
.attr('opacity', 0)
.transition()
.duration(1500)
.delay(d => d.data.depth * 500)
.attr('opacity', 1)
},[familyData, dimension, history, genColors,isProfile])
return (
<div ref={wrapperRef} className='tree__wrapper'></div>
)
}
export default Tree```
How I can drag a chart smoothly?
The chart is construct from two charts that candlestick and line chart.
The line chart is corresponding to candle chart, The line indicates low value of each candle.
these charts are overlap on a graph area.
This graph is able to drag horizontally, but it is very choppy.
About 750 candles does exists in my chart.
Drag become smoothly when the line chart is disabled, so seems like it caused by line chart.
I suppose to this graph taking time to redraw all of path commands in line.
But I dont know what make it choppy actually.
How I can fix it?
margin = {top: 0, bottom: 20, left: 40, right: 20}
const f = async () => {
let num
let res = await fetch("https://gist.githubusercontent.com/KiYugadgeter/f2f861798257118cb420c9cdb1f830f6/raw/b2c4217e569b3b2064f88bb7ac2f8a1e328ff516/data2.csv")
let d = await res.text()
let min_value = 999999999
let max_value = 0
data = parsed_data = d3.csvParse(d, (dt) => {
const j = dt.date.split("-").map((i) => {
return parseInt(i)
})
const date = new Date(...j)
high_value = parseFloat(dt.high)
low_value = parseFloat(dt.low)
open_value = parseFloat(dt.open)
close_value = parseFloat(dt.close)
if (low_value < min_value) {
min_value = low_value
}
if (high_value > max_value) {
max_value = high_value
}
return {
open: open_value,
close: close_value,
high: high_value,
low: low_value,
date: date
}
})
return {data: data, min: min_value, max: max_value}
}
f().then((d) => {
let num = 0
let recently_date = d.data[d.data.length-1].date
let minvalue = 99999999999
let maxvalue = 0
recently_date.setMinutes(recently_date.getMinutes() - (recently_date.getMinutes() % 30))
recently_date.setSeconds(0)
recently_date.setMilliseconds(0)
const limit_date = (recently_date - (60 * 90 * 1000))
const data = d.data.filter((d) => {
num++
if (d.low < minvalue) {
minvalue = d.low
}
if (d.high > maxvalue) {
maxvalue = d.high
}
return true
})
const svg = d3.select("svg")
const xScale = d3.scaleTime().domain(
[
limit_date,
recently_date
]
).nice().range([margin.left, 600-margin.left-margin.right])
const yScale = d3.scaleLinear().domain([(maxvalue - (maxvalue%1000) + 1000), (minvalue - (minvalue%1000) - 1000)]).range([0, 410-margin.top-margin.bottom])
//console.log(recently_date, limit_date)
const canvas = svg.append("g").attr("width", 600).attr("height", 410)
const xaxis = d3.axisBottom(xScale).ticks().tickFormat(d3.timeFormat("%H:%M"))
//console.log(canvas.attr("width"))
//console.log(svg.node().width.baseVal.value-300)
const clip = svg.append("clipPath").attr("id", "clip").append("rect").attr("width", 600-margin.left-margin.right).attr("height", 410-margin.bottom-margin.top).attr("x", margin.left).attr("y", 0)
const g = canvas // Data group
.append("g")
.attr("stroke-linecap", "square")
.attr("stroke", "black")
.attr("clip-path", "url(#clip)")
.selectAll("g")
.data(data)
.join("g")
.classed("ticks", true)
/*.attr("transform", (d) => {
return `translate(${xScale(d.date)},0)`
}
)*/
g.append("line")
.attr("y1", (d) => yScale(d.high))
.attr("y2", (d) => yScale(d.low))
.attr("x1", (d) => xScale(d.date))
.attr("x2", (d) => xScale(d.date))
.attr("stroke-width", 0.6)
.classed("line_high_low", true)
g.append("line")
.attr("y1", (d) => yScale(d.open))
.attr("y2", (d) => yScale(d.close))
.attr("x1", (d) => xScale(d.date))
.attr("x2", (d) => xScale(d.date))
.attr("stroke-width", 3)
.attr("stroke", (d) => d.open > d.close ? d3.schemeSet1[0]
: d.close > d.open ? d3.schemeSet1[2] : d3.schemeSet1[8]
)
.classed("line_open_close", true)
const linefunc = (xScale, yScale) => {
return d3.line()
.x((d) => {
if (isNaN(d.date)) {
return 0
}
let retval = xScale(d.date)
return retval
})
.y((d) => {
if (isNaN(d.low)) {
return 0
}
let retval = yScale(d.low)
return retval
})
}
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#00ff00")
.attr("d", linefunc(xScale, yScale))
const group_x = canvas
.append("g")
.attr("transform", "translate(0," + String(410-margin.top - margin.bottom) + ")") // X axis
.call(xaxis).style("font-size", "5")
const yaxis = d3.axisLeft(yScale)
const group_y = canvas
.append("g")
.attr("class", "axis axis--y")
.attr("transform", "translate(" + String(margin.left) + ",0)") // Y axis
.call(yaxis)
.style("font-size", "5")
zoom_func = d3.zoom().on("zoom", (e) => {
let newX = e.transform.rescaleX(xScale)
let newscale = xaxis.scale(newX)
group_x.call(newscale)
//g.selectAll(".ticks").data(data).join(".ticks line").attr("x1", (d) => {
g.selectAll("line").join("line").attr("x1", (d) => {
return newX(d.date)
}).attr("x2", (d) => {return newX(d.date)})
g.selectAll("path").attr("transform", `translate(${e.transform.x} 0)`)
//.attr("transform", (d) => `translate({$newX(d.date)}, 0)`) this is not use
})
svg.call(zoom_func)
})
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script src="node_modules/d3/dist/d3.min.js"></script>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="index.css">
</head>
<body>
<svg id="svg" viewBox="0 0 600 410">
</svg>
<script src="https://d3js.org/d3.v6.min.js"></script>
</body>
</html>
I am working on the d3 treemap v5 in which I need to persist the state of the treemap in localstorage on each user click. My code is in https://codesandbox.io/s/d3-treemap-wfbtg
When a user clicks the top parent tile it is drilled down to the children tiles. How to persist that in local storage when the user reloads the browser he wants to see the drilled down children tiles.
class Treegraph extends React.Component {
createTreeChart = () => {
const width = 550;
const height = 500;
var paddingAllowance = 2;
const format = d3.format(",d");
const checkLowVal = d => {
console.log("ChecklowVal", d);
if (d.value < 2) {
return true;
}
};
const name = d =>
d
.ancestors()
.reverse()
.map(d => d.data.name)
.join(" / ");
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + (child.x0 / width) * (x1 - x0);
child.x1 = x0 + (child.x1 / width) * (x1 - x0);
child.y0 = y0 + (child.y0 / height) * (y1 - y0);
child.y1 = y0 + (child.y1 / height) * (y1 - y0);
}
}
const treemap = data =>
d3.treemap().tile(tile)(
d3
.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value)
);
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "16px sans-serif");
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
let group = svg.append("g").call(render, treemap(data));
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node
.filter(d => (d === root ? d.parent : d.children))
.attr("cursor", "pointer")
.on("click", d => (d === root ? zoomout(root) : zoomin(d)));
var tool = d3
.select("body")
.append("div")
.attr("class", "toolTip");
d3.select(window.frameElement).style("height", height - 20 + "px");
d3.select(window.frameElement).style("width", width - 20 + "px");
node
.append("rect")
.attr("id", d => (d.leafUid = "leaf"))
.attr("fill", d =>
d === root ? "#fff" : d.children ? "#045c79" : "#045c79"
)
.attr("stroke", "#fff")
.on("mousemove", function(d) {
tool.style("left", d3.event.pageX + 10 + "px");
tool.style("top", d3.event.pageY - 20 + "px");
tool.style("display", "inline-block");
tool.html(`${d.data.name}<br />(${format(d.data.value)})`);
})
.on("click", function(d) {
tool.style("display", "none");
})
.on("mouseout", function(d) {
tool.style("display", "none");
});
node
.append("foreignObject")
.attr("class", "foreignObject")
.attr("width", function(d) {
return d.dx - paddingAllowance;
})
.attr("height", function(d) {
return d.dy - paddingAllowance;
})
.append("xhtml:body")
.attr("class", "labelbody")
.append("div")
.attr("class", "label")
.text(function(d) {
return d.name;
})
.attr("text-anchor", "middle");
node
.append("clipPath")
.attr("id", d => (d.clipUid = "clip"))
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node
.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => (d === root ? "bold" : null))
.attr("font-size", d => {
if (d === root) return "0.8em";
const width = x(d.x1) - x(d.x0),
height = y(d.y1) - y(d.y0);
return Math.max(
Math.min(
width / 5,
height / 2,
Math.sqrt(width * width + height * height) / 25
),
9
);
})
.attr("text-anchor", d => (d === root ? null : "middle"))
.attr("transform", d =>
d === root
? null
: `translate(${(x(d.x1) - x(d.x0)) / 2}, ${(y(d.y1) - y(d.y0)) /
2})`
)
.selectAll("tspan")
.data(d =>
d === root
? name(d).split(/(?=\/)/g)
: checkLowVal(d)
? d.data.name.split(/(\s+)/).concat(format(d.data.value))
: d.data.name.split(/(\s+)/).concat(format(d.data.value))
)
.join("tspan")
.attr("x", 3)
.attr(
"y",
(d, i, nodes) =>
`${(i === nodes.length - 1) * 0.3 + (i - nodes.length / 2) * 0.9}em`
)
.text(d => d);
node
.selectAll("text")
.classed("text-title", d => d === root)
.classed("text-tile", d => d !== root)
.filter(d => d === root)
.selectAll("tspan")
.attr("y", "1.1em")
.attr("x", undefined);
group.call(position, root);
}
function position(group, root) {
group
.selectAll("g")
.attr("transform", d =>
d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`
)
.select("rect")
.attr("width", d => (d === root ? width : x(d.x1) - x(d.x0)))
.attr("height", d => (d === root ? 30 : y(d.y1) - y(d.y0)));
}
// When zooming in, draw the new nodes on top, and fade them in.
function zoomin(d) {
console.log("The zoomin func", d.data);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
const group0 = group.attr("pointer-events", "none");
const group1 = (group = svg.append("g").call(render, d));
svg
.transition()
.duration(750)
.call(t =>
group0
.transition(t)
.remove()
.call(position, d.parent)
)
.call(t =>
group1
.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d)
);
}
// When zooming out, draw the old nodes on top, and fade them out.
function zoomout(d) {
console.log("The zoomout func", d.parent.data);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
const group0 = group.attr("pointer-events", "none");
const group1 = (group = svg.insert("g", "*").call(render, d.parent));
svg
.transition()
.duration(750)
.call(t =>
group0
.transition(t)
.remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d)
)
.call(t => group1.transition(t).call(position, d.parent));
}
return svg.node();
};
componentDidMount() {
this.createTreeChart();
}
render() {
return (
<React.Fragment>
<div id="chart" />
</React.Fragment>
);
}
}
Here is a quick idea, store the name as ID on local storage. Hopefully name is unique, otherwise make sure you have an unique ID for each node.
https://codesandbox.io/s/d3-treemap-4ehbf?file=/src/treegraph.js
first load localstorage for lastSelection with last selected node name
componentDidMount() {
const lastSelection = localStorage.getItem("lastSelection");
console.log("lastSelection:", lastSelection);
this.createTreeChart(lastSelection);
}
add a parameter to you createTreeChart so when it loads and there is a lastaSelection you have to reload that state
createTreeChart = (lastSelection = null) => {
decouple your treemap in a variable, filter the lastSelection node and zoomin into it. this step could be improved, not sure if the refreshing zoomin is interesting.
const tree = treemap(data);
let group = svg.append("g").call(render, tree);
if (lastSelection !== null) {
const lastNode = tree
.descendants()
.find(e => e.data.name === lastSelection);
zoomin(lastNode);
}
finally update localStorage on node click.
node
.filter(d => (d === root ? d.parent : d.children))
.attr("cursor", "pointer")
.on("click", d => {
if (d === root) {
localStorage.setItem("lastSelection", null);
zoomout(root);
} else {
localStorage.setItem("lastSelection", d.data.name);
zoomin(d);
}
});
This could be improved, but it's a starting point.