You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Angular 5中Chart.js双折线图交点计算及自定义Div展示求助

好的,我来帮你解决这个在Chart.js折线图上标记两个数据集交点并显示自定义div的问题。这里有一套可行的方案,不需要额外复杂插件,基于Chart.js本身的API和简单的几何计算就能实现:

步骤1:实现线段交点计算函数

首先我们需要一个函数来计算两条线段的交点——折线图的每一段都是由两个数据点连接成的线段,我们需要遍历所有线段对来找到交点。

// 定义点的类型
interface Point {
  x: number;
  y: number;
}

// 计算两条线段的交点(返回交点坐标或null表示无交点)
function findIntersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
  const den = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x);
  if (den === 0) return null; // 线段平行或重合,无有效交点

  const t = ((p1.x - p3.x) * (p3.y - p4.y) - (p1.y - p3.y) * (p3.x - p4.x)) / den;
  const u = -((p1.x - p2.x) * (p1.y - p3.y) - (p1.y - p2.y) * (p1.x - p3.x)) / den;

  // 检查交点是否落在两条线段的范围内
  if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
    return {
      x: p1.x + t * (p2.x - p1.x),
      y: p1.y + t * (p2.y - p1.y)
    };
  }
  return null;
}
步骤2:整合到Angular组件,实现交点标记逻辑

在你的Angular组件中,我们会在图表初始化完成后,获取数据点的像素坐标,遍历线段对找到交点,然后将自定义div定位到交点位置。同时处理窗口resize的情况,保证div位置始终正确。

import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import Chart from 'chart.js';

@Component({
  selector: 'app-line-chart',
  template: `
    <div class="chart-container">
      <canvas #chartCanvas></canvas>
      <div #intersectionDiv class="custom-intersection-mark"></div>
    </div>
  `,
  styles: [`
    .chart-container {
      position: relative;
      width: 80%;
      margin: 0 auto;
    }
    .custom-intersection-mark {
      display: none;
      position: absolute;
      background: #ff4444;
      color: white;
      padding: 3px 6px;
      border-radius: 3px;
      font-size: 12px;
      pointer-events: none;
      z-index: 10;
    }
  `]
})
export class LineChartComponent implements AfterViewInit {
  @ViewChild('chartCanvas') chartCanvas!: ElementRef<HTMLCanvasElement>;
  @ViewChild('intersectionDiv') intersectionDiv!: ElementRef<HTMLDivElement>;
  myChart!: Chart;

  ngAfterViewInit(): void {
    const ctx = this.chartCanvas.nativeElement.getContext('2d')!;
    const labels = ['2018', '2019', '2020', '2021', '2022'];
    // 注意:我给data2补了一个值,保证和data1、labels长度一致,否则线段计算会出错
    const data1 = [100, 345, 657, 788, 300];
    const data2 = [567, 879, 200, 800, 450];

    this.myChart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: labels,
        datasets: [{
          label: 'Data1',
          data: data1,
          fill: false,
          backgroundColor: '#3C96D2',
          pointRadius: 1,
          borderColor: '#3C96D2',
          pointHoverRadius: 3
        }, {
          label: 'Data2',
          data: data2,
          fill: false,
          backgroundColor: '#122449',
          pointRadius: 1,
          borderColor: '#122449',
          pointHoverRadius: 3
        }]
      },
      options: {
        layout: {
          padding: { left: 50, right: 0, top: 0, bottom: 0 }
        },
        hover: { mode: 'point' },
        scales: {
          yAxes: [{
            ticks: { beginAtZero: false },
            scaleLabel: { display: true, labelString: 'prices' }
          }],
          xAxes: [{ scaleLabel: { display: true, labelString: 'Year' } }],
        },
        animation: {
          // 动画完成后再计算交点,确保像素坐标正确
          onComplete: () => this.findAndMarkIntersections()
        }
      }
    });

    // 监听窗口 resize,重新计算交点位置
    window.addEventListener('resize', () => {
      this.myChart.resize();
      this.findAndMarkIntersections();
    });
  }

  private findAndMarkIntersections(): void {
    // 获取两个数据集的元数据(包含每个点的像素坐标)
    const meta1 = this.myChart.getDatasetMeta(0);
    const meta2 = this.myChart.getDatasetMeta(1);
    const points1 = meta1.data.map((point: any) => ({ x: point._model.x, y: point._model.y }));
    const points2 = meta2.data.map((point: any) => ({ x: point._model.x, y: point._model.y }));

    let intersectionPoint: Point | null = null;

    // 遍历所有线段对,查找第一个有效交点
    for (let i = 0; i < points1.length - 1; i++) {
      const p1 = points1[i];
      const p2 = points1[i + 1];
      for (let j = 0; j < points2.length - 1; j++) {
        const p3 = points2[j];
        const p4 = points2[j + 1];
        const point = findIntersection(p1, p2, p3, p4);
        if (point) {
          intersectionPoint = point;
          break;
        }
      }
      if (intersectionPoint) break;
    }

    const div = this.intersectionDiv.nativeElement;
    if (intersectionPoint) {
      // 定位div到交点位置,需要考虑canvas的偏移
      const canvasRect = this.chartCanvas.nativeElement.getBoundingClientRect();
      const containerRect = this.chartCanvas.nativeElement.parentElement?.getBoundingClientRect();
      if (containerRect) {
        div.style.display = 'block';
        // 让div居中对准交点
        div.style.left = `${intersectionPoint.x - div.offsetWidth / 2}px`;
        div.style.top = `${intersectionPoint.y - div.offsetHeight / 2}px`;
        div.textContent = '交点'; // 可自定义显示内容
      }
    } else {
      div.style.display = 'none';
    }
  }
}

// 引入交点计算函数
interface Point {
  x: number;
  y: number;
}

function findIntersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
  const den = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x);
  if (den === 0) return null;

  const t = ((p1.x - p3.x) * (p3.y - p4.y) - (p1.y - p3.y) * (p3.x - p4.x)) / den;
  const u = -((p1.x - p2.x) * (p1.y - p3.y) - (p1.y - p2.y) * (p1.x - p3.x)) / den;

  if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
    return {
      x: p1.x + t * (p2.x - p1.x),
      y: p1.y + t * (p2.y - p1.y)
    };
  }
  return null;
}
关键说明
  1. 数据长度匹配:我给data2补了一个值,确保两个数据集和labels的长度一致,这样每个数据点都能对应到x轴的正确位置,否则线段计算会出现偏差。你需要根据实际业务数据调整,保证数据集长度统一。
  2. 交点定位逻辑:通过getDatasetMeta获取数据点的像素坐标,再用几何函数判断线段是否相交,最后将div定位到交点位置(让div居中对准交点,提升视觉体验)。
  3. 响应式处理:监听窗口resize事件,重新计算交点并更新div位置,保证在图表尺寸变化时标记始终准确。
  4. 多交点支持:当前代码只标记第一个找到的交点,如果需要处理多个交点,可以修改逻辑,创建多个div或者在一个div中显示所有交点信息。

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

火山引擎 最新活动