promotional bannermobile promotional banner

LibGraph-2.0 - Revived

Reviving LibGraph-2.0 until the original author comes back and fixes it. They are welcome to any work I do on it to keep it function if they do return.
Back to Files

LibGraph-2.0-v2.0.7

File nameLibGraph-2.0-LibGraph-2.0-v2.0.7.zip
Uploader
PmptastyPmptasty
Uploaded
Aug 26, 2026
Downloads
1.0K
Size
57.9 KB
Flavors
RetailMoP ClassicClassic TBCClassic
File ID
8736054
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7
  • 12.0.5
  • 11.2.7
  • 5.5.4
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.15.9

What's new

Lib: Graph-2.0

[v2.0.7] (2026-08-25) — The realtime graph dropped only half its stale samples

New — the standalone addon now tells you when it is out of date

All six TOCs declare ## Dependencies: Ace3, VersionCheck-1.0, .pkgmeta lists both under required-dependencies so CurseForge installs them alongside this addon, and the library registers itself with VersionCheck-1.0 — which runs one batched guild-wide check at login and pops a single alert if a guildmate is on a newer copy. Ace3 is there because VersionCheck needs its AceComm and AceSerializer and does not embed them, not because any drawing code uses Ace3.

Those two lists have to agree and nothing checks that they do: the TOC line decides whether the addon loads, the .pkgmeta line decides what gets installed. A slug in one and not the other is a dependency the player never receives, or an addon that silently refuses to load.

An embedded copy does not register, and that is the load-bearing half. This file ships inside other addons' Libs folders, and a copy loaded from there is not an addon the client holds metadata for — it would report its version as unknown, and could not be updated on its own even if the report were right. So registration is gated on GetAddOnMetadata returning a real version for an addon folder actually named LibGraph-2.0, which is true of the standalone install and nothing else. A working copy straight from git is skipped too, since its version is still the packager's LibGraph-2.0-v2.0.7 placeholder and broadcasting that would be worse than staying quiet.

Embedding is unaffected. An embedded copy never loads this TOC, so it pulls in neither Ace3 nor VersionCheck, and the drawing code still depends on nothing but LibStub. There is no host-side call to add.

Six specs cover it, and three of them assert that nothing happens — removing the gate turns exactly those three red.

Fixed — five prune loops removed from a table while iterating it

OnUpdateGraphRealtime ages old samples out of self.Data in five places — SLOW/RECT, SLOW/TRI, FAST/RECT, EXP and EXPFAST — and every one of them did it as for k, v in pairs(self.Data) do … tremove(self.Data, k) … end. tremove shifts every later element down one while the iterator counts up, so the element that slides into the vacated slot is never visited.

This is not undefined behaviour, and the distinction is the whole explanation. The Lua 5.1 manual makes next undefined only if you assign to a non-existent field, and then says: "You may however modify existing fields. In particular, you may clear existing fields." tremove shifts existing fields and clears the last, so the traversal was explicitly permitted. What it is instead is a deterministic shift-versus-increment race — which is why the loss is exactly half rather than arbitrary. Undefined behaviour has no signature; this one does.

Measured, with a backlog of six stale samples: three survived. Exactly half, in SLOW/RECT, SLOW/TRI and EXPFAST. EXP escaped only because its outer per-bar loop re-runs the prune enough times to clear up after itself.

All five now walk the table backwards with a numeric for, so a removal only affects indices already passed. Every accumulation in those loops is a sum, so visiting in reverse changes nothing else.

The consequence in game was never a crash: self.Data only grows while a feed is live, and a leaked sample keeps contributing to the convolution — 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.

Every prune path was already covered and already green. The four existing specs each dropped exactly one sample, which cannot expose this — there is no following element to skip. Coverage said the lines ran; it could not say an assertion would notice the traversal going wrong. Four new specs drive the bulk case and go red without the fix.

Changed — four loops that needed an order were asking for one that is not promised

Four pairs loops iterate sequences where the order is the meaning, not a convenience:

  • AddPie and CompletePie walk the slice textures halving the piece size each step, so they are only correct largest-piece-first.
  • RefreshLineGraph chains a segment from each point to the next, for both line and filled series — the sequence order is the graph.

