chart.js

Annotation dataset position instead of pixel position


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.


Solution

  • 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 types 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 Elements 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>