Compare commits

...

103 Commits

Author SHA1 Message Date
Abhi Kumar
14fadfba14 fix(dashboard): rank top lists of any size 2026-11-04 02:13:22 +05:30
Abhi Kumar
c3985edb97 Merge branch 'feat/top-list-panel-spec' into feat/top-list-panel 2026-11-04 01:55:01 +05:30
Abhi Kumar
e8b93fcec7 Merge remote-tracking branch 'origin/feat/geo-map-panel' into feat/top-list-panel-spec 2026-11-04 01:54:51 +05:30
Abhi Kumar
dc30218668 Merge remote-tracking branch 'origin/feat/geo-map-chart' into feat/geo-map-panel 2026-11-04 01:53:43 +05:30
Abhi Kumar
1bc56ebf7f Merge remote-tracking branch 'origin/feat/geo-map-panel-spec' into feat/geo-map-chart 2026-11-04 01:52:54 +05:30
Abhi Kumar
947e36a62c Merge remote-tracking branch 'origin/main' into feat/geo-map-panel-spec 2026-11-04 01:52:24 +05:30
Abhi Kumar
8e4607b121 Merge commit 'ea2bffa3a47a6ae98c5aac484f99130078fadaea' into HEAD 2026-11-04 01:50:29 +05:30
Abhi Kumar
ea2bffa3a4 Merge remote-tracking branch 'origin/feat/geo-map-panel' into HEAD 2026-11-04 01:50:15 +05:30
Abhi Kumar
bc86199d1f fix(dashboard): clear formula limits a new panel kind hides 2026-11-04 01:49:55 +05:30
Abhi Kumar
b3f4721b12 Merge branch 'feat/top-list-panel-spec' into feat/top-list-panel 2026-11-04 01:32:08 +05:30
Abhi Kumar
c24dc451bc Merge remote-tracking branch 'origin/feat/geo-map-panel' into feat/top-list-panel-spec 2026-11-04 01:32:01 +05:30
Abhi Kumar
1d24ca7948 Merge remote-tracking branch 'origin/feat/geo-map-chart' into feat/geo-map-panel 2026-11-04 01:30:10 +05:30
Abhi Kumar
8af7a26f52 fix(dashboard): find geo map coordinates across group and value columns 2026-11-04 01:29:50 +05:30
Abhi Kumar
90711c5c46 fix(charts): keep an explicit color ramp minimum below the log floor 2026-11-04 01:29:40 +05:30
Abhi Kumar
27c1484d0f test(charts): check the geo map zoom buttons through aria-disabled 2026-11-04 01:27:44 +05:30
Abhi Kumar
7f2dbb9e1d test(dashboards): expect the heatmap's log colour bar to start at its floor 2026-11-04 01:27:44 +05:30
Abhi Kumar
f9708b8eae refactor(dashboard): rename shared color ramp editor fields 2026-11-04 01:06:35 +05:30
Abhi Kumar
4f739959e4 fix(dashboard): address geo map panel review
Coordinate-named numeric columns stay colorable unless they place points.
2026-11-04 01:04:49 +05:30
Abhi Kumar
dcee1b5bc5 docs(charts): describe each geo outlines script 2026-11-04 00:48:46 +05:30
Abhi Kumar
51cd9d11b2 docs(charts): restore geo outlines build note 2026-11-04 00:46:06 +05:30
Vinicius Lourenço
9d465d54f5 test(e2e): retry clicks swallowed by dnd-kit and loading antd buttons (#13401)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
#### Description

- `app-shell/sidenav` TC-03: dnd-kit stops every click for 50 ms after a
drop, so the Save click that follows the drag was sometimes lost and no
preference request went out. `saveShortcuts` now clicks again while the
dialog is still open.
- `messaging-queues/kafka` TC-01: the health check button is a `loading`
antd `Button` while its onboarding queries fetch, and a loading antd
button ignores clicks without being disabled. `openHealthCheck` now
clicks again until the dialog opens.
- Both tests came in with #13189 and have been failing or needing
retries in CI on unrelated PRs. Notes added to
`tests/e2e/docs/flake-notes.md`.
2026-11-03 19:13:45 +00:00
Abhi Kumar
f7bf99a3dd chore(claude): drop geo outlines rule 2026-11-04 00:27:12 +05:30
Abhi Kumar
78b447c768 docs(charts): document geo map iso3 and tidy tooltip footer props 2026-11-04 00:17:49 +05:30
Abhi Kumar
60d9912efb ci(charts): check geo map outlines match their build
Runs only on PRs touching the outline inputs or outputs, so it must not be a required check.
2026-11-04 00:17:49 +05:30
Abhi Kumar
7037a6cf49 test(charts): narrow geo map stories to visual snapshots
Stories exist to catch visual changes, so the controls playground and asset table go.
2026-11-04 00:17:35 +05:30
Abhi kumar
7d95fa7b25 fix(query-builder): added query follows the previous query's signal (#13323)
#### Description

- In the V2 panel editor, "Add query" always created a Metrics query:
`createNewBuilderQuery` fell back to `DataSource.METRICS` because the
panel editor passes no fixed `initialDataSource`. A logs or traces panel
got a metrics query B.
- The new query now continues the last query's signal (and its `source`,
so a meter query stays meter). Hosts with a fixed data source
(explorers, alerts) are unchanged.
- No story adds a query, so no story renders differently.

#### Issues closed by this PR

Closes SigNoz/pulse-pod#659

#### Additional Information

- Stacked on #13315; #13313 (query builder e2e) sits on top and covers
this with a regression test.
2026-11-03 17:47:48 +00:00
Abhi kumar
cd1068e1c3 fix(query-builder): legacy having casing and non-positive limits (#13315)
#### Description

- **Legacy having:** the old having filter stored `COUNT()` (upper-cased
function). `convertHavingToExpression` passed it through and the v5
backend rejects it with a 400 (`Invalid references in Having expression:
[COUNT]`), so any saved legacy query with a having failed to run. Only
the function name is now lower-cased; attribute keys keep their case.
- **Limit:** a negative limit was sent as-is and the backend returned a
400. The query and formula Limit inputs now accept digits only (`-10`
shows as `10`), so the shown value is always what's sent. Empty or `0`
means no limit.
- `InputWithLabel` gains an optional `sanitize` prop for this.

#### Issues closed by this PR

Closes SigNoz/pulse-pod#657

#### Additional Information

- Found by the query builder e2e suite (#13313), where both cases are
`test.fixme` until this merges. The negative-limit case should expect
the sign to be dropped.
2026-11-03 17:43:47 +00:00
Nityananda Gohain
dca68d89bc feat(stats): report AI observability usage from the stats endpoint (#13359)
#### Description

 New stats from `/api/v1/stats`:

- `telemetry.traces.gen_ai.request.model.exists` – a span carried a
request model since the start of the previous day.
  - `telemetry.traces.gen_ai.tool.name.exists` – same, for a tool name.
- `telemetry.traces.gen_ai.agent.name.exists` – same, for an agent name.
- `llmpricingrule.custom.count` – pricing rules users added themselves.
  - `llmpricingrule.override.count` – synced default rules users pinned.
- `llmpricingrule.unmapped_model.count` – models seen in that window
that no rule prices.
  - `dashboard.panels.ai.count` – panels with an AI builder query.
 
The gen_ai values come from the traces tag table through the metadata
store, never from a span scan. The model list is capped at 100 names,
which is more than any org is expected to use in two days; past the cap
the unmapped count is a lower bound.

Existing stats change: every `dashboard.*` count now skips system
dashboards, and AI queries no longer count under
`dashboard.panels.traces.count`.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Closes https://github.com/SigNoz/nerve-pod/issues/577

  #### Additional Information

- `/api/v1/stats` latency against `main` on the same ClickHouse with
200k spans: sequential p50 14 ms → 21 ms, at 10 concurrent callers p50
78 ms → 101 ms. The three lookups run concurrently and cost about 8 ms
each regardless of limit.
- If ClickHouse is unreachable the rule counts are still reported and
only the gen_ai keys are omitted, so the reporter keeps their last
value.
2026-11-03 17:15:15 +00:00
Vishal Sharma
1be806c58d fix(ai-assistant): open explorer chips in the view the query implies (#11733)
#### Description
- Noz's `apply_filter` ("Open in Explorer") chips always opened the
Explorer in List view: the handler dropped the query's `requestType` and
never set `panelTypes`, so a grouped "count by service" landed on raw
rows.
- The chip now opens the view the query implies, on both the on-page
redirect and off-page navigation: `scalar` → Table, `time_series` → Time
Series, `trace` → Traces (traces signal only), anything else → List.

#### Issues closed by this PR


https://github.com/orgs/SigNoz/projects/59/views/18?pane=issue&itemId=292161688&issue=SigNoz%7Cnerve-pod%7C584
#### Screenshots / Screen Recordings
Prev - 


https://github.com/user-attachments/assets/a5f9f45c-6460-4109-a061-e2d49901d438



Now - 


https://github.com/user-attachments/assets/15c17c68-538a-43b9-965a-4440ec24a1ec



#### Additional Information
- Reviewable commit by commit: the fix, the `panelTypes` encoding
regression test, then the `trace` mapping from review.
- `distribution` isn't mapped: `query_range` rejects it, so the agent
can't send it.
- `trace` stays on List for logs and metrics, which have no Traces view.
- The existing `[apply_filter]` debug logs are left untouched.

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
Co-authored-by: Gaurav Tewari <gauravtewari111@gmail.com>
2026-11-03 17:10:21 +00:00
Abhi Kumar
880ec26c9f Merge branch 'feat/top-list-panel-spec' into feat/top-list-panel 2026-11-03 10:40:07 +05:30
Abhi Kumar
c74e0d69e9 Merge branch 'feat/geo-map-panel' into feat/top-list-panel-spec 2026-11-03 10:40:03 +05:30
Abhi Kumar
55de87e8e1 fix(dashboard): read numeric SQL coordinates as geo map locations 2026-11-03 10:39:54 +05:30
Abhi Kumar
bb86fc8fca Merge branch 'feat/top-list-panel-spec' into feat/top-list-panel 2026-11-03 10:34:04 +05:30
Abhi Kumar
4952d0b9c5 Merge branch 'feat/geo-map-panel' into feat/top-list-panel-spec 2026-11-03 10:34:00 +05:30
Abhi Kumar
bd15313096 test(dashboard): split geo map panel tests by behaviour 2026-11-03 10:33:44 +05:30
Abhi Kumar
bb6542524d fix(dashboard): keep geo map points on area change, exact drilldown values and row cap 2026-11-03 10:33:40 +05:30
Abhi Kumar
7b2724871b Merge branch 'origin/feat/top-list-panel-spec' into feat/top-list-panel 2026-11-03 10:25:54 +05:30
Abhi Kumar
7de589c04e Merge branch 'origin/feat/geo-map-panel' into feat/top-list-panel-spec 2026-11-03 10:25:50 +05:30
Abhi Kumar
133b50ebbe Merge branch 'feat/geo-map-chart' into feat/geo-map-panel 2026-11-03 10:25:46 +05:30
Abhi Kumar
21f978da41 test(charts): split geo map tests by helper 2026-11-03 10:25:26 +05:30
Abhi Kumar
f7324c51b3 fix(charts): keep geo map retry clickable and reject inherited names 2026-11-03 10:25:19 +05:30
Abhi Kumar
ab029fc0ca Merge branch 's-top-list-panel-spec' into s-top-list-panel 2026-11-03 10:16:53 +05:30
Abhi Kumar
b49259f2a7 Merge branch 's-geo-map-panel' into s-top-list-panel-spec 2026-11-03 10:16:52 +05:30
Abhi Kumar
8d1365d16a Merge branch 's-geo-map-chart' into s-geo-map-panel
# Conflicts:
#	frontend/src/pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/components/PanelTypePreview/panelTypePreviews.tsx
2026-11-03 10:16:51 +05:30
Abhi Kumar
682afc26d6 Merge branch 'feat/geo-map-panel-spec' into s-geo-map-chart
# Conflicts:
#	frontend/package.json
#	frontend/pnpm-lock.yaml
2026-11-03 10:16:30 +05:30
Abhi Kumar
7ce8285091 Merge remote-tracking branch 'origin/main' into feat/geo-map-panel-spec
# Conflicts:
#	docs/api/openapi.yml
#	pkg/types/dashboardtypes/perses_dashboard_test.go
#	pkg/types/dashboardtypes/perses_plugin_wrappers.go
#	pkg/types/dashboardtypes/perses_public_dashboard.go
#	pkg/types/dashboardtypes/perses_public_dashboard_panel_query_test.go
#	pkg/types/dashboardtypes/perses_signoz_plugins.go
2026-11-03 10:14:51 +05:30
Abhi Kumar
ab5c434ce2 feat(dashboard): download top list panels as csv 2026-11-02 16:12:31 +05:30
Abhi Kumar
7192819499 chore(openapi): regenerate specs 2026-11-02 16:12:31 +05:30
Abhi Kumar
8632444cbe feat(dashboard): add the top list panel 2026-11-02 16:12:31 +05:30
Abhi Kumar
2f14e74786 feat(dashboards): add plugin schema for top list panel 2026-11-02 16:12:31 +05:30
Abhi Kumar
a92fae6c31 feat(dashboard): add an appearance section for rank, share and bar color 2026-11-02 16:12:31 +05:30
Abhi Kumar
dd4a25e188 feat(dashboard): draw top list rows 2026-11-02 16:12:31 +05:30
Abhi Kumar
9c921d980a feat(dashboard): rank top list rows 2026-11-02 16:12:31 +05:30
Abhi Kumar
759f966581 refactor(charts): share the scatter tooltip card 2026-11-02 16:12:31 +05:30
Abhi Kumar
a412555b08 feat(query-builder): show the default limit placeholder only for a lone query 2026-11-02 16:12:31 +05:30
Abhi Kumar
27658e88e7 feat(dashboard): add the top list panel type 2026-11-02 16:12:31 +05:30
Abhi Kumar
bc511ba02c chore(api): regenerate client types for the top list spec 2026-11-02 16:12:31 +05:30
Abhi Kumar
4f3a73a887 feat(dashboard): let geo map values take a legend
Legend is pinned so it opens empty instead of seeding a group-by template, which
would name the value literally.
2026-11-02 16:12:24 +05:30
Abhi Kumar
bee625d7bd feat(dashboard): download geo map panels as csv 2026-11-02 16:10:55 +05:30
Abhi Kumar
4ca398963f fix(dashboard): clear fields the new panel kind hides when switching kinds 2026-11-02 16:10:55 +05:30
Abhi Kumar
ebc8b7f9c1 feat(dashboard): preview geo map placement in the panel editor 2026-11-02 16:10:55 +05:30
Abhi Kumar
c23d9c3d7b feat(dashboard): add the geo map panel 2026-11-02 16:10:55 +05:30
Abhi Kumar
b9e94d8804 feat(dashboard): add the geo map panel type 2026-11-02 16:10:55 +05:30
Abhi Kumar
8bbe890cf9 feat(dashboard): color geo map locations by threshold in the color column's unit 2026-11-02 16:10:55 +05:30
Abhi Kumar
a55363b55f feat(dashboard): edit geo map colors, bubbles and labels 2026-11-02 16:10:55 +05:30
Abhi Kumar
172c4b57c6 feat(dashboard): add the geo map location section 2026-11-02 16:10:55 +05:30
Abhi Kumar
7bbf84a1f0 feat(dashboard): place geo map rows by country, state or lat/lon 2026-11-02 16:10:55 +05:30
Abhi Kumar
59cd09cca0 feat(dashboard): group select options and describe tiles in the config pane 2026-11-02 16:10:55 +05:30
Abhi Kumar
82e0fd08e3 feat(dashboard): recompute panel warnings when their data source changes 2026-11-02 16:10:55 +05:30
Abhi Kumar
b7ddfa27ab refactor(dashboard): share query column keys for stale binding checks 2026-11-02 16:10:55 +05:30
Abhi Kumar
ae6e47a8f7 feat(charts): add geo map stories 2026-11-02 16:10:54 +05:30
Abhi Kumar
e06b07788f chore(openapi): regenerate specs 2026-11-02 16:10:54 +05:30
Abhi Kumar
57087f20d1 chore(api): regenerate client types for the geo map spec 2026-11-02 16:10:54 +05:30
Abhi Kumar
407998a39e feat(charts): add the geo map chart 2026-11-02 16:10:54 +05:30
Abhi Kumar
8e63624de8 feat(dashboards): join geo map results on public dashboards 2026-11-02 16:10:54 +05:30
Abhi Kumar
53faacea59 feat(charts): add the geo map legend, tooltip and zoom controls 2026-11-02 16:10:54 +05:30
Abhi Kumar
4c815e0224 feat(dashboards): add plugin schema for geo map panel 2026-11-02 16:10:54 +05:30
Abhi Kumar
d40aee55f0 feat(charts): draw geo map layers with OpenLayers 2026-11-02 16:10:54 +05:30
Abhi Kumar
338dabf09e feat(charts): look up geo codes and their outlines 2026-11-02 16:10:54 +05:30
Abhi Kumar
9455e6aba3 feat(charts): add an opacity mode to the colour ramp 2026-11-02 16:10:54 +05:30
Abhi Kumar
846db801e1 build(charts): build country and state outlines for the geo map 2026-11-02 16:10:54 +05:30
Abhi Kumar
3c701576af fix(charts): start the heatmap's log colour bar at its floor
A log scale can't place 0, so the bar's bottom label now shows the value the ramp
actually starts at.
2026-11-02 16:10:54 +05:30
Abhi Kumar
1fbc3e4c28 refactor(charts): share the heatmap colour ramp
The geo map reuses the palettes and scales, so they move out of the
uPlot heatmap plugin under generic names. Stored panel specs are unchanged.
2026-11-02 16:10:54 +05:30
Abhi Kumar
223b64e5af feat(dashboard): let scatter plot values take a legend
Legend is pinned so it opens empty instead of seeding a group-by template, which
would name the value literally. Value labels take the query-name prefix only when
two value columns share a name.
2026-11-02 16:10:36 +05:30
Abhi kumar
10f4364abc fix(dashboards): scatter plot save validation fixes (#13329)
#### Description

Stacked on #13030. Addresses its two open Greptile findings.

- Editing a query no longer blocks saving: scatter
`x`/`y`/`sizeBy`/`colorBy` bindings the query no longer produces are
cleared on save, so the saved panel matches the preview's fallback
columns. Done through a new optional `prepareSaveSpec` hook on query
panel definitions.
- `points.minSize` / `maxSize` are compared at their renderer defaults
(4 / 24) when one is null, so `{"maxSize": 2}` or `{"minSize": 40}` can
no longer invert the size range.

#### Issues closed by this PR

Closes SigNoz/pulse-pod#660
2026-11-02 12:02:00 +05:30
Naman Verma
3aa53d67e3 test: follow test guidelines 2026-10-30 20:00:22 +05:30
Abhi Kumar
8f87ed3ed4 chore(api): regenerate frontend api client 2026-10-30 17:15:43 +05:30
Abhi kumar
1161c249f2 Merge branch 'main' into feat/scatter-plot-panel-spec 2026-10-30 17:01:48 +05:30
Abhi kumar
3778d23a41 feat(charts): add the scatter chart layer (#13050)
#### Description

- Adds the scatter chart layer: a uPlot faceted-mode (`mode: 2`)
`ScatterPlugin` with quadtree hover/click hit-testing and a
hovered-point outline, plus a `Scatter` chart with its own tooltip.
- Per-axis scales `auto` | `linear` | `log` | `symlog` and titles.
`auto` picks log (symlog with zero or negatives) when it spreads the
middle half of the points at least 2× more than linear, over ≥ 2
decades.
- Log axes tick at 1/2/5 × 10ⁿ since the scatter range is left
unsnapped; dots stay whole at plot edges.
- Click handlers report the scatter point hit; `buildClickData` skips x
lookups on faceted plots and `ChartWrapper` forwards `plotRef`.

#### Additional Information

- Shared uPlotV2 changes affecting every chart: axes now draw an axis
line in the grid's style (off when the grid is hidden), and value-axis
tick labels outside the scale's range are blanked.
- New shared config: `PlotMode`, `DrawStyle.Scatter`,
`DistributionType.SymmetricLog`.
- Chart library and Storybook stories only; the dashboard panel lands in
#13051.
2026-10-30 17:01:17 +05:30
Naman Verma
b9104596ca fix: add dot in comment 2026-10-30 16:03:46 +05:30
Naman Verma
e9ab776ced chore: add decsriptions and todo 2026-10-30 13:25:09 +05:30
Naman Verma
eaa922cabe fix: dont allow trace operator 2026-10-30 12:27:10 +05:30
Abhi Kumar
f64bc495cc fix(dashboards): key metric aggregations by query name and reject unknown point fields 2026-10-30 12:27:10 +05:30
Naman Verma
6a42b9af90 feat: add validation to ensure queries have same group by 2026-10-30 12:27:10 +05:30
Abhi Kumar
517cba0bee feat(dashboards): add an axis to scatter plot thresholds 2026-10-30 12:27:10 +05:30
Naman Verma
118ccb8f12 chore: add more validations for scatter plot 2026-10-30 12:27:10 +05:30
Abhi Kumar
3bd710deb7 feat(dashboards): rename scatter plot dimensions to colorBy and sizeBy 2026-10-30 12:27:10 +05:30
Abhi Kumar
44e31d7dd8 chore(openapi): regenerate specs for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-30 12:27:10 +05:30
Abhi Kumar
5d411a8221 feat(dashboards): add point size and opacity to scatter plot spec
The fixed size and the min/max range are both stored so binding or unbinding
the size dimension restores the other's last setting.

Assisted-by: Claude Opus 5.5
2026-10-30 12:27:10 +05:30
Abhi Kumar
effb7df6fa chore(openapi): regenerate specs for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-30 12:27:10 +05:30
Abhi Kumar
12a0c3fd64 feat(dashboards): add axis labels and multi-key colour to scatter plot spec
An empty colour list colours by every group-by label.

Assisted-by: Claude Opus 5.5
2026-10-30 12:27:10 +05:30
Abhi Kumar
d34b7ee637 chore(openapi): regenerate specs for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-30 12:27:10 +05:30
Abhi Kumar
a802e1422e feat(dashboards): add plugin schema for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-30 12:27:10 +05:30
504 changed files with 21804 additions and 1023 deletions

32
.github/workflows/geoci.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: geoci
on:
pull_request:
branches:
- main
paths:
- frontend/scripts/geo-outlines/**
- frontend/src/lib/visualization/charts/GeoMap/assets/**
- frontend/src/lib/visualization/charts/GeoMap/geo/subdivisionCountries.ts
- frontend/pnpm-lock.yaml
jobs:
geo-outlines:
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install-pnpm
uses: pnpm/action-setup@v6
with:
version: 10
- name: install-frontend
run: cd frontend && pnpm install
- name: generate-geo-outlines
run: |
cd frontend && pnpm build:geo-outlines
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in geo map outlines. Run pnpm build:geo-outlines in frontend/ locally and commit."; exit 1)

View File

@@ -4280,6 +4280,11 @@ components:
last_6_hr, last_1_day, last_3_days, last_1_week or last_1_month pin a
fixed window.'
type: object
DashboardtypesBubbleRadius:
maximum: 64
minimum: 1
nullable: true
type: integer
DashboardtypesBuilderQuerySpec:
discriminator:
mapping:
@@ -4512,6 +4517,231 @@ components:
minimum: 0
nullable: true
type: number
DashboardtypesGeoMapBubbles:
properties:
maxRadius:
allOf:
- $ref: '#/components/schemas/DashboardtypesBubbleRadius'
description: Radius of the largest circle, in CSS pixels from 1 to 64. Null
means 8 on the points layer, 28 otherwise.
minRadius:
allOf:
- $ref: '#/components/schemas/DashboardtypesBubbleRadius'
description: Radius of the smallest circle, in CSS pixels from 1 to 64.
Null means 2 on the points layer, 3 otherwise.
type: object
DashboardtypesGeoMapChartAppearance:
properties:
bubbles:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapBubbles'
description: The circle radius range of the bubbles and points layers.
colors:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapColors'
description: The color scale applied to the colorBy values.
labels:
description: Shows country and state names once each fits inside its shape.
Null shows them.
nullable: true
type: boolean
type: object
DashboardtypesGeoMapColors:
properties:
fill:
description: The hex color varied in opacity mode. Empty uses the default
fill.
type: string
max:
description: The value mapped to the highest color. Null uses the highest
value in the data.
nullable: true
type: number
min:
description: The value mapped to the lowest color. Null uses the lowest
value in the data. Must not exceed max.
nullable: true
type: number
mode:
allOf:
- $ref: '#/components/schemas/DashboardtypesHeatmapColorMode'
description: 'How values are colored: palette (default) picks a hue from
palette, opacity varies the opacity of fill.'
palette:
allOf:
- $ref: '#/components/schemas/DashboardtypesHeatmapPalette'
description: The named color ramp used in palette mode, such as lava (default),
ice or ember.
scale:
allOf:
- $ref: '#/components/schemas/DashboardtypesHeatmapColorScale'
description: 'How values map onto the color range: log (default), sqrt or
linear.'
steps:
description: The number of distinct color steps, 2 to 128. 0 uses the chart
default.
type: integer
type: object
DashboardtypesGeoMapCountryFormat:
enum:
- auto
- alpha2
- alpha3
- name
type: string
DashboardtypesGeoMapLayer:
enum:
- regions
- bubbles
- points
type: string
DashboardtypesGeoMapLocation:
properties:
country:
description: Group-by key holding the country, for the country and subdivision
modes. Empty detects it by name, such as geo.country.iso_code.
type: string
countryFormat:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapCountryFormat'
description: 'How country values are written: auto (default) detects it,
alpha2, alpha3 or name.'
lat:
description: Group-by key holding the latitude, for the coordinates mode.
Empty detects it by name.
type: string
lon:
description: Group-by key holding the longitude, for the coordinates mode.
Empty detects it by name.
type: string
mode:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapLocationMode'
description: 'How rows are placed: auto (default) detects it from the group-by
keys, country, subdivision or coordinates.'
region:
description: Group-by key holding the ISO 3166-2 region code (e.g. geo.region.iso_code),
for the subdivision mode. Empty detects it by name.
type: string
required:
- mode
- countryFormat
type: object
DashboardtypesGeoMapLocationMode:
enum:
- auto
- country
- subdivision
- coordinates
type: string
DashboardtypesGeoMapPanelSpec:
properties:
chartAppearance:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapChartAppearance'
description: Color scale, bubble size and labels.
formatting:
allOf:
- $ref: '#/components/schemas/DashboardtypesTableFormatting'
description: Per-column units and decimal precision.
legend:
allOf:
- $ref: '#/components/schemas/DashboardtypesLegend'
description: Legend placement.
location:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapLocation'
description: The group-by columns that place each row on the map.
map:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapSettings'
description: Layer, view, projection and the value columns drawn.
thresholds:
description: Rules that recolor a shape, bubble or point when its value
compares to a threshold as specified.
items:
$ref: '#/components/schemas/DashboardtypesGeoMapThreshold'
nullable: true
type: array
visualization:
allOf:
- $ref: '#/components/schemas/DashboardtypesBasicVisualization'
description: The time range the values are computed over.
type: object
DashboardtypesGeoMapProjection:
enum:
- mercator
- equal_earth
- natural_earth
type: string
DashboardtypesGeoMapSettings:
properties:
colorBy:
description: Value column key, as in a Table panel, that sets the color.
Empty uses the first value column.
type: string
layer:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapLayer'
description: 'What is drawn: regions (default) fills each shape, bubbles
draws a circle per region, points a circle per coordinate.'
projection:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapProjection'
description: 'The map projection: mercator (default), equal_earth or natural_earth.'
sizeBy:
description: Value column key, as in a Table panel, that sets the bubble
or point size. Empty uses colorBy.
type: string
view:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapView'
description: 'The area shown: world (default), fit to the data, a continent,
or country with viewCountry.'
viewCountry:
description: Uppercase ISO 3166-1 alpha-2 code of the country the country
view shows.
type: string
required:
- layer
- view
- projection
type: object
DashboardtypesGeoMapThreshold:
properties:
color:
description: 'The CSS color of the shape, bubble or point while the threshold
is active, for example #F1575F; CSS named colors such as red also work.'
type: string
operator:
allOf:
- $ref: '#/components/schemas/DashboardtypesComparisonOperator'
description: 'The comparison that activates the threshold: above (default),
below, above_or_equal, below_or_equal, equal or not_equal.'
unit:
description: The unit id of value, such as ms or percent. Empty means the
colorBy column's unit.
type: string
value:
description: The value compared against, in unit.
format: double
type: number
required:
- value
- color
type: object
DashboardtypesGeoMapView:
enum:
- world
- fit
- africa
- asia
- europe
- north_america
- south_america
- oceania
- country
type: string
DashboardtypesGettableDashboardV2:
properties:
createdAt:
@@ -5249,6 +5479,7 @@ components:
mapping:
signoz/AreaChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
signoz/BarChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
signoz/GeoMapPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesGeoMapPanelSpec'
signoz/HeatmapPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
signoz/HistogramPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
@@ -5258,6 +5489,7 @@ components:
signoz/TablePanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
signoz/TextPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
signoz/TimeSeriesPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
signoz/TopListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTopListPanelSpec'
propertyName: kind
oneOf:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
@@ -5271,6 +5503,8 @@ components:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTopListPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesGeoMapPanelSpec'
type: object
DashboardtypesPanelPluginKind:
enum:
@@ -5285,6 +5519,8 @@ components:
- signoz/TextPanel
- signoz/HeatmapPanel
- signoz/ScatterPlotPanel
- signoz/TopListPanel
- signoz/GeoMapPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
properties:
@@ -5316,6 +5552,21 @@ components:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesGeoMapPanelSpec:
properties:
kind:
description: The panel kind.
enum:
- signoz/GeoMapPanel
type: string
spec:
allOf:
- $ref: '#/components/schemas/DashboardtypesGeoMapPanelSpec'
description: The options for this panel kind. {} accepts every default.
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec:
properties:
kind:
@@ -5451,6 +5702,21 @@ components:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTopListPanelSpec:
properties:
kind:
description: The panel kind.
enum:
- signoz/TopListPanel
type: string
spec:
allOf:
- $ref: '#/components/schemas/DashboardtypesTopListPanelSpec'
description: The options for this panel kind. {} accepts every default.
required:
- kind
- spec
type: object
DashboardtypesPanelSpec:
properties:
display:
@@ -6213,6 +6479,43 @@ components:
last_6_hr, last_1_day, last_3_days, last_1_week or last_1_month pin a
fixed window.'
type: object
DashboardtypesTopListAppearance:
properties:
barColor:
description: Hex color of every bar; a matching background threshold overrides
it.
type: string
showRank:
description: Numbers each row by its position in the list.
type: boolean
showShare:
description: Shows each row's share of the listed rows' total.
type: boolean
required:
- barColor
type: object
DashboardtypesTopListPanelSpec:
properties:
appearance:
allOf:
- $ref: '#/components/schemas/DashboardtypesTopListAppearance'
description: Rank, share and bar color options.
formatting:
allOf:
- $ref: '#/components/schemas/DashboardtypesPanelFormatting'
description: Unit and decimal precision of the values.
thresholds:
description: Rules that recolor a row when its value compares to a threshold
as specified.
items:
$ref: '#/components/schemas/DashboardtypesComparisonThreshold'
nullable: true
type: array
visualization:
allOf:
- $ref: '#/components/schemas/DashboardtypesBasicVisualization'
description: The time range the rows are computed over.
type: object
DashboardtypesUpdatableDashboardV2:
properties:
image:

View File

@@ -56,10 +56,10 @@ const config: Config.InitialOptions = {
transformIgnorePatterns: [
// @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis.
// Pattern 1: allow .pnpm virtual store through (handled by pattern 2), plus root-level ESM packages.
'node_modules/(?!(\\.pnpm|react-json-tree|react-base16-styling|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|remark-gfm|mdast-util-gfm|mdast-util-gfm-autolink-literal|mdast-util-gfm-footnote|mdast-util-gfm-strikethrough|mdast-util-gfm-table|mdast-util-gfm-task-list-item|mdast-util-find-and-replace|mdast-util-phrasing|mdast-util-to-markdown|markdown-table|longest-streak|ccount|escape-string-regexp|mdast-util-from-markdown|mdast-util-to-string|micromark|micromark-core-commonmark|micromark-extension-gfm|micromark-extension-gfm-autolink-literal|micromark-extension-gfm-footnote|micromark-extension-gfm-strikethrough|micromark-extension-gfm-table|micromark-extension-gfm-tagfilter|micromark-extension-gfm-task-list-item|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-resolve-all|micromark-util-sanitize-uri|micromark-util-subtokenize|micromark-util-symbol|micromark-util-types|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)',
'node_modules/(?!(\\.pnpm|react-json-tree|react-base16-styling|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|d3-geo|d3-array|internmap|ol|rbush|quickselect|earcut|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|remark-gfm|mdast-util-gfm|mdast-util-gfm-autolink-literal|mdast-util-gfm-footnote|mdast-util-gfm-strikethrough|mdast-util-gfm-table|mdast-util-gfm-task-list-item|mdast-util-find-and-replace|mdast-util-phrasing|mdast-util-to-markdown|markdown-table|longest-streak|ccount|escape-string-regexp|mdast-util-from-markdown|mdast-util-to-string|micromark|micromark-core-commonmark|micromark-extension-gfm|micromark-extension-gfm-autolink-literal|micromark-extension-gfm-footnote|micromark-extension-gfm-strikethrough|micromark-extension-gfm-table|micromark-extension-gfm-tagfilter|micromark-extension-gfm-task-list-item|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-resolve-all|micromark-util-sanitize-uri|micromark-util-subtokenize|micromark-util-symbol|micromark-util-types|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)',
// Pattern 2: pnpm virtual store — ignore everything except ESM-only packages.
// pnpm encodes scoped packages as @scope+name@version, so match on scope prefix.
'node_modules/\\.pnpm/(?!(react-json-tree|react-base16-styling|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|remark-gfm|mdast-util-gfm|mdast-util-gfm-autolink-literal|mdast-util-gfm-footnote|mdast-util-gfm-strikethrough|mdast-util-gfm-table|mdast-util-gfm-task-list-item|mdast-util-find-and-replace|mdast-util-phrasing|mdast-util-to-markdown|markdown-table|longest-streak|ccount|escape-string-regexp|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)[^/]*/node_modules)',
'node_modules/\\.pnpm/(?!(react-json-tree|react-base16-styling|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|d3-geo|d3-array|internmap|ol|rbush|quickselect|earcut|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|remark-gfm|mdast-util-gfm|mdast-util-gfm-autolink-literal|mdast-util-gfm-footnote|mdast-util-gfm-strikethrough|mdast-util-gfm-table|mdast-util-gfm-task-list-item|mdast-util-find-and-replace|mdast-util-phrasing|mdast-util-to-markdown|markdown-table|longest-streak|ccount|escape-string-regexp|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)[^/]*/node_modules)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -30,7 +30,8 @@
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:config:web-settings": "json2ts ./src/schemas/generated/webSettings.schema.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */'"
"generate:config:web-settings": "json2ts ./src/schemas/generated/webSettings.schema.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */'",
"build:geo-outlines": "node scripts/geo-outlines/build.mjs"
},
"engines": {
"node": ">=22.0.0",
@@ -78,6 +79,7 @@
"classnames": "2.3.2",
"color": "^4.2.1",
"crypto-js": "4.2.0",
"d3-geo": "3.1.1",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.4.15",
@@ -96,6 +98,7 @@
"monaco-editor": "0.55.1",
"motion": "12.4.13",
"nuqs": "2.8.8",
"ol": "10.11.0",
"overlayscrollbars": "^2.16.0",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
@@ -130,6 +133,7 @@
"rrule": "2.8.1",
"styled-components": "^5.3.11",
"timestamp-nano": "^1.0.0",
"topojson-client": "3.1.0",
"translation-resilience": "^0.2.0",
"typescript": "5.9.3",
"uplot": "1.6.31",
@@ -170,8 +174,10 @@
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/crypto-js": "4.2.2",
"@types/d3-geo": "3.1.1",
"@types/d3-hierarchy": "1.1.11",
"@types/event-source-polyfill": "^1.0.0",
"@types/geojson": "7946.0.16",
"@types/history": "4.7.11",
"@types/jest": "30.0.0",
"@types/lodash-es": "^4.17.4",
@@ -188,6 +194,8 @@
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/topojson-client": "3.1.5",
"@types/topojson-specification": "1.0.5",
"@typescript/native-preview": "7.0.0-dev.20260430.1",
"eslint-plugin-sonarjs": "4.0.2",
"husky": "^9.1.7",
@@ -209,6 +217,8 @@
"storybook": "10.6.3",
"stylelint": "17.15.0",
"svgo": "4.1.0",
"topojson-server": "3.0.1",
"topojson-simplify": "3.0.3",
"ts-jest": "29.4.15",
"typescript-plugin-css-modules": "5.2.0",
"use-sync-external-store": "1.6.0",

201
frontend/pnpm-lock.yaml generated
View File

@@ -150,6 +150,9 @@ importers:
crypto-js:
specifier: 4.2.0
version: 4.2.0
d3-geo:
specifier: 3.1.1
version: 3.1.1
d3-hierarchy:
specifier: 3.1.2
version: 3.1.2
@@ -204,6 +207,9 @@ importers:
nuqs:
specifier: 2.8.8
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
ol:
specifier: 10.11.0
version: 10.11.0
overlayscrollbars:
specifier: ^2.16.0
version: 2.16.0
@@ -306,6 +312,9 @@ importers:
timestamp-nano:
specifier: ^1.0.0
version: 1.0.1
topojson-client:
specifier: 3.1.0
version: 3.1.0
translation-resilience:
specifier: ^0.2.0
version: 0.2.0
@@ -385,12 +394,18 @@ importers:
'@types/crypto-js':
specifier: 4.2.2
version: 4.2.2
'@types/d3-geo':
specifier: 3.1.1
version: 3.1.1
'@types/d3-hierarchy':
specifier: 1.1.11
version: 1.1.11
'@types/event-source-polyfill':
specifier: ^1.0.0
version: 1.0.1
'@types/geojson':
specifier: 7946.0.16
version: 7946.0.16
'@types/history':
specifier: 4.7.11
version: 4.7.11
@@ -439,6 +454,12 @@ importers:
'@types/testing-library__jest-dom':
specifier: ^5.14.5
version: 5.14.5
'@types/topojson-client':
specifier: 3.1.5
version: 3.1.5
'@types/topojson-specification':
specifier: 1.0.5
version: 1.0.5
'@typescript/native-preview':
specifier: 7.0.0-dev.20260430.1
version: 7.0.0-dev.20260430.1
@@ -502,6 +523,12 @@ importers:
svgo:
specifier: 4.1.0
version: 4.1.0
topojson-server:
specifier: 3.0.1
version: 3.0.1
topojson-simplify:
specifier: 3.0.3
version: 3.0.3
ts-jest:
specifier: 29.4.15
version: 29.4.15(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.2.0)(babel-jest@29.6.4(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@30.4.1)(jest@30.2.0(@types/node@22.20.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@22.20.5)(typescript@5.9.3)))(typescript@5.9.3)
@@ -2903,6 +2930,9 @@ packages:
'@petamoriken/float16@3.8.0':
resolution: {integrity: sha512-AhVAm6SQ+zgxIiOzwVdUcDmKlu/qU39FiYD2UD6kQQaVenrn0dGZewIghWAENGQsvC+1avLCuT+T2/3Gsp/W3w==}
'@petamoriken/float16@3.9.3':
resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -3956,6 +3986,9 @@ packages:
'@types/d3-geo@3.1.0':
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
'@types/d3-geo@3.1.1':
resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==}
'@types/d3-hierarchy@1.1.11':
resolution: {integrity: sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==}
@@ -4001,8 +4034,8 @@ packages:
'@types/file-saver@2.0.5':
resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==}
'@types/geojson@7946.0.14':
resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/graceful-fs@4.1.6':
resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
@@ -4082,6 +4115,9 @@ packages:
'@types/prop-types@15.7.5':
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
'@types/rbush@4.0.0':
resolution: {integrity: sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==}
'@types/react-addons-update@0.14.21':
resolution: {integrity: sha512-HOxr0Hd8C1L4uw8DHyv2etqMVIj78oLEpe567/HgjoE+1Lc+PUsTGXTrkr1BDvFqsu5r49mSlgI5evwrk9eutA==}
@@ -4133,6 +4169,12 @@ packages:
'@types/testing-library__jest-dom@5.14.5':
resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==}
'@types/topojson-client@3.1.5':
resolution: {integrity: sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==}
'@types/topojson-specification@1.0.5':
resolution: {integrity: sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
@@ -4418,6 +4460,9 @@ packages:
'@xobotyi/scrollbar-width@1.9.5':
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
'@zarrita/storage@0.2.0':
resolution: {integrity: sha512-855ZXqtnds7spnT8vNvD+MXa3QExP1m2GqShe8yt7uZXHnQLgJHgkpVwFjE1B0KDDRO0ki09hmk6OboTaIfPsQ==}
'@zxing/text-encoding@0.9.0':
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
@@ -5217,6 +5262,10 @@ packages:
resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==}
engines: {node: '>=12'}
d3-geo@3.1.1:
resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
engines: {node: '>=12'}
d3-hierarchy@1.1.9:
resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==}
@@ -5492,6 +5541,9 @@ packages:
earcut@2.2.4:
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
earcut@3.2.4:
resolution: {integrity: sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -5759,6 +5811,9 @@ packages:
fflate@0.4.9:
resolution: {integrity: sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==}
fflate@0.8.3:
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@@ -5936,6 +5991,10 @@ packages:
resolution: {integrity: sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==}
engines: {node: '>=10.19'}
geotiff@3.1.0-beta.0:
resolution: {integrity: sha512-wbVwOaQYuN+ywly5NoMXn37nHslwkQg5EFHqMeUTtjDACrSag0EEVlFsc+2DK3Pzmmp6XzJW2RTZOamPsBM6ng==}
engines: {node: '>=10.19'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -7508,6 +7567,9 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
numcodecs@0.3.2:
resolution: {integrity: sha512-6YSPnmZgg0P87jnNhi3s+FVLOcIn3y+1CTIgUulA3IdASzK9fJM87sUFkpyA+be9GibGRaST2wCgkD+6U+fWKw==}
nuqs@2.8.8:
resolution: {integrity: sha512-LF5sw9nWpHyPWzMMu9oho3r9C5DvkpmBIg4LQN78sexIzGaeRx8DWr0uy3YiFx5i2QGZN1Qqcb+OAtEVRa2bnA==}
peerDependencies:
@@ -7584,6 +7646,9 @@ packages:
ol-mapbox-style@10.7.0:
resolution: {integrity: sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw==}
ol@10.11.0:
resolution: {integrity: sha512-OJ+lrx7kx/1O7Xx18Ff9QkVnwZIEL0OuhJQSy2sYgzzDvyZ2qHvuZEGAajbSstUyBwaS1OtQ4RWnY84tRMDdqg==}
ol@7.4.0:
resolution: {integrity: sha512-bgBbiah694HhC0jt8ptEFNRXwgO8d6xWH3G97PCg4bmn9Li5nLLbi59oSrvqUI6VPVwonPQF1YcqJymxxyMC6A==}
@@ -7808,6 +7873,10 @@ packages:
resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==}
hasBin: true
pbf@5.1.2:
resolution: {integrity: sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -8013,9 +8082,15 @@ packages:
quickselect@2.0.0:
resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
quickselect@3.0.0:
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
rbush@3.0.1:
resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==}
rbush@4.0.1:
resolution: {integrity: sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==}
rc-cascader@3.20.0:
resolution: {integrity: sha512-lkT9EEwOcYdjZ/jvhLoXGzprK1sijT3/Tp4BLxQQcHDZkkOzzwYQC9HgmKoJz0K7CukMfgvO9KqHeBdgE+pELw==}
peerDependencies:
@@ -8576,6 +8651,9 @@ packages:
resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
reference-spec-reader@0.2.0:
resolution: {integrity: sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==}
refractor@3.6.0:
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==}
@@ -9262,6 +9340,18 @@ packages:
toggle-selection@1.0.6:
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
topojson-client@3.1.0:
resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==}
hasBin: true
topojson-server@3.0.1:
resolution: {integrity: sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==}
hasBin: true
topojson-simplify@3.0.3:
resolution: {integrity: sha512-V+pBjLVzSQ3+hSOxBiV01OVXgFiCmMO8ia3huxKEyIMTC1ApQHBcdXdOqcQ6U2JJJD31TZduwY6KyF15R8sUgg==}
hasBin: true
tough-cookie@4.1.4:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
@@ -9510,6 +9600,10 @@ packages:
unrs-resolver@1.11.1:
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
unzipit@2.0.0:
resolution: {integrity: sha512-DVeVIWUZCAQPNzm5sB0hpsG1GygTTdBnzNtYYEpInkttx5evkyqRgZi6rTczoySqp8hO5jHVKzrH0f23X8FZLg==}
engines: {node: '>=18'}
update-browserslist-db@1.3.3:
resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==}
hasBin: true
@@ -9828,6 +9922,9 @@ packages:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
xml-utils@1.10.2:
resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==}
xml-utils@1.7.0:
resolution: {integrity: sha512-bWB489+RQQclC7A9OW8e5BzbT8Tu//jtAOvkYwewFr+Q9T9KDGvfzC1lp0pYPEQPEoPQLDkmxkepSC/2gIAZGw==}
@@ -9910,12 +10007,18 @@ packages:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'}
zarrita@0.7.5:
resolution: {integrity: sha512-ZriOSEne+Wf2iuM0yjDZvUKpHNlug3dO7qJ6mAxXoOgsYV18DPOkAkwB9e0rf21bHcKcOffFuwoZwn5g6pvUeA==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zstddec@0.1.0:
resolution: {integrity: sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==}
zstddec@0.2.0:
resolution: {integrity: sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==}
zustand@5.0.11:
resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
engines: {node: '>=12.20.0'}
@@ -12497,6 +12600,8 @@ snapshots:
'@petamoriken/float16@3.8.0': {}
'@petamoriken/float16@3.9.3': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -13584,7 +13689,11 @@ snapshots:
'@types/d3-geo@3.1.0':
dependencies:
'@types/geojson': 7946.0.14
'@types/geojson': 7946.0.16
'@types/d3-geo@3.1.1':
dependencies:
'@types/geojson': 7946.0.16
'@types/d3-hierarchy@1.1.11': {}
@@ -13624,7 +13733,7 @@ snapshots:
'@types/file-saver@2.0.5': {}
'@types/geojson@7946.0.14': {}
'@types/geojson@7946.0.16': {}
'@types/graceful-fs@4.1.6':
dependencies:
@@ -13713,6 +13822,8 @@ snapshots:
'@types/prop-types@15.7.5': {}
'@types/rbush@4.0.0': {}
'@types/react-addons-update@0.14.21':
dependencies:
'@types/react': 18.0.26
@@ -13783,6 +13894,15 @@ snapshots:
dependencies:
'@types/jest': 30.0.0
'@types/topojson-client@3.1.5':
dependencies:
'@types/geojson': 7946.0.16
'@types/topojson-specification': 1.0.5
'@types/topojson-specification@1.0.5':
dependencies:
'@types/geojson': 7946.0.16
'@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7':
@@ -14078,6 +14198,11 @@ snapshots:
'@xobotyi/scrollbar-width@1.9.5': {}
'@zarrita/storage@0.2.0':
dependencies:
reference-spec-reader: 0.2.0
unzipit: 2.0.0
'@zxing/text-encoding@0.9.0':
optional: true
@@ -14966,6 +15091,10 @@ snapshots:
dependencies:
d3-array: 3.2.4
d3-geo@3.1.1:
dependencies:
d3-array: 3.2.4
d3-hierarchy@1.1.9: {}
d3-hierarchy@3.1.2: {}
@@ -15232,6 +15361,8 @@ snapshots:
earcut@2.2.4: {}
earcut@3.2.4: {}
eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.427: {}
@@ -15548,6 +15679,8 @@ snapshots:
fflate@0.4.9: {}
fflate@0.8.3: {}
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -15736,6 +15869,17 @@ snapshots:
xml-utils: 1.7.0
zstddec: 0.1.0
geotiff@3.1.0-beta.0:
dependencies:
'@petamoriken/float16': 3.9.3
lerc: 3.0.0
pako: 2.1.0
parse-headers: 2.0.5
quick-lru: 6.1.1
web-worker: 1.5.0
xml-utils: 1.10.2
zstddec: 0.2.0
get-caller-file@2.0.5: {}
get-east-asian-width@1.5.0: {}
@@ -17793,6 +17937,10 @@ snapshots:
dependencies:
boolbase: 1.0.0
numcodecs@0.3.2:
dependencies:
fflate: 0.8.3
nuqs@2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0):
dependencies:
'@standard-schema/spec': 1.0.0
@@ -17871,6 +18019,15 @@ snapshots:
mapbox-to-css-font: 2.4.5
ol: 7.4.0
ol@10.11.0:
dependencies:
'@types/rbush': 4.0.0
earcut: 3.2.4
geotiff: 3.1.0-beta.0
pbf: 5.1.2
rbush: 4.0.1
zarrita: 0.7.5
ol@7.4.0:
dependencies:
earcut: 2.2.4
@@ -18197,6 +18354,10 @@ snapshots:
ieee754: 1.2.1
resolve-protobuf-schema: 2.1.0
pbf@5.1.2:
dependencies:
resolve-protobuf-schema: 2.1.0
picocolors@1.1.1: {}
picomatch@2.3.2: {}
@@ -18380,10 +18541,16 @@ snapshots:
quickselect@2.0.0: {}
quickselect@3.0.0: {}
rbush@3.0.1:
dependencies:
quickselect: 2.0.0
rbush@4.0.1:
dependencies:
quickselect: 3.0.0
rc-cascader@3.20.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.28.2
@@ -19117,6 +19284,8 @@ snapshots:
dependencies:
'@eslint-community/regexpp': 4.12.2
reference-spec-reader@0.2.0: {}
refractor@3.6.0:
dependencies:
hastscript: 6.0.0
@@ -19887,6 +20056,19 @@ snapshots:
toggle-selection@1.0.6: {}
topojson-client@3.1.0:
dependencies:
commander: 2.20.3
topojson-server@3.0.1:
dependencies:
commander: 2.20.3
topojson-simplify@3.0.3:
dependencies:
commander: 2.20.3
topojson-client: 3.1.0
tough-cookie@4.1.4:
dependencies:
psl: 1.15.0
@@ -20155,6 +20337,8 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
unzipit@2.0.0: {}
update-browserslist-db@1.3.3(browserslist@4.28.9):
dependencies:
browserslist: 4.28.9
@@ -20476,6 +20660,8 @@ snapshots:
xml-name-validator@4.0.0: {}
xml-utils@1.10.2: {}
xml-utils@1.7.0: {}
xml@1.0.1: {}
@@ -20552,10 +20738,17 @@ snapshots:
yoctocolors@2.1.2: {}
zarrita@0.7.5:
dependencies:
'@zarrita/storage': 0.2.0
numcodecs: 0.3.2
zod@4.3.6: {}
zstddec@0.1.0: {}
zstddec@0.2.0: {}
zustand@5.0.11(@types/react@18.0.26)(immer@11.1.3)(react@18.2.0)(use-sync-external-store@1.6.0(react@18.2.0)):
optionalDependencies:
'@types/react': 18.0.26

View File

@@ -0,0 +1,210 @@
// Builds the Geo Map outlines assets: Natural Earth 50m admin-0 countries, and
// 10m admin-1 states/provinces for every country with at least two of them.
// Run by hand when bumping Natural Earth or editing regions/aliases; the output is committed.
// Usage: pnpm build:geo-outlines
//
// Nothing is written until every invariant in validate.mjs holds. The assets
// are built in a staging folder and swapped in whole, so a failed run leaves
// the committed ones as they were.
import {
existsSync,
mkdirSync,
readdirSync,
renameSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { join } from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
import { gzipSync } from 'node:zlib';
import {
ASSETS_DIR,
COUNTRIES_URL,
SUBDIVISION_LIST_FILE,
SUBDIVISIONS_URL,
} from './config.mjs';
import { buildCountries } from './countries.mjs';
import { buildSubdivisions } from './subdivisions.mjs';
import { validate } from './validate.mjs';
const DOWNLOAD_TIMEOUT_MS = 120_000;
const DOWNLOAD_ATTEMPTS = 3;
const STAGING_DIR = `${ASSETS_DIR}.staging`;
const PREVIOUS_DIR = `${ASSETS_DIR}.previous`;
// Everything in the assets folder is generated; anything else would be lost
// in the swap, so its presence stops the build.
const GENERATED_ENTRIES = new Set([
'countries-50m.json',
'lookups.json',
'subdivisions',
]);
const kb = (bytes) => `${(bytes / 1024).toFixed(0)} KB`;
const sizeOf = (json) =>
`${kb(Buffer.byteLength(json))} raw / ${kb(gzipSync(json).length)} gzip`;
async function downloadFeatures(url) {
for (let attempt = 1; ; attempt += 1) {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const body = await response.json();
if (
body?.type !== 'FeatureCollection' ||
!Array.isArray(body.features) ||
body.features.length === 0
) {
throw new Error('response is not a non-empty GeoJSON FeatureCollection');
}
return body;
} catch (error) {
if (attempt === DOWNLOAD_ATTEMPTS) {
throw new Error(
`download failed after ${attempt} attempts: ${url}\n ${error.message}`,
);
}
await sleep(attempt * 2_000);
}
}
}
function subdivisionListSource(countries) {
return `// Generated by scripts/geo-outlines/build.mjs; do not edit.
export const SUBDIVISION_COUNTRY_CODES = [
${countries.map((country) => `\t'${country}',`).join('\n')}
] as const;
`;
}
// A run that died mid-swap leaves the assets in the previous folder.
function recoverInterruptedSwap() {
if (!existsSync(ASSETS_DIR) && existsSync(PREVIOUS_DIR)) {
renameSync(PREVIOUS_DIR, ASSETS_DIR);
}
rmSync(STAGING_DIR, { recursive: true, force: true });
rmSync(PREVIOUS_DIR, { recursive: true, force: true });
}
function assertOnlyGeneratedEntries() {
if (!existsSync(ASSETS_DIR)) {
return;
}
const foreign = readdirSync(ASSETS_DIR).filter(
(entry) => !GENERATED_ENTRIES.has(entry),
);
if (foreign.length > 0) {
throw new Error(
`${ASSETS_DIR} holds files this build doesn't generate: ${foreign.join(', ')}`,
);
}
}
/** Writes every output under the staging folder; returns the sizes report. */
function stage(outputs) {
const subdivisionDir = join(STAGING_DIR, 'subdivisions');
mkdirSync(subdivisionDir, { recursive: true });
writeFileSync(join(STAGING_DIR, 'countries-50m.json'), outputs.countriesJson);
writeFileSync(join(STAGING_DIR, 'lookups.json'), outputs.lookupsJson);
const sizes = [];
for (const [country, file] of outputs.subdivisionFiles) {
const json = JSON.stringify(file);
writeFileSync(join(subdivisionDir, `${country}.json`), json);
sizes.push({
country,
shapes: file.objects.subdivisions.geometries.length,
gzip: gzipSync(json).length,
});
}
writeFileSync(`${SUBDIVISION_LIST_FILE}.staging`, outputs.subdivisionList);
return sizes;
}
/** Swaps the staged assets in; puts the previous ones back if that fails. */
function commitStaged() {
const hadAssets = existsSync(ASSETS_DIR);
if (hadAssets) {
renameSync(ASSETS_DIR, PREVIOUS_DIR);
}
try {
renameSync(STAGING_DIR, ASSETS_DIR);
// A failed rename leaves the previous list in place.
renameSync(`${SUBDIVISION_LIST_FILE}.staging`, SUBDIVISION_LIST_FILE);
} catch (error) {
rmSync(ASSETS_DIR, { recursive: true, force: true });
if (hadAssets) {
renameSync(PREVIOUS_DIR, ASSETS_DIR);
}
throw error;
}
rmSync(PREVIOUS_DIR, { recursive: true, force: true });
}
function cleanUpStaging() {
rmSync(STAGING_DIR, { recursive: true, force: true });
rmSync(`${SUBDIVISION_LIST_FILE}.staging`, { force: true });
}
function report(countries, subdivisions, outputs, sizes) {
const totalGzip = sizes.reduce((sum, { gzip }) => sum + gzip, 0);
return {
countries: {
...countries.report,
sizes: {
'countries-50m.json': sizeOf(outputs.countriesJson),
'lookups.json': sizeOf(outputs.lookupsJson),
},
},
subdivisions: {
...subdivisions.report,
countries: sizes.length,
totalGzip: kb(totalGzip),
largest: [...sizes]
.sort((a, b) => b.gzip - a.gzip)
.slice(0, 10)
.map(
({ country, shapes, gzip }) =>
`${country}: ${shapes} shapes, ${kb(gzip)} gzip`,
),
},
};
}
async function main() {
recoverInterruptedSwap();
assertOnlyGeneratedEntries();
const [countrySource, subdivisionSource] = await Promise.all([
downloadFeatures(COUNTRIES_URL),
downloadFeatures(SUBDIVISIONS_URL),
]);
const countries = buildCountries(countrySource);
const subdivisions = buildSubdivisions(subdivisionSource);
validate(countries, subdivisions);
const outputs = {
countriesJson: JSON.stringify(countries.topology),
lookupsJson: JSON.stringify(countries.lookups),
subdivisionFiles: subdivisions.files,
subdivisionList: subdivisionListSource([...subdivisions.files.keys()]),
};
try {
const sizes = stage(outputs);
commitStaged();
process.stdout.write(
`${JSON.stringify(report(countries, subdivisions, outputs, sizes), null, 2)}\n`,
);
} finally {
cleanUpStaging();
}
}
main().catch((error) => {
process.stderr.write(`Geo outlines build failed: ${error.message}\n`);
process.exitCode = 1;
});

View File

@@ -0,0 +1,181 @@
// Inputs and tuning for the Geo Map outlines build. Tables only; no logic.
// Read by every other module here; not run on its own.
//
// Edit this file to bump Natural Earth (the pinned version below), retune
// simplification, or fix a region, alias or merge; then run
// pnpm build:geo-outlines and commit the rebuilt assets.
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const NATURAL_EARTH_BASE =
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson';
export const COUNTRIES_URL = `${NATURAL_EARTH_BASE}/ne_50m_admin_0_countries.geojson`;
export const SUBDIVISIONS_URL = `${NATURAL_EARTH_BASE}/ne_10m_admin_1_states_provinces.geojson`;
const GEO_MAP_DIR = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/lib/visualization/charts/GeoMap',
);
export const ASSETS_DIR = join(GEO_MAP_DIR, 'assets');
export const SUBDIVISION_LIST_FILE = join(
GEO_MAP_DIR,
'geo/subdivisionCountries.ts',
);
export const COUNTRY_QUANTIZATION = 1e5;
// Simplification drops every point whose triangle with its neighbours is
// smaller than this, in square degrees. A fixed size, unlike a share of all
// points, still leaves a small island a few points of its own.
export const COUNTRY_MIN_WEIGHT = 3e-3;
// 10m is street-level detail; a country view needs far less.
export const SUBDIVISION_QUANTIZATION = 1e4;
export const SUBDIVISION_MIN_WEIGHT = 1e-3;
// A shape this many times the minimum weight or smaller could lose every polygon.
export const PROTECTED_SHAPE_FACTOR = 10;
// A country view needs at least this many coded states to be worth drawing.
export const MIN_SUBDIVISIONS = 2;
// ISO 3166-2 codes that changed after Natural Earth v5.1.2, or older spellings
// still in GeoIP data, mapped to the code the shape carries.
export const SUBDIVISION_ALIASES = {
IN: {
'IN-CG': 'IN-CT',
'IN-UK': 'IN-UT',
'IN-TS': 'IN-TG',
'IN-OD': 'IN-OR',
'IN-DD': 'IN-DH',
'IN-DN': 'IN-DH',
},
// Natural Earth carries Metro Manila under its retired code.
PH: { 'PH-00': 'PH-MNL' },
};
// Shapes Natural Earth still splits along retired lines, merged into the codes
// GeoIP reports now; the retired codes become aliases. Northern Ireland's 26
// districts became 11 councils in 2015, each a union of old districts.
// Current code → [current name, retired codes].
export const SUBDIVISION_MERGES = {
GB: {
'GB-ANN': ['Antrim and Newtownabbey', ['GB-ANT', 'GB-NTA']],
'GB-AND': ['Ards and North Down', ['GB-ARD', 'GB-NDN']],
'GB-ABC': [
'Armagh City, Banbridge and Craigavon',
['GB-ARM', 'GB-BNB', 'GB-CGV'],
],
'GB-BFS': ['Belfast', ['GB-BFS']],
'GB-CCG': [
'Causeway Coast and Glens',
['GB-BLY', 'GB-CLR', 'GB-LMV', 'GB-MYL'],
],
'GB-DRS': ['Derry City and Strabane', ['GB-DRY', 'GB-STB']],
'GB-FMO': ['Fermanagh and Omagh', ['GB-FER', 'GB-OMH']],
'GB-LBC': ['Lisburn and Castlereagh', ['GB-LSB', 'GB-CSR']],
'GB-MEA': ['Mid and East Antrim', ['GB-BLA', 'GB-CKF', 'GB-LRN']],
'GB-MUL': ['Mid Ulster', ['GB-CKT', 'GB-DGN', 'GB-MFT']],
'GB-NMD': ['Newry, Mourne and Down', ['GB-NYM', 'GB-DOW']],
},
};
// Natural Earth puts these islands in "Seven seas (open ocean)".
export const OPEN_OCEAN_REGION = {
Africa: 'africa',
Asia: 'asia',
Americas: 'south_america',
};
export const CONTINENT_REGION = {
Africa: 'africa',
Asia: 'asia',
Europe: 'europe',
'North America': 'north_america',
'South America': 'south_america',
Oceania: 'oceania',
};
// Transcontinental countries belong to every region they span.
export const EXTRA_REGIONS = {
RU: ['asia'],
TR: ['europe'],
KZ: ['europe'],
GE: ['europe'],
AM: ['europe'],
AZ: ['europe'],
CY: ['europe'],
EG: ['asia'],
};
// ISO codes GeoIP emits for places Natural Earth draws inside another country.
export const COUNTRY_ALIASES = {
GF: 'FR',
GP: 'FR',
MQ: 'FR',
RE: 'FR',
YT: 'FR',
BQ: 'NL',
SJ: 'NO',
BV: 'NO',
CX: 'AU',
CC: 'AU',
HM: 'AU',
UM: 'US',
TK: 'NZ',
AC: 'SH',
TA: 'SH',
DG: 'IO',
CP: 'FR',
FX: 'FR',
EA: 'ES',
IC: 'ES',
CQ: 'GB',
GI: 'GB',
// Retired or reserved codes still seen in older data.
UK: 'GB',
SU: 'RU',
YU: 'RS',
CS: 'RS',
TP: 'TL',
BU: 'MM',
ZR: 'CD',
DD: 'DE',
VD: 'VN',
YD: 'YE',
RH: 'ZW',
NH: 'VU',
HV: 'BF',
DY: 'BJ',
AN: 'CW',
};
export const COUNTRY_NAME_ALIASES = {
usa: 'US',
'united states of america': 'US',
america: 'US',
uk: 'GB',
'great britain': 'GB',
britain: 'GB',
england: 'GB',
russia: 'RU',
'south korea': 'KR',
'north korea': 'KP',
czechia: 'CZ',
'czech republic': 'CZ',
'ivory coast': 'CI',
turkiye: 'TR',
türkiye: 'TR',
burma: 'MM',
'cape verde': 'CV',
swaziland: 'SZ',
macedonia: 'MK',
'east timor': 'TL',
vatican: 'VA',
};
// Alpha-2 codes Intl names that aren't countries, so none needs a shape.
export const NON_COUNTRY_CODES = new Set([
'EU',
'EZ',
'UN',
'ZZ',
'QO',
'XA',
'XB',
'AQ',
]);

