DEV Community

Yao
Yao

Posted on

利用 svg 实现折线图

在web开发中数据可视化在企业中后台是很重要的一部分,常见的图表有折线图、柱状图、饼图...等等。开发者为了效率和效果会使用第三方图表库,例如:echartsantVD3,这些图表库的底层实现包括 canvas、SVG、webGL,接下来我会使用 svg 来实现一个简单的折线图。

创建折线图组件

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

组件接受一个名为 data 的 props。

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;
Enter fullscreen mode Exit fullscreen mode

设置这线图

这步设置折线图不同部分的变量

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;
Enter fullscreen mode Exit fullscreen mode

properties 是使用来自 data prop 的值和上一步中定义的变量的组合创建的。每个返回值都用于折线图的不同部分。

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;
};
Enter fullscreen mode Exit fullscreen mode

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

创建 X 坐标

x 坐标用于创建标记的位置、值和折线点的 x 值。

const x = (index / data.length) * (chartWidth - paddingX) + paddingX / 2;
Enter fullscreen mode Exit fullscreen mode

x 坐标是通过使用 .map 中的索引值并将其除以在 data prop 上传递的数据的长度来创建的。将其乘以 chartWidth 并减去 paddingX 值可确保 x 坐标值永远不会超过图表宽度的范围。

创建 Y 坐标

y 坐标用于创建标记的位置、值和折线点的 y 值。

const y = chartHeight - offsetY - (total / maxY) * (chartHeight - (paddingY + offsetY)) - paddingY + offsetY;
Enter fullscreen mode Exit fullscreen mode

y 坐标是通过将总数除以 maxY 值再乘以 chartHeight 减去 paddingY 值来创建的。作为一个额外的步骤,减去 paddingY 加上 offsetY 值,这为图表底部的刻度创建了一些额外的空间。

创建点数组

SVG Polyline 元素可用于创建由点连接的线。要为折线创建点,您可以使用属性数组中的 x 和 y 值,并将它们作为 x、y 位置的数组返回。

  const points = properties.map((point) => {
    const { x, y } = point;
    return `${x},${y}`;
  });
Enter fullscreen mode Exit fullscreen mode

点数组将返回类似于下面的内容。数组中的每个索引都包含 x 和 y 坐标。

[
  '25,25', '47,232', '69,40', '91,235' ...
]
Enter fullscreen mode Exit fullscreen mode

创建 SVG

创建一个新的 元素,定义 viewBox 属性并将角色设置为presentation。

const LineChart = ({ data }) => {
   // ... 
  return (
   <svg viewBox={`0 0 ${chartWidth} ${chartHeight}`} role="presentation">

   </svg>
  );
};
Enter fullscreen mode Exit fullscreen mode

创建折线

创建一个新的 <polyline /> 元素,将填充设置为 none 并添加相关 class来设置样式。还可以使用属性定义strokeWidth。使用 points 属性,可以传递 points 数组返回的值。

<polyline fill="none" className="stroke-gray-400" strokeWidth={2} points={points} />
Enter fullscreen mode Exit fullscreen mode

创建标记和值

使用 properties 数组的返回值,现在可以添加和定位 svg 元素以显示数据数组中的总数。

  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>
  );
Enter fullscreen mode Exit fullscreen mode

元素使用 x 和 y 属性使用 cx 和 cy 属性定位,并使用 r 属性指定半径为 12。您可以添加自己的类名和 strokeWidth 的值以实现所需的外观。

元素也使用 x 和 y 属性使用 x 和 y 属性定位。 元素接受子元素,因此添加 total 属性以显示它。我在 y 中添加了一个额外的 2.8,以确保文本在 中垂直居中。可以添加自己的类名或属性来获得所需的外观。

创建刻度

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>
        );
Enter fullscreen mode Exit fullscreen mode

刻度的创建方式略有不同, 元素被包裹在 中。

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

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

创建指南

指南是最后添加的元素;但是这次你将迭代guides数组,而不是遍历properties数组。
这只是一个空数组,索引用于为 React 提供键。 y 坐标的创建方式与以前类似,其中创建了一个比率以确保 y 位置保持在边界内并且不与刻度重叠。

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

使用从属性数组返回的值,可以添加任意数量的不同 SVG 元素,以帮助显示来自不同类型数据的不同值。

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

Top comments (0)