Skip to content

Market charts: calendar, candles, fitted axes, live candles, axis tags (ADR 0085–0092) - #56

Merged
timzifer merged 42 commits into
mainfrom
feat/market-charts
Sep 29, 2026
Merged

timzifer merged 42 commits into
mainfrom
feat/market-charts

Conversation

@timzifer

@timzifer timzifer commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Market charts, from a catalogue (ADR 0085) to every mechanism it ranked, each recorded before it was built.

Records

ADR Decision Status
0085 What a market chart needs — catalogue and ranking Proposed (every rank landed)
0086 A calendar says when time counts; the axis folds the rest and starts its weeks where the calendar does Accepted, amended
0087 An orientation is the encoding read a quarter turn round Accepted
0088 A candle is one mark that reads four values and decides its own direction Accepted, amended
0089 A value axis can fit what the other axis shows; a stream can revise its last row Accepted, amended
0090 An axis describes the frame it is drawn in Accepted, amended
0091 A direction is a colour channel any mark can take Accepted, amended
0092 A value is tagged on its axis, a crosshair snaps to a row, and an indicator earns a function only where composing it goes wrong Accepted, amended
0093 Several symbols are a facet whose panels carry their own tracks, and a comparison divides each series by its value where the view begins Proposed — record only, not built in this PR

