首页 > 解决方案 > 将 observable 适配到 D3 但网页中没有渲染

问题描述

我正在关注https://observablehq.com/@d3/bar-chart-race-explained中的教程。

我正在使用版本 89.0.4389.90 的 chrome 来测试代码。

我主要是复制代码并下载了数据 csv 文件,我正在尝试在本地运行代码 - 这里是:

<!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>

我已经更改了代码以在没有 observablehq 的环境的情况下运行,但是在运行代码之后,网页上没有显示任何内容。

控制台日志显示数据处理逻辑正常,但是对于渲染部分,除了打印什么都不做iteration

我的代码有什么问题?

标签: javascriptd3.js

解决方案


您需要将svg刚刚创建的内容附加到 HTML 中的某个元素以查看输出。

d3.create只是创建一个分离的元素。由于您没有将其附加到任何地方,因此我们没有在屏幕中看到输出。

我们可以使用d3.select选择我们想要放置此图表的位置,然后使用 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>


推荐阅读