Skip to content

Commit 499fa7b

Browse files
authored
Merge pull request #1 from 01CodeLT/feature/draggable
Enhancements
2 parents b1ca53f + c10f1da commit 499fa7b

File tree

6 files changed

+444
-233
lines changed

6 files changed

+444
-233
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@01coder/chartjs-plugin-selectdrag",
3-
"version": "1.0.4",
3+
"version": "2.0.0",
44
"description": "Chartjs plugin which allows you to select a range of data by dragging over a chart",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

src/index.ts

Lines changed: 83 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,7 @@
1-
// Store chart data
2-
const states = new WeakMap();
3-
const getState = (chart) => {
4-
const state = states.get(chart);
5-
return state || null;
6-
}
7-
const setState = (chart, updatedState) => {
8-
const originalState = getState(chart);
9-
states.set(
10-
chart,
11-
updatedState == null ? null : Object.assign({}, originalState, updatedState)
12-
);
13-
return updatedState;
14-
}
15-
16-
// Store options
17-
const pluginOptions = {
18-
output: 'label',
19-
highlight: true,
20-
colors: {
21-
selection: "#e8eff6",
22-
selected: "#1f77b4", // Unused if backgroundColorDefault set on dataset
23-
unselected: "#cccccc"
24-
}
25-
};
1+
import { Selection } from "./selection";
2+
import { getOptions, highlightChartData } from "./utils";
263

