简体   繁体   中英

javascript canvas: draw moving average line with curves

So basically, I want to draw a curved average line over a certain amount of points of a time-series line chart. Like this:

在此处输入图片说明

I want it to span the entire length of the chart but I can't figure out how to calculate the start and end points because the average would (I think) be a point in the middle of each section. Looking at a stock chart with moving average you can see what I want to acheive:

在此处输入图片说明

I calculate the averages first by splitting the data array up into chunks based on a period of time. So if I start with:

[
    { time: 1, value: 2 }, 
    { time: 2, value: 4 },
    { time: 3, value: 5 },
    { time: 4, value: 7 },
]

I get to:

var averages = [
   {
      x: 1.5,
      y: 3,
   },
   {
       x: 3.5  (the average time)
       y: 6    (the average value)
   },
]

This is what I've tried where I end up with an incomplete line, one that doesnt start at the beginning of the chart and doesnt stop at the end, but stars and ends inside the chart at the first average time:

            ctx.moveTo((averages[0].x), averages[0].y);

            for(var i = 0; i < averages.length-1; i ++)
            {

              var x_mid = (averages[i].x + averages[i+1].x) / 2;
              var y_mid = (averages[i].y + averages[i+1].y) / 2;
              var cp_x1 = (x_mid + averages[i].x) / 2;
              var cp_x2 = (x_mid + averages[i+1].x) / 2;
              ctx.quadraticCurveTo(cp_x1, averages[i].y ,x_mid, y_mid);
              ctx.quadraticCurveTo(cp_x2, averages[i+1].y ,averages[i+1].x, averages[i+1].y);
            }

            ctx.stroke();

How would you do this?

To get a moving mean you need to just get the mean of n points either side of the current sample.

For example

// array of data points 
const movingMean = []; // the resulting means
const data = [12,345,123,53,134,...,219]; // data with index representing x axis
const sampleSize = 5;
for(var i = sampleSize; i < data.length - sampleSize; i++){
    var total = 0;
    for(var j = i- sampleSize; j < i + sampleSize;  j++){
         total += data[j];
    }
    movingMean[i] = total / (sampleSize * 2);
}

This method does not pull the mean forward giving the most accurate mean for each data point.

The problem with this method is that you do not get a mean for the first n and last n samples, where n is the number of samples either side of the mean.

You can do an alternative that will pull the mean forward a little but by applying a weighted mean you can reduce the bias a little

for(var i = sampleSize; i < data.length + Math.floor(sampleSize / 4); i++){
    var total = 0;
    var count = 0;
    for(var j = sampleSize; j > 0; j --){
        var index = i - (sampleSize - j);
        if(index < data.length){
             total += data[index] * j; // linear weighting
            count += j; 
        }
    }
    movingMean[i-Math.floor(sampleSize / 4)] = total / count;
}

This method keeps that mean closer to the current sample end.

The example show a random data set and the two types of means plotted over it. Click to get a new plot. The red line is the moving mean and the blue is the weighted mean. Note how the blue line tends to follow the data a little slow. The green line is a weighted mean that has a sample range 4 times greater than the other two.

 // helper functions const doFor = (count, callback) => {var i = 0; while (i < count) { callback(i ++) } }; const setOf = (count, callback) => {var a = [],i = 0; while (i < count) { a.push(callback(i ++)) } return a }; const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min; const randG = (dis, min, max) => {var r = 0; doFor(dis,()=>r+=rand(min,max)); return r / dis}; function getMinMax(data){ var min = data[0]; var max = data[0]; doFor(data.length - 1, i => { min = Math.min(min,data[i+1]); max = Math.max(max,data[i+1]); }); var range = max-min; return {min,max,range}; } function plotData(data,minMax){ ctx.beginPath(); for(var i = 0; i < data.length; i++){ if(data[i] !== undefined){ var y = (data[i] - minMax.min) / minMax.range; y = y *(ctx.canvas.height - 2) + 1; ctx.lineTo(i/2,y); } } ctx.stroke(); } function getMovingMean(data,sampleSize){ const movingMean = []; // the resulting means for(var i = sampleSize; i < data.length - sampleSize; i++){ var total = 0; for(var j = i- sampleSize; j < i + sampleSize; j++){ total += data[j]; } movingMean[i] = total / (sampleSize * 2); } return movingMean[i]; } function getMovingMean(data,sampleSize){ const movingMean = []; // the resulting means for(var i = sampleSize; i < data.length - sampleSize; i++){ var total = 0; for(var j = i- sampleSize; j < i + sampleSize; j++){ total += data[j]; } movingMean[i] = total / (sampleSize * 2); } return movingMean; } function getWeightedMean(data,sampleSize){ const weightedMean = []; for(var i = sampleSize; i < data.length+Math.floor(sampleSize/4); i++){ var total = 0; var count = 0; for(var j = sampleSize; j > 0; j --){ var index = i - (sampleSize - j); if(index < data.length){ total += data[index] * j; // linear weighting count += j; } } weightedMean[i-Math.floor(sampleSize/4)] = total / count; } return weightedMean; } const dataSize = 1000; const sampleSize = 50; canvas.width = dataSize/2; canvas.height = 200; const ctx = canvas.getContext("2d"); function displayData(){ ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); var dataPoint = 100; var distribution = Math.floor(rand(1,8)); var movement = rand(2,20); const data = setOf(dataSize,i => dataPoint += randG(distribution, -movement, movement)); const movingMean = getMovingMean(data, sampleSize); const weightedMean = getWeightedMean(data, sampleSize*2); const weightedMean1 = getWeightedMean(data, sampleSize*8); var minMax = getMinMax(data); ctx.strokeStyle = "#ccc"; plotData(data,minMax); ctx.strokeStyle = "#F50"; plotData(movingMean,minMax); ctx.strokeStyle = "#08F"; plotData(weightedMean,minMax); ctx.strokeStyle = "#4C0"; plotData(weightedMean1,minMax); } displayData(); document.onclick = displayData; 
 body { font-family : arial; } .red { color : #F50; } .blue { color : #0AF; } .green { color : #4C0; } canvas { position : absolute; top : 0px; left :130px; } 
 <canvas id="canvas"></canvas> <div class="red">Moving mean</div> <div class="blue">Weighted mean</div> <div class="green">Wide weighted mean</div> <div>Click for another sample</div> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM