利用 svg 实现折线图
Tags: svg, chart
Published: 2022-10-30 08:21:18

在web开发中数据可视化在企业中后台是很重要的一部分，常见的图表有折线图、柱状图、饼图...等等。开发者为了效率和效果会使用第三方图表库，例如：[echarts](https://echarts.apache.org/zh/index.html)、[antV](https://antv.vision/)、[D3](https://d3js.org/)，这些图表库的底层实现包括 canvas、SVG、webGL，接下来我会使用 svg 来实现一个简单的折线图。

## 创建折线图组件
创建一个名为 `<LineChart />` 的新组件并将文件命名为 line-chart.js。

<LineChart /> 组件接受一个名为 data 的 props。

```js
import React, { useRef, useState } from 'react';
import PropTypes from 'prop-types';

const LineChart = ({ data }) => {
  return null;
};

LineChart.propTypes = {
  /** The shape of the data  */
  data: PropTypes.arrayOf(
    PropTypes.shape({
      total: PropTypes.number.isRequired,
      date: PropTypes.string.isRequired
    })
  ).isRequired
};

export default LineChart;
```

## 设置这线图
这步设置折线图不同部分的变量
```js
import React from 'react';
import PropTypes from 'prop-types';

const LineChart = ({ data }) => {
  const chartWidth = 1200; // SVG 视图框的宽度
  const chartHeight = 600; // SVG 视图框的高度
  const offsetY = 40; // 用于定位刻度
  const paddingX = 50; // 折线图周围的左右内边距
  const paddingY = 90; // 折线图周围的上下内边距
  const maxY = Math.max(...data.map((item) => item.total)); // 数据数组中的最大总值
  const guides = [...Array(16).keys()]; // 一个空数组，用于确定指南的数量

  return null;
};

LineChart.propTypes = {
  /** The shape of the data  */
  data: PropTypes.arrayOf(
    PropTypes.shape({
      total: PropTypes.number.isRequired,
      date: PropTypes.string.isRequired
    })
  ).isRequired
};

export default LineChart;
```
properties 是使用来自 data prop 的值和上一步中定义的变量的组合创建的。每个返回值都用于折线图的不同部分。
```js
const LineChart = ({ data }) => {
  // ...

  const properties = data.map((property, index) => {
    const { total, date } = property;
    const x = (index / data.length) * (chartWidth - paddingX)  paddingX / 2;
    const y = chartHeight - offsetY - (total / maxY) * (chartHeight - (paddingY + offsetY)) - paddingY + offsetY;
    return {
      total: total,
      date: date,
      x: x,
      y: y
    };
  });

  return null;
};
```
这里要查看的两个重要值是 x 和 y。这些变量是通过组合前面定义的一些变量和 .map 中的索引值来创建的。

## 创建 X 坐标
x 坐标用于创建标记的位置、值和折线点的 x 值。
```js
const x = (index / data.length) * (chartWidth - paddingX) + paddingX / 2;
```
x 坐标是通过使用 .map 中的索引值并将其除以在 data prop 上传递的数据的长度来创建的。将其乘以 chartWidth 并减去 paddingX 值可确保 x 坐标值永远不会超过图表宽度的范围。

## 创建 Y 坐标
y 坐标用于创建标记的位置、值和折线点的 y 值。
```js
const y = chartHeight - offsetY - (total / maxY) * (chartHeight - (paddingY + offsetY)) - paddingY + offsetY;
```
y 坐标是通过将总数除以 maxY 值再乘以 chartHeight 减去 paddingY 值来创建的。作为一个额外的步骤，减去 paddingY 加上 offsetY 值，这为图表底部的刻度创建了一些额外的空间。

## 创建点数组
SVG Polyline 元素可用于创建由点连接的线。要为折线创建点，您可以使用属性数组中的 x 和 y 值，并将它们作为 x、y 位置的数组返回。
```js
  const points = properties.map((point) => {
    const { x, y } = point;
    return `${x},${y}`;
  });
```
点数组将返回类似于下面的内容。数组中的每个索引都包含 x 和 y 坐标。
```js
[
  '25,25', '47,232', '69,40', '91,235' ...
]
```

## 创建 SVG
创建一个新的 <svg /> 元素，定义 viewBox 属性并将角色设置为presentation。
```js
const LineChart = ({ data }) => {
   // ... 
  return (
   <svg viewBox={`0 0 ${chartWidth} ${chartHeight}`} role="presentation">

   </svg>
  );
};
```

## 创建折线
创建一个新的 `<polyline />` 元素，将填充设置为 none 并添加相关 class来设置样式。还可以使用属性定义strokeWidth。使用 points 属性，可以传递 points 数组返回的值。
```html
<polyline fill="none" className="stroke-gray-400" strokeWidth={2} points={points} />
```

## 创建标记和值
使用 properties 数组的返回值，现在可以添加和定位 svg 元素以显示数据数组中的总数。
```jsx
  return (
    <svg viewBox={`0 0 ${chartWidth} ${chartHeight}`} role="presentation">
      <polyline fill="none" className="stroke-gray-400" strokeWidth={2} points={points} />
      {properties.map((property, index) => {
        const { total, date, x, y } = property;

        return (
          <g key={index}>
            <circle className="stroke-gray-400 fill-white" cx={x} cy={y} r={12} strokeWidth={2} />
            <text x={x} y={y + 2.8} textAnchor="middle" fontSize={8} className="font-bold fill-gray-800 select-none">
              {total}
            </text>
          </g>
        );
      })}
    </svg>
  );
```
<circle /> 元素使用 x 和 y 属性使用 cx 和 cy 属性定位，并使用 r 属性指定半径为 12。您可以添加自己的类名和 strokeWidth 的值以实现所需的外观。

<text /> 元素也使用 x 和 y 属性使用 x 和 y 属性定位。 <text /> 元素接受子元素，因此添加 total 属性以显示它。我在 y 中添加了一个额外的 2.8，以确保文本在 <circle /> 中垂直居中。可以添加自己的类名或属性来获得所需的外观。
## 创建刻度
```jsx
return (
          <g key={index}>
            ...
            <g transform={`translate(${x} ${chartHeight - (paddingY - offsetY)})`}>
              <text transform="rotate(45)" textAnchor="start" transformorigin="50% 50%" fontSize={10} className="fill-gray-800 select-none">
                {new Date(date).toLocaleDateString(undefined, { year: '2-digit', month: 'numeric', day: 'numeric' })}
              </text>
            </g>
          </g>
        );
```
刻度的创建方式略有不同，<text /> 元素被包裹在 <g /> 中。

这是为了创建一个新的坐标系，以便在对文本应用旋转时，其顶部/左侧位置是根据 <g /> 而不是 <svg /> 元素的顶部和左侧位置计算的。

<g /> 元素不支持 x 或 y 属性，因此您将使用 transform 属性并为 x 和 y 位置提供平移值。我还减去了 paddingY 和 offsetY 值来正确定位刻度。

## 创建指南
指南是最后添加的元素；但是这次你将迭代guides数组，而不是遍历properties数组。
这只是一个空数组，索引用于为 React 提供键。 y 坐标的创建方式与以前类似，其中创建了一个比率以确保 y 位置保持在边界内并且不与刻度重叠。
```jsx
const LineChart = ({ data }) => {
  return (
    <svg viewBox={`0 0 ${chartWidth} ${chartHeight}`} role="presentation">
       ...
      {guides.map((_, index) => {
        const ratio = index / guides.length;
        const y = chartHeight - paddingY - chartHeight * ratio;

        return <polyline key={index} className="stroke-gray-200" fill="none" strokeWidth={1} points={`${paddingX / 2},${y} ${chartWidth - paddingX / 2},${y}`} />;
      })}
       ...
    </svg>
  );
};
```
使用从属性数组返回的值，可以添加任意数量的不同 SVG 元素，以帮助显示来自不同类型数据的不同值。

x 和 y 属性应该是您所需要的。虽然创建它们有点棘手，但它们为您想添加到图表中的任何新元素创建了边界。