{# Area chart region — UX-068 (cycle 28, v0.60.0). Contract: ~/.claude/skills/ux-architect/components/area-chart-region.md Stacked-area time-series from a multi-dim pivot: dim 0 = time bucket (BucketRef), dim 1 = series key (scalar / enum / FK). Consumes `pivot_buckets` + `pivot_dim_specs` so the same data path that feeds pivot_table feeds this chart too — the dimensions just get pivoted from rows to stacked series. Card safety: region emits zero chrome + zero title. #} {% from 'macros/region_wrapper.html' import region_card %} {% call region_card(title) %}
{% if pivot_buckets and pivot_dim_specs and pivot_dim_specs | length == 2 and pivot_dim_specs[0].is_time_bucket %} {% set time_spec = pivot_dim_specs[0] %} {% set series_spec = pivot_dim_specs[1] %} {% set metric_name = (pivot_buckets[0].keys() | reject('in', [time_spec.name, time_spec.name ~ '_label', series_spec.name, series_spec.name ~ '_label']) | list)[0] | default('count') %} {# Group buckets into (time_bucket_label → {series: value}). Also track distinct time buckets in order and distinct series keys. #} {% set by_bucket = {} %} {% set time_keys = [] %} {% set series_keys = [] %} {% for row in pivot_buckets %} {% set tk = row[time_spec.name ~ '_label'] %} {% set sk = row[series_spec.name ~ '_label'] if series_spec.is_fk else row[series_spec.name] %} {% if tk not in time_keys %}{% set _ = time_keys.append(tk) %}{% endif %} {% if sk not in series_keys %}{% set _ = series_keys.append(sk) %}{% endif %} {% if tk not in by_bucket %}{% set _ = by_bucket.update({tk: {}}) %}{% endif %} {% set _ = by_bucket[tk].update({sk: row[metric_name]}) %} {% endfor %} {# Stacked totals per time bucket for Y-max. Reference overlays (#883) expand the upper bound so target lines/bands above the data peak still fit inside the plot area. #} {% set max_total = 1 %} {% for tk in time_keys %} {% set tot = by_bucket[tk].values() | sum %} {% if tot > max_total %}{% set max_total = tot %}{% endif %} {% endfor %} {% set _ref_line_vals = (reference_lines or []) | map(attribute='value') | list %} {% set _ref_band_tops = (reference_bands or []) | map(attribute='to') | list %} {% set _max_candidates = [max_total] + _ref_line_vals + _ref_band_tops %} {% set max_total = _max_candidates | max %} {% set max_total = max_total if max_total > 0 else 1 %} {% set w = 460 %} {% set h = 180 %} {% set pt = 8 %} {% set pr = 8 %} {% set pb = 36 %} {% set pl = 8 %} {% set plot_w = w - pl - pr %} {% set plot_h = h - pt - pb %} {% set tcount = time_keys | length %} {# Series colour palette — cycle through HSL hues. Legend uses same. #} {% set palette = [ 'hsl(var(--primary))', 'hsl(var(--accent))', 'hsl(210, 80%, 55%)', 'hsl(145, 55%, 45%)', 'hsl(30, 85%, 55%)', 'hsl(290, 55%, 55%)', ] %} {# Reference bands — drawn before the data so the stacks sit on top. Token-driven colour (#883). #} {% set _band_colours = { 'target': 'hsl(var(--primary))', 'positive': 'hsl(145, 55%, 45%)', 'warning': 'hsl(40, 90%, 55%)', 'destructive': 'hsl(var(--destructive))', 'muted': 'hsl(var(--muted-foreground))', } %} {% for band in (reference_bands or []) %} {% set band_top_y = pt + plot_h - ((band.to / max_total) * plot_h) %} {% set band_bot_y = pt + plot_h - ((band['from'] / max_total) * plot_h) %} {% set band_h = band_bot_y - band_top_y %} {% if band_h > 0 %} {% set band_colour = _band_colours.get(band.color, _band_colours['target']) %} {{ band.label }}: {{ band['from'] }}–{{ band.to }} {% endif %} {% endfor %} {# Reference lines — horizontal markers, dashed/dotted per `style:`. #} {% set _line_dasharray = {'solid': '', 'dashed': '4,3', 'dotted': '1,3'} %} {% for ref in (reference_lines or []) %} {% set ref_y = pt + plot_h - ((ref.value / max_total) * plot_h) %} {{ ref.label }}: {{ ref.value }} {% endfor %} {% if tcount > 1 %} {% set step = plot_w / (tcount - 1) %} {# Build per-series stacked polygons. Render from top of the stack down so earlier series sit below later ones visually. #} {# Running top-of-stack per time bucket. #} {% set tops = [] %} {% for _ in time_keys %}{% set _ = tops.append(0) %}{% endfor %} {% for series_key in series_keys %} {% set colour = palette[loop.index0 % (palette | length)] %} {% set top_points = [] %} {% set bottom_points = [] %} {% for tk in time_keys %} {% set x = pl + (loop.index0 * step) %} {% set v = by_bucket[tk][series_key] | default(0) %} {% set prev_top = tops[loop.index0] %} {% set new_top = prev_top + v %} {% set _ = tops.__setitem__(loop.index0, new_top) %} {% set y_top = pt + plot_h - (new_top / max_total * plot_h) %} {% set y_bot = pt + plot_h - (prev_top / max_total * plot_h) %} {% set _ = top_points.append(x | round(2) ~ ',' ~ y_top | round(2)) %} {% set _ = bottom_points.append(x | round(2) ~ ',' ~ y_bot | round(2)) %} {% endfor %} {# Polygon = top-left → top-right → bottom-right → bottom-left (reversed). #} {% endfor %} {# X-axis tick labels — first, last, and spaced to 5. #} {% set show_every = 1 if tcount <= 5 else ((tcount / 5) | round(0, 'ceil') | int) %} {% for tk in time_keys %} {% if loop.index0 == 0 or loop.last or (loop.index0 % show_every == 0) %} {% set x = pl + (loop.index0 * step) %} {{ tk }} {% endif %} {% endfor %} {% endif %} {# Legend — colour key → series name. #}
{% for series_key in series_keys %} {% set colour = palette[loop.index0 % (palette | length)] %} {{ series_key }} {% endfor %}

{{ tcount }} buckets · {{ series_keys | length }} series · peak {{ max_total }}

{% else %}

{{ empty_message | default("No data available.") }}

{% endif %}
{% endcall %}