Created
May 25, 2024 08:41
-
-
Save hydrangeas/b251a37d32b50eedaf8689b2442dc782 to your computer and use it in GitHub Desktop.
d3.jsでグラフを2つ+縦線が動く+ツールチップありのサンプル
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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