制作响应式、无 JavaScript 图表的新技术

2025-05-26

制作响应式、无 JavaScript 图表的新技术

网络上有无数用于生成图表的库。每个库的服务领域略有不同,但它们都有一个共同点:都需要 JavaScript。

当然,这很有道理——你的图表通常依赖于必须使用 JS 通过网络获取的数据,或者渲染到<canvas>元素中。但这并不理想。并非每个人都有 JS,而且无论如何,依赖它意味着你会在页面上留下一个图表形状的空洞,直到它加载完成,而只有当你的所有数据可视化都隐藏在页面首屏下方时,你才能真正避免这种情况。

另一个更微妙的问题是,流体图表(那些自适应其容器宽度的图表)在调整大小时必须重新绘制,以避免潜在的崩溃。这意味着开发人员的工作量会更大(尤其是在使用像D3这样的低级库的情况下),浏览器的工作量也必然会更大。

对于最近的《纽约时报》文章,我想看看是否有可能创建无需 JS 即可运行的 SVG 图表。

Cyber​​Tipline 报告与资金

嗯,确实如此。我还没见过其他地方使用过同样的技术组合,所以想把整个过程写下来。我还创建了一个名为 Pancake 的实验性 Svelte 组件库,让这些技术更容易使用。

问题

创建 SVG 折线图(我们稍后会介绍其他图表类型)其实相当简单。假设我们有如下一系列数据……

const data = [
  { x: 0,  y: 0 },
  { x: 1,  y: 1 },
  { x: 2,  y: 4 },
  { x: 3,  y: 9 },
  { x: 4,  y: 16 },
  { x: 5,  y: 25 },
  { x: 6,  y: 36 },
  { x: 7,  y: 49 },
  { x: 8,  y: 64 },
  { x: 9,  y: 81 },
  { x: 10, y: 100 }
];
Enter fullscreen mode Exit fullscreen mode

...以及一个 300px x 100px 的图表。如果我们将这些x值乘以 30,再y从 100 中减去这些值,我们将得到填充空间的坐标:

<polyline points="
  0,0
  30,99
  60,96
  90,91
  120,84
  150,75
  180,64
  210,51
  240,36
  270,19
  300,0
"></polyline>
Enter fullscreen mode Exit fullscreen mode

当然,通常情况下,你会使用缩放函数,而不是手动计算坐标:

function scale(domain, range) {
  const m = (range[1] - range[0]) / (domain[1] - domain[0]);
  return num => range[0] + m * (num - domain[0]);
}

const x = scale([0, Math.max(...data.map(d => d.x))], [0, 300]);
const y = scale([0, Math.max(...data.map(d => d.y))], [100, 0]);

const points = data.map(d => `${x(d.x)},${y(d.y)}`).join(' ');

const chart = `
<svg width="300" height="100">
  <polyline points="${points}"></polyline>
</svg>
`;
Enter fullscreen mode Exit fullscreen mode

添加一些轴和一些样式,我们就得到了一个图表

简单折线图

该逻辑可以全部存在于 Node.js 脚本中,这意味着可以轻松创建此图表而无需任何客户端 JS。

但它无法适应其容器的大小——它永远是一个 300px x 100px 的图表。在大多数网站上,这是一个问题。

解决方案(第一部分)

SVG 有一个属性viewBox,它定义了一个独立于元素本身大小的坐标系<svg>。通常情况下,无论元素的宽高比如何,viewBox 的宽高比都会保持不变<svg>,但我们可以使用 禁用此功能preserveAspectRatio="none"

我们可以选择一个简单的坐标系,像这样......

<svg viewBox="0 0 100 100" preserveAspectRatio="none">
Enter fullscreen mode Exit fullscreen mode

……并将我们的数据投影进去。现在,我们的图表可以流畅地适应其环境

流畅但不完整的图表

但它显然在两个重要方面存在缺陷。首先,文本缩放比例非常糟糕,在某些情况下甚至难以辨认。其次,线条笔画也随着线条本身被拉伸,看起来很糟糕。

第二个问题很简单,只需一个鲜为人知的 CSS 属性即可解决 ——vector-effect: non-scaling-stroke应用于每个元素

流畅且略微流畅的图表

但据我所知,第一个问题无法在 SVG 中解决。

解决方案(第二部分)

我们可以使用 HTML 元素来代替 SVG 元素作为坐标轴,并通过 CSS 进行定位。由于我们使用的是基于百分比的坐标系,因此很容易将 HTML 层和 SVG 层粘合在一起。

使用 HTML 重新创建上述轴非常简单:

<!-- x axis -->
<div class="x axis" style="top: 100%; width: 100%; border-top: 1px solid black;">
  <span style="left: 0">0</span>
  <span style="left: 20%">2</span>
  <span style="left: 40%">4</span>
  <span style="left: 60%">6</span>
  <span style="left: 80%">8</span>
  <span style="left: 100%">10</span>
</div>

<!-- y axis -->
<div class="y axis" style="height: 100%; border-left: 1px solid black;">
  <span style="top: 100%">0</span>
  <span style="top: 50%">50</span>
  <span style="top: 0%">100</span>
</div>

<style>
  .axis {
    position: absolute;
  }

  .axis span {
    position: absolute;
    line-height: 1;
  }

  .x.axis span {
    top: 0.5em;
    transform: translate(-50%,0);
  }

  .y.axis span {
    left: -0.5em;
    transform: translate(-100%,-50%);
  }
</style>
Enter fullscreen mode Exit fullscreen mode

我们的图表不再混乱

非钻孔流体线图

使用 HTML 元素的另一个好处是它们会自动捕捉到最近的像素,这意味着您不会遇到 SVG 元素容易出现的“模糊”效果。

