如何选择日期范围(如onClick但拖动/选择)

Tob*_*nke 11 javascript chart.js

我想用Chart.js 重写vizwit,我很难弄清楚如何让日期/时间图表交互起作用.如果您尝试在此演示中选择日期范围,您会看到它会过滤其他图表.如何让Chart.js让我在其时间尺度图表上选择这样的范围?它似乎默认情况下只允许我点击特定的日期点.

谢谢你的时间.

Jon*_*mit 23

在@ jordanwillis和你的答案的基础上,你可以轻松地实现你想要的任何东西,方法是在你的图表上放置另一个画布.
只需添加pointer-events:none它的样式,以确保它不会影响图表的事件.
无需使用注释插件.
例如(在此示例中canvas是原始图表画布,overlay并且您的新画布位于顶部):

var options = {
  type: 'line',
  data: {
    labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
    datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderWidth: 1
      },
      {
        label: '# of Points',
        data: [7, 11, 5, 8, 3, 7],
        borderWidth: 1
      }
    ]
  },
  options: {
    scales: {
      yAxes: [{
        ticks: {
          reverse: false
        }
      }]
    }
  }
}

var canvas = document.getElementById('chartJSContainer');
var ctx = canvas.getContext('2d');
var chart = new Chart(ctx, options);
var overlay = document.getElementById('overlay');
var startIndex = 0;
overlay.width = canvas.width;
overlay.height = canvas.height;
var selectionContext = overlay.getContext('2d');
var selectionRect = {
  w: 0,
  startX: 0,
  startY: 0
};
var drag = false;
canvas.addEventListener('pointerdown', evt => {
  const points = chart.getElementsAtEventForMode(evt, 'index', {
    intersect: false
  });
  startIndex = points[0]._index;
  const rect = canvas.getBoundingClientRect();
  selectionRect.startX = evt.clientX - rect.left;
  selectionRect.startY = chart.chartArea.top;
  drag = true;
  // save points[0]._index for filtering
});
canvas.addEventListener('pointermove', evt => {

  const rect = canvas.getBoundingClientRect();
  if (drag) {
    const rect = canvas.getBoundingClientRect();
    selectionRect.w = (evt.clientX - rect.left) - selectionRect.startX;
    selectionContext.globalAlpha = 0.5;
    selectionContext.clearRect(0, 0, canvas.width, canvas.height);
    selectionContext.fillRect(selectionRect.startX,
      selectionRect.startY,
      selectionRect.w,
      chart.chartArea.bottom - chart.chartArea.top);
  } else {
    selectionContext.clearRect(0, 0, canvas.width, canvas.height);
    var x = evt.clientX - rect.left;
    if (x > chart.chartArea.left) {
      selectionContext.fillRect(x,
        chart.chartArea.top,
        1,
        chart.chartArea.bottom - chart.chartArea.top);
    }
  }
});
canvas.addEventListener('pointerup', evt => {

  const points = chart.getElementsAtEventForMode(evt, 'index', {
    intersect: false
  });
  drag = false;
  console.log('implement filter between ' + options.data.labels[startIndex] + ' and ' + options.data.labels[points[0]._index]);  
});
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.0/Chart.js"></script>

<body>
  <canvas id="overlay" width="600" height="400" style="position:absolute;pointer-events:none;"></canvas>
  <canvas id="chartJSContainer" width="600" height="400"></canvas>
</body>
Run Code Online (Sandbox Code Playgroud)

请注意,我们将事件和坐标基于原始画布,但我们在叠加层上绘制.这样我们就不会弄乱图表的功能.


Qua*_*ali 7

对于所有对 Jony Adamits 解决方案感兴趣的人,我根据他的实现创建了一个 ChartJs 插件。另外,我修复了一些有关调整图表大小和检测所选数据点的小问题。

请随意使用它或为其创建插件 github 存储库。

安装

import "chart.js";
import {Chart} from 'chart.js';
import {ChartJsPluginRangeSelect} from "./chartjs-plugin-range-select";

Chart.pluginService.register(new ChartJsPluginRangeSelect());
Run Code Online (Sandbox Code Playgroud)

配置

let chartOptions = rangeSelect: {
  onSelectionChanged: (result: Array<Array<any>>) => {
    console.log(result);
  }
}
Run Code Online (Sandbox Code Playgroud)

插件代码

import {Chart, ChartSize, PluginServiceGlobalRegistration, PluginServiceRegistrationOptions} from "chart.js";

interface ChartJsPluginRangeSelectExtendedOptions {
  rangeSelect?: RangeSelectOptions;
}

interface RangeSelectOptions {
  onSelectionChanged?: (filteredDataSets: Array<Array<any>>) => void;
  fillColor?: string | CanvasGradient | CanvasPattern;
  cursorColor?: string | CanvasGradient | CanvasPattern;
  cursorWidth?: number;
  state?: RangeSelectState;
}

interface RangeSelectState {
  canvas: HTMLCanvasElement;
}

interface ActiveSelection {
  x: number;
  w: number;
}