What it adds

  • stat: RSI (Wilder's) and VWAP (per session); trailing windows (TrailingMean, EMA, RollingStdDev, RollingMin/Max, Cumsum), OHLC, OHLCAt, BinWeighted, and Resampler for a tick feed. All Append forms allocation-free.
  • scale: Calendar (interface) with Workweek, Days, CalendarFunc, Always, day rules (Weekdays, Dates, EveryNth, Between, And/Or/Not) and wrappers (Closed, Special, StartWeek); Folds, Opens; WeekStart, TimeCalendar; FitView/LogFitView, Pinner, Retrainer, HighWater, ValueLabelOf.
  • geom: AxisTag/Tagger, LastValue; Candle with OHLC, Direction, Rising, Falling, Hollow, CandleStyle; Orient(Horizontal) for Bar, Histogram, Boxplot; DirectionBy; Training.Within.
  • interact: Index.NearestAlong; overlay: Crosshair.Snap, Crosshair.Tags.
  • data: Stream.ReplaceLast. ir: MarkerTriangleDown. figure: CandleTable.
  • render: a fitting axis trains on the rows in view; every render derives trained ranges again (pinned domains, category slots and series colours are kept); a day tick that lands in a fold moves to the next open.
  • Live: wheel, rubber band and pan leave a fitting axis alone.
  • Dialect: lastValue, rule, candlestick, open/high/low/close, direction/directionFrom, orientation on bar/histogram/boxplot, fit, highWater, weekStart.

Behaviour changes worth a look

  • ADR 0090: an unpinned axis no longer keeps every row it has ever been shown. A windowed stream's time axis now slides (as examples/stream always claimed), and a repeated Plot.Render after the data changed describes the new data. No golden file changed.
  • Week ticks were already Monday (Go's zero time); WeekStart makes it a choice.

Examples

  • go run ./examples/market — candles, an RSI track, a last-price tag, Bollinger band, EMA/SMA, buy/sell markers, volume by direction, volume profile, weekends and a holiday folded from one calendar.
  • go run ./examples/market -live — tick feed into minute candles, open candle revised in place, zoomed time axis with fitted price and volume axes, a last-price tag and a snapping crosshair.

Also in this branch

The first seven commits are docs/roadmap-and-v1 (ADR 0084 and the roadmap/features/milestones docs), which this branch was started from and which had not been pushed.

Checks

go vet ./... and go test ./... pass; data, stat and examples/stream also under -race.

timzifer and others added 30 commits September 23, 2026 08:58
Everything "What is next" listed — the tidy tree, probability paper, the
statistical instruments, the raster, the horizon chart, the bivariate
channel, the map — has shipped. The section now groups what exists by the
question each answers and keeps a short list of what is genuinely open:
v1.0, a mark that fills a closed ring, a conic projection, labels that
avoid other layers', a keyframe timeline. The status note at the top said
the seams were still being reshaped; they are not.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Text, ErrorBar, QQ, Graph, Contour, Raster and Horizon; axis breaks,
label formats and locales; second axes and tracks; label avoidance and
callouts; and nulls in text and time columns. Each shipped with its ADR
and none reached the one-list summary CONCEPT §14 now points to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The record stopped at the gantt chart while ternary charts, horizons,
rasters, the bivariate channel, probability paper, the tidy tree and the
instruments came before it and hatching through axis breaks came after.
Each entry is drawn from its ADR and its amendments, in the order they
were implemented.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CONCEPT §15 says v1.0.0 returns when the surface settles and nothing says
what settled means. The record makes it four stdlib-only checks and a
second audit: a test that applies the growth rule, with a point, an
interval or a rectangle counted as one value; a committed API baseline
CI diffs; a JSON corpus that is only ever added to; and the first
audit's verdicts extended over what arrived since. Four interfaces that
break the rule as clarified — stat.Family, coord.Exploder,
geom.LabelPlacer, scale.Breaker — change before the tag.

Proposed rather than accepted: nothing in it is implemented yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review of the proposal. The baseline check let every added line through
after v1, which is the one addition that breaks every outside
implementation; it now refuses a method added to an exported interface
and leaves a new field to the reviewer's zero-value question. The
exception to the growth rule is a value of fixed arity rather than a
geometric shape, which is what ColorAt's bivariate reading is. The test
is said to be an allowlist by name and to read every exported interface,
the corpus pins documents rather than pixels, and the Go floor is out of
scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Second review. A committed manifest compared with the code refuses
nothing, because a change updates both; compatibility is now checked
against the manifest at the latest release tag, so a declaration added
in a minor is protected from then on. The manifest records whether each
struct is comparable, since a slice field breaks == and map keys with no
change in behaviour; covers js/wasm, where backend/canvas and Live.Bind
exist; and unkeyed struct literals are stated outside the promise. The
release tooling's Major: 0 lock from ADR 0059 is lifted as a step of its
own rather than found at the tag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…view

The review reproduced two silent round-trip losses — a parallel coord
reading an undescribable dimension back as linear, and every size scale
reading back as the built-in area mapping — and found four things v1
would freeze by accident: a window's positional callbacks, registry names
that can collide with a later built-in, tuning budgets exported as
constants, and comparability on request types that exist to grow. Each
is recorded as a change the second audit starts from.

The checks are sharpened to match: the growth-rule test reads callbacks
and every module that tags v1; the compatibility reference is the latest
earlier release, never the tag under test, and v1.0.0 has none; a method
is refused only on an interface already published; and a declaration
gaining a platform is an addition.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MarkerTriangleDown is appended to the marker set, as its doc comment
requires, and mirrors the upward triangle through its centroid so a pair
drawn at one point occupies the same box turned over. The dialect spells
it "triangle-down", Vega-Lite's own name. The theme's redundant-encoding
ladder is left alone: it is a separate decision and every golden draws it.

Generated with AI

Co-Authored-By: AI <ai@example.com>
MovingAverage is centred, which is wrong at the right-hand edge of a live
chart: its last value moves when the next row arrives. TrailingMean, EMA,
RollingStdDev, RollingMin, RollingMax and Cumsum look only backwards, so
appending a row changes nothing already drawn, and a test says so.
RollingStdDev is the population form a Bollinger band is defined with,
summed twice per window for the cancellation StdDev avoids.

OHLC summarises ticks into candles of a fixed width from an origin and
leaves an interval nobody traded in without one. BinWeighted is Bin
summing weights, with Bin's own edges, so the buys and the sells of a
volume profile line up. Every Append form is allocation-free into a warm
slice.

Generated with AI

Co-Authored-By: AI <ai@example.com>
…arks

A Rect body over an ErrorBar wick, a Bollinger band as an Area, an EMA
and an SMA, crossings as upward and downward triangles, daily volume in a
bottom track sharing time, and the volume at each price in a right track
sharing price, on a time axis with the weekends folded. Each place the
recipe costs the caller a line — the direction column, the weekend spans,
the profile's hand-stacked rects — is a gap ADR 0085 ranks.

Generated with AI

Co-Authored-By: AI <ai@example.com>
A catalogue in the manner of 0058: the candle, the trading calendar, the
combined panels, indicators, annotations and interaction, each with what
exists, what is a recipe and what is missing, ranked by what a reader
loses. The trading calendar is first, which is 0083's deferred
business-day axis; the candle as one mark second. The Stat interface
stays refused, because an indicator is numbers in and numbers out drawn
by an ordinary mark. The easy wins landed with the record.

Generated with AI

Co-Authored-By: AI <ai@example.com>
A calendar is a value in scale — sessions per weekday in a location,
holidays and short days as the caller's lists — whose Folds(from, to)
the time axis takes like any others, so the dialect is unchanged. The
one amendment to 0083: a tick at a calendar step of a day or longer that
lands in a fold moves to the next open, because with the nights folded
every midnight is inside one and the axis has no dates. Opens(from, to)
gives stat.OHLCAt the session edges a fixed width cannot across DST.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Under Horizontal the slots are the Y column and the measurement the X
one, with X2 and Y2 swapping along. A mark reads its columns through
config.roles, trains and maps through config.axes, and turns once where
it emits with config.box and config.point, so the coord, explode,
extrude and hit-testing are unchanged. A dodged group lists its first
series on top. The dialect writes "orientation" on these marks, puts a
horizontal stack on the x channel, and reads Vega-Lite's "orient" on a
bar or a boxplot as the same thing.

Generated with AI

Co-Authored-By: AI <ai@example.com>
The volume profile in examples/market is now a stacked horizontal Bar
grouped by side, drawing the same picture as the hand-stacked rects it
replaces. ADR 0085's fourth rank is marked landed, and 0053's note that
bars and boxplots had no orientation points to 0087.

Generated with AI

Co-Authored-By: AI <ai@example.com>
A calendar is now an interface — Location, WeekStart, and the stretches
of one day that count — so a roster, a term list or a calendar computed
in another calendar system is a caller's type. Workweek is the
configurable value for the rule-shaped ones, with no default set of
days: Monday to Friday, Monday to Saturday and Sunday to Thursday are
all spelled out. The week start is the calendar's and the axis's, via
scale.WeekStart; week ticks are Monday today only because Go truncates
from its zero time, which was a Monday.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Workweek takes a DayRule — func(day time.Time) bool — and the hours of
an open day, Days takes each weekday's own hours, and CalendarFunc takes
the whole per-day question. The library ships the rules (Weekdays,
Dates, EveryNth, Between, And, Or, Not) and three wrappers for any
calendar (Closed, Special, StartWeek) instead of a builder on one type.
The rule is about a day rather than an instant because a predicate over
instants cannot give the exact edges a fold needs.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Calendar is an interface — Location, WeekStart, and Open for one day —
so a roster, a term list or a calendar computed in another calendar
system is a caller's type or a CalendarFunc. Workweek and Days build the
rule-shaped ones from DayRules the package ships (Weekdays, Dates,
EveryNth, Between, And, Or, Not), and Closed, Special and StartWeek wrap
any calendar. Folds turns the closed time between two instants into a
time axis's folds and Opens gives the start of every open stretch.

Spans are wall clock, read through time.Date, so an 09:30 open stays at
09:30 across a daylight saving change; an overnight span is split at
midnight. An invalid answer from a calendar is ErrCalendar.

Generated with AI

Co-Authored-By: AI <ai@example.com>
WeekStart sets the weekday a week tick falls on, walked by date rather
than truncated from Go's zero time, which is why every weekly axis was
Monday until now; Monday stays the default. TimeCalendar gives an axis
a calendar's week start and zone. The dialect writes "weekStart".

On a folded axis a tick at a day, week, month or year step that lands
inside a fold moves to the fold's upper edge and is labelled for where
it stands, so an intraday axis with its nights folded has dates. A tick
inside a break is still dropped.

Generated with AI

Co-Authored-By: AI <ai@example.com>
For periods that are not a fixed width — a session, a shift, a day
across a clock change. Each row finds its period by binary search, and
the Append form is allocation-free into a warm slice.

Generated with AI

Co-Authored-By: AI <ai@example.com>
examples/market takes its trading days and its folds from one calendar,
Monday to Friday with a holiday closed. ADR 0086 is Accepted, amended:
spans are wall clock, Dates reads a date as written, a closed day is
closed midnight to midnight, Opens merges across midnight, and a tick
moves out of a fold but not out of a break.

Generated with AI

Co-Authored-By: AI <ai@example.com>
One mark reading a position and the four values through one option,
geom.OHLC, deciding each row's direction itself (since its open, or
since the previous close), and drawing wick and body in four calls
whatever the row count. Direction is drawn twice — theme colours that
survive a colour-vision deficiency, and hollow or filled, on by default
under redundant encoding. The OHLC bar is a style of the same mark.
stat.Candle gains End and figure.CandleTable turns candles into the
table the mark reads. The dialect gains "candlestick" with open, high,
low and close channels rather than spending y and y2 on two of them.

Generated with AI

Co-Authored-By: AI <ai@example.com>
stat.Candle gains End — the next period's start for OHLC and OHLCAt,
and the last row's time for OHLCAt's still-open last candle.
figure.CandleTable turns candles into the table geom.Candle reads.

Generated with AI

Co-Authored-By: AI <ai@example.com>
geom.Candle takes a position and geom.OHLC(open, high, low, close), and
draws wick and body as one mark: wicks of a colour one stroked path,
bodies one filled path, so a thousand candles are four calls. A row
rises when its close is at or above its open, or with
Direction(SincePrevious) its previous close; a doji is at least a pixel
tall. Rising and Falling set each direction's colour and label, with
theme defaults of blue and vermilion; Hollow outlines rising bodies and
is on under redundant encoding; CandleStyle(Ticks) is the OHLC bar.
Inconsistent data is drawn as given and the row is reported at its
close. The dialect writes "candlestick" with open, high, low and close
channels.

A tick at the lower edge of a fold now moves with ticks inside it, so a
day tick at a weekend's Saturday midnight is labelled Monday.

Generated with AI

Co-Authored-By: AI <ai@example.com>
examples/market replaces its Rect-and-ErrorBar recipe with one Candle
layer. ADR 0088 is Accepted, amended; 0086 records the fold-edge tick,
0087 lists Candle under marks never oriented, and 0085's second rank is
marked landed.

Generated with AI

Co-Authored-By: AI <ai@example.com>
scale.FitView makes a value axis fit the rows the panel's pinned X axis
shows: render hands each layer that interval as Training.Within in the
one training pass, point, interval and connected marks restrict their
value training to it, and a layer that cannot fits loosely rather than
clipping. Live's wheel, rubber band and pan leave a fitting axis alone.
data.Stream.ReplaceLast revises the newest row only, and stat.Resampler
turns OHLC inside out for a feed that arrives a tick at a time.

Generated with AI

Co-Authored-By: AI <ai@example.com>
FitView and LogFitView declare that an axis is trained on the rows its
panel's other axis shows. Fitter reports it, and Pinner — an interface
of its own, since Zoomer is published — reports whether a domain is
pinned, which is when a fit applies. The dialect writes "fit": "view".

Generated with AI

Co-Authored-By: AI <ai@example.com>
Render passes a pinned X domain as Training.Within to every layer whose
Y fits, and resets each fitting axis once per frame first, because a
trained domain only widens. Points train on rows in view, marks with
width on rows whose span overlaps it, connected paths also on the value
where they cross each edge, and stacks on a view padded by their reach.
A layer that does not read Within trains on every row, fitting loosely
rather than clipping. Live's wheel, rubber band and pan leave a fitting
axis alone, and a View does not record one.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Stream.ReplaceLast overwrites the newest row under Append's lock, the
one revision a live source makes. stat.Resampler is OHLC for a feed
that arrives a tick at a time: Add returns the candle as it stands and
whether it is new, over a width or a calendar's edges, and refuses a
tick for a candle that has closed.

Generated with AI

Co-Authored-By: AI <ai@example.com>
-live folds a seeded tick feed into one-minute candles, revises the
open one in place, and draws them on a fitted price axis and a fitted
volume track; the reader wheels in at the midpoint and the host keeps
the view on the newest candle. ADR 0089 is Accepted, amended: a fitting
axis is reset every frame, stacks fit a padded view, and following the
data stays the host's one line.

Generated with AI

Co-Authored-By: AI <ai@example.com>
A probe shows a windowed stream's axes never shrink: a trained domain
only widens and render keeps its scales between renders, so the time
axis keeps the history the window dropped and examples/stream's
sliding window never slid. Every render now forgets each trained range
once before its layers train. A pinned domain is kept, so a zoom
survives; a set of discovered names — a category's slot, a series'
colour — is remembered as identity; continuous colour and size ramps
follow the range rule; scale.HighWater opts back into accumulation.

Generated with AI

Co-Authored-By: AI <ai@example.com>
timzifer and others added 11 commits September 28, 2026 10:38
Retrain drops what a scale was trained on and keeps its pin, options
and range — on linear, log, symlog, time and probability axes, the
continuous and classed colour scales, both bivariate scales and the
size scale. A pinned scale ignores it. Ordinal axes and discrete colour
scales do not implement it: their names are identity. HighWater opts a
linear axis back into keeping its extent across renders, written
"highWater": true.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Before any layer trains, render retrains each panel's axes and the
colour and size scales its layers describe. A trained domain only
widens and a plot keeps its scales between renders, so a windowed
stream's time axis kept the history the window had dropped and its
value axis kept a peak long gone. A zoom is a pinned domain and
survives; categories keep their slots and series their colours. This
replaces ADR 0089's refit.

Generated with AI

Co-Authored-By: AI <ai@example.com>
examples/stream builds its chart in a function its new test shares,
and the test checks deterministically that a full window's time axis
starts where the window does. ADR 0090 is Accepted, amended.

Generated with AI

Co-Authored-By: AI <ai@example.com>
geom.DirectionBy(from, to) colours each row by whether to is at or
above from — or above the previous row's to — through the channel
ColorBy feeds, so every mark with a colour column takes it and the two
are exclusive. It paints in the candle's Rising and Falling colours
and labels, so a volume track agrees with its candles, works on a
numeric stream, and the legend's merge by label lists the directions
once. The dialect gains direction and directionFrom channels.
A row rises when its to column is at or above its from column, or —
from empty — above the previous row's to; a tie rises. The direction
is a discrete colour scale of two labels installed on the layer, so
bars batch by it, paths colour in stretches, the legend names rising
and falling, and a row missing a value is not drawn, with no mark
knowing what a direction is. Colours and labels are the candle's
Rising and Falling, or the theme's, bound by render's retrain pass.
ColorBy and DirectionBy on one layer is ErrColorAndDirection. The
dialect writes direction and directionFrom channels.
examples/market drops its hand-computed dir column and its Named
scale, and the live chart's volume track is coloured by direction
straight from the stream's numeric open and close. ADR 0091 is
Accepted, amended.
The last price and a crosshair readout are one mechanism: a value
written on its axis, which nothing drew outside a panel before. An
AxisTag is furniture a layer (geom.Tagger) or an overlay asks for,
drawn in the gutter over the tick labels it covers; geom.LastValue asks
for one at the newest row. Crosshair snaps to the row nearest the
pointer along X through Index.NearestAlong and tags its position. RSI
and VWAP become stat functions because composing them gets Wilder's
smoothing and the session reset wrong; MACD stays a recipe.
RSI uses Wilder's averages — seeded with the simple mean of the first
window of changes, then moved 1/window of the way — which an EMA
composition does not; it is checked against the seed's own arithmetic
and StockCharts' published example to its rounding. VWAP resets its
sums at every session edge, which a Cumsum composition forgets. MACD
stays a recipe: composing EMAs is its definition.
A tick label has the tick step's precision, so a last price of 94.37
on an axis ticked in whole units would read 94, and a time axis could
not write an arbitrary instant at all. ValueLabelOf gives a linear
value as many decimals as it needs, up to two past the ticks, and a
time value its date, with the time of day when it is not a midnight.
An AxisTag is a value written on an axis in its gutter. Layers ask for
tags through the optional geom.Tagger; render places them after ranging
a panel, leaves out the tick labels they cover and draws them after the
data, pushing tags on one axis apart. geom.LastValue draws a rule and a
tag at the newest row, coloured by Color or DirectionBy, and trains its
axis on that value only. Index.NearestAlong finds the row nearest a
position along one axis; Crosshair.Snap uses it to cross at the candle
under the pointer's time, and Crosshair.Tags writes its position on the
axes through render.DrawAxisTags. The dialect writes lastValue and rule.
examples/market gains an RSI track and a last-price tag; -live tags the
open candle's price and draws a crosshair snapping to the candle under
a resting pointer into the file it writes. chart-types.md carries the
RSI, MACD, VWAP, last-price and crosshair recipes. ADR 0092 is
Accepted, amended.
@timzifer timzifer changed the title Market charts: calendar, candles, fitted axes, live candles (ADR 0085–0091) Market charts: calendar, candles, fitted axes, live candles, axis tags (ADR 0085–0092) Sep 29, 2026
A watchlist is a facet by symbol whose panels each carry the plot's
tracks — repeated per panel and subset by the facet like its layers,
free where the panel is free — laid out as a block per facet cell with
a fixed panel order. ErrTrackWithFacet, which refused a band spanning
a facet, is retired; the spanning band stays unbuilt. A comparison is
geom.Relative: each series divided by its first value in view, so the
zero follows the view, on a percent axis whose NumberFormat gains a +
flag.
@timzifer
timzifer merged commit 6aeb6b1 into main Sep 29, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant