5
votes

I am currently modifying a Chart.js line chart to be interactive. The idea is that the user can move the points of the graph with its fingers.

Here is an example how it looks like with the current mapping:

chart-test.html

I already managed it to read the events and get the data points which I have to modify:

// register pointer event
canvas.addEventListener('pointerdown', evt => {
    const points = interactiveChart.getElementsAtEventForMode(evt, 'index', {
        intersect: false
    });

    moveDataSetPoint(points, evt);
});

// change point relatively to y point
function moveDataSetPoint(points, evt)
{
    // read active data point
    var activePoint = points[0];
    var data = activePoint._chart.data;
    var datasetIndex = activePoint._datasetIndex;

    // read mouse position
    const helpers = Chart.helpers;
    var position = helpers.getRelativePosition(evt, interactiveChart);

    // calculate y axis value (ugly)
    // todo: map this with a chartjs method to map mouse inputs to yAxis values
    var yValue = map(position.y, window.myLine.height, 0, yMin, yMax);

    data.datasets[datasetIndex].data[activePoint._index] = yValue;
    window.myLine.update();
};

// attached the map function
function map(value, start1, stop1, start2, stop2) {
    return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
};

But the problem is that my map function is just mapping the y pointer position relatively to the canvas size and then y-axis min and max values.

This is very inaccurate and I am looking for a method which gives me the correct y-axis value from the pointer event values.

Is there something already implemented in chartjs? Or is there a method to get just the size of the chart itself, without the legends and borders around it (currently I am mapping to the whole canvas)?

2

2 Answers

2
votes

I found the solution for it by myself. You have to use the chart bottom and top properties to map the correct size:

// read chart area
var chartArea = chart.chartArea;
var yValue = map(position.y, chartArea.bottom, chartArea.top, yMin, yMax);

Then the mapping is pixel perfect.

1
votes

The answer was very useful but it took me quite a while to understand how to apply it.

Therefore in case it is helpful to someone here's a simple working example.

In case it's not obvious, the three files below are respectively script.js, style.css and index.html.

// some data to be plotted
var x_data = [1500,1600,1700,1750,1800,1850,1900,1950,1999,2050];
var y_data_1 = [86,114,106,106,107,111,133,221,783,2478];
var y_data_2 = [2000,700,200,100,100,100,100,50,25,0];

// globals
var activePoint = null;
var canvas = null;

// draw a line chart on the canvas context
window.onload = function () {

    // Draw a line chart with a single data set
    var ctx = document.getElementById("canvas").getContext("2d");
    canvas = document.getElementById("canvas");
    window.myChart = Chart.Line(ctx, {
        data: {
            labels: x_data,
            datasets: [
                {
                    data: y_data_1,
                    label: "Data 1",
                    borderColor: "#3e95cd",
                    fill: false
                },
                {
                    data: y_data_2,
                    label: "Data 2",
                    borderColor: "#cd953e",
                    fill: false
                }
            ]
        },
        options: {
            animation: {
                duration: 0
            },
            tooltips: {
                mode: 'nearest'
            }
        }
    });

    // set pointer event handlers for canvas element
    canvas.onpointerdown = down_handler;
    canvas.onpointerup = up_handler;
    canvas.onpointermove = null;
};

function down_handler(event) {
    // check for data point near event location
    const points = window.myChart.getElementAtEvent(event, {intersect: false});
    if (points.length > 0) {
        // grab nearest point, start dragging
        activePoint = points[0];
        canvas.onpointermove = move_handler;
    };
};

function up_handler(event) {
    // release grabbed point, stop dragging
    activePoint = null;
    canvas.onpointermove = null;
};

function move_handler(event)
{
    // locate grabbed point in chart data
    if (activePoint != null) {
        var data = activePoint._chart.data;
        var datasetIndex = activePoint._datasetIndex;

        // read mouse position
        const helpers = Chart.helpers;
        var position = helpers.getRelativePosition(event, myChart);

        // convert mouse position to chart y axis value 
        var chartArea = window.myChart.chartArea;
        var yAxis = window.myChart.scales["y-axis-0"];
        var yValue = map(position.y, chartArea.bottom, chartArea.top, yAxis.min, yAxis.max);

        // update y value of active data point
        data.datasets[datasetIndex].data[activePoint._index] = yValue;
        window.myChart.update();
    };
};

// map value to other coordinate system
function map(value, start1, stop1, start2, stop2) {
    return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
};
body {
  font-family: Helvetica Neue, Arial, sans-serif;
  text-align: center;
}

.wrapper {
  max-width: 800px;
  margin: 50px auto;
}

h1 {
  font-weight: 200;
  font-size: 3em;
  margin: 0 0 0.1em 0;
}

h2 {
  font-weight: 200;
  font-size: 0.9em;
  margin: 0 0 50px;
  color: #555;
}

a {
  margin-top: 50px;
  display: block;
  color: #3e95cd;
}
<!DOCTYPE html>
<html>

  <!-- HEAD element: load the stylesheet and the chart.js library -->
  <head>
    <title>Draggable Points Example</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Chart.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
  </head>

  <!-- BODY element: create a canvas and render a chart on it -->
  <body>

    <!-- canvas element in a container -->
    <div class="wrapper">
      <h1>Draggable Points Example</h1>
      <h2>Click a data point and drag up/down</h2>

      <canvas id="canvas" width="1600" height="900"></canvas>
    </div>

    <!-- call external script to create and render a chart on the canvas -->
    <script src="script.js"></script>
  </body>

</html>