View File

@@ -0,0 +1,190 @@
// Natural Earth admin-0 → the countries topology and the lookup tables.
// Called by build.mjs; not run on its own.
//
// Merges each country's parts into one shape by ISO alpha-2, drops Antarctica,
// assigns view regions and builds the alpha-3/name/alias lookups
// normalizeCountry reads. Returns the topology, the lookups and a report.
import { geoArea, geoBounds } from 'd3-geo';
import { feature } from 'topojson-client';
import {
CONTINENT_REGION,
COUNTRY_ALIASES,
COUNTRY_MIN_WEIGHT,
COUNTRY_NAME_ALIASES,
COUNTRY_QUANTIZATION,
EXTRA_REGIONS,
NON_COUNTRY_CODES,
OPEN_OCEAN_REGION,
} from './config.mjs';
import {
polygonsOf,
round,
roundBounds,
simplifyAndQuantize,
} from './geometry.mjs';
export const isCountryCode = (value) => /^[A-Z]{2}$/.test(value ?? '');
function regionsOf(props, iso2) {
const base = props.CONTINENT.startsWith('Seven seas')
? OPEN_OCEAN_REGION[props.REGION_UN]
: CONTINENT_REGION[props.CONTINENT];
return [...new Set([base, ...(EXTRA_REGIONS[iso2] ?? [])].filter(Boolean))];
}
// Group features by code so a country's overseas parts (Australia's Indian
// Ocean territories) become one shape that one data row colours.
function groupByCode(sourceFeatures, report) {
const groups = new Map();
for (const f of sourceFeatures) {
const p = f.properties;
if (p.CONTINENT === 'Antarctica') {
report.dropped.push(p.NAME);
continue;
}
const iso2 = isCountryCode(p.ISO_A2_EH) ? p.ISO_A2_EH : null;
const key = iso2 ?? `uncoded:${p.NAME}`;
if (!iso2) {
report.uncoded.push(p.NAME);
}
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(f);
}
return groups;
}
function toCountryFeature(key, members, report) {
// The sovereign part carries the name, label point and region.
const primary =
members.find((f) => f.properties.ISO_A2 === f.properties.ISO_A2_EH) ??
members.reduce((a, b) => (geoArea(a) >= geoArea(b) ? a : b));
const p = primary.properties;
const iso2 = key.startsWith('uncoded:') ? null : key;
if (members.length > 1) {
report.merged.push(
`${iso2}: ${members.map((f) => f.properties.NAME).join(' + ')}`,
);
}
const polygons = members.flatMap((f) => polygonsOf(f.geometry));
const geometry = { type: 'MultiPolygon', coordinates: polygons };
const mainland = polygons
.map((coordinates) => ({ type: 'Polygon', coordinates }))
.reduce((a, b) => (geoArea(a) >= geoArea(b) ? a : b));
return {
type: 'Feature',
id: iso2 ?? p.NAME,
properties: {
code: iso2,
iso3: /^[A-Z]{3}$/.test(p.ISO_A3_EH) ? p.ISO_A3_EH : null,
name: p.NAME,
label: [round(p.LABEL_X), round(p.LABEL_Y)],
mainland: roundBounds(geoBounds(mainland)),
bounds: roundBounds(geoBounds(geometry)),
regions: regionsOf(p, iso2 ?? ''),
},
geometry,
};
}
/** Alpha-3 codes and English names → alpha-2, for normalising data. */
function buildLookups(sourceFeatures) {
const alpha3 = {};
const names = { ...COUNTRY_NAME_ALIASES };
for (const { properties: p } of sourceFeatures) {
if (!isCountryCode(p.ISO_A2_EH) || p.CONTINENT === 'Antarctica') {
continue;
}
if (/^[A-Z]{3}$/.test(p.ISO_A3_EH)) {
alpha3[p.ISO_A3_EH] = p.ISO_A2_EH;
}
for (const name of [
p.NAME,
p.NAME_LONG,
p.ADMIN,
p.NAME_EN,
p.BRK_NAME,
p.NAME_SORT,
p.NAME_CIAWF,
]) {
if (name) {
names[name.toLowerCase()] ??= p.ISO_A2_EH;
}
}
}
return { alpha3, names, aliases: COUNTRY_ALIASES };
}
/** ISO alpha-2 codes Intl knows that neither a shape nor an alias covers. */
function findUncoveredCodes(drawn) {
const display = new Intl.DisplayNames(['en'], {
type: 'region',
fallback: 'none',
});
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const uncovered = [];
for (const a of letters) {
for (const b of letters) {
const code = a + b;
const name = display.of(code);
if (
name &&
!NON_COUNTRY_CODES.has(code) &&
!drawn.has(code) &&
!COUNTRY_ALIASES[code]
) {
uncovered.push(`${code} ${name}`);
}
}
}
return uncovered;
}
/** Natural Earth admin-0 GeoJSON → `{ topology, lookups, report }`. */
export function buildCountries(source) {
const report = { dropped: [], uncoded: [], merged: [] };
const features = [...groupByCode(source.features, report)]
.map(([key, members]) => toCountryFeature(key, members, report))
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
const { topo, flipped, collapsed, emptied } = simplifyAndQuantize(
'countries',
features,
COUNTRY_MIN_WEIGHT,
COUNTRY_QUANTIZATION,
);
const drawn = new Set(features.map((f) => f.properties.code).filter(Boolean));
// Lets data be checked against the drawn codes without decoding the outlines.
const lookups = { codes: [...drawn].sort(), ...buildLookups(source.features) };
const regionCounts = {};
for (const f of features) {
for (const region of f.properties.regions) {
regionCounts[region] = (regionCounts[region] ?? 0) + 1;
}
}
return {
topology: topo,
lookups,
report: {
features: features.length,
decodedFeatures: feature(topo, topo.objects.countries).features.length,
droppedFlippedIslands: flipped,
droppedCollapsedPolygons: collapsed,
emptied: emptied.join(' '),
regionCounts,
...report,
lookupCounts: {
alpha3: Object.keys(lookups.alpha3).length,
names: Object.keys(lookups.names).length,
aliases: Object.keys(lookups.aliases).length,
},
uncoveredIsoCodes: findUncoveredCodes(drawn),
},
};
}

View File

@@ -0,0 +1,118 @@
// Geometry helpers and the simplification shared by countries and states.
// Used by countries.mjs and subdivisions.mjs; not run on its own.
//
// Simplification protects small shapes so islands keep an outline, and drops
// polygons that collapse to slivers or flip inside out after quantizing.
import { geoArea } from 'd3-geo';
import { topology } from 'topojson-server';
import { presimplify, simplify } from 'topojson-simplify';
import { feature, quantize } from 'topojson-client';
import { PROTECTED_SHAPE_FACTOR } from './config.mjs';
export const round = (n) => Math.round(n * 1e3) / 1e3;
export const roundBounds = (bounds) =>
bounds.map(([x, y]) => [round(x), round(y)]);
export function polygonsOf(geometry) {
if (geometry.type === 'Polygon') {
return [geometry.coordinates];
}
if (geometry.type === 'MultiPolygon') {
return geometry.coordinates;
}
return [];
}
/** Area of a lon/lat ring in square degrees, the unit of the simplify weights. */
function planarRingArea(ring) {
let twice = 0;
ring.forEach(([x, y], i) => {
const [nx, ny] = ring[(i + 1) % ring.length];
twice += x * ny - nx * y;
});
return Math.abs(twice) / 2;
}
// Weights every point of a small shape's largest outline as Infinity, which
// simplify never removes, so a city-state or city-sized province (Monaco, a
// London borough) still has an outline to colour. Neighbours share those arcs,
// so their common border stays in step.
function protectSmallShapes(topo, objectName, features, minWeight) {
topo.objects[objectName].geometries.forEach((geometry, i) => {
const polygons = polygonsOf(features[i].geometry);
const areas = polygons.map(([outer]) => planarRingArea(outer));
const total = areas.reduce((sum, area) => sum + area, 0);
if (polygons.length === 0 || total > minWeight * PROTECTED_SHAPE_FACTOR) {
return;
}
const largest = areas.indexOf(Math.max(...areas));
const rings =
geometry.type === 'Polygon' ? geometry.arcs : geometry.arcs[largest];
rings[0].forEach((index) => {
topo.arcs[index < 0 ? ~index : index].forEach((point) => {
point[2] = Infinity;
});
});
});
}
const isCollapsed = ([outer]) =>
new Set(outer.map((point) => point.join())).size < 3;
// Simplifying can collapse a tiny island until its ring runs the other way; d3
// then reads it as "the whole globe except this island" and floods the map.
const isInverted = (coordinates) =>
geoArea({ type: 'Polygon', coordinates }) > 2 * Math.PI;
/**
* One feature collection to a simplified, quantized topology under
* `objectName`. Polygons that collapse to a sliver or flip inside out are
* dropped and the topology rebuilt without them.
*/
export function simplifyAndQuantize(
objectName,
features,
minWeight,
quantization,
) {
let topo = presimplify(
topology({ [objectName]: { type: 'FeatureCollection', features } }),
);
protectSmallShapes(topo, objectName, features, minWeight);
topo = simplify(topo, minWeight);
// presimplify stores each point's weight as a third coordinate and simplify
// keeps it, so it is dropped before quantizing (which would carry it through).
topo = { ...topo, arcs: topo.arcs.map((arc) => arc.map(([x, y]) => [x, y])) };
topo = quantize(topo, quantization);
let flipped = 0;
let collapsed = 0;
const cleaned = feature(topo, topo.objects[objectName]).features.map((f) => {
const kept = polygonsOf(f.geometry).filter((coordinates) => {
if (isCollapsed(coordinates)) {
collapsed += 1;
return false;
}
if (isInverted(coordinates)) {
flipped += 1;
return false;
}
return true;
});
return { ...f, geometry: { type: 'MultiPolygon', coordinates: kept } };
});
const emptied = cleaned
.filter((f) => f.geometry.coordinates.length === 0)
.map((f) => f.properties.code ?? f.properties.name);
if (flipped > 0 || collapsed > 0) {
topo = quantize(
topology({
[objectName]: { type: 'FeatureCollection', features: cleaned },
}),
quantization,
);
}
return { topo, flipped, collapsed, emptied };
}

View File

@@ -0,0 +1,193 @@
// Natural Earth admin-1 → one states/provinces topology per country.
// Called by build.mjs; not run on its own.
//
// Skips countries with fewer than MIN_SUBDIVISIONS coded states, merges shapes
// split along retired lines, and records aliases for renamed codes. Returns one
// file per country and a report of what was skipped, merged or dropped.
import { geoArea } from 'd3-geo';
import { topology } from 'topojson-server';
import { merge } from 'topojson-client';
import {
MIN_SUBDIVISIONS,
SUBDIVISION_ALIASES,
SUBDIVISION_MERGES,
SUBDIVISION_MIN_WEIGHT,
SUBDIVISION_QUANTIZATION,
} from './config.mjs';
import { isCountryCode } from './countries.mjs';
import { round, simplifyAndQuantize } from './geometry.mjs';
const SUBDIVISION_CODE = /^[A-Z]{2}-[A-Z0-9]{1,3}$/;
/** Retired code → `[current code, current name]`, from `SUBDIVISION_MERGES`. */
const RECODES = new Map(
Object.values(SUBDIVISION_MERGES).flatMap((merges) =>
Object.entries(merges).flatMap(([code, [name, retired]]) =>
retired.map((old) => [old, [code, name]]),
),
),
);
/** A country's aliases: the hand-written ones, then each merge's retired codes. */
function aliasesOf(country) {
const aliases = { ...SUBDIVISION_ALIASES[country] };
for (const [code, [, retired]] of Object.entries(
SUBDIVISION_MERGES[country] ?? {},
)) {
for (const old of retired) {
if (old !== code) {
aliases[old] = code;
}
}
}
return aliases;
}
// Shapes without an ISO 3166-2 code can't be matched to data, and a code under
// another country's prefix (Puerto Rico's `US-PR`) belongs to that country's
// file. Retired codes are rewritten to the current ones here, so the merge
// below dissolves them.
function groupByCountry(sourceFeatures) {
const byCountry = new Map();
const uncoded = {};
const recoded = new Set();
for (const f of sourceFeatures) {
const country = f.properties.iso_a2;
if (!isCountryCode(country)) {
continue;
}
const code = f.properties.iso_3166_2;
if (!SUBDIVISION_CODE.test(code ?? '') || !code.startsWith(`${country}-`)) {
uncoded[country] = (uncoded[country] ?? 0) + 1;
continue;
}
if (!byCountry.has(country)) {
byCountry.set(country, []);
}
const recode = RECODES.get(code);
if (recode) {
recoded.add(code);
}
byCountry.get(country).push(
recode
? {
...f,
properties: {
...f.properties,
iso_3166_2: recode[0],
name: recode[1],
},
}
: f,
);
}
return { byCountry, uncoded, recoded };
}
// Natural Earth splits some codes across shapes (Dublin's four councils are all
// `IE-D`). Merging them through a topology drops the borders between the parts,
// so one code is one shape with one outline.
function mergeByCode(features) {
const byCode = new Map();
for (const f of features) {
const code = f.properties.iso_3166_2;
if (!byCode.has(code)) {
byCode.set(code, []);
}
byCode.get(code).push(f);
}
const topo = topology({ parts: { type: 'FeatureCollection', features } });
const { geometries } = topo.objects.parts;
let merged = 0;
const shapes = [...byCode].map(([code, members]) => {
// The largest part carries the name and label point.
const primary = members.reduce((a, b) => (geoArea(a) >= geoArea(b) ? a : b));
let geometry = primary.geometry;
if (members.length > 1) {
merged += members.length - 1;
geometry = merge(
topo,
geometries.filter((_, i) => features[i].properties.iso_3166_2 === code),
);
}
return {
type: 'Feature',
id: code,
properties: {
code,
name: primary.properties.name,
label: [
round(primary.properties.longitude),
round(primary.properties.latitude),
],
},
geometry,
};
});
return { shapes: shapes.sort((a, b) => a.id.localeCompare(b.id)), merged };
}
/** Lower-cased English names → code, for data that names a state. */
function namesOf(members) {
const names = {};
for (const { properties: p } of members) {
for (const name of [p.name, p.name_en, p.gn_name, p.woe_name]) {
if (name) {
names[name.toLowerCase()] ??= p.iso_3166_2;
}
}
}
return names;
}
/**
* Natural Earth admin-1 GeoJSON → `{ files, report }`, where `files` maps each
* country with enough coded states to its outlines file, in code order.
*/
export function buildSubdivisions(source) {
const { byCountry, uncoded, recoded } = groupByCountry(source.features);
const files = new Map();
const report = {
skippedUnderMinimum: [],
mergedParts: {},
uncodedShapes: uncoded,
droppedFlippedIslands: {},
droppedCollapsedPolygons: 0,
emptiedShapes: [],
// Merge entries whose retired code Natural Earth no longer has.
unmatchedRetiredCodes: [...RECODES.keys()].filter(
(code) => !recoded.has(code),
),
};
const countries = [...byCountry.keys()].sort((a, b) => a.localeCompare(b));
for (const country of countries) {
const members = byCountry.get(country);
const { shapes, merged } = mergeByCode(members);
if (shapes.length < MIN_SUBDIVISIONS) {
report.skippedUnderMinimum.push(country);
continue;
}
if (merged) {
report.mergedParts[country] = merged;
}
const { topo, flipped, collapsed, emptied } = simplifyAndQuantize(
'subdivisions',
shapes,
SUBDIVISION_MIN_WEIGHT,
SUBDIVISION_QUANTIZATION,
);
if (flipped) {
report.droppedFlippedIslands[country] = flipped;
}
report.droppedCollapsedPolygons += collapsed;
report.emptiedShapes.push(...emptied);
files.set(country, {
...topo,
aliases: aliasesOf(country),
names: namesOf(members),
});
}
return { files, report };
}

View File

@@ -0,0 +1,114 @@
// Invariants the chart relies on, checked before anything is written. A
// failure names every problem; the committed assets are left untouched.
// Called by build.mjs; not run on its own.
//
// Checks shape counts, that no shape is emptied or drawn twice, and that every
// code, alias and name resolves to a drawn shape.
import { feature } from 'topojson-client';
import { MIN_SUBDIVISIONS } from './config.mjs';
// Natural Earth v5.1.2 yields 239 countries and 198 countries with states. A
// drop below these means a changed or truncated download, not real geography.
const MIN_COUNTRIES = 230;
const MIN_SUBDIVISION_COUNTRIES = 190;
function duplicatesOf(values) {
const seen = new Set();
return values.filter((value) => seen.has(value) || !seen.add(value));
}
function checkCountries({ topology, lookups, report }, problems) {
const shapes = feature(topology, topology.objects.countries).features;
const codes = shapes.map((shape) => shape.properties.code).filter(Boolean);
const drawn = new Set(codes);
if (shapes.length < MIN_COUNTRIES) {
problems.push(
`only ${shapes.length} countries (expected ≥ ${MIN_COUNTRIES})`,
);
}
if (report.emptied) {
problems.push(`countries left with no outline: ${report.emptied}`);
}
for (const code of duplicatesOf(codes)) {
problems.push(`country code ${code} is drawn twice`);
}
if (lookups.codes.join() !== [...drawn].sort().join()) {
problems.push('lookups.codes differs from the drawn country codes');
}
for (const [alias, target] of Object.entries(lookups.aliases)) {
if (!drawn.has(target)) {
problems.push(`country alias ${alias} → ${target}, which has no shape`);
}
}
for (const [alpha3, target] of Object.entries(lookups.alpha3)) {
if (!drawn.has(target)) {
problems.push(`alpha-3 ${alpha3} → ${target}, which has no shape`);
}
}
}
function checkSubdivisionFile(country, file, problems) {
const codes = file.objects.subdivisions.geometries.map(
(geometry) => geometry.properties.code,
);
const known = new Set(codes);
if (codes.length < MIN_SUBDIVISIONS) {
problems.push(
`${country}: ${codes.length} states (expected ≥ ${MIN_SUBDIVISIONS})`,
);
}
for (const code of duplicatesOf(codes)) {
problems.push(`${country}: state code ${code} is drawn twice`);
}
for (const code of codes) {
if (!code?.startsWith(`${country}-`)) {
problems.push(`${country}: state code ${code} is outside the country`);
}
}
for (const [alias, target] of Object.entries(file.aliases)) {
if (!known.has(target)) {
problems.push(`${country}: alias ${alias} → ${target}, which has no shape`);
}
}
for (const [name, code] of Object.entries(file.names)) {
if (!known.has(code)) {
problems.push(`${country}: name "${name}" → ${code}, which has no shape`);
}
}
}
function checkSubdivisions({ files, report }, problems) {
if (files.size < MIN_SUBDIVISION_COUNTRIES) {
problems.push(
`only ${files.size} countries with states (expected ≥ ${MIN_SUBDIVISION_COUNTRIES})`,
);
}
if (report.emptiedShapes.length > 0) {
problems.push(
`states left with no outline: ${report.emptiedShapes.join(' ')}`,
);
}
if (report.unmatchedRetiredCodes.length > 0) {
problems.push(
`merge codes not found in Natural Earth: ${report.unmatchedRetiredCodes.join(' ')}`,
);
}
for (const [country, file] of files) {
checkSubdivisionFile(country, file, problems);
}
}
/** Throws one error listing every broken invariant. */
export function validate(countries, subdivisions) {
const problems = [];
checkCountries(countries, problems);
checkSubdivisions(subdivisions, problems);
if (problems.length > 0) {
throw new Error(
`${problems.length} problem(s) in the generated outlines:\n- ${problems.join('\n- ')}`,
);
}
}

