Concepts¶
How InsightDock Charts is put together, for anyone extending a chart type, embedding a custom overlay, or just wanting to know why a call is shaped the way it is. If you only need to use existing chart types, the Usage guide and Chart types pages are a faster path — come back here when you need the "why."
Class hierarchy¶
Chart (abstract)
├── PieChart — pie slices, Tableau-10/Chart.js palette, raster + vector PDF + native vector SVG
├── ScatterChart — scatter/bubble; continuous numeric x/y Scales, not a category axis
└── CartesianChart (abstract) — shared axes, grid, legend, title rendering
├── BarChart — grouped or stacked vertical bars
├── LineChart — multi-series lines with optional area fill and point markers
├── AreaChart — filled-area polygons, optional stacking
└── ComboChart — grouped bars + line overlay; series opt into a shared or independent axis
Two base classes, not one, because the axis model genuinely differs:
Chartis the root: it ownsChartOptions(title, size, background, margins, font) and requires onlyrender() -> ImageBuffer.PieChartandScatterChartderive from it directly.CartesianChartadds everything charts-with-a-category-axis share: category labels, one or moreDataSeries, axis/grid/legend rendering, and thedraw_series(PlotContext&)extension point.BarChart,LineChart,AreaChart, andComboChartderive from it.
ScatterChart deliberately does not derive from CartesianChart even though it also has axes
and a legend — its x-axis is a continuous numeric Scale over arbitrary (possibly unsorted,
unevenly-spaced) point data, not a fixed list of category labels. Reusing CartesianChart would
have meant bolting a second, incompatible axis model onto it. When you're deciding which base to
derive a new chart type from, this is the test to apply: is the x-axis a category list, or a
continuous numeric domain?
The render pipeline¶
Every chart's public entry point is render() -> ImageBuffer (RGBA, row-major). For
CartesianChart subclasses, render() itself is not virtual — it creates the image, wraps it in a
RasterTarget and calls Chart::draw(DrawTarget&). That same draw() is what the SVG and PDF
exporters call with an SvgTarget / PdfTarget, which is how Cartesian charts get native vector
output (see Vector export). draw() runs, in order:
draw_background()— fills the canvas withChartOptions::background.draw_title()— chart title, if set.compute_plot_rect()— works out the plot rectangle inside the margins, leaving room for axis labels/ticks and the legend.draw_axes_and_grid()— buildsPlotContext::x_scale/y_scale(andy_scale2if a secondary axis is enabled), then draws ticks, gridlines, and axis labels.draw_series(PlotContext&)— pure virtual; this is what each subclass implements.BarChartdraws grouped/stacked rectangles,LineChartdraws polylines and point markers,AreaChartdraws filled polygons,ComboChartdraws bars and dispatches line-series drawing to the same overlay hook it also uses for itself (see below).draw_overlay(PlotContext&)— virtual, default no-op; called afterdraw_series. This is the supported extension point for adding things without touching the render pipeline (see below).draw_legend()— one entry per series, using eachDataSeries::color.
PieChart and ScatterChart don't go through this pipeline — they override render() directly
since they don't share the category/series axis model. Both implement Chart::draw() to share
their geometry and layout across raster, SVG and PDF.
PlotContext¶
draw_series and draw_overlay receive a CartesianChart::PlotContext&, a bundle of everything
needed to draw geometry without recomputing it:
| Field | Purpose |
|---|---|
target |
DrawTarget& — draw onto this (raster, SVG or PDF). Use its methods (fill_rect, draw_dashed_polyline, draw_text(font, ...), …) rather than ImageBuffer ones. |
plot |
Plot area Rect, already inset for margins/labels. |
x_scale |
Category index → pixel X. |
y_scale |
Data value → pixel Y (primary, left axis). |
y_scale2 / has_y2 |
Secondary (right) axis scale, populated only when has_secondary_y_axis() returns true. |
font |
For any text the overlay draws. |
opts / cart_opts |
Common and Cartesian-specific options. |
categories / series |
The chart's data. |
Because PlotContext carries the already-computed scales, an overlay or a new chart type never
needs to re-derive pixel mapping — call ctx.y_scale.to_pixel(value) and draw.
Scale¶
Scale (include/insightdock/scale.h) is the one class doing the linear-mapping work behind every
axis, in both directions:
to_pixel(value)— data value → pixel coordinate, for drawing.from_pixel(px)— pixel coordinate → data value, for hit-testing or overlays anchored to a screen position.linear_nice(min, max, tick_hint)— expands a raw data range to "nice" round numbers before computing ticks (this is what backsCartesianOptions::min_y_labels/max_y_labels).nice_ticks()— generates the actual tick positions/labels for an axis.
ScaleType (Linear, Log, DateTime) controls how nice_ticks buckets and formats values —
DateTime combined with TickFormat::DateTime is what a time-series x or y axis uses.
Extending: the draw_overlay hook¶
draw_overlay(PlotContext&) is the supported way to add custom drawing to an existing
CartesianChart subclass without subclassing the render pipeline itself. It runs once, after
draw_series, so anything it draws sits on top of bars/lines/areas but under the legend.
class AnnotatedBar : public BarChart {
void draw_overlay(PlotContext& ctx) const override {
float py = ctx.y_scale.to_pixel(target_);
ctx.buffer.draw_line(ctx.plot.left(), py, ctx.plot.right(), py,
1.5f, {255, 80, 80, 200});
}
double target_ = 100.0;
};
This is also the mechanism ComboChart itself is built on: bars are drawn in draw_series, and
the line-series pass happens in its own draw_overlay override, reusing the same hook rather than
inventing a second combo-specific pipeline.
Adding a new chart type¶
- Derive from
Chart(freeform axes/layout) orCartesianChart(category axis, shared axes/grid/legend) — see the test above for which one fits. - Implement
render()(freeform) ordraw_series(PlotContext&)(Cartesian). Overridedraw_overlaytoo if the chart needs a second drawing pass (dual-axis, annotations). - If the chart needs its own axis behavior — a secondary y-axis, a non-default y-domain — override
has_secondary_y_axis()/compute_y_min()/compute_y_max()/compute_y2_min()/compute_y2_max()as needed.ComboChart's dual-axis support is the reference implementation for this. - Add the new
.ccfile toINSIGHTDOCK_SOURCESinCMakeLists.txtand toSRCSinMakefile. - Include the new header in
include/insightdock/charts.h(the umbrella include). - Add a CTest smoke test and a
gallery_regressionreference PNG so the new type has a committed visual baseline (see Troubleshooting → updating the visual regression baseline). - Add an entry to
CHANGELOG.mdand updateROADMAP.md.
Chart-type-specific integration code (application/report samples) belongs in examples/, never in
include/insightdock/ or src/.
Determinism¶
Rendering is deterministic given the same inputs — no randomness, no wall-clock or locale
dependence in geometry or color selection. This is what makes gallery_regression (pixel-diff
comparison against docs/gallery/, tolerant of ≤0.1% differing pixels for cross-platform font
rasterization variance) a meaningful CI gate rather than a fuzzy visual diff.