| title | BreakdownChart Specs |
|---|
This document captures design and implementation notes for BreakdownChart.
Note
For end-user usage and examples, see BreakdownChart.
- Status: Implemented
- Primary purpose: A segmented proportional bar (one row) with an optional legend. Useful for “parts of a whole” visualizations.
- Composition:
- optional
Titlevisual (top) - bar (always 1 row)
- legend (above or below the bar)
- optional
- Interaction:
- hover shows a tooltip per segment
- click raises a routed event with the segment + index
BreakdownChart : Visual
Segments : BindableList<BreakdownSegment>(read-only property)- The chart’s segment collection (bindable list with dependency tracking).
Title : Visual?LegendPlacement : BreakdownLegendPlacement(Above/Below)ShowPercentages : bool(default:true)ShowValues : bool(default:false)
SegmentClicked(bubble routing)BreakdownSegmentClickedEventArgscarries:Index : intSegment : BreakdownSegment
BreakdownSegment is a state container (not a Visual) with bindables:
Value : doubleLabel : Visual?Color : Color?Tooltip : Visual?
Segments are attached/detached when added/removed from Segments so they can participate in UI element ownership:
BreakdownSegment : DispatcherObject, IVisualElement
Internally the chart hosts two child visuals (and an optional third):
BreakdownBar(renders the segmented row and handles hover/click)BreakdownLegend(renders the legend, in either compact or expanded layout)Titleis attached as a child when non-null
Child order depends on LegendPlacement:
Above:Title?,Legend,BarBelow:Title?,Bar,Legend
The chart measures:
Titlewith the full provided width and height constraints.Barwith height forced to1.Legendwith the remaining height afterTitle+ bar.
The natural size is:
- width =
max(TitleWidth, BarWidth, LegendWidth) - height =
TitleHeight + 1 + LegendHeight
Size hints:
growX = 1(the chart likes to stretch horizontally)growY = 0(height is content-driven)
Arranges:
- optional title at the top
- bar in a 1-row rectangle
- legend in the remaining rectangle
The bar draws a single line of segment cells.
From BreakdownStyle:
FillRune(default: space) — the rune stamped in segment cellsSegmentGap(default:1) — empty cells between segmentsBarStyle— base style used for the bar (defaults totheme.ControlFillStyle())
From each BreakdownSegment:
Value(negative values are treated as0for sizing)Color(optional override)
Given:
W = Bounds.Widthgap = max(0, SegmentGap)n = Segments.Count
Usable bar width is:
usable = max(0, W - gap * max(0, n - 1))
If n == 0, usable == 0, or total value is <= 0, the bar is filled with FillRune using the base style.
Otherwise:
- Compute total:
total = sum(max(0, segment.Value)) - For each segment:
- initial width =
floor((value / total) * usable)
- initial width =
- Distribute remaining cells (due to flooring) left-to-right:
- iterate segments in order, and for each segment with
Value > 0, add1width until remaining is0
- iterate segments in order, and for each segment with
This guarantees:
- segment widths sum to
usable - earlier segments receive the “rounding remainder” first
Segment cell background is chosen in order:
segment.ColorBreakdownStyle.DefaultSegmentColors(cycled)- fallback based on theme tones (Primary/Success/Warning/Error/Accent)
The bar fills:
- each segment with
FillRuneand the chosen background color - gaps with spaces and base style
- any “tail” cells (if widths don’t cover
Bounds.Width) withFillRuneand base style
Legend layout is configured by BreakdownStyle:
LegendLayout : BreakdownLegendLayoutCompact: usesWrapHStackExpanded: usesVStack(one item per row)
LegendItemSpacing : int(compact spacing, default4)LegendJustify : WrapJustify(compact justification, defaultSpaceBetween)LegendStyle : Style?(applied to legend text)LegendMutedStyle : Style?(applied to suffix text: percentages/values)
Each segment produces a LegendItem visual composed of:
- a 1x1 swatch (
■) in the segment color - the segment
Label(via aComputedVisualso changes are tracked) - an optional suffix
TextBlockcontaining:(NN%)whenShowPercentages == true- the formatted raw value when
ShowValues == true
Formatting uses Visual.ToStringValue(...), which is culture-aware (see CultureStyle).
Legend items are reused:
_itemslist grows/shrinks withSegments.Count- switching
LegendLayoutmoves the same legend item instances between the wrap layout and the expanded layout
This is important because BreakdownSegment.Label is a Visual instance: re-creating legend items while reusing the
same label instances would trigger “visual already has a parent” failures.
Hover is handled by the bar (BreakdownBar):
- pointer movement performs a segment hit test based on the same width distribution algorithm used for rendering
- when hovered segment changes, a tooltip window is updated/shown
Tooltip content selection:
BreakdownSegment.Tooltipif provided- otherwise a default tooltip is created:
Label(if non-null)- a line with
Value (Percent%)
Important
The default tooltip currently reuses BreakdownSegment.Label (a Visual) inside the tooltip tree.
A visual cannot have two parents, so if you need the legend label and the tooltip label simultaneously, provide
an explicit BreakdownSegment.Tooltip instead of relying on the default tooltip.
Tooltips are shown via TooltipWindow:
- anchored at the last pointer UI cell (
AnchorRect = 1x1) - placed above the pointer with a 1-cell offset
While a tooltip is visible, the bar registers as an IAnimatedVisual and requests animation ticks so it can close the
tooltip when hover is lost (even if no further mouse events arrive).
Click selection is press/release-based:
- on left press inside the bar: record the pressed segment index
- on left release:
- if released inside and hit-tests to the same segment index, raise
SegmentClicked - otherwise do nothing
- if released inside and hit-tests to the same segment index, raise
BreakdownStyle is resolved from the environment via BreakdownStyle.Key.
Defaults:
FillRune = ' '(segments are primarily background-color blocks)SegmentGap = 1LegendLayout = CompactLegendItemSpacing = 4LegendJustify = SpaceBetween
Tests that lock down current behavior:
src/XenoAtom.Terminal.UI.Tests/BreakdownTests.cs- legend reflow stability (no reparent exceptions)
- left-to-right remainder distribution
SegmentClickedrouted event
- Avoid reusing
BreakdownSegment.Labelfor the default tooltip (clone or render as text) to prevent parent conflicts. - Add keyboard navigation and activation (segments as focusable targets) for non-mouse terminals.
- Add an optional “selected segment” state (with style override) for dashboard use-cases.