chartjs-plugin-cursors
Version:
Chart.js plugin to draw and sync cursor lines
279 lines • 12.4 kB
JavaScript
const defaultOptions = {
enabled: false,
visible: true,
positions: [],
line: {
color: "blue",
width: 1,
dashPattern: [],
},
};
const getXScale = (chart) => {
const meta = chart.getDatasetMeta(0);
return chart.data.datasets.length && meta.xAxisID
? chart.scales[meta.xAxisID]
: null;
};
const getYScale = (chart) => {
const meta = chart.getDatasetMeta(0);
return chart.data.datasets.length && meta.yAxisID
? chart.scales[meta.yAxisID]
: null;
};
const CursorsPlugin = {
id: "cursors",
afterInit(chart) {
var _a, _b, _c, _d, _e;
if (!((_b = (_a = chart.options) === null || _a === void 0 ? void 0 : _a.scales) === null || _b === void 0 ? void 0 : _b.x)) {
return;
}
const xScaleType = chart.options.scales.x.type;
if (![
"linear",
"time",
"timeseries",
"category",
"logarithmic",
"realtime",
].includes(xScaleType)) {
return;
}
if (!((_c = chart.options) === null || _c === void 0 ? void 0 : _c.plugins)) {
chart.options.plugins = {};
}
if (((_d = chart.options.plugins) === null || _d === void 0 ? void 0 : _d.cursors) === undefined) {
chart.options.plugins.cursors = defaultOptions;
}
if (!chart.options.plugins.cursors.enabled) {
return;
}
let positions = [];
if (chart.options.plugins.cursors.positions) {
positions = chart.options.plugins.cursors.positions.map((position) => {
var _a, _b;
return ({
previousX: undefined,
previousY: undefined,
x: (_a = position.x) !== null && _a !== void 0 ? _a : 0,
y: (_b = position.y) !== null && _b !== void 0 ? _b : 0,
});
});
}
chart.cursors = {
enabled: true,
visible: (_e = chart.options.plugins.cursors.visible) !== null && _e !== void 0 ? _e : true,
suppressUpdate: false,
ignoreNextEvents: 0,
selected: -1,
positions,
startTime: undefined,
centreCursors: this.centreCursors,
hitTest: (x, y, threshold) => this.hitTestCursor(chart, x, y, threshold),
setStartTime: (time) => {
chart.cursors.startTime = time;
},
};
},
afterEvent(chart, args) {
var _a, _b, _c, _d, _e, _f, _g;
if (!((_a = chart.cursors) === null || _a === void 0 ? void 0 : _a.enabled) ||
chart.cursors.suppressUpdate ||
!((_c = (_b = chart.options) === null || _b === void 0 ? void 0 : _b.scales) === null || _c === void 0 ? void 0 : _c.x)) {
return;
}
// do nothing if xScale is not linear, time, timeseries, category, logarithmic or realtime
const xScaleType = chart.options.scales.x.type;
if (![
"linear",
"time",
"timeseries",
"category",
"logarithmic",
"realtime",
].includes(xScaleType)) {
return;
}
// if startTime is not set, check options else do nothing
if (chart.cursors.startTime === undefined &&
xScaleType === "realtime") {
chart.cursors.startTime = (_e = (_d = chart.options.plugins) === null || _d === void 0 ? void 0 : _d.cursors) === null || _e === void 0 ? void 0 : _e.startTime;
if (chart.cursors.startTime === undefined) {
return;
}
}
const xScale = getXScale(chart);
const yScale = getYScale(chart);
if (!xScale || !yScale) {
return;
}
if (chart.cursors.ignoreNextEvents > 0) {
chart.cursors.ignoreNextEvents -= 1;
return;
}
const e = args.event;
if (!e.x || !e.y) {
return false;
}
// used to do a check for cursor outside chart here
switch (e.type) {
case "mousedown":
const selected = chart.cursors.hitTest(e.x, e.y, 10);
chart.cursors.selected = selected;
break;
case "mouseup":
chart.cursors.selected = -1;
break;
}
if (chart.cursors.positions &&
chart.cursors.selected >= chart.cursors.positions.length) {
chart.cursors.selected = -1;
}
if (chart.cursors.selected >= 0) {
chart.cursors.positions[chart.cursors.selected].x =
(_f = xScale.getValueForPixel(e.x)) !== null && _f !== void 0 ? _f : 0;
chart.cursors.positions[chart.cursors.selected].y =
(_g = yScale.getValueForPixel(e.y)) !== null && _g !== void 0 ? _g : 0;
}
chart.draw();
},
afterDraw(chart) {
var _a, _b, _c, _d, _e, _f, _g;
if (!((_a = chart.cursors) === null || _a === void 0 ? void 0 : _a.enabled)) {
return false;
}
const newVisible = (_c = (_b = chart.options.plugins) === null || _b === void 0 ? void 0 : _b.cursors) === null || _c === void 0 ? void 0 : _c.visible;
if (!newVisible) {
chart.cursors.visible = false;
return false;
}
// if visible has changed, centre cursors
if (chart.cursors.visible !== newVisible) {
chart.cursors.visible = newVisible;
this.centreCursors(chart);
}
const xScaleType = (_e = (_d = chart.options.scales) === null || _d === void 0 ? void 0 : _d.x) === null || _e === void 0 ? void 0 : _e.type;
if (xScaleType === "realtime" &&
chart.cursors.startTime === undefined) {
chart.cursors.startTime = (_g = (_f = chart.options.plugins) === null || _f === void 0 ? void 0 : _f.cursors) === null || _g === void 0 ? void 0 : _g.startTime;
if (chart.cursors.startTime === undefined) {
return false;
}
}
this.drawCursors(chart);
return true;
},
drawCursors(chart) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
if (!((_a = chart.cursors) === null || _a === void 0 ? void 0 : _a.enabled) || !chart.cursors.visible) {
return;
}
const xScale = getXScale(chart);
const yScale = getYScale(chart);
if (!xScale || !yScale) {
return;
}
const xScaleType = (_c = (_b = chart.options.scales) === null || _b === void 0 ? void 0 : _b.x) === null || _c === void 0 ? void 0 : _c.type;
if ((_d = chart.cursors.positions) === null || _d === void 0 ? void 0 : _d.some((p) => p.x === undefined || p.y === undefined)) {
this.centreCursors(chart);
chart.draw();
}
const xMinPixel = xScale.getPixelForValue(xScale.min);
const xMaxPixel = xScale.getPixelForValue(xScale.max);
const yMinPixel = yScale.getPixelForValue(yScale.min);
const yMaxPixel = yScale.getPixelForValue(yScale.max);
const lineWidth = (_h = (_g = (_f = (_e = chart.options.plugins) === null || _e === void 0 ? void 0 : _e.cursors) === null || _f === void 0 ? void 0 : _f.line) === null || _g === void 0 ? void 0 : _g.width) !== null && _h !== void 0 ? _h : 1;
const color = (_m = (_l = (_k = (_j = chart.options.plugins) === null || _j === void 0 ? void 0 : _j.cursors) === null || _k === void 0 ? void 0 : _k.line) === null || _l === void 0 ? void 0 : _l.color) !== null && _m !== void 0 ? _m : "blue";
const dashPattern = ((_r = (_q = (_p = (_o = chart.options.plugins) === null || _o === void 0 ? void 0 : _o.cursors) === null || _p === void 0 ? void 0 : _p.line) === null || _q === void 0 ? void 0 : _q.dashPattern) !== null && _r !== void 0 ? _r : []);
(_s = chart.cursors.positions) === null || _s === void 0 ? void 0 : _s.forEach((cursor, index) => {
if (cursor.x === undefined || cursor.y === undefined) {
return;
}
const cursorX = xScale.getPixelForValue(cursor.x);
const cursorY = yScale.getPixelForValue(cursor.y);
let xString = cursor.x.toFixed(3);
if (xScaleType === "realtime" &&
chart.cursors.startTime !== undefined) {
const secondsSinceStart = ((cursor.x - chart.cursors.startTime) /
1000).toFixed(2);
xString = `(${secondsSinceStart}s`;
}
const ctx = chart.ctx;
ctx.save();
ctx.fillStyle = color;
ctx.font = "12px Arial";
if (chart.cursors.selected === index) {
ctx.setLineDash([]);
ctx.fillText(`${xString}, ${cursor.y.toFixed(3)})`, cursorX + 5, cursorY - 5);
}
else {
ctx.beginPath();
ctx.arc(cursorX, cursorY, 3, 0, 2 * Math.PI);
ctx.fill();
ctx.fillText(`${index + 1}`, cursorX - 12, cursorY + 12);
}
// Draw vertical and horizontal lines
ctx.beginPath();
ctx.setLineDash(dashPattern);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.moveTo(cursorX, yMaxPixel);
ctx.lineTo(cursorX, yMinPixel);
ctx.moveTo(xMinPixel, cursorY);
ctx.lineTo(xMaxPixel, cursorY);
ctx.stroke();
ctx.restore();
});
const afterMoveCallback = (_v = (_u = (_t = chart.options.plugins) === null || _t === void 0 ? void 0 : _t.cursors) === null || _u === void 0 ? void 0 : _u.callbacks) === null || _v === void 0 ? void 0 : _v.afterMove;
if (afterMoveCallback && chart.cursors.positions) {
const changed = chart.cursors.positions.some((cursor) => cursor.previousX !== cursor.x ||
cursor.previousY !== cursor.y);
if (changed) {
const positions = chart.cursors.positions.map((p, i) => {
var _a, _b, _c, _d;
return ({
index: i,
x: parseFloat((_b = (_a = p.x) === null || _a === void 0 ? void 0 : _a.toFixed(2)) !== null && _b !== void 0 ? _b : "0"),
y: parseFloat((_d = (_c = p.y) === null || _c === void 0 ? void 0 : _c.toFixed(2)) !== null && _d !== void 0 ? _d : "0"),
});
});
afterMoveCallback(positions);
chart.cursors.positions.forEach((cursor) => {
cursor.previousX = cursor.x;
cursor.previousY = cursor.y;
});
}
}
},
hitTestCursor(chart, x, y, threshold) {
const xScale = getXScale(chart);
const yScale = getYScale(chart);
if (!xScale || !yScale || !chart.cursors.positions)
return -1;
return chart.cursors.positions.findIndex((cursor) => {
if (cursor.x === undefined || cursor.y === undefined)
return false;
const cursorX = xScale.getPixelForValue(cursor.x);
const cursorY = yScale.getPixelForValue(cursor.y);
const distance = Math.sqrt(Math.pow((x - cursorX), 2) + Math.pow((y - cursorY), 2));
return distance < threshold;
});
},
centreCursors(chart) {
var _a;
const xScale = getXScale(chart);
const yScale = getYScale(chart);
if (!xScale || !yScale) {
return;
}
const xDistance = xScale.max - xScale.min;
const yDistance = yScale.max - yScale.min;
const xPrecision = xDistance <= 5 ? 1 : 0;
const yPrecision = yDistance <= 5 ? 1 : 0;
(_a = chart.cursors.positions) === null || _a === void 0 ? void 0 : _a.forEach(function (cursor) {
cursor.x = parseFloat(((xScale.min + xScale.max) / 2).toFixed(xPrecision));
cursor.y = parseFloat(((yScale.min + yScale.max) / 2).toFixed(yPrecision));
});
},
};
export default CursorsPlugin;
//# sourceMappingURL=index.js.map