Skip to content

Instantly share code, notes, and snippets.

@hydrangeas
Created May 25, 2024 08:41
Show Gist options
  • Save hydrangeas/b251a37d32b50eedaf8689b2442dc782 to your computer and use it in GitHub Desktop.
Save hydrangeas/b251a37d32b50eedaf8689b2442dc782 to your computer and use it in GitHub Desktop.
d3.jsでグラフを2つ+縦線が動く+ツールチップありのサンプル
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<h1>こんにちは、世界!</h1>
<p>これは、HTMLの基本的な構造です。</p>
<svg id="svgArea1" style="width: 100%; height: 100%"></svg>
<svg id="svgArea2" style="width: 100%; height: 100%"></svg>
<script>
(async () => {
await d3.json("stocks.json").then((stocks) => {
// Specify the chart’s dimensions.
const width = 928;
const height = 600;
const marginTop = 20;
const marginRight = 40;
const marginBottom = 30;
const marginLeft = 40;
// Create the horizontal time scale.
//var parseDate = d3.timeParse("%Y-%m-%d");
const x = d3
.scaleUtc()
//.domain(d3.extent(stocks, (d) => parseDate(d.Date)))
.domain(d3.extent(stocks, (d) => d3.isoParse(d.Date)))
.range([marginLeft, width - marginRight])
.clamp(true);
// Normalize the series with respect to the value on the first date. Note that normalizing
// the whole series with respect to a different date amounts to a simple vertical translation,
// thanks to the logarithmic scale! See also https://observablehq.com/@d3/change-line-chart
const series = d3
.groups(stocks, (d) => d.Symbol)
.map(([key, values]) => {
const v = values[0].Close;
return {
key,
values: values.map(({ Date, Close }) => ({
Date: d3.isoParse(Date),
value: Close / v,
})),
};
});
// Create the vertical scale. For each series, compute the ratio *s* between its maximum and
// minimum values; the path is going to move between [1 / s, 1] when the reference date
// corresponds to its maximum and [1, s] when it corresponds to its minimum. To have enough
// room, the scale is based on the series that has the maximum ratio *k* (in this case, AMZN).
const k = d3.max(
series,
({ values }) =>
d3.max(values, (d) => d.value) / d3.min(values, (d) => d.value)
);
const y = d3
.scaleLog()
.domain([1 / k, k])
.rangeRound([height - marginBottom, marginTop])
.clamp(true);
// Create a color scale to identify series.
const z = d3
.scaleOrdinal(d3.schemeCategory10)
.domain(series.map((d) => d.Symbol));
// For each given series, the update function needs to identify the date—closest to the current
// date—that actually contains a value. To do this efficiently, it uses a bisector:
const bisect = d3.bisector((d) => d.Date).left;
// Create the SVG container.
const svg1 = d3
//.create("svg")
.selectAll("#svgArea1,#svgArea2")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr(
"style",
"max-width: 100%; height: auto; -webkit-tap-highlight-color: transparent;"
);
// Create the axes and central rule.
svg1
.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(
d3
.axisBottom(x)
.ticks(width / 80)
.tickSizeOuter(0)
)
.call((g) => g.select(".domain").remove());
svg1
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).ticks(null, (x) => +x.toFixed(6) + "×"))
.call((g) =>
g
.selectAll(".tick line")
.clone()
.attr("stroke-opacity", (d) => (d === 1 ? null : 0.2))
.attr("x2", width - marginLeft - marginRight)
)
.call((g) => g.select(".domain").remove());
const rule = svg1
.append("g")
.append("line")
.attr("y1", height)
.attr("y2", 0)
.attr("stroke", "black");
// Create a line and a label for each series.
const serie = svg1
.append("g")
.style("font", "bold 10px sans-serif")
.selectAll("g")
.data(series)
.join("g");
const line = d3
.line()
.x((d) => x(d.Date))
.y((d) => y(d.value));
serie
.append("path")
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke", (d) => z(d.key))
.attr("d", (d) => line(d.values));
serie
.append("text")
.datum((d) => ({
key: d.key,
value: d.values[d.values.length - 1].value,
}))
.attr("fill", (d) => z(d.key))
.attr("paint-order", "stroke")
.attr("stroke", "white")
.attr("stroke-width", 3)
.attr("x", x.range()[1] + 3)
.attr("y", (d) => y(d.value))
.attr("dy", "0.35em")
.text((d) => d.key);
// Define the update function, that translates each of the series vertically depending on the
// ratio between its value at the current date and the value at date 0. Thanks to the log
// scale, this gives the same result as a normalization by the value at the current date.
function update(date) {
date = d3.utcDay.round(date);
rule.attr("transform", `translate(${x(date) + 0.5},0)`);
// serie.attr("transform", ({ values }) => {
// const i = bisect(values, date, 0, values.length - 1);
// return `translate(0,${
// y(1) - y(values[i].value / values[0].value)
// })`;
// });
svg1.property("value", date).dispatch("input"); // for viewof compatibility
}
// Create the introductory animation. It repeatedly calls the update function for dates ranging
// from the last to the first date of the x scale.
d3.transition()
.ease(d3.easeCubicOut)
.duration(1500)
.tween("date", () => {
const i = d3.interpolateDate(x.domain()[1], x.domain()[0]);
return (t) => update(i(t));
});
// When the user mouses over the chart, update it according to the date that is
// referenced by the horizontal position of the pointer.
svg1.on("mousemove touchmove", function (event) {
update(x.invert(d3.pointer(event, this)[0]));
//d3.event.preventDefault();
});
// --- Tooltip ---
const tooltip = svg1.append("g");
// Add the event listeners that show or hide the tooltip.
const bisect2 = d3.bisector((d) => d3.isoParse(d.Date)).center;
function pointermoved(event) {
// AAPLを基準としてbisect2を実行する
const aaplValues = series.find((d) => d.key === "AAPL").values;
const i = bisect2(aaplValues, x.invert(d3.pointer(event)[0]));
tooltip.style("display", null);
tooltip.attr(
"transform",
`translate(${x(aaplValues[i].Date)},${y(aaplValues[i].value)})`
);
const path = tooltip
.selectAll("path")
.data([,])
.join("path")
.attr("fill", "white")
.attr("stroke", "black");
const text = tooltip
.selectAll("text")
.data([,])
.join("text")
.call((text) =>
text
.selectAll("tspan")
.data([d3.isoParse(stocks[i].Date), stocks[i].Close])
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => (i ? null : "bold"))
.text((d) => d)
);
size(text, path);
}
function pointerleft() {
tooltip.style("display", "none");
}
// Wraps the text with a callout path of the correct size, as measured in the page.
function size(text, path) {
const { x, y, width: w, height: h } = text.node().getBBox();
text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr(
"d",
`M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${
w + 20
}z`
);
}
svg1
.on("pointerenter pointermove", pointermoved)
.on("pointerleave", pointerleft);
});
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment