Last active
January 7, 2021 10:14
-
-
Save crisu83/fcdc1cb7021d65c54f3a3365f419fecf to your computer and use it in GitHub Desktop.
Draggable plugin for Chart.js 3 written in TypeScript.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Chart as ChartJsChart, ChartOptions} from 'chart.js'; | |
| import {D3DragEvent, drag} from 'd3-drag'; | |
| import {select} from 'd3-selection'; | |
| type ChartElement = any; | |
| type Chart = ChartJsChart & {options: ChartOptions & DraggableOptions}; | |
| type DragEvent = D3DragEvent<ChartElement, number, number>; | |
| enum DragEventType { | |
| Start = 'start', | |
| Drag = 'drag', | |
| End = 'end', | |
| } | |
| export type DraggableStartEvent = { | |
| chart: Chart; | |
| datasetIndex: number; | |
| dragEvent: DragEvent; | |
| element: ChartElement; | |
| index: number; | |
| value: number; | |
| }; | |
| export type DraggableStartCallback = (event: DraggableStartEvent) => void; | |
| export type DraggableChangeEvent = { | |
| chart: Chart; | |
| datasetIndex: number; | |
| dragEvent: DragEvent; | |
| element: ChartElement; | |
| index: number; | |
| nextValue: number; | |
| prevValue: number; | |
| }; | |
| export type DraggableChangeCallback = (event: DraggableChangeEvent) => boolean; | |
| export type DraggableEndEvent = { | |
| chart: Chart; | |
| datasetIndex: number; | |
| dragEvent: DragEvent; | |
| element: ChartElement; | |
| index: number; | |
| value: number; | |
| }; | |
| export type DraggableEndCallback = (event: DraggableEndEvent) => void; | |
| export type DraggableOptions = { | |
| onDragChange?: DraggableChangeCallback; | |
| onDragEnd?: DraggableEndCallback; | |
| onDragStart?: DraggableStartCallback; | |
| scaleId?: ScaleId; | |
| }; | |
| enum ScaleId { | |
| X = 'x', | |
| Y = 'y', | |
| } | |
| const isDraggableEnabled = (options: DraggableOptions): boolean => | |
| Object.keys(options).length > 0; | |
| const getDraggableOptions = (chart: Chart): DraggableOptions => | |
| chart.options.plugins['draggable']; | |
| const sanitzeValue = (value: number, minValue: number, maxValue: number) => | |
| Math.round(Math.min(Math.max(value > 0 ? value : 0, minValue), maxValue)); | |
| const calculateNextValue = ( | |
| dragEvent: DragEvent, | |
| chart: Chart, | |
| scaleId: ScaleId | |
| ): number => { | |
| const scale = chart.scales[scaleId]; | |
| const canvasBounds = chart.canvas.getBoundingClientRect(); | |
| const value = scale.getValueForPixel( | |
| scaleId === ScaleId.X | |
| ? dragEvent.sourceEvent.clientX - canvasBounds.left | |
| : dragEvent.sourceEvent.clientY - canvasBounds.top | |
| ); | |
| return sanitzeValue(value, scale.min, scale.max); | |
| }; | |
| interface DragStartFn { | |
| (chart: Chart, callback: DraggableStartCallback): ( | |
| dragEvent: DragEvent | |
| ) => void; | |
| } | |
| const getElementAtEvent = (chart: Chart, dragEvent: DragEvent) => | |
| chart.getElementsAtEventForMode( | |
| dragEvent.sourceEvent, | |
| 'index', | |
| {intersect: false}, | |
| false | |
| )[0]; | |
| const handleDragStart: DragStartFn = (chart, callback) => dragEvent => { | |
| const element = getElementAtEvent(chart, dragEvent); | |
| if (element && typeof callback === 'function') { | |
| const datasetIndex = element.datasetIndex; | |
| const index = element.index; | |
| const value = chart.data.datasets[datasetIndex].data[index] as number; | |
| callback({chart, datasetIndex, dragEvent, element, index, value}); | |
| } | |
| }; | |
| interface DragFn { | |
| (chart: Chart, callback: DraggableChangeCallback): ( | |
| dragEvent: DragEvent | |
| ) => void; | |
| } | |
| const handleDrag: DragFn = (chart, callback) => dragEvent => { | |
| const element = getElementAtEvent(chart, dragEvent); | |
| if (element) { | |
| const datasetIndex = element.datasetIndex; | |
| const index = element.index; | |
| const options = getDraggableOptions(chart); | |
| const prevValue = chart.data.datasets[datasetIndex].data[index] as number; | |
| const nextValue = calculateNextValue( | |
| dragEvent, | |
| chart, | |
| options.scaleId || ScaleId.Y | |
| ); | |
| if ( | |
| typeof callback === 'function' && | |
| callback({ | |
| prevValue, | |
| nextValue, | |
| datasetIndex, | |
| index, | |
| dragEvent, | |
| chart, | |
| element, | |
| }) === false | |
| ) { | |
| return; | |
| } | |
| if (nextValue === prevValue) return; | |
| chart.data.datasets[datasetIndex].data[index] = nextValue; | |
| chart.update('none'); | |
| } | |
| }; | |
| interface DragEndFn { | |
| (chart: Chart, callback: DraggableEndCallback): ( | |
| dragEvent: DragEvent | |
| ) => void; | |
| } | |
| const handleDragEnd: DragEndFn = (chart, callback) => dragEvent => { | |
| const element = getElementAtEvent(chart, dragEvent); | |
| if (element && typeof callback === 'function') { | |
| const datasetIndex = element.datasetIndex; | |
| const index = element.index; | |
| const value = chart.data.datasets[datasetIndex].data[index] as number; | |
| callback({chart, datasetIndex, dragEvent, element, index, value}); | |
| } | |
| }; | |
| export const DraggableChartPlugin = { | |
| id: 'draggable', | |
| afterInit: (chart: Chart) => { | |
| const options = getDraggableOptions(chart); | |
| if (isDraggableEnabled(options)) { | |
| select(chart.canvas).call( | |
| drag() | |
| .container(chart.canvas) | |
| .on(DragEventType.Start, handleDragStart(chart, options.onDragStart)) | |
| .on(DragEventType.Drag, handleDrag(chart, options.onDragChange)) | |
| .on(DragEventType.End, handleDragEnd(chart, options.onDragEnd)) | |
| ); | |
| } | |
| }, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment