LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

使用 HTML + JavaScript 实现折线图(附完整代码)

admin
2025年12月15日 9:7 本文热度 748
在现代前端开发中,数据可视化是提升用户体验的重要手段之一。虽然像 ECharts 这样的成熟库已经能满足大部分需求,但亲手实现一个简易版本的图表绘制工具不仅能加深对图形编程的理解,也能带来极大的成就感。本文将使用 HTML、CSS 和 JavaScript 实现一个类似 ECharts 的交互式折线图。

效果演示

折线图支持多个系列同时展示,并配有不同颜色区分,鼠标悬停时出现提示框显示具体数值,底部图例可控制各条折线的显隐状态。

页面结构

整个页面采用简洁清晰的结构组织,主要包括以下几个部分:

  • .chart-container 主容器,包含标题、画布和图例

  • #lineChart canvas 元素,用于实际绘图

  • .legend 图例区域,显示各个数据系列及其开关按钮

  • .tooltip 浮动提示框,鼠标悬停时展示详细信息

<div class="chart-container">  <div class="chart-title">销售数据趋势图</div>  <div id="chart">    <canvas id="lineChart"></canvas>  </div>  <div class="legend" id="legend"></div></div><div class="tooltip" id="tooltip"></div>

核心功能实现

初始化图表类

首先我们定义了一个 LineChart 类来封装所有的图表行为。其构造函数接收 canvas ID 和配置参数。
class LineChart {  constructor(canvasId, options) {    this.canvas = document.getElementById(canvasId);    this.ctx = this.canvas.getContext('2d');    this.options = options;    this.data = options.data;    this.tooltip = document.getElementById('tooltip');    this.legend = document.getElementById('legend');
    this.padding = { top: 40, right: 40, bottom: 60, left: 60 };    this.colors = ['#4da6ff''#00e6b8''#ffcc00''#ff6666''#b3b3ff'];    this.hiddenSeries = new Set();
    this.init();  }}

绘图流程

初始化完成后,依次执行以下步骤完成图表绘制:

  1. 调整画布大小 (resizeCanvas):根据父容器自动缩放;

  2. 绘制主图内容 (draw):清空画布后调用其他绘图子方法;

  3. 创建底部图例 (createLegend):动态生成图例项;

  4. 绑定事件监听器 (bindEvents):处理鼠标移动与离开事件。

init() {  this.resizeCanvas();  this.draw();  this.createLegend();  this.bindEvents();  window.addEventListener('resize', () => {    this.resizeCanvas();    this.draw();  });}

各部件绘制详解

网格线绘制

