javascriptmathbeziersplinecubic

Generating heights from a Bezier curve or bell / cubic/ quadratic function over a range of integers?


I want to generate a set of heights that fit a the onto a bell shaped curve for an array of points [0,1,2,3,4,5,6,7,8,9, 10].

I don't have a formula for the curve, all though this question does have various implementations.

The 0 should be at the beginning of the curve and the 10 should be at the end and the max height should be 100.

And the maximum point could be at the center but I'm also wondering if there is a way to skew it, so for example the max could be above 3?

If there's a library that can do this that would be great too.

Something like BezierJS that would allow for dragging points over an image and then sampling heights from the curve over the range of integers would be really awesome.

In Inkscape we can take the Bezier tool and fit a curve to a set of points. I wonder if it's possible to recreate a "Mini" version of this on canvas so that we could fit a curve to a set of points and then sample the heights over the integer range?


Solution

  • The function to calculate height of x given mean and standard deviation is called probability density function. See here for the formula and math behind. The stretching of the height can be done in a linear fashion. I've added also drag plugin for chart js.

    // calculate probability density function (chatGPT)
    function calculateNormalPDF(x, mean, stdDev) {
      return 1 / (stdDev * Math.sqrt(2 * Math.PI)) * Math.exp(-Math.pow(x - mean, 2) / (2 * Math.pow(stdDev, 2)));
    }
    
    const step = 1;
    const maxHeight = 100;
    const left = 0;
    const right = 10;
    const stdDev = 2; // affects "thickness" of bell
    const mean = (left + right) / 2 // or another center of graph (3?)
    
    const dataPoints = [];
    const labels = []
    
    var maxY = -Infinity
    for (let x = left; x <= right; x += step) {
      const y = calculateNormalPDF(x, mean, stdDev);
      dataPoints.push(y);
      labels.push(x.toFixed(1))
      if (maxY < y) {
        maxY = y
      }
    }
    
    // stretch
    const factor = maxHeight / maxY;
    for (let i = 0; i < dataPoints.length; i++) {
      dataPoints[i] = dataPoints[i] * factor
    }
    
    // graph
    const ctx = myChart.getContext("2d");
    const graph = new Chart(ctx, {
      type: "line",
      data: {
        labels: labels,
        datasets: [{
          data: dataPoints,
          label: "normal distribution - draggable points!",
          pointStyle: 'rect',
          radius: 10,
        }]
      },
      options: {
        onHover: function(e) {
          const point = e.chart.getElementsAtEventForMode(e, 'nearest', {
            intersect: true
          }, false)
          if (point.length) e.native.target.style.cursor = 'grab'
          else e.native.target.style.cursor = 'default'
        },
        plugins: {
          tooltip: {
            enabled: false // <-- this option disables tooltips
          },
          dragData: {
            round: 1,
            showTooltip: true,
            onDragStart: function(e, datasetIndex, index, value) {
              // console.log(e)
            },
            onDrag: function(e, datasetIndex, index, value) {
              e.target.style.cursor = 'grabbing'
              // console.log(e, datasetIndex, index, value)
            },
            onDragEnd: function(e, datasetIndex, index, value) {
              e.target.style.cursor = 'default'
              // console.log(datasetIndex, index, value)
            },
          }
        }
      }
    });
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://chrispahm.github.io/chartjs-plugin-dragdata/assets/chartjs-plugin-dragdata.min.js"></script>
    <canvas id="myChart" width="400" height="140"></canvas>