@opendata-ai/openchart-vanilla 7.2.4 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- .oc-root,.oc-chart-root,.oc-table-wrapper,.oc-table-root,.oc-graph-wrapper,.oc-graph-root,.oc-sankey-root,.oc-tilemap-root,.oc-barlist-root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--oc-font-family:"Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--oc-font-mono:"JetBrains Mono", "Fira Code", "Cascadia Code", monospace;--oc-ease-smooth:linear(0, .157, .438, .64, .766, .85, .906, .941, .964, .978, .988, .994, .998, 1);--oc-ease-snappy:linear(0, .012, .048, .108, .194, .302, .426, .559, .69, .808, .905, .973, 1.013, 1.028, 1.023, 1.006, .984, .966, .957, .957, .964, .975, .986, .995, 1, 1.003, 1.002, 1, .998, .998, .999, 1);--oc-animation-duration:.5s;--oc-animation-stagger:80ms;--oc-annotation-delay:.2s;--oc-title-size:26px;--oc-title-weight:590;--oc-title-tracking:-.022em;--oc-subtitle-size:14px;--oc-subtitle-weight:450;--oc-source-size:11px;--oc-source-weight:450;--oc-body-size:13px;--oc-eyebrow-size:11px;--oc-eyebrow-weight:550;--oc-eyebrow-tracking:.08em;--oc-bg:#fff;--oc-card:#fff;--oc-secondary:#f4f4f5;--oc-text:#09090b;--oc-text-secondary:#3f3f46;--oc-text-muted:#71717a;--oc-text-subtle:#a1a1aa;--oc-text-faint:#d4d4d8;--oc-gridline:rgba(0,0,0,.1);--oc-axis:rgba(0,0,0,.1);--oc-border:rgba(0,0,0,.08);--oc-border-radius:2px;--oc-accent:#06b6d4;--oc-accent-strong:#0891b2;--oc-positive:#10b981;--oc-negative:#e11d48;--oc-focus:#3b82f6;--oc-space-1:4px;--oc-space-2:8px;--oc-space-3:12px;--oc-space-4:16px;--oc-space-6:24px;--oc-space-8:32px;--oc-hover-bg:rgba(0,0,0,.024);--oc-tooltip-bg:rgba(255,255,255,.88);--oc-tooltip-border:rgba(0,0,0,.08);--oc-tooltip-shadow:0 2px 8px rgba(0,0,0,.08), 0 0 1px rgba(0,0,0,.12);--oc-tooltip-text:#09090b;--oc-legend-text:#3f3f46}.oc-dark{--oc-bg:#09090b;--oc-card:#111113;--oc-secondary:#27272a;--oc-text:#f7f8f8;--oc-text-secondary:#d0d6e0;--oc-text-muted:#a1a1aa;--oc-text-subtle:#71717a;--oc-text-faint:#52525b;--oc-gridline:rgba(255,255,255,.05);--oc-axis:rgba(255,255,255,.1);--oc-border:rgba(255,255,255,.1);--oc-accent:#06b6d4;--oc-accent-strong:#06b6d4;--oc-positive:#34d399;--oc-negative:#fb7185;--oc-focus:#60a5fa;--oc-hover-bg:rgba(255,255,255,.05);--oc-tooltip-bg:rgba(17,17,19,.92);--oc-tooltip-border:rgba(255,255,255,.08);--oc-tooltip-shadow:0 2px 8px rgba(0,0,0,.3), 0 0 1px rgba(0,0,0,.4);--oc-tooltip-text:#f7f8f8;--oc-legend-text:#d0d6e0}.oc-chart-root,.oc-table-root,.oc-graph-root,.oc-sankey-root,.oc-barlist-root{width:100%;height:100%}.oc-tilemap-root{width:100%}.oc-table-root{overflow:auto}.oc-chart{font-family:var(--oc-font-family);width:100%;display:block}.oc-tilemap{width:100%;height:auto;display:block}.oc-barlist{width:100%;display:block}.oc-sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.oc-editable-hover{outline-offset:2px;border-radius:2px;outline:1.5px solid rgba(79,70,229,.35)}.oc-chrome{font-family:var(--oc-font-family)}.oc-eyebrow{font-size:var(--oc-eyebrow-size);font-weight:var(--oc-eyebrow-weight);letter-spacing:var(--oc-eyebrow-tracking);text-transform:uppercase;fill:var(--oc-accent)}.oc-title{font-size:var(--oc-title-size);font-weight:var(--oc-title-weight);letter-spacing:var(--oc-title-tracking);fill:var(--oc-text)}.oc-subtitle{font-size:var(--oc-subtitle-size);font-weight:var(--oc-subtitle-weight);fill:var(--oc-text-muted)}.oc-source,.oc-byline,.oc-footer{font-size:var(--oc-source-size);font-weight:var(--oc-source-weight);fill:var(--oc-text-muted)}.oc-brand{letter-spacing:.02em;fill:var(--oc-text-faint);font-size:11px;font-weight:510}.oc-brand-dot,.oc-eyebrow-dot{fill:var(--oc-accent)}.oc-metrics{font-family:var(--oc-font-family)}.oc-metric-label{letter-spacing:.08em;text-transform:uppercase;fill:var(--oc-text-muted);font-size:10px;font-weight:510}.oc-metric-value{letter-spacing:-.01em;fill:var(--oc-text);font-variant-numeric:tabular-nums;font-size:22px;font-weight:510}.oc-metric-delta-up{fill:var(--oc-positive);font-size:12px;font-weight:510}.oc-metric-delta-down{fill:var(--oc-negative);font-size:12px;font-weight:510}.oc-metric-secondary{fill:var(--oc-positive);font-size:12px;font-weight:400}.oc-axis-tick-inline{fill:var(--oc-text-muted);font-size:11px;font-weight:400}.oc-endpoint-labels{font-family:var(--oc-font-family)}.oc-endpoint-label{fill:var(--oc-endpoint-label-color,var(--oc-text))}.oc-endpoint-value{fill:var(--oc-endpoint-value-color,var(--oc-text-muted))}.oc-endpoint-leader{stroke:var(--oc-endpoint-leader-color,currentColor)}.oc-annotation-subtitle{fill:var(--oc-annotation-subtitle-color,var(--oc-text-muted))}.oc-table-footer-row{align-items:baseline;gap:16px;padding:16px 16px 4px;display:flex}.oc-chrome-footer{padding-top:0}.oc-table-footer-row .oc-table-ref{text-align:right;flex-shrink:0;margin-left:auto}.oc-tooltip{pointer-events:none;z-index:1000;background:var(--oc-tooltip-bg);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);border:1px solid var(--oc-tooltip-border);border-radius:var(--oc-border-radius,4px);box-shadow:var(--oc-tooltip-shadow);color:var(--oc-tooltip-text);font-family:var(--oc-font-family);min-width:160px;max-width:280px;padding:0;font-size:12px;line-height:1.4;transition:left .1s ease-in-out,top .1s ease-in-out;animation:.12s ease-out oc-tooltip-in;display:none;position:absolute}.oc-tooltip .oc-tooltip-header{align-items:center;gap:8px;padding:8px 12px 6px;display:flex}.oc-tooltip .oc-tooltip-dot{border-radius:50%;flex-shrink:0;width:8px;height:8px}.oc-tooltip .oc-tooltip-title{letter-spacing:.04em;text-transform:uppercase;color:var(--oc-text-muted);white-space:nowrap;text-overflow:ellipsis;font-size:11px;font-weight:590;overflow:hidden}.oc-tooltip .oc-tooltip-body{border-top:1px solid var(--oc-tooltip-border);padding:6px 12px 10px}.oc-tooltip .oc-tooltip-body:first-child{border-top:none;padding-top:10px}.oc-tooltip .oc-tooltip-row{justify-content:space-between;align-items:baseline;gap:16px;padding:2px 0;display:flex}.oc-tooltip .oc-tooltip-row-swatch{border-radius:50%;flex-shrink:0;width:8px;height:8px;margin-right:6px;display:inline-block;transform:translateY(-1px)}.oc-tooltip .oc-tooltip-label{color:var(--oc-text-secondary);white-space:nowrap;flex-shrink:0;align-items:center;font-size:12px;font-weight:400;display:inline-flex}.oc-tooltip .oc-tooltip-value{font-variant-numeric:tabular-nums;color:var(--oc-tooltip-text);text-align:right;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:510;overflow:hidden}.oc-crosshair{pointer-events:none;transition:x1 50ms ease-in-out,x2 50ms ease-in-out}.oc-snap-dots circle{pointer-events:none;transition:cx 50ms ease-in-out,cy 50ms ease-in-out}@media (prefers-reduced-motion:reduce){.oc-tooltip,.oc-crosshair,.oc-snap-dots circle{transition:none}}.oc-legend{font-family:var(--oc-font-family);font-size:var(--oc-body-size)}.oc-legend-entry{cursor:default}.oc-legend text{fill:var(--oc-legend-text)}.oc-table-wrapper{font-family:var(--oc-font-family);color:var(--oc-text);background:var(--oc-bg)}.oc-table-wrapper>.oc-chrome{margin-bottom:16px;padding-left:16px;padding-right:16px}.oc-table-wrapper>.oc-chrome:first-child{padding-top:4px}.oc-table-wrapper table{border-collapse:collapse;width:100%}.oc-table-wrapper th{text-align:left;border-bottom:1px solid var(--oc-border);padding:10px 16px}.oc-table-wrapper td{text-align:left;border-bottom:1px solid var(--oc-border);padding:10px 16px}.oc-table-wrapper th{text-transform:uppercase;letter-spacing:.05em;color:var(--oc-text-secondary);white-space:nowrap;font-size:12px;font-weight:600}.oc-table-wrapper thead{z-index:2;background:var(--oc-bg);position:sticky;top:0}.oc-table-wrapper thead th{border-bottom-width:2px}.oc-table-wrapper td{font-variant-numeric:tabular-nums;font-size:14px}.oc-table-wrapper th:focus{outline:2px solid var(--oc-focus);outline-offset:-2px}.oc-table-wrapper tbody:focus{outline:none}.oc-table-title{font-size:var(--oc-title-computed-size,var(--oc-title-size));font-weight:var(--oc-title-computed-weight,var(--oc-title-weight));color:var(--oc-title-computed-color,var(--oc-text));margin-bottom:4px}.oc-table-subtitle{font-size:var(--oc-subtitle-computed-size,var(--oc-subtitle-size));font-weight:var(--oc-subtitle-computed-weight,var(--oc-subtitle-weight));color:var(--oc-subtitle-computed-color,var(--oc-text-secondary));margin-bottom:8px}.oc-table-source{font-size:var(--oc-source-computed-size,var(--oc-source-size));color:var(--oc-source-computed-color,var(--oc-text-muted))}.oc-table-footer-text{font-size:var(--oc-footer-computed-size,var(--oc-source-size));color:var(--oc-footer-computed-color,var(--oc-text-muted))}.oc-table-scroll{overflow-x:auto}.oc-table--sticky th:first-child{z-index:1;background:var(--oc-bg);position:sticky;left:0}.oc-table--sticky td:first-child{z-index:1;background:var(--oc-bg);position:sticky;left:0}.oc-table-sort-btn{cursor:pointer;vertical-align:middle;background:0 0;border:none;flex-direction:column;align-items:center;gap:2px;margin-left:6px;padding:2px;display:inline-flex}.oc-table-sort-btn:before{content:"";border-left:5px solid transparent;border-right:5px solid transparent;width:0;height:0;transition:opacity .15s,border-color .15s;display:block}.oc-table-sort-btn:after{content:"";border-left:5px solid transparent;border-right:5px solid transparent;width:0;height:0;transition:opacity .15s,border-color .15s;display:block}.oc-table-sort-btn:before{border-bottom:4.5px solid var(--oc-text-secondary);opacity:.45}.oc-table-sort-btn:after{border-top:4.5px solid var(--oc-text-secondary);opacity:.45}.oc-table-sort-btn:hover:before{opacity:.75}.oc-table-sort-btn:hover:after{opacity:.75}th[aria-sort=ascending] .oc-table-sort-btn:before{opacity:1;border-bottom-color:var(--oc-text)}th[aria-sort=ascending] .oc-table-sort-btn:after{opacity:.15}th[aria-sort=descending] .oc-table-sort-btn:after{opacity:1;border-top-color:var(--oc-text)}th[aria-sort=descending] .oc-table-sort-btn:before{opacity:.15}.oc-table-search{padding:8px 0}.oc-table-search input{border:1px solid var(--oc-border);background:var(--oc-bg);width:100%;color:var(--oc-text);box-sizing:border-box;border-radius:6px;padding:8px 12px;font-family:inherit;font-size:13px;transition:border-color .15s}.oc-table-search input::-ms-input-placeholder{color:var(--oc-text-muted);font-size:13px}.oc-table-search input::placeholder{color:var(--oc-text-muted);font-size:13px}.oc-table-search input:focus{border-color:var(--oc-focus);outline:none;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.oc-table-pagination{color:var(--oc-text-secondary);justify-content:space-between;align-items:center;padding:12px 0 4px;font-size:13px;display:flex}.oc-table-pagination button{border:1px solid var(--oc-border);background:var(--oc-bg);color:var(--oc-text);cursor:pointer;border-radius:6px;padding:6px 14px;font-family:inherit;font-size:13px;transition:background .15s,border-color .15s}.oc-table-pagination button:disabled{opacity:.35;cursor:not-allowed}.oc-table-pagination button:hover:not(:disabled){background:var(--oc-hover-bg);border-color:var(--oc-axis)}.oc-table-pagination button:focus-visible{outline:2px solid var(--oc-focus);outline-offset:1px}.oc-table-pagination-info{font-variant-numeric:tabular-nums}.oc-table-pagination-btns{gap:8px;display:flex}.oc-table-bar{position:relative}.oc-table-bar-fill{opacity:.15;pointer-events:none;border-radius:2px;position:absolute;top:6px;bottom:6px;left:0}.oc-table-bar-value{z-index:1;position:relative}.oc-table-sparkline{width:100%;display:block;position:relative}.oc-table-sparkline svg{width:100%;display:block;overflow:visible}.oc-table-sparkline-dot{border-radius:50%;width:5px;height:5px;position:absolute}.oc-table-sparkline-labels{justify-content:space-between;font-size:11px;line-height:1;display:flex}.oc-table-image{vertical-align:middle;display:inline-block}.oc-table-image img{object-fit:cover}.oc-table-image-rounded img{border-radius:50%}.oc-table-flag{font-size:1.2em}.oc-table--compact th{padding:4px 8px;font-size:13px}.oc-table--compact td{padding:4px 8px;font-size:13px}.oc-table--compact th{font-size:11px}.oc-table--clickable tbody tr{cursor:pointer}.oc-table--clickable tbody tr:hover{background:var(--oc-hover-bg)}.oc-table-cell-focus{outline:2px solid var(--oc-focus);outline-offset:-2px}.oc-table-empty{text-align:center;color:var(--oc-text-secondary);padding:32px 16px;font-size:14px;font-style:italic}.oc-table-wrapper.oc-animate>.oc-chrome{animation:oc-enter-fade calc(var(--oc-animation-duration) * .6) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-table-wrapper.oc-animate thead{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .4) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-table-wrapper.oc-animate tbody tr{animation:oc-table-enter-row var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0))}.oc-table-wrapper.oc-animate tbody td{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0))}.oc-table-wrapper.oc-animate td.oc-table-heatmap{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .7) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .3)}.oc-table-wrapper.oc-animate td.oc-table-category{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .7) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .3)}.oc-table-wrapper.oc-animate .oc-table-bar-fill{animation:oc-table-enter-bar-fill calc(var(--oc-animation-duration) * .8) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .3)}.oc-table-wrapper.oc-animate .oc-table-sparkline>svg{animation:oc-enter-line calc(var(--oc-animation-duration) * .8) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .4)}.oc-table-wrapper.oc-animate .oc-table-sparkline-dot{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .3) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .8)}.oc-table-wrapper.oc-animate .oc-table-sparkline-labels{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .3) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .8)}.oc-table-wrapper.oc-animate .oc-table-search{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-table-wrapper.oc-animate .oc-table-pagination{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-graph-wrapper{background:var(--oc-bg);font-family:var(--oc-font-family);width:100%;height:100%;position:relative;overflow:hidden}.oc-graph-canvas{cursor:grab;width:100%;height:100%;display:block}.oc-graph-canvas--dragging{cursor:grabbing}.oc-graph-chrome{z-index:2;pointer-events:none;padding:16px 16px 8px;position:absolute;top:0;left:0;right:0}.oc-graph-chrome .oc-title{font-size:var(--oc-title-size);font-weight:var(--oc-title-weight);letter-spacing:var(--oc-title-tracking);color:var(--oc-text);--_stroke:color-mix(in srgb, var(--oc-bg) 80%, transparent);text-shadow:-2px -2px 0 var(--_stroke), 2px -2px 0 var(--_stroke), -2px 2px 0 var(--_stroke), 2px 2px 0 var(--_stroke), 0 -2px 0 var(--_stroke), 0 2px 0 var(--_stroke), -2px 0 0 var(--_stroke), 2px 0 0 var(--_stroke);margin:0 0 4px}.oc-graph-chrome .oc-subtitle{font-size:var(--oc-subtitle-size);color:var(--oc-text-secondary);--_stroke:color-mix(in srgb, var(--oc-bg) 80%, transparent);text-shadow:-1px -1px 0 var(--_stroke), 1px -1px 0 var(--_stroke), -1px 1px 0 var(--_stroke), 1px 1px 0 var(--_stroke);margin:0}.oc-graph-legend{background:var(--oc-bg);border:1px solid var(--oc-border);border-radius:var(--oc-border-radius);color:var(--oc-text-secondary);max-height:200px;padding:8px 12px;font-size:12px;position:absolute;top:8px;right:8px;overflow-y:auto}.oc-graph-legend-item{align-items:center;gap:6px;padding:2px 0;display:flex}.oc-graph-legend-swatch{border-radius:50%;flex-shrink:0;width:10px;height:10px}.oc-graph-search{position:absolute;top:8px;left:8px}.oc-graph-search input{font-family:var(--oc-font-family);font-size:var(--oc-body-size);border:1px solid var(--oc-border);border-radius:var(--oc-border-radius);background:var(--oc-bg);color:var(--oc-text);outline:none;padding:6px 10px}.oc-graph-search input:focus{border-color:var(--oc-focus);box-shadow:0 0 0 2px rgba(59,130,246,.25)}.oc-dark .oc-graph-wrapper,.oc-graph-wrapper.oc-dark{--oc-bg:#0d1117}.oc-dark .oc-graph-legend,.oc-dark.oc-graph-wrapper .oc-graph-legend,.oc-dark .oc-graph-search input{background:rgba(13,17,23,.85);border-color:rgba(255,255,255,.1)}.oc-chart[data-display=sparkline]{margin:0;padding:0;display:block}@keyframes oc-enter-bar{0%{clip-path:inset(100% 0 0);opacity:0}75%{opacity:1}to{clip-path:inset(0);opacity:1}}@keyframes oc-enter-bar-h{0%{clip-path:inset(0 100% 0 0);opacity:0}75%{opacity:1}to{clip-path:inset(0);opacity:1}}@keyframes oc-enter-line{0%{clip-path:inset(0 100% 0 0);opacity:0}15%{opacity:1}to{clip-path:inset(0);opacity:1}}@keyframes oc-enter-point{0%{opacity:0;transform:scale(.3)}to{opacity:1;transform:scale(1)}}@keyframes oc-enter-fade-only{0%{opacity:0}to{opacity:1}}@keyframes oc-enter-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes oc-table-enter-row{0%{transform:translateY(6px)}to{transform:translateY(0)}}@keyframes oc-table-enter-bar-fill{0%{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0)}}@keyframes oc-tooltip-in{0%{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}.oc-animate .oc-mark-rect rect{animation:oc-enter-bar var(--oc-animation-duration) var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-bar rect{animation:oc-enter-bar var(--oc-animation-duration) var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-rect[data-orient=horizontal] rect{animation-name:oc-enter-bar-h}.oc-animate .oc-mark-bar[data-orient=horizontal] rect{animation-name:oc-enter-bar-h}.oc-animate .oc-mark-rect[data-stack-pos] rect{animation-duration:var(--oc-stack-segment-duration,.15s);animation-timing-function:linear;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0) + var(--oc-stack-pos,0) * var(--oc-stack-segment-duration,.15s))}.oc-animate .oc-mark-line{animation:oc-enter-line var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-area{animation:oc-enter-line var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-arc{animation:oc-enter-fade-only var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate circle.oc-mark-point{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .4) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate circle.oc-mark-circle{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .4) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-line~circle.oc-mark-point{animation-delay:calc(var(--oc-animation-duration) * .35 + var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-area~circle.oc-mark-point{animation-delay:calc(var(--oc-animation-duration) * .35 + var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-text text{animation:oc-enter-fade calc(var(--oc-animation-duration) * .6) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-rule line{animation:oc-enter-fade calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-tick line{animation:oc-enter-fade calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-label{animation:oc-enter-fade .3s var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0) + var(--oc-animation-duration) * .7)}.oc-animate .oc-annotation{animation:oc-enter-fade .4s var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-duration) + var(--oc-annotation-delay,.2s))}.oc-animate .oc-tilemap-tile{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .35) ease-in both;animation-delay:var(--oc-tile-delay,0s)}.oc-animate .oc-sankey-node rect{animation:oc-enter-fade-only var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-sankey-link path{animation:oc-enter-fade-only var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-duration) * .3 + var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-barlist-row{animation:oc-enter-fade calc(var(--oc-animation-duration) * .6) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:var(--oc-row-delay,0s)}.oc-animate .oc-barlist-bar{animation:oc-enter-bar-h var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:var(--oc-row-delay,0s)}.oc-animate[data-display=sparkline] .oc-mark-line,.oc-animate[data-display=sparkline] .oc-mark-area{animation-timing-function:cubic-bezier(.16,1,.3,1)}@media (prefers-reduced-motion:reduce){.oc-table-sort-btn:before,.oc-table-sort-btn:after,.oc-table-search input,.oc-table-pagination button{transition:none}.oc-animate .oc-mark-rect rect,.oc-animate .oc-mark-bar rect,.oc-animate .oc-mark-arc,.oc-animate .oc-mark-line,.oc-animate .oc-mark-area,.oc-animate circle.oc-mark-point,.oc-animate circle.oc-mark-circle,.oc-animate .oc-mark-text text,.oc-animate .oc-mark-rule line,.oc-animate .oc-mark-tick line,.oc-animate .oc-mark-label,.oc-animate .oc-annotation,.oc-animate .oc-sankey-node rect,.oc-animate .oc-sankey-link path,.oc-table-wrapper.oc-animate>.oc-chrome,.oc-table-wrapper.oc-animate thead,.oc-table-wrapper.oc-animate tbody tr,.oc-table-wrapper.oc-animate tbody td,.oc-table-wrapper.oc-animate td.oc-table-heatmap,.oc-table-wrapper.oc-animate td.oc-table-category,.oc-table-wrapper.oc-animate .oc-table-bar-fill,.oc-table-wrapper.oc-animate .oc-table-sparkline>svg,.oc-table-wrapper.oc-animate .oc-table-sparkline-dot,.oc-table-wrapper.oc-animate .oc-table-sparkline-labels,.oc-table-wrapper.oc-animate .oc-table-search,.oc-table-wrapper.oc-animate .oc-table-pagination{animation:none}}
1
+ .oc-root,.oc-chart-root,.oc-table-wrapper,.oc-table-root,.oc-graph-wrapper,.oc-graph-root,.oc-sankey-root,.oc-tilemap-root,.oc-barlist-root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%;--oc-font-family:"Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--oc-font-mono:"JetBrains Mono", "Fira Code", "Cascadia Code", monospace;--oc-ease-smooth:linear(0, .157, .438, .64, .766, .85, .906, .941, .964, .978, .988, .994, .998, 1);--oc-ease-snappy:linear(0, .012, .048, .108, .194, .302, .426, .559, .69, .808, .905, .973, 1.013, 1.028, 1.023, 1.006, .984, .966, .957, .957, .964, .975, .986, .995, 1, 1.003, 1.002, 1, .998, .998, .999, 1);--oc-animation-duration:.5s;--oc-animation-stagger:80ms;--oc-annotation-delay:.2s;--oc-title-size:26px;--oc-title-weight:590;--oc-title-tracking:-.022em;--oc-subtitle-size:14px;--oc-subtitle-weight:450;--oc-source-size:11px;--oc-source-weight:450;--oc-body-size:13px;--oc-eyebrow-size:11px;--oc-eyebrow-weight:550;--oc-eyebrow-tracking:.08em;--oc-bg:#fff;--oc-card:#fff;--oc-secondary:#f4f4f5;--oc-text:#09090b;--oc-text-secondary:#3f3f46;--oc-text-muted:#71717a;--oc-text-subtle:#a1a1aa;--oc-text-faint:#d4d4d8;--oc-gridline:rgba(0,0,0,.1);--oc-axis:rgba(0,0,0,.1);--oc-border:rgba(0,0,0,.08);--oc-border-radius:2px;--oc-accent:#06b6d4;--oc-accent-strong:#0891b2;--oc-positive:#10b981;--oc-negative:#e11d48;--oc-focus:#3b82f6;--oc-space-1:4px;--oc-space-2:8px;--oc-space-3:12px;--oc-space-4:16px;--oc-space-6:24px;--oc-space-8:32px;--oc-hover-bg:rgba(0,0,0,.024);--oc-tooltip-bg:rgba(255,255,255,.88);--oc-tooltip-border:rgba(0,0,0,.08);--oc-tooltip-shadow:0 2px 8px rgba(0,0,0,.08), 0 0 1px rgba(0,0,0,.12);--oc-tooltip-text:#09090b;--oc-legend-text:#3f3f46}.oc-dark{--oc-bg:#09090b;--oc-card:#111113;--oc-secondary:#27272a;--oc-text:#f7f8f8;--oc-text-secondary:#d0d6e0;--oc-text-muted:#a1a1aa;--oc-text-subtle:#71717a;--oc-text-faint:#52525b;--oc-gridline:rgba(255,255,255,.05);--oc-axis:rgba(255,255,255,.1);--oc-border:rgba(255,255,255,.1);--oc-accent:#06b6d4;--oc-accent-strong:#06b6d4;--oc-positive:#34d399;--oc-negative:#fb7185;--oc-focus:#60a5fa;--oc-hover-bg:rgba(255,255,255,.05);--oc-tooltip-bg:rgba(17,17,19,.92);--oc-tooltip-border:rgba(255,255,255,.08);--oc-tooltip-shadow:0 2px 8px rgba(0,0,0,.3), 0 0 1px rgba(0,0,0,.4);--oc-tooltip-text:#f7f8f8;--oc-legend-text:#d0d6e0}.oc-chart-root,.oc-table-root,.oc-graph-root,.oc-sankey-root,.oc-barlist-root{width:100%;height:100%}.oc-tilemap-root{width:100%}.oc-table-root{overflow:auto}.oc-chart{font-family:var(--oc-font-family);width:100%;display:block}.oc-tilemap{width:100%;height:auto;display:block}.oc-barlist{width:100%;display:block}.oc-sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.oc-editable-hover{outline-offset:2px;border-radius:2px;outline:1.5px solid rgba(79,70,229,.35)}.oc-chrome{font-family:var(--oc-font-family)}.oc-eyebrow{font-size:var(--oc-eyebrow-size);font-weight:var(--oc-eyebrow-weight);letter-spacing:var(--oc-eyebrow-tracking);text-transform:uppercase;fill:var(--oc-accent)}.oc-title{font-size:var(--oc-title-size);font-weight:var(--oc-title-weight);letter-spacing:var(--oc-title-tracking);fill:var(--oc-text)}.oc-subtitle{font-size:var(--oc-subtitle-size);font-weight:var(--oc-subtitle-weight);fill:var(--oc-text-muted)}.oc-source,.oc-byline,.oc-footer{font-size:var(--oc-source-size);font-weight:var(--oc-source-weight);fill:var(--oc-text-muted)}.oc-brand{letter-spacing:.02em;fill:var(--oc-text-faint);font-size:11px;font-weight:510}.oc-brand-dot,.oc-eyebrow-dot{fill:var(--oc-accent)}.oc-metrics{font-family:var(--oc-font-family)}.oc-metric-label{letter-spacing:.08em;text-transform:uppercase;fill:var(--oc-text-muted);font-size:10px;font-weight:510}.oc-metric-value{letter-spacing:-.01em;fill:var(--oc-text);font-variant-numeric:tabular-nums;font-size:22px;font-weight:510}.oc-metric-delta-up{fill:var(--oc-positive);font-size:12px;font-weight:510}.oc-metric-delta-down{fill:var(--oc-negative);font-size:12px;font-weight:510}.oc-metric-secondary{fill:var(--oc-positive);font-size:12px;font-weight:400}.oc-axis-tick-inline{fill:var(--oc-text-muted);font-size:11px;font-weight:400}.oc-endpoint-labels{font-family:var(--oc-font-family)}.oc-endpoint-label{fill:var(--oc-endpoint-label-color,var(--oc-text))}.oc-endpoint-value{fill:var(--oc-endpoint-value-color,var(--oc-text-muted))}.oc-endpoint-leader{stroke:var(--oc-endpoint-leader-color,currentColor)}.oc-annotation-subtitle{fill:var(--oc-annotation-subtitle-color,var(--oc-text-muted))}.oc-table-footer-row{align-items:baseline;gap:16px;padding:16px 16px 4px;display:flex}.oc-chrome-footer{padding-top:0}.oc-table-footer-row .oc-table-ref{text-align:right;flex-shrink:0;margin-left:auto}.oc-tooltip{pointer-events:none;z-index:1000;background:var(--oc-tooltip-bg);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);border:1px solid var(--oc-tooltip-border);border-radius:var(--oc-border-radius,4px);box-shadow:var(--oc-tooltip-shadow);color:var(--oc-tooltip-text);font-family:var(--oc-font-family);min-width:160px;max-width:280px;padding:0;font-size:12px;line-height:1.4;transition:left .1s ease-in-out,top .1s ease-in-out;animation:.12s ease-out oc-tooltip-in;display:none;position:absolute}.oc-tooltip .oc-tooltip-header{align-items:center;gap:8px;padding:8px 12px 6px;display:flex}.oc-tooltip .oc-tooltip-dot{border-radius:50%;flex-shrink:0;width:8px;height:8px}.oc-tooltip .oc-tooltip-title{letter-spacing:.04em;text-transform:uppercase;color:var(--oc-text-muted);white-space:nowrap;text-overflow:ellipsis;font-size:11px;font-weight:590;overflow:hidden}.oc-tooltip .oc-tooltip-body{border-top:1px solid var(--oc-tooltip-border);padding:6px 12px 10px}.oc-tooltip .oc-tooltip-body:first-child{border-top:none;padding-top:10px}.oc-tooltip .oc-tooltip-row{justify-content:space-between;align-items:baseline;gap:16px;padding:2px 0;display:flex}.oc-tooltip .oc-tooltip-row-swatch{border-radius:50%;flex-shrink:0;width:8px;height:8px;margin-right:6px;display:inline-block;transform:translateY(-1px)}.oc-tooltip .oc-tooltip-label{color:var(--oc-text-secondary);white-space:nowrap;flex-shrink:0;align-items:center;font-size:12px;font-weight:400;display:inline-flex}.oc-tooltip .oc-tooltip-value{font-variant-numeric:tabular-nums;color:var(--oc-tooltip-text);text-align:right;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:510;overflow:hidden}.oc-crosshair{pointer-events:none;transition:x1 50ms ease-in-out,x2 50ms ease-in-out}.oc-snap-dots circle{pointer-events:none;transition:cx 50ms ease-in-out,cy 50ms ease-in-out}@media (prefers-reduced-motion:reduce){.oc-tooltip,.oc-crosshair,.oc-snap-dots circle{transition:none}}.oc-legend{font-family:var(--oc-font-family);font-size:var(--oc-body-size)}.oc-legend-entry{cursor:default}.oc-legend text{fill:var(--oc-legend-text)}.oc-table-wrapper{font-family:var(--oc-font-family);color:var(--oc-text);background:var(--oc-bg)}.oc-table-wrapper>.oc-chrome{margin-bottom:16px;padding-left:16px;padding-right:16px}.oc-table-wrapper>.oc-chrome:first-child{padding-top:4px}.oc-table-wrapper table{border-collapse:collapse;width:100%}.oc-table-wrapper th{text-align:left;border-bottom:1px solid var(--oc-border);padding:10px 16px}.oc-table-wrapper td{text-align:left;border-bottom:1px solid var(--oc-border);padding:10px 16px}.oc-table-wrapper th{text-transform:uppercase;letter-spacing:.05em;color:var(--oc-text-secondary);white-space:nowrap;font-size:12px;font-weight:600}.oc-table-wrapper thead{z-index:2;background:var(--oc-bg);position:sticky;top:0}.oc-table-wrapper thead th{border-bottom-width:2px}.oc-table-wrapper td{font-variant-numeric:tabular-nums;font-size:14px}.oc-table-wrapper th:focus{outline:2px solid var(--oc-focus);outline-offset:-2px}.oc-table-wrapper tbody:focus{outline:none}.oc-table-title{font-size:var(--oc-title-computed-size,var(--oc-title-size));font-weight:var(--oc-title-computed-weight,var(--oc-title-weight));color:var(--oc-title-computed-color,var(--oc-text));margin-bottom:4px}.oc-table-subtitle{font-size:var(--oc-subtitle-computed-size,var(--oc-subtitle-size));font-weight:var(--oc-subtitle-computed-weight,var(--oc-subtitle-weight));color:var(--oc-subtitle-computed-color,var(--oc-text-secondary));margin-bottom:8px}.oc-table-source{font-size:var(--oc-source-computed-size,var(--oc-source-size));color:var(--oc-source-computed-color,var(--oc-text-muted))}.oc-table-footer-text{font-size:var(--oc-footer-computed-size,var(--oc-source-size));color:var(--oc-footer-computed-color,var(--oc-text-muted))}.oc-table-scroll{overflow-x:auto}.oc-table--sticky th:first-child{z-index:1;background:var(--oc-bg);position:sticky;left:0}.oc-table--sticky td:first-child{z-index:1;background:var(--oc-bg);position:sticky;left:0}.oc-table-sort-btn{cursor:pointer;vertical-align:middle;background:0 0;border:none;flex-direction:column;align-items:center;gap:2px;margin-left:6px;padding:2px;display:inline-flex}.oc-table-sort-btn:before{content:"";border-left:5px solid transparent;border-right:5px solid transparent;width:0;height:0;transition:opacity .15s,border-color .15s;display:block}.oc-table-sort-btn:after{content:"";border-left:5px solid transparent;border-right:5px solid transparent;width:0;height:0;transition:opacity .15s,border-color .15s;display:block}.oc-table-sort-btn:before{border-bottom:4.5px solid var(--oc-text-secondary);opacity:.45}.oc-table-sort-btn:after{border-top:4.5px solid var(--oc-text-secondary);opacity:.45}.oc-table-sort-btn:hover:before{opacity:.75}.oc-table-sort-btn:hover:after{opacity:.75}th[aria-sort=ascending] .oc-table-sort-btn:before{opacity:1;border-bottom-color:var(--oc-text)}th[aria-sort=ascending] .oc-table-sort-btn:after{opacity:.15}th[aria-sort=descending] .oc-table-sort-btn:after{opacity:1;border-top-color:var(--oc-text)}th[aria-sort=descending] .oc-table-sort-btn:before{opacity:.15}.oc-table-search{padding:8px 0}.oc-table-search input{border:1px solid var(--oc-border);background:var(--oc-bg);width:100%;color:var(--oc-text);box-sizing:border-box;border-radius:6px;padding:8px 12px;font-family:inherit;font-size:13px;transition:border-color .15s}.oc-table-search input::-ms-input-placeholder{color:var(--oc-text-muted);font-size:13px}.oc-table-search input::placeholder{color:var(--oc-text-muted);font-size:13px}.oc-table-search input:focus{border-color:var(--oc-focus);outline:none;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.oc-table-pagination{color:var(--oc-text-secondary);justify-content:space-between;align-items:center;padding:12px 0 4px;font-size:13px;display:flex}.oc-table-pagination button{border:1px solid var(--oc-border);background:var(--oc-bg);color:var(--oc-text);cursor:pointer;border-radius:6px;padding:6px 14px;font-family:inherit;font-size:13px;transition:background .15s,border-color .15s}.oc-table-pagination button:disabled{opacity:.35;cursor:not-allowed}.oc-table-pagination button:hover:not(:disabled){background:var(--oc-hover-bg);border-color:var(--oc-axis)}.oc-table-pagination button:focus-visible{outline:2px solid var(--oc-focus);outline-offset:1px}.oc-table-pagination-info{font-variant-numeric:tabular-nums}.oc-table-pagination-btns{gap:8px;display:flex}.oc-table-bar{position:relative}.oc-table-bar-fill{opacity:.15;pointer-events:none;border-radius:2px;position:absolute;top:6px;bottom:6px;left:0}.oc-table-bar-value{z-index:1;position:relative}.oc-table-sparkline{width:100%;display:block;position:relative}.oc-table-sparkline svg{width:100%;display:block;overflow:visible}.oc-table-sparkline-dot{border-radius:50%;width:5px;height:5px;position:absolute}.oc-table-sparkline-labels{justify-content:space-between;font-size:11px;line-height:1;display:flex}.oc-table-image{vertical-align:middle;display:inline-block}.oc-table-image img{object-fit:cover}.oc-table-image-rounded img{border-radius:50%}.oc-table-flag{font-size:1.2em}.oc-table--compact th{padding:4px 8px;font-size:13px}.oc-table--compact td{padding:4px 8px;font-size:13px}.oc-table--compact th{font-size:11px}.oc-table--clickable tbody tr{cursor:pointer}.oc-table--clickable tbody tr:hover{background:var(--oc-hover-bg)}.oc-table-cell-focus{outline:2px solid var(--oc-focus);outline-offset:-2px}.oc-table-empty{text-align:center;color:var(--oc-text-secondary);padding:32px 16px;font-size:14px;font-style:italic}.oc-table-wrapper.oc-animate>.oc-chrome{animation:oc-enter-fade calc(var(--oc-animation-duration) * .6) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-table-wrapper.oc-animate thead{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .4) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-table-wrapper.oc-animate tbody tr{animation:oc-table-enter-row var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0))}.oc-table-wrapper.oc-animate tbody td{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0))}.oc-table-wrapper.oc-animate td.oc-table-heatmap{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .7) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .3)}.oc-table-wrapper.oc-animate td.oc-table-category{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .7) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .3)}.oc-table-wrapper.oc-animate .oc-table-bar-fill{animation:oc-table-enter-bar-fill calc(var(--oc-animation-duration) * .8) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .3)}.oc-table-wrapper.oc-animate .oc-table-sparkline>svg{animation:oc-enter-line calc(var(--oc-animation-duration) * .8) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .4)}.oc-table-wrapper.oc-animate .oc-table-sparkline-dot{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .3) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .8)}.oc-table-wrapper.oc-animate .oc-table-sparkline-labels{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .3) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-row-index,0) + var(--oc-animation-duration) * .8)}.oc-table-wrapper.oc-animate .oc-table-search{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-table-wrapper.oc-animate .oc-table-pagination{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both}.oc-graph-wrapper{background:var(--oc-bg);font-family:var(--oc-font-family);width:100%;height:100%;position:relative;overflow:hidden}.oc-graph-canvas{cursor:grab;width:100%;height:100%;display:block}.oc-graph-canvas--dragging{cursor:grabbing}.oc-graph-chrome{z-index:2;pointer-events:none;padding:16px 16px 8px;position:absolute;top:0;left:0;right:0}.oc-graph-chrome .oc-title{font-size:var(--oc-title-size);font-weight:var(--oc-title-weight);letter-spacing:var(--oc-title-tracking);color:var(--oc-text);--_stroke:color-mix(in srgb, var(--oc-bg) 80%, transparent);text-shadow:-2px -2px 0 var(--_stroke), 2px -2px 0 var(--_stroke), -2px 2px 0 var(--_stroke), 2px 2px 0 var(--_stroke), 0 -2px 0 var(--_stroke), 0 2px 0 var(--_stroke), -2px 0 0 var(--_stroke), 2px 0 0 var(--_stroke);margin:0 0 4px}.oc-graph-chrome .oc-subtitle{font-size:var(--oc-subtitle-size);color:var(--oc-text-secondary);--_stroke:color-mix(in srgb, var(--oc-bg) 80%, transparent);text-shadow:-1px -1px 0 var(--_stroke), 1px -1px 0 var(--_stroke), -1px 1px 0 var(--_stroke), 1px 1px 0 var(--_stroke);margin:0}.oc-graph-legend{background:var(--oc-bg);border:1px solid var(--oc-border);border-radius:var(--oc-border-radius);color:var(--oc-text-secondary);max-height:200px;padding:8px 12px;font-size:12px;position:absolute;top:8px;right:8px;overflow-y:auto}.oc-graph-legend-item{align-items:center;gap:6px;padding:2px 0;display:flex}.oc-graph-legend-swatch{border-radius:50%;flex-shrink:0;width:10px;height:10px}.oc-graph-search{position:absolute;top:8px;left:8px}.oc-graph-search input{font-family:var(--oc-font-family);font-size:var(--oc-body-size);border:1px solid var(--oc-border);border-radius:var(--oc-border-radius);background:var(--oc-bg);color:var(--oc-text);outline:none;padding:6px 10px}.oc-graph-search input:focus{border-color:var(--oc-focus);box-shadow:0 0 0 2px rgba(59,130,246,.25)}.oc-dark .oc-graph-wrapper,.oc-graph-wrapper.oc-dark{--oc-bg:#0d1117}.oc-dark .oc-graph-legend,.oc-dark.oc-graph-wrapper .oc-graph-legend,.oc-dark .oc-graph-search input{background:rgba(13,17,23,.85);border-color:rgba(255,255,255,.1)}.oc-chart[data-display=sparkline]{margin:0;padding:0;display:block}@keyframes oc-enter-bar{0%{clip-path:inset(100% 0 0);opacity:0}75%{opacity:1}to{clip-path:inset(0);opacity:1}}@keyframes oc-enter-bar-h{0%{clip-path:inset(0 100% 0 0);opacity:0}75%{opacity:1}to{clip-path:inset(0);opacity:1}}@keyframes oc-enter-line{0%{clip-path:inset(0 100% 0 0);opacity:0}15%{opacity:1}to{clip-path:inset(0);opacity:1}}@keyframes oc-enter-point{0%{opacity:0;transform:scale(.3)}to{opacity:1;transform:scale(1)}}@keyframes oc-enter-fade-only{0%{opacity:0}to{opacity:1}}@keyframes oc-enter-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes oc-table-enter-row{0%{transform:translateY(6px)}to{transform:translateY(0)}}@keyframes oc-table-enter-bar-fill{0%{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0)}}@keyframes oc-tooltip-in{0%{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}.oc-animate .oc-mark-rect rect{animation:oc-enter-bar var(--oc-animation-duration) var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-bar rect{animation:oc-enter-bar var(--oc-animation-duration) var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-rect[data-orient=horizontal] rect{animation-name:oc-enter-bar-h}.oc-animate .oc-mark-bar[data-orient=horizontal] rect{animation-name:oc-enter-bar-h}.oc-animate .oc-mark-rect[data-stack-pos] rect{animation-duration:var(--oc-stack-segment-duration,.15s);animation-timing-function:linear;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0) + var(--oc-stack-pos,0) * var(--oc-stack-segment-duration,.15s))}.oc-animate .oc-mark-line{animation:oc-enter-line var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-area{animation:oc-enter-line var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-arc{animation:oc-enter-fade-only var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate circle.oc-mark-point{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .4) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate circle.oc-mark-circle{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .4) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-line~circle.oc-mark-point{animation-delay:calc(var(--oc-animation-duration) * .35 + var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-area~circle.oc-mark-point{animation-delay:calc(var(--oc-animation-duration) * .35 + var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-text text{animation:oc-enter-fade calc(var(--oc-animation-duration) * .6) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-rule line{animation:oc-enter-fade calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-tick line{animation:oc-enter-fade calc(var(--oc-animation-duration) * .5) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-mark-label{animation:oc-enter-fade .3s var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0) + var(--oc-animation-duration) * .7)}.oc-animate .oc-annotation{animation:oc-enter-fade .4s var(--oc-ease-smooth) both;animation-delay:calc(var(--oc-animation-duration) + var(--oc-annotation-delay,.2s))}.oc-animate .oc-tilemap-tile{animation:oc-enter-fade-only calc(var(--oc-animation-duration) * .35) ease-in both;animation-delay:var(--oc-tile-delay,0s)}.oc-animate .oc-sankey-node rect{animation:oc-enter-fade-only var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-sankey-link path{animation:oc-enter-fade-only var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:calc(var(--oc-animation-duration) * .3 + var(--oc-animation-stagger) * var(--oc-mark-index,0))}.oc-animate .oc-barlist-row{animation:oc-enter-fade calc(var(--oc-animation-duration) * .6) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:var(--oc-row-delay,0s)}.oc-animate .oc-barlist-bar{animation:oc-enter-bar-h var(--oc-animation-duration) var(--oc-animation-ease,var(--oc-ease-smooth)) both;animation-delay:var(--oc-row-delay,0s)}.oc-animate[data-display=sparkline] .oc-mark-line,.oc-animate[data-display=sparkline] .oc-mark-area{animation-timing-function:cubic-bezier(.16,1,.3,1)}@media (prefers-reduced-motion:reduce){.oc-table-sort-btn:before,.oc-table-sort-btn:after,.oc-table-search input,.oc-table-pagination button{transition:none}.oc-animate .oc-mark-rect rect,.oc-animate .oc-mark-bar rect,.oc-animate .oc-mark-arc,.oc-animate .oc-mark-line,.oc-animate .oc-mark-area,.oc-animate circle.oc-mark-point,.oc-animate circle.oc-mark-circle,.oc-animate .oc-mark-text text,.oc-animate .oc-mark-rule line,.oc-animate .oc-mark-tick line,.oc-animate .oc-mark-label,.oc-animate .oc-annotation,.oc-animate .oc-sankey-node rect,.oc-animate .oc-sankey-link path,.oc-table-wrapper.oc-animate>.oc-chrome,.oc-table-wrapper.oc-animate thead,.oc-table-wrapper.oc-animate tbody tr,.oc-table-wrapper.oc-animate tbody td,.oc-table-wrapper.oc-animate td.oc-table-heatmap,.oc-table-wrapper.oc-animate td.oc-table-category,.oc-table-wrapper.oc-animate .oc-table-bar-fill,.oc-table-wrapper.oc-animate .oc-table-sparkline>svg,.oc-table-wrapper.oc-animate .oc-table-sparkline-dot,.oc-table-wrapper.oc-animate .oc-table-sparkline-labels,.oc-table-wrapper.oc-animate .oc-table-search,.oc-table-wrapper.oc-animate .oc-table-pagination{animation:none}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-vanilla",
3
- "version": "7.2.4",
3
+ "version": "7.4.0",
4
4
  "description": "Vanilla JS renderer for openchart: SVG charts, HTML tables, force-directed graphs",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -50,8 +50,8 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@floating-ui/dom": "^1.7.6",
53
- "@opendata-ai/openchart-core": "7.2.4",
54
- "@opendata-ai/openchart-engine": "7.2.4",
53
+ "@opendata-ai/openchart-core": "7.4.0",
54
+ "@opendata-ai/openchart-engine": "7.4.0",
55
55
  "d3-force": "^3.0.0",
56
56
  "d3-quadtree": "^3.0.1"
57
57
  },
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Font-race regression tests.
3
+ *
4
+ * On real devices the primary webfont (Inter via display=swap) swaps in after
5
+ * first paint, changing text metrics. These tests cover the two halves of the
6
+ * fix: the measurer honors the provided font family, and the mount recompiles
7
+ * exactly once when a late FontFaceSet resolves.
8
+ */
9
+
10
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11
+ import { createContainer } from '../__test-fixtures__/dom';
12
+ import { lineSpec } from '../__test-fixtures__/specs';
13
+ import { createMeasureText } from '../measure-text';
14
+ import { createChart } from '../mount';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Canvas ctx stub: happy-dom's getContext returns null, so stub it to capture
18
+ // the font string createMeasureText writes.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ interface FakeCtx {
22
+ font: string;
23
+ measureText(text: string): { width: number };
24
+ }
25
+
26
+ function stubCanvasContext(): FakeCtx {
27
+ const ctx: FakeCtx = {
28
+ font: '',
29
+ measureText: (text: string) => ({ width: text.length * 7 }),
30
+ };
31
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
32
+ ctx as unknown as CanvasRenderingContext2D,
33
+ );
34
+ return ctx;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Controllable FontFaceSet stub. check() reports the font missing; ready is a
39
+ // promise we resolve on demand to simulate a late font swap.
40
+ // ---------------------------------------------------------------------------
41
+
42
+ interface FakeFontFaceSet {
43
+ check: () => boolean;
44
+ ready: Promise<unknown>;
45
+ resolveReady: () => void;
46
+ }
47
+
48
+ function makeFontFaceSet(loaded: boolean): FakeFontFaceSet {
49
+ let resolveReady: () => void = () => {};
50
+ const ready = new Promise<void>((resolve) => {
51
+ resolveReady = resolve;
52
+ });
53
+ return {
54
+ check: () => loaded,
55
+ ready,
56
+ resolveReady,
57
+ };
58
+ }
59
+
60
+ function installFonts(set: FakeFontFaceSet | undefined): void {
61
+ Object.defineProperty(document, 'fonts', {
62
+ configurable: true,
63
+ value: set,
64
+ });
65
+ }
66
+
67
+ describe('createMeasureText font family', () => {
68
+ afterEach(() => {
69
+ vi.restoreAllMocks();
70
+ });
71
+
72
+ it('uses the provided font family in ctx.font', () => {
73
+ const ctx = stubCanvasContext();
74
+ const measure = createMeasureText('"My Font", Georgia, serif');
75
+
76
+ measure('hello', 14, 600);
77
+
78
+ expect(ctx.font).toBe('600 14px "My Font", Georgia, serif');
79
+ });
80
+
81
+ it('defaults to the Inter fallback stack when no family is given', () => {
82
+ const ctx = stubCanvasContext();
83
+ const measure = createMeasureText();
84
+
85
+ measure('hi', 12);
86
+
87
+ expect(ctx.font).toBe('400 12px Inter, sans-serif');
88
+ });
89
+ });
90
+
91
+ describe('createChart font-load recompile', () => {
92
+ let container: HTMLDivElement;
93
+ const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
94
+
95
+ beforeEach(() => {
96
+ container = createContainer();
97
+ });
98
+
99
+ afterEach(() => {
100
+ vi.restoreAllMocks();
101
+ if (originalFonts) {
102
+ Object.defineProperty(document, 'fonts', originalFonts);
103
+ } else {
104
+ // happy-dom has no document.fonts by default; remove our stub.
105
+ Object.defineProperty(document, 'fonts', { configurable: true, value: undefined });
106
+ }
107
+ document.body.innerHTML = '';
108
+ });
109
+
110
+ it('marks pending then ready and re-renders once when fonts resolve late', async () => {
111
+ const fonts = makeFontFaceSet(false);
112
+ installFonts(fonts);
113
+
114
+ const chart = createChart(container, lineSpec);
115
+
116
+ expect(container.dataset.ocFontsState).toBe('pending');
117
+ const genBefore = Number(container.dataset.ocRenderGen);
118
+ expect(genBefore).toBeGreaterThanOrEqual(1);
119
+
120
+ fonts.resolveReady();
121
+ await fonts.ready;
122
+ // Let the .then() microtask flush.
123
+ await Promise.resolve();
124
+
125
+ expect(container.dataset.ocFontsState).toBe('ready');
126
+ const genAfter = Number(container.dataset.ocRenderGen);
127
+ expect(genAfter).toBe(genBefore + 1);
128
+
129
+ chart.destroy();
130
+ });
131
+
132
+ it('marks ready immediately and does not re-render when fonts already loaded', async () => {
133
+ const fonts = makeFontFaceSet(true);
134
+ installFonts(fonts);
135
+
136
+ const chart = createChart(container, lineSpec);
137
+
138
+ expect(container.dataset.ocFontsState).toBe('ready');
139
+ const genBefore = Number(container.dataset.ocRenderGen);
140
+
141
+ fonts.resolveReady();
142
+ await Promise.resolve();
143
+
144
+ expect(Number(container.dataset.ocRenderGen)).toBe(genBefore);
145
+
146
+ chart.destroy();
147
+ });
148
+
149
+ it('defers ready until the post-animation recompile, not the deferred resize', async () => {
150
+ // Entrance animation running -> resize() defers to pendingResize. The
151
+ // fonts-ready flip must wait for the recompile that actually happens on the
152
+ // pendingResize replay, so 'ready' never claims final-font layout early.
153
+ vi.useFakeTimers();
154
+ try {
155
+ const fonts = makeFontFaceSet(false);
156
+ installFonts(fonts);
157
+
158
+ const animatedSpec = { ...lineSpec, animation: true as const };
159
+ const chart = createChart(container, animatedSpec);
160
+ expect(container.dataset.ocFontsState).toBe('pending');
161
+ const genBefore = Number(container.dataset.ocRenderGen);
162
+
163
+ fonts.resolveReady();
164
+ await fonts.ready;
165
+ await Promise.resolve();
166
+
167
+ // resize() deferred (animation in flight): no recompile yet, still pending.
168
+ expect(container.dataset.ocRenderGen).toBe(String(genBefore));
169
+ expect(container.dataset.ocFontsState).toBe('pending');
170
+
171
+ // Animation completes -> pendingResize replays -> recompile -> ready.
172
+ vi.runAllTimers();
173
+ expect(Number(container.dataset.ocRenderGen)).toBe(genBefore + 1);
174
+ expect(container.dataset.ocFontsState).toBe('ready');
175
+
176
+ chart.destroy();
177
+ } finally {
178
+ vi.useRealTimers();
179
+ }
180
+ });
181
+
182
+ it('does not re-render after the chart is destroyed', async () => {
183
+ const fonts = makeFontFaceSet(false);
184
+ installFonts(fonts);
185
+
186
+ const chart = createChart(container, lineSpec);
187
+ expect(container.dataset.ocFontsState).toBe('pending');
188
+ const genBefore = Number(container.dataset.ocRenderGen);
189
+
190
+ chart.destroy();
191
+
192
+ fonts.resolveReady();
193
+ await fonts.ready;
194
+ await Promise.resolve();
195
+
196
+ // Destroyed chart must not bump the render generation.
197
+ expect(Number(container.dataset.ocRenderGen)).toBe(genBefore);
198
+ });
199
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Red-locked tests for known layout bugs.
3
+ *
4
+ * These use `test.fails(...)` so they pass today (the assertion is inverted).
5
+ * When the underlying bug is fixed, the test will start failing, signaling
6
+ * that the red-lock can be converted to a normal passing test.
7
+ */
8
+
9
+ import type { CategoricalLegendLayout } from '@opendata-ai/openchart-core';
10
+ import { estimateTextWidth } from '@opendata-ai/openchart-core';
11
+ import { compileChart } from '@opendata-ai/openchart-engine';
12
+ import { describe, expect, test } from 'vitest';
13
+ import { renderLegend } from '../renderers/legend';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Helpers
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Collect distinct y-attribute values from <text> elements, sorted ascending. */
20
+ function distinctLabelYValues(svg: SVGElement): number[] {
21
+ const texts = svg.querySelectorAll('text');
22
+ const ySet = new Set<number>();
23
+ for (const t of texts) {
24
+ const y = t.getAttribute('y');
25
+ if (y != null) ySet.add(Number(y));
26
+ }
27
+ return [...ySet].sort((a, b) => a - b);
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Constants (mirrors packages/engine/src/legend/wrap.ts)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const SWATCH_SIZE = 12;
35
+ const SWATCH_GAP = 6;
36
+ const ENTRY_GAP = 16;
37
+ const ENGINE_ROW_HEIGHT = SWATCH_SIZE + 4; // 16 - what the engine reserves
38
+ const LEGEND_PADDING = 8;
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Fixtures
42
+ // ---------------------------------------------------------------------------
43
+
44
+ const entries: CategoricalLegendLayout['entries'] = [
45
+ { label: 'United States of America', color: '#1f77b4', shape: 'square', active: true },
46
+ { label: 'United Kingdom', color: '#ff7f0e', shape: 'square', active: true },
47
+ { label: 'Federal Republic of Germany', color: '#2ca02c', shape: 'square', active: true },
48
+ { label: 'French Republic', color: '#d62728', shape: 'square', active: true },
49
+ { label: 'Kingdom of Spain', color: '#9467bd', shape: 'square', active: true },
50
+ { label: 'Republic of Italy', color: '#8c564b', shape: 'square', active: true },
51
+ { label: 'Kingdom of Netherlands', color: '#e377c2', shape: 'square', active: true },
52
+ { label: 'Swiss Confederation', color: '#7f7f7f', shape: 'square', active: true },
53
+ ];
54
+
55
+ const labelStyle = {
56
+ fontSize: 11,
57
+ lineHeight: 1.3,
58
+ fontWeight: 400 as const,
59
+ fontFamily: 'sans-serif',
60
+ fill: '#333',
61
+ };
62
+
63
+ /**
64
+ * Compute row count by simulating the same wrap logic the engine uses
65
+ * (measureLegendWrap in packages/engine/src/legend/wrap.ts).
66
+ */
67
+ function computeRowCount(maxWidth: number): number {
68
+ let rowCount = 1;
69
+ let rowWidth = 0;
70
+ for (const entry of entries) {
71
+ const labelWidth = estimateTextWidth(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
72
+ const entryWidth = SWATCH_SIZE + SWATCH_GAP + labelWidth + ENTRY_GAP;
73
+ if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
74
+ rowCount++;
75
+ rowWidth = entryWidth;
76
+ } else {
77
+ rowWidth += entryWidth;
78
+ }
79
+ }
80
+ return rowCount;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Tests
85
+ // ---------------------------------------------------------------------------
86
+
87
+ describe('known layout bugs', () => {
88
+ // Red-locked: fixed by docs/plans/04-resolved-layout-contract.md
89
+ test.fails('5a.1: legend row advancement matches engine row height', () => {
90
+ const boundsWidth = 400;
91
+ const rowCount = computeRowCount(boundsWidth);
92
+ const boundsHeight = rowCount * ENGINE_ROW_HEIGHT + LEGEND_PADDING * 2;
93
+
94
+ const legendFixture: CategoricalLegendLayout = {
95
+ position: 'top',
96
+ entries,
97
+ swatchSize: SWATCH_SIZE,
98
+ swatchGap: SWATCH_GAP,
99
+ entryGap: ENTRY_GAP,
100
+ swatchChipFill: '#f0f0f0',
101
+ labelStyle,
102
+ bounds: { x: 0, y: 0, width: boundsWidth, height: boundsHeight },
103
+ };
104
+
105
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
106
+ renderLegend(svg, legendFixture);
107
+
108
+ const yValues = distinctLabelYValues(svg);
109
+ // With 4 rows we expect 4 distinct y-values. Consecutive rows should
110
+ // differ by ENGINE_ROW_HEIGHT (16). The renderer uses swatchSize + 6 = 18,
111
+ // so this assertion will fail until the drift is fixed.
112
+ expect(yValues.length).toBeGreaterThanOrEqual(2);
113
+ for (let i = 1; i < yValues.length; i++) {
114
+ const gap = yValues[i] - yValues[i - 1];
115
+ expect(gap).toBe(ENGINE_ROW_HEIGHT);
116
+ }
117
+ });
118
+
119
+ // Red-locked: fixed by docs/plans/04-resolved-layout-contract.md and docs/plans/03-measure-then-freeze-layout.md
120
+ test.fails('5a.2: rows drawn by renderer match rows reserved by engine', () => {
121
+ // Build a line spec with 8 series whose labels vary in length.
122
+ const countries = [
123
+ 'United States of America',
124
+ 'United Kingdom',
125
+ 'Federal Republic of Germany',
126
+ 'French Republic',
127
+ 'Kingdom of Spain',
128
+ 'Republic of Italy',
129
+ 'Kingdom of Netherlands',
130
+ 'Swiss Confederation',
131
+ ];
132
+
133
+ const data = countries.flatMap((country) => [
134
+ { date: '2020-01-01', value: 10, country },
135
+ { date: '2021-01-01', value: 20, country },
136
+ ]);
137
+
138
+ const spec = {
139
+ mark: 'line' as const,
140
+ data,
141
+ encoding: {
142
+ x: { field: 'date', type: 'temporal' as const },
143
+ y: { field: 'value', type: 'quantitative' as const },
144
+ color: { field: 'country', type: 'nominal' as const },
145
+ },
146
+ legend: { position: 'top' as const },
147
+ };
148
+
149
+ // Try widths 500-700 to find one where the legend wraps AND the
150
+ // bounds.width narrowing causes extra rows in the renderer.
151
+ let foundMismatch = false;
152
+
153
+ for (let width = 500; width <= 700; width += 10) {
154
+ const layout = compileChart(spec, { width, height: 400 });
155
+ const legend = layout.legend as CategoricalLegendLayout;
156
+ if (!legend || !legend.entries || legend.entries.length === 0) continue;
157
+
158
+ // Rows reserved by the engine
159
+ const effectivePadding = width < 420 ? 2 : LEGEND_PADDING;
160
+ const rowsReserved = Math.round(
161
+ (legend.bounds.height - effectivePadding * 2) / ENGINE_ROW_HEIGHT,
162
+ );
163
+ if (rowsReserved <= 1) continue; // only interesting when wrapping occurs
164
+
165
+ // Render and count rows drawn
166
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
167
+ renderLegend(svg, legend);
168
+ const yValues = distinctLabelYValues(svg);
169
+ const rowsDrawn = yValues.length;
170
+
171
+ if (rowsDrawn !== rowsReserved) {
172
+ foundMismatch = true;
173
+ // This is the bug: the renderer drew more rows than the engine reserved.
174
+ expect(rowsDrawn).toBe(rowsReserved);
175
+ return;
176
+ }
177
+ }
178
+
179
+ // If no width triggered a mismatch, fail so test.fails still inverts.
180
+ // If this path fires, widen the width range above.
181
+ expect(foundMismatch).toBe(true);
182
+ });
183
+ });
@@ -541,11 +541,13 @@ describe('axis rendering', () => {
541
541
  expect(labels.length).toBeGreaterThan(0);
542
542
 
543
543
  const pad = layout.theme.spacing.xAxisLabelPadding;
544
+ const fontSize = layout.axes.x.tickLabelStyle.fontSize;
544
545
  for (const label of labels) {
545
- // Anchored at the top edge so the gap holds regardless of font size.
546
- expect(label.getAttribute('dominant-baseline')).toBe('hanging');
547
- // The label top sits a full padding gap below the axis line.
548
- expect(Number(label.getAttribute('y'))).toBeCloseTo(axisLineY + pad, 1);
546
+ // No dominant-baseline: the renderer positions the alphabetic baseline
547
+ // directly (WebKit mishandles hanging). The label TOP still sits a full
548
+ // padding gap below the axis line; the baseline is that top + ascent.
549
+ expect(label.getAttribute('dominant-baseline')).toBeNull();
550
+ expect(Number(label.getAttribute('y'))).toBeCloseTo(axisLineY + pad + fontSize * 0.8, 1);
549
551
  }
550
552
  });
551
553
  });
@@ -24,7 +24,7 @@ import {
24
24
  type JPGExportOptions,
25
25
  type SVGExportOptions,
26
26
  } from './export';
27
- import { createMeasureText } from './measure-text';
27
+ import { createMeasureText, resolveFontFamily, scheduleFontReload } from './measure-text';
28
28
  import { observeResize } from './resize-observer';
29
29
  import { createTooltipManager, type TooltipManager } from './tooltip';
30
30
 
@@ -88,7 +88,30 @@ export function createBarList(
88
88
  let animationCleanup: (() => void) | null = null;
89
89
  let pendingResize = false;
90
90
 
91
- const measureText = createMeasureText();
91
+ // Set when webfonts have loaded and a recompile is owed. The next render()
92
+ // that recompiles flips data-oc-fonts-state to 'ready' and clears this, so
93
+ // the attribute stays honest when resize() defers to pendingResize during
94
+ // the entrance animation.
95
+ let fontsReloadPending = false;
96
+
97
+ // Apply the root class up front so getComputedStyle sees --oc-font-family
98
+ // before the text measurer is built.
99
+ container.classList.add('oc-barlist-root');
100
+
101
+ // Resolve the effective font the way compile() will: compile merges
102
+ // { ...spec.theme, ...options.theme }, so options.theme wins over the
103
+ // spec-level theme; fall back to the container's computed font. Measuring a
104
+ // different font than gets rendered desyncs layout metrics and the reload watcher.
105
+ function resolveEffectiveFont(): string {
106
+ return (
107
+ options?.theme?.fonts?.family ??
108
+ currentSpec.theme?.fonts?.family ??
109
+ resolveFontFamily(container)
110
+ );
111
+ }
112
+ let fontFamily = resolveEffectiveFont();
113
+ let measureText = createMeasureText(fontFamily);
114
+ let renderGen = 0;
92
115
 
93
116
  function getContainerDimensions(): { width: number; height: number } {
94
117
  const rect = container.getBoundingClientRect();
@@ -233,10 +256,28 @@ export function createBarList(
233
256
  }
234
257
  });
235
258
  }
259
+
260
+ renderGen += 1;
261
+ container.dataset.ocRenderGen = String(renderGen);
262
+
263
+ // This render recompiled with the loaded webfonts; publish 'ready' now
264
+ // rather than right after the fonts-ready resize() (which may have deferred
265
+ // to pendingResize during the entrance animation).
266
+ if (fontsReloadPending) {
267
+ fontsReloadPending = false;
268
+ container.dataset.ocFontsState = 'ready';
269
+ }
236
270
  }
237
271
 
238
272
  function update(newSpec: BarListSpec): void {
239
273
  currentSpec = newSpec;
274
+ // A new spec can change theme.fonts.family; rebuild the measurer so layout
275
+ // measures the font compile will actually render with.
276
+ const nextFont = resolveEffectiveFont();
277
+ if (nextFont !== fontFamily) {
278
+ fontFamily = nextFont;
279
+ measureText = createMeasureText(fontFamily);
280
+ }
240
281
  currentLayout = compile();
241
282
  render();
242
283
  }
@@ -304,7 +345,6 @@ export function createBarList(
304
345
  }
305
346
 
306
347
  // Initialize
307
- container.classList.add('oc-barlist-root');
308
348
  if (resolveDarkMode(options?.darkMode)) {
309
349
  container.classList.add('oc-dark');
310
350
  }
@@ -312,6 +352,21 @@ export function createBarList(
312
352
  currentLayout = compile();
313
353
  render(true);
314
354
 
355
+ // Recompile once after webfonts load so row labels aren't stuck measured
356
+ // against fallback metrics on real devices.
357
+ const fontsPending = scheduleFontReload(
358
+ fontFamily,
359
+ () => !destroyed,
360
+ () => {
361
+ // Mark the reload owed, then recompile. render() flips the attribute to
362
+ // 'ready' once it actually recompiles, including the pendingResize replay
363
+ // after the entrance animation finishes.
364
+ fontsReloadPending = true;
365
+ resize();
366
+ },
367
+ );
368
+ container.dataset.ocFontsState = fontsPending ? 'pending' : 'ready';
369
+
315
370
  if (options?.responsive !== false) {
316
371
  disconnectResize = observeResize(container, () => resize());
317
372
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { BarListLayout, BarListRowMark, ResolvedAnimation } from '@opendata-ai/openchart-core';
9
+ import { textAscent } from '@opendata-ai/openchart-core';
9
10
 
10
11
  const SVG_NS = 'http://www.w3.org/2000/svg';
11
12
  const XLINK_NS = 'http://www.w3.org/1999/xlink';
@@ -42,9 +43,11 @@ function renderChrome(parent: SVGElement, layout: BarListLayout): void {
42
43
 
43
44
  const isBottom = key === 'source' || key === 'byline' || key === 'footer';
44
45
  const text = createSVGElement('text');
46
+ // el.y is the TOP of the text box; convert to the alphabetic baseline
47
+ // (WebKit mishandles dominant-baseline:hanging, see renderers/chrome.ts).
45
48
  setAttrs(text, {
46
49
  x: el.x,
47
- y: isBottom ? bottomOffset + el.y : el.y,
50
+ y: (isBottom ? bottomOffset + el.y : el.y) + textAscent(el.style.fontSize),
48
51
  });
49
52
  text.setAttribute('class', `oc-${key}`);
50
53
  text.setAttribute('font-family', el.style.fontFamily);