如何检测图表js 3.7.1轴标签的点击?

发布于 2025-01-16 21:07:20 字数 219 浏览 2 评论 0 原文

如何使用 Chart.js 检测轴标签上的点击

在下面的示例中,我只能检测图表本身的点击

https://stackblitz.com/edit/ng2-charts-bar-template-qchyz6

How to detect click on an axis label with chart.js

In the example bellow, I can only detect click on the graph itself

https://stackblitz.com/edit/ng2-charts-bar-template-qchyz6

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

纵性 2025-01-23 21:07:20

您需要实现一个自定义插件来监听画布的所有事件:

const findLabel = (labels, evt) => {
  let found = false;
  let res = null;

  labels.forEach(l => {
    l.labels.forEach((label, index) => {
      if (evt.x > label.x && evt.x < label.x2 && evt.y > label.y && evt.y < label.y2) {
        res = {
          label: label.label,
          index
        };
        found = true;
      }
    });
  });

  return [found, res];
};

const getLabelHitboxes = (scales) => (Object.values(scales).map((s) => ({
  scaleId: s.id,
  labels: s._labelItems.map((e, i) => ({
    x: e.translation[0] - s._labelSizes.widths[i],
    x2: e.translation[0] + s._labelSizes.widths[i] / 2,
    y: e.translation[1] - s._labelSizes.heights[i] / 2,
    y2: e.translation[1] + s._labelSizes.heights[i] / 2,
    label: e.label,
    index: i
  }))
})));

const plugin = {
  id: 'customHover',
  afterEvent: (chart, event, opts) => {
    const evt = event.event;

    if (evt.type !== 'click') {
      return;
    }

    const [found, labelInfo] = findLabel(getLabelHitboxes(chart.scales), evt);

    if (found) {
      console.log(labelInfo);
    }

  }
}

Chart.register(plugin);

const options = {
  type: 'line',
  data: {
    labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
    datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderColor: 'pink'
      },
      {
        label: '# of Points',
        data: [7, 11, 5, 8, 3, 7],
        borderColor: 'orange'
      }
    ]
  },
  options: {}
}

const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
  <canvas id="chartJSContainer" width="600" height="400"></canvas>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
</body>

You will need to implement a custom plugin that can listen to all the events of the canvas:

const findLabel = (labels, evt) => {
  let found = false;
  let res = null;

  labels.forEach(l => {
    l.labels.forEach((label, index) => {
      if (evt.x > label.x && evt.x < label.x2 && evt.y > label.y && evt.y < label.y2) {
        res = {
          label: label.label,
          index
        };
        found = true;
      }
    });
  });

  return [found, res];
};

const getLabelHitboxes = (scales) => (Object.values(scales).map((s) => ({
  scaleId: s.id,
  labels: s._labelItems.map((e, i) => ({
    x: e.translation[0] - s._labelSizes.widths[i],
    x2: e.translation[0] + s._labelSizes.widths[i] / 2,
    y: e.translation[1] - s._labelSizes.heights[i] / 2,
    y2: e.translation[1] + s._labelSizes.heights[i] / 2,
    label: e.label,
    index: i
  }))
})));

const plugin = {
  id: 'customHover',
  afterEvent: (chart, event, opts) => {
    const evt = event.event;

    if (evt.type !== 'click') {
      return;
    }

    const [found, labelInfo] = findLabel(getLabelHitboxes(chart.scales), evt);

    if (found) {
      console.log(labelInfo);
    }

  }
}

Chart.register(plugin);

const options = {
  type: 'line',
  data: {
    labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
    datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderColor: 'pink'
      },
      {
        label: '# of Points',
        data: [7, 11, 5, 8, 3, 7],
        borderColor: 'orange'
      }
    ]
  },
  options: {}
}

const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
  <canvas id="chartJSContainer" width="600" height="400"></canvas>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
</body>

沉默的熊 2025-01-23 21:07:20

如果你的X轴标签有旋转,那么你可以尝试下面的代码:

import {Chart as ChartJS} from 'chart.js';

// Function to calculate cotangent (ctg) from radians
const ctg = (radians: number): number => 1 / Math.tan(radians);