打包

这解决了问题,但需要大量的手动操作,因此有了Pancake。使用 Pancake 后,上面的图表看起来会像这样

<script>
  import * as Pancake from '@sveltejs/pancake';

  const points = [
    { x: 0,  y: 0 },
    { x: 1,  y: 1 },
    { x: 2,  y: 4 },
    { x: 3,  y: 9 },
    { x: 4,  y: 16 },
    { x: 5,  y: 25 },
    { x: 6,  y: 36 },
    { x: 7,  y: 49 },
    { x: 8,  y: 64 },
    { x: 9,  y: 81 },
    { x: 10, y: 100 }
  ];
</script>

<div class="chart">
  <Pancake.Chart x1={0} x2={10} y1={0} y2={100}>
    <Pancake.Box x2={10} y2={100}>
      <div class="axes"></div>
    </Pancake.Box>

    <Pancake.Grid vertical count={5} let:value>
      <span class="x label">{value}</span>
    </Pancake.Grid>

    <Pancake.Grid horizontal count={3} let:value>
      <span class="y label">{value}</span>
    </Pancake.Grid>

    <Pancake.Svg>
      <Pancake.SvgLine data={points} let:d>
        <path class="data" {d}/>
      </Pancake.SvgLine>
    </Pancake.Svg>
  </Pancake.Chart>
</div>

<style>
  .chart {
    height: 100%;
    padding: 3em 2em 2em 3em;
    box-sizing: border-box;
  }

  .axes {
    width: 100%;
    height: 100%;
    border-left: 1px solid black;
    border-bottom: 1px solid black;
  }

  .y.label {
    position: absolute;
    left: -2.5em;
    width: 2em;
    text-align: right;
    bottom: -0.5em;
  }

  .x.label {
    position: absolute;
    width: 4em;
    left: -2em;
    bottom: -22px;
    font-family: sans-serif;
    text-align: center;
  }

  path.data {
    stroke: red;
    stroke-linejoin: round;
    stroke-linecap: round;
    stroke-width: 2px;
    fill: none;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

由于我们使用了 Svelte,因此此图表可以在构建时使用 Node.js 轻松渲染,也可以使用客户端 JS 注入到 DOM 中。对于具有一定交互性的图表(例如Pancake 主页上的大型示例图表),您可能需要同时执行这两种操作:使用 HTML 提供基本图表,然后通过填充初始 DOM 逐步增强其交互性。如果没有像 Svelte 这样的组件框架,这很难做到。

请注意,Pancake 实际上并没有创建构成图表的<span>和节点。相反,这些组件主要是逻辑性的——你只需要添加标记,这意味着你可以对图表元素的外观进行细粒度的控制。<path>

进一步

除了简单的折线图,我们还能做更多的事情:

不同的煎饼图类型

散点图尤其有趣。因为我们不能使用<circle>元素——它们会像之前的线条和文本元素那样拉伸——所以我们必须稍微发挥一下创造力。该<Pancake.Scatterplot>组件会生成一条半径为零的不连续圆弧路径。通过使用描边宽度渲染该路径,我们可以让它看起来像是在绘制圆形。

因为我们使用的是 Svelte 组件,所以我们可以轻松地在图表中引入动效,就像这个小倍数示例一样。我们还可以用最少的麻烦添加诸如声明式过渡之类的功能。

Pancake 图表中的交互功能也可以通过声明的方式处理。例如,我们可以创建一个四叉树(大量借鉴 D3 的思想),用来找到距离鼠标最近的点:

<Pancake.SvgScatterplot data={points} let:d>
  <path class="data" {d}/>
</Pancake.SvgScatterplot>

<Pancake.Quadtree data={points} let:closest>
  {#if closest}
    <Pancake.SvgPoint x={closest.x} y={closest.y} let:d>
      <path class="highlight" {d}/>
    </Pancake.SvgPoint>
  {/if}
</Pancake.Quadtree>
Enter fullscreen mode Exit fullscreen mode

在《纽约时报》,我们正在使用一种非常类似的技术来创建无需 JavaScript 的地图,以追踪新冠病毒疫情。还有一些工作要做,但这项工作最终可能会被纳入 Pancake 项目。

未来,该库可能会添加对画布层(2D 和 WebGL)渲染的支持。使用 JS 的图表<canvas>将严格依赖于 JS,但当数据量超过 SVG 能够高效渲染的范围时,这种做法是必要的。

注意事项

这仍然处于实验阶段;它还没有像现有的图表库那样经过实战检验。

它专注于管理二维图表的坐标系统。这对于折线图、条形图、散点图、堆积面积图等等来说已经足够了,但如果你需要制作饼图,你就得另寻他处了。

目前还没有文档,但主页上有一些示例可以借鉴。随着我们遇到更多实际问题,API 可能会发生变化。

致谢

“Pancake” 这个名字源于图表是通过层层叠加而构建的。我非常感谢Michael Keller创作了Layer Cake,Pancake 的很多灵感都来源于此,我也从中借鉴了一些上面链接的示例图表。Michael 也报道了上面链接的故事,这给了我创作 Pancake 的最初动力。

我还要感谢D3Observable 的创始人Mike Bostock他分享了深刻的见解、示例和代码,使这类项目得以实现。Pancake 主页上的几个示例都是从D3 示例页面中毫不掩饰地抄袭而来的,而 D3 示例页面对于任何想要测试新图表库的人来说都是一座金矿。

文章来源:https://dev.to/richharris/a-new-technique-for-making-responsive-javascript-free-charts-gmp
PREV
捍卫现代网络
NEXT
停止使用 Try-Catch:处理 JavaScript 错误的更好方法