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

新手上路,请多包涵

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

谢谢你的时间。

原文由 Tobias Fünke 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 799
2 个回答

在@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]);
});
 <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>

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

原文由 Jony Adamit 发布,翻译遵循 CC BY-SA 3.0 许可协议

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

随意使用它或为它创建一个插件 github repo。

安装

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

Chart.pluginService.register(new ChartJsPluginRangeSelect());

配置

let chartOptions = rangeSelect: {
  onSelectionChanged: (result: Array<Array<any>>) => {
    console.log(result);
  }
}

插件代码

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;
  }
}

原文由 Kali 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题