Skip to content

玩转d3.js

Posted on:September 4, 2023 at 01:06 PM
预计阅读时长:6 min read 字数:1051

目录

简介

什么是D3

D3 (or D3.js) is a free, open-source JavaScript library for visualizing data. Its low-level approach built on web standards offers unparalleled flexibility in authoring dynamic, data-driven graphics. For more than a decade D3 has powered groundbreaking and award-winning visualizations, become a foundational building block of higher-level chart libraries, and fostered a vibrant community of data practitioners around the world.

getting started

npm install d3
<!DOCTYPE html>
<div id="container"></div>
<script type="module">

import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

// Declare the chart dimensions and margins.
const width = 640;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;

// Declare the x (horizontal position) scale.
const x = d3.scaleUtc()
    .domain([new Date("2023-01-01"), new Date("2024-01-01")])
    .range([marginLeft, width - marginRight]);

// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
    .domain([0, 100])
    .range([height - marginBottom, marginTop]);

// Create the SVG container.
const svg = d3.create("svg")
    .attr("width", width)
    .attr("height", height);

// Add the x-axis.
svg.append("g")
    .attr("transform", `translate(0,${height - marginBottom})`)
    .call(d3.axisBottom(x));

// Add the y-axis.
svg.append("g")
    .attr("transform", `translate(${marginLeft},0)`)
    .call(d3.axisLeft(y));

// Append the SVG element.
container.append(svg.node());

</script>

demo

tree组件

import Tree from '@components/react/d3/tree'

<Tree client:load/>

齿轮组件

tsx component
import * as d3 from "d3";
import { useEffect, useRef } from "react"


const Gear = () => {
    const ref = useRef<SVGSVGElement | null>(null);

    function gear({ teeth, radius, annulus, toothRadius, holeRadius }: any) {
        const n = teeth;
        let r2 = Math.abs(radius);
        let r0 = r2 - toothRadius;
        let r1 = r2 + toothRadius;
        let r3 = holeRadius;
        if (annulus) r3 = r0, r0 = r1, r1 = r3, r3 = r2 + toothRadius * 3;
        const da = Math.PI / n;
        let a0 = -Math.PI / 2 + (annulus ? Math.PI / n : 0);
        const path = ["M", r0 * Math.cos(a0), ",", r0 * Math.sin(a0)];
        let i = -1;
        while (++i < n) {
            path.push(
                "A", r0, ",", r0, " 0 0,1 ", r0 * Math.cos(a0 += da), ",", r0 * Math.sin(a0),
                "L", r2 * Math.cos(a0), ",", r2 * Math.sin(a0),
                "L", r1 * Math.cos(a0 += da / 3), ",", r1 * Math.sin(a0),
                "A", r1, ",", r1, " 0 0,1 ", r1 * Math.cos(a0 += da / 3), ",", r1 * Math.sin(a0),
                "L", r2 * Math.cos(a0 += da / 3), ",", r2 * Math.sin(a0),
                "L", r0 * Math.cos(a0), ",", r0 * Math.sin(a0)
            );
        }
        path.push("M0,", -r3, "A", r3, ",", r3, " 0 0,0 0,", r3, "A", r3, ",", r3, " 0 0,0 0,", -r3, "Z");
        return path.join("");
    }

    const graphic = () => {

        const x = Math.sin(2 * Math.PI / 3);
        const y = Math.cos(2 * Math.PI / 3);
        const svgElement = d3.select(ref.current);

        const svg = svgElement
            .attr("width", 640)
            .attr("viewBox", [-0.53, -0.53, 1.06, 1.06])
            .attr("stroke", "black")
            .attr("stroke-width", 1 / 640)
            .attr("style", `width:${100}vw;max-width: 100%; height: calc( 100vh - 100px);`);

        const frame = svg.append("g");

        const path = frame.selectAll()
            .data([
                { fill: "#c6dbef", teeth: 80, radius: -0.5, origin: [0, 0], annulus: true },
                { fill: "#6baed6", teeth: 16, radius: +0.1, origin: [0, 0] },
                { fill: "#9ecae1", teeth: 32, radius: -0.2, origin: [0, -0.3] },
                { fill: "#9ecae1", teeth: 32, radius: -0.2, origin: [-0.3 * x, -0.3 * y] },
                { fill: "#9ecae1", teeth: 32, radius: -0.2, origin: [0.3 * x, -0.3 * y] }
            ])
            .join("path")
            .attr("fill", d => d.fill)
            .attr("d", d => gear({ ...d, toothRadius: 0.008, holeRadius: 0.02 }));

        Object.assign(svg.node() as SVGSVGElement, {
            update({ angle, frameAngle }: { angle: number, frameAngle: number }) {
                path.attr("transform", d => `translate(${d.origin}) rotate(${(angle / d.radius) % 360})`);
                frame.attr("transform", `rotate(${frameAngle % 360})`);
            }
        });
    }
    useEffect(() => {
        graphic();

        let id = 0;
        const animate = () => {
            const speed = 0.08;
            let angle = 0;
            let frameAngle = 0;

            let start = 0;
            const draw = function (timestamp: number) {
                if (start === 0) {
                    start = timestamp;
                }
                (d3.select(ref.current).node() as any).update({ angle, frameAngle });
                angle += speed;
                frameAngle += speed / 360;
                id = requestAnimationFrame(draw)

            }
            id = requestAnimationFrame(draw)
        }
        animate();
        return () => {
            cancelAnimationFrame(id);
        }

    })

    return (
        <svg ref={ref} >
        </svg>
    )
}

