Skip to content

Instantly share code, notes, and snippets.

@crisu83
Last active January 7, 2021 10:14
Show Gist options
  • Select an option

  • Save crisu83/fcdc1cb7021d65c54f3a3365f419fecf to your computer and use it in GitHub Desktop.

Select an option

Save crisu83/fcdc1cb7021d65c54f3a3365f419fecf to your computer and use it in GitHub Desktop.
Draggable plugin for Chart.js 3 written in TypeScript.
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