Skip to content

Changelog

All notable changes to InsightDock Charts for C++ are documented here.

Format based on Keep a Changelog. Versioning follows Semantic Versioning.

[Unreleased]

Added

  • ChartExporter::capabilities() queries advertised native drawing support and compiled PDF availability without rendering. Built-in charts declare VectorCapability::Native; custom charts default to Unknown and may override vector_capability().
  • VectorFallback::Reject requires native SVG/PDF output: save()/encode() return ChartError::VectorUnsupported when draw() declines, without rasterizing or replacing the destination file. Existing custom draw overrides continue to work. Void PDF embedding APIs throw std::runtime_error on strict rejection.
  • Const ChartExporter::save() / encode() overloads, with documented exception contracts and installed-consumer/concurrent-export coverage.

Fixed

  • Documentation and public display names consistently use InsightDock Charts for C++ or InsightDock Charts. Existing API identifiers and package names retain their spelling.

  • Allowed custom-chart vector fallback now uses the regular raster supersampling, clone and allocation checks. Raster fallback render failures return RenderFailure; partial standalone vector output is discarded.

  • Supersampled raster exports preserve logical layout and scale all drawing primitives, including line widths, dash lengths, marker/bubble radii, rounded corners, borders, leader lines and independently sized data labels. Fonts retain their original TTF data. Exporting still uses a clone and does not change the caller's chart. Custom render-only charts retain the legacy path.

  • Raster thick strokes now fill their interior with distance-based edge coverage instead of sparse parallel traces; translucent interiors blend once per segment. Gallery references reflect the corrected strokes.
  • Scatter/bubble now draw through DrawTarget, enabling consistent supersampling and native SVG/PDF shapes and text instead of raster fallback.

  • Pie callouts now use measured columns with vertical spacing, reserved title/legend areas and leaders routed outside the circle. Automatic radii reserve label width. Overfull columns keep larger slices first with stable input-order ties; blocks that cannot fit are omitted in full. Slice geometry and legend visibility remain independent of callout omission.

  • Linear nice axes now enclose the lower data extent as well as the upper extent, including one-tick hints. Positive scatter data no longer rounds inward and appears in the axis/legend margins; zero-based chart domains retain their zero baseline.

  • Pie, Cartesian and scatter/bubble legends measure individual entries, wrap labels/newlines, reserve right margins and pack top/bottom rows. Legends use at most one third of the canvas height and summarize excess entries as +N more; their data stays visible. Scatter Top legends now render above the plot. Secondary-axis titles remain beside the axis with a right legend.

  • Automatic pie center/radius options stay unresolved until rendering, reserving space for the title and legend and adapting to later canvas size changes. Existing gallery references updated for measured legend placement and automatic pie geometry.

  • Pie chart exports now share geometry and text wrapping through DrawTarget, preserving explicit colors, alpha, strokes, gaps, legends, titles and in-slice labels in native SVG/PDF output. UTF-8 token wrapping preserves code points and labels longer than 159 bytes are no longer truncated.

  • Pie proportions normalize large finite values before summing; negative values count as zero. Fill opacity is clamped and multiplies the slice color alpha. Fully consumed gaps leave no stray geometry. Pie percentage labels and SVG coordinates ignore process locale.
  • Legacy PDF report label wrapping also preserves UTF-8, and hidden small labels no longer shift subsequent callout angles.
  • DateTime ticks now use UTC with checked timestamp conversion; Raw labels preserve fractions and value axes choose precision from tick spacing. Numeric tick/data labels ignore process locale.
  • Top Cartesian legends render above the plot; category truncation removes whole UTF-8 code points.
  • Exporters report failed writes as BadPath, catch rendering/encoding exceptions, reject oversized vector dimensions and report unavailable raster/vector PDF as PdfUnsupported. Custom charts without clone() fail supersampling without modifying const input.
  • Font files exceeding 64 MiB are rejected before their contents are allocated/read.
  • Installed CMake targets advertise C++17. pkg-config metadata is relocatable and includes feature definitions and the optional libpodofo dependency. Third-party headers no longer trigger unused-function diagnostics in strict core builds.

Added

  • PieChart::draw() and a default DrawTarget::fill_sector() implementation for existing custom targets. The doughnut example exports PNG, native SVG and optional native PDF.
  • CartesianOptions::custom_x_formatter for continuous DateTime tick presentation.
  • Reliability regressions run in UTC, Karachi and New York process timezones.
  • PDF CI job pinned to PoDoFo revision 00f86c8ad7b78430b25ad2c81edb704ae8daa6ea, with report examples and an installed PDF-enabled consumer.
  • Governance/acceptance guidance, corrected security trust boundaries, and bundled font license notices installed with the library. Roadmap reconciled with implemented features and expanded with reliability, axes/layout, analytics, accessibility and ecosystem milestones.

Changed

  • Default vector PDF pie exports now use the chart's raster/SVG layout, including its background and title. The explicit PieVectorOptions overload of podofo::draw_chart_vector() and the draw_pie() / draw_pie_slices() helpers retain the separate PDF report layout.
  • SOVERSION tracks major.minor at all versions to match the documented ABI policy. Rebuild applications when upgrading between minor releases; patch releases must preserve ABI.
  • CMake examples/tests default off when InsightDock Charts is included as a subdirectory.
  • Date labels previously formatted in the machine's local timezone now default to UTC. Regenerate affected output baselines or supply a custom formatter for another timezone.
  • Refresh the time-series gallery baseline for UTC labels.

1.0.0-rc1 — 2026-10-02 — first public release candidate

Release candidate for 1.0. The API is frozen for the feedback window except for fixes to problems found by outside users. Binary compatibility is not promised, only source compatibility.

Phase E — platforms and packaging

  • CI matrix — Windows (MSVC, static and DLL), macOS and a shared-library Linux build, in addition to x86-64 and ARM64 Linux. (Added without a local MSVC/macOS run; the first GitHub run is the real test.)
  • MSVC build flags — /W4 /utf-8 /permissive-, /WX under INSIGHTDOCK_WERROR; on Windows the DLL and all executables share one output directory so shared builds run from the build tree.
  • Gallery regression compares decoded pixels (≤ 8 per channel, ≤ 0.1 % of pixels) instead of encoded PNG bytes, so it no longer depends on the zlib/FPU of one platform. Test-only tests/third_party/stb_image.h (never shipped).
  • Automated find_package consumer test (find_package_consumer in CTest): installs the build to a scratch prefix and builds and runs an out-of-tree program, for static and shared builds.
  • Windows paths — STBIW_WINDOWS_UTF8 for PNG/JPEG output and a UTF-8-safe Font::load_from_file.
  • vcpkg port under packaging/vcpkg/insightdock-charts (features font-latin, podofo); SHA512 is a placeholder until the first tag exists.
  • scripts/check_version.sh (CI job version-consistency) fails if CMake, OpenWrt PKG_VERSION and the vcpkg port disagree.

Phase D — chart features

  • Step lines — LineChartOptions::step / AreaChartOptions::step (StepMode::After, Before, Middle).
  • 100%-stacked bar (BarMode::Stacked100, vertical and horizontal) and area (AreaChartOptions::stacked_percent): the value axis is fixed at 0–100% and labelled with % automatically.
  • Histogram — compute_histogram() and make_histogram_chart() (insightdock/histogram.h), a BarChart preset with touching bars.
  • Colorblind-safe palette — PaletteKind::ColorblindSafe (Okabe–Ito) and the new ChartOptions::palette, which selects the color cycle for series added without an explicit color.
  • Reference lines and bands now work in horizontal bar charts (previously skipped silently): Horizontal means a constant value (drawn vertically there), Vertical a category position.
  • Multi-line text — chart titles, category labels and the x-axis title may contain \n; each line is aligned independently and the margins grow to fit.
  • Non-finite values in stacked bars now count as 0 (they used to poison the stack).
  • Not in 1.0: error bars (see ROADMAP).

Phase C — native vector output

  • Native vector SVG and PDF for bar, line, area and combo charts (grouped/stacked/horizontal bars, splines, per-series dash and markers, translucent fills, data labels, reference lines/bands, secondary axis, rotated axis titles, legend). ExportOptions::svg_backend = SvgBackend::Vector and pdf_backend = PdfBackend::Vector now apply to every Cartesian chart; only scatter still embeds a raster. Selectable text, scales to any size. New guide: Vector export.
  • DrawTarget (include/insightdock/draw_target.h) — pixel-space drawing interface that Cartesian charts render through, with RasterTarget (PNG/JPEG; gallery output is byte-identical), detail::SvgTarget and podofo::PdfTarget. Chart::draw(DrawTarget&) is a new virtual (default returns false; all Cartesian charts implement it).
  • Doughnut cutout in vector pie output — SVG and PDF pies now honour PieOptions::inner_radius_ratio (hole painted in the background color, as the raster path does). PieVectorOptions gains inner_radius_ratio and hole_color.
  • Font::baseline_offset() and Font::ascent() are now public so non-raster targets place text exactly where the raster path does.

Changed (API, pre-1.0)

  • CartesianChart::PlotContext::buffer (ImageBuffer&) is now target (DrawTarget&); detail::fill_bar / detail::draw_marker take a DrawTarget&; compute_plot_rect takes (width, height, font). Custom CartesianChart subclasses draw with ctx.target.draw_text(font, ...) instead of font.draw_text(ctx.buffer, ...).

Fixed

  • Stacked AreaChart clipped its top series — the y-axis was sized from the tallest single series instead of the per-category total (BarChart already did this). The area_stacked gallery baseline was regenerated.

