A way to drag a chart smoothly in d3js - javascript

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>

Related

Enter, update, and exit selections in join() from a dropdown menu

I am currently testing out d3's join, enter, update, exit and desiring to produce something like
chart update 1 or chart update 2.
To achieve this, I have built a HTML dropdown with select element from a dataset and I expect to wire up the dropdown to the viz to achieve at least updated values as per the dropdown selection. But it is failing.
It is a hard concept to grasp and I am not sure where the code is failing. If any value is selected from the dropdown, the chart does not update at all.
////////////////////////////////////////////////////////////
//////////////////////// 00 BUILD DATA//////// /////////////
////////////////////////////////////////////////////////////
//desired permutation length
const length = 4;
//build array from the above length
const perm = Array.from(Array(length).keys()).map((d) => d + 1);
//generate corresponding alphabets for name
const name = perm.map((x) => String.fromCharCode(x - 1 + 65));
//permutation function - https://stackoverflow.com/questions/9960908/permutations-in-javascript/24622772#24622772
function permute(permutation) {
var length = permutation.length,
result = [permutation.slice()],
c = new Array(length).fill(0),
i = 1,
k, p;
while (i < length) {
if (c[i] < i) {
k = i % 2 && c[i];
p = permutation[i];
permutation[i] = permutation[k];
permutation[k] = p;
++c[i];
i = 1;
result.push(permutation.slice());
} else {
c[i] = 0;
++i;
}
}
return result;
};
//generate permutations
const permut = permute(perm);
//generate year based on permutation
const year = permut.map((x, i) => i + 2000);
//generate a yearly constant based on year to generate final value as per the rank {year-name}
const constant = year.map(d => Math.round(d * Math.random()));
const src =
year.map((y, i) => {
return name.map((d, j) => {
return {
Name: d,
Year: y,
Rank: permut[i][j],
Const: constant[i],
Value: Math.round(constant[i] / permut[i][j])
};
});
}).flat();
//console.log(src);
////////////////////////////////////////////////////////////
//////////////////////// 0 BUILD HTML DROPDOWN /////////////
////////////////////////////////////////////////////////////
d3.select('body')
.append('div', 'dropdown')
.style('position', 'absolute')
.style('top', '400px')
.append('select')
.attr('name', 'input')
.classed('Year', true)
.selectAll('option')
.data(year)
.enter()
.append('option')
//.join('option')
.text((d) => d)
.attr("value", (d) => d )
//get the dropdown value
const filterYr = parseFloat(d3.select('.Year').node().value);
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA WRANGLING //////////////////
////////////////////////////////////////////////////////////
const xAccessor = (d) => d.Year;
const yAccessor = (d) => d.Value;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SVG //////////////////////
////////////////////////////////////////////////////////////
//namespace
//define dimension
const width = 1536;
const height = 720;
const svgns = "http://www.w3.org/2000/svg";
const svg = d3.select("svg");
svg.attr("xmlns", svgns).attr("viewBox", `0 0 ${width} ${height}`);
svg
.append("rect")
.attr("class", "vBoxRect")
//.style("overflow", "visible")
.attr("width", `${width}`)
.attr("height", `${height}`)
.attr("stroke", "black")
.attr("fill", "white");
////////////////////////////////////////////////////////////
//////////////////////// 3 CREATE BOUND ////////////////////
////////////////////////////////////////////////////////////
const padding = {
top: 70,
bottom: 100,
left: 120,
right: 120
};
const multiplierH = 1; //controls the height of the visual container
const multiplierW = 1; //controls the width of the visual container
const boundHeight = height * multiplierH - padding.top - padding.bottom;
const boundWidth = width * multiplierW - padding.right - padding.left;
//create BOUND rect -- to be deleted later
svg
.append("rect")
.attr("class", "boundRect")
.attr("x", `${padding.left}`)
.attr("y", `${padding.top}`)
.attr("width", `${boundWidth}`)
.attr("height", `${boundHeight}`)
.attr("fill", "white");
//create bound element
const bound = svg
.append("g")
.attr("class", "bound")
.style("transform", `translate(${padding.left}px,${padding.top}px)`);
function draw() {
// filter data as per dropdown
const data = src.filter(a => a.Year == filterYr);
////////////////////////////////////////////////////////////
//////////////////////// 4 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3
.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, xAccessor));
const scaleY = d3
.scaleLinear()
.range([boundHeight, 0])
.domain(d3.extent(data, yAccessor));
bound.append('g')
.classed('textContainer', true)
.selectAll('text')
.data(data)
.join(
enter => enter.append('text')
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "blue"),
update =>
update
.transition()
.duration(500)
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "red")
/*,
(exit) =>
exit
.style("fill", "black")
.transition()
.duration(1000)
.attr("transform", (d, i) => `translate(${300},${30 + i * 30})`)
.remove()*/
)
}
draw();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<!--d3 script-->
<script type="text/javascript">
</script>
</body>
</html>
You have a dropdown, but you're not listening to it. For example:
select.on("change", () => {
const filterYr = parseFloat(d3.select('.Year').node().value);
draw(filterYr);
});
Which alternatively can also be:
select.on("change", event => {
const filterYr = +event.currentTarget.value;
draw(filterYr);
});
Note that I'm passing filterYr to the draw() function as an argument. Also, do not append the containing <g> inside draw(), otherwise you'll have only the enter selection.
Here's your code with those changes:
////////////////////////////////////////////////////////////
//////////////////////// 00 BUILD DATA//////// /////////////
////////////////////////////////////////////////////////////
//desired permutation length
const length = 4;
//build array from the above length
const perm = Array.from(Array(length).keys()).map((d) => d + 1);
//generate corresponding alphabets for name
const name = perm.map((x) => String.fromCharCode(x - 1 + 65));
//permutation function - https://stackoverflow.com/questions/9960908/permutations-in-javascript/24622772#24622772
function permute(permutation) {
var length = permutation.length,
result = [permutation.slice()],
c = new Array(length).fill(0),
i = 1,
k, p;
while (i < length) {
if (c[i] < i) {
k = i % 2 && c[i];
p = permutation[i];
permutation[i] = permutation[k];
permutation[k] = p;
++c[i];
i = 1;
result.push(permutation.slice());
} else {
c[i] = 0;
++i;
}
}
return result;
};
//generate permutations
const permut = permute(perm);
//generate year based on permutation
const year = permut.map((x, i) => i + 2000);
//generate a yearly constant based on year to generate final value as per the rank {year-name}
const constant = year.map(d => Math.round(d * Math.random()));
const src =
year.map((y, i) => {
return name.map((d, j) => {
return {
Name: d,
Year: y,
Rank: permut[i][j],
Const: constant[i],
Value: Math.round(constant[i] / permut[i][j])
};
});
}).flat();
//console.log(src);
////////////////////////////////////////////////////////////
//////////////////////// 0 BUILD HTML DROPDOWN /////////////
////////////////////////////////////////////////////////////
const select = d3.select('body')
.append('div', 'dropdown')
.style('position', 'absolute')
.style('top', '400px')
.append('select')
.attr('name', 'input')
.classed('Year', true);
select.selectAll('option')
.data(year)
.enter()
.append('option')
//.join('option')
.text((d) => d)
.attr("value", (d) => d)
//get the dropdown value
const filterYr = parseFloat(d3.select('.Year').node().value);
select.on("change", event => {
const filterYr = +event.currentTarget.value;
draw(filterYr);
});
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA WRANGLING //////////////////
////////////////////////////////////////////////////////////
const xAccessor = (d) => d.Year;
const yAccessor = (d) => d.Value;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SVG //////////////////////
////////////////////////////////////////////////////////////
//namespace
//define dimension
const width = 1536;
const height = 720;
const svgns = "http://www.w3.org/2000/svg";
const svg = d3.select("svg");
svg.attr("xmlns", svgns).attr("viewBox", `0 0 ${width} ${height}`);
svg
.append("rect")
.attr("class", "vBoxRect")
//.style("overflow", "visible")
.attr("width", `${width}`)
.attr("height", `${height}`)
.attr("stroke", "black")
.attr("fill", "white");
////////////////////////////////////////////////////////////
//////////////////////// 3 CREATE BOUND ////////////////////
////////////////////////////////////////////////////////////
const padding = {
top: 70,
bottom: 100,
left: 120,
right: 120
};
const multiplierH = 1; //controls the height of the visual container
const multiplierW = 1; //controls the width of the visual container
const boundHeight = height * multiplierH - padding.top - padding.bottom;
const boundWidth = width * multiplierW - padding.right - padding.left;
//create BOUND rect -- to be deleted later
svg
.append("rect")
.attr("class", "boundRect")
.attr("x", `${padding.left}`)
.attr("y", `${padding.top}`)
.attr("width", `${boundWidth}`)
.attr("height", `${boundHeight}`)
.attr("fill", "white");
//create bound element
const bound = svg
.append("g")
.attr("class", "bound")
.style("transform", `translate(${padding.left}px,${padding.top}px)`);
const g = bound.append('g')
.classed('textContainer', true);
function draw(filterYr) {
// filter data as per dropdown
const data = src.filter(a => a.Year == filterYr);
////////////////////////////////////////////////////////////
//////////////////////// 4 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3
.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, xAccessor));
const scaleY = d3
.scaleLinear()
.range([boundHeight, 0])
.domain(d3.extent(data, yAccessor));
g.selectAll('text')
.data(data)
.join(
enter => enter.append('text')
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "blue"),
update =>
update
.transition()
.duration(500)
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "red")
/*,
(exit) =>
exit
.style("fill", "black")
.transition()
.duration(1000)
.attr("transform", (d, i) => `translate(${300},${30 + i * 30})`)
.remove()*/
)
}
draw(filterYr);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<!--d3 script-->
<script type="text/javascript">
</script>
</body>
</html>