const findLabel = (labels: any, evt: any): LabelData | undefined => {
  let res: LabelData | undefined = undefined;
  const scalesYwidth = evt.chart.scales.y.width;
  const scalesXheight = evt.chart.scales.x.height;
  const eventY = evt.y - (evt.chart.height - scalesXheight);
  let min = Infinity;

  for (const l of labels) {
    for (const index in l.labels) {
      const label = l.labels[index];

      if (eventY > 10 && evt.x > scalesYwidth - 10) {
        if (evt.x > label.x && evt.x < label.x2) {
          const b = Math.abs(eventY * ctg(label.rotation));
          const xClosest = Math.abs(label.x2 - (evt.x + b));
     
          if (xClosest < min) {
            min = xClosest;
            res = {
              label: label.label,
              index: +index,
              axis: 'x'
            };
          }
        }
      } else if (evt.x < scalesYwidth - 10 && evt.y > label.y && evt.y < label.y2) {
        res = {
          label: label.label,
          index: +index,
          axis: 'y'
        };
      }
    }
  }

  return res;
};

const getLabelHitboxes = (scales: any) =>
  Object.values(scales).map((s: any) => {
    return {
      scaleId: s.id,
      labels: s._labelItems.map((e: any, i: number) => {
        const ts = e.options.translation;
        return {
          x: ts[0] - s._labelSizes.widths[i],
          x2: ts[0] + s._labelSizes.widths[i] / 2,
          y: ts[1] - s._labelSizes.heights[i] / 2,
          y2: ts[1] + s._labelSizes.heights[i] / 2,
          rotation: e.options.rotation,
          label: e.label,
          index: i
        };
      })
    };
  });

const getLabelEventData = (chart: ChartJS, chartEvent: any): LabelData | undefined =>
  findLabel(getLabelHitboxes(chart.scales), chartEvent);

export const getChartPluginLabelClick = (onClick?: (data?: LabelData) => void) => ({
  id: 'customHover',
  afterEvent: (chart: ChartJS, event: any, opts: any) => {
    const evt = event.event;
    if (evt.type !== 'click') return;
    const data = getLabelEventData(chart, evt);
    onClick && onClick(data);
  }
});
// register global
// ChartJS.register(getPlugin());

export interface LabelData {
  label: string | string[];
  index: number;
  axis: 'x' | 'y';
}
import {Chart as ChartJS} from 'chart.js';
import {Chart} from 'react-chartjs-2';
import 'chart.js/auto';

<div style={{position: 'relative', height: '600px', width: '100%'}}>
        <Chart
          ref={ref}
          type="bar"
          plugins={[
            getChartPluginLabelClick((data?: LabelData) => {
              console.log(data);
            })
          ]}
          options={options}
          data={data}
          onClick={onClick}
        />
</div>

If your X-axis labels have rotation, then you can try the below code:

import {Chart as ChartJS} from 'chart.js';

// Function to calculate cotangent (ctg) from radians
const ctg = (radians: number): number => 1 / Math.tan(radians);

const findLabel = (labels: any, evt: any): LabelData | undefined => {
  let res: LabelData | undefined = undefined;
  const scalesYwidth = evt.chart.scales.y.width;
  const scalesXheight = evt.chart.scales.x.height;
  const eventY = evt.y - (evt.chart.height - scalesXheight);
  let min = Infinity;

  for (const l of labels) {
    for (const index in l.labels) {
      const label = l.labels[index];

      if (eventY > 10 && evt.x > scalesYwidth - 10) {
        if (evt.x > label.x && evt.x < label.x2) {
          const b = Math.abs(eventY * ctg(label.rotation));
          const xClosest = Math.abs(label.x2 - (evt.x + b));
     
          if (xClosest < min) {
            min = xClosest;
            res = {
              label: label.label,
              index: +index,
              axis: 'x'
            };
          }
        }
      } else if (evt.x < scalesYwidth - 10 && evt.y > label.y && evt.y < label.y2) {
        res = {
          label: label.label,
          index: +index,
          axis: 'y'
        };
      }
    }
  }

  return res;
};

const getLabelHitboxes = (scales: any) =>
  Object.values(scales).map((s: any) => {
    return {
      scaleId: s.id,
      labels: s._labelItems.map((e: any, i: number) => {
        const ts = e.options.translation;
        return {
          x: ts[0] - s._labelSizes.widths[i],
          x2: ts[0] + s._labelSizes.widths[i] / 2,
          y: ts[1] - s._labelSizes.heights[i] / 2,
          y2: ts[1] + s._labelSizes.heights[i] / 2,
          rotation: e.options.rotation,
          label: e.label,
          index: i
        };
      })
    };
  });

const getLabelEventData = (chart: ChartJS, chartEvent: any): LabelData | undefined =>
  findLabel(getLabelHitboxes(chart.scales), chartEvent);

export const getChartPluginLabelClick = (onClick?: (data?: LabelData) => void) => ({
  id: 'customHover',
  afterEvent: (chart: ChartJS, event: any, opts: any) => {
    const evt = event.event;
    if (evt.type !== 'click') return;
    const data = getLabelEventData(chart, evt);
    onClick && onClick(data);
  }
});
// register global
// ChartJS.register(getPlugin());

