drawDataPoints method

  1. @override
void drawDataPoints()
override

Draws graph data points, clearing areas outside the grid. It is possible that some data points lie outside the graph grid, whether it lies outside of the X range or Y range or both. Those that are out of range will be removed, but a line will still be drawn towards the "invisible" data point.

Because areas outside the grid will be cleared (to clear out those liens and shading that are caused by points that are out of range), text labels must be drawn only after drawing data points.

Implementation

@override
void drawDataPoints() {
  var minX = getLabelMinX();
  var minY = getLabelMinY();
  var maxX = getLabelMaxX();
  var maxY = getLabelMaxY();
  var allMapPoints = { for (var e in _dataPoints.entries) e.key : SplayTreeMap<double, double>.from(e.value) };
  var allDotPoints = <String, SplayTreeMap<double, double>>{};
  var allLinePoints = <String, SplayTreeMap<double, double>>{};
  // ctx.globalCompositeOperation = 'source-over';
  allMapPoints.forEach((key, mapPoints) {
    var belowRange = <double>[];
    var aboveRange = <double>[];
    mapPoints.forEach((key, value) {
      if (key < minX) {
        belowRange.add(key);
      } else if (key > maxX) {
        aboveRange.add(key);
      }
    });

    // Remove all points but one that are below range.
    // The closest point that is below range is skipped.
    if (belowRange.length > 1) {
      for (var i = 0; i < belowRange.length - 1; i++) {
        mapPoints.remove(belowRange[i]);
      }
    }

    // Remove all points but one that are above range.
    // The closest point that is above range is skipped.
    if (aboveRange.length > 1) {
      aboveRange.removeAt(0);
      for (var i = 1; i < aboveRange.length; i++) {
        mapPoints.remove(aboveRange[i]);
      }
    }

    _hoverPaths.clear();
    // Points that need to be drawn as dots.
    allDotPoints[key] = SplayTreeMap<double, double>.from(mapPoints);
    var dotPoints = allDotPoints[key]!;
    // Points that the line graph has to go through, this includes interpolated
    // points.
    allLinePoints[key] = SplayTreeMap<double, double>.from(mapPoints);
    var linePoints = allLinePoints[key]!;

    // If the first point that is in range is == max, also remove
    // all points that are below range.
    if (belowRange.isNotEmpty) {
      dotPoints.remove(dotPoints.firstKey());
      linePoints.remove(linePoints.firstKey());
      if (mapPoints.firstKeyAfter(mapPoints.firstKey()!) == minX) {
        mapPoints.remove(mapPoints.firstKey());
      } else {
        var x1 = mapPoints.firstKey()!;
        var x2 = mapPoints.firstKeyAfter(mapPoints.firstKey()!)!;
        var y1 = mapPoints[x1]!;
        var y2 = mapPoints[x2]!;
        linePoints[minX] = lerp(minX, x1, x2, y1, y2);
      }
    }

    // If the last point that is in range is == max, also remove
    // all points that are above range.
    if (aboveRange.isNotEmpty) {
      dotPoints.remove(dotPoints.lastKey());
      linePoints.remove(linePoints.lastKey());
      if (mapPoints.lastKeyBefore(mapPoints.lastKey()!) == maxX) {
        mapPoints.remove(mapPoints.lastKey());
      } else {
        var x1 = mapPoints.lastKeyBefore(mapPoints.lastKey()!)!;
        var x2 = mapPoints.lastKey()!;
        var y1 = mapPoints[x1]!;
        var y2 = mapPoints[x2]!;
        linePoints[maxX] = lerp(maxX, x1, x2, y1, y2);
      }
    }

    // At this point, all points should be in range, with one point below
    // and one point outside range, if needed, however, this is only for x.
    // Dot points that are out of range of Y range will just be removed.
    // Line points are not removed, they are still drawn even when out of the grid,
    // but later will be masked with clearRect.
    var outsideRangeY = SplayTreeMap<double, double>();
    mapPoints.forEach((x, y) {
      if (y > maxY || y < minY) {
        outsideRangeY[x] = y;
      }
    });
    dotPoints.removeWhere((key, value) => outsideRangeY.containsKey(key));

    ctx.globalCompositeOperation = 'lighter';
    if (linePoints.isNotEmpty) {
      // Draw shading.
      ctx.beginPath();
      ctx.fillStyle = (shadingFillStyle[key] ?? shadingFillStyle[keyAllGraph])!.toJS;
      ctx.moveTo(lerp(linePoints.firstKey()!, minX, maxX, gridMinPxX, gridMaxPxX), gridMaxPxY);
      linePoints.forEach((x, y) {
        ctx.lineTo(lerp(x, minX, maxX, gridMinPxX, gridMaxPxX), lerp(y, minY, maxY, gridMaxPxY, gridMinPxY));
      });
      ctx.lineTo(lerp(linePoints.lastKey()!, minX, maxX, gridMinPxX, gridMaxPxX), gridMaxPxY);
      ctx.fill();
    }
  });

  ctx.globalCompositeOperation = 'source-over';

  allMapPoints.forEach((key, value) {
    var linePoints = allLinePoints[key]!;
    if (linePoints.isNotEmpty) {
      // Draw line.
      ctx.beginPath();
      ctx.moveTo(lerp(linePoints.firstKey()!, minX, maxX, gridMinPxX, gridMaxPxX), lerp(linePoints[linePoints.firstKey()!]!, minY, maxY, gridMaxPxY, gridMinPxY));
      ctx.lineWidth = dataLineWidth[key] ?? dataLineWidth[keyAllGraph]!;
      ctx.strokeStyle = (dataLineStrokeStyle[key] ?? dataLineStrokeStyle[keyAllGraph])!.toJS;
      linePoints.forEach((x, y) {
        ctx.lineTo(lerp(x, minX, maxX, gridMinPxX, gridMaxPxX), lerp(y, minY, maxY, gridMaxPxY, gridMinPxY));
      });
      ctx.stroke();
    }
  });

  // Mask all lines and shading that are outside of the grid.
  ctx.clearRect(0, gridMaxPxY, ctx.canvas.width, ctx.canvas.height - gridMaxPxY);
  ctx.clearRect(0, 0, gridMinPxX, ctx.canvas.height);
  ctx.clearRect(0, 0, ctx.canvas.width, gridMinPxY);
  ctx.clearRect(gridMaxPxX, 0, ctx.canvas.width - gridMaxPxX, ctx.canvas.height);

  _hoverPaths.clear();
  allMapPoints.forEach((key, value) {
    var dotPoints = allDotPoints[key]!;
    if (dotPoints.isNotEmpty) {
      // Draw dot points.
      ctx.fillStyle = (dotFillStyle[key] ?? dotFillStyle[keyAllGraph])!.toJS;
      ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
      ctx.shadowBlur = 4;
      ctx.shadowOffsetX = 1;
      ctx.shadowOffsetY = 1;
      ctx.lineWidth = dotStrokeWidth[key] ?? dotStrokeWidth[keyAllGraph]!;
      ctx.strokeStyle = (dotStrokeStyle[key] ?? dotStrokeStyle[keyAllGraph])!.toJS;
      dotPoints.forEach((x, y) {
        var dot = Path2D();
        var radius = dotRadius[key] ?? dotRadius[keyAllGraph]!;
        var radiusOffset = hoverDotRadiusOffset[key] ?? hoverDotRadiusOffset[keyAllGraph]!;
        dot.arc(lerp(x, minX, maxX, gridMinPxX, gridMaxPxX), lerp(y, minY, maxY, gridMaxPxY, gridMinPxY), radius, 0, 2 * pi, false);
        ctx.fill(dot);
        ctx.stroke(dot);
        var hoverDot = Path2D();
        hoverDot.arc(lerp(x, minX, maxX, gridMinPxX, gridMaxPxX), lerp(y, minY, maxY, gridMaxPxY, gridMinPxY), radius + radiusOffset, 0, 2 * pi, false);
        if (!_hoverPaths.containsKey(key)) {
          _hoverPaths[key] = <MapEntry<double, double>, Path2D>{MapEntry(x, y): hoverDot};
        } else {
          _hoverPaths[key]![MapEntry(x, y)] = hoverDot;
        }
      });
    }
  });
}