View File

@@ -5634,6 +5634,13 @@ export interface DashboardtypesBasicVisualizationDTO {
timePreference?: DashboardtypesTimePreferenceDTO;
}
/**
* @minimum 1
* @maximum 64
* @nullable
*/
export type DashboardtypesBubbleRadiusDTO = number | null;
export interface Querybuildertypesv5LogAggregationDTO {
/**
* @type string
@@ -6917,6 +6924,303 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
spec: DashboardtypesScatterPlotPanelSpecDTO;
}
/**
* The panel kind.
*/
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTopListPanelSpecDTOKind {
'signoz/TopListPanel' = 'signoz/TopListPanel',
}
export interface DashboardtypesTopListAppearanceDTO {
/**
* @type string
* @description Hex color of every bar; a matching background threshold overrides it.
*/
barColor: string;
/**
* @type boolean
* @description Numbers each row by its position in the list.
*/
showRank?: boolean;
/**
* @type boolean
* @description Shows each row's share of the listed rows' total.
*/
showShare?: boolean;
}
export interface DashboardtypesTopListPanelSpecDTO {
/**
* @description Rank, share and bar color options.
*/
appearance?: DashboardtypesTopListAppearanceDTO;
/**
* @description Unit and decimal precision of the values.
*/
formatting?: DashboardtypesPanelFormattingDTO;
/**
* @type array,null
* @description Rules that recolor a row when its value compares to a threshold as specified.
*/
thresholds?: DashboardtypesComparisonThresholdDTO[] | null;
/**
* @description The time range the rows are computed over.
*/
visualization?: DashboardtypesBasicVisualizationDTO;
}
export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTopListPanelSpecDTO {
/**
* @enum signoz/TopListPanel
* @type string
* @description The panel kind.
*/
kind: DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTopListPanelSpecDTOKind;
/**
* @description The options for this panel kind. {} accepts every default.
*/
spec: DashboardtypesTopListPanelSpecDTO;
}
/**
* The panel kind.
*/
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesGeoMapPanelSpecDTOKind {
'signoz/GeoMapPanel' = 'signoz/GeoMapPanel',
}
/**
* Radius of the largest circle, in CSS pixels from 1 to 64. Null means 8 on the points layer, 28 otherwise.
*/
export type DashboardtypesGeoMapBubblesDTOMaxRadius = DashboardtypesBubbleRadiusDTO | null;
/**
* Radius of the smallest circle, in CSS pixels from 1 to 64. Null means 2 on the points layer, 3 otherwise.
*/
export type DashboardtypesGeoMapBubblesDTOMinRadius = DashboardtypesBubbleRadiusDTO | null;
export interface DashboardtypesGeoMapBubblesDTO {
/**
* @description Radius of the largest circle, in CSS pixels from 1 to 64. Null means 8 on the points layer, 28 otherwise.
*/
maxRadius?: DashboardtypesGeoMapBubblesDTOMaxRadius;
/**
* @description Radius of the smallest circle, in CSS pixels from 1 to 64. Null means 2 on the points layer, 3 otherwise.
*/
minRadius?: DashboardtypesGeoMapBubblesDTOMinRadius;
}
export interface DashboardtypesGeoMapColorsDTO {
/**
* @type string
* @description The hex color varied in opacity mode. Empty uses the default fill.
*/
fill?: string;
/**
* @type number,null
* @description The value mapped to the highest color. Null uses the highest value in the data.
*/
max?: number | null;
/**
* @type number,null
* @description The value mapped to the lowest color. Null uses the lowest value in the data. Must not exceed max.
*/
min?: number | null;
/**
* @description How values are colored: palette (default) picks a hue from palette, opacity varies the opacity of fill.
*/
mode?: DashboardtypesHeatmapColorModeDTO;
/**
* @description The named color ramp used in palette mode, such as lava (default), ice or ember.
*/
palette?: DashboardtypesHeatmapPaletteDTO;
/**
* @description How values map onto the color range: log (default), sqrt or linear.
*/
scale?: DashboardtypesHeatmapColorScaleDTO;
/**
* @type integer
* @description The number of distinct color steps, 2 to 128. 0 uses the chart default.
*/
steps?: number;
}
export interface DashboardtypesGeoMapChartAppearanceDTO {
/**
* @description The circle radius range of the bubbles and points layers.
*/
bubbles?: DashboardtypesGeoMapBubblesDTO;
/**
* @description The color scale applied to the colorBy values.
*/
colors?: DashboardtypesGeoMapColorsDTO;
/**
* @type boolean,null
* @description Shows country and state names once each fits inside its shape. Null shows them.
*/
labels?: boolean | null;
}
export enum DashboardtypesGeoMapCountryFormatDTO {
auto = 'auto',
alpha2 = 'alpha2',
alpha3 = 'alpha3',
name = 'name',
}
export enum DashboardtypesGeoMapLocationModeDTO {
auto = 'auto',
country = 'country',
subdivision = 'subdivision',
coordinates = 'coordinates',
}
export interface DashboardtypesGeoMapLocationDTO {
/**
* @type string
* @description Group-by key holding the country, for the country and subdivision modes. Empty detects it by name, such as geo.country.iso_code.
*/
country?: string;
/**
* @description How country values are written: auto (default) detects it, alpha2, alpha3 or name.
*/
countryFormat: DashboardtypesGeoMapCountryFormatDTO;
/**
* @type string
* @description Group-by key holding the latitude, for the coordinates mode. Empty detects it by name.
*/
lat?: string;
/**
* @type string
* @description Group-by key holding the longitude, for the coordinates mode. Empty detects it by name.
*/
lon?: string;
/**
* @description How rows are placed: auto (default) detects it from the group-by keys, country, subdivision or coordinates.
*/
mode: DashboardtypesGeoMapLocationModeDTO;
/**
* @type string
* @description Group-by key holding the ISO 3166-2 region code (e.g. geo.region.iso_code), for the subdivision mode. Empty detects it by name.
*/
region?: string;
}
export enum DashboardtypesGeoMapLayerDTO {
regions = 'regions',
bubbles = 'bubbles',
points = 'points',
}
export enum DashboardtypesGeoMapProjectionDTO {
mercator = 'mercator',
equal_earth = 'equal_earth',
natural_earth = 'natural_earth',
}
export enum DashboardtypesGeoMapViewDTO {
world = 'world',
fit = 'fit',
africa = 'africa',
asia = 'asia',
europe = 'europe',
north_america = 'north_america',
south_america = 'south_america',
oceania = 'oceania',
country = 'country',
}
export interface DashboardtypesGeoMapSettingsDTO {
/**
* @type string
* @description Value column key, as in a Table panel, that sets the color. Empty uses the first value column.
*/
colorBy?: string;
/**
* @description What is drawn: regions (default) fills each shape, bubbles draws a circle per region, points a circle per coordinate.
*/
layer: DashboardtypesGeoMapLayerDTO;
/**
* @description The map projection: mercator (default), equal_earth or natural_earth.
*/
projection: DashboardtypesGeoMapProjectionDTO;
/**
* @type string
* @description Value column key, as in a Table panel, that sets the bubble or point size. Empty uses colorBy.
*/
sizeBy?: string;
/**
* @description The area shown: world (default), fit to the data, a continent, or country with viewCountry.
*/
view: DashboardtypesGeoMapViewDTO;
/**
* @type string
* @description Uppercase ISO 3166-1 alpha-2 code of the country the country view shows.
*/
viewCountry?: string;
}
export interface DashboardtypesGeoMapThresholdDTO {
/**
* @type string
* @description The CSS color of the shape, bubble or point while the threshold is active, for example #F1575F; CSS named colors such as red also work.
*/
color: string;
/**
* @description The comparison that activates the threshold: above (default), below, above_or_equal, below_or_equal, equal or not_equal.
*/
operator?: DashboardtypesComparisonOperatorDTO;
/**
* @type string
* @description The unit id of value, such as ms or percent. Empty means the colorBy column's unit.
*/
unit?: string;
/**
* @type number
* @format double
* @description The value compared against, in unit.
*/
value: number;
}
export interface DashboardtypesGeoMapPanelSpecDTO {
/**
* @description Color scale, bubble size and labels.
*/
chartAppearance?: DashboardtypesGeoMapChartAppearanceDTO;
/**
* @description Per-column units and decimal precision.
*/
formatting?: DashboardtypesTableFormattingDTO;
/**
* @description Legend placement.
*/
legend?: DashboardtypesLegendDTO;
/**
* @description The group-by columns that place each row on the map.
*/
location?: DashboardtypesGeoMapLocationDTO;
/**
* @description Layer, view, projection and the value columns drawn.
*/
map?: DashboardtypesGeoMapSettingsDTO;
/**
* @type array,null
* @description Rules that recolor a shape, bubble or point when its value compares to a threshold as specified.
*/
thresholds?: DashboardtypesGeoMapThresholdDTO[] | null;
/**
* @description The time range the values are computed over.
*/
visualization?: DashboardtypesBasicVisualizationDTO;
}
export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesGeoMapPanelSpecDTO {
/**
* @enum signoz/GeoMapPanel
* @type string
* @description The panel kind.
*/
kind: DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesGeoMapPanelSpecDTOKind;
/**
* @description The options for this panel kind. {} accepts every default.
*/
spec: DashboardtypesGeoMapPanelSpecDTO;
}
export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpecDTO
@@ -6928,7 +7232,9 @@ export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpecDTO;
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTopListPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesGeoMapPanelSpecDTO;
export enum Querybuildertypesv5RequestTypeDTO {
scalar = 'scalar',
@@ -8209,6 +8515,8 @@ export enum DashboardtypesPanelPluginKindDTO {
'signoz/TextPanel' = 'signoz/TextPanel',
'signoz/HeatmapPanel' = 'signoz/HeatmapPanel',
'signoz/ScatterPlotPanel' = 'signoz/ScatterPlotPanel',
'signoz/TopListPanel' = 'signoz/TopListPanel',
'signoz/GeoMapPanel' = 'signoz/GeoMapPanel',
}
/**
* The RFC 6902 operations, applied in order against the dashboard's postable shape.

View File

@@ -54,6 +54,8 @@ export function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType {
case PANEL_TYPES.SCATTER:
case PANEL_TYPES.PIE:
case PANEL_TYPES.VALUE:
case PANEL_TYPES.TOP_LIST:
case PANEL_TYPES.GEO_MAP:
return 'scalar';
case PANEL_TYPES.TRACE:
return 'trace';
@@ -277,7 +279,9 @@ export function createAggregation(
(panelType === PANEL_TYPES.TABLE ||
panelType === PANEL_TYPES.SCATTER ||
panelType === PANEL_TYPES.PIE ||
panelType === PANEL_TYPES.VALUE);
panelType === PANEL_TYPES.VALUE ||
panelType === PANEL_TYPES.TOP_LIST ||
panelType === PANEL_TYPES.GEO_MAP);
if (queryData.dataSource === DataSource.METRICS) {
return [

View File

@@ -18,6 +18,7 @@ function InputWithLabel({
className,
closeIcon,
disabled,
sanitize,
}: {
label: string;
initialValue?: string | number | null;
@@ -29,14 +30,17 @@ function InputWithLabel({
className?: string;
closeIcon?: React.ReactNode;
disabled?: boolean;
/** Rewrites the typed value before it is shown and reported. */
sanitize?: (value: string) => string;
}): JSX.Element {
const [inputValue, setInputValue] = useState<string>(
initialValue ? initialValue.toString() : '',
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
onChange?.(e.target.value);
const value = sanitize ? sanitize(e.target.value) : e.target.value;
setInputValue(value);
onChange?.(value);
};
return (
@@ -78,6 +82,7 @@ InputWithLabel.defaultProps = {
initialValue: undefined,
className: undefined,
closeIcon: undefined,
sanitize: undefined,
};
export default InputWithLabel;

View File

@@ -12,7 +12,13 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { get, isEmpty } from 'lodash-es';
import { ChevronUp } from '@signozhq/icons';
import { Querybuildertypesv5BucketOptionsDTO } from 'api/generated/services/sigNoz.schemas';
import {
Querybuildertypesv5BucketOptionsDTO,
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
Querybuildertypesv5QueryEnvelopeFormulaDTOType,
Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType,
} from 'api/generated/services/sigNoz.schemas';
import { isLoneBuilderQuery } from 'lib/query/builderEnvelope';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
@@ -26,6 +32,7 @@ import {
RAW_QUERY_FIELDS,
resolveQueryBuilderFields,
} from '../../queryBuilderFields.utils';
import { parseLimit, sanitizeLimit } from '../../utils';
import BucketOptions from './BucketOptions/BucketOptions';
import HavingFilter from './HavingFilter/HavingFilter';
@@ -146,10 +153,34 @@ function QueryAddOns({
[resolvedFields],
);
// A panel's default limit applies only to a lone query: per-query limits can keep
// different groups and break a formula's join.
const isLoneQuery = useMemo(
() =>
isLoneBuilderQuery([
...(currentQuery.builder?.queryData ?? [query]).map((data) => ({
type:
data.builderQueryType ??
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
})),
...(currentQuery.builder?.queryFormulas ?? []).map(() => ({
type: Querybuildertypesv5QueryEnvelopeFormulaDTOType.builder_formula,
})),
...(currentQuery.builder?.queryTraceOperator ?? []).map(() => ({
type:
Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType.builder_trace_operator,
})),
]),
[currentQuery.builder, query],
);
const placeholderOf = useCallback(
(key: InputAddOnField): string =>
resolvedFields.get(key)?.placeholder ?? ADD_ON_DEFINITIONS[key].placeholder,
[resolvedFields],
(key === QueryBuilderField.Limit && !isLoneQuery
? undefined
: resolvedFields.get(key)?.placeholder) ??
ADD_ON_DEFINITIONS[key].placeholder,
[resolvedFields, isLoneQuery],
);
const isDisabled = useCallback(
@@ -278,7 +309,7 @@ function QueryAddOns({
const handleChangeLimit = useCallback(
(value: string) => {
handleChangeQueryData('limit', Number(value) || null);
handleChangeQueryData('limit', parseLimit(value));
},
[handleChangeQueryData],
);
@@ -374,6 +405,7 @@ function QueryAddOns({
label="Limit"
type="number"
onChange={handleChangeLimit}
sanitize={sanitizeLimit}
initialValue={query?.limit ?? undefined}
placeholder={placeholderOf(QueryBuilderField.Limit)}
onClose={(): void => handleRemoveView(QueryBuilderField.Limit)}

View File

@@ -67,7 +67,9 @@ function QueryAggregationOptions({
onChange={onChange}
queryData={queryData}
maxAggregations={
panelType === PANEL_TYPES.VALUE || panelType === PANEL_TYPES.PIE
panelType === PANEL_TYPES.VALUE ||
panelType === PANEL_TYPES.PIE ||
panelType === PANEL_TYPES.TOP_LIST
? 1
: undefined
}

View File

@@ -6,6 +6,8 @@ const SCALAR_PANEL_TYPES: ReadonlySet<string> = new Set<string>([
PANEL_TYPES.SCATTER,
PANEL_TYPES.PIE,
PANEL_TYPES.VALUE,
PANEL_TYPES.TOP_LIST,
PANEL_TYPES.GEO_MAP,
]);
export function isScalarPanelType(panelType?: string | null): boolean {

View File

@@ -15,6 +15,9 @@ import QueryAddOns from '../QueryV2/QueryAddOns/QueryAddOns';
// Mocks: only what is required for this component to render and for us to assert handler calls
const mockHandleChangeQueryData = jest.fn();
const mockHandleSetQueryData = jest.fn();
let mockCurrentQuery: { unit: string | undefined; builder?: unknown } = {
unit: undefined,
};
jest.mock('hooks/queryBuilder/useQueryBuilderOperations', () => ({
useQueryOperations: (): {
@@ -27,10 +30,10 @@ jest.mock('hooks/queryBuilder/useQueryBuilderOperations', () => ({
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): {
handleSetQueryData: typeof mockHandleSetQueryData;
currentQuery: { unit: string | undefined };
currentQuery: typeof mockCurrentQuery;
} => ({
handleSetQueryData: mockHandleSetQueryData,
currentQuery: { unit: undefined },
currentQuery: mockCurrentQuery,
}),
}));
@@ -169,8 +172,15 @@ describe('QueryAddOns', () => {
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
expect(input.value).toBe('5');
fireEvent.change(input, { target: { value: '10' } });
expect(mockHandleChangeQueryData).toHaveBeenCalledWith('limit', 10);
const user = userEvent.setup();
await user.clear(input);
await user.type(input, '10');
expect(mockHandleChangeQueryData).toHaveBeenLastCalledWith('limit', 10);
await user.clear(input);
await user.type(input, '-5');
expect(input.value).toBe('5');
expect(mockHandleChangeQueryData).toHaveBeenLastCalledWith('limit', 5);
});
it.each([
@@ -222,6 +232,77 @@ describe('QueryAddOns', () => {
);
});
describe('a pinned limit with a default', () => {
const renderPinnedLimit = (): void => {
render(
<QueryAddOns
query={baseQuery()}
version="v5"
isRawQuery={false}
showReduceTo={false}
panelType={PANEL_TYPES.TOP_LIST}
index={0}
fieldsConfig={{
[QueryBuilderField.Limit]: {
state: 'pinned',
placeholder: 'Default 10',
},
}}
isForTraceOperator={false}
/>,
);
};
afterEach(() => {
mockCurrentQuery = { unit: undefined };
});
it('is open with its default as the placeholder', () => {
renderPinnedLimit();
expect(screen.getByTestId('input-Limit')).toHaveAttribute(
'placeholder',
'Default 10',
);
});
it('drops the default for an AI query, which the request leaves unlimited', () => {
mockCurrentQuery = {
unit: undefined,
builder: {
queryData: [{ ...baseQuery(), builderQueryType: 'builder_ai_query' }],
queryFormulas: [],
queryTraceOperator: [],
},
};
renderPinnedLimit();
expect(screen.getByTestId('input-Limit')).toHaveAttribute(
'placeholder',
'Enter limit',
);
});
it('drops the default once a formula joins the query', () => {
mockCurrentQuery = {
unit: undefined,
builder: {
queryData: [baseQuery()],
queryFormulas: [{ expression: 'A * 2' }],
queryTraceOperator: [],
},
};
renderPinnedLimit();
expect(screen.getByTestId('input-Limit')).toHaveAttribute(
'placeholder',
'Enter limit',
);
});
});
it('auto-opens Order By and Limit when present in query', () => {
const query = baseQuery({
orderBy: [{ columnName: 'duration', order: 'desc' }],

View File

@@ -12,10 +12,62 @@ import {
convertExpressionToFilters,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
convertHavingToExpression,
formatValueForExpression,
parseLimit,
removeKeysFromExpression,
sanitizeLimit,
} from '../utils';
describe('convertHavingToExpression', () => {
it.each([
['COUNT()', 'count() > 5'],
['COUNT_DISTINCT(service.name)', 'count_distinct(service.name) > 5'],
['P99(duration_nano)', 'p99(duration_nano) > 5'],
['SUM(http.Request.Size)', 'sum(http.Request.Size) > 5'],
['count()', 'count() > 5'],
])('references %s by its lower-case function', (columnName, expected) => {
expect(
convertHavingToExpression([{ columnName, op: '>', value: 5 }]),
).toStrictEqual({ expression: expected });
});
it('joins several conditions and formats list values', () => {
expect(
convertHavingToExpression([
{ columnName: 'COUNT()', op: '>', value: 5 },
{ columnName: 'COUNT()', op: 'NOT IN', value: [1, 2] },
]),
).toStrictEqual({ expression: 'count() > 5 AND count() NOT IN [1, 2]' });
});
});
describe('sanitizeLimit', () => {
it.each([
['10', '10'],
['-10', '10'],
['2.5', '25'],
['1e3', '13'],
['', ''],
])('sanitizes %j to %j', (value, expected) => {
expect(sanitizeLimit(value)).toBe(expected);
});
});
describe('parseLimit', () => {
it.each([
['10', 10],
['1', 1],
['', null],
['0', null],
['-5', null],
['2.5', null],
['abc', null],
])('parses %j as %j', (value, expected) => {
expect(parseLimit(value)).toBe(expected);
});
});
describe('convertFiltersToExpression', () => {
beforeEach(() => {
jest.clearAllMocks();

View File

@@ -840,6 +840,16 @@ export const appendAndClause = (
return hasTopLevelOr ? `(${base}) AND ${clause}` : `${base} AND ${clause}`;
};
/** Strips everything but digits, so a limit input can't hold a sign or decimal. */
export const sanitizeLimit = (value: string): string =>
value.replace(/\D/g, '');
/** A query or formula limit: a positive integer, otherwise none. */
export const parseLimit = (value: string): number | null => {
const limit = Number(value);
return Number.isInteger(limit) && limit > 0 ? limit : null;
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value
@@ -871,7 +881,14 @@ export const convertHavingToExpression = (
formattedValue = String(value);
}
return `${columnName} ${op} ${formattedValue}`;
// The old having filter upper-cased the function (`COUNT()`); v5 only
// resolves the aggregation's own lower-case expression.
const reference = columnName.replace(
/^\s*([A-Za-z_]\w*)\(/,
(_, fn: string) => `${fn.toLowerCase()}(`,
);
return `${reference} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions

View File

@@ -34,6 +34,8 @@ export const getComponentForPanelType = (
[PANEL_TYPES.HISTOGRAM]: Uplot,
// V2-only kind; it renders through the V2 panel registry.
[PANEL_TYPES.HEATMAP]: null,
[PANEL_TYPES.TOP_LIST]: null,
[PANEL_TYPES.GEO_MAP]: null,
// Dashboards v2 renders this kind; nothing reaches the V1 chart map for it.
[PANEL_TYPES.TEXT]: null,
[PANEL_TYPES.SCATTER]: null,

View File

@@ -340,6 +340,8 @@ export enum PANEL_TYPES {
PIE = 'pie',
HISTOGRAM = 'histogram',
HEATMAP = 'heatmap',
TOP_LIST = 'top_list',
GEO_MAP = 'geo_map',
TEXT = 'text',
EMPTY_WIDGET = 'EMPTY_WIDGET',
}

View File

@@ -26,6 +26,7 @@ import {
} from 'api/ai-assistant/chat';
import ROUTES from 'constants/routes';
import { QueryParams } from 'constants/query';
import { buildExplorerNavigationUrl } from 'utils/buildExplorerNavigationUrl';
import { openInNewTab } from 'utils/navigation';
import {
ArchiveRestore,
@@ -47,6 +48,10 @@ import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import {
getPanelTypeForRequestType,
requestTypeFromActionQuery,
} from './utils/getPanelTypeForRequestType';
import { openSavedViewByKey } from './utils/openSavedView';
import {
isSavedViewOpenAction,
@@ -333,6 +338,11 @@ function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
);
return;
}
// Read before toUrlCompositeQuery, which drops the requestType envelope.
const panelType = getPanelTypeForRequestType(
requestTypeFromActionQuery(action.query),
action.signal,
);
const normalized = withDerivedFilterExpressions(urlQuery as unknown as Query);
// eslint-disable-next-line no-console
console.log('[apply_filter] normalized', normalized);
@@ -342,7 +352,9 @@ function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
normalized.builder.queryData.forEach((q, i) => {
deps.handleSetQueryData(i, q);
});
deps.redirectWithQueryBuilderData(normalized);
deps.redirectWithQueryBuilderData(normalized, {
[QueryParams.panelTypes]: panelType,
});
return;
}
const base = explorerRouteForSignal(action.signal);
@@ -353,8 +365,10 @@ function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
}
// eslint-disable-next-line no-console
console.log('[apply_filter] off-page → navigate', base);
const encoded = encodeURIComponent(JSON.stringify(normalized));
navigate(`${base}?${QueryParams.compositeQuery}=${encoded}`);
const url = buildExplorerNavigationUrl(base, normalized, {
[QueryParams.panelTypes]: panelType,
});
navigate(url);
}
/** Picks the right rollback API call for a given action kind. */
@@ -613,6 +627,7 @@ export default function ActionsSection({
disabled={isSuccess}
loading={isLoading}
aria-label={action.label}
testId={`ai-action-chip-${action.kind}`}
prefix={icon}
>
<span className={styles.chipLabel}>{action.label}</span>

View File

@@ -1,7 +1,16 @@
import { undoExecution } from 'api/ai-assistant/chat';
import type { MessageActionDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { MessageActionKindDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import {
ApplyFilterSignalDTO,
MessageActionKindDTO,
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { getCurrentLocation } from 'lib/router/navigation';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { QueryBuilderContextType } from 'types/common/queryBuilder';
import ActionsSection from '../ActionsSection';
@@ -28,6 +37,37 @@ function renderUndoChip(): HTMLElement {
return screen.getByRole('button', { name: 'Undo change' });
}
// Labels differ per test: auto-applied filters are deduped module-wide by label.
function countByServiceAction(label: string): MessageActionDTO {
return {
kind: MessageActionKindDTO.apply_filter,
label,
signal: ApplyFilterSignalDTO.logs,
query: {
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
compositeQuery: {
queries: [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'logs',
aggregations: [{ expression: 'count()' }],
groupBy: [{ name: 'service.name' }],
},
},
],
},
},
};
}
function currentPanelTypesParam(): string | null {
return new URLSearchParams(getCurrentLocation().search).get(
QueryParams.panelTypes,
);
}
describe('ActionsSection rollback chip', () => {
beforeEach(() => {
mockUndo.mockReset();
@@ -81,3 +121,52 @@ describe('ActionsSection rollback chip', () => {
).resolves.toBeInTheDocument();
});
});
describe('ActionsSection apply_filter', () => {
it('opens the explorer in the Table view when the chip is clicked from another page', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<ActionsSection
actions={[countByServiceAction('Open in Logs Explorer')]}
messageId="message-1"
/>,
undefined,
{ initialRoute: ROUTES.HOME },
);
await user.click(screen.getByTestId('ai-action-chip-apply_filter'));
await waitFor(() =>
expect(getCurrentLocation().pathname).toBe(ROUTES.LOGS_EXPLORER),
);
expect(currentPanelTypesParam()).toBe(JSON.stringify(PANEL_TYPES.TABLE));
});
it('switches the open explorer to the Table view when applied on it', async () => {
const redirectWithQueryBuilderData = jest.fn() as jest.MockedFunction<
QueryBuilderContextType['redirectWithQueryBuilderData']
>;
const handleSetQueryData = jest.fn() as jest.MockedFunction<
QueryBuilderContextType['handleSetQueryData']
>;
render(
<ActionsSection
actions={[countByServiceAction('Apply on Logs Explorer')]}
messageId="message-2"
/>,
undefined,
{
initialRoute: ROUTES.LOGS_EXPLORER,
queryBuilderOverrides: { redirectWithQueryBuilderData, handleSetQueryData },
},
);
await waitFor(() =>
expect(redirectWithQueryBuilderData).toHaveBeenCalledTimes(1),
);
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
expect.objectContaining({ builder: expect.anything() }),
{ [QueryParams.panelTypes]: PANEL_TYPES.TABLE },
);
});
});

View File

@@ -0,0 +1,106 @@
import { ApplyFilterSignalDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
getPanelTypeForRequestType,
requestTypeFromActionQuery,
} from '../getPanelTypeForRequestType';
describe('getPanelTypeForRequestType', () => {
it.each([
[
Querybuildertypesv5RequestTypeDTO.scalar,
ApplyFilterSignalDTO.logs,
PANEL_TYPES.TABLE,
],
[
Querybuildertypesv5RequestTypeDTO.time_series,
ApplyFilterSignalDTO.logs,
PANEL_TYPES.TIME_SERIES,
],
[
Querybuildertypesv5RequestTypeDTO.raw,
ApplyFilterSignalDTO.logs,
PANEL_TYPES.LIST,
],
[
Querybuildertypesv5RequestTypeDTO.trace,
ApplyFilterSignalDTO.traces,
PANEL_TYPES.TRACE,
],
[
Querybuildertypesv5RequestTypeDTO.trace,
ApplyFilterSignalDTO.logs,
PANEL_TYPES.LIST,
],
[
Querybuildertypesv5RequestTypeDTO.trace,
ApplyFilterSignalDTO.metrics,
PANEL_TYPES.LIST,
],
])('maps %s on the %s signal to the %s view', (requestType, signal, view) => {
expect(getPanelTypeForRequestType(requestType, signal)).toBe(view);
});
it.each([
undefined,
null,
'',
Querybuildertypesv5RequestTypeDTO.raw_stream,
Querybuildertypesv5RequestTypeDTO.heatmap,
'distribution',
'nonsense',
'toString',
'constructor',
42,
{},
])(
'falls back to the List view for unmapped or invalid requestType (%p)',
(value) => {
expect(getPanelTypeForRequestType(value, ApplyFilterSignalDTO.traces)).toBe(
PANEL_TYPES.LIST,
);
},
);
});
describe('requestTypeFromActionQuery', () => {
it('reads the requestType envelope field and returns undefined when absent', () => {
expect(
requestTypeFromActionQuery({
requestType: 'scalar',
schemaVersion: 'v5',
compositeQuery: { queries: [] },
}),
).toBe('scalar');
expect(requestTypeFromActionQuery({})).toBeUndefined();
expect(requestTypeFromActionQuery(null)).toBeUndefined();
expect(requestTypeFromActionQuery(undefined)).toBeUndefined();
});
it('maps a grouped count() payload to the Table view', () => {
const query = {
requestType: 'scalar',
schemaVersion: 'v5',
compositeQuery: {
queries: [
{
type: 'builder_query',
spec: {
signal: 'logs',
aggregations: [{ expression: 'count()' }],
groupBy: [{ name: 'service.name' }],
},
},
],
},
};
expect(
getPanelTypeForRequestType(
requestTypeFromActionQuery(query),
ApplyFilterSignalDTO.logs,
),
).toBe(PANEL_TYPES.TABLE);
});
});

View File

@@ -0,0 +1,30 @@
import {
ApplyFilterSignalDTO,
type MessageActionDTOQuery,
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
export function getPanelTypeForRequestType(
requestType: unknown,
signal: ApplyFilterSignalDTO,
): PANEL_TYPES {
switch (requestType) {
case Querybuildertypesv5RequestTypeDTO.scalar:
return PANEL_TYPES.TABLE;
case Querybuildertypesv5RequestTypeDTO.time_series:
return PANEL_TYPES.TIME_SERIES;
case Querybuildertypesv5RequestTypeDTO.trace:
return signal === ApplyFilterSignalDTO.traces
? PANEL_TYPES.TRACE
: PANEL_TYPES.LIST;
default:
return PANEL_TYPES.LIST;
}
}
export function requestTypeFromActionQuery(
query: MessageActionDTOQuery | undefined,
): unknown {
return query?.requestType;
}

View File

@@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { Col, Input, Row, Select } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import BucketOptions from 'components/QueryBuilderV2/QueryV2/QueryAddOns/BucketOptions/BucketOptions';
import { parseLimit, sanitizeLimit } from 'components/QueryBuilderV2/utils';
import { LEGEND } from 'constants/global';
import { PANEL_TYPES } from 'constants/queryBuilder';
// ** Hooks
@@ -171,7 +172,8 @@ export function Formula({
<InputWithLabel
label="Limit"
type="number"
onChange={(value): void => handleChangeLimit(Number(value))}
onChange={(value): void => handleChangeLimit(parseLimit(value))}
sanitize={sanitizeLimit}
initialValue={formula?.limit ?? undefined}
placeholder="Enter limit"
/>

View File

@@ -32,6 +32,8 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
PIE: false,
HISTOGRAM: false,
HEATMAP: false,
TOP_LIST: false,
GEO_MAP: false,
TEXT: false,
EMPTY_WIDGET: false,
};

View File

@@ -24,4 +24,6 @@ export const PanelTypeVsPanelWrapper = {
[PANEL_TYPES.HISTOGRAM]: HistogramPanel,
// V2-only kind; it renders through the V2 panel registry.
[PANEL_TYPES.HEATMAP]: null,
[PANEL_TYPES.TOP_LIST]: null,
[PANEL_TYPES.GEO_MAP]: null,
};

View File

@@ -32,6 +32,17 @@ export function isAIBuilderEnvelope(
);
}
/** Exactly one `builder_query`: the only request a panel's default row limit applies to. */
export function isLoneBuilderQuery<T extends { type: string }>(
envelopes: T[],
): envelopes is [Extract<T, Querybuildertypesv5QueryEnvelopeBuilderDTO>] {
return (
envelopes.length === 1 &&
envelopes[0].type ===
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
);
}
const BUILDER_PLUGIN_KINDS: string[] = [
DashboardtypesQueryPluginKindDTO['signoz/BuilderQuery'],
DashboardtypesQueryPluginKindDTO['signoz/AIBuilderQuery'],

View File

@@ -17,6 +17,8 @@ export type PartialPanelTypes = {
[PANEL_TYPES.SCATTER]: 'scatter';
[PANEL_TYPES.HISTOGRAM]: 'histogram';
[PANEL_TYPES.HEATMAP]: 'heatmap';
[PANEL_TYPES.TOP_LIST]: 'top_list';
[PANEL_TYPES.GEO_MAP]: 'geo_map';
};
export type BuilderField = keyof IBuilderQuery;
@@ -152,6 +154,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
[PANEL_TYPES.TABLE]: bySource(SERIES, SCALAR_METRICS),
[PANEL_TYPES.PIE]: bySource(SERIES, SCALAR_METRICS),
[PANEL_TYPES.SCATTER]: bySource(SERIES, SCALAR_METRICS),
[PANEL_TYPES.TOP_LIST]: bySource(SERIES, SCALAR_METRICS),
[PANEL_TYPES.GEO_MAP]: bySource(SERIES, SCALAR_METRICS),
[PANEL_TYPES.VALUE]: bySource(SINGLE_VALUE, SINGLE_VALUE_METRICS),
[PANEL_TYPES.LIST]: bySource(RAW_ROWS, RAW_ROWS_METRICS),
};

View File

@@ -72,3 +72,32 @@
var(--muted-foreground) 2px 3px
);
}
.vertical {
display: flex;
gap: 10px;
height: 100%;
min-height: 0;
.track {
flex: 0 0 8px;
width: 8px;
height: auto;
}
.marker {
top: auto;
right: -3px;
left: -3px;
width: auto;
height: 2px;
transform: translateY(1px);
}
}
.scale {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
}

View File

@@ -14,6 +14,8 @@ export interface ColorBarProps {
label?: string;
/** Keys the one state a ramp cannot express: a hatched data gap. */
showNoDataKey?: boolean;
/** Vertical runs low to high from the bottom, with labels to the right. */
orientation?: 'horizontal' | 'vertical';
'data-testid'?: string;
}
@@ -25,8 +27,10 @@ export default function ColorBar({
markerPosition = null,
label,
showNoDataKey = false,
orientation = 'horizontal',
'data-testid': testId = 'color-bar',
}: ColorBarProps): JSX.Element | null {
const isVertical = orientation === 'vertical';
const gradient = useMemo(() => {
if (ramp.length === 0) {
return undefined;
@@ -39,8 +43,8 @@ export default function ColorBar({
const to = ((index + 1) / ramp.length) * 100;
return [`${color} ${from}%`, `${color} ${to}%`];
});
return `linear-gradient(to right, ${stops.join(', ')})`;
}, [ramp]);
return `linear-gradient(to ${isVertical ? 'top' : 'right'}, ${stops.join(', ')})`;
}, [ramp, isVertical]);
if (gradient === undefined) {
return null;
@@ -49,6 +53,26 @@ export default function ColorBar({
const clampedMarker =
markerPosition === null ? null : Math.min(Math.max(markerPosition, 0), 1);
if (isVertical) {
return (
<div className={Styles.vertical} data-testid={testId}>
<div className={Styles.track} style={{ background: gradient }}>
{clampedMarker !== null && (
<span
className={Styles.marker}
style={{ bottom: `${clampedMarker * 100}%` }}
data-testid={`${testId}-marker`}
/>
)}
</div>
<div className={Styles.scale}>
<span className={Styles.label}>{maxLabel}</span>
<span className={Styles.label}>{minLabel}</span>
</div>
</div>
);
}
return (
<div className={Styles.container} data-testid={testId}>
{label && <span className={Styles.caption}>{label}</span>}

View File

@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen } from 'tests/test-utils';
import ColorBar from '../ColorBar';
@@ -35,12 +35,13 @@ describe('ColorBar', () => {
});
it('clamps a marker outside the ramp to its ends', () => {
const { rerender } = render(
const { unmount } = render(
<ColorBar ramp={RAMP} minLabel="0" maxLabel="10" markerPosition={-2} />,
);
expect(screen.getByTestId('color-bar-marker')).toHaveStyle({ left: '0%' });
unmount();
rerender(
render(
<ColorBar ramp={RAMP} minLabel="0" maxLabel="10" markerPosition={4} />,
);
expect(screen.getByTestId('color-bar-marker')).toHaveStyle({ left: '100%' });
@@ -82,4 +83,20 @@ describe('ColorBar', () => {
'linear-gradient(to right, #111111 0%, #111111 50%, #dddddd 50%, #dddddd 100%)',
});
});
it('runs low to high from the bottom when vertical', () => {
render(
<ColorBar
ramp={['#000', '#fff']}
minLabel="0.18%"
maxLabel="14.2%"
markerPosition={0.25}
orientation="vertical"
/>,
);
const labels = screen.getAllByText(/%$/).map((label) => label.textContent);
expect(labels).toStrictEqual(['14.2%', '0.18%']);
expect(screen.getByTestId('color-bar-marker')).toHaveStyle({ bottom: '25%' });
});
});

View File

@@ -1,14 +1,9 @@
import { useMemo } from 'react';
import cx from 'classnames';
import { Pin } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { ScatterTooltipProps } from '../types';
import TooltipCard from './components/TooltipCard/TooltipCard';
import { buildChannelRows, resolveHoveredPoint } from './scatterTooltipContent';
import Styles from './ScatterTooltip.module.scss';
/** Own component: the shared Tooltip lists one row per series at a shared x. */
export default function ScatterTooltip({
uPlotInstance,
@@ -50,90 +45,18 @@ export default function ScatterTooltip({
}
return (
<div
className={cx(Styles.container, { [Styles.pinned]: isPinned })}
data-pinned={isPinned}
data-testid="scatter-tooltip"
>
<div className={Styles.header}>
<span
className={Styles.marker}
style={{
backgroundColor: hoveredPoint.color,
borderColor: hoveredPoint.color,
}}
/>
<Typography.Text
as="span"
className={Styles.title}
title={hoveredPoint.label}
testId="scatter-tooltip-title"
>
{hoveredPoint.label}
</Typography.Text>
{isPinned && (
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<Pin size={12} />}
testId="scatter-tooltip-status"
>
Pinned
</Button>
)}
</div>
<span className={Styles.divider} />
<div className={Styles.rows}>
{rows.map((row) => (
<div
key={row.label}
className={Styles.row}
data-testid="scatter-tooltip-row"
>
<Typography.Text as="span" className={Styles.rowLabel}>
{row.label}
</Typography.Text>
<Typography.Text as="span" className={Styles.rowValue}>
{row.value}
</Typography.Text>
</div>
))}
</div>
{labels.length > 0 && (
<>
<span className={Styles.divider} />
<div className={Styles.rows}>
{labels.map((label) => (
<div
key={label.key}
className={cx(Styles.row, Styles.rowMuted)}
data-testid="scatter-tooltip-label"
>
<Typography.Text
as="span"
className={Styles.rowLabel}
title={label.key}
>
{label.key}
</Typography.Text>
<Typography.Text
as="span"
className={Styles.rowValue}
title={label.value}
>
{label.value}
</Typography.Text>
</div>
))}
</div>
</>
)}
{renderTooltipFooter?.({ isPinned, dismiss })}
</div>
<TooltipCard
title={hoveredPoint.label}
color={hoveredPoint.color}
rows={rows.map((row) => ({ key: row.label, ...row }))}
mutedRows={labels.map((label) => ({
key: label.key,
label: label.key,
value: label.value,
}))}
isPinned={isPinned}
footer={renderTooltipFooter?.({ isPinned, dismiss })}
testId="scatter-tooltip"
/>
);
}

View File

@@ -49,7 +49,7 @@
font-family: var(--font-mono);
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--text-vanilla-100);
color: var(--l1-foreground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -70,11 +70,11 @@
font-family: var(--font-mono);
font-size: var(--font-size-xs);
font-variant-numeric: tabular-nums;
color: var(--text-vanilla-100);
color: var(--l1-foreground);
}
.rowMuted {
color: var(--text-vanilla-400);
color: var(--l3-foreground);
}
.rowLabel {

View File

@@ -0,0 +1,119 @@
import type { ReactNode } from 'react';
import cx from 'classnames';
import { Pin } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import Styles from './TooltipCard.module.scss';
export interface TooltipCardRow {
key: string;
label: string;
value: string;
}
interface TooltipCardProps {
title: string;
/** The marker beside the title, matching the item's mark on the chart. */
color: string;
/** What the item says. */
rows: TooltipCardRow[];
/** What names the item, below the rows and muted. */
mutedRows?: TooltipCardRow[];
isPinned?: boolean;
footer?: ReactNode;
/** Prefixes the test ids of the title, status, rows and muted rows. */
testId: string;
}
/** One item's tooltip: a marked title, then label/value rows, on the shared tooltip surface. */
export default function TooltipCard({
title,
color,
rows,
mutedRows = [],
isPinned = false,
footer,
testId,
}: TooltipCardProps): JSX.Element {
return (
<div
className={cx(Styles.container, { [Styles.pinned]: isPinned })}
data-pinned={isPinned}
data-testid={testId}
>
<div className={Styles.header}>
<span
className={Styles.marker}
style={{ backgroundColor: color, borderColor: color }}
/>
<Typography.Text
as="span"
className={Styles.title}
title={title}
testId={`${testId}-title`}
>
{title}
</Typography.Text>
{isPinned && (
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<Pin size={12} />}
testId={`${testId}-status`}
>
Pinned
</Button>
)}
</div>
<span className={Styles.divider} />
<div className={Styles.rows}>
{rows.map((row) => (
<div key={row.key} className={Styles.row} data-testid={`${testId}-row`}>
<Typography.Text as="span" className={Styles.rowLabel}>
{row.label}
</Typography.Text>
<Typography.Text as="span" className={Styles.rowValue}>
{row.value}
</Typography.Text>
</div>
))}
</div>
{mutedRows.length > 0 && (
<>
<span className={Styles.divider} />
<div className={Styles.rows}>
{mutedRows.map((row) => (
<div
key={row.key}
className={cx(Styles.row, Styles.rowMuted)}
data-testid={`${testId}-label`}
>
<Typography.Text
as="span"
className={Styles.rowLabel}
title={row.label}
>
{row.label}
</Typography.Text>
<Typography.Text
as="span"
className={Styles.rowValue}
title={row.value}
>
{row.value}
</Typography.Text>
</div>
))}
</div>
</>
)}
{footer}
</div>
);
}

View File

@@ -1,290 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import {
clampColorSteps,
createHeatmapColorResolver,
DEFAULT_COLOR_STEPS,
DEFAULT_HEATMAP_COLORS,
getMaxCount,
getSmallestPositiveCount,
MAX_COLOR_STEPS,
MIN_OPACITY_ALPHA,
normalizeCount,
resolveCountDomain,
} from '../colorScale';
import { HeatmapColorMode, HeatmapColorScale } from '../types';
const SERIES_COLOR = '#4e74f8';
describe('getMaxCount', () => {
it('ignores null cells', () => {
expect(
getMaxCount([
[1, null, 9],
[null, 4],
]),
).toBe(9);
});
it('returns 0 for an empty or all-null grid', () => {
expect(getMaxCount([])).toBe(0);
expect(getMaxCount([[null, null]])).toBe(0);
});
it('ignores non-finite counts', () => {
expect(getMaxCount([[3, Number.POSITIVE_INFINITY, Number.NaN]])).toBe(3);
});
});
describe('getSmallestPositiveCount', () => {
it('ignores nulls, zeros and non-finite counts', () => {
expect(
getSmallestPositiveCount([
[0, null, 4],
[Number.NaN, 2, -3],
]),
).toBe(2);
});
it('returns null when nothing is above zero', () => {
expect(getSmallestPositiveCount([[0, null]])).toBeNull();
});
});
describe('resolveCountDomain', () => {
it('floors at 0 on auto so a zero count sits at the bottom of the scale', () => {
expect(
resolveCountDomain({ minCount: null, maxCount: null }, [[5, 20]]),
).toStrictEqual({
min: 0,
max: 20,
logFloor: 5,
});
});
it('honours explicit clamps', () => {
expect(
resolveCountDomain({ minCount: 10, maxCount: 100 }, [[5, 20]]),
).toStrictEqual({
min: 10,
max: 100,
logFloor: 5,
});
});
it('collapses a max at or below min', () => {
expect(
resolveCountDomain({ minCount: 50, maxCount: 10 }, [[5]]),
).toStrictEqual({
min: 50,
max: 50,
logFloor: 5,
});
});
it('takes the log floor from the smallest positive count, below 1 included', () => {
expect(
resolveCountDomain({ minCount: null, maxCount: null }, [[0, 0.02, 0.8]])
.logFloor,
).toBeCloseTo(0.02, 6);
});
it('keeps the log floor within MAX_LOG_DECADES of the max', () => {
expect(
resolveCountDomain({ minCount: null, maxCount: null }, [[1, 1e9]]).logFloor,
).toBe(1e3);
});
it('falls back to a floor of 1 for a grid without a positive count', () => {
expect(
resolveCountDomain({ minCount: null, maxCount: null }, [[null, 0]]).logFloor,
).toBe(1);
});
});
describe('normalizeCount', () => {
const domain = { min: 0, max: 1000, logFloor: 1 };
it('spreads low counts on a log scale where a linear one washes them out', () => {
const log = (count: number): number =>
normalizeCount({ count, domain, scale: HeatmapColorScale.Log });
expect(log(10)).toBeCloseTo(1 / 3, 5);
expect(log(20)).toBeCloseTo(Math.log10(20) / 3, 5);
expect(
normalizeCount({ count: 10, domain, scale: HeatmapColorScale.Linear }),
).toBeCloseTo(0.01, 5);
});
it('puts 0 and 1 at the bottom of a log scale', () => {
expect(
normalizeCount({ count: 0, domain, scale: HeatmapColorScale.Log }),
).toBe(0);
expect(
normalizeCount({ count: 1, domain, scale: HeatmapColorScale.Log }),
).toBe(0);
});
it('reaches the top of the scale at max on every scale', () => {
[
HeatmapColorScale.Log,
HeatmapColorScale.Sqrt,
HeatmapColorScale.Linear,
].forEach((scale) => {
expect(normalizeCount({ count: 1000, domain, scale })).toBeCloseTo(1, 6);
});
});
it('takes the square root of the linear position on a sqrt scale', () => {
expect(
normalizeCount({
count: 250,
domain: { min: 0, max: 1000, logFloor: 1 },
scale: HeatmapColorScale.Sqrt,
}),
).toBeCloseTo(0.5, 6);
});
it('clamps counts outside the domain', () => {
const scale = HeatmapColorScale.Linear;
expect(normalizeCount({ count: -5, domain, scale })).toBe(0);
expect(normalizeCount({ count: 5000, domain, scale })).toBe(1);
});
it('returns the bottom of the scale when min equals max', () => {
expect(
normalizeCount({
count: 7,
domain: { min: 7, max: 7, logFloor: 1 },
scale: HeatmapColorScale.Log,
}),
).toBe(0);
});
it('spreads a log domain that sits entirely below a count of 1', () => {
const fractional = { min: 0, max: 1, logFloor: 0.001 };
const log = (count: number): number =>
normalizeCount({
count,
domain: fractional,
scale: HeatmapColorScale.Log,
});
expect(log(0.001)).toBe(0);
expect(log(0.1)).toBeCloseTo(2 / 3, 5);
expect(log(1)).toBeCloseTo(1, 6);
});
it('reads a log scale linearly when the floor reaches the top of the domain', () => {
const domainAtFloor = { min: 0, max: 1, logFloor: 1 };
expect(
normalizeCount({
count: 1,
domain: domainAtFloor,
scale: HeatmapColorScale.Log,
}),
).toBe(1);
expect(
normalizeCount({
count: 0.5,
domain: domainAtFloor,
scale: HeatmapColorScale.Log,
}),
).toBe(0.5);
});
});
describe('clampColorSteps', () => {
it('clamps to the supported range', () => {
expect(clampColorSteps(1)).toBe(2);
expect(clampColorSteps(500)).toBe(MAX_COLOR_STEPS);
expect(clampColorSteps(32)).toBe(32);
});
it('falls back to the default for a non-finite value', () => {
expect(clampColorSteps(Number.NaN)).toBe(DEFAULT_COLOR_STEPS);
});
});
describe('createHeatmapColorResolver', () => {
const build = (
overrides: Partial<typeof DEFAULT_HEATMAP_COLORS> = {},
isDarkMode = true,
): ReturnType<typeof createHeatmapColorResolver> =>
createHeatmapColorResolver({
options: { ...DEFAULT_HEATMAP_COLORS, ...overrides },
domain: { min: 0, max: 1000, logFloor: 1 },
isDarkMode,
seriesColor: SERIES_COLOR,
});
it('leaves null cells uncoloured so they can be hatched', () => {
const resolver = build();
expect(resolver.colorFor(null)).toBeNull();
expect(resolver.positionOf(null)).toBeNull();
});
it('recedes a zero count into the surface, not the null treatment', () => {
const dark = build();
const light = build({}, false);
expect(dark.colorFor(0)).toBe(Color.BG_INK_500);
expect(light.colorFor(0)).toBe(Color.BG_VANILLA_100);
});
it('keeps the bottom of the ramp for the smallest count that occurred', () => {
const resolver = build();
// Zero and the floor both sit at position 0; only the floor takes a colour.
expect(resolver.colorFor(1)).toBe(resolver.ramp[0]);
});
it('colours zero off the ramp once an explicit minimum lifts it off the bottom', () => {
const resolver = createHeatmapColorResolver({
options: { ...DEFAULT_HEATMAP_COLORS, minCount: 5 },
domain: { min: 5, max: 1000, logFloor: 5 },
isDarkMode: true,
seriesColor: SERIES_COLOR,
});
expect(resolver.colorFor(0)).toBe(resolver.ramp[0]);
});
it('emits one ramp entry per step', () => {
expect(build({ steps: 8 }).ramp).toHaveLength(8);
});
it('maps the max count to the top of the ramp', () => {
const resolver = build({ steps: 8 });
expect(resolver.colorFor(1000)).toBe(resolver.ramp[7]);
});
it('picks different stops per theme so low counts stay near the surface', () => {
expect(build({}, true).ramp[0]).not.toBe(build({}, false).ramp[0]);
});
it('varies alpha in opacity mode, never below the visibility floor', () => {
const resolver = build({ mode: HeatmapColorMode.Opacity, steps: 4 });
expect(resolver.ramp[0]).toBe(`rgba(78, 116, 248, ${MIN_OPACITY_ALPHA})`);
// `color` drops the alpha channel from the string once it reaches 1.
expect(resolver.ramp[3]).toBe('rgb(78, 116, 248)');
});
it('prefers an explicit opacity fill over the series colour', () => {
const resolver = build({
mode: HeatmapColorMode.Opacity,
fill: '#e5484d',
steps: 2,
});
expect(resolver.ramp[1]).toBe('rgb(229, 72, 77)');
});
it('reports the domain it applied', () => {
expect(build().domain).toStrictEqual({ min: 0, max: 1000, logFloor: 1 });
});
});

View File

@@ -1,6 +1,6 @@
import type uPlot from 'uplot';
import { DEFAULT_HEATMAP_COLORS } from '../colorScale';
import { DEFAULT_COLOR_RAMP_OPTIONS } from 'lib/visualization/colorRamp/colorScale';
import { resolveHeatmapYAxis } from '../geometry';
import { createHeatmapHooks } from '../heatmapPlugin';
import { HeatmapAxisScale, HeatmapCell } from '../types';
@@ -103,7 +103,7 @@ function createHooks(
return createHeatmapHooks({
yAxis: Y_AXIS,
step: STEP,
colors: DEFAULT_HEATMAP_COLORS,
colors: DEFAULT_COLOR_RAMP_OPTIONS,
isDarkMode: true,
seriesColor: '#4e74f8',
dimOnHover,

View File

@@ -1,257 +0,0 @@
import { Color as DesignToken } from '@signozhq/design-tokens';
import Color from 'color';
import { DEFAULT_HEATMAP_PALETTE, getPaletteStops } from './palettes';
import {
HeatmapColorMode,
HeatmapColorOptions,
HeatmapColorScale,
} from './types';
export const MIN_COLOR_STEPS = 2;
export const MAX_COLOR_STEPS = 128;
export const DEFAULT_COLOR_STEPS = 64;
/** Without a floor, the lowest counts read as "no data". */
export const MIN_OPACITY_ALPHA = 0.1;
/** Log's bottom when the grid holds no positive count to take it from. */
const DEFAULT_LOG_FLOOR = 1;
/** Widest span a single ramp is stretched over on a log scale. */
const MAX_LOG_DECADES = 6;
/** Used when neither an explicit fill nor a series colour is available. */
export const DEFAULT_OPACITY_FILL = DesignToken.BG_ROBIN_500;
/** Zero takes the panel surface so an empty stretch recedes, leaving the ramp's
* bottom to the smallest count that did occur — on a log scale the two collapse. */
const ZERO_FILL: Record<'dark' | 'light', string> = {
dark: DesignToken.BG_INK_500,
light: DesignToken.BG_VANILLA_100,
};
export const DEFAULT_HEATMAP_COLORS: HeatmapColorOptions = {
mode: HeatmapColorMode.Palette,
scale: HeatmapColorScale.Log,
minCount: null,
maxCount: null,
palette: DEFAULT_HEATMAP_PALETTE,
steps: DEFAULT_COLOR_STEPS,
fill: '',
};
export interface CountDomain {
min: number;
max: number;
/** Bottom decade of the log scale: the smallest count it separates from zero.
* Always positive, since log has no bottom otherwise. */
logFloor: number;
}
/** Highest count, ignoring `null`. 0 for an empty grid. */
export function getMaxCount(counts: Array<Array<number | null>>): number {
let max = 0;
for (const row of counts) {
for (const count of row) {
if (count !== null && Number.isFinite(count) && count > max) {
max = count;
}
}
}
return max;
}
/** Smallest count above zero, ignoring `null`. `null` for a grid without one. */
export function getSmallestPositiveCount(
counts: Array<Array<number | null>>,
): number | null {
let smallest: number | null = null;
for (const row of counts) {
for (const count of row) {
if (
count !== null &&
Number.isFinite(count) &&
count > 0 &&
(smallest === null || count < smallest)
) {
smallest = count;
}
}
}
return smallest;
}
/**
* The grid's own resolution: whole counts floor at 1, while a heatmap of rates or
* ratios can live entirely below it, where a fixed floor of 1 would flatten every
* cell onto the bottom colour. Capped at `MAX_LOG_DECADES` so one stray tiny cell
* cannot stretch the ramp over a range nothing else occupies.
*/
function resolveLogFloor(
counts: Array<Array<number | null>>,
max: number,
): number {
const smallest = getSmallestPositiveCount(counts) ?? DEFAULT_LOG_FLOOR;
return Math.max(smallest, max / 10 ** MAX_LOG_DECADES);
}
/** Explicit clamps win; otherwise 0 to the grid's highest count. */
export function resolveCountDomain(
options: Pick<HeatmapColorOptions, 'minCount' | 'maxCount'>,
counts: Array<Array<number | null>>,
): CountDomain {
const min = options.minCount ?? 0;
const max = options.maxCount ?? getMaxCount(counts);
const logFloor = resolveLogFloor(counts, max);
return max > min ? { min, max, logFloor } : { min, max: min, logFloor };
}
/** Position on the colour scale, 0..1. A degenerate domain collapses to 0 so an
* all-zero grid renders at the bottom rather than disappearing. */
export function normalizeCount({
count,
domain,
scale,
}: {
count: number;
domain: CountDomain;
scale: HeatmapColorScale;
}): number {
const { min, max } = domain;
if (!(max > min)) {
return 0;
}
const clamped = Math.min(Math.max(count, min), max);
if (scale === HeatmapColorScale.Log) {
// Anything at or below the floor sits at the bottom; log cannot place it.
const bottom = Math.max(min, domain.logFloor);
const logMin = Math.log10(bottom);
const logMax = Math.log10(max);
if (logMax > logMin) {
return (Math.log10(Math.max(clamped, bottom)) - logMin) / (logMax - logMin);
}
// The floor already reaches the top of the domain, so there is no span to
// spread logarithmically. Fall through rather than flatten the whole grid.
}
const linear = (clamped - min) / (max - min);
return scale === HeatmapColorScale.Sqrt ? Math.sqrt(linear) : linear;
}
export function clampColorSteps(steps: number): number {
if (!Number.isFinite(steps)) {
return DEFAULT_COLOR_STEPS;
}
return Math.min(Math.max(Math.round(steps), MIN_COLOR_STEPS), MAX_COLOR_STEPS);
}
/** Colour at `t` (0..1) along a multi-stop ramp. */
function sampleStops(stops: string[], t: number): string {
if (stops.length === 0) {
return 'transparent';
}
if (stops.length === 1) {
return stops[0];
}
const scaled = Math.min(Math.max(t, 0), 1) * (stops.length - 1);
const lower = Math.min(Math.floor(scaled), stops.length - 2);
return Color(stops[lower])
.mix(Color(stops[lower + 1]), scaled - lower)
.hex();
}
/**
* Colour the densest cells are drawn with — the palette's extreme, or the opacity
* fill at full strength. Depends only on the options, not on the data, so callers
* can read it before a grid exists.
*/
export function resolveExtremeColor({
options,
isDarkMode,
seriesColor,
}: {
options: HeatmapColorOptions;
isDarkMode: boolean;
seriesColor: string;
}): string {
if (options.mode === HeatmapColorMode.Opacity) {
return options.fill || seriesColor || DEFAULT_OPACITY_FILL;
}
const stops = getPaletteStops(options.palette, isDarkMode);
return stops[stops.length - 1] ?? DEFAULT_OPACITY_FILL;
}
export interface HeatmapColorResolver {
/** `null` for a `null` count, which must be hatched. */
colorFor: (count: number | null) => string | null;
/** 0..1, or `null` for a `null` count. */
positionOf: (count: number | null) => number | null;
/** Low to high. The colour bar renders exactly these. */
ramp: string[];
domain: CountDomain;
}
/** Palette mode walks a sequential ramp; opacity mode varies the alpha of one
* fill, so the grid matches its group's legend swatch. */
export function createHeatmapColorResolver({
options,
domain,
isDarkMode,
seriesColor,
}: {
options: HeatmapColorOptions;
domain: CountDomain;
isDarkMode: boolean;
/** Opacity-mode fill when `options.fill` is empty. */
seriesColor: string;
}): HeatmapColorResolver {
const steps = clampColorSteps(options.steps);
const positions = Array.from({ length: steps }, (_, index) =>
steps === 1 ? 0 : index / (steps - 1),
);
let ramp: string[];
if (options.mode === HeatmapColorMode.Opacity) {
const base = Color(options.fill || seriesColor || DEFAULT_OPACITY_FILL);
ramp = positions.map((t) =>
base
.alpha(MIN_OPACITY_ALPHA + t * (1 - MIN_OPACITY_ALPHA))
.rgb()
.string(),
);
} else {
const stops = getPaletteStops(options.palette, isDarkMode);
ramp = positions.map((t) => sampleStops(stops, t));
}
const positionOf = (count: number | null): number | null => {
if (count === null || !Number.isFinite(count)) {
return null;
}
return normalizeCount({ count, domain, scale: options.scale });
};
// An explicit `minCount` above zero makes it a clamped value like any other.
const zeroFill =
domain.min <= 0 ? ZERO_FILL[isDarkMode ? 'dark' : 'light'] : null;
return {
positionOf,
colorFor: (count): string | null => {
if (count === 0 && zeroFill !== null) {
return zeroFill;
}
const t = positionOf(count);
if (t === null) {
return null;
}
const index = Math.min(Math.floor(t * steps), steps - 1);
return ramp[index];
},
ramp,
domain,
};
}

View File

@@ -1,10 +1,10 @@
import uPlot from 'uplot';
import {
createHeatmapColorResolver,
HeatmapColorResolver,
resolveCountDomain,
} from './colorScale';
createColorRampResolver,
ColorRampResolver,
resolveColorDomain,
} from 'lib/visualization/colorRamp/colorScale';
import { resolveColumnIndex, resolveRowIndex } from './geometry';
import {
createHoverOverlay,
@@ -12,13 +12,14 @@ import {
showHoverOverlay,
} from './hoverOverlay';
import { createHatchPattern, drawCells, drawOverflowBoundary } from './paint';
import { HeatmapCell, HeatmapColorOptions, HeatmapYAxis } from './types';
import { HeatmapCell, HeatmapYAxis } from './types';
import { ColorRampOptions } from 'lib/visualization/colorRamp/types';
export interface HeatmapRenderOptions {
yAxis: HeatmapYAxis;
/** Column width in seconds. */
step: number;
colors: HeatmapColorOptions;
colors: ColorRampOptions;
isDarkMode: boolean;
/** Opacity-mode fill when `colors.fill` is empty. */
seriesColor: string;
@@ -58,15 +59,15 @@ export function createHeatmapHooks({
// config-build time. Resolving lazily keeps a refetch on uPlot's `setData` path
// rather than forcing a rebuild.
let cachedData: uPlot.AlignedData | null = null;
let cachedResolver: HeatmapColorResolver | null = null;
let cachedResolver: ColorRampResolver | null = null;
function getResolver(u: uPlot): HeatmapColorResolver {
function getResolver(u: uPlot): ColorRampResolver {
if (cachedResolver && cachedData === u.data) {
return cachedResolver;
}
cachedResolver = createHeatmapColorResolver({
cachedResolver = createColorRampResolver({
options: colors,
domain: resolveCountDomain(
domain: resolveColorDomain(
colors,
u.data.slice(1) as Array<Array<number | null>>,
),

View File

@@ -1,7 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import uPlot from 'uplot';
import { HeatmapColorResolver } from './colorScale';
import { ColorRampResolver } from 'lib/visualization/colorRamp/colorScale';
import { HeatmapYAxis } from './types';
/** Cells at least this wide/tall keep a hairline separator. */
@@ -54,7 +54,7 @@ export function drawCells({
u: uPlot;
yAxis: HeatmapYAxis;
step: number;
resolver: HeatmapColorResolver;
resolver: ColorRampResolver;
hatchPattern: CanvasPattern | null;
}): void {
const { ctx } = u;

View File

@@ -1,44 +1,3 @@
export enum HeatmapColorScale {
Log = 'log',
Sqrt = 'sqrt',
Linear = 'linear',
}
export enum HeatmapColorMode {
Palette = 'palette',
Opacity = 'opacity',
}
/** Sequential ramps only: colour means "count", so a midpoint or hue cycle would
* read as a threshold that does not exist. */
export enum HeatmapColorPalette {
Ice = 'ice',
Moss = 'moss',
Rust = 'rust',
Graphite = 'graphite',
Ember = 'ember',
Lagoon = 'lagoon',
Orchid = 'orchid',
Verdant = 'verdant',
Lava = 'lava',
Beacon = 'beacon',
}
export interface HeatmapColorOptions {
mode: HeatmapColorMode;
scale: HeatmapColorScale;
/** `null` derives it, which is always 0 — a count of 0 belongs at the bottom. */
minCount: number | null;
/** `null` derives it from the grid's highest count. */
maxCount: number | null;
palette: HeatmapColorPalette;
/** Colour steps the ramp is quantised into, 2..128. Unrelated to `step`, the
* column width in seconds. */
steps: number;
/** Opacity mode. Empty falls back to the caller's series colour. */
fill: string;
}
/** Row-height distribution of the bucket axis. */
export enum HeatmapAxisScale {
/** Whichever of the three below the boundaries admit: log when they are all

View File

@@ -0,0 +1,35 @@
// Same theme hooks as the SVG chart; canvas reads them through readPalette().
.wrapper {
--geo-no-data: var(--l3-background);
--geo-context: var(--l2-background);
--geo-border: var(--l1-background);
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}
.wrapper[data-legend-position='right'] {
flex-direction: row;
gap: 12px;
}
.mapArea {
position: relative;
flex: 1 1 auto;
min-width: 0;
min-height: 0;
}
.canvas {
position: absolute;
inset: 0;
}
.tooltip {
position: absolute;
pointer-events: none;
z-index: 9999;
}

View File

@@ -0,0 +1,251 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import CountryTooltip from './components/CountryTooltip/CountryTooltip';
import MapLegend from './components/MapLegend/MapLegend';
import MapStatus from './components/MapStatus/MapStatus';
import ZoomControls from './components/ZoomControls/ZoomControls';
import { useGeoMapScales } from './hooks/useGeoMapScales';
import { useGeoMapShapes } from './hooks/useGeoMapShapes';
import { formatCoordinates } from './utils/coordinates';
import { findShapeAt } from './utils/olFeatures';
import { pointAsDatum } from './utils/scales';
import { GeoMapProps } from './types';
import { useOlLayers } from './hooks/useOlLayers';
import { useOlMap } from './hooks/useOlMap';
import { useOlPointer } from './hooks/useOlPointer';
import { useOlPointsLayer } from './hooks/useOlPointsLayer';
import { useOlView } from './hooks/useOlView';
import { OlPalette, readPalette } from './utils/olStyles';
import styles from './GeoMap.module.scss';
export default function GeoMap({
data,
points,
view,
country,
projection,
layer,
colors,
bubbleRadius,
formatValue,
valueLabel,
sizeMetric,
showLabels = true,
colorKeys,
legendPosition = LegendPosition.BOTTOM,
isDarkMode,
onSelect,
renderTooltipFooter,
}: GeoMapProps): JSX.Element {
const wrapperRef = useRef<HTMLDivElement>(null);
const mapAreaRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLDivElement>(null);
const { width, height } = useResizeObserver(mapAreaRef, 100);
const [palette, setPalette] = useState<OlPalette | null>(null);
const {
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
showTooltip,
hideTooltip,
} = useTooltip<string>();
const { containerRef, TooltipInPortal } = useTooltipInPortal({
scroll: true,
detectBounds: true,
});
// On the points layer the scales colour and size points, keyed by point id.
const scaleData = useMemo(
() => (layer === 'points' ? (points ?? []).map(pointAsDatum) : data),
[layer, points, data],
);
const { resolver, valueByCode, sizeByCode, radiusFor, fillFor } =
useGeoMapScales(
scaleData,
colors,
bubbleRadius,
isDarkMode,
Boolean(sizeMetric),
);
const maxSize = useMemo(
() => [...sizeByCode.values()].reduce((max, size) => Math.max(max, size), 0),
[sizeByCode],
);
const { shapes, status, retry } = useGeoMapShapes(view, country);
const handle = useOlMap(canvasRef);
const pointsLayer = useOlPointsLayer(handle, layer === 'points');
useEffect(() => {
if (wrapperRef.current) {
setPalette(readPalette(wrapperRef.current));
}
}, [isDarkMode]);
const handleHover = useCallback(
(code: string | null, clientX: number, clientY: number): void => {
const rect = mapAreaRef.current?.getBoundingClientRect();
if (!code || !rect || !valueByCode.has(code)) {
hideTooltip();
return;
}
showTooltip({
tooltipData: code,
tooltipLeft: clientX - rect.left,
tooltipTop: clientY - rect.top,
});
},
[valueByCode, showTooltip, hideTooltip],
);
const pointById = useMemo(
() =>
new Map(
(layer === 'points' ? (points ?? []) : []).map((point) => [
point.id,
point,
]),
),
[layer, points],
);
const handleSelect = useCallback(
(code: string, clientX: number, clientY: number): void => {
const datum = valueByCode.get(code);
if (datum && onSelect) {
hideTooltip();
onSelect({ datum, point: pointById.get(code), clientX, clientY });
}
},
[valueByCode, pointById, onSelect, hideTooltip],
);
const hovered =
tooltipOpen && tooltipData ? valueByCode.get(tooltipData) : undefined;
const hoveredPoint = hovered ? pointById.get(hovered.code) : undefined;
// A point without a label is named by the shape it falls in.
const hoveredPlace = useMemo(() => {
if (!hoveredPoint || hoveredPoint.label || !handle || !shapes) {
return undefined;
}
const code = findShapeAt(
[handle.layers.members.getSource(), handle.layers.context.getSource()],
[hoveredPoint.lon, hoveredPoint.lat],
projection,
);
return code ? shapes.nameOf(code) : undefined;
}, [hoveredPoint, handle, shapes, projection]);
useOlLayers({
handle,
shapes,
projection,
data,
points,
pointsLayer,
layer,
palette,
fillFor,
sizeByCode,
radiusFor,
hasSizeMetric: Boolean(sizeMetric),
showLabels,
hoveredCode: hovered?.code ?? null,
});
const zoom = useOlView(
handle,
view,
projection,
country,
data,
width,
height,
layer === 'points' ? points : undefined,
);
useOlPointer(handle, handleHover, handleSelect, pointsLayer.layer);
return (
<div
className={styles.wrapper}
ref={wrapperRef}
data-geo-map
data-legend-position={legendPosition}
data-testid="geo-map"
>
<div
className={styles.mapArea}
ref={(node): void => {
(mapAreaRef as React.MutableRefObject<HTMLDivElement | null>).current =
node;
containerRef(node);
}}
>
<div
ref={canvasRef}
className={styles.canvas}
data-testid="geo-map-canvas"
/>
{status !== 'ready' && <MapStatus status={status} onRetry={retry} />}
{status === 'ready' && (
<ZoomControls
canZoomIn={zoom.canZoomIn}
canZoomOut={zoom.canZoomOut}
onZoomIn={zoom.zoomIn}
onZoomOut={zoom.zoomOut}
onReset={zoom.reset}
/>
)}
{hovered && (
<TooltipInPortal
left={tooltipLeft}
top={tooltipTop}
className={styles.tooltip}
unstyled
applyPositionStyle
>
<CountryTooltip
datum={hovered}
name={
hoveredPoint
? hoveredPoint.label ||
hoveredPlace ||
formatCoordinates(hoveredPoint.lat, hoveredPoint.lon)
: (shapes?.nameOf(hovered.code) ?? hovered.code)
}
place={
hoveredPoint && formatCoordinates(hoveredPoint.lat, hoveredPoint.lon)
}
color={fillFor(hovered.code) ?? resolver.colorFor(hovered.value)}
formatValue={formatValue}
valueLabel={valueLabel}
sizeMetric={sizeMetric}
footer={renderTooltipFooter?.()}
/>
</TooltipInPortal>
)}
</div>
<MapLegend
resolver={resolver}
scale={colors.scale}
formatValue={formatValue}
markerValue={hovered?.value ?? null}
valueLabel={valueLabel}
colorKeys={colorKeys}
position={legendPosition}
size={
sizeMetric && {
label: sizeMetric.label,
maxSize,
radiusFor,
formatValue: sizeMetric.formatValue,
}
}
/>
</div>
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[1.406456,42.428677,1.765091,42.649362],"transform":{"scale":[0.00003586708670867087,0.000022070707070707384],"translate":[1.406456,42.428677]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1,2]],"id":"AD-02","properties":{"code":"AD-02","name":"Canillo","label":[1.605,42.584]}},{"type":"Polygon","arcs":[[3,4,5,-2]],"id":"AD-03","properties":{"code":"AD-03","name":"Encamp","label":[1.644,42.538]}},{"type":"Polygon","arcs":[[6,-6,7,8,9]],"id":"AD-04","properties":{"code":"AD-04","name":"La Massana","label":[1.471,42.55]}},{"type":"Polygon","arcs":[[-3,-7,10]],"id":"AD-05","properties":{"code":"AD-05","name":"Ordino","label":[1.524,42.604]}},{"type":"Polygon","arcs":[[-9,11,12,13]],"id":"AD-06","properties":{"code":"AD-06","name":"Sant Julià de Lòria","label":[1.48,42.451]}},{"type":"Polygon","arcs":[[14,-12,-8]],"id":"AD-07","properties":{"code":"AD-07","name":"Andorra la Vella","label":[1.511,42.513]}},{"type":"Polygon","arcs":[[15,-13,-15,-5]],"id":"AD-08","properties":{"code":"AD-08","name":"Escaldes-Engordany","label":[1.594,42.495]}}]}},"arcs":[[[5322,8756],[306,-172],[3169,-375],[-242,-920],[450,-342],[648,-241],[235,-409],[111,-194],[-700,-79],[-510,-594],[-128,-391]],[[8661,5039],[-190,223],[-269,202],[-291,168],[-103,573],[-104,404],[-394,-202],[-539,0],[-581,-101],[-663,-168],[-539,0],[-394,0],[-436,-68]],[[4158,6070],[63,539],[186,438],[291,337],[373,135],[290,135],[0,404],[-39,698]],[[8661,5039],[-181,-551],[-100,-1130],[-266,-377],[-311,-175],[-337,-5],[-331,146],[-72,140],[-78,38]],[[6985,3125],[-90,553],[-352,371],[-332,202],[-580,67],[-436,-101],[-373,236],[-83,404],[-207,135],[-353,-34]],[[4179,4958],[0,641],[-21,471]],[[1253,7855],[231,-235],[311,0],[290,-67],[145,-236],[166,-573],[249,-303],[394,-135],[477,-135],[373,-67],[269,-34]],[[4179,4958],[-352,0],[-332,-168],[-394,-101],[-415,-337],[-435,-303]],[[2251,4049],[-394,-135],[-373,-236],[-271,-262]],[[1213,3416],[-95,717],[-490,524],[-628,-101],[92,516],[461,959],[3,175],[-233,190],[34,427],[156,458],[124,272],[616,302]],[[1253,7855],[430,1786],[882,-55],[818,376],[426,37],[1513,-1243]],[[2251,4049],[41,-405],[21,-269],[104,-438],[166,-202]],[[2583,2735],[477,-135],[373,-169],[518,101],[456,68],[291,-68],[-187,-404],[41,-472],[82,-551]],[[4634,1105],[-787,-123],[-156,-213],[-121,-260],[-176,-257],[-322,-215],[-228,-37],[-1688,270],[-320,286],[0,568],[-804,1508],[472,258],[159,50],[550,476]],[[4179,4958],[-186,-370],[-311,-371],[-146,-236],[-165,-370],[-229,-472],[-331,-236],[-228,-168]],[[6985,3125],[-84,-51],[-101,-141],[-302,-1223],[-893,-453],[-971,-152]]],"aliases":{},"names":{"ordino":"AD-05","parroquia d'ordino":"AD-05","canillo":"AD-02","encamp":"AD-03","parroquia d'encamp":"AD-03","la massana":"AD-04","parroquia de la massana":"AD-04","massana":"AD-04","sant julià de lòria":"AD-06","parroquia de sant julia de loria":"AD-06","sant juliã  de lã²ria":"AD-06","escaldes-engordany":"AD-08","parroquia d'escaldes-engordany":"AD-08","andorra la vella":"AD-07","parroquia d'andorra la vella":"AD-07"}}

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[51.569347,22.620946,56.383155522852284,26.07341032343234],"transform":{"scale":[0.0004814289951847469,0.0003452809604392781],"translate":[51.569347,22.620946]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1]]],"id":"AE-AJ","properties":{"code":"AE-AJ","name":"Ajman","label":[55.541,25.397]}},{"type":"MultiPolygon","arcs":[[[2,3,4]],[[5]],[[6]],[[7]],[[8]],[[9]],[[10]],[[11]],[[12]]],"id":"AE-AZ","properties":{"code":"AE-AZ","name":"Abu Dhabi","label":[54.082,23.524]}},{"type":"MultiPolygon","arcs":[[[13,14]],[[15,-4,16]]],"id":"AE-DU","properties":{"code":"AE-DU","name":"Dubay","label":[55.344,24.951]}},{"type":"MultiPolygon","arcs":[[[17,18,19,20,21,22,23]],[[24,25,26,27,28,29]],[[30,31,32]]],"id":"AE-FU","properties":{"code":"AE-FU","name":"Fujayrah","label":[56.222,25.475]}},{"type":"MultiPolygon","arcs":[[[33,-23,34,-31,35,-15,36,37,-26]],[[-28,38,39,40]]],"id":"AE-RK","properties":{"code":"AE-RK","name":"Ras Al Khaymah","label":[55.979,25.696]}},{"type":"MultiPolygon","arcs":[[[41,-30,42,-18]],[[-20,43]],[[44,-5,-16,45,-2,46,47,-39,-27,-38]],[[-22,48,-32,-35]]],"id":"AE-SH","properties":{"code":"AE-SH","name":"Sharjah","label":[55.779,25.172]}},{"type":"MultiPolygon","arcs":[[[49,-40,-48]]],"id":"AE-UQ","properties":{"code":"AE-UQ","name":"Umm Al Qaywayn","label":[55.722,25.522]}}]}},"arcs":[[[8008,8033],[140,190]],[[8148,8223],[291,-136],[-72,-155],[-175,12],[-184,89]],[[8788,6001],[38,-144],[-122,-246],[31,-384],[68,-246],[-107,-320],[190,-55],[178,27],[135,-403],[-397,-214],[-92,60],[-240,-9],[-395,-179],[153,-257],[16,-303],[-124,-272],[-69,-359],[-145,-240],[-113,-562],[-198,-446],[-71,-365],[13,-422],[-23,-423],[-169,-239],[-5240,898],[-92,68],[-1963,3255],[-42,143],[-8,372],[44,82],[-34,231],[103,-72],[22,-326],[79,148],[95,-159],[84,195],[49,-145],[-21,-185],[78,-509],[445,-37],[90,-70],[326,30],[243,97],[485,419],[13,137],[110,-28],[75,-131],[351,21],[225,-40],[109,80],[140,-127],[165,-1],[-9,129],[152,-71],[79,-124],[381,61],[171,-66],[129,-140],[252,64],[314,-25],[202,121],[357,118],[134,171],[136,50],[146,144],[124,-47],[132,179],[82,209],[0,169],[113,-41],[-85,160],[-170,137],[142,100],[57,-158],[143,-20],[113,259],[-42,68],[113,130],[-62,192],[138,208],[620,480]],[[7138,6803],[275,-965],[130,-121],[440,23],[537,307]],[[8520,6047],[268,-46]],[[4336,4453],[-61,39],[198,168],[242,100],[260,-266],[-92,-95],[-178,36],[-194,-54],[-175,72]],[[5446,4652],[-163,129],[51,160],[106,-143],[6,-146]],[[2088,4990],[76,85],[78,-185],[-118,-162],[-75,155],[39,107]],[[3642,4755],[-283,38],[363,217],[115,180],[63,-96],[-40,-148],[-200,-116],[-18,-75]],[[5844,5267],[-99,85],[127,156],[1,-114],[152,-136],[-181,9]],[[5603,4847],[-173,59],[102,124],[71,-183]],[[5726,5074],[-35,-171],[-62,146],[97,25]],[[1538,5556],[61,-110],[-116,-13],[55,123]],[[9559,6381],[-110,-258],[-87,-15],[61,262]],[[9423,6370],[136,11]],[[7801,7779],[257,0],[316,-309],[58,-104],[60,-317],[-79,-161],[86,-300],[42,-367],[-21,-174]],[[7138,6803],[159,136],[449,700],[55,140]],[[9928,7742],[71,79]],[[9999,7821],[-43,-733]],[[9956,7088],[-160,30],[42,-168],[-49,-200]],[[9789,6750],[-24,-36]],[[9765,6714],[-35,110],[-186,221]],[[9544,7045],[4,482],[85,20]],[[9633,7547],[60,-45],[225,160],[10,80]],[[9745,7849],[-57,-54]],[[9688,7795],[-147,65],[25,-142],[-137,-196],[-255,21]],[[9174,7543],[-18,164],[12,144],[38,142]],[[9206,7993],[158,-109],[116,62],[-86,153],[75,140],[-49,100],[-253,-14],[-35,235],[214,188],[174,48]],[[9520,8796],[143,-128],[120,39],[126,-66],[63,-229],[-11,-296]],[[9961,8116],[-216,-267]],[[9774,6520],[-45,66]],[[9729,6586],[47,55]],[[9776,6641],[-2,-121]],[[9688,7795],[-55,-248]],[[9544,7045],[35,-379],[150,-80]],[[9774,6520],[-215,-139]],[[9423,6370],[-167,180],[-103,260]],[[9153,6810],[-52,324],[58,177],[-53,93],[68,139]],[[9206,7993],[-152,-96]],[[9054,7897],[-99,133],[-75,231],[-88,442],[-116,106]],[[8676,8809],[274,225],[283,402],[-14,81],[145,446],[177,36],[43,-170],[-27,-399],[-54,-113],[48,-174],[-55,-130],[24,-217]],[[9928,7742],[-183,107]],[[9961,8116],[38,-295]],[[9956,7088],[43,-261],[-210,-77]],[[9153,6810],[-164,-10],[-140,-138],[-85,-191],[24,-470]],[[7801,7779],[207,254]],[[8148,8223],[48,153]],[[8196,8376],[512,-417],[94,-240],[202,26],[50,152]],[[9765,6714],[11,-73]],[[8196,8376],[231,67],[64,178],[185,188]]],"aliases":{},"names":{"fujayrah":"AE-FU","emirate of fujairah":"AE-FU","al fujayrah":"AE-FU","sharjah":"AE-SH","emirate of sharjah":"AE-SH","ash shariqah":"AE-SH","ras al khaymah":"AE-RK","emirate of ras al khaimah":"AE-RK","ra's al khaymah":"AE-RK","dubay":"AE-DU","emirate of dubai":"AE-DU","dubayy":"AE-DU","abu dhabi":"AE-AZ","emirate of abu dhabi":"AE-AZ","abu zaby":"AE-AZ","umm al qaywayn":"AE-UQ","emirate of umm al quwain":"AE-UQ","ajman":"AE-AJ","emirate of ajman":"AE-AJ","`ajman":"AE-AJ"}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[-62.34826,16.93197,-61.667592,17.727688],"transform":{"scale":[0.0000680736073607365,0.00007957975797579766],"translate":[-62.34826,16.93197]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1,2]],"id":"AG-03","properties":{"code":"AG-03","name":"Saint George","label":[-61.78,17.108]}},{"type":"Polygon","arcs":[[3,-3,4,5,6]],"id":"AG-04","properties":{"code":"AG-04","name":"Saint John","label":[-61.824,17.11]}},{"type":"Polygon","arcs":[[7,-7,8]],"id":"AG-05","properties":{"code":"AG-05","name":"Saint Mary","label":[-61.857,17.037]}},{"type":"Polygon","arcs":[[9,-9,-6,10,11]],"id":"AG-06","properties":{"code":"AG-06","name":"Saint Paul","label":[-61.767,17.035]}},{"type":"Polygon","arcs":[[12,13,-11,-5,-2]],"id":"AG-07","properties":{"code":"AG-07","name":"Saint Peter","label":[-61.74,17.094]}},{"type":"Polygon","arcs":[[14,-12,-14]],"id":"AG-08","properties":{"code":"AG-08","name":"Saint Philip","label":[-61.702,17.06]}},{"type":"Polygon","arcs":[[15]],"id":"AG-10","properties":{"code":"AG-10","name":"Barbuda","label":[-61.792,17.616]}},{"type":"Polygon","arcs":[[16]],"id":"AG-11","properties":{"code":"AG-11","name":"Redonda","label":[-62.347,16.934]}}]}},"arcs":[[[8151,2928],[187,-224],[112,-259],[244,-156]],[[8694,2289],[-81,-75],[75,-103],[15,-128],[-165,-51],[-105,-180]],[[8433,1752],[-105,39],[-210,115],[30,141],[-45,270],[-120,89],[-15,257],[210,13],[-27,252]],[[7081,1946],[-105,34],[-305,138],[162,125],[260,92],[339,83],[-119,166],[24,149],[134,131],[212,112],[468,-48]],[[8433,1752],[0,-141]],[[8433,1611],[-150,26],[-330,-77],[-120,-102]],[[7833,1458],[-75,141],[-225,-206],[60,321],[75,141],[-15,38],[-255,52],[-134,102],[-183,-101]],[[7994,947],[-202,-30],[-306,-1],[-192,35],[-469,234],[-99,115],[100,171],[64,144],[-52,122],[5,108],[238,101]],[[7833,1458],[105,-167],[-45,-141],[101,-203]],[[9138,1338],[-115,-45],[-5,-114],[69,-309],[-110,-150],[-240,95],[-239,178],[-104,95],[-99,-21],[-301,-120]],[[8433,1611],[375,0],[224,52]],[[9032,1663],[106,-325]],[[8694,2289],[14,112],[40,74],[139,158],[83,-107],[114,-63],[144,-20],[168,18],[-71,-66],[-41,-47],[-63,-34],[-134,-25],[67,-180],[151,-84]],[[9305,2025],[-198,-119],[-75,-243]],[[9305,2025],[194,4],[199,89],[109,0],[75,-62],[51,-62],[66,-141],[-100,6],[-202,-12],[-100,6],[137,-140],[126,-67],[71,-68],[-32,-147],[-166,-217],[-166,-10],[-190,80],[-239,54]],[[7482,8311],[-501,1474],[1745,-526],[375,-802],[-616,-747],[-1003,601]],[[39,90],[15,16],[22,6],[2,-32],[-2,-41],[-20,-32],[-38,-7],[-18,21],[14,47],[25,22]]],"aliases":{},"names":{"saint paul":"AG-06","parish of saint paul":"AG-06","saint philip":"AG-08","parish of saint philip":"AG-08","saint peter":"AG-07","parish of saint peter":"AG-07","saint george":"AG-03","parish of saint george":"AG-03","saint john":"AG-04","parish of saint john":"AG-04","saint mary":"AG-05","parish of saint mary":"AG-05","barbuda":"AG-10","redonda":"AG-11"}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[43.436294,38.863701,46.602612,41.290452],"transform":{"scale":[0.00031666346634663504,0.000242699369936994],"translate":[43.436294,38.863701]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1,2,3,4,5]],"id":"AM-AG","properties":{"code":"AM-AG","name":"Aragatsotn","label":[44.074,40.321]}},{"type":"Polygon","arcs":[[6,7,8,9,10,11]],"id":"AM-AR","properties":{"code":"AM-AR","name":"Ararat","label":[44.73,39.92]}},{"type":"Polygon","arcs":[[-6,12,-8,13]],"id":"AM-AV","properties":{"code":"AM-AV","name":"Armavir","label":[44.046,40.099]}},{"type":"Polygon","arcs":[[-9,-13,-5,14]],"id":"AM-ER","properties":{"code":"AM-ER","name":"Erevan","label":[44.533,40.147]}},{"type":"MultiPolygon","arcs":[[[15,16,-11,17,18]],[[19]]],"id":"AM-GR","properties":{"code":"AM-GR","name":"Gegharkunik","label":[45.385,40.278]}},{"type":"Polygon","arcs":[[20,-18,-10,-15,-4,21]],"id":"AM-KT","properties":{"code":"AM-KT","name":"Kotayk","label":[44.719,40.375]}},{"type":"Polygon","arcs":[[22,-22,-3,23,24]],"id":"AM-LO","properties":{"code":"AM-LO","name":"Lori","label":[44.452,40.935]}},{"type":"Polygon","arcs":[[-24,-2,25]],"id":"AM-SH","properties":{"code":"AM-SH","name":"Shirak","label":[43.818,40.819]}},{"type":"Polygon","arcs":[[26,27]],"id":"AM-SU","properties":{"code":"AM-SU","name":"Syunik","label":[46.152,39.248]}},{"type":"Polygon","arcs":[[-19,-21,-23,28],[29],[30]],"id":"AM-TV","properties":{"code":"AM-TV","name":"Tavush","label":[45.176,40.888]}},{"type":"Polygon","arcs":[[31,-28,32,-12,-17]],"id":"AM-VD","properties":{"code":"AM-VD","name":"Vayots Dzor","label":[45.442,39.718]}}]}},"arcs":[[[802,5673],[-303,432],[95,271]],[[594,6376],[257,222],[276,136],[470,-110],[198,64],[137,-161],[478,263],[-59,308],[-170,-8],[-148,231],[271,412]],[[2304,7733],[281,-88],[103,91],[354,-129],[59,-116]],[[3101,7491],[33,-282],[393,-202],[-178,-610],[24,-149],[-301,-369],[53,-370]],[[3125,5509],[-24,-47]],[[3101,5462],[-430,202],[-339,18],[-121,-237],[-375,3],[-638,406],[-396,-181]],[[5175,3651],[-131,85],[-336,-188],[-482,-91],[-291,389],[-363,313],[-143,249],[-446,314]],[[2983,4722],[-94,302],[282,290]],[[3171,5314],[123,-234],[240,-172],[291,250]],[[3825,5158],[142,-133],[287,70],[564,255],[214,5]],[[5032,5355],[-47,-346],[217,-544],[82,-336]],[[5284,4129],[-109,-478]],[[3101,5462],[70,-148]],[[2983,4722],[-397,139],[-703,-144],[-380,65],[-346,191],[-433,163],[-41,119],[217,125],[-98,293]],[[3125,5509],[461,117],[146,-140],[-88,-248],[181,-80]],[[6118,7365],[482,-866],[187,-174],[735,-416],[137,-151],[199,21],[172,-176],[-89,-438],[-244,-443],[-220,-22],[-519,87],[-196,-171]],[[6762,4616],[-100,61],[-272,-198],[-624,-43],[-482,-307]],[[5032,5355],[-262,324],[34,208],[-148,546],[-146,155],[7,182],[-184,102],[-37,488]],[[4296,7360],[375,79],[257,-130],[214,68],[106,184],[240,137],[214,-24],[416,-309]],[[6581,7403],[128,-53],[-149,-188],[-108,199],[129,42]],[[4220,7423],[76,-63]],[[3101,7491],[342,74],[500,-143],[277,1]],[[4476,9723],[216,-112],[-121,-322],[165,-227],[-204,-156],[-313,-105],[420,-373],[-195,-356],[-289,-207],[65,-442]],[[2304,7733],[-179,13],[-228,169],[-69,186],[79,215],[127,-30],[134,345],[-205,61],[61,125],[-351,350],[37,313]],[[1710,9480],[397,44],[276,226],[239,-139],[286,65],[309,-130],[195,108],[110,-122],[194,190],[196,-61],[462,-9],[102,71]],[[594,6376],[-192,221],[281,266],[242,609],[-57,431],[-163,289],[-38,255],[-135,204],[-456,245],[-63,345],[399,72],[515,-71],[421,215],[362,23]],[[7493,4069],[167,-192],[370,-149],[209,-333],[318,-203],[191,-208],[433,126],[229,-183],[200,19],[151,-167],[-92,-265],[-292,-93],[-98,-298],[381,-226],[71,-186],[268,-262],[-279,-127],[-303,113],[-70,-196],[263,-205],[57,-252],[-68,-321],[120,-385],[-385,166],[-276,-25],[-323,-189],[-210,-28],[-184,358],[-165,466],[-212,362],[88,343],[-86,155],[-427,291],[-179,47],[114,386],[-47,415],[-250,81]],[[7177,2904],[71,571],[-60,112],[305,482]],[[4476,9723],[-104,206],[424,-44],[150,114],[156,-405],[261,-18],[210,-252],[-421,-65],[-33,-132],[198,3],[321,-268],[359,-85],[265,97],[-9,-231],[384,-352],[193,-55],[-137,-354],[-496,-279],[-79,-238]],[[4988,8898],[10,213],[-184,-62],[174,-151]],[[5601,8670],[0,0]],[[6762,4616],[106,-102],[329,-34],[155,-119],[141,-292]],[[7177,2904],[-387,-95],[-407,-231],[-457,183],[-167,272],[-291,-141],[-74,100],[-10,390],[-209,269]]],"aliases":{},"names":{"tavush":"AM-TV","tavushi marz":"AM-TV","lori":"AM-LO","lorru marz":"AM-LO","shirak":"AM-SH","shiraki marz":"AM-SH","gegharkunik":"AM-GR","geghark'unik'i marz":"AM-GR","vayots dzor":"AM-VD","vayots' dzori marz":"AM-VD","syunik":"AM-SU","syunik'i marz":"AM-SU","ararat":"AM-AR","ararati marz":"AM-AR","aragatsotn":"AM-AG","aragatsotni marz":"AM-AG","armavir":"AM-AV","armaviri marz":"AM-AV","kotayk":"AM-KT","kotayk'i marz":"AM-KT","erevan":"AM-ER","yerevan":"AM-ER"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[15.716074,42.574393,19.057069,45.218533],"transform":{"scale":[0.0003341329132913289,0.00026444044404440445],"translate":[15.716074,42.574393]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1]]],"id":"BA-BIH","properties":{"code":"BA-BIH","name":"West Bosnia","label":[16.858,43.977]}},{"type":"Polygon","arcs":[[2,-1,3,4]],"id":"BA-BRC","properties":{"code":"BA-BRC","name":"Brčko Distrikt","label":[18.854,44.831]}},{"type":"MultiPolygon","arcs":[[[-5,5]],[[6]]],"id":"BA-SRP","properties":{"code":"BA-SRP","name":"Posavina","label":[18.298,45.06]}}]}},"arcs":[[[9074,8586],[81,-373],[86,-5]],[[9241,8208],[-17,-152],[-167,-97],[-9,-206],[326,-314],[128,33],[180,314],[214,0],[17,-541],[-317,-241],[-252,-38],[-253,-222],[-4,-222],[120,-319],[-167,-357],[-231,-205],[-249,-65],[-201,-214],[-9,-514],[-72,-87],[-296,17],[-21,-184],[120,-195],[210,-11],[376,-200],[184,-16],[197,335],[557,-11],[308,-173],[86,-167],[-514,-309],[-574,-32],[-689,405],[-150,0],[-115,-408],[47,-546],[-1079,27],[51,-190],[30,-708],[-197,-195],[193,-179],[-124,-538],[77,-308],[488,-449],[428,-444],[71,-282],[-436,203],[-1094,714],[-140,352],[-330,36],[-144,-108],[-290,166],[318,117],[-103,306],[-123,134],[-434,266],[-126,240],[-214,168],[-183,352],[57,317],[-141,156],[-463,131],[-781,734],[-333,243],[-101,262],[-792,813],[-261,101],[-332,475],[-85,281],[46,293],[-129,154],[-130,457],[-330,74],[116,183],[-227,188],[-220,300],[-269,-83],[-264,340],[168,222],[-128,310],[98,183],[-23,252],[72,447],[134,191],[517,29],[210,-185],[109,-216],[326,-306],[407,-151],[-210,-70],[-17,-195],[180,-119],[676,-49],[433,-81],[372,-336],[30,-313],[120,-82],[9,-562],[-86,-206],[-295,-16],[-561,179],[-176,-6],[13,-140],[612,-449],[120,-11],[129,-211],[736,-363],[184,-140],[64,-266],[360,-59],[43,433],[-231,287],[-99,254],[-4,503],[766,-62],[283,-287],[685,32],[557,363],[-5,287],[-175,156],[-64,195],[158,306],[381,0],[103,-87],[261,-443],[741,11],[-82,270],[-788,281],[-25,222],[604,54],[179,368],[150,76],[291,-35],[0,-130],[296,16],[462,-130]],[[9356,8705],[397,-51],[-196,-298],[-316,-148]],[[9074,8586],[63,179]],[[9137,8765],[37,-192],[182,132]],[[9137,8765],[-354,232],[-368,122],[-138,307],[178,-44],[-19,133],[461,-39],[112,-205],[188,-77],[-17,-348],[176,-141]],[[7547,9768],[253,-131],[211,-16],[177,-127],[-44,-136],[-257,-99],[-60,-134],[-225,12],[-249,226],[-88,179],[282,226]]],"aliases":{},"names":{"herzegovina-neretva":"BA-BIH","herzegovina-neretva canton":"BA-BIH","hercegovacko-neretvanski kanton":"BA-BIH","una-sana":"BA-BIH","una-sana canton":"BA-BIH","unsko-sanski kanton":"BA-BIH","posavina":"BA-SRP","posavina canton":"BA-SRP","posavski kanton":"BA-SRP","brčko distrikt":"BA-BRC","brčko":"BA-BRC","brcko district of bosnia and herzegovina":"BA-BRC","west bosnia":"BA-BIH","livno":"BA-BIH","hercegovacko-bosanski kanton":"BA-BIH","livanjski kanton":"BA-BIH","west herzegovina":"BA-BIH","west herzegovina canton":"BA-BIH","zapadno-hercegovacki kanton":"BA-BIH","central bosnia":"BA-BIH","central bosnia canton":"BA-BIH","srednjobosanski kanton":"BA-BIH","srednjebosanski kanton":"BA-BIH","zenica-doboj":"BA-BIH","zenica-doboj canton":"BA-BIH","zenicko-dobojski kanton":"BA-BIH","zeničko-dobojski kanton":"BA-BIH","tuzla":"BA-BIH","tuzla canton":"BA-BIH","tuzlanski kanton":"BA-BIH","sarajevo":"BA-BIH","sarajevo canton":"BA-BIH","kanton sarajevo":"BA-BIH","sarajevski kanton":"BA-BIH","bosnian podrinje":"BA-BIH","bosnian podrinje canton":"BA-BIH","bosansko-podrinjski kanton":"BA-BIH"}}

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[-59.654205,13.051174,-59.42691,13.34455],"transform":{"scale":[0.000022731773177317536,0.00002934053405340537],"translate":[-59.654205,13.051174]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1,2,3]],"id":"BB-01","properties":{"code":"BB-01","name":"Christ Church","label":[-59.538,13.087]}},{"type":"Polygon","arcs":[[4,5,6,7,8]],"id":"BB-02","properties":{"code":"BB-02","name":"Saint Andrew","label":[-59.576,13.255]}},{"type":"Polygon","arcs":[[9,10,-3,11,12,13]],"id":"BB-03","properties":{"code":"BB-03","name":"Saint George","label":[-59.548,13.149]}},{"type":"Polygon","arcs":[[14,15,-8,16,17]],"id":"BB-04","properties":{"code":"BB-04","name":"Saint James","label":[-59.626,13.192]}},{"type":"Polygon","arcs":[[18,19,-10,20]],"id":"BB-05","properties":{"code":"BB-05","name":"Saint John","label":[-59.504,13.18]}},{"type":"Polygon","arcs":[[21,-21,-14,22,-6]],"id":"BB-06","properties":{"code":"BB-06","name":"Saint Joseph","label":[-59.549,13.212]}},{"type":"Polygon","arcs":[[23,24]],"id":"BB-07","properties":{"code":"BB-07","name":"Saint Lucy","label":[-59.615,13.318]}},{"type":"Polygon","arcs":[[25,-18,26,-12,-2]],"id":"BB-08","properties":{"code":"BB-08","name":"Saint Michael","label":[-59.605,13.118]}},{"type":"Polygon","arcs":[[27,-9,-16,28,-25]],"id":"BB-09","properties":{"code":"BB-09","name":"Saint Peter","label":[-59.611,13.268]}},{"type":"Polygon","arcs":[[29,-4,-11,-20]],"id":"BB-10","properties":{"code":"BB-10","name":"Saint Philip","label":[-59.47,13.137]}},{"type":"Polygon","arcs":[[-23,-13,-27,-17,-7]],"id":"BB-11","properties":{"code":"BB-11","name":"Saint Thomas","label":[-59.59,13.188]}}]}},"arcs":[[[7849,1204],[-517,-271],[-938,-724],[-346,-31],[-281,2],[-272,-36],[-300,-144],[-434,563],[-812,352],[-988,176],[-588,32]],[[2373,1123],[1363,547],[19,2],[-5,46],[-66,225]],[[3684,1943],[2523,724]],[[6207,2667],[412,-97],[64,-49],[75,-92],[72,-371],[13,-27],[258,-334],[55,-95],[-1,-14],[42,-25],[74,-18],[251,-74],[113,-42],[214,-225]],[[3750,8129],[843,-1381],[175,-182]],[[4768,6566],[-103,-19],[-556,-199],[-133,-26],[-55,19],[-12,9],[-15,18],[-11,7],[-12,5],[-16,2],[-18,-2],[-25,-5],[-137,-168],[-454,-799]],[[3221,5408],[-201,201],[-157,44],[-524,-23]],[[2339,5630],[-220,877]],[[2119,6507],[26,1150],[58,88],[42,148],[26,7],[15,7],[12,3],[12,-3],[13,-4],[14,-10],[27,-11],[31,-8],[36,-9],[22,0],[22,5],[19,4],[20,1],[34,-3],[13,0],[15,2],[11,1],[16,-3],[25,-14],[66,5],[104,32],[188,137],[100,44],[64,14],[21,-9],[23,-5],[16,-2],[15,-9],[20,-1],[26,1],[73,50],[9,0],[5,-4],[61,-10],[256,7],[75,21]],[[5015,4476],[923,-645],[87,-74],[375,-533]],[[6400,3224],[-193,-557]],[[3684,1943],[-732,1811]],[[2952,3754],[1038,310],[88,40],[317,289]],[[4395,4393],[590,81],[30,2]],[[598,3083],[-240,639],[-208,2674]],[[150,6396],[277,-55],[738,263],[954,-97]],[[2339,5630],[-237,-242],[-65,-47],[-7,3],[-9,2],[-5,-2],[-9,-7],[-11,-7],[-7,-2],[-14,2],[-6,0],[-12,2],[-16,0],[-10,-2],[-10,-5],[-30,-14],[-7,-4],[-14,-5],[-7,-2],[-7,0],[-48,-31],[-16,-7],[-10,-4],[-8,0],[-7,-3],[-17,0],[-9,0],[-8,-2],[-20,-11],[-82,-23],[-26,-60],[-116,-384],[-62,-112],[-45,-67],[-45,-11],[-16,-9],[-12,-12],[-16,-32],[-9,-17],[-12,-7],[21,-18],[57,-25],[291,-250],[256,-838]],[[1897,3377],[-467,-275],[-39,-31],[-79,-46],[-218,-72],[-89,-41],[-3,-7],[-54,25],[-225,160],[-125,-7]],[[5989,5669],[1155,-667],[666,-291],[395,-97]],[[8205,4614],[-67,-82],[-482,-443],[-56,-43],[-297,-141],[-315,-95],[-588,-586]],[[5015,4476],[41,534],[10,47],[22,64],[38,0],[18,1],[20,4],[721,509],[104,34]],[[4768,6566],[405,-424],[816,-473]],[[4395,4393],[-374,571],[-118,107],[-61,43],[-621,294]],[[56,7590],[-56,724],[88,528],[311,447],[625,485],[795,225],[809,-333],[605,-690],[211,-346]],[[3444,8630],[-40,-19],[-2361,-581],[-814,-412],[-173,-28]],[[2373,1123],[-347,18],[-590,318],[-509,747],[-329,877]],[[1897,3377],[705,109],[62,21],[288,247]],[[3444,8630],[306,-501]],[[150,6396],[-94,1194]],[[8205,4614],[466,-115],[812,-283],[516,-494],[-138,-1174],[-1129,-882],[-883,-462]]],"aliases":{},"names":{"saint joseph":"BB-06","saint andrew":"BB-06","saint peter":"BB-09","saint lucy":"BB-07","saint james":"BB-04","saint michael":"BB-08","christ church":"BB-01","saint philip":"BB-10","saint john":"BB-05","saint george":"BB-03","saint thomas":"BB-11"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[50.4529852360236,25.579891,50.774310367036705,26.287439],"transform":{"scale":[0.000032135726673977823,0.00007076187618761868],"translate":[50.4529852360236,25.579891]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1]]],"id":"BH-13","properties":{"code":"BH-13","name":"Al Manāmah","label":[50.578,26.22]}},{"type":"MultiPolygon","arcs":[[[2,3,4]],[[5]]],"id":"BH-14","properties":{"code":"BH-14","name":"Al Janūbīyah","label":[50.562,25.969]}},{"type":"MultiPolygon","arcs":[[[6]]],"id":"BH-15","properties":{"code":"BH-15","name":"Al Muḩarraq","label":[50.638,26.261]}},{"type":"MultiPolygon","arcs":[[[7,-5,8]]],"id":"BH-16","properties":{"code":"BH-16","name":"Al Wusţá","label":[50.574,26.137]}},{"type":"MultiPolygon","arcs":[[[9,-2,-9,-4]]],"id":"BH-17","properties":{"code":"BH-17","name":"Ash Shamālīyah","label":[50.487,26.162]}}]}},"arcs":[[[1585,9357],[388,-33],[498,-93],[406,-76],[3,1],[1281,208],[205,-55],[128,-185],[229,-128],[68,-58],[68,-58],[-175,-150],[-294,-33],[-886,50],[-436,-65]],[[3068,8682],[-514,272],[-1028,37],[59,366]],[[5082,7293],[66,-1854],[-464,-1557],[-1189,-919],[-564,896],[-2699,1870],[948,886]],[[1180,6615],[1497,117],[-452,1027]],[[2225,7759],[1398,-187],[369,93],[1090,-372]],[[9999,2003],[-551,-2003],[-356,1520],[907,483]],[[4626,9878],[969,121],[701,-152],[33,-212],[0,-212],[468,-273],[402,-76],[-168,-198],[-167,-75],[-501,121],[-134,197],[-166,197],[-435,46],[-401,30],[-266,-45],[-401,303],[66,228]],[[3068,8682],[1343,-480],[316,204],[467,152],[200,-212],[201,-334],[401,-15],[-316,-161],[-18,-264],[-628,-64],[48,-215]],[[2225,7759],[-453,691],[371,149],[925,83]],[[1180,6615],[-1180,1436],[256,1069],[1329,237]]],"aliases":{},"names":{"al manāmah":"BH-13","capital":"BH-13","capital governorate":"BH-13","ash shamālīyah":"BH-17","northern":"BH-17","northern governorate":"BH-17","al janūbīyah":"BH-14","southern":"BH-14","southern governorate":"BH-14","al wusţá":"BH-16","central":"BH-16","central governorate":"BH-16","al muḩarraq":"BH-15","muharraq":"BH-15","muharraq governorate":"BH-15"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[-64.885994,32.248077,-64.647631,32.388658],"transform":{"scale":[0.000023838683868386095,0.0000140595059505948],"translate":[-64.885994,32.248077]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1,2,3,4]]],"id":"BM-DEV","properties":{"code":"BM-DEV","name":"Devonshire","label":[-64.757,32.303]}},{"type":"MultiPolygon","arcs":[[[5,6]]],"id":"BM-HA","properties":{"code":"BM-HA","name":"City of Hamilton","label":[-64.788,32.294]}},{"type":"MultiPolygon","arcs":[[[7,8,9,10]],[[11,12,13,14]],[[15,16]]],"id":"BM-HAM","properties":{"code":"BM-HAM","name":"Hamilton","label":[-64.72,32.345]}},{"type":"MultiPolygon","arcs":[[[17,-4,18,19]]],"id":"BM-PAG","properties":{"code":"BM-PAG","name":"Paget","label":[-64.781,32.278]}},{"type":"MultiPolygon","arcs":[[[20,-5,21,-7]]],"id":"BM-PEM","properties":{"code":"BM-PEM","name":"Pembroke","label":[-64.8,32.3]}},{"type":"MultiPolygon","arcs":[[[22,23]]],"id":"BM-SAN","properties":{"code":"BM-SAN","name":"Sandys","label":[-64.869,32.298]}},{"type":"MultiPolygon","arcs":[[[24]]],"id":"BM-SG","properties":{"code":"BM-SG","name":"City of Saint George","label":[-64.674,32.375]}},{"type":"MultiPolygon","arcs":[[[25,-13,-11]],[[26,-9],[-25]]],"id":"BM-SGE","properties":{"code":"BM-SGE","name":"Saint George's","label":[-64.7,32.365]}},{"type":"MultiPolygon","arcs":[[[27,-15,28,-2,29,-17]]],"id":"BM-SMI","properties":{"code":"BM-SMI","name":"Smith's","label":[-64.732,32.314]}},{"type":"MultiPolygon","arcs":[[[30,31,32,-24]]],"id":"BM-SOU","properties":{"code":"BM-SOU","name":"Southampton","label":[-64.838,32.252]}},{"type":"MultiPolygon","arcs":[[[33,-20,34,-32]]],"id":"BM-WAR","properties":{"code":"BM-WAR","name":"Warwick","label":[-64.804,32.266]}}]}},"arcs":[[[4440,3936],[769,535],[445,191],[128,207]],[[5782,4869],[429,-1024]],[[6211,3845],[-112,-120],[-599,-521],[-249,-494]],[[5251,2710],[-258,725],[-506,-375]],[[4487,3060],[-47,876]],[[4261,3022],[-424,-72]],[[3837,2950],[29,485],[394,69],[1,-482]],[[7576,6258],[-325,468],[-482,-274],[-347,-360],[-77,-429],[-199,71],[-91,311],[221,764],[602,619]],[[6878,7428],[585,-67]],[[7463,7361],[130,-607],[422,-221]],[[8015,6533],[-439,-275]],[[7219,5789],[357,469]],[[7576,6258],[293,-689]],[[7869,5569],[-373,-369]],[[7496,5200],[-277,589]],[[6012,5240],[480,96]],[[6492,5336],[-354,-437],[-126,341]],[[3729,2206],[679,601],[79,253]],[[5251,2710],[-130,-256],[-812,-1076],[-211,-154]],[[4098,1224],[-369,982]],[[3837,2950],[-455,275],[-361,751],[132,255],[1287,-295]],[[4487,3060],[-226,-38]],[[245,1270],[-115,412],[-130,772],[393,-261],[-57,510],[144,683],[-220,694],[309,-165],[146,413],[195,-413],[261,-386],[-374,-469],[-118,-805],[-94,-243],[212,-248],[764,-137],[0,-221],[-602,83]],[[959,1489],[-714,-219]],[[8974,9452],[150,-477],[-357,-319],[-169,446],[376,350]],[[8015,6533],[-146,-964]],[[6878,7428],[373,842],[666,652],[888,1077],[1194,-1453],[-325,-634],[-16,-634],[-196,220],[-292,386],[-562,200],[-186,-310],[-260,469],[-699,-882]],[[6492,5336],[532,40],[195,413]],[[7496,5200],[-407,-403],[-878,-952]],[[5782,4869],[230,371]],[[959,1489],[-49,-469],[277,-551],[455,0],[585,358]],[[2227,827],[513,156],[232,-542]],[[2972,441],[-793,-441],[-634,28],[-488,27],[-651,634],[-161,581]],[[2227,827],[163,496],[390,55],[699,607],[250,221]],[[4098,1224],[-879,-645],[-247,-138]]],"aliases":{},"names":{"hamilton":"BM-HAM","hamilton parish":"BM-HAM","smith's":"BM-SMI","smith's parish":"BM-SMI","devonshire":"BM-DEV","devonshire parish":"BM-DEV","pembroke":"BM-PEM","pembroke parish":"BM-PEM","city of hamilton":"BM-HA","paget":"BM-PAG","paget parish":"BM-PAG","warwick":"BM-WAR","warwick parish":"BM-WAR","southampton":"BM-SOU","southampton parish":"BM-SOU","sandys":"BM-SAN","sandys parish":"BM-SAN","saint george's":"BM-SGE","saint george":"BM-SGE","saint george's parish":"BM-SGE","city of saint george":"BM-SG","st. george":"BM-SG"}}

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[113.99879,4.016681,115.360741,5.057196],"transform":{"scale":[0.00013620872087208768,0.00010406190619061908],"translate":[113.99879,4.016681]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1]],"id":"BN-BE","properties":{"code":"BN-BE","name":"Belait","label":[114.408,4.456]}},{"type":"Polygon","arcs":[[2,3]],"id":"BN-BM","properties":{"code":"BN-BM","name":"Brunei and Muara","label":[114.92,4.902]}},{"type":"Polygon","arcs":[[4]],"id":"BN-TE","properties":{"code":"BN-TE","name":"Temburong","label":[115.191,4.602]}},{"type":"Polygon","arcs":[[5,-1,6,-3]],"id":"BN-TU","properties":{"code":"BN-TU","name":"Tutong","label":[114.671,4.645]}}]}},"arcs":[[[3639,6386],[342,-763],[304,-36],[105,-728],[876,-1120],[309,-824],[264,-190]],[[5839,2725],[170,-390],[-370,-1229],[-675,-843],[-648,-217],[-127,413],[-675,774],[-422,1106],[-1008,46],[103,498],[-375,1583],[-930,771],[-882,379],[1857,-81],[550,137],[1232,714]],[[5465,6860],[125,1616]],[[5590,8476],[1235,979],[950,514],[-257,-1156],[-469,-649],[-78,-455],[-515,-58],[-991,-791]],[[8424,8570],[449,-797],[480,-1814],[-2,-1558],[648,-1394],[-267,-144],[-761,434],[-791,217],[-569,2296],[-93,1154],[186,581],[603,482],[117,543]],[[5465,6860],[251,-1114],[35,-1123],[176,-619],[307,-92],[-84,-639],[-311,-548]],[[3639,6386],[256,121],[532,828],[1163,1141]]],"aliases":{},"names":{"temburong":"BN-TE","temburong district":"BN-TE","brunei and muara":"BN-BM","brunei-muara":"BN-BM","brunei and muara district":"BN-BM","tutong":"BN-TU","tutong district":"BN-TU","belait":"BN-BE","belait district":"BN-BE"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[-89.23418626022602,15.879652,-87.78307,18.48449171647165],"transform":{"scale":[0.00014512613863646662,0.0002605100226494298],"translate":[-89.23418626022602,15.879652]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1,2,3,4]],[[5]],[[6]],[[7]],[[8]]],"id":"BZ-BZ","properties":{"code":"BZ-BZ","name":"Belize","label":[-88.419,17.537]}},{"type":"MultiPolygon","arcs":[[[9,10,-3,11,12]]],"id":"BZ-CY","properties":{"code":"BZ-CY","name":"Cayo","label":[-88.883,16.99]}},{"type":"MultiPolygon","arcs":[[[-5,13,14]]],"id":"BZ-CZL","properties":{"code":"BZ-CZL","name":"Corozal","label":[-88.349,18.167]}},{"type":"MultiPolygon","arcs":[[[15,-14,-4,-11]]],"id":"BZ-OW","properties":{"code":"BZ-OW","name":"Orange Walk","label":[-88.855,17.712]}},{"type":"MultiPolygon","arcs":[[[16,17,-12,-2]]],"id":"BZ-SC","properties":{"code":"BZ-SC","name":"Stann Creek","label":[-88.496,16.809]}},{"type":"MultiPolygon","arcs":[[[-13,-18,18]]],"id":"BZ-TOL","properties":{"code":"BZ-TOL","name":"Toledo","label":[-88.812,16.376]}}]}},"arcs":[[[7355,7985],[-287,-299],[-41,-418],[-475,-740],[800,-262],[-438,-59],[-383,-603],[-98,-620],[77,-162]],[[6510,4822],[-331,-103],[-1826,-18]],[[4353,4701],[372,268],[-149,230],[-60,907],[-345,-62]],[[4171,6044],[-80,624],[167,200],[328,75],[65,769],[365,111],[1528,210]],[[6544,8033],[811,-48]],[[9618,5792],[283,-213],[-188,-156],[-95,369]],[[9692,6394],[307,-128],[-432,-374],[-374,-87],[-281,-382],[216,514],[727,252],[-163,205]],[[7399,6922],[615,197],[-225,-201],[-390,4]],[[9101,8239],[-11,-166],[-350,-265],[420,919],[385,-74],[-444,-414]],[[282,1969],[304,2471],[-11,1093]],[[575,5533],[494,193],[1083,51],[305,113],[744,-12],[970,166]],[[4353,4701],[243,-227],[-661,-511],[-71,-378],[-565,-238],[-129,-271]],[[3170,3076],[-311,-259],[-826,-274],[-120,-111],[-485,-1],[-826,-216],[-320,-246]],[[6544,8033],[-946,457],[-501,363],[-824,235]],[[4273,9088],[408,403],[151,350],[301,158],[1238,-8],[158,-75],[-669,-299],[456,-39],[-298,-256],[701,204],[444,-79],[711,119],[-346,-181],[345,15],[1,-806],[-519,-609]],[[575,5533],[-73,2228],[198,264],[573,112],[1165,-425],[992,627],[274,484],[569,265]],[[6510,4822],[454,-432],[42,-260],[-454,-247],[214,-303],[-340,-156],[-32,-456],[-599,-388],[21,-207]],[[5816,2373],[-1163,81],[65,185],[-335,384],[-497,94],[-716,-41]],[[5816,2373],[-397,-395],[-193,-52],[-415,-411],[-788,43],[-75,-116],[-544,-97],[-39,-288],[-1253,-657],[95,-345],[-262,-55],[-609,84],[-945,-5],[-350,-74],[-41,284],[282,1680]]],"aliases":{},"names":{"cayo":"BZ-CY","cayo district":"BZ-CY","orange walk":"BZ-OW","orange walk district":"BZ-OW","corozal":"BZ-CZL","corozal district":"BZ-CZL","toledo":"BZ-TOL","toledo district":"BZ-TOL","stann creek":"BZ-SC","stann creek district":"BZ-SC","belize":"BZ-BZ","belize district":"BZ-BZ"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[-85.94650160486049,8.101943827182719,-82.57376968046805,11.209937],"transform":{"scale":[0.00033730692313155714,0.0003108304003217603],"translate":[-85.94650160486049,8.101943827182719]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1,2,3,4]]],"id":"CR-A","properties":{"code":"CR-A","name":"Alajuela","label":[-84.799,10.753]}},{"type":"MultiPolygon","arcs":[[[5,6]]],"id":"CR-C","properties":{"code":"CR-C","name":"Cartago","label":[-83.699,9.784]}},{"type":"MultiPolygon","arcs":[[[-4,7,8,9,10]]],"id":"CR-G","properties":{"code":"CR-G","name":"Guanacaste","label":[-85.355,10.333]}},{"type":"MultiPolygon","arcs":[[[11,12,-1,13]]],"id":"CR-H","properties":{"code":"CR-H","name":"Heredia","label":[-83.953,10.463]}},{"type":"MultiPolygon","arcs":[[[14,15,-6,16,-12,17]]],"id":"CR-L","properties":{"code":"CR-L","name":"Limón","label":[-83.149,9.702]}},{"type":"MultiPolygon","arcs":[[[18,-8,-3,19,-15]],[[20,-10]],[[21]]],"id":"CR-P","properties":{"code":"CR-P","name":"Puntarenas","label":[-83.229,8.934]}},{"type":"MultiPolygon","arcs":[[[-17,-7,-16,-20,-2,-13]]],"id":"CR-SJ","properties":{"code":"CR-SJ","name":"San José","label":[-84.01,9.604]}}]}},"arcs":[[[5303,8643],[-24,-2026],[-35,-497],[-66,-152]],[[5178,5968],[-377,-147],[-397,19],[-327,-299]],[[4077,5541],[-305,194],[90,158],[277,207],[-128,49],[-165,251],[-58,292],[60,187],[-104,124],[-195,-38],[-111,155]],[[3438,7120],[68,409],[-87,176],[-538,344],[-125,286],[-482,240],[-306,-25],[-99,198],[-225,109],[-136,167],[597,471]],[[2105,9495],[904,-359],[138,20],[525,370],[93,24],[767,-372],[185,128],[133,-241],[303,-189],[-55,-134],[205,-99]],[[5941,6545],[1048,-511],[741,-13],[10,-491],[-216,-286],[-91,-222],[-212,-311],[72,-317]],[[7293,4394],[-451,328],[-229,-101],[-140,185],[-145,9],[-110,193],[-191,46],[-221,306],[-238,-39],[-1,106],[196,251],[8,204],[281,49],[116,105],[-19,154],[-208,355]],[[3438,7120],[-235,-33],[-23,-405],[-132,-212],[-196,141],[-250,-4]],[[2602,6607],[-317,32],[-232,243],[82,-487],[174,-226]],[[2309,6169],[-47,-131],[-316,-107],[339,-295],[-34,-247],[-125,-169]],[[2126,5220],[-306,328],[-318,123],[-91,-46],[-498,178],[-415,561],[-194,485],[17,260],[-109,140],[294,297],[-100,152],[82,155],[239,63],[206,188],[-182,-55],[90,242],[-8,321],[-721,346],[-112,15],[350,187],[350,-68],[47,136],[-153,163],[232,60],[-101,132],[210,372],[99,44],[264,-185],[807,-319]],[[6472,8595],[175,-278],[-202,-26],[-161,-128],[11,-497],[-139,-232],[81,-255],[-71,-273],[-139,-159]],[[6027,6747],[-200,-118],[-268,-34],[183,-376],[-54,-170],[-366,-118],[-144,37]],[[5303,8643],[241,-79],[159,75],[265,-223],[219,13],[285,166]],[[8915,3117],[-350,300],[-65,235],[-255,256],[-185,4],[-277,154],[-211,-123],[-109,84]],[[7463,4027],[-170,367]],[[5941,6545],[86,202]],[[6472,8595],[298,109],[-77,410],[132,-29],[135,-222],[48,-260],[-122,262],[45,-359],[98,71],[324,-879],[212,-382],[566,-793],[351,-410],[176,9],[227,-442],[343,-412],[75,26],[107,-279],[405,-84],[184,-189],[-134,-288],[-175,37],[-100,152],[-347,185],[-150,-137],[54,-244],[-214,-44],[-32,-108],[14,-1178]],[[8915,3117],[89,3],[475,-315],[88,-168],[-131,-134],[-307,-136],[-166,-262],[218,-406],[40,-323],[-101,-295],[-247,-115],[-296,-281],[325,-214],[171,-471],[-245,383],[-540,476],[153,200],[-195,427],[123,81],[-142,155],[-204,-44],[-233,149],[-38,168],[-154,57],[-291,-88],[229,-397],[347,-152],[0,-532],[-519,213],[-335,7],[-487,575],[213,191],[238,491],[-126,310],[80,111],[-78,222],[-466,528],[-157,68],[-232,238],[-265,145],[-508,187],[-88,216],[-740,196],[-189,-20],[-281,188],[-183,235],[129,425],[-189,275],[-104,317],[-348,0],[186,66],[-325,90],[-507,450]],[[4077,5541],[72,-209],[-126,-22],[30,-242],[158,-348],[290,-22],[100,220],[748,-61],[46,-172],[151,-20],[14,-154],[374,-341],[107,13],[186,-295],[74,-229],[314,-27],[175,-297],[419,-233],[97,92],[12,287],[-65,213],[210,333]],[[2309,6169],[454,-240],[164,6],[143,-132],[-65,-222],[202,-44],[-280,-308],[-307,-199],[-163,-354],[-331,544]],[[2416,6424],[-81,-73],[-119,134],[200,-61]]],"aliases":{},"names":{"alajuela":"CR-A","provincia de alajuela":"CR-A","guanacaste":"CR-G","provincia de guanacaste":"CR-G","limón":"CR-L","provincia de limon":"CR-L","puntarenas":"CR-P","provincia de puntarenas":"CR-P","heredia":"CR-H","provincia de heredia":"CR-H","san josé":"CR-SJ","provincia de san jose":"CR-SJ","cartago":"CR-C","provincia de cartago":"CR-C"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[32.28654236363636,34.63443983968397,34.099132,35.197737119811976],"transform":{"scale":[0.00018127709134549855,0.00005633536154895537],"translate":[32.28654236363636,34.63443983968397]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1,2,3]]],"id":"CY-01","properties":{"code":"CY-01","name":"Nicosia","label":[33.043,35.021]}},{"type":"MultiPolygon","arcs":[[[4,-2,5,6]]],"id":"CY-02","properties":{"code":"CY-02","name":"Limassol","label":[32.967,34.776]}},{"type":"MultiPolygon","arcs":[[[-6,-1,7]],[[8,9]]],"id":"CY-03","properties":{"code":"CY-03","name":"Larnaca","label":[33.868,34.973]}},{"type":"MultiPolygon","arcs":[[[-9,10]]],"id":"CY-04","properties":{"code":"CY-04","name":"Famagusta","label":[33.949,35.016]}},{"type":"MultiPolygon","arcs":[[[11,-3,-5]]],"id":"CY-05","properties":{"code":"CY-05","name":"Paphos","label":[32.528,34.931]}}]}},"arcs":[[[6644,7369],[164,-879],[-135,-625],[-442,18],[-109,-678],[-423,85],[-307,-707],[-461,320],[-274,-549]],[[4657,4354],[-411,190],[-405,1163],[-613,-387],[-367,359],[-209,-920]],[[2652,4759],[-421,-233],[-164,418],[64,1219],[-358,1429],[120,777],[-348,1185]],[[1545,9554],[799,159],[111,-683],[349,-290],[245,-832],[460,334],[955,1260],[521,358],[690,-497],[469,636],[422,-1827],[78,-803]],[[1959,469],[-23,592],[377,839],[-25,639],[451,1753],[-87,467]],[[4657,4354],[439,-743],[341,-1148],[2,-1169]],[[5439,1294],[-1105,-365],[-312,-929],[-155,691],[-568,-120],[-289,610],[-395,-848],[-656,136]],[[6644,7369],[274,416],[280,-694],[486,0],[199,-650],[-339,-933],[-267,-2183],[-1296,-1613],[-542,-418]],[[8511,6510],[369,-696]],[[8880,5814],[-321,31],[-48,665]],[[8511,6510],[-167,663],[447,612],[729,-164],[479,-1751],[-483,399],[-636,-455]],[[1959,469],[-818,824],[-441,820],[-189,1697],[-274,663],[-237,3741],[473,-989],[428,308],[429,1744],[215,277]]],"aliases":{},"names":{"larnaca":"CY-03","larnaca district":"CY-03","famagusta":"CY-04","nicosia":"CY-01","eparchia lefkosias":"CY-01","limassol":"CY-02","eparchia lemesou":"CY-02","paphos":"CY-05","eparchia pafou":"CY-05"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[41.74911,10.929825,43.418712,12.707913],"transform":{"scale":[0.00016697689768976873,0.00017782658265826586],"translate":[41.74911,10.929825]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1,2,3,4,5]],"id":"DJ-AR","properties":{"code":"DJ-AR","name":"Arta","label":[42.803,11.446]}},{"type":"Polygon","arcs":[[6,7,-1]],"id":"DJ-AS","properties":{"code":"DJ-AS","name":"Ali Sabieh","label":[42.899,11.211]}},{"type":"Polygon","arcs":[[8,-2,-8,9]],"id":"DJ-DI","properties":{"code":"DJ-DI","name":"Dikhil","label":[42.228,11.388]}},{"type":"Polygon","arcs":[[10,-5]],"id":"DJ-DJ","properties":{"code":"DJ-DJ","name":"Djibouti","label":[43.037,11.552]}},{"type":"Polygon","arcs":[[11,12]],"id":"DJ-OB","properties":{"code":"DJ-OB","name":"Obock","label":[43.046,12.284]}},{"type":"Polygon","arcs":[[13,-12,14,-3,-9]],"id":"DJ-TA","properties":{"code":"DJ-TA","name":"Tadjourah","label":[42.504,11.797]}}]}},"arcs":[[[8622,2688],[-780,398],[-875,-162],[-287,-486],[-946,-550]],[[5734,1888],[-1242,202],[-490,670],[-311,1314]],[[3691,4074],[303,-307],[697,-294]],[[4691,3473],[38,-250],[316,-171],[575,139],[-133,103],[446,269],[964,116]],[[6897,3679],[333,-334],[564,-32],[734,128]],[[8528,3441],[405,-303],[-311,-450]],[[8622,2688],[-1587,-2300],[-303,-116],[-607,103],[-253,385],[-727,136]],[[5145,896],[106,734],[483,258]],[[1351,5348],[1317,-323],[1023,-951]],[[5145,896],[-770,-169],[-526,-299],[-319,54],[-813,-174],[-799,-17],[-186,-175],[-504,-52],[-931,166],[-224,143],[145,401],[15,1081],[-233,1565],[354,1115],[768,513],[229,300]],[[6897,3679],[1540,97],[91,-335]],[[8006,5112],[-431,639],[-1236,1267],[-568,330],[-216,351],[117,457]],[[5672,8156],[575,275],[-42,496],[501,612],[189,-55],[1301,515],[992,-1189],[276,-43],[267,-1101],[224,-288],[44,-847],[-249,-486],[-532,-210],[-490,-54],[-722,-669]],[[1351,5348],[737,1065],[1144,1410],[182,368],[669,753],[543,-135],[938,-766],[108,113]],[[8006,5112],[-266,-227],[-868,-101],[-558,-183],[-295,-225],[-420,-707],[-282,-128],[-659,129],[33,-197]]],"aliases":{},"names":{"ali sabieh":"DJ-AS","ali sabieh region":"DJ-AS","arta":"DJ-AR","arta region":"DJ-AR","dikhil":"DJ-DI","tadjourah":"DJ-TA","obock":"DJ-OB","djibouti":"DJ-DJ","djibouti region":"DJ-DJ"}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"Topology","bbox":[-61.488922,15.201809,-61.249257,15.633857],"transform":{"scale":[0.000023968896889689194,0.00004320912091209121],"translate":[-61.488922,15.201809]},"objects":{"subdivisions":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[0,1,2,3,4]],"id":"DM-02","properties":{"code":"DM-02","name":"Saint Andrew","label":[-61.359,15.538]}},{"type":"Polygon","arcs":[[5,6,7,8,9,-2]],"id":"DM-03","properties":{"code":"DM-03","name":"Saint David","label":[-61.292,15.396]}},{"type":"Polygon","arcs":[[10,11,-8,12,13]],"id":"DM-04","properties":{"code":"DM-04","name":"Saint George","label":[-61.349,15.3]}},{"type":"Polygon","arcs":[[14,-5,15]],"id":"DM-05","properties":{"code":"DM-05","name":"Saint John","label":[-61.448,15.577]}},{"type":"Polygon","arcs":[[16,17,-3,-10,18]],"id":"DM-06","properties":{"code":"DM-06","name":"Saint Joseph","label":[-61.386,15.436]}},{"type":"Polygon","arcs":[[19,-14,20,21]],"id":"DM-07","properties":{"code":"DM-07","name":"Saint Luke","label":[-61.364,15.248]}},{"type":"Polygon","arcs":[[22,-22,23]],"id":"DM-08","properties":{"code":"DM-08","name":"Saint Mark","label":[-61.361,15.222]}},{"type":"Polygon","arcs":[[24,-24,-21,-13,-7]],"id":"DM-09","properties":{"code":"DM-09","name":"Saint Patrick","label":[-61.305,15.277]}},{"type":"Polygon","arcs":[[25,-19,-9,-12]],"id":"DM-10","properties":{"code":"DM-10","name":"Saint Paul","label":[-61.372,15.353]}},{"type":"Polygon","arcs":[[26,-16,-4,-18]],"id":"DM-11","properties":{"code":"DM-11","name":"Saint Peter","label":[-61.445,15.499]}}]}},"arcs":[[[1657,9981],[2694,-1035],[3210,-211],[1630,-1604]],[[9191,7131],[-230,-157],[-68,-86],[16,-33],[31,-470],[-11,-45],[-16,-3],[-26,-11],[-151,-83],[-43,-19],[-24,-7],[-23,-4],[-19,-2],[-68,0],[-14,-2],[-16,-4],[-106,-48],[-71,-42],[-36,-17],[-29,-9],[-14,0],[-28,3],[-16,1],[-30,-4],[-12,-1],[-14,3],[-18,4],[-16,5],[-191,-72],[-790,-403]],[[7158,5625],[-230,312],[-636,275],[-245,73],[-161,37],[-127,7],[-132,15],[-152,28],[-435,103],[-51,6],[-46,-5],[-171,-46],[-168,-29],[-246,-14],[-200,48],[-150,52],[-535,286]],[[3473,6773],[-85,198]],[[3388,6971],[-101,454],[-54,114],[-273,121],[-37,137],[25,670],[-40,250],[-19,5],[-32,25],[-11,4],[-14,3],[-13,2],[-15,-2],[-14,-1],[-19,-2],[-39,1],[-22,2],[-75,17],[-16,6],[-16,9],[-19,21],[-11,10],[-151,32],[-281,151],[-112,103],[-372,878]],[[9191,7131],[61,-49],[396,-815],[69,-848],[-288,-649],[190,-37],[173,-77],[207,-44],[-261,-845],[61,-738]],[[9799,3029],[-1766,-166],[-159,-45],[-335,-160]],[[7539,2658],[65,135],[2,25],[-9,44],[-19,38],[-33,47],[-8,17],[-2,16],[17,39],[-139,68],[-681,239]],[[6732,3326],[5,978],[-297,397]],[[6440,4701],[718,924]],[[4829,1355],[-4,140],[20,99],[-229,263],[-528,330],[-95,277],[-16,122]],[[3977,2586],[486,-15],[13,0],[74,-2],[25,1],[79,12],[22,0],[18,0],[19,-3],[17,-2],[29,4],[32,5],[62,29],[39,36],[92,163],[8,10],[6,8],[13,17],[1721,477]],[[7539,2658],[150,-59],[19,-13],[-6,-35],[-512,-459],[-120,-62],[-167,-63],[-386,-109],[-415,-179],[-582,-476]],[[5520,1203],[-691,152]],[[205,7586],[-35,17],[-170,125],[2,216],[203,151],[698,264],[214,231],[-289,30],[-505,99],[-321,16],[344,272],[486,992],[825,-18]],[[3388,6971],[-2316,666],[-15,-4],[-27,-2],[-32,2],[-178,18],[-18,0],[-218,30],[-34,3],[-39,-4],[-26,0],[-93,12],[-32,-10],[-17,-8],[-138,-88]],[[3042,4359],[-86,366],[-131,203],[-959,511],[-153,192],[-80,221],[-187,235],[-44,35]],[[1402,6122],[674,305],[254,138],[41,39],[60,40],[284,74],[758,55]],[[6440,4701],[-1396,-352],[-130,-9],[-181,14],[-712,92],[-515,-7],[-464,-80]],[[4845,819],[-16,536]],[[5520,1203],[40,-231]],[[5560,972],[-717,-183],[2,30]],[[5947,229],[-182,-152],[-506,-77],[-468,70],[-85,185],[139,564]],[[5560,972],[387,-743]],[[9799,3029],[13,-185],[-122,-911],[-857,-812],[-301,-126],[-203,-28],[-163,7],[-425,-30],[-355,45],[-263,-27],[-226,-111],[-213,-301],[-583,-137],[-136,-169],[-18,-15]],[[3977,2586],[-59,452],[-190,418],[-591,681],[-81,164],[-14,58]],[[1402,6122],[-197,152],[-231,75],[-271,121],[-92,280],[-39,562],[-173,178],[-194,96]]],"aliases":{},"names":{"saint patrick":"DM-09","saint david":"DM-03","saint andrew":"DM-02","saint john":"DM-05","saint peter":"DM-11","saint joseph":"DM-06","saint paul":"DM-10","saint george":"DM-04","saint luke":"DM-07","saint mark":"DM-08"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More