I am using ChartJS to display a chart and would like to interact with the annotations via the annotationsplugin.
In addition the annotations are draggable, so the user can drag them around freely. I am using the drag documentation for that.
When the user has dragged an annotation, I would like to get the x
coordinate of the dataset value of where the annotation was left off, however I am not sure how to do this.
As far as I can tell, the only values I can get are x
and y
values of that annotation as pixel values of the canvas
, but not the dataset value.
Would appreciate some help.
To transform the pixel positions to real world coordinates, one could use the function of the Scale
class:
Scale#getValueForPixel
and related methods, accessing first the scales under chart.scales
.
Your post would have benefited from a [mre] as the solution depends on the type
s of scales used.
In the absence of that, I used the "Dragging annotation" sample
of the chartjs-plugin-annotation
docs, to which I added a few more annotations to cover all types. That chart features - the default for line
charts - a type: category
x
scale and a
type: linear
y
scale.
Another challenge is to identify the position of the "anchors" - public options like real world coordinates xValue
or yValue
- in the geometry of the Element
s implementing it, that have pixel measured properties like x
, y
, x2
, y2
, centerX
, centerY
, pointX
, pointY
. In the code below I'll try to reconstitute for each type of annotation, the defining options that where changed
by the drag operation, for instance for a polygon
, xValue
and yValue
, and for an ellipse
, xMin
, xMax
,
yMin
, yMax
.
One has to be aware of the fact that for a category axis the value
is the zero-based index of the category label
; in the settings
of the plugin one can use interchangeably the label or the value, i.e., January
or 0
, February
and 1
and so on.
In the code below, I'll report as xValue
that index value, as xLabel
the text label and as xValueFrac
, the fractional value for
exact positioning between category centers; the value under xValueFrac
can be used for xValue
in the initial options of the
annotation.
For the linear y axis things are simpler, there is only the yValue
which is the real world coordinate of a point.
Using the code from the sample mentioned above, I defined a function that is called after each drag
and logs to the console the aforementioned
properties:
const drag = function(moveX, moveY) {
//.......................
registerRealValues(element);
};
function registerRealValues(element){
const chart = element.$context.chart; //alternatively, `chart` could be sent down from `beforeEvent`
const options = element.options,
{type, xScaleID, yScaleID} = options,
xScale = chart.scales[xScaleID || 'x'],
yScale = chart.scales[yScaleID || 'y'];
if(type === 'polygon' || type === 'point'){
const xValue = xScale.getValueForPixel(element.centerX),
xLabel = xScale.getLabelForValue(xValue),
dec = xScale.getDecimalForPixel(element.centerX),
xValueFrac = dec * xScale.max;
const yValue = yScale.getValueForPixel(element.centerY);
console.log({type, xValue, xLabel, xValueFrac, yValue})
}
else if(type === 'label'){
const xValue = xScale.getValueForPixel(element.pointX),
xLabel = xScale.getLabelForValue(xValue),
dec = xScale.getDecimalForPixel(element.pointX),
xValueFrac = dec * xScale.max;
const yValue = yScale.getValueForPixel(element.pointY);
console.log({type, xValue, xLabel, xValueFrac, yValue})
}
else if(type === 'box' || type === 'ellipse' || type === 'line'){
const xValue1 = xScale.getValueForPixel(element.x),
xValue2 = xScale.getValueForPixel(element.x2);
let xMin, xMax, xMinFrac, xMaxFrac;
if(xValue1 < xValue2){
xMin = xValue1;
xMax = xValue2;
xMinFrac = xScale.getDecimalForPixel(element.x) * xScale.max;
xMaxFrac = xScale.getDecimalForPixel(element.x2) * xScale.max;
}
else{
xMin = xValue2;
xMax = xValue1;
xMinFrac = xScale.getDecimalForPixel(element.x2) * xScale.max;
xMaxFrac = xScale.getDecimalForPixel(element.x1) * xScale.max;
}
const xMinLabel = xScale.getLabelForValue(xMin),
xMaxLabel = xScale.getLabelForValue(xMax);
const yValue1 = yScale.getValueForPixel(element.y),
yValue2 = yScale.getValueForPixel(element.y2),
yMin = Math.min(yValue1, yValue2),
yMax = Math.max(yValue1, yValue2);
console.log({type, xMin, xMinLabel, xMinFrac, xMax, xMaxLabel, xMaxFrac, yMin, yMax})
}
}
Demo snippet:
// Aside from function registerRealValues, annotation5 and annotation6 the code comes from
// https://www.chartjs.org/chartjs-plugin-annotation/3.1.0/samples/interaction/dragging.html
const data = {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
type: 'line',
label: 'Dataset 1',
borderColor: 'rgb(54, 162, 235)',
borderWidth: 2,
fill: false,
data: Array.from({length: 100}, ()=>Math.round(Math.random() * 100))
}]
};
const annotation1 = {
type: 'box',
backgroundColor: 'rgba(165, 214, 167, 0.2)',
borderColor: 'rgb(165, 214, 167)',
borderWidth: 2,
label: {
display: true,
content: ['Box annotation', 'to drag'],
textAlign: 'center'
},
xMax: 'May',
xMin: 'April',
xScaleID: 'x',
yMax: 75,
yMin: 25,
yScaleID: 'y'
};
const annotation2 = {
type: 'label',
backgroundColor: 'rgba(255, 99, 132, 0.25)',
borderWidth: 3,
borderColor: 'black',
content: ['Label annotation', 'to drag'],
callout: {
display: true,
borderColor: 'black',
},
position: {
x: "25%",
y: "start"
},
xValue: 1,
yValue: 40
};
const annotation3 = {
type: 'point',
backgroundColor: 'rgba(0, 255, 255, 0.4)',
borderWidth: 2,
borderColor: 'black',
radius: 20,
xValue: 'March', // oe 2
yValue: 50
};
const annotation4 = {
type: 'polygon',
backgroundColor: 'rgba(150, 0, 0, 0.25)',
borderWidth: 2,
borderColor: 'black',
radius: 50,
sides: 6,
xValue: 5,// or 'June',
yValue: 20
};
const annotation5 = {
type: 'ellipse',
xMin: 2, // or "March"
xMax: 3, // or "April"
yMin: 50,
yMax: 70,
backgroundColor: 'rgba(255, 99, 132, 0.25)'
};
const annotation6 = {
type: 'line',
borderColor: 'black',
borderWidth: 3,
xScaleID: 'x',
yScaleID: 'y',
xMin: 3,
xMax: 6,
yMin: ({chart}) => chart.data.datasets[0].data[1] / 2,
yMax: ({chart}) => chart.data.datasets[0].data[6] / 2,
curve: true,
arrowHeads: {
end: {
display: true
}
}
};
let element;
let lastEvent;
const drag = function(moveX, moveY) {
element.x += moveX;
element.y += moveY;
element.x2 += moveX;
element.y2 += moveY;
element.centerX += moveX;
element.centerY += moveY;
if (element.elements && element.elements.length) {
for (const subEl of element.elements) {
subEl.x += moveX;
subEl.y += moveY;
subEl.x2 += moveX;
subEl.y2 += moveY;
subEl.centerX += moveX;
subEl.centerY += moveY;
subEl.bX += moveX;
subEl.bY += moveY;
}
}
registerRealValues(element);
};
function registerRealValues(element){
const chart = element.$context.chart; //alternatively, `chart` could be sent down from `beforeEvent`
const options = element.options,
{type, xScaleID, yScaleID} = options,
xScale = chart.scales[xScaleID || 'x'],
yScale = chart.scales[yScaleID || 'y'];
if(type === 'polygon' || type === 'point'){
const xValue = xScale.getValueForPixel(element.centerX),
xLabel = xScale.getLabelForValue(xValue),
dec = xScale.getDecimalForPixel(element.centerX),
xValueFrac = dec * xScale.max;
const yValue = yScale.getValueForPixel(element.centerY);
console.log({type, xValue, xLabel, xValueFrac, yValue})
}
else if(type === 'label'){
const xValue = xScale.getValueForPixel(element.pointX),
xLabel = xScale.getLabelForValue(xValue),
dec = xScale.getDecimalForPixel(element.pointX),
xValueFrac = dec * xScale.max;
const yValue = yScale.getValueForPixel(element.pointY);
console.log({type, xValue, xLabel, xValueFrac, yValue})
}
else if(type === 'box' || type === 'ellipse' || type === 'line'){
const xValue1 = xScale.getValueForPixel(element.x),
xValue2 = xScale.getValueForPixel(element.x2);
let xMin, xMax, xMinFrac, xMaxFrac;
if(xValue1 < xValue2){
xMin = xValue1;
xMax = xValue2;
xMinFrac = xScale.getDecimalForPixel(element.x) * xScale.max;
xMaxFrac = xScale.getDecimalForPixel(element.x2) * xScale.max;
}
else{
xMin = xValue2;
xMax = xValue1;
xMinFrac = xScale.getDecimalForPixel(element.x2) * xScale.max;
xMaxFrac = xScale.getDecimalForPixel(element.x1) * xScale.max;
}
const xMinLabel = xScale.getLabelForValue(xMin),
xMaxLabel = xScale.getLabelForValue(xMax);
const yValue1 = yScale.getValueForPixel(element.y),
yValue2 = yScale.getValueForPixel(element.y2),
yMin = Math.min(yValue1, yValue2),
yMax = Math.max(yValue1, yValue2);
console.log({type, xMin, xMinLabel, xMinFrac, xMax, xMaxLabel, xMaxFrac, yMin, yMax})
}
}
const handleElementDragging = function(event) {
if (!lastEvent || !element) {
return;
}
const moveX = event.x - lastEvent.x;
const moveY = event.y - lastEvent.y;
drag(moveX, moveY);
lastEvent = event;
return true;
};
const handleDrag = function(event) {
if (element) {
switch (event.type) {
case 'mousemove':
return handleElementDragging(event);
case 'mouseout':
case 'mouseup':
lastEvent = undefined;
break;
case 'mousedown':
lastEvent = event;
break;
default:
}
}
};
const config = {
type: 'line',
plugins: [{beforeEvent(chart, args) {
if (handleDrag(args.event)) {
//console.log(args)
args.changed = true;
return;
}
}}],
data,
options: {
events: ['mousedown', 'mouseup', 'mousemove', 'mouseout'],
scales: {
y: {
beginAtZero: true,
min: 0,
max: 100
}
},
plugins: {
annotation: {
enter(ctx) {
element = ctx.element;
},
leave() {
element = undefined;
lastEvent = undefined;
},
annotations: {
annotation1,
annotation2,
annotation3,
annotation4,
annotation5,
annotation6
}
}
}
}
};
new Chart('myChart', config)
<div style="height: 300px">
<canvas id="myChart">
</canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/3.1.0/chartjs-plugin-annotation.min.js"></script>