export interface LabelData {
  label: string | string[];
  index: number;
  axis: 'x' | 'y';
}
import {Chart as ChartJS} from 'chart.js';
import {Chart} from 'react-chartjs-2';
import 'chart.js/auto';

<div style={{position: 'relative', height: '600px', width: '100%'}}>
        <Chart
          ref={ref}
          type="bar"
          plugins={[
            getChartPluginLabelClick((data?: LabelData) => {
              console.log(data);
            })
          ]}
          options={options}
          data={data}
          onClick={onClick}
        />
</div>
被翻牌 2025-01-23 21:07:20

更新以使其与 ChartJs 4.4.2 一起使用。这也应该适用于旋转标签:

// Helper function to rotate a point around a center
function rotatePoint(cx, cy, angle, px, py) {
    const s = Math.sin(angle);
    const c = Math.cos(angle);

    // Translate point to origin
    px -= cx;
    py -= cy;

    // Rotate point
    const xnew = px * c - py * s;
    const ynew = px * s + py * c;

    // Translate point back
    return { x: xnew + cx, y: ynew + cy };
}

//Helper function to check if a point is inside a polygon
function isPointInPolygon(polygonObj, px, py) {
    const polygon = Object.values(polygonObj).filter(point => typeof point === 'object');
    let inside = false;
    for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
         const xi = polygon[i].x, yi = polygon[i].y;
         const xj = polygon[j].x, yj = polygon[j].y;

         // Check if point is on the edge
         const onEdge = (py - yi) * (xj - xi) === (px - xi) * (yj - yi) &&
              Math.min(xi, xj) <= px && px <= Math.max(xi, xj) &&
              Math.min(yi, yj) <= py && py <= Math.max(yi, yj);
         if (onEdge) {
              return true;
         }

         const intersect = ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi);
         if (intersect) inside = !inside;
    }
    return inside;
}

const findLabel = (labels, evt) => {
    if (!labels) {
        return [false, null];
    }
    let found = false;
    let res = null;

    labels.forEach((label) => {
        console.log(isPointInPolygon(label, evt.x, evt.y));
        if (isPointInPolygon(label, evt.x, evt.y)) {
            res = {
                label: label.label,
                index: label.index,
            };
            found = true;
        }
    });
    return [found, res];
};

const getLabelHitBoxes = (x) => {
    if (!x._labelItems) {
        return;
    }

    const hitBoxes = x._labelItems.map((e, i) => {
        const width = x._labelSizes.widths[i];
        const height = x._labelSizes.heights[i];
        const rotation = e.options.rotation;
        //if there is no rotation the translation is the top center of the label box
        //If there is a rotation the translation is the top right corner of the label box
        let urx;
        if (rotation === 0) {
            urx = e.options.translation[0] + width / 2;
        } else {
            urx = e.options.translation[0];
        }
        const ury = e.options.translation[1];

        // Step 2: Calculate the corners of the rectangle
        const corners = [
            { x: urx, y: ury }, // Top-right
            { x: urx - width, y: ury }, // Top-left
            { x: urx - width, y: ury + height }, // Bottom-left
            { x: urx, y: ury + height }, // Bottom-right
        ];
        // Step 3: Rotate corners around top right corner
        const hitBox = corners.map((corner) =>
            rotatePoint(urx, ury, rotation, corner.x, corner.y)
        );

        return { ...hitBox, label: e.label, index: i };
    });
    console.log(typeof hitBoxes);
    return hitBoxes;
};

const plugin = {
    id: 'chartClickXLabel',
    afterEvent: (chart, event, opts) => {
        const evt = event.event;

        if (evt.type !== 'click') {
            return;
        }

        const [found, labelInfo] = findLabel(getLabelHitBoxes(chart.scales.x), evt);

        if (found) {
            console.log(labelInfo);
            // opts.listeners.click();
        }

    }
};

Update to make it work with ChartJs 4.4.2. This should work with rotated label too:

// Helper function to rotate a point around a center
function rotatePoint(cx, cy, angle, px, py) {
    const s = Math.sin(angle);
    const c = Math.cos(angle);

    // Translate point to origin
    px -= cx;
    py -= cy;

    // Rotate point
    const xnew = px * c - py * s;
    const ynew = px * s + py * c;

    // Translate point back
    return { x: xnew + cx, y: ynew + cy };
}

