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

如何在Chart.js中实现画布输入框的时间范围选择与区间高亮?

实现Chart.js时间轴拖动选框与区间高亮

核心思路

通过绝对定位的输入框实现拖动交互,利用Chart.js的坐标转换方法关联像素位置与时间值,再借助annotation插件实现区间颜色填充。

完整代码实现

HTML结构

<div style="position: relative; width: 800px; margin: 0 auto;">
  <input type="text" id="startInput" placeholder="开始时间" style="position: absolute; top: 0; left: 50px; z-index: 10;">
  <input type="text" id="endInput" placeholder="结束时间" style="position: absolute; top: 0; left: 200px; z-index: 10;">
  <canvas id="myChart"></canvas>
</div>

CSS样式

input {
  padding: 4px 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 12px;
  cursor: grab;
}
input:active {
  cursor: grabbing;
}

JavaScript逻辑

// 先在HTML中加载依赖脚本
// <script src="https://cdn.jsdelivr.net/npm/chart.js@4.2.1/dist/chart.umd.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@2.1.1/dist/chartjs-plugin-annotation.min.js"></script>

const ctx = document.getElementById('myChart').getContext('2d');

// 生成模拟时间序列数据
const generateTimeData = () => {
  const data = [];
  let currentDate = new Date();
  for (let i = 0; i < 30; i++) {
    data.push({
      x: new Date(currentDate.getTime() - (30 - i) * 3600000), // 每小时一个数据点
      y: Math.floor(Math.random() * 50) + 10
    });
    currentDate.setHours(currentDate.getHours() - 1);
  }
  return data;
};

// 初始化图表
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    datasets: [{
      label: '时间序列数据',
      data: generateTimeData(),
      borderColor: '#36A2EB',
      tension: 0.1,
      fill: false
    }]
  },
  options: {
    scales: {
      x: {
        type: 'time',
        time: {
          unit: 'hour',
          displayFormats: {
            hour: 'yyyy-MM-dd HH:mm'
          }
        },
        position: 'bottom'
      },
      y: {
        beginAtZero: true
      }
    },
    plugins: {
      annotation: {
        annotations: {
          highlightRange: {
            type: 'box',
            xMin: null,
            xMax: null,
            backgroundColor: 'rgba(255, 99, 132, 0.2)',
            borderColor: 'rgba(255, 99, 132, 0.5)',
            borderWidth: 1
          }
        }
      }
    }
  },
  plugins: [ChartAnnotation]
});

// 获取输入框元素
const startInput = document.getElementById('startInput');
const endInput = document.getElementById('endInput');
let draggingElement = null;

// 格式化时间显示
const formatTime = (date) => {
  return date.toISOString().slice(0, 16).replace('T', ' ');
};

// 更新输入框位置与对应时间值
const updateInput = (input, pixelX) => {
  const xAxis = chart.scales.x;
  // 限制输入框在X轴范围内拖动
  const minPixel = xAxis.getPixelForValue(xAxis.min);
  const maxPixel = xAxis.getPixelForValue(xAxis.max);
  const clampedPixel = Math.max(minPixel, Math.min(maxPixel, pixelX));

  input.style.left = `${clampedPixel}px`;
  // 转换像素位置为时间值
  const timeValue = xAxis.getValueForPixel(clampedPixel);
  input.value = formatTime(timeValue);
  
  // 更新高亮区间
  updateHighlightRange();
};

// 更新区间高亮效果
const updateHighlightRange = () => {
  const startValue = startInput.value ? new Date(startInput.value) : null;
  const endValue = endInput.value ? new Date(endInput.value) : null;

  if (startValue && endValue) {
    const minTime = new Date(Math.min(startValue.getTime(), endValue.getTime()));
    const maxTime = new Date(Math.max(startValue.getTime(), endValue.getTime()));
    
    chart.options.plugins.annotation.annotations.highlightRange.xMin = minTime;
    chart.options.plugins.annotation.annotations.highlightRange.xMax = maxTime;
    chart.update();
  }
};

// 拖动事件处理
const handleMouseDown = (e) => {
  draggingElement = e.target;
};

const handleMouseMove = (e) => {
  if (!draggingElement) return;
  
  const containerRect = ctx.canvas.parentElement.getBoundingClientRect();
  const pixelX = e.clientX - containerRect.left;
  updateInput(draggingElement, pixelX);
};

const handleMouseUp = () => {
  draggingElement = null;
};

// 绑定拖动事件
startInput.addEventListener('mousedown', handleMouseDown);
endInput.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);

// 初始化输入框默认位置与值
const initInputs = () => {
  const xAxis = chart.scales.x;
  const startPixel = xAxis.getPixelForValue(xAxis.min) + 50;
  const midPixel = (xAxis.getPixelForValue(xAxis.min) + xAxis.getPixelForValue(xAxis.max)) / 2;
  
  updateInput(startInput, startPixel);
  updateInput(endInput, midPixel);
};

// 图表渲染完成后初始化输入框
chart.addEventListener('render', initInputs);

关键细节说明

  1. 坐标转换:利用Chart.js的getPixelForValue和getValueForPixel方法,实现像素位置与时间值的双向映射,这是拖动交互的核心。
  2. 边界限制:通过X轴的最小/最大像素值,限制输入框拖动范围,避免生成无效时间值。
  3. 区间高亮:使用chartjs-plugin-annotation插件绘制半透明矩形,实现两输入框之间的区域高亮;也可通过Chart.js的beforeDraw钩子自定义绘制逻辑。
  4. 时间适配:依托chartjs-adapter-date-fns插件,实现时间轴的解析与格式化,确保输入框显示格式与X轴刻度一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 14:35:35