Plottr — animated, strictly-typed graphs for your GUIs

Plottr — animated, strictly-typed graphs for your GUIs :chart_increasing:

Plottr is an open-source graph rendering library for Roblox. Give it any GuiObject and some data, and it draws a clean, animated, interactive chart, no UI framework dependencies, no assets to import, just Luau.

Lines are rendered as antialiased Path2D strokes that draw themselves in like a pen, bars grow out of the axis, and a gradient infill rises in the pen’s wake. Everything re-renders automatically when your data, config, or container size changes, and for live data you can stream values straight into the chart.

Features

  • Line charts — antialiased Path2D strokes (auto-segmented so long paths never hit the engine’s control point limit)

  • Bar charts — rounded bars growing from the baseline, grouped automatically per slot

  • Mixed series — combine lines and bars in one chart, with an automatic legend

  • Animated entries — pen-stroke line reveal, staggered marker pop-ins, growing bars and fill, all on one synced timeline; duration, easing, and stagger are configurable, or turn animation off entirely

  • Gradient infill — the area under a line fades toward the baseline; configurable per graph or per series

  • Curve smoothing — optional monotone cubic interpolation that passes through every data point and never overshoots (no fake peaks)

  • Hover tooltips — points and bars show formatted values on hover

  • Realtime streaminggraph:push(series, value) appends and redraws instantly (no animation replay), with a maxPoints window that scrolls old values off; multiple pushes per frame coalesce into one redraw

  • DashboardsPlottr.newGroup lays out multiple graphs in a resizable grid

  • ThemesDark, Light, and Midnight presets, plus Themes.extend for custom palettes; swap live with setTheme

  • Smart axes — “nice” tick values (1/2/5 steps), abbreviated labels (1.5k, 2.3M), custom x-axis labels, optional zero baseline, configurable grid and padding

  • Reactive — re-renders (animated) on data/config changes and redraws instantly on container resize

  • Strictly typed--!strict throughout with exported types for every config table; great autocomplete in Studio

  • Modular & zero-dependency — small single-purpose modules; adding a new chart kind is just another renderer

Quick start


local Plottr = require(ReplicatedStorage.Plottr)

local graph = Plottr.new(containerFrame, {

theme = Plottr.Themes.Midnight,

smoothing = { enabled = true },

xLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun" },

})

graph:setSeries({

{ name = "Revenue", values = { 4200, 5100, 4800, 6900, 8400, 9600 } },

{ name = "Costs", values = { 3100, 3300, 3600, 4100, 4400, 4900 }, kind = "bar" },

})

Realtime data


local live = Plottr.new(containerFrame, { maxPoints = 40, showPoints = false })

live:setSeries({ { name = "CPU %", values = {} } })

task.spawn(function()

while task.wait(0.2) do

live:push("CPU %", getCpuSample())

end

end)

Dashboards


local group = Plottr.newGroup(containerFrame, { columns = 2, spacing = 12 })

local monthly = group:addGraph("Monthly", { xLabels = months })

local live = group:addGraph("Live", { maxPoints = 40 })

Installation

Wally (recommended):


[dependencies]

Plottr = "alternativelua/plottr@0.1.1"

Manual: grab the source from GitHub and drop src into your project (e.g. as ReplicatedStorage.Plottr).

Credits

Inspired by boatbeaker’s GraphModule. MIT licensed.


Feedback, bug reports, and PRs are very welcome. What should Plottr support next, let me know below! :slightly_smiling_face:

17 Likes

I’m surprised why this doesn’t have more likes. It’s genius, thanks for sharing it!

Changelog 0.2.0

Fixed

  • push/maxPoints no longer mutate caller-owned arrays — setSeries/addSeries deep-copy each series and its values, so two graphs can share one table safely.
  • Infinite loop in the Path2D chunker when GetMaxControlPoints() returns 2 — chunk size floored at 2.
  • NaN and infinite values are rejected at validation instead of reaching UDim2.fromOffset
  • Empty theme palettes are rejected instead of erroring on % 0 → nil colour
  • Tooltips clamp to the canvas and flip below the point when there’s no room above, so ClipsDescendants can’t cut them off
  • Duplicate series names now throw (push/removeSeries silently took the first match)
  • Config validation: wrong types, non-finite numbers, and negative sizes throw; yTickCount, maxXTickCount, smoothing.*, fill.* are clamped, so yTickCount = 1e6 can’t freeze the client
  • render() cancels a queued redraw instead of double-rendering
  • Tick values are computed as min + i * spacing rather than accumulated, and niceNum guards non-positive ranges
  • Optional config fields can be cleared with the new Plottr.None sentinel
  • GraphGroup.getGraph/removeGraph assert on a destroyed group like the other methods

Performance

  • New Internal/Pool recycles every instance a render produces — redraws update properties instead of destroying and rebuilding. Animator cancels in-flight tweens on recycled instances, and every kind has an explicit ZIndex so pooled sibling order can’t affect draw order
  • The pen reveal appends one control point per data point crossed and nudges a single interpolated tip, instead of rebuilding the whole prefix each frame (with a capability probe and a SetControlPoints fallback)
  • Point markers are skipped when spacing drops below pointRadius * 2 — they’d overlap into a band and their hitboxes would be unreachable anyway
  • maxPoints trims with one table.move instead of repeated table.remove(values, 1)

Added

  • yMin/yMax to pin the axis (ticks then span the range exactly, via Scale.fixedTicks)
  • formatValue for tick and tooltip text; xLabels also accepts a function
  • title, xAxisTitle, yAxisTitle
  • Series.visible, Graph:setSeriesVisible, and a clickable legend (legendToggle)
  • Graph:updateSeries, getSeries, getSeriesNames, clear, onPointClicked; GraphGroup:getGraphNames
  • Touch support — tap-and-hold tooltips and touch clicks alongside mouse hover
  • Theme.font / Theme.textSize (optional, default BuilderSans Medium 12px), replacing the two hardcoded font constants

Internal

  • _render split into Internal/Layout (data summary, y range, plot rect, x placement, tick thinning — all pure) and Internal/Validate, plus Internal/Constants for the shared metrics

Tooling & docs

  • .github/workflows/ci.yml: stylua check, luau-lsp analyze, Moonwave extraction, Rojo build on push and PR
  • Specs grew from 15 to ~60, including a rendering block that mounts into a real ScreenGui — the renderers previously had zero coverage because every test container was 0×0 and _render bailed early. Covers pooling reuse, leftover destruction, marker suppression, tooltip clamping, pinned bounds, and the aliasing fix
  • README cut to a landing page; the detail moved into docs/intro.md and the generated API reference.

Amazing! I will use it. I appreciate your making this.