feat(chart): add c2-overlap-chart (Venn diagram) and fix declared series after hydration

Merged
CO
1mo ago
+3607-72

Summary

Adds c2-overlap-chart to @c2n/chart: a Venn diagram of two or three sets. Each set is a circle, and each shared area is labelled with the number of members in exactly that combination (or its share, or both). A whole set (one circle) can be highlighted or selected, including by clicking anywhere inside it with selection="set". The other circles can be restyled while one is highlighted, and custom tooltips receive the hovered region or set.

For every chart:

  • A click highlights: clicking a series in the plot, or its legend entry, brings it forward and dims the others. legend-action="toggle" restores hide/show for the legend.
  • The legend and tooltip slots are verified on every chart type.

It also fixes three existing bugs:

  • After server rendering, every chart ignored its <c2-chart-series> children and drew series guessed from its data instead.
  • point-click never fired on the uPlot charts.
  • On the uPlot charts the cursor always resolved to the first series, because the left-axis series carried an explicit scale: undefined. That overwrote uPlot's default scale, so valToPos returned NaN.

c2-overlap-chart

<c2-overlap-chart selectable selection="set" labels="count-percent" data='[{"sets":["web"],"size":18420},{"sets":["web","mobile"],"size":6880}, …]'>
  <c2-chart-series field="web" label="Web app"></c2-chart-series>
  <c2-chart-series field="mobile" label="Mobile app"></c2-chart-series>
  <c2-chart-series field="api" label="Public API"></c2-chart-series>
</c2-overlap-chart>
  • Data: one row per combination, giving the number of members in all of its sets. Missing combinations count as 0, and the chart works out the exclusive regions itself. sets-field and size-field rename the row fields. The regions getter returns the computed regions, and setDetail(key) returns one set's totals.
  • Engine: the ECharts custom series, registered as a new custom feature in the loader. @upsetjs/venn.js 2.0.0 is a new exact-pinned dependency for the proportional layout; the functions we use come to about 6 KB gzip, with no D3. It is kept external in the Vite build.
  • Region shapes: each exclusive region is one path whose outlines are wound by inclusion–exclusion, so the canvas fill and ECharts' hit-testing are both exact under the non-zero rule. A browser test checks this against isPointInPath in both layouts.
  • Options:
    • layout is proportional (the default) or uniform.
    • labels is count, percent, count-percent or none. With count-percent the share goes on a smaller second line under the count, so the label stays narrow enough to sit inside most regions.
    • format sets the number format.
  • Set names and legend:
    • By default the sets are named in the chart's legend, all in one place that legend positions (bottom by default), instead of names scattered around the circles.
    • set-labels="around" writes each set's name and total beside its circle instead; pair it with legend="none". Set names are pushed apart or moved to the free side of their circle.
    • Custom legends work as on every chart: the legend slot, renderLegendItem or a linked c2-chart-legend.
    • A legend click highlights the set. With legend-action="toggle" it switches the set off and lays the diagram out again without it; the last visible set stays on.
  • Region labels: text is measured on a canvas. A region too small for its label gets it outside with a leader line, placed clear of the other labels.
  • Regions: hovering a region shows its name, count, share and combination total, and fires region-hover; clicking fires region-click.
  • Whole sets:
    • Hovering a set's legend entry (or its name, with set-labels="around") highlights the whole circle and dims the others.
    • The highlighted circle comes forward:
      • its outline is drawn above the others with a glow in the set's colour;
      • its fill goes to --c2-chart__set__highlight--fill-opacity (0.4);
      • each part it shares with another circle is tinted in that other circle's colour at --c2-chart__set-overlap__highlight--fill-opacity (0.35, a lens path per pair). With web highlighted, the web + mobile zone mixes in mobile's colour, so the mixed zones show which set they are shared with.
    • The tooltip describes the set: members, share of the whole, and members in no other set.
    • It fires set-hover and set-click. A click highlights the set, and set-click is cancelable.
    • highlighted (shared with every chart) sets the highlight from the application.
    • Clicks that land while the chart redraws under the pointer are read from the plot's DOM click.
  • Dimmed circles:
    • The other circles keep their own colour at --c2-chart__set__dimmed--opacity (0.6) of their fill and outline. Their fill is drawn only outside the highlighted circle (differencePath), so the areas they share with it read as part of the highlight.
    • --c2-chart__set__dimmed--color repaints them in one colour, resolved through a hidden probe so var() and color-mix() values work.
    • --c2-chart__set__dimmed--fill-style fills them solid, hatch, dots or none.
  • Selection:
    • With selectable, clicking a region sets selected and clicking a set's name sets selected-set. The two replace each other. selection-change carries { selected, selectedSet }.
    • selection="set" makes a click anywhere inside a circle select that whole circle. In an overlap the smallest circle comes first, then the next on further clicks, then none.
  • Custom tooltip: it works through the tooltip slot, renderTooltip or a linked c2-chart-tooltip. renderTooltip and tooltip-change receive an OverlapTooltipContext, which carries the hovered region or set.
  • Keyboard: the plot is one tab stop. The arrow keys move through the sets, then the regions from largest to smallest. Enter selects (following selection) and Escape clears. A focused set gets a dashed ring around its name, or around its circle when names are off.
  • Accessibility: a hidden table of regions, and a live region that announces the focused set or region.
  • Limits: with more than three sets the chart shows its error state.
  • CSS variables:
    • Sets: --c2-chart__set--fill-opacity, --c2-chart__set--stroke-width, --c2-chart__set__highlight--fill-opacity and --c2-chart__set-overlap__highlight--fill-opacity.
    • Dimmed sets: --c2-chart__set__dimmed--opacity, --c2-chart__set__dimmed--color and --c2-chart__set__dimmed--fill-style.
    • Regions: --c2-chart__region__hover--opacity and --c2-chart__region__selected--opacity.
    • Labels: --c2-chart__region-label--font-size, --c2-chart__set-label--font-size and --c2-chart__set-label--font-weight.
    • Focus: --c2-chart__plot__focus--outline.