Added

  • Per-series styling and missing-data handling (Phase B of the v1.0 plan).
  • DataSeries::style (SeriesStyle, new include/insightdock/series_style.h): optional per-series dash, marker, line_width and fill_opacity; unset fields inherit the chart-wide options. PointMarkerShape/DashPattern moved there from chart_style.h (still included by it). New overloads CartesianChart::add_series(label, values, color, SeriesStyle) and ComboChart::add_line_series(label, values, color, SeriesStyle, secondary_axis). Legend swatches show the dash/marker for styled series. The hard-coded fill alphas (60/120/180) are now the defaults that fill_opacity overrides.
  • GapPolicy (Gap default, Interpolate, Zero) on LineChartOptions/AreaChartOptions/ComboLineOptions: NaN, Inf and series shorter than the category list break the line (or are joined/zeroed) instead of always drawing as 0. Stacked areas treat a missing value as 0. lttb_decimate keeps non-finite points so decimation doesn't hide gaps.
  • Fonts and text. ChartOptions::font (FontDataPtr from the new load_font_data()/make_font_data()) makes font injection actually work for every chart (previously Chart::font_ was never read, so FONT_MODE=system could not render text). Text is decoded as UTF-8 (previously every byte above 127 was dropped); missing glyphs render as ?; \n is handled in measure_width. New optional embedded Latin-1/Latin-Extended-A font (INSIGHTDOCK_FONT_LATIN / FONT_MODE=embedded-latin, embedded_font_latin.h). Font files are validated (≤ 64 MiB, parseable). CartesianOptions::x_label_mode (Auto/Rotate90/Truncate) for wide category labels.
  • Thread safety. Chart::clone() (overridden by all built-in charts) lets ChartExporter render supersampled output from a scaled copy; exporting no longer mutates the chart, and concurrent exports of one chart are safe. Chart is now copyable (the unused font_ member was removed).
  • Versioning/ABI. Library VERSION/SOVERSION (major.minor while 0.x), hidden default symbol visibility with INSIGHTDOCK_API export macros (Windows DLLs now export the API), SameMinorVersion package compatibility before 1.0, find_dependency(podofo) in the installed config when built with PoDoFo. Documented as source-compatible only.

  • Log y-axis and continuous DateTime x-axis actually wired into charts — Scale's ScaleType::Log/ScaleType::DateTime interpolation and tick generation (nice_ticks()) were already fully implemented and unit-tested, but no chart type ever exposed a way to use them: CartesianChart::render() always built a plain linear y_scale, and the x-axis was always evenly-spaced category slots. This left the "network traffic and time-series dashboards" use case ROADMAP.md names as the primary OpenWrt motivation for AreaChart unreachable — examples/time_series_line_example.cc faked a time axis with hand-formatted date strings as category labels, which can't represent irregular/gapped real time deltas.

  • CartesianOptions::y_scale_type (ScaleType, default Linear) — set to Log on BarChart/LineChart/AreaChart/ComboChart for data spanning multiple orders of magnitude. New Scale::log_nice() (mirrors linear_nice()) builds a power-of-10/1-2-5-mantissa "nice" domain; extends (never shrinks) to the actual data range, since log ticks don't necessarily reach the true min/max the way linear "nice" rounding does. CartesianOptions::begin_at_zero is ignored when y_scale_type = Log (log(0) is undefined — forcing it would otherwise stretch the domain across hundreds of decades of empty ticks). Secondary (right) y-axis stays Linear-only in v1.
  • CartesianOptions::x_scale_type (ScaleType, default Linear) plus CartesianChart::set_x_values() — set x_scale_type = DateTime and supply Unix-timestamp x_values() to position points by real value instead of index. Only LineChart/AreaChart opt in (new supports_continuous_x_axis() hook, default false); BarChart/ComboChart silently ignore it and keep category-slot positioning, since a fixed-width bar-per-category layout has no well-defined meaning for irregularly-spaced continuous x-positions. draw_axes_and_grid() draws nice TickFormat::DateTime-formatted ticks from the continuous domain instead of per-category labels when active.
  • examples/time_series_line_example.cc and its gallery_regression.cc/docs/gallery/time_series_line.png counterpart converted from hand-formatted date-string categories to real Unix timestamps via set_x_values() + x_scale_type = DateTime.
  • New examples/log_scale_example.cc. New unit tests: test_scale_log_nice_* (primitive), test_line_chart_log_y_axis_renders_and_spans_range, test_line_chart_datetime_x_axis_positions_by_value_not_index, test_bar_chart_ignores_datetime_x_scale_type, test_cartesian_x_values_roundtrip.
  • Horizontal bar chart — BarOptions::orientation (BarOrientation::Vertical default, Horizontal) on BarChart: bars extend rightward from a category axis on the left instead of upward from one at the bottom, for rankings/leaderboards and long category names that would otherwise collide under a vertical x-axis. New CartesianChart::is_horizontal() hook (default false, only BarChart overrides it) makes the shared axis/grid/margin code in draw_axes_and_grid()/compute_plot_rect() orientation-aware: value ticks + vertical gridlines move to the bottom, category labels move to the left (left margin widens to fit the longest one), grouped/stacked bar geometry gets dedicated draw_grouped_horizontal()/draw_stacked_horizontal() implementations (fill_bar() itself is already orientation-agnostic — just a rect fill). ReferenceLine/ReferenceBand overlays are not supported in horizontal mode (v1) — their orientation semantics assume a vertical value axis, so BarChart::draw_overlay() is a no-op when horizontal rather than rendering them scrambled. Not available on ComboChart (line-series geometry has no horizontal equivalent). New examples/bar_horizontal_example.cc; new unit tests test_bar_chart_horizontal_*.
  • pkg-config support (cmake/insightdock.pc.in) — cmake --install now also installs lib/pkgconfig/insightdock.pc, for Meson and plain-Makefile consumers that don't use CMake's find_package(). Paths resolve via CMAKE_INSTALL_FULL_*, so -DCMAKE_INSTALL_PREFIX must be set at the cmake -B configure step (not just cmake --install --prefix) for a custom prefix to be reflected correctly — documented in README.
  • packaging/openwrt/Makefile — a standard OpenWrt feed package sample (package.mk-based, Build/Compile invoking the project's own plain Makefile with cross toolchain variables), wired to the existing CHART_TYPES/EXPORT_FORMATS/FONT_MODE trim knobs. Not built by this repo's CI; a starting point for downstream feeds. See packaging/openwrt/README.md.

  • Data labels on marks — all 6 chart types — DataLabelOptions (include/insightdock/chart_style.h: enabled, color, font_size, position, decimals, format) added as a data_labels field on PieOptions, BarOptions, LineChartOptions, AreaChartOptions, ComboLineOptions, ScatterOptions — default enabled = false, so existing renders are byte-identical until opted in. Bar/Line/Area/Combo reuse the same TickFormat-based formatting as y-axis ticks (hoisted from cartesian_chart.cc's file-local format_y_tick into detail::format_tick_value() / detail::format_label_value() in the new include/insightdock/detail/tick_format.h), so a label matches its axis's number formatting. PieChart gets a new in-slice value/percentage label at radius*0.65 along each slice's midpoint angle, distinct from the existing external leader-line label. ScatterChart labels the formatted Y value only (no per-point custom label in v1).

  • Bar rounded corners + border — BarChart's previously-scattered private members (mode_/group_gap_/bar_gap_) promoted to a BarOptions struct (include/insightdock/bar_chart.h) with new corner_radius, border_color (default transparent = no border), and border_width fields; old set_mode()/set_group_gap()/set_bar_gap() setters kept as forwarders for source compatibility. ImageBuffer::fill_rounded_rect() (new primitive, mirrors the existing fill_circle/fill_sector 4x4 supersample-AA technique via a rounded-box signed-distance-field) and a shared detail::fill_bar() helper (include/insightdock/detail/bar_shape.h) apply the same rounding/border logic to both BarChart and ComboChart's bar series (which gained a parallel BarOptions bar_opts_). A rounded bar with a border draws a plain rectangular outline for v1 — a minor accepted overlap at the 4 corners.
  • Pie doughnut cutout — PieOptions::inner_radius_ratio (0–1, default 0) punches a hole in the pie's center down to the chart's background color after slices are drawn (PieChart::render()). Assumes a solid background, which is already the only backdrop this library supports. Raster (PNG/JPEG/PDF-raster-embed) only — the native vector PDF/SVG pie paths (podofo_vector.cc, svg_pie.cc) don't draw the cutout yet; tracked as a follow-up.
  • Line/area spline smoothing, dash patterns, point marker shapes — LineChartOptions/AreaChartOptions/ComboLineOptions gain spline (bool), dash (DashPattern = std::vector<float> of alternating on/off pixel lengths; empty = solid), and point_shape (PointMarkerShape: Circle/Square/Triangle/Cross); ScatterOptions gains point_shape. These are chart-wide settings, not per-series. New ImageBuffer::draw_dashed_line()/draw_dashed_polyline() (dash phase carries continuously across polyline vertices) and detail::catmull_rom_expand() (Catmull-Rom curve expansion in include/insightdock/detail/spline.h, used only for the stroke/fill geometry — markers and data labels still anchor to the original per-category points) and detail::draw_marker() (include/insightdock/detail/marker.h) back all four chart types. AreaChart also gains point markers (show_points, off by default) — it previously had none at all.
  • Reference lines & shaded bands — CartesianOptions::reference_lines (std::vector<ReferenceLine>) and reference_bands (std::vector<ReferenceBand>) draw threshold lines/bands (e.g. an SLA line, a target range) behind a chart's own series, via a new shared detail::draw_reference_lines() (include/insightdock/detail/reference_lines.h) called from BarChart/LineChart/AreaChart's new draw_overlay() overrides and from the top of ComboChart::draw_overlay() (ahead of its existing line-series drawing, so reference lines sit behind data but the combo's own lines still draw on top). ReferenceLine/ReferenceBand support Horizontal (y-value, optionally against the secondary axis) and Vertical (fractional category-index) orientation. Required promoting CartesianChart::PlotContext from protected to public so the shared overlay helper — outside the class hierarchy — can take it as a parameter. Not available on PieChart/ScatterChart, which have no draw_overlay() hook. Pulls forward the Phase 1 "step lines and spline" and Phase 2 "basic annotations and trend lines" roadmap items — see ROADMAP.md.
  • Doc site restructured from 5 flat nav entries into Concepts/Guides/Chart types (mkdocs.yml, docs-site/) — closes the gap where the richest usage content in the repo (docs/API.md: error handling, theming, tick formatting, extending via draw_overlay) existed only in the repo and was never reachable from the published MkDocs site. New pages: docs-site/concepts.md (class hierarchy, render pipeline, PlotContext/Scale, steps for adding a new chart type — narrative content that previously only existed in CLAUDE.md, which isn't a public doc); docs-site/guides/usage.md (transcludes docs/API.md, same single-source-of-truth pattern comparison.md already used for docs/COMPARISON.md); docs-site/guides/pdf-reports.md (new PdfReportBuilder walkthrough — previously only a one-line type-table entry plus the example program); docs-site/guides/troubleshooting.md (real incidents: the stale-install ABI hazard, clangd false positives, MrDocs aarch64 emulation, gallery baseline updates — previously scattered across CHANGELOG.md/CLAUDE.md/DOCUMENTATION.md with no single discoverable FAQ); docs-site/chart-types/{pie,bar,line,area,combo,scatter}.md (full options table + rendered example per chart type, replacing the previous "raw MrDocs struct page or nothing" gap); docs-site/changelog.md (transcludes CHANGELOG.md). docs/API.md trimmed (doc-tooling setup steps now just point at DOCUMENTATION.md instead of duplicating them) and cross-linked to the new pages.

  • CartesianOptions::min_y_labels / max_y_labels — two new integer fields (default 0 = unconstrained / default ~6) that control y-axis tick density on all CartesianChart subclasses (BarChart, LineChart, AreaChart, ComboChart). max_y_labels passes the value as the tick-count hint to both Scale::linear_nice (which sets the domain) and nice_ticks (which generates labels), giving approximately that many evenly-spaced, human-readable ticks. min_y_labels does the same, and if the nice-rounding step produces fewer ticks than requested it retries with a doubled hint (best-effort; capped at that retry if max_y_labels is also set). Setting both to the same value (e.g. min_y_labels = max_y_labels = 12) instructs the renderer to attempt exactly that many y-axis labels. If min_y_labels > max_y_labels the values are swapped internally so the constraint is always valid. Works correctly with positive/negative/mixed data, float values, TickFormat, custom_y_formatter, and begin_at_zero. Six new unit tests added (test_y_labels_* in unit_tests.cc).

  • CartesianOptions::max_x_labels — integer field (default 0 = unconstrained) that caps the number of x-axis category labels rendered. When set, a second step ceiling ceil(n / max_x_labels) is applied on top of the existing collision-avoidance skip, so the two constraints compose: collision avoidance prevents overlapping text; max_x_labels independently limits label count regardless of chart width. Useful for dense time-series categories (e.g. 100 daily date strings) where you want ~12 readable tick marks irrespective of pixel width.

  • examples/time_series_line_example.cc — demonstrates a 100-point daily time-series LineChart: decimates 100 → 25 points with lttb_decimate_indexed, uses max_x_labels = 12 to show ~12 evenly-spaced date labels, and a standard y-axis label for pre-scaled GB values. Wired into INSIGHTDOCK_BUILD_EXAMPLES in CMake; gallery image at docs-site/img/gallery/time_series_line.png.

  • ComboChart secondary (right) y-axis — add_bar_series()/add_line_series() take an optional secondary_axis flag (default false, fully backward compatible); series flagged true are scaled against an independent right-side axis instead of sharing the left one, for series with different units or magnitude (e.g. session count vs. average duration). Implemented as generic CartesianChart hooks (has_secondary_y_axis(), compute_y2_min()/compute_y2_max()) plus PlotContext::y_scale2/has_y2, so any CartesianChart subclass can opt in the same way later; BarChart/LineChart/AreaChart are unaffected since the hooks default to disabled. New CartesianOptions::y2_axis_label/y2_tick_format/custom_y2_formatter mirror the existing left-axis fields. Right margin widens automatically when a secondary axis is active. Added Font::draw_text_rotated_cw() (right-axis label, reads top-to-bottom) alongside the existing draw_text_rotated_ccw() (left-axis label); both now share one blit_rotated() implementation. Covered by two new unit_tests cases, demonstrated in examples/all_charts_raster_pdf.cc's combo chart page, and added to the public gallery: new examples/combo_chart_example.cc, a combo_chart.png reference baseline in docs/gallery/ (gallery_regression CTest), and a section in docs-site/examples.md — closing the gap where ComboChart (added in Phase 0.6) had never been added to the gallery/regression baseline or docs site.

  • ScatterChart (include/insightdock/scatter_chart.h, src/scatter_chart.cc) — scatter and bubble charts with continuous numeric x/y axes (not a category list, unlike BarChart/LineChart). Set ScatterPoint::radius to enable the bubble variant; size scales between ScatterOptions::min_bubble_radius and max_bubble_radius. Derives directly from Chart rather than CartesianChart since the x-axis must be continuous. Demonstrated in examples/scatter_bubble_example.cc; covered by gallery_regression and unit_tests.
  • Native SVG vector pie — ExportOptions::svg_backend (SvgBackend::Raster default, SvgBackend::Vector) mirrors the existing PdfBackend pattern. Vector mode emits real <path>/<circle>/<text> SVG for PieChart (src/svg_pie.cc) instead of a base64-inlined raster PNG; other chart types fall back to raster embed. Handles the single-100%-slice edge case with a <circle> (an arc with identical start/end points is degenerate).
  • LTTB decimation (include/insightdock/decimate.h, src/decimate.cc) — lttb_decimate() and the category-index convenience lttb_decimate_indexed() implement largest-triangle-three-buckets downsampling for reducing large line/area datasets to a target point count without losing visual shape (peaks/valleys), as a pre-processing step before LineChart/AreaChart.
  • benchmarks/bench_render.cc — dependency-free std::chrono timing harness (INSIGHTDOCK_BUILD_BENCHMARKS CMake option, default OFF). Covers line render at 1k/10k/100k points, bar render (50×10), pie render (20 slices), PNG encode, and PDF raster export. Baseline numbers recorded in docs/BENCHMARKS.md.
  • docs-mrdocs CI job — runs mrdocs docs/mrdocs.yml and checks docs/reference/index.html is produced (scripts/ci_docs_mrdocs.sh).
  • Gallery embedded in README — pie/bar/line/area sample images now render inline in README.md instead of only being linked.
  • docs/mrdocs-theme/wrapper.html.hbs — mrdocs 0.8.0's stock HTML generator ships with zero CSS (plain browser-default white-on-black text, no typography/table/code-block styling). This is a themed replacement for its one layout template (clean type, bordered/zebra-striped tables, code chips, prefers-color-scheme dark mode) — copied over the installed mrdocs binary's own template before each generation, both by scripts/ci_docs_mrdocs.sh and the local steps in docs/DOCUMENTATION.md. --addons would require vendoring mrdocs's entire internal helpers/partials tree just to change one file, so this overwrites the single layout in place instead.
  • One-stop MkDocs Material doc site (mkdocs.yml, docs-site/, scripts/build_docs_site.sh) — pulled forward from the post-v1.0.0 plan in FUTURE.md: Home, Getting started, Examples (each example's full source transcluded from examples/*.cc via pymdownx.snippets, paired with its rendered output from docs/gallery/), API Reference (the MrDocs output copied in by the build script), and Comparison (transcludes docs/COMPARISON.md directly — single source of truth). Built and published to GitHub Pages via .github/workflows/docs.yml. Verified visually with headless Chrome screenshots, not just a successful mkdocs build.

Changed

  • -DBUILD_SHARED_LIBS=ON + -DINSIGHTDOCK_WITH_PODOFO=ON verified end-to-end on OpenWrt (x86_64/musl toolchain): find_package(podofo CONFIG) resolves, libinsightdock.so links against PoDoFo, and headers/CMake export/.so all stage correctly for downstream packages.
  • Repository prepared for open-source release — removed non-public sample code and the previous CI configuration; added GitHub Actions CI (.github/workflows/ci.yml: lint, clang-tidy, build-and-test, sanitizers; .github/workflows/docs.yml: MrDocs + MkDocs site published to GitHub Pages) and standard GitHub OSS scaffolding (CODE_OF_CONDUCT.md, issue/PR templates). CMake/Makefile output filename is libinsightdock.a/.so.
  • Embedded font subset regenerated to cover full printable ASCII (include/insightdock/embedded_font.h) — the previous subset was missing glyphs for 20 punctuation codepoints, including $, !, ", #, ', *, ;, =, ?, @, brackets, backslash, backtick, braces, |, and ~ (each silently rendered as blank space via a .notdef glyph — not caught by Font::draw_text/measure_width's range check, which only validates the codepoint is in [32,127], not that the font actually maps it). Traced the embedded binary to DejaVu Sans via its TTF name table and regenerated the subset with fonttools' pyftsubset --unicodes="U+0020-007E" from a full DejaVu Sans install; all 95 printable-ASCII codepoints now resolve to a real glyph (verified with a standalone stbtt_FindGlyphIndex sweep). New subset is also smaller (~9.3 KB vs ~21 KB). NOTICE updated with accurate DejaVu Sans / Bitstream-Vera-derived license attribution (previously a vague "verify font license before redistribution" placeholder). docs/gallery/*.png baselines regenerated (UPDATE_GALLERY=1) since text metrics shifted slightly with the new subset.
  • CMake find_package(InsightDock) now exports the target as InsightDock::Charts (previously InsightDock::InsightDockCharts) — added EXPORT_NAME Charts to the library target. The in-tree InsightDock::Charts ALIAS masked this for anyone building this repo directly; every out-of-tree consumer following the README's own find_package example hit a target_link_libraries "target was not found" configure error. Verified with a clean cmake --install to a temp prefix followed by a throwaway consumer project that configures, builds, links, and runs.

Fixed

  • ChartExporter no longer turns a PNG/JPEG/SVG export into a PDF when ExportOptions::pdf_backend is Vector; the backend only applies to PDF output (save("x.pdf"), PdfRaster/PdfVector) and to draw_pdf(). save("x.pdf") now keeps an explicit OutputFormat::PdfVector.

  • Robustness against non-finite and hostile input (Phase A of the v1.0 plan).

  • NaN/Inf/huge coordinates no longer hang or invoke undefined behaviour: ImageBuffer primitives (draw_line, draw_dashed_*, fill_polygon, fill_rect, fill_circle, fill_sector, fill_rounded_rect) now reject non-finite geometry, clamp before every float→int cast, and clip very long segments to the buffer; dash cycles shorter than 0.25 px render solid. Font::draw_text/blit_rotated ignore non-finite anchors. Previously draw_dashed_segment looped forever on an Inf pixel and nice_ticks on Inf cast to int (UB; on ARM64 it saturated and looped ~2 billion times).
  • Scale sanitizes non-finite domains and caps tick generation (nice_ticks, linear_nice, DateTime/Log branches). Axis bounds in Bar/Line/Area/Combo/Scatter now come from a shared finite-only helper (detail/finite_range.h), so a NaN first value no longer poisons the domain; stacked-bar sums skip non-finite values. PieChart::add_slice stores non-finite values as 0.
  • ImageBuffer refuses to allocate beyond ImageBuffer::kMaxPixels (2^27 px) and ChartExporter returns ChartError::InvalidInput for oversized charts instead of letting bad_alloc escape; the supersample factor is capped at 8.
  • Makefile build: area_chart.cc was missing from SRCS (any AreaChart use failed to link); default CHART_TYPES is now pie bar line area scatter (was pie bar line, which silently omitted scatter and area); the never-read INSIGHTDOCK_HAS_BAR/LINE/SCATTER defines were removed; header dependency tracking (-MMD -MP) added; make test now also builds and runs the unit tests.
  • CI: sanitizer job now also enables float-cast-overflow,float-divide-by-zero (with -fno-sanitize-recover), build-and-test also runs on an ARM64 runner, and a new make-build job covers the Makefile (default and minimal OpenWrt configurations). New hostile-input unit tests plus a deterministic fuzz test cover every chart type.
  • Docs honesty: README/CLAUDE.md/getting-started no longer claim a ChartOptions::font field or Font::load_ttf() that don't exist yet (font injection is planned before 1.0); Make default CHART_TYPES documented correctly; OpenWrt sample PKG_VERSION aligned with the CMake project version.

  • Pie chart leader-line labels could render off-canvas for wide/near-horizontal slices — PieOptions::radius defaulted to a fixed 250.0, a non-zero value that reads as "explicitly configured" to the auto-fit logic in PieChart::render() (and the duplicated copy in src/svg_pie.cc's native SVG vector path), so the branch that reserves ~35% margin for callout labels only ran when the bare circle literally overflowed the canvas — never when the circle fit but left no room for the leader line's knee/elbow segments. A slice whose midpoint angle points near-horizontal (e.g. an exact 50% slice) extends its leader line straight out from the circle's edge; with the old default on the docs/gallery/pie_chart.png example (600×600, radius 250, only 50px margin), that leader line and its label landed past the canvas edge entirely — reproducible in examples/main.cc too, which mirrored the same radius = 250.0. Fixed by changing the default to 0.0, the same "auto" sentinel already used by center_x/center_y, so the existing margin-reserving auto-fit branch triggers for the common case of never calling set_pie_options(). An explicitly-configured non-zero radius that fits the canvas is still trusted as-is. docs/gallery/pie_chart.png regenerated; examples/main.cc's redundant explicit radius = 250.0 override removed.

  • Vertical ReferenceLine/ReferenceBand rendered mirrored (right-to-left) on the category axis — CartesianChart::PlotContext::x_scale was built as Scale(0.0, n, plot.left(), plot.right()), but Scale::to_pixel() uses the same "domain min → pixel_max, domain max → pixel_min" convention on every axis (the convention that makes higher y-values render higher on screen), so x_scale.to_pixel() mapped category index 0 to the right edge and index n to the left edge — backwards from how BarChart/LineChart/AreaChart/ComboChart actually position category geometry (plot.left() + slot*(i + 0.5), left-to-right). x_scale had no other reader anywhere in the codebase before the reference-line/band feature above started calling to_pixel() on it directly, so the bug was latent until that new code path exercised it. Fixed by swapping the pixel_min/pixel_max arguments at x_scale's single construction site in cartesian_chart.cc (ScatterChart already applies the same swap to its own x-axis scale, for the same reason). Found by manually rendering BarChart/LineChart with both a horizontal and a vertical reference line and inspecting the output — the "renders without crashing" unit test added alongside the original feature didn't catch it. Two new regression tests (test_reference_line_vertical_not_mirrored_bar/_line_chart) assert a reference line near the low end of the category domain renders left of one near the high end; both fail against the pre-fix code.
  • Makefile (OpenWrt core build) was missing src/svg_writer.cc from SRCS — any build that actually called ChartExporter::save/encode with SVG output (or just linked export.cc, which references write_svg/encode_svg unconditionally) would fail at link time. Only the CMake build had this source listed. Found while wiring src/svg_pie.cc into the same conditional-pie block.
  • Diagnosed intermittent std::bad_alloc/std::length_error reported from ChartExporter::save() — not a defect in pie_chart.cc/export.cc/chart.h (verified clean across 4000+ ASan/UBSan render/save cycles, including with PoDoFo linked). Root cause was an ABI-incompatible stale install: a cmake --install had been re-run against a build directory whose library binary was never rebuilt after ExportOptions grew from 16 to 20 bytes (Phase 0.6's svg_backend field). That crosses the x86-64 SysV ABI's struct-passing threshold (≤16 bytes passed in registers, >16 bytes passed on the stack): callers built against the newer 20-byte header pass ExportOptions on the stack, while the stale precompiled ChartExporter::save()/render_mutable() (16-byte header) read it out of registers instead, so opts.supersample came back as register garbage. When that garbage was >1, SupersampleScope multiplied the chart's width/height by it, and ImageBuffer's backing std::vector allocation blew up. Root-caused with a GDB hardware watchpoint (ASLR had to be kept enabled — GDB disables it by default, which is why the bug never reproduced under a debugger or under ASan's allocator) and confirmed by relinking the same stale .a against its matching old header (0/60 failures) vs. the mismatched new header (~50% failures). No source change required; see the new "Installing for out-of-tree consumers" note in CLAUDE.md.
  • docs/mrdocs.yml defined INSIGHTDOCK_HAS_PODOFO=0 to skip PoDoFo includes during doc generation, but export.cc guards with #ifdef INSIGHTDOCK_HAS_PODOFO (existence, not value) — so the =0 define still satisfied the guard and pulled in <podofo/podofo.h>, which isn't available at doc-gen time, aborting the whole run. Fixed by not defining the macro at all. Latent since the job was introduced; never surfaced because it always short-circuited before mrdocs was installed on the runner.

0.7.0 — 2026-06-18 — Phase 0.5 / 0.6 foundations

Added

  • PdfReportBuilder::draw_chart() / draw_chart_flow() — embed any Chart (bar/line/pie) into a PdfReportBuilder-driven report at mm coordinates, via ChartExporter::draw_pdf(). draw_chart_flow() adds an optional caption, border, and auto-pagination (ensure_space()), advancing the builder's vertical cursor like draw_table(). Closes the Phase 0.5 blocker for the Phase 1 "Multi-page PDF report builder" roadmap item. Demonstrated in examples/podofo_report_example.cc.
  • insightdock/pdf/report.h — PdfReportBuilder for multi-page PDF reports: tables, section headings, footers, pagination, and inline formatted text (PoDoFo build only).
  • Log and DateTime scale implementations — ScaleType::Log uses base-10 mapping; ScaleType::DateTime uses Unix-timestamp ticks with automatic time-step selection. TickFormat::DateTime formats axis labels as YYYY-MM-DD HH:MM.
  • OutputFormat::Svg and svg_writer.cc — SVG export via PNG raster base64-inlined in SVG XML (ChartExporter::save("chart.svg")).
  • AreaChart — area and stacked-area charts on CartesianChart (area_options().stacked).
  • Visual regression — tests/gallery_regression.cc CTest target compares renders against reference PNGs in docs/gallery/. Regenerate with UPDATE_GALLERY=1.
  • lint-tidy CI job — clang-tidy on src/*.cc with compile_commands.json (optional until toolchain is validated).
  • 32 unit tests (was 27) — log/datetime scale roundtrip and SVG encode coverage.

Added (prior Phase 0.5)

  • Makefile feature-selection flags — three variables let you trim the library for size-constrained targets such as OpenWrt routers:
  • CHART_TYPES (default pie bar line) — omit chart types you don't need; each omitted type is not compiled, and cartesian_chart.cc is skipped automatically when neither bar nor line is selected.
  • EXPORT_FORMATS (default png jpeg) — omit jpeg to strip the JPEG codec and recover ~15 KB.
  • FONT_MODE (default embedded) — set to system to drop the 20 KB baked-in TTF and load a font from the filesystem at runtime instead (see Font::load_from_file()).
  • INSIGHTDOCK_NO_EMBEDDED_FONT preprocessor guard in font.cc — when defined, Font::Font() is a no-op (font not loaded); caller must invoke Font::load_from_file() or Font::load_from_memory() before rendering.
  • INSIGHTDOCK_NO_JPEG preprocessor guard in png_writer.cc — stubs write_jpeg / encode_jpeg to return false / empty so the linker can GC the stb JPEG codec via --gc-sections.
  • INSIGHTDOCK_HAS_PIE preprocessor guard in export.cc — SupersampleScope only references PieChart and PieOptions when pie support is compiled in, preventing linker errors in pie-less builds.

Measured footprint reduction (pie + PNG + system font vs full default build):

Configuration Code section
Full (all types, embedded font, JPEG) ~287 KB
Minimal (pie only, PNG only, system font) ~90 KB
With -Os -s strip (estimated) ~45–55 KB
  • insightdock/error.h — ChartError enum + ChartResult<T> result type. ChartExporter::save() now returns ChartResult<void>; encode() returns ChartResult<std::vector<uint8_t>>. Both are contextually convertible to bool so existing if (!save(...)) callers compile unchanged.
  • insightdock/theme.h — ChartTheme struct with light() and dark() built-in presets. Chart::apply_theme(theme) batch-applies background, title, axis, grid, and text colors plus font size.
  • ScaleType enum (Linear, Log, DateTime) in scale.h with Scale::set_type() / Scale::type() accessors. API shape is established; Log and DateTime implementations follow in Phase 0.5/0.6.
  • TickFormat enum (Raw, SI, Currency, Percent) and CartesianOptions::y_tick_format in cartesian_chart.h. Custom formatting via CartesianOptions::custom_y_formatter callback. Replaces the hard-coded %.0f rendering in cartesian_chart.cc.
  • CartesianChart::draw_overlay(PlotContext&) — virtual hook called after draw_series(). Default no-op; override to add reference lines, annotations, watermarks, or combo-chart geometry without replacing the full render pipeline.
  • Font::TextMetrics struct (width, height) and Font::measure(text, size) in font.h / font.cc — measures text at an explicit pixel size without changing font state.
  • Label collision avoidance for Cartesian x-axis labels — draw_axes_and_grid() now computes a skip step so adjacent labels do not overlap at any category count.
  • tests/unit_tests.cc — 27 deterministic unit tests for Scale, Palette, ImageBuffer, ChartResult, ChartTheme, and Font::measure. Registered as unit_tests CTest target alongside the existing smoke tests.
  • CI sanitizer jobs — ASan + UBSan build and test run (clang).
  • PoDoFo CI made automatic — the PoDoFo build runs by default now instead of manual-only; checks that podofo_report_example produces a non-empty PDF.

Added

  • ComboChart — overlays grouped bar series and line series on the same Cartesian canvas; uses the draw_overlay() hook introduced in Phase 0.5. add_bar_series() / add_line_series() API mirrors the existing BarChart / LineChart interfaces. Registered in CMakeLists.txt, Makefile, and include/insightdock/charts.h.
  • examples/all_charts_raster_pdf.cc — five-page A4 PDF (Bar, Line, Area, Combo, Pie) rendered at 2× supersample via ChartExporter::draw_pdf(). Demonstrates raster-quality zoom testing for all chart types in a single document.
  • INSIGHTDOCK_HAS_PIE=1 unconditionally defined on InsightDockCharts in CMakeLists.txt — pie_chart.cc is always compiled and SupersampleScope in export.cc needs this guard to scale pie center/radius correctly at supersample > 1.

Fixed

  • Pie supersample rendering — INSIGHTDOCK_HAS_PIE was never defined in the CMake build, so the SupersampleScope pie-scaling block in src/export.cc was dead code. At supersample > 1 the canvas doubled but pie_opts_.center_x/y/radius stayed at original values, placing the pie in the upper-left corner of the scaled buffer before downsampling. Fixed by the unconditional INSIGHTDOCK_HAS_PIE=1 definition above.
  • Legend/x-axis label overlap in Cartesian charts — CartesianChart::compute_plot_rect() used a fixed 36 px bottom margin that did not account for all three stacked layers (x-axis category labels, legend row, optional x-axis title). The margin is now computed dynamically from font.line_height(): 8 + lh + 8 + (lh + 10) + x_title_h. draw_legend() positioning updated to match.
  • Pie chart label / percentage overlap — long slice labels are word-wrapped at the chart edge, drawn one line at a time (PoDoFo DrawText does not honor \n), and the percentage stays anchored to the elbow callout with wrapped lines growing upward. Fixed in raster (pie_chart.cc) and PoDoFo vector (podofo_vector.cc).
  • Inverted Y-axis — CartesianChart had pixel_min / pixel_max swapped in the Scale construction, causing bar and line charts to render with higher values at the bottom and bars growing downward from zero. Fixed by passing (plot.top(), plot.bottom()) in the correct order to Scale::linear_nice. Bar and line charts now render with the standard orientation (higher values higher on the chart).
  • ImageBuffer::pixel() OOB — the header documented "returns nullptr if out of bounds" but the implementation never checked bounds. Fixed with explicit x >= width_ || y >= height_ guards.

0.6.0 — 2026-06-05

Added

  • InsightDock Charts rebrand: namespace insightdock::charts, headers under include/insightdock/
  • Umbrella header insightdock/charts.h and PDF header insightdock/pdf/podofo.h
  • CMake project InsightDockCharts, target InsightDock::Charts, find_package(InsightDock)
  • CI pipeline (core build + CTest)
  • Smoke tests in tests/ (PNG/JPEG magic-byte checks)
  • Project metadata: LICENSE (MIT), NOTICE, CONTRIBUTING, SECURITY, brand clearance notes
  • Documentation: API policy, release checklist, benchmarks strategy, future roadmap items
  • .clang-format, .editorconfig, .gitattributes
  • insightdock/detail/math_constants.h for portable M_PI

Changed

  • Generic examples use InsightDock Charts naming
  • PoDoFo generated include dirs are PRIVATE on the library target
  • Makefile produces libInsightDockCharts.a

Removed

  • Public chartlib namespace and include/chartlib/ headers (clean break, no compatibility aliases)

[0.5.0] — prior release (chartlib)

  • ChartExporter unified export API (PNG, JPEG, PDF raster/vector)
  • Native vector PDF pies via podofo_vector
  • JPEG export via stb_image_write

[0.3.0] — prior release

  • PDF report example, vector PoDoFo pies, Tableau-10 palette