You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

低JS依赖的环形进度与折线图实现:适配多端及PDF导出

优化方案:低JS依赖的环形进度图与折线图(适配PDF导出)

需求概述

  • 用HTML/CSS/JS(Chart.js)实现环形进度图+折线图,尽可能减少JS依赖
  • 确保在所有浏览器正常渲染,且导出PDF后在iOS、Acrobat等阅读器无显示故障
  • 环形进度图以12点钟为顶部基准,需实现三种状态:
    • 进度恰好到12点:蓝色渐变
    • 进度未到12点:淡绿色渐变,进度未填满
    • 进度超过12点:紫色渐变,进度末端深色渐变+圆角,端点样式随偏离12点距离变化

当前实现代码

HTML

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Age Progress and Graph</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
</head>
<body>
  <div class="progress-container">
    <svg width="160" height="160">
      <circle
        cx="80"
        cy="80"
        r="70"
        stroke="#eee"
        stroke-width="8"
        fill="none"
      ></circle>
      <circle
        cx="80"
        cy="80"
        r="70"
        stroke="url(#gradient)"
        stroke-width="8"
        fill="none"
        stroke-dasharray="440"
        stroke-dashoffset="{{DASH_OFFSET}}"
        stroke-linecap="round"
      ></circle>
      <defs>
        <linearGradient id="gradient" x1="1" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#a646d7" />
          <stop offset="100%" stop-color="#e84fd1" />
        </linearGradient>
      </defs>
    </svg>
    <div class="progress-text">
      <h2 style="font-weight:300;">44.37</h2>
      <small>Older ⤴</small>
    </div>
  </div>

  <div class="chart-container">
    <canvas id="ageChart"></canvas>
  </div>
</body>

CSS

body {
  font-family: sans-serif;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #fff;
  margin: 0;
  padding: 2rem;
  width: 700px;
}

.progress-container {
  position: relative;
  width: 160px;
  height: 160px;
  margin-bottom: 2rem;
}

.progress-container svg {
  transform: rotate(-90deg);
}

.progress-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
}

.progress-text h2 {
  margin: 0;
  font-size: 2rem;
}

.chart-container {
  width: 440px;
  height: 150px;
}

JavaScript

Chart.register(ChartDataLabels);

const currentAge = 42;
const biologicalAge = 44.37;
const fullCircle = 440;
const percent = Math.min(biologicalAge / currentAge, 1);
const offset = fullCircle * (1 - percent);

document.querySelector("circle[stroke-dashoffset='{{DASH_OFFSET}}']")
        .setAttribute("stroke-dashoffset", offset);

const ctx = document.getElementById('ageChart').getContext('2d');
const dataPoints = [40.44, 45.54, 44.37];

const ageChart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['Jan 2024', 'Apr 2024', 'Jul 2024'],
    datasets: [{
      label: 'Biological Age',
      data: dataPoints,
      fill: false,
      tension: 0.4,
      borderColor: function(context) {
        const chart = context.chart;
        const {ctx, chartArea} = chart;
        if (!chartArea) return;
        const gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0);
        gradient.addColorStop(0, '#36d1dc');
        gradient.addColorStop(1, '#a646d7');
        return gradient;
      },
      borderWidth: 3,
      pointRadius: 0
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    layout: {
      padding: {
        left: 20,
        right: 20
      }
    },
    scales: {
      y: {
        suggestedMin: 35,
        suggestedMax: 50,
        ticks: {
          stepSize: 5
        },
        grid: {
          drawTicks: true,
          drawOnChartArea: true
        }
      },
      x: {
        grid: {
          drawTicks: false,
          drawOnChartArea: false,
          drawBorder: false,
          color: 'transparent',
          lineWidth: 0
        },
        border: {
          display: false
        },
        ticks: {
          padding: 8
        },
        offset: true
      }
    },
    plugins: {
      legend: {
        display: false
      },
      tooltip: {
        callbacks: {
          label: ctx => `Age: ${ctx.raw}`
        }
      },
      datalabels: {
        align: 'center',
        anchor: 'center',
        formatter: (value) => value.toFixed(2),
        backgroundColor: (context) => context.dataIndex === context.dataset.data.length - 1 ? '#000' : '#fff',
        borderRadius: 999,
        color: (context) => context.dataIndex === context.dataset.data.length - 1 ? '#fff' : '#000',
        font: {
          weight: 'bold'
        },
        padding: 6,
        borderWidth: 1,
        borderColor: '#000'
      }
    }
  },
  plugins: [ChartDataLabels]
});

