drawSparkline function

void drawSparkline(
  1. Screen screen,
  2. Rectangle area,
  3. List<double> values, {
  4. UvStyle style = const UvStyle(),
  5. bool showGrid = false,
  6. UvStyle gridStyle = const UvStyle(),
  7. double baseline = 0.0,
  8. double? minValue,
  9. double? maxValue,
  10. UvStyle? gradientLow,
  11. UvStyle? gradientHigh,
})

Draws a compact sparkline of values into area on screen.

When area has a height of 1, each column uses a single block character from the 8-level set () to indicate the value.

When area has a height greater than 1, the sparkline fills the full vertical extent: full-block () cells below the value level, and a fractional block character at the top of each column for sub-cell precision. This gives 8× the vertical resolution of the cell grid.

baseline controls the value below which columns render as empty (default: 0.0). minValue and maxValue allow explicit bounds; when null, they are auto-detected from the data.

gradientLow and gradientHigh enable per-column color interpolation based on value: low values get gradientLow, high values get gradientHigh, with linear interpolation between.

Implementation

void drawSparkline(
  Screen screen,
  Rectangle area,
  List<double> values, {
  UvStyle style = const UvStyle(),
  bool showGrid = false,
  UvStyle gridStyle = const UvStyle(),
  double baseline = 0.0,
  double? minValue,
  double? maxValue,
  UvStyle? gradientLow,
  UvStyle? gradientHigh,
}) {
  final width = area.width;
  final height = area.height;
  if (width <= 0 || height <= 0) return;

  final samples = sampleSeries(values, width);
  if (samples.isEmpty) return;

  // Resolve bounds.
  var dataMin = minValue ?? samples.reduce((a, b) => a < b ? a : b);
  var dataMax = maxValue ?? samples.reduce((a, b) => a > b ? a : b);
  if (!dataMin.isFinite) dataMin = 0;
  if (!dataMax.isFinite) dataMax = 1;
  if (dataMin >= dataMax) {
    dataMin -= 0.5;
    dataMax += 0.5;
  }
  final range = dataMax - dataMin;

  if (showGrid) {
    drawGrid(
      screen,
      area,
      rows: height > 3 ? (height ~/ 3).clamp(1, 5) : 1,
      cols: 0,
      style: gridStyle,
      hChar: '┄',
      vChar: '┆',
      intersectionChar: '┼',
    );
  }

  final useGradient = gradientLow != null && gradientHigh != null;

  if (height == 1) {
    // Single-row mode: one character per column from the 8-level set.
    final row = area.minY;
    for (var x = 0; x < width; x++) {
      if (samples[x] <= baseline) continue; // at or below baseline → empty
      final normalized = clamp01((samples[x] - dataMin) / range);
      final idx = (normalized * 8).round();
      if (idx <= 0) continue;
      final glyph = sparkChars[idx.clamp(0, 8)];
      var cellStyle = style;
      if (useGradient) {
        cellStyle = _lerpStyle(gradientLow, gradientHigh, normalized);
      }
      putCell(screen, area.minX + x, row, glyph, cellStyle);
    }
    return;
  }

  // Multi-row mode: fill columns from the bottom using full blocks.
  final totalSubCells = height * 8;

  for (var x = 0; x < width; x++) {
    if (samples[x] <= baseline) continue;
    final normalized = clamp01((samples[x] - dataMin) / range);
    final subCellHeight = (normalized * totalSubCells).round().clamp(
      0,
      totalSubCells,
    );
    if (subCellHeight <= 0) continue;

    var cellStyle = style;
    if (useGradient) {
      cellStyle = _lerpStyle(gradientLow, gradientHigh, normalized);
    }

    final fullRows = subCellHeight ~/ 8;
    final remainder = subCellHeight % 8;

    for (var r = 0; r < fullRows; r++) {
      final cellY = area.maxY - 1 - r;
      if (cellY < area.minY) break;
      putCell(screen, area.minX + x, cellY, '█', cellStyle);
    }

    if (remainder > 0) {
      final cellY = area.maxY - 1 - fullRows;
      if (cellY >= area.minY) {
        final glyph = sparkChars[remainder];
        putCell(screen, area.minX + x, cellY, glyph, cellStyle);
      }
    }
  }
}