Lib: Graph-2.0 — Line, Scatter, Realtime and Pie Charts for WoW Addons
LibGraph-2.0 draws graphs inside a WoW addon: line graphs, filled area graphs, scatter plots with linear regression, live scrolling realtime bars, and pie charts. It is a LibStub library with no dependencies beyond LibStub itself, and it works unchanged on Classic Era, TBC, Wrath, Cataclysm, Mists and Retail.
This is the maintained continuation of Cryect and Xinhuan's original library, published as one shared copy so the addons that embed it stop drifting apart.
The Problem
WoW gives addon authors no charting primitives at all. There is no line, no plotted point, no filled region — only textures you can stretch and rotate. Drawing a graph means building that layer yourself: rotating a 1-pixel texture to an arbitrary angle, pooling the textures so a redraw does not leak frames, working out gridline intervals that land on round numbers, and doing it every OnUpdate without costing frames.
Every damage meter and statistics panel that shows a graph has solved this, and LibGraph-2.0 is the solution most of them use. But because it is embedded rather than installed, each addon carries its own private copy — and those copies drift. The two forks merged into this release had both fixed real bugs, neither knew about the other, and one of them was still crashing on Retail.
The Solution
One library, one repository, four graph types, and an API that is a handful of calls: create a graph frame, hand it your data as a table of {x, y} pairs, and it draws and redraws itself. Texture pooling, gridline placement, axis labelling, autoscaling and mouse hit-testing are all handled inside.
Core Features
Line Graphs
- Multiple data series on one graph — call
AddDataSeriesonce per series, each with its own colour. Two shapes are accepted: a list of{x, y}pairs, or two parallel arrays{xs, ys}with then2argument set totrue. The table is stored by reference and not copied, and must be a proper sequence — a hole makes the graph stop drawing at the gap, which the library now reports in chat naming the call that was given it - Filled area series —
AddFilledDataSeriesshades the region between two bounds, for things like a min/max band around an average - Custom line textures —
SetLineTextureswaps the line art per series or per graph - Autoscaling —
SetAutoScale(true)fits the axes to the data as it changes, withLockXMin,LockXMax,LockYMinandLockYMaxto pin any edge you want held still - Borders —
SetBorderSize("left"|"right"|"top"|"bottom", size)replaces the automatic 10% margin on one edge while autoscaling. Mind the sign: the size is added to the data extreme on every edge, so a positive value widens the axis at the top and right but moves it inward at the bottom and left. Pass a negative number on those two edges to get an outward margin. This is the original library's behaviour, kept deliberately so a graph drawn by any copy of LibGraph-2.0 renders identically
Scatter Plots
- Plots each point as its own pooled texture, from the same
{x, y}data series a line graph takes - Linear regression —
SetLinearFit(true)draws a least-squares fit line through the data;LinearRegressionis exposed directly if you want the coefficients rather than the line - Shares the full axis, gridline and locking API with line graphs
Realtime Graphs
- A scrolling bar graph fed one value at a time —
AddTimeData(value)as the data arrives, and the graph scrolls left on its own - Stop feeding a realtime graph you have hidden. Samples are aged out by the graph's own per-frame update, and the game does not run that on a hidden frame — so a feed that keeps calling
AddTimeDatainto a hidden graph accumulates samples that nothing can reclaim. Unhide it, stop the feed, or callResetData(). Recount is worth copying here: it unregisters its tracking callback when a realtime window closes, so the feed stops with the window - Five modes —
SetMode("FAST")(default) shifts whole bars and convolves only the newly exposed ones;"SLOW"rebuilds every bar from the raw samples each frame for a smoother trace;"EXP"and"EXPFAST"keep an exponentially decaying running value;"RAW"does no time convolution at all and draws only whatAddBarpushes - Filter shape —
graph.Filteris a field rather than a setter:"RECT"(default) or"TRI"for a triangular weighting, applying toFASTandSLOW - Filtering —
SetFilterRadiusandSetDecaycontrol how much each sample bleeds into its neighbours, so a spiky feed reads as a curve rather than a comb - Gradient bars —
SetBarColors(bottom, top)sets a vertical gradient across every bar - Frame-rate control —
SetUpdateLimitcaps how often the graph redraws, independently of how often you feed it
Pie Charts
AddPie(percent, color)per slice,CompletePie(color)to fill whatever is left,ResetPie()to start over- Slices are drawn from rotated textures, so they are smooth at any angle rather than stepped
- Mouse tracking —
SetSelectionFuncis called with the slice under the cursor as it moves, so you can highlight a slice or drive a tooltip from it
Axes, Gridlines and Labels
SetXAxis(min, max)andSetYAxis(min, max), orSetYMaxalone when the bottom is fixed at zeroSetGridSpacing(x, y)for the interval, andSetGridSecondaryMultiple(x, y)for a heavier line every n gridlinesSetAxisColor,SetGridColorandSetGridColorSecondary, each taking an{r, g, b, a}tableSetAxisDrawing(xaxis, yaxis)to turn either axis off entirelySetYLabels(left, right)puts numeric labels down either side, or both
Localisable Labels — graph.LabelHook
- The library draws its own axis numbers, which used to make them unlocalisable. Set
graph.LabelHookto a function and it is handed each label'sFontStringright after the text is set and before it is shown - That is enough to substitute a locale's own digits, or apply a font that can draw them, without the library knowing anything about localisation
- Called at every label site on every graph type. Leave it
niland nothing changes
X-Axis Labels — graph.XLabelsEnabled
- Set
graph.XLabelsEnabled = trueand a line graph labels its X gridlines along the bottom, following the same secondary-gridline spacing the gridlines use - The rightmost label is skipped so it cannot run off the edge of the frame
- Off by default — the original library drew no X-axis labels at all
Drawing Primitives
DrawLine,DrawVLine,DrawHLineandDrawBarare public, so you can draw arbitrary rotated lines onto any frame without creating a graph at all- Every texture is pooled and reused —
HideLinesandHideBarsreturn them rather than destroying them, so a graph that redraws every frame does not leak
How It Works
Drawing a graph
- Create it —
local graph = LibStub("LibGraph-2.0"):CreateGraphLine(name, parent, "CENTER", "CENTER", 0, 0, width, height)returns an ordinaryFramewith the graph methods attached - Set the axes —
graph:SetXAxis(0, 60),graph:SetYAxis(0, 5000), or turn onSetAutoScale(true)and let it follow the data - Add data —
graph:AddDataSeries({{0, 120}, {1, 340}, {2, 90}}, {1, 0, 0, 1}), once per series - That is all — the graph redraws itself on its own
OnUpdate;ResetData()clears the series when you want to start again
Why the version is bumped
- Every consumer embeds its own copy of LibGraph-2.0, and LibStub loads whichever copy registers the highest
minor - Stock registers
90000 + SVN revision; this copy registers90000 + 2000 + SVN revision - Without that bump, an addon in your install shipping a stock copy would win the load race and the Retail fix,
LabelHookandXLabelsEnabledwould silently not exist - It is
+2000rather than+1000because LibStub refuses an equal minor, and+1000collided with an older private fork that still crashes on Retail — a tie is decided by load order, and the broken copy was winning it - The bump is what makes this copy the one that loads, whatever else is installed alongside it
- Graphs another copy already created are retrofitted when this one loads over it, so the fix reaches you without every addon that embeds LibGraph needing an update first
API Summary
- lib:CreateGraphLine(name, parent, relative, relativeTo, offsetX, offsetY, width, height)
- lib:CreateGraphScatterPlot(name, parent, relative, relativeTo, offsetX, offsetY, width, height)
- lib:CreateGraphRealtime(name, parent, relative, relativeTo, offsetX, offsetY, width, height)
- lib:CreateGraphPieChart(name, parent, relative, relativeTo, offsetX, offsetY, width, height)
- lib:DrawLine(frame, sx, sy, ex, ey, width, color, layer, texture) · DrawVLine(frame, x, sy, ey, width, color, layer) · DrawHLine(frame, sx, ex, y, width, color, layer)
- lib:DrawBar(frame, sx, sy, ex, ey, color, level) · HideLines(frame) · HideBars(frame) —
levelputs the bar in a sub-frame atframe:GetFrameLevel() + leveland lifts the frame's text above it; omit it to draw on the frame itself - graph:AddDataSeries(points, color, n2, lineTexture) · AddFilledDataSeries(points, color, n2) · ResetData()
- graph:AddTimeData(value) · AddBar(value) · GetValue(time) · GetMaxValue()
- graph:AddPie(percent, color) · CompletePie(color) · ResetPie() · SetSelectionFunc(fn)
- graph:SetXAxis(min, max) · SetYAxis(min, max) · SetYMax(max) · SetMinMaxY(value)
- graph:SetAutoScale(bool) · LockXMin / LockXMax / LockYMin / LockYMax(bool)
- graph:SetGridSpacing(x, y) · SetGridSecondaryMultiple(x, y) · SetAxisDrawing(x, y) · CreateGridlines()
- graph:SetAxisColor(color) · SetGridColor(color) · SetGridColorSecondary(color) · SetBarColors(bottom, top)
- graph:SetYLabels(left, right) · SetLineTexture(texture) · SetBorderSize(border, size)
- graph:SetMode("FAST" | "SLOW" | "EXP" | "EXPFAST" | "RAW") · SetFilterRadius(r) · SetDecay(d) · SetUpdateLimit(seconds) · graph.Filter = "RECT" | "TRI"
- graph:SetLinearFit(bool) · LinearRegression(data) · RefreshGraph()
- graph.LabelHook = function(fontString) — called for each axis label before it is shown
- graph.XLabelsEnabled = true — draw numeric labels along the X axis
Requirements
- LibStub — bundled, and registered only if your install does not already have a newer one
- Ace3 and VersionCheck-1.0 — required by this standalone addon only, and installed for you by CurseForge. They exist so this install can tell you when it is out of date; VersionCheck needs Ace3's AceComm and AceSerializer and does not embed them
- The drawing code itself has no dependencies. An addon that embeds LibGraph-2.0 in its own
Libsfolder needs LibStub and nothing more — an embedded copy never loads this TOC, so it never pulls Ace3 or VersionCheck in, and it deliberately does not register for version checking (it cannot be updated on its own, so the advice would be unactionable) - WoW version: Classic Era (11509) · TBC Classic (20506) · Wrath Classic (30405) · Cataclysm Classic (40402) · MoP Classic (50504) · Retail (110207 / 120005 / 120007 / 120100)
- This is a library, not an addon with a user interface. It loads on demand when an addon that declares it asks for it. Installing it on its own does nothing visible
Recent Updates
v2.0.7 (2026-08-25)
Fixed
- The realtime graph dropped only half its stale samples. The five places that age old samples out of the sample list — SLOW/RECT, SLOW/TRI, FAST/RECT, EXP and EXPFAST — each removed entries while counting forwards through the same table, so the element that slid into a vacated slot was never visited. With a backlog of six stale samples, three of them survived — exactly half. All five now walk backwards, so a removal only touches indices already passed. This was never a crash — the leaked samples keep contributing to the smoothing, so a busy realtime graph plausibly read a little high and held values a little too long. That part is not demonstrated and is not claimed.
AddPiehad the number of slice textures written as a literal7. It is now derived from the texture list, so the two pie entry points cannot disagree if that list ever changes.- Two defects in
DrawBar, both reachable only through the public drawing primitive. When a bar's level would bury the graph's text, the library lifts the text frame and asks for a redraw — but that request was addressed to the wrong object, so on the public path it set a field nothing reads and the redraw never came. Separately, the texture pool returns a bar without restoring its parent while the parent was only ever set when a level was given, so a recycled bar could be drawn by an unlevelled call at whatever level the previous caller used. Neither is reachable from inside the library, so no existing graph renders differently.
New
- The standalone addon now tells you when it is out of date. It depends on Ace3 and VersionCheck-1.0 (both installed for you by CurseForge) and registers with VersionCheck, which runs one batched guild-wide check at login and pops a single alert if a guildmate is on a newer copy. An addon that embeds LibGraph-2.0 is unaffected — an embedded copy never loads this addon's TOC, so it pulls in neither dependency, and it deliberately does not register: a copy that arrived inside another addon is updated when that addon is, so the alert would be advice nobody could act on.
- A data series with a hole in it is now diagnosed instead of quietly drawn short.
AddDataSeriesandAddFilledDataSeriesstore the caller's table by reference and never checked it was a proper sequence — so a consumer building points with explicit indices, or from a filtered source that skips entries, could hand over a table with a gap and get a clean, plausible, shorter line: a graph that looks correct and is missing data. Both entry points now compare a counted walk against the length and print one chat line naming which call was given the bad table, then store it anyway. - It warns rather than refuses, and it warns once. Refusing would not draw the missing points either — the outcome is truncation whichever way, so the real choice was truncating loudly or truncating silently. And it reports once per graph per entry point:
AddDataSeriesis half the reset-then-re-add refresh idiom, so an unbounded per-call warning on a chart redrawing on a timer would be one chat line per refresh, forever, naming a function the player never called. - The check covers both data shapes. Both entry points accept a list of
{x, y}pairs or two parallel arrays (then2form), and the parallel-array form is rebuilt internally into a series that is well-formed by construction — so a check placed after that rebuild is blind to a hole in the input. The check now runs before it, on the array that is actually walked, and the message names which array it is. This is the form real consumers use: Recount passes it at all four of its call sites.
Changed
- Four loops that depend on order now ask for order. Slice decomposition in
AddPie/CompletePie, and the point-to-point chaining in both line and filled series, are only correct in sequence order — and Lua does not promise that order for the loop they were using, even for numeric indices. They have always drawn correctly, which is exactly the problem: the code was relying on an implementation detail to draw a graph. Nothing about the rendering changes. - Two long-standing behaviours are documented rather than changed.
SetBorderSize's size is added to the data extreme on every edge, so a positive value widens the axis at the top and right but crops at the bottom and left — the sign rule is now stated on the function itself and in the error it raises when misused. And feeding a hidden realtime graph accumulates samples nothing can reclaim, because the per-frame update that ages them out does not run on a hidden frame. Both are the original library's behaviour and are kept, so a graph drawn by any copy of LibGraph-2.0 renders identically.
v2.0.6 (2026-08-25)
Fixed
- 148 identical errors per session on Retail 12.0. The pie chart's mouse test used the
MouseIsOverglobal, which Retail 12.0 no longer defines. This file caches its globals at load time, so the value wasnilfor the whole session and every frame the pie chart updated threw. It now callsself:IsMouseOver()— which is what that global always forwarded to, and which exists on every flavour, so no version guard is needed. DrawHLinedrew its texture upside down. One fork had replaced the original 8-parameterSetTexCoordwith a 4-parameter call described as the simpler equivalent. It was not equivalent: the 4-parameter order is(left, right, bottom, top), and the replacement swapped the last two, flipping the texture vertically. Corrected to the true equivalent. You may never have seen this — if the line texture is symmetric top to bottom, both forms render the same.
New
graph.LabelHook— localisable axis labels. The library draws its own axis numbers, which made them impossible to localise from outside. Set this to a function and it is handed each label'sFontStringafter the text is set and before it is shown, which is enough to substitute a locale's own digits or apply a font that can draw them. Leave it unset and nothing changes.graph.XLabelsEnabled— numeric labels along the X axis. The original library labelled the Y axis and left the X axis bare. Switch this on and a line graph labels its X gridlines along the bottom, on the same spacing the gridlines use, skipping the rightmost so it cannot run off the edge. Off by default.
Changed
- One shared copy, replacing several private ones. LibGraph-2.0 is embedded rather than installed, so every addon that used it carried its own copy and the copies had drifted apart. This release merges the two forks that had real changes in them — the Retail fix came from one, the label additions from the other, and neither knew about the other. Consumers can now embed the same file.
- The library version is bumped
+2000over stock, so that when several addons in one install each ship a copy, this one wins LibStub's load race and the additions above are actually present.+1000was tried first and was not enough: LibStub refuses an equal minor, and+1000tied exactly with an older private fork that still crashes on Retail — a tie is decided by load order, and the broken copy was winning it. - Packaged for release — a TOC per live flavour, a
.pkgmeta, a packager workflow, and aLICENSE, none of which the library had before.
Credits
Cryect — original author of GraphLib / LibGraph-2.0; designed and built the graph engine, the rotated-texture line drawing, and all four graph types.
Xinhuan — long-running maintenance of the original library on WowAce.
Nelson Minar — caught several errors where width was used in place of height, credited in the original source.
Pimptasty — this consolidation: merging the divergent forks, the Retail 12.0 fix, the LabelHook and XLabelsEnabled additions, multi-flavour TOCs, packaging and documentation.
LibStub — Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke. Public domain.
Licence
The original library remains All Rights Reserved to Cryect and Xinhuan. Maintenance from v2.0.6 onward is MIT. LibStub.lua is public domain. Full terms in the LICENSE file that ships with the addon.
Community
Bug reports, feature requests, or questions: Join the Discord, or open an issue on GitHub.