export default Gear;

数据可视化

html + css

<div class="relative w-full border-solid border-lightgrey bg-skin-fill border-[1px]">
    <div class="w-[95%] bg-skin-accent h-3"/>
</div>

css-doodle

import Common from '@components/react/doodle/common.tsx';

<Common  client:load>
@grid: 1 / 100vmin 100vmin / #0a0c27;
    background-size: 200px 200px;
    background-image: @doodle(
      @grid: 6 / 100%;
      @size: 4px;
      font-size: 4px;
      color: hsl(@r240, 30%, 50%);
      box-shadow: @m3x5(
        calc(4em - @nx * 1em) calc(@ny * 1em)
          @p(@m3(currentColor), @m2(transparent)),
        calc(2em + @nx * 1em) calc(@ny * 1em)
          @lp
      );
    );
</Common>

webgl

svg

d3基础

d3.arc() 弧

practice-arc
import * as d3 from "d3";
import { useEffect, useRef } from "react"

export default () => {
    const ref = useRef<SVGSVGElement | null>(null);
    useEffect(() => {
        const svgElement = d3.select(ref.current);
        const g = svgElement
            .attr("width", "100")
            .attr("height", "100")
            .append("path")

        g.attr("d", d3.arc()({
            innerRadius: 19,
            outerRadius: 40,
            startAngle: -Math.PI / 2,
            endAngle: Math.PI / 2
        }))
            .attr("fill", "cornflowerblue")
            .attr("style", "transform: translate(50%, 50%)"
            )
    })

    return (
        <svg ref={ref} >
        </svg>
    )
}

d3.axisBottom() 底部坐标轴

practice-axis-bottom
import * as d3 from "d3";
import { useEffect, useRef } from "react"


const Template = () => {
    const ref = useRef<SVGSVGElement | null>(null);
    useEffect(() => {
        const xScale = d3.scaleLinear()
            .domain([0, 100])
            .range([10, 290])
        const svgElement = d3.select(ref.current).attr("style", "width:100%;max-height:4em");
        const axisGenerator = d3.axisBottom(xScale);
        svgElement.append("g")
            .call(axisGenerator)
        return () => {
            svgElement.remove();
        }
    })
    return (
        <svg ref={ref} >
        </svg>
    )
}
export default Template;

链接