为了增强可读性,在坐标系内添加了横向辅助线。
drawGrid() {  const { width, height } = this.canvas;  const { top, right, bottom, left } = this.padding;  const chartHeight = height - top - bottom;
  this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';  this.ctx.lineWidth = 1;  // 横向网格线  const ySteps = 5;  for (let i = 0; i <= ySteps; i++) {    const y = top + (chartHeight / ySteps) * i;    this.ctx.beginPath();    this.ctx.moveTo(left, y);    this.ctx.lineTo(width - right, y);    this.ctx.stroke();  }}

坐标轴与标签

分别绘制 X 轴和 Y 轴,并标注对应刻度值。
drawAxes() {  const { width, height } = this.canvas;  const { top, right, bottom, left } = this.padding;  this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';  this.ctx.lineWidth = 2;  // X轴  this.ctx.beginPath();  this.ctx.moveTo(left, height - bottom);  this.ctx.lineTo(width - right, height - bottom);  this.ctx.stroke();  // Y轴  this.ctx.beginPath();  this.ctx.moveTo(left, top);  this.ctx.lineTo(left, height - bottom);  this.ctx.stroke();  // 绘制刻度和标签  this.drawLabels();}

数据线条与点

循环遍历每个数据系列,计算出每一点的像素位置并连线描点。
drawLines() {  const { width, height } = this.canvas;  const { top, right, bottom, left } = this.padding;  const chartWidth = width - left - right;  const chartHeight = height - top - bottom;
  const maxValue = Math.max(...this.data.series.flatMap(s => s.data));  const xData = this.data.xAxis.data;  const xStep = chartWidth / (xData.length - 1);  const yScale = chartHeight / maxValue;
  this.data.series.forEach((series, seriesIndex) => {    if (this.hiddenSeries.has(seriesIndex)) return;    this.ctx.strokeStyle = this.colors[seriesIndex % this.colors.length];    this.ctx.lineWidth = 2;    this.ctx.beginPath();    series.data.forEach((value, index) => {      const x = left + xStep * index;      const y = top + chartHeight - value * yScale;      if (index === 0) {        this.ctx.moveTo(x, y);      } else {        this.ctx.lineTo(x, y);      }    });    this.ctx.stroke();  });}

动态交互功能

通过监听 mousemove 事件捕捉最近的数据点并在浮动层上显示相关信息;点击图例则切换对应曲线的可见状态。
handleMouseMove(e) {  const rect = this.canvas.getBoundingClientRect();  const mouseX = e.clientX - rect.left;  const { width, height } = this.canvas;  const { top, right, bottom, left } = this.padding;  const chartWidth = width - left - right;  const xData = this.data.xAxis.data;  const xStep = chartWidth / (xData.length - 1);  // 找到最近的数据点  let closestIndex = -1;  let minDistance = Infinity;  xData.forEach((_, index) => {    const px = left + xStep * index;    const distance = Math.abs(mouseX - px);    if (distance < minDistance && distance < 20) {      minDistance = distance;      closestIndex = index;    }  });  if (closestIndex !== -1) {    let tooltipHTML = `<div style="font-weight: bold; margin-bottom: 5px;">${xData[closestIndex]}</div>`;    this.data.series.forEach((series, seriesIndex) => {      if (!this.hiddenSeries.has(seriesIndex)) {        const value = series.data[closestIndex];        const color = this.colors[seriesIndex % this.colors.length];        tooltipHTML += `<div style="display: flex; align-items: center; margin: 3px 0;">                        <div style="width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 5px;"></div>                        ${series.name}: ${value}                    </div>`;      }    });    this.tooltip.innerHTML = tooltipHTML;    this.tooltip.style.display = 'block';    this.tooltip.style.left = e.clientX + 10 + 'px';    this.tooltip.style.top = e.clientY - 10 + 'px';  } else {    this.tooltip.style.display = 'none';  }}

扩展建议

  • 增加动画过渡:让线条绘制过程更平滑自然

  • 平滑曲线图:使用更柔和的视觉平滑曲线样式

  • 加入导出图像功能:一键下载高清图片

  • 提供更多自定义选项:字体大小、网格密度、颜色主题等

完整代码

git地址:https://gitee.com/ironpro/hjdemo/blob/master/chart-line/index.html
<!DOCTYPE html><html lang="zh-CN"><head>  <meta charset="utf-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>仿ECharts折线图</title>  <style>      * {          margin: 0;          padding: 0;          box-sizing: border-box;      }      body {          background-color: #f5f5f5;          margin: 0;          padding: 20px;          min-height: 100vh;      }
      .chart-container {          background: rgba(2552552550.08);          border-radius: 16px;          box-shadow: 0 8px 32px rgba(0000.3);          padding: 25px;          max-width: 800px;          margin: 0 auto;          border: 1px solid rgba(2552552550.1);          backdrop-filter: blur(14px);      }
      .chart-title {          text-align: center;          font-size: 20px;          font-weight: 600;          margin-bottom: 25px;          letter-spacing: 0.5px;      }
      #chart {          position: relative;          width: 100%;          height: 400px;      }
      .tooltip {          position: fixed;          background: rgba(3030300.9);          color: white;          padding: 10px 15px;          border-radius: 8px;          font-size: 13px;          pointer-events: none;          display: none;          z-index: 1000;          backdrop-filter: blur(4px);          border: 1px solid rgba(2552552550.15);          box-shadow: 0 4px 20px rgba(0000.3);          transition: opacity 0.2s ease;      }
      .legend {          display: flex;          justify-content: center;          margin-top: 25px;          flex-wrap: wrap;          gap: 15px;      }
      .legend-item {          display: flex;          align-items: center;          cursor: pointer;          transition: all 0.3s ease;          padding: 6px 14px;          border-radius: 20px;          background: rgba(2552552550.05);      }
      .legend-item:hover {          background: rgba(2552552550.15);      }
      .legend-color {          width: 14px;          height: 14px;          border-radius: 50%;          margin-right: 8px;      }
      .legend-text {                    font-weight: 500;      }
      .legend-item.inactive {          opacity: 0.3;      }  </style></head><body><div class="chart-container">  <div class="chart-title">销售数据趋势图</div>  <div id="chart">    <canvas id="lineChart"></canvas>  </div>  <div class="legend" id="legend"></div></div><div class="tooltip" id="tooltip"></div>
<script>  class LineChart {    constructor(canvasId, options) {      this.canvas = document.getElementById(canvasId);      this.ctx = this.canvas.getContext('2d');      this.options = options;      this.data = options.data;      this.tooltip = document.getElementById('tooltip');      this.legend = document.getElementById('legend');
      this.padding = { top: 40, right: 40, bottom: 60, left: 60 };      this.colors = ['#4da6ff''#00e6b8''#ffcc00''#ff6666''#b3b3ff'];      this.hiddenSeries = new Set();
      this.init();    }
    init() {      this.resizeCanvas();      this.draw();      this.createLegend();      this.bindEvents();      window.addEventListener('resize', () => {        this.resizeCanvas();        this.draw();      });    }
    resizeCanvas() {      const container = this.canvas.parentElement;      this.canvas.width = container.offsetWidth;      this.canvas.height = container.offsetHeight;    }
    draw() {      this.ctx.clearRect(00this.canvas.width, this.canvas.height);      this.drawGrid();      this.drawAxes();      this.drawLines();      this.drawPoints();    }
    drawGrid() {      const { width, height } = this.canvas;      const { top, right, bottom, left } = this.padding;      const chartHeight = height - top - bottom;
      this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';      this.ctx.lineWidth = 1;      // 横向网格线      const ySteps = 5;      for (let i = 0; i <= ySteps; i++) {        const y = top + (chartHeight / ySteps) * i;        this.ctx.beginPath();        this.ctx.moveTo(left, y);        this.ctx.lineTo(width - right, y);        this.ctx.stroke();      }    }
    drawAxes() {      const { width, height } = this.canvas;      const { top, right, bottom, left } = this.padding;      this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';      this.ctx.lineWidth = 2;      // X轴      this.ctx.beginPath();      this.ctx.moveTo(left, height - bottom);      this.ctx.lineTo(width - right, height - bottom);      this.ctx.stroke();      // Y轴      this.ctx.beginPath();      this.ctx.moveTo(left, top);      this.ctx.lineTo(left, height - bottom);      this.ctx.stroke();      // 绘制刻度和标签      this.drawLabels();    }
    drawLabels() {      const { width, height } = this.canvas;      const { top, right, bottom, left } = this.padding;      const chartWidth = width - left - right;      const chartHeight = height - top - bottom;
      this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';      this.ctx.font = '14px Arial';      this.ctx.textAlign = 'center';      // X轴标签      const xData = this.data.xAxis.data;      const step = chartWidth / (xData.length - 1);      xData.forEach((label, index) => {        const x = left + step * index;        this.ctx.fillText(label, x, height - bottom + 20);      });      // Y轴标签      this.ctx.textAlign = 'right';      const maxValue = Math.max(...this.data.series.flatMap(s => s.data));      const ySteps = 5;      for (let i = 0; i <= ySteps; i++) {        const value = Math.round((maxValue / ySteps) * (ySteps - i));        const y = top + (chartHeight / ySteps) * i;        this.ctx.fillText(value, left - 10, y + 5);      }      // 轴标题      this.ctx.fillStyle = 'rgba(0, 0, 0, 0.9)';      this.ctx.font = '14px Arial';      this.ctx.textAlign = 'center';      this.ctx.fillText(this.data.xAxis.name, width / 2, height - 10);
      this.ctx.save();      this.ctx.translate(15, height / 2);      this.ctx.rotate(-Math.PI / 2);      this.ctx.fillText(this.data.yAxis.name, 00);      this.ctx.restore();    }
    drawLines() {      const { width, height } = this.canvas;      const { top, right, bottom, left } = this.padding;      const chartWidth = width - left - right;      const chartHeight = height - top - bottom;
      const maxValue = Math.max(...this.data.series.flatMap(s => s.data));      const xData = this.data.xAxis.data;      const xStep = chartWidth / (xData.length - 1);      const yScale = chartHeight / maxValue;
      this.data.series.forEach((series, seriesIndex) => {        if (this.hiddenSeries.has(seriesIndex)) return;        this.ctx.strokeStyle = this.colors[seriesIndex % this.colors.length];        this.ctx.lineWidth = 2;        this.ctx.beginPath();        series.data.forEach((value, index) => {          const x = left + xStep * index;          const y = top + chartHeight - value * yScale;          if (index === 0) {            this.ctx.moveTo(x, y);          } else {            this.ctx.lineTo(x, y);          }        });        this.ctx.stroke();      });    }
    drawPoints() {      const { width, height } = this.canvas;      const { top, right, bottom, left } = this.padding;      const chartWidth = width - left - right;      const chartHeight = height - top - bottom;
      const maxValue = Math.max(...this.data.series.flatMap(s => s.data));      const xData = this.data.xAxis.data;      const xStep = chartWidth / (xData.length - 1);      const yScale = chartHeight / maxValue;
      this.data.series.forEach((series, seriesIndex) => {        if (this.hiddenSeries.has(seriesIndex)) return;
        this.ctx.fillStyle = this.colors[seriesIndex % this.colors.length];
        series.data.forEach((value, index) => {          const x = left + xStep * index;          const y = top + chartHeight - value * yScale;
          this.ctx.beginPath();          this.ctx.arc(x, y, 40, Math.PI * 2);          this.ctx.fill();
          // 白色边框          this.ctx.strokeStyle = '#fff';          this.ctx.lineWidth = 2;          this.ctx.stroke();        });      });    }
    createLegend() {      this.legend.innerHTML = '';      this.data.series.forEach((series, index) => {        const item = document.createElement('div');        item.className = 'legend-item';        item.innerHTML = `<div class="legend-color" style="background-color: ${this.colors[index % this.colors.length]}"></div>                        <div class="legend-text">${series.name}</div>`;
        item.addEventListener('click', () => {          if (this.hiddenSeries.has(index)) {            this.hiddenSeries.delete(index);            item.classList.remove('inactive');          } else {            this.hiddenSeries.add(index);            item.classList.add('inactive');          }          this.draw();        });
        this.legend.appendChild(item);      });    }
    bindEvents() {      this.canvas.addEventListener('mousemove', (e) => {        this.handleMouseMove(e);      });
      this.canvas.addEventListener('mouseleave', () => {        this.tooltip.style.display = 'none';      });    }
    handleMouseMove(e) {      const rect = this.canvas.getBoundingClientRect();      const mouseX = e.clientX - rect.left;      const { width, height } = this.canvas;      const { top, right, bottom, left } = this.padding;      const chartWidth = width - left - right;      const xData = this.data.xAxis.data;      const xStep = chartWidth / (xData.length - 1);      // 找到最近的数据点      let closestIndex = -1;      let minDistance = Infinity;      xData.forEach((_, index) => {        const px = left + xStep * index;        const distance = Math.abs(mouseX - px);        if (distance < minDistance && distance < 20) {          minDistance = distance;          closestIndex = index;        }      });      if (closestIndex !== -1) {        let tooltipHTML = `<div style="font-weight: bold; margin-bottom: 5px;">${xData[closestIndex]}</div>`;        this.data.series.forEach((series, seriesIndex) => {          if (!this.hiddenSeries.has(seriesIndex)) {            const value = series.data[closestIndex];            const color = this.colors[seriesIndex % this.colors.length];            tooltipHTML += `<div style="display: flex; align-items: center; margin: 3px 0;">                            <div style="width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 5px;"></div>                            ${series.name}: ${value}                        </div>`;          }        });        this.tooltip.innerHTML = tooltipHTML;        this.tooltip.style.display = 'block';        this.tooltip.style.left = e.clientX + 10 + 'px';        this.tooltip.style.top = e.clientY - 10 + 'px';      } else {        this.tooltip.style.display = 'none';      }    }  }  // 使用示例  const chartData = {    xAxis: {      name: '月份',      data: ['1月''2月''3月''4月''5月''6月''7月''8月''9月''10月''11月''12月']    },    yAxis: {      name: '销售额(万元)'    },    series: [      {        name: '产品A',        data: [12013210113490230210182191234290330]      },      {        name: '产品B',        data: [220182191234290330310323334390330320]      },      {        name: '产品C',        data: [150232201154190330410402334390430420]      }    ]  };  const lineChart = new LineChart('lineChart', {    data: chartData  });</script></body></html>


阅读原文:https://mp.weixin.qq.com/s/Ebefo1b_vso2qznWTxoN9Q


该文章在 2025/12/15 9:07:22 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved