Skip to main content
The Scatter Plot widget renders point clouds with automatic axis scaling and per-point coloring.

Basic Usage

import { ui } from "@rezi-ui/core";

ui.scatter({
  id: "scatter-demo",
  width: 50,
  height: 25,
  points: [
    { x: 10, y: 20 },
    { x: 30, y: 45 },
    { x: 50, y: 30 },
    { x: 70, y: 60 },
  ],
  color: "#3b82f6",
});

Props

id
string
Widget identifier for debugging.
width
number
required
Chart width in terminal columns.
height
number
required
Chart height in terminal rows.
points
ScatterPoint[]
required
Data points to plot. Each point has:
  • x: number - X coordinate
  • y: number - Y coordinate
  • color?: string - Optional per-point color (overrides default)
axes
{ x?: ChartAxis, y?: ChartAxis }
Axis configuration:
  • label?: string - Axis label
  • min?: number - Minimum value (auto if omitted)
  • max?: number - Maximum value (auto if omitted)
color
string
default:"#3b82f6"
Default color for points (hex or theme). Overridden by per-point colors.
blitter
GraphicsBlitter
default:"braille"
Rendering mode. One of "braille", "sextant", "quadrant", "halfblock". Note: "ascii" is not supported for scatter plots.

Data Format

Points are objects with x, y, and optional color:
type ScatterPoint = {
  x: number;
  y: number;
  color?: string; // Optional per-point color
};

Uniform Color

ui.scatter({
  id: "uniform",
  width: 50,
  height: 25,
  points: [
    { x: 10, y: 20 },
    { x: 30, y: 40 },
    { x: 50, y: 30 },
  ],
  color: "#3b82f6",
});

Per-Point Colors

ui.scatter({
  id: "colored",
  width: 50,
  height: 25,
  points: [
    { x: 10, y: 20, color: "#ef4444" },
    { x: 30, y: 40, color: "#10b981" },
    { x: 50, y: 30, color: "#3b82f6" },
  ],
});

Mixed (Default + Override)

ui.scatter({
  id: "mixed",
  width: 50,
  height: 25,
  color: "#6b7280", // Default gray
  points: [
    { x: 10, y: 20 }, // Uses default
    { x: 30, y: 40, color: "#ef4444" }, // Red outlier
    { x: 50, y: 30 }, // Uses default
  ],
});

Axis Configuration

Auto-Scaling (Default)

ui.scatter({
  width: 50,
  height: 25,
  points: [
    { x: 5, y: 10 },
    { x: 95, y: 90 },
  ],
  // X-axis: 5 to 95
  // Y-axis: 10 to 90
  color: "#3b82f6",
});

Fixed Ranges

ui.scatter({
  width: 50,
  height: 25,
  points: data,
  axes: {
    x: { min: 0, max: 100, label: "Score" },
    y: { min: 0, max: 100, label: "Accuracy %" },
  },
  color: "#3b82f6",
});

Partial Override

ui.scatter({
  width: 50,
  height: 25,
  points: data,
  axes: {
    x: { min: 0 }, // Fix min, auto max
    y: { max: 100 }, // Auto min, fix max
  },
  color: "#3b82f6",
});

Examples

Correlation Plot

function correlationPlot(
  xValues: number[],
  yValues: number[],
  xLabel: string,
  yLabel: string
): VNode {
  const points = xValues.map((x, i) => ({
    x,
    y: yValues[i],
  }));
  
  return ui.column({ gap: 1 }, [
    ui.text(`${xLabel} vs ${yLabel}`, { variant: "heading" }),
    ui.scatter({
      id: "correlation",
      width: 60,
      height: 30,
      points,
      axes: {
        x: { label: xLabel },
        y: { label: yLabel },
      },
      color: "#3b82f6",
    }),
  ]);
}

Clustered Data

type DataPoint = { x: number; y: number; cluster: number };

const CLUSTER_COLORS = [
  "#ef4444", // Red
  "#3b82f6", // Blue
  "#10b981", // Green
  "#f59e0b", // Yellow
];

function clusterPlot(data: DataPoint[]): VNode {
  const points = data.map((pt) => ({
    x: pt.x,
    y: pt.y,
    color: CLUSTER_COLORS[pt.cluster % CLUSTER_COLORS.length],
  }));
  
  return ui.scatter({
    id: "clusters",
    width: 70,
    height: 35,
    points,
    axes: {
      x: { label: "Feature A" },
      y: { label: "Feature B" },
    },
  });
}

Outlier Detection

function outlierPlot(
  data: Array<{ x: number; y: number }>,
  threshold: number
): VNode {
  const maxDistance = Math.max(...data.map((p) =>
    Math.sqrt(p.x ** 2 + p.y ** 2)
  ));
  
  const points = data.map((pt) => {
    const distance = Math.sqrt(pt.x ** 2 + pt.y ** 2);
    const isOutlier = distance > maxDistance * threshold;
    
    return {
      x: pt.x,
      y: pt.y,
      color: isOutlier ? "#ef4444" : "#3b82f6",
    };
  });
  
  return ui.scatter({
    id: "outliers",
    width: 60,
    height: 30,
    points,
    axes: {
      x: { label: "X" },
      y: { label: "Y" },
    },
  });
}

Time Series Scatter

function timeSeriesScatter(
  timestamps: number[],
  values: number[]
): VNode {
  const points = timestamps.map((ts, i) => ({
    x: ts,
    y: values[i],
  }));
  
  return ui.scatter({
    id: "timeseries",
    width: 80,
    height: 25,
    points,
    axes: {
      x: { label: "Time (s)" },
      y: { label: "Value", min: 0 },
    },
    color: "#8b5cf6",
  });
}

Distribution Density

function densityPlot(data: Array<{ x: number; y: number }>): VNode {
  // Color by local density
  const getDensity = (point: { x: number; y: number }): number => {
    const radius = 10;
    return data.filter((p) => {
      const dx = p.x - point.x;
      const dy = p.y - point.y;
      return Math.sqrt(dx * dx + dy * dy) < radius;
    }).length;
  };
  
  const maxDensity = Math.max(...data.map(getDensity));
  
  const points = data.map((pt) => {
    const density = getDensity(pt);
    const ratio = density / maxDensity;
    
    // Gradient from blue (sparse) to red (dense)
    const r = Math.floor(ratio * 255);
    const b = Math.floor((1 - ratio) * 255);
    const color = `rgb(${r}, 0, ${b})`;
    
    return { x: pt.x, y: pt.y, color };
  });
  
  return ui.scatter({
    id: "density",
    width: 60,
    height: 30,
    points,
  });
}

Multi-Series Scatter

function multiSeriesScatter(
  series: Array<{
    name: string;
    data: Array<{ x: number; y: number }>;
    color: string;
  }>
): VNode {
  // Flatten all series into single point array
  const points = series.flatMap((s) =>
    s.data.map((pt) => ({ ...pt, color: s.color }))
  );
  
  // Create legend
  const legend = ui.row({ gap: 2 }, [
    ...series.map((s) =>
      ui.row({ gap: 1, key: s.name }, [
        ui.text("●", { style: { fg: s.color } }),
        ui.text(s.name),
      ])
    ),
  ]);
  
  return ui.column({ gap: 1 }, [
    legend,
    ui.scatter({
      id: "multi-series",
      width: 70,
      height: 30,
      points,
    }),
  ]);
}

Real-Time Streaming

import { defineWidget } from "@rezi-ui/core";

type StreamState = {
  points: Array<{ x: number; y: number }>;
  maxPoints: number;
};

function streamingScatter(state: StreamState): VNode {
  // Keep only last N points
  const recentPoints = state.points.slice(-state.maxPoints);
  
  // Fade older points
  const points = recentPoints.map((pt, i) => {
    const age = recentPoints.length - i;
    const alpha = 1 - (age / recentPoints.length) * 0.7;
    const gray = Math.floor(59 + alpha * 196); // 59 to 255
    
    return {
      x: pt.x,
      y: pt.y,
      color: `rgb(59, 130, ${gray})`,
    };
  });
  
  return ui.scatter({
    id: "streaming",
    width: 60,
    height: 30,
    points,
    axes: {
      x: { label: "Time" },
      y: { label: "Value" },
    },
  });
}

Performance

Point Limits

  • Braille: Smooth up to ~1000 points
  • Sextant: Smooth up to ~800 points
  • Quadrant: Smooth up to ~600 points
  • Halfblock: Smooth up to ~400 points
For larger datasets:
  • Downsample using grid binning
  • Filter to visible viewport
  • Use heatmap for very dense data

Grid Binning Example

function binPoints(
  points: Array<{ x: number; y: number }>,
  gridSize: number
): Array<{ x: number; y: number }> {
  const bins = new Map<string, { x: number; y: number; count: number }>();
  
  for (const pt of points) {
    const binX = Math.floor(pt.x / gridSize) * gridSize;
    const binY = Math.floor(pt.y / gridSize) * gridSize;
    const key = `${binX},${binY}`;
    
    const existing = bins.get(key);
    if (existing) {
      existing.x = (existing.x * existing.count + pt.x) / (existing.count + 1);
      existing.y = (existing.y * existing.count + pt.y) / (existing.count + 1);
      existing.count++;
    } else {
      bins.set(key, { x: pt.x, y: pt.y, count: 1 });
    }
  }
  
  return Array.from(bins.values());
}

const largeDataset = generatePoints(10000);
const binned = binPoints(largeDataset, 5);

ui.scatter({
  id: "binned",
  width: 60,
  height: 30,
  points: binned,
  color: "#3b82f6",
});

Blitter Modes

Braille (Default)

Highest resolution for detailed point clouds:
ui.scatter({
  width: 60,
  height: 30,
  points: data,
  blitter: "braille",
  color: "#3b82f6",
});

Sextant

Block-based for better visibility of individual points:
ui.scatter({
  width: 60,
  height: 30,
  points: data,
  blitter: "sextant",
  color: "#3b82f6",
});

Quadrant

Larger points, good for sparse data:
ui.scatter({
  width: 60,
  height: 30,
  points: data,
  blitter: "quadrant",
  color: "#3b82f6",
});

See Also

Build docs developers (and LLMs) love