diff --git a/backend/gg/cmd/gallery/main.go b/backend/gg/cmd/gallery/main.go index 1d5da0d..b29ec69 100644 --- a/backend/gg/cmd/gallery/main.go +++ b/backend/gg/cmd/gallery/main.go @@ -1043,7 +1043,7 @@ func figures() []plate { add("saturation", func(x float64) float64 { return 0.45 + 0.3*math.Sin(x/3) }, 3) }, }, - }, append(hatchFigures(), axisBreakFigures()...)...) + }, slices.Concat(hatchFigures(), axisBreakFigures(), marketFigures())...) } // budgets is five teams: what each spent as a share of the total, where its diff --git a/backend/gg/cmd/gallery/market.go b/backend/gg/cmd/gallery/market.go new file mode 100644 index 0000000..c5fc26e --- /dev/null +++ b/backend/gg/cmd/gallery/market.go @@ -0,0 +1,374 @@ +package main + +import ( + "io" + "math" + "math/rand/v2" + "time" + + "github.com/timzifer/figure" + "github.com/timzifer/figure/data" + "github.com/timzifer/figure/geom" + "github.com/timzifer/figure/ir" + "github.com/timzifer/figure/palette" + "github.com/timzifer/figure/scale" + "github.com/timzifer/figure/stat" + "github.com/timzifer/figure/theme" +) + +// marketFigures are the charts a trading screen draws, from one simulated +// market: the daily chart with everything on it, the same prices as OHLC bars +// and as hollow candles, and a frame of the live chart with its crosshair. +// They are the gallery's copy of examples/market, which is the one to read for +// how the parts fit; see docs/adr/0085-what-a-market-chart-needs.md for what +// they are. +func marketFigures() []plate { + return []plate{ + marketFigure(), + marketBarsFigure(), + marketHollowFigure(), + marketLiveFigure(), + } +} + +// Rising and falling are blue and vermilion rather than green and red, the +// pair the most common colour deficiency cannot tell apart. +var ( + marketUp = palette.Blue + marketDown = palette.Vermilion +) + +// marketStart is the chart's first calendar day, a Monday, and marketDays how +// many calendar days it runs: twelve weeks. +var marketStart = time.Date(2026, time.June, 1, 0, 0, 0, 0, time.UTC) + +const marketDays = 7 * 12 + +var marketEnd = marketStart.AddDate(0, 0, marketDays) + +// marketCalendar is Monday to Friday without the Friday Independence Day is +// observed on in 2026. +var marketCalendar = scale.Closed( + scale.Workweek(time.UTC, scale.Weekdays(time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday)), + scale.Dates(time.Date(2026, time.July, 3, 0, 0, 0, 0, time.UTC))) + +// ticks is the simulated trading: a seeded random walk between 09:30 and 16:00 +// on the days the calendar opens, with each tick's volume split into what was +// bought and what was sold. +type ticks struct { + ts, ps, vs []float64 + buys, sells []float64 +} + +func simulateMarket() ticks { + r := rand.New(rand.NewPCG(7, 85)) + var m ticks + price := 100.0 + for d := range marketDays { + day := marketStart.AddDate(0, 0, d) + if len(marketCalendar.Open(nil, day)) == 0 { + continue + } + // A drift that changes sign every few weeks, so the averages cross. + drift := 0.04 * math.Sin(float64(d)/9) + for tick := range 40 { + at := day.Add(9*time.Hour + 30*time.Minute + time.Duration(tick)*585*time.Second) + price *= 1 + drift/40 + r.NormFloat64()*0.004 + v := 100 + r.Float64()*900 + bought := min(max(v*(0.5+drift*4+r.NormFloat64()*0.1), 0), v) + m.ts = append(m.ts, scale.Nanos(at)) + m.ps = append(m.ps, price) + m.vs = append(m.vs, v) + m.buys = append(m.buys, bought) + m.sells = append(m.sells, v-bought) + } + } + return m +} + +func (m ticks) days() []stat.Candle { + return stat.OHLC(m.ts, m.ps, m.vs, scale.Nanos(marketStart), float64(24*time.Hour)) +} + +// dailyTable is the day candles with the indicators beside them. A candle sits +// at the middle of its day so that its slot is the day rather than straddling +// midnight into a fold, and the indicators run over the row rather than the +// time, because a weekend is not two days of average. +func dailyTable(days []stat.Candle) *data.Table { + n := len(days) + t := make([]time.Time, n) + xs := make([]float64, n) + o, h, l, c, v := make([]float64, n), make([]float64, n), make([]float64, n), make([]float64, n), make([]float64, n) + for i, d := range days { + t[i] = scale.FromNanos(d.Start + float64(12*time.Hour)) + xs[i] = float64(i) + o[i], h[i], l[i], c[i], v[i] = d.Open, d.High, d.Low, d.Close, d.Volume + } + ema := pointYs(stat.EMA(xs, c, 9)) + mid := pointYs(stat.TrailingMean(xs, c, 20)) + sd := pointYs(stat.RollingStdDev(xs, c, 20)) + rsi := pointYs(stat.RSI(xs, c, 14)) + lower, upper := make([]float64, n), make([]float64, n) + for i := range n { + lower[i], upper[i] = mid[i]-2*sd[i], mid[i]+2*sd[i] + } + return figure.NewTable(). + Time("t", t). + Float64("open", o).Float64("high", h).Float64("low", l).Float64("close", c). + Float64("volume", v). + Float64("ema", ema).Float64("mid", mid).Float64("rsi", rsi). + Float64("lower", lower).Float64("upper", upper) +} + +// crossings is where the EMA crosses the simple mean, after the mean's first +// window: a buy under the day's low, a sell over its high. +func crossings(days []stat.Candle) (buys, sells *data.Table) { + xs := make([]float64, len(days)) + closes := make([]float64, len(days)) + for i, d := range days { + xs[i], closes[i] = float64(i), d.Close + } + ema := pointYs(stat.EMA(xs, closes, 9)) + mid := pointYs(stat.TrailingMean(xs, closes, 20)) + var bt, st []time.Time + var ba, sa []float64 + for i := 20; i < len(days); i++ { + at := scale.FromNanos(days[i].Start + float64(12*time.Hour)) + pad := (days[i].High - days[i].Low) * 0.4 + switch { + case ema[i-1] <= mid[i-1] && ema[i] > mid[i]: + bt, ba = append(bt, at), append(ba, days[i].Low-pad) + case ema[i-1] >= mid[i-1] && ema[i] < mid[i]: + st, sa = append(st, at), append(sa, days[i].High+pad) + } + } + return figure.NewTable().Time("t", bt).Float64("at", ba), + figure.NewTable().Time("t", st).Float64("at", sa) +} + +// volumeAtPrice is the volume bought and sold in each price bucket, one row per +// bucket and side, binned over one interval so the two sides line up. +func volumeAtPrice(m ticks) *data.Table { + lo, hi := math.Inf(1), math.Inf(-1) + for _, p := range m.ps { + lo, hi = min(lo, p), max(hi, p) + } + b := stat.BinWeighted(m.ps, m.buys, lo, hi, 28) + s := stat.BinWeighted(m.ps, m.sells, lo, hi, 28) + var price, volume []float64 + var side []string + for i := range b { + price = append(price, b[i].Mid(), s[i].Mid()) + volume = append(volume, b[i].Sum, s[i].Sum) + side = append(side, "buy", "sell") + } + return figure.NewTable().Float64("price", price).Float64("volume", volume).String("side", side) +} + +func pointYs(ps []stat.Point) []float64 { + out := make([]float64, len(ps)) + for i, p := range ps { + out[i] = p.Y + } + return out +} + +// foldedTime is a time axis on the market's calendar with the closed days +// between from and marketEnd folded out. +func foldedTime(from time.Time) scale.Scale { + closed, err := scale.Folds(marketCalendar, from, marketEnd) + if err != nil { + panic(err) + } + return scale.Time(scale.TimeCalendar(marketCalendar), scale.TimeFold(closed...)) +} + +// marketFigure is examples/market: candles on a calendar with the weekends and +// the holiday folded, a Bollinger band and two moving averages, their crossings +// as buys and sells, volume and RSI in tracks under the price, the last close +// tagged on the axis, and the volume at each price in a track beside it. +func marketFigure() plate { + return plate{ + name: "market", width: 1000, high: 660, theme: theme.Light, + title: "FIG — daily, weekends and holidays folded", + build: func(p *figure.Plot) { + m := simulateMarket() + days := m.days() + candles := dailyTable(days) + p.X(foldedTime(marketStart)) + p.Y(scale.Linear(scale.Nice())) + + p.Add(geom.Area(candles, geom.X("t"), geom.Y("lower"), geom.Y2("upper"), + geom.Color(palette.Gray), geom.Opacity(0.18), geom.Label("Bollinger 20, 2σ"))) + p.Add(geom.Candle(candles, geom.X("t"), geom.OHLC("open", "high", "low", "close"), + geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"))) + p.Add(geom.Line(candles, geom.X("t"), geom.Y("ema"), geom.Color(palette.Orange), geom.Label("EMA 9"))) + p.Add(geom.Line(candles, geom.X("t"), geom.Y("mid"), geom.Color(palette.Purple), geom.Label("SMA 20"))) + + buys, sells := crossings(days) + p.Add(geom.Scatter(buys, geom.X("t"), geom.Y("at"), + geom.Shape(ir.MarkerTriangle), geom.Size(10), geom.Color(marketUp), geom.Label("buy"))) + p.Add(geom.Scatter(sells, geom.X("t"), geom.Y("at"), + geom.Shape(ir.MarkerTriangleDown), geom.Size(10), geom.Color(marketDown), geom.Label("sell"))) + + p.Track(figure.Bottom, figure.TrackFraction(0.2), figure.TrackScale(scale.Linear(scale.Zero())), figure.TrackAxis(true)). + Add(geom.Bar(candles, geom.X("t"), geom.Y("volume"), geom.BarWidth(0.7), + geom.DirectionBy("open", "close"), geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"), + geom.Opacity(0.6))) + p.Track(figure.Bottom, figure.TrackFraction(0.16), figure.TrackScale(scale.Linear(scale.Domain(0, 100))), figure.TrackAxis(true)). + Add(geom.HBand(30, 70, geom.Label("RSI 30–70")), + geom.Line(candles, geom.X("t"), geom.Y("rsi"), geom.Color(palette.Purple), geom.Label("RSI 14"))) + + p.Add(geom.LastValue(candles, geom.X("t"), geom.Y("close"), geom.Dash(3, 3), + geom.DirectionBy("open", "close"), geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"))) + + p.Track(figure.Right, figure.TrackSize(140), figure.TrackScale(scale.Linear(scale.Zero())), figure.TrackAxis(true)). + Add(geom.Bar(volumeAtPrice(m), geom.Y("price"), geom.X("volume"), + geom.Orient(geom.Horizontal), geom.BarWidth(1), geom.GroupBy("side"), + geom.ColorBy("side", scale.Named(map[string]ir.Color{"buy": marketUp, "sell": marketDown})), + geom.Opacity(0.7))) + }, + } +} + +// lastWeeks is the daily table cut to the last n trading days, and the first +// of them: close enough for a candle's shape to read. +func lastWeeks(n int) (*data.Table, time.Time) { + days := simulateMarket().days() + days = days[len(days)-n:] + return dailyTable(days), scale.FromNanos(days[0].Start) +} + +// marketBarsFigure is the same prices as OHLC bars — a rule from low to high, +// the open ticked to the left and the close to the right — coloured by the +// close against the previous close rather than against the day's own open. +func marketBarsFigure() plate { + return plate{ + name: "market-bars", width: 620, high: 400, theme: theme.Light, + title: "geom.CandleStyle(geom.Ticks), since the previous close", + opts: []figure.Option{figure.Legend(false)}, + build: func(p *figure.Plot) { + candles, from := lastWeeks(30) + p.X(foldedTime(from)) + p.Y(scale.Linear(scale.Nice())) + p.Add(geom.Candle(candles, geom.X("t"), geom.OHLC("open", "high", "low", "close"), + geom.CandleStyle(geom.Ticks), geom.Direction(geom.SincePrevious), + geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"))) + }, + } +} + +// marketHollowFigure is the same thirty days as candles under redundant +// encoding, which draws the rising bodies as outlines: the print convention, +// readable without the colour. +func marketHollowFigure() plate { + return plate{ + name: "market-hollow", width: 620, high: 400, theme: theme.Light.With(theme.Redundant(true)), + title: "Hollow candles under theme.Redundant(true)", + opts: []figure.Option{figure.Legend(false)}, + build: func(p *figure.Plot) { + candles, from := lastWeeks(30) + p.X(foldedTime(from)) + p.Y(scale.Linear(scale.Nice())) + p.Add(geom.Candle(candles, geom.X("t"), geom.OHLC("open", "high", "low", "close"), + geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"))) + }, + } +} + +// marketLiveFigure is the frame a live chart ends on: a tick feed folded into +// minute candles, zoomed into the last half hour, the price axis fitted to +// what is in view, and a crosshair snapped to a candle with its values tagged +// on both axes. See docs/adr/0089 and docs/adr/0092. +func marketLiveFigure() plate { + return plate{ + name: "market-live", width: 900, high: 420, theme: theme.Light, + title: "FIG — live, one-minute candles", + scene: func(f plate) chart { return liveMarket{f} }, + } +} + +// liveMarket plays the feed into a Live drawn to nowhere and then draws the +// frame it ended on once more into the target, which is what a host exporting +// an interactive chart does. +type liveMarket struct{ f plate } + +var liveOpen = time.Date(2026, time.September, 28, 9, 30, 0, 0, time.UTC) + +func (c liveMarket) Render(target figure.Target) error { + const feed = 7200 + st := data.NewStream("start", "end", "open", "high", "low", "close", "volume").Window(120) + p := figure.New(figure.Theme(c.f.theme), figure.Size(c.f.width, c.f.high), figure.Title(c.f.title)) + x := scale.Time() + p.X(x).Y(scale.Linear(scale.Nice(), scale.FitView())) + p.Add(geom.Candle(st.Source(), geom.X("start"), geom.X2("end"), + geom.OHLC("open", "high", "low", "close"), + geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"))) + p.Track(figure.Bottom, figure.TrackFraction(0.2), + figure.TrackScale(scale.Linear(scale.Zero(), scale.FitView())), figure.TrackAxis(true)). + Add(geom.Bar(st.Source(), geom.X("start"), geom.Y("volume"), geom.BarWidth(0.7), geom.Opacity(0.6), + geom.DirectionBy("open", "close"), geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"))) + p.Add(geom.LastValue(st.Source(), geom.X("start"), geom.Y("close"), geom.Dash(3, 3), + geom.DirectionBy("open", "close"), geom.Rising(marketUp, "up"), geom.Falling(marketDown, "down"))) + + live, err := p.Live(figure.SVGWriter(io.Discard)) + if err != nil { + return err + } + defer live.Close() + + r := stat.Resampler{Origin: scale.Nanos(liveOpen), Width: float64(time.Minute)} + rng := rand.New(rand.NewPCG(3, 89)) + price := 100.0 + row := make([]float64, 7) + zoomed, width := false, 0.0 + var at ir.Point + for i := range feed { + t := liveOpen.Add(time.Duration(i) * 2 * time.Second) + price *= 1 + 0.0001*math.Sin(float64(i)/400) + rng.NormFloat64()*0.0008 + cd, fresh, err := r.Add(scale.Nanos(t), price, 1+rng.Float64()*9) + if err != nil { + return err + } + row[0], row[1], row[2], row[3], row[4], row[5], row[6] = cd.Start, cd.End, cd.Open, cd.High, cd.Low, cd.Close, cd.Volume + if fresh { + err = st.Append(row...) + } else { + err = st.ReplaceLast(row...) + } + if err != nil { + return err + } + if i%10 != 9 { + continue + } + st.Snapshot() + if zoomed { + x.(scale.Zoomer).SetDomain(cd.End-width, cd.End) + } + if err := live.Draw(); err != nil { + return err + } + if !zoomed && i >= feed/2 { + area := live.Index().Panels()[0].Area + if err := live.Wheel(float64(area.Max.X)-2, float64(area.Min.Y+area.Max.Y)/2, 0.25); err != nil { + return err + } + lo, hi := x.Domain() + width, zoomed = hi-lo, true + at = ir.Point{X: area.Min.X + (area.Max.X-area.Min.X)*2/3, Y: (area.Min.Y + area.Max.Y) / 2} + } + } + st.Snapshot() + + final, err := p.Live(target) + if err != nil { + return err + } + final.TrackRows(true) + final.Overlay(&figure.Crosshair{Snap: final.Index(), Tags: true, Show: true, Panel: -1, At: at}) + if err := final.Draw(); err != nil { + return err + } + return final.Close() +} diff --git a/docs/gallery.md b/docs/gallery.md index ff03794..b94ec68 100644 --- a/docs/gallery.md +++ b/docs/gallery.md @@ -1,6 +1,7 @@ # The gallery -Every figure the library draws, rendered from the code that draws it. +Every figure the library draws, rendered from the code that draws it, in +eight groups, 63 figures in all. Each picture below opens its group. Every figure here is rendered by [`backend/gg/cmd/gallery`](../backend/gg/cmd/gallery) and re-checked in CI, so a @@ -8,35 +9,10 @@ picture here cannot drift away from the code that produced it. | | | |---|---| -| ![Three series with a legend](images/series.png) | ![Two groups of scattered points](images/scatter.png) | -| ![A response time histogram](images/bars.png) | ![A damped sine on a time axis](images/signal.png) | -| ![An estimate with a shaded interval](images/area.png) | ![A step chart of replica counts](images/steps.png) | -| ![Bars by region, coloured by value with a colourbar](images/categories.png) | ![Latency distributions as boxplots](images/boxplot.png) | -| ![Two growth curves on a log axis](images/logscale.png) | ![A series read against thresholds and a shaded window](images/annotations.png) | -| ![Throughput faceted into one panel per region](images/facets.png) | ![Four subplots on one dark canvas](images/subplots.png) | -| ![A quarter of a million samples drawn as a clean line](images/decimation.png) | ![A million points drawn as a density raster](images/density.png) | -| ![Standard error curves labelled with typeset notation](images/notation.png) | ![Revenue stacked by product, one layer over a long table](images/stacked.png) | -| ![Traffic by channel as a streamgraph](images/stream.png) | ![Calls per hour as a heatmap of coloured cells](images/heatmap.png) | -| ![Browser share as a donut](images/pie.png) | ![Two designs compared on five axes as a radar chart](images/radar.png) | -| ![Spend by team as a donut whose slices reach as far as each team used of its budget, with the team that went over broken out of the ring](images/donut.png) | ![Request latency as a histogram](images/histogram.png) | -| ![Latency by service as violins, one per region within each service](images/violin.png) | ![A year of daily maxima as a ridgeline, one density per month](images/ridgeline.png) | -| ![Scores by cohort as a beeswarm, every observation placed](images/beeswarm.png) | ![Scores by cohort as three empirical CDFs on one axis](images/ecdf.png) | -| ![Fifty thousand observations binned into hexagons with a loess trend through them](images/hexbin.png) | ![Income against life expectancy as bubbles sized by population, with a size key beside the legend](images/bubbles.png) | -| ![Mean latency per service with a 95 % interval drawn over each bar](images/errorbars.png) | ![Revenue as bars against a left axis and margin as a percentage line against a right one](images/twoaxes.png) | -| ![An oven temperature curve read against elapsed minutes along the bottom and cycle number along the top](images/twoextents.png) | ![A patch antenna's reflection swept across its band, on a Smith chart](images/smith.png) | -| ![Disk usage by directory as a treemap, one rectangle per file sized by its share](images/treemap.png) | ![The same directory tree as a sunburst, the root at the middle and the files at the rim](images/sunburst.png) | -| ![Requests per second through a service, drawn as a sankey diagram](images/sankey.png) | ![The same traffic as an arc diagram, each service a segment of the rail and each route a band arcing over it](images/arc.png) | -| ![The same traffic again as a chord diagram, each service an arc and each route a ribbon crossing the disc](images/chord.png) | ![A response surface drawn from three cameras at once: a three-quarter view, a plan and a front elevation of one scene](images/surface.png) | -| ![Thirty sweeps of a spectrum offset by sweep number, with a carrier drifting upward and a harmonic growing behind it](images/cascade.png) | ![An open loop at two gains on a Nichols diagram, drawn over the contours of constant closed-loop gain and phase](images/nichols.png) | -| ![A rebuild plan as a gantt chart: bars part filled by how far each task has got, arrows between them with the critical path in orange, milestone diamonds and a dashed line at today](images/gantt.png) | ![The same stacked revenue chart under redundant encoding: a different hatch pattern over each product, so the stack reads without colour](images/hatched.png) | -| ![The same stacked revenue chart with the colour taken out: three products as three greys, two of them almost the same one](images/greyscale-plain.png) | ![The same greyscale chart with redundant encoding on: the greys are unchanged and each product carries a pattern, so the stack reads again](images/greyscale-hatched.png) | -| ![Traffic by channel as stacked areas, each band hatched and its crest marked with points](images/hatched-area.png) | ![The fourteen hatch patterns as fourteen labelled samples: lines, dots, wavering lines and tilings](images/hatch-kinds.png) | -| ![Customers by which products they subscribe to, as an UpSet plot: a bar per combination over a matrix of dots saying which products that combination is](images/upset.png) | ![The same three products as a Venn diagram, each region carrying the customers who have exactly those products](images/venn.png) | -| ![Sixty cars as a parallel-coordinates plot: four vertical axes in four different units, one line per car crossing all of them, coloured by origin](images/parallel.png) | ![A quarter of support tickets as a parallel-sets diagram: three columns of category boxes for channel, urgency and outcome, with ribbons between them as thick as the tickets that answer both that way, split into the solved and escalated share](images/parallelsets.png) | -| ![Who worked with whom over a quarter, as a node-link diagram: three teams as three clusters, one person joining all of them, and a pair working on their own off to the side](images/network.png) | ![Last month's rainfall over twenty gauges, as the panel divided into the part nearest each gauge and filled with that gauge's reading, wettest in the north-west](images/voronoi.png) | -| ![Mean July cloud cover over the whole world on an equal-area projection: the globe as an ellipse, one coloured cell per ten degrees, cloudy along the equator and over the midlatitude oceans and clear through the subtropics](images/map.png) | ![Edinburgh to Tokyo on a globe seen from over the pole: the great circle running close to the ice and the rhumb line bowing far south of it, with the graticule and the rim of the visible hemisphere](images/globe.png) | -| ![Requests per second by site as bars on an axis broken between 12 and 85, marked with a double slash, so the one site thirty times the rest and the five small ones all read](images/axis-break.png) | ![The same bars with the break marked by two parallel zigzags across the panel, the tall bar cut where they run](images/axis-break-zigzag.png) | -| ![A machine's state log over two shifts with every idle period folded off the time axis, each fold marked by a small slash on the axis line](images/machine-folds.png) | | +| [![Three series with a legend](images/series.png)](gallery/basics.md)
**[Lines, points and bars](gallery/basics.md)** · 17 figures
The everyday forms: series over a continuous or time axis, scatter, bars, areas and steps, with intervals, thresholds, second axes and typeset labels. | [![Latency by service as violins, one per region within each service](images/violin.png)](gallery/distributions.md)
**[Distributions](gallery/distributions.md)** · 8 figures
How a sample is spread: binned, summarised, smoothed, or every observation placed. | +| [![Disk usage by directory as a treemap, one rectangle per file sized by its share](images/treemap.png)](gallery/parts.md)
**[Parts of a whole](gallery/parts.md)** · 6 figures
One total split into its shares, flat, stacked over time or nested. | [![The same traffic again as a chord diagram, each service an arc and each route a ribbon crossing the disc](images/chord.png)](gallery/relations.md)
**[Flows, networks and sets](gallery/relations.md)** · 8 figures
What connects to what: flows between stages, links between people, overlaps between sets and one line per record across many axes. | +| [![A patch antenna's reflection swept across its band, on a Smith chart](images/smith.png)](gallery/coordinates.md)
**[Coordinates and projections](gallery/coordinates.md)** · 8 figures
Charts whose frame is not a plain rectangle: polar, Smith and Nichols charts, map projections, the plane cut into cells and a 3-D scene from several cameras. | [![A daily price chart as a trading screen draws it: candles with weekends and a holiday folded out of the time axis, a Bollinger band, two moving averages and their crossings as buy and sell markers, the last close tagged on the price axis, volume and RSI in tracks underneath and the volume at each price in a track beside](images/market.png)](gallery/markets.md)
**[Markets](gallery/markets.md)** · 4 figures
What a trading screen draws: candles and OHLC bars on a calendar that folds out the closed days, indicators, volume tracks, the last price on the axis, and a live chart with a crosshair. | +| [![Throughput faceted into one panel per region](images/facets.png)](gallery/scale.md)
**[Layout, axes and scale](gallery/scale.md)** · 7 figures
One chart split into panels or placed on a grid, axes broken or folded, and data far larger than the pixels it lands on. | [![The same greyscale chart with redundant encoding on: the greys are unchanged and each product carries a pattern, so the stack reads again](images/greyscale-hatched.png)](gallery/colour.md)
**[Without colour](gallery/colour.md)** · 5 figures
Redundant encoding: hatch patterns that carry a series when the colour cannot, in print, in greyscale or for a reader who cannot tell the hues apart. | --- diff --git a/docs/gallery/basics.md b/docs/gallery/basics.md new file mode 100644 index 0000000..cd58def --- /dev/null +++ b/docs/gallery/basics.md @@ -0,0 +1,25 @@ +# The gallery: lines, points and bars + +The everyday forms: series over a continuous or time axis, scatter, bars, areas and steps, with intervals, thresholds, second axes and typeset labels. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · **Lines, points and bars** · [Distributions](distributions.md) · [Parts of a whole](parts.md) · [Flows, networks and sets](relations.md) · [Coordinates and projections](coordinates.md) · [Markets](markets.md) · [Layout, axes and scale](scale.md) · [Without colour](colour.md) + +| | | +|---|---| +| ![Three series with a legend](../images/series.png) | ![Two groups of scattered points](../images/scatter.png) | +| ![A damped sine on a time axis](../images/signal.png) | ![An estimate with a shaded interval](../images/area.png) | +| ![A step chart of replica counts](../images/steps.png) | ![A response time histogram](../images/bars.png) | +| ![Bars by region, coloured by value with a colourbar](../images/categories.png) | ![Income against life expectancy as bubbles sized by population, with a size key beside the legend](../images/bubbles.png) | +| ![Mean latency per service with a 95 % interval drawn over each bar](../images/errorbars.png) | ![Two growth curves on a log axis](../images/logscale.png) | +| ![A series read against thresholds and a shaded window](../images/annotations.png) | ![Revenue as bars against a left axis and margin as a percentage line against a right one](../images/twoaxes.png) | +| ![An oven temperature curve read against elapsed minutes along the bottom and cycle number along the top](../images/twoextents.png) | ![Standard error curves labelled with typeset notation](../images/notation.png) | +| ![Nearby labels placed without overlapping one another](../images/label-placement.png) | ![Calls per hour as a heatmap of coloured cells](../images/heatmap.png) | +| ![A rebuild plan as a gantt chart: bars part filled by how far each task has got, arrows between them with the critical path in orange, milestone diamonds and a dashed line at today](../images/gantt.png) | | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/gallery/colour.md b/docs/gallery/colour.md new file mode 100644 index 0000000..b05761f --- /dev/null +++ b/docs/gallery/colour.md @@ -0,0 +1,19 @@ +# The gallery: without colour + +Redundant encoding: hatch patterns that carry a series when the colour cannot, in print, in greyscale or for a reader who cannot tell the hues apart. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · [Lines, points and bars](basics.md) · [Distributions](distributions.md) · [Parts of a whole](parts.md) · [Flows, networks and sets](relations.md) · [Coordinates and projections](coordinates.md) · [Markets](markets.md) · [Layout, axes and scale](scale.md) · **Without colour** + +| | | +|---|---| +| ![The same stacked revenue chart with the colour taken out: three products as three greys, two of them almost the same one](../images/greyscale-plain.png) | ![The same greyscale chart with redundant encoding on: the greys are unchanged and each product carries a pattern, so the stack reads again](../images/greyscale-hatched.png) | +| ![The same stacked revenue chart under redundant encoding: a different hatch pattern over each product, so the stack reads without colour](../images/hatched.png) | ![Traffic by channel as stacked areas, each band hatched and its crest marked with points](../images/hatched-area.png) | +| ![The fourteen hatch patterns as fourteen labelled samples: lines, dots, wavering lines and tilings](../images/hatch-kinds.png) | | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/gallery/coordinates.md b/docs/gallery/coordinates.md new file mode 100644 index 0000000..4cf58f5 --- /dev/null +++ b/docs/gallery/coordinates.md @@ -0,0 +1,20 @@ +# The gallery: coordinates and projections + +Charts whose frame is not a plain rectangle: polar, Smith and Nichols charts, map projections, the plane cut into cells and a 3-D scene from several cameras. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · [Lines, points and bars](basics.md) · [Distributions](distributions.md) · [Parts of a whole](parts.md) · [Flows, networks and sets](relations.md) · **Coordinates and projections** · [Markets](markets.md) · [Layout, axes and scale](scale.md) · [Without colour](colour.md) + +| | | +|---|---| +| ![Two designs compared on five axes as a radar chart](../images/radar.png) | ![A patch antenna's reflection swept across its band, on a Smith chart](../images/smith.png) | +| ![An open loop at two gains on a Nichols diagram, drawn over the contours of constant closed-loop gain and phase](../images/nichols.png) | ![Mean July cloud cover over the whole world on an equal-area projection: the globe as an ellipse, one coloured cell per ten degrees, cloudy along the equator and over the midlatitude oceans and clear through the subtropics](../images/map.png) | +| ![Edinburgh to Tokyo on a globe seen from over the pole: the great circle running close to the ice and the rhumb line bowing far south of it, with the graticule and the rim of the visible hemisphere](../images/globe.png) | ![Last month's rainfall over twenty gauges, as the panel divided into the part nearest each gauge and filled with that gauge's reading, wettest in the north-west](../images/voronoi.png) | +| ![A response surface drawn from three cameras at once: a three-quarter view, a plan and a front elevation of one scene](../images/surface.png) | ![Thirty sweeps of a spectrum offset by sweep number, with a carrier drifting upward and a harmonic growing behind it](../images/cascade.png) | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/gallery/distributions.md b/docs/gallery/distributions.md new file mode 100644 index 0000000..d3e59a5 --- /dev/null +++ b/docs/gallery/distributions.md @@ -0,0 +1,20 @@ +# The gallery: distributions + +How a sample is spread: binned, summarised, smoothed, or every observation placed. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · [Lines, points and bars](basics.md) · **Distributions** · [Parts of a whole](parts.md) · [Flows, networks and sets](relations.md) · [Coordinates and projections](coordinates.md) · [Markets](markets.md) · [Layout, axes and scale](scale.md) · [Without colour](colour.md) + +| | | +|---|---| +| ![Request latency as a histogram](../images/histogram.png) | ![Latency distributions as boxplots](../images/boxplot.png) | +| ![Latency by service as violins, one per region within each service](../images/violin.png) | ![A year of daily maxima as a ridgeline, one density per month](../images/ridgeline.png) | +| ![Scores by cohort as a beeswarm, every observation placed](../images/beeswarm.png) | ![Scores by cohort as three empirical CDFs on one axis](../images/ecdf.png) | +| ![Ordered observations against normal quantiles](../images/qq.png) | ![Fifty thousand observations binned into hexagons with a loess trend through them](../images/hexbin.png) | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/gallery/markets.md b/docs/gallery/markets.md new file mode 100644 index 0000000..60ff4c5 --- /dev/null +++ b/docs/gallery/markets.md @@ -0,0 +1,18 @@ +# The gallery: markets + +What a trading screen draws: candles and OHLC bars on a calendar that folds out the closed days, indicators, volume tracks, the last price on the axis, and a live chart with a crosshair. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · [Lines, points and bars](basics.md) · [Distributions](distributions.md) · [Parts of a whole](parts.md) · [Flows, networks and sets](relations.md) · [Coordinates and projections](coordinates.md) · **Markets** · [Layout, axes and scale](scale.md) · [Without colour](colour.md) + +| | | +|---|---| +| ![A daily price chart as a trading screen draws it: candles with weekends and a holiday folded out of the time axis, a Bollinger band, two moving averages and their crossings as buy and sell markers, the last close tagged on the price axis, volume and RSI in tracks underneath and the volume at each price in a track beside](../images/market.png) | ![The frame a live chart ends on: one-minute candles zoomed into the last half hour, the price axis fitted to what is in view, the last price tagged in its direction and a crosshair snapped to a candle with its time and price tagged on the axes](../images/market-live.png) | +| ![The last thirty trading days as OHLC bars, a rule from low to high with the open ticked left and the close right, coloured by the close against the previous close](../images/market-bars.png) | ![The same thirty days as candles under redundant encoding: rising bodies drawn as outlines and falling bodies filled, so direction reads without colour](../images/market-hollow.png) | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/gallery/parts.md b/docs/gallery/parts.md new file mode 100644 index 0000000..519fc9d --- /dev/null +++ b/docs/gallery/parts.md @@ -0,0 +1,19 @@ +# The gallery: parts of a whole + +One total split into its shares, flat, stacked over time or nested. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · [Lines, points and bars](basics.md) · [Distributions](distributions.md) · **Parts of a whole** · [Flows, networks and sets](relations.md) · [Coordinates and projections](coordinates.md) · [Markets](markets.md) · [Layout, axes and scale](scale.md) · [Without colour](colour.md) + +| | | +|---|---| +| ![Revenue stacked by product, one layer over a long table](../images/stacked.png) | ![Traffic by channel as a streamgraph](../images/stream.png) | +| ![Browser share as a donut](../images/pie.png) | ![Spend by team as a donut whose slices reach as far as each team used of its budget, with the team that went over broken out of the ring](../images/donut.png) | +| ![Disk usage by directory as a treemap, one rectangle per file sized by its share](../images/treemap.png) | ![The same directory tree as a sunburst, the root at the middle and the files at the rim](../images/sunburst.png) | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/gallery/relations.md b/docs/gallery/relations.md new file mode 100644 index 0000000..da887ec --- /dev/null +++ b/docs/gallery/relations.md @@ -0,0 +1,20 @@ +# The gallery: flows, networks and sets + +What connects to what: flows between stages, links between people, overlaps between sets and one line per record across many axes. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · [Lines, points and bars](basics.md) · [Distributions](distributions.md) · [Parts of a whole](parts.md) · **Flows, networks and sets** · [Coordinates and projections](coordinates.md) · [Markets](markets.md) · [Layout, axes and scale](scale.md) · [Without colour](colour.md) + +| | | +|---|---| +| ![Requests per second through a service, drawn as a sankey diagram](../images/sankey.png) | ![The same traffic as an arc diagram, each service a segment of the rail and each route a band arcing over it](../images/arc.png) | +| ![The same traffic again as a chord diagram, each service an arc and each route a ribbon crossing the disc](../images/chord.png) | ![Who worked with whom over a quarter, as a node-link diagram: three teams as three clusters, one person joining all of them, and a pair working on their own off to the side](../images/network.png) | +| ![Customers by which products they subscribe to, as an UpSet plot: a bar per combination over a matrix of dots saying which products that combination is](../images/upset.png) | ![The same three products as a Venn diagram, each region carrying the customers who have exactly those products](../images/venn.png) | +| ![Sixty cars as a parallel-coordinates plot: four vertical axes in four different units, one line per car crossing all of them, coloured by origin](../images/parallel.png) | ![A quarter of support tickets as a parallel-sets diagram: three columns of category boxes for channel, urgency and outcome, with ribbons between them as thick as the tickets that answer both that way, split into the solved and escalated share](../images/parallelsets.png) | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/gallery/scale.md b/docs/gallery/scale.md new file mode 100644 index 0000000..bef9f9b --- /dev/null +++ b/docs/gallery/scale.md @@ -0,0 +1,20 @@ +# The gallery: layout, axes and scale + +One chart split into panels or placed on a grid, axes broken or folded, and data far larger than the pixels it lands on. + +Every figure here is rendered by +[`backend/gg/cmd/gallery`](../../backend/gg/cmd/gallery) and re-checked in CI, so a +picture here cannot drift away from the code that produced it. + +[← The gallery](../gallery.md) · [Lines, points and bars](basics.md) · [Distributions](distributions.md) · [Parts of a whole](parts.md) · [Flows, networks and sets](relations.md) · [Coordinates and projections](coordinates.md) · [Markets](markets.md) · **Layout, axes and scale** · [Without colour](colour.md) + +| | | +|---|---| +| ![Throughput faceted into one panel per region](../images/facets.png) | ![Four subplots on one dark canvas](../images/subplots.png) | +| ![A quarter of a million samples drawn as a clean line](../images/decimation.png) | ![A million points drawn as a density raster](../images/density.png) | +| ![Requests per second by site as bars on an axis broken between 12 and 85, marked with a double slash, so the one site thirty times the rest and the five small ones all read](../images/axis-break.png) | ![The same bars with the break marked by two parallel zigzags across the panel, the tall bar cut where they run](../images/axis-break-zigzag.png) | +| ![A machine's state log over two shifts with every idle period folded off the time axis, each fold marked by a small slash on the axis line](../images/machine-folds.png) | | + +--- + +**[README](../../README.md)** · **[CONCEPT](../../CONCEPT.md)** · **[ADRs](../adr)** · [The gallery](../gallery.md) · [Chart forms](../charts.md) · [Interaction](../interaction.md) · [A million rows](../scale-out.md) · [Reading a chart](../reading.md) · [JSON and Arrow](../spec.md) · [Features](../features.md) · [Chart-type catalogue](../chart-types.md) · [Benchmarks](../benchmarks.md) · [How it was built](../milestones.md) diff --git a/docs/images/market-bars.png b/docs/images/market-bars.png new file mode 100644 index 0000000..9e0ebfe Binary files /dev/null and b/docs/images/market-bars.png differ diff --git a/docs/images/market-bars.svg b/docs/images/market-bars.svg new file mode 100644 index 0000000..6339ec4 --- /dev/null +++ b/docs/images/market-bars.svg @@ -0,0 +1 @@ +geom.CandleStyle(geom.Ticks), since the previous closeJul 20Jul 27Aug 3Aug 10Aug 17100120140160180geom.CandleStyle(geom.Ticks), since the previous close \ No newline at end of file diff --git a/docs/images/market-hollow.png b/docs/images/market-hollow.png new file mode 100644 index 0000000..a2134c1 Binary files /dev/null and b/docs/images/market-hollow.png differ diff --git a/docs/images/market-hollow.svg b/docs/images/market-hollow.svg new file mode 100644 index 0000000..4b138c0 --- /dev/null +++ b/docs/images/market-hollow.svg @@ -0,0 +1 @@ +Hollow candles under theme.Redundant(true)Jul 20Jul 27Aug 3Aug 10Aug 17100120140160180Hollow candles under theme.Redundant(true) \ No newline at end of file diff --git a/docs/images/market-live.png b/docs/images/market-live.png new file mode 100644 index 0000000..5acbaab Binary files /dev/null and b/docs/images/market-live.png differ diff --git a/docs/images/market-live.svg b/docs/images/market-live.svg new file mode 100644 index 0000000..569e059 --- /dev/null +++ b/docs/images/market-live.svg @@ -0,0 +1 @@ +FIG — live, one-minute candles9294969813:0013:0513:1013:1513:2013:2513:30050100150200FIG — live, one-minute candles89.99updownSep 28 13:2092.40 \ No newline at end of file diff --git a/docs/images/market.png b/docs/images/market.png new file mode 100644 index 0000000..4355c02 Binary files /dev/null and b/docs/images/market.png differ diff --git a/docs/images/market.svg b/docs/images/market.svg new file mode 100644 index 0000000..5f86f1f --- /dev/null +++ b/docs/images/market.svg @@ -0,0 +1 @@ +FIG — daily, weekends and holidays folded801001201401600500010000150002000025000Jun 8Jun 15Jun 22Jun 29Jul 6Jul 13Jul 20Jul 27Aug 3Aug 10Aug 17025507510002500075000125000FIG — daily, weekends and holidays folded184.14Bollinger 20, 2σupdownEMA 9SMA 20buysellRSI 30–70RSI 14 \ No newline at end of file