diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c15a67b..fce8ed0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -204,14 +204,6 @@ jobs: id: run-tests run: yarn e2e - - name: Upload e2e test summary - uses: grafana/plugin-actions/playwright-gh-pages/upload-report-artifacts@upload-report-artifacts/v1.0.1 - if: ${{ always() && !cancelled() }} - with: - upload-report: false - github-token: ${{ secrets.GITHUB_TOKEN }} - test-outcome: ${{ steps.run-tests.outcome }} - - name: Docker logs if: ${{ always() && steps.run-tests.outcome == 'failure' }} run: | @@ -228,21 +220,3 @@ jobs: # name: ${{ matrix.GRAFANA_IMAGE.NAME }}-v${{ matrix.GRAFANA_IMAGE.VERSION }}-${{github.run_id}}-server-log # path: grafana-server.log # retention-days: 5 - - publish-report: - if: ${{ always() && !cancelled() }} - permissions: - contents: write - id-token: write - pull-requests: write - needs: [playwright-tests] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - # required for playwright-gh-pages - persist-credentials: true - - name: Publish report - uses: grafana/plugin-actions/playwright-gh-pages/deploy-report-pages@deploy-report-pages/v1.1.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2072353..0068510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,4 +32,24 @@ update grafana tooling to webpack - fixes truncation bug that was introduced - adds sort options for rows/columns - refactor to align with create-plugin 7.3 -- improves type annotations \ No newline at end of file +- improves type annotations + +## Unreleased +- **Rebuilt the legend on thresholds.** It previously sampled 10 evenly-spaced values + between the data's min and max, which meant the swatches never lined up with the + threshold steps that were actually coloring the cells, the configured boundary values + were never shown, and any step outside the data range disappeared. The legend is now + derived from `Standard Options > Thresholds`: one segment per step, boundaries printed + at the divisions, every step shown whether or not data falls into it. Percentage-mode + thresholds are converted over the same field range Grafana colors by. +- **Legend Type is now `Auto`** (thresholds / gradient / categorical), resolved from + whatever colors the cells; the old `range` and `categorical` values still load. +- **Cell Color Mode now defaults to `Standard Options (thresholds)`.** Panels saved + before that option existed were threshold-colored, so this also restores them instead + of restyling them with a ramp. +- Categorical legends wrap into rows and cap at 24 entries, instead of laying out one + circle per unique value at fixed 75px spacing (which ran off-panel past ~8 values). +- The legend reserves its measured height rather than a flat 64px, so the grid is no + longer squeezed by a legend smaller than the reservation, or overlapped by a larger one. +- Hovering a legend entry now highlights exactly the cells in its band, rather than + everything within ±6% of the color domain. \ No newline at end of file diff --git a/README.md b/README.md index 8e1f8f1..c9231c7 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,17 @@ Note: This is NOT inteded to use with time series data or large data sets. It a ## Options ### Display -**Show Legend:** Adds legend to bottom of panel. +**Show Legend:** Adds a legend to the bottom of the panel. The legend is built from whatever actually colors the cells — normally the steps you configure under **Standard Options > Thresholds**. +**Legend Type:** Leave this on **Auto** and the legend follows the cell color source: -**Legend Type:** There are two types. Categorical will produce a legend with distinct colors for each unique detected value. Range will generate a bar with the detected range of colors & values. +- *Thresholds* — one segment per configured threshold step, with the boundary values printed underneath. Every step is shown, including ones no data happens to fall into. This is what you get with the default **Standard Options (thresholds)** color mode. +- *Gradient* — a continuous bar with a labeled tick axis, used when a ramp colors the cells (the Sequential or Diverging color modes, or a `continuous-*` scheme picked in Standard Options). +- *Categorical* — one swatch per distinct value, wrapped over as many rows as it needs and capped at 24 entries so a high-cardinality field cannot bury the matrix. + +You can pin a specific type instead of Auto, but a type that disagrees with how the cells are colored will read as misleading. + +Hovering a legend entry highlights exactly the cells it covers, and a **null** / **no data** key appears whenever such cells are present. **Source Field:** The field you want to use on the vertical axis. @@ -33,7 +40,7 @@ Note: This is NOT inteded to use with time series data or large data sets. It a **No Data Color:** The color to use for a square when there is no value returned by the query for the specified source and target pair. ### Colors -The color of each square will be determined by the selected metric and the color choice in the **Standard Options**. +By default the color of each square is determined by the selected metric and the thresholds under **Standard Options**. **Cell Color Mode** can switch that to one of the panel's own ramps (Sequential or Diverging) instead; the legend follows whichever you pick. diff --git a/jest.config.js b/jest.config.js index 79fd52a..f457d3f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,14 @@ // generally used by snapshots, but can affect specific tests process.env.TZ = 'UTC'; +const { grafanaESModules, nodeModulesToTransform } = require('./.config/jest/utils'); + +// matrix.js pulls in d3-selection/d3-transition, which ship ESM only. The scaffolded +// list covers plain `d3` (and d3-color/d3-interpolate) but not these, so extend it. +const esModules = [...grafanaESModules, 'd3-selection', 'd3-transition', 'd3-dispatch', 'd3-ease', 'd3-timer']; + module.exports = { // Jest configuration provided by Grafana scaffolding ...require('./.config/jest.config'), + transformIgnorePatterns: [nodeModulesToTransform(esModules)], }; diff --git a/makeover.md b/makeover.md new file mode 100644 index 0000000..c1fbf31 --- /dev/null +++ b/makeover.md @@ -0,0 +1,231 @@ +# ESnet Matrix Panel — Visual Makeover + +A visual-design overhaul of the matrix (heatmap) panel. The goal was to make it +more legible, more insightful, and better-looking out of the box, following +established data-visualization guidance (sequential/diverging color, mark spacers, +a first-class hover layer, and size encoding). + +**Design principle:** the improved look ships as the **new default**, and every new +behavior has its own opt-out toggle so existing dashboards can revert piece by +piece. One intentional default change to know about: cells now color via a built-in +**sequential** ramp instead of the Standard Options thresholds — set +**Cell Color Mode → Standard Options** on any panel that relied on thresholds. + +--- + +## What changed, by area + +### Cells & interaction +- **Rounded corners + surface-gap spacers.** Cells are no longer hard squares butted + together; they have a small corner radius and a consistent gap in the surface color. +- **Hover cross-highlight.** Hovering a cell outlines it, dims every cell outside its + row/column, and bolds the two matching axis labels — so you can trace a value back + to its row and column at a glance. +- **Null vs. no-data are now distinct.** Previously both rendered as the same gray. + Now a cell that was queried but null fills with the null color, while a cell with + **no matching data at all** renders empty with a hairline outline. +- **Tooltip polish.** Fixed a CSS typo that broke the tooltip border radius, and + restructured the tooltip so the **value leads** (large, bold) with the + source/target labels as secondary detail. Labels remain HTML-sanitized. + +### Color +- **Built-in color modes.** A new **Cell Color Mode** option: + - **Sequential** (default) — a single-hue blue ramp, light→dark for magnitude. The + anchor flips in dark mode so high values stay bright and low values recede. + - **Diverging** — blue↔red around a configurable neutral midpoint, for + "above/below a baseline" values (e.g. latency vs. an SLA). + - **Standard Options** — the previous behavior (thresholds / color scheme). + - The ramps are self-contained (no rainbow) and derive neutrals from the Grafana + theme, so they track light/dark correctly. + +### Layout & reading order +- **Sort / seriation.** New **Row Order** / **Column Order** options: keep the + alphabetical default, or sort **by total** (largest marginal sum first) to surface + the hottest rows and columns. +- **Category group dividers.** When grouping is enabled, alternating category groups + now get a subtle background band instead of being separated by whitespace alone. +- **Fit-to-panel sizing.** New **Sizing** option: **Fit** (default) scales cells to + fill the panel, or **Fixed** uses the exact cell size in pixels (may scroll). The + panel's real width/height are now consumed (previously ignored). +- **Frozen axis labels.** Row and column labels stay pinned while the cell grid + scrolls, so you never lose track of which row/column you're reading. + +### Encoding & annotation +- **In-cell value labels.** Values print inside cells when they fit, with automatic + white/dark ink chosen by the cell's luminance (measured first — skipped when the + text wouldn't fit). +- **Marginal total bars** (opt-in). Small bar charts of row totals (right edge) and + column totals (bottom edge), each sized by its total. +- **Size encodes value** (opt-in). Each cell's filled area scales with its value, so + magnitude reads through both color *and* size (robust to colorblindness and + grayscale printing). + +### Legend +- The range legend gained a **title** (the value text) and **start / middle / end + tick labels**, instead of labeling only the two endpoints. + +### Under the hood +- **Pixel-accurate label truncation.** Truncation now measures rendered text width + (`getComputedTextLength`) instead of counting characters, so proportional fonts no + longer over- or under-truncate. +- **Axis margins sized to the labels.** The row/column label margins are now measured + from the actual rendered width of the longest label (capped at the **Text Length** + option), so labels get the space they need instead of being cut off after a few + characters. Increase **Text Length** to allow longer labels (and a wider margin); + decrease it to cap long labels and reclaim space. + +--- + +## New panel options + +Grouped under the **Display**, **Colors**, **Layout**, and **Row/Column Options** +categories in the panel editor. + +| Option | Category | Default | What it does | +|---|---|---|---| +| Cell Corner Radius | Display | `2` | Corner rounding in px (`0` = square) | +| Highlight Row & Column on Hover | Display | `on` | Dim other cells + emphasize hovered row/column | +| Show Values in Cells | Display | `on` | Print values inside cells that are large enough | +| Show Category Group Dividers | Display | `on` | Background bands behind grouped rows/columns | +| Cell Color Mode | Colors | `sequential` | `sequential` / `diverging` / `standard` | +| Diverging Midpoint | Colors | `0` | Neutral value for diverging mode | +| Distinguish No-Data Cells | Colors | `on` | Render no-data cells empty/outlined | +| Row Order | Row/Column | `name` | `name`, `total` (largest first), or `cluster` (seriation) | +| Column Order | Row/Column | `name` | `name`, `total` (largest first), or `cluster` (seriation) | +| Color Scale Range | Colors | `auto` | `auto` (data min/max) or `manual` (fixed, comparable across panels) | +| Color Scale Min / Max | Colors | `0` / `100` | Fixed color-domain bounds (manual mode) | +| Focus Value Range | Display | `off` | Spotlight cells in a value range | +| Focus Min / Max | Display | — | Bounds of the focused range | +| Out-of-Focus Cells | Display | `dim` | `dim` or `hide` cells outside the range | +| Sizing | Layout | `fit` | `fit` to panel or `fixed` px cell size | +| Freeze Axis Labels | Layout | `on` | Pin labels while the grid scrolls | +| Column Label Orientation | Layout | `auto` | `auto` / `rotated` / `horizontal` | +| Show Marginal Totals | Layout | `off` | Row/column total bars on the edges | +| Size Encodes Value | Layout | `off` | Scale cell area by value (dual encoding) | +| Show as Table | Layout | `off` | Accessible HTML table view of the same data | + +--- + +## Round 2 — clarity, analytical & accessibility + +A second wave focused on comprehension, analytical power, and accessibility. + +### Clarity +- **Legend null / no-data key.** The legend now shows swatches labeled "null" and + "no data" whenever such cells are present, so viewers can read the gray/outlined + cells. +- **Continuous gradient legend.** For sequential/diverging color modes the legend is + a smooth gradient bar with labeled min/max (and a labeled midpoint tick for + diverging), instead of discrete swatches. Standard-options mode keeps stepped + swatches (matching its thresholds). + *Superseded by Round 3 — see "Legend rebuilt on thresholds" below.* +- **Guided empty states.** Instead of a bare "No Data", the panel now tells you what + to do — map Rows/Columns/Value fields, add a numeric column, reduce cell count, + etc. — depending on what's actually missing. Parse failures render a friendly + message instead of blanking the panel. + +### Analytical +- **Cluster ordering (seriation).** A third Row/Column order mode reorders similar + rows and columns next to each other to reveal block structure, via dependency-free + spectral seriation (column-centered power iteration on the value matrix). + Deterministic across renders; applied within categories when grouping is on. +- **Shared / manual color domain.** Pin the color scale's min and max so multiple + matrix panels are directly comparable and colors don't shift when a filter changes + the data. +- **Value-threshold focus.** Spotlight cells whose value falls in a range; the rest + are dimmed or hidden. In-cell labels and hover restore respect the focus state. + +### Interaction & accessibility +- **Keyboard navigation + focus parity.** Cells are focusable with a roving tabindex; + arrow keys move between cells, Enter/Space pins a row+column, Escape clears. Focus + shows the same tooltip and cross-highlight as hover. +- **ARIA + table view.** The grid exposes `role="img"`/grid roles and per-cell + `aria-label`s, and **Show as Table** renders an accessible HTML `` of the + same data as a screen-reader/text-friendly alternative. +- **Interactive legend.** Hovering the gradient (or a swatch, or a null/no-data key) + highlights the matching cells and dims the rest. +- **Click-to-pin.** Clicking a cell or an axis label pins that row/column highlight so + it persists while you scan; click again (or Escape) to release. +- **Auto-horizontal labels.** Column labels render horizontally when they fit inside a + cell (few columns / wide cells) and rotate vertically otherwise; overridable via + Column Label Orientation. + +--- + +## Round 3 — legend rebuilt on thresholds + +The legend was still derived from the **data's min and max**: it sampled 10 evenly-spaced +values across the range and drew them as a gradient or as 10 equal-width swatches. In +`standard` color mode — the mode the plugin declares as its color source, via +`preferThresholdMode` — that meant the legend never agreed with the cells: + +- swatch boundaries didn't line up with the threshold steps doing the coloring, +- the configured boundary values were never printed, +- threshold steps outside the data range vanished entirely, +- hover highlighted everything within ±6% of the color domain rather than a real band, +- `categorical` drew one circle per unique value at fixed 75px spacing, which ran off the + panel past ~8 values (≈800 circles on the 30×30 dataset). + +### What it does now +- **Thresholds drive the legend.** Steps come from `field.config.thresholds` — real + colors, real boundary values, every step shown whether or not data lands in it. + Percentage-mode thresholds are converted to absolute values over the same field range + `getScaleCalculator` uses, so the printed numbers match the cell colors exactly. +- **The legend follows the color source.** `Legend Type: Auto` resolves to *thresholds*, + *gradient* (the panel's ramps, or a Grafana `continuous-*` scheme), or *categorical* + (non-numeric values, a fixed color, or value mappings). A form can still be pinned. +- **It fits.** A stepped bar is used when the boundary labels fit; otherwise the entries + wrap into a swatch list. Categorical caps at 24 entries with a `+N more` note. +- **It reserves what it needs.** `layoutLegend` computes the geometry, and the grid + sizes itself against that instead of a flat 64px guess. +- **Hover is exact.** Each entry carries the half-open band `[from, to)` it stands for, + so hovering highlights precisely those cells. +- **`Cell Color Mode` now defaults to `standard`**, and the parser's fallback moved with + it — panels saved before that option existed were threshold-colored, so the fallback + now restores them rather than restyling them as a sequential ramp. + +--- + +## Files changed + +| File | Change | +|---|---| +| `src/types.ts` | Added the new fields to the `MatrixOptions` interface; replaced `LegendData` with the `LegendModel` / `LegendItem` / `LegendStop` / `LegendTick` model | +| `src/module.ts` | Registered the new panel-editor options; added `Colors` and `Layout` option categories; `Legend Type` → Auto/thresholds/gradient/categorical; `Cell Color Mode` defaults to `standard` | +| `src/legend.ts` | **New.** Builds the legend model from thresholds / ramps / distinct values, and computes its layout and height (pure, unit-tested) | +| `src/legendRender.js` | **New.** Draws the model: stepped threshold bar, gradient bar with tick axis, wrapped swatch list, null/no-data key | +| `src/dataParser.ts` | Built-in sequential/diverging color ramps; total-sort; null-vs-no-data tracking; marginal totals + value domain; numeric/NaN guards; delegates the legend to `buildLegend` | +| `src/matrix.js` | Rounded cells, spacers, hover cross-highlight, distinct no-data, in-cell labels, group bands, marginal bars, size encoding, fit sizing, frozen labels, pixel-accurate truncation, tooltip fix; legend delegated to `renderLegend`, highlight now band-based | +| `src/matrix.d.ts` | Updated the `matrix()` signature (added `width`, `rowTotals`, `colTotals`, `valueDomain`) | +| `src/EsnetMatrix.tsx` | Pass panel `width` and the new parsed-data fields into the matrix renderer | + +No new runtime dependencies were added — the color ramps are computed with a small +self-contained interpolator. + +--- + +## Building & loading (local, no Docker) + +```bash +yarn install --ignore-engines # a transitive dep wants Node ≥20; repo is on Node 16 +yarn build # or: yarn dev (watch mode) +``` + +`dist/` is symlinked into the Homebrew Grafana plugins directory and the plugin id is +in `allow_loading_unsigned_plugins`, so a frontend rebuild only needs a **browser +hard-refresh** (no Grafana restart unless `plugin.json` changes). + +## Verifying + +Sample datasets live in `sample-data/` (load them via the TestData datasource → +**CSV Content** scenario): + +- `1-basic-traffic.csv` — 5×5, missing diagonal → exercises no-data cells +- `2-grouped-with-categories.csv` — row/column grouping, dividers, sort-by-total +- `3-edge-cases.csv` — null vs. no-data distinction +- `4-stress-30x30.csv` — fit-to-panel, frozen labels, in-cell label thresholding + +To exercise everything, add an **ESnet Matrix** panel to any dashboard, point it at a +TestData datasource loading one of the CSVs above, and map the Rows / Columns / Value +fields. Check it in **both light and dark** Grafana themes, and hover cells to see the +row/column cross-highlight. (No demo dashboard is bundled in the repo.) diff --git a/package.json b/package.json index 5d7b605..be6d382 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,8 @@ "@grafana/runtime": "12.4.2", "@grafana/schema": "12.4.2", "@grafana/ui": "12.4.2", + "d3-selection": "3", + "d3-transition": "3", "react": "^18.3.0", "react-dom": "^18.3.0" }, diff --git a/sample-data/1-basic-traffic.csv b/sample-data/1-basic-traffic.csv new file mode 100644 index 0000000..6b1632a --- /dev/null +++ b/sample-data/1-basic-traffic.csv @@ -0,0 +1,21 @@ +source,target,gbps +SUNN,DENV,42.5 +SUNN,CHIC,18.2 +SUNN,WASH,7.4 +SUNN,ATLA,3.1 +DENV,SUNN,40.9 +DENV,CHIC,55.7 +DENV,WASH,12.6 +DENV,ATLA,9.8 +CHIC,SUNN,17.5 +CHIC,DENV,54.1 +CHIC,WASH,88.3 +CHIC,ATLA,33.0 +WASH,SUNN,6.9 +WASH,DENV,11.4 +WASH,CHIC,90.2 +WASH,ATLA,61.5 +ATLA,SUNN,2.8 +ATLA,DENV,10.1 +ATLA,CHIC,31.7 +ATLA,WASH,60.3 diff --git a/sample-data/2-grouped-with-categories.csv b/sample-data/2-grouped-with-categories.csv new file mode 100644 index 0000000..e227b30 --- /dev/null +++ b/sample-data/2-grouped-with-categories.csv @@ -0,0 +1,19 @@ +src_region,source,dst_region,target,latency_ms +West,SUNN,West,LOSA,4.2 +West,SUNN,Central,DENV,18.5 +West,SUNN,East,WASH,61.0 +West,LOSA,West,SUNN,4.1 +West,LOSA,Central,DENV,22.3 +West,LOSA,East,NEWY,68.7 +Central,DENV,West,SUNN,18.6 +Central,DENV,Central,CHIC,14.9 +Central,DENV,East,WASH,33.2 +Central,CHIC,West,LOSA,41.0 +Central,CHIC,Central,DENV,15.1 +Central,CHIC,East,NEWY,21.4 +East,WASH,West,SUNN,60.8 +East,WASH,Central,CHIC,20.0 +East,WASH,East,NEWY,6.3 +East,NEWY,West,LOSA,69.1 +East,NEWY,Central,DENV,29.7 +East,NEWY,East,WASH,6.1 diff --git a/sample-data/3-edge-cases.csv b/sample-data/3-edge-cases.csv new file mode 100644 index 0000000..1acd6fe --- /dev/null +++ b/sample-data/3-edge-cases.csv @@ -0,0 +1,9 @@ +source,target,errors +alpha,x,0 +alpha,y,5 +alpha,z, +beta,x,12 +beta,y, +beta,z,3 +gamma,x,7 +gamma,z,20 diff --git a/sample-data/4-stress-30x30.csv b/sample-data/4-stress-30x30.csv new file mode 100644 index 0000000..15a3acf --- /dev/null +++ b/sample-data/4-stress-30x30.csv @@ -0,0 +1,871 @@ +source,target,gbps +N01,N02,0.1 +N01,N03,5.0 +N01,N04,45.8 +N01,N05,0.8 +N01,N06,0.1 +N01,N07,25.5 +N01,N08, +N01,N09,42.2 +N01,N10,4.9 +N01,N11,65.5 +N01,N12, +N01,N13,48.7 +N01,N14,2.4 +N01,N15,11.3 +N01,N16,0.9 +N01,N17,36.4 +N01,N18,53.3 +N01,N19,94.7 +N01,N20,30.5 +N01,N21,38.3 +N01,N22,33.3 +N01,N23,0.2 +N01,N24,8.4 +N01,N25,5.4 +N01,N26,7.7 +N01,N27,13.3 +N01,N28,4.4 +N01,N29,87.7 +N01,N30,37.1 +N02,N01,53.2 +N02,N03,14.4 +N02,N04,41.0 +N02,N05,46.9 +N02,N06,60.2 +N02,N07,0.1 +N02,N08,7.2 +N02,N09,88.9 +N02,N10,9.9 +N02,N11,15.7 +N02,N12,21.1 +N02,N13,6.1 +N02,N14,6.9 +N02,N15,80.6 +N02,N16,4.8 +N02,N17,26.0 +N02,N18,0.2 +N02,N19,39.4 +N02,N20,17.8 +N02,N21,14.6 +N02,N22,28.0 +N02,N23,74.1 +N02,N24, +N02,N25,46.5 +N02,N26,7.1 +N02,N27,1.2 +N02,N28,20.6 +N02,N29,76.7 +N02,N30,25.1 +N03,N01,83.3 +N03,N02,8.9 +N03,N04,37.1 +N03,N05,58.1 +N03,N06,60.6 +N03,N07,0.0 +N03,N08,0.0 +N03,N09,77.2 +N03,N10,9.5 +N03,N11,77.1 +N03,N12,0.7 +N03,N13,0.5 +N03,N14,58.7 +N03,N15,22.6 +N03,N16,7.0 +N03,N17,17.9 +N03,N18,29.1 +N03,N19,4.0 +N03,N20,99.0 +N03,N21,19.2 +N03,N22,1.5 +N03,N23,11.4 +N03,N24,5.3 +N03,N25,0.5 +N03,N26,5.2 +N03,N27,73.9 +N03,N28,5.7 +N03,N29,4.6 +N03,N30,87.5 +N04,N01,22.3 +N04,N02,65.2 +N04,N03,0.9 +N04,N05,17.9 +N04,N06,53.2 +N04,N07,96.9 +N04,N08,16.2 +N04,N09,74.2 +N04,N10,3.6 +N04,N11,17.8 +N04,N12,6.2 +N04,N13,19.6 +N04,N14,30.3 +N04,N15,99.9 +N04,N16,93.9 +N04,N17,72.0 +N04,N18,23.6 +N04,N19,16.1 +N04,N20,14.4 +N04,N21,7.0 +N04,N22,20.7 +N04,N23,91.6 +N04,N24,30.9 +N04,N25,2.4 +N04,N26,93.8 +N04,N27,29.4 +N04,N28,0.3 +N04,N29,25.3 +N04,N30,2.5 +N05,N01,0.6 +N05,N02,35.4 +N05,N03,5.5 +N05,N04,79.3 +N05,N06,35.3 +N05,N07,17.6 +N05,N08,27.3 +N05,N09,4.2 +N05,N10,5.7 +N05,N11,45.1 +N05,N12,10.0 +N05,N13,0.5 +N05,N14,99.7 +N05,N15,0.5 +N05,N16,7.0 +N05,N17,77.6 +N05,N18,13.7 +N05,N19,69.5 +N05,N20,37.4 +N05,N21,42.8 +N05,N22, +N05,N23,9.0 +N05,N24,88.2 +N05,N25,1.3 +N05,N26,30.6 +N05,N27,36.6 +N05,N28,4.1 +N05,N29,7.0 +N05,N30,82.0 +N06,N01,0.9 +N06,N02,7.7 +N06,N03, +N06,N04,40.6 +N06,N05,54.9 +N06,N07,18.3 +N06,N08, +N06,N09,78.0 +N06,N10,29.8 +N06,N11,33.9 +N06,N12,1.6 +N06,N13,80.8 +N06,N14,74.1 +N06,N15,4.4 +N06,N16,1.1 +N06,N17,78.2 +N06,N18,38.5 +N06,N19,86.5 +N06,N20,95.3 +N06,N21,77.7 +N06,N22, +N06,N23,11.0 +N06,N24,64.4 +N06,N25,65.7 +N06,N26,62.0 +N06,N27,76.1 +N06,N28,4.9 +N06,N29,21.2 +N06,N30,63.3 +N07,N01,0.1 +N07,N02,10.8 +N07,N03,93.5 +N07,N04,41.1 +N07,N05,96.3 +N07,N06,88.2 +N07,N08,94.2 +N07,N09,92.6 +N07,N10,1.2 +N07,N11,53.1 +N07,N12,36.7 +N07,N13,14.8 +N07,N14,6.5 +N07,N15,0.0 +N07,N16,29.0 +N07,N17,55.0 +N07,N18,13.3 +N07,N19,44.1 +N07,N20,9.9 +N07,N21,51.8 +N07,N22,9.6 +N07,N23,16.2 +N07,N24,1.6 +N07,N25,88.4 +N07,N26,81.5 +N07,N27,9.1 +N07,N28,0.0 +N07,N29,18.5 +N07,N30,42.9 +N08,N01,19.6 +N08,N02,22.4 +N08,N03,63.4 +N08,N04,0.7 +N08,N05,40.1 +N08,N06,67.0 +N08,N07,45.3 +N08,N09,4.0 +N08,N10, +N08,N11,22.6 +N08,N12,0.5 +N08,N13,39.7 +N08,N14,48.5 +N08,N15,6.0 +N08,N16,0.0 +N08,N17,59.3 +N08,N18,18.1 +N08,N19,91.8 +N08,N20,0.3 +N08,N21,72.0 +N08,N22,64.2 +N08,N23,97.6 +N08,N24,90.3 +N08,N25,37.5 +N08,N26,25.5 +N08,N27,30.0 +N08,N28,55.3 +N08,N29,6.7 +N08,N30,40.7 +N09,N01,27.2 +N09,N02,7.5 +N09,N03,8.2 +N09,N04,10.2 +N09,N05,1.9 +N09,N06,48.2 +N09,N07,0.4 +N09,N08,29.4 +N09,N10,4.3 +N09,N11,81.9 +N09,N12,48.4 +N09,N13,58.6 +N09,N14,0.0 +N09,N15,56.8 +N09,N16,90.9 +N09,N17,55.9 +N09,N18,36.4 +N09,N19,4.8 +N09,N20,0.1 +N09,N21,46.1 +N09,N22,2.7 +N09,N23,1.6 +N09,N24,0.1 +N09,N25,31.9 +N09,N26, +N09,N27,1.8 +N09,N28,0.3 +N09,N29,4.5 +N09,N30,57.9 +N10,N01,56.6 +N10,N02,6.4 +N10,N03,0.0 +N10,N04,100.0 +N10,N05,42.3 +N10,N06,42.5 +N10,N07,90.2 +N10,N08,0.0 +N10,N09,1.6 +N10,N11,31.8 +N10,N12,48.9 +N10,N13,2.8 +N10,N14,55.9 +N10,N15,67.1 +N10,N16,1.2 +N10,N17, +N10,N18,45.9 +N10,N19,15.7 +N10,N20,0.6 +N10,N21,39.3 +N10,N22,59.7 +N10,N23,36.0 +N10,N24,96.8 +N10,N25,12.1 +N10,N26,13.7 +N10,N27,11.6 +N10,N28,67.6 +N10,N29,92.3 +N10,N30,68.7 +N11,N01,19.0 +N11,N02,93.2 +N11,N03,65.3 +N11,N04,23.4 +N11,N05,53.4 +N11,N06,72.5 +N11,N07,0.8 +N11,N08,5.9 +N11,N09,37.3 +N11,N10,0.1 +N11,N12,3.3 +N11,N13,63.7 +N11,N14,77.5 +N11,N15,7.6 +N11,N16, +N11,N17,0.7 +N11,N18,23.9 +N11,N19,47.7 +N11,N20,24.1 +N11,N21,0.9 +N11,N22,47.9 +N11,N23,33.8 +N11,N24,28.2 +N11,N25,55.6 +N11,N26,49.4 +N11,N27,6.3 +N11,N28,3.7 +N11,N29,28.7 +N11,N30,3.4 +N12,N01,23.4 +N12,N02,95.4 +N12,N03,8.0 +N12,N04,3.8 +N12,N05,3.2 +N12,N06, +N12,N07,7.5 +N12,N08,30.6 +N12,N09,1.6 +N12,N10,24.1 +N12,N11,33.0 +N12,N13,19.4 +N12,N14,0.3 +N12,N15,22.8 +N12,N16,16.1 +N12,N17,39.6 +N12,N18,2.2 +N12,N19,9.2 +N12,N20,1.4 +N12,N21,36.8 +N12,N22,5.1 +N12,N23,20.3 +N12,N24,74.0 +N12,N25,9.3 +N12,N26,37.2 +N12,N27,89.8 +N12,N28,4.5 +N12,N29,2.5 +N12,N30,0.6 +N13,N01, +N13,N02,35.3 +N13,N03,5.4 +N13,N04,49.4 +N13,N05,47.2 +N13,N06,62.1 +N13,N07,43.7 +N13,N08,18.1 +N13,N09,41.9 +N13,N10,68.3 +N13,N11,2.8 +N13,N12,56.1 +N13,N14,8.3 +N13,N15,47.4 +N13,N16,88.9 +N13,N17,24.4 +N13,N18,0.2 +N13,N19,10.4 +N13,N20,0.8 +N13,N21,69.9 +N13,N22,90.4 +N13,N23,45.2 +N13,N24,0.2 +N13,N25,22.1 +N13,N26,83.9 +N13,N27,34.3 +N13,N28,24.2 +N13,N29,12.1 +N13,N30,44.9 +N14,N01,10.9 +N14,N02,8.3 +N14,N03,66.2 +N14,N04,20.7 +N14,N05,10.5 +N14,N06,16.3 +N14,N07,97.6 +N14,N08,29.4 +N14,N09,3.5 +N14,N10,57.2 +N14,N11,57.8 +N14,N12,30.2 +N14,N13,19.2 +N14,N15,1.5 +N14,N16,37.1 +N14,N17,2.5 +N14,N18,30.5 +N14,N19,98.5 +N14,N20,21.3 +N14,N21,69.2 +N14,N22,51.4 +N14,N23,7.5 +N14,N24,96.1 +N14,N25,30.4 +N14,N26,85.0 +N14,N27,77.3 +N14,N28,7.6 +N14,N29,17.2 +N14,N30,25.8 +N15,N01,8.0 +N15,N02,34.4 +N15,N03,24.0 +N15,N04,29.0 +N15,N05,30.5 +N15,N06,20.7 +N15,N07,3.6 +N15,N08,32.7 +N15,N09,60.1 +N15,N10,55.5 +N15,N11,65.8 +N15,N12,44.0 +N15,N13,96.2 +N15,N14,0.1 +N15,N16,34.8 +N15,N17,76.4 +N15,N18,27.7 +N15,N19,52.2 +N15,N20,42.9 +N15,N21,22.0 +N15,N22,11.5 +N15,N23,42.2 +N15,N24,72.6 +N15,N25,14.4 +N15,N26,51.7 +N15,N27,76.1 +N15,N28,0.5 +N15,N29,84.8 +N15,N30,55.8 +N16,N01,1.0 +N16,N02,76.1 +N16,N03,48.2 +N16,N04,0.2 +N16,N05,8.6 +N16,N06,2.1 +N16,N07,32.0 +N16,N08,2.9 +N16,N09,75.8 +N16,N10,5.8 +N16,N11,2.0 +N16,N12,6.5 +N16,N13,0.0 +N16,N14,81.2 +N16,N15,2.5 +N16,N17,11.9 +N16,N18,40.8 +N16,N19,6.3 +N16,N20,4.0 +N16,N21,23.3 +N16,N22,32.7 +N16,N23,98.5 +N16,N24,95.6 +N16,N25,7.5 +N16,N26,47.0 +N16,N27,0.2 +N16,N28,24.7 +N16,N29,8.2 +N16,N30,36.9 +N17,N01,40.5 +N17,N02,45.9 +N17,N03,43.5 +N17,N04,39.5 +N17,N05,41.8 +N17,N06,19.4 +N17,N07,53.6 +N17,N08,8.7 +N17,N09,3.1 +N17,N10,29.1 +N17,N11,28.2 +N17,N12,69.0 +N17,N13,68.0 +N17,N14,65.0 +N17,N15,11.5 +N17,N16,92.7 +N17,N18,93.4 +N17,N19,52.4 +N17,N20,93.6 +N17,N21,13.4 +N17,N22,0.0 +N17,N23,20.7 +N17,N24,45.2 +N17,N25,67.6 +N17,N26,1.2 +N17,N27,0.1 +N17,N28,31.5 +N17,N29,4.9 +N17,N30,67.9 +N18,N01,9.1 +N18,N02,2.0 +N18,N03,9.3 +N18,N04,0.9 +N18,N05,1.8 +N18,N06,45.0 +N18,N07,89.5 +N18,N08,55.1 +N18,N09,17.2 +N18,N10,23.9 +N18,N11,90.5 +N18,N12,13.7 +N18,N13,90.4 +N18,N14,1.0 +N18,N15,29.6 +N18,N16,12.9 +N18,N17,3.6 +N18,N19,71.9 +N18,N20,43.9 +N18,N21,35.7 +N18,N22, +N18,N23,5.9 +N18,N24,31.9 +N18,N25,58.5 +N18,N26,4.7 +N18,N27,10.8 +N18,N28,81.1 +N18,N29, +N18,N30,2.1 +N19,N01,6.3 +N19,N02,43.7 +N19,N03, +N19,N04, +N19,N05,5.7 +N19,N06,3.0 +N19,N07,55.0 +N19,N08,55.6 +N19,N09,60.5 +N19,N10,1.2 +N19,N11,89.4 +N19,N12,61.3 +N19,N13,27.2 +N19,N14,92.9 +N19,N15,22.9 +N19,N16,47.1 +N19,N17,82.8 +N19,N18,0.7 +N19,N20,0.4 +N19,N21,40.1 +N19,N22,10.6 +N19,N23,28.1 +N19,N24,36.7 +N19,N25,49.2 +N19,N26,42.4 +N19,N27,52.0 +N19,N28,20.4 +N19,N29,11.5 +N19,N30,17.3 +N20,N01,18.2 +N20,N02,14.0 +N20,N03,85.2 +N20,N04,69.2 +N20,N05,0.9 +N20,N06,65.9 +N20,N07,34.4 +N20,N08,10.9 +N20,N09,12.5 +N20,N10,56.3 +N20,N11,52.0 +N20,N12,36.0 +N20,N13,33.4 +N20,N14,43.1 +N20,N15,1.2 +N20,N16,13.5 +N20,N17,33.0 +N20,N18,71.4 +N20,N19,67.0 +N20,N21,41.3 +N20,N22, +N20,N23,68.8 +N20,N24,3.3 +N20,N25,9.5 +N20,N26,0.0 +N20,N27,32.1 +N20,N28,2.0 +N20,N29,0.1 +N20,N30,4.6 +N21,N01,11.6 +N21,N02,52.1 +N21,N03,32.2 +N21,N04,0.3 +N21,N05,38.2 +N21,N06,7.4 +N21,N07,23.6 +N21,N08,7.5 +N21,N09,1.3 +N21,N10,8.0 +N21,N11,23.7 +N21,N12,0.2 +N21,N13,35.9 +N21,N14, +N21,N15,4.5 +N21,N16,6.5 +N21,N17,0.0 +N21,N18,3.1 +N21,N19,49.5 +N21,N20,69.4 +N21,N22,0.5 +N21,N23,0.2 +N21,N24, +N21,N25,74.3 +N21,N26,32.9 +N21,N27,17.4 +N21,N28,0.0 +N21,N29,64.2 +N21,N30,69.2 +N22,N01,0.8 +N22,N02,5.9 +N22,N03,27.5 +N22,N04,9.6 +N22,N05,11.1 +N22,N06,26.1 +N22,N07,26.0 +N22,N08,12.2 +N22,N09,67.1 +N22,N10,5.6 +N22,N11,3.9 +N22,N12,57.8 +N22,N13,3.1 +N22,N14,24.4 +N22,N15,57.7 +N22,N16,85.4 +N22,N17,40.4 +N22,N18,74.7 +N22,N19,2.3 +N22,N20,19.6 +N22,N21,7.5 +N22,N23,25.7 +N22,N24,20.4 +N22,N25,69.2 +N22,N26,74.7 +N22,N27,37.8 +N22,N28,21.4 +N22,N29,62.7 +N22,N30,20.2 +N23,N01,42.5 +N23,N02,22.6 +N23,N03,0.4 +N23,N04,80.8 +N23,N05,51.0 +N23,N06,3.0 +N23,N07,19.2 +N23,N08,27.3 +N23,N09,13.9 +N23,N10,16.7 +N23,N11,35.7 +N23,N12,41.9 +N23,N13,0.9 +N23,N14,8.1 +N23,N15,43.1 +N23,N16,76.3 +N23,N17,34.0 +N23,N18,12.2 +N23,N19,48.8 +N23,N20,35.4 +N23,N21,9.6 +N23,N22,62.7 +N23,N24,44.9 +N23,N25,54.6 +N23,N26,27.7 +N23,N27,17.9 +N23,N28,13.1 +N23,N29,4.6 +N23,N30,23.6 +N24,N01,1.9 +N24,N02,71.3 +N24,N03,59.4 +N24,N04,78.1 +N24,N05,11.3 +N24,N06,1.7 +N24,N07,2.6 +N24,N08,59.5 +N24,N09,2.7 +N24,N10,16.9 +N24,N11,5.6 +N24,N12,8.1 +N24,N13,20.2 +N24,N14,9.6 +N24,N15,22.0 +N24,N16,13.5 +N24,N17,96.9 +N24,N18,7.9 +N24,N19,27.8 +N24,N20,66.7 +N24,N21,1.9 +N24,N22,41.1 +N24,N23,30.8 +N24,N25,71.6 +N24,N26,8.1 +N24,N27,7.4 +N24,N28,2.2 +N24,N29,89.6 +N24,N30,20.4 +N25,N01,0.1 +N25,N02,25.2 +N25,N03,98.9 +N25,N04,0.1 +N25,N05,70.4 +N25,N06,62.6 +N25,N07,8.2 +N25,N08,48.5 +N25,N09,49.8 +N25,N10,0.0 +N25,N11,6.5 +N25,N12,30.1 +N25,N13,27.9 +N25,N14,8.3 +N25,N15,0.2 +N25,N16,63.0 +N25,N17,1.2 +N25,N18,35.6 +N25,N19, +N25,N20,5.9 +N25,N21,18.4 +N25,N22,5.8 +N25,N23,44.1 +N25,N24,95.0 +N25,N26,27.7 +N25,N27,97.7 +N25,N28,15.2 +N25,N29,40.4 +N25,N30,6.4 +N26,N01, +N26,N02,11.9 +N26,N03,39.5 +N26,N04,54.0 +N26,N05,0.2 +N26,N06,66.2 +N26,N07,60.7 +N26,N08,48.4 +N26,N09,65.9 +N26,N10,60.2 +N26,N11,8.6 +N26,N12,4.0 +N26,N13,87.1 +N26,N14,97.8 +N26,N15,6.4 +N26,N16,3.7 +N26,N17,61.0 +N26,N18,11.0 +N26,N19,13.5 +N26,N20,55.3 +N26,N21,14.9 +N26,N22,24.6 +N26,N23,85.4 +N26,N24,64.2 +N26,N25,0.6 +N26,N27,67.6 +N26,N28,10.3 +N26,N29,43.7 +N26,N30,36.3 +N27,N01,47.6 +N27,N02,0.2 +N27,N03,12.4 +N27,N04,7.5 +N27,N05,89.9 +N27,N06,40.6 +N27,N07,64.2 +N27,N08,90.8 +N27,N09,36.9 +N27,N10,0.1 +N27,N11,60.6 +N27,N12,14.7 +N27,N13,36.6 +N27,N14,90.0 +N27,N15,58.2 +N27,N16,28.0 +N27,N17,42.2 +N27,N18,1.3 +N27,N19,24.9 +N27,N20,31.5 +N27,N21,6.8 +N27,N22,99.3 +N27,N23,84.0 +N27,N24,1.5 +N27,N25,20.4 +N27,N26,19.8 +N27,N28,46.5 +N27,N29,10.2 +N27,N30,0.4 +N28,N01,79.4 +N28,N02,50.7 +N28,N03,40.7 +N28,N04,25.9 +N28,N05,4.0 +N28,N06,62.5 +N28,N07, +N28,N08,13.6 +N28,N09,30.4 +N28,N10,0.7 +N28,N11,99.9 +N28,N12,27.6 +N28,N13,67.8 +N28,N14,94.6 +N28,N15,20.2 +N28,N16,11.9 +N28,N17,60.9 +N28,N18,3.3 +N28,N19,18.7 +N28,N20,0.3 +N28,N21,2.3 +N28,N22,10.4 +N28,N23,12.0 +N28,N24,80.1 +N28,N25,6.3 +N28,N26,30.3 +N28,N27,9.2 +N28,N29,25.3 +N28,N30,88.7 +N29,N01,43.4 +N29,N02,36.6 +N29,N03,31.8 +N29,N04,0.1 +N29,N05,41.1 +N29,N06,42.4 +N29,N07,17.4 +N29,N08,24.8 +N29,N09,8.4 +N29,N10,23.3 +N29,N11,46.9 +N29,N12,0.5 +N29,N13,19.3 +N29,N14,4.2 +N29,N15,9.8 +N29,N16,53.9 +N29,N17,95.1 +N29,N18,13.7 +N29,N19,10.2 +N29,N20,7.2 +N29,N21,0.9 +N29,N22,14.8 +N29,N23,6.2 +N29,N24,2.6 +N29,N25,33.4 +N29,N26,58.2 +N29,N27,26.5 +N29,N28,9.5 +N29,N30, +N30,N01,25.5 +N30,N02,4.6 +N30,N03,0.3 +N30,N04,77.9 +N30,N05,22.1 +N30,N06,71.8 +N30,N07,77.9 +N30,N08,58.3 +N30,N09,16.0 +N30,N10,3.8 +N30,N11,0.5 +N30,N12,58.4 +N30,N13,97.8 +N30,N14,1.4 +N30,N15,15.5 +N30,N16,11.5 +N30,N17,57.0 +N30,N18,25.9 +N30,N19,0.2 +N30,N20,11.1 +N30,N21,20.9 +N30,N22,26.6 +N30,N23,37.6 +N30,N24,98.1 +N30,N25,9.0 +N30,N26,68.6 +N30,N27,50.2 +N30,N28,66.5 +N30,N29,45.4 diff --git a/src/EsnetMatrix.tsx b/src/EsnetMatrix.tsx index f117d8f..e0de90d 100644 --- a/src/EsnetMatrix.tsx +++ b/src/EsnetMatrix.tsx @@ -1,54 +1,246 @@ import React from 'react'; -import { FieldConfigSource, PanelProps } from '@grafana/data'; +import { PanelProps, GrafanaTheme2 } from '@grafana/data'; import { MatrixOptions } from 'types'; import { parseData } from 'dataParser'; -import { useTheme2, CustomScrollbar } from '@grafana/ui'; +import { useTheme2, ScrollContainer } from '@grafana/ui'; import * as Matrix from './matrix.js'; interface Props extends PanelProps { - fieldConfig: FieldConfigSource; + fieldConfig: any; options: MatrixOptions; } +/** A centered, themed message panel used for empty / guidance / error states. */ +const StatePanel: React.FC<{ theme: GrafanaTheme2; title: string; lines: string[] }> = ({ theme, title, lines }) => ( +
+
+ {title} +
+ {lines.map((line, i) => ( +
+ {line} +
+ ))} +
+); + +/** Render a cell's value as plain text for the table view. */ +const cellText = (d: any): string => { + if (d === -1) { + return '—'; + } + if (d.isNull) { + return 'null'; + } + return `${d.display.text}${d.display.suffix ? ' ' + d.display.suffix : ''}`; +}; + +/** An accessible HTML table alternative to the SVG matrix (keyboard + screen-reader friendly). */ +const MatrixTable: React.FC<{ theme: GrafanaTheme2; parsed: any; options: MatrixOptions }> = ({ + theme, + parsed, + options, +}) => { + const th: React.CSSProperties = { + border: `1px solid ${theme.colors.border.weak}`, + padding: '4px 8px', + textAlign: 'right', + fontWeight: theme.typography.fontWeightMedium as any, + whiteSpace: 'nowrap', + position: 'sticky', + top: 0, + background: theme.colors.background.primary, + }; + const td: React.CSSProperties = { + border: `1px solid ${theme.colors.border.weak}`, + padding: '4px 8px', + textAlign: 'right', + whiteSpace: 'nowrap', + }; + return ( + +
+ + + + + {parsed.columns.map((c: string, i: number) => ( + + ))} + + + + {parsed.rows.map((r: string, ri: number) => ( + + + {parsed.columns.map((c: string, ci: number) => ( + + ))} + + ))} + +
+ {`${options.sourceText || 'Rows'} × ${options.targetText || 'Columns'} — ${options.valueText || 'Value'}`} +
+ {options.sourceText || 'Rows'} + + {c} +
+ {r} + + {cellText(parsed.data[ri][ci])} +
+ + ); +}; + +const EMPTY_PARSED = { + rows: null, + columns: null, + data: null, + legend: null, + colMetadata: [], + colCategories: [], + rowMetadata: [], + rowCategories: [], + rowTotals: [], + colTotals: [], + valueDomain: null, +}; + export const EsnetMatrix: React.FC = ({ options, data, width, height, id }) => { const theme = useTheme2(); - // console.log(options); - const parsedData = parseData(data, options, theme); - if (typeof parsedData.data === "string") { - console.error(parsedData.data); - switch (parsedData.data) { - case 'too many inputs': - return
Too many data points! Try adding limits to your query.
; - break; - default: - return
Unknown error: {parsedData.data}
; - } - } - - if ( - parsedData.rowNames === null - || parsedData.colNames === null - || parsedData.data === null - || parsedData.legend === null) { - return
No data
; + + // Inspect the incoming data + field mapping to choose the most helpful guidance. + const series = data && data.series && data.series.length ? data.series[0] : undefined; + const fields = series && series.fields ? series.fields : []; + const hasNumericField = fields.some((f: any) => f.type === 'number'); + + // Parse (never throws out of render — a failure becomes an error state below). + let parsedData: any = EMPTY_PARSED; + let parseError = false; + try { + parsedData = parseData(data, options, theme); + } catch (error) { + console.error('matrix data error: ', error); + parseError = true; + parsedData = EMPTY_PARSED; } + // IMPORTANT: call the render hook unconditionally (stable hook order across all + // branches below). When we render a non-SVG state, the ref stays unattached and + // the D3 effect harmlessly no-ops on a null element. const ref = Matrix.matrix( - parsedData.rowNames, - parsedData.colNames, + parsedData.rows, + parsedData.columns, parsedData.data, id, + width, + height, options, parsedData.legend, + parsedData.colMetadata, parsedData.colCategories, + parsedData.rowMetadata, parsedData.rowCategories, + parsedData.rowTotals, + parsedData.colTotals, + parsedData.valueDomain ); - const thisPanelClass = `matrix-panel-${id}`; + if (!series || fields.length === 0) { + return ( + + ); + } + + if (fields.length < 2 || !hasNumericField) { + return ( + + ); + } + + if (parseError) { + return ( + + ); + } + + if (parsedData.data === 'too many inputs') { + return ( + + ); + } + + if (parsedData.data == null) { + return ( + + ); + } + + if (options.showTableView) { + return ; + } + + const thisPanelClass = `matrix-panel-${id}`; return ( - +
-
+ ); }; diff --git a/src/d3.min.js b/src/d3.min.js deleted file mode 100644 index 33bb880..0000000 --- a/src/d3.min.js +++ /dev/null @@ -1,2 +0,0 @@ -// https://d3js.org v7.9.0 Copyright 2010-2023 Mike Bostock -!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";function n(t,n){return null==t||null==n?NaN:tn?1:t>=n?0:NaN}function e(t,n){return null==t||null==n?NaN:nt?1:n>=t?0:NaN}function r(t){let r,o,a;function u(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<0?e=r+1:i=r}while(en(t(e),r),a=(n,e)=>t(n)-e):(r=t===n||t===e?t:i,o=t,a=t),{left:u,center:function(t,n,e=0,r=t.length){const i=u(t,n,e,r-1);return i>e&&a(t[i-1],n)>-a(t[i],n)?i-1:i},right:function(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<=0?e=r+1:i=r}while(e{n(t,e,(r<<=2)+0,(i<<=2)+0,o<<=2),n(t,e,r+1,i+1,o),n(t,e,r+2,i+2,o),n(t,e,r+3,i+3,o)}}));function d(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:i,width:o,height:a}=n;if(!((o=Math.floor(o))>=0))throw new RangeError("invalid width");if(!((a=Math.floor(void 0!==a?a:i.length/o))>=0))throw new RangeError("invalid height");if(!o||!a||!e&&!r)return n;const u=e&&t(e),c=r&&t(r),f=i.slice();return u&&c?(p(u,f,i,o,a),p(u,i,f,o,a),p(u,f,i,o,a),g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)):u?(p(u,i,f,o,a),p(u,f,i,o,a),p(u,i,f,o,a)):c&&(g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)),n}}function p(t,n,e,r,i){for(let o=0,a=r*i;o{if(!((o-=a)>=i))return;let u=t*r[i];const c=a*t;for(let t=i,n=i+c;t{if(!((a-=u)>=o))return;let c=n*i[o];const f=u*n,s=f+u;for(let t=o,n=o+f;t=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function _(t){return 0|t.length}function b(t){return!(t>0)}function m(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function x(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function w(t,n){const e=x(t,n);return e?Math.sqrt(e):e}function M(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r=o&&(e=r=o):(e>o&&(e=o),r0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(A(this,t))}has(t){return super.has(A(this,t))}set(t,n){return super.set(S(this,t),n)}delete(t){return super.delete(E(this,t))}}class InternSet extends Set{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(A(this,t))}add(t){return super.add(S(this,t))}delete(t){return super.delete(E(this,t))}}function A({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):e}function S({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):(t.set(r,e),e)}function E({_intern:t,_key:n},e){const r=n(e);return t.has(r)&&(e=t.get(r),t.delete(r)),e}function N(t){return null!==t&&"object"==typeof t?t.valueOf():t}function k(t){return t}function C(t,...n){return F(t,k,k,n)}function P(t,...n){return F(t,Array.from,k,n)}function z(t,n){for(let e=1,r=n.length;et.pop().map((([n,e])=>[...t,n,e]))));return t}function $(t,n,...e){return F(t,k,n,e)}function D(t,n,...e){return F(t,Array.from,n,e)}function R(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function F(t,n,e,r){return function t(i,o){if(o>=r.length)return e(i);const a=new InternMap,u=r[o++];let c=-1;for(const t of i){const n=u(t,++c,i),e=a.get(n);e?e.push(t):a.set(n,[t])}for(const[n,e]of a)a.set(n,t(e,o));return n(a)}(t,0)}function q(t,n){return Array.from(n,(n=>t[n]))}function U(t,...n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");t=Array.from(t);let[e]=n;if(e&&2!==e.length||n.length>1){const r=Uint32Array.from(t,((t,n)=>n));return n.length>1?(n=n.map((n=>t.map(n))),r.sort(((t,e)=>{for(const r of n){const n=O(r[t],r[e]);if(n)return n}}))):(e=t.map(e),r.sort(((t,n)=>O(e[t],e[n])))),q(t,r)}return t.sort(I(e))}function I(t=n){if(t===n)return O;if("function"!=typeof t)throw new TypeError("compare is not a function");return(n,e)=>{const r=t(n,e);return r||0===r?r:(0===t(e,e))-(0===t(n,n))}}function O(t,n){return(null==t||!(t>=t))-(null==n||!(n>=n))||(tn?1:0)}var B=Array.prototype.slice;function Y(t){return()=>t}const L=Math.sqrt(50),j=Math.sqrt(10),H=Math.sqrt(2);function X(t,n,e){const r=(n-t)/Math.max(0,e),i=Math.floor(Math.log10(r)),o=r/Math.pow(10,i),a=o>=L?10:o>=j?5:o>=H?2:1;let u,c,f;return i<0?(f=Math.pow(10,-i)/a,u=Math.round(t*f),c=Math.round(n*f),u/fn&&--c,f=-f):(f=Math.pow(10,i)*a,u=Math.round(t/f),c=Math.round(n/f),u*fn&&--c),c0))return[];if((t=+t)===(n=+n))return[t];const r=n=i))return[];const u=o-i+1,c=new Array(u);if(r)if(a<0)for(let t=0;t0?(t=Math.floor(t/i)*i,n=Math.ceil(n/i)*i):i<0&&(t=Math.ceil(t*i)/i,n=Math.floor(n*i)/i),r=i}}function K(t){return Math.max(1,Math.ceil(Math.log(v(t))/Math.LN2)+1)}function Q(){var t=k,n=M,e=K;function r(r){Array.isArray(r)||(r=Array.from(r));var i,o,a,u=r.length,c=new Array(u);for(i=0;i=h)if(t>=h&&n===M){const t=V(l,h,e);isFinite(t)&&(t>0?h=(Math.floor(h/t)+1)*t:t<0&&(h=(Math.ceil(h*-t)+1)/-t))}else d.pop()}for(var p=d.length,g=0,y=p;d[g]<=l;)++g;for(;d[y-1]>h;)--y;(g||y0?d[i-1]:l,v.x1=i0)for(i=0;i=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e=i)&&(e=i)}return e}function tt(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e=o)&&(e=o,r=i);return r}function nt(t,n){let e;if(void 0===n)for(const n of t)null!=n&&(e>n||void 0===e&&n>=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e>i||void 0===e&&i>=i)&&(e=i)}return e}function et(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e>n||void 0===e&&n>=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e>o||void 0===e&&o>=o)&&(e=o,r=i);return r}function rt(t,n,e=0,r=1/0,i){if(n=Math.floor(n),e=Math.floor(Math.max(0,e)),r=Math.floor(Math.min(t.length-1,r)),!(e<=n&&n<=r))return t;for(i=void 0===i?O:I(i);r>e;){if(r-e>600){const o=r-e+1,a=n-e+1,u=Math.log(o),c=.5*Math.exp(2*u/3),f=.5*Math.sqrt(u*c*(o-c)/o)*(a-o/2<0?-1:1);rt(t,n,Math.max(e,Math.floor(n-a*c/o+f)),Math.min(r,Math.floor(n+(o-a)*c/o+f)),i)}const o=t[n];let a=e,u=r;for(it(t,e,n),i(t[r],o)>0&&it(t,e,r);a0;)--u}0===i(t[e],o)?it(t,e,u):(++u,it(t,u,r)),u<=n&&(e=u+1),n<=u&&(r=u-1)}return t}function it(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function ot(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)>0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)>0:0===e(n,n))&&(r=n,i=!0);return r}function at(t,n,e){if(t=Float64Array.from(function*(t,n){if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(yield n);else{let e=-1;for(let r of t)null!=(r=n(r,++e,t))&&(r=+r)>=r&&(yield r)}}(t,e)),(r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return nt(t);if(n>=1)return J(t);var r,i=(r-1)*n,o=Math.floor(i),a=J(rt(t,o).subarray(0,o+1));return a+(nt(t.subarray(o+1))-a)*(i-o)}}function ut(t,n,e=o){if((r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,a=Math.floor(i),u=+e(t[a],a,t);return u+(+e(t[a+1],a+1,t)-u)*(i-a)}}function ct(t,n,e=o){if(!isNaN(n=+n)){if(r=Float64Array.from(t,((n,r)=>o(e(t[r],r,t)))),n<=0)return et(r);if(n>=1)return tt(r);var r,i=Uint32Array.from(t,((t,n)=>n)),a=r.length-1,u=Math.floor(a*n);return rt(i,u,0,a,((t,n)=>O(r[t],r[n]))),(u=ot(i.subarray(0,u+1),(t=>r[t])))>=0?u:-1}}function ft(t){return Array.from(function*(t){for(const n of t)yield*n}(t))}function st(t,n){return[t,n]}function lt(t,n,e){t=+t,n=+n,e=(i=arguments.length)<2?(n=t,t=0,1):i<3?1:+e;for(var r=-1,i=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(i);++r+t(n)}function kt(t,n){return n=Math.max(0,t.bandwidth()-2*n)/2,t.round()&&(n=Math.round(n)),e=>+t(e)+n}function Ct(){return!this.__axis}function Pt(t,n){var e=[],r=null,i=null,o=6,a=6,u=3,c="undefined"!=typeof window&&window.devicePixelRatio>1?0:.5,f=t===xt||t===Tt?-1:1,s=t===Tt||t===wt?"x":"y",l=t===xt||t===Mt?St:Et;function h(h){var d=null==r?n.ticks?n.ticks.apply(n,e):n.domain():r,p=null==i?n.tickFormat?n.tickFormat.apply(n,e):mt:i,g=Math.max(o,0)+u,y=n.range(),v=+y[0]+c,_=+y[y.length-1]+c,b=(n.bandwidth?kt:Nt)(n.copy(),c),m=h.selection?h.selection():h,x=m.selectAll(".domain").data([null]),w=m.selectAll(".tick").data(d,n).order(),M=w.exit(),T=w.enter().append("g").attr("class","tick"),A=w.select("line"),S=w.select("text");x=x.merge(x.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),w=w.merge(T),A=A.merge(T.append("line").attr("stroke","currentColor").attr(s+"2",f*o)),S=S.merge(T.append("text").attr("fill","currentColor").attr(s,f*g).attr("dy",t===xt?"0em":t===Mt?"0.71em":"0.32em")),h!==m&&(x=x.transition(h),w=w.transition(h),A=A.transition(h),S=S.transition(h),M=M.transition(h).attr("opacity",At).attr("transform",(function(t){return isFinite(t=b(t))?l(t+c):this.getAttribute("transform")})),T.attr("opacity",At).attr("transform",(function(t){var n=this.parentNode.__axis;return l((n&&isFinite(n=n(t))?n:b(t))+c)}))),M.remove(),x.attr("d",t===Tt||t===wt?a?"M"+f*a+","+v+"H"+c+"V"+_+"H"+f*a:"M"+c+","+v+"V"+_:a?"M"+v+","+f*a+"V"+c+"H"+_+"V"+f*a:"M"+v+","+c+"H"+_),w.attr("opacity",1).attr("transform",(function(t){return l(b(t)+c)})),A.attr(s+"2",f*o),S.attr(s,f*g).text(p),m.filter(Ct).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===wt?"start":t===Tt?"end":"middle"),m.each((function(){this.__axis=b}))}return h.scale=function(t){return arguments.length?(n=t,h):n},h.ticks=function(){return e=Array.from(arguments),h},h.tickArguments=function(t){return arguments.length?(e=null==t?[]:Array.from(t),h):e.slice()},h.tickValues=function(t){return arguments.length?(r=null==t?null:Array.from(t),h):r&&r.slice()},h.tickFormat=function(t){return arguments.length?(i=t,h):i},h.tickSize=function(t){return arguments.length?(o=a=+t,h):o},h.tickSizeInner=function(t){return arguments.length?(o=+t,h):o},h.tickSizeOuter=function(t){return arguments.length?(a=+t,h):a},h.tickPadding=function(t){return arguments.length?(u=+t,h):u},h.offset=function(t){return arguments.length?(c=+t,h):c},h}var zt={value:()=>{}};function $t(){for(var t,n=0,e=arguments.length,r={};n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ut.hasOwnProperty(n)?{space:Ut[n],local:t}:t}function Ot(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===qt&&n.documentElement.namespaceURI===qt?n.createElement(t):n.createElementNS(e,t)}}function Bt(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function Yt(t){var n=It(t);return(n.local?Bt:Ot)(n)}function Lt(){}function jt(t){return null==t?Lt:function(){return this.querySelector(t)}}function Ht(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function Xt(){return[]}function Gt(t){return null==t?Xt:function(){return this.querySelectorAll(t)}}function Vt(t){return function(){return this.matches(t)}}function Wt(t){return function(n){return n.matches(t)}}var Zt=Array.prototype.find;function Kt(){return this.firstElementChild}var Qt=Array.prototype.filter;function Jt(){return Array.from(this.children)}function tn(t){return new Array(t.length)}function nn(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function en(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function cn(t){return function(){this.removeAttribute(t)}}function fn(t){return function(){this.removeAttributeNS(t.space,t.local)}}function sn(t,n){return function(){this.setAttribute(t,n)}}function ln(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function hn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function dn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function pn(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function gn(t){return function(){this.style.removeProperty(t)}}function yn(t,n,e){return function(){this.style.setProperty(t,n,e)}}function vn(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function _n(t,n){return t.style.getPropertyValue(n)||pn(t).getComputedStyle(t,null).getPropertyValue(n)}function bn(t){return function(){delete this[t]}}function mn(t,n){return function(){this[t]=n}}function xn(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function wn(t){return t.trim().split(/^|\s+/)}function Mn(t){return t.classList||new Tn(t)}function Tn(t){this._node=t,this._names=wn(t.getAttribute("class")||"")}function An(t,n){for(var e=Mn(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Gn=[null];function Vn(t,n){this._groups=t,this._parents=n}function Wn(){return new Vn([[document.documentElement]],Gn)}function Zn(t){return"string"==typeof t?new Vn([[document.querySelector(t)]],[document.documentElement]):new Vn([[t]],Gn)}Vn.prototype=Wn.prototype={constructor:Vn,select:function(t){"function"!=typeof t&&(t=jt(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=m&&(m=b+1);!(_=y[m])&&++m=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=un);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?gn:"function"==typeof n?vn:yn)(t,n,null==e?"":e)):_n(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?bn:"function"==typeof n?xn:mn)(t,n)):this.node()[t]},classed:function(t,n){var e=wn(t+"");if(arguments.length<2){for(var r=Mn(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?Ln:Yn,r=0;r()=>t;function fe(t,{sourceEvent:n,subject:e,target:r,identifier:i,active:o,x:a,y:u,dx:c,dy:f,dispatch:s}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},subject:{value:e,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:i,enumerable:!0,configurable:!0},active:{value:o,enumerable:!0,configurable:!0},x:{value:a,enumerable:!0,configurable:!0},y:{value:u,enumerable:!0,configurable:!0},dx:{value:c,enumerable:!0,configurable:!0},dy:{value:f,enumerable:!0,configurable:!0},_:{value:s}})}function se(t){return!t.ctrlKey&&!t.button}function le(){return this.parentNode}function he(t,n){return null==n?{x:t.x,y:t.y}:n}function de(){return navigator.maxTouchPoints||"ontouchstart"in this}function pe(t,n,e){t.prototype=n.prototype=e,e.constructor=t}function ge(t,n){var e=Object.create(t.prototype);for(var r in n)e[r]=n[r];return e}function ye(){}fe.prototype.on=function(){var t=this._.on.apply(this._,arguments);return t===this._?this:t};var ve=.7,_e=1/ve,be="\\s*([+-]?\\d+)\\s*",me="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",xe="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",we=/^#([0-9a-f]{3,8})$/,Me=new RegExp(`^rgb\\(${be},${be},${be}\\)$`),Te=new RegExp(`^rgb\\(${xe},${xe},${xe}\\)$`),Ae=new RegExp(`^rgba\\(${be},${be},${be},${me}\\)$`),Se=new RegExp(`^rgba\\(${xe},${xe},${xe},${me}\\)$`),Ee=new RegExp(`^hsl\\(${me},${xe},${xe}\\)$`),Ne=new RegExp(`^hsla\\(${me},${xe},${xe},${me}\\)$`),ke={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function Ce(){return this.rgb().formatHex()}function Pe(){return this.rgb().formatRgb()}function ze(t){var n,e;return t=(t+"").trim().toLowerCase(),(n=we.exec(t))?(e=n[1].length,n=parseInt(n[1],16),6===e?$e(n):3===e?new qe(n>>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?De(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?De(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=Me.exec(t))?new qe(n[1],n[2],n[3],1):(n=Te.exec(t))?new qe(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Ae.exec(t))?De(n[1],n[2],n[3],n[4]):(n=Se.exec(t))?De(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Ee.exec(t))?Le(n[1],n[2]/100,n[3]/100,1):(n=Ne.exec(t))?Le(n[1],n[2]/100,n[3]/100,n[4]):ke.hasOwnProperty(t)?$e(ke[t]):"transparent"===t?new qe(NaN,NaN,NaN,0):null}function $e(t){return new qe(t>>16&255,t>>8&255,255&t,1)}function De(t,n,e,r){return r<=0&&(t=n=e=NaN),new qe(t,n,e,r)}function Re(t){return t instanceof ye||(t=ze(t)),t?new qe((t=t.rgb()).r,t.g,t.b,t.opacity):new qe}function Fe(t,n,e,r){return 1===arguments.length?Re(t):new qe(t,n,e,null==r?1:r)}function qe(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ue(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}`}function Ie(){const t=Oe(this.opacity);return`${1===t?"rgb(":"rgba("}${Be(this.r)}, ${Be(this.g)}, ${Be(this.b)}${1===t?")":`, ${t})`}`}function Oe(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function Be(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function Ye(t){return((t=Be(t))<16?"0":"")+t.toString(16)}function Le(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Xe(t,n,e,r)}function je(t){if(t instanceof Xe)return new Xe(t.h,t.s,t.l,t.opacity);if(t instanceof ye||(t=ze(t)),!t)return new Xe;if(t instanceof Xe)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new Xe(a,u,c,t.opacity)}function He(t,n,e,r){return 1===arguments.length?je(t):new Xe(t,n,e,null==r?1:r)}function Xe(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Ge(t){return(t=(t||0)%360)<0?t+360:t}function Ve(t){return Math.max(0,Math.min(1,t||0))}function We(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}pe(ye,ze,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Ce,formatHex:Ce,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return je(this).formatHsl()},formatRgb:Pe,toString:Pe}),pe(qe,Fe,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new qe(Be(this.r),Be(this.g),Be(this.b),Oe(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Ue,formatHex:Ue,formatHex8:function(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}${Ye(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:Ie,toString:Ie})),pe(Xe,He,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new Xe(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new Xe(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new qe(We(t>=240?t-240:t+120,i,r),We(t,i,r),We(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new Xe(Ge(this.h),Ve(this.s),Ve(this.l),Oe(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Oe(this.opacity);return`${1===t?"hsl(":"hsla("}${Ge(this.h)}, ${100*Ve(this.s)}%, ${100*Ve(this.l)}%${1===t?")":`, ${t})`}`}}));const Ze=Math.PI/180,Ke=180/Math.PI,Qe=.96422,Je=1,tr=.82521,nr=4/29,er=6/29,rr=3*er*er,ir=er*er*er;function or(t){if(t instanceof ur)return new ur(t.l,t.a,t.b,t.opacity);if(t instanceof pr)return gr(t);t instanceof qe||(t=Re(t));var n,e,r=lr(t.r),i=lr(t.g),o=lr(t.b),a=cr((.2225045*r+.7168786*i+.0606169*o)/Je);return r===i&&i===o?n=e=a:(n=cr((.4360747*r+.3850649*i+.1430804*o)/Qe),e=cr((.0139322*r+.0971045*i+.7141733*o)/tr)),new ur(116*a-16,500*(n-a),200*(a-e),t.opacity)}function ar(t,n,e,r){return 1===arguments.length?or(t):new ur(t,n,e,null==r?1:r)}function ur(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function cr(t){return t>ir?Math.pow(t,1/3):t/rr+nr}function fr(t){return t>er?t*t*t:rr*(t-nr)}function sr(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function lr(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function hr(t){if(t instanceof pr)return new pr(t.h,t.c,t.l,t.opacity);if(t instanceof ur||(t=or(t)),0===t.a&&0===t.b)return new pr(NaN,0=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r()=>t;function Cr(t,n){return function(e){return t+e*n}}function Pr(t,n){var e=n-t;return e?Cr(t,e>180||e<-180?e-360*Math.round(e/360):e):kr(isNaN(t)?n:t)}function zr(t){return 1==(t=+t)?$r:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):kr(isNaN(n)?e:n)}}function $r(t,n){var e=n-t;return e?Cr(t,e):kr(isNaN(t)?n:t)}var Dr=function t(n){var e=zr(n);function r(t,n){var r=e((t=Fe(t)).r,(n=Fe(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=$r(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function Rr(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:Yr(e,r)})),o=Hr.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Yr(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Yr(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:Yr(t,e)},{i:u-2,x:Yr(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(void 0,t),n=n._next;--yi}function Ci(){xi=(mi=Mi.now())+wi,yi=vi=0;try{ki()}finally{yi=0,function(){var t,n,e=pi,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:pi=n);gi=t,zi(r)}(),xi=0}}function Pi(){var t=Mi.now(),n=t-mi;n>bi&&(wi-=n,mi=t)}function zi(t){yi||(vi&&(vi=clearTimeout(vi)),t-xi>24?(t<1/0&&(vi=setTimeout(Ci,t-Mi.now()-wi)),_i&&(_i=clearInterval(_i))):(_i||(mi=Mi.now(),_i=setInterval(Pi,bi)),yi=1,Ti(Ci)))}function $i(t,n,e){var r=new Ei;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}Ei.prototype=Ni.prototype={constructor:Ei,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Ai():+e)+(null==n?0:+n),this._next||gi===this||(gi?gi._next=this:pi=this,gi=this),this._call=t,this._time=e,zi()},stop:function(){this._call&&(this._call=null,this._time=1/0,zi())}};var Di=$t("start","end","cancel","interrupt"),Ri=[],Fi=0,qi=1,Ui=2,Ii=3,Oi=4,Bi=5,Yi=6;function Li(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=qi,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay)}function a(o){var f,s,l,h;if(e.state!==qi)return c();for(f in i)if((h=i[f]).name===e.name){if(h.state===Ii)return $i(a);h.state===Oi?(h.state=Yi,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fFi)throw new Error("too late; already scheduled");return e}function Hi(t,n){var e=Xi(t,n);if(e.state>Ii)throw new Error("too late; already running");return e}function Xi(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Gi(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>Ui&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ji:Hi;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=It(t),r="transform"===e?ni:Ki;return this.attrTween(t,"function"==typeof n?(e.local?ro:eo)(e,r,Zi(this,"attr."+t,n)):null==n?(e.local?Ji:Qi)(e):(e.local?no:to)(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=It(t);return this.tween(e,(r.local?io:oo)(r,n))},style:function(t,n,e){var r="transform"==(t+="")?ti:Ki;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=_n(this,t),a=(this.style.removeProperty(t),_n(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,lo(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=_n(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=_n(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Zi(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=Hi(this,t),f=c.on,s=null==c.value[a]?o||(o=lo(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=_n(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Zi(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var n="text";if(arguments.length<1)return(n=this.tween(n))&&n._value;if(null==t)return this.tween(n,null);if("function"!=typeof t)throw new Error;return this.tween(n,function(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&function(t){return function(n){this.textContent=t.call(this,n)}}(r)),n}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Xi(this.node(),e).tween,o=0,a=i.length;o()=>t;function Qo(t,{sourceEvent:n,target:e,selection:r,mode:i,dispatch:o}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},selection:{value:r,enumerable:!0,configurable:!0},mode:{value:i,enumerable:!0,configurable:!0},_:{value:o}})}function Jo(t){t.preventDefault(),t.stopImmediatePropagation()}var ta={name:"drag"},na={name:"space"},ea={name:"handle"},ra={name:"center"};const{abs:ia,max:oa,min:aa}=Math;function ua(t){return[+t[0],+t[1]]}function ca(t){return[ua(t[0]),ua(t[1])]}var fa={name:"x",handles:["w","e"].map(va),input:function(t,n){return null==t?null:[[+t[0],n[0][1]],[+t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},sa={name:"y",handles:["n","s"].map(va),input:function(t,n){return null==t?null:[[n[0][0],+t[0]],[n[1][0],+t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},la={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(va),input:function(t){return null==t?null:ca(t)},output:function(t){return t}},ha={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},da={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},pa={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},ga={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},ya={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function va(t){return{type:t}}function _a(t){return!t.ctrlKey&&!t.button}function ba(){var t=this.ownerSVGElement||this;return t.hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function ma(){return navigator.maxTouchPoints||"ontouchstart"in this}function xa(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function wa(t){var n,e=ba,r=_a,i=ma,o=!0,a=$t("start","brush","end"),u=6;function c(n){var e=n.property("__brush",g).selectAll(".overlay").data([va("overlay")]);e.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",ha.overlay).merge(e).each((function(){var t=xa(this).extent;Zn(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])})),n.selectAll(".selection").data([va("selection")]).enter().append("rect").attr("class","selection").attr("cursor",ha.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var r=n.selectAll(".handle").data(t.handles,(function(t){return t.type}));r.exit().remove(),r.enter().append("rect").attr("class",(function(t){return"handle handle--"+t.type})).attr("cursor",(function(t){return ha[t.type]})),n.each(f).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",h).filter(i).on("touchstart.brush",h).on("touchmove.brush",d).on("touchend.brush touchcancel.brush",p).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function f(){var t=Zn(this),n=xa(this).selection;n?(t.selectAll(".selection").style("display",null).attr("x",n[0][0]).attr("y",n[0][1]).attr("width",n[1][0]-n[0][0]).attr("height",n[1][1]-n[0][1]),t.selectAll(".handle").style("display",null).attr("x",(function(t){return"e"===t.type[t.type.length-1]?n[1][0]-u/2:n[0][0]-u/2})).attr("y",(function(t){return"s"===t.type[0]?n[1][1]-u/2:n[0][1]-u/2})).attr("width",(function(t){return"n"===t.type||"s"===t.type?n[1][0]-n[0][0]+u:u})).attr("height",(function(t){return"e"===t.type||"w"===t.type?n[1][1]-n[0][1]+u:u}))):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function s(t,n,e){var r=t.__brush.emitter;return!r||e&&r.clean?new l(t,n,e):r}function l(t,n,e){this.that=t,this.args=n,this.state=t.__brush,this.active=0,this.clean=e}function h(e){if((!n||e.touches)&&r.apply(this,arguments)){var i,a,u,c,l,h,d,p,g,y,v,_=this,b=e.target.__data__.type,m="selection"===(o&&e.metaKey?b="overlay":b)?ta:o&&e.altKey?ra:ea,x=t===sa?null:ga[b],w=t===fa?null:ya[b],M=xa(_),T=M.extent,A=M.selection,S=T[0][0],E=T[0][1],N=T[1][0],k=T[1][1],C=0,P=0,z=x&&w&&o&&e.shiftKey,$=Array.from(e.touches||[e],(t=>{const n=t.identifier;return(t=ne(t,_)).point0=t.slice(),t.identifier=n,t}));Gi(_);var D=s(_,arguments,!0).beforestart();if("overlay"===b){A&&(g=!0);const n=[$[0],$[1]||$[0]];M.selection=A=[[i=t===sa?S:aa(n[0][0],n[1][0]),u=t===fa?E:aa(n[0][1],n[1][1])],[l=t===sa?N:oa(n[0][0],n[1][0]),d=t===fa?k:oa(n[0][1],n[1][1])]],$.length>1&&I(e)}else i=A[0][0],u=A[0][1],l=A[1][0],d=A[1][1];a=i,c=u,h=l,p=d;var R=Zn(_).attr("pointer-events","none"),F=R.selectAll(".overlay").attr("cursor",ha[b]);if(e.touches)D.moved=U,D.ended=O;else{var q=Zn(e.view).on("mousemove.brush",U,!0).on("mouseup.brush",O,!0);o&&q.on("keydown.brush",(function(t){switch(t.keyCode){case 16:z=x&&w;break;case 18:m===ea&&(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra,I(t));break;case 32:m!==ea&&m!==ra||(x<0?l=h-C:x>0&&(i=a-C),w<0?d=p-P:w>0&&(u=c-P),m=na,F.attr("cursor",ha.selection),I(t));break;default:return}Jo(t)}),!0).on("keyup.brush",(function(t){switch(t.keyCode){case 16:z&&(y=v=z=!1,I(t));break;case 18:m===ra&&(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea,I(t));break;case 32:m===na&&(t.altKey?(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra):(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea),F.attr("cursor",ha[b]),I(t));break;default:return}Jo(t)}),!0),ae(e.view)}f.call(_),D.start(e,m.name)}function U(t){for(const n of t.changedTouches||[t])for(const t of $)t.identifier===n.identifier&&(t.cur=ne(n,_));if(z&&!y&&!v&&1===$.length){const t=$[0];ia(t.cur[0]-t[0])>ia(t.cur[1]-t[1])?v=!0:y=!0}for(const t of $)t.cur&&(t[0]=t.cur[0],t[1]=t.cur[1]);g=!0,Jo(t),I(t)}function I(t){const n=$[0],e=n.point0;var r;switch(C=n[0]-e[0],P=n[1]-e[1],m){case na:case ta:x&&(C=oa(S-i,aa(N-l,C)),a=i+C,h=l+C),w&&(P=oa(E-u,aa(k-d,P)),c=u+P,p=d+P);break;case ea:$[1]?(x&&(a=oa(S,aa(N,$[0][0])),h=oa(S,aa(N,$[1][0])),x=1),w&&(c=oa(E,aa(k,$[0][1])),p=oa(E,aa(k,$[1][1])),w=1)):(x<0?(C=oa(S-i,aa(N-i,C)),a=i+C,h=l):x>0&&(C=oa(S-l,aa(N-l,C)),a=i,h=l+C),w<0?(P=oa(E-u,aa(k-u,P)),c=u+P,p=d):w>0&&(P=oa(E-d,aa(k-d,P)),c=u,p=d+P));break;case ra:x&&(a=oa(S,aa(N,i-C*x)),h=oa(S,aa(N,l+C*x))),w&&(c=oa(E,aa(k,u-P*w)),p=oa(E,aa(k,d+P*w)))}ht+e))}function za(t,n){var e=0,r=null,i=null,o=null;function a(a){var u,c=a.length,f=new Array(c),s=Pa(0,c),l=new Array(c*c),h=new Array(c),d=0;a=Float64Array.from({length:c*c},n?(t,n)=>a[n%c][n/c|0]:(t,n)=>a[n/c|0][n%c]);for(let n=0;nr(f[t],f[n])));for(const e of s){const r=n;if(t){const t=Pa(1+~c,c).filter((t=>t<0?a[~t*c+e]:a[e*c+t]));i&&t.sort(((t,n)=>i(t<0?-a[~t*c+e]:a[e*c+t],n<0?-a[~n*c+e]:a[e*c+n])));for(const r of t)if(r<0){(l[~r*c+e]||(l[~r*c+e]={source:null,target:null})).target={index:e,startAngle:n,endAngle:n+=a[~r*c+e]*d,value:a[~r*c+e]}}else{(l[e*c+r]||(l[e*c+r]={source:null,target:null})).source={index:e,startAngle:n,endAngle:n+=a[e*c+r]*d,value:a[e*c+r]}}h[e]={index:e,startAngle:r,endAngle:n,value:f[e]}}else{const t=Pa(0,c).filter((t=>a[e*c+t]||a[t*c+e]));i&&t.sort(((t,n)=>i(a[e*c+t],a[e*c+n])));for(const r of t){let t;if(e=0))throw new Error(`invalid digits: ${t}`);if(n>15)return qa;const e=10**n;return function(t){this._+=t[0];for(let n=1,r=t.length;nRa)if(Math.abs(s*u-c*f)>Ra&&i){let h=e-o,d=r-a,p=u*u+c*c,g=h*h+d*d,y=Math.sqrt(p),v=Math.sqrt(l),_=i*Math.tan(($a-Math.acos((p+l-g)/(2*y*v)))/2),b=_/v,m=_/y;Math.abs(b-1)>Ra&&this._append`L${t+b*f},${n+b*s}`,this._append`A${i},${i},0,0,${+(s*h>f*d)},${this._x1=t+m*u},${this._y1=n+m*c}`}else this._append`L${this._x1=t},${this._y1=n}`;else;}arc(t,n,e,r,i,o){if(t=+t,n=+n,o=!!o,(e=+e)<0)throw new Error(`negative radius: ${e}`);let a=e*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;null===this._x1?this._append`M${c},${f}`:(Math.abs(this._x1-c)>Ra||Math.abs(this._y1-f)>Ra)&&this._append`L${c},${f}`,e&&(l<0&&(l=l%Da+Da),l>Fa?this._append`A${e},${e},0,1,${s},${t-a},${n-u}A${e},${e},0,1,${s},${this._x1=c},${this._y1=f}`:l>Ra&&this._append`A${e},${e},0,${+(l>=$a)},${s},${this._x1=t+e*Math.cos(i)},${this._y1=n+e*Math.sin(i)}`)}rect(t,n,e,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${e=+e}v${+r}h${-e}Z`}toString(){return this._}};function Ia(){return new Ua}Ia.prototype=Ua.prototype;var Oa=Array.prototype.slice;function Ba(t){return function(){return t}}function Ya(t){return t.source}function La(t){return t.target}function ja(t){return t.radius}function Ha(t){return t.startAngle}function Xa(t){return t.endAngle}function Ga(){return 0}function Va(){return 10}function Wa(t){var n=Ya,e=La,r=ja,i=ja,o=Ha,a=Xa,u=Ga,c=null;function f(){var f,s=n.apply(this,arguments),l=e.apply(this,arguments),h=u.apply(this,arguments)/2,d=Oa.call(arguments),p=+r.apply(this,(d[0]=s,d)),g=o.apply(this,d)-Ea,y=a.apply(this,d)-Ea,v=+i.apply(this,(d[0]=l,d)),_=o.apply(this,d)-Ea,b=a.apply(this,d)-Ea;if(c||(c=f=Ia()),h>Ca&&(Ma(y-g)>2*h+Ca?y>g?(g+=h,y-=h):(g-=h,y+=h):g=y=(g+y)/2,Ma(b-_)>2*h+Ca?b>_?(_+=h,b-=h):(_-=h,b+=h):_=b=(_+b)/2),c.moveTo(p*Ta(g),p*Aa(g)),c.arc(0,0,p,g,y),g!==_||y!==b)if(t){var m=v-+t.apply(this,arguments),x=(_+b)/2;c.quadraticCurveTo(0,0,m*Ta(_),m*Aa(_)),c.lineTo(v*Ta(x),v*Aa(x)),c.lineTo(m*Ta(b),m*Aa(b))}else c.quadraticCurveTo(0,0,v*Ta(_),v*Aa(_)),c.arc(0,0,v,_,b);if(c.quadraticCurveTo(0,0,p*Ta(g),p*Aa(g)),c.closePath(),f)return c=null,f+""||null}return t&&(f.headRadius=function(n){return arguments.length?(t="function"==typeof n?n:Ba(+n),f):t}),f.radius=function(t){return arguments.length?(r=i="function"==typeof t?t:Ba(+t),f):r},f.sourceRadius=function(t){return arguments.length?(r="function"==typeof t?t:Ba(+t),f):r},f.targetRadius=function(t){return arguments.length?(i="function"==typeof t?t:Ba(+t),f):i},f.startAngle=function(t){return arguments.length?(o="function"==typeof t?t:Ba(+t),f):o},f.endAngle=function(t){return arguments.length?(a="function"==typeof t?t:Ba(+t),f):a},f.padAngle=function(t){return arguments.length?(u="function"==typeof t?t:Ba(+t),f):u},f.source=function(t){return arguments.length?(n=t,f):n},f.target=function(t){return arguments.length?(e=t,f):e},f.context=function(t){return arguments.length?(c=null==t?null:t,f):c},f}var Za=Array.prototype.slice;function Ka(t,n){return t-n}var Qa=t=>()=>t;function Ja(t,n){for(var e,r=-1,i=n.length;++rr!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function nu(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function eu(){}var ru=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function iu(){var t=1,n=1,e=K,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(Ka);else{const e=M(t,ou);for(n=G(...Z(e[0],e[1],n),n);n[n.length-1]>=e[1];)n.pop();for(;n[1]o(t,n)))}function o(e,i){const o=null==i?NaN:+i;if(isNaN(o))throw new Error(`invalid value: ${i}`);var u=[],c=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=au(e[0],r),ru[f<<1].forEach(p);for(;++o=r,ru[s<<2].forEach(p);for(;++o0?u.push([t]):c.push(t)})),c.forEach((function(t){for(var n,e=0,r=u.length;e0&&o0&&a=0&&o>=0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:eu,i):r===u},i}function ou(t){return isFinite(t)?t:NaN}function au(t,n){return null!=t&&+t>=n}function uu(t){return null==t||isNaN(t=+t)?-1/0:t}function cu(t,n,e,r){const i=r-n,o=e-n,a=isFinite(i)||isFinite(o)?i/o:Math.sign(i)/Math.sign(o);return isNaN(a)?t:t+a-.5}function fu(t){return t[0]}function su(t){return t[1]}function lu(){return 1}const hu=134217729,du=33306690738754706e-32;function pu(t,n,e,r,i){let o,a,u,c,f=n[0],s=r[0],l=0,h=0;s>f==s>-f?(o=f,f=n[++l]):(o=s,s=r[++h]);let d=0;if(lf==s>-f?(a=f+o,u=o-(a-f),f=n[++l]):(a=s+o,u=o-(a-s),s=r[++h]),o=a,0!==u&&(i[d++]=u);lf==s>-f?(a=o+f,c=a-o,u=o-(a-c)+(f-c),f=n[++l]):(a=o+s,c=a-o,u=o-(a-c)+(s-c),s=r[++h]),o=a,0!==u&&(i[d++]=u);for(;l=33306690738754716e-32*f?c:-function(t,n,e,r,i,o,a){let u,c,f,s,l,h,d,p,g,y,v,_,b,m,x,w,M,T;const A=t-i,S=e-i,E=n-o,N=r-o;m=A*N,h=hu*A,d=h-(h-A),p=A-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=E*S,h=hu*E,d=h-(h-E),p=E-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,_u[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,_u[1]=b-(v+l)+(l-w),T=_+v,l=T-_,_u[2]=_-(T-l)+(v-l),_u[3]=T;let k=function(t,n){let e=n[0];for(let r=1;r=C||-k>=C)return k;if(l=t-A,u=t-(A+l)+(l-i),l=e-S,f=e-(S+l)+(l-i),l=n-E,c=n-(E+l)+(l-o),l=r-N,s=r-(N+l)+(l-o),0===u&&0===c&&0===f&&0===s)return k;if(C=vu*a+du*Math.abs(k),k+=A*s+N*u-(E*f+S*c),k>=C||-k>=C)return k;m=u*N,h=hu*u,d=h-(h-u),p=u-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=c*S,h=hu*c,d=h-(h-c),p=c-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const P=pu(4,_u,4,wu,bu);m=A*s,h=hu*A,d=h-(h-A),p=A-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=E*f,h=hu*E,d=h-(h-E),p=E-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const z=pu(P,bu,4,wu,mu);m=u*s,h=hu*u,d=h-(h-u),p=u-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=c*f,h=hu*c,d=h-(h-c),p=c-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const $=pu(z,mu,4,wu,xu);return xu[$-1]}(t,n,e,r,i,o,f)}const Tu=Math.pow(2,-52),Au=new Uint32Array(512);class Su{static from(t,n=zu,e=$u){const r=t.length,i=new Float64Array(2*r);for(let o=0;o>1;if(n>0&&"number"!=typeof t[0])throw new Error("Expected coords to contain numbers.");this.coords=t;const e=Math.max(2*n-5,0);this._triangles=new Uint32Array(3*e),this._halfedges=new Int32Array(3*e),this._hashSize=Math.ceil(Math.sqrt(n)),this._hullPrev=new Uint32Array(n),this._hullNext=new Uint32Array(n),this._hullTri=new Uint32Array(n),this._hullHash=new Int32Array(this._hashSize),this._ids=new Uint32Array(n),this._dists=new Float64Array(n),this.update()}update(){const{coords:t,_hullPrev:n,_hullNext:e,_hullTri:r,_hullHash:i}=this,o=t.length>>1;let a=1/0,u=1/0,c=-1/0,f=-1/0;for(let n=0;nc&&(c=e),r>f&&(f=r),this._ids[n]=n}const s=(a+c)/2,l=(u+f)/2;let h,d,p;for(let n=0,e=1/0;n0&&(d=n,e=r)}let v=t[2*d],_=t[2*d+1],b=1/0;for(let n=0;nr&&(n[e++]=i,r=o)}return this.hull=n.subarray(0,e),this.triangles=new Uint32Array(0),void(this.halfedges=new Uint32Array(0))}if(Mu(g,y,v,_,m,x)<0){const t=d,n=v,e=_;d=p,v=m,_=x,p=t,m=n,x=e}const w=function(t,n,e,r,i,o){const a=e-t,u=r-n,c=i-t,f=o-n,s=a*a+u*u,l=c*c+f*f,h=.5/(a*f-u*c),d=t+(f*s-u*l)*h,p=n+(a*l-c*s)*h;return{x:d,y:p}}(g,y,v,_,m,x);this._cx=w.x,this._cy=w.y;for(let n=0;n0&&Math.abs(f-o)<=Tu&&Math.abs(s-a)<=Tu)continue;if(o=f,a=s,c===h||c===d||c===p)continue;let l=0;for(let t=0,n=this._hashKey(f,s);t=0;)if(y=g,y===l){y=-1;break}if(-1===y)continue;let v=this._addTriangle(y,c,e[y],-1,-1,r[y]);r[c]=this._legalize(v+2),r[y]=v,M++;let _=e[y];for(;g=e[_],Mu(f,s,t[2*_],t[2*_+1],t[2*g],t[2*g+1])<0;)v=this._addTriangle(_,c,g,r[c],-1,r[_]),r[c]=this._legalize(v+2),e[_]=_,M--,_=g;if(y===l)for(;g=n[y],Mu(f,s,t[2*g],t[2*g+1],t[2*y],t[2*y+1])<0;)v=this._addTriangle(g,c,y,-1,r[y],r[g]),this._legalize(v+2),r[g]=v,e[y]=y,M--,y=g;this._hullStart=n[c]=y,e[y]=n[_]=c,e[c]=_,i[this._hashKey(f,s)]=c,i[this._hashKey(t[2*y],t[2*y+1])]=y}this.hull=new Uint32Array(M);for(let t=0,n=this._hullStart;t0?3-e:1+e)/4}(t-this._cx,n-this._cy)*this._hashSize)%this._hashSize}_legalize(t){const{_triangles:n,_halfedges:e,coords:r}=this;let i=0,o=0;for(;;){const a=e[t],u=t-t%3;if(o=u+(t+2)%3,-1===a){if(0===i)break;t=Au[--i];continue}const c=a-a%3,f=u+(t+1)%3,s=c+(a+2)%3,l=n[o],h=n[t],d=n[f],p=n[s];if(Nu(r[2*l],r[2*l+1],r[2*h],r[2*h+1],r[2*d],r[2*d+1],r[2*p],r[2*p+1])){n[t]=p,n[a]=l;const r=e[s];if(-1===r){let n=this._hullStart;do{if(this._hullTri[n]===s){this._hullTri[n]=t;break}n=this._hullPrev[n]}while(n!==this._hullStart)}this._link(t,r),this._link(a,e[o]),this._link(o,s);const u=c+(a+1)%3;i=e&&n[t[a]]>o;)t[a+1]=t[a--];t[a+1]=r}else{let i=e+1,o=r;Pu(t,e+r>>1,i),n[t[e]]>n[t[r]]&&Pu(t,e,r),n[t[i]]>n[t[r]]&&Pu(t,i,r),n[t[e]]>n[t[i]]&&Pu(t,e,i);const a=t[i],u=n[a];for(;;){do{i++}while(n[t[i]]u);if(o=o-e?(Cu(t,n,i,r),Cu(t,n,e,o-1)):(Cu(t,n,e,o-1),Cu(t,n,i,r))}}function Pu(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function zu(t){return t[0]}function $u(t){return t[1]}const Du=1e-6;class Ru{constructor(){this._x0=this._y0=this._x1=this._y1=null,this._=""}moveTo(t,n){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}`}closePath(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._+="Z")}lineTo(t,n){this._+=`L${this._x1=+t},${this._y1=+n}`}arc(t,n,e){const r=(t=+t)+(e=+e),i=n=+n;if(e<0)throw new Error("negative radius");null===this._x1?this._+=`M${r},${i}`:(Math.abs(this._x1-r)>Du||Math.abs(this._y1-i)>Du)&&(this._+="L"+r+","+i),e&&(this._+=`A${e},${e},0,1,1,${t-e},${n}A${e},${e},0,1,1,${this._x1=r},${this._y1=i}`)}rect(t,n,e,r){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${+e}v${+r}h${-e}Z`}value(){return this._||null}}class Fu{constructor(){this._=[]}moveTo(t,n){this._.push([t,n])}closePath(){this._.push(this._[0].slice())}lineTo(t,n){this._.push([t,n])}value(){return this._.length?this._:null}}class qu{constructor(t,[n,e,r,i]=[0,0,960,500]){if(!((r=+r)>=(n=+n)&&(i=+i)>=(e=+e)))throw new Error("invalid bounds");this.delaunay=t,this._circumcenters=new Float64Array(2*t.points.length),this.vectors=new Float64Array(2*t.points.length),this.xmax=r,this.xmin=n,this.ymax=i,this.ymin=e,this._init()}update(){return this.delaunay.update(),this._init(),this}_init(){const{delaunay:{points:t,hull:n,triangles:e},vectors:r}=this;let i,o;const a=this.circumcenters=this._circumcenters.subarray(0,e.length/3*2);for(let r,u,c=0,f=0,s=e.length;c1;)i-=2;for(let t=2;t0){if(n>=this.ymax)return null;(i=(this.ymax-n)/r)0){if(t>=this.xmax)return null;(i=(this.xmax-t)/e)this.xmax?2:0)|(nthis.ymax?8:0)}_simplify(t){if(t&&t.length>4){for(let n=0;n2&&function(t){const{triangles:n,coords:e}=t;for(let t=0;t1e-10)return!1}return!0}(t)){this.collinear=Int32Array.from({length:n.length/2},((t,n)=>n)).sort(((t,e)=>n[2*t]-n[2*e]||n[2*t+1]-n[2*e+1]));const t=this.collinear[0],e=this.collinear[this.collinear.length-1],r=[n[2*t],n[2*t+1],n[2*e],n[2*e+1]],i=1e-8*Math.hypot(r[3]-r[1],r[2]-r[0]);for(let t=0,e=n.length/2;t0&&(this.triangles=new Int32Array(3).fill(-1),this.halfedges=new Int32Array(3).fill(-1),this.triangles[0]=r[0],o[r[0]]=1,2===r.length&&(o[r[1]]=0,this.triangles[1]=r[1],this.triangles[2]=r[1]))}voronoi(t){return new qu(this,t)}*neighbors(t){const{inedges:n,hull:e,_hullIndex:r,halfedges:i,triangles:o,collinear:a}=this;if(a){const n=a.indexOf(t);return n>0&&(yield a[n-1]),void(n=0&&i!==e&&i!==r;)e=i;return i}_step(t,n,e){const{inedges:r,hull:i,_hullIndex:o,halfedges:a,triangles:u,points:c}=this;if(-1===r[t]||!c.length)return(t+1)%(c.length>>1);let f=t,s=Iu(n-c[2*t],2)+Iu(e-c[2*t+1],2);const l=r[t];let h=l;do{let r=u[h];const l=Iu(n-c[2*r],2)+Iu(e-c[2*r+1],2);if(l9999?"+"+Ku(n,6):Ku(n,4))+"-"+Ku(t.getUTCMonth()+1,2)+"-"+Ku(t.getUTCDate(),2)+(o?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"."+Ku(o,3)+"Z":i?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"Z":r||e?"T"+Ku(e,2)+":"+Ku(r,2)+"Z":"")}function Ju(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return Hu;if(f)return f=!1,ju;var n,r,i=a;if(t.charCodeAt(i)===Xu){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===Gu?f=!0:r===Vu&&(f=!0,t.charCodeAt(a)===Gu&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;amc(n,e).then((n=>(new DOMParser).parseFromString(n,t)))}var Sc=Ac("application/xml"),Ec=Ac("text/html"),Nc=Ac("image/svg+xml");function kc(t,n,e,r){if(isNaN(n)||isNaN(e))return t;var i,o,a,u,c,f,s,l,h,d=t._root,p={data:r},g=t._x0,y=t._y0,v=t._x1,_=t._y1;if(!d)return t._root=p,t;for(;d.length;)if((f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function Cc(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Pc(t){return t[0]}function zc(t){return t[1]}function $c(t,n,e){var r=new Dc(null==n?Pc:n,null==e?zc:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function Dc(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function Rc(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var Fc=$c.prototype=Dc.prototype;function qc(t){return function(){return t}}function Uc(t){return 1e-6*(t()-.5)}function Ic(t){return t.x+t.vx}function Oc(t){return t.y+t.vy}function Bc(t){return t.index}function Yc(t,n){var e=t.get(n);if(!e)throw new Error("node not found: "+n);return e}Fc.copy=function(){var t,n,e=new Dc(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=Rc(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=Rc(n));return e},Fc.add=function(t){const n=+this._x.call(null,t),e=+this._y.call(null,t);return kc(this.cover(n,e),n,e,t)},Fc.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));if(c>s||f>l)return this;for(this.cover(c,f).cover(s,l),e=0;et||t>=i||r>n||n>=o;)switch(u=(nh||(o=c.y0)>d||(a=c.x1)=v)<<1|t>=y)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,g.data),b=n-+this._y.call(null,g.data),m=_*_+b*b;if(m=(u=(p+y)/2))?p=u:y=u,(s=a>=(c=(g+v)/2))?g=c:v=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},Fc.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function Zc(t){return(t=Wc(Math.abs(t)))?t[1]:NaN}var Kc,Qc=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Jc(t){if(!(n=Qc.exec(t)))throw new Error("invalid format: "+t);var n;return new tf({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function tf(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function nf(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Jc.prototype=tf.prototype,tf.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var ef={"%":(t,n)=>(100*t).toFixed(n),b:t=>Math.round(t).toString(2),c:t=>t+"",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:(t,n)=>t.toExponential(n),f:(t,n)=>t.toFixed(n),g:(t,n)=>t.toPrecision(n),o:t=>Math.round(t).toString(8),p:(t,n)=>nf(100*t,n),r:nf,s:function(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(Kc=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Wc(t,Math.max(0,n+o-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function rf(t){return t}var of,af=Array.prototype.map,uf=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function cf(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?rf:(n=af.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",a=void 0===t.decimal?".":t.decimal+"",u=void 0===t.numerals?rf:function(t){return function(n){return n.replace(/[0-9]/g,(function(n){return t[+n]}))}}(af.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",f=void 0===t.minus?"−":t.minus+"",s=void 0===t.nan?"NaN":t.nan+"";function l(t){var n=(t=Jc(t)).fill,e=t.align,l=t.sign,h=t.symbol,d=t.zero,p=t.width,g=t.comma,y=t.precision,v=t.trim,_=t.type;"n"===_?(g=!0,_="g"):ef[_]||(void 0===y&&(y=12),v=!0,_="g"),(d||"0"===n&&"="===e)&&(d=!0,n="0",e="=");var b="$"===h?i:"#"===h&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",m="$"===h?o:/[%p]/.test(_)?c:"",x=ef[_],w=/[defgprs%]/.test(_);function M(t){var i,o,c,h=b,M=m;if("c"===_)M=x(t)+M,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?s:x(Math.abs(t),y),v&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),T&&0==+t&&"+"!==l&&(T=!1),h=(T?"("===l?l:f:"-"===l||"("===l?"":l)+h,M=("s"===_?uf[8+Kc/3]:"")+M+(T&&"("===l?")":""),w)for(i=-1,o=t.length;++i(c=t.charCodeAt(i))||c>57){M=(46===c?a+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}g&&!d&&(t=r(t,1/0));var A=h.length+t.length+M.length,S=A>1)+h+t+M+S.slice(A);break;default:t=S+h+t+M}return u(t)}return y=void 0===y?6:/[gprs]/.test(_)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y)),M.toString=function(){return t+""},M}return{format:l,formatPrefix:function(t,n){var e=l(((t=Jc(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3))),i=Math.pow(10,-r),o=uf[8+r/3];return function(t){return e(i*t)+o}}}}function ff(n){return of=cf(n),t.format=of.format,t.formatPrefix=of.formatPrefix,of}function sf(t){return Math.max(0,-Zc(Math.abs(t)))}function lf(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3)))-Zc(Math.abs(t)))}function hf(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Zc(n)-Zc(t))+1}t.format=void 0,t.formatPrefix=void 0,ff({thousands:",",grouping:[3],currency:["$",""]});var df=1e-6,pf=1e-12,gf=Math.PI,yf=gf/2,vf=gf/4,_f=2*gf,bf=180/gf,mf=gf/180,xf=Math.abs,wf=Math.atan,Mf=Math.atan2,Tf=Math.cos,Af=Math.ceil,Sf=Math.exp,Ef=Math.hypot,Nf=Math.log,kf=Math.pow,Cf=Math.sin,Pf=Math.sign||function(t){return t>0?1:t<0?-1:0},zf=Math.sqrt,$f=Math.tan;function Df(t){return t>1?0:t<-1?gf:Math.acos(t)}function Rf(t){return t>1?yf:t<-1?-yf:Math.asin(t)}function Ff(t){return(t=Cf(t/2))*t}function qf(){}function Uf(t,n){t&&Of.hasOwnProperty(t.type)&&Of[t.type](t,n)}var If={Feature:function(t,n){Uf(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=Tf(n=(n*=mf)/2+vf),a=Cf(n),u=Vf*a,c=Gf*o+u*Tf(i),f=u*r*Cf(i);as.add(Mf(f,c)),Xf=t,Gf=o,Vf=a}function ds(t){return[Mf(t[1],t[0]),Rf(t[2])]}function ps(t){var n=t[0],e=t[1],r=Tf(e);return[r*Tf(n),r*Cf(n),Cf(e)]}function gs(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function ys(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function vs(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function _s(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function bs(t){var n=zf(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var ms,xs,ws,Ms,Ts,As,Ss,Es,Ns,ks,Cs,Ps,zs,$s,Ds,Rs,Fs={point:qs,lineStart:Is,lineEnd:Os,polygonStart:function(){Fs.point=Bs,Fs.lineStart=Ys,Fs.lineEnd=Ls,rs=new T,cs.polygonStart()},polygonEnd:function(){cs.polygonEnd(),Fs.point=qs,Fs.lineStart=Is,Fs.lineEnd=Os,as<0?(Wf=-(Kf=180),Zf=-(Qf=90)):rs>df?Qf=90:rs<-df&&(Zf=-90),os[0]=Wf,os[1]=Kf},sphere:function(){Wf=-(Kf=180),Zf=-(Qf=90)}};function qs(t,n){is.push(os=[Wf=t,Kf=t]),nQf&&(Qf=n)}function Us(t,n){var e=ps([t*mf,n*mf]);if(es){var r=ys(es,e),i=ys([r[1],-r[0],0],r);bs(i),i=ds(i);var o,a=t-Jf,u=a>0?1:-1,c=i[0]*bf*u,f=xf(a)>180;f^(u*JfQf&&(Qf=o):f^(u*Jf<(c=(c+360)%360-180)&&cQf&&(Qf=n)),f?tjs(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t):Kf>=Wf?(tKf&&(Kf=t)):t>Jf?js(Wf,t)>js(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t)}else is.push(os=[Wf=t,Kf=t]);nQf&&(Qf=n),es=e,Jf=t}function Is(){Fs.point=Us}function Os(){os[0]=Wf,os[1]=Kf,Fs.point=qs,es=null}function Bs(t,n){if(es){var e=t-Jf;rs.add(xf(e)>180?e+(e>0?360:-360):e)}else ts=t,ns=n;cs.point(t,n),Us(t,n)}function Ys(){cs.lineStart()}function Ls(){Bs(ts,ns),cs.lineEnd(),xf(rs)>df&&(Wf=-(Kf=180)),os[0]=Wf,os[1]=Kf,es=null}function js(t,n){return(n-=t)<0?n+360:n}function Hs(t,n){return t[0]-n[0]}function Xs(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:ngf&&(t-=Math.round(t/_f)*_f),[t,n]}function ul(t,n,e){return(t%=_f)?n||e?ol(fl(t),sl(n,e)):fl(t):n||e?sl(n,e):al}function cl(t){return function(n,e){return xf(n+=t)>gf&&(n-=Math.round(n/_f)*_f),[n,e]}}function fl(t){var n=cl(t);return n.invert=cl(-t),n}function sl(t,n){var e=Tf(t),r=Cf(t),i=Tf(n),o=Cf(n);function a(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*e+u*r;return[Mf(c*i-s*o,u*e-f*r),Rf(s*i+c*o)]}return a.invert=function(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*i-c*o;return[Mf(c*i+f*o,u*e+s*r),Rf(s*e-u*r)]},a}function ll(t){function n(n){return(n=t(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n}return t=ul(t[0]*mf,t[1]*mf,t.length>2?t[2]*mf:0),n.invert=function(n){return(n=t.invert(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n},n}function hl(t,n,e,r,i,o){if(e){var a=Tf(n),u=Cf(n),c=r*e;null==i?(i=n+r*_f,o=n-c/2):(i=dl(a,i),o=dl(a,o),(r>0?io)&&(i+=r*_f));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function gl(t,n){return xf(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function _l(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,E=S*A,N=E>gf,k=y*w;if(c.add(Mf(k*S*Cf(E),v*M+k*Tf(E))),a+=N?A+S*_f:A,N^p>=e^m>=e){var C=ys(ps(d),ps(b));bs(C);var P=ys(o,C);bs(P);var z=(N^A>=0?-1:1)*Rf(P[2]);(r>z||r===z&&(C[0]||C[1]))&&(u+=N^A>=0?1:-1)}}return(a<-df||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter(wl))}return h}}function wl(t){return t.length>1}function Ml(t,n){return((t=t.x)[0]<0?t[1]-yf-df:yf-t[1])-((n=n.x)[0]<0?n[1]-yf-df:yf-n[1])}al.invert=al;var Tl=xl((function(){return!0}),(function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?gf:-gf,c=xf(o-e);xf(c-gf)0?yf:-yf),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=gf&&(xf(e-i)df?wf((Cf(n)*(o=Tf(r))*Cf(e)-Cf(r)*(i=Tf(n))*Cf(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}}),(function(t,n,e,r){var i;if(null==t)i=e*yf,r.point(-gf,i),r.point(0,i),r.point(gf,i),r.point(gf,0),r.point(gf,-i),r.point(0,-i),r.point(-gf,-i),r.point(-gf,0),r.point(-gf,i);else if(xf(t[0]-n[0])>df){var o=t[0]0,i=xf(n)>df;function o(t,e){return Tf(t)*Tf(e)>n}function a(t,e,r){var i=[1,0,0],o=ys(ps(t),ps(e)),a=gs(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=ys(i,o),h=_s(i,f);vs(h,_s(o,s));var d=l,p=gs(h,d),g=gs(d,d),y=p*p-g*(gs(h,h)-1);if(!(y<0)){var v=zf(y),_=_s(d,(-p-v)/g);if(vs(_,h),_=ds(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(xf(_[0]-m)gf^(m<=_[0]&&_[0]<=x)){var S=_s(d,(-p+v)/g);return vs(S,h),[_,ds(S)]}}}function u(n,e){var i=r?t:gf-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return xl(o,(function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],g=o(l,h),y=r?g?0:u(l,h):g?u(l+(l<0?gf:-gf),h):0;if(!n&&(f=c=g)&&t.lineStart(),g!==c&&(!(d=a(n,p))||gl(n,d)||gl(p,d))&&(p[2]=1),g!==c)s=0,g?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1],2),t.lineEnd()),n=d;else if(i&&n&&r^g){var v;y&e||!(v=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(v[0][0],v[0][1]),t.point(v[1][0],v[1][1]),t.lineEnd()):(t.point(v[1][0],v[1][1]),t.lineEnd(),t.lineStart(),t.point(v[0][0],v[0][1],3)))}!g||n&&gl(n,p)||t.point(p[0],p[1]),n=p,c=g,e=y},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}}),(function(n,r,i,o){hl(o,t,e,i,n,r)}),r?[0,-t]:[-gf,t-gf])}var Sl,El,Nl,kl,Cl=1e9,Pl=-Cl;function zl(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return xf(r[0]-t)0?0:3:xf(r[0]-e)0?2:1:xf(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,g,y,v,_,b=a,m=pl(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);v=!0,y=!1,p=g=NaN},lineEnd:function(){c&&(M(l,h),d&&y&&m.rejoin(),c.push(m.result()));x.point=w,y&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=ft(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&vl(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),v)l=o,h=a,d=u,v=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&y)b.point(o,a);else{var c=[p=Math.max(Pl,Math.min(Cl,p)),g=Math.max(Pl,Math.min(Cl,g))],m=[o=Math.max(Pl,Math.min(Cl,o)),a=Math.max(Pl,Math.min(Cl,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(y||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,g=a,y=u}return x}}var $l={sphere:qf,point:qf,lineStart:function(){$l.point=Rl,$l.lineEnd=Dl},lineEnd:qf,polygonStart:qf,polygonEnd:qf};function Dl(){$l.point=$l.lineEnd=qf}function Rl(t,n){El=t*=mf,Nl=Cf(n*=mf),kl=Tf(n),$l.point=Fl}function Fl(t,n){t*=mf;var e=Cf(n*=mf),r=Tf(n),i=xf(t-El),o=Tf(i),a=r*Cf(i),u=kl*e-Nl*r*o,c=Nl*e+kl*r*o;Sl.add(Mf(zf(a*a+u*u),c)),El=t,Nl=e,kl=r}function ql(t){return Sl=new T,Lf(t,$l),+Sl}var Ul=[null,null],Il={type:"LineString",coordinates:Ul};function Ol(t,n){return Ul[0]=t,Ul[1]=n,ql(Il)}var Bl={Feature:function(t,n){return Ll(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r0&&(i=Ol(t[o],t[o-1]))>0&&e<=i&&r<=i&&(e+r-i)*(1-Math.pow((e-r)/i,2))df})).map(c)).concat(lt(Af(o/d)*d,i,d).filter((function(t){return xf(t%g)>df})).map(f))}return v.lines=function(){return _().map((function(t){return{type:"LineString",coordinates:t}}))},v.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},v.extent=function(t){return arguments.length?v.extentMajor(t).extentMinor(t):v.extentMinor()},v.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),v.precision(y)):[[r,u],[e,a]]},v.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),v.precision(y)):[[n,o],[t,i]]},v.step=function(t){return arguments.length?v.stepMajor(t).stepMinor(t):v.stepMinor()},v.stepMajor=function(t){return arguments.length?(p=+t[0],g=+t[1],v):[p,g]},v.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],v):[h,d]},v.precision=function(h){return arguments.length?(y=+h,c=Wl(o,i,90),f=Zl(n,t,y),s=Wl(u,a,90),l=Zl(r,e,y),v):y},v.extentMajor([[-180,-90+df],[180,90-df]]).extentMinor([[-180,-80-df],[180,80+df]])}var Ql,Jl,th,nh,eh=t=>t,rh=new T,ih=new T,oh={point:qf,lineStart:qf,lineEnd:qf,polygonStart:function(){oh.lineStart=ah,oh.lineEnd=fh},polygonEnd:function(){oh.lineStart=oh.lineEnd=oh.point=qf,rh.add(xf(ih)),ih=new T},result:function(){var t=rh/2;return rh=new T,t}};function ah(){oh.point=uh}function uh(t,n){oh.point=ch,Ql=th=t,Jl=nh=n}function ch(t,n){ih.add(nh*t-th*n),th=t,nh=n}function fh(){ch(Ql,Jl)}var sh=oh,lh=1/0,hh=lh,dh=-lh,ph=dh,gh={point:function(t,n){tdh&&(dh=t);nph&&(ph=n)},lineStart:qf,lineEnd:qf,polygonStart:qf,polygonEnd:qf,result:function(){var t=[[lh,hh],[dh,ph]];return dh=ph=-(hh=lh=1/0),t}};var yh,vh,_h,bh,mh=gh,xh=0,wh=0,Mh=0,Th=0,Ah=0,Sh=0,Eh=0,Nh=0,kh=0,Ch={point:Ph,lineStart:zh,lineEnd:Rh,polygonStart:function(){Ch.lineStart=Fh,Ch.lineEnd=qh},polygonEnd:function(){Ch.point=Ph,Ch.lineStart=zh,Ch.lineEnd=Rh},result:function(){var t=kh?[Eh/kh,Nh/kh]:Sh?[Th/Sh,Ah/Sh]:Mh?[xh/Mh,wh/Mh]:[NaN,NaN];return xh=wh=Mh=Th=Ah=Sh=Eh=Nh=kh=0,t}};function Ph(t,n){xh+=t,wh+=n,++Mh}function zh(){Ch.point=$h}function $h(t,n){Ch.point=Dh,Ph(_h=t,bh=n)}function Dh(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Ph(_h=t,bh=n)}function Rh(){Ch.point=Ph}function Fh(){Ch.point=Uh}function qh(){Ih(yh,vh)}function Uh(t,n){Ch.point=Ih,Ph(yh=_h=t,vh=bh=n)}function Ih(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Eh+=(i=bh*t-_h*n)*(_h+t),Nh+=i*(bh+n),kh+=3*i,Ph(_h=t,bh=n)}var Oh=Ch;function Bh(t){this._context=t}Bh.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,_f)}},result:qf};var Yh,Lh,jh,Hh,Xh,Gh=new T,Vh={point:qf,lineStart:function(){Vh.point=Wh},lineEnd:function(){Yh&&Zh(Lh,jh),Vh.point=qf},polygonStart:function(){Yh=!0},polygonEnd:function(){Yh=null},result:function(){var t=+Gh;return Gh=new T,t}};function Wh(t,n){Vh.point=Zh,Lh=Hh=t,jh=Xh=n}function Zh(t,n){Hh-=t,Xh-=n,Gh.add(zf(Hh*Hh+Xh*Xh)),Hh=t,Xh=n}var Kh=Vh;let Qh,Jh,td,nd;class ed{constructor(t){this._append=null==t?rd:function(t){const n=Math.floor(t);if(!(n>=0))throw new RangeError(`invalid digits: ${t}`);if(n>15)return rd;if(n!==Qh){const t=10**n;Qh=n,Jh=function(n){let e=1;this._+=n[0];for(const r=n.length;e4*n&&g--){var m=a+h,x=u+d,w=c+p,M=zf(m*m+x*x+w*w),T=Rf(w/=M),A=xf(xf(w)-1)n||xf((v*k+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*mf:0,k()):[y*bf,v*bf,_*bf]},E.angle=function(t){return arguments.length?(b=t%360*mf,k()):b*bf},E.reflectX=function(t){return arguments.length?(m=t?-1:1,k()):m<0},E.reflectY=function(t){return arguments.length?(x=t?-1:1,k()):x<0},E.precision=function(t){return arguments.length?(a=dd(u,S=t*t),C()):zf(S)},E.fitExtent=function(t,n){return ud(E,t,n)},E.fitSize=function(t,n){return cd(E,t,n)},E.fitWidth=function(t,n){return fd(E,t,n)},E.fitHeight=function(t,n){return sd(E,t,n)},function(){return n=t.apply(this,arguments),E.invert=n.invert&&N,k()}}function _d(t){var n=0,e=gf/3,r=vd(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*mf,e=t[1]*mf):[n*bf,e*bf]},i}function bd(t,n){var e=Cf(t),r=(e+Cf(n))/2;if(xf(r)0?n<-yf+df&&(n=-yf+df):n>yf-df&&(n=yf-df);var e=i/kf(Nd(n),r);return[e*Cf(r*t),i-e*Tf(r*t)]}return o.invert=function(t,n){var e=i-n,o=Pf(r)*zf(t*t+e*e),a=Mf(t,xf(e))*Pf(e);return e*r<0&&(a-=gf*Pf(t)*Pf(e)),[a/r,2*wf(kf(i/o,1/r))-yf]},o}function Cd(t,n){return[t,n]}function Pd(t,n){var e=Tf(t),r=t===n?Cf(t):(e-Tf(n))/(n-t),i=e/r+t;if(xf(r)=0;)n+=e[r].value;else n=1;t.value=n}function Gd(t,n){t instanceof Map?(t=[void 0,t],void 0===n&&(n=Wd)):void 0===n&&(n=Vd);for(var e,r,i,o,a,u=new Qd(t),c=[u];e=c.pop();)if((i=n(e.data))&&(a=(i=Array.from(i)).length))for(e.children=i,o=a-1;o>=0;--o)c.push(r=i[o]=new Qd(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(Kd)}function Vd(t){return t.children}function Wd(t){return Array.isArray(t)?t[1]:null}function Zd(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function Kd(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Qd(t){this.data=t,this.depth=this.height=0,this.parent=null}function Jd(t){return null==t?null:tp(t)}function tp(t){if("function"!=typeof t)throw new Error;return t}function np(){return 0}function ep(t){return function(){return t}}qd.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(zd+$d*i+o*(Dd+Rd*i))-n)/(zd+3*$d*i+o*(7*Dd+9*Rd*i)))*r)*i*i,!(xf(e)df&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},Od.invert=Md(Rf),Bd.invert=Md((function(t){return 2*wf(t)})),Yd.invert=function(t,n){return[-n,2*wf(Sf(t))-yf]},Qd.prototype=Gd.prototype={constructor:Qd,count:function(){return this.eachAfter(Xd)},each:function(t,n){let e=-1;for(const r of this)t.call(n,r,++e,this);return this},eachAfter:function(t,n){for(var e,r,i,o=this,a=[o],u=[],c=-1;o=a.pop();)if(u.push(o),e=o.children)for(r=0,i=e.length;r=0;--r)o.push(e[r]);return this},find:function(t,n){let e=-1;for(const r of this)if(t.call(n,r,++e,this))return r},sum:function(t){return this.eachAfter((function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e}))},sort:function(t){return this.eachBefore((function(n){n.children&&n.children.sort(t)}))},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;t=e.pop(),n=r.pop();for(;t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(n){n.children||t.push(n)})),t},links:function(){var t=this,n=[];return t.each((function(e){e!==t&&n.push({source:e.parent,target:e})})),n},copy:function(){return Gd(this).eachBefore(Zd)},[Symbol.iterator]:function*(){var t,n,e,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,n=i.children)for(e=0,r=n.length;e(t=(rp*t+ip)%op)/op}function up(t,n){for(var e,r,i=0,o=(t=function(t,n){let e,r,i=t.length;for(;i;)r=n()*i--|0,e=t[i],t[i]=t[r],t[r]=e;return t}(Array.from(t),n)).length,a=[];i0&&e*e>r*r+i*i}function lp(t,n){for(var e=0;e1e-6?(E+Math.sqrt(E*E-4*S*N))/(2*S):N/E);return{x:r+w+M*k,y:i+T+A*k,r:k}}function gp(t,n,e){var r,i,o,a,u=t.x-n.x,c=t.y-n.y,f=u*u+c*c;f?(i=n.r+e.r,i*=i,a=t.r+e.r,i>(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function yp(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function vp(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function _p(t){this._=t,this.next=null,this.previous=null}function bp(t,n){if(!(o=(t=function(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}(t)).length))return 0;var e,r,i,o,a,u,c,f,s,l,h;if((e=t[0]).x=0,e.y=0,!(o>1))return e.r;if(r=t[1],e.x=-r.r,r.x=e.r,r.y=0,!(o>2))return e.r+r.r;gp(r,e,i=t[2]),e=new _p(e),r=new _p(r),i=new _p(i),e.next=i.previous=r,r.next=e.previous=i,i.next=r.previous=e;t:for(c=3;c1&&!zp(t,n););return t.slice(0,n)}function zp(t,n){if("/"===t[n]){let e=0;for(;n>0&&"\\"===t[--n];)++e;if(!(1&e))return!0}return!1}function $p(t,n){return t.parent===n.parent?1:2}function Dp(t){var n=t.children;return n?n[0]:t.t}function Rp(t){var n=t.children;return n?n[n.length-1]:t.t}function Fp(t,n,e){var r=e/(n.i-t.i);n.c-=r,n.s+=e,t.c+=r,n.z+=e,n.m+=e}function qp(t,n,e){return t.a.parent===n.parent?t.a:e}function Up(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function Ip(t,n,e,r,i){for(var o,a=t.children,u=-1,c=a.length,f=t.value&&(i-e)/t.value;++uh&&(h=u),y=s*s*g,(d=Math.max(h/y,y/l))>p){s-=u;break}p=d}v.push(a={value:s,dice:c1?n:1)},e}(Op);var Lp=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(Op);function jp(t,n,e){return(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0])}function Hp(t,n){return t[0]-n[0]||t[1]-n[1]}function Xp(t){const n=t.length,e=[0,1];let r,i=2;for(r=2;r1&&jp(t[e[i-2]],t[e[i-1]],t[r])<=0;)--i;e[i++]=r}return e.slice(0,i)}var Gp=Math.random,Vp=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(Gp),Wp=function t(n){function e(t,e){return arguments.length<2&&(e=t,t=0),t=Math.floor(t),e=Math.floor(e)-t,function(){return Math.floor(n()*e+t)}}return e.source=t,e}(Gp),Zp=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(Gp),Kp=function t(n){var e=Zp.source(n);function r(){var t=e.apply(this,arguments);return function(){return Math.exp(t())}}return r.source=t,r}(Gp),Qp=function t(n){function e(t){return(t=+t)<=0?()=>0:function(){for(var e=0,r=t;r>1;--r)e+=n();return e+r*n()}}return e.source=t,e}(Gp),Jp=function t(n){var e=Qp.source(n);function r(t){if(0==(t=+t))return n;var r=e(t);return function(){return r()/t}}return r.source=t,r}(Gp),tg=function t(n){function e(t){return function(){return-Math.log1p(-n())/t}}return e.source=t,e}(Gp),ng=function t(n){function e(t){if((t=+t)<0)throw new RangeError("invalid alpha");return t=1/-t,function(){return Math.pow(1-n(),t)}}return e.source=t,e}(Gp),eg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return function(){return Math.floor(n()+t)}}return e.source=t,e}(Gp),rg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return 0===t?()=>1/0:1===t?()=>1:(t=Math.log1p(-t),function(){return 1+Math.floor(Math.log1p(-n())/t)})}return e.source=t,e}(Gp),ig=function t(n){var e=Zp.source(n)();function r(t,r){if((t=+t)<0)throw new RangeError("invalid k");if(0===t)return()=>0;if(r=null==r?1:+r,1===t)return()=>-Math.log1p(-n())*r;var i=(t<1?t+1:t)-1/3,o=1/(3*Math.sqrt(i)),a=t<1?()=>Math.pow(n(),1/t):()=>1;return function(){do{do{var t=e(),u=1+o*t}while(u<=0);u*=u*u;var c=1-n()}while(c>=1-.0331*t*t*t*t&&Math.log(c)>=.5*t*t+i*(1-u+Math.log(u)));return i*u*a()*r}}return r.source=t,r}(Gp),og=function t(n){var e=ig.source(n);function r(t,n){var r=e(t),i=e(n);return function(){var t=r();return 0===t?0:t/(t+i())}}return r.source=t,r}(Gp),ag=function t(n){var e=rg.source(n),r=og.source(n);function i(t,n){return t=+t,(n=+n)>=1?()=>t:n<=0?()=>0:function(){for(var i=0,o=t,a=n;o*a>16&&o*(1-a)>16;){var u=Math.floor((o+1)*a),c=r(u,o-u+1)();c<=a?(i+=u,o-=u,a=(a-c)/(1-c)):(o=u-1,a/=c)}for(var f=a<.5,s=e(f?a:1-a),l=s(),h=0;l<=o;++h)l+=s();return i+(f?h:o-h)}}return i.source=t,i}(Gp),ug=function t(n){function e(t,e,r){var i;return 0==(t=+t)?i=t=>-Math.log(t):(t=1/t,i=n=>Math.pow(n,t)),e=null==e?0:+e,r=null==r?1:+r,function(){return e+r*i(-Math.log1p(-n()))}}return e.source=t,e}(Gp),cg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){return t+e*Math.tan(Math.PI*n())}}return e.source=t,e}(Gp),fg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){var r=n();return t+e*Math.log(r/(1-r))}}return e.source=t,e}(Gp),sg=function t(n){var e=ig.source(n),r=ag.source(n);function i(t){return function(){for(var i=0,o=t;o>16;){var a=Math.floor(.875*o),u=e(a)();if(u>o)return i+r(a-1,o/u)();i+=a,o-=u}for(var c=-Math.log1p(-n()),f=0;c<=o;++f)c-=Math.log1p(-n());return i+f}}return i.source=t,i}(Gp);const lg=1/4294967296;function hg(t,n){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(n).domain(t)}return this}function dg(t,n){switch(arguments.length){case 0:break;case 1:"function"==typeof t?this.interpolator(t):this.range(t);break;default:this.domain(t),"function"==typeof n?this.interpolator(n):this.range(n)}return this}const pg=Symbol("implicit");function gg(){var t=new InternMap,n=[],e=[],r=pg;function i(i){let o=t.get(i);if(void 0===o){if(r!==pg)return r;t.set(i,o=n.push(i)-1)}return e[o%e.length]}return i.domain=function(e){if(!arguments.length)return n.slice();n=[],t=new InternMap;for(const r of e)t.has(r)||t.set(r,n.push(r)-1);return i},i.range=function(t){return arguments.length?(e=Array.from(t),i):e.slice()},i.unknown=function(t){return arguments.length?(r=t,i):r},i.copy=function(){return gg(n,e).unknown(r)},hg.apply(i,arguments),i}function yg(){var t,n,e=gg().unknown(void 0),r=e.domain,i=e.range,o=0,a=1,u=!1,c=0,f=0,s=.5;function l(){var e=r().length,l=an&&(e=t,t=n,n=e),function(e){return Math.max(t,Math.min(n,e))}}(a[0],a[t-1])),r=t>2?Mg:wg,i=o=null,l}function l(n){return null==n||isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),Yr)))(e)))},l.domain=function(t){return arguments.length?(a=Array.from(t,_g),s()):a.slice()},l.range=function(t){return arguments.length?(u=Array.from(t),s()):u.slice()},l.rangeRound=function(t){return u=Array.from(t),c=Vr,s()},l.clamp=function(t){return arguments.length?(f=!!t||mg,s()):f!==mg},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function Sg(){return Ag()(mg,mg)}function Eg(n,e,r,i){var o,a=W(n,e,r);switch((i=Jc(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=lf(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=hf(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=sf(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function Ng(t){var n=t.domain;return t.ticks=function(t){var e=n();return G(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return Eg(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i,o=n(),a=0,u=o.length-1,c=o[a],f=o[u],s=10;for(f0;){if((i=V(c,f,e))===r)return o[a]=c,o[u]=f,n(o);if(i>0)c=Math.floor(c/i)*i,f=Math.ceil(f/i)*i;else{if(!(i<0))break;c=Math.ceil(c*i)/i,f=Math.floor(f*i)/i}r=i}return t},t}function kg(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a-t(-n,e)}function Fg(n){const e=n(Cg,Pg),r=e.domain;let i,o,a=10;function u(){return i=function(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),n=>Math.log(n)/t)}(a),o=function(t){return 10===t?Dg:t===Math.E?Math.exp:n=>Math.pow(t,n)}(a),r()[0]<0?(i=Rg(i),o=Rg(o),n(zg,$g)):n(Cg,Pg),e}return e.base=function(t){return arguments.length?(a=+t,u()):a},e.domain=function(t){return arguments.length?(r(t),u()):r()},e.ticks=t=>{const n=r();let e=n[0],u=n[n.length-1];const c=u0){for(;l<=h;++l)for(f=1;fu)break;p.push(s)}}else for(;l<=h;++l)for(f=a-1;f>=1;--f)if(s=l>0?f/o(-l):f*o(l),!(su)break;p.push(s)}2*p.length{if(null==n&&(n=10),null==r&&(r=10===a?"s":","),"function"!=typeof r&&(a%1||null!=(r=Jc(r)).precision||(r.trim=!0),r=t.format(r)),n===1/0)return r;const u=Math.max(1,a*n/e.ticks().length);return t=>{let n=t/o(Math.round(i(t)));return n*ar(kg(r(),{floor:t=>o(Math.floor(i(t))),ceil:t=>o(Math.ceil(i(t)))})),e}function qg(t){return function(n){return Math.sign(n)*Math.log1p(Math.abs(n/t))}}function Ug(t){return function(n){return Math.sign(n)*Math.expm1(Math.abs(n))*t}}function Ig(t){var n=1,e=t(qg(n),Ug(n));return e.constant=function(e){return arguments.length?t(qg(n=+e),Ug(n)):n},Ng(e)}function Og(t){return function(n){return n<0?-Math.pow(-n,t):Math.pow(n,t)}}function Bg(t){return t<0?-Math.sqrt(-t):Math.sqrt(t)}function Yg(t){return t<0?-t*t:t*t}function Lg(t){var n=t(mg,mg),e=1;return n.exponent=function(n){return arguments.length?1===(e=+n)?t(mg,mg):.5===e?t(Bg,Yg):t(Og(e),Og(1/e)):e},Ng(n)}function jg(){var t=Lg(Ag());return t.copy=function(){return Tg(t,jg()).exponent(t.exponent())},hg.apply(t,arguments),t}function Hg(t){return Math.sign(t)*t*t}const Xg=new Date,Gg=new Date;function Vg(t,n,e,r){function i(n){return t(n=0===arguments.length?new Date:new Date(+n)),n}return i.floor=n=>(t(n=new Date(+n)),n),i.ceil=e=>(t(e=new Date(e-1)),n(e,1),t(e),e),i.round=t=>{const n=i(t),e=i.ceil(t);return t-n(n(t=new Date(+t),null==e?1:Math.floor(e)),t),i.range=(e,r,o)=>{const a=[];if(e=i.ceil(e),o=null==o?1:Math.floor(o),!(e0))return a;let u;do{a.push(u=new Date(+e)),n(e,o),t(e)}while(uVg((n=>{if(n>=n)for(;t(n),!e(n);)n.setTime(n-1)}),((t,r)=>{if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})),e&&(i.count=(n,r)=>(Xg.setTime(+n),Gg.setTime(+r),t(Xg),t(Gg),Math.floor(e(Xg,Gg))),i.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?n=>r(n)%t==0:n=>i.count(0,n)%t==0):i:null)),i}const Wg=Vg((()=>{}),((t,n)=>{t.setTime(+t+n)}),((t,n)=>n-t));Wg.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?Vg((n=>{n.setTime(Math.floor(n/t)*t)}),((n,e)=>{n.setTime(+n+e*t)}),((n,e)=>(e-n)/t)):Wg:null);const Zg=Wg.range,Kg=1e3,Qg=6e4,Jg=36e5,ty=864e5,ny=6048e5,ey=2592e6,ry=31536e6,iy=Vg((t=>{t.setTime(t-t.getMilliseconds())}),((t,n)=>{t.setTime(+t+n*Kg)}),((t,n)=>(n-t)/Kg),(t=>t.getUTCSeconds())),oy=iy.range,ay=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getMinutes())),uy=ay.range,cy=Vg((t=>{t.setUTCSeconds(0,0)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getUTCMinutes())),fy=cy.range,sy=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg-t.getMinutes()*Qg)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getHours())),ly=sy.range,hy=Vg((t=>{t.setUTCMinutes(0,0,0)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getUTCHours())),dy=hy.range,py=Vg((t=>t.setHours(0,0,0,0)),((t,n)=>t.setDate(t.getDate()+n)),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ty),(t=>t.getDate()-1)),gy=py.range,yy=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>t.getUTCDate()-1)),vy=yy.range,_y=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>Math.floor(t/ty))),by=_y.range;function my(t){return Vg((n=>{n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)}),((t,n)=>{t.setDate(t.getDate()+7*n)}),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ny))}const xy=my(0),wy=my(1),My=my(2),Ty=my(3),Ay=my(4),Sy=my(5),Ey=my(6),Ny=xy.range,ky=wy.range,Cy=My.range,Py=Ty.range,zy=Ay.range,$y=Sy.range,Dy=Ey.range;function Ry(t){return Vg((n=>{n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+7*n)}),((t,n)=>(n-t)/ny))}const Fy=Ry(0),qy=Ry(1),Uy=Ry(2),Iy=Ry(3),Oy=Ry(4),By=Ry(5),Yy=Ry(6),Ly=Fy.range,jy=qy.range,Hy=Uy.range,Xy=Iy.range,Gy=Oy.range,Vy=By.range,Wy=Yy.range,Zy=Vg((t=>{t.setDate(1),t.setHours(0,0,0,0)}),((t,n)=>{t.setMonth(t.getMonth()+n)}),((t,n)=>n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())),(t=>t.getMonth())),Ky=Zy.range,Qy=Vg((t=>{t.setUTCDate(1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCMonth(t.getUTCMonth()+n)}),((t,n)=>n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())),(t=>t.getUTCMonth())),Jy=Qy.range,tv=Vg((t=>{t.setMonth(0,1),t.setHours(0,0,0,0)}),((t,n)=>{t.setFullYear(t.getFullYear()+n)}),((t,n)=>n.getFullYear()-t.getFullYear()),(t=>t.getFullYear()));tv.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)}),((n,e)=>{n.setFullYear(n.getFullYear()+e*t)})):null;const nv=tv.range,ev=Vg((t=>{t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCFullYear(t.getUTCFullYear()+n)}),((t,n)=>n.getUTCFullYear()-t.getUTCFullYear()),(t=>t.getUTCFullYear()));ev.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)}),((n,e)=>{n.setUTCFullYear(n.getUTCFullYear()+e*t)})):null;const rv=ev.range;function iv(t,n,e,i,o,a){const u=[[iy,1,Kg],[iy,5,5e3],[iy,15,15e3],[iy,30,3e4],[a,1,Qg],[a,5,3e5],[a,15,9e5],[a,30,18e5],[o,1,Jg],[o,3,108e5],[o,6,216e5],[o,12,432e5],[i,1,ty],[i,2,1728e5],[e,1,ny],[n,1,ey],[n,3,7776e6],[t,1,ry]];function c(n,e,i){const o=Math.abs(e-n)/i,a=r((([,,t])=>t)).right(u,o);if(a===u.length)return t.every(W(n/ry,e/ry,i));if(0===a)return Wg.every(Math.max(W(n,e,i),1));const[c,f]=u[o/u[a-1][2]=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:k_,s:C_,S:Zv,u:Kv,U:Qv,V:t_,w:n_,W:e_,x:null,X:null,y:r_,Y:o_,Z:u_,"%":N_},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:c_,e:c_,f:d_,g:T_,G:S_,H:f_,I:s_,j:l_,L:h_,m:p_,M:g_,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:k_,s:C_,S:y_,u:v_,U:__,V:m_,w:x_,W:w_,x:null,X:null,y:M_,Y:A_,Z:E_,"%":N_},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p.get(r[0].toLowerCase()),e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h.get(r[0].toLowerCase()),e+r[0].length):-1},b:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=_.get(r[0].toLowerCase()),e+r[0].length):-1},B:function(t,n,e){var r=g.exec(n.slice(e));return r?(t.m=y.get(r[0].toLowerCase()),e+r[0].length):-1},c:function(t,e,r){return T(t,n,e,r)},d:zv,e:zv,f:Uv,g:Nv,G:Ev,H:Dv,I:Dv,j:$v,L:qv,m:Pv,M:Rv,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s.get(r[0].toLowerCase()),e+r[0].length):-1},q:Cv,Q:Ov,s:Bv,S:Fv,u:Mv,U:Tv,V:Av,w:wv,W:Sv,x:function(t,n,r){return T(t,e,n,r)},X:function(t,n,e){return T(t,r,n,e)},y:Nv,Y:Ev,Z:kv,"%":Iv};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=sv(lv(o.y,0,1))).getUTCDay(),r=i>4||0===i?qy.ceil(r):qy(r),r=yy.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=fv(lv(o.y,0,1))).getDay(),r=i>4||0===i?wy.ceil(r):wy(r),r=py.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?sv(lv(o.y,0,1)).getUTCDay():fv(lv(o.y,0,1)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,sv(o)):fv(o)}}function T(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in pv?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",!1);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t+="",!0);return n.toString=function(){return t},n}}}var dv,pv={"-":"",_:" ",0:"0"},gv=/^\s*\d+/,yv=/^%/,vv=/[\\^$*+?|[\]().{}]/g;function _v(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o[t.toLowerCase(),n])))}function wv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.w=+r[0],e+r[0].length):-1}function Mv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.u=+r[0],e+r[0].length):-1}function Tv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.U=+r[0],e+r[0].length):-1}function Av(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.V=+r[0],e+r[0].length):-1}function Sv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.W=+r[0],e+r[0].length):-1}function Ev(t,n,e){var r=gv.exec(n.slice(e,e+4));return r?(t.y=+r[0],e+r[0].length):-1}function Nv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),e+r[0].length):-1}function kv(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function Cv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.q=3*r[0]-3,e+r[0].length):-1}function Pv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function zv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function $v(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function Dv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function Rv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function Fv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function qv(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function Uv(t,n,e){var r=gv.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function Iv(t,n,e){var r=yv.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Ov(t,n,e){var r=gv.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function Bv(t,n,e){var r=gv.exec(n.slice(e));return r?(t.s=+r[0],e+r[0].length):-1}function Yv(t,n){return _v(t.getDate(),n,2)}function Lv(t,n){return _v(t.getHours(),n,2)}function jv(t,n){return _v(t.getHours()%12||12,n,2)}function Hv(t,n){return _v(1+py.count(tv(t),t),n,3)}function Xv(t,n){return _v(t.getMilliseconds(),n,3)}function Gv(t,n){return Xv(t,n)+"000"}function Vv(t,n){return _v(t.getMonth()+1,n,2)}function Wv(t,n){return _v(t.getMinutes(),n,2)}function Zv(t,n){return _v(t.getSeconds(),n,2)}function Kv(t){var n=t.getDay();return 0===n?7:n}function Qv(t,n){return _v(xy.count(tv(t)-1,t),n,2)}function Jv(t){var n=t.getDay();return n>=4||0===n?Ay(t):Ay.ceil(t)}function t_(t,n){return t=Jv(t),_v(Ay.count(tv(t),t)+(4===tv(t).getDay()),n,2)}function n_(t){return t.getDay()}function e_(t,n){return _v(wy.count(tv(t)-1,t),n,2)}function r_(t,n){return _v(t.getFullYear()%100,n,2)}function i_(t,n){return _v((t=Jv(t)).getFullYear()%100,n,2)}function o_(t,n){return _v(t.getFullYear()%1e4,n,4)}function a_(t,n){var e=t.getDay();return _v((t=e>=4||0===e?Ay(t):Ay.ceil(t)).getFullYear()%1e4,n,4)}function u_(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+_v(n/60|0,"0",2)+_v(n%60,"0",2)}function c_(t,n){return _v(t.getUTCDate(),n,2)}function f_(t,n){return _v(t.getUTCHours(),n,2)}function s_(t,n){return _v(t.getUTCHours()%12||12,n,2)}function l_(t,n){return _v(1+yy.count(ev(t),t),n,3)}function h_(t,n){return _v(t.getUTCMilliseconds(),n,3)}function d_(t,n){return h_(t,n)+"000"}function p_(t,n){return _v(t.getUTCMonth()+1,n,2)}function g_(t,n){return _v(t.getUTCMinutes(),n,2)}function y_(t,n){return _v(t.getUTCSeconds(),n,2)}function v_(t){var n=t.getUTCDay();return 0===n?7:n}function __(t,n){return _v(Fy.count(ev(t)-1,t),n,2)}function b_(t){var n=t.getUTCDay();return n>=4||0===n?Oy(t):Oy.ceil(t)}function m_(t,n){return t=b_(t),_v(Oy.count(ev(t),t)+(4===ev(t).getUTCDay()),n,2)}function x_(t){return t.getUTCDay()}function w_(t,n){return _v(qy.count(ev(t)-1,t),n,2)}function M_(t,n){return _v(t.getUTCFullYear()%100,n,2)}function T_(t,n){return _v((t=b_(t)).getUTCFullYear()%100,n,2)}function A_(t,n){return _v(t.getUTCFullYear()%1e4,n,4)}function S_(t,n){var e=t.getUTCDay();return _v((t=e>=4||0===e?Oy(t):Oy.ceil(t)).getUTCFullYear()%1e4,n,4)}function E_(){return"+0000"}function N_(){return"%"}function k_(t){return+t}function C_(t){return Math.floor(+t/1e3)}function P_(n){return dv=hv(n),t.timeFormat=dv.format,t.timeParse=dv.parse,t.utcFormat=dv.utcFormat,t.utcParse=dv.utcParse,dv}t.timeFormat=void 0,t.timeParse=void 0,t.utcFormat=void 0,t.utcParse=void 0,P_({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var z_="%Y-%m-%dT%H:%M:%S.%LZ";var $_=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat(z_),D_=$_;var R_=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse(z_),F_=R_;function q_(t){return new Date(t)}function U_(t){return t instanceof Date?+t:+new Date(+t)}function I_(t,n,e,r,i,o,a,u,c,f){var s=Sg(),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),g=f("%I:%M"),y=f("%I %p"),v=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y");function x(t){return(c(t)Fr(t[t.length-1]),ib=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(H_),ob=rb(ib),ab=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(H_),ub=rb(ab),cb=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(H_),fb=rb(cb),sb=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(H_),lb=rb(sb),hb=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(H_),db=rb(hb),pb=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(H_),gb=rb(pb),yb=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(H_),vb=rb(yb),_b=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(H_),bb=rb(_b),mb=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(H_),xb=rb(mb),wb=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(H_),Mb=rb(wb),Tb=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(H_),Ab=rb(Tb),Sb=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(H_),Eb=rb(Sb),Nb=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(H_),kb=rb(Nb),Cb=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(H_),Pb=rb(Cb),zb=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(H_),$b=rb(zb),Db=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(H_),Rb=rb(Db),Fb=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(H_),qb=rb(Fb),Ub=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(H_),Ib=rb(Ub),Ob=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(H_),Bb=rb(Ob),Yb=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(H_),Lb=rb(Yb),jb=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(H_),Hb=rb(jb),Xb=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(H_),Gb=rb(Xb),Vb=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(H_),Wb=rb(Vb),Zb=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(H_),Kb=rb(Zb),Qb=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(H_),Jb=rb(Qb),tm=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(H_),nm=rb(tm),em=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(H_),rm=rb(em);var im=hi(Tr(300,.5,0),Tr(-240,.5,1)),om=hi(Tr(-100,.75,.35),Tr(80,1.5,.8)),am=hi(Tr(260,.75,.35),Tr(80,1.5,.8)),um=Tr();var cm=Fe(),fm=Math.PI/3,sm=2*Math.PI/3;function lm(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}}var hm=lm(H_("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),dm=lm(H_("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),pm=lm(H_("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),gm=lm(H_("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));function ym(t){return function(){return t}}const vm=Math.abs,_m=Math.atan2,bm=Math.cos,mm=Math.max,xm=Math.min,wm=Math.sin,Mm=Math.sqrt,Tm=1e-12,Am=Math.PI,Sm=Am/2,Em=2*Am;function Nm(t){return t>=1?Sm:t<=-1?-Sm:Math.asin(t)}function km(t){let n=3;return t.digits=function(e){if(!arguments.length)return n;if(null==e)n=null;else{const t=Math.floor(e);if(!(t>=0))throw new RangeError(`invalid digits: ${e}`);n=t}return t},()=>new Ua(n)}function Cm(t){return t.innerRadius}function Pm(t){return t.outerRadius}function zm(t){return t.startAngle}function $m(t){return t.endAngle}function Dm(t){return t&&t.padAngle}function Rm(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/Mm(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,g=r+l,y=(h+p)/2,v=(d+g)/2,_=p-h,b=g-d,m=_*_+b*b,x=i-o,w=h*g-p*d,M=(b<0?-1:1)*Mm(mm(0,x*x*m-w*w)),T=(w*b-_*M)/m,A=(-w*_-b*M)/m,S=(w*b+_*M)/m,E=(-w*_+b*M)/m,N=T-y,k=A-v,C=S-y,P=E-v;return N*N+k*k>C*C+P*P&&(T=S,A=E),{cx:T,cy:A,x01:-s,y01:-l,x11:T*(i/x-1),y11:A*(i/x-1)}}var Fm=Array.prototype.slice;function qm(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}function Um(t){this._context=t}function Im(t){return new Um(t)}function Om(t){return t[0]}function Bm(t){return t[1]}function Ym(t,n){var e=ym(!0),r=null,i=Im,o=null,a=km(u);function u(u){var c,f,s,l=(u=qm(u)).length,h=!1;for(null==r&&(o=i(s=a())),c=0;c<=l;++c)!(c=l;--h)u.point(v[h],_[h]);u.lineEnd(),u.areaEnd()}y&&(v[s]=+t(d,s,f),_[s]=+n(d,s,f),u.point(r?+r(d,s,f):v[s],e?+e(d,s,f):_[s]))}if(p)return u=null,p+""||null}function s(){return Ym().defined(i).curve(a).context(o)}return t="function"==typeof t?t:void 0===t?Om:ym(+t),n="function"==typeof n?n:ym(void 0===n?0:+n),e="function"==typeof e?e:void 0===e?Bm:ym(+e),f.x=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),r=null,f):t},f.x0=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),f):t},f.x1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:ym(+t),f):r},f.y=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),e=null,f):n},f.y0=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),f):n},f.y1=function(t){return arguments.length?(e=null==t?null:"function"==typeof t?t:ym(+t),f):e},f.lineX0=f.lineY0=function(){return s().x(t).y(n)},f.lineY1=function(){return s().x(t).y(e)},f.lineX1=function(){return s().x(r).y(n)},f.defined=function(t){return arguments.length?(i="function"==typeof t?t:ym(!!t),f):i},f.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),f):a},f.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),f):o},f}function jm(t,n){return nt?1:n>=t?0:NaN}function Hm(t){return t}Um.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Xm=Vm(Im);function Gm(t){this._curve=t}function Vm(t){function n(n){return new Gm(t(n))}return n._curve=t,n}function Wm(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Vm(t)):n()._curve},t}function Zm(){return Wm(Ym().curve(Xm))}function Km(){var t=Lm().curve(Xm),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Wm(e())},delete t.lineX0,t.lineEndAngle=function(){return Wm(r())},delete t.lineX1,t.lineInnerRadius=function(){return Wm(i())},delete t.lineY0,t.lineOuterRadius=function(){return Wm(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Vm(t)):n()._curve},t}function Qm(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}Gm.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};class Jm{constructor(t,n){this._context=t,this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line}point(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n)}this._x0=t,this._y0=n}}class tx{constructor(t){this._context=t}lineStart(){this._point=0}lineEnd(){}point(t,n){if(t=+t,n=+n,0===this._point)this._point=1;else{const e=Qm(this._x0,this._y0),r=Qm(this._x0,this._y0=(this._y0+n)/2),i=Qm(t,this._y0),o=Qm(t,n);this._context.moveTo(...e),this._context.bezierCurveTo(...r,...i,...o)}this._x0=t,this._y0=n}}function nx(t){return new Jm(t,!0)}function ex(t){return new Jm(t,!1)}function rx(t){return new tx(t)}function ix(t){return t.source}function ox(t){return t.target}function ax(t){let n=ix,e=ox,r=Om,i=Bm,o=null,a=null,u=km(c);function c(){let c;const f=Fm.call(arguments),s=n.apply(this,f),l=e.apply(this,f);if(null==o&&(a=t(c=u())),a.lineStart(),f[0]=s,a.point(+r.apply(this,f),+i.apply(this,f)),f[0]=l,a.point(+r.apply(this,f),+i.apply(this,f)),a.lineEnd(),c)return a=null,c+""||null}return c.source=function(t){return arguments.length?(n=t,c):n},c.target=function(t){return arguments.length?(e=t,c):e},c.x=function(t){return arguments.length?(r="function"==typeof t?t:ym(+t),c):r},c.y=function(t){return arguments.length?(i="function"==typeof t?t:ym(+t),c):i},c.context=function(n){return arguments.length?(null==n?o=a=null:a=t(o=n),c):o},c}const ux=Mm(3);var cx={draw(t,n){const e=.59436*Mm(n+xm(n/28,.75)),r=e/2,i=r*ux;t.moveTo(0,e),t.lineTo(0,-e),t.moveTo(-i,-r),t.lineTo(i,r),t.moveTo(-i,r),t.lineTo(i,-r)}},fx={draw(t,n){const e=Mm(n/Am);t.moveTo(e,0),t.arc(0,0,e,0,Em)}},sx={draw(t,n){const e=Mm(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}};const lx=Mm(1/3),hx=2*lx;var dx={draw(t,n){const e=Mm(n/hx),r=e*lx;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},px={draw(t,n){const e=.62625*Mm(n);t.moveTo(0,-e),t.lineTo(e,0),t.lineTo(0,e),t.lineTo(-e,0),t.closePath()}},gx={draw(t,n){const e=.87559*Mm(n-xm(n/7,2));t.moveTo(-e,0),t.lineTo(e,0),t.moveTo(0,e),t.lineTo(0,-e)}},yx={draw(t,n){const e=Mm(n),r=-e/2;t.rect(r,r,e,e)}},vx={draw(t,n){const e=.4431*Mm(n);t.moveTo(e,e),t.lineTo(e,-e),t.lineTo(-e,-e),t.lineTo(-e,e),t.closePath()}};const _x=wm(Am/10)/wm(7*Am/10),bx=wm(Em/10)*_x,mx=-bm(Em/10)*_x;var xx={draw(t,n){const e=Mm(.8908130915292852*n),r=bx*e,i=mx*e;t.moveTo(0,-e),t.lineTo(r,i);for(let n=1;n<5;++n){const o=Em*n/5,a=bm(o),u=wm(o);t.lineTo(u*e,-a*e),t.lineTo(a*r-u*i,u*r+a*i)}t.closePath()}};const wx=Mm(3);var Mx={draw(t,n){const e=-Mm(n/(3*wx));t.moveTo(0,2*e),t.lineTo(-wx*e,-e),t.lineTo(wx*e,-e),t.closePath()}};const Tx=Mm(3);var Ax={draw(t,n){const e=.6824*Mm(n),r=e/2,i=e*Tx/2;t.moveTo(0,-e),t.lineTo(i,r),t.lineTo(-i,r),t.closePath()}};const Sx=-.5,Ex=Mm(3)/2,Nx=1/Mm(12),kx=3*(Nx/2+1);var Cx={draw(t,n){const e=Mm(n/kx),r=e/2,i=e*Nx,o=r,a=e*Nx+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(Sx*r-Ex*i,Ex*r+Sx*i),t.lineTo(Sx*o-Ex*a,Ex*o+Sx*a),t.lineTo(Sx*u-Ex*c,Ex*u+Sx*c),t.lineTo(Sx*r+Ex*i,Sx*i-Ex*r),t.lineTo(Sx*o+Ex*a,Sx*a-Ex*o),t.lineTo(Sx*u+Ex*c,Sx*c-Ex*u),t.closePath()}},Px={draw(t,n){const e=.6189*Mm(n-xm(n/6,1.7));t.moveTo(-e,-e),t.lineTo(e,e),t.moveTo(-e,e),t.lineTo(e,-e)}};const zx=[fx,sx,dx,yx,xx,Mx,Cx],$x=[fx,gx,Px,Ax,cx,vx,px];function Dx(){}function Rx(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function Fx(t){this._context=t}function qx(t){this._context=t}function Ux(t){this._context=t}function Ix(t,n){this._basis=new Fx(t),this._beta=n}Fx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Rx(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},qx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ux.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ix.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var Ox=function t(n){function e(t){return 1===n?new Fx(t):new Ix(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function Bx(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Yx(t,n){this._context=t,this._k=(1-n)/6}Yx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Bx(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Lx=function t(n){function e(t){return new Yx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function jx(t,n){this._context=t,this._k=(1-n)/6}jx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Hx=function t(n){function e(t){return new jx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Xx(t,n){this._context=t,this._k=(1-n)/6}Xx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Gx=function t(n){function e(t){return new Xx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Vx(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>Tm){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>Tm){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function Wx(t,n){this._context=t,this._alpha=n}Wx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Zx=function t(n){function e(t){return n?new Wx(t,n):new Yx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Kx(t,n){this._context=t,this._alpha=n}Kx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Qx=function t(n){function e(t){return n?new Kx(t,n):new jx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Jx(t,n){this._context=t,this._alpha=n}Jx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var tw=function t(n){function e(t){return n?new Jx(t,n):new Xx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function nw(t){this._context=t}function ew(t){return t<0?-1:1}function rw(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(ew(o)+ew(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function iw(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function ow(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function aw(t){this._context=t}function uw(t){this._context=new cw(t)}function cw(t){this._context=t}function fw(t){this._context=t}function sw(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function pw(t,n){return t[n]}function gw(t){const n=[];return n.key=t,n}function yw(t){var n=t.map(vw);return dw(t).sort((function(t,e){return n[t]-n[e]}))}function vw(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function _w(t){var n=t.map(bw);return dw(t).sort((function(t,e){return n[t]-n[e]}))}function bw(t){for(var n,e=0,r=-1,i=t.length;++r=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var mw=t=>()=>t;function xw(t,{sourceEvent:n,target:e,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function ww(t,n,e){this.k=t,this.x=n,this.y=e}ww.prototype={constructor:ww,scale:function(t){return 1===t?this:new ww(this.k*t,this.x,this.y)},translate:function(t,n){return 0===t&0===n?this:new ww(this.k,this.x+this.k*t,this.y+this.k*n)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Mw=new ww(1,0,0);function Tw(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Mw;return t.__zoom}function Aw(t){t.stopImmediatePropagation()}function Sw(t){t.preventDefault(),t.stopImmediatePropagation()}function Ew(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Nw(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function kw(){return this.__zoom||Mw}function Cw(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function Pw(){return navigator.maxTouchPoints||"ontouchstart"in this}function zw(t,n,e){var r=t.invertX(n[0][0])-e[0][0],i=t.invertX(n[1][0])-e[1][0],o=t.invertY(n[0][1])-e[0][1],a=t.invertY(n[1][1])-e[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Tw.prototype=ww.prototype,t.Adder=T,t.Delaunay=Lu,t.FormatSpecifier=tf,t.InternMap=InternMap,t.InternSet=InternSet,t.Node=Qd,t.Path=Ua,t.Voronoi=qu,t.ZoomTransform=ww,t.active=function(t,n){var e,r,i=t.__transition;if(i)for(r in n=null==n?null:n+"",i)if((e=i[r]).state>qi&&e.name===n)return new po([[t]],Zo,n,+r);return null},t.arc=function(){var t=Cm,n=Pm,e=ym(0),r=null,i=zm,o=$m,a=Dm,u=null,c=km(f);function f(){var f,s,l=+t.apply(this,arguments),h=+n.apply(this,arguments),d=i.apply(this,arguments)-Sm,p=o.apply(this,arguments)-Sm,g=vm(p-d),y=p>d;if(u||(u=f=c()),hTm)if(g>Em-Tm)u.moveTo(h*bm(d),h*wm(d)),u.arc(0,0,h,d,p,!y),l>Tm&&(u.moveTo(l*bm(p),l*wm(p)),u.arc(0,0,l,p,d,y));else{var v,_,b=d,m=p,x=d,w=p,M=g,T=g,A=a.apply(this,arguments)/2,S=A>Tm&&(r?+r.apply(this,arguments):Mm(l*l+h*h)),E=xm(vm(h-l)/2,+e.apply(this,arguments)),N=E,k=E;if(S>Tm){var C=Nm(S/l*wm(A)),P=Nm(S/h*wm(A));(M-=2*C)>Tm?(x+=C*=y?1:-1,w-=C):(M=0,x=w=(d+p)/2),(T-=2*P)>Tm?(b+=P*=y?1:-1,m-=P):(T=0,b=m=(d+p)/2)}var z=h*bm(b),$=h*wm(b),D=l*bm(w),R=l*wm(w);if(E>Tm){var F,q=h*bm(m),U=h*wm(m),I=l*bm(x),O=l*wm(x);if(g1?0:t<-1?Am:Math.acos(t)}((B*L+Y*j)/(Mm(B*B+Y*Y)*Mm(L*L+j*j)))/2),X=Mm(F[0]*F[0]+F[1]*F[1]);N=xm(E,(l-X)/(H-1)),k=xm(E,(h-X)/(H+1))}else N=k=0}T>Tm?k>Tm?(v=Rm(I,O,z,$,h,k,y),_=Rm(q,U,D,R,h,k,y),u.moveTo(v.cx+v.x01,v.cy+v.y01),kTm&&M>Tm?N>Tm?(v=Rm(D,R,q,U,l,-N,y),_=Rm(z,$,I,O,l,-N,y),u.lineTo(v.cx+v.x01,v.cy+v.y01),N=0))throw new RangeError("invalid r");let e=t.length;if(!((e=Math.floor(e))>=0))throw new RangeError("invalid length");if(!e||!n)return t;const r=y(n),i=t.slice();return r(t,i,0,e,1),r(i,t,0,e,1),r(t,i,0,e,1),t},t.blur2=l,t.blurImage=h,t.brush=function(){return wa(la)},t.brushSelection=function(t){var n=t.__brush;return n?n.dim.output(n.selection):null},t.brushX=function(){return wa(fa)},t.brushY=function(){return wa(sa)},t.buffer=function(t,n){return fetch(t,n).then(_c)},t.chord=function(){return za(!1,!1)},t.chordDirected=function(){return za(!0,!1)},t.chordTranspose=function(){return za(!1,!0)},t.cluster=function(){var t=Ld,n=1,e=1,r=!1;function i(i){var o,a=0;i.eachAfter((function(n){var e=n.children;e?(n.x=function(t){return t.reduce(jd,0)/t.length}(e),n.y=function(t){return 1+t.reduce(Hd,0)}(e)):(n.x=o?a+=t(n,o):0,n.y=0,o=n)}));var u=function(t){for(var n;n=t.children;)t=n[0];return t}(i),c=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(i),f=u.x-t(u,c)/2,s=c.x+t(c,u)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*n,t.y=(i.y-t.y)*e}:function(t){t.x=(t.x-f)/(s-f)*n,t.y=(1-(i.y?t.y/i.y:1))*e})}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.color=ze,t.contourDensity=function(){var t=fu,n=su,e=lu,r=960,i=500,o=20,a=2,u=3*o,c=r+2*u>>a,f=i+2*u>>a,s=Qa(20);function h(r){var i=new Float32Array(c*f),s=Math.pow(2,-a),h=-1;for(const o of r){var d=(t(o,++h,r)+u)*s,p=(n(o,h,r)+u)*s,g=+e(o,h,r);if(g&&d>=0&&d=0&&pt*r)))(n).map(((t,n)=>(t.value=+e[n],p(t))))}function p(t){return t.coordinates.forEach(g),t}function g(t){t.forEach(y)}function y(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function _(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,d}return d.contours=function(t){var n=h(t),e=iu().size([c,f]),r=Math.pow(2,2*a),i=t=>{t=+t;var i=p(e.contour(n,t*r));return i.value=t,i};return Object.defineProperty(i,"max",{get:()=>J(n)/r}),i},d.x=function(n){return arguments.length?(t="function"==typeof n?n:Qa(+n),d):t},d.y=function(t){return arguments.length?(n="function"==typeof t?t:Qa(+t),d):n},d.weight=function(t){return arguments.length?(e="function"==typeof t?t:Qa(+t),d):e},d.size=function(t){if(!arguments.length)return[r,i];var n=+t[0],e=+t[1];if(!(n>=0&&e>=0))throw new Error("invalid size");return r=n,i=e,_()},d.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),_()},d.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),d):s},d.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=(Math.sqrt(4*t*t+1)-1)/2,_()},d},t.contours=iu,t.count=v,t.create=function(t){return Zn(Yt(t).call(document.documentElement))},t.creator=Yt,t.cross=function(...t){const n="function"==typeof t[t.length-1]&&function(t){return n=>t(...n)}(t.pop()),e=(t=t.map(m)).map(_),r=t.length-1,i=new Array(r+1).fill(0),o=[];if(r<0||e.some(b))return o;for(;;){o.push(i.map(((n,e)=>t[e][n])));let a=r;for(;++i[a]===e[a];){if(0===a)return n?o.map(n):o;i[a--]=0}}},t.csv=wc,t.csvFormat=rc,t.csvFormatBody=ic,t.csvFormatRow=ac,t.csvFormatRows=oc,t.csvFormatValue=uc,t.csvParse=nc,t.csvParseRows=ec,t.cubehelix=Tr,t.cumsum=function(t,n){var e=0,r=0;return Float64Array.from(t,void 0===n?t=>e+=+t||0:i=>e+=+n(i,r++,t)||0)},t.curveBasis=function(t){return new Fx(t)},t.curveBasisClosed=function(t){return new qx(t)},t.curveBasisOpen=function(t){return new Ux(t)},t.curveBumpX=nx,t.curveBumpY=ex,t.curveBundle=Ox,t.curveCardinal=Lx,t.curveCardinalClosed=Hx,t.curveCardinalOpen=Gx,t.curveCatmullRom=Zx,t.curveCatmullRomClosed=Qx,t.curveCatmullRomOpen=tw,t.curveLinear=Im,t.curveLinearClosed=function(t){return new nw(t)},t.curveMonotoneX=function(t){return new aw(t)},t.curveMonotoneY=function(t){return new uw(t)},t.curveNatural=function(t){return new fw(t)},t.curveStep=function(t){return new lw(t,.5)},t.curveStepAfter=function(t){return new lw(t,1)},t.curveStepBefore=function(t){return new lw(t,0)},t.descending=e,t.deviation=w,t.difference=function(t,...n){t=new InternSet(t);for(const e of n)for(const n of e)t.delete(n);return t},t.disjoint=function(t,n){const e=n[Symbol.iterator](),r=new InternSet;for(const n of t){if(r.has(n))return!1;let t,i;for(;({value:t,done:i}=e.next())&&!i;){if(Object.is(n,t))return!1;r.add(t)}}return!0},t.dispatch=$t,t.drag=function(){var t,n,e,r,i=se,o=le,a=he,u=de,c={},f=$t("start","drag","end"),s=0,l=0;function h(t){t.on("mousedown.drag",d).filter(u).on("touchstart.drag",y).on("touchmove.drag",v,ee).on("touchend.drag touchcancel.drag",_).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function d(a,u){if(!r&&i.call(this,a,u)){var c=b(this,o.call(this,a,u),a,u,"mouse");c&&(Zn(a.view).on("mousemove.drag",p,re).on("mouseup.drag",g,re),ae(a.view),ie(a),e=!1,t=a.clientX,n=a.clientY,c("start",a))}}function p(r){if(oe(r),!e){var i=r.clientX-t,o=r.clientY-n;e=i*i+o*o>l}c.mouse("drag",r)}function g(t){Zn(t.view).on("mousemove.drag mouseup.drag",null),ue(t.view,e),oe(t),c.mouse("end",t)}function y(t,n){if(i.call(this,t,n)){var e,r,a=t.changedTouches,u=o.call(this,t,n),c=a.length;for(e=0;e+t,t.easePoly=wo,t.easePolyIn=mo,t.easePolyInOut=wo,t.easePolyOut=xo,t.easeQuad=_o,t.easeQuadIn=function(t){return t*t},t.easeQuadInOut=_o,t.easeQuadOut=function(t){return t*(2-t)},t.easeSin=Ao,t.easeSinIn=function(t){return 1==+t?1:1-Math.cos(t*To)},t.easeSinInOut=Ao,t.easeSinOut=function(t){return Math.sin(t*To)},t.every=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(!n(r,++e,t))return!1;return!0},t.extent=M,t.fcumsum=function(t,n){const e=new T;let r=-1;return Float64Array.from(t,void 0===n?t=>e.add(+t||0):i=>e.add(+n(i,++r,t)||0))},t.filter=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");const e=[];let r=-1;for(const i of t)n(i,++r,t)&&e.push(i);return e},t.flatGroup=function(t,...n){return z(P(t,...n),n)},t.flatRollup=function(t,n,...e){return z(D(t,n,...e),e)},t.forceCenter=function(t,n){var e,r=1;function i(){var i,o,a=e.length,u=0,c=0;for(i=0;if+p||os+p||ac.index){var g=f-u.x-u.vx,y=s-u.y-u.vy,v=g*g+y*y;vt.r&&(t.r=t[n].r)}function c(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r[u(t,n,r),t])));for(a=0,i=new Array(f);a=u)){(t.data!==n||t.next)&&(0===l&&(p+=(l=Uc(e))*l),0===h&&(p+=(h=Uc(e))*h),p(t=(Lc*t+jc)%Hc)/Hc}();function l(){h(),f.call("tick",n),e1?(null==e?u.delete(t):u.set(t,p(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=qc(.1);function o(t){for(var i,o=0,a=n.length;o=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++ejs(r[0],r[1])&&(r[1]=i[1]),js(i[0],r[1])>js(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=js(r[1],i[0]))>a&&(a=u,Wf=i[0],Kf=r[1])}return is=os=null,Wf===1/0||Zf===1/0?[[NaN,NaN],[NaN,NaN]]:[[Wf,Zf],[Kf,Qf]]},t.geoCentroid=function(t){ms=xs=ws=Ms=Ts=As=Ss=Es=0,Ns=new T,ks=new T,Cs=new T,Lf(t,Gs);var n=+Ns,e=+ks,r=+Cs,i=Ef(n,e,r);return i=0))throw new RangeError(`invalid digits: ${t}`);i=n}return null===n&&(r=new ed(i)),a},a.projection(t).digits(i).context(n)},t.geoProjection=yd,t.geoProjectionMutator=vd,t.geoRotation=ll,t.geoStereographic=function(){return yd(Bd).scale(250).clipAngle(142)},t.geoStereographicRaw=Bd,t.geoStream=Lf,t.geoTransform=function(t){return{stream:id(t)}},t.geoTransverseMercator=function(){var t=Ed(Yd),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):[(t=n())[1],-t[0]]},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=Yd,t.gray=function(t,n){return new ur(t,0,0,null==n?1:n)},t.greatest=ot,t.greatestIndex=function(t,e=n){if(1===e.length)return tt(t,e);let r,i=-1,o=-1;for(const n of t)++o,(i<0?0===e(n,n):e(n,r)>0)&&(r=n,i=o);return i},t.group=C,t.groupSort=function(t,e,r){return(2!==e.length?U($(t,e,r),(([t,e],[r,i])=>n(e,i)||n(t,r))):U(C(t,r),(([t,r],[i,o])=>e(r,o)||n(t,i)))).map((([t])=>t))},t.groups=P,t.hcl=dr,t.hierarchy=Gd,t.histogram=Q,t.hsl=He,t.html=Ec,t.image=function(t,n){return new Promise((function(e,r){var i=new Image;for(var o in n)i[o]=n[o];i.onerror=r,i.onload=function(){e(i)},i.src=t}))},t.index=function(t,...n){return F(t,k,R,n)},t.indexes=function(t,...n){return F(t,Array.from,R,n)},t.interpolate=Gr,t.interpolateArray=function(t,n){return(Ir(n)?Ur:Or)(t,n)},t.interpolateBasis=Er,t.interpolateBasisClosed=Nr,t.interpolateBlues=Gb,t.interpolateBrBG=ob,t.interpolateBuGn=Mb,t.interpolateBuPu=Ab,t.interpolateCividis=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(-4.54-t*(35.34-t*(2381.73-t*(6402.7-t*(7024.72-2710.57*t)))))))+", "+Math.max(0,Math.min(255,Math.round(32.49+t*(170.73+t*(52.82-t*(131.46-t*(176.58-67.37*t)))))))+", "+Math.max(0,Math.min(255,Math.round(81.24+t*(442.36-t*(2482.43-t*(6167.24-t*(6614.94-2475.67*t)))))))+")"},t.interpolateCool=am,t.interpolateCubehelix=li,t.interpolateCubehelixDefault=im,t.interpolateCubehelixLong=hi,t.interpolateDate=Br,t.interpolateDiscrete=function(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}},t.interpolateGnBu=Eb,t.interpolateGreens=Wb,t.interpolateGreys=Kb,t.interpolateHcl=ci,t.interpolateHclLong=fi,t.interpolateHsl=oi,t.interpolateHslLong=ai,t.interpolateHue=function(t,n){var e=Pr(+t,+n);return function(t){var n=e(t);return n-360*Math.floor(n/360)}},t.interpolateInferno=pm,t.interpolateLab=function(t,n){var e=$r((t=ar(t)).l,(n=ar(n)).l),r=$r(t.a,n.a),i=$r(t.b,n.b),o=$r(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}},t.interpolateMagma=dm,t.interpolateNumber=Yr,t.interpolateNumberArray=Ur,t.interpolateObject=Lr,t.interpolateOrRd=kb,t.interpolateOranges=rm,t.interpolatePRGn=ub,t.interpolatePiYG=fb,t.interpolatePlasma=gm,t.interpolatePuBu=$b,t.interpolatePuBuGn=Pb,t.interpolatePuOr=lb,t.interpolatePuRd=Rb,t.interpolatePurples=Jb,t.interpolateRainbow=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return um.h=360*t-100,um.s=1.5-1.5*n,um.l=.8-.9*n,um+""},t.interpolateRdBu=db,t.interpolateRdGy=gb,t.interpolateRdPu=qb,t.interpolateRdYlBu=vb,t.interpolateRdYlGn=bb,t.interpolateReds=nm,t.interpolateRgb=Dr,t.interpolateRgbBasis=Fr,t.interpolateRgbBasisClosed=qr,t.interpolateRound=Vr,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,cm.r=255*(n=Math.sin(t))*n,cm.g=255*(n=Math.sin(t+fm))*n,cm.b=255*(n=Math.sin(t+sm))*n,cm+""},t.interpolateSpectral=xb,t.interpolateString=Xr,t.interpolateTransformCss=ti,t.interpolateTransformSvg=ni,t.interpolateTurbo=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-14825.05*t)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+707.56*t)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-6838.66*t)))))))+")"},t.interpolateViridis=hm,t.interpolateWarm=om,t.interpolateYlGn=Bb,t.interpolateYlGnBu=Ib,t.interpolateYlOrBr=Lb,t.interpolateYlOrRd=Hb,t.interpolateZoom=ri,t.interrupt=Gi,t.intersection=function(t,...n){t=new InternSet(t),n=n.map(vt);t:for(const e of t)for(const r of n)if(!r.has(e)){t.delete(e);continue t}return t},t.interval=function(t,n,e){var r=new Ei,i=n;return null==n?(r.restart(t,n,e),r):(r._restart=r.restart,r.restart=function(t,n,e){n=+n,e=null==e?Ai():+e,r._restart((function o(a){a+=i,r._restart(o,i+=n,e),t(a)}),n,e)},r.restart(t,n,e),r)},t.isoFormat=D_,t.isoParse=F_,t.json=function(t,n){return fetch(t,n).then(Tc)},t.lab=ar,t.lch=function(t,n,e,r){return 1===arguments.length?hr(t):new pr(e,n,t,null==r?1:r)},t.least=function(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)<0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)<0:0===e(n,n))&&(r=n,i=!0);return r},t.leastIndex=ht,t.line=Ym,t.lineRadial=Zm,t.link=ax,t.linkHorizontal=function(){return ax(nx)},t.linkRadial=function(){const t=ax(rx);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.linkVertical=function(){return ax(ex)},t.local=Qn,t.map=function(t,n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");if("function"!=typeof n)throw new TypeError("mapper is not a function");return Array.from(t,((e,r)=>n(e,r,t)))},t.matcher=Vt,t.max=J,t.maxIndex=tt,t.mean=function(t,n){let e=0,r=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(++e,r+=n);else{let i=-1;for(let o of t)null!=(o=n(o,++i,t))&&(o=+o)>=o&&(++e,r+=o)}if(e)return r/e},t.median=function(t,n){return at(t,.5,n)},t.medianIndex=function(t,n){return ct(t,.5,n)},t.merge=ft,t.min=nt,t.minIndex=et,t.mode=function(t,n){const e=new InternMap;if(void 0===n)for(let n of t)null!=n&&n>=n&&e.set(n,(e.get(n)||0)+1);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&i>=i&&e.set(i,(e.get(i)||0)+1)}let r,i=0;for(const[t,n]of e)n>i&&(i=n,r=t);return r},t.namespace=It,t.namespaces=Ut,t.nice=Z,t.now=Ai,t.pack=function(){var t=null,n=1,e=1,r=np;function i(i){const o=ap();return i.x=n/2,i.y=e/2,t?i.eachBefore(xp(t)).eachAfter(wp(r,.5,o)).eachBefore(Mp(1)):i.eachBefore(xp(mp)).eachAfter(wp(np,1,o)).eachAfter(wp(r,i.r/Math.min(n,e),o)).eachBefore(Mp(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=Jd(n),i):t},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:ep(+t),i):r},i},t.packEnclose=function(t){return up(t,ap())},t.packSiblings=function(t){return bp(t,ap()),t},t.pairs=function(t,n=st){const e=[];let r,i=!1;for(const o of t)i&&e.push(n(r,o)),r=o,i=!0;return e},t.partition=function(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&Ap(r,r.x0,t*(r.depth+1)/n,r.x1,t*(r.depth+2)/n);var i=r.x0,o=r.y0,a=r.x1-e,u=r.y1-e;a0&&(d+=l);for(null!=n?p.sort((function(t,e){return n(g[t],g[e])})):null!=e&&p.sort((function(t,n){return e(a[t],a[n])})),u=0,f=d?(v-h*b)/d:0;u0?l*f:0)+b,g[c]={data:a[c],index:u,value:l,startAngle:y,endAngle:s,padAngle:_};return g}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:ym(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:ym(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:ym(+t),a):o},a},t.piecewise=di,t.pointRadial=Qm,t.pointer=ne,t.pointers=function(t,n){return t.target&&(t=te(t),void 0===n&&(n=t.currentTarget),t=t.touches||[t]),Array.from(t,(t=>ne(t,n)))},t.polygonArea=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++eu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonHull=function(t){if((e=t.length)<3)return null;var n,e,r=new Array(e),i=new Array(e);for(n=0;n=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;n(n=1664525*n+1013904223|0,lg*(n>>>0))},t.randomLogNormal=Kp,t.randomLogistic=fg,t.randomNormal=Zp,t.randomPareto=ng,t.randomPoisson=sg,t.randomUniform=Vp,t.randomWeibull=ug,t.range=lt,t.rank=function(t,e=n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");let r=Array.from(t);const i=new Float64Array(r.length);2!==e.length&&(r=r.map(e),e=n);const o=(t,n)=>e(r[t],r[n]);let a,u;return(t=Uint32Array.from(r,((t,n)=>n))).sort(e===n?(t,n)=>O(r[t],r[n]):I(o)),t.forEach(((t,n)=>{const e=o(t,void 0===a?t:a);e>=0?((void 0===a||e>0)&&(a=t,u=n),i[t]=u):i[t]=NaN})),i},t.reduce=function(t,n,e){if("function"!=typeof n)throw new TypeError("reducer is not a function");const r=t[Symbol.iterator]();let i,o,a=-1;if(arguments.length<3){if(({done:i,value:e}=r.next()),i)return;++a}for(;({done:i,value:o}=r.next()),!i;)e=n(e,o,++a,t);return e},t.reverse=function(t){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");return Array.from(t).reverse()},t.rgb=Fe,t.ribbon=function(){return Wa()},t.ribbonArrow=function(){return Wa(Va)},t.rollup=$,t.rollups=D,t.scaleBand=yg,t.scaleDiverging=function t(){var n=Ng(L_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleDivergingLog=function t(){var n=Fg(L_()).domain([.1,1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleDivergingPow=j_,t.scaleDivergingSqrt=function(){return j_.apply(null,arguments).exponent(.5)},t.scaleDivergingSymlog=function t(){var n=Ig(L_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleIdentity=function t(n){var e;function r(t){return null==t||isNaN(t=+t)?e:t}return r.invert=r,r.domain=r.range=function(t){return arguments.length?(n=Array.from(t,_g),r):n.slice()},r.unknown=function(t){return arguments.length?(e=t,r):e},r.copy=function(){return t(n).unknown(e)},n=arguments.length?Array.from(n,_g):[0,1],Ng(r)},t.scaleImplicit=pg,t.scaleLinear=function t(){var n=Sg();return n.copy=function(){return Tg(n,t())},hg.apply(n,arguments),Ng(n)},t.scaleLog=function t(){const n=Fg(Ag()).domain([1,10]);return n.copy=()=>Tg(n,t()).base(n.base()),hg.apply(n,arguments),n},t.scaleOrdinal=gg,t.scalePoint=function(){return vg(yg.apply(null,arguments).paddingInner(1))},t.scalePow=jg,t.scaleQuantile=function t(){var e,r=[],i=[],o=[];function a(){var t=0,n=Math.max(1,i.length);for(o=new Array(n-1);++t0?o[n-1]:r[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},u.unknown=function(t){return arguments.length?(n=t,u):u},u.thresholds=function(){return o.slice()},u.copy=function(){return t().domain([e,r]).range(a).unknown(n)},hg.apply(Ng(u),arguments)},t.scaleRadial=function t(){var n,e=Sg(),r=[0,1],i=!1;function o(t){var r=function(t){return Math.sign(t)*Math.sqrt(Math.abs(t))}(e(t));return isNaN(r)?n:i?Math.round(r):r}return o.invert=function(t){return e.invert(Hg(t))},o.domain=function(t){return arguments.length?(e.domain(t),o):e.domain()},o.range=function(t){return arguments.length?(e.range((r=Array.from(t,_g)).map(Hg)),o):r.slice()},o.rangeRound=function(t){return o.range(t).round(!0)},o.round=function(t){return arguments.length?(i=!!t,o):i},o.clamp=function(t){return arguments.length?(e.clamp(t),o):e.clamp()},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t(e.domain(),r).round(i).clamp(e.clamp()).unknown(n)},hg.apply(o,arguments),Ng(o)},t.scaleSequential=function t(){var n=Ng(O_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=Fg(O_()).domain([1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleSequentialPow=Y_,t.scaleSequentialQuantile=function t(){var e=[],r=mg;function i(t){if(null!=t&&!isNaN(t=+t))return r((s(e,t,1)-1)/(e.length-1))}return i.domain=function(t){if(!arguments.length)return e.slice();e=[];for(let n of t)null==n||isNaN(n=+n)||e.push(n);return e.sort(n),i},i.interpolator=function(t){return arguments.length?(r=t,i):r},i.range=function(){return e.map(((t,n)=>r(n/(e.length-1))))},i.quantiles=function(t){return Array.from({length:t+1},((n,r)=>at(e,r/t)))},i.copy=function(){return t(r).domain(e)},dg.apply(i,arguments)},t.scaleSequentialSqrt=function(){return Y_.apply(null,arguments).exponent(.5)},t.scaleSequentialSymlog=function t(){var n=Ig(O_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleSqrt=function(){return jg.apply(null,arguments).exponent(.5)},t.scaleSymlog=function t(){var n=Ig(Ag());return n.copy=function(){return Tg(n,t()).constant(n.constant())},hg.apply(n,arguments)},t.scaleThreshold=function t(){var n,e=[.5],r=[0,1],i=1;function o(t){return null!=t&&t<=t?r[s(e,t,0,i)]:n}return o.domain=function(t){return arguments.length?(e=Array.from(t),i=Math.min(e.length,r.length-1),o):e.slice()},o.range=function(t){return arguments.length?(r=Array.from(t),i=Math.min(e.length,r.length-1),o):r.slice()},o.invertExtent=function(t){var n=r.indexOf(t);return[e[n-1],e[n]]},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t().domain(e).range(r).unknown(n)},hg.apply(o,arguments)},t.scaleTime=function(){return hg.apply(I_(uv,cv,tv,Zy,xy,py,sy,ay,iy,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)},t.scaleUtc=function(){return hg.apply(I_(ov,av,ev,Qy,Fy,yy,hy,cy,iy,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)},t.scan=function(t,n){const e=ht(t,n);return e<0?void 0:e},t.schemeAccent=G_,t.schemeBlues=Xb,t.schemeBrBG=ib,t.schemeBuGn=wb,t.schemeBuPu=Tb,t.schemeCategory10=X_,t.schemeDark2=V_,t.schemeGnBu=Sb,t.schemeGreens=Vb,t.schemeGreys=Zb,t.schemeObservable10=W_,t.schemeOrRd=Nb,t.schemeOranges=em,t.schemePRGn=ab,t.schemePaired=Z_,t.schemePastel1=K_,t.schemePastel2=Q_,t.schemePiYG=cb,t.schemePuBu=zb,t.schemePuBuGn=Cb,t.schemePuOr=sb,t.schemePuRd=Db,t.schemePurples=Qb,t.schemeRdBu=hb,t.schemeRdGy=pb,t.schemeRdPu=Fb,t.schemeRdYlBu=yb,t.schemeRdYlGn=_b,t.schemeReds=tm,t.schemeSet1=J_,t.schemeSet2=tb,t.schemeSet3=nb,t.schemeSpectral=mb,t.schemeTableau10=eb,t.schemeYlGn=Ob,t.schemeYlGnBu=Ub,t.schemeYlOrBr=Yb,t.schemeYlOrRd=jb,t.select=Zn,t.selectAll=function(t){return"string"==typeof t?new Vn([document.querySelectorAll(t)],[document.documentElement]):new Vn([Ht(t)],Gn)},t.selection=Wn,t.selector=jt,t.selectorAll=Gt,t.shuffle=dt,t.shuffler=pt,t.some=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(n(r,++e,t))return!0;return!1},t.sort=U,t.stack=function(){var t=ym([]),n=dw,e=hw,r=pw;function i(i){var o,a,u=Array.from(t.apply(this,arguments),gw),c=u.length,f=-1;for(const t of i)for(o=0,++f;o0)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):(r[0]=0,r[1]=i)},t.stackOffsetExpand=function(t,n){if((r=t.length)>0){for(var e,r,i,o=0,a=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;afunction(t){t=`${t}`;let n=t.length;zp(t,n-1)&&!zp(t,n-2)&&(t=t.slice(0,-1));return"/"===t[0]?t:`/${t}`}(t(n,e,r)))),e=n.map(Pp),i=new Set(n).add("");for(const t of e)i.has(t)||(i.add(t),n.push(t),e.push(Pp(t)),h.push(Np));d=(t,e)=>n[e],p=(t,n)=>e[n]}for(a=0,i=h.length;a=0&&(f=h[t]).data===Np;--t)f.data=null}if(u.parent=Sp,u.eachBefore((function(t){t.depth=t.parent.depth+1,--i})).eachBefore(Kd),u.parent=null,i>0)throw new Error("cycle");return u}return r.id=function(t){return arguments.length?(n=Jd(t),r):n},r.parentId=function(t){return arguments.length?(e=Jd(t),r):e},r.path=function(n){return arguments.length?(t=Jd(n),r):t},r},t.style=_n,t.subset=function(t,n){return _t(n,t)},t.sum=function(t,n){let e=0;if(void 0===n)for(let n of t)(n=+n)&&(e+=n);else{let r=-1;for(let i of t)(i=+n(i,++r,t))&&(e+=i)}return e},t.superset=_t,t.svg=Nc,t.symbol=function(t,n){let e=null,r=km(i);function i(){let i;if(e||(e=i=r()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),i)return e=null,i+""||null}return t="function"==typeof t?t:ym(t||fx),n="function"==typeof n?n:ym(void 0===n?64:+n),i.type=function(n){return arguments.length?(t="function"==typeof n?n:ym(n),i):t},i.size=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),i):n},i.context=function(t){return arguments.length?(e=null==t?null:t,i):e},i},t.symbolAsterisk=cx,t.symbolCircle=fx,t.symbolCross=sx,t.symbolDiamond=dx,t.symbolDiamond2=px,t.symbolPlus=gx,t.symbolSquare=yx,t.symbolSquare2=vx,t.symbolStar=xx,t.symbolTimes=Px,t.symbolTriangle=Mx,t.symbolTriangle2=Ax,t.symbolWye=Cx,t.symbolX=Px,t.symbols=zx,t.symbolsFill=zx,t.symbolsStroke=$x,t.text=mc,t.thresholdFreedmanDiaconis=function(t,n,e){const r=v(t),i=at(t,.75)-at(t,.25);return r&&i?Math.ceil((e-n)/(2*i*Math.pow(r,-1/3))):1},t.thresholdScott=function(t,n,e){const r=v(t),i=w(t);return r&&i?Math.ceil((e-n)*Math.cbrt(r)/(3.49*i)):1},t.thresholdSturges=K,t.tickFormat=Eg,t.tickIncrement=V,t.tickStep=W,t.ticks=G,t.timeDay=py,t.timeDays=gy,t.timeFormatDefaultLocale=P_,t.timeFormatLocale=hv,t.timeFriday=Sy,t.timeFridays=$y,t.timeHour=sy,t.timeHours=ly,t.timeInterval=Vg,t.timeMillisecond=Wg,t.timeMilliseconds=Zg,t.timeMinute=ay,t.timeMinutes=uy,t.timeMonday=wy,t.timeMondays=ky,t.timeMonth=Zy,t.timeMonths=Ky,t.timeSaturday=Ey,t.timeSaturdays=Dy,t.timeSecond=iy,t.timeSeconds=oy,t.timeSunday=xy,t.timeSundays=Ny,t.timeThursday=Ay,t.timeThursdays=zy,t.timeTickInterval=cv,t.timeTicks=uv,t.timeTuesday=My,t.timeTuesdays=Cy,t.timeWednesday=Ty,t.timeWednesdays=Py,t.timeWeek=xy,t.timeWeeks=Ny,t.timeYear=tv,t.timeYears=nv,t.timeout=$i,t.timer=Ni,t.timerFlush=ki,t.transition=go,t.transpose=gt,t.tree=function(){var t=$p,n=1,e=1,r=null;function i(i){var c=function(t){for(var n,e,r,i,o,a=new Up(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new Up(r[i],i)),e.parent=n;return(a.parent=new Up(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore((function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)}));var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),g=e/(l.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*g}))}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=Rp(u),o=Dp(o),u&&o;)c=Dp(c),(a=Rp(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(Fp(qp(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!Rp(a)&&(a.t=u,a.m+=l-s),o&&!Dp(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=Yp,n=!1,e=1,r=1,i=[0],o=np,a=np,u=np,c=np,f=np;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(Tp),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}var l=f[n],h=r/2+l,d=n+1,p=e-1;for(;d>>1;f[g]c-o){var _=r?(i*v+a*y)/r:a;t(n,d,y,i,o,_,c),t(d,e,v,_,o,a,c)}else{var b=r?(o*v+c*y)/r:c;t(n,d,y,i,o,a,b),t(d,e,v,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ap,t.treemapResquarify=Lp,t.treemapSlice=Ip,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Ip:Ap)(t,n,e,r,i)},t.treemapSquarify=Yp,t.tsv=Mc,t.tsvFormat=lc,t.tsvFormatBody=hc,t.tsvFormatRow=pc,t.tsvFormatRows=dc,t.tsvFormatValue=gc,t.tsvParse=fc,t.tsvParseRows=sc,t.union=function(...t){const n=new InternSet;for(const e of t)for(const t of e)n.add(t);return n},t.unixDay=_y,t.unixDays=by,t.utcDay=yy,t.utcDays=vy,t.utcFriday=By,t.utcFridays=Vy,t.utcHour=hy,t.utcHours=dy,t.utcMillisecond=Wg,t.utcMilliseconds=Zg,t.utcMinute=cy,t.utcMinutes=fy,t.utcMonday=qy,t.utcMondays=jy,t.utcMonth=Qy,t.utcMonths=Jy,t.utcSaturday=Yy,t.utcSaturdays=Wy,t.utcSecond=iy,t.utcSeconds=oy,t.utcSunday=Fy,t.utcSundays=Ly,t.utcThursday=Oy,t.utcThursdays=Gy,t.utcTickInterval=av,t.utcTicks=ov,t.utcTuesday=Uy,t.utcTuesdays=Hy,t.utcWednesday=Iy,t.utcWednesdays=Xy,t.utcWeek=Fy,t.utcWeeks=Ly,t.utcYear=ev,t.utcYears=rv,t.variance=x,t.version="7.9.0",t.window=pn,t.xml=Sc,t.zip=function(){return gt(arguments)},t.zoom=function(){var t,n,e,r=Ew,i=Nw,o=zw,a=Cw,u=Pw,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=ri,h=$t("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",kw).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",S).filter(u).on("touchstart.zoom",E).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",k).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new ww(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new ww(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),c=null==e?m(u):"function"==typeof e?e.apply(t,o):e,f=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),s=t.__zoom,h="function"==typeof n?n.apply(t,o):n,d=l(s.invert(c).concat(f/s.k),h.invert(c).concat(f/h.k));return function(t){if(1===t)t=h;else{var n=d(t),e=f/n[2];t=new ww(e,c[0]-n[0]*e,c[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new M(t,n)}function M(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function T(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(c[0],Math.min(c[1],i.k*Math.pow(2,a.apply(this,arguments)))),s=ne(t);if(e.wheel)e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=i.invert(e.mouse[0]=s)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[s,i.invert(s)],Gi(this),e.start()}Sw(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(b(_(i,u),e.mouse[0],e.mouse[1]),e.extent,f))}}function A(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=Zn(t.view).on("mousemove.zoom",(function(t){if(Sw(t),!a.moved){var n=t.clientX-s,e=t.clientY-l;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(b(a.that.__zoom,a.mouse[0]=ne(t,i),a.mouse[1]),a.extent,f))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),ue(t.view,a.moved),Sw(t),a.event(t).end()}),!0),c=ne(t,i),s=t.clientX,l=t.clientY;ae(t.view),Aw(t),a.mouse=[c,this.__zoom.invert(c)],Gi(this),a.start()}}function S(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=ne(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),c=e.k*(t.shiftKey?.5:2),l=o(b(_(e,c),a,u),i.apply(this,n),f);Sw(t),s>0?Zn(this).transition().duration(s).call(x,l,a,t):Zn(this).call(v.transform,l,a,t)}}function E(e,...i){if(r.apply(this,arguments)){var o,a,u,c,f=e.touches,s=f.length,l=w(this,i,e.changedTouches.length===s).event(e);for(Aw(e),a=0;a { + it('gives selections a transition() method', () => { + const el = document.createElement('div'); + document.body.appendChild(el); + + const sel = select(el).append('div').style('opacity', '0'); + expect(typeof sel.transition).toBe('function'); + + // exercise the chain matrix.js actually uses for tooltip fades + expect(() => sel.transition().delay(10).duration(150).style('opacity', 1)).not.toThrow(); + }); + + it('provides the other d3 helpers matrix.js uses', () => { + const el = document.createElement('div'); + expect(typeof local().set).toBe('function'); + expect(typeof select(el).raise).toBe('function'); + }); +}); diff --git a/src/dataParser.ts b/src/dataParser.ts index e4f311d..a319f6a 100644 --- a/src/dataParser.ts +++ b/src/dataParser.ts @@ -1,95 +1,281 @@ -import { DataFrameView, Field, FieldType, getFieldDisplayName, GrafanaTheme2, PanelData } from '@grafana/data'; -import { Category, DataMatrixCell, LegendData, MatrixData, MatrixOptions } from './types'; +import { DataFrameView } from '@grafana/data'; +import { buildLegend } from './legend'; /** - * this function creates an adjacency matrix to be consumed by the matrix diagram + * this function creates an adjacency matrix to be consumed by the chord * function returns the matrix + forward and reverse lookup Maps to go from * source and target id to description assumes that data coming to us has at * least 3 columns if no preferences provided, assumes the first 3 columns are * source and target dimensions then value to display - * @param {PanelData} data Data for the matrix diagram - * @param {MatrixOptions} options Panel configuration - * @param {GrafanaTheme2} theme Grafana theme - * @return {MatrixData} + * @param {*} data Data for the chord diagram + * @param {string} src The data series that will act as the source + * @param {string} target The data series that will act as * the target + * @param {string} val The data series that will act as the value + * @return {[rowNames, colNames, dataMatrix]} */ -export function parseData(data: PanelData, options: MatrixOptions, theme: GrafanaTheme2): MatrixData { +// ---- Color ramp helpers (self-contained, no d3 dependency) ---- +function hexToRgb(h: string): [number, number, number] { + let s = h.replace('#', ''); + if (s.length === 3) { + s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]; + } + return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)]; +} +function rgbToHex(r: number, g: number, b: number): string { + const c = (x: number) => Math.round(Math.max(0, Math.min(255, x))).toString(16).padStart(2, '0'); + return '#' + c(r) + c(g) + c(b); +} +// Build an interpolator over a list of hex stops, t in [0,1]. +// Non-finite input (NaN/Infinity) falls back to the low end rather than throwing. +function makeRamp(stops: string[]): (t: number) => string { + const rgb = stops.map(hexToRgb); + const n = rgb.length - 1; + return (t: number) => { + if (!isFinite(t)) { + t = 0; + } + t = Math.max(0, Math.min(1, t)); + const f = t * n; + let i = Math.floor(f); + if (i < 0) { + i = 0; + } else if (i > n - 1) { + i = n - 1; + } + const frac = f - i; + const a = rgb[i]; + const b = rgb[i + 1]; + return rgbToHex(a[0] + (b[0] - a[0]) * frac, a[1] + (b[1] - a[1]) * frac, a[2] + (b[2] - a[2]) * frac); + }; +} + +// Sequential single-hue blue ramp, light -> dark (dataviz reference palette) +const SEQ_STOPS = ['#cde2fb', '#9ec5f4', '#6da7ec', '#3987e5', '#256abf', '#184f95', '#0d366b']; + +/** + * Return a field's values as a plain array, tolerant of Grafana's differing + * representations: a plain array (Grafana 13+ runtime), a Vector with toArray(), + * or a Vector-like object with length + get(). Using Object.values() directly on a + * Vector returns the internal buffer, not the values — this normalizes that away. + */ +function fieldValuesArray(field: any): any[] { + const v = field && field.values; + if (!v) { + return []; + } + if (Array.isArray(v)) { + return v; + } + if (typeof v.toArray === 'function') { + return v.toArray(); + } + if (typeof v.length === 'number' && typeof v.get === 'function') { + const out: any[] = []; + for (let i = 0; i < v.length; i++) { + out.push(v.get(i)); + } + return out; + } + return Object.values(v); +} + +/** + * Spectral seriation: order rows and columns so similar ones sit next to each other, + * revealing block structure. Builds the value matrix, column-centers it, then finds + * the dominant right singular vector via power iteration (dependency-free). Rows are + * ordered by their projection onto that vector; columns by the vector itself. + * Returns null score maps when the matrix is too small or degenerate (caller falls + * back to name order). Deterministic (fixed seed) so re-renders don't reshuffle. + */ +function computeSeriation( + rowKeys: string[], + colKeys: string[], + cellRaw: Map +): { rowScores: Map | null; colScores: Map | null } { + const nR = rowKeys.length; + const nC = colKeys.length; + const none = { rowScores: null, colScores: null }; + if (nR < 3 || nC < 3) { + return none; + } + + // build value matrix M (nR x nC) + const M: number[][] = []; + for (let i = 0; i < nR; i++) { + const row = new Array(nC); + for (let j = 0; j < nC; j++) { + row[j] = cellRaw.get(`${rowKeys[i]}||${colKeys[j]}`) || 0; + } + M.push(row); + } + // column-center (PCA): removes the all-positive component so the ordering reflects + // covariance structure rather than raw totals + for (let j = 0; j < nC; j++) { + let s = 0; + for (let i = 0; i < nR; i++) { + s += M[i][j]; + } + const mean = s / nR; + for (let i = 0; i < nR; i++) { + M[i][j] -= mean; + } + } + + const normalize = (x: number[]): number[] | null => { + let n = 0; + for (const e of x) { + n += e * e; + } + n = Math.sqrt(n); + if (n < 1e-12) { + return null; + } + return x.map((e) => e / n); + }; + + // deterministic non-degenerate seed + let v: number[] | null = new Array(nC).fill(0).map((_, j) => Math.cos(j * 0.7) + 1.5); + v = normalize(v); + if (!v) { + return none; + } + // power iteration on M^T M without materializing it: v <- normalize(M^T (M v)) + for (let iter = 0; iter < 60; iter++) { + const u = new Array(nR).fill(0); + for (let i = 0; i < nR; i++) { + let s = 0; + const Mi = M[i]; + for (let j = 0; j < nC; j++) { + s += Mi[j] * v![j]; + } + u[i] = s; + } + const w = new Array(nC).fill(0); + for (let i = 0; i < nR; i++) { + const Mi = M[i]; + const ui = u[i]; + for (let j = 0; j < nC; j++) { + w[j] += Mi[j] * ui; + } + } + const nv = normalize(w); + if (!nv) { + return none; + } + v = nv; + } + + // final row scores = M v (projection onto the dominant direction) + const rowScores = new Map(); + for (let i = 0; i < nR; i++) { + let s = 0; + const Mi = M[i]; + for (let j = 0; j < nC; j++) { + s += Mi[j] * v![j]; + } + rowScores.set(rowKeys[i], s); + } + const colScores = new Map(); + for (let j = 0; j < nC; j++) { + colScores.set(colKeys[j], v![j]); + } + return { rowScores, colScores }; +} + +export function parseData(data: { series: any[] }, options: any, theme: any) { const series = data.series[0]; if (series === null || series === undefined) { // no data, bail console.error('no data'); - return { rowNames: null, colNames: null, colCategories: [], rowCategories: [], data: null, legend: null }; + return { rows: null, columns: null, colMetadata: [], colCategories: [], rowMetadata: [], rowCategories: [], data: null, legend: null, rowTotals: [], colTotals: [], valueDomain: null }; } const frame = new DataFrameView(series); if (frame === null || frame === undefined) { // no data, bail console.error('no data'); - return { rowNames: null, colNames: null, colCategories: [], rowCategories: [], data: null, legend: null }; + return { rows: null, columns: null, colMetadata: [], colCategories: [], rowMetadata: [], rowCategories: [], data: null, legend: null, rowTotals: [], colTotals: [], valueDomain: null }; } // set fields - const sourceField = series.fields.find((f: Field) => - options.sourceField !== undefined && ( - options.sourceField === f.name - || options.sourceField === f.config?.displayNameFromDS - || options.sourceField === getFieldDisplayName(f) - ) - ) ?? series.fields.find((f: Field) => f.type === FieldType.string); - const targetField = series.fields.find((f: Field) => - options.targetField !== undefined && ( - options.targetField === f.name - || options.targetField === f.config?.displayNameFromDS - || options.targetField === getFieldDisplayName(f) - ) - ) ?? series.fields.find((f: Field) => f.type === FieldType.string && f.name !== sourceField?.name); - const colCategoryField = series.fields.find((f: Field) => - options.colCategoryField !== undefined && ( - options.colCategoryField === f.name - || options.colCategoryField === f.config?.displayNameFromDS - || options.colCategoryField === getFieldDisplayName(f) - ) - ); - const rowCategoryField = series.fields.find((f: Field) => - options.rowCategoryField !== undefined && ( - options.rowCategoryField === f.name - || options.rowCategoryField === f.config?.displayNameFromDS - || options.rowCategoryField === getFieldDisplayName(f) - ) - ); + let sourceKey = options.sourceField; + let targetKey = options.targetField; + let colCategoryKey = options.colCategoryField; + let rowCategoryKey = options.rowCategoryField; + if (!sourceKey) { + sourceKey = 0; + } + if (!targetKey) { + targetKey = 1; + } + // assign valueField to the specified field or use the first number field by default - const valueField = series.fields.find((f: Field) => - options.valueField !== undefined && ( - options.valueField === f.name - || options.valueField === f.config?.displayNameFromDS - || options.valueField === getFieldDisplayName(f) - ) - ) ?? series.fields.find((f: Field) => f.type === FieldType.number); - const sourceKey = sourceField?.name; - const targetKey = targetField?.name; - const colCategoryKey = colCategoryField?.name; - const rowCategoryKey = rowCategoryField?.name; - const valKey = valueField?.name; - - if ( - sourceKey === undefined || targetKey === undefined - || valueField === undefined || valKey === undefined - ) { - // no data, bail - console.error('no data'); - return { rowNames: null, colNames: null, colCategories: [], rowCategories: [], data: null, legend: null }; + const val = options.valueField; + const valueField = val + ? data.series.map((series: { fields: any[] }) => series.fields.find((field: { name: any }) => field.name === val)) + : data.series.map((series: { fields: any[] }) => + series.fields.find((field: { type: string }) => field.type === 'number') + ); + const valKey = valueField[0].name; + + // ---- Determine the value domain (min/max) for the built-in color ramps ---- + let allNumericValues: number[] = fieldValuesArray(frame.fields[valKey]).filter( + (v) => typeof v === 'number' && !isNaN(v) + ); + const dataMin = allNumericValues.length ? Math.min(...allNumericValues) : 0; + const dataMax = allNumericValues.length ? Math.max(...allNumericValues) : 0; + // Effective color domain: auto (data min/max) or manual (fixed, for comparable panels) + let domainMin = dataMin; + let domainMax = dataMax; + if (options.colorDomainMode === 'manual') { + if (typeof options.colorDomainMin === 'number') { + domainMin = options.colorDomainMin; + } + if (typeof options.colorDomainMax === 'number') { + domainMax = options.colorDomainMax; + } + } + const valueDomain = allNumericValues.length || options.colorDomainMode === 'manual' ? { min: domainMin, max: domainMax } : null; + + // ---- Build the color scale for the chosen colorMode ---- + // Default to 'standard' (Standard Options thresholds): it is what the plugin declares + // as its color source (preferThresholdMode), and it is how panels saved before this + // option existed were colored — so the fallback restores them rather than restyling them. + const colorMode = options.colorMode || 'standard'; + const isDark = !!theme.isDark; + let colorScale: ((v: number) => string) | null = null; + if (colorMode === 'sequential') { + // In dark mode flip the ramp so high magnitude reads bright, low recedes to the surface + const stops = isDark ? [...SEQ_STOPS].reverse() : SEQ_STOPS; + const ramp = makeRamp(stops); + colorScale = (v: number) => { + if (domainMax === domainMin) { + return ramp(isDark ? 1 : 0.5); + } + return ramp((v - domainMin) / (domainMax - domainMin)); + }; + } else if (colorMode === 'diverging') { + const mid = typeof options.divergingMidpoint === 'number' ? options.divergingMidpoint : 0; + const gray = isDark ? '#383835' : '#f0efec'; + // blue (low) <-> neutral gray (mid) <-> red (high) + const ramp = makeRamp(['#184f95', '#3987e5', gray, '#e34948', '#b3261e']); + const maxAbs = Math.max(Math.abs(mid - domainMin), Math.abs(domainMax - mid)) || 1; + colorScale = (v: number) => ramp(0.5 + 0.5 * ((v - mid) / maxAbs)); } // function that maps value to color specified by Standard Options panel. // if value is null or was not returned by query, use different value const nullColor = theme.visualization.getColorByName(options.nullColor); const defaultColor = theme.visualization.getColorByName(options.defaultColor); - function colorMap(v: any): string { - if (v === null) { + function colorMap(v: any) { + if (v == null) { return nullColor; } else if (v === -1) { return defaultColor; + } else if (colorScale) { + return colorScale(v); } else { - return valueField.display(v).color; + return valueField[0].display(v).color; } } @@ -97,186 +283,363 @@ export function parseData(data: PanelData, options: MatrixOptions, theme: Grafan const colGrouping = !!(colCategoryKey && options.enableColGrouping); const rowGrouping = !!(rowCategoryKey && options.enableRowGrouping); - // Store unique set of row and column headings/categories - const rowNamesSet = new Set(); - const colNamesSet = new Set(); - const rowCategoriesMap = new Map(); - const colCategoriesMap = new Map(); + // Aggregation function to combine multiple values for the same cell based on user-selected method + // By default: aggregate is set to "sum" + // Falls back to "last" for non-numeric values (strings, etc.) + function aggregate(values: any[], method: string): any { + if (values.length === 0) { + return null; + } + // For non-numeric values, only "last" is meaningful + if (typeof values[0] !== 'number') { + return values[values.length - 1]; + } + switch (method) { + case 'sum': return values.reduce((a: number, b: number) => a + b, 0); + case 'avg': return values.reduce((a: number, b: number) => a + b, 0) / values.length; + case 'min': return Math.min(...values); + case 'max': return Math.max(...values); + case 'last': return values[values.length - 1]; + default: return values.reduce((a: number, b: number) => a + b, 0); + } + } + + // Build composite keys from data + // When grouping is enabled, use "category::name" as the key to allow + // the same name in multiple categories. Otherwise, just use the name. + let compositeColKeys: string[] = []; + let compositeRowKeys: string[] = []; + + // Marginal sums per composite key (used for "total" sort and for tracking cells that were seen) + const colKeySums = new Map(); + const rowKeySums = new Map(); + // Raw aggregated value per (rowKey||colKey) pair (used for "cluster" seriation). + const cellRaw = new Map(); + // Track every (rowKey, colKey) pair that appears in the data, even when null, + // so we can tell "queried and null" apart from "no data at all". + const seenCells = new Set(); // IF static list toggle is set, use input list if (options.inputList) { - if (options.staticRows !== undefined) { - options.staticRows.split(',').forEach((item: string) => rowNamesSet.add(item)); - } - if (options.staticColumns !== undefined) { - options.staticColumns.split(',').forEach((item: string) => colNamesSet.add(item)); - } + // Tolerate empty/undefined lists (the option can be enabled before the fields + // are filled in) and trim whitespace so " a, b " doesn't create phantom / padded keys. + const splitList = (s: string | undefined) => + (s || '') + .split(',') + .map((t: string) => t.trim()) + .filter((t: string) => t.length > 0); + const staticRows = splitList(options.staticRows); + const staticCols = splitList(options.staticColumns); + compositeRowKeys = Array.from(new Set(staticRows)); + compositeColKeys = Array.from(new Set(staticCols)); } else { - // Collect all unique row and column headings/categories + // Build unique composite keys from data + const colKeySet = new Set(); + const rowKeySet = new Set(); + frame.forEach((row) => { - const rowName = row[sourceKey]; - if (!rowNamesSet.has(rowName)) { - // new row heading - rowNamesSet.add(rowName); - - const categoryName = row[rowCategoryKey]; - if (rowGrouping && categoryName != null) { - if (!rowCategoriesMap.has(categoryName)) { - // new row category - rowCategoriesMap.set(categoryName, { - name: categoryName, - items: [rowName], - }); - } else { - // append row heading to existing row category - const category = rowCategoriesMap.get(categoryName); - category?.items.push(rowName); - } - } + const rowName = String(row[sourceKey]); + const colName = String(row[targetKey]); + const colCat = colGrouping && row[colCategoryKey] != null ? String(row[colCategoryKey]) : ''; + const rowCat = rowGrouping && row[rowCategoryKey] != null ? String(row[rowCategoryKey]) : ''; + + const colKey = colGrouping ? `${colCat}::${colName}` : colName; + const rowKey = rowGrouping ? `${rowCat}::${rowName}` : rowName; + + colKeySet.add(colKey); + rowKeySet.add(rowKey); + seenCells.add(`${rowKey}||${colKey}`); + + const v = row[valKey]; + if (typeof v === 'number' && !isNaN(v)) { + colKeySums.set(colKey, (colKeySums.get(colKey) || 0) + v); + rowKeySums.set(rowKey, (rowKeySums.get(rowKey) || 0) + v); + const pk = `${rowKey}||${colKey}`; + cellRaw.set(pk, (cellRaw.get(pk) || 0) + v); } + }); - const colName = row[targetKey]; - if (!colNamesSet.has(colName)) { - // new column heading - colNamesSet.add(colName); - - const categoryName = row[colCategoryKey]; - if (colGrouping && categoryName != null) { - if (!colCategoriesMap.has(categoryName)) { - // new column category - colCategoriesMap.set(categoryName, { - name: categoryName, - items: [colName], - }); - } else { - // append column heading to existing column category - const category = colCategoriesMap.get(categoryName); - category?.items.push(colName); - } - } + // Sort keys by name (default), by marginal total, or by cluster (seriation) + const byName = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0); + const byTotal = (sums: Map) => (a: string, b: string) => { + const diff = (sums.get(b) || 0) - (sums.get(a) || 0); + return diff !== 0 ? diff : byName(a, b); + }; + const byScore = (scores: Map) => (a: string, b: string) => { + const diff = (scores.get(a) || 0) - (scores.get(b) || 0); + return diff !== 0 ? diff : byName(a, b); + }; + + // Compute cluster ordering only when requested + let rowScores: Map | null = null; + let colScores: Map | null = null; + if (options.rowSort === 'cluster' || options.colSort === 'cluster') { + const seriation = computeSeriation(Array.from(rowKeySet), Array.from(colKeySet), cellRaw); + rowScores = seriation.rowScores; + colScores = seriation.colScores; + } + + const pickComparator = (mode: string, sums: Map, scores: Map | null) => { + if (mode === 'total') { + return byTotal(sums); } + if (mode === 'cluster' && scores) { + return byScore(scores); + } + return byName; + }; + + compositeColKeys = Array.from(colKeySet).sort(pickComparator(options.colSort, colKeySums, colScores)); + compositeRowKeys = Array.from(rowKeySet).sort(pickComparator(options.rowSort, rowKeySums, rowScores)); + } + + // Extract display names from composite keys + let colNames = compositeColKeys.map((k) => colGrouping && k.includes('::') ? k.split('::').slice(1).join('::') : k); + let rowNames = compositeRowKeys.map((k) => rowGrouping && k.includes('::') ? k.split('::').slice(1).join('::') : k); + + // Build column metadata and category groupings + let colMetadata: any[] = []; + let colCategories: any[] = []; + + if (colGrouping) { + // Group columns by category using composite keys + const categoryToColumns = new Map>(); + + compositeColKeys.forEach((key) => { + const [cat, ...nameParts] = key.split('::'); + const name = nameParts.join('::'); + if (!categoryToColumns.has(cat)) { + categoryToColumns.set(cat, []); + } + categoryToColumns.get(cat)!.push({ name, key }); + }); + + // Build CategoryGroup array (sorted by category name) + const sortedCategories = Array.from(categoryToColumns.keys()).sort(); + let globalIndex = 0; + + sortedCategories.forEach((catName) => { + const columnsInCat = categoryToColumns.get(catName)!; + colCategories.push({ + name: catName, + columns: columnsInCat.map((c) => c.name), + startIndex: globalIndex, + endIndex: globalIndex + columnsInCat.length - 1, + }); + globalIndex += columnsInCat.length; }); + + // Build ColumnInfo array and re-order colNames/compositeColKeys to match grouped order + const reorderedColKeys: string[] = []; + const reorderedColNames: string[] = []; + colCategories.forEach((cat: any, catIndex: number) => { + cat.columns.forEach((colName: string, indexInCat: number) => { + colMetadata.push({ + name: colName, + category: cat.name, + categoryIndex: catIndex, + indexInCategory: indexInCat, + }); + reorderedColKeys.push(`${cat.name}::${colName}`); + reorderedColNames.push(colName); + }); + }); + + compositeColKeys = reorderedColKeys; + colNames = reorderedColNames; + } else { + // No categories: create default metadata + colMetadata = colNames.map((name: string, idx: number) => ({ + name, + category: '', + categoryIndex: 0, + indexInCategory: idx, + })); } - if (rowNamesSet.size === 0 || colNamesSet.size === 0) { - // no data, bail - console.error('no data'); - return { rowNames: null, colNames: null, colCategories: [], rowCategories: [], data: null, legend: null }; + // Build row metadata and row category groupings + let rowMetadata: any[] = []; + let rowCategories: any[] = []; + + if (rowGrouping) { + // Group rows by category using composite keys + const categoryToRows = new Map>(); + + compositeRowKeys.forEach((key) => { + const [cat, ...nameParts] = key.split('::'); + const name = nameParts.join('::'); + if (!categoryToRows.has(cat)) { + categoryToRows.set(cat, []); + } + categoryToRows.get(cat)!.push({ name, key }); + }); + + // Build RowCategoryGroup array (sorted by category name) + const sortedRowCategories = Array.from(categoryToRows.keys()).sort(); + let globalRowIndex = 0; + + sortedRowCategories.forEach((catName) => { + const rowsInCat = categoryToRows.get(catName)!; + rowCategories.push({ + name: catName, + rows: rowsInCat.map((r) => r.name), + startIndex: globalRowIndex, + endIndex: globalRowIndex + rowsInCat.length - 1, + }); + globalRowIndex += rowsInCat.length; + }); + + // Build RowInfo array and re-order rowNames/compositeRowKeys to match grouped order + const reorderedRowKeys: string[] = []; + const reorderedRowNames: string[] = []; + rowCategories.forEach((cat: any, catIndex: number) => { + cat.rows.forEach((rowName: string, indexInCat: number) => { + rowMetadata.push({ + name: rowName, + category: cat.name, + categoryIndex: catIndex, + indexInCategory: indexInCat, + }); + reorderedRowKeys.push(`${cat.name}::${rowName}`); + reorderedRowNames.push(rowName); + }); + }); + + compositeRowKeys = reorderedRowKeys; + rowNames = reorderedRowNames; + } else { + // No row categories: create default metadata + rowMetadata = rowNames.map((name: string, idx: number) => ({ + name, + category: '', + categoryIndex: 0, + indexInCategory: idx, + })); } - const numSquaresInMatrix = rowNamesSet.size * colNamesSet.size; + const numSquaresInMatrix = rowNames.length * colNames.length; if (numSquaresInMatrix > 50000) { - return { rowNames: null, colNames: null, colCategories: [], rowCategories: [], data: 'too many inputs', legend: null }; + return { rows: null, columns: null, colMetadata: [], colCategories: [], rowMetadata: [], rowCategories: [], data: 'too many inputs', legend: null, rowTotals: [], colTotals: [], valueDomain: null }; } - const rowNames: any[] = Array.from(rowNamesSet); - const colNames: any[] = Array.from(colNamesSet); - const rowCategories: Category[] = []; - const colCategories: Category[] = []; + // Phase 1: Collect all values per cell using composite keys + const cellValues = new Map(); - switch(options.sortType) { - case "natural-asc": - case "natural-desc": - const naturalSort = (a: any, b: any) => { - return a.toString().localeCompare(b.toString(), undefined, { numeric: true }); - }; + frame.forEach((row) => { + const rowName = String(row[sourceKey]); + const colName = String(row[targetKey]); + const colCat = colGrouping && row[colCategoryKey] != null ? String(row[colCategoryKey]) : ''; + const rowCat = rowGrouping && row[rowCategoryKey] != null ? String(row[rowCategoryKey]) : ''; - let sort = naturalSort; - if (options.sortType.endsWith("-desc")) { - sort = (a, b) => naturalSort(b, a); - } + const colKey = colGrouping ? `${colCat}::${colName}` : colName; + const rowKey = rowGrouping ? `${rowCat}::${rowName}` : rowName; - // sort row and column headings - rowNames.sort(sort); - colNames.sort(sort); + const r = compositeRowKeys.indexOf(rowKey); + const c = compositeColKeys.indexOf(colKey); + const v = row[valKey]; + const isValid = v != null && !(typeof v === 'number' && isNaN(v)); - // sort row and column categories and items in each category - Array.from(rowCategoriesMap.keys()).sort(sort).forEach((name) => { - const category = rowCategoriesMap.get(name); - if (category !== undefined) { - category.items.sort(sort); - rowCategories.push(category); - } - }); - Array.from(colCategoriesMap.keys()).sort(sort).forEach((name) => { - const category = colCategoriesMap.get(name); - if (category !== undefined) { - category.items.sort(sort); - colCategories.push(category); + if (r > -1 && c > -1 && isValid) { + const cellKey = `${r}:${c}`; + if (!cellValues.has(cellKey)) { + cellValues.set(cellKey, []); } - }); - break; - case "none": - rowCategories.push(...rowCategoriesMap.values()); - colCategories.push(...colCategoriesMap.values()); - break; - } + cellValues.get(cellKey)!.push(v); + } + }); - // create data matrix - const dataMatrix: DataMatrixCell[][] = []; + // Phase 2: Aggregate values and build data matrix + // Cells start as -1 (no data). Cells that appeared in the query but had only + // null values become explicit null cells so they can be styled distinctly. + let dataMatrix: any[][] = []; for (let i = 0; i < rowNames.length; i++) { dataMatrix.push(new Array(colNames.length).fill(-1)); } + // running totals aligned to the final row/column order + const rowTotals = new Array(rowNames.length).fill(0); + const colTotals = new Array(colNames.length).fill(0); - frame.forEach((row) => { - const rowName = row[sourceKey]; - const colName = row[targetKey]; - const r = rowNames.indexOf(rowName); - const c = colNames.indexOf(colName); - const v = row[valKey]; - if (r > -1 && c > -1) { - dataMatrix[r][c] = { - row: rowName, - col: colName, - val: v, - color: colorMap(v), - display: valueField.display(v), - }; + const aggMethod = options.aggregationMethod || 'sum'; + + cellValues.forEach((values, cellKey) => { + const [r, c] = cellKey.split(':').map(Number); + const v = values.length === 1 ? values[0] : aggregate(values, aggMethod); + dataMatrix[r][c] = { + row: rowNames[r], + col: colNames[c], + val: v, + color: colorMap(v), + display: valueField[0].display(v), + isNull: false, + }; + if (typeof v === 'number' && !isNaN(v)) { + rowTotals[r] += v; + colTotals[c] += v; } }); - // parse data for legend - const legendData: LegendData[] = []; - if (options.showLegend) { - let tempValues: any[] = []; - if (options.legendType === 'range') { - // get min & max, steps - let allValues: number[] = Object.values(frame.fields[valKey].values); - let thisMin = Math.min(...allValues); - let thisMax = Math.max(...allValues); - let step = (thisMax - thisMin) / 10; - // push 10 steps to use for legend - tempValues = []; - for(let i = 0; i <= 10; i++) { - tempValues.push(thisMin + (i*step)); + // Mark null cells: seen in the query but no non-null value landed in the cell + if (!options.inputList) { + for (let r = 0; r < compositeRowKeys.length; r++) { + for (let c = 0; c < compositeColKeys.length; c++) { + if (dataMatrix[r][c] === -1 && seenCells.has(`${compositeRowKeys[r]}||${compositeColKeys[c]}`)) { + dataMatrix[r][c] = { + row: rowNames[r], + col: colNames[c], + val: null, + color: nullColor, + display: valueField[0].display(null), + isNull: true, + }; + } } - } else { - // get unique categories - let allValues: string[] = Object.values(frame.fields[valKey].values); - let unique = new Set(allValues); - tempValues = [...unique]; } - tempValues.forEach((val) => { - // find display values, unit & color for each - // store in array - let text = valueField.display(val).text; - if (valueField.display(val).suffix) { - text = text + ` ${valueField.display(val).suffix}`; + } + + // Which special-cell states are present, so the legend knows whether to key them. + // Collected here while we already hold the matrix, rather than re-walking it later. + let hasNull = false; + let hasNoData = false; + for (let r = 0; r < dataMatrix.length && !(hasNull && hasNoData); r++) { + for (let c = 0; c < dataMatrix[r].length; c++) { + const cell = dataMatrix[r][c]; + if (cell === -1) { + hasNoData = true; + } else if (cell.isNull) { + hasNull = true; } - legendData.push({ - label: text, - color: colorMap(val), - }); - }); + } } + // The legend describes whatever actually colored the cells — threshold steps when + // the field's thresholds are the color source, a ramp when one is in play. + const legendModel = options.showLegend + ? buildLegend(valueField[0], options, theme, { + dataMin, + dataMax, + domain: valueDomain, + colorScale, + colorMap, + uniqueValues: [...new Set(fieldValuesArray(frame.fields[valKey]))].filter( + (v) => v != null && !(typeof v === 'number' && isNaN(v)) + ), + hasNull, + hasNoData, + }) + : null; + let dataObject = { - rowNames: rowNames, - colNames: colNames, + rows: rowNames, + columns: colNames, + colMetadata: colMetadata, colCategories: colCategories, + rowMetadata: rowMetadata, rowCategories: rowCategories, data: dataMatrix, - legend: legendData, + legend: legendModel, + rowTotals: rowTotals, + colTotals: colTotals, + valueDomain: valueDomain, }; return dataObject; } diff --git a/src/legend.test.ts b/src/legend.test.ts new file mode 100644 index 0000000..e958646 --- /dev/null +++ b/src/legend.test.ts @@ -0,0 +1,419 @@ +import { createTheme, FieldType, getDisplayProcessor, ThresholdsMode, toDataFrame } from '@grafana/data'; +import { + buildLegend, + CATEGORICAL_CAP, + estimateLegendHeight, + layoutLegend, + LEGEND_LAYOUT, + normalizeLegendType, + resolveLegendKind, +} from './legend'; +import { LegendModel } from './types'; + +const theme = createTheme(); + +/** A numeric value field with a real display processor, so legend labels are + * formatted through exactly the same path the cells use. */ +function makeField(overrides: any = {}, values: number[] = [0, 5, 10, 20, 50, 100]) { + const frame = toDataFrame({ + fields: [{ name: 'gbps', type: FieldType.number, values, config: overrides }], + }); + const field = frame.fields[0]; + field.display = getDisplayProcessor({ field, theme }); + return field; +} + +const absoluteThresholds = { + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { value: -Infinity, color: 'green' }, + { value: 20, color: 'orange' }, + { value: 50, color: 'red' }, + ], + }, +}; + +const baseOptions: any = { + showLegend: true, + legendType: 'auto', + colorMode: 'standard', + valueText: 'Gbps', +}; + +function ctxFor(values: number[] = [0, 5, 10, 20, 50, 100], extra: any = {}) { + const min = Math.min(...values); + const max = Math.max(...values); + return { + dataMin: min, + dataMax: max, + domain: { min, max }, + colorScale: null, + colorMap: (v: any) => (typeof v === 'number' && v >= 50 ? '#ff0000' : '#00ff00'), + uniqueValues: values, + hasNull: false, + hasNoData: false, + ...extra, + }; +} + +describe('legend kind resolution', () => { + test('standard color mode with thresholds gives a threshold legend', () => { + const field = makeField(absoluteThresholds); + expect(resolveLegendKind(field, baseOptions, true)).toBe('thresholds'); + }); + + test("the panel's own ramps give a gradient legend", () => { + const field = makeField(absoluteThresholds); + expect(resolveLegendKind(field, { ...baseOptions, colorMode: 'sequential' }, true)).toBe('gradient'); + expect(resolveLegendKind(field, { ...baseOptions, colorMode: 'diverging' }, true)).toBe('gradient'); + }); + + test('a continuous Grafana color scheme gives a gradient legend', () => { + const field = makeField({ ...absoluteThresholds, color: { mode: 'continuous-GrYlRd' } }); + expect(resolveLegendKind(field, baseOptions, true)).toBe('gradient'); + }); + + test('a fixed color gives a categorical legend', () => { + const field = makeField({ color: { mode: 'fixed', fixedColor: 'blue' } }); + expect(resolveLegendKind(field, baseOptions, true)).toBe('categorical'); + }); + + test('non-numeric values give a categorical legend whatever the color mode', () => { + const field = makeField(absoluteThresholds); + expect(resolveLegendKind(field, { ...baseOptions, colorMode: 'sequential' }, false)).toBe('categorical'); + }); + + test('an explicit Legend Type overrides auto-detection', () => { + const field = makeField(absoluteThresholds); + expect(resolveLegendKind(field, { ...baseOptions, legendType: 'categorical' }, true)).toBe('categorical'); + expect(resolveLegendKind(field, { ...baseOptions, legendType: 'gradient' }, true)).toBe('gradient'); + }); + + test('legend types saved by older dashboards still resolve', () => { + // 'range' was the old continuous option; it now defers to Auto + expect(normalizeLegendType('range')).toBe('auto'); + expect(normalizeLegendType('categorical')).toBe('categorical'); + expect(normalizeLegendType(undefined)).toBe('auto'); + }); +}); + +describe('threshold legend is built from the thresholds, not the data range', () => { + test('one entry per step, with the configured boundaries', () => { + const model = buildLegend(makeField(absoluteThresholds), baseOptions, theme, ctxFor()); + expect(model.kind).toBe('thresholds'); + expect(model.items).toHaveLength(3); + expect(model.items.map((i) => [i.from, i.to])).toEqual([ + [null, 20], + [20, 50], + [50, null], + ]); + }); + + test('boundary labels never print an infinity', () => { + const model = buildLegend(makeField(absoluteThresholds), baseOptions, theme, ctxFor()); + expect(model.items[0].label).toBe('< 20'); + expect(model.items[1].label).toBe('20 – 50'); + expect(model.items[2].label).toBe('≥ 50'); + expect(JSON.stringify(model.items)).not.toContain('Infinity'); + }); + + test('a legend min floors the first band instead of leaving it unbounded', () => { + const model = buildLegend(makeField(absoluteThresholds), { ...baseOptions, legendMin: 0 }, theme, ctxFor()); + expect(model.items[0].from).toBe(0); + expect(model.items[0].label).toBe('0 – 20'); + // the floor is printed at the bar's left edge + expect(model.items[0].fromLabel).toBe('0'); + // the rest of the scale is untouched + expect(model.items.map((i) => i.to)).toEqual([20, 50, null]); + }); + + test('a non-zero legend min works, and omitting it keeps the unbounded reading', () => { + const floored = buildLegend(makeField(absoluteThresholds), { ...baseOptions, legendMin: 10 }, theme, ctxFor()); + expect(floored.items[0].label).toBe('10 – 20'); + + const open = buildLegend(makeField(absoluteThresholds), baseOptions, theme, ctxFor()); + expect(open.items[0].from).toBeNull(); + expect(open.items[0].label).toBe('< 20'); + expect(open.items[0].fromLabel).toBe(''); + }); + + test('labels carry the field unit, matching the cells', () => { + const field = makeField({ ...absoluteThresholds, unit: 'bps' }); + const model = buildLegend(field, baseOptions, theme, ctxFor()); + // getDisplayProcessor scales bps and appends a suffix + expect(model.items[1].label).toMatch(/–/); + expect(model.items[2].label.startsWith('≥ ')).toBe(true); + }); + + test('a step above every data point is still shown', () => { + // data tops out at 30 but a threshold sits at 90 + const field = makeField({ + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { value: -Infinity, color: 'green' }, + { value: 90, color: 'red' }, + ], + }, + }); + const model = buildLegend(field, baseOptions, theme, ctxFor([1, 10, 30])); + expect(model.items).toHaveLength(2); + expect(model.items[1].from).toBe(90); + }); + + test('steps are sorted even when the config is out of order', () => { + const field = makeField({ + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { value: 50, color: 'red' }, + { value: -Infinity, color: 'green' }, + { value: 20, color: 'orange' }, + ], + }, + }); + const model = buildLegend(field, baseOptions, theme, ctxFor()); + expect(model.items.map((i) => i.to)).toEqual([20, 50, null]); + }); + + test('step colors are resolved through the theme', () => { + const model = buildLegend(makeField(absoluteThresholds), baseOptions, theme, ctxFor()); + model.items.forEach((item) => { + expect(item.color).toMatch(/^(#|rgb)/); + }); + expect(new Set(model.items.map((i) => i.color)).size).toBe(3); + }); + + test('percentage thresholds convert to absolute values over the field range', () => { + const field = makeField( + { + min: 0, + max: 200, + thresholds: { + mode: ThresholdsMode.Percentage, + steps: [ + { value: -Infinity, color: 'green' }, + { value: 25, color: 'orange' }, + { value: 75, color: 'red' }, + ], + }, + }, + [0, 100, 200] + ); + const model = buildLegend(field, baseOptions, theme, ctxFor([0, 100, 200])); + // 25% and 75% of 0..200 + expect(model.items[1].from).toBe(50); + expect(model.items[2].from).toBe(150); + }); + + test('with no thresholds configured it falls back to a gradient rather than nothing', () => { + const field = makeField({ thresholds: { mode: ThresholdsMode.Absolute, steps: [] } }); + const model = buildLegend(field, baseOptions, theme, ctxFor()); + expect(model.kind).toBe('gradient'); + expect(model.stops.length).toBeGreaterThan(1); + }); +}); + +describe('gradient legend', () => { + test('samples the panel ramp across the color domain', () => { + const colorScale = (v: number) => (v > 50 ? '#ff0000' : '#0000ff'); + const model = buildLegend( + makeField(absoluteThresholds), + { ...baseOptions, colorMode: 'sequential' }, + theme, + ctxFor([0, 100], { colorScale }) + ); + expect(model.kind).toBe('gradient'); + expect(model.stops.length).toBeGreaterThan(1); + expect(model.stops[0].offset).toBe(0); + expect(model.stops[model.stops.length - 1].offset).toBe(1); + expect(model.stops[0].color).toBe('#0000ff'); + expect(model.stops[model.stops.length - 1].color).toBe('#ff0000'); + }); + + test('ticks label the ends and middle of the domain', () => { + const model = buildLegend( + makeField(), + { ...baseOptions, colorMode: 'sequential' }, + theme, + ctxFor([0, 100], { colorScale: () => '#123456' }) + ); + expect(model.ticks.map((t) => t.value)).toEqual([0, 50, 100]); + expect(model.ticks[0].offset).toBe(0); + expect(model.ticks[2].offset).toBe(1); + }); + + test('diverging adds an emphasized midpoint tick', () => { + const model = buildLegend( + makeField(), + { ...baseOptions, colorMode: 'diverging', divergingMidpoint: 20 }, + theme, + ctxFor([0, 100], { colorScale: () => '#123456' }) + ); + const mid = model.ticks.find((t) => t.emphasis); + expect(mid).toBeDefined(); + expect(mid!.value).toBe(20); + expect(mid!.offset).toBeCloseTo(0.2); + }); + + test('a midpoint sitting at the centre replaces the centre tick instead of stacking on it', () => { + const model = buildLegend( + makeField(), + { ...baseOptions, colorMode: 'diverging', divergingMidpoint: 50 }, + theme, + ctxFor([0, 100], { colorScale: () => '#123456' }) + ); + expect(model.ticks).toHaveLength(3); + expect(model.ticks.filter((t) => t.emphasis)).toHaveLength(1); + }); +}); + +describe('categorical legend', () => { + test('caps high-cardinality fields instead of drawing one entry per value', () => { + const many = Array.from({ length: 200 }, (_, i) => i); + const model = buildLegend(makeField(), { ...baseOptions, legendType: 'categorical' }, theme, ctxFor(many)); + expect(model.items).toHaveLength(CATEGORICAL_CAP); + expect(model.truncated).toBe(200 - CATEGORICAL_CAP); + }); + + test('entries are sorted and stand for their exact value', () => { + const model = buildLegend( + makeField(), + { ...baseOptions, legendType: 'categorical' }, + theme, + ctxFor([30, 10, 20]) + ); + expect(model.items.map((i) => i.from)).toEqual([10, 20, 30]); + model.items.forEach((item) => expect(item.from).toBe(item.to)); + expect(model.truncated).toBe(0); + }); +}); + +describe('null / no-data key', () => { + test('is flagged only when such cells exist', () => { + const off = buildLegend(makeField(absoluteThresholds), baseOptions, theme, ctxFor()); + expect(off.states).toEqual({ hasNull: false, hasNoData: false }); + const on = buildLegend( + makeField(absoluteThresholds), + baseOptions, + theme, + ctxFor([1, 2], { hasNull: true, hasNoData: true }) + ); + expect(on.states).toEqual({ hasNull: true, hasNoData: true }); + }); +}); + +describe('layout', () => { + const thresholdModel = () => buildLegend(makeField(absoluteThresholds), baseOptions, theme, ctxFor()); + + test('a threshold bar that fits stays a bar', () => { + const layout = layoutLegend(thresholdModel(), 600); + expect(layout.form).toBe('bar'); + expect(layout.barW).toBeLessThanOrEqual(600); + expect(layout.stepW).toBeGreaterThan(0); + }); + + test('a threshold bar that will not fit wraps into a list', () => { + const many = { + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [{ value: -Infinity, color: 'green' }].concat( + Array.from({ length: 20 }, (_, i) => ({ value: (i + 1) * 1000, color: 'red' })) + ), + }, + }; + const model = buildLegend(makeField(many), baseOptions, theme, ctxFor()); + const layout = layoutLegend(model, 200); + expect(layout.form).toBe('list'); + expect(layout.rows.length).toBeGreaterThan(1); + }); + + test('nothing is laid out wider than the panel', () => { + const many = Array.from({ length: 60 }, (_, i) => i * 11); + const model = buildLegend(makeField(), { ...baseOptions, legendType: 'categorical' }, theme, ctxFor(many)); + const layout = layoutLegend(model, 400); + layout.rows.forEach((row) => { + const last = row[row.length - 1]; + expect(last.x + last.width).toBeLessThanOrEqual(400); + }); + }); + + test('height grows with entries and shrinks as the panel widens', () => { + const many = Array.from({ length: 20 }, (_, i) => i * 7); + const model = buildLegend(makeField(), { ...baseOptions, legendType: 'categorical' }, theme, ctxFor(many)); + const narrow = estimateLegendHeight(model, 200); + const wide = estimateLegendHeight(model, 1200); + expect(narrow).toBeGreaterThan(wide); + + const few = buildLegend(makeField(), { ...baseOptions, legendType: 'categorical' }, theme, ctxFor([1, 2])); + expect(estimateLegendHeight(few, 400)).toBeLessThan(estimateLegendHeight(model, 400)); + }); + + test('the null / no-data keys ride beside the bar, costing width but no height', () => { + const plain = thresholdModel(); + const keyed = buildLegend( + makeField(absoluteThresholds), + baseOptions, + theme, + ctxFor([1, 2], { hasNull: true, hasNoData: true }) + ); + expect(estimateLegendHeight(keyed, 600)).toBe(estimateLegendHeight(plain, 600)); + + const layout = layoutLegend(keyed, 600); + const plainLayout = layoutLegend(plain, 600); + expect(layout.width).toBeGreaterThan(plainLayout.width); + expect(layout.keys.map((k) => k.kind)).toEqual(['null', 'noData']); + // one row, clear of the bar, and inside the bar's vertical band + layout.keys.forEach((key) => { + expect(key.x).toBeGreaterThan(LEGEND_LAYOUT.padX + layout.barW); + expect(key.y).toBe(layout.keys[0].y); + expect(key.y).toBeGreaterThanOrEqual(layout.bodyY); + expect(key.y + LEGEND_LAYOUT.swatch).toBeLessThanOrEqual(layout.bodyY + LEGEND_LAYOUT.barH); + }); + expect(layout.keys[1].x).toBeGreaterThan(layout.keys[0].x); + }); + + test('the keys drop below the bar when there is no room beside it', () => { + const keyed = buildLegend( + makeField(absoluteThresholds), + baseOptions, + theme, + ctxFor([1, 2], { hasNull: true, hasNoData: true }) + ); + const narrow = layoutLegend(keyed, 150); + expect(narrow.form).toBe('bar'); + narrow.keys.forEach((key) => { + expect(key.y).toBeGreaterThan(narrow.bodyY + LEGEND_LAYOUT.barH); + }); + expect(narrow.height).toBeGreaterThan(estimateLegendHeight(thresholdModel(), 150)); + }); + + test('a list legend puts the keys on the last row when it fits', () => { + const keyed = buildLegend( + makeField(), + { ...baseOptions, legendType: 'categorical' }, + theme, + ctxFor([1, 2], { hasNull: true, hasNoData: true }) + ); + const layout = layoutLegend(keyed, 600); + expect(layout.form).toBe('list'); + const lastRow = layout.rows[layout.rows.length - 1]; + const lastEntry = lastRow[lastRow.length - 1]; + expect(layout.keys[0].y).toBe(layout.bodyY + (layout.rows.length - 1) * LEGEND_LAYOUT.rowStep); + expect(layout.keys[0].x).toBeGreaterThan(LEGEND_LAYOUT.padX + lastEntry.x + lastEntry.width); + const unkeyed = { ...keyed, states: { hasNull: false, hasNoData: false } } as LegendModel; + expect(layout.height).toBe(estimateLegendHeight(unkeyed, 600)); + }); + + test('a truncation note reserves its own row', () => { + const many = Array.from({ length: 200 }, (_, i) => i); + const model = buildLegend(makeField(), { ...baseOptions, legendType: 'categorical' }, theme, ctxFor(many)); + const capped = { ...model, truncated: 0 } as LegendModel; + expect(estimateLegendHeight(model, 600)).toBeGreaterThan(estimateLegendHeight(capped, 600)); + }); + + test('a missing legend reserves nothing', () => { + expect(estimateLegendHeight(null, 600)).toBe(0); + }); +}); diff --git a/src/legend.ts b/src/legend.ts new file mode 100644 index 0000000..92c8751 --- /dev/null +++ b/src/legend.ts @@ -0,0 +1,606 @@ +import { getFieldColorModeForField, getMinMaxAndDelta, sortThresholds, ThresholdsMode } from '@grafana/data'; +import { LegendItem, LegendKind, LegendModel, LegendStop, LegendTick } from './types'; + +/** + * Builds the legend's data model. + * + * The whole point of this module is that the legend describes the *actual* source of + * cell color. When cells are colored from the field's thresholds (Standard Options), + * the legend is built from those threshold steps — their real colors and their real + * boundary values — not from an arbitrary sampling of the data's min/max. Steps that + * no data happens to fall into are still shown; that a band is empty is information. + * + * The model is deliberately free of any drawing concerns so it can be unit tested, + * and so `layoutLegend` can size the legend before anything is rendered. + */ + +/** Distinct categorical entries drawn before we give up and summarize the rest. */ +export const CATEGORICAL_CAP = 24; + +/** Number of color samples taken along a continuous ramp to build the gradient. */ +const GRADIENT_SAMPLES = 12; + +/** Shared geometry, in px. Exported so the renderer and the sizing math cannot drift. */ +export const LEGEND_LAYOUT = { + padX: 4, + padTop: 2, + padBottom: 4, + titleH: 15, + barH: 14, + /** Space under the bar for boundary / tick labels. */ + barLabelH: 17, + /** Vertical pitch of a swatch row (list form, and the null / no-data key). */ + rowStep: 18, + swatch: 12, + swatchGap: 6, + /** Horizontal gap between entries on the same list row. */ + itemGap: 14, + /** Horizontal gap between the color scale and the null / no-data keys beside it. */ + keyGap: 16, + /** Approximate advance width of an 11px sans-serif character. */ + charW: 6.4, + /** Minimum width of one segment in the stepped-bar form. */ + minStepW: 34, + /** Preferred width of a continuous gradient bar. */ + gradientW: 200, +}; + +/** Approximate rendered width of an 11px label. Good enough for layout decisions; + * the renderer never relies on it for anything that would clip. */ +function textW(s: string): number { + return String(s == null ? '' : s).length * LEGEND_LAYOUT.charW; +} + +/** Format a value exactly the way the cells format it, so legend numbers and cell + * tooltips agree on units and decimals. Falls back to String() when a field has no + * display processor attached (i.e. outside a real panel render). */ +function fmt(field: any, v: any): string { + try { + if (field && typeof field.display === 'function') { + const d = field.display(v); + // Grafana's suffix usually already carries its own leading space + const suffix = d.suffix ? String(d.suffix).trim() : ''; + return suffix ? `${d.text} ${suffix}` : d.text; + } + } catch (e) { + // fall through to the raw value + } + return String(v); +} + +/** Label for a half-open band, avoiding ever printing an infinity. */ +function bandLabel(field: any, from: number | null, to: number | null): string { + if (from == null && to == null) { + return 'all values'; + } + if (from == null) { + return `< ${fmt(field, to)}`; + } + if (to == null) { + return `≥ ${fmt(field, from)}`; + } + return `${fmt(field, from)} – ${fmt(field, to)}`; +} + +function safeColorMode(field: any): any { + try { + return getFieldColorModeForField(field) || {}; + } catch (e) { + return {}; + } +} + +/** The min/max percentage thresholds are relative to. Mirrors what `getScaleCalculator` + * uses internally (field.state.range, else field config min/max, else the data), so + * legend boundaries land on the same values the cells were colored by. */ +function fieldRange(field: any, dataMin: number, dataMax: number): { min: number; max: number; delta: number } { + const stateRange = field && field.state ? field.state.range : null; + if (stateRange && isFinite(stateRange.min) && isFinite(stateRange.max)) { + const delta = isFinite(stateRange.delta) ? stateRange.delta : stateRange.max - stateRange.min; + return { min: stateRange.min, max: stateRange.max, delta }; + } + try { + const m: any = getMinMaxAndDelta(field); + if (isFinite(m.min) && isFinite(m.max)) { + return { min: m.min, max: m.max, delta: isFinite(m.delta) ? m.delta : m.max - m.min }; + } + } catch (e) { + // fall through to the data range + } + return { min: dataMin, max: dataMax, delta: dataMax - dataMin }; +} + +function hasColoredValueMappings(field: any): boolean { + const mappings = field && field.config ? field.config.mappings : null; + if (!Array.isArray(mappings) || mappings.length === 0) { + return false; + } + return mappings.some((m: any) => { + const opts = m && m.options; + if (!opts) { + return false; + } + if (opts.result && opts.result.color) { + return true; + } + // value maps store one result per matched value + return Object.keys(opts).some((k) => opts[k] && opts[k].color); + }); +} + +/** Old dashboards saved `range` / `categorical`; map them onto the new vocabulary + * so upgrading a panel never blanks its legend. */ +export function normalizeLegendType(legendType: any): 'auto' | LegendKind { + switch (legendType) { + case 'thresholds': + case 'gradient': + case 'categorical': + return legendType; + case 'range': + // 'range' used to mean "whatever continuous form fits" — that is now Auto's job. + case 'auto': + case undefined: + case null: + case '': + return 'auto'; + default: + return 'auto'; + } +} + +/** Decide which form the legend takes, following whatever actually colors the cells. */ +export function resolveLegendKind(field: any, options: any, isNumeric: boolean): LegendKind { + const requested = normalizeLegendType(options.legendType); + if (requested !== 'auto') { + return requested; + } + if (!isNumeric) { + return 'categorical'; + } + const colorMode = options.colorMode || 'standard'; + if (colorMode === 'sequential' || colorMode === 'diverging') { + // the panel's own ramp colors the cells, so the legend is that ramp + return 'gradient'; + } + const fcm = safeColorMode(field); + if (fcm.isContinuous) { + return 'gradient'; + } + if (fcm.id === 'fixed' || hasColoredValueMappings(field)) { + return 'categorical'; + } + return 'thresholds'; +} + +/** One entry per configured threshold step, in ascending order, whether or not any + * data falls inside it. The first step is unbounded below (that is how + * `getActiveThreshold` treats it) and the last is unbounded above. + * + * `legendMin` floors that first step: with it the band reads "0 – 30" instead of + * "< 30", and its `from` becomes the bound, so hovering it highlights exactly the + * cells the label claims. Leave it undefined for the unbounded reading. */ +export function buildThresholdItems( + field: any, + theme: any, + dataMin: number, + dataMax: number, + legendMin?: number +): LegendItem[] { + const cfg = field && field.config ? field.config.thresholds : null; + const rawSteps = cfg && Array.isArray(cfg.steps) ? cfg.steps : []; + if (rawSteps.length === 0) { + return []; + } + const steps = sortThresholds(rawSteps.slice()); + const isPercent = cfg.mode === ThresholdsMode.Percentage; + const range = isPercent ? fieldRange(field, dataMin, dataMax) : null; + const toAbsolute = (stepValue: number): number => { + if (!isPercent || !range || !isFinite(stepValue)) { + return stepValue; + } + return range.min + (stepValue / 100) * range.delta; + }; + + const bounded = typeof legendMin === 'number' && isFinite(legendMin); + + return steps.map((step: any, i: number) => { + const from = i === 0 ? (bounded ? (legendMin as number) : null) : toAbsolute(step.value); + const to = i === steps.length - 1 ? null : toAbsolute(steps[i + 1].value); + return { + label: bandLabel(field, from, to), + color: theme.visualization.getColorByName(step.color), + from, + to, + fromLabel: from == null ? '' : fmt(field, from), + }; + }); +} + +/** Color stops for a continuous bar, taken from whichever ramp is in play. */ +function buildStops( + field: any, + options: any, + theme: any, + domain: { min: number; max: number } | null, + colorScale: ((v: number) => string) | null, + colorMap: (v: any) => string +): LegendStop[] { + const colorMode = options.colorMode || 'standard'; + const offsets: number[] = []; + for (let i = 0; i < GRADIENT_SAMPLES; i++) { + offsets.push(i / (GRADIENT_SAMPLES - 1)); + } + + if ((colorMode === 'sequential' || colorMode === 'diverging') && colorScale && domain) { + return offsets.map((t) => ({ offset: t, color: colorScale(domain.min + t * (domain.max - domain.min)) })); + } + + // A Grafana continuous-* scheme already publishes its own stops — use them directly + // rather than re-sampling through the display processor. + const fcm = safeColorMode(field); + if (fcm.isContinuous && typeof fcm.getColors === 'function') { + const colors = fcm.getColors(theme) || []; + if (colors.length > 0) { + return colors.map((color: string, i: number) => ({ + offset: colors.length > 1 ? i / (colors.length - 1) : 0, + color, + })); + } + } + + if (domain) { + return offsets.map((t) => ({ offset: t, color: colorMap(domain.min + t * (domain.max - domain.min)) })); + } + return []; +} + +/** Min / midpoint / max labels for a continuous bar, plus the diverging midpoint. */ +function buildTicks(field: any, options: any, domain: { min: number; max: number } | null): LegendTick[] { + if (!domain) { + return []; + } + const span = domain.max - domain.min; + const at = (value: number): LegendTick => ({ + value, + label: fmt(field, value), + offset: span ? (value - domain.min) / span : 0, + }); + if (!span) { + return [at(domain.min)]; + } + const ticks: LegendTick[] = [at(domain.min), at(domain.min + span / 2), at(domain.max)]; + + if ((options.colorMode || 'standard') === 'diverging') { + const mid = typeof options.divergingMidpoint === 'number' ? options.divergingMidpoint : 0; + const offset = (mid - domain.min) / span; + if (offset > 0.02 && offset < 0.98) { + const emphasized = { ...at(mid), emphasis: true }; + // don't stack the midpoint on top of the centre tick + if (Math.abs(offset - 0.5) < 0.08) { + ticks[1] = emphasized; + } else { + ticks.splice(2, 0, emphasized); + } + } + } + return ticks; +} + +/** Distinct values as swatches, sorted, capped so a high-cardinality field cannot + * produce hundreds of entries. */ +function buildCategoricalItems( + field: any, + uniqueValues: any[], + colorMap: (v: any) => string +): { items: LegendItem[]; truncated: number } { + const values = uniqueValues.slice().sort((a, b) => { + if (typeof a === 'number' && typeof b === 'number') { + return a - b; + } + return String(a).localeCompare(String(b)); + }); + const shown = values.slice(0, CATEGORICAL_CAP); + const items = shown.map((v) => ({ + label: fmt(field, v), + color: colorMap(v), + // an exact-value band: `from === to` means "only cells equal to this" + from: typeof v === 'number' ? v : null, + to: typeof v === 'number' ? v : null, + fromLabel: '', + })); + return { items, truncated: values.length - shown.length }; +} + +export type LegendContext = { + dataMin: number; + dataMax: number; + /** Effective color domain — may be the manual range rather than the data's. */ + domain: { min: number; max: number } | null; + /** The panel's own ramp (sequential / diverging modes only). */ + colorScale: ((v: number) => string) | null; + /** The same value -> color function the cells were painted with. */ + colorMap: (v: any) => string; + /** Distinct non-null values present in the value field. */ + uniqueValues: any[]; + hasNull: boolean; + hasNoData: boolean; +}; + +export function buildLegend(field: any, options: any, theme: any, ctx: LegendContext): LegendModel { + const isNumeric = ctx.uniqueValues.some((v) => typeof v === 'number' && !isNaN(v)) || ctx.uniqueValues.length === 0; + let kind = resolveLegendKind(field, options, isNumeric); + + const model: LegendModel = { + kind, + title: options.valueText || (field && field.name) || '', + items: [], + stops: [], + domain: ctx.domain, + ticks: [], + states: { hasNull: ctx.hasNull, hasNoData: ctx.hasNoData }, + truncated: 0, + }; + + if (kind === 'thresholds') { + model.items = buildThresholdItems(field, theme, ctx.dataMin, ctx.dataMax, options.legendMin); + if (model.items.length === 0) { + // No thresholds configured at all — a gradient still describes the cells honestly. + kind = 'gradient'; + model.kind = kind; + } + } + + if (kind === 'gradient') { + model.stops = buildStops(field, options, theme, ctx.domain, ctx.colorScale, ctx.colorMap); + model.ticks = buildTicks(field, options, ctx.domain); + if (model.stops.length === 0) { + // nothing numeric to ramp over; fall back to naming the values we do have + kind = 'categorical'; + model.kind = kind; + } + } + + if (kind === 'categorical') { + const { items, truncated } = buildCategoricalItems(field, ctx.uniqueValues, ctx.colorMap); + model.items = items; + model.truncated = truncated; + } + + return model; +} + +/* ------------------------------------------------------------------ * + * Layout + * + * Computed from the model alone so the panel can reserve exactly the height the + * legend needs *before* drawing it — the old code reserved a flat 64px whatever + * the legend turned out to be. + * ------------------------------------------------------------------ */ + +export type LegendRowEntry = { item: LegendItem; x: number; width: number }; + +/** A placed null / no-data swatch. The keys ride beside the color scale when there is + * room for them, so they cost no vertical space at all. */ +export type LegendKeyEntry = { + kind: 'null' | 'noData'; + label: string; + x: number; + y: number; + width: number; +}; + +export type LegendLayout = { + /** `bar` = one contiguous stepped / gradient bar. `list` = wrapped swatch rows. */ + form: 'bar' | 'list'; + width: number; + height: number; + /** Bar form: total bar width, and the width of one step (thresholds only). */ + barW: number; + stepW: number; + /** List form: entries already assigned to wrapped rows. */ + rows: LegendRowEntry[][]; + /** y of the bar / first swatch row. */ + bodyY: number; + /** Placed null / no-data keys, in draw order. Empty when neither state is present. */ + keys: LegendKeyEntry[]; +}; + +function swatchEntryWidth(label: string): number { + const L = LEGEND_LAYOUT; + return L.swatch + L.swatchGap + textW(label); +} + +function entryWidth(item: LegendItem): number { + return swatchEntryWidth(item.label); +} + +/** The state keys a model needs, unplaced. Order matters: null then "no data". */ +function stateKeys(model: LegendModel): Array<{ kind: 'null' | 'noData'; label: string; width: number }> { + const keys: Array<{ kind: 'null' | 'noData'; label: string; width: number }> = []; + if (model.states.hasNull) { + keys.push({ kind: 'null', label: 'null', width: swatchEntryWidth('null') }); + } + if (model.states.hasNoData) { + keys.push({ kind: 'noData', label: 'no data', width: swatchEntryWidth('no data') }); + } + return keys; +} + +/** Lay the keys out left-to-right on one row starting at `x`. */ +function placeKeys( + keys: Array<{ kind: 'null' | 'noData'; label: string; width: number }>, + x: number, + y: number +): LegendKeyEntry[] { + const L = LEGEND_LAYOUT; + let cursor = x; + return keys.map((key) => { + const placed = { ...key, x: cursor, y }; + cursor += key.width + L.itemGap; + return placed; + }); +} + +/** Total width of a one-row key block, gaps included. */ +function keysWidth(keys: Array<{ width: number }>): number { + const L = LEGEND_LAYOUT; + if (keys.length === 0) { + return 0; + } + return keys.reduce((sum, key) => sum + key.width, 0) + (keys.length - 1) * L.itemGap; +} + +/** Greedily pack entries into rows no wider than `avail`. Always emits at least one + * entry per row so a single over-long label cannot produce an empty row. */ +function packRows(items: LegendItem[], avail: number): LegendRowEntry[][] { + const L = LEGEND_LAYOUT; + const rows: LegendRowEntry[][] = []; + let row: LegendRowEntry[] = []; + let x = 0; + items.forEach((item) => { + const w = entryWidth(item); + if (row.length > 0 && x + w > avail) { + rows.push(row); + row = []; + x = 0; + } + row.push({ item, x, width: w }); + x += w + L.itemGap; + }); + if (row.length > 0) { + rows.push(row); + } + return rows; +} + +export function layoutLegend(model: LegendModel | null, availableWidth: number): LegendLayout { + const L = LEGEND_LAYOUT; + const empty: LegendLayout = { + form: 'list', + width: 0, + height: 0, + barW: 0, + stepW: 0, + rows: [], + bodyY: 0, + keys: [], + }; + if (!model) { + return empty; + } + + const avail = Math.max(120, (availableWidth || 0) - L.padX * 2); + const bodyY = L.padTop + L.titleH; + const keys = stateKeys(model); + const keysW = keysWidth(keys); + + /** Both bar forms place their keys the same way: beside the bar when they fit, + * on their own row underneath when they don't. */ + const barKeys = (barW: number, scaleRight: number) => { + // the bar's own labels already reserve barLabelH below it + const scaleBottom = bodyY + L.barH + L.barLabelH; + if (keys.length === 0) { + return { keys: [] as LegendKeyEntry[], right: scaleRight, bottom: scaleBottom }; + } + const besideX = L.padX + barW + L.keyGap; + if (besideX + keysW <= avail) { + // centred on the bar, so the keys cost no height at all + return { + keys: placeKeys(keys, besideX, bodyY + (L.barH - L.swatch) / 2), + right: Math.max(scaleRight, besideX + keysW), + bottom: scaleBottom, + }; + } + return { + keys: placeKeys(keys, L.padX, scaleBottom), + right: Math.max(scaleRight, L.padX + keysW), + bottom: scaleBottom + L.rowStep, + }; + }; + + // --- continuous bar ------------------------------------------------- + if (model.kind === 'gradient') { + const barW = Math.min(L.gradientW, avail); + const widestTick = model.ticks.reduce((m, t) => Math.max(m, textW(t.label)), 0); + const placed = barKeys(barW, Math.max(L.padX + barW + widestTick / 2, L.padX + textW(model.title))); + return { + form: 'bar', + width: placed.right + L.padX, + height: placed.bottom + L.padBottom, + barW, + stepW: 0, + rows: [], + bodyY, + keys: placed.keys, + }; + } + + // --- stepped threshold bar, when the boundary labels can fit --------- + if (model.kind === 'thresholds' && model.items.length > 1) { + // one label sits under each internal division + const widestBoundary = model.items.slice(1).reduce((m, item) => Math.max(m, textW(item.fromLabel)), 0); + // a bounded first step also prints its value at the bar's left edge; keep that + // start-anchored label clear of the centred label at the first division + const firstW = textW(model.items[0].fromLabel); + const stepW = Math.max(L.minStepW, widestBoundary + 8, firstW + widestBoundary / 2 + 6); + const barW = stepW * model.items.length; + if (barW <= avail) { + const placed = barKeys(barW, Math.max(L.padX + barW, L.padX + textW(model.title))); + return { + form: 'bar', + width: placed.right + L.padX, + height: placed.bottom + L.padBottom, + barW, + stepW, + rows: [], + bodyY, + keys: placed.keys, + }; + } + } + + // --- wrapped swatch list (categorical, or thresholds that won't fit) -- + const rows = packRows(model.items, avail); + const noteRows = model.truncated > 0 ? 1 : 0; + const listBottom = bodyY + (rows.length + noteRows) * L.rowStep; + const widestRow = rows.reduce((m, row) => { + const last = row[row.length - 1]; + return last ? Math.max(m, last.x + last.width) : m; + }, 0); + + // tack the keys onto the end of the last row when it has room — but never onto a + // row a truncation note is about to follow + const lastRow = rows[rows.length - 1]; + const lastRight = lastRow ? lastRow[lastRow.length - 1].x + lastRow[lastRow.length - 1].width : 0; + const canShareRow = keys.length > 0 && noteRows === 0 && lastRow && lastRight + L.itemGap + keysW <= avail; + let placedKeys: LegendKeyEntry[] = []; + let keysRight = 0; + let bottom = listBottom; + if (keys.length > 0) { + if (canShareRow) { + const x = L.padX + lastRight + L.itemGap; + placedKeys = placeKeys(keys, x, bodyY + (rows.length - 1) * L.rowStep); + keysRight = x + keysW; + } else { + placedKeys = placeKeys(keys, L.padX, listBottom); + keysRight = L.padX + keysW; + bottom = listBottom + L.rowStep; + } + } + + return { + form: 'list', + width: Math.max(L.padX + widestRow, L.padX + textW(model.title), keysRight) + L.padX, + height: bottom + L.padBottom, + barW: 0, + stepW: 0, + rows, + bodyY, + keys: placedKeys, + }; +} + +/** Total vertical space the legend needs, so the grid can be sized around it. */ +export function estimateLegendHeight(model: LegendModel | null, availableWidth: number): number { + return layoutLegend(model, availableWidth).height; +} diff --git a/src/legendRender.js b/src/legendRender.js new file mode 100644 index 0000000..b4720a0 --- /dev/null +++ b/src/legendRender.js @@ -0,0 +1,321 @@ +import { select } from 'd3-selection'; +import { LEGEND_LAYOUT, layoutLegend } from './legend'; + +/** + * Draws the legend model produced by `./legend.ts`. + * + * Every form here describes the real source of cell color, and every entry knows the + * value band it stands for, so hovering it highlights exactly the cells it covers + * rather than an approximate tolerance window. + * + * All geometry comes from `layoutLegend`, the same function the panel uses to reserve + * vertical space before the grid is sized — the legend can never claim more room than + * it was given. + */ + +const FONT_SIZE = '11px'; + +/** Draw the "null" / "no data" keys wherever `layoutLegend` placed them — beside the + * color scale when it left room, otherwise on their own row beneath it. */ +function drawStateKeys(svg, keys, opts, theme, handlers) { + const L = LEGEND_LAYOUT; + keys.forEach((key) => { + const isNull = key.kind === 'null'; + const fill = isNull ? opts.nullColor : opts.distinctNoData ? 'none' : opts.defaultColor; + const stroke = isNull ? 'none' : opts.distinctNoData ? opts.noDataStroke : 'none'; + svg + .append('rect') + .attr('x', key.x) + .attr('y', key.y) + .attr('width', L.swatch) + .attr('height', L.swatch) + .attr('rx', Math.min(opts.cornerRadius, 3)) + .attr('fill', fill) + .attr('stroke', stroke) + .attr('stroke-width', stroke === 'none' ? 0 : 1) + .style('cursor', 'pointer') + .on('mouseover', function () { + handlers.highlightByState(isNull); + }) + .on('mouseout', function () { + handlers.clearHighlight(); + }) + .append('title') + .text(key.label); + svg + .append('text') + .attr('x', key.x + L.swatch + L.swatchGap) + .attr('y', key.y + L.swatch - 2) + .attr('font-size', FONT_SIZE) + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.secondary) + .text(key.label); + }); +} + +/** Continuous ramp: a smooth bar with a real tick axis. */ +function drawGradient(svg, model, layout, id, theme, handlers) { + const L = LEGEND_LAYOUT; + const barW = layout.barW; + const y = layout.bodyY; + + const gradId = `matrix-grad-${id}`; + const grad = svg + .append('defs') + .append('linearGradient') + .attr('id', gradId) + .attr('x1', '0%') + .attr('x2', '100%') + .attr('y1', '0%') + .attr('y2', '0%'); + model.stops.forEach((s) => { + grad + .append('stop') + .attr('offset', `${s.offset * 100}%`) + .attr('stop-color', s.color); + }); + + const domain = model.domain; + const span = domain ? domain.max - domain.min : 0; + svg + .append('rect') + .attr('x', L.padX) + .attr('y', y) + .attr('width', barW) + .attr('height', L.barH) + .attr('rx', 2) + .attr('fill', `url(#${gradId})`) + .attr('stroke', theme.colors.border.weak) + .attr('stroke-width', 1) + .style('cursor', domain ? 'crosshair' : null) + .on('mousemove', function (event) { + if (!domain) { + return; + } + const mx = Math.max(0, Math.min(barW, event.offsetX - L.padX)); + const at = domain.min + (mx / barW) * span; + // a narrow window around the cursor, so sweeping the bar sweeps the matrix + const band = (span || 1) * 0.03; + handlers.highlightByRange(at - band, at + band); + }) + .on('mouseout', function () { + handlers.clearHighlight(); + }); + + const labelY = y + L.barH + 13; + model.ticks.forEach((tick) => { + const tx = L.padX + tick.offset * barW; + if (tick.emphasis) { + // the diverging midpoint reads as a line through the ramp + svg + .append('line') + .attr('x1', tx) + .attr('x2', tx) + .attr('y1', y - 2) + .attr('y2', y + L.barH + 2) + .attr('stroke', theme.colors.text.primary) + .attr('stroke-width', 1); + } else { + svg + .append('line') + .attr('x1', tx) + .attr('x2', tx) + .attr('y1', y + L.barH) + .attr('y2', y + L.barH + 3) + .attr('stroke', theme.colors.border.weak) + .attr('stroke-width', 1); + } + svg + .append('text') + .attr('x', tx) + .attr('y', labelY) + .attr('text-anchor', tick.offset <= 0.01 ? 'start' : tick.offset >= 0.99 ? 'end' : 'middle') + .attr('font-size', FONT_SIZE) + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.primary) + .text(tick.label); + }); +} + +/** Thresholds: contiguous equal-width steps with the configured boundary values + * printed under each division. Step widths are equal because the outer bands are + * unbounded — there is no honest proportional width for "≥ 80". */ +function drawSteppedBar(svg, model, layout, opts, theme, handlers) { + const L = LEGEND_LAYOUT; + const y = layout.bodyY; + const stepW = layout.stepW; + + model.items.forEach((item, i) => { + svg + .append('rect') + .attr('x', L.padX + i * stepW) + .attr('y', y) + .attr('width', stepW) + .attr('height', L.barH) + .attr('fill', item.color) + .style('cursor', 'pointer') + .on('mouseover', function () { + handlers.highlightByRange(item.from, item.to); + }) + .on('mouseout', function () { + handlers.clearHighlight(); + }) + .append('title') + .text(item.label); + }); + + // single outline over the whole bar, so the steps read as one scale + svg + .append('rect') + .attr('x', L.padX) + .attr('y', y) + .attr('width', layout.barW) + .attr('height', L.barH) + .attr('rx', 2) + .attr('fill', 'none') + .attr('stroke', theme.colors.border.weak) + .attr('stroke-width', 1) + .style('pointer-events', 'none'); + + // boundary values sit at the divisions between steps, not under their centres + const labelY = y + L.barH + 13; + + // a bounded first step prints its floor at the bar's left edge, so the scale reads + // "0 30 70" rather than starting at nothing + if (model.items[0] && model.items[0].fromLabel) { + svg + .append('line') + .attr('x1', L.padX) + .attr('x2', L.padX) + .attr('y1', y + L.barH) + .attr('y2', y + L.barH + 3) + .attr('stroke', theme.colors.border.weak) + .attr('stroke-width', 1); + svg + .append('text') + .attr('x', L.padX) + .attr('y', labelY) + .attr('text-anchor', 'start') + .attr('font-size', FONT_SIZE) + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.primary) + .text(model.items[0].fromLabel); + } + + model.items.slice(1).forEach((item, i) => { + const tx = L.padX + (i + 1) * stepW; + svg + .append('line') + .attr('x1', tx) + .attr('x2', tx) + .attr('y1', y + L.barH) + .attr('y2', y + L.barH + 3) + .attr('stroke', theme.colors.border.weak) + .attr('stroke-width', 1); + svg + .append('text') + .attr('x', tx) + .attr('y', labelY) + .attr('text-anchor', 'middle') + .attr('font-size', FONT_SIZE) + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.primary) + .text(item.fromLabel); + }); +} + +/** Wrapped swatch + label rows. Used for categorical values, and for threshold + * steps whose labels are too wide to fit one bar across the panel. */ +function drawList(svg, model, layout, opts, theme, handlers) { + const L = LEGEND_LAYOUT; + layout.rows.forEach((row, ri) => { + const y = layout.bodyY + ri * L.rowStep; + row.forEach((entry) => { + svg + .append('rect') + .attr('x', L.padX + entry.x) + .attr('y', y) + .attr('width', L.swatch) + .attr('height', L.swatch) + .attr('rx', Math.min(opts.cornerRadius, 3)) + .attr('fill', entry.item.color) + .style('cursor', 'pointer') + .on('mouseover', function () { + handlers.highlightByRange(entry.item.from, entry.item.to); + }) + .on('mouseout', function () { + handlers.clearHighlight(); + }) + .append('title') + .text(entry.item.label); + svg + .append('text') + .attr('x', L.padX + entry.x + L.swatch + L.swatchGap) + .attr('y', y + L.swatch - 2) + .attr('font-size', FONT_SIZE) + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.primary) + .text(entry.item.label); + }); + }); + + if (model.truncated > 0) { + svg + .append('text') + .attr('x', L.padX) + .attr('y', layout.bodyY + layout.rows.length * L.rowStep + L.swatch - 2) + .attr('font-size', FONT_SIZE) + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.secondary) + .text(`+${model.truncated} more`); + } +} + +/** + * @param {HTMLElement} elem Container the matrix was drawn into + * @param {*} model The legend model from buildLegend() + * @param {*} id The panel id + * @param {*} opts { availableWidth, cornerRadius, nullColor, defaultColor, distinctNoData, noDataStroke } + * @param {*} theme Grafana theme + * @param {*} handlers { highlightByRange, highlightByState, clearHighlight } + */ +function renderLegend(elem, model, id, opts, theme, handlers) { + if (!model) { + return; + } + const L = LEGEND_LAYOUT; + const layout = layoutLegend(model, opts.availableWidth); + + const svg = select(elem) + .append('div') + .attr('class', `matrix-legend-${id}`) + .append('svg') + .attr('id', `legend-${id}`) + .attr('width', layout.width) + .attr('height', layout.height) + .attr('role', 'img') + .attr('aria-label', `${model.title} legend`); + + if (model.title) { + svg + .append('text') + .attr('x', L.padX) + .attr('y', L.padTop + 11) + .attr('font-size', FONT_SIZE) + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.secondary) + .text(model.title); + } + + if (model.kind === 'gradient') { + drawGradient(svg, model, layout, id, theme, handlers); + } else if (layout.form === 'bar') { + drawSteppedBar(svg, model, layout, opts, theme, handlers); + } else { + drawList(svg, model, layout, opts, theme, handlers); + } + + drawStateKeys(svg, layout.keys, opts, theme, handlers); +} + +export { renderLegend }; diff --git a/src/legendRender.test.js b/src/legendRender.test.js new file mode 100644 index 0000000..9470472 --- /dev/null +++ b/src/legendRender.test.js @@ -0,0 +1,300 @@ +import { createTheme, FieldType, getDisplayProcessor, ThresholdsMode, toDataFrame } from '@grafana/data'; +import { buildLegend, layoutLegend } from './legend'; +import { renderLegend } from './legendRender.js'; + +const theme = createTheme(); + +function makeField(config = {}, values = [0, 5, 10, 20, 50, 100]) { + const frame = toDataFrame({ fields: [{ name: 'gbps', type: FieldType.number, values, config }] }); + const field = frame.fields[0]; + field.display = getDisplayProcessor({ field, theme }); + return field; +} + +const steps = (...pairs) => pairs.map(([value, color]) => ({ value, color })); + +const options = { showLegend: true, legendType: 'auto', colorMode: 'standard', valueText: 'Gbps' }; + +const renderOpts = { + availableWidth: 600, + cornerRadius: 2, + nullColor: '#e6e6e6', + defaultColor: '#dddddd', + distinctNoData: true, + noDataStroke: '#cccccc', +}; + +function ctx(values = [0, 5, 10, 20, 50, 100], extra = {}) { + const min = Math.min(...values); + const max = Math.max(...values); + return { + dataMin: min, + dataMax: max, + domain: { min, max }, + colorScale: null, + colorMap: () => '#336699', + uniqueValues: values, + hasNull: false, + hasNoData: false, + ...extra, + }; +} + +/** Render into a detached container and hand back the container + legend svg. */ +function draw(model, width = 600, handlers = {}) { + const el = document.createElement('div'); + document.body.appendChild(el); + const calls = { range: [], state: [], clear: 0 }; + renderLegend( + el, + model, + 'test', + { ...renderOpts, availableWidth: width }, + theme, + { + highlightByRange: (from, to) => calls.range.push([from, to]), + highlightByState: (wantNull) => calls.state.push(wantNull), + clearHighlight: () => (calls.clear += 1), + ...handlers, + } + ); + return { el, svg: el.querySelector('svg'), calls }; +} + +const texts = (el) => [...el.querySelectorAll('text')].map((t) => t.textContent); +const titles = (el) => [...el.querySelectorAll('title')].map((t) => t.textContent); + +const thresholdModel = (config, values, opts = {}) => + buildLegend(makeField(config), { ...options, ...opts }, theme, ctx(values)); + +describe('threshold legend rendering', () => { + const three = { + thresholds: { + mode: ThresholdsMode.Absolute, + steps: steps([-Infinity, 'green'], [20, 'orange'], [50, 'red']), + }, + }; + + test('draws one segment per step and prints the boundaries between them', () => { + const { el } = draw(thresholdModel(three)); + // 3 step rects + 1 outline + expect(el.querySelectorAll('rect')).toHaveLength(4); + expect(texts(el)).toEqual(['Gbps', '20', '50']); + expect(titles(el)).toEqual(['< 20', '20 – 50', '≥ 50']); + }); + + test('a legend min prints the floor at the left edge and bounds the first band', () => { + const model = thresholdModel(three, undefined, { legendMin: 0 }); + const { el, calls } = draw(model); + expect(texts(el)).toEqual(['Gbps', '0', '20', '50']); + expect(titles(el)).toEqual(['0 – 20', '20 – 50', '≥ 50']); + + const floorLabel = [...el.querySelectorAll('text')].find((t) => t.textContent === '0'); + expect(+floorLabel.getAttribute('x')).toBe(4); + expect(floorLabel.getAttribute('text-anchor')).toBe('start'); + + // hovering the first step now highlights the bounded range, not everything below 20 + el.querySelectorAll('rect')[0].dispatchEvent(new MouseEvent('mouseover')); + expect(calls.range).toEqual([[0, 20]]); + }); + + test('segments are contiguous and stay inside the declared svg width', () => { + const model = thresholdModel(three); + const { el, svg } = draw(model); + const layout = layoutLegend(model, 600); + const rects = [...el.querySelectorAll('rect')].slice(0, 3); + rects.forEach((r, i) => { + expect(+r.getAttribute('x')).toBeCloseTo(4 + i * layout.stepW); + expect(+r.getAttribute('width')).toBeCloseTo(layout.stepW); + }); + const right = +rects[2].getAttribute('x') + +rects[2].getAttribute('width'); + expect(right).toBeLessThanOrEqual(+svg.getAttribute('width')); + }); + + test('hovering a step highlights exactly that band', () => { + const { el, calls } = draw(thresholdModel(three)); + const rects = [...el.querySelectorAll('rect')]; + rects[0].dispatchEvent(new MouseEvent('mouseover')); + rects[1].dispatchEvent(new MouseEvent('mouseover')); + rects[2].dispatchEvent(new MouseEvent('mouseover')); + expect(calls.range).toEqual([ + [null, 20], + [20, 50], + [50, null], + ]); + rects[0].dispatchEvent(new MouseEvent('mouseout')); + expect(calls.clear).toBe(1); + }); + + test('too many steps for the width fall back to a wrapped list', () => { + const many = { + thresholds: { + mode: ThresholdsMode.Absolute, + steps: steps([-Infinity, 'green']).concat( + Array.from({ length: 9 }, (_, i) => ({ value: (i + 1) * 1000, color: 'red' })) + ), + }, + }; + const model = thresholdModel(many); + const { el, svg } = draw(model, 200); + expect(layoutLegend(model, 200).form).toBe('list'); + // every entry still gets a swatch and a readable band label + expect(el.querySelectorAll('rect')).toHaveLength(10); + expect(titles(el)).toHaveLength(10); + // and the drawing stays within the svg it declared + [...el.querySelectorAll('rect')].forEach((r) => { + expect(+r.getAttribute('x') + +r.getAttribute('width')).toBeLessThanOrEqual(+svg.getAttribute('width')); + }); + }); +}); + +describe('gradient legend rendering', () => { + test('emits a gradient fill with stops and a tick axis', () => { + const model = buildLegend(makeField(), { ...options, colorMode: 'sequential' }, theme, { + ...ctx([0, 100]), + colorScale: (v) => (v > 50 ? '#ff0000' : '#0000ff'), + }); + const { el } = draw(model); + expect(el.querySelectorAll('linearGradient')).toHaveLength(1); + expect(el.querySelectorAll('stop').length).toBeGreaterThan(1); + const bar = el.querySelector('rect'); + expect(bar.getAttribute('fill')).toMatch(/^url\(#matrix-grad-/); + expect(texts(el)).toEqual(['Gbps', '0', '50', '100']); + }); + + test('sweeping the bar highlights a window around the cursor value', () => { + const model = buildLegend(makeField(), { ...options, colorMode: 'sequential' }, theme, { + ...ctx([0, 100]), + colorScale: () => '#123456', + }); + const { el, calls } = draw(model); + const bar = el.querySelector('rect'); + const ev = new MouseEvent('mousemove'); + Object.defineProperty(ev, 'offsetX', { get: () => 104 }); // padX 4 + half of a 200px bar + bar.dispatchEvent(ev); + expect(calls.range).toHaveLength(1); + const [from, to] = calls.range[0]; + expect((from + to) / 2).toBeCloseTo(50, 5); + expect(to - from).toBeGreaterThan(0); + }); + + test('the diverging midpoint is drawn as a line through the bar', () => { + const model = buildLegend( + makeField(), + { ...options, colorMode: 'diverging', divergingMidpoint: 30 }, + theme, + { ...ctx([0, 100]), colorScale: () => '#123456' } + ); + const { el } = draw(model); + expect(texts(el)).toContain('30'); + // the emphasized tick spans the bar height; plain ticks are 3px stubs + const spans = [...el.querySelectorAll('line')].map((l) => +l.getAttribute('y2') - +l.getAttribute('y1')); + expect(Math.max(...spans)).toBeGreaterThan(10); + }); +}); + +describe('categorical legend rendering', () => { + test('wraps into rows instead of running off the panel', () => { + const values = Array.from({ length: 40 }, (_, i) => i * 3); + const model = buildLegend(makeField(), { ...options, legendType: 'categorical' }, theme, ctx(values)); + const { el, svg } = draw(model, 300); + const layout = layoutLegend(model, 300); + expect(layout.rows.length).toBeGreaterThan(1); + const width = +svg.getAttribute('width'); + [...el.querySelectorAll('rect')].forEach((r) => { + expect(+r.getAttribute('x') + +r.getAttribute('width')).toBeLessThanOrEqual(width); + }); + // distinct y positions prove it actually wrapped + expect(new Set([...el.querySelectorAll('rect')].map((r) => r.getAttribute('y'))).size).toBe(layout.rows.length); + }); + + test('summarizes the entries past the cap rather than drawing them', () => { + const values = Array.from({ length: 200 }, (_, i) => i); + const model = buildLegend(makeField(), { ...options, legendType: 'categorical' }, theme, ctx(values)); + const { el } = draw(model, 600); + expect(el.querySelectorAll('rect')).toHaveLength(24); + expect(texts(el)).toContain('+176 more'); + }); +}); + +describe('null / no-data key', () => { + test('is drawn only for the states present, and highlights them', () => { + const model = buildLegend( + makeField({ thresholds: { mode: ThresholdsMode.Absolute, steps: steps([-Infinity, 'green'], [50, 'red']) } }), + options, + theme, + ctx([1, 2], { hasNull: true, hasNoData: true }) + ); + const { el, calls } = draw(model); + expect(texts(el)).toEqual(expect.arrayContaining(['null', 'no data'])); + const keyRects = [...el.querySelectorAll('rect')].slice(-2); + keyRects[0].dispatchEvent(new MouseEvent('mouseover')); + keyRects[1].dispatchEvent(new MouseEvent('mouseover')); + expect(calls.state).toEqual([true, false]); + }); + + test('sits beside the bar on one row rather than stacked underneath it', () => { + const model = buildLegend( + makeField({ thresholds: { mode: ThresholdsMode.Absolute, steps: steps([-Infinity, 'green'], [50, 'red']) } }), + options, + theme, + ctx([1, 2], { hasNull: true, hasNoData: true }) + ); + const layout = layoutLegend(model, 600); + const { el } = draw(model, 600); + const keyRects = [...el.querySelectorAll('rect')].slice(-2); + const y = keyRects.map((r) => +r.getAttribute('y')); + const x = keyRects.map((r) => +r.getAttribute('x')); + + // same row, left to right, clear of the bar + expect(y[0]).toBe(y[1]); + expect(x[1]).toBeGreaterThan(x[0]); + expect(x[0]).toBeGreaterThan(layout.barW); + // inside the bar's vertical band, so they add no height + expect(y[0]).toBeGreaterThanOrEqual(layout.bodyY); + expect(y[0] + 12).toBeLessThanOrEqual(layout.bodyY + 14); + }); + + test('is absent when no such cells exist', () => { + const model = buildLegend( + makeField({ thresholds: { mode: ThresholdsMode.Absolute, steps: steps([-Infinity, 'green'], [50, 'red']) } }), + options, + theme, + ctx() + ); + const { el } = draw(model); + expect(texts(el)).not.toContain('null'); + expect(texts(el)).not.toContain('no data'); + }); +}); + +describe('the drawn legend never exceeds the box it declared', () => { + test.each([ + ['thresholds', { thresholds: { mode: ThresholdsMode.Absolute, steps: steps([-Infinity, 'g'], [20, 'o'], [50, 'r']) } }, 'auto', 600], + ['narrow thresholds', { thresholds: { mode: ThresholdsMode.Absolute, steps: steps([-Infinity, 'g'], [20, 'o'], [50, 'r']) } }, 'auto', 150], + ['categorical', {}, 'categorical', 300], + ['gradient', {}, 'gradient', 600], + ])('%s', (_name, config, legendType, width) => { + const model = buildLegend(makeField(config), { ...options, legendType }, theme, ctx([0, 5, 10, 20, 50, 100], { hasNull: true, hasNoData: true })); + const { el, svg } = draw(model, width); + const nodes = [...el.querySelectorAll('rect, text, line')]; + + const declaredH = +svg.getAttribute('height'); + const lowest = nodes.reduce((m, node) => { + const y = +(node.getAttribute('y') || node.getAttribute('y2') || 0); + const h = +(node.getAttribute('height') || 0); + return Math.max(m, y + h); + }, 0); + expect(lowest).toBeLessThanOrEqual(declaredH); + + // text advance is unmeasurable in jsdom, so this covers the boxes and rules — + // enough to catch a key block placed off the right edge + const declaredW = +svg.getAttribute('width'); + const rightmost = nodes.reduce((m, node) => { + const x = +(node.getAttribute('x') || node.getAttribute('x2') || 0); + const w = +(node.getAttribute('width') || 0); + return Math.max(m, x + w); + }, 0); + expect(rightmost).toBeLessThanOrEqual(declaredW); + }); +}); diff --git a/src/matrix.d.ts b/src/matrix.d.ts index 7ef8bb1..bcdec50 100644 --- a/src/matrix.d.ts +++ b/src/matrix.d.ts @@ -1,12 +1,17 @@ -import { MatrixData, MatrixOptions } from './types'; - export function matrix( - rowNames: any[], - colNames: any[], - matrix: DataMatrixCell[][], + rowNames: any, + colNames: any, + matrix: any, id: number, - options: MatrixOptions, - legend: LegendData[], - colCategories: Category[], - rowCategories: Category[], + width: number, + height: number, + options: any, + legend: any, + colMetadata: any, + colCategories: any, + rowMetadata: any, + rowCategories: any, + rowTotals: any, + colTotals: any, + valueDomain: any ): LegacyRef | undefined; diff --git a/src/matrix.js b/src/matrix.js index 68a415a..e19e0a5 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1,52 +1,308 @@ import { useD3 } from './useD3.js'; -import * as d3 from './d3.min.js'; +import { estimateLegendHeight } from './legend'; +import { renderLegend } from './legendRender.js'; +import { select, local } from 'd3-selection'; +import 'd3-transition'; // side-effect import: adds selection.transition() import { css } from '@emotion/css'; import { GrafanaTheme2, textUtil } from '@grafana/data'; import { useStyles2, useTheme2 } from '@grafana/ui'; -import { Heading } from './types'; +// 2.1.1 dropped the sanitize-html dependency in favor of Grafana's built-in +// sanitizer; keep the same local `sanitizeHtml` name so the rest of the file is unchanged. const sanitizeHtml = textUtil.sanitize; +/** Pick a readable text color (dark ink or white) for a given fill color. */ +function contrastText(fill) { + try { + let s = String(fill).trim(); + if (s.startsWith('#')) { + s = s.replace('#', ''); + if (s.length === 3) { + s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]; + } + const r = parseInt(s.slice(0, 2), 16); + const g = parseInt(s.slice(2, 4), 16); + const b = parseInt(s.slice(4, 6), 16); + // relative luminance (sRGB approximation) + const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return lum > 0.6 ? '#0b0b0b' : '#ffffff'; + } + if (s.startsWith('rgb')) { + const nums = s.match(/[\d.]+/g); + if (nums && nums.length >= 3) { + const lum = (0.299 * +nums[0] + 0.587 * +nums[1] + 0.114 * +nums[2]) / 255; + return lum > 0.6 ? '#0b0b0b' : '#ffffff'; + } + } + } catch (e) { + // fall through + } + return '#ffffff'; +} + /** Create the matrix diagram using d3. - * @param {SvgInHtml} elem The parent svg element that will house this diagram - * @param {number} id The panel id - * @param {any[]} rowNames Row names - * @param {any[]} colNames Column names - * @param {DataMatrixCell[][]} matrix The data that will populate the diagram - * @param {MatrixOptions} options Panel configuration - * @param {GrafanaTheme} theme Grafana theme - * @param {LegendData[]} legend Legend data - * @param {CSSReturnValue} styles CSS styles - * @param {Category[]} column categories - * @param {Category[]} row categories + * @param {*} elem The parent svg element that will house this diagram + * @param {*} id The panel id + * @param {number} panelWidth The current width of the panel + * @param {number} panelHeight The current height of the panel + * @param {*} matrix The data that will populate the diagram + * @param {GrafanaTheme} theme + * @param {CSSReturnValue} styles */ -function createViz(elem, id, rowNames, colNames, matrix, options, theme, legend, styles, colCategories, rowCategories) { +function createViz( + elem, + id, + panelWidth, + panelHeight, + rowNames, + colNames, + matrix, + options, + theme, + legend, + styles, + colMetadata, + colCategories, + rowMetadata, + rowCategories, + rowTotals, + colTotals, + valueDomain +) { const srcText = sanitizeHtml(options.sourceText), targetText = sanitizeHtml(options.targetText), valText = sanitizeHtml(options.valueText), - cellSize = options.cellSize, cellPadding = options.cellPadding / 100, // convert the cellPadding integer to a float that can be used by d3 + cornerRadius = options.cellCornerRadius != null ? options.cellCornerRadius : 2, txtLength = options.txtLength, txtSize = options.txtSize / 10, //convert this val to EM scaling 90 = .9em 100 = 1em ... etc linkURL = options.url, urlVar1 = options.urlVar1, urlVar2 = options.urlVar2, - defaultColor = theme.visualization.getColorByName(options.defaultColor); + nullColor = theme.visualization.getColorByName(options.nullColor), + defaultColor = theme.visualization.getColorByName(options.defaultColor), + distinctNoData = options.distinctNoData !== false, + highlightRowCol = options.highlightRowCol !== false, + showCellValues = options.showCellValues !== false, + showGroupDividers = options.showGroupDividers !== false, + sizeEncodesValue = !!options.sizeEncodesValue, + showMarginalTotals = !!options.showMarginalTotals, + freezeLabels = options.freezeLabels !== false; - // urlOther = options.urlOther, - // urlOtherText = options.urlOtherText; + // neutral theme tokens used for chrome + const noDataStroke = theme.colors.border.weak; + const bandFill = theme.colors.background.secondary; + + // value-threshold focus + const focusEnabled = !!options.focusEnabled; + const focusMode = options.focusMode || 'dim'; + const focusMin = typeof options.focusMin === 'number' ? options.focusMin : null; + const focusMax = typeof options.focusMax === 'number' ? options.focusMax : null; + function inFocus(d) { + if (!focusEnabled) { + return true; + } + if (d === -1 || d.isNull || typeof d.val !== 'number') { + return false; + } + if (focusMin !== null && d.val < focusMin) { + return false; + } + if (focusMax !== null && d.val > focusMax) { + return false; + } + return true; + } + function baseOpacity(d) { + return focusEnabled && !inFocus(d) && focusMode === 'dim' ? 0.15 : 1; + } + function isHidden(d) { + return focusEnabled && focusMode === 'hide' && !inFocus(d); + } + + // ---- cross-highlight, pin, tooltip & keyboard infrastructure ---- + // (function declarations are hoisted; they reference rectArea/xAxisGroup/etc. + // which are assigned later but only ever called at event time.) + let pinned = null; // { r, c } where either may be null for a row-only / column-only pin + const rcMap = new Map(); // 'r:c' -> rect node, built after cells render + + function applyHighlight(r, c) { + rectArea.selectAll('rect.matrix-cell').style('opacity', function (cd) { + const rr = +this.getAttribute('data-r'); + const cc = +this.getAttribute('data-c'); + const on = (r !== null && rr === r) || (c !== null && cc === c); + return on ? baseOpacity(cd) : 0.2; + }); + svg.selectAll('.cell-value').style('opacity', function () { + const rr = +this.getAttribute('data-r'); + const cc = +this.getAttribute('data-c'); + return (r !== null && rr === r) || (c !== null && cc === c) ? 1 : 0.12; + }); + xAxisGroup.selectAll('text').attr('font-weight', function () { + return c !== null && +this.getAttribute('data-c') === c ? 'bold' : null; + }); + yAxisGroup.selectAll('text').attr('font-weight', function () { + return r !== null && +this.getAttribute('data-r') === r ? 'bold' : null; + }); + } + function clearHighlight() { + if (pinned) { + applyHighlight(pinned.r, pinned.c); + return; + } + rectArea.selectAll('rect.matrix-cell').style('opacity', function (cd) { + return baseOpacity(cd); + }); + svg.selectAll('.cell-value').style('opacity', 1); + xAxisGroup.selectAll('text').attr('font-weight', null); + yAxisGroup.selectAll('text').attr('font-weight', null); + } + function togglePin(r, c) { + if (pinned && pinned.r === r && pinned.c === c) { + pinned = null; + clearHighlight(); + } else { + pinned = { r, c }; + applyHighlight(r, c); + } + } + // Highlight the cells a legend entry actually stands for: the half-open band + // [from, to), with null meaning unbounded on that side. `from === to` is the + // exact-value case used by categorical entries. + function inBand(val, from, to) { + if (typeof val !== 'number' || isNaN(val)) { + return false; + } + if (from != null && to != null && from === to) { + return val === from; + } + if (from != null && val < from) { + return false; + } + if (to != null && val >= to) { + return false; + } + return true; + } + function highlightByRange(from, to) { + rectArea.selectAll('rect.matrix-cell').style('opacity', function (cd) { + const on = cd !== -1 && !cd.isNull && inBand(cd.val, from, to); + return on ? 1 : 0.15; + }); + } + // highlight only null cells (wantNull=true) or only no-data cells (wantNull=false) + function highlightByState(wantNull) { + rectArea.selectAll('rect.matrix-cell').style('opacity', function (cd) { + const on = wantNull ? cd !== -1 && cd.isNull : cd === -1; + return on ? 1 : 0.15; + }); + } + + function cellTooltipHtml(d) { + const thisRow = sanitizeHtml(d.row); + const thisColumn = sanitizeHtml(d.col); + const thisText = sanitizeHtml(d.isNull ? 'null' : d.display.text); + const thisSuffix = sanitizeHtml(d.isNull ? '' : d.display.suffix); + return `
${thisText} ${thisSuffix ? thisSuffix : ''}
+
+
${srcText}
+
${thisRow}
+
${targetText}
+
${thisColumn}
+
${valText}
+
${thisText} ${thisSuffix ? thisSuffix : ''}
+
`; + } + // shared activation used by both hover and keyboard focus + function activateCell(node, d) { + if (d === -1) { + return; + } + select(node).raise().attr('stroke', theme.colors.text.primary).attr('stroke-width', 2); + if (highlightRowCol) { + applyHighlight(+node.getAttribute('data-r'), +node.getAttribute('data-c')); + } + tooltip + .html(() => cellTooltipHtml(d)) + .transition() + .duration(150) + .style('opacity', 1); + } + function deactivateCell(node, d) { + select(node).attr('stroke', cellStroke(d)).attr('stroke-width', cellStrokeWidth(d)); + if (highlightRowCol) { + clearHighlight(); + } + tooltip.transition().delay(100).duration(150).style('opacity', 0); + } + function positionTooltipAtCell(node) { + try { + const cr = node.getBoundingClientRect(); + const er = elem.getBoundingClientRect(); + const sx = elem.parentElement ? elem.parentElement.scrollLeft : 0; + const sy = elem.parentElement ? elem.parentElement.scrollTop : 0; + moveTooltip( + { offsetX: cr.left - er.left + sx + cr.width / 2, offsetY: cr.top - er.top + sy + cr.height / 2 }, + elem, + tooltip + ); + } catch (e) { + /* no-op */ + } + } + function cellNodeAt(r, c) { + return rcMap.get(r + ':' + c) || null; + } + function handleCellKeydown(event, node) { + let r = +node.getAttribute('data-r'); + let c = +node.getAttribute('data-c'); + const key = event.key; + let moved = true; + if (key === 'ArrowRight') { + c++; + } else if (key === 'ArrowLeft') { + c--; + } else if (key === 'ArrowDown') { + r++; + } else if (key === 'ArrowUp') { + r--; + } else if (key === 'Enter' || key === ' ') { + if (!linkURL) { + togglePin(r, c); + } + moved = false; + } else if (key === 'Escape') { + pinned = null; + clearHighlight(); + moved = false; + } else { + return; + } + event.preventDefault(); + if (!moved) { + return; + } + const target = cellNodeAt(r, c); + if (target && target !== node) { + node.setAttribute('tabindex', '-1'); + target.setAttribute('tabindex', '0'); + target.focus(); + } + } // do a bit of work to setup the visual layout of the wiget -------- - if (elem === null) { - console.error('bailing after failing to find parent element'); + // Bail if the element isn't mounted (e.g. the panel is showing an empty/table + // state) or the data isn't a real matrix. The render hook is always called for + // stable React hook order, so createViz may run before there's anything to draw. + if (!elem || !Array.isArray(rowNames) || !Array.isArray(colNames) || !Array.isArray(matrix) || rowNames.length === 0 || colNames.length === 0) { return; } // find the length of the longest name. this will inform the margin and name truncation - const longestColName = colNames.reduce((a, b) => { + let longestColName = colNames.reduce((a, b) => { return a.length > b.length ? a : b; }); - const longestRowName = rowNames.reduce((a, b) => { + let longestRowName = rowNames.reduce((a, b) => { return a.length > b.length ? a : b; }); @@ -57,84 +313,134 @@ function createViz(elem, id, rowNames, colNames, matrix, options, theme, legend, const maxColTxtLength = longestColName.length < txtLength ? longestColName.length : txtLength + 3; const maxRowTxtLength = longestRowName.length < txtLength ? longestRowName.length : txtLength + 3; - // Calculate the margins needed - const colTxtOffset = maxColTxtLength * txtSize * 5 + 25; - const rowTxtOffset = maxRowTxtLength * txtSize * 5 + 25; + // Measure the ACTUAL rendered width of the longest label (labels are first + // capped to `txtLength` characters, then the margin is sized to fit that width). + // Rotated x-axis labels contribute their width to the TOP margin. This replaces + // the old character-count estimate, which underestimated proportional-font width + // and caused labels to be truncated to only a few characters. + const labelPad = 24; // breathing room beyond the measured text + const measuredColW = measureMaxLabelWidth(elem, colNames, txtSize, theme.typography.fontFamily, txtLength); + const measuredRowW = measureMaxLabelWidth(elem, rowNames, txtSize, theme.typography.fontFamily, txtLength); + // fall back to a character-count estimate (8px/char/em) if measurement is unavailable + const colFallback = maxColTxtLength * txtSize * 8; + const rowFallback = maxRowTxtLength * txtSize * 8; + const effColW = measuredColW > 0 ? measuredColW : colFallback; + const effRowW = measuredRowW > 0 ? measuredRowW : rowFallback; + // Column label orientation: horizontal (short labels) vs rotated -90 (long labels). + // The auto decision needs the final cell width, so it is finalized after cellSize + // is computed below; here we assume rotated (the larger top margin) so fit-sizing + // stays conservative. + const labelOrientation = options.labelOrientation || 'auto'; + const colLineHeight = Math.ceil(txtSize * 18) + 12; + let colHorizontal = labelOrientation === 'horizontal'; + let colTxtOffset = (colHorizontal ? colLineHeight : effColW) + labelPad; + let rowTxtOffset = effRowW + labelPad; // Category header configuration - const colCategoryHeaderHeight = options.enableColGrouping && colCategories && colCategories.length > 0 + const colGrouping = options.enableColGrouping && colCategories && colCategories.length > 0; + const rowGrouping = options.enableRowGrouping && rowCategories && rowCategories.length > 0; + + const colCategoryHeaderHeight = colGrouping ? (options.colCategoryHeaderHeight !== undefined ? options.colCategoryHeaderHeight : 40) : 0; - const colCategoryGap = options.enableColGrouping && colCategories && colCategories.length > 0 + const colCategoryGap = colGrouping ? (options.colCategoryGap !== undefined ? options.colCategoryGap : 4) : 0; + const rowCategoryHeaderWidth = rowGrouping + ? (options.rowCategoryHeaderWidth !== undefined ? options.rowCategoryHeaderWidth : 100) + : 0; + const rowCategoryGap = rowGrouping + ? (options.rowCategoryGap !== undefined ? options.rowCategoryGap : 4) + : 0; + + // marginal total bands (extra space reserved on the right/bottom) + const marginalBand = showMarginalTotals ? 48 : 0; + + // ---- Determine cell size (fixed px, or fit-to-panel) ---- + const nCols = colNames.length; + const nRows = rowNames.length; + const colGapTotal = colGrouping ? (colCategories.length - 1) * colCategoryGap : 0; + const rowGapTotal = rowGrouping ? (rowCategories.length - 1) * rowCategoryGap : 0; + + let cellSize = options.cellSize; + if (options.sizeMode === 'fit' && panelWidth > 0 && panelHeight > 0) { + const marginLeftEst = rowTxtOffset + rowCategoryHeaderWidth; + const marginTopEst = colTxtOffset + colCategoryHeaderHeight; + // reserve exactly what this legend needs, not a flat guess + const legendReserve = options.showLegend ? estimateLegendHeight(legend, panelWidth) : 0; + const availW = panelWidth - marginLeftEst - marginalBand - colGapTotal - 4; + const availH = panelHeight - marginTopEst - marginalBand - rowGapTotal - legendReserve - 4; + const fitW = availW / nCols; + const fitH = availH / nRows; + let fit = Math.floor(Math.min(fitW, fitH)); + if (!isFinite(fit) || fit < 4) { + fit = 4; + } + if (fit > 80) { + fit = 80; + } + cellSize = fit; + } + + // Finalize column-label orientation now that the real cell width is known. + // Auto: horizontal when the longest label fits inside a cell; otherwise rotate. + if (labelOrientation === 'auto') { + colHorizontal = effColW <= cellSize - 4; + } else { + colHorizontal = labelOrientation === 'horizontal'; + } + colTxtOffset = (colHorizontal ? colLineHeight : effColW) + labelPad; + + const bandwidth = cellSize * (1 - cellPadding); + // per-cell offset so the padded cell is centered in its slot + const cellInset = (cellSize - bandwidth) / 2; // Calculate column positions with gaps between colCategories - const columnPositions: Heading[] = []; + let columnPositions = []; let totalWidth = 0; - if (options.enableColGrouping && colCategories && colCategories.length > 0) { - // Calculate positions with category gaps + if (colGrouping) { colCategories.forEach((category, catIndex) => { - const numCols = category.items.length; + const numCols = category.columns.length; const groupWidth = numCols * cellSize; - - category.items.forEach((colName, indexInCat) => { + category.columns.forEach((colName, indexInCat) => { columnPositions.push({ name: colName, - x: totalWidth + (indexInCat * cellSize), + x: totalWidth + indexInCat * cellSize, width: cellSize, category: category.name, - categoryIndex: catIndex + categoryIndex: catIndex, }); }); - totalWidth += groupWidth; if (catIndex < colCategories.length - 1) { totalWidth += colCategoryGap; } }); } else { - // No grouping: standard layout colNames.forEach((colName, idx) => { - columnPositions.push({ - name: colName, - x: idx * cellSize, - width: cellSize, - category: '', - categoryIndex: 0 - }); + columnPositions.push({ name: colName, x: idx * cellSize, width: cellSize, category: '', categoryIndex: 0 }); }); totalWidth = colNames.length * cellSize; } - // Row category configuration - const rowCategoryHeaderWidth = options.enableRowGrouping && rowCategories && rowCategories.length > 0 - ? (options.rowCategoryHeaderWidth !== undefined ? options.rowCategoryHeaderWidth : 100) - : 0; - const rowCategoryGap = options.enableRowGrouping && rowCategories && rowCategories.length > 0 - ? (options.rowCategoryGap !== undefined ? options.rowCategoryGap : 4) - : 0; - - // Calculate row positions with gaps between colCategories - const rowPositions: Heading[] = []; + // Calculate row positions with gaps between rowCategories + let rowPositions = []; let totalHeight = 0; - if (options.enableRowGrouping && rowCategories && rowCategories.length > 0) { + if (rowGrouping) { rowCategories.forEach((category, catIndex) => { - const numRows = category.items.length; + const numRows = category.rows.length; const groupHeight = numRows * cellSize; - - category.items.forEach((rowName, indexInCat) => { + category.rows.forEach((rowName, indexInCat) => { rowPositions.push({ name: rowName, - y: totalHeight + (indexInCat * cellSize), + y: totalHeight + indexInCat * cellSize, height: cellSize, category: category.name, - categoryIndex: catIndex + categoryIndex: catIndex, }); }); - totalHeight += groupHeight; if (catIndex < rowCategories.length - 1) { totalHeight += rowCategoryGap; @@ -142,19 +448,18 @@ function createViz(elem, id, rowNames, colNames, matrix, options, theme, legend, }); } else { rowNames.forEach((rowName, idx) => { - rowPositions.push({ - name: rowName, - y: idx * cellSize, - height: cellSize, - category: '', - categoryIndex: 0 - }); + rowPositions.push({ name: rowName, y: idx * cellSize, height: cellSize, category: '', categoryIndex: 0 }); }); totalHeight = rowNames.length * cellSize; } // set the dimensions and margins of the graph - const margin = { top: colTxtOffset + colCategoryHeaderHeight, right: 0, bottom: 0, left: rowTxtOffset + rowCategoryHeaderWidth }, + let margin = { + top: colTxtOffset + colCategoryHeaderHeight, + right: marginalBand, + bottom: marginalBand, + left: rowTxtOffset + rowCategoryHeaderWidth, + }, width = totalWidth, height = totalHeight; @@ -163,16 +468,14 @@ function createViz(elem, id, rowNames, colNames, matrix, options, theme, legend, elem.replaceChildren(); } - const tooltip = d3 - .select(elem) + const tooltip = select(elem) .append('div') .attr('class', `${styles.tooltip} matrix-tooltip-${id}`) .style('opacity', 0); // append the svg object to the body of the page - const svgClass = `svg-${id}`; - const svgMatrix = d3 - .select(elem) + let svgClass = `svg-${id}`; + const svg = select(elem) .append('svg') .attr('id', svgClass) .attr('width', width + margin.left + margin.right) @@ -180,60 +483,109 @@ function createViz(elem, id, rowNames, colNames, matrix, options, theme, legend, .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - // Build X scales and axis: - // Index-based scale to support duplicate column names across categories - const x = function(colIndex) { + // index-based scales to support duplicate names across categories + let x = function (colIndex) { return columnPositions[colIndex] ? columnPositions[colIndex].x : 0; }; - x.bandwidth = function() { - return cellSize * (1 - cellPadding); + let y = function (rowIndex) { + return rowPositions[rowIndex] ? rowPositions[rowIndex].y : 0; }; - // Create axis manually with proper styling - const xAxisGroup = svgMatrix.append('g').attr('class', 'x-axis').attr('font-size', '10'); - columnPositions.forEach(pos => { - const label = xAxisGroup.append('text') - .attr('transform', `translate(${pos.x + cellSize / 2},-12)rotate(-90)`) - .attr('text-anchor', 'start') - .attr('font-size', txtSize + 'em') - .style('font-family', theme.typography.fontFamily) - .attr('fill', theme.colors.text.primary) - .text(pos.name); + // ---- Category group background bands (drawn first, behind everything) ---- + if (showGroupDividers && (colGrouping || rowGrouping)) { + const bandGroup = svg.append('g').attr('class', `group-bands-${id}`).lower(); + if (colGrouping) { + colCategories.forEach((category, catIndex) => { + if (catIndex % 2 === 1) { + const startPos = columnPositions[category.startIndex]; + const endPos = columnPositions[category.endIndex]; + if (startPos && endPos) { + bandGroup + .append('rect') + .attr('x', startPos.x - cellInset) + .attr('y', -cellInset) + .attr('width', endPos.x + cellSize - startPos.x) + .attr('height', height + cellInset) + .attr('fill', bandFill) + .attr('opacity', 0.5); + } + } + }); + } + if (rowGrouping) { + rowCategories.forEach((category, catIndex) => { + if (catIndex % 2 === 1) { + const startPos = rowPositions[category.startIndex]; + const endPos = rowPositions[category.endIndex]; + if (startPos && endPos) { + bandGroup + .append('rect') + .attr('x', -cellInset) + .attr('y', startPos.y - cellInset) + .attr('width', width + cellInset) + .attr('height', endPos.y + cellSize - startPos.y) + .attr('fill', bandFill) + .attr('opacity', 0.5); + } + } + }); + } + } - // Apply truncation and tooltips - label.call(truncateLabel, txtLength); - label.on('mouseover', function (event, d) { - tooltip - .html(sanitizeHtml(pos.name)) - .transition() - .duration(150) - .style('opacity', 1); - }) - .on('mousemove', function (event) { - moveTooltip(event, elem, tooltip); - }) - .on('mouseout', function () { - tooltip - .transition() - .delay(100) - .duration(150) - .style('opacity', 0); - }); + // ---- X axis labels (rotated -90 by default; horizontal when they fit) ---- + const xAxisGroup = svg.append('g').attr('class', 'x-axis'); + columnPositions.forEach((pos, idx) => { + let label; + if (colHorizontal) { + label = xAxisGroup + .append('text') + .attr('data-c', idx) + .attr('x', pos.x + cellSize / 2) + .attr('y', -8) + .attr('text-anchor', 'middle') + .attr('font-size', txtSize + 'em') + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.primary) + .text(pos.name); + label.call(truncateToPixels, cellSize - 2); + } else { + label = xAxisGroup + .append('text') + .attr('data-c', idx) + .attr('transform', `translate(${pos.x + cellSize / 2},-12)rotate(-90)`) + .attr('text-anchor', 'start') + .attr('font-size', txtSize + 'em') + .style('font-family', theme.typography.fontFamily) + .attr('fill', theme.colors.text.primary) + .text(pos.name); + label.call(truncateToPixels, colTxtOffset - 16); + } + if (highlightRowCol) { + label.style('cursor', 'pointer'); + } + label + .on('mouseover', function () { + tooltip.html(sanitizeHtml(pos.name)).transition().duration(150).style('opacity', 1); + }) + .on('mousemove', function (event) { + moveTooltip(event, elem, tooltip); + }) + .on('mouseout', function () { + tooltip.transition().delay(100).duration(150).style('opacity', 0); + }) + .on('click', function () { + if (highlightRowCol) { + togglePin(null, idx); + } + }); }); - // Build Y scales and axis: - // Index-based scale to support duplicate row names across categories - const y = function(rowIndex) { - return rowPositions[rowIndex] ? rowPositions[rowIndex].y : 0; - }; - y.bandwidth = function() { - return cellSize * (1 - cellPadding); - }; - - // Create Y-axis manually with proper styling - const yAxisGroup = svgMatrix.append('g').attr('class', 'y-axis').attr('font-size', '10'); - rowPositions.forEach(pos => { - const label = yAxisGroup.append('text') + // ---- Y axis labels ---- + const yAxisGroup = svg.append('g').attr('class', 'y-axis'); + rowPositions.forEach((pos, idx) => { + const label = yAxisGroup + .append('text') + .attr('data-r', idx) .attr('x', -10) .attr('y', pos.y + cellSize / 2) .attr('text-anchor', 'end') @@ -243,169 +595,136 @@ function createViz(elem, id, rowNames, colNames, matrix, options, theme, legend, .attr('fill', theme.colors.text.primary) .text(pos.name); - label.call(truncateLabel, txtLength); - label.on('mouseover', function (event, d) { - tooltip - .html(sanitizeHtml(pos.name)) - .transition() - .duration(150) - .style('opacity', 1); - }) - .on('mousemove', function (event) { - moveTooltip(event, elem, tooltip); - }) - .on('mouseout', function () { - tooltip - .transition() - .delay(100) - .duration(150) - .style('opacity', 0); - }); + label.call(truncateToPixels, rowTxtOffset - 16); + if (highlightRowCol) { + label.style('cursor', 'pointer'); + } + label + .on('mouseover', function () { + tooltip.html(sanitizeHtml(pos.name)).transition().duration(150).style('opacity', 1); + }) + .on('mousemove', function (event) { + moveTooltip(event, elem, tooltip); + }) + .on('mouseout', function () { + tooltip.transition().delay(100).duration(150).style('opacity', 0); + }) + .on('click', function () { + if (highlightRowCol) { + togglePin(idx, null); + } + }); }); - // Render category headers - if (options.enableColGrouping && colCategories && colCategories.length > 0) { - const categoryHeaderGroup = svgMatrix.append('g') + // ---- Column category headers ---- + let colHeaderGroup = null; + if (colGrouping) { + colHeaderGroup = svg + .append('g') .attr('class', `category-headers-${id}`) - .attr('font-size', '10') .attr('transform', `translate(0, ${-colTxtOffset - colCategoryHeaderHeight})`); - colCategories.forEach((category, catIndex) => { - // find first and last column heading for this category - let startPos = undefined; - let endPos = undefined; - for (let i = 0; i < columnPositions.length; i++) { - if (columnPositions[i].category === category.name) { - if (startPos === undefined) { - // first column heading for the category - startPos = columnPositions[i]; - endPos = columnPositions[i]; - } else { - endPos = columnPositions[i]; - } - } else if (startPos !== undefined) { - // no more column headings for this category - break; - } - } - + colCategories.forEach((category) => { + const startPos = columnPositions[category.startIndex]; + const endPos = columnPositions[category.endIndex]; if (startPos && endPos) { const groupX = startPos.x; - const groupWidth = (endPos.x + cellSize) - startPos.x; - - // Category label rotated vertically; truncated to fit colCategoryHeaderHeight. - const colGroupLabelMaxChars = Math.max(3, Math.floor((colCategoryHeaderHeight - 12) / (txtSize * 1.2 * 8))); - categoryHeaderGroup.append('text') + colHeaderGroup + .append('text') .attr('transform', `translate(${groupX + cellSize / 2}, ${colCategoryHeaderHeight - 12})rotate(-90)`) .attr('text-anchor', 'start') - .attr('font-size', (txtSize * 1.2) + 'em') + .attr('font-size', txtSize * 1.2 + 'em') .attr('font-weight', 'bold') .attr('fill', theme.colors.text.primary) .style('font-family', theme.typography.fontFamily) .text(category.name) - .call(truncateLabel, colGroupLabelMaxChars) - .on('mouseover', function (event, d) { - tooltip - .html(sanitizeHtml(category.name)) - .transition() - .duration(150) - .style('opacity', 1); + .call(truncateToPixels, colCategoryHeaderHeight - 12) + .on('mouseover', function () { + tooltip.html(sanitizeHtml(category.name)).transition().duration(150).style('opacity', 1); }) .on('mousemove', function (event) { moveTooltip(event, elem, tooltip); }) .on('mouseout', function () { - tooltip - .transition() - .delay(100) - .duration(150) - .style('opacity', 0); + tooltip.transition().delay(100).duration(150).style('opacity', 0); }); } }); } - // Render row category headers - if (options.enableRowGrouping && rowCategories && rowCategories.length > 0) { - const rowCategoryHeaderGroup = svgMatrix.append('g') + // ---- Row category headers ---- + let rowHeaderGroup = null; + if (rowGrouping) { + rowHeaderGroup = svg + .append('g') .attr('class', `row-category-headers-${id}`) - .attr('font-size', '10') .attr('transform', `translate(${-rowTxtOffset - rowCategoryHeaderWidth}, 0)`); - rowCategories.forEach((category, catIndex) => { - // find first and last row heading for this category - let startPos = undefined; - let endPos = undefined; - for (let i = 0; i < rowPositions.length; i++) { - if (rowPositions[i].category === category.name) { - if (startPos === undefined) { - // first row heading for the category - startPos = rowPositions[i]; - endPos = rowPositions[i]; - } else { - endPos = rowPositions[i]; - } - } else if (startPos !== undefined) { - // no more row headings for this category - break; - } - } - - if (startPos && endPos) { + rowCategories.forEach((category) => { + const startPos = rowPositions[category.startIndex]; + if (startPos) { const groupY = startPos.y; - const groupHeight = (endPos.y + cellSize) - startPos.y; - - // Group label: right-aligned so text grows leftward into rowCategoryHeaderWidth. - // Truncated when wider than the allocated area; tooltip shows full name. - const groupLabelMaxChars = Math.max(3, Math.floor((rowCategoryHeaderWidth - 10) / (txtSize * 1.2 * 8))); - rowCategoryHeaderGroup.append('text') + rowHeaderGroup + .append('text') .attr('x', rowCategoryHeaderWidth - 5) .attr('y', groupY + cellSize / 2) .attr('text-anchor', 'end') .attr('dominant-baseline', 'middle') - .attr('font-size', (txtSize * 1.2) + 'em') + .attr('font-size', txtSize * 1.2 + 'em') .attr('font-weight', 'bold') .attr('fill', theme.colors.text.primary) .style('font-family', theme.typography.fontFamily) .text(category.name) - .call(truncateLabel, groupLabelMaxChars) - .on('mouseover', function (event, d) { - tooltip - .html(sanitizeHtml(category.name)) - .transition() - .duration(150) - .style('opacity', 1); + .call(truncateToPixels, rowCategoryHeaderWidth - 8) + .on('mouseover', function () { + tooltip.html(sanitizeHtml(category.name)).transition().duration(150).style('opacity', 1); }) .on('mousemove', function (event) { moveTooltip(event, elem, tooltip); }) .on('mouseout', function () { - tooltip - .transition() - .delay(100) - .duration(150) - .style('opacity', 0); + tooltip.transition().delay(100).duration(150).style('opacity', 0); }); } }); } - //build the matrix ///////////////////////////////////////// - - //use d3's local stuff to record where we are in the outer loop - const outer = d3.local(); - - const svg_g = d3.select('#' + svgClass).selectAll('svg > g'); + // ---- helpers for cell appearance (used at creation + hover restore) ---- + function cellFill(d) { + if (d === -1) { + return distinctNoData ? 'none' : defaultColor; + } + return d.color || defaultColor; + } + function cellStroke(d) { + if (d === -1 && distinctNoData) { + return noDataStroke; + } + return 'none'; + } + function cellStrokeWidth(d) { + return d === -1 && distinctNoData ? 1 : 0; + } + // area-proportional side length within the slot when size encoding is on + function encodedSide(d) { + if (!sizeEncodesValue || d === -1 || d.val == null || !valueDomain) { + return bandwidth; + } + const span = valueDomain.max - valueDomain.min || 1; + let norm = (d.val - valueDomain.min) / span; + norm = Math.max(0.05, Math.min(1, norm)); + return bandwidth * Math.sqrt(norm); + } - //create the area where we will put all the boxes + // ---- the matrix cells ---- + let outer = local(); + let svg_g = select('#' + svgClass).selectAll('svg > g'); const rectClass = `rectArea-${id}`; const rectArea = svg_g.append('g').attr('class', rectClass); - //this selection breaks the data down to the row level. This is - //needed because the underlying datastructure is a 2d array - const rows = rectArea.selectAll('g').data(matrix).enter().append('g').attr('class', 'row'); + let rows = rectArea.selectAll('g').data(matrix).enter().append('g').attr('class', 'row'); - const rects = rows + let rects = rows .selectAll('rect') .data(function (d, i) { outer.set(this, i); @@ -426,193 +745,274 @@ function createViz(elem, id, rowNames, colNames, matrix, options, theme, legend, } }) .append('rect') - .attr('id', `rect-${id}`) - .attr('x', function (d, i, j) { - return x(i); + .attr('class', 'matrix-cell') + .attr('data-c', function (d, i) { + return i; }) - .attr('y', function (d, i, j) { - const outer_counter = outer.get(this); - return y(outer_counter); + .attr('data-r', function () { + return outer.get(this); }) - .attr('width', x.bandwidth()) - .attr('height', y.bandwidth()) - //this places a 'data' attribute into the HTML to make debugging easier. Allows you to see the inner/outer loop counts and the datum used - .attr('data', function (d, i) { - const outer_counter = outer.get(this); - const str = '' + outer_counter + ':' + i + ' ' + rowNames[outer_counter] + ':' + colNames[i] + ' ' + d; - return str; + .attr('rx', cornerRadius) + .attr('ry', cornerRadius) + .attr('x', function (d, i) { + const side = encodedSide(d); + return x(i) + cellInset + (bandwidth - side) / 2; }) - .attr('fill', (d) => { - if (d.color) { - return d.color; - } else { - return defaultColor; - } + .attr('y', function (d) { + const side = encodedSide(d); + return y(outer.get(this)) + cellInset + (bandwidth - side) / 2; + }) + .attr('width', function (d) { + return encodedSide(d); }) - // the tooltip for boxes + .attr('height', function (d) { + return encodedSide(d); + }) + .attr('fill', cellFill) + .attr('stroke', cellStroke) + .attr('stroke-width', cellStrokeWidth) + .style('opacity', baseOpacity) + .style('display', function (d) { + return isHidden(d) ? 'none' : null; + }) + .attr('role', 'gridcell') + .attr('tabindex', -1) .on('mouseover', function (event, d) { - if (d !== -1) { - //turn down the opacity slightly to show the hover - d3.select(this) - // .attr('opacity', '.75') - .attr('width', x.bandwidth() + 5) - .attr('height', y.bandwidth() + 5) - .attr('transform', 'translate(-1, -1)'); - - tooltip - .html(() => { - const thisRow = sanitizeHtml(d.row); - const thisColumn =sanitizeHtml(d.col); - const thisText = sanitizeHtml(d.display.text); - const thisSuffix = sanitizeHtml(d.display.suffix); - const text = `
-
-
${srcText}
-
-
-
${thisRow}
-
-
-
${targetText}
-
-
-
${thisColumn}
-
-
-
${valText}
-
-
-
${thisText} ${thisSuffix ? thisSuffix : ''}
-
-
`; - return text; - }) - .transition() - .duration(150) - .style('opacity', 1); - } + activateCell(this, d); }) .on('mousemove', function (event) { moveTooltip(event, elem, tooltip); }) - .on('mouseout', function () { - //reset the opacity and move the tooltip out of the way. If we dont move it it will prevent hovering over other boxes. - d3.select(this) - // .attr('opacity', '1') - .attr('transform', 'translate(0, 0)') - .attr('width', x.bandwidth()) - .attr('height', y.bandwidth()); - - tooltip - .transition() - .delay(100) - .duration(150) - .style('opacity', 0) + .on('mouseout', function (event, d) { + deactivateCell(this, d); + }) + .on('focus', function (event, d) { + activateCell(this, d); + positionTooltipAtCell(this); + }) + .on('blur', function (event, d) { + deactivateCell(this, d); }) - .on('click', function (d) { - if(linkURL) { + .on('keydown', function (event) { + handleCellKeydown(event, this); + }) + .on('click', function (event, d) { + if (linkURL) { tooltip.remove(); + } else if (d !== -1 && highlightRowCol) { + togglePin(+this.getAttribute('data-r'), +this.getAttribute('data-c')); + } + }); + + // ---- accessibility: build the r:c lookup, per-cell aria-labels, roving tabindex ---- + rectArea.selectAll('rect.matrix-cell').each(function () { + const rr = this.getAttribute('data-r'); + const cc = this.getAttribute('data-c'); + rcMap.set(rr + ':' + cc, this); + const rowName = rowNames[+rr]; + const colName = colNames[+cc]; + const d = matrix[+rr][+cc]; + let valStr; + if (d === -1) { + valStr = 'no data'; + } else if (d.isNull) { + valStr = 'null'; + } else { + valStr = `${d.display.text}${d.display.suffix ? ' ' + d.display.suffix : ''}`; + } + this.setAttribute('aria-label', `${srcText} ${rowName}, ${targetText} ${colName}, ${valText} ${valStr}`); + }); + // make the first visible cell keyboard-reachable (roving tabindex entry point) + const firstCell = rcMap.get('0:0') || rectArea.select('rect.matrix-cell').node(); + if (firstCell) { + firstCell.setAttribute('tabindex', '0'); + } + // label the grid for screen readers + select('#' + svgClass) + .attr('role', 'img') + .attr('aria-label', `Matrix of ${rowNames.length} rows by ${colNames.length} columns`); + rectArea.attr('role', 'grid'); + rows.attr('role', 'row'); + + // ---- In-cell value labels (only when they fit) ---- + if (showCellValues) { + const labelGroup = svg.append('g').attr('class', `cell-values-${id}`).style('pointer-events', 'none'); + const labelFontPx = Math.min(bandwidth * 0.42, 13); + if (labelFontPx >= 7) { + for (let r = 0; r < matrix.length; r++) { + for (let c = 0; c < matrix[r].length; c++) { + const d = matrix[r][c]; + if (d === -1 || d.isNull || d.val == null) { + continue; + } + // declutter: no label on cells outside the focused range + if (focusEnabled && !inFocus(d)) { + continue; + } + const text = d.display && d.display.text != null ? String(d.display.text) : String(d.val); + // estimate rendered width; skip if it won't fit with padding + const estWidth = text.length * labelFontPx * 0.6; + if (estWidth > bandwidth - 4) { + continue; + } + labelGroup + .append('text') + .attr('class', 'cell-value') + .attr('data-r', r) + .attr('data-c', c) + .attr('x', x(c) + cellSize / 2) + .attr('y', y(r) + cellSize / 2) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('font-size', `${labelFontPx}px`) + .style('font-family', theme.typography.fontFamily) + .attr('fill', contrastText(d.color)) + .text(text); + } } + } + } + + // ---- Marginal total bars (row sums on the right, column sums on the bottom) ---- + if (showMarginalTotals) { + const barColor = theme.colors.text.secondary; + const maxRowTotal = Math.max(1, ...rowTotals.map((v) => Math.abs(v))); + const maxColTotal = Math.max(1, ...colTotals.map((v) => Math.abs(v))); + const marginalGroup = svg.append('g').attr('class', `marginal-totals-${id}`).style('pointer-events', 'none'); + + // row totals -> horizontal bars just right of the grid + rowPositions.forEach((pos, i) => { + const len = (Math.abs(rowTotals[i]) / maxRowTotal) * (marginalBand - 8); + marginalGroup + .append('rect') + .attr('x', width + 4) + .attr('y', pos.y + cellInset) + .attr('width', Math.max(0, len)) + .attr('height', bandwidth) + .attr('rx', Math.min(cornerRadius, 3)) + .attr('fill', barColor) + .attr('opacity', 0.55); + }); + + // column totals -> vertical bars just below the grid + columnPositions.forEach((pos, j) => { + const len = (Math.abs(colTotals[j]) / maxColTotal) * (marginalBand - 8); + marginalGroup + .append('rect') + .attr('x', pos.x + cellInset) + .attr('y', height + 4) + .attr('width', bandwidth) + .attr('height', Math.max(0, len)) + .attr('rx', Math.min(cornerRadius, 3)) + .attr('fill', barColor) + .attr('opacity', 0.55); }); + } + + // ---- Freeze axis labels: counter-translate label groups on scroll ---- + if (freezeLabels) { + const scroller = elem.parentElement; + if (scroller) { + const colHeaderBaseY = -colTxtOffset - colCategoryHeaderHeight; + const rowHeaderBaseX = -rowTxtOffset - rowCategoryHeaderWidth; + const onScroll = () => { + const sx = scroller.scrollLeft; + const sy = scroller.scrollTop; + xAxisGroup.attr('transform', `translate(0, ${sy})`); + yAxisGroup.attr('transform', `translate(${sx}, 0)`); + if (colHeaderGroup) { + colHeaderGroup.attr('transform', `translate(0, ${colHeaderBaseY + sy})`); + } + if (rowHeaderGroup) { + rowHeaderGroup.attr('transform', `translate(${rowHeaderBaseX + sx}, 0)`); + } + }; + // avoid stacking listeners across re-renders + if (scroller.__matrixScrollHandler) { + scroller.removeEventListener('scroll', scroller.__matrixScrollHandler); + } + scroller.__matrixScrollHandler = onScroll; + scroller.addEventListener('scroll', onScroll, { passive: true }); + } + } ////// LEGEND //////////// - if (options.showLegend) { - const legendClass = `legend-${id}`; + if (options.showLegend && legend) { + renderLegend( + elem, + legend, + id, + { + availableWidth: panelWidth, + cornerRadius, + nullColor, + defaultColor, + distinctNoData, + noDataStroke, + }, + theme, + { highlightByRange, highlightByState, clearHighlight } + ); + } +} - d3 - .select(elem) - .append('div') - .attr('class', `matrix-legend-${id}`) +/** Measure the widest rendered label (each capped to capChars) so the axis margin + * can be sized to actually fit it. Uses a hidden, off-layout SVG so it never flashes. + * @param {HTMLElement} elem The container element (already attached to the DOM) + * @param {string[]} names The label strings + * @param {number} fontEm The font size in em (matches the axis label font-size) + * @param {string} fontFamily The font family used for the labels + * @param {number} capChars Truncate labels to at most this many characters before measuring + * @return {number} The maximum rendered width in pixels (0 if measurement is unavailable) + */ +function measureMaxLabelWidth(elem, names, fontEm, fontFamily, capChars) { + let maxW = 0; + try { + const tmp = select(elem) .append('svg') - .attr('id', legendClass); - -////////////// range - bar ////////////////////// - if (options.legendType === 'range') { - const svgLegend = d3.select(`#${legendClass}`); - svgLegend - // legend bar starts at x=25, legend squares are 10x10, allow 9px per label character - .attr('width', 25 + (legend.length - 1) * 10 + legend[legend.length - 1].label.length * 9) - // legend label starts at y=50, allow 16px per label character - .attr('height', 50 + 16) - .append('g') - .selectAll('legendBars') - .data(legend) - .enter() - .append('rect') - .attr('class', `legend-bar-${id}`) - .attr('width', 10) - .attr('height', 10) - .attr('fill', function (d) { - return d.color; - }) - .attr('x', function (d, i) { - return 25 + i * 10; - }) - .attr('y', 20); - svgLegend - .append('g') - .selectAll('legendLabels') - .data(legend) - .enter() - .append('text') - .attr('x', function (d, i) { - return 20 + i * 10; - }) - .attr('y', 50) - .text(function (d, i) { - if ((i === 0) | (i === legend.length - 1)) { - return d.label; - } else { - return; - } - }) - .attr('fill', theme.colors.text.primary); - } else { -/////////// categorical - circles //////////////////////////// - const svgLegend = d3.select(`#${legendClass}`); - svgLegend - // legend bar starts at x=25, legend circles are drawn every 75px and have a 20px diameter, - // allow 9px per label character - .attr('width', 25 + (legend.length - 1) * 75 + 20 + legend[legend.length - 1].label.length * 9) - // legend label starts at y=50, allow 16px per label character - .attr('height', 50 + 16) - .append('g') - .selectAll('legendCircles') - .data(legend) - .enter() - .append('circle') - .attr('class', `legend-circle-${id}`) - .attr('r', 10) - .attr('fill', function (d) { - return d.color; - }) - .attr('cx', function (d, i) { - return 25 + i * 75; - }) - .attr('cy', 20); - svg - .append('g') - .selectAll('legendLabels') - .data(legend) - .enter() + .attr('class', 'matrix-label-measure') + .style('position', 'absolute') + .style('visibility', 'hidden') + .style('width', '0') + .style('height', '0') + .style('overflow', 'visible'); + names.forEach((n) => { + const s = String(n); + const t = s.length > capChars ? s.slice(0, capChars) + '…' : s; + const node = tmp .append('text') - .attr('x', function (d, i) { - return 15 + i * 75; - }) - .attr('y', 50) - .text(function (d) { return d.label }) - .attr('fill', theme.colors.text.primary); - } + .attr('font-size', fontEm + 'em') + .style('font-family', fontFamily) + .text(t) + .node(); + const w = node && node.getComputedTextLength ? node.getComputedTextLength() : 0; + if (w > maxW) { + maxW = w; + } + }); + tmp.remove(); + } catch (e) { + return 0; } + return maxW; } -function truncateLabel(text, width) { +/** Trim an SVG selection until it fits within maxPixels, adding an ellipsis. */ +function truncateToPixels(text, maxPixels) { text.each(function () { - let label = d3.select(this).text(); - if (label.length > width) { - label = label.slice(0, width) + '...'; + const node = this; + const full = select(node).text(); + if (maxPixels <= 0) { + return; + } + if (node.getComputedTextLength() <= maxPixels) { + return; + } + let label = full; + while (label.length > 1 && node.getComputedTextLength() > maxPixels) { + label = label.slice(0, -1); + select(node).text(label + '…'); } - d3.select(this).text(label); }); } @@ -638,7 +1038,7 @@ function moveTooltip(event, elem, tooltip) { if (event.offsetX - mouseDistance >= xMin && event.offsetX + mouseDistance >= xMax) { // Draw tooltip left of the mouse - xPos = Math.max(event.offsetX - tooltipRect.width - mouseDistance, xMin) + xPos = Math.max(event.offsetX - tooltipRect.width - mouseDistance, xMin); } else { // Draw tooltip right of the mouse xPos = Math.min(event.offsetX + mouseDistance, xMax); @@ -646,15 +1046,13 @@ function moveTooltip(event, elem, tooltip) { if (event.offsetY - mouseDistance >= yMin && event.offsetY + mouseDistance >= yMax) { // Draw tooltip above the mouse - yPos = Math.max(event.offsetY - tooltipRect.height - mouseDistance, yMin) + yPos = Math.max(event.offsetY - tooltipRect.height - mouseDistance, yMin); } else { // Draw tooltip below the mouse yPos = Math.min(event.offsetY + mouseDistance, yMax); } - tooltip - .style('left', `${xPos}px`) - .style('top', `${yPos}px`) + tooltip.style('left', `${xPos}px`).style('top', `${yPos}px`); } const getStyles = (theme: GrafanaTheme2) => { @@ -666,18 +1064,23 @@ const getStyles = (theme: GrafanaTheme2) => { font-size: ${theme.typography.size.sm}; font-weight: ${theme.typography.fontWeightRegular}; border: 1px solid ${theme.colors.border.weak}; - border-radius: ${theme.shape.radius.default}: + border-radius: ${theme.shape.radius.default}; box-shadow: ${theme.shadows.z3}; - padding: 5px; + padding: 8px 10px; z-index: 500; position: absolute; width: fit-content; `, + tooltipValue: css` + font-size: ${theme.typography.h5.fontSize}; + font-weight: ${theme.typography.fontWeightBold}; + margin-bottom: 4px; + `, tooltipTable: css` display: grid; grid-template-columns: max-content 1fr; gap: 2px; - padding: 3px; + padding: 0; `, tooltipTableCell: css` display: flex; @@ -695,23 +1098,53 @@ const getStyles = (theme: GrafanaTheme2) => { }; /** - * - * @param {any[]} rowNames Row names - * @param {any[]} colNames Column names - * @param {DataMatrixCell[][]} matrix Data for the matrix diagram - * @param {number} id The panel id - * @param {MatrixOptions} options Panel configuration - * @param {LegendData[]} legend Legend data - * @param {Category[]} column categories - * @param {Category[]} row categories - * @return {SvgInHtml} A d3 callback + * @param {*} matrix Data for the matrix diagram + * @param {*} id The panel id + * @param {number} width Width of panel + * @param {number} height Height of panel + * @return {*} A d3 callback */ -function matrix(rowNames, colNames, matrix, id, options, legend, colCategories, rowCategories) { - /* eslint-disable react-hooks/rules-of-hooks */ +/* eslint-disable react-hooks/rules-of-hooks */ +function matrix( + rowNames, + colNames, + matrix, + id, + width, + height, + options, + legend, + colMetadata, + colCategories, + rowMetadata, + rowCategories, + rowTotals, + colTotals, + valueDomain +) { const theme = useTheme2(); const styles = useStyles2(getStyles); const ref = useD3((svg) => { - createViz(svg, id, rowNames, colNames, matrix, options, theme, legend, styles, colCategories, rowCategories); + createViz( + svg, + id, + width, + height, + rowNames, + colNames, + matrix, + options, + theme, + legend, + styles, + colMetadata, + colCategories, + rowMetadata, + rowCategories, + rowTotals, + colTotals, + valueDomain + ); }); return ref; } diff --git a/src/module.ts b/src/module.ts old mode 100644 new mode 100755 index f0cd151..2c48b3d --- a/src/module.ts +++ b/src/module.ts @@ -1,4 +1,4 @@ -import { Field, FieldConfigProperty, FieldType, PanelPlugin } from '@grafana/data'; +import { FieldOverrideContext, getFieldDisplayName, PanelPlugin, FieldConfigProperty } from '@grafana/data'; // import { standardOptionsCompat } from 'grafana-plugin-support'; import { MatrixOptions } from './types'; import { EsnetMatrix } from './EsnetMatrix'; @@ -15,6 +15,8 @@ import { EsnetMatrix } from './EsnetMatrix'; const OptionsCategory = ['Display']; const URLCategory = ['Link Options']; const RowOptions = ['Row/Column Options']; +const ColorCategory = ['Colors']; +const LayoutCategory = ['Layout']; const urlBool = (addUrl: boolean) => (config: MatrixOptions) => config.addUrl === addUrl; // const eurlOtherBool = (urlOther: boolean) => (config: MatrixOptions) => config.urlOther === urlOther; @@ -45,86 +47,154 @@ plugin.useFieldConfig({ ] }); -plugin.setMigrationHandler((panel) => { - if (panel.options.sortType === undefined) { - panel.options.sortType = 'natural-asc' - } - - return panel.options; -}); - plugin.setPanelOptions((builder) => { /////////--------- Row and Column options ---------//////////////// - builder.addSelect({ - path: 'sortType', - name: 'Sort Type', - description: 'Sorting to apply to row/column headings', - category: RowOptions, - defaultValue: 'natural-asc', - settings: { - allowCustomValue: false, - options: [ - { value: 'none', label: 'None' }, - { value: 'natural-asc', label: 'Natural ascending' }, - { value: 'natural-desc', label: 'Natural descending' }, - ], - }, - }); builder.addBooleanSwitch({ path: 'inputList', - name: 'Use Static Row/Column Headings', + name: 'Use Static Row/Column Lists', category: RowOptions, defaultValue: false, }); builder.addTextInput({ path: 'staticRows', - name: 'Row Headings', + name: 'Row Array', description: 'Terms to use as matrix rows (comma separated)', category: RowOptions, showIf: staticBool(true), }); builder.addTextInput({ path: 'staticColumns', - name: 'Column Headings', + name: 'Column Array', description: 'Terms to use as matrix columns (comma separated)', category: RowOptions, showIf: staticBool(true), }); - builder.addFieldNamePicker({ + builder.addSelect({ path: 'sourceField', name: 'Rows Field', description: 'Select the field that should be used for the rows', category: RowOptions, settings: { - filter: (field: Field) => field.type === FieldType.string, + allowCustomValue: false, + options: [], + getOptions: async (context: FieldOverrideContext) => { + const options = []; + if (context && context.data) { + for (const frame of context.data) { + for (const field of frame.fields) { + const name = getFieldDisplayName(field, frame, context.data); + const value = name; + options.push({ value, label: name }); + } + } + } + return Promise.resolve(options); + }, }, + // defaultValue: options[0], }); - builder.addFieldNamePicker({ + builder.addSelect({ path: 'targetField', name: 'Columns Field', description: 'Select the field to use for the columns', category: RowOptions, settings: { - filter: (field: Field) => field.type === FieldType.string, + allowCustomValue: false, + options: [], + getOptions: async (context: FieldOverrideContext) => { + const options = []; + if (context && context.data) { + for (const frame of context.data) { + for (const field of frame.fields) { + const name = getFieldDisplayName(field, frame, context.data); + const value = name; + options.push({ value, label: name }); + } + } + } + return Promise.resolve(options); + }, }, }); - builder.addFieldNamePicker({ + builder.addSelect({ path: 'valueField', name: 'Value Field', description: 'Select the numeric field used to color the matrix cells.', category: RowOptions, settings: { - filter: (field: Field) => field.type === FieldType.number, + allowCustomValue: false, + options: [], + getOptions: async (context: FieldOverrideContext) => { + const options = []; + if (context && context.data) { + for (const frame of context.data) { + for (const field of frame.fields) { + if (field.type === 'number') { + const name = getFieldDisplayName(field, frame, context.data); + const value = name; + options.push({ value, label: name }); + } + } + } + } + return Promise.resolve(options); + }, }, }); - builder.addFieldNamePicker({ + builder.addSelect({ + path: 'rowSort', + name: 'Row Order', + description: + 'How to order the rows. Total sorts by each row\'s summed value (largest first). Cluster reorders similar rows next to each other to reveal block structure.', + category: RowOptions, + defaultValue: 'name', + settings: { + allowCustomValue: false, + options: [ + { value: 'name', label: 'By name (A→Z)' }, + { value: 'total', label: 'By total (largest first)' }, + { value: 'cluster', label: 'By cluster (seriation)' }, + ], + }, + }); + builder.addSelect({ + path: 'colSort', + name: 'Column Order', + description: + 'How to order the columns. Total sorts by each column\'s summed value (largest first). Cluster reorders similar columns next to each other.', + category: RowOptions, + defaultValue: 'name', + settings: { + allowCustomValue: false, + options: [ + { value: 'name', label: 'By name (A→Z)' }, + { value: 'total', label: 'By total (largest first)' }, + { value: 'cluster', label: 'By cluster (seriation)' }, + ], + }, + }); + builder.addSelect({ path: 'colCategoryField', name: 'Column Category Field', description: 'Select the field to use for grouping columns into categories', category: RowOptions, showIf: colGroupingBool(true), settings: { - filter: (field: Field) => field.type === FieldType.string, + allowCustomValue: false, + options: [], + getOptions: async (context: FieldOverrideContext) => { + const options = [{ value: '', label: 'None' }]; + if (context && context.data) { + for (const frame of context.data) { + for (const field of frame.fields) { + const name = getFieldDisplayName(field, frame, context.data); + const value = name; + options.push({ value, label: name }); + } + } + } + return Promise.resolve(options); + }, }, }); builder.addBooleanSwitch({ @@ -171,14 +241,28 @@ plugin.setPanelOptions((builder) => { }); ////////------------ Row Grouping Options ----------------///////////// - builder.addFieldNamePicker({ + builder.addSelect({ path: 'rowCategoryField', name: 'Row Category Field', description: 'Select the field to use for grouping rows into categories', category: RowOptions, showIf: rowGroupingBool(true), settings: { - filter: (field: Field) => field.type === FieldType.string, + allowCustomValue: false, + options: [], + getOptions: async (context: FieldOverrideContext) => { + const options = [{ value: '', label: 'None' }]; + if (context && context.data) { + for (const frame of context.data) { + for (const field of frame.fields) { + const name = getFieldDisplayName(field, frame, context.data); + const value = name; + options.push({ value, label: name }); + } + } + } + return Promise.resolve(options); + }, }, }); builder.addNumberInput({ @@ -211,26 +295,59 @@ plugin.setPanelOptions((builder) => { }); ////////------------ General Matrix Options ----------------///////////// + builder.addSelect({ + path: 'aggregationMethod', + name: 'Aggregation Method', + description: 'How to aggregate values when multiple data points map to the same cell', + category: OptionsCategory, + showIf: (config: MatrixOptions) => config.enableColGrouping || config.enableRowGrouping, + defaultValue: 'sum', + settings: { + allowCustomValue: false, + options: [ + { value: 'sum', label: 'Sum' }, + { value: 'avg', label: 'Average' }, + { value: 'min', label: 'Min' }, + { value: 'max', label: 'Max' }, + { value: 'last', label: 'Last' }, + ], + }, + }); builder.addBooleanSwitch({ path: 'showLegend', name: 'Show Legend', + description: + 'Adds a legend below the matrix, built from whatever colors the cells — the steps under Standard Options > Thresholds, or the active color ramp.', category: OptionsCategory, defaultValue: false, }); builder.addSelect({ path: 'legendType', name: 'Legend Type', + description: + 'Auto follows the cell color source. Pin a form to override it — note that a form which disagrees with how cells are colored will read as misleading.', category: OptionsCategory, showIf: legendBool(true), - defaultValue: 'range', + defaultValue: 'auto', settings: { allowCustomValue: false, options: [ - { value: 'categorical', label: 'categorical' }, - { value: 'range', label: 'range' }, + { value: 'auto', label: 'Auto' }, + { value: 'thresholds', label: 'Thresholds (steps)' }, + { value: 'gradient', label: 'Gradient (continuous)' }, + { value: 'categorical', label: 'Categorical (swatches)' }, ], }, }); + builder.addNumberInput({ + path: 'legendMin', + name: 'Legend Min', + description: + 'Lower bound of the legend scale. The lowest threshold step is unbounded below, so it normally reads "< 30"; with a bound it reads "0 – 30", and hovering it highlights only that range. Leave blank for unbounded.', + category: OptionsCategory, + showIf: legendBool(true), + defaultValue: 0, + }); builder.addTextInput({ path: 'sourceText', name: 'Source Text', @@ -284,6 +401,82 @@ plugin.setPanelOptions((builder) => { defaultValue: 5, }); + builder.addNumberInput({ + path: 'cellCornerRadius', + name: 'Cell Corner Radius', + description: 'Rounds the corners of each cell (pixels). Set to 0 for square cells.', + category: OptionsCategory, + settings: { + placeholder: 'Auto', + integer: true, + min: 0, + max: 20, + }, + defaultValue: 2, + }); + + builder.addBooleanSwitch({ + path: 'highlightRowCol', + name: 'Highlight Row & Column on Hover', + description: 'Dims other cells and emphasizes the hovered cell\'s row and column.', + category: OptionsCategory, + defaultValue: true, + }); + + builder.addBooleanSwitch({ + path: 'showCellValues', + name: 'Show Values in Cells', + description: 'Prints the value inside each cell when it is large enough to fit.', + category: OptionsCategory, + defaultValue: true, + }); + + builder.addBooleanSwitch({ + path: 'showGroupDividers', + name: 'Show Category Group Dividers', + description: 'Draws subtle background bands behind grouped rows/columns.', + category: OptionsCategory, + showIf: (config: MatrixOptions) => config.enableColGrouping || config.enableRowGrouping, + defaultValue: true, + }); + + builder.addBooleanSwitch({ + path: 'focusEnabled', + name: 'Focus Value Range', + description: 'Spotlight cells whose value falls within a range; others are dimmed or hidden.', + category: OptionsCategory, + defaultValue: false, + }); + builder.addNumberInput({ + path: 'focusMin', + name: 'Focus Min', + description: 'Lower bound of the focused value range (leave blank for no lower bound).', + category: OptionsCategory, + showIf: (config: MatrixOptions) => config.focusEnabled, + }); + builder.addNumberInput({ + path: 'focusMax', + name: 'Focus Max', + description: 'Upper bound of the focused value range (leave blank for no upper bound).', + category: OptionsCategory, + showIf: (config: MatrixOptions) => config.focusEnabled, + }); + builder.addSelect({ + path: 'focusMode', + name: 'Out-of-Focus Cells', + description: 'How to treat cells outside the focused range.', + category: OptionsCategory, + showIf: (config: MatrixOptions) => config.focusEnabled, + defaultValue: 'dim', + settings: { + allowCustomValue: false, + options: [ + { value: 'dim', label: 'Dim' }, + { value: 'hide', label: 'Hide' }, + ], + }, + }); + builder.addNumberInput({ path: 'txtLength', name: 'Text Length', @@ -311,21 +504,143 @@ plugin.setPanelOptions((builder) => { }, defaultValue: 10, }); + builder.addSelect({ + path: 'colorMode', + name: 'Cell Color Mode', + description: + 'How cell colors are derived. Sequential = single-hue ramp for magnitude. Diverging = two hues around a baseline. Standard = use the Standard Options color scheme / thresholds.', + category: ColorCategory, + defaultValue: 'standard', + settings: { + allowCustomValue: false, + options: [ + { value: 'standard', label: 'Standard Options (thresholds)' }, + { value: 'sequential', label: 'Sequential (single hue)' }, + { value: 'diverging', label: 'Diverging (around a baseline)' }, + ], + }, + }); + builder.addNumberInput({ + path: 'divergingMidpoint', + name: 'Diverging Midpoint', + description: 'The value treated as neutral (gray). Values above and below diverge to the two hues.', + category: ColorCategory, + showIf: (config: MatrixOptions) => config.colorMode === 'diverging', + defaultValue: 0, + }); + builder.addSelect({ + path: 'colorDomainMode', + name: 'Color Scale Range', + description: + 'Auto scales colors to this panel\'s min/max. Manual lets you pin the range so multiple matrix panels share one comparable scale.', + category: ColorCategory, + showIf: (config: MatrixOptions) => config.colorMode !== 'standard', + defaultValue: 'auto', + settings: { + allowCustomValue: false, + options: [ + { value: 'auto', label: 'Auto (data min/max)' }, + { value: 'manual', label: 'Manual (fixed range)' }, + ], + }, + }); + builder.addNumberInput({ + path: 'colorDomainMin', + name: 'Color Scale Min', + description: 'Value mapped to the low end of the color scale.', + category: ColorCategory, + showIf: (config: MatrixOptions) => config.colorMode !== 'standard' && config.colorDomainMode === 'manual', + defaultValue: 0, + }); + builder.addNumberInput({ + path: 'colorDomainMax', + name: 'Color Scale Max', + description: 'Value mapped to the high end of the color scale.', + category: ColorCategory, + showIf: (config: MatrixOptions) => config.colorMode !== 'standard' && config.colorDomainMode === 'manual', + defaultValue: 100, + }); + builder.addBooleanSwitch({ + path: 'distinctNoData', + name: 'Distinguish No-Data Cells', + description: 'Renders cells with no matching data as empty/outlined so they read differently from null values.', + category: ColorCategory, + defaultValue: true, + }); builder.addColorPicker({ path: 'nullColor', name: 'Null Color', description: 'The color to use when the query returns a null value', - category: OptionsCategory, + category: ColorCategory, defaultValue: '#E6E6E6', }); builder.addColorPicker({ path: 'defaultColor', name: 'No Data Color', description: 'The color to use when there is no data returned by the query', - category: OptionsCategory, + category: ColorCategory, defaultValue: '#E6E6E6', }); + /////////----------- Layout & encoding options ---------------//////////////// + builder.addSelect({ + path: 'sizeMode', + name: 'Sizing', + description: 'Fit scales cells to fill the panel. Fixed uses the exact Cell Size in pixels (may scroll).', + category: LayoutCategory, + defaultValue: 'fit', + settings: { + allowCustomValue: false, + options: [ + { value: 'fit', label: 'Fit to panel' }, + { value: 'fixed', label: 'Fixed cell size' }, + ], + }, + }); + builder.addBooleanSwitch({ + path: 'freezeLabels', + name: 'Freeze Axis Labels', + description: 'Keeps the row and column labels pinned while the cell grid scrolls.', + category: LayoutCategory, + defaultValue: true, + }); + builder.addSelect({ + path: 'labelOrientation', + name: 'Column Label Orientation', + description: 'Auto shows horizontal labels when they fit in a cell, otherwise rotates them vertically.', + category: LayoutCategory, + defaultValue: 'auto', + settings: { + allowCustomValue: false, + options: [ + { value: 'auto', label: 'Auto' }, + { value: 'rotated', label: 'Rotated (vertical)' }, + { value: 'horizontal', label: 'Horizontal' }, + ], + }, + }); + builder.addBooleanSwitch({ + path: 'showTableView', + name: 'Show as Table', + description: 'Renders an accessible HTML table instead of the matrix — a text/screen-reader-friendly view of the same data.', + category: LayoutCategory, + defaultValue: false, + }); + builder.addBooleanSwitch({ + path: 'showMarginalTotals', + name: 'Show Marginal Totals', + description: 'Adds small summary bars of row and column totals along the edges.', + category: LayoutCategory, + defaultValue: false, + }); + builder.addBooleanSwitch({ + path: 'sizeEncodesValue', + name: 'Size Encodes Value', + description: 'Scales each cell\'s filled area by its value, so magnitude reads through both color and size.', + category: LayoutCategory, + defaultValue: false, + }); + /////////----------- Link URL options ---------------//////////////// builder.addBooleanSwitch({ path: 'addUrl', diff --git a/src/types.ts b/src/types.ts index b8edff1..9e81a14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,10 +15,40 @@ export interface MatrixOptions { rowCategoryGap: number; cellSize: number; cellPadding: number; + cellCornerRadius: number; txtLength: number; txtSize: number; nullColor: string; defaultColor: string; + distinctNoData: boolean; + highlightRowCol: boolean; + // Color presets (Phase 2) + colorMode: string; // 'standard' | 'sequential' | 'diverging' + divergingMidpoint: number; + // Shared / explicit color domain (Round 2) + colorDomainMode: string; // 'auto' | 'manual' + colorDomainMin: number; + colorDomainMax: number; + // Value-threshold focus (Round 2) + focusEnabled: boolean; + focusMin: number; + focusMax: number; + focusMode: string; // 'dim' | 'hide' + // Sorting / seriation (Phase 2 + Round 2) + rowSort: string; // 'name' | 'total' | 'cluster' + colSort: string; // 'name' | 'total' | 'cluster' + // Grouping visuals (Phase 2) + showGroupDividers: boolean; + // In-cell value labels (Phase 2) + showCellValues: boolean; + // Layout & encoding (Phase 3) + sizeMode: string; // 'fixed' | 'fit' + freezeLabels: boolean; + showMarginalTotals: boolean; + sizeEncodesValue: boolean; + // Interaction & accessibility (Round 2) + labelOrientation: string; // 'auto' | 'rotated' | 'horizontal' + showTableView: boolean; sourceText: string; targetText: string; valueText: string; @@ -32,8 +62,8 @@ export interface MatrixOptions { staticRows: string; staticColumns: string; showLegend: boolean; - legendType: string; - thresholds: any[]; + legendType: string; // 'auto' | 'thresholds' | 'gradient' | 'categorical' + legendMin?: number; // floor of the first threshold band; blank = unbounded below } export type Category = { @@ -55,7 +85,7 @@ export type MatrixData = { colCategories: Category[]; rowCategories: Category[]; data: DataMatrixCell[][] | string | null; - legend: LegendData[] | null; + legend: LegendModel | null; }; export type DataMatrixCell = { @@ -66,7 +96,54 @@ export type DataMatrixCell = { display: DisplayValue; }; -export type LegendData = { +/** Which form the legend takes. Chosen automatically from the cell color source + * unless the user pins one via the Legend Type option. */ +export type LegendKind = 'thresholds' | 'gradient' | 'categorical'; + +/** One discrete legend entry (a threshold step, or a distinct category). + * `from`/`to` describe the half-open value band [from, to) the entry stands for, + * so hovering it can highlight exactly the cells it covers. `null` means unbounded + * on that side, and `from === to` means the single exact value (categorical entries). */ +export type LegendItem = { label: string; color: string; + from: number | null; + to: number | null; + /** `from` formatted like the cells — the boundary printed under a stepped bar. + * Empty for the unbounded first step. */ + fromLabel: string; +}; + +/** A single color stop along a continuous legend bar. `offset` is 0..1. */ +export type LegendStop = { + offset: number; + color: string; +}; + +/** A labeled tick on a continuous legend bar. `offset` is 0..1 along the bar. + * `emphasis` marks a tick that should also be drawn as a line across the bar + * (used for the diverging midpoint). */ +export type LegendTick = { + value: number; + label: string; + offset: number; + emphasis?: boolean; +}; + +export type LegendModel = { + kind: LegendKind; + /** Value-field name/label, drawn above the legend. */ + title: string; + /** Entries for `thresholds` and `categorical`; empty for `gradient`. */ + items: LegendItem[]; + /** Color stops for `gradient`; empty otherwise. */ + stops: LegendStop[]; + /** Value range a `gradient` bar spans; null when there is nothing numeric to span. */ + domain: { min: number; max: number } | null; + /** Axis labels under a `gradient` bar. */ + ticks: LegendTick[]; + /** Whether to draw the null / no-data key. */ + states: { hasNull: boolean; hasNoData: boolean }; + /** How many `categorical` entries were dropped past the display cap (0 when none). */ + truncated: number; }; diff --git a/src/verify.test.tsx b/src/verify.test.tsx new file mode 100644 index 0000000..7cf00cc --- /dev/null +++ b/src/verify.test.tsx @@ -0,0 +1,172 @@ +import { toDataFrame, FieldType, createTheme, getDisplayProcessor, ThresholdsMode } from '@grafana/data'; +import { parseData } from './dataParser'; + +const theme = createTheme(); + +const thresholds = { + mode: ThresholdsMode.Absolute, + steps: [ + { value: -Infinity, color: 'green' }, + { value: 20, color: 'red' }, + ], +}; + +function makeData(withCategories = false, values?: any[]) { + const src = ['a', 'a', 'a', 'b', 'b', 'c', 'c']; + const tgt = ['x', 'y', 'z', 'x', 'y', 'x', 'z']; + const gbps = values ?? [10, 5, null, 0, 20, 7, 100]; + const fields: any[] = [ + { name: 'source', type: FieldType.string, values: src }, + { name: 'target', type: FieldType.string, values: tgt }, + { name: 'gbps', type: FieldType.number, values: gbps, config: { thresholds } }, + ]; + if (withCategories) { + fields.push({ name: 'src_region', type: FieldType.string, values: ['R1', 'R1', 'R1', 'R2', 'R2', 'R1', 'R2'] }); + fields.push({ name: 'dst_region', type: FieldType.string, values: ['C1', 'C2', 'C2', 'C1', 'C2', 'C1', 'C2'] }); + } + const frame = toDataFrame({ fields }); + const vf = frame.fields.find((f) => f.type === FieldType.number)!; + vf.display = getDisplayProcessor({ field: vf, theme }); + return { series: [frame] } as any; +} + +const base: any = { + sourceField: 'source', + targetField: 'target', + valueField: 'gbps', + sourceText: 'From', + targetText: 'To', + valueText: 'Gbps', + cellSize: 20, + cellPadding: 5, + txtLength: 50, + txtSize: 10, + nullColor: '#E6E6E6', + defaultColor: '#E6E6E6', +}; + +const combos: Array<[string, any, boolean]> = [ + ['standard + threshold legend', { ...base, colorMode: 'standard', showLegend: true, legendType: 'auto' }, false], + ['sequential + legend', { ...base, colorMode: 'sequential', showLegend: true, legendType: 'auto' }, false], + ['diverging + midpoint', { ...base, colorMode: 'diverging', divergingMidpoint: 20, showLegend: true, legendType: 'auto' }, false], + ['standard + categorical legend', { ...base, colorMode: 'standard', showLegend: true, legendType: 'categorical' }, false], + ['legacy legendType "range"', { ...base, colorMode: 'standard', showLegend: true, legendType: 'range' }, false], + ['manual domain', { ...base, colorDomainMode: 'manual', colorDomainMin: 0, colorDomainMax: 50, showLegend: true }, false], + ['sort total', { ...base, rowSort: 'total', colSort: 'total' }, false], + ['sort cluster', { ...base, rowSort: 'cluster', colSort: 'cluster' }, false], + ['grouping + cluster', { ...base, enableColGrouping: true, colCategoryField: 'dst_region', enableRowGrouping: true, rowCategoryField: 'src_region', rowSort: 'cluster', colSort: 'cluster', showLegend: true }, true], +]; + +describe('parseData runs without runtime errors across option combos', () => { + test.each(combos)('%s', (_n, options, withCats) => { + const parsed = parseData(makeData(withCats), options, theme); + expect(parsed).toBeTruthy(); + expect(Array.isArray(parsed.rows)).toBe(true); + expect(Array.isArray(parsed.columns)).toBe(true); + expect(Array.isArray(parsed.data)).toBe(true); + // data matrix shape matches rows x columns + expect(parsed.data!.length).toBe(parsed.rows!.length); + (parsed.data as any[][]).forEach((row: any[]) => expect(row.length).toBe(parsed.columns!.length)); + // marginal totals align to axes + expect(parsed.rowTotals.length).toBe(parsed.rows!.length); + expect(parsed.colTotals.length).toBe(parsed.columns!.length); + }); +}); + +describe('null vs no-data distinction', () => { + test('null value cell is flagged isNull; missing pair stays -1 (no data)', () => { + const parsed = parseData(makeData(false), { ...base }, theme); + const ri = parsed.rows!.indexOf('a'); + const ci = parsed.columns!.indexOf('z'); + // a->z was present with a null value => null cell + const nullCell = parsed.data![ri][ci]; + expect(nullCell).not.toBe(-1); + expect(nullCell.isNull).toBe(true); + // b->z never appeared in the data => no-data sentinel + const bi = parsed.rows!.indexOf('b'); + const bz = parsed.data![bi][parsed.columns!.indexOf('z')]; + expect(bz).toBe(-1); + }); +}); + +describe('cluster seriation returns a valid permutation', () => { + test('rows/columns are a permutation of the name-sorted set', () => { + const named = parseData(makeData(false), { ...base, rowSort: 'name', colSort: 'name' }, theme); + const clustered = parseData(makeData(false), { ...base, rowSort: 'cluster', colSort: 'cluster' }, theme); + expect([...clustered.rows!].sort()).toEqual([...named.rows!].sort()); + expect([...clustered.columns!].sort()).toEqual([...named.columns!].sort()); + }); +}); + +describe('color domain is computed from the data', () => { + test('auto valueDomain reflects the data min/max (values [0..100])', () => { + const parsed = parseData(makeData(false), { ...base, colorMode: 'sequential' }, theme); + expect(parsed.valueDomain).toEqual({ min: 0, max: 100 }); + }); + test('manual valueDomain reflects manual min/max', () => { + const parsed = parseData(makeData(false), { ...base, colorMode: 'sequential', colorDomainMode: 'manual', colorDomainMin: -10, colorDomainMax: 200 }, theme); + expect(parsed.valueDomain).toEqual({ min: -10, max: 200 }); + }); + test('sequential cells actually vary in color across the domain', () => { + const parsed = parseData(makeData(false), { ...base, colorMode: 'sequential' }, theme); + const colors = new Set(); + (parsed.data as any[][]).forEach((row: any[]) => + row.forEach((cell: any) => { + if (cell !== -1 && !cell.isNull) { + colors.add(cell.color); + } + }) + ); + // if the domain were empty/broken every cell would share one color + expect(colors.size).toBeGreaterThan(1); + }); +}); + +describe('legend comes out of the parser describing the real color source', () => { + test('default color mode is thresholds, and the legend is built from them', () => { + const parsed = parseData(makeData(false), { ...base, showLegend: true }, theme); + expect(parsed.legend!.kind).toBe('thresholds'); + // the two configured steps, not 10 samples of the data range + expect(parsed.legend!.items).toHaveLength(2); + expect(parsed.legend!.items.map((i: any) => [i.from, i.to])).toEqual([ + [null, 20], + [20, null], + ]); + }); + + test('the null / no-data key reflects the matrix the parser just built', () => { + // a->z is present but null; b->z never appears at all + const parsed = parseData(makeData(false), { ...base, showLegend: true }, theme); + expect(parsed.legend!.states).toEqual({ hasNull: true, hasNoData: true }); + }); + + test('a ramp color mode yields a gradient instead', () => { + const parsed = parseData(makeData(false), { ...base, colorMode: 'sequential', showLegend: true }, theme); + expect(parsed.legend!.kind).toBe('gradient'); + expect(parsed.legend!.stops.length).toBeGreaterThan(1); + }); + + test('no legend is built when the option is off', () => { + const parsed = parseData(makeData(false), { ...base, showLegend: false }, theme); + expect(parsed.legend).toBeNull(); + }); +}); + +describe('sort by total orders by descending marginal sum', () => { + test('row with the largest total sorts first', () => { + // totals: a=15, b=20, c=107 => expected order c, b, a + const parsed = parseData(makeData(false), { ...base, rowSort: 'total' }, theme); + expect(parsed.rows![0]).toBe('c'); + }); +}); + +describe('empty / degenerate inputs do not throw', () => { + test('no series returns null data', () => { + const parsed = parseData({ series: [] } as any, { ...base }, theme); + expect(parsed.data).toBeNull(); + }); + test('all-null values still parse (every present cell is null)', () => { + const parsed = parseData(makeData(false, [null, null, null, null, null, null, null]), { ...base }, theme); + expect(Array.isArray(parsed.data)).toBe(true); + }); +});