Adapt observable to D3 but nothing rendering in the web page

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>

D3 line chart and adding data points real time

I am beginner to d3 and trying to create real time chart which adds in new values on the go. I want the chart to shift the old points to the left as new points are added. Below is my code but for some reason, the browser freezes with the code (I have commented out the .on() line in the end that causes the freeze).
What am I doing wrong?
<!DOCTYPE html>
<head></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js">
</script>
<script>
<!DOCTYPE html>
<head></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js">
</script>
<script>
var t = -1;
var n = 40;
var duration = 750;
var data = [];
console.log('hello');
function next() {
return {
time: ++t,
value: Math.random() * 10
}
}
var margin = {
top: 6,
right: 0,
bottom: 20,
left: 40
},
width = 560 - margin.right,
height = 120 - margin.top - margin.bottom;
var xScale = d3.scaleTime()
.domain([t - n + 1, t])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
var line = d3.line()
.x((d) => xScale(d.time))
.y((d) => yScale(d.value));
var svg = d3.select('body').append('p').append('svg');
var chartArea = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
chartArea.append('defs').append('clipPath')
.attr('id', 'clip2')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height);
chartArea.append('rect')
.attr('class', 'bg')
.attr('x', 0)
.attr('y', 0)
.attr('width', this.chartWidth)
.attr('height', this.chartHeight);
var xAxis = d3.axisBottom(xScale);
var xAxisG = chartArea.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${height})`);
xAxisG.call(xAxis);
d3.selectAll('x-axis path').style('stroke', 'red')
.style('stroke-width', 2);
var yAxis = d3.axisLeft(yScale);
var yAxisG = chartArea.append('g').attr('class', 'y-axis');
yAxisG.call(yAxis);
var grids = chartArea.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(yScale).tickSize(-(width)).tickFormat((domain, number) => {
return ""
}));
var pathsG = chartArea.append('g')
.attr('id', 'paths')
.attr('class', 'paths')
.attr('clip-path', 'url(#clip2)');
tick();
function tick() {
console.log('working');
var newValue = {
time: ++t,
value: Math.random() * 10
};
data.push(newValue);
xScale.domain([newValue.time - n + 2, newValue.time]);
xAxisG.transition().duration(500).ease(d3.easeLinear).call(xAxis);
console.log('is it?');
var minerG = pathsG.selectAll('.minerLine').data([data]);
var minerGEnter = minerG.enter()
.append('g')
.attr('class', 'minerLine')
.merge(minerG);
var minerSVG = minerGEnter.selectAll('path').data((d) => [d]);
var minerSVGEnter = minerSVG.enter()
.append('path')
.attr('class', 'line')
.merge(minerSVG)
.transition()
.duration(500)
.ease(d3.easeLinear, 2)
.attr('d', line(data))
.on('end', () => {
requestAnimationFrame(tick)
})
}
</script>
</body>
</html>
The problem is caused by the fact that tick is called immediately and synchronously at the end of the transition. The CPU process executing the JavaScript remains busy updating data and chart, and is not available to do anything else on this tab.
One way to fix this is to use Window.requestAnimationFrame().
.on('end', () => {
requestAnimationFrame(tick)
})
The updated snippet below shows this solution in action.
It does not fix other issues not mentioned in the question, like the fact that no data is shown in the chart.
<!DOCTYPE html>
<head></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js">
</script>
<script>
var t = -1;
var n = 40;
var duration = 750;
var data = [];
console.log('hello');
function next() {
return {
time: ++t,
value: Math.random() * 10
}
}
var margin = {
top: 6,
right: 0,
bottom: 20,
left: 40
},
width = 560 - margin.right,
height = 120 - margin.top - margin.bottom;
var xScale = d3.scaleTime()
.domain([t - n + 1, t])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
var line = d3.line()
.x((d) => xScale(d.time))
.y((d) => yScale(d.value));
var svg = d3.select('body').append('p').append('svg');
var chartArea = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
chartArea.append('defs').append('clipPath')
.attr('id', 'clip2')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height);
chartArea.append('rect')
.attr('class', 'bg')
.attr('x', 0)
.attr('y', 0)
.attr('width', this.chartWidth)
.attr('height', this.chartHeight);
var xAxis = d3.axisBottom(xScale);
var xAxisG = chartArea.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${height})`);
xAxisG.call(xAxis);
d3.selectAll('x-axis path').style('stroke', 'red')
.style('stroke-width', 2);
var yAxis = d3.axisLeft(yScale);
var yAxisG = chartArea.append('g').attr('class', 'y-axis');
yAxisG.call(yAxis);
var grids = chartArea.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(yScale).tickSize(-(width)).tickFormat((domain, number) => {
return ""
}));
var pathsG = chartArea.append('g')
.attr('id', 'paths')
.attr('class', 'paths')
.attr('clip-path', 'url(#clip2)');
tick();
function tick() {
console.log('working');
var newValue = {
time: ++t,
value: Math.random() * 10
};
data.push(newValue);
xScale.domain([newValue.time - n + 2]);
xAxisG.transition().duration(500).ease().call(xAxis);
console.log('is it?');
var minerG = pathsG.selectAll('.minerLine').data([data]);
var minerGEnter = minerG.enter()
.append('g')
.attr('class', 'minerLine')
.merge(minerG);
var minerSVG = minerGEnter.selectAll('path').data((d) => [d]);
var minerSVGEnter = minerSVG.enter()
.append('path')
.attr('class', 'line')
.merge(minerSVG)
.transition()
.duration(500)
.ease(d3.easeLinear, 2)
.attr('d', line(data))
.on('end', () => {
requestAnimationFrame(tick)
})
}
</script>
</body>
</html>