优化建议

1. 环形进度图:减少JS依赖,适配三种状态

核心思路:用CSS+少量JS切换渐变与状态,避免复杂JS计算

  • 预定义三种渐变:在SVG的<defs>中提前定义蓝色、淡绿色、紫色三种渐变
    <defs>
      <!-- 恰好到12点:蓝色渐变 -->
      <linearGradient id="gradient-blue" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="#36d1dc" />
        <stop offset="100%" stop-color="#29b6f6" />
      </linearGradient>
      <!-- 未到12点:淡绿色渐变 -->
      <linearGradient id="gradient-green" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="#81c784" />
        <stop offset="100%" stop-color="#a5d6a7" />
      </linearGradient>
      <!-- 超过12点:紫色渐变(末端深色) -->
      <linearGradient id="gradient-purple" x1="1" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="#7b1fa2" />
        <stop offset="70%" stop-color="#a646d7" />
        <stop offset="100%" stop-color="#e84fd1" />
      </linearGradient>
    </defs>
    
  • JS动态切换渐变与端点样式:根据percent值判断状态,直接修改stroke属性和stroke-linecap
    const progressCircle = document.querySelector("circle[stroke-dashoffset='{{DASH_OFFSET}}']");
    progressCircle.setAttribute("stroke-dashoffset", offset);
    
    // 移除模板占位符,设置初始值
    progressCircle.setAttribute("stroke-dashoffset", "440");
    
    // 判断状态并切换样式
    if (percent === 1) {
      // 恰好到12点
      progressCircle.setAttribute("stroke", "url(#gradient-blue)");
      progressCircle.setAttribute("stroke-linecap", "butt"); // 无圆角,刚好闭合
    } else if (percent < 1) {
      // 未到12点
      progressCircle.setAttribute("stroke", "url(#gradient-green)");
      progressCircle.setAttribute("stroke-linecap", "round");
    } else {
      // 超过12点(移除Math.min限制,根据需求保留)
      progressCircle.setAttribute("stroke", "url(#gradient-purple)");
      // 端点圆角大小随偏离距离变化:percent越大,圆角越大
      // 注:SVG原生stroke-linecap不支持动态半径,若需精细控制,可在进度末端添加圆形元素用JS定位
      progressCircle.setAttribute("stroke-linecap", "round");
    }
    

2. 折线图:降低JS复杂度,提升PDF兼容性

  • 移除不必要的插件:若chartjs-plugin-datalabels导致PDF渲染问题,改用静态HTML文本标注数据点,完全去掉插件依赖
    <!-- 在.chart-container中添加静态数据标签 -->
    <div class="chart-container">
      <canvas id="ageChart"></canvas>
      <div class="data-labels">
        <span style="left: 10%; top: 30%;">40.44</span>
        <span style="left: 50%; top: 10%;">45.54</span>
        <span style="left: 90%; top: 20%;" class="last-label">44.37</span>
      </div>
    </div>
    
    .chart-container {
      position: relative;
      width: 440px;
      height: 150px;
    }
    .data-labels span {
      position: absolute;
      padding: 4px 8px;
      border-radius: 999px;
      font-weight: bold;
      font-size: 12px;
    }
    .data-labels span:not(.last-label) {
      background: #fff;
      color: #000;
      border: 1px solid #000;
    }
    .last-label {
      background: #000;
      color: #fff;
    }
    
  • 禁用Chart.js动态特性:在options中关闭动画,确保导出时是静态渲染结果
    options: {
      animation: false, // 关键:避免PDF捕获未完成的动画状态
      // ...其他配置
    }
    

3. 整体PDF兼容性优化

  • 内联所有资源:把Chart.js和CSS代码内联到HTML中,避免CDN资源加载失败导致PDF渲染异常
  • 固定尺寸:确保所有容器使用固定像素尺寸,避免响应式布局在PDF中变形
  • 优先用浏览器导出:使用浏览器自带的"打印"功能选择"保存为PDF",兼容性优于第三方工具

内容的提问来源于stack exchange,提问作者Straxx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 10:25:07