Highlight, legend and tooltip, every chart

  • Click to highlight (the default): clicking a series in the plot (a line, a bar, a scatter point, a pie slice, a stacked area band) or its legend entry highlights it, fading the others to --c2-chart__series__dimmed--opacity (0.25). A second click clears it.
    • highlighted holds the key: the series field, a pie's slice label, or an overlap chart's set key. The application can set it.
    • highlight(key) and the non-bubbling series-highlight event report it.
    • point-click fires first and is cancelable: preventDefault() keeps the highlight as it is.
    • Single-series charts (gauge, candlestick) have nothing to set apart.
  • The highlighted series comes forward instead of only staying put while the rest fades:
    • uPlot: a line 1px heavier, and a deeper fill on area charts.
    • ECharts: drawn above the others (z: 3) with a line 1px heavier.
    • Pie: a drop shadow on the slice.
  • Area charts hit-test the band under the pointer (hitMode = 'band'): the band is the closest stacked line above the cursor. So every band can be hovered and clicked, not just the top one. The other uPlot charts keep the nearest-point search, which now compares CSS pixels.
  • legend-action: highlight is the default. toggle makes a legend click hide or show the series instead, as it did before this PR. This is a behaviour change for existing charts; see the note below.
  • Slots: slot="legend" replaces the generated entries in place, and slot="tooltip" becomes the body of the chart's own tooltip bubble. A test checks both on line, area, bar, scatter, radar, pie, gauge, candlestick and overlap charts.
  • uPlot click fix: the uPlot adapter never reported clicks, so point-click never fired on line, area, bar or sparkline charts. It now reads clicks from the plot overlay and ignores the release of a drag-to-zoom.
  • Engines: uPlot dims through the series alpha, ECharts multiplies the opacity of each series' style blocks (dimSeries), and the pie fades the other slices per datum.
  • Legend entries: ChartLegendItem gains key, pressed and dimmed, and toggle runs the chart's legend action, so a linked c2-chart-legend follows the mode. The faded legend style moved from [aria-pressed='false'] to a dimmed class.

Shared chart changes

  • Protected hooks:
    • on ChartBase: handleEngineHover, handleEngineClick, tooltipContextAt, pointAt, defaultTooltip, validationError and highlightKeyAt;
    • on UplotChartBase: hitMode and highlightStyle.
  • Declared series after hydration: the chart now also reads its definitions from the light DOM on the first update, because after hydration slotchange fires before Lit adds its listener.
  • Legend entries are rendered into the legend slot after each update, not as part of the hydrated template.
  • Legend highlight hook: ChartLegendItem gains an optional highlight(active) callback for hover and focus.
  • Lazy engine updates: EchartsChartBase#lazyEngineUpdates (default true) is turned off by the overlap chart. A stress run of 40 rapid click-and-move cycles on pie, scatter and radar raised no errors.

Docs site

  • mdx-codeblock-remark.mjs: nested plain tags (c2-chart-series, c2-tab, c2-tree-item) now get capitalised attribute names, so field and label reach the HTML.
  • Behaviour change: docs charts now draw the series they declare. For example, one line-chart example on the site declares 3 series but used to draw 5.
  • The chart overview's "Slots" and "Hide or highlight from the legend" sections describe highlight as the default and toggle as the alternative, and how a highlighted series comes forward. The event demo now also shows series-highlight.
  • A new Overlap chart docs page with:
    • a "Click a circle" example (labels="count-percent", OverlapSelectionDemo.astro), where the other circles keep their own colours;
    • a gallery of 7 cards, using both the grouped legend and set-labels="around", with a grey hatched "Highlighted set" card as the opt-in alternative;
    • a landing-page preview, presets and slot-audit entries.
  • Regenerated the manifest, React/Vue types, umbrella package and skill catalog.

Behaviour change for existing charts

Legend clicks now highlight by default instead of hiding the series. An app that relies on the old behaviour sets legend-action="toggle". series-toggle still fires whenever a series is hidden or shown.

Testing

  • packages/components/chart/test plus the umbrella suite: 105 tests pass in Chromium. They cover:
    • for each of 9 chart types: slot replacement, legend highlight, click-to-highlight (and clearing), and preventDefault();
    • the new default and toggle;
    • a real uPlot click and a drag;
    • area band hits on each of three bands, with fixed and auto-ranged y axes;
    • 21 overlap-chart tests, including that each shared zone of a highlighted circle is tinted in the other set's colour, that dimmed fills stay outside the highlighted circle, count-percent labels, and the grouped legend by default versus set-labels="around".
  • apps/ui/test: all 14 tests pass, including the hydration suite.
  • ui:build, build:tools, mcp:smoke, docs:check, check:dogfood, lint, format:check and test:type-check pass on Node 24.
  • On the built docs site (DPR 2, light and dark):
    • real clicks on each band of the area chart highlight solar, wind and hydro respectively;
    • with each set of the "Click a circle" example selected, the shared zones show the other set's colour.

Known cosmetic issues:

  • On the narrow "Two products" gallery card, one leader label still sits on a circle outline.
  • With set-labels="around", a set name at the top edge of the plot gets its tooltip below the name, covering it.

🤖 Generated with Claude Code

https://claude.ai/code/session_019BEJx97gwjUM1RsPaFbTsR