Stacked area graph not rendering

I have a stacked area chart that reads from CSV. But it doesn't render the x axis and the graph correctly.
I have tried to change the x axis values, but didn't help.
below is the sample CSV file.
Currently the view shows the y axis of values, and the drug names on the right, but it doesn't show the actual stacked chart or the date x axis values.
So far return x(d.data.date) = returning NaN.
Thank you so much for your help!
date,drug,market_share
2016-01-01,insulin lispro,.01
2016-01-01,alogliptin,0.001323754341
2016-01-01,sitagliptin,.01
2016-01-01,canagliflozin,0.02842158621
2016-01-01,glimepiride,0.05668845799
2016-01-01,repaglinide,0.0005768749342
2016-01-01,insulin glargine,.01
2016-01-01,metformin,0.4972895171
2016-01-01,mifepristone,.02
2016-01-01,exenatide,.02
2016-01-01,bromocriptine,0.0002109506723
2016-01-01,pioglitazone,0.02324500184
2016-01-01,metformin hydrochloride,0.392074889
2016-02-01,saxagliptin,.02
2016-02-01,pioglitazone,0.02247815103
2016-02-01,exenatide,0.03
2016-02-01,repaglinide,0.0006204220565
2016-02-01,metformin hydrochloride,0.394153624
2016-02-01,sitagliptin,.08
2016-02-01,insulin lispro,.05
2016-02-01,canagliflozin,0.02907988245
2016-02-01,metformin,0.4933502396
2016-02-01,insulin glargine,.02
2016-02-01,bromocriptine,0.0002076549233
2016-02-01,mifepristone,.02
2016-02-01,alogliptin,0.001364306972
2016-02-01,glimepiride,0.05857620484
2016-03-01,canagliflozin,0.02908102306
2016-03-01,bromocriptine,0.000195238081
2016-03-01,metformin,0.4966376769
2016-03-01,alogliptin,0.00133532899
2016-03-01,insulin glargine,.03
2016-03-01,sitagliptin,.04
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<div id="div1"></div>
<div id="div2"></div>
</body>
<script src="https://d3js.org/d3.v4.js"></script>
<script>
var parseDate = d3.timeParse("%Y-%m-%d");
function type2(d, i, columns) {
d.date = parseDate(d.date);
return d;
}
function type(d, i, columns) {
d.date = parseDate(d.date);
for (var i = 1, n = columns.length; i < n; ++i) d[columns[i]] = d[columns[i]] / 100;
return d;
}
function drawGraph(error, data, selector, width, height) {
console.log("DATA "+selector+JSON.stringify(data));
console.log("COL "+selector+"---"+data.columns);
var svg = d3.select(selector).append("svg")
.attr("width", width)
.attr("height", height),
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
z = d3.scaleOrdinal(d3.schemeCategory10);
var stack = d3.stack();
var area = d3.area()
.x(function (d, i) {
return x(d.data.date);
})
.y0(function (d) {
return y(d[0]);
})
.y1(function (d) {
return y(d[1]);
});
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var keys = data.columns.slice(1);
x.domain(d3.extent(data, function (d) {
return d.date;
}));
z.domain(keys);
stack.keys(keys);
console.log("Stacked Data "+ selector+"---" + JSON.stringify(stack(data)));
var layer = g.selectAll(".layer")
.data(stack(data))
.enter().append("g")
.attr("class", "layer");
layer.append("path")
.attr("class", "area")
.style("fill", function (d) {
return z(d.key);
})
.attr("d", area);
layer.filter(function (d) {
return d[d.length - 1][1] - d[d.length - 1][0] > 0.01;
})
.append("text")
.attr("x", width - 6)
.attr("y", function (d) {
return y((d[d.length - 1][0] + d[d.length - 1][1]) / 2);
})
.attr("dy", ".35em")
.style("font", "10px sans-serif")
.style("text-anchor", "end")
.text(function (d) {
return d.key;
});
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).ticks(10, "%"));
}
d3.csv("market_shares.csv", type2, function (error, data) {
let stackedByDate = {}
let drugSet = new Set();
let defaultDrugMarketShareProp = {};
let newData = []
data.forEach(function (item, index) {
drugSet.add(item.drug);
stackedByDate[item.date] = {};
});
let drugNames = [...drugSet];
drugNames.forEach(function (item, index) {
defaultDrugMarketShareProp[item] = 0;
});
data.forEach(function (item, index) {
stackedByDate[item.date] = Object.assign({}, defaultDrugMarketShareProp);
});
data.forEach(function (item, index) {
stackedByDate[item.date][item.drug] = item.market_share;
});
Object.keys(stackedByDate).forEach(function (key) {
hash = {}
hash['date'] = key;
Object.keys(stackedByDate[key]).forEach(function (innerKey) {
hash[innerKey] = stackedByDate[key][innerKey]
});
newData.push(hash);
});
newData.columns = drugNames;
newData.columns.unshift('date');
drawGraph(error, newData, "#div2", 960, 500);
});
</script>
You correctly parsed the date strings. However, in this function...
Object.keys(stackedByDate).forEach(function (key) {
hash = {}
hash['date'] = key;//<----- HERE
Object.keys(stackedByDate[key]).forEach(function (innerKey) {
hash[innerKey] = stackedByDate[key][innerKey]
});
newData.push(hash);
});
... you're converting the date objects back to strings again.
The quickest solution is just:
hash['date'] = new Date(key);
Here is your code with that change only: https://bl.ocks.org/GerardoFurtado/e9538de82e96cc9e3efb5fc4c7b9b970/5ca6920405243c39b93d0245230b955c37c85a2c

D3.js hierarchical edge bundling coloring by group

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>

Categories

Resources