@opendata-ai/openchart-vanilla 6.28.6 → 7.0.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/index.d.ts +13 -8
- package/dist/index.js +2797 -2356
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/src/__tests__/crosshair.test.ts +11 -2
- package/src/__tests__/events.test.ts +55 -10
- package/src/graph/__tests__/canvas-renderer.test.ts +1 -0
- package/src/interactions/chart-events.ts +139 -0
- package/src/interactions/crosshair.ts +228 -0
- package/src/interactions/drag-handler.ts +175 -0
- package/src/interactions/editing-drags.ts +512 -0
- package/src/interactions/index.ts +25 -0
- package/src/interactions/keyboard-nav.ts +111 -0
- package/src/interactions/legend-interaction.ts +38 -0
- package/src/interactions/selection.ts +271 -0
- package/src/interactions/tooltip-events.ts +72 -0
- package/src/mount.ts +182 -1761
- package/src/renderers/annotations.ts +82 -2
- package/src/renderers/axes.ts +18 -1
- package/src/renderers/brand.ts +7 -1
- package/src/renderers/chrome.ts +50 -3
- package/src/renderers/endpoint-labels.ts +164 -0
- package/src/renderers/legend.ts +32 -27
- package/src/renderers/marks.ts +65 -17
- package/src/renderers/metrics.ts +50 -0
- package/src/svg-renderer.ts +80 -20
- package/src/tilemap-mount.ts +6 -6
- package/src/tilemap-renderer.ts +0 -2
- package/src/tooltip.ts +27 -7
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{--oc-font-family: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:22px;--oc-title-weight:700;--oc-title-tracking:-.02em;--oc-subtitle-size:14px;--oc-subtitle-weight:400;--oc-source-size:12px;--oc-source-weight:400;--oc-body-size:13px;--oc-bg:#fff;--oc-text:#1d1d1d;--oc-text-secondary:#5c5c5c;--oc-text-muted:#999;--oc-gridline:#e8e8e8;--oc-axis:#888;--oc-border:#e2e2e2;--oc-border-radius:4px;--oc-focus:#3b82f6;--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:#1d1d1d;--oc-legend-text:#555}.oc-dark{--oc-bg:#1a1a2e;--oc-text:#e0e0e0;--oc-text-secondary:#b0b0b0;--oc-text-muted:gray;--oc-gridline:#335;--oc-axis:#999;--oc-border:#446;--oc-focus:#60a5fa;--oc-hover-bg:rgba(255,255,255,.05);--oc-tooltip-bg:rgba(30,30,50,.85);--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:#e0e0e0;--oc-legend-text:#b0b0b0}.oc-chart-root{width:100%}.oc-table-root,.oc-graph-root,.oc-sankey-root,.oc-tilemap-root,.oc-barlist-root{width:100%;height:100%}.oc-table-root{overflow:auto}.oc-chart{font-family:var(--oc-font-family);width:100%;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-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-secondary)}.oc-source,.oc-byline,.oc-footer{font-size:var(--oc-source-size);font-weight:var(--oc-source-weight);fill:var(--oc-text-muted)}.oc-chrome-footer{padding-top:16px}.oc-tooltip{pointer-events:none;z-index:1000;background:var(--oc-tooltip-bg);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--oc-tooltip-border);box-shadow:var(--oc-tooltip-shadow);color:var(--oc-tooltip-text);font-family:var(--oc-font-family);border-radius:8px;min-width:140px;max-width:260px;padding:0;font-size:12px;line-height:1.4;animation:.12s ease-out oc-tooltip-in;display:none;position:absolute}.oc-tooltip .oc-tooltip-header{align-items:center;gap:6px;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:-.01em;color:var(--oc-tooltip-text);white-space:nowrap;text-overflow:ellipsis;font-size:12px;font-weight:600;overflow:hidden}.oc-tooltip .oc-tooltip-body{border-top:1px solid var(--oc-tooltip-border);padding:4px 12px 8px}.oc-tooltip .oc-tooltip-header+.oc-tooltip-body{padding-top:6px}.oc-tooltip .oc-tooltip-body:first-child{border-top:none;padding-top:8px}.oc-tooltip .oc-tooltip-row{justify-content:space-between;align-items:baseline;gap:12px;padding:1px 0;display:flex}.oc-tooltip .oc-tooltip-label{color:var(--oc-text-muted);white-space:nowrap;flex-shrink:0;font-size:11px}.oc-tooltip .oc-tooltip-value{font-variant-numeric:tabular-nums;text-align:right;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:500;overflow:hidden}.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{--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:400;--oc-source-size:11px;--oc-source-weight:400;--oc-body-size:13px;--oc-eyebrow-size:11px;--oc-eyebrow-weight:510;--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,.06);--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-chrome-footer{padding-top:16px}.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": "
|
|
3
|
+
"version": "7.0.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": "
|
|
54
|
-
"@opendata-ai/openchart-engine": "
|
|
53
|
+
"@opendata-ai/openchart-core": "7.0.0",
|
|
54
|
+
"@opendata-ai/openchart-engine": "7.0.0",
|
|
55
55
|
"d3-force": "^3.0.0",
|
|
56
56
|
"d3-quadtree": "^3.0.1"
|
|
57
57
|
},
|
|
@@ -75,9 +75,18 @@ describe('crosshair', () => {
|
|
|
75
75
|
chart.destroy();
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it('
|
|
78
|
+
it('crosshair defaults to on for line charts (omitted spec)', () => {
|
|
79
79
|
const chart = createChart(container, lineSpecNoCrosshair);
|
|
80
80
|
|
|
81
|
+
const crosshair = container.querySelector('[data-crosshair]');
|
|
82
|
+
expect(crosshair).not.toBeNull();
|
|
83
|
+
|
|
84
|
+
chart.destroy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not create crosshair when explicitly disabled', () => {
|
|
88
|
+
const chart = createChart(container, { ...lineSpecNoCrosshair, crosshair: false });
|
|
89
|
+
|
|
81
90
|
const crosshair = container.querySelector('[data-crosshair]');
|
|
82
91
|
expect(crosshair).toBeNull();
|
|
83
92
|
|
|
@@ -102,7 +111,7 @@ describe('crosshair', () => {
|
|
|
102
111
|
|
|
103
112
|
const crosshair = container.querySelector('[data-crosshair]') as SVGLineElement;
|
|
104
113
|
expect(crosshair).not.toBeNull();
|
|
105
|
-
expect(crosshair.getAttribute('stroke-dasharray')).toBe('
|
|
114
|
+
expect(crosshair.getAttribute('stroke-dasharray')).toBe('3,3');
|
|
106
115
|
expect(crosshair.getAttribute('stroke-width')).toBe('1');
|
|
107
116
|
expect(crosshair.getAttribute('pointer-events')).toBe('none');
|
|
108
117
|
|
|
@@ -112,7 +112,7 @@ describe('chart event handlers', () => {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
describe('onLegendToggle', () => {
|
|
115
|
-
it('fires when a legend entry is clicked', () => {
|
|
115
|
+
it('fires when a legend entry is clicked, and toggling back restores the same series', () => {
|
|
116
116
|
const onLegendToggle = vi.fn();
|
|
117
117
|
const chart = createChart(
|
|
118
118
|
container,
|
|
@@ -122,19 +122,64 @@ describe('chart event handlers', () => {
|
|
|
122
122
|
},
|
|
123
123
|
);
|
|
124
124
|
|
|
125
|
-
const
|
|
126
|
-
expect(
|
|
125
|
+
const firstEntry = container.querySelector('[data-legend-index]') as HTMLElement | null;
|
|
126
|
+
expect(firstEntry).not.toBeNull();
|
|
127
|
+
const targetLabel = firstEntry!.getAttribute('data-legend-label')!;
|
|
128
|
+
expect(targetLabel).toBeTypeOf('string');
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
firstEntry!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
129
131
|
|
|
130
132
|
expect(onLegendToggle).toHaveBeenCalledTimes(1);
|
|
131
|
-
const [
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const [hiddenSeries, visibleAfterFirst] = onLegendToggle.mock.calls[0];
|
|
134
|
+
expect(hiddenSeries).toBe(targetLabel);
|
|
135
|
+
expect(visibleAfterFirst).toBe(false);
|
|
136
|
+
|
|
137
|
+
// Toggling triggers a recompile (so the y-scale rebalances and any
|
|
138
|
+
// endpoint labels for the hidden series clear), which replaces the
|
|
139
|
+
// legend DOM. Find the same entry by label and click it again.
|
|
140
|
+
const sameEntry = container.querySelector(
|
|
141
|
+
`[data-legend-label="${targetLabel}"]`,
|
|
142
|
+
) as HTMLElement | null;
|
|
143
|
+
expect(sameEntry).not.toBeNull();
|
|
144
|
+
sameEntry!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
135
145
|
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
expect(onLegendToggle).toHaveBeenCalledTimes(2);
|
|
147
|
+
const [shownSeries, visibleAfterSecond] = onLegendToggle.mock.calls[1];
|
|
148
|
+
expect(shownSeries).toBe(targetLabel); // same series, not the other one
|
|
149
|
+
expect(visibleAfterSecond).toBe(true);
|
|
150
|
+
|
|
151
|
+
chart.destroy();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('refuses to hide the last visible series', () => {
|
|
155
|
+
// Two-series chart: hide one, then attempt to hide the other.
|
|
156
|
+
// The second click should be a no-op (no toggle callback for the hide).
|
|
157
|
+
const onLegendToggle = vi.fn();
|
|
158
|
+
const chart = createChart(
|
|
159
|
+
container,
|
|
160
|
+
{ ...lineSpec, legend: { show: true } },
|
|
161
|
+
{ onLegendToggle },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const entries = container.querySelectorAll('[data-legend-label]');
|
|
165
|
+
expect(entries.length).toBeGreaterThanOrEqual(2);
|
|
166
|
+
const firstLabel = (entries[0] as HTMLElement).getAttribute('data-legend-label')!;
|
|
167
|
+
|
|
168
|
+
// Hide the first one.
|
|
169
|
+
(entries[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
170
|
+
expect(onLegendToggle).toHaveBeenCalledTimes(1);
|
|
171
|
+
|
|
172
|
+
// Click the *other* (still-visible) series. The guard should refuse.
|
|
173
|
+
const remaining = Array.from(container.querySelectorAll('[data-legend-label]')).find(
|
|
174
|
+
(el) => (el as HTMLElement).getAttribute('data-legend-label') !== firstLabel,
|
|
175
|
+
) as HTMLElement | undefined;
|
|
176
|
+
expect(remaining).toBeTruthy();
|
|
177
|
+
remaining!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
178
|
+
|
|
179
|
+
// The handler still fires (the click bubbled and the wired listener ran),
|
|
180
|
+
// but the visible flag should report that nothing was actually hidden.
|
|
181
|
+
// Implementation detail: we expect the second call to report visible=true
|
|
182
|
+
// (the series is still visible because the toggle was refused).
|
|
138
183
|
expect(onLegendToggle).toHaveBeenCalledTimes(2);
|
|
139
184
|
const [, visibleAfter] = onLegendToggle.mock.calls[1];
|
|
140
185
|
expect(visibleAfter).toBe(true);
|
|
@@ -98,6 +98,7 @@ function makeTheme(isDark = false): ResolvedTheme {
|
|
|
98
98
|
},
|
|
99
99
|
borderRadius: 4,
|
|
100
100
|
chrome: {
|
|
101
|
+
eyebrow: { fontSize: 11, fontWeight: 510, color: '#06b6d4', lineHeight: 1.4 },
|
|
101
102
|
title: { fontSize: 18, fontWeight: 600, color: '#1a1a2e', lineHeight: 1.2 },
|
|
102
103
|
subtitle: { fontSize: 14, fontWeight: 400, color: '#666', lineHeight: 1.3 },
|
|
103
104
|
source: { fontSize: 10, fontWeight: 400, color: '#999', lineHeight: 1.2 },
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Annotation, ChartEventHandlers, ChartLayout } from '@opendata-ai/openchart-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a map from data-mark-id to { datum, series } so event handlers
|
|
5
|
+
* can look up the data row associated with a clicked/hovered mark element.
|
|
6
|
+
*/
|
|
7
|
+
function buildMarkDataMap(
|
|
8
|
+
layout: ChartLayout,
|
|
9
|
+
): Map<string, { datum: Record<string, unknown>; series?: string }> {
|
|
10
|
+
const map = new Map<string, { datum: Record<string, unknown>; series?: string }>();
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < layout.marks.length; i++) {
|
|
13
|
+
const mark = layout.marks[i];
|
|
14
|
+
switch (mark.type) {
|
|
15
|
+
case 'line':
|
|
16
|
+
map.set(`line-${mark.seriesKey ?? i}`, {
|
|
17
|
+
datum: mark.data[0] ?? {},
|
|
18
|
+
series: mark.seriesKey,
|
|
19
|
+
});
|
|
20
|
+
break;
|
|
21
|
+
case 'area':
|
|
22
|
+
map.set(`area-${mark.seriesKey ?? i}`, {
|
|
23
|
+
datum: mark.data[0] ?? {},
|
|
24
|
+
series: mark.seriesKey,
|
|
25
|
+
});
|
|
26
|
+
break;
|
|
27
|
+
case 'rect':
|
|
28
|
+
map.set(`rect-${i}`, { datum: mark.data });
|
|
29
|
+
break;
|
|
30
|
+
case 'arc':
|
|
31
|
+
map.set(`arc-${i}`, { datum: mark.data });
|
|
32
|
+
break;
|
|
33
|
+
case 'point':
|
|
34
|
+
map.set(`point-${i}`, { datum: mark.data });
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wire chart event handlers (onMarkClick, onMarkHover, onMarkLeave) to mark
|
|
44
|
+
* elements, onLegendToggle to legend entries, and onAnnotationClick to annotation
|
|
45
|
+
* elements inside an SVG.
|
|
46
|
+
*
|
|
47
|
+
* Returns a cleanup function to remove all listeners.
|
|
48
|
+
*/
|
|
49
|
+
export function wireChartEvents(
|
|
50
|
+
svg: SVGElement,
|
|
51
|
+
layout: ChartLayout,
|
|
52
|
+
specAnnotations: Annotation[],
|
|
53
|
+
handlers: ChartEventHandlers,
|
|
54
|
+
): () => void {
|
|
55
|
+
const cleanups: Array<() => void> = [];
|
|
56
|
+
const markDataMap = buildMarkDataMap(layout);
|
|
57
|
+
|
|
58
|
+
if (handlers.onMarkClick || handlers.onMarkHover || handlers.onMarkLeave) {
|
|
59
|
+
const markElements = svg.querySelectorAll('[data-mark-id]');
|
|
60
|
+
|
|
61
|
+
for (const el of markElements) {
|
|
62
|
+
const markId = el.getAttribute('data-mark-id');
|
|
63
|
+
if (!markId) continue;
|
|
64
|
+
|
|
65
|
+
const markInfo = markDataMap.get(markId);
|
|
66
|
+
if (!markInfo) continue;
|
|
67
|
+
|
|
68
|
+
const series = markInfo.series ?? el.getAttribute('data-series') ?? undefined;
|
|
69
|
+
|
|
70
|
+
if (handlers.onMarkClick) {
|
|
71
|
+
const handleClick = (e: Event) => {
|
|
72
|
+
const mouseEvent = e as MouseEvent;
|
|
73
|
+
const svgRect = svg.getBoundingClientRect();
|
|
74
|
+
handlers.onMarkClick!({
|
|
75
|
+
datum: markInfo.datum,
|
|
76
|
+
series,
|
|
77
|
+
position: {
|
|
78
|
+
x: mouseEvent.clientX - svgRect.left,
|
|
79
|
+
y: mouseEvent.clientY - svgRect.top,
|
|
80
|
+
},
|
|
81
|
+
event: mouseEvent,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
el.addEventListener('click', handleClick);
|
|
85
|
+
cleanups.push(() => el.removeEventListener('click', handleClick));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (handlers.onMarkHover) {
|
|
89
|
+
const handleEnter = (e: Event) => {
|
|
90
|
+
const mouseEvent = e as MouseEvent;
|
|
91
|
+
const svgRect = svg.getBoundingClientRect();
|
|
92
|
+
handlers.onMarkHover!({
|
|
93
|
+
datum: markInfo.datum,
|
|
94
|
+
series,
|
|
95
|
+
position: {
|
|
96
|
+
x: mouseEvent.clientX - svgRect.left,
|
|
97
|
+
y: mouseEvent.clientY - svgRect.top,
|
|
98
|
+
},
|
|
99
|
+
event: mouseEvent,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
el.addEventListener('mouseenter', handleEnter);
|
|
103
|
+
cleanups.push(() => el.removeEventListener('mouseenter', handleEnter));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (handlers.onMarkLeave) {
|
|
107
|
+
const handleLeave = () => {
|
|
108
|
+
handlers.onMarkLeave!();
|
|
109
|
+
};
|
|
110
|
+
el.addEventListener('mouseleave', handleLeave);
|
|
111
|
+
cleanups.push(() => el.removeEventListener('mouseleave', handleLeave));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (handlers.onAnnotationClick) {
|
|
117
|
+
const annotationElements = svg.querySelectorAll('.oc-annotation');
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < annotationElements.length; i++) {
|
|
120
|
+
const el = annotationElements[i];
|
|
121
|
+
const specAnnotation = specAnnotations[i];
|
|
122
|
+
if (!specAnnotation) continue;
|
|
123
|
+
|
|
124
|
+
const handleClick = (e: Event) => {
|
|
125
|
+
const mouseEvent = e as MouseEvent;
|
|
126
|
+
handlers.onAnnotationClick!(specAnnotation, mouseEvent);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
el.addEventListener('click', handleClick);
|
|
130
|
+
cleanups.push(() => el.removeEventListener('click', handleClick));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
for (const cleanup of cleanups) {
|
|
136
|
+
cleanup();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { ChartLayout, TooltipContent } from '@opendata-ai/openchart-core';
|
|
2
|
+
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
3
|
+
import type { TooltipManager } from '../tooltip';
|
|
4
|
+
|
|
5
|
+
interface SeriesPoint {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
datum: Record<string, unknown>;
|
|
9
|
+
tooltip?: TooltipContent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SeriesGroup {
|
|
13
|
+
seriesKey: string;
|
|
14
|
+
color: string;
|
|
15
|
+
pointsByX: Map<number, SeriesPoint>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function snapKey(x: number): number {
|
|
19
|
+
return Math.round(x);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function collectSeriesGroups(layout: ChartLayout): SeriesGroup[] {
|
|
23
|
+
const groups: SeriesGroup[] = [];
|
|
24
|
+
for (let i = 0; i < layout.marks.length; i++) {
|
|
25
|
+
const mark = layout.marks[i];
|
|
26
|
+
if ((mark.type === 'line' || mark.type === 'area') && mark.dataPoints?.length) {
|
|
27
|
+
const color = mark.type === 'line' ? mark.stroke : getRepresentativeColor(mark.fill);
|
|
28
|
+
const pointsByX = new Map<number, SeriesPoint>();
|
|
29
|
+
for (const dp of mark.dataPoints) {
|
|
30
|
+
pointsByX.set(snapKey(dp.x), { ...dp });
|
|
31
|
+
}
|
|
32
|
+
groups.push({
|
|
33
|
+
seriesKey: mark.seriesKey ?? `${mark.type}-${i}`,
|
|
34
|
+
color,
|
|
35
|
+
pointsByX,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return groups;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectSnapXs(groups: SeriesGroup[]): number[] {
|
|
43
|
+
const seen = new Set<number>();
|
|
44
|
+
for (const g of groups) {
|
|
45
|
+
for (const k of g.pointsByX.keys()) seen.add(k);
|
|
46
|
+
}
|
|
47
|
+
return Array.from(seen).sort((a, b) => a - b);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findNearestX(sortedXs: number[], x: number): number | null {
|
|
51
|
+
if (sortedXs.length === 0) return null;
|
|
52
|
+
let lo = 0;
|
|
53
|
+
let hi = sortedXs.length - 1;
|
|
54
|
+
while (lo < hi) {
|
|
55
|
+
const mid = (lo + hi) >> 1;
|
|
56
|
+
if (sortedXs[mid] < x) lo = mid + 1;
|
|
57
|
+
else hi = mid;
|
|
58
|
+
}
|
|
59
|
+
const candidate = sortedXs[lo];
|
|
60
|
+
if (lo > 0) {
|
|
61
|
+
const prev = sortedXs[lo - 1];
|
|
62
|
+
if (Math.abs(prev - x) < Math.abs(candidate - x)) return prev;
|
|
63
|
+
}
|
|
64
|
+
return candidate;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildSliceTooltip(
|
|
68
|
+
hits: Array<{ group: SeriesGroup; point: SeriesPoint }>,
|
|
69
|
+
): TooltipContent | null {
|
|
70
|
+
if (hits.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
const title = hits[0].point.tooltip?.title;
|
|
73
|
+
const fields: Array<{ label: string; value: string; color?: string }> = [];
|
|
74
|
+
|
|
75
|
+
const isMulti = hits.length > 1;
|
|
76
|
+
for (const { group, point } of hits) {
|
|
77
|
+
const tip = point.tooltip;
|
|
78
|
+
if (!tip) continue;
|
|
79
|
+
if (isMulti) {
|
|
80
|
+
const yField =
|
|
81
|
+
tip.fields.find((f) => !f.color && f.label !== title) ??
|
|
82
|
+
tip.fields[tip.fields.length - 1] ??
|
|
83
|
+
null;
|
|
84
|
+
if (!yField) continue;
|
|
85
|
+
fields.push({
|
|
86
|
+
label: group.seriesKey,
|
|
87
|
+
value: yField.value,
|
|
88
|
+
color: group.color,
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
for (const f of tip.fields) {
|
|
92
|
+
fields.push({ ...f, color: f.color ?? group.color });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fields.length === 0) return null;
|
|
98
|
+
return { title, fields };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Wire snap-to-x multi-series tooltip events for line/area charts.
|
|
103
|
+
* On mousemove over the chart area we find the nearest x in the union of all
|
|
104
|
+
* series, render one snap dot per series at that x, and show one merged
|
|
105
|
+
* tooltip listing every series' value.
|
|
106
|
+
*/
|
|
107
|
+
export function wireVoronoiTooltipEvents(
|
|
108
|
+
svg: SVGElement,
|
|
109
|
+
layout: ChartLayout,
|
|
110
|
+
tooltipManager: TooltipManager,
|
|
111
|
+
): () => void {
|
|
112
|
+
const overlay = svg.querySelector('[data-voronoi-overlay]');
|
|
113
|
+
if (!overlay) return () => {};
|
|
114
|
+
|
|
115
|
+
const groups = collectSeriesGroups(layout);
|
|
116
|
+
if (groups.length === 0) return () => {};
|
|
117
|
+
|
|
118
|
+
const snapXs = collectSnapXs(groups);
|
|
119
|
+
if (snapXs.length === 0) return () => {};
|
|
120
|
+
|
|
121
|
+
const crosshair = svg.querySelector('[data-crosshair]') as SVGLineElement | null;
|
|
122
|
+
const dotsLayer = svg.querySelector('[data-snap-dots]') as SVGGElement | null;
|
|
123
|
+
|
|
124
|
+
const dots: SVGCircleElement[] = [];
|
|
125
|
+
if (dotsLayer) {
|
|
126
|
+
while (dotsLayer.firstChild) dotsLayer.removeChild(dotsLayer.firstChild);
|
|
127
|
+
for (const group of groups) {
|
|
128
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
129
|
+
circle.setAttribute('r', '4');
|
|
130
|
+
circle.setAttribute('fill', layout.theme.colors.background);
|
|
131
|
+
circle.setAttribute('stroke', group.color);
|
|
132
|
+
circle.setAttribute('stroke-width', '2');
|
|
133
|
+
circle.setAttribute('pointer-events', 'none');
|
|
134
|
+
circle.style.display = 'none';
|
|
135
|
+
dotsLayer.appendChild(circle);
|
|
136
|
+
dots.push(circle);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const positionAt = (svgX: number, _svgY: number, viewBoxToContainer: number): boolean => {
|
|
141
|
+
const snappedX = findNearestX(snapXs, svgX);
|
|
142
|
+
if (snappedX === null) return false;
|
|
143
|
+
|
|
144
|
+
const hits: Array<{ group: SeriesGroup; point: SeriesPoint }> = [];
|
|
145
|
+
let anchorY = 0;
|
|
146
|
+
let anchorCount = 0;
|
|
147
|
+
for (let i = 0; i < groups.length; i++) {
|
|
148
|
+
const group = groups[i];
|
|
149
|
+
const point = group.pointsByX.get(snappedX);
|
|
150
|
+
const dot = dots[i];
|
|
151
|
+
if (point) {
|
|
152
|
+
hits.push({ group, point });
|
|
153
|
+
anchorY += point.y;
|
|
154
|
+
anchorCount += 1;
|
|
155
|
+
if (dot) {
|
|
156
|
+
dot.setAttribute('cx', String(point.x));
|
|
157
|
+
dot.setAttribute('cy', String(point.y));
|
|
158
|
+
dot.style.display = '';
|
|
159
|
+
}
|
|
160
|
+
} else if (dot) {
|
|
161
|
+
dot.style.display = 'none';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (crosshair) {
|
|
166
|
+
crosshair.setAttribute('x1', String(snappedX));
|
|
167
|
+
crosshair.setAttribute('x2', String(snappedX));
|
|
168
|
+
crosshair.style.display = '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tooltip = buildSliceTooltip(hits);
|
|
172
|
+
if (!tooltip) return false;
|
|
173
|
+
|
|
174
|
+
const containerAnchorX = snappedX * viewBoxToContainer;
|
|
175
|
+
const containerAnchorY = anchorCount > 0 ? (anchorY / anchorCount) * viewBoxToContainer : 0;
|
|
176
|
+
tooltipManager.show(tooltip, containerAnchorX, containerAnchorY, {
|
|
177
|
+
placement: 'right',
|
|
178
|
+
});
|
|
179
|
+
return true;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const toSvgCoords = (clientX: number, clientY: number) => {
|
|
183
|
+
const svgEl = svg as unknown as SVGSVGElement;
|
|
184
|
+
const svgRect = svgEl.getBoundingClientRect();
|
|
185
|
+
const viewBox = svgEl.viewBox?.baseVal;
|
|
186
|
+
const scaleX = viewBox?.width && svgRect.width ? viewBox.width / svgRect.width : 1;
|
|
187
|
+
const scaleY = viewBox?.height && svgRect.height ? viewBox.height / svgRect.height : 1;
|
|
188
|
+
const svgX = (clientX - svgRect.left) * scaleX;
|
|
189
|
+
const svgY = (clientY - svgRect.top) * scaleY;
|
|
190
|
+
const viewBoxToContainer = scaleX > 0 ? 1 / scaleX : 1;
|
|
191
|
+
return { svgX, svgY, viewBoxToContainer };
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleMouseMove = (e: Event) => {
|
|
195
|
+
const me = e as MouseEvent;
|
|
196
|
+
const { svgX, svgY, viewBoxToContainer } = toSvgCoords(me.clientX, me.clientY);
|
|
197
|
+
positionAt(svgX, svgY, viewBoxToContainer);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const hideAll = () => {
|
|
201
|
+
if (crosshair) crosshair.style.display = 'none';
|
|
202
|
+
for (const dot of dots) dot.style.display = 'none';
|
|
203
|
+
tooltipManager.hide();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleTouch = (e: Event) => {
|
|
207
|
+
const te = e as TouchEvent;
|
|
208
|
+
if (te.touches.length === 0) return;
|
|
209
|
+
const t = te.touches[0];
|
|
210
|
+
const { svgX, svgY, viewBoxToContainer } = toSvgCoords(t.clientX, t.clientY);
|
|
211
|
+
if (te.cancelable) te.preventDefault();
|
|
212
|
+
positionAt(svgX, svgY, viewBoxToContainer);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
overlay.addEventListener('mousemove', handleMouseMove);
|
|
216
|
+
overlay.addEventListener('mouseleave', hideAll);
|
|
217
|
+
overlay.addEventListener('touchstart', handleTouch, { passive: false });
|
|
218
|
+
overlay.addEventListener('touchmove', handleTouch, { passive: false });
|
|
219
|
+
overlay.addEventListener('touchend', hideAll);
|
|
220
|
+
|
|
221
|
+
return () => {
|
|
222
|
+
overlay.removeEventListener('mousemove', handleMouseMove);
|
|
223
|
+
overlay.removeEventListener('mouseleave', hideAll);
|
|
224
|
+
overlay.removeEventListener('touchstart', handleTouch);
|
|
225
|
+
overlay.removeEventListener('touchmove', handleTouch);
|
|
226
|
+
overlay.removeEventListener('touchend', hideAll);
|
|
227
|
+
};
|
|
228
|
+
}
|