//Helper function to check if a point is inside a polygon
function isPointInPolygon(polygonObj, px, py) {
    const polygon = Object.values(polygonObj).filter(point => typeof point === 'object');
    let inside = false;
    for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
         const xi = polygon[i].x, yi = polygon[i].y;
         const xj = polygon[j].x, yj = polygon[j].y;

         // Check if point is on the edge
         const onEdge = (py - yi) * (xj - xi) === (px - xi) * (yj - yi) &&
              Math.min(xi, xj) <= px && px <= Math.max(xi, xj) &&
              Math.min(yi, yj) <= py && py <= Math.max(yi, yj);
         if (onEdge) {
              return true;
         }

         const intersect = ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi);
         if (intersect) inside = !inside;
    }
    return inside;
}

const findLabel = (labels, evt) => {
    if (!labels) {
        return [false, null];
    }
    let found = false;
    let res = null;

    labels.forEach((label) => {
        console.log(isPointInPolygon(label, evt.x, evt.y));
        if (isPointInPolygon(label, evt.x, evt.y)) {
            res = {
                label: label.label,
                index: label.index,
            };
            found = true;
        }
    });
    return [found, res];
};

const getLabelHitBoxes = (x) => {
    if (!x._labelItems) {
        return;
    }

    const hitBoxes = x._labelItems.map((e, i) => {
        const width = x._labelSizes.widths[i];
        const height = x._labelSizes.heights[i];
        const rotation = e.options.rotation;
        //if there is no rotation the translation is the top center of the label box
        //If there is a rotation the translation is the top right corner of the label box
        let urx;
        if (rotation === 0) {
            urx = e.options.translation[0] + width / 2;
        } else {
            urx = e.options.translation[0];
        }
        const ury = e.options.translation[1];

        // Step 2: Calculate the corners of the rectangle
        const corners = [
            { x: urx, y: ury }, // Top-right
            { x: urx - width, y: ury }, // Top-left
            { x: urx - width, y: ury + height }, // Bottom-left
            { x: urx, y: ury + height }, // Bottom-right
        ];
        // Step 3: Rotate corners around top right corner
        const hitBox = corners.map((corner) =>
            rotatePoint(urx, ury, rotation, corner.x, corner.y)
        );

        return { ...hitBox, label: e.label, index: i };
    });
    console.log(typeof hitBoxes);
    return hitBoxes;
};

const plugin = {
    id: 'chartClickXLabel',
    afterEvent: (chart, event, opts) => {
        const evt = event.event;

        if (evt.type !== 'click') {
            return;
        }

        const [found, labelInfo] = findLabel(getLabelHitBoxes(chart.scales.x), evt);

        if (found) {
            console.log(labelInfo);
            // opts.listeners.click();
        }

    }
};

兔姬 2025-01-23 21:07:20

使用 ng2-charts (chart-js v3.7.1) 适配 Angular

只需使用 Chart.register

即可将以下函数放入组件 ngOnInit()

RegisterPlugin() {
    Chart.register(
      {
        id: 'yAxisCustomClick',
        afterEvent: (chart: Chart<'bar'>, event: {
          event: ChartEvent;
          replay: boolean;
          changed?: boolean | undefined;
          cancelable: false;
          inChartArea: boolean
        }) => {
          const evt = event.event;
          if (evt.type === 'click' && evt.x! < Object.values(chart.scales).filter(s => s.id === 'x')[0].getBasePixel()) {
            const labelIndex = Object.values(chart.scales).filter(s => s.id === 'y')[0].getValueForPixel(evt.y!);
            const label = Object.values(chart.scales).filter(s => s.id === 'y')[0].getTicks()[labelIndex!]?.label;
            if (label) {
              console.log('Do the stuff for', label)
            }
          }
        }
      }
    );
  }

stackblitz 中的示例已更新
https://stackblitz.com/edit/ng2-charts-bar-template- qchyz6

Adpatation for Angular with ng2-charts (chart-js v3.7.1)

Just use Chart.register

i.e. put the following functiion into component ngOnInit()

RegisterPlugin() {
    Chart.register(
      {
        id: 'yAxisCustomClick',
        afterEvent: (chart: Chart<'bar'>, event: {
          event: ChartEvent;
          replay: boolean;
          changed?: boolean | undefined;
          cancelable: false;
          inChartArea: boolean
        }) => {
          const evt = event.event;
          if (evt.type === 'click' && evt.x! < Object.values(chart.scales).filter(s => s.id === 'x')[0].getBasePixel()) {
            const labelIndex = Object.values(chart.scales).filter(s => s.id === 'y')[0].getValueForPixel(evt.y!);
            const label = Object.values(chart.scales).filter(s => s.id === 'y')[0].getTicks()[labelIndex!]?.label;
            if (label) {
              console.log('Do the stuff for', label)
            }
          }
        }
      }
    );
  }

Example in stackblitz udpated
https://stackblitz.com/edit/ng2-charts-bar-template-qchyz6

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文