Skip to content

Instantly share code, notes, and snippets.

@hydrangeas
Created May 25, 2024 08:41

Revisions

  1. hydrangeas created this gist May 25, 2024.
    252 changes: 252 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,252 @@
    <!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>