27-
// Export main plugin
4+
const states = new WeakMap();
285
export default {
296
id: "selectdrag",
307

@@ -36,126 +13,89 @@ export default {
3613

3714
// Get chart canvas
3815
const canvasElement = chart.canvas;
16+
canvasElement.style.cursor = 'crosshair';
3917

40-
// Draw begin
18+
// Add listen events
4119
canvasElement.addEventListener("mousedown", (e) => {
42-
// Get elements
43-
const axisElements = chart.getElementsAtEventForMode(e, "index", { intersect: false });
44-
if(axisElements.length === 0) { return; }
45-
46-
// Create state
47-
const state = {
48-
selectionXY: {
49-
drawing: true,
50-
start: { axisValue: null, axisIndex: null, x: e.offsetX, y: e.offsetY },
51-
end: {}
52-
}
53-
};
54-
55-
// Get axis value
56-
const output = chart?.config?.options?.plugins?.selectdrag?.output || pluginOptions.output;
57-
({
58-
'label': () => {
59-
const axisIndex = chart.getElementsAtEventForMode(e, "index", { intersect: false })[0].index;
60-
state.selectionXY.start.axisIndex = axisIndex;
61-
state.selectionXY.start.axisValue = chart.data.labels[axisIndex];
62-
},
63-
'value': () => {
64-
// Get value by scale
65-
state.selectionXY.start.axisValue = chart.scales.x.getValueForPixel(e.offsetX);
66-
},
67-
})[output]();
20+
// Get state
21+
const selection: Selection = states.get(chart) || new Selection();
22+
23+
// Check for drag?
24+
if(selection.isDrag(e)) {
25+
selection.handleDragStart(chart, e);
26+
} else {
27+
selection.handleSelectStart(chart, e);
28+
}
6829

69-
// Set selection origin
70-
setState(chart, state);
30+
states.set(chart, selection);
7131
});
7232

73-
// Draw end
74-
window.addEventListener("mouseup", (e) => {
75-
// Check drawing status
76-
const state = getState(chart);
77-
if(!state || state?.selectionXY?.drawing === false) {
78-
return;
33+
canvasElement.addEventListener('mousemove', (e) => {
34+
// Get existing selection
35+
const selection: Selection = states.get(chart);
36+
if(!selection) { return; }
37+
38+
if(selection.isSelecting === true) {
39+
selection.handleSelectMove(chart, e);
7940
}
8041

81-
// Get axis value
82-
const output = chart?.config?.options?.plugins?.selectdrag?.output || pluginOptions.output;
83-
({
84-
'label': () => {
85-
// Get value by label
86-
const axisElements = chart.getElementsAtEventForMode(e, "index", { intersect: false });
87-
const axisIndex = axisElements.length > 0 ? axisElements[0].index : chart.data.labels.length - 1;
88-
const axisValue = chart.data.labels[axisIndex];
42+
if(selection.isDragging == true) {
43+
selection.handleDragMove(chart, e);
44+
}
8945

90-
// Check values & set end origin
91-
if(state.selectionXY.start.axisIndex > axisIndex) {
92-
// Switch values - user has selected opposite way
93-
state.selectionXY.end = JSON.parse(JSON.stringify(state.selectionXY.start));
94-
state.selectionXY.start = { axisValue, axisIndex, x: e.offsetX, y: e.offsetY }
95-
} else {
96-
// Set end origin
97-
state.selectionXY.end = { axisValue, axisIndex, x: e.offsetX, y: e.offsetY };
98-
}
99-
},
100-
'value': () => {
101-
// Get value by scale
102-
const axisValue = chart.scales.x.getValueForPixel(e.offsetX);
103-
104-
// Check values & set end origin
105-
if(state.selectionXY.start.axisValue > axisValue) {
106-
// Switch values - user has selected opposite way
107-
state.selectionXY.end = JSON.parse(JSON.stringify(state.selectionXY.start));
108-
state.selectionXY.start = { axisValue, axisIndex: null, x: e.offsetX, y: e.offsetY }
109-
} else {
110-
// Set end origin
111-
state.selectionXY.end = { axisValue, axisIndex: null, x: e.offsetX, y: e.offsetY };
112-
}
113-
},
114-
})[output]();
46+
if(!selection.isDragging && !selection.isSelecting) {
47+
selection.handleSelectHover(chart, e);
48+
}
11549

116-
// End drawing
117-
state.selectionXY.drawing = false;
118-
setState(chart, state);
50+
states.set(chart, selection);
51+
});
11952

120-
// Render rectangle
121-
chart.update();
53+
// Draw end
54+
let mouseUpTimeout;
55+
window.addEventListener("mouseup", (e) => {
56+
// Get existing selection
57+
const selection: Selection = states.get(chart);
58+
if(!selection) { return; }
59+
60+
const selectComplete = (selection) => {
61+
clearTimeout(mouseUpTimeout);
62+
mouseUpTimeout = setTimeout(() => {
63+
const pluginOptions = getOptions(chart);
64+
if(pluginOptions.onSelectComplete) {
65+
const range = selection.values.getRange();
66+
if(range.length > 0) {
67+
pluginOptions.onSelectComplete({
68+
range: range,
69+
boundingBox: [
70+
selection.selection.start.x,
71+
[
72+
selection.selection.end.x,
73+
selection.selection.start.y,
74+
],
75+
selection.selection.end,
76+
[
77+
selection.selection.start.x,
78+
selection.selection.end.y,
79+
]
80+
]
81+
});
82+
}
83+
}
84+
}, 500);
85+
}
12286

123-
// Emit event
124-
const selectCompleteCallback = chart?.config?.options?.plugins?.selectdrag?.onSelectComplete;
125-
if(selectCompleteCallback) {
126-
selectCompleteCallback({
127-
range: [
128-
state.selectionXY.start.axisValue,
129-
state.selectionXY.end.axisValue
130-
],
131-
boundingBox: [
132-
state.selectionXY.start,
133-
[
134-
state.selectionXY.end.x,
135-
state.selectionXY.start.y,
136-
],
137-
state.selectionXY.end,
138-
[
139-
state.selectionXY.start.x,
140-
state.selectionXY.end.y,
141-
]
142-
]
143-
});
87+
if(selection.isSelecting == true) {
88+
selection.handleSelectEnd(chart, e);
89+
selectComplete(selection);
14490
}
145-
});
14691

147-
// Draw extend
148-
canvasElement.addEventListener("mousemove", (e) => {
149-
// Check drawing status
150-
const state = getState(chart);
151-
if(!state || state?.selectionXY?.drawing === false) {
152-
return;
92+
if(selection.isDragging == true) {
93+
selection.handleDragEnd(chart, e);
94+
selectComplete(selection);
15395
}
15496

155-
// Set end origin
156-
state.selectionXY.end = { x: e.offsetX, y: e.offsetY };
157-
chart.render();
158-
setState(chart, state);
97+
states.set(chart, selection);
98+
chart.update();
15999
});
160100
},
161101

@@ -170,72 +110,17 @@ export default {
170110
if(highlight !== undefined && highlight == false) { return; }
171111

172112
// Check drawing status
173-
const state = getState(chart);
174-
175-
// Color based on output
176-
const output = chart?.config?.options?.plugins?.selectdrag?.output || pluginOptions.output;
177-
const colors = chart?.config?.options?.plugins?.selectdrag?.colors || pluginOptions.colors;
178-
const backgroundColorCallback = {
179-
'label': (value, index, defaultColor) => {
180-
// Show selected/unselected
181-
if(index >= state.selectionXY.start?.axisIndex && index <= state.selectionXY.end?.axisIndex) {
182-
return defaultColor;
183-
} else {
184-
return colors.unselected;
185-
}
186-
},
187-
'value': (value, index, defaultColor) => {
188-
// Show selected/unselected
189-
const v = value.x || value;
190-
if(v >= state.selectionXY.start?.axisValue && v <= state.selectionXY.end?.axisValue) {
191-
return defaultColor;
192-
} else {
193-
return colors.unselected;
194-
}
195-
}
196-
}[output];
197-
198-
// Set highlighted
199-
chart.data.datasets = chart.data.datasets.map((dataset) => {
200-
dataset.backgroundColor = (
201-
output == 'value' ? dataset.data : chart.data.labels
202-
).map((value, index) => {
203-
if(!state || !state?.selectionXY?.start?.x || !state?.selectionXY?.end?.x) {
204-
// Show default
205-
return dataset.backgroundColorDefault || colors.selected;
206-
} else {
207-
// Show selected/unselected
208-
return backgroundColorCallback(value, index, dataset.backgroundColorDefault || colors.selected);
209-
}
210-
});
211-
return dataset;
212-
});
113+
highlightChartData(chart, states.get(chart) || null);
213114
},
214115

215116
afterDraw: (chart, args, options) => {
216117
// Check drawing status
217-
const state = getState(chart);
218-
if(!state || (state?.selectionXY?.drawing === false && !state.selectionXY.end?.x)) {
118+
const selection: Selection = states.get(chart);
119+
if(!selection?.selection || (selection?.isSelecting === false && !selection.selection.end?.x)) {
219120
return;
220121
}
221122

222-
// Save canvas state
223-
const {ctx} = chart;
224-
ctx.save();
225-
226-
// Draw user rectangle
227-
ctx.globalCompositeOperation = "destination-over";
228-
229-
// Draw selection
230-
ctx.fillStyle = pluginOptions.colors.selection;
231-
ctx.fillRect(
232-
(state.selectionXY.start?.x || 0), chart.chartArea.top,
233-
(state.selectionXY.end?.x || 0) - (state.selectionXY.start?.x || 0),
234-
chart.chartArea.height
235-
);
236-
237-
// Restore canvas
238-
ctx.restore();
123+
selection.draw(chart, getOptions(chart));
239124
},
240125

241126
setSelection: (chart, range = []) => {
@@ -247,56 +132,24 @@ export default {
247132
// Check if new data blank
248133
if(range.length === 0) {
249134
// Clear selection
250-
setState(chart, null);
135+
states.delete(chart);
251136
chart.update();
137+
return;
252138
}
253139

254-
// Create state
255-
const state = {
256-
selectionXY: {
257-
drawing: false,
258-
start: {},
259-
end: {}
260-
}
261-
};
140+
// Creat new selection
141+
const selection = new Selection();
262142

263-
// Get output type
264-
const output = chart?.config?.options?.plugins?.selectdrag?.output || pluginOptions.output;
265-
const getValue = {
266-
'label': (v) => {
267-
const axisIndex = chart.data.labels.findIndex((item) => item === v);
268-
return { i: axisIndex, v: chart.data.labels[axisIndex] };
269-
},
270-
'value': (v) => {
271-
return { i: null, v: v.x || v };
272-
}
273-
}[output];
274-
275-
// Set start axis
276-
const startValue = getValue(range[0]);
277-
state.selectionXY.start = {
278-
axisValue: startValue.v,
279-
axisIndex: startValue.i,
280-
x: chart.scales.x.getPixelForValue(startValue.v),
281-
y: 0
282-
}
283-
284-
// Set end axis
285-
const endValue = getValue(range[1]);
286-
state.selectionXY.end = {
287-
axisValue: endValue.v,
288-
axisIndex: endValue.i,
289-
x: chart.scales.x.getPixelForValue(endValue.v),
290-
y: chart.chartArea.height
291-
}
143+
// Set range and store
144+
selection.set(chart, range); states.set(chart, selection);
292145

293-
setState(chart, state);
146+
// Update chart with selection
294147
chart.update();
295148
},
296149

297150
clearSelection: (chart) => {
298151
// Clear state
299-
setState(chart, null);
152+
states.delete(chart);
300153
chart.update();
301154
}
302155
}

0 commit comments

Comments
 (0)