export class ChartJsPluginRangeSelect implements PluginServiceRegistrationOptions, PluginServiceGlobalRegistration {
  public id = 'rangeSelect';

  beforeInit(chartInstance: Chart, options?: any) {
    const opts = (chartInstance.config.options as ChartJsPluginRangeSelectExtendedOptions);
    if (opts.rangeSelect) {
      const canvas = this.createOverlayCanvas(chartInstance);
      opts.rangeSelect = Object.assign({}, opts.rangeSelect, {state: {canvas: canvas}});
      chartInstance.canvas.parentElement.prepend(canvas);
    }
  }

  resize(chartInstance: Chart, newChartSize: ChartSize, options?: any) {
    const rangeSelectOptions = (chartInstance.config.options as ChartJsPluginRangeSelectExtendedOptions).rangeSelect;
    if (rangeSelectOptions) {
      rangeSelectOptions.state.canvas.width = newChartSize.width;
      rangeSelectOptions.state.canvas.height = newChartSize.height;
    }
  }

  destroy(chartInstance: Chart) {
    const rangeSelectOptions = (chartInstance.config.options as ChartJsPluginRangeSelectExtendedOptions).rangeSelect;
    if (rangeSelectOptions) {
      rangeSelectOptions.state.canvas.remove();
      delete rangeSelectOptions.state;
    }
  }

  private createOverlayCanvas(chart: Chart): HTMLCanvasElement {
    const rangeSelectOptions = (chart.config.options as ChartJsPluginRangeSelectExtendedOptions).rangeSelect;
    const overlay = this.createOverlayHtmlCanvasElement(chart);
    const ctx = overlay.getContext('2d');

    let selection: ActiveSelection = {x: 0, w: 0};
    let isDragging = false;

    chart.canvas.addEventListener('pointerdown', evt => {
      const rect = chart.canvas.getBoundingClientRect();
      selection.x = this.getXInChartArea(evt.clientX - rect.left, chart);
      isDragging = true;
    });

    chart.canvas.addEventListener('pointerleave', evt => {
      if (!isDragging) {
        ctx.clearRect(0, 0, overlay.width, overlay.height);
      }
    });

    chart.canvas.addEventListener('pointermove', evt => {
      ctx.clearRect(0, 0, chart.canvas.width, chart.canvas.height);

      const chartContentRect = chart.canvas.getBoundingClientRect();
      const currentX = this.getXInChartArea(evt.clientX - chartContentRect.left, chart);
      if (isDragging) {
        selection.w = currentX - selection.x;
        ctx.fillStyle = rangeSelectOptions.fillColor || '#00000044';
        ctx.fillRect(selection.x, chart.chartArea.top, selection.w, chart.chartArea.bottom - chart.chartArea.top);
      } else {
        const cursorWidth = rangeSelectOptions.cursorWidth || 1;
        ctx.fillStyle = rangeSelectOptions.cursorColor || '#00000088';
        ctx.fillRect(currentX, chart.chartArea.top, cursorWidth, chart.chartArea.bottom - chart.chartArea.top);
      }
    });

    chart.canvas.addEventListener('pointerup', evt => {
      const onSelectionChanged = rangeSelectOptions.onSelectionChanged;
      if (onSelectionChanged) {
        onSelectionChanged(this.getDataSetDataInSelection(selection, chart));
      }
      selection = {w: 0, x: 0};
      isDragging = false;
      ctx.clearRect(0, 0, overlay.width, overlay.height);
    });
    return overlay;
  }

  private createOverlayHtmlCanvasElement(chartInstance: Chart): HTMLCanvasElement {
    const overlay = document.createElement('canvas');
    overlay.style.position = 'absolute';
    overlay.style.pointerEvents = 'none';
    overlay.width = chartInstance.canvas.width;
    overlay.height = chartInstance.canvas.height;
    return overlay;
  }

  private getXInChartArea(val: number, chartInstance: Chart) {
    return Math.min(Math.max(val, chartInstance.chartArea.left), chartInstance.chartArea.right);
  }

  private getDataSetDataInSelection(selection: ActiveSelection, chartInstance: Chart): Array<any> {
    const result = [];
    const xMin = Math.min(selection.x, selection.x + selection.w);
    const xMax = Math.max(selection.x, selection.x + selection.w);
    for (let i = 0; i < chartInstance.data.datasets.length; i++) {
      result[i] = chartInstance.getDatasetMeta(i)
        .data
        .filter(data => xMin <= data._model.x && xMax >= data._model.x)
        .map(data => chartInstance.data.datasets[i].data[data._index]);
    }
    return result;
  }
}
Run Code Online (Sandbox Code Playgroud)

  • Kali 你没有提到这是在 TypeScript 中。主要代码也使用 TS 的东西 Array&lt;Array&lt;any&gt;&gt; 请在 jsfiddle 中给出一个工作示例,消除所有 TS 毕竟我们正在处理 Chart.js 而不是 Chart.ts 我可以感觉到你的代码可能会工作,因此请帮忙。 (3认同)