The Lua 5.1 manual is explicit: "The order in which the indices are enumerated is not specified, even for numeric indices. (To traverse a table in numeric order, use a numerical for or the ipairs function.)" That "even for numeric indices" is the sentence that removes the it-is-a-plain- array defence, and the parenthesis names the remedy. pairs walks an array part in order in practice, which is why this has always drawn correctly — the code was relying on an implementation detail to draw a graph. All four are now ipairs, which states the requirement.

New — a sparse data series is diagnosed at ingest instead of drawn short

AddDataSeries stores the caller's points table by reference and never checked it was dense, so a consumer building points with explicit indices, or from a filtered source that skips entries, could hand the library a table with a hole. Neither drawing behaviour was a diagnosis: the old unordered walk drew every point chained arbitrarily — a visible scribble — and the ordered walk draws a clean, plausible, shorter line, which is worse, because the graph looks correct and is missing data.

Both entry points now compare a counted walk against the length and warn, naming which call was given the bad table, before storing it — once per graph per entry point, not once per call. AddDataSeries is half the reset-then-re-add refresh idiom, so a chart redrawing on a timer would otherwise produce one chat line per refresh, forever, naming a function the player never called. It is deliberately not re-armed by ResetData, because that reset is the refresh cycle.

It warns rather than refuses because refusing would not fix anything. The rendering outcome for a sparse series is truncation either way — the ordered walk stops at the hole whatever AddDataSeries decided earlier. So the choice was never refuse-or-truncate, it was truncate loudly or truncate silently; a refusal would replace a quiet wrong picture with a hard error and still not draw the missing points.

Raised in review, which also corrected the spec that pinned truncation as the correct rendering of a sparse series. It is the chosen behaviour for malformed input, and the spec now says so.

The first version of this check was blind on the one form real consumers use, and that is the fourth time the same mistake has been found here. Both entry points accept two shapes: a list of {x, y} pairs, and two parallel arrays ({times, values}, the n2 form). On the n2 form the library rebuilds the series by walking points[1] with ipairs — so the table it hands on is dense by construction whatever went in, and the tail past a hole is dropped on that line. The check inspected the rebuilt table, so it could never fire there however sparse the caller's data was. Both entry points now check points[1] before the rebuild, and the message names which array it is.

This matters because n2 is not the exotic form. Reading the two live consumers on this machine: Details never calls AddDataSeries at all — its framework draws with DrawLine directly (Details/Libs/DF/panel.lua:2916) — and Recount passes n2 at all four of its call sites (GUI_Graph.lua:497, :571; GUI_CompareGraph.lua:313, :403). So the diagnostic was armed only on the shape nobody passes. Neither addon builds a sparse table — Recount's arrays are appended densely and decimated with table.remove in lockstep (GUI_Graph.lua:73-146, :148-192) — so this is a latent gap rather than an observed failure, and that is all it is claimed to be.

Four specs, none of which the suite had: every existing sparse spec drove the list-of-pairs form. Removing the fix turns the two warning specs red and leaves the other 260 green — including the one that pins the truncation itself, which is the cost the warning reports and is present either way.

Fixed — two defects in DrawBar, both reachable only through the public drawing primitive

DrawBar is reached two ways, and self means something different on each: internally it is called as self:DrawBar(self, …) where self is the graph, and publicly as lib:DrawBar(frame, …) — the documented primitive — where self is the library table shared by every graph in the install.

The redraw request went to the wrong object. When a bar's level would bury the graph's text, the library lifts the text frame and asks for a re-render. That request was addressed to self, so on the public path it set a field on the shared library table that nothing anywhere reads: the lift happened and the re-render never came. It now asks the frame being drawn into, which is the same object on both paths.

A recycled bar kept the parent of whoever used it last. The pool returns a bar with Hide() alone and never restores its parent, while the parent was only ever set when a level was given. So a levelled bar, once recycled, would be drawn by an unlevelled call at the level the previous caller happened to use — a wrong frame level chosen by pool order. Bars now return to the frame they were drawn into when no level is asked for.

Neither is reachable from inside the library, which always passes a level — so no existing graph renders differently. Both are pinned by specs that go red without the fix.

Changed — feeding a hidden realtime graph is now a documented hazard instead of an undocumented one

AddTimeData appends one table per sample and bounds nothing. The only code that ever removes an entry is the prune inside the graph's own per-frame update — and the client does not run OnUpdate on a hidden frame. So a consumer that keeps feeding a hidden realtime graph allocates for as long as the feed runs, with nothing able to reclaim it: the graph that would prune is the graph that is not ticking.

Recount, the realtime consumer this was checked against, is safe by its own discipline rather than by anything the library does — it unregisters its tracking callback when a realtime window closes, so the feed stops with the window. That is a consumer holding an invariant the library never stated.

No cap was added, for the same reason the SetBorderSize sign was left alone: a bound would silently change what every existing consumer's graph draws, and winning LibStub's race means they cannot decline that by shipping their own copy. The invariant is now written on the function, in the API documentation, and pinned by a spec that drives 500 samples through a graph that never updates and finds all 500 still there.

Distinct from, and adjacent to, the idle-prune behaviour raised in review: that one is bounded and self-healing — nothing new arrives, and the first sample after a silence prunes normally — so it is deliberately unchanged.

Changed — SetBorderSize's sign is now documented where a developer will actually hit it

SetBorderSize(edge, size) replaces the automatic 10% margin on one edge, and every edge is applied as extreme + size. The default it replaces is not symmetric — XMin and YMin subtract while XMax and YMax add — so a positive size widens the axis at the top and right and moves it inward, cropping the data, at the bottom and left. The same parameter means opposite things on opposite edges.

The behaviour is unchanged and will stay unchanged. It is upstream's, and the +2000 band is what makes leaving it correct rather than merely convenient: winning LibStub's race in every install removes every other consumer's ability to decline a behaviour change by shipping their own copy. A fork that wins every race owes the field bug-for-bug compatibility on everything except the bug it forked to fix.

What was wrong was that the rule was written down in the CurseForge store listing — read by players deciding whether to install, and by nobody integrating the library — while the function itself said nothing, and its usage assert named the four edges and was silent on sign. A developer who passes 10 for LEFT, sees their data cropped and goes looking would read the function, then the error, and find the explanation in neither. The rule is now in a comment on the function, and the assert says that LEFT and BOTTOM need a negative size.

New — tools/check-minor-band.lua, because the spec that "pinned" the band could never fail

The suite asserted that this copy's minor beats every other LibGraph-2.0 installed. It did that by comparing a constant against three hardcoded constants — the copies measured on 2026-08-25 — so it could not fail, could not notice anything, and would pass forever. Its name claimed to watch a live, changing fact; its assertion was a claim about one day.

That gap has teeth: if another addon's private fork is bumped above ours, LibStub hands it the load race, this library's body never runs, and nothing reports it — the losing copy bails silently at if not lib then return end. The suite would have stayed entirely green.

The spec is now named for what it actually witnesses (the decision, and the date it was measured), and the live question moved to a tool that reads the real AddOns tree, parses every copy's minor, and exits non-zero if anything ties or beats us. Run it before a release — that is when the answer matters and when somebody is present to act on it. It is deliberately not a spec: a suite that read the installed AddOns tree would fail on a machine without these addons and pass vacuously everywhere else.

Both raised in review.

Fixed — the pie-chart hover used the wrong scale, and nothing could have noticed

PieChart_OnUpdate converts the cursor from screen space with GetEffectiveScale, which is right — but every spec fed the cursor through a helper that multiplied by the same getter the code divides by. The term cancelled, so swapping it for GetScale, or dropping the conversion entirely, left every assertion passing while the cursor and the pie disagreed by the scale factor in game.

Two specs now put the scale on an ancestor, so GetScale and GetEffectiveScale genuinely differ, and supply it to the cursor as a literal. Verified by making the one-word change the review described: both go red, every other pie-chart assertion stays green.

Fixed — the pie chart's k == 7 was #PiePieces written as a literal

AddPie set a boundary-line fudge on the last slice by comparing the loop index against 7, which is the length of the slice table spelled out. Add a "1-256" slice — the obvious way to increase pie resolution — and it fires one iteration early; remove one and it never fires, so the boundary line is drawn slightly short on every full pie. Neither case errors and neither is visible in a percentage: the arithmetic stays self-consistent and only the picture is wrong. Now #PiePieces.

Raised in review as surviving the pairsipairs change untouched, which it did — ipairs yields the same integer keys, so the fix next door would have left it sitting there. A new spec drives AddPie and CompletePie over the same percentage and requires the same decomposition, because they are line-for-line the same loop and nothing asserted they stayed in step.

No behaviour change on any well-formed input, and the suite is identical either way, which is the point: nothing would have caught someone changing it back. One real difference, now pinned by a spec: a caller passing a sparse points table gets a line truncated at the hole rather than segments joined in arbitrary order. Truncated-but-coherent is the better failure.

The other eighteen pairs loops in the file were checked and left alone — they set a property on each element, or accumulate a sum, and order genuinely does not matter.

[v2.0.6] (2026-08-25) — One shared copy: the FastGuildInvite and Recount forks merged, and the Retail 12.0 crash fixed

LibGraph-2.0 had been carried around as a private copy inside every addon that used it, and the copies had drifted. This release is the point where they stop drifting: one repository, merged from the two forks that had real changes in them, published so consumers can embed the same file.

Fixed — 148 identical errors per session on Retail 12.0

GraphFunctions:PieChart_OnUpdate tested the mouse with the MouseIsOver global. Retail 12.0 no longer defines it, and this file caches its globals as upvalues at load time — so the upvalue was nil for the entire session and every frame the pie chart updated threw. One sitting produced 148 copies of the same error.

The call is now self:IsMouseOver(). That is not a workaround: the global was only ever a one-line forwarder to this widget method (Blizzard_UIParent/Shared/UIParent.lua:530), and the method is a SimpleScriptRegion member documented in Classic Era's own API tree as well as Retail's. It is correct on every flavour and needs no version guard, so the local MouseIsOver = MouseIsOver upvalue is gone rather than made conditional.

Fixed — DrawHLine drew its texture upside down

The FastGuildInvite copy had replaced the 8-parameter SetTexCoord(0,0, 0,1, 1,0, 1,1) in lib:DrawHLine with the 4-parameter SetTexCoord(0, 1, 1, 0), described in its own comment as the simpler equivalent. It is not equivalent. The 4-parameter argument order is (left, right, bottom, top) — per SimpleTextureBaseAPIDocumentation.lua:395 — so that call swaps the last two and draws sline vertically flipped.

It is now SetTexCoord(0, 1, 0, 1), which is the true 4-parameter equivalent of the 8-parameter identity mapping the original used. This keeps the simplification the fork was after without the flip. In fairness this may never have been visible: if sline.tga is symmetric top to bottom, both forms render identically. It was corrected because a behaviour difference nobody intended is worth removing whether or not anyone could see it.

New — graph.LabelHook

Set graph.LabelHook to a function and it is called with each axis-label FontString immediately after the label's text is set, before it is shown. It exists so a consumer can localise the library's internal axis labels — substituting a locale's own digits, or applying a font that can draw them — without the library needing to know anything about localisation.

graph.LabelHook = function(fontString)
    fontString:SetText(MyLocale:Digits(fontString:GetText()))
    fontString:SetFont(MyLocale:FontPath(), 10)
end

It is called at all five label sites: both Y-axis label passes, the X-axis bottom labels, and the two remaining axis labels. Leave it nil and nothing changes.

New — graph.XLabelsEnabled

Set graph.XLabelsEnabled to true and a line graph draws numeric labels along the bottom, under each X gridline, following the same secondary-gridline spacing the gridlines themselves 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.

Changed — the library version is bumped +2000 over stock, and +1000 was not enough

minor is 90000 + 2000 + <SVN revision> rather than stock's 90000 + <SVN revision>. Every consumer embeds its own copy of LibGraph-2.0 and LibStub loads whichever registers the highest minor, so without a bump an addon shipping a stock copy would win the load race and everything above would silently not exist.

The band is +2000 because +1000 tied, and a tie loses. LibStub:NewLibrary refuses an equal minor — if oldminor and oldminor >= minor then return nil end — so the second copy to load bails at if not lib then return end and the winner is decided by load order alone. Measured across one install:

Copy Registers Carries
Details 90062 stock r62, the MouseIsOver crash
Recount 90068 stock r68, the MouseIsOver crash
FastGuildInvite 91068 a private fork: the crash and the flipped DrawHLine
this copy, before 91068 both fixed — and tied

fastguildinvite sorts early in load order, so on an install carrying both, the crashing copy won and this one never registered. That is the exact outcome the bump exists to prevent, so the bands are now explicit: +0 stock, +1000 a private per-addon fork, +2000 this shared copy, which supersedes both. A spec asserts the minor beats each of the three measured above, and a second one pins the equal-minor-loses rule so nobody simplifies the band back.

Existing graphs are retrofitted, so this reaches players who never update the addon that embeds the old copy. When this file loads over a lower-minor copy it re-runs SetupGraph*Functions over everything in lib.RegisteredGraph*, repointing already-created graphs at the new implementations. Verified against Details' r62 copy, which carries the same registration tables and the same retrofit loop — so a graph built by a stock copy before this one loads still gets the Retail 12.0 fix.

Changed — packaged for release

The repository now carries a TOC for each live flavour (Classic Era 11509, TBC 20506, Wrath 30405, Cataclysm 40402, Mists 50504, and Retail 110207 / 120005 / 120007 / 120100), a .pkgmeta, and a BigWigs packager workflow that builds on a pushed tag. It also carries a LICENSE, which it did not before: the original work stays All Rights Reserved to Cryect and Xinhuan, maintenance from this release onward is MIT, and LibStub.lua remains public domain.

New — an offline test suite, and the two standing review files

None of this ships: .pkgmeta excludes Tests and docs, and everything else added here is a dotfile or CLAUDE.md, both of which the packager prunes on its own.

The repository now carries the shared WoWAPITesting harness as a git submodule at Tests/wowapi (pinned 1f8fe09), with six spec files covering the library end to end: the load-time contract and LibStub load race, the debugstack-derived texture directory, the data-series shape sniff, the axis and lock setters, LinearRegression, the rotated- texture drawing primitives and their object pools, gridlines and both label hooks, the line and scatter refresh paths, all five realtime convolution modes, the pie chart including its hover surface, and the shipped TestGraph2Lib demo. 242 examples, 0 failed.

Line coverage of LibGraph-2.0/LibGraph-2.0.lua is a measured 98.05% (1461/1490). The 29 uncovered lines are three upstream blocks with no production entry point — GraphFunctions:SetAutoscaleYAxis (its assignment is commented out), the non-realtime SetBarColors (shadowed by RealtimeSetColors), and TestRealtimeGraphRaw (nothing calls it). They are left in place because this file is kept close to upstream so the three-way diff against the forks stays readable, and docs/AUDIT.md records them rather than rounding the number up.

Run it from the repo root with a Lua 5.1 interpreter and nothing else:

lua Tests/wowapi/run.lua

Fixed — FindFontString raised on a graph that had not drawn gridlines yet

GraphFunctions:FindFontString iterates self.FontStrings, and nothing constructs that table — only HideFontStrings creates it. CreateGridlines calls HideFontStrings first, which is the only reason this has never been seen in game; a consumer calling the public FindFontString directly on a fresh line or scatter graph got bad argument #1 to 'pairs' (table expected, got nil). It now carries the same guard HideFontStrings has. Found by the new spec suite, not by a bug report.

Two append-only files come with it. docs/AUDIT.md is where a peer-review session writes findings against this library and where we answer them; Tests/HARNESS_CONTRACT.md is where we ask the harness for something it does not model yet. One request went in there immediately — StatusBar:SetOrientation, which Classic Era documents and the harness gives only to Slider, and without which no realtime graph can be constructed offline at all. It came back DELIVERED the same day but not yet pushed, so Tests/env_local.lua stays: it wraps CreateFrame and fills the two methods only where they are absent, so it yields on its own the moment the real implementation arrives in a pin we can move to.

Removed — a changelog paragraph that had got into LICENSE

Section 2's MIT grant carried a paragraph listing this release's changes. A licence states terms and scope; what changed belongs here. The grant is unaffected — its scope is still the section heading, "Modifications from 2026-08-25 onward".


Provenance: this is GraphLib / LibGraph-2.0 by Cryect and Xinhuan, SVN r68, as vendored in FastGuildInvite and Recount. Version 2.0.5 and earlier predate this repository and have no changelog entries here.

This mod has no additional files