Compare commits

..

76 Commits

Author SHA1 Message Date
Enzo Martellucci
a3057488cf Merge branch 'master' into enxdev/fix/custom-tabs 2026-05-05 23:44:47 +02:00
Richard Fogaca Nienkotter
9459bc7bf4 fix(mcp): warn on invalid chart preview form data key (#39891)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 16:40:00 -03:00
Beto Dealmeida
cb53745d43 feat: semantic layer extension (#37815) 2026-05-05 12:07:46 -04:00
jesperct
9e91ae8cff fix(colors): reassign colliding series when dashboard locks shared dimension color (#39297)
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2026-05-05 08:38:19 -07:00
jesperct
5b5f23d127 test(plugin-chart-echarts): regression guards for temporal x-axis labels on timeseries charts (#39208) 2026-05-05 08:37:35 -07:00
Mehmet Salih Yavuz
8173cfe9e3 fix(CollectionControl): assign stable ids to keyless items (#39862) 2026-05-05 17:52:36 +03:00
Mehmet Salih Yavuz
586de12a05 fix(embedded): prevent duplicate React root on rehandshake (#39860) 2026-05-05 17:52:01 +03:00
dependabot[bot]
d6188374b4 chore(deps): bump docusaurus-theme-openapi-docs from 5.0.1 to 5.0.2 in /docs (#39846)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 09:16:53 +07:00
dependabot[bot]
2edae162f0 chore(deps): bump baseline-browser-mapping from 2.10.24 to 2.10.27 in /docs (#39848)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 09:16:33 +07:00
dependabot[bot]
e80207218b chore(deps-dev): bump eslint from 10.2.1 to 10.3.0 in /superset-websocket (#39843)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 17:02:27 -07:00
Beto Dealmeida
76955017eb chore: bump shillelagh to 1.4.4 (#39870) 2026-05-04 19:39:38 -04:00
Beto Dealmeida
5325b87e73 fix(clickhouse): prevent expensive table scan (#39867) 2026-05-04 19:39:10 -04:00
Đỗ Trọng Hải
e76318633e fix(helm): allow chart to work out-of-the-box with legacy Bitnami images (#39839)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-05-04 15:54:01 -07:00
Sam Firke
c2725e86f3 fix(markdown): Allow "target" attribute (#39868) 2026-05-04 18:27:43 -04:00
dependabot[bot]
2f605724e7 chore(deps-dev): bump globals from 17.5.0 to 17.6.0 in /superset-websocket (#39844)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:09:36 -07:00
dependabot[bot]
ebb02d0ecf chore(deps): bump @swc/core from 1.15.32 to 1.15.33 in /docs (#39845)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:09:21 -07:00
dependabot[bot]
319b8a1124 chore(deps-dev): bump globals from 17.5.0 to 17.6.0 in /docs (#39847)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:08:59 -07:00
dependabot[bot]
2be971ce77 chore(deps): bump docusaurus-plugin-openapi-docs from 5.0.1 to 5.0.2 in /docs (#39849)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:08:10 -07:00
dependabot[bot]
812f4ae080 chore(deps): update zod requirement from ^4.4.1 to ^4.4.3 in /superset-frontend/plugins/plugin-chart-echarts (#39850)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:07:55 -07:00
dependabot[bot]
af8d15fdfc chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#39852)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:07:41 -07:00
Amin Ghadersohi
673634f7af fix(mcp): point get_dataset_info url to explore view instead of legacy tablemodelview edit (#39838) 2026-05-04 13:39:05 -04:00
Mehmet Salih Yavuz
41a22d7918 chore: Upgrade to React 18 (#38563)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-04 19:19:36 +03:00
Amin Ghadersohi
28239c18d4 feat(mcp): warn when execute_sql template_params used with templating disabled (#39858) 2026-05-04 12:14:44 -04:00
dependabot[bot]
6205afbaa0 chore(deps-dev): bump webpack-sources from 3.4.0 to 3.4.1 in /superset-frontend (#39851) 2026-05-04 22:25:31 +07:00
EMMANUELA OPURUM
dc1c0f6ba1 docs: add user-facing Handlebars chart page with full helpers reference (#39591)
Co-authored-by: Emmanuela Opurum <youremail@example.com>
2026-05-02 13:16:39 -04:00
dependabot[bot]
ad73395c89 chore(deps-dev): bump yeoman-test from 11.3.1 to 11.4.2 in /superset-frontend (#39816)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 13:46:47 +07:00
Evan Rusackas
867e173427 chore(deps): drop stale legacy-plugin-chart-map-box lockfile entry (#39825)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 13:35:25 +07:00
dependabot[bot]
c90c8612ad chore(deps): bump @docusaurus/faster from 3.10.0 to 3.10.1 in /docs (#39804)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-05-02 13:32:37 +07:00
Abdul Rehman
b14cca15f6 fix(table): preserve decimals in totals row when Time Comparison is enabled (#39747) 2026-05-02 13:31:54 +07:00
dependabot[bot]
9d4384e49e chore(deps-dev): bump @babel/preset-env from 7.29.2 to 7.29.3 in /superset-frontend (#39822)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 12:38:54 +07:00
jesperct
d8dd2d99b3 fix(time-comparison): use chart row_limit instead of instance config in offset queries (#39490) 2026-05-01 16:24:59 -07:00
dependabot[bot]
dbe26d81ce chore(deps-dev): bump baseline-browser-mapping from 2.10.21 to 2.10.24 in /superset-frontend (#39759)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:24:23 -07:00
Elizabeth Thompson
98eaaaa6d6 fix(mcp): clear stale thread-local DB session in sync tool wrapper (#39798)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:24:48 -07:00
Jay Masiwal
cb74438865 fix(viz): correct table chart drill-to-detail temporal boundaries and null handling (#39668)
Co-authored-by: Samuelinto <samuel.mantilla@mail.utoronto.ca>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:46:18 -04:00
Danylo Korostil
e77fb5e3fc feat(i18n): updated Ukrainian translation (#39720) 2026-05-01 11:12:05 -04:00
dependabot[bot]
1ac113fd44 chore(deps): bump aws-actions/amazon-ecs-render-task-definition from 1.8.4 to 1.8.5 (#39809)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 06:31:48 -07:00
dependabot[bot]
6bfdee98cd chore(deps-dev): bump @docusaurus/tsconfig from 3.10.0 to 3.10.1 in /docs (#39811)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:31:29 -04:00
dependabot[bot]
de45f3a928 chore(deps): bump aws-actions/amazon-ecs-deploy-task-definition from 2.6.1 to 2.6.2 (#39806)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:30:49 -04:00
dependabot[bot]
2ec53c0694 chore(deps): bump mapbox-gl from 3.22.0 to 3.23.0 in /superset-frontend (#39769)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:30:21 -04:00
Michael S. Molina
d23b0cad92 chore: Bump core packages to 0.1.0 RC3 (#39823) 2026-05-01 09:54:39 -03:00
Evan Rusackas
e585406fff chore(codeowners): notify @sfirke on translation changes (#39794)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-30 23:07:29 -04:00
Amin Ghadersohi
957b298ae1 fix(mcp): add default request parameter to list_charts and list_dashboards (#39730) 2026-04-30 18:04:39 -04:00
Amin Ghadersohi
f29d82b3b1 feat(mcp): add query_dataset tool to query datasets using semantic layer (#39727) 2026-04-30 18:03:41 -04:00
Vitor Avila
3f550f166f fix(GSheets OAuth2): Re-add UnauthenticatedError (#39785) 2026-04-30 18:57:00 -03:00
Vitor Avila
86eb6176d1 fix: Enforce per-user caching on legacy API endpoint (#39789) 2026-04-30 18:04:33 -03:00
Joe Li
4244ae87bf fix(deps): regenerate pinned requirements for psycopg2-binary 2.9.12 (#39790)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 17:46:23 -03:00
Jakub Hrubý
512ba43e76 feat(i18n): add Czech translations (#36153)
Co-authored-by: Jakub Hrubý <jakub.hruby@orgis.cz>
Co-authored-by: Jezevec <panjzvc@gmail.com>
Co-authored-by: David Kopelent <david.kopelent@saltpay.co>
Co-authored-by: David Kopelent <66686489+davidkopelent@users.noreply.github.com>
2026-04-30 11:14:58 -04:00
xavier-GitHub76
f57ba7645d fix(CountryMap): ISO updated for France overseas (complete run) (#36055) 2026-04-30 11:13:51 -04:00
marun
12f69760f9 fix(table): conditionally render search dropdown only when search input is enabled (#35204)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-30 11:08:01 -04:00
Geidō
4fcb3144ff fix(dashboard): prevent duplicate screenshot downloads (#39525)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:04:24 +03:00
dependabot[bot]
3f68104007 chore(deps-dev): bump @swc/plugin-emotion from 14.8.0 to 14.9.0 in /superset-frontend (#39715)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 10:55:08 -04:00
dependabot[bot]
9faeda5723 chore(deps): bump @ant-design/icons from 6.2.0 to 6.2.2 in /docs (#39691)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 10:54:04 -04:00
dependabot[bot]
c15b208fda chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#39745)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 10:34:21 -04:00
dependabot[bot]
6ad503201b chore(deps): bump zod from 4.3.6 to 4.4.1 in /superset-frontend (#39770)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 20:58:22 +07:00
Hardik Thaker
56e9331dad chore: add Aadhar Housing Finance Limited to INTHEWILD (#38366)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-04-30 06:57:49 -07:00
mapledan
a135e29035 fix(time-format): handle string input in TimeFormatter to fix pivot table NaN dates (#38949)
Co-authored-by: RD-Dan <mapledan@staff.ruten.com.tw>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2026-04-30 09:51:37 -04:00
EPoikans
bc875aa3e3 feat: Latvian localization (#38965)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-04-30 06:19:42 -07:00
Joe Li
7842a9b05d fix(playwright): remove Google Sheets dependency from dataset tests (#39143)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 06:18:50 -07:00
dependabot[bot]
1061b0612c chore(deps-dev): bump eslint-plugin-no-only-tests from 3.3.0 to 3.4.0 in /superset-frontend (#39768)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:16:27 -07:00
dependabot[bot]
bfacc3b5ac chore(deps): bump xlsxwriter from 3.0.9 to 3.2.9 (#39757)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:15:12 -07:00
dependabot[bot]
9001e7dcf2 chore(deps): bump pandas from 2.1.4 to 2.3.3 (#39754)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:14:20 -07:00
dependabot[bot]
a4532844f4 chore(deps): bump msgpack from 1.0.8 to 1.1.2 (#39752)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:12:33 -07:00
dependabot[bot]
43a2cd3660 chore(deps-dev): bump psycopg2-binary from 2.9.9 to 2.9.12 (#39749)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:11:29 -07:00
dependabot[bot]
c895c4ffa9 chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#39744)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:11:10 -07:00
dependabot[bot]
ce3f19d373 chore(deps): bump swagger-ui-react from 5.32.4 to 5.32.5 in /docs (#39693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:10:49 -07:00
dependabot[bot]
2c26914c2e chore(deps-dev): bump typescript-eslint from 8.59.0 to 8.59.1 in /docs (#39694)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 06:08:58 -07:00
innovark
f7c955f81a feat: provide full endpoint URL construction for plugin developers (#37360)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-04-30 05:59:11 -07:00
Jean Massucatto
9c3c8dcc0b fix(table): restore dropdown arrow visibility on paginated table page… (#39305) 2026-04-30 05:56:51 -07:00
Enzo Martellucci
a3c5d0356d Merge branch 'master' into enxdev/fix/custom-tabs 2026-04-27 10:32:48 +02:00
Enzo Martellucci
60ef1ed246 Merge branch 'master' into enxdev/fix/custom-tabs 2026-02-10 16:03:44 +01:00
Enzo Martellucci
624b5ec260 Merge branch 'master' into enxdev/fix/custom-tabs 2026-02-04 09:41:39 +01:00
Enzo Martellucci
c288f3ffd6 Merge branch 'master' into enxdev/fix/custom-tabs 2026-01-28 11:28:36 +01:00
Enzo Martellucci
e21fb5659a Merge branch 'master' into enxdev/fix/custom-tabs 2026-01-20 09:43:43 +01:00
Enzo Martellucci
f80c46005e Merge branch 'master' into enxdev/fix/custom-tabs 2026-01-13 16:29:02 +01:00
Enzo Martellucci
b5e76ccf35 lint 2026-01-02 13:00:18 +01:00
Enzo Martellucci
ad53a4c744 fix(dashboard): disable drag on input fields during tab reorder
Prevents drag activation when clicking on input/textarea elements,
allowing users to edit tab titles without accidentally triggering
drag-and-drop reordering
2026-01-02 12:02:41 +01:00
458 changed files with 68625 additions and 16383 deletions

4
.github/CODEOWNERS vendored
View File

@@ -36,6 +36,10 @@
**/*.geojson @villebro @rusackas
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
# Notify translation maintainers of changes to translations
/superset/translations/ @sfirke
# Notify PMC members of changes to extension-related files
/docs/developer_portal/extensions/ @michael-s-molina @villebro @rusackas

10
.github/labeler.yml vendored
View File

@@ -77,6 +77,11 @@
- any-glob-to-any-file:
- 'superset/translations/zh/**'
"i18n:czech":
- changed-files:
- any-glob-to-any-file:
- 'superset/translations/cs/**'
"i18n:traditional-chinese":
- changed-files:
- any-glob-to-any-file:
@@ -122,6 +127,11 @@
- any-glob-to-any-file:
- 'superset/translations/sk/**'
"i18n:latvian":
- changed-files:
- any-glob-to-any-file:
- 'superset/translations/lv/**'
"i18n:ukrainian":
- changed-files:
- any-glob-to-any-file:

View File

@@ -127,6 +127,20 @@ playwright_testdata() {
superset load_test_users
superset load_examples
superset init
# Enable DML on the examples database so Playwright tests can create/drop
# temporary tables via SQL Lab without depending on external data sources.
superset shell <<'PYEOF'
import sys
from superset.extensions import db
from superset.models.core import Database
examples_db = db.session.query(Database).filter_by(database_name='examples').first()
if not examples_db:
sys.exit('ERROR: examples database not found. load_examples may have failed.')
examples_db.allow_dml = True
db.session.commit()
print('Enabled allow_dml on examples database')
PYEOF
say "::endgroup::"
}

View File

@@ -265,7 +265,7 @@ jobs:
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1
uses: aws-actions/amazon-ecs-render-task-definition@6853cfae8c3a7d978fbf68b5a55453395541dfbb # v1
with:
task-definition: .github/workflows/ecs-task-definition.json
container-name: superset-ci
@@ -300,7 +300,7 @@ jobs:
--tags key=pr,value=$PR_NUMBER key=github_user,value=${{ github.actor }}
- name: Deploy Amazon ECS task definition
id: deploy-task
uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2
uses: aws-actions/amazon-ecs-deploy-task-definition@a310a830f5c14e583e35d84e4e1ec7dd177c3c9c # v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service

View File

@@ -54,6 +54,7 @@ jobs:
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:

View File

@@ -58,6 +58,10 @@ categories:
url: https://www.ontruck.com/
Financial Services:
- name: Aadhar Housing Finance Limited
url: https://www.aadharhousing.com
contributors: ["@thakerhardiks"]
- name: Aktia Bank plc
url: https://www.aktia.com

View File

@@ -46,6 +46,13 @@ The Deck.gl MapBox chart's **Opacity**, **Default longitude**, **Default latitud
**To restore fit-to-data behavior:** Open the chart in Explore, clear the **Default longitude**, **Default latitude**, and **Zoom** fields in the Viewport section, and re-save the chart.
### Combined datasource list endpoint
Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to serve datasets and semantic views in one response.
- The endpoint is available to users with at least one of `can_read` on `Dataset` or `SemanticView`.
- Semantic views are included only when the `SEMANTIC_LAYERS` feature flag is enabled.
- The endpoint enforces strict `order_column` validation and returns `400` for invalid sort columns.
### ClickHouse minimum driver version bump
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.

View File

@@ -105,7 +105,13 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
"SEMANTIC_LAYERS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.

View File

@@ -224,3 +224,52 @@ async def analysis_guide(ctx: Context) -> str:
```
See [MCP Integration](./mcp) for implementation details.
### Semantic Layers
Extensions can register custom semantic layer implementations that allow Superset to connect to external data modeling frameworks. Each semantic layer defines how to authenticate, discover semantic views (tables/metrics/dimensions), and execute queries against the external system.
```python
from superset_core.semantic_layers.decorators import semantic_layer
from superset_core.semantic_layers.layer import SemanticLayer
from my_extension.config import MyConfig
from my_extension.view import MySemanticView
@semantic_layer(
id="my_platform",
name="My Data Platform",
description="Connect to My Data Platform's semantic layer",
)
class MySemanticLayer(SemanticLayer[MyConfig, MySemanticView]):
configuration_class = MyConfig
@classmethod
def from_configuration(cls, configuration: dict) -> "MySemanticLayer":
config = MyConfig.model_validate(configuration)
return cls(config)
@classmethod
def get_configuration_schema(cls, configuration=None) -> dict:
return MyConfig.model_json_schema()
@classmethod
def get_runtime_schema(cls, configuration=None, runtime_data=None) -> dict:
return {"type": "object", "properties": {}}
def get_semantic_views(self, runtime_configuration: dict) -> set[MySemanticView]:
# Return available views from the external platform
...
def get_semantic_view(self, name: str, additional_configuration: dict) -> MySemanticView:
# Return a specific view by name
...
```
**Note**: The `@semantic_layer` decorator automatically detects context and applies appropriate ID prefixing:
- **Extension context**: ID prefixed as `extensions.{publisher}.{name}.{id}`
- **Host context**: Original ID used as-is
The decorator registers the class in the semantic layers registry, making it available in the UI for users to create connections. The `configuration_class` should be a Pydantic model that defines the fields needed to connect (credentials, project, database, etc.). Superset uses the model's JSON schema to render the configuration form dynamically.

View File

@@ -0,0 +1,143 @@
---
title: Handlebars Chart
hide_title: true
sidebar_position: 10
version: 1
---
## Handlebars Chart
The Handlebars chart lets you render query results using a custom [Handlebars](https://handlebarsjs.com/) template. This gives you full control over how your data is displayed — from simple tables to rich HTML layouts.
### Basic Usage
In the chart editor, write a Handlebars template in the **Template** field. Your query results are available as `data`, an array of row objects.
```handlebars
{{#each data}}
<p>{{this.name}}: {{this.value}}</p>
{{/each}}
```
### Built-in Helpers
Superset registers several custom helpers on top of the standard Handlebars built-ins.
#### `dateFormat`
Formats a date value using [Day.js](https://day.js.org/) format strings.
```handlebars
{{dateFormat my_date format="MMMM YYYY"}}
```
| Option | Default | Description |
|--------|---------|-------------|
| `format` | `YYYY-MM-DD` | A Day.js-compatible format string |
---
#### `stringify`
Converts an object to a JSON string, or any other value to its string representation.
```handlebars
{{stringify myObj}}
```
---
#### `formatNumber`
Formats a number using locale-aware formatting.
```handlebars
{{formatNumber myNumber "en-US"}}
```
| Option | Default | Description |
|--------|---------|-------------|
| `locale` | `en-US` | A BCP 47 language tag |
---
#### `parseJson`
Parses a JSON string into an object that can be used in your template.
```handlebars
{{parseJson myJsonString}}
```
---
#### `groupBy`
Groups an array of objects by a key, powered by [handlebars-group-by](https://github.com/nicktindall/handlebars-group-by).
```handlebars
{{#groupBy data "department"}}
<h3>{{value}}</h3>
{{#each items}}
<p>{{this.name}}</p>
{{/each}}
{{/groupBy}}
```
---
### Helpers from just-handlebars-helpers
Superset also registers all helpers from the [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) library. These include a wide range of comparison, math, string, and conditional helpers. Commonly used ones include:
#### Comparison
| Helper | Description | Example |
|--------|-------------|---------|
| `eq` | Strict equality | `{{#if (eq status "active")}}` |
| `eqw` | Weak equality | `{{#if (eqw count "5")}}` |
| `neq` | Strict inequality | `{{#if (neq role "admin")}}` |
| `lt` | Less than | `{{#if (lt score 50)}}` |
| `lte` | Less than or equal | `{{#if (lte score 100)}}` |
| `gt` | Greater than | `{{#if (gt price 0)}}` |
| `gte` | Greater than or equal | `{{#if (gte age 18)}}` |
#### Logical
| Helper | Description | Example |
|--------|-------------|---------|
| `and` | Logical AND | `{{#if (and isActive isVerified)}}` |
| `or` | Logical OR | `{{#if (or isAdmin isMod)}}` |
| `not` | Logical NOT | `{{#if (not isDisabled)}}` |
| `ifx` | Inline conditional | `{{ifx isActive "Yes" "No"}}` |
| `coalesce` | Returns first non-falsy value | `{{coalesce nickname name "Anonymous"}}` |
#### String
| Helper | Description | Example |
|--------|-------------|---------|
| `capitalize` | Capitalizes first letter | `{{capitalize name}}` |
| `uppercase` | Converts to uppercase | `{{uppercase status}}` |
| `lowercase` | Converts to lowercase | `{{lowercase email}}` |
| `truncate` | Truncates a string | `{{truncate description 100}}` |
| `contains` | Checks if string contains substring | `{{#if (contains tag "urgent")}}` |
#### Math
| Helper | Description | Example |
|--------|-------------|---------|
| `add` | Addition | `{{add a b}}` |
| `subtract` | Subtraction | `{{subtract total discount}}` |
| `multiply` | Multiplication | `{{multiply price quantity}}` |
| `divide` | Division | `{{divide total count}}` |
| `ceil` | Ceiling | `{{ceil value}}` |
| `floor` | Floor | `{{floor value}}` |
| `round` | Round | `{{round value}}` |
For the full list of available helpers, see the [just-handlebars-helpers documentation](https://github.com/leapfrogtechnology/just-handlebars-helpers).
### Tips
- Use raw blocks to escape Handlebars syntax if you need to display double curly braces literally.
- Comparison helpers like `eq` must be wrapped in a subexpression when used with `#if`: `{{#if (eq myVal "foo")}}`.
- HTML output is sanitized by default based on your Superset configuration (`HTML_SANITIZATION`).

View File

@@ -40,13 +40,13 @@
"version:remove:components": "node scripts/manage-versions.mjs remove components"
},
"dependencies": {
"@ant-design/icons": "^6.2.0",
"@docusaurus/core": "^3.10.0",
"@docusaurus/faster": "^3.10.0",
"@docusaurus/plugin-client-redirects": "^3.10.0",
"@docusaurus/preset-classic": "3.10.0",
"@docusaurus/theme-live-codeblock": "^3.10.0",
"@docusaurus/theme-mermaid": "^3.10.0",
"@ant-design/icons": "^6.2.2",
"@docusaurus/core": "^3.10.1",
"@docusaurus/faster": "^3.10.1",
"@docusaurus/plugin-client-redirects": "^3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@docusaurus/theme-live-codeblock": "^3.10.1",
"@docusaurus/theme-mermaid": "^3.10.1",
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1",
@@ -67,12 +67,12 @@
"@storybook/preview-api": "^8.6.18",
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.32",
"@swc/core": "^1.15.33",
"antd": "^6.3.7",
"baseline-browser-mapping": "^2.10.24",
"baseline-browser-mapping": "^2.10.27",
"caniuse-lite": "^1.0.30001791",
"docusaurus-plugin-openapi-docs": "^5.0.1",
"docusaurus-theme-openapi-docs": "^5.0.1",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^4.1.1",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
@@ -86,14 +86,14 @@
"remark-import-partial": "^0.0.2",
"reselect": "^5.1.1",
"storybook": "^8.6.18",
"swagger-ui-react": "^5.32.4",
"swagger-ui-react": "^5.32.5",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.0",
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/module-type-aliases": "^3.10.1",
"@docusaurus/tsconfig": "^3.10.1",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
@@ -103,10 +103,10 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.5.0",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.59.0",
"typescript-eslint": "^8.59.1",
"webpack": "^5.106.2"
},
"browserslist": {
@@ -124,8 +124,7 @@
"resolutions": {
"react-redux": "^9.2.0",
"@reduxjs/toolkit": "^2.5.0",
"baseline-browser-mapping": "^2.9.19",
"webpackbar": "^7.0.0"
"baseline-browser-mapping": "^2.9.19"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@@ -81,6 +81,12 @@
"lifecycle": "development",
"description": "Expand nested types in Presto into extra columns/arrays. Experimental, doesn't work with all nested types."
},
{
"name": "SEMANTIC_LAYERS",
"default": false,
"lifecycle": "development",
"description": "Enable semantic layers and show semantic views alongside datasets"
},
{
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
"default": false,

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.15.4 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.15.5 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.15.4](https://img.shields.io/badge/Version-0.15.4-informational?style=flat-square)
![Version: 0.15.5](https://img.shields.io/badge/Version-0.15.5-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application

View File

@@ -844,6 +844,8 @@ postgresql:
database: superset
image:
registry: docker.io
repository: bitnamilegacy/postgresql
tag: "14.17.0-debian-12-r3"
## PostgreSQL Primary parameters
@@ -918,6 +920,11 @@ redis:
accessModes:
- ReadWriteOnce
image:
registry: docker.io
repository: bitnamilegacy/redis
tag: 7.0.10-debian-11-r4
nodeSelector: {}
tolerations: []

View File

@@ -70,13 +70,13 @@ dependencies = [
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
"marshmallow>=3.0, <4",
"marshmallow-union>=0.1",
"msgpack>=1.0.0, <1.1",
"msgpack>=1.0.0, <1.2",
"nh3>=0.2.11, <0.3",
"numpy>1.23.5, <2.3",
"packaging",
# --------------------------
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
"pandas[excel]>=2.1.4, <2.2",
"pandas[excel]>=2.1.4, <2.4",
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
# --------------------------
"parsedatetime",
@@ -95,7 +95,7 @@ dependencies = [
"redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0",
"selenium>=4.14.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.3, <2.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
"slack_sdk>=3.19.0, <4",
@@ -109,7 +109,7 @@ dependencies = [
"watchdog>=6.0.0",
"wtforms>=2.3.3, <4",
"wtforms-json",
"xlsxwriter>=3.0.7, <3.1",
"xlsxwriter>=3.0.7, <3.3",
]
[project.optional-dependencies]
@@ -149,7 +149,7 @@ fastmcp = ["fastmcp>=3.2.4,<4.0"]
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.3, <2"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
@@ -175,13 +175,13 @@ oracle = ["cx-Oracle>8.0.0, <8.1"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <6.0.0"]
playwright = ["playwright>=1.37.0, <2"]
postgres = ["psycopg2-binary==2.9.9"]
postgres = ["psycopg2-binary==2.9.12"]
presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.328.0"]
prophet = ["prophet>=1.1.6, <2"]
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.3, <2"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
sqlite = ["syntaqlite>=0.1.0"]
@@ -288,6 +288,7 @@ module = [
"superset.tags.filters",
"superset.commands.security.update",
"superset.commands.security.create",
"superset.semantic_layers.api",
]
warn_unused_ignores = false

View File

@@ -381,7 +381,7 @@ selenium==4.32.0
# via apache-superset (pyproject.toml)
setuptools==80.9.0
# via -r requirements/base.in
shillelagh==1.4.3
shillelagh==1.4.4
# via apache-superset (pyproject.toml)
simplejson==3.20.1
# via apache-superset (pyproject.toml)

View File

@@ -707,7 +707,7 @@ protobuf==4.25.8
# proto-plus
psutil==6.1.0
# via apache-superset
psycopg2-binary==2.9.9
psycopg2-binary==2.9.12
# via apache-superset
py-key-value-aio==0.4.4
# via fastmcp
@@ -931,7 +931,7 @@ setuptools==80.9.0
# pydata-google-auth
# zope-event
# zope-interface
shillelagh==1.4.3
shillelagh==1.4.4
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -18,7 +18,7 @@
[project]
name = "apache-superset-core"
version = "0.1.0rc2"
version = "0.1.0rc3"
description = "Core Python package for building Apache Superset backend extensions and integrations"
readme = "README.md"
authors = [
@@ -43,6 +43,8 @@ classifiers = [
]
dependencies = [
"flask-appbuilder>=5.0.2,<6",
"isodate>=0.7.0",
"pyarrow>=16.0.0",
"pydantic>=2.8.0",
"sqlalchemy>=1.4.0,<2.0",
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris

View File

@@ -0,0 +1,73 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import Any
from pydantic import BaseModel
def build_configuration_schema(
config_class: type[BaseModel],
configuration: BaseModel | None = None,
) -> dict[str, Any]:
"""
Build a JSON schema from a Pydantic configuration class.
Handles generic boilerplate that any semantic layer with dynamic fields needs:
- Reorders properties to match model field order (Pydantic sorts alphabetically)
- When ``configuration`` is None, sets ``enum: []`` on all ``x-dynamic`` properties
so the frontend renders them as empty dropdowns
Semantic layer implementations call this instead of
``model_json_schema()`` directly,
then only need to add their own dynamic population logic.
"""
schema = config_class.model_json_schema()
# Pydantic sorts properties alphabetically; restore model field order
field_order = [
field.alias or name for name, field in config_class.model_fields.items()
]
schema["properties"] = {
key: schema["properties"][key]
for key in field_order
if key in schema["properties"]
}
if configuration is None:
for prop_schema in schema["properties"].values():
if prop_schema.get("x-dynamic"):
prop_schema["enum"] = []
return schema
def check_dependencies(
prop_schema: dict[str, Any],
configuration: BaseModel,
) -> bool:
"""
Check whether a dynamic property's dependencies are satisfied.
Reads the ``x-dependsOn`` list from the property schema and returns ``True``
when every referenced attribute on ``configuration`` is truthy.
"""
dependencies = prop_schema.get("x-dependsOn", [])
return all(getattr(configuration, dep, None) for dep in dependencies)

View File

@@ -0,0 +1,169 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Semantic layer DAO interfaces for superset-core.
Provides abstract DAO classes for semantic layers and views that define the
interface contract. Host implementations replace these with concrete classes
backed by SQLAlchemy during initialization.
Usage:
from superset_core.semantic_layers.daos import (
AbstractSemanticLayerDAO,
AbstractSemanticViewDAO,
)
"""
from __future__ import annotations
from abc import abstractmethod
from typing import Any, ClassVar
from superset_core.common.daos import BaseDAO
from superset_core.semantic_layers.models import SemanticLayerModel, SemanticViewModel
class AbstractSemanticLayerDAO(BaseDAO[SemanticLayerModel]):
"""
Abstract DAO interface for SemanticLayer.
Host implementations will replace this class during initialization
with a concrete DAO providing actual database access.
"""
model_cls: ClassVar[type[Any] | None] = None
base_filter = None
id_column_name = "uuid"
uuid_column_name = "uuid"
@classmethod
@abstractmethod
def validate_uniqueness(cls, name: str) -> bool:
"""
Validate that a semantic layer name is unique.
:param name: Semantic layer name to validate
:return: True if the name is unique, False otherwise
"""
...
@classmethod
@abstractmethod
def validate_update_uniqueness(cls, layer_uuid: str, name: str) -> bool:
"""
Validate that a semantic layer name is unique for an update operation,
excluding the layer being updated.
:param layer_uuid: UUID of the semantic layer being updated
:param name: New name to validate
:return: True if the name is unique, False otherwise
"""
...
@classmethod
@abstractmethod
def find_by_name(cls, name: str) -> SemanticLayerModel | None:
"""
Find a semantic layer by name.
:param name: Semantic layer name
:return: SemanticLayerModel instance or None
"""
...
@classmethod
@abstractmethod
def get_semantic_views(cls, layer_uuid: str) -> list[SemanticViewModel]:
"""
Get all semantic views associated with a semantic layer.
:param layer_uuid: UUID of the semantic layer
:return: List of SemanticViewModel instances
"""
...
class AbstractSemanticViewDAO(BaseDAO[SemanticViewModel]):
"""
Abstract DAO interface for SemanticView.
Host implementations will replace this class during initialization
with a concrete DAO providing actual database access.
"""
model_cls: ClassVar[type[Any] | None] = None
base_filter = None
id_column_name = "id"
uuid_column_name = "uuid"
@classmethod
@abstractmethod
def validate_uniqueness(
cls,
name: str,
layer_uuid: str,
configuration: dict[str, Any],
) -> bool:
"""
Validate that a semantic view is unique within a semantic layer.
Uniqueness is determined by the combination of name, layer UUID, and
configuration.
:param name: View name
:param layer_uuid: UUID of the parent semantic layer
:param configuration: Configuration dict to compare
:return: True if unique, False otherwise
"""
...
@classmethod
@abstractmethod
def validate_update_uniqueness(
cls,
view_uuid: str,
name: str,
layer_uuid: str,
configuration: dict[str, Any],
) -> bool:
"""
Validate that a semantic view is unique within a semantic layer for an
update operation, excluding the view being updated.
:param view_uuid: UUID of the view being updated
:param name: New name to validate
:param layer_uuid: UUID of the parent semantic layer
:param configuration: Configuration dict to compare
:return: True if unique, False otherwise
"""
...
@classmethod
@abstractmethod
def find_by_name(cls, name: str, layer_uuid: str) -> SemanticViewModel | None:
"""
Find a semantic view by name within a semantic layer.
:param name: View name
:param layer_uuid: UUID of the parent semantic layer
:return: SemanticViewModel instance or None
"""
...
__all__ = ["AbstractSemanticLayerDAO", "AbstractSemanticViewDAO"]

View File

@@ -0,0 +1,102 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Semantic layer registration decorator for Superset.
This module provides a decorator interface to register semantic layer
implementations with the host application, enabling automatic discovery
by the extensions framework.
Usage:
from superset_core.semantic_layers.decorators import semantic_layer
@semantic_layer(
id="snowflake",
name="Snowflake Cortex",
description="Snowflake semantic layer via Cortex Analyst",
)
class SnowflakeSemanticLayer(SemanticLayer[SnowflakeConfig, SnowflakeView]):
...
# Or with minimal arguments:
@semantic_layer(id="dbt", name="dbt Semantic Layer")
class DbtSemanticLayer(SemanticLayer[DbtConfig, DbtView]):
...
"""
from __future__ import annotations
from typing import Callable, TypeVar
# Type variable for decorated semantic layer classes
T = TypeVar("T")
def semantic_layer(
id: str,
name: str,
description: str | None = None,
) -> Callable[[T], T]:
"""
Decorator to register a semantic layer implementation.
Automatically detects extension context and applies appropriate
namespacing to prevent ID conflicts between host and extension
semantic layers.
Host implementations will replace this function during initialization
with a concrete implementation providing actual functionality.
Args:
id: Unique semantic layer type identifier (e.g., "snowflake",
"dbt"). Used as the key in the semantic layers registry and
stored in the ``type`` column of the ``SemanticLayer`` model.
name: Human-readable display name (e.g., "Snowflake Cortex").
Shown in the UI when listing available semantic layer types.
description: Optional description for documentation and UI
tooltips.
Returns:
Decorated semantic layer class registered with the host
application.
Raises:
NotImplementedError: If called before host implementation is
initialized.
Example:
from superset_core.semantic_layers.decorators import semantic_layer
from superset_core.semantic_layers.layer import SemanticLayer
@semantic_layer(
id="snowflake",
name="Snowflake Cortex",
description="Connect to Snowflake Cortex Analyst",
)
class SnowflakeSemanticLayer(
SemanticLayer[SnowflakeConfig, SnowflakeView]
):
...
"""
raise NotImplementedError(
"Semantic layer decorator not initialized. "
"This decorator should be replaced during Superset startup."
)
__all__ = ["semantic_layer"]

View File

@@ -0,0 +1,129 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar
from pydantic import BaseModel
from superset_core.semantic_layers.view import SemanticView
ConfigT = TypeVar("ConfigT", bound=BaseModel)
SemanticViewT = TypeVar("SemanticViewT", bound="SemanticView")
class SemanticLayer(ABC, Generic[ConfigT, SemanticViewT]):
"""
Abstract base class for semantic layers.
"""
configuration_class: type[BaseModel]
@classmethod
@abstractmethod
def from_configuration(
cls,
configuration: dict[str, Any],
) -> SemanticLayer[ConfigT, SemanticViewT]:
"""
Create a semantic layer from its configuration.
"""
raise NotImplementedError(
"Semantic layers must implement the from_configuration method"
)
@classmethod
@abstractmethod
def get_configuration_schema(
cls,
configuration: ConfigT | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the configuration needed to add the semantic layer.
A partial configuration `configuration` can be sent to improve the schema,
allowing for progressive validation and better UX. For example, a semantic
layer might require:
- auth information
- a database
If the user provides the auth information, a client can send the partial
configuration to this method, and the resulting JSON schema would include
the list of databases the user has access to, allowing a dropdown to be
populated.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are populated based on the provided connection info.
"""
raise NotImplementedError(
"Semantic layers must implement the get_configuration_schema method"
)
@classmethod
@abstractmethod
def get_runtime_schema(
cls,
configuration: ConfigT,
runtime_data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the runtime parameters needed to load semantic views.
This returns the schema needed to connect to a semantic view given the
configuration for the semantic layer. For example, a semantic layer might
be configured by:
- auth information
- an optional database
If the user does not provide a database when creating the semantic layer, the
runtime schema would require the database name to be provided before loading any
semantic views. This allows users to create semantic layers that connect to a
specific database (or project, account, etc.), or that allow users to select it
at query time.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are required if they were not provided in the initial
configuration.
"""
raise NotImplementedError(
"Semantic layers must implement the get_runtime_schema method"
)
@abstractmethod
def get_semantic_views(
self,
runtime_configuration: dict[str, Any],
) -> set[SemanticViewT]:
"""
Get the semantic views available in the semantic layer.
The runtime configuration can provide information like a given project or
schema, used to restrict the semantic views returned.
"""
@abstractmethod
def get_semantic_view(
self,
name: str,
additional_configuration: dict[str, Any],
) -> SemanticViewT:
"""
Get a specific semantic view by its name and additional configuration.
"""

View File

@@ -0,0 +1,85 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Semantic layer model interfaces for superset-core.
Provides abstract model classes for semantic layers and views that will be
replaced by the host implementation's concrete SQLAlchemy models during
initialization.
Usage:
from superset_core.semantic_layers.models import (
SemanticLayerModel,
SemanticViewModel,
)
"""
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from superset_core.common.models import CoreModel
class SemanticLayerModel(CoreModel):
"""
Abstract interface for the SemanticLayer database model.
Host implementations will replace this class during initialization
with a concrete SQLAlchemy model providing actual persistence.
"""
__abstract__ = True
# Type hints for expected column attributes
uuid: UUID
name: str
description: str | None
type: str
configuration: str
configuration_version: int
cache_timeout: int | None
created_on: datetime | None
changed_on: datetime | None
class SemanticViewModel(CoreModel):
"""
Abstract interface for the SemanticView database model.
Host implementations will replace this class during initialization
with a concrete SQLAlchemy model providing actual persistence.
"""
__abstract__ = True
# Type hints for expected column attributes
id: int
uuid: UUID
name: str
description: str | None
configuration: str
configuration_version: int
cache_timeout: int | None
semantic_layer_uuid: UUID
created_on: datetime | None
changed_on: datetime | None
__all__ = ["SemanticLayerModel", "SemanticViewModel"]

View File

@@ -0,0 +1,209 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import enum
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
import isodate
import pyarrow as pa
@dataclass(frozen=True)
class Grain:
"""
Represents a time grain (e.g., day, month, year).
Attributes:
name: Human-readable name of the grain (e.g., "Second")
representation: ISO 8601 duration (e.g., "PT1S", "P1D", "P1M")
"""
name: str
representation: str
def __post_init__(self) -> None:
isodate.parse_duration(self.representation)
def __eq__(self, other: object) -> bool:
if isinstance(other, Grain):
return self.representation == other.representation
return NotImplemented
def __hash__(self) -> int:
return hash(self.representation)
class Grains:
"""Pre-defined common grains and factory for custom ones."""
SECOND = Grain("Second", "PT1S")
MINUTE = Grain("Minute", "PT1M")
HOUR = Grain("Hour", "PT1H")
DAY = Grain("Day", "P1D")
WEEK = Grain("Week", "P1W")
MONTH = Grain("Month", "P1M")
QUARTER = Grain("Quarter", "P3M")
YEAR = Grain("Year", "P1Y")
_REGISTRY: dict[str, Grain] = {
"PT1S": SECOND,
"PT1M": MINUTE,
"PT1H": HOUR,
"P1D": DAY,
"P1W": WEEK,
"P1M": MONTH,
"P3M": QUARTER,
"P1Y": YEAR,
}
@classmethod
def get(cls, representation: str, name: str | None = None) -> Grain:
"""Return a pre-defined grain or create a custom one."""
if grain := cls._REGISTRY.get(representation):
return grain
return Grain(name or representation, representation)
@dataclass(frozen=True)
class Dimension:
id: str
name: str
type: pa.DataType
definition: str | None = None
description: str | None = None
grain: Grain | None = None
@dataclass(frozen=True)
class Metric:
id: str
name: str
type: pa.DataType
definition: str
description: str | None = None
@dataclass(frozen=True)
class AdhocExpression:
id: str
definition: str
class Operator(str, enum.Enum):
EQUALS = "="
NOT_EQUALS = "!="
GREATER_THAN = ">"
LESS_THAN = "<"
GREATER_THAN_OR_EQUAL = ">="
LESS_THAN_OR_EQUAL = "<="
IN = "IN"
NOT_IN = "NOT IN"
LIKE = "LIKE"
NOT_LIKE = "NOT LIKE"
IS_NULL = "IS NULL"
IS_NOT_NULL = "IS NOT NULL"
ADHOC = "ADHOC"
FilterValues = str | int | float | bool | datetime | date | time | timedelta | None
class PredicateType(enum.Enum):
WHERE = "WHERE"
HAVING = "HAVING"
@dataclass(frozen=True, order=True)
class Filter:
type: PredicateType
column: Dimension | Metric | None
operator: Operator
value: FilterValues | frozenset[FilterValues]
class OrderDirection(enum.Enum):
ASC = "ASC"
DESC = "DESC"
OrderTuple = tuple[Metric | Dimension | AdhocExpression, OrderDirection]
@dataclass(frozen=True)
class GroupLimit:
"""
Limit query to top/bottom N combinations of specified dimensions.
The `filters` parameter allows specifying separate filter constraints for the
group limit subquery. This is useful when you want to determine the top N groups
using different criteria (e.g., a different time range) than the main query.
For example, you might want to find the top 10 products by sales over the last
30 days, but then show daily sales for those products over the last 7 days.
"""
dimensions: list[Dimension]
top: int
metric: Metric | None
direction: OrderDirection = OrderDirection.DESC
group_others: bool = False
filters: set[Filter] | None = None
@dataclass(frozen=True)
class SemanticRequest:
"""
Represents a request made to obtain semantic results.
This could be a SQL query, an HTTP request, etc.
"""
type: str
definition: str
@dataclass(frozen=True)
class SemanticResult:
"""
Represents the results of a semantic query.
This includes any requests (SQL queries, HTTP requests) that were performed in order
to obtain the results, in order to help troubleshooting.
"""
requests: list[SemanticRequest]
results: pa.Table
@dataclass(frozen=True)
class SemanticQuery:
"""
Represents a semantic query.
"""
metrics: list[Metric]
dimensions: list[Dimension]
filters: set[Filter] | None = None
order: list[OrderTuple] | None = None
limit: int | None = None
offset: int | None = None
group_limit: GroupLimit | None = None

View File

@@ -0,0 +1,113 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import enum
from abc import ABC, abstractmethod
from superset_core.semantic_layers.types import (
Dimension,
Filter,
Metric,
SemanticQuery,
SemanticResult,
)
# TODO (betodealmeida): move to the extension JSON
class SemanticViewFeature(enum.Enum):
"""
Custom features supported by semantic layers.
"""
ADHOC_EXPRESSIONS_IN_ORDERBY = "ADHOC_EXPRESSIONS_IN_ORDERBY"
GROUP_LIMIT = "GROUP_LIMIT"
GROUP_OTHERS = "GROUP_OTHERS"
class SemanticView(ABC):
"""
Abstract base class for semantic views.
"""
features: frozenset[SemanticViewFeature]
# Implementations must expose a display name for the view.
# Declared here as a type annotation (not abstract) so that existing
# implementations are not required to add a formal @abstractmethod.
name: str
@abstractmethod
def uid(self) -> str:
"""
Returns a unique identifier for the semantic view.
"""
@abstractmethod
def get_dimensions(self) -> set[Dimension]:
"""
Get the dimensions defined in the semantic view.
"""
@abstractmethod
def get_metrics(self) -> set[Metric]:
"""
Get the metrics defined in the semantic view.
"""
@abstractmethod
def get_values(
self,
dimension: Dimension,
filters: set[Filter] | None = None,
) -> SemanticResult:
"""
Return distinct values for a dimension.
"""
@abstractmethod
def get_table(self, query: SemanticQuery) -> SemanticResult:
"""
Execute a semantic query and return the results.
"""
@abstractmethod
def get_row_count(self, query: SemanticQuery) -> SemanticResult:
"""
Execute a query and return the number of rows the result would have.
"""
@abstractmethod
def get_compatible_metrics(
self,
selected_metrics: set[Metric],
selected_dimensions: set[Dimension],
) -> set[Metric]:
"""
Return metrics compatible with the selected dimensions.
"""
@abstractmethod
def get_compatible_dimensions(
self,
selected_metrics: set[Metric],
selected_dimensions: set[Dimension],
) -> set[Dimension]:
"""
Return dimensions compatible with the selected metrics.
"""

View File

@@ -17,7 +17,7 @@
[project]
name = "apache-superset-extensions-cli"
version = "0.1.0rc2"
version = "0.1.0rc3"
description = "Official command-line interface for building, bundling, and managing Apache Superset extensions"
readme = "README.md"
authors = [

File diff suppressed because it is too large Load Diff

View File

@@ -117,7 +117,14 @@
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/antd": "^5.24.13",
"@rjsf/core": "^5.24.13",
"@rjsf/utils": "^5.24.3",
"@rjsf/validator-ajv8": "^5.24.13",
@@ -183,7 +190,7 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.22.0",
"mapbox-gl": "^3.23.0",
"markdown-to-jsx": "^9.7.16",
"match-sorter": "^8.3.0",
"memoize-one": "^5.2.1",
@@ -194,13 +201,13 @@
"pretty-ms": "^9.3.0",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react": "^18.2.0",
"react-arborist": "^3.5.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.2",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^10.0.3",
"react-json-tree": "^0.20.0",
@@ -211,7 +218,6 @@
"react-reverse-portal": "^2.3.0",
"react-router-dom": "^5.3.4",
"react-search-input": "^0.11.3",
"react-sortable-hoc": "^2.0.0",
"react-split": "^2.0.9",
"react-table": "^7.8.0",
"react-transition-group": "^4.4.5",
@@ -244,14 +250,13 @@
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.29.2",
"@babel/runtime-corejs3": "^7.29.2",
"@babel/types": "^7.28.6",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
@@ -271,12 +276,11 @@
"@storybook/test-runner": "^0.17.0",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.32",
"@swc/plugin-emotion": "^14.8.0",
"@swc/plugin-emotion": "^14.9.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
@@ -286,8 +290,8 @@
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.6.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
@@ -306,7 +310,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.21",
"baseline-browser-mapping": "^2.10.24",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0",
@@ -323,7 +327,7 @@
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.3",
@@ -373,7 +377,7 @@
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.3",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.4.0",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"
},
"peerDependencies": {

View File

@@ -37,7 +37,7 @@
"cross-env": "^10.1.0",
"fs-extra": "^11.3.4",
"jest": "^30.3.0",
"yeoman-test": "^11.3.1"
"yeoman-test": "^11.4.2"
},
"engines": {
"npm": ">= 4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@apache-superset/core",
"version": "0.1.0-rc2",
"version": "0.1.0-rc3",
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
"sideEffects": false,
"main": "lib/index.js",
@@ -75,16 +75,15 @@
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"typescript": "^5.0.0",
"@emotion/styled": "^11.14.1",
"@types/lodash": "^4.17.24",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -98,8 +97,8 @@
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.6",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*",
"lodash": "^4.18.1",

View File

@@ -18,7 +18,7 @@
*/
import userEvent from '@testing-library/user-event';
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { render, RenderOptions, RenderResult } from '@testing-library/react';
import '@testing-library/jest-dom';
import { themeObject } from './theme';
@@ -33,7 +33,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => (
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>,
) => render(ui, { wrapper: Providers, ...options });
): RenderResult => render(ui, { wrapper: Providers, ...options });
export {
createEvent,

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { ThemeProvider } from '@emotion/react';
import { theme as antdTheme } from 'antd';
import {

View File

@@ -33,17 +33,16 @@
"@ant-design/icons": "^5.6.1",
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
"memoize-one": "^5.1.1",
"react": "^17.0.2",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-dom": "^17.0.2"
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -91,10 +91,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -102,8 +101,8 @@
"@types/tinycolor2": "*",
"antd": "^5.26.0",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
},

View File

@@ -94,11 +94,20 @@ class CategoricalColorScale extends ExtensibleFunction {
/**
* Increment the color range with analogous colors
*
* @param forceMinimumExpansion When true, expand at least once even if the
* ordinal domain is still shorter than the palette. Shared dashboard labels
* can resolve from the global map without entering the scale domain, so
* domain-based sizing alone would skip expansion while collision resolution
* still needs analogous colors.
*/
incrementColorRange() {
const multiple = Math.floor(
incrementColorRange(forceMinimumExpansion = false) {
const domainBasedMultiple = Math.floor(
this.domain().length / this.originColors.length,
);
const multiple = forceMinimumExpansion
? Math.max(domainBasedMultiple, 1)
: domainBasedMultiple;
// the domain has grown larger than the original range
// increments the range with analogous colors
if (multiple > this.multiple) {
@@ -144,6 +153,7 @@ class CategoricalColorScale extends ExtensibleFunction {
if (isFeatureEnabled(FeatureFlag.UseAnalogousColors)) {
this.incrementColorRange();
}
if (
// feature flag to be deprecated (will become standard behaviour)
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
@@ -154,6 +164,39 @@ class CategoricalColorScale extends ExtensibleFunction {
}
}
if (
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
source === LabelsColorMapSource.Dashboard &&
(forcedColor || isExistingLabel)
) {
const colliding = [...this.chartLabelsColorMap.entries()].filter(
([labelKey, c]) => c === color && labelKey !== cleanedValue,
);
if (
colliding.length > 0 &&
isFeatureEnabled(FeatureFlag.UseAnalogousColors)
) {
this.incrementColorRange(true);
}
for (const [otherLabel] of colliding) {
if (
Object.prototype.hasOwnProperty.call(this.forcedColors, otherLabel)
) {
continue;
}
const newColor = this.getNextAvailableColor(otherLabel, color);
this.chartLabelsColorMap.set(otherLabel, newColor);
if (sliceId) {
this.labelsColorMapInstance.addSlice(
otherLabel,
newColor,
sliceId,
appliedColorScheme,
);
}
}
}
// keep track of values in this slice
this.chartLabelsColorMap.set(cleanedValue, color);

View File

@@ -17,19 +17,15 @@
* under the License.
*/
import type { ReactElement } from 'react';
import {
Tooltip,
type TooltipPlacement,
type IconType,
} from '@superset-ui/core/components';
import type { ReactElement, ReactNode } from 'react';
import { Tooltip, type TooltipPlacement } from '@superset-ui/core/components';
import { css, useTheme } from '@apache-superset/core/theme';
export interface ActionProps {
label: string;
tooltip?: string | ReactElement;
placement?: TooltipPlacement;
icon: IconType;
icon: ReactNode;
onClick: () => void;
}

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useJsonValidation } from './useJsonValidation';
describe('useJsonValidation', () => {

View File

@@ -16,16 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
useEffect,
useState,
RefObject,
forwardRef,
ComponentType,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
} from 'react';
import React, { useEffect, useState, forwardRef, ComponentType } from 'react';
import { Loading } from '../Loading';
import type { PlaceholderProps } from './types';
@@ -93,15 +84,16 @@ export function AsyncEsmComponent<
return promise;
}
type AsyncComponent = ForwardRefExoticComponent<
PropsWithoutRef<FullProps> & RefAttributes<ComponentType<FullProps>>
type AsyncComponent = React.ForwardRefExoticComponent<
React.PropsWithoutRef<FullProps> & React.RefAttributes<unknown>
> & {
preload?: typeof waitForPromise;
};
// @ts-expect-error -- generic forwardRef has PropsWithoutRef incompatibility with FullProps
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: FullProps,
ref: RefObject<ComponentType<FullProps>>,
ref,
) {
const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => {

View File

@@ -24,7 +24,6 @@ import type {
ButtonVariantType,
ButtonColorType,
} from 'antd/es/button';
import { IconType } from '@superset-ui/core/components/Icons/types';
import type { TooltipPlacement } from '../Tooltip/types';
export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
@@ -49,5 +48,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
buttonStyle?: ButtonStyle;
cta?: boolean;
showMarginRight?: boolean;
icon?: IconType;
icon?: ReactNode;
};

View File

@@ -73,7 +73,7 @@ export const Component = (props: DropdownContainerProps) => {
const [overflowingState, setOverflowingState] = useState<OverflowingState>();
const containerRef = useRef<DropdownRef>(null);
const onOverflowingStateChange = useCallback(
value => {
(value: OverflowingState) => {
if (!isEqual(overflowingState, value)) {
setItems(generateItems(value));
setOverflowingState(value);

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
import { IconType } from '../Icons';
/**
* Container item.
@@ -70,7 +69,7 @@ export interface DropdownContainerProps {
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: IconType;
dropdownTriggerIcon?: ReactNode;
/**
* Text of the dropdown trigger.
*/

View File

@@ -0,0 +1,80 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { fireEvent, render, screen, userEvent } from '@superset-ui/core/spec';
import { useState } from 'react';
import { DynamicEditableTitle } from '.';
const Harness = ({ initialTitle = 'Original' }: { initialTitle?: string }) => {
const [title, setTitle] = useState(initialTitle);
return (
<DynamicEditableTitle
title={title}
placeholder="placeholder"
canEdit
label="Title"
onSave={setTitle}
/>
);
};
test('rapid typing then backspacing keeps every keystroke', async () => {
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'abc', { delay: 1 });
expect(input.value).toBe('Originalabc');
await userEvent.type(input, '{backspace}{backspace}{backspace}', {
delay: 1,
});
expect(input.value).toBe('Original');
});
test('a change event that arrives before isEditing flips is not dropped', () => {
// Reproduces the regression: the input is focused but `isEditing` is still
// false because no click has been registered yet (e.g. focus arrived via
// tab, autofocus, or programmatic focus). The pre-fix `handleChange`
// bailed out with `!isEditing`, dropping the keystroke. Because the
// input is controlled, antd's internal `useMergedState` then resyncs the
// DOM value back to the (stale) `props.value`, so the user sees their
// typed character disappear. This test fires a raw change event so it
// doesn't go through userEvent's implicit click.
const onSave = jest.fn();
render(
<DynamicEditableTitle
title="Foo"
placeholder="placeholder"
canEdit
label="Title"
onSave={onSave}
/>,
);
const input = screen.getByRole('textbox') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'FooX' } });
expect(input.value).toBe('FooX');
});
test('prop changes mid-edit do not clobber unsaved typing', async () => {
const { rerender } = render(<Harness initialTitle="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'X', { delay: 1 });
expect(input.value).toBe('FooX');
rerender(<Harness initialTitle="Foo" />);
expect(input.value).toBe('FooX');
});

View File

@@ -23,6 +23,7 @@ import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { t } from '@apache-superset/core/translation';
@@ -30,6 +31,7 @@ import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
import { useResizeDetector } from 'react-resize-detector';
import { Tooltip } from '../Tooltip';
import { Input } from '../Input';
import type { InputRef } from '../Input';
import type { DynamicEditableTitleProps } from './types';
const titleStyles = (theme: SupersetTheme) => css`
@@ -75,8 +77,10 @@ export const DynamicEditableTitle = memo(
const [isEditing, setIsEditing] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [currentTitle, setCurrentTitle] = useState(title || '');
const [inputWidth, setInputWidth] = useState<number>(0);
const { width: inputWidth, ref: sizerRef } = useResizeDetector();
const sizerRef = useRef<HTMLSpanElement>(null);
const inputRef = useRef<InputRef>(null);
const { width: containerWidth, ref: containerRef } = useResizeDetector({
refreshMode: 'debounce',
});
@@ -85,27 +89,33 @@ export const DynamicEditableTitle = memo(
setCurrentTitle(title);
}, [title]);
useEffect(() => {
if (isEditing && sizerRef?.current) {
if (isEditing) {
// move cursor and scroll to the end
if (sizerRef.current.setSelectionRange) {
const { length } = sizerRef.current.value;
sizerRef.current.setSelectionRange(length, length);
sizerRef.current.scrollLeft = sizerRef.current.scrollWidth;
const inputElement = inputRef.current?.input;
if (inputElement) {
const { length } = inputElement.value;
inputElement.setSelectionRange(length, length);
inputElement.scrollLeft = inputElement.scrollWidth;
}
}
}, [isEditing]);
// a trick to make the input grow when user types text
// we make additional span component, place it somewhere out of view and copy input
// then we can measure the width of that span to resize the input element
// we make an additional span component, place it somewhere out of view and
// mirror the input value, then measure the span synchronously (pre-paint)
// to resize the input element. Reading offsetWidth in a useLayoutEffect
// forces a sync layout, so the input width updates in the same commit as
// the value change — preventing a flicker frame where the input is shown
// with new value but stale width.
useLayoutEffect(() => {
if (sizerRef?.current) {
if (sizerRef.current) {
sizerRef.current.textContent = currentTitle || placeholder;
setInputWidth(sizerRef.current.offsetWidth);
}
}, [currentTitle, placeholder, sizerRef]);
}, [currentTitle, placeholder]);
useEffect(() => {
const inputElement = sizerRef.current?.input;
const inputElement = inputRef.current?.input;
if (inputElement) {
if (inputElement.scrollWidth > inputElement.clientWidth) {
@@ -137,9 +147,17 @@ export const DynamicEditableTitle = memo(
const handleChange = useCallback(
(ev: ChangeEvent<HTMLInputElement>) => {
if (!canEdit || !isEditing) {
if (!canEdit) {
return;
}
// Any change implies the user is editing. Ensure isEditing is true
// even if the change event arrives before the click handler has
// committed (e.g. focus via tab, autofocus, or batched click+type
// events). Otherwise the keystroke would be dropped and the
// controlled input would revert to the previous value.
if (!isEditing) {
setIsEditing(true);
}
setCurrentTitle(ev.target.value);
},
[canEdit, isEditing],
@@ -168,6 +186,7 @@ export const DynamicEditableTitle = memo(
}
>
<Input
ref={inputRef}
data-test="editable-title-input"
variant="borderless"
aria-label={label ?? t('Title')}

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { ReactNode, SyntheticEvent } from 'react';
import type { IconType } from '@superset-ui/core/components';
export type EmptyStateSize = 'small' | 'medium' | 'large';
@@ -26,7 +25,7 @@ export type EmptyStateProps = {
description?: ReactNode;
image?: ReactNode | string;
buttonText?: ReactNode;
buttonIcon?: IconType;
buttonIcon?: ReactNode;
buttonAction?: (event: SyntheticEvent) => void;
/** Controls image size. Defaults to 'medium'. */
size?: EmptyStateSize;

View File

@@ -20,7 +20,7 @@ import { Form as AntdForm } from 'antd';
import { FormProps } from './types';
function CustomForm(props: FormProps) {
return <AntdForm {...props} />;
return <AntdForm {...(props as any)} />;
}
export const Form = Object.assign(CustomForm, {

View File

@@ -41,7 +41,6 @@ test('renders with monospace prop', () => {
// test stories from the storybook!
test('renders all the storybook gallery variants', () => {
// @ts-expect-error: Suppress TypeScript error for LabelGallery usage
const { container } = render(<LabelGallery />);
const nonInteractiveLabelCount = 4;
const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount;

View File

@@ -23,7 +23,7 @@ import { Label } from '..';
// Define the prop types for DatasetTypeLabel
interface DatasetTypeLabelProps {
datasetType: 'physical' | 'virtual'; // Accepts only 'physical' or 'virtual'
datasetType: 'physical' | 'virtual' | 'semantic_view';
}
const SIZE = 's'; // Define the size as a constant
@@ -32,6 +32,22 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
datasetType,
}) => {
const theme = useTheme();
if (datasetType === 'semantic_view') {
return (
<Label
icon={
<Icons.ApartmentOutlined
iconSize={SIZE}
iconColor={theme.colorInfo}
/>
}
type="info"
style={{ color: theme.colorInfo }}
>
{t('Semantic')}
</Label>
);
}
const isPhysical = datasetType === 'physical';
const label: string = isPhysical ? t('Physical') : t('Virtual');
const labelType = isPhysical ? 'primary' : 'default';

View File

@@ -21,6 +21,7 @@ import type { BackgroundPosition } from './ImageLoader';
export interface LinkProps {
to: string;
children?: ReactNode;
}
export interface ListViewCardProps {

View File

@@ -194,7 +194,7 @@ const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
}
const onResize = useCallback(
width => {
(width: number | undefined) => {
// Calculates the breakpoint width to collapse the bar.
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
const breakpoint =

View File

@@ -54,7 +54,7 @@ export function FormModal({
}, [onSave, resetForm]);
const handleFormSubmit = useCallback(
async values => {
async (values: object) => {
try {
setIsSaving(true);
await formSubmitHandler(values);

View File

@@ -104,6 +104,9 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
right: 0;
display: flex;
justify-content: center;
// Keep the close button clickable when modal body content uses
// position: sticky with elevated z-index (e.g. DatabaseModal header).
z-index: ${theme.zIndexPopupBase + 1};
}
.ant-modal-close:hover {

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import type { CSSProperties, ReactNode } from 'react';
import type { ModalFuncProps } from 'antd';
import type { FormInstance, ModalFuncProps } from 'antd';
import type { ResizableProps } from 're-resizable';
import type { DraggableProps } from 'react-draggable';
import { ButtonStyle } from '../Button/types';
@@ -68,7 +68,8 @@ export interface StyledModalProps {
export type { ModalFuncProps };
export interface FormModalProps extends ModalProps {
export interface FormModalProps extends Omit<ModalProps, 'children'> {
children: ReactNode | ((form: FormInstance) => ReactNode);
initialValues?: object;
formSubmitHandler: (values: object) => Promise<void>;
onSave: () => void;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, ReactElement } from 'react';
import { ReactNode, ReactElement, memo } from 'react';
import { t } from '@apache-superset/core/translation';
import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
@@ -118,62 +118,64 @@ export type PageHeaderWithActionsProps = {
};
};
export const PageHeaderWithActions = ({
editableTitleProps,
showTitlePanelItems,
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
showMenuDropdown = true,
tooltipProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
<div css={headerStyles} className="header-with-actions">
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
<div css={buttonsStyles}>
{certificatiedBadgeProps?.certifiedBy && (
<CertifiedBadge {...certificatiedBadgeProps} />
)}
{showFaveStar && <FaveStar {...faveStarProps} />}
{titlePanelAdditionalItems}
</div>
)}
</div>
<div className="right-button-panel">
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
{showMenuDropdown && (
<Dropdown
trigger={['click']}
popupRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<span>
<Button
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
tooltip={tooltipProps?.text}
placement={tooltipProps?.placement}
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colorPrimary}
iconSize="l"
/>
</Button>
</span>
</Dropdown>
export const PageHeaderWithActions = memo(
({
editableTitleProps,
showTitlePanelItems,
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
showMenuDropdown = true,
tooltipProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
<div css={headerStyles} className="header-with-actions">
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
<div css={buttonsStyles}>
{certificatiedBadgeProps?.certifiedBy && (
<CertifiedBadge {...certificatiedBadgeProps} />
)}
{showFaveStar && <FaveStar {...faveStarProps} />}
{titlePanelAdditionalItems}
</div>
)}
</div>
<div className="right-button-panel">
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
{showMenuDropdown && (
<Dropdown
trigger={['click']}
popupRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<span>
<Button
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
tooltip={tooltipProps?.text}
placement={tooltipProps?.placement}
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colorPrimary}
iconSize="l"
/>
</Button>
</span>
</Dropdown>
)}
</div>
</div>
</div>
</div>
);
};
);
},
);

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { render, screen, fireEvent } from '@superset-ui/core/spec';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { TableInstance, useTable } from 'react-table';
import TableCollection from '.';

View File

@@ -91,7 +91,7 @@ export function mapColumns<T extends object>(
return columns.map(column => {
const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id);
return {
title: column.Header,
title: column.Header as ReactNode,
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
hidden: column.hidden,
key: column.id,
@@ -121,7 +121,7 @@ export function mapColumns<T extends object>(
column,
});
}
return val;
return val as ReactNode;
},
className: column.className,
};

View File

@@ -19,6 +19,14 @@
import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
import { TableView, TableViewProps } from '.';
// Mock window.scrollTo to prevent jsdom "Not implemented" errors
beforeAll(() => {
window.scrollTo = jest.fn();
});
afterAll(() => {
jest.restoreAllMocks();
});
const mockedProps: TableViewProps = {
columns: [
{
@@ -125,27 +133,25 @@ test('should change page when pagination is clicked', async () => {
expect(screen.getByText('Emily')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('321')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('Kate')).toBeInTheDocument();
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('321')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
const page1 = screen.getByRole('listitem', { name: '1' });
await userEvent.click(page1);
await userEvent.click(screen.getByTitle('Previous Page'));
await waitFor(() => {
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('27')).toBeInTheDocument();
expect(screen.getByText('Emily')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('27')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
});
test('should sort by age', async () => {
@@ -240,8 +246,7 @@ test('should handle server-side pagination', async () => {
render(<TableView {...serverPaginationProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(onServerPagination).toHaveBeenCalledWith({
@@ -301,9 +306,7 @@ test('should scroll to top when scrollTopOnPagination is true', async () => {
};
render(<TableView {...scrollProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
@@ -324,9 +327,7 @@ test('should NOT scroll to top when scrollTopOnPagination is false', async () =>
};
render(<TableView {...scrollProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(screen.getByText('321')).toBeInTheDocument();

View File

@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { memo, useEffect, useRef, useMemo, useCallback } from 'react';
import { memo, useEffect, useRef, useMemo, useCallback, useState } from 'react';
import { isEqual } from 'lodash';
import { styled } from '@apache-superset/core/theme';
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
import { useFilters, useSortBy, useTable } from 'react-table';
import { Empty } from '@superset-ui/core/components';
import TableCollection from '@superset-ui/core/components/TableCollection';
import { TableSize } from '@superset-ui/core/components/Table';
@@ -117,43 +117,45 @@ const RawTableView = ({
...props
}: TableViewProps) => {
const tableRef = useRef<HTMLTableElement>(null);
const effectivePageSize = initialPageSize ?? DEFAULT_PAGE_SIZE;
const [pageIndex, setPageIndex] = useState(initialPageIndex ?? 0);
const initialState = useMemo(
() => ({
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageIndex: initialPageIndex ?? 0,
pageSize: effectivePageSize,
pageIndex: 0,
sortBy: initialSortBy,
}),
[initialPageSize, initialPageIndex, initialSortBy],
[effectivePageSize, initialSortBy],
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
rows,
prepareRow,
gotoPage,
setSortBy,
state: { pageIndex, sortBy },
state: { sortBy },
} = useTable(
{
columns,
data,
initialState,
manualPagination: serverPagination,
manualPagination: true,
manualSortBy: serverPagination,
pageCount: serverPagination
? Math.ceil(totalCount / initialState.pageSize)
: undefined,
autoResetSortBy: false,
},
useFilters,
useSortBy,
...(withPagination ? [usePagination] : []),
);
const content = useMemo(() => {
if (!withPagination || serverPagination) return rows;
const start = pageIndex * effectivePageSize;
return rows.slice(start, start + effectivePageSize);
}, [withPagination, serverPagination, rows, pageIndex, effectivePageSize]);
const EmptyWrapperComponent = useMemo(() => {
switch (emptyWrapperType) {
case EmptyWrapperType.Small:
@@ -164,11 +166,6 @@ const RawTableView = ({
}
}, [emptyWrapperType]);
const content = useMemo(
() => (withPagination ? page : rows),
[withPagination, page, rows],
);
const isEmpty = useMemo(
() => !loading && content.length === 0,
[loading, content.length],
@@ -192,10 +189,9 @@ const RawTableView = ({
const handlePageChange = useCallback(
(p: number) => {
if (scrollTopOnPagination) handleScrollToTop();
gotoPage(p);
setPageIndex(p);
},
[scrollTopOnPagination, handleScrollToTop, gotoPage],
[scrollTopOnPagination, handleScrollToTop],
);
const paginationProps = useMemo(() => {
@@ -211,7 +207,7 @@ const RawTableView = ({
if (serverPagination) {
return {
pageIndex,
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageSize: effectivePageSize,
totalCount,
onPageChange: handlePageChange,
};
@@ -219,7 +215,7 @@ const RawTableView = ({
return {
pageIndex,
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageSize: effectivePageSize,
totalCount: data.length,
onPageChange: handlePageChange,
};
@@ -227,28 +223,28 @@ const RawTableView = ({
withPagination,
serverPagination,
pageIndex,
initialPageSize,
effectivePageSize,
totalCount,
data.length,
handlePageChange,
]);
useEffect(() => {
if (serverPagination && pageIndex !== initialState.pageIndex) {
if (serverPagination && pageIndex !== (initialPageIndex ?? 0)) {
onServerPagination({
pageIndex,
});
}
}, [initialState.pageIndex, onServerPagination, pageIndex, serverPagination]);
}, [initialPageIndex, onServerPagination, pageIndex, serverPagination]);
useEffect(() => {
if (serverPagination && !isEqual(sortBy, initialState.sortBy)) {
if (serverPagination && !isEqual(sortBy, initialSortBy)) {
onServerPagination({
pageIndex: 0,
sortBy,
});
}
}, [initialState.sortBy, onServerPagination, serverPagination, sortBy]);
}, [initialSortBy, onServerPagination, serverPagination, sortBy]);
return (
<TableViewStyles {...props} ref={tableRef}>

View File

@@ -97,8 +97,8 @@ const StyledPlus = styled.span`
export default function TruncatedList<ListItemType>({
items,
renderVisibleItem = item => item,
renderTooltipItem = item => item,
renderVisibleItem = item => item as ReactNode,
renderTooltipItem = item => item as ReactNode,
getKey = item => item as unknown as Key,
maxLinks = 20,
}: TruncatedListProps<ListItemType>) {

View File

@@ -51,6 +51,7 @@ const SupersetClient: SupersetClientInterface = {
reAuthenticate: () => getInstance().reAuthenticate(),
request: request => getInstance().request(request),
getCSRFToken: () => getInstance().getCSRFToken(),
getUrl: (...args) => getInstance().getUrl(...args),
};
export default SupersetClient;

View File

@@ -158,6 +158,7 @@ export interface SupersetClientInterface extends Pick<
| 'isAuthenticated'
| 'reAuthenticate'
| 'getGuestToken'
| 'getUrl'
> {
configure: (config?: ClientConfig) => SupersetClientInterface;
reset: () => void;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useChangeEffect } from './useChangeEffect';
test('call callback the first time with undefined and value', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useComponentDidMount } from './useComponentDidMount';
test('the effect should only be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useComponentDidUpdate } from './useComponentDidUpdate';
test('the effect should not be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { useElementOnScreen } from './useElementOnScreen';
const observeMock = jest.fn();
@@ -46,10 +46,9 @@ test('should return isSticky as true when intersectionRatio < 1', async () => {
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
);
const callback = IntersectionObserverMock.mock.calls[0][0];
const callBack = callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
const observer = new IntersectionObserverMock(callBack, {});
const newDiv = document.createElement('div');
observer.observe(newDiv);
act(() => {
callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
});
expect(hook.result.current[1]).toEqual(true);
});
@@ -58,10 +57,9 @@ test('should return isSticky as false when intersectionRatio >= 1', async () =>
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
);
const callback = IntersectionObserverMock.mock.calls[0][0];
const callBack = callback([{ isIntersecting: true, intersectionRatio: 1 }]);
const observer = new IntersectionObserverMock(callBack, {});
const newDiv = document.createElement('div');
observer.observe(newDiv);
act(() => {
callback([{ isIntersecting: true, intersectionRatio: 1 }]);
});
expect(hook.result.current[1]).toEqual(false);
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { usePrevious } from './usePrevious';
test('get undefined on the first render when initialValue is not defined', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import useCSSTextTruncation from './useCSSTextTruncation';
afterEach(() => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { RefObject } from 'react';
import useChildElementTruncation from './useChildElementTruncation';

View File

@@ -19,6 +19,15 @@
import { DatasourceType } from './types/Datasource';
const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = {
table: DatasourceType.Table,
query: DatasourceType.Query,
dataset: DatasourceType.Dataset,
sl_table: DatasourceType.SlTable,
saved_query: DatasourceType.SavedQuery,
semantic_view: DatasourceType.SemanticView,
};
export default class DatasourceKey {
readonly id: number;
@@ -27,8 +36,7 @@ export default class DatasourceKey {
constructor(key: string) {
const [idStr, typeStr] = key.split('__');
this.id = parseInt(idStr, 10);
this.type = DatasourceType.Table; // default to SqlaTable model
this.type = typeStr === 'query' ? DatasourceType.Query : this.type;
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
}
public toString() {

View File

@@ -26,6 +26,7 @@ export enum DatasourceType {
Dataset = 'dataset',
SlTable = 'sl_table',
SavedQuery = 'saved_query',
SemanticView = 'semantic_view',
}
export interface Currency {
@@ -40,6 +41,13 @@ export interface Datasource {
id: number;
name: string;
type: DatasourceType;
/**
* The parent resource that owns this datasource.
* For SQL-based datasets this is the database; for semantic views it is the
* semantic layer. Use this field instead of the legacy `database` field when
* you only need the display name.
*/
parent?: { name: string };
columns: Column[];
metrics: Metric[];
description?: string;

View File

@@ -28,7 +28,7 @@ export const PREVIEW_TIME = new Date(Date.UTC(2017, 1, 14, 11, 22, 33));
// Use type augmentation to indicate that
// an instance of TimeFormatter is also a function
interface TimeFormatter {
(value: Date | number | null | undefined): string;
(value: Date | number | string | null | undefined): string;
}
class TimeFormatter extends ExtensibleFunction {
@@ -49,7 +49,9 @@ class TimeFormatter extends ExtensibleFunction {
formatFunc: TimeFormatFunction;
useLocalTime?: boolean;
}) {
super((value: Date | number | null | undefined) => this.format(value));
super((value: Date | number | string | null | undefined) =>
this.format(value),
);
const {
id = isRequired('config.id'),
@@ -66,7 +68,7 @@ class TimeFormatter extends ExtensibleFunction {
this.useLocalTime = useLocalTime;
}
format(value: Date | number | null | undefined) {
format(value: Date | number | string | null | undefined) {
return stringifyTimeInput(value, time => this.formatFunc(time));
}

View File

@@ -18,12 +18,18 @@
*/
export default function stringifyTimeInput(
value: Date | number | undefined | null,
value: Date | number | string | undefined | null,
fn: (time: Date) => string,
) {
if (value === null || value === undefined) {
return `${value}`;
}
if (typeof value === 'string') {
const trimmed = value.trim();
const isIntegerString = /^-?\d+$/.test(trimmed);
return fn(new Date(isIntegerString ? Number(trimmed) : value));
}
return fn(value instanceof Date ? value : new Date(value));
}

View File

@@ -249,7 +249,8 @@ export type Extensions = Partial<{
'navbar.right-menu.item.icon': ComponentType<RightMenuItemIconProps>;
'navbar.right': ComponentType;
'report-modal.dropdown.item.icon': ComponentType;
'root.context.provider': ComponentType;
'root.context.provider': ComponentType<{ children?: ReactNode }>;
'welcome.message': ComponentType;
'welcome.banner': ComponentType;
'welcome.main.replacement': ComponentType;

View File

@@ -61,6 +61,7 @@ export enum FeatureFlag {
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
Matrixify = 'MATRIXIFY',
ScheduledQueries = 'SCHEDULED_QUERIES',
SemanticLayers = 'SEMANTIC_LAYERS',
SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
SshTunneling = 'SSH_TUNNELING',

View File

@@ -143,7 +143,7 @@ describe('SuperChart', () => {
);
expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument();
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
expect(CustomFallbackComponent).toHaveBeenCalled();
});
test('call onErrorBoundary', async () => {
expectedErrors = 1;

View File

@@ -21,6 +21,7 @@ import { ScaleOrdinal } from 'd3-scale';
import {
CategoricalColorScale,
FeatureFlag,
getLabelsColorMap,
LabelsColorMapSource,
} from '@superset-ui/core';
@@ -199,10 +200,42 @@ describe('CategoricalColorScale', () => {
const returnedColor = scale.getColor(value, sliceId);
expect(returnedColor).toBe(expectedColor);
});
test('reassigns colliding colors when no sliceId is provided', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
const labelsColorMap = chartAScale.labelsColorMapInstance;
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
try {
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE);
// Call getColor without sliceId (or with undefined)
chartBScale.getColor('Classic Cars', undefined, 'testScheme');
chartBScale.getColor('Trains', undefined, 'testScheme');
const classicCarsColor =
chartBScale.chartLabelsColorMap.get('Classic Cars');
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
expect(trainsColor).toBe('red');
expect(classicCarsColor).toBeDefined();
expect(classicCarsColor).not.toBe('red');
} finally {
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
}
});
test('conditionally calls getNextAvailableColor', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
scale.getColor('testValue1');
scale.getColor('testValue2');
@@ -225,6 +258,27 @@ describe('CategoricalColorScale', () => {
expect(getNextAvailableColorSpy).not.toHaveBeenCalled();
});
test('reassigns non-forced labels when a dashboard-synced label would duplicate their color', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const dashScale = new CategoricalColorScale(['red', 'blue', 'green']);
const sliceId = 501;
const colorScheme = 'preset';
dashScale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
jest
.spyOn(dashScale.labelsColorMapInstance, 'getColorMap')
.mockReturnValue(new Map([['Trains', 'red']]));
dashScale.getColor('Classic Cars', sliceId, colorScheme);
dashScale.getColor('Trains', sliceId, colorScheme);
expect(dashScale.chartLabelsColorMap.get('Trains')).toBe('red');
expect(dashScale.chartLabelsColorMap.get('Classic Cars')).not.toBe('red');
expect(dashScale.chartLabelsColorMap.get('Classic Cars')).toBeDefined();
});
});
describe('.setColor(value, forcedColor)', () => {
@@ -479,6 +533,131 @@ describe('CategoricalColorScale', () => {
});
});
describe('dashboard shared-dimension color collision', () => {
let labelsColorMap: ReturnType<typeof getLabelsColorMap>;
beforeEach(() => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const sentinel = new CategoricalColorScale(['red', 'blue', 'green']);
labelsColorMap = sentinel.labelsColorMapInstance;
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
});
afterEach(() => {
jest.restoreAllMocks();
labelsColorMap.reset();
});
test('reproduces the bug without the fix: Classic Cars and Trains would both be red', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: false,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
expect(labelsColorMap.getColorMap().get('Trains')).toBe('red');
const chartBScale = new CategoricalColorScale(PALETTE);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
const classicCarsColor =
chartBScale.chartLabelsColorMap.get('Classic Cars');
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
expect(trainsColor).toBe('red');
expect(classicCarsColor).toBe('red');
});
test('fix: Classic Cars is reassigned when Trains locks red from the dashboard', () => {
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
expect(labelsColorMap.getColorMap().get('Trains')).toBe('red');
const chartBScale = new CategoricalColorScale(PALETTE);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
const classicCarsColor =
chartBScale.chartLabelsColorMap.get('Classic Cars');
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
expect(trainsColor).toBe('red');
expect(classicCarsColor).toBeDefined();
expect(classicCarsColor).not.toBe('red');
});
test('fix: no series in Chart B share a color when palette has enough colors', () => {
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
const colors = Array.from(chartBScale.chartLabelsColorMap.values());
const uniqueColors = new Set(colors);
expect(uniqueColors.size).toBe(colors.length);
});
test('fix: increments analogous color range for dashboard collisions when UseAnalogousColors is enabled', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
[FeatureFlag.UseAnalogousColors]: true,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE);
const addSliceSpy = jest.spyOn(
chartBScale.labelsColorMapInstance,
'addSlice',
);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Model T', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
expect(chartBScale.chartLabelsColorMap.get('Trains')).toBe('red');
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).toBeDefined();
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).not.toBe(
'red',
);
expect(chartBScale.range()).toHaveLength(6);
expect(
addSliceSpy.mock.calls.some(
([label, color]) => label === 'Classic Cars' && color !== 'red',
),
).toBe(true);
});
test('fix: forced colors (user-set in dashboard JSON) are never reassigned', () => {
const PALETTE = ['red', 'blue', 'green'];
const forcedColors = { 'Classic Cars': 'red' };
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE, forcedColors);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).toBe('red');
});
});
describe("is compatible with D3's ScaleOrdinal", () => {
test('passes type check', () => {
const scale: ScaleOrdinal<{ toString(): string }, string> =

View File

@@ -19,6 +19,7 @@
import fetchMock from 'fetch-mock';
import { SupersetClient, SupersetClientClass } from '@superset-ui/core';
import type { SupersetClientInterface } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
beforeAll(() => fetchMock.mockGlobal());
@@ -31,33 +32,46 @@ describe('SupersetClient', () => {
afterEach(() => SupersetClient.reset());
test('exposes reset, configure, init, get, post, postForm, isAuthenticated, and reAuthenticate methods', () => {
const clientWithGetUrl = SupersetClient as SupersetClientInterface & {
getUrl: (...args: unknown[]) => string;
};
test('exposes configure, init, get, post, postForm, delete, put, request, reset, getGuestToken, getCSRFToken, getUrl, isAuthenticated, and reAuthenticate methods', () => {
expect(typeof SupersetClient.configure).toBe('function');
expect(typeof SupersetClient.init).toBe('function');
expect(typeof SupersetClient.get).toBe('function');
expect(typeof SupersetClient.post).toBe('function');
expect(typeof SupersetClient.postForm).toBe('function');
expect(typeof SupersetClient.isAuthenticated).toBe('function');
expect(typeof SupersetClient.reAuthenticate).toBe('function');
expect(typeof SupersetClient.getGuestToken).toBe('function');
expect(typeof SupersetClient.delete).toBe('function');
expect(typeof SupersetClient.put).toBe('function');
expect(typeof SupersetClient.request).toBe('function');
expect(typeof SupersetClient.reset).toBe('function');
expect(typeof SupersetClient.getGuestToken).toBe('function');
expect(typeof SupersetClient.getCSRFToken).toBe('function');
expect(typeof clientWithGetUrl.getUrl).toBe('function');
expect(typeof SupersetClient.isAuthenticated).toBe('function');
expect(typeof SupersetClient.reAuthenticate).toBe('function');
});
test('throws if you call init, get, post, postForm, isAuthenticated, or reAuthenticate before configure', () => {
test('throws if you call init, get, post, postForm, delete, put, request, getGuestToken, getCSRFToken, getUrl, isAuthenticated, or reAuthenticate before configure', () => {
expect(SupersetClient.init).toThrow();
expect(SupersetClient.get).toThrow();
expect(SupersetClient.post).toThrow();
expect(SupersetClient.postForm).toThrow();
expect(SupersetClient.delete).toThrow();
expect(SupersetClient.put).toThrow();
expect(SupersetClient.request).toThrow();
expect(SupersetClient.getGuestToken).toThrow();
expect(SupersetClient.getCSRFToken).toThrow();
expect(clientWithGetUrl.getUrl).toThrow();
expect(SupersetClient.isAuthenticated).toThrow();
expect(SupersetClient.reAuthenticate).toThrow();
expect(SupersetClient.request).toThrow();
expect(SupersetClient.configure).not.toThrow();
});
// this also tests that the ^above doesn't throw if configure is called appropriately
test('calls appropriate SupersetClient methods when configured', async () => {
expect.assertions(16);
expect.assertions(18);
const mockGetUrl = '/mock/get/url';
const mockPostUrl = '/mock/post/url';
const mockRequestUrl = '/mock/request/url';
@@ -88,6 +102,13 @@ describe('SupersetClient', () => {
SupersetClientClass.prototype,
'getGuestToken',
);
const getUrlSpy = jest.spyOn(SupersetClientClass.prototype, 'getUrl');
SupersetClient.configure({ appRoot: '/app' });
expect(clientWithGetUrl.getUrl({ endpoint: '/some/path' })).toContain(
'/app/some/path',
);
expect(getUrlSpy).toHaveBeenCalledTimes(1);
SupersetClient.configure({});
await SupersetClient.init();
@@ -141,6 +162,7 @@ describe('SupersetClient', () => {
postSpy.mockRestore();
authenticatedSpy.mockRestore();
csrfSpy.mockRestore();
getUrlSpy.mockRestore();
fetchMock.clearHistory().removeRoutes();
});

View File

@@ -28,10 +28,11 @@ test('DEFAULT_METRICS', () => {
});
test('DatasourceType', () => {
expect(Object.keys(DatasourceType).length).toBe(5);
expect(Object.keys(DatasourceType).length).toBe(6);
expect(DatasourceType.Table).toBe('table');
expect(DatasourceType.Query).toBe('query');
expect(DatasourceType.Dataset).toBe('dataset');
expect(DatasourceType.SlTable).toBe('sl_table');
expect(DatasourceType.SavedQuery).toBe('saved_query');
expect(DatasourceType.SemanticView).toBe('semantic_view');
});

View File

@@ -67,6 +67,21 @@ describe('TimeFormatter', () => {
test('handles number, treating it as a timestamp', () => {
expect(formatter.format(PREVIEW_TIME.getTime())).toEqual('2017');
});
test('handles numeric string, treating it as a timestamp', () => {
// PivotData.processRecord coerces values with String(), turning numeric
// timestamps into strings.
const timestamp = PREVIEW_TIME.getTime().toString();
expect(formatter.format(timestamp as unknown as number | Date)).toEqual(
'2017',
);
});
test('handles ISO-8601 string without misinterpreting it as a number', () => {
expect(
formatter.format(
'2017-02-14T11:22:33.000Z' as unknown as number | Date,
),
).toEqual('2017');
});
test('otherwise returns formatted value', () => {
expect(formatter.format(PREVIEW_TIME)).toEqual('2017');
});

View File

@@ -39,6 +39,24 @@ export class ImportDatasetModal extends Modal {
.setInputFiles(filePath);
}
/**
* Upload a file buffer to the import modal (no temp file needed)
* @param buffer - File contents as a Buffer
* @param fileName - Name to use for the uploaded file
*/
async uploadFileBuffer(
buffer: Buffer,
fileName: string = 'dataset_export.zip',
): Promise<void> {
await this.page
.locator(ImportDatasetModal.SELECTORS.FILE_INPUT)
.setInputFiles({
name: fileName,
mimeType: 'application/zip',
buffer,
});
}
/**
* Fill the overwrite confirmation input (only needed if dataset exists)
*/

View File

@@ -17,9 +17,10 @@
* under the License.
*/
import type { Response, APIResponse } from '@playwright/test';
import type { Page, Response, APIResponse } from '@playwright/test';
import { expect } from '@playwright/test';
import * as unzipper from 'unzipper';
import { apiGet } from './requests';
/**
* Common interface for response types with status() method.
@@ -61,6 +62,35 @@ export function expectStatusOneOf<T extends ResponseLike>(
return response;
}
/**
* Poll an API endpoint until it returns 404, confirming a resource was deleted.
* Shared across chart, dashboard, and dataset list tests.
* @param page - Playwright page instance (provides authentication context)
* @param endpoint - API endpoint path (e.g. 'api/v1/dataset/')
* @param id - Resource ID to check
* @param options - Optional timeout (default 10000ms) and label for error messages
*/
export async function expectDeleted(
page: Page,
endpoint: string,
id: number,
options?: { timeout?: number; label?: string },
): Promise<void> {
const timeout = options?.timeout ?? 10000;
const label = options?.label ?? `Resource ${id}`;
await expect
.poll(
async () => {
const response = await apiGet(page, `${endpoint}${id}`, {
failOnStatusCode: false,
});
return response.status();
},
{ timeout, message: `${label} should return 404 after delete` },
)
.toBe(404);
}
/**
* Extract the resource ID from a JSON response body.
* Handles both `{ result: { id } }` and `{ id }` shapes.

View File

@@ -203,6 +203,21 @@ export async function apiDeleteDataset(
return apiDelete(page, `${ENDPOINTS.DATASET}${datasetId}`, options);
}
/**
* Export datasets as a zip file via the API.
* Uses Rison encoding for the query parameter (required by the endpoint).
* @param page - Playwright page instance (provides authentication context)
* @param datasetIds - Array of dataset IDs to export
* @returns API response containing the export zip
*/
export async function apiExportDatasets(
page: Page,
datasetIds: number[],
): Promise<APIResponse> {
const query = rison.encode(datasetIds);
return apiGet(page, `${ENDPOINTS.DATASET_EXPORT}?q=${query}`);
}
/**
* Duplicate a dataset via the API
* @param page - Playwright page instance (provides authentication context)

View File

@@ -0,0 +1,53 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Page, APIResponse } from '@playwright/test';
import { apiPost, ApiRequestOptions } from './requests';
const ENDPOINTS = {
SQLLAB_EXECUTE: 'api/v1/sqllab/execute/',
} as const;
/**
* Execute a SQL query via SQL Lab API.
* Requires `allow_dml=True` on the target database for DDL/DML statements.
* @param page - Playwright page instance (provides authentication context)
* @param databaseId - ID of the database to execute against
* @param sql - SQL statement to execute
* @param schema - Optional schema context for the query
* @returns API response from SQL Lab execution
*/
export async function apiExecuteSql(
page: Page,
databaseId: number,
sql: string,
schema?: string,
options?: ApiRequestOptions,
): Promise<APIResponse> {
return apiPost(
page,
ENDPOINTS.SQLLAB_EXECUTE,
{
database_id: databaseId,
sql,
schema: schema ?? null,
},
options,
);
}

View File

@@ -32,7 +32,7 @@ export class CreateDatasetPage {
*/
private static readonly SELECTORS = {
DATABASE: '[data-test="select-database"]',
SCHEMA: '[data-test="Select schema or type to search schemas"]',
SCHEMA: '[data-test="Select schema"]',
TABLE: '[data-test="Select table or type to search tables"]',
} as const;

View File

@@ -28,6 +28,7 @@ import { apiGetChart, ENDPOINTS } from '../../helpers/api/chart';
import { createTestChart } from './chart-test-helpers';
import { waitForGet, waitForPut } from '../../helpers/api/intercepts';
import {
expectDeleted,
expectStatusOneOf,
expectValidExportZip,
} from '../../helpers/api/assertions';
@@ -88,17 +89,9 @@ test('should delete a chart with confirmation', async ({
await expect(chartListPage.getChartRow(chartName)).not.toBeVisible();
// Backend verification: API returns 404
await expect
.poll(
async () => {
const response = await apiGetChart(page, chartId, {
failOnStatusCode: false,
});
return response.status();
},
{ timeout: 10000, message: `Chart ${chartId} should return 404` },
)
.toBe(404);
await expectDeleted(page, ENDPOINTS.CHART, chartId, {
label: `Chart ${chartId}`,
});
});
test('should edit chart name via properties modal', async ({
@@ -246,17 +239,9 @@ test('should bulk delete multiple charts', async ({
// Backend verification: Both return 404
for (const chart of [chart1, chart2]) {
await expect
.poll(
async () => {
const response = await apiGetChart(page, chart.id, {
failOnStatusCode: false,
});
return response.status();
},
{ timeout: 10000, message: `Chart ${chart.id} should return 404` },
)
.toBe(404);
await expectDeleted(page, ENDPOINTS.CHART, chart.id, {
label: `Chart ${chart.id}`,
});
}
});

View File

@@ -25,7 +25,6 @@ import {
} from '../../components/modals';
import { Toast } from '../../components/core';
import {
apiGetDashboard,
apiDeleteDashboard,
apiExportDashboards,
getDashboardByName,
@@ -34,6 +33,7 @@ import {
import { createTestDashboard } from './dashboard-test-helpers';
import { waitForGet, waitForPost } from '../../helpers/api/intercepts';
import {
expectDeleted,
expectStatusOneOf,
expectValidExportZip,
} from '../../helpers/api/assertions';
@@ -97,17 +97,9 @@ test('should delete a dashboard with confirmation', async ({
).not.toBeVisible();
// Backend verification: API returns 404
await expect
.poll(
async () => {
const response = await apiGetDashboard(page, dashboardId, {
failOnStatusCode: false,
});
return response.status();
},
{ timeout: 10000, message: `Dashboard ${dashboardId} should return 404` },
)
.toBe(404);
await expectDeleted(page, ENDPOINTS.DASHBOARD, dashboardId, {
label: `Dashboard ${dashboardId}`,
});
});
test('should export a dashboard as a zip file', async ({
@@ -210,20 +202,9 @@ test('should bulk delete multiple dashboards', async ({
// Backend verification: Both return 404
for (const dashboard of [dashboard1, dashboard2]) {
await expect
.poll(
async () => {
const response = await apiGetDashboard(page, dashboard.id, {
failOnStatusCode: false,
});
return response.status();
},
{
timeout: 10000,
message: `Dashboard ${dashboard.id} should return 404`,
},
)
.toBe(404);
await expectDeleted(page, ENDPOINTS.DASHBOARD, dashboard.id, {
label: `Dashboard ${dashboard.id}`,
});
}
});
@@ -308,20 +289,9 @@ test.describe('import dashboard', () => {
await apiDeleteDashboard(page, dashboardId);
// Verify it's gone
await expect
.poll(
async () => {
const response = await apiGetDashboard(page, dashboardId, {
failOnStatusCode: false,
});
return response.status();
},
{
timeout: 10000,
message: `Dashboard ${dashboardId} should return 404 after delete`,
},
)
.toBe(404);
await expectDeleted(page, ENDPOINTS.DASHBOARD, dashboardId, {
label: `Dashboard ${dashboardId}`,
});
// Refresh to confirm dashboard is no longer in the list
await dashboardListPage.goto();

View File

@@ -27,193 +27,190 @@ import { ChartCreationPage } from '../../pages/ChartCreationPage';
import { ENDPOINTS } from '../../helpers/api/dataset';
import { waitForPost } from '../../helpers/api/intercepts';
import { expectStatusOneOf } from '../../helpers/api/assertions';
import { apiPostDatabase } from '../../helpers/api/database';
import { getDatabaseByName } from '../../helpers/api/database';
import { apiExecuteSql } from '../../helpers/api/sqllab';
interface GsheetsSetupResult {
sheetName: string;
dbName: string;
interface ExamplesSetupResult {
tableName: string;
dbId: number;
createDatasetPage: CreateDatasetPage;
}
/**
* Sets up gsheets database and navigates to create dataset page.
* Skips test if gsheets connector unavailable (test.skip() throws, so no return).
* @param testInfo - Test info for parallelIndex to avoid name collisions in parallel runs
* @returns Setup result with names and page object
* Creates a temporary table in the examples database via SQL Lab,
* then navigates to the create dataset wizard with it pre-selected.
*
* Requires `allow_dml=True` on the examples database (configured in CI setup).
*
* @param page - Playwright page instance
* @param testAssets - Test assets tracker for cleanup
* @param testInfo - Test info for parallelIndex to avoid name collisions
* @returns Setup result with table name, database ID, and page object
*/
async function setupGsheetsDataset(
async function setupExamplesDataset(
page: Page,
testAssets: TestAssets,
_testAssets: TestAssets,
testInfo: TestInfo,
): Promise<GsheetsSetupResult> {
// Public Google Sheet for testing (published to web, no auth required).
// This is a Netflix dataset that is publicly accessible via the Google Visualization API.
// NOTE: This sheet is hosted on an external Google account and is not created by the test itself.
// If this sheet is deleted, its ID changes, or its sharing settings are restricted,
// these tests will start failing when they attempt to create a database pointing at it.
// In that case, create or select a new publicly readable test sheet, update `sheetUrl`
// to use its URL, and update this comment to describe who owns/maintains that sheet
// and the expected access controls (e.g., "anyone with the link can view").
const sheetUrl =
'https://docs.google.com/spreadsheets/d/19XNqckHGKGGPh83JGFdFGP4Bw9gdXeujq5EoIGwttdM/edit#gid=347941303';
// Include parallelIndex to avoid collisions when tests run in parallel
): Promise<ExamplesSetupResult> {
// Look up the examples database (always available in CI via load_examples)
const examplesDb = await getDatabaseByName(page, 'examples');
if (!examplesDb) {
throw new Error(
'Examples database not found. Ensure "superset load_examples" has run.',
);
}
const dbId = examplesDb.id;
// Create a uniquely-named temporary table via SQL Lab
const uniqueSuffix = `${Date.now()}_${testInfo.parallelIndex}`;
const sheetName = `test_netflix_${uniqueSuffix}`;
const dbName = `test_gsheets_db_${uniqueSuffix}`;
const tableName = `test_pw_${uniqueSuffix}`;
// Create a Google Sheets database via API
// The catalog must be in `extra` as JSON with engine_params.catalog format
const catalogDict = { [sheetName]: sheetUrl };
const createDbRes = await apiPostDatabase(page, {
database_name: dbName,
engine: 'gsheets',
sqlalchemy_uri: 'gsheets://',
configuration_method: 'dynamic_form',
expose_in_sqllab: true,
extra: JSON.stringify({
engine_params: {
catalog: catalogDict,
},
}),
});
// Check if gsheets connector is available
if (!createDbRes.ok()) {
const errorBody = await createDbRes.json();
const errorText = JSON.stringify(errorBody);
// Skip test if gsheets connector not installed
if (
errorText.includes('gsheets') ||
errorText.includes('No such DB engine')
) {
await test.info().attach('skip-reason', {
body: `Google Sheets connector unavailable: ${errorText}`,
contentType: 'text/plain',
});
test.skip(); // throws, no return needed
}
throw new Error(`Failed to create gsheets database: ${errorText}`);
// CI examples DB is always PostgreSQL, so 'public' is the correct schema.
const createTableRes = await apiExecuteSql(
page,
dbId,
`CREATE TABLE ${tableName} AS SELECT 1 AS id, 'test' AS name`,
'public',
);
if (!createTableRes.ok()) {
const errorBody = await createTableRes.json().catch(() => ({}));
throw new Error(
`Failed to create temp table "${tableName}": ${JSON.stringify(errorBody)}`,
);
}
const createDbBody = await createDbRes.json();
const dbId = createDbBody.result?.id ?? createDbBody.id;
if (!dbId) {
throw new Error('Database creation did not return an ID');
}
testAssets.trackDatabase(dbId);
// Navigate to create dataset page
const createDatasetPage = new CreateDatasetPage(page);
await createDatasetPage.goto();
await createDatasetPage.waitForPageLoad();
// Select the Google Sheets database
await createDatasetPage.selectDatabase(dbName);
// Select the examples database, public schema, and temp table.
// Schema is 'public' because the CI examples DB is always PostgreSQL.
await createDatasetPage.selectDatabase('examples');
await createDatasetPage.selectSchema('public');
await createDatasetPage.selectTable(tableName);
// Try to select the sheet - if not found due to timeout, skip
try {
await createDatasetPage.selectTable(sheetName);
} catch (error) {
// Only skip on TimeoutError (sheet not loaded); re-throw everything else
if (!(error instanceof Error) || error.name !== 'TimeoutError') {
throw error;
}
await test.info().attach('skip-reason', {
body: `Table "${sheetName}" not found in dropdown after timeout.`,
contentType: 'text/plain',
});
test.skip(); // throws, no return needed
}
return { sheetName, dbName, createDatasetPage };
return { tableName, dbId, createDatasetPage };
}
test('should create a dataset via wizard', async ({ page, testAssets }) => {
const { sheetName, createDatasetPage } = await setupGsheetsDataset(
/**
* Drop a temporary table created during test setup.
* Uses failOnStatusCode: false so cleanup doesn't throw if the table was already removed.
*/
async function dropTempTable(
page: Page,
dbId: number,
tableName: string,
): Promise<void> {
// Schema matches 'public' used in setupExamplesDataset (CI examples DB is PostgreSQL).
await apiExecuteSql(
page,
testAssets,
test.info(),
dbId,
`DROP TABLE IF EXISTS ${tableName}`,
'public',
{ failOnStatusCode: false },
);
}
// Set up response intercept to capture new dataset ID
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
pathMatch: true,
// Both tests create a temp table and use the dataset wizard, so they must run serially.
// Uses test.describe only because Playwright's serial mode API requires it -
// (Deviation from "avoid describe" guideline is necessary for functional reasons)
test.describe('create dataset wizard', () => {
test.describe.configure({ mode: 'serial' });
test('should create a dataset via wizard', async ({ page, testAssets }) => {
const { tableName, dbId, createDatasetPage } = await setupExamplesDataset(
page,
testAssets,
test.info(),
);
// Set up response intercept to capture new dataset ID
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
pathMatch: true,
});
// Click "Create and explore dataset" button
await createDatasetPage.clickCreateAndExploreDataset();
// Wait for dataset creation and capture ID for cleanup
const createResponse = expectStatusOneOf(
await createResponsePromise,
[200, 201],
);
const createBody = await createResponse.json();
const newDatasetId = createBody.result?.id ?? createBody.id;
if (newDatasetId) {
testAssets.trackDataset(newDatasetId);
}
// Verify we navigated to Chart Creation page with dataset pre-selected
await page.waitForURL(/.*\/chart\/add.*/);
const chartCreationPage = new ChartCreationPage(page);
await chartCreationPage.waitForPageLoad();
// Verify the dataset is pre-selected
await chartCreationPage.expectDatasetSelected(tableName);
// Select a visualization type and create chart
await chartCreationPage.selectVizType('Table');
// Click "Create new chart" to go to Explore
await chartCreationPage.clickCreateNewChart();
// Verify we navigated to Explore page
await page.waitForURL(/.*\/explore\/.*/);
const explorePage = new ExplorePage(page);
await explorePage.waitForPageLoad();
// Verify the dataset name is shown in Explore
const loadedDatasetName = await explorePage.getDatasetName();
expect(loadedDatasetName).toContain(tableName);
// Clean up temp table (dataset cleanup handled by testAssets)
await dropTempTable(page, dbId, tableName);
});
// Click "Create and explore dataset" button
await createDatasetPage.clickCreateAndExploreDataset();
// Wait for dataset creation and capture ID for cleanup
const createResponse = expectStatusOneOf(
await createResponsePromise,
[200, 201],
);
const createBody = await createResponse.json();
const newDatasetId = createBody.result?.id ?? createBody.id;
if (newDatasetId) {
testAssets.trackDataset(newDatasetId);
}
// Verify we navigated to Chart Creation page with dataset pre-selected
await page.waitForURL(/.*\/chart\/add.*/);
const chartCreationPage = new ChartCreationPage(page);
await chartCreationPage.waitForPageLoad();
// Verify the dataset is pre-selected
await chartCreationPage.expectDatasetSelected(sheetName);
// Select a visualization type and create chart
await chartCreationPage.selectVizType('Table');
// Click "Create new chart" to go to Explore
await chartCreationPage.clickCreateNewChart();
// Verify we navigated to Explore page
await page.waitForURL(/.*\/explore\/.*/);
const explorePage = new ExplorePage(page);
await explorePage.waitForPageLoad();
// Verify the dataset name is shown in Explore
const loadedDatasetName = await explorePage.getDatasetName();
expect(loadedDatasetName).toContain(sheetName);
});
test('should create a dataset without exploring', async ({
page,
testAssets,
}) => {
const { sheetName, createDatasetPage } = await setupGsheetsDataset(
test('should create a dataset without exploring', async ({
page,
testAssets,
test.info(),
);
}) => {
const { tableName, dbId, createDatasetPage } = await setupExamplesDataset(
page,
testAssets,
test.info(),
);
// Set up response intercept to capture dataset ID
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
pathMatch: true,
// Set up response intercept to capture dataset ID
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
pathMatch: true,
});
// Click "Create dataset" (not explore)
await createDatasetPage.clickCreateDataset();
// Capture dataset ID from response for cleanup
const createResponse = expectStatusOneOf(
await createResponsePromise,
[200, 201],
);
const createBody = await createResponse.json();
const datasetId = createBody.result?.id ?? createBody.id;
if (datasetId) {
testAssets.trackDataset(datasetId);
}
// Verify redirect to dataset list (not chart creation)
// Note: "Create dataset" action does not show a toast
await page.waitForURL(/.*tablemodelview\/list.*/);
// Wait for table load, verify row visible
const datasetListPage = new DatasetListPage(page);
await datasetListPage.waitForTableLoad();
await expect(datasetListPage.getDatasetRow(tableName)).toBeVisible();
// Clean up temp table (dataset cleanup handled by testAssets)
await dropTempTable(page, dbId, tableName);
});
// Click "Create dataset" (not explore)
await createDatasetPage.clickCreateDataset();
// Capture dataset ID from response for cleanup
const createResponse = expectStatusOneOf(
await createResponsePromise,
[200, 201],
);
const createBody = await createResponse.json();
const datasetId = createBody.result?.id ?? createBody.id;
if (datasetId) {
testAssets.trackDataset(datasetId);
}
// Verify redirect to dataset list (not chart creation)
// Note: "Create dataset" action does not show a toast
await page.waitForURL(/.*tablemodelview\/list.*/);
// Wait for table load, verify row visible
const datasetListPage = new DatasetListPage(page);
await datasetListPage.waitForTableLoad();
await expect(datasetListPage.getDatasetRow(sheetName)).toBeVisible();
});

View File

@@ -18,7 +18,6 @@
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import path from 'path';
import { DatasetListPage } from '../../pages/DatasetListPage';
import { ExplorePage } from '../../pages/ExplorePage';
import {
@@ -31,6 +30,7 @@ import {
import { Toast } from '../../components/core';
import {
apiDeleteDataset,
apiExportDatasets,
apiGetDataset,
apiPostVirtualDataset,
getDatasetByName,
@@ -43,6 +43,7 @@ import {
waitForPut,
} from '../../helpers/api/intercepts';
import {
expectDeleted,
expectStatusOneOf,
expectValidExportZip,
} from '../../helpers/api/assertions';
@@ -135,17 +136,9 @@ test('should delete a dataset with confirmation', async ({
await expect(datasetListPage.getDatasetRow(datasetName)).not.toBeVisible();
// Verify via API that dataset no longer exists (404)
await expect
.poll(
async () => {
const response = await apiGetDataset(page, datasetId, {
failOnStatusCode: false,
});
return response.status();
},
{ timeout: 10000, message: `Dataset ${datasetId} should return 404` },
)
.toBe(404);
await expectDeleted(page, ENDPOINTS.DATASET, datasetId, {
label: `Dataset ${datasetId}`,
});
});
test('should duplicate a dataset with new name', async ({
@@ -420,34 +413,17 @@ test('should bulk delete multiple datasets', async ({
await expect(datasetListPage.getDatasetRow(dataset2.name)).not.toBeVisible();
// Verify via API that datasets no longer exist (404)
// Use polling with explicit timeout since deletes may be async
await expect
.poll(
async () => {
const response = await apiGetDataset(page, dataset1.id, {
failOnStatusCode: false,
});
return response.status();
},
{ timeout: 10000, message: `Dataset ${dataset1.id} should return 404` },
)
.toBe(404);
await expect
.poll(
async () => {
const response = await apiGetDataset(page, dataset2.id, {
failOnStatusCode: false,
});
return response.status();
},
{ timeout: 10000, message: `Dataset ${dataset2.id} should return 404` },
)
.toBe(404);
await expectDeleted(page, ENDPOINTS.DATASET, dataset1.id, {
label: `Dataset ${dataset1.id}`,
});
await expectDeleted(page, ENDPOINTS.DATASET, dataset2.id, {
label: `Dataset ${dataset2.id}`,
});
});
// Import test uses a fixed dataset name from the zip fixture.
// Import test uses export-then-reimport approach (no static fixture needed).
// Uses test.describe only because Playwright's serial mode API requires it -
// this prevents race conditions when parallel workers import the same fixture.
// this prevents race conditions when parallel workers import the same dataset.
// (Deviation from "avoid describe" guideline is necessary for functional reasons)
test.describe('import dataset', () => {
test.describe.configure({ mode: 'serial' });
@@ -456,22 +432,33 @@ test.describe('import dataset', () => {
datasetListPage,
testAssets,
}) => {
// Dataset name from fixture (test_netflix_1768502050965)
// Note: Fixture contains a Google Sheets dataset backed by shillelagh[gsheetsapi],
// which is a base dependency — import failure fails the test hard (no skip).
const importedDatasetName = 'test_netflix_1768502050965';
const fixturePath = path.resolve(
__dirname,
'../../fixtures/dataset_export.zip',
test.setTimeout(60_000);
// Create a dataset, export it via API, then delete it, then reimport via UI
const { id: datasetId, name: datasetName } = await createTestDataset(
page,
testAssets,
test.info(),
{ prefix: 'test_import' },
);
// Cleanup: Delete any existing dataset with the same name from previous runs
const existingDataset = await getDatasetByName(page, importedDatasetName);
if (existingDataset) {
await apiDeleteDataset(page, existingDataset.id, {
failOnStatusCode: false,
});
}
// Export the dataset via API to get a zip buffer
const exportResponse = await apiExportDatasets(page, [datasetId]);
expect(exportResponse.ok()).toBe(true);
const exportBuffer = await exportResponse.body();
// Delete the dataset so reimport creates it fresh
await apiDeleteDataset(page, datasetId);
// Verify it's gone
await expectDeleted(page, ENDPOINTS.DATASET, datasetId, {
label: `Dataset ${datasetId}`,
});
// Refresh to confirm dataset is no longer in the list
await datasetListPage.goto();
await datasetListPage.waitForTableLoad();
await expect(datasetListPage.getDatasetRow(datasetName)).not.toBeVisible();
// Click the import button
await datasetListPage.clickImportButton();
@@ -480,11 +467,10 @@ test.describe('import dataset', () => {
const importModal = new ImportDatasetModal(page);
await importModal.waitForReady();
// Upload the fixture zip file
await importModal.uploadFile(fixturePath);
// Upload the exported zip via buffer (no temp file needed)
await importModal.uploadFileBuffer(exportBuffer);
// Set up response intercept to catch the import POST
// Use pathMatch to avoid false matches if URL lacks trailing slash
let importResponsePromise = waitForPost(page, ENDPOINTS.DATASET_IMPORT, {
pathMatch: true,
});
@@ -496,35 +482,27 @@ test.describe('import dataset', () => {
let importResponse = await importResponsePromise;
// Handle overwrite confirmation if dataset already exists
// First response may be 409/422 indicating overwrite is required - this is expected
// First response may be 409/422 indicating overwrite is required
const overwriteInput = importModal.getOverwriteInput();
await overwriteInput
.waitFor({ state: 'visible', timeout: 3000 })
.catch(error => {
// Only ignore TimeoutError (input not visible); re-throw other errors
if (!(error instanceof Error) || error.name !== 'TimeoutError') {
throw error;
}
});
if (await overwriteInput.isVisible()) {
// Set up new intercept for the actual import after overwrite confirmation
importResponsePromise = waitForPost(page, ENDPOINTS.DATASET_IMPORT, {
pathMatch: true,
});
await importModal.fillOverwriteConfirmation();
await importModal.clickImport();
// Wait for the second (final) import response
importResponse = await importResponsePromise;
}
// Fail hard if dataset import fails.
// The fixture contains a gsheets dataset; shillelagh[gsheetsapi] is a base
// dependency (pyproject.toml), so the engine is always available in CI.
if (!importResponse.ok()) {
const errorBody = await importResponse.json().catch(() => ({}));
throw new Error(`Import failed: ${JSON.stringify(errorBody)}`);
}
// Verify import succeeded
expectStatusOneOf(importResponse, [200]);
// Modal should close on success
await importModal.waitForHidden({ timeout: TIMEOUT.FILE_IMPORT });
@@ -533,19 +511,19 @@ test.describe('import dataset', () => {
const toast = new Toast(page);
await expect(toast.getSuccess()).toBeVisible({ timeout: 10000 });
// Refresh the page to see the imported dataset
// Refresh to see the imported dataset
await datasetListPage.goto();
await datasetListPage.waitForTableLoad();
// Verify dataset appears in list
await expect(
datasetListPage.getDatasetRow(importedDatasetName),
).toBeVisible();
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible();
// Get dataset ID for cleanup
const importedDataset = await getDatasetByName(page, importedDatasetName);
expect(importedDataset).not.toBeNull();
testAssets.trackDataset(importedDataset!.id);
// Track for cleanup: the dataset import API returns {"message": "OK"}
// with no ID, so look up the reimported dataset by name.
const reimported = await getDatasetByName(page, datasetName);
if (reimported) {
testAssets.trackDataset(reimported.id);
}
});
});

View File

@@ -33,7 +33,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -34,6 +34,6 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -29,27 +29,27 @@
{ "type": "Feature", "properties": { "ISO": "DZ-46", "NAME_1": "Aïn Témouchent" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -1.361975709870848, 35.319898656042554 ], [ -1.267730272999927, 35.390326239000046 ], [ -1.183338995999918, 35.57648346600007 ], [ -1.106353318999936, 35.623236395000049 ], [ -1.006728888227599, 35.524912014078154 ], [ -1.06920569458714, 35.454451199263417 ], [ -1.059025438273352, 35.411533922552451 ], [ -1.017787644584132, 35.455872300766032 ], [ -0.954122279819387, 35.437320462369428 ], [ -0.715428840368702, 35.482485662882993 ], [ -0.629490933259945, 35.409621893735391 ], [ -0.668868374076226, 35.332003892765442 ], [ -0.880845098622387, 35.254308377429652 ], [ -0.890456915953905, 35.159895535450573 ], [ -1.000527715581597, 35.087393499710231 ], [ -1.271622280443921, 35.195655626006328 ], [ -1.337664761119868, 35.190823879818197 ], [ -1.320663215435047, 35.283789780973564 ], [ -1.361975709870848, 35.319898656042554 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-19", "NAME_1": "Sétif" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.838444044756841, 36.304967760054069 ], [ 4.789919874602504, 36.333854885294727 ], [ 4.801081983746542, 36.379743557119468 ], [ 4.922573275835987, 36.38940705039505 ], [ 4.986807082281018, 36.462425849173599 ], [ 4.949858433320003, 36.502991848395084 ], [ 5.045304803174133, 36.524179186020604 ], [ 5.076517368381815, 36.574305324830846 ], [ 5.187621697683198, 36.53448863444288 ], [ 5.165555860914253, 36.494155178318806 ], [ 5.187776727314088, 36.408423976785059 ], [ 5.255421176645996, 36.378865057976043 ], [ 5.303221877287797, 36.389303696708225 ], [ 5.327871534643577, 36.449300035268379 ], [ 5.457889439346161, 36.499917101393066 ], [ 5.475459424912515, 36.590635076947194 ], [ 5.542793816781227, 36.523972480445593 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.776164585427239, 36.439998277198697 ], [ 5.872386101637289, 36.401886909153518 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.94018558060003, 35.862126573082833 ], [ 5.822880079552021, 35.914733181491101 ], [ 5.745623813787972, 35.89021271444517 ], [ 5.711620720619749, 35.849672552746085 ], [ 5.761901889960257, 35.810915229554155 ], [ 5.736425409405115, 35.771046861423486 ], [ 5.575091586707345, 35.818279120385057 ], [ 5.476854688892786, 35.736087754746052 ], [ 5.423472935027917, 35.635654609072901 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.065768670387797, 35.762081000138039 ], [ 5.054916618706955, 35.820862941871269 ], [ 5.139976026672286, 35.856623033326287 ], [ 5.184056024266113, 36.115366929739992 ], [ 5.25356082467232, 36.197377428225707 ], [ 5.212426384669925, 36.196653957813908 ], [ 5.188086785676603, 36.245514023853104 ], [ 5.125558301574358, 36.232362372425541 ], [ 5.034762810755126, 36.310884710960636 ], [ 4.838444044756841, 36.304967760054069 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-34", "NAME_1": "Bordj Bou Arréridj" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.801081983746542, 36.379743557119468 ], [ 4.785217318724278, 36.346644802415653 ], [ 4.821235792597747, 36.306853950449408 ], [ 5.034762810755126, 36.310884710960636 ], [ 5.125558301574358, 36.232362372425541 ], [ 5.188086785676603, 36.245514023853104 ], [ 5.212426384669925, 36.196653957813908 ], [ 5.25356082467232, 36.197377428225707 ], [ 5.184056024266113, 36.115366929739992 ], [ 5.139976026672286, 35.856623033326287 ], [ 5.066543816743717, 35.840474148363228 ], [ 5.05445153161287, 35.777325548435272 ], [ 4.854928827403455, 35.873159491917022 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.356354608178606, 36.340443631568348 ], [ 4.423378939886163, 36.22616119977954 ], [ 4.511383905442869, 36.225747789528839 ], [ 4.630084669571829, 36.253343004476051 ], [ 4.635820754224426, 36.338454088385447 ], [ 4.748010288244188, 36.420748805912638 ], [ 4.801081983746542, 36.379743557119468 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-10", "NAME_1": "Bouira" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.356354608178606, 36.340443631568348 ], [ 4.093631626298532, 36.046068834009475 ], [ 4.035133905405416, 35.870808214427541 ], [ 3.954156934794696, 35.890755316804416 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.578934359632967, 35.999353338985372 ], [ 3.551752556735096, 36.298301500313983 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.456357862825087, 36.576837470372936 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.477441846763782, 36.688561917298614 ], [ 3.635726759082672, 36.694401352940076 ], [ 3.680736931763988, 36.666806138892184 ], [ 3.641927930829354, 36.600401923010338 ], [ 3.733653599434035, 36.607610785109614 ], [ 3.888786247687165, 36.488522447353034 ], [ 4.37092736200816, 36.47265778323009 ], [ 4.398729281631006, 36.373697415003676 ], [ 4.356354608178606, 36.340443631568348 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-10", "NAME_1": "Bouira" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.398729281631006, 36.373697415003676 ], [ 4.093631626298532, 36.046068834009475 ], [ 4.035133905405416, 35.870808214427541 ], [ 3.954156934794696, 35.890755316804416 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.578934359632967, 35.999353338985372 ], [ 3.551752556735096, 36.298301500313983 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.456357862825087, 36.576837470372936 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.477441846763782, 36.688561917298614 ], [ 3.635726759082672, 36.694401352940076 ], [ 3.680736931763988, 36.666806138892184 ], [ 3.641927930829354, 36.600401923010338 ], [ 3.733653599434035, 36.607610785109614 ], [ 3.888786247687165, 36.488522447353034 ], [ 4.37092736200816, 36.47265778323009 ], [ 4.398729281631006, 36.373697415003676 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-09", "NAME_1": "Blida" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.477441846763782, 36.688561917298614 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.467364943237556, 36.583167833328844 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.008064812940745, 36.356437486001141 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.824458448798339, 36.365170803289914 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.64338423019808, 36.397261868540397 ], [ 2.52251305483361, 36.363982244884653 ], [ 2.476262647802969, 36.420619614703412 ], [ 2.499723747652808, 36.470384020106394 ], [ 2.602456495770639, 36.467438463414339 ], [ 2.837377557128548, 36.637867336808142 ], [ 3.02992394413468, 36.662232774223128 ], [ 3.025118036368269, 36.71005931238733 ], [ 3.097413363835642, 36.649933784416589 ], [ 3.323652784748219, 36.712307237089306 ], [ 3.477441846763782, 36.688561917298614 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-38", "NAME_1": "Tissemsilt" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.712691685372988, 35.92341482193649 ], [ 1.853561639058398, 35.860498766005207 ], [ 1.964510938728836, 35.879825750757789 ], [ 2.008745965054231, 35.917911282179887 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.158194207296845, 35.698234768219947 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.249713169427196, 35.559612739236513 ], [ 1.744937777556402, 35.551938788244456 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.658069696259474, 35.547933865255629 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.346460809019106, 35.68981151019301 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.37720828623344, 35.788048408007626 ], [ 1.447953321888292, 35.914268093497697 ], [ 1.504900750069567, 35.935222887126542 ], [ 1.548360630039042, 36.003952542076149 ], [ 1.638484328189179, 35.934292711139676 ], [ 1.712691685372988, 35.92341482193649 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-38", "NAME_1": "Tissemsilt" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.638484328189179, 35.934292711139676 ], [ 1.853561639058398, 35.860498766005207 ], [ 1.964510938728836, 35.879825750757789 ], [ 2.008745965054231, 35.917911282179887 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.158194207296845, 35.698234768219947 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.249713169427196, 35.559612739236513 ], [ 1.744937777556402, 35.551938788244456 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.658069696259474, 35.547933865255629 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.346460809019106, 35.68981151019301 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.37720828623344, 35.788048408007626 ], [ 1.447953321888292, 35.914268093497697 ], [ 1.504900750069567, 35.935222887126542 ], [ 1.548360630039042, 36.003952542076149 ], [ 1.638484328189179, 35.934292711139676 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-44", "NAME_1": "Aïn Defla" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.853561639058398, 35.860498766005207 ], [ 1.712691685372988, 35.92341482193649 ], [ 1.717187533877564, 35.973205063962496 ], [ 1.588513218110506, 36.143298041471439 ], [ 1.628717482126092, 36.2114850940618 ], [ 1.594921095432198, 36.234377753130786 ], [ 1.603292676615695, 36.279077867449587 ], [ 1.553166537805453, 36.367677110410284 ], [ 1.647786086258861, 36.443150540365139 ], [ 1.716722445884159, 36.36666942005769 ], [ 1.793048536560718, 36.43198843032178 ], [ 2.476262647802969, 36.420619614703412 ], [ 2.52251305483361, 36.363982244884653 ], [ 2.652685988267706, 36.39209422556803 ], [ 2.647880079601975, 36.33251129815784 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.531659784171666, 36.189574286024481 ], [ 2.577445103208959, 36.211795152424315 ], [ 2.639043410425018, 36.181564439147508 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.008745965054231, 35.917911282179887 ], [ 1.964510938728836, 35.879825750757789 ], [ 1.853561639058398, 35.860498766005207 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-48", "NAME_1": "Relizane" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.411624789652308, 35.841559353081664 ], [ 1.361808709204581, 35.776524562758368 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.010977409879558, 35.575038153787773 ], [ 1.01795372888148, 35.544238999730055 ], [ 0.959456007988365, 35.54775299720302 ], [ 0.866076693884281, 35.436080227120783 ], [ 0.532298617986839, 35.5099775250427 ], [ 0.456851027353025, 35.558139959991138 ], [ 0.319081659091296, 35.557778224785238 ], [ 0.229371372091123, 35.695935167573907 ], [ 0.423984815746621, 35.817633165238362 ], [ 0.451735060324779, 35.951966051292231 ], [ 0.529043002932212, 35.968063259411849 ], [ 0.542840610405847, 36.048962713858145 ], [ 0.704071079416792, 36.186525377444184 ], [ 0.897392611879525, 36.197635809744781 ], [ 0.948138869213437, 36.032012844117446 ], [ 1.040949740737915, 36.025449937164865 ], [ 1.159340448303055, 35.94597158512056 ], [ 1.288893263213538, 35.965867011553314 ], [ 1.356227655082193, 35.868146878575544 ], [ 1.411624789652308, 35.841559353081664 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-32", "NAME_1": "El Bayadh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.340785759755022, 34.234138088701798 ], [ 0.729495884027813, 34.435469468940823 ], [ 0.973718702556084, 34.382087714176635 ], [ 1.031958041930068, 34.339351305518335 ], [ 1.023689812634757, 34.191246650412552 ], [ 1.231480747038859, 34.191892605559303 ], [ 1.409144320953658, 33.961803290389526 ], [ 1.793978713446847, 33.816411647979123 ], [ 2.019287956573919, 33.499040839463134 ], [ 2.031380242604087, 33.392535712353208 ], [ 1.975207960778732, 33.292180081045899 ], [ 2.054221225728952, 33.108909613687729 ], [ 2.174937370563214, 32.97499013878388 ], [ 2.291467726154622, 32.695989080731465 ], [ 2.297513869169734, 32.377300523500367 ], [ 2.344849480918811, 32.172791043471875 ], [ 2.315652297315637, 32.062177638786977 ], [ 2.215296665109008, 31.923297227385092 ], [ 2.24211673280098, 31.791186428510855 ], [ 2.199432000086745, 31.735246689782912 ], [ 2.085382114093306, 31.658584703221436 ], [ 0.842460565302815, 31.096939399333792 ], [ 0.399128451916511, 30.708694363054406 ], [ -0.154016078679092, 31.123785305447484 ], [ -0.400822719592838, 31.390668239947274 ], [ -0.003585985404925, 32.443963120901287 ], [ -0.012887742575288, 32.500006212416736 ], [ -0.083942836592655, 32.524397488253442 ], [ -0.10156449990177, 32.567108059389398 ], [ -0.006686570828606, 32.804767970965088 ], [ -0.027047085254765, 33.02062042908949 ], [ 0.063490024045336, 33.308096422012227 ], [ 0.050622592558568, 33.803104966920671 ], [ 0.086020948807743, 34.048309638279136 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.340785759755022, 34.234138088701798 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-20", "NAME_1": "Saïda" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.75760786291255, 34.438182480736884 ], [ 0.351482781804918, 34.238117174168224 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.086020948807743, 34.048309638279136 ], [ -0.044100307782969, 33.987253933422267 ], [ -0.081772427155784, 34.08153758419212 ], [ -0.220213588985928, 34.059445909001454 ], [ -0.346614141629345, 33.955214545015224 ], [ -0.356122606173358, 33.915036119421359 ], [ -0.499679735031748, 34.193158678330292 ], [ -0.060895147892779, 34.550087795715172 ], [ -0.056502651276332, 34.609050605501011 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.24687862794633, 35.098891506537768 ], [ -0.135567593069993, 35.12764944146852 ], [ -0.111693081170756, 35.160593167440709 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.014334683398943, 35.06385488549455 ], [ 0.112220899774684, 35.096100979476603 ], [ 0.189993931274898, 35.023495591848075 ], [ 0.625057813567253, 35.095920112323313 ], [ 0.754197218227034, 35.007837633300142 ], [ 0.783032668422891, 34.948151353102446 ], [ 0.669757927886053, 34.855495510309538 ], [ 0.673478630934085, 34.789375515267807 ], [ 0.856878288602104, 34.499858303218105 ], [ 0.75760786291255, 34.438182480736884 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-20", "NAME_1": "Saïda" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.856878288602104, 34.499858303218105 ], [ 0.351482781804918, 34.238117174168224 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.086020948807743, 34.048309638279136 ], [ -0.044100307782969, 33.987253933422267 ], [ -0.081772427155784, 34.08153758419212 ], [ -0.220213588985928, 34.059445909001454 ], [ -0.346614141629345, 33.955214545015224 ], [ -0.356122606173358, 33.915036119421359 ], [ -0.499679735031748, 34.193158678330292 ], [ -0.060895147892779, 34.550087795715172 ], [ -0.056502651276332, 34.609050605501011 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.24687862794633, 35.098891506537768 ], [ -0.135567593069993, 35.12764944146852 ], [ -0.111693081170756, 35.160593167440709 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.014334683398943, 35.06385488549455 ], [ 0.112220899774684, 35.096100979476603 ], [ 0.189993931274898, 35.023495591848075 ], [ 0.625057813567253, 35.095920112323313 ], [ 0.754197218227034, 35.007837633300142 ], [ 0.783032668422891, 34.948151353102446 ], [ 0.669757927886053, 34.855495510309538 ], [ 0.673478630934085, 34.789375515267807 ], [ 0.856878288602104, 34.499858303218105 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-22", "NAME_1": "Sidi Bel Abbès" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -0.16517818782313, 35.122921047168575 ], [ -0.250702683781867, 35.093568833934512 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.055004035407933, 34.60403799126027 ], [ -0.071902228305191, 34.529701442867292 ], [ -0.485727097927224, 34.229332180036067 ], [ -0.523140834881644, 34.290362047370593 ], [ -0.578176235145122, 34.312944648077007 ], [ -0.836196662046348, 34.215121161412469 ], [ -1.120417039015138, 34.271293443237766 ], [ -0.880018276322403, 34.467612210135371 ], [ -0.903789435434135, 34.547193914967181 ], [ -0.829582078250326, 34.578380641753199 ], [ -0.758475308288837, 34.711912543029428 ], [ -0.775631882705227, 34.737440701327273 ], [ -0.886116095281579, 34.74302175634898 ], [ -0.89056026874141, 34.843609930753701 ], [ -0.941358201120067, 34.842576401979329 ], [ -0.924149949860293, 34.881514594123189 ], [ -0.96611121306205, 34.922726549390745 ], [ -0.929834356770186, 35.047163398172245 ], [ -1.000527715581597, 35.087393499710231 ], [ -0.890456915953905, 35.159895535450573 ], [ -0.880845098622387, 35.254308377429652 ], [ -0.753669398723787, 35.318257962135249 ], [ -0.668868374076226, 35.332003892765442 ], [ -0.632281460321167, 35.298155829228108 ], [ -0.569184536337275, 35.405229397118944 ], [ -0.464436408413519, 35.429388128958976 ], [ -0.463661261158279, 35.358048814101494 ], [ -0.361807013083194, 35.358772284513293 ], [ -0.189207729353257, 35.299318549211648 ], [ -0.216957973931358, 35.190927231706382 ], [ -0.16517818782313, 35.122921047168575 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-29", "NAME_1": "Mascara" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.229371372091123, 35.695935167573907 ], [ 0.319081659091296, 35.557778224785238 ], [ 0.456851027353025, 35.558139959991138 ], [ 0.532298617986839, 35.5099775250427 ], [ 0.827887810573998, 35.443469957272669 ], [ 0.878168979914506, 35.374740302323062 ], [ 0.778536818119676, 35.279371445935453 ], [ 0.684330681715551, 35.299551093208379 ], [ 0.617771437102135, 35.276038316515098 ], [ 0.448789503632668, 35.059074815250483 ], [ 0.209837680864268, 35.02148021114283 ], [ 0.112220899774684, 35.096100979476603 ], [ -0.014334683398943, 35.06385488549455 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.105956997417479, 35.159740505819684 ], [ -0.16517818782313, 35.122921047168575 ], [ -0.216957973931358, 35.190927231706382 ], [ -0.189207729353257, 35.299318549211648 ], [ -0.361807013083194, 35.358772284513293 ], [ -0.463661261158279, 35.358048814101494 ], [ -0.464436408413519, 35.429388128958976 ], [ -0.545361701281479, 35.411973172124192 ], [ -0.302482468990775, 35.669399318923411 ], [ -0.220575324191884, 35.683558660703625 ], [ -0.114380256343793, 35.783759264178684 ], [ -0.043996954995464, 35.714461168448167 ], [ 0.104469435316162, 35.756732490012382 ], [ 0.152115106327074, 35.682628486515398 ], [ 0.229371372091123, 35.695935167573907 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-47", "NAME_1": "Ghardaïa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.085382114093306, 31.658584703221436 ], [ 2.239326205739815, 31.77353892678002 ], [ 2.215296665109008, 31.923297227385092 ], [ 2.34329918820697, 32.132664292922755 ], [ 2.297513869169734, 32.377300523500367 ], [ 2.291467726154622, 32.695989080731465 ], [ 3.100668979789589, 32.830683701991234 ], [ 3.321327344781139, 32.780790107177666 ], [ 3.427470736685166, 32.87551300751926 ], [ 3.738614535932072, 33.000466620238285 ], [ 3.884445427914159, 32.981863104998297 ], [ 4.176210564665837, 33.029637966319058 ], [ 4.978280469667936, 32.840760606416836 ], [ 4.738863559805452, 32.531890571192889 ], [ 4.359145135239771, 32.255990099463475 ], [ 4.110839877558305, 31.791393134085865 ], [ 3.802538283115268, 29.989849352465626 ], [ 3.721509636560484, 29.863345445236064 ], [ 3.479612257999293, 29.636847642804469 ], [ 3.397550082670193, 29.364874578798776 ], [ 3.34447838716784, 29.306557725958271 ], [ 2.9859989761718, 29.123545641018495 ], [ 2.079335971977514, 29.036367499560413 ], [ 1.943426954790141, 30.094804185964392 ], [ 2.071739536250561, 30.859692898008802 ], [ 2.085382114093306, 31.658584703221436 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-03", "NAME_1": "Laghouat" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.291467726154622, 32.695989080731465 ], [ 2.174937370563214, 32.97499013878388 ], [ 2.054221225728952, 33.108909613687729 ], [ 1.975207960778732, 33.292180081045899 ], [ 2.031380242604087, 33.392535712353208 ], [ 2.019287956573919, 33.499040839463134 ], [ 1.776925490019323, 33.831346137014577 ], [ 1.428109572298922, 33.951183782705414 ], [ 1.336228874962615, 34.068928534224426 ], [ 1.64158491361286, 34.256152249526679 ], [ 1.691090935698128, 34.345810858784091 ], [ 1.806226027309322, 34.449086209261111 ], [ 1.981099074162955, 34.506989650951596 ], [ 2.220567660868824, 34.674679674127617 ], [ 2.357716912405579, 34.692869778217641 ], [ 2.295808546826947, 34.439035143257229 ], [ 2.360972528359468, 34.352735500942572 ], [ 2.393735386279047, 34.225301419524897 ], [ 2.48339399643578, 34.113576971699842 ], [ 2.458279250187218, 34.003247788754436 ], [ 2.474867383822698, 33.92371775986669 ], [ 2.639198439156644, 33.911005357111549 ], [ 2.650205518669736, 34.09797068909603 ], [ 2.817637160326683, 34.138846747579294 ], [ 2.949412062416741, 34.263619493145086 ], [ 3.093537632056041, 34.240029202085964 ], [ 3.14924482588799, 34.074767970765208 ], [ 3.232133822617811, 33.947411403713318 ], [ 3.640997755741807, 33.564334011305561 ], [ 4.059060093248718, 33.252518419389503 ], [ 4.176210564665837, 33.029637966319058 ], [ 3.884445427914159, 32.981863104998297 ], [ 3.738614535932072, 33.000466620238285 ], [ 3.427470736685166, 32.87551300751926 ], [ 3.321327344781139, 32.780790107177666 ], [ 3.100668979789589, 32.830683701991234 ], [ 2.291467726154622, 32.695989080731465 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-26", "NAME_1": "Médéa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.249713169427196, 35.559612739236513 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.169511346071772, 35.666815497437256 ], [ 2.16269005670074, 35.708983466213965 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.643694288560539, 36.191537990785605 ], [ 2.526388786613211, 36.192778225134987 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.614083692908082, 36.309670315032974 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.845284051217959, 36.369227403122125 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.996282586172413, 36.353285223734076 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.552217644728501, 36.30827505105276 ], [ 3.56436160580347, 36.05097809546271 ], [ 3.633866408008316, 35.889540920876755 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.540900505953573, 35.803938910552233 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.411037631781255, 35.805928452835815 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.336726921809998, 35.747534084730205 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.081910434918598, 35.67187978852138 ], [ 2.960264113198264, 35.804739895329874 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.671031121089356, 35.44657054269635 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.285731642401345, 35.439077459756959 ], [ 2.249713169427196, 35.559612739236513 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-26", "NAME_1": "Médéa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.285731642401345, 35.439077459756959 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.169511346071772, 35.666815497437256 ], [ 2.16269005670074, 35.708983466213965 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.643694288560539, 36.191537990785605 ], [ 2.526388786613211, 36.192778225134987 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.614083692908082, 36.309670315032974 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.845284051217959, 36.369227403122125 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.996282586172413, 36.353285223734076 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.552217644728501, 36.30827505105276 ], [ 3.56436160580347, 36.05097809546271 ], [ 3.633866408008316, 35.889540920876755 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.540900505953573, 35.803938910552233 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.411037631781255, 35.805928452835815 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.336726921809998, 35.747534084730205 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.081910434918598, 35.67187978852138 ], [ 2.960264113198264, 35.804739895329874 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.671031121089356, 35.44657054269635 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.285731642401345, 35.439077459756959 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-14", "NAME_1": "Tiaret" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.346460809019106, 35.68981151019301 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.634608594610938, 35.5504660098984 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.744937777556402, 35.551938788244456 ], [ 2.249713169427196, 35.559612739236513 ], [ 2.286816847119781, 35.329265042547661 ], [ 2.360042352372659, 35.287975572015 ], [ 2.379317661181119, 35.205009060020075 ], [ 2.620284864654764, 35.031143704418412 ], [ 2.523443230820419, 34.999104316011369 ], [ 2.460294630892463, 34.870171616926541 ], [ 2.369964227167316, 34.776301378206028 ], [ 2.357716912405579, 34.692869778217641 ], [ 2.220567660868824, 34.674679674127617 ], [ 1.981099074162955, 34.506989650951596 ], [ 1.806226027309322, 34.449086209261111 ], [ 1.691090935698128, 34.345810858784091 ], [ 1.64158491361286, 34.256152249526679 ], [ 1.336228874962615, 34.068928534224426 ], [ 1.231480747038859, 34.191892605559303 ], [ 1.023689812634757, 34.191246650412552 ], [ 1.031958041930068, 34.339351305518335 ], [ 0.973718702556084, 34.382087714176635 ], [ 0.75760786291255, 34.438182480736884 ], [ 0.856878288602104, 34.499858303218105 ], [ 0.693529086997728, 34.736329658187174 ], [ 0.667432488818292, 34.84221466677343 ], [ 0.781637404442677, 34.972697659469361 ], [ 0.652549675726959, 35.089796454043096 ], [ 0.492249382702823, 35.086334133413516 ], [ 0.617771437102135, 35.276038316515098 ], [ 0.684330681715551, 35.299551093208379 ], [ 0.778536818119676, 35.279371445935453 ], [ 0.819981317383906, 35.30611400016096 ], [ 0.880494418982266, 35.386367499460505 ], [ 0.827887810573998, 35.443469957272669 ], [ 0.881114535707241, 35.443702501269399 ], [ 0.959456007988365, 35.54775299720302 ], [ 1.01795372888148, 35.544238999730055 ], [ 1.010977409879558, 35.575038153787773 ], [ 1.206210972058727, 35.645679837554439 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.346460809019106, 35.68981151019301 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-28", "NAME_1": "M'Sila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.05445153161287, 35.777325548435272 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.379702995796606, 35.632915757955857 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.022980583986737, 35.520674547092653 ], [ 4.963242628744297, 35.4887901883165 ], [ 4.876477899335555, 35.519150091903214 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.779016146977597, 35.172840481303126 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.041119012132697, 34.918644111136814 ], [ 4.932805209892535, 34.838907375774738 ], [ 4.394646844276451, 34.746587428866746 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.036374138855422, 34.441929023105899 ], [ 3.921084019411978, 34.672664293422372 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.67112511623111, 34.790279852832896 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.571803012798796, 35.005227973392209 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.657585890276607, 35.374146023120431 ], [ 3.695929803217837, 35.472047024150811 ], [ 3.436927524385737, 35.671983141308885 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.394294467615566, 35.793836167704967 ], [ 3.432276646250216, 35.796600857243732 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.536714714912137, 35.792234199049005 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.954156934794696, 35.890755316804416 ], [ 4.022266473019329, 35.866854967382835 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.854928827403455, 35.873159491917022 ], [ 5.05445153161287, 35.777325548435272 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-28", "NAME_1": "M'Sila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.854928827403455, 35.873159491917022 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.379702995796606, 35.632915757955857 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.022980583986737, 35.520674547092653 ], [ 4.963242628744297, 35.4887901883165 ], [ 4.876477899335555, 35.519150091903214 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.779016146977597, 35.172840481303126 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.041119012132697, 34.918644111136814 ], [ 4.932805209892535, 34.838907375774738 ], [ 4.394646844276451, 34.746587428866746 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.036374138855422, 34.441929023105899 ], [ 3.921084019411978, 34.672664293422372 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.67112511623111, 34.790279852832896 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.571803012798796, 35.005227973392209 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.657585890276607, 35.374146023120431 ], [ 3.695929803217837, 35.472047024150811 ], [ 3.436927524385737, 35.671983141308885 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.394294467615566, 35.793836167704967 ], [ 3.432276646250216, 35.796600857243732 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.536714714912137, 35.792234199049005 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.954156934794696, 35.890755316804416 ], [ 4.022266473019329, 35.866854967382835 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.854928827403455, 35.873159491917022 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-05", "NAME_1": "Batna" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.94018558060003, 35.862126573082833 ], [ 5.976669141567641, 35.915792547787817 ], [ 6.275513950108689, 35.866079820127595 ], [ 6.351064894429328, 35.884735012211024 ], [ 6.47694868313522, 35.840396633098123 ], [ 6.487025587560822, 35.781433824211604 ], [ 6.649496290921093, 35.796678372508836 ], [ 6.769127231036862, 35.683920395909524 ], [ 6.781684605060491, 35.562558295029248 ], [ 6.592703892370707, 35.436958727163528 ], [ 6.584022251026056, 35.34332103154037 ], [ 6.628360630138957, 35.316862698154978 ], [ 6.514930860870493, 35.119717108957389 ], [ 6.512140333809327, 35.052382717088676 ], [ 6.598595004855554, 34.900350654259285 ], [ 6.58102501838988, 34.769660955988286 ], [ 6.508729689123868, 34.775500393428388 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.072218866007859, 35.058583888835358 ], [ 5.993102248270134, 34.989363308369946 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.932279087409938, 35.143565782434905 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.830631544909863, 35.288440660008405 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.610748325374914, 35.126383367798155 ], [ 5.596640658639501, 35.091760158804277 ], [ 5.306012404348962, 35.021428534299389 ], [ 5.184831169722713, 35.031040350731587 ], [ 5.004325391903308, 35.08545563427009 ], [ 4.925053745434013, 35.142196356876354 ], [ 4.782426791663113, 35.166871853553175 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.880198602383587, 35.525402940493279 ], [ 4.963242628744297, 35.4887901883165 ], [ 5.022980583986737, 35.520674547092653 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.438200717589041, 35.641855780819583 ], [ 5.476854688892786, 35.736087754746052 ], [ 5.575091586707345, 35.818279120385057 ], [ 5.736425409405115, 35.771046861423486 ], [ 5.761901889960257, 35.810915229554155 ], [ 5.71379113095594, 35.860421250740103 ], [ 5.80009077327054, 35.913467108720056 ], [ 5.94018558060003, 35.862126573082833 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-40", "NAME_1": "Khenchela" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.599680209573989, 35.327818101724006 ], [ 6.592703892370707, 35.436958727163528 ], [ 6.781684605060491, 35.562558295029248 ], [ 6.769127231036862, 35.683920395909524 ], [ 6.82498945539902, 35.623562323942053 ], [ 7.00327314603885, 35.629815172532176 ], [ 7.051228875412278, 35.577182725702187 ], [ 7.141197543931469, 35.617464504982877 ], [ 7.267546420630822, 35.621236883974973 ], [ 7.30335818892928, 35.600695502395524 ], [ 7.315037062010788, 35.535247300922151 ], [ 7.385161980940666, 35.571756700311425 ], [ 7.462263217973145, 35.539123032701752 ], [ 7.544842157239771, 35.447810777045675 ], [ 7.413067254250393, 35.38662588097958 ], [ 7.411516960639233, 35.156097317137437 ], [ 7.222226189586991, 34.94629100202809 ], [ 7.26553103992552, 34.832344468822157 ], [ 7.264445835207141, 34.940632433539974 ], [ 7.30831912632658, 34.977193507973993 ], [ 7.365886672132206, 34.962465725412869 ], [ 7.402835321093164, 34.74604482740682 ], [ 7.274057650739962, 34.540734360802105 ], [ 7.135771519440084, 34.16057668756406 ], [ 6.960588413324615, 34.193933823786892 ], [ 6.714970329916753, 34.326613064341416 ], [ 6.696521844307654, 34.475828761687978 ], [ 6.748456659147507, 34.726097724130625 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.587897982805657, 34.778549302908004 ], [ 6.570483025970873, 34.839010728562243 ], [ 6.598595004855554, 34.900350654259285 ], [ 6.512140333809327, 35.052382717088676 ], [ 6.569707878715633, 35.253455714909308 ], [ 6.628360630138957, 35.316862698154978 ], [ 6.599680209573989, 35.327818101724006 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-24", "NAME_1": "Guelma" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 7.010559523403288, 36.155157783504876 ], [ 7.04564782128989, 36.275770575551576 ], [ 6.93795413577476, 36.432866930364526 ], [ 7.075723504036489, 36.502552598823343 ], [ 7.077118768016703, 36.545263169959298 ], [ 7.123989291772375, 36.585829169180784 ], [ 7.314727003648272, 36.651613268337655 ], [ 7.3672819352131, 36.721376451162257 ], [ 7.456837191683064, 36.693057765803246 ], [ 7.475595736553998, 36.638849188739073 ], [ 7.533783399983918, 36.615388088889176 ], [ 7.697029249700108, 36.657736924819176 ], [ 7.771546665246376, 36.581643378139347 ], [ 7.98786421136424, 36.484388333155039 ], [ 7.871437208560337, 36.432582709524411 ], [ 7.795266148413987, 36.440980130028947 ], [ 7.835005324436167, 36.334862576546641 ], [ 7.813869662754712, 36.304916083210628 ], [ 7.410431755920797, 36.181254380785049 ], [ 7.357515090049333, 36.129758816416199 ], [ 7.223776483198151, 36.144099026249023 ], [ 7.194889357058173, 36.080743719846794 ], [ 7.133601108204573, 36.053148504899582 ], [ 7.066421745966807, 36.047283229937079 ], [ 7.010559523403288, 36.155157783504876 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-25", "NAME_1": "Constantine" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.93795413577476, 36.432866930364526 ], [ 7.048283318720166, 36.238408515440597 ], [ 7.010559523403288, 36.155157783504876 ], [ 6.898525018115095, 36.10689199486967 ], [ 6.87992150197573, 36.146424465316784 ], [ 6.772692905353267, 36.192442328350751 ], [ 6.577149285710959, 36.10919159641503 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.599370151211474, 36.57399526646833 ], [ 6.652441846713828, 36.607895005949729 ], [ 6.777188754757219, 36.556270250371711 ], [ 6.920125766890635, 36.557148749515136 ], [ 6.904106073136745, 36.529036769731135 ], [ 6.845453321713421, 36.5314138865416 ], [ 6.818013136397099, 36.466818345789989 ], [ 6.93795413577476, 36.432866930364526 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-43", "NAME_1": "Mila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.810167676796823, 36.431135768700756 ], [ 5.766242709733262, 36.45260732716639 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.904322137256827, 36.545521552377693 ], [ 6.023229607860117, 36.605931301188605 ], [ 6.215310906872844, 36.582625230070278 ], [ 6.294582554241458, 36.624069729334508 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.525007765296095, 36.204663804690824 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.170300734191528, 35.951242580880375 ], [ 6.149216750252833, 35.900392970758958 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.872386101637289, 36.401886909153518 ], [ 5.810167676796823, 36.431135768700756 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-43", "NAME_1": "Mila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.872386101637289, 36.401886909153518 ], [ 5.766242709733262, 36.45260732716639 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.904322137256827, 36.545521552377693 ], [ 6.023229607860117, 36.605931301188605 ], [ 6.215310906872844, 36.582625230070278 ], [ 6.294582554241458, 36.624069729334508 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.525007765296095, 36.204663804690824 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.170300734191528, 35.951242580880375 ], [ 6.149216750252833, 35.900392970758958 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.872386101637289, 36.401886909153518 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-04", "NAME_1": "Oum el Bouaghi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.275513950108689, 35.866079820127595 ], [ 6.149216750252833, 35.900392970758958 ], [ 6.151387159689648, 35.935274563070607 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.477103712766109, 36.158620103235137 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.584797397381976, 36.108700669999905 ], [ 6.772692905353267, 36.192442328350751 ], [ 6.87992150197573, 36.146424465316784 ], [ 6.898525018115095, 36.10689199486967 ], [ 7.010559523403288, 36.155157783504876 ], [ 7.078979119990379, 36.042503159693069 ], [ 7.194889357058173, 36.080743719846794 ], [ 7.223776483198151, 36.144099026249023 ], [ 7.290800814905651, 36.143323879893103 ], [ 7.325889112792311, 36.127407538027455 ], [ 7.324958936805501, 36.080020250334258 ], [ 7.398339470789949, 35.967003893115134 ], [ 7.577243279953393, 35.833497830260626 ], [ 7.660132276683214, 35.834195462250761 ], [ 7.734598016285418, 35.934499416714687 ], [ 7.865856154437949, 35.858715929296693 ], [ 7.823378127298724, 35.739007473016443 ], [ 7.840741408189444, 35.615164903437517 ], [ 7.544842157239771, 35.447810777045675 ], [ 7.462263217973145, 35.539123032701752 ], [ 7.385161980940666, 35.571756700311425 ], [ 7.315037062010788, 35.535247300922151 ], [ 7.30335818892928, 35.600695502395524 ], [ 7.267546420630822, 35.621236883974973 ], [ 7.141197543931469, 35.617464504982877 ], [ 7.083009881400869, 35.576872667339728 ], [ 6.992576124888217, 35.631985581968991 ], [ 6.82498945539902, 35.623562323942053 ], [ 6.649496290921093, 35.796678372508836 ], [ 6.487025587560822, 35.781433824211604 ], [ 6.466820102765496, 35.846391100169114 ], [ 6.339437697291885, 35.88680206975971 ], [ 6.275513950108689, 35.866079820127595 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-07", "NAME_1": "Biskra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.83208784337927, 34.819244493338658 ], [ 4.932805209892535, 34.838907375774738 ], [ 5.028406610276818, 34.900195623729019 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.272474399174143, 35.018431300763893 ], [ 5.536747673766115, 35.0663611917156 ], [ 5.613538853335399, 35.1064362663206 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.830631544909863, 35.288440660008405 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.932279087409938, 35.143565782434905 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.993102248270134, 34.989363308369946 ], [ 6.072218866007859, 35.058583888835358 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.540407342324954, 34.765681871421236 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.749541863865886, 34.712222602291263 ], [ 6.695901726683303, 34.376093248004963 ], [ 6.558545769571595, 34.317027086330938 ], [ 6.211435174193923, 34.428415636472437 ], [ 5.998528272761575, 34.394955146562722 ], [ 5.477164748154564, 34.449835517195368 ], [ 5.311748488102239, 34.498256334562143 ], [ 5.29903608624636, 34.449060369940128 ], [ 5.217232293335655, 34.411956692247543 ], [ 5.185451287347007, 34.356662909565614 ], [ 5.359859246207236, 34.166209418529832 ], [ 5.37381188331176, 34.058386541805419 ], [ 5.334847852746179, 33.845996406109236 ], [ 5.121785922582262, 33.572679754966714 ], [ 4.406635775720474, 33.927645169389052 ], [ 4.335063917765581, 33.986039537494662 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.210627068984024, 34.174606839034368 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.273310580918576, 34.625845444711501 ], [ 4.434696078661091, 34.772658189523781 ], [ 4.83208784337927, 34.819244493338658 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-07", "NAME_1": "Biskra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.434696078661091, 34.772658189523781 ], [ 4.932805209892535, 34.838907375774738 ], [ 5.028406610276818, 34.900195623729019 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.272474399174143, 35.018431300763893 ], [ 5.536747673766115, 35.0663611917156 ], [ 5.613538853335399, 35.1064362663206 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.830631544909863, 35.288440660008405 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.932279087409938, 35.143565782434905 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.993102248270134, 34.989363308369946 ], [ 6.072218866007859, 35.058583888835358 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.540407342324954, 34.765681871421236 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.749541863865886, 34.712222602291263 ], [ 6.695901726683303, 34.376093248004963 ], [ 6.558545769571595, 34.317027086330938 ], [ 6.211435174193923, 34.428415636472437 ], [ 5.998528272761575, 34.394955146562722 ], [ 5.477164748154564, 34.449835517195368 ], [ 5.311748488102239, 34.498256334562143 ], [ 5.29903608624636, 34.449060369940128 ], [ 5.217232293335655, 34.411956692247543 ], [ 5.185451287347007, 34.356662909565614 ], [ 5.359859246207236, 34.166209418529832 ], [ 5.37381188331176, 34.058386541805419 ], [ 5.334847852746179, 33.845996406109236 ], [ 5.121785922582262, 33.572679754966714 ], [ 4.406635775720474, 33.927645169389052 ], [ 4.335063917765581, 33.986039537494662 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.210627068984024, 34.174606839034368 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.273310580918576, 34.625845444711501 ], [ 4.434696078661091, 34.772658189523781 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "DZ-17", "NAME_1": "Djelfa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.978280469667936, 32.840760606416836 ], [ 4.176210564665837, 33.029637966319058 ], [ 4.059060093248718, 33.252518419389503 ], [ 3.640997755741807, 33.564334011305561 ], [ 3.232133822617811, 33.947411403713318 ], [ 3.14924482588799, 34.074767970765208 ], [ 3.093537632056041, 34.240029202085964 ], [ 2.949412062416741, 34.263619493145086 ], [ 2.817637160326683, 34.138846747579294 ], [ 2.650205518669736, 34.09797068909603 ], [ 2.639198439156644, 33.911005357111549 ], [ 2.474867383822698, 33.92371775986669 ], [ 2.458279250187218, 34.003247788754436 ], [ 2.48339399643578, 34.113576971699842 ], [ 2.393735386279047, 34.225301419524897 ], [ 2.293483106859867, 34.474950263443873 ], [ 2.309451124669636, 34.563420315195344 ], [ 2.364383172145665, 34.639591376241015 ], [ 2.369964227167316, 34.776301378206028 ], [ 2.460294630892463, 34.870171616926541 ], [ 2.523443230820419, 34.999104316011369 ], [ 2.620284864654764, 35.031143704418412 ], [ 2.379317661181119, 35.205009060020075 ], [ 2.360042352372659, 35.287975572015 ], [ 2.286816847119781, 35.329265042547661 ], [ 2.285731642401345, 35.439077459756959 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.619819776661359, 35.445692044452244 ], [ 2.687154167630695, 35.453701891329217 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.960264113198264, 35.804739895329874 ], [ 3.081910434918598, 35.67187978852138 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.305979444595664, 35.754587918097911 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.449691603085, 35.659115708922798 ], [ 3.680426873401473, 35.495559800844092 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.572113071161311, 35.001688137497524 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.658826124625932, 34.805576077074249 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.921084019411978, 34.672664293422372 ], [ 4.036374138855422, 34.441929023105899 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.218068475080031, 34.166235256052232 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.362400750294398, 33.957384955351415 ], [ 5.121785922582262, 33.572679754966714 ], [ 5.008046094951283, 33.441886704807587 ], [ 4.95311404837463, 33.24148550145469 ], [ 4.978280469667936, 32.840760606416836 ] ] ] } }
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,13 +9,13 @@
{ "type": "Feature", "properties": { "ISO": "TD-MC", "NAME_1": "Moyen-Chari" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 19.932091369000148, 9.063330566000076 ], [ 19.914473917000066, 9.069266663000064 ], [ 19.889462525000056, 9.046374004000128 ], [ 19.784662720000085, 9.048751120000091 ], [ 19.640072062000087, 9.013998718000053 ], [ 19.613613729000122, 9.031672059000044 ], [ 19.521319621000117, 9.008831075000046 ], [ 19.420963989000057, 9.017435202000073 ], [ 19.36939091000005, 9.00038197800005 ], [ 19.253428996000139, 9.027951356000131 ], [ 19.192037394000067, 9.020174052000115 ], [ 19.168783000000133, 9.002216492000045 ], [ 19.100570109000103, 9.015264791000092 ], [ 19.021918579000044, 8.985214946000028 ], [ 18.971895793000044, 8.938215230000054 ], [ 18.922906535000038, 8.91821645100012 ], [ 18.917738892000045, 8.898811951000141 ], [ 18.892727498000056, 8.897933452000103 ], [ 18.869783162000118, 8.849409282000124 ], [ 18.88652632700007, 8.835844218000133 ], [ 18.902959432000017, 8.844810079000069 ], [ 18.929107707000128, 8.796544292000064 ], [ 19.048686971000109, 8.745668844000136 ], [ 19.124134562000108, 8.675078837000058 ], [ 19.061296021000146, 8.625624492000028 ], [ 19.02047163900005, 8.545577698000073 ], [ 18.813042440000089, 8.276421001000045 ], [ 18.638582804000094, 8.177641500000092 ], [ 18.618635702000063, 8.138651632000105 ], [ 18.617912231000076, 8.090127462000027 ], [ 18.589283488000092, 8.047881979000053 ], [ 18.508254842000071, 8.030673727000064 ], [ 18.175430591551454, 8.021952410863994 ], [ 18.126927773098259, 8.214065959676304 ], [ 18.128662000550889, 8.378817569475245 ], [ 18.090508995693597, 8.467263170458921 ], [ 18.088774768240967, 8.52102422238994 ], [ 18.031545262304007, 8.576519500874269 ], [ 18.012468759425701, 8.621609415542082 ], [ 18.014202986878331, 8.803703299867379 ], [ 17.937510613124289, 8.859727280789343 ], [ 17.961192253719503, 8.884466864054502 ], [ 17.967458529187752, 9.007391669190156 ], [ 17.98389733129676, 9.016628322638951 ], [ 18.004323764230321, 8.999457098266475 ], [ 18.091481966819288, 9.041205145700928 ], [ 18.165456576759595, 9.160101629286771 ], [ 18.141736600763522, 9.187974781510945 ], [ 17.995813352004575, 9.200135051791221 ], [ 17.795168885635576, 9.291337082040684 ], [ 17.73436753243567, 9.431180195209834 ], [ 17.713087058321094, 9.455500736669649 ], [ 17.624925096316133, 9.45246066932441 ], [ 17.579324080741742, 9.558863037199387 ], [ 17.536532016639228, 9.587063707224274 ], [ 17.561026646162759, 9.653622951837747 ], [ 17.543818394003665, 9.68395701700274 ], [ 17.427288039311577, 9.774804185564676 ], [ 17.329516228591046, 9.815835272779623 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.827160271829086, 10.47388296092987 ], [ 17.931288283027811, 10.382415676542223 ], [ 18.038826938912109, 10.344691881225344 ], [ 18.145125360447025, 10.341487942114838 ], [ 18.156752556685149, 10.311257228838031 ], [ 18.218505894431473, 10.288571275344054 ], [ 18.264756300562794, 10.247798570547559 ], [ 18.349919061315632, 10.236378078985126 ], [ 18.381596713617398, 10.204803779470808 ], [ 18.453116895628227, 10.179120592441393 ], [ 18.500762566639139, 10.10791046789376 ], [ 18.607060988174055, 10.020784003279118 ], [ 18.600394728433969, 9.913090317763931 ], [ 18.653466423936322, 9.900171210333099 ], [ 18.697701450261718, 9.866684882001664 ], [ 18.762710402163293, 9.884513250885789 ], [ 18.795266553608542, 9.994842433831252 ], [ 18.998871697870527, 10.007038071749605 ], [ 19.049462924674231, 10.035666816370451 ], [ 19.111991407877156, 10.120312812286443 ], [ 19.164132928291963, 10.137521064445536 ], [ 19.19358849521285, 10.202995103441253 ], [ 19.302677442909669, 10.299940090063103 ], [ 19.281593458970917, 10.34923940747268 ], [ 19.316371697595741, 10.357404283081166 ], [ 19.336835564809462, 10.419622707921633 ], [ 19.378590122436208, 10.469128730006844 ], [ 19.442203810357569, 10.502046617557369 ], [ 19.464372999914019, 10.482357895800249 ], [ 19.5337227715886, 10.495380356917906 ], [ 19.557803989062847, 10.552741197148521 ], [ 19.580128208250187, 10.541165675955142 ], [ 19.61428633014998, 10.595374253918692 ], [ 19.658366326844487, 10.618473619461952 ], [ 19.605914748067164, 10.359161282267337 ], [ 19.640951369110383, 10.225215968941825 ], [ 19.604519484086893, 10.10868561424968 ], [ 19.572066685429149, 10.075612697967642 ], [ 19.651028272636665, 9.92812816048621 ], [ 19.932091369000148, 9.063330566000076 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-MA", "NAME_1": "Mandoul" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 18.175430591551454, 8.021952410863994 ], [ 18.07045210800004, 8.019201558000049 ], [ 17.858785441000123, 7.960445455000084 ], [ 17.639884074000122, 7.985043437000073 ], [ 17.580766235000084, 7.940601706000095 ], [ 17.503664998000119, 7.926235657000092 ], [ 17.466354614000096, 7.884119364000057 ], [ 17.41912235500007, 7.898227030000086 ], [ 17.385739380000075, 7.870476787000129 ], [ 17.233790205553532, 7.810801425045724 ], [ 17.080177443034529, 8.136816311123823 ], [ 17.166735466868261, 8.158468735843485 ], [ 17.249469434866512, 8.290553697195378 ], [ 17.033461948010483, 8.632651679231572 ], [ 17.117384474413939, 8.783650214186025 ], [ 17.125911086127701, 8.995936997994022 ], [ 17.104827101289686, 9.069782619971932 ], [ 17.185649041370141, 9.156547349380673 ], [ 17.06322757329383, 9.263362534853115 ], [ 17.210040317206847, 9.3859907094037 ], [ 17.218256869658774, 9.507326971862256 ], [ 17.197637973713483, 9.555386054023188 ], [ 17.234224888367862, 9.592024643722311 ], [ 17.455193311721928, 9.641892401912799 ], [ 17.579324080741742, 9.558863037199387 ], [ 17.624925096316133, 9.45246066932441 ], [ 17.713087058321094, 9.455500736669649 ], [ 17.795168885635576, 9.291337082040684 ], [ 17.995813352004575, 9.200135051791221 ], [ 18.141736600763522, 9.187974781510945 ], [ 18.165456576759595, 9.160101629286771 ], [ 18.091481966819288, 9.041205145700928 ], [ 18.004323764230321, 8.999457098266475 ], [ 17.98389733129676, 9.016628322638951 ], [ 17.967458529187752, 9.007391669190156 ], [ 17.961192253719503, 8.884466864054502 ], [ 17.937510613124289, 8.859727280789343 ], [ 18.014202986878331, 8.803703299867379 ], [ 18.012468759425701, 8.621609415542082 ], [ 18.031545262304007, 8.576519500874269 ], [ 18.088774768240967, 8.52102422238994 ], [ 18.090508995693597, 8.467263170458921 ], [ 18.128662000550889, 8.378817569475245 ], [ 18.126927773098259, 8.214065959676304 ], [ 18.175430591551454, 8.021952410863994 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-LR", "NAME_1": "Logone Oriental" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.233790205553532, 7.810801425045724 ], [ 17.211073038000023, 7.768312480000077 ], [ 17.188645467000129, 7.764126689000051 ], [ 17.101932414000061, 7.677723694000065 ], [ 17.059351033000098, 7.696533916000078 ], [ 17.038990519000066, 7.662479147000028 ], [ 16.997546020000101, 7.66671661400008 ], [ 16.981939738000079, 7.648733215000092 ], [ 16.880653930000051, 7.632868551000087 ], [ 16.854815715000115, 7.611474508000072 ], [ 16.853885539000089, 7.567497864000117 ], [ 16.805516398000123, 7.543675029000056 ], [ 16.768619425000111, 7.55023793600013 ], [ 16.709811646000048, 7.627752584000092 ], [ 16.614210245000066, 7.679584046000073 ], [ 16.613073364000059, 7.748210347000025 ], [ 16.54930464600011, 7.794719137000058 ], [ 16.54868453000006, 7.870011699000116 ], [ 16.491737101000069, 7.84923777300007 ], [ 16.450912720000076, 7.792083638000051 ], [ 16.407401164000078, 7.796062724000123 ], [ 16.392208292000134, 7.783608703000127 ], [ 16.38714400200007, 7.69467356400007 ], [ 16.370814249000119, 7.672504374000056 ], [ 16.2827576090001, 7.660102031000079 ], [ 16.207206665000058, 7.613541565000034 ], [ 16.042978964000042, 7.583930970000083 ], [ 15.975696248000077, 7.515201315000112 ], [ 15.925260050000077, 7.488174541000063 ], [ 15.758345174000112, 7.455566711000102 ], [ 15.720001262000096, 7.468640849000067 ], [ 15.668531535000113, 7.516286519000104 ], [ 15.515569295000148, 7.512204081000093 ], [ 15.48104943900006, 7.523262838000093 ], [ 15.548952271000104, 7.630801493000121 ], [ 15.562904907000103, 7.792445374000039 ], [ 15.487870727000114, 7.804951071000048 ], [ 15.440741821000103, 7.83941925100001 ], [ 15.345347127000082, 8.135990296000088 ], [ 15.210706216000091, 8.421822448000057 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.431440870599658, 8.355149237047613 ], [ 15.481722039040847, 8.347914537425936 ], [ 15.520065951982076, 8.317012030580713 ], [ 15.519755893619561, 8.28362905503684 ], [ 15.558048129717406, 8.278358059277025 ], [ 15.570812209315989, 8.258152574481699 ], [ 15.664708285558902, 8.276394355415164 ], [ 15.728166944748693, 8.322696438389926 ], [ 15.813639763863989, 8.315823473074829 ], [ 15.85740970219598, 8.366466375822597 ], [ 15.867951693715668, 8.413543606052542 ], [ 16.004119093321492, 8.511005357511237 ], [ 16.145195754380552, 8.597511705400848 ], [ 16.365027297072061, 8.648568020197956 ], [ 16.409572380860652, 8.706548977153545 ], [ 16.513493687383743, 8.753161119390143 ], [ 16.5971061536261, 8.881111964745344 ], [ 16.596175977639234, 8.90266103847614 ], [ 16.569820997940667, 8.945345771190375 ], [ 16.549305453883562, 8.952167060561408 ], [ 16.542484165411906, 8.980124009815199 ], [ 16.520780063848861, 8.980124009815199 ], [ 16.489722528272068, 9.021465155392605 ], [ 16.435358920677629, 9.127608547296632 ], [ 16.568735793222288, 9.136083482166953 ], [ 16.91910200365453, 9.036141262009608 ], [ 16.998993767748175, 9.037484849146438 ], [ 17.104827101289686, 9.069782619971932 ], [ 17.125911086127701, 8.995936997994022 ], [ 17.117384474413939, 8.783650214186025 ], [ 17.033461948010483, 8.632651679231572 ], [ 17.249469434866512, 8.284559231023707 ], [ 17.166735466868261, 8.158468735843485 ], [ 17.080177443034529, 8.136816311123823 ], [ 17.233790205553532, 7.810801425045724 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-HL", "NAME_1": "Hadjer-Lamis" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.560692179000085, 12.766224467000143 ], [ 14.54901330600012, 12.81821095800008 ], [ 14.49010217300011, 12.873608094000119 ], [ 14.507672159000094, 12.952440491000047 ], [ 14.481040720000067, 13.000507963000089 ], [ 15.01405032666122, 13.00334096901048 ], [ 15.043195835219592, 13.097960517463889 ], [ 15.002578159154723, 13.188239244345652 ], [ 14.996635369826436, 13.25965607356892 ], [ 15.17404056222216, 13.203742174162016 ], [ 15.185512729728657, 13.173769843303603 ], [ 15.269900343226254, 13.102042954818501 ], [ 15.286540154604438, 13.143694158758422 ], [ 15.327984652969349, 13.144159246751826 ], [ 15.383175082863772, 13.19878123586534 ], [ 15.372012973719734, 13.221828925464536 ], [ 15.305918817099723, 13.241052558328875 ], [ 15.21450320775682, 13.372414049268968 ], [ 15.356820103165205, 13.405125230345107 ], [ 15.441052687031856, 13.31867055929888 ], [ 15.743049757840026, 13.182141425386476 ], [ 15.847642857032213, 13.168757229062862 ], [ 15.883609653162921, 13.187877509139696 ], [ 15.965258416442737, 13.121990058094696 ], [ 15.986807489274213, 13.051503403958918 ], [ 15.999984979123496, 13.047575995335876 ], [ 16.246894971925371, 13.051968491952323 ], [ 16.324926384944717, 13.074447739871289 ], [ 16.447812941014377, 13.050986640021392 ], [ 16.489412469010233, 13.081630764448221 ], [ 16.719372592970785, 13.113050035230913 ], [ 16.818694696403099, 13.159507147836564 ], [ 16.880137973988269, 13.143177394820896 ], [ 16.974034051130559, 13.16291779252208 ], [ 16.997288446304765, 13.182089749442355 ], [ 17.059661898977424, 13.170565904193097 ], [ 17.131698845825042, 13.144004218020257 ], [ 17.072684360095138, 13.063957424295666 ], [ 16.969848260089123, 12.970578111090958 ], [ 16.941116163580091, 12.908411363093933 ], [ 17.038887974300565, 12.829914862980502 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.122203616603599, 11.564674405499375 ], [ 16.989452205454882, 11.687214169498304 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.060192328313292, 12.463299342090977 ], [ 15.978499152014251, 12.371394519541411 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.069662568272918, 12.259066401793007 ], [ 14.90486270493879, 12.248664494924668 ], [ 14.908267863000049, 12.326897278000075 ], [ 14.877468709000084, 12.447044983000126 ], [ 14.849563436000096, 12.457018534000042 ], [ 14.863826131000053, 12.495465800000076 ], [ 14.819177694000132, 12.638816223000035 ], [ 14.768844849000061, 12.633235168000112 ], [ 14.734118286000125, 12.680415751000041 ], [ 14.713654419000051, 12.652510478000053 ], [ 14.70207889800011, 12.668995260000131 ], [ 14.709933716000137, 12.718242900000106 ], [ 14.664458455000101, 12.715969136000098 ], [ 14.618363078000073, 12.759196472000042 ], [ 14.575264933000142, 12.744907939000058 ], [ 14.560692179000085, 12.766224467000143 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-HL", "NAME_1": "Hadjer-Lamis" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.575264933000142, 12.744907939000058 ], [ 14.54901330600012, 12.81821095800008 ], [ 14.49010217300011, 12.873608094000119 ], [ 14.507672159000094, 12.952440491000047 ], [ 14.481040720000067, 13.000507963000089 ], [ 15.01405032666122, 13.00334096901048 ], [ 15.043195835219592, 13.097960517463889 ], [ 15.002578159154723, 13.188239244345652 ], [ 14.996635369826436, 13.25965607356892 ], [ 15.17404056222216, 13.203742174162016 ], [ 15.185512729728657, 13.173769843303603 ], [ 15.269900343226254, 13.102042954818501 ], [ 15.286540154604438, 13.143694158758422 ], [ 15.327984652969349, 13.144159246751826 ], [ 15.383175082863772, 13.19878123586534 ], [ 15.372012973719734, 13.221828925464536 ], [ 15.305918817099723, 13.241052558328875 ], [ 15.21450320775682, 13.372414049268968 ], [ 15.356820103165205, 13.405125230345107 ], [ 15.441052687031856, 13.31867055929888 ], [ 15.743049757840026, 13.182141425386476 ], [ 15.847642857032213, 13.168757229062862 ], [ 15.883609653162921, 13.187877509139696 ], [ 15.965258416442737, 13.121990058094696 ], [ 15.986807489274213, 13.051503403958918 ], [ 15.999984979123496, 13.047575995335876 ], [ 16.246894971925371, 13.051968491952323 ], [ 16.324926384944717, 13.074447739871289 ], [ 16.447812941014377, 13.050986640021392 ], [ 16.489412469010233, 13.081630764448221 ], [ 16.719372592970785, 13.113050035230913 ], [ 16.818694696403099, 13.159507147836564 ], [ 16.880137973988269, 13.143177394820896 ], [ 16.974034051130559, 13.16291779252208 ], [ 16.997288446304765, 13.182089749442355 ], [ 17.059661898977424, 13.170565904193097 ], [ 17.131698845825042, 13.144004218020257 ], [ 17.072684360095138, 13.063957424295666 ], [ 16.969848260089123, 12.970578111090958 ], [ 16.941116163580091, 12.908411363093933 ], [ 17.038887974300565, 12.829914862980502 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.122203616603599, 11.564674405499375 ], [ 16.989452205454882, 11.687214169498304 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.060192328313292, 12.463299342090977 ], [ 15.978499152014251, 12.371394519541411 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.069662568272918, 12.259066401793007 ], [ 14.90486270493879, 12.248664494924668 ], [ 14.908267863000049, 12.326897278000075 ], [ 14.877468709000084, 12.447044983000126 ], [ 14.849563436000096, 12.457018534000042 ], [ 14.863826131000053, 12.495465800000076 ], [ 14.819177694000132, 12.638816223000035 ], [ 14.768844849000061, 12.633235168000112 ], [ 14.734118286000125, 12.680415751000041 ], [ 14.713654419000051, 12.652510478000053 ], [ 14.70207889800011, 12.668995260000131 ], [ 14.709933716000137, 12.718242900000106 ], [ 14.664458455000101, 12.715969136000098 ], [ 14.618363078000073, 12.759196472000042 ], [ 14.575264933000142, 12.744907939000058 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-CB", "NAME_1": "Chari-Baguirmi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.964801879000106, 12.092441305000079 ], [ 14.898035929000059, 12.152799377000079 ], [ 14.911575154000047, 12.1801362100001 ], [ 14.898035929000059, 12.207473043000036 ], [ 14.90486270493879, 12.248664494924668 ], [ 15.069662568272918, 12.259066401793007 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.978499152014251, 12.371394519541411 ], [ 16.060192328313292, 12.463299342090977 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.989452205454882, 11.687214169498304 ], [ 17.122203616603599, 11.564674405499375 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.254955027752317, 11.237901701202532 ], [ 17.316224909751782, 10.74774264520687 ], [ 17.295801616351525, 10.584356293508108 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.329516228591046, 9.815835272779623 ], [ 17.227558627728456, 9.831854967432776 ], [ 17.156193475348573, 9.874332994572001 ], [ 17.130148553113202, 9.916966051342172 ], [ 17.028966098606531, 9.938980211267733 ], [ 16.936930373437974, 9.938515123274328 ], [ 16.879827914726491, 10.000113430490387 ], [ 16.73988813612857, 10.026468411088274 ], [ 16.632969597868623, 10.095611477187902 ], [ 16.607803175675997, 10.145427558535005 ], [ 16.605012647715512, 10.21312368381092 ], [ 16.568115675597937, 10.25167430322648 ], [ 16.500729607785161, 10.204131985003073 ], [ 16.4402165061868, 10.192866523071586 ], [ 16.219403109765665, 10.195863755707762 ], [ 16.196303745121725, 10.211935126305036 ], [ 16.177390170619844, 10.263043117945529 ], [ 16.129899530139198, 10.302627265236083 ], [ 16.108453810095227, 10.446029364463584 ], [ 16.13398196839313, 10.746476142559914 ], [ 16.07481245303228, 10.767301744080214 ], [ 16.063340284626406, 10.75123037438226 ], [ 16.001225212573502, 10.789419256793281 ], [ 15.921798537372581, 10.775776678950535 ], [ 15.745581903382117, 10.811226711143775 ], [ 15.665380080026637, 10.8888447130131 ], [ 15.591534458048784, 10.901298733349847 ], [ 15.548746371647724, 10.925948390705628 ], [ 15.513864780235394, 11.009974269896645 ], [ 15.438262159970691, 11.050281886699679 ], [ 15.432060988224009, 11.116272691431561 ], [ 15.342867466060682, 11.170377915708229 ], [ 15.23129804876595, 11.184537258387763 ], [ 15.150579460573624, 11.117926336930907 ], [ 15.028830280692887, 11.080218266259976 ], [ 15.021232544000043, 11.182548523000051 ], [ 15.033324829000037, 11.26026987700007 ], [ 15.063090454000104, 11.30993092900006 ], [ 15.049447876000102, 11.337267762000096 ], [ 15.060609985000042, 11.416125997000023 ], [ 15.135644165000116, 11.53082183800008 ], [ 15.069394979000037, 11.660788066000094 ], [ 15.076112915000039, 11.72145619700008 ], [ 15.097196899000068, 11.72765736900007 ], [ 15.083554321000065, 11.748172913000133 ], [ 15.110942831000102, 11.782899476000068 ], [ 15.079833618000066, 11.851215719000038 ], [ 15.044176880000066, 11.877312317000118 ], [ 15.042006469000086, 11.902375387000035 ], [ 15.063400512000101, 11.927283427000106 ], [ 15.048310995000094, 11.962785136000051 ], [ 15.081177205000103, 11.97167348300006 ], [ 15.049447876000102, 12.011929423000097 ], [ 15.040559529000092, 12.055647685000082 ], [ 15.104691864338065, 12.07118935600397 ], [ 15.125295796463831, 12.113568067412359 ], [ 15.1074616269525, 12.154100271010691 ], [ 15.050006169049425, 12.180917084955468 ], [ 15.021597762826502, 12.15508291524435 ], [ 15.011827432789801, 12.108616027907374 ], [ 14.964801879000106, 12.092441305000079 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-ND", "NAME_1": "Ville de N'Djamena" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.040559529000092, 12.055647685000082 ], [ 15.049447876000102, 12.084586487000138 ], [ 15.011827432789801, 12.108616027907374 ], [ 15.013491321567244, 12.134006169049428 ], [ 15.036189355654244, 12.17453837264776 ], [ 15.07432549192788, 12.179295797063389 ], [ 15.118810643996028, 12.134644813607224 ], [ 15.12252603384934, 12.09064481340738 ], [ 15.088478983618188, 12.061461626852577 ], [ 15.040559529000092, 12.055647685000082 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-ME", "NAME_1": "Mayo-Kebbi Est" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.382967570000119, 9.930196025000086 ], [ 15.681243937000147, 9.991277568000072 ], [ 15.476398560000092, 10.132741801000108 ], [ 15.439191528000094, 10.185245057000074 ], [ 15.301422160000072, 10.311748963000056 ], [ 15.278374471000092, 10.39427622500007 ], [ 15.241167440000083, 10.432904358000073 ], [ 15.218326457000074, 10.487216288000127 ], [ 15.138227986000118, 10.521658631000079 ], [ 15.132336873000014, 10.565402731000077 ], [ 15.149906860000101, 10.623125305000059 ], [ 15.065880981000078, 10.793114929000041 ], [ 15.079006795000026, 10.898147278000081 ], [ 15.035185181000116, 10.994627177000098 ], [ 15.028830280692887, 11.080218266259976 ], [ 15.150579460573624, 11.117926336930907 ], [ 15.236879102888281, 11.185777492737088 ], [ 15.342867466060682, 11.170377915708229 ], [ 15.421984083798407, 11.123972479945962 ], [ 15.438262159970691, 11.050281886699679 ], [ 15.513864780235394, 11.009974269896645 ], [ 15.548746371647724, 10.925948390705628 ], [ 15.591534458048784, 10.901298733349847 ], [ 15.665380080026637, 10.8888447130131 ], [ 15.745581903382117, 10.811226711143775 ], [ 15.921798537372581, 10.775776678950535 ], [ 16.001225212573502, 10.789419256793281 ], [ 16.063340284626406, 10.75123037438226 ], [ 16.07481245303228, 10.767301744080214 ], [ 16.13398196839313, 10.746476142559914 ], [ 16.108453810095227, 10.446029364463584 ], [ 16.126488884554362, 10.315029608729446 ], [ 16.177390170619844, 10.263043117945529 ], [ 16.196303745121725, 10.211935126305036 ], [ 16.246274855200397, 10.149716702363889 ], [ 16.234647658062954, 10.023781235915294 ], [ 16.1584249201739, 9.855729478432693 ], [ 15.973940056888125, 9.683233547490204 ], [ 15.912186720041063, 9.731189276863631 ], [ 15.830692987291513, 9.741317857233298 ], [ 15.71633304203624, 9.604065252909095 ], [ 15.67416507325953, 9.495906480299823 ], [ 15.523941684660997, 9.325839342111919 ], [ 15.438262159970691, 9.186829739500865 ], [ 15.29956135846993, 9.244814462817203 ], [ 15.08963651271489, 9.394434942318696 ], [ 15.071186992885373, 9.527896838568211 ], [ 15.022724939804107, 9.591270293289369 ], [ 15.026452789764448, 9.641596272250126 ], [ 14.994766062403869, 9.671419074630592 ], [ 14.94071223348277, 9.695650101171225 ], [ 14.792935054808936, 9.662967116539164 ], [ 14.737544393558892, 9.665827298790816 ], [ 14.722846171713456, 9.787266672119188 ], [ 14.732464640000074, 9.923813985000052 ], [ 14.77256555200006, 9.92174692800009 ], [ 14.898139282000074, 9.960478414000107 ], [ 15.033014770000136, 9.942856751000122 ], [ 15.109599244000037, 9.981536560000038 ], [ 15.214915812000072, 9.984094543000126 ], [ 15.382967570000119, 9.930196025000086 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-MO", "NAME_1": "Mayo-Kebbi Ouest" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.440492798000037, 9.995308329000068 ], [ 14.732464640000074, 9.923813985000052 ], [ 14.722846171713456, 9.787266672119188 ], [ 14.737544393558892, 9.665827298790816 ], [ 14.792935054808936, 9.662967116539164 ], [ 14.94071223348277, 9.695650101171225 ], [ 14.994766062403869, 9.671419074630592 ], [ 15.026452789764448, 9.641596272250126 ], [ 15.022724939804107, 9.591270293289369 ], [ 15.071186992885373, 9.527896838568211 ], [ 15.08963651271489, 9.394434942318696 ], [ 15.220104139594355, 9.292488794322367 ], [ 15.442189568593676, 9.18269562440355 ], [ 15.463893670156722, 9.02673615205174 ], [ 15.41779829275697, 9.004308580076895 ], [ 15.37712894074798, 8.883282375980855 ], [ 15.37542361840525, 8.746236477231605 ], [ 15.305918817099723, 8.671770738528721 ], [ 15.264939405828898, 8.502117011490895 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.210706216000091, 8.421822448000057 ], [ 15.183703247000068, 8.479147644000122 ], [ 15.051928345000078, 8.643788758000056 ], [ 14.955086710000074, 8.676215719000069 ], [ 14.940100545000092, 8.729649149000082 ], [ 14.899379516000124, 8.774323425000119 ], [ 14.846049438000136, 8.810987854000089 ], [ 14.793856242000061, 8.813623353000096 ], [ 14.349542277000126, 9.168356221000082 ], [ 14.321326945000067, 9.243157858000103 ], [ 14.036486450000069, 9.568771057000077 ], [ 13.947602987000039, 9.637759094000032 ], [ 14.006720825000087, 9.739277446000102 ], [ 14.119788859000096, 9.85200958300004 ], [ 14.173532348000094, 9.975025329000061 ], [ 14.440492798000037, 9.995308329000068 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-X01~", "NAME_1": "Ennedi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 20.893328898000107, 21.020940247000013 ], [ 23.981305786000036, 19.496123759000014 ], [ 23.98440637200008, 15.721160381000018 ], [ 23.972624146000072, 15.691084697000051 ], [ 23.707524048000067, 15.748858948000063 ], [ 23.592699016000068, 15.749013977000104 ], [ 23.395811808000076, 15.688345846000018 ], [ 23.320570923000105, 15.681317851000088 ], [ 23.166781860000128, 15.712943827000103 ], [ 23.119249041000103, 15.707223545000105 ], [ 23.066220330603755, 15.788442288081114 ], [ 22.997645705285038, 15.816502590122411 ], [ 22.957183057951738, 15.820946764481562 ], [ 22.875534294671922, 15.797485662833083 ], [ 22.75931399924167, 15.838413398159787 ], [ 22.56082482290725, 15.80957794796393 ], [ 22.470701124757113, 15.908900051396245 ], [ 22.370913934230657, 15.948432521843415 ], [ 22.311796094813928, 15.927606920323115 ], [ 22.270971714073312, 15.931430976158595 ], [ 22.1771273128752, 15.999798895902302 ], [ 22.144209426224052, 15.978921617538617 ], [ 22.018222283831335, 15.986001288428724 ], [ 21.915954623706909, 15.944453437276309 ], [ 21.731779818783593, 15.999850571846423 ], [ 21.388803337603917, 15.923782864487578 ], [ 21.312632276558304, 15.887919420245055 ], [ 21.096624789702275, 15.985174465229363 ], [ 20.9587003918096, 15.918718573403453 ], [ 20.896326939136941, 15.839446926034782 ], [ 20.832868279947149, 15.799449368493526 ], [ 20.660268996217212, 15.737024238078106 ], [ 20.567199741374964, 15.685451158444209 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.893328898000107, 21.020940247000013 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-BO", "NAME_1": "Borkou" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 19.185837029855008, 21.864177058924099 ], [ 20.893328898000107, 21.020940247000013 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.291764356739634, 15.669018053540299 ], [ 20.11286054937483, 15.764102688188473 ], [ 19.863780145337444, 15.798674221238286 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.046982455975524, 15.614654445945916 ], [ 18.935413038680792, 15.521998603153008 ], [ 18.791235793097371, 15.4505300970863 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.270647413946961, 15.431461492953531 ], [ 18.084922316311861, 15.53166209642859 ], [ 17.84281823127634, 15.792524726335046 ], [ 15.468517057815973, 16.904909712513188 ], [ 15.490247843000077, 17.124537252000025 ], [ 17.359841679150179, 20.416379890019869 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.952986281791482, 20.83652398359493 ], [ 18.150701149038809, 20.935381417218593 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.917118774000073, 21.996849671000078 ], [ 19.185837029855008, 21.864177058924099 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-BO", "NAME_1": "Borkou" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 18.917118774000073, 21.996849671000078 ], [ 20.893328898000107, 21.020940247000013 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.291764356739634, 15.669018053540299 ], [ 20.11286054937483, 15.764102688188473 ], [ 19.863780145337444, 15.798674221238286 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.046982455975524, 15.614654445945916 ], [ 18.935413038680792, 15.521998603153008 ], [ 18.791235793097371, 15.4505300970863 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.270647413946961, 15.431461492953531 ], [ 18.084922316311861, 15.53166209642859 ], [ 17.84281823127634, 15.792524726335046 ], [ 15.468517057815973, 16.904909712513188 ], [ 15.490247843000077, 17.124537252000025 ], [ 17.359841679150179, 20.416379890019869 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.952986281791482, 20.83652398359493 ], [ 18.150701149038809, 20.935381417218593 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.917118774000073, 21.996849671000078 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-TI", "NAME_1": "Tibesti" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.985101359000112, 23.444719951000067 ], [ 18.917118774000073, 21.996849671000078 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.150701149038809, 20.935381417218593 ], [ 17.952986281791482, 20.83652398359493 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.359841679150179, 20.416379890019869 ], [ 15.490247843000077, 17.124537252000025 ], [ 15.736020955000129, 19.903540751000023 ], [ 15.970321899000112, 20.336330871000101 ], [ 15.953992147000065, 20.374571432 ], [ 15.669461711000054, 20.671865946000068 ], [ 15.570242961000105, 20.751912740000122 ], [ 15.544198039000037, 20.798989970000022 ], [ 15.544301391000147, 20.890302226000117 ], [ 15.569002727000054, 20.928904521 ], [ 15.609310343000118, 20.950660299000035 ], [ 15.301008748000044, 21.401330465000072 ], [ 15.266592245000083, 21.440656230000045 ], [ 15.184530070000108, 21.491195780000069 ], [ 15.172024373000113, 21.993387350000134 ], [ 14.979909027000133, 22.995663760000085 ], [ 15.985101359000112, 23.444719951000067 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-KA", "NAME_1": "Kanem" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 13.608052034873481, 14.518266228003995 ], [ 13.665604695000098, 14.566889751000105 ], [ 13.648344767000111, 14.649417013000033 ], [ 13.709994751000067, 14.705124207000068 ], [ 13.764358358000038, 14.719076843000053 ], [ 13.772006469000104, 14.762872620000053 ], [ 13.755159952000042, 14.847208558000062 ], [ 13.833914836000133, 15.019601136000105 ], [ 14.368972615000104, 15.749634095000076 ], [ 15.468517057815973, 16.904909712513188 ], [ 16.671386334311876, 16.341868536703601 ], [ 16.489081502309489, 16.037375794157583 ], [ 16.131673857462545, 14.811978153269138 ], [ 16.111250563162969, 14.577110272421123 ], [ 16.049980681163504, 14.474993802721826 ], [ 15.774266211716281, 14.148221098424926 ], [ 15.590456565717943, 14.076739569275674 ], [ 15.6007221766871, 13.753926464834478 ], [ 15.71617801330467, 13.754923000895758 ], [ 15.758242629293875, 13.790166327513987 ], [ 15.841751742748727, 13.765775050777961 ], [ 15.85740970219598, 13.74510447798923 ], [ 15.865006137922933, 13.700921129406595 ], [ 15.840201450036886, 13.642630113189171 ], [ 15.8581848485519, 13.581238512447385 ], [ 15.898389112567486, 13.521500556305625 ], [ 15.899939406178646, 13.250457669186062 ], [ 15.871672397663019, 13.171496080179963 ], [ 15.776226026909626, 13.171857815385863 ], [ 15.560136878913795, 13.264834071335315 ], [ 15.375818277791268, 13.751985246721745 ], [ 15.213262974306815, 13.750582180223432 ], [ 14.915813428846661, 13.678751938950825 ], [ 14.839642367800991, 13.685883287583636 ], [ 14.805174188438059, 13.663817449915371 ], [ 14.643788689796168, 13.908918769385707 ], [ 14.477442253756976, 14.104514064972079 ], [ 14.356571079291882, 14.134951483823897 ], [ 14.30737511556913, 14.198823554163653 ], [ 14.261744826162783, 14.213189602418197 ], [ 14.149400261612755, 14.308480943540701 ], [ 14.131881951990465, 14.378347480052128 ], [ 14.087336867302554, 14.416691392094037 ], [ 14.017366978003622, 14.428060207712406 ], [ 13.995817905172146, 14.452348130761607 ], [ 13.950497674128371, 14.466145738235241 ], [ 13.78492638534442, 14.476377672291733 ], [ 13.735575391990722, 14.515703437163893 ], [ 13.608052034873481, 14.518266228003995 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-OD", "NAME_1": "Ouaddaï" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 22.180762493000145, 13.920554946000053 ], [ 22.073721964000129, 13.771356913000147 ], [ 22.112892700000089, 13.729834900000071 ], [ 22.132116333000113, 13.63865183500009 ], [ 22.195885050000129, 13.580464173000081 ], [ 22.215625448000083, 13.464993184000022 ], [ 22.275983521000086, 13.376316427000106 ], [ 22.267611939000119, 13.334561869000069 ], [ 22.139971151000054, 13.193511048000047 ], [ 22.016051066000074, 13.140232646000072 ], [ 21.964064575000037, 13.098348898000083 ], [ 21.852960246000066, 12.90572499600006 ], [ 21.809448689905082, 12.793664652701523 ], [ 21.574481710962175, 12.881976869836649 ], [ 21.482576888412666, 12.851341928387285 ], [ 21.380460417813993, 12.79007204638782 ], [ 21.288555595264484, 12.698167222938991 ], [ 21.237497360414807, 12.677743929538678 ], [ 21.094534302116358, 12.698167222938991 ], [ 20.982206185267216, 12.667532282388891 ], [ 20.849454774118499, 12.718590517238567 ], [ 20.839243126968711, 12.881976869836649 ], [ 20.686068421520417, 12.91261181038675 ], [ 20.675856774370629, 13.045363221535411 ], [ 20.410353952972514, 13.045363221535411 ], [ 20.294554884700176, 13.092327786498117 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.000050896831397, 14.155983791818528 ], [ 20.288043653691659, 14.441392727192579 ], [ 20.434959751291444, 14.437878729719614 ], [ 20.492113885047729, 14.464130357529939 ], [ 20.551541781927654, 14.470228176489115 ], [ 20.7355615581194, 14.411678778752616 ], [ 20.844185417822757, 14.355041408933857 ], [ 20.89896243656716, 14.264762682052094 ], [ 20.958080275084626, 14.204146225867589 ], [ 21.000299919805457, 14.196859849402529 ], [ 21.168196648556489, 14.101465155492519 ], [ 21.232275425370574, 14.093403631772162 ], [ 21.333096143772025, 14.142651272338355 ], [ 21.421721226053705, 14.151901353564597 ], [ 21.702272576817961, 14.110870266349707 ], [ 22.042251825361461, 13.999455877786488 ], [ 22.084626498813861, 13.973462633293877 ], [ 22.124520705366251, 13.920494290579029 ], [ 22.180762493000145, 13.920554946000053 ] ] ] } },
@@ -24,6 +24,6 @@
{ "type": "Feature", "properties": { "ISO": "TD-TA", "NAME_1": "Tandjilé" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 16.435358920677629, 9.127608547296632 ], [ 16.420527785329, 9.180835273329194 ], [ 16.357430861345165, 9.238712877497278 ], [ 16.304307488999427, 9.192410792723877 ], [ 16.204210239211136, 9.155927231756323 ], [ 16.146952751768083, 9.088541164842866 ], [ 16.042359652575897, 9.117118231721008 ], [ 16.001431919047832, 9.113965969453886 ], [ 15.818135614167261, 9.025082506552394 ], [ 15.7552970717025, 9.032213853386622 ], [ 15.569727003698233, 8.991389471746686 ], [ 15.489008416405284, 9.007719223863091 ], [ 15.452421501750905, 9.049990546326626 ], [ 15.438262159970691, 9.186829739500865 ], [ 15.523941684660997, 9.325839342111919 ], [ 15.67416507325953, 9.495906480299823 ], [ 15.71633304203624, 9.604065252909095 ], [ 15.830692987291513, 9.741317857233298 ], [ 15.912186720041063, 9.731189276863631 ], [ 15.973940056888125, 9.683233547490204 ], [ 16.166486443894257, 9.866788234789169 ], [ 16.211496615676253, 9.955206611495896 ], [ 16.243329299407606, 10.071581936557095 ], [ 16.246274855200397, 10.149716702363889 ], [ 16.219403109765665, 10.195863755707762 ], [ 16.4402165061868, 10.192866523071586 ], [ 16.500729607785161, 10.204131985003073 ], [ 16.568115675597937, 10.25167430322648 ], [ 16.605012647715512, 10.21312368381092 ], [ 16.607803175675997, 10.145427558535005 ], [ 16.632969597868623, 10.095611477187902 ], [ 16.73988813612857, 10.026468411088274 ], [ 16.879827914726491, 10.000113430490387 ], [ 16.936930373437974, 9.938515123274328 ], [ 17.028966098606531, 9.938980211267733 ], [ 17.130148553113202, 9.916966051342172 ], [ 17.156193475348573, 9.874332994572001 ], [ 17.227558627728456, 9.831854967432776 ], [ 17.347499627106117, 9.813974920805947 ], [ 17.427288039311577, 9.774804185564676 ], [ 17.558391147833163, 9.663183092325824 ], [ 17.536532016639228, 9.587063707224274 ], [ 17.443721144215431, 9.643752752987155 ], [ 17.306881951940511, 9.600137844286053 ], [ 17.254585401894758, 9.601481432322203 ], [ 17.204459263084516, 9.568201809565835 ], [ 17.197637973713483, 9.549701646213975 ], [ 17.218256869658774, 9.507326971862256 ], [ 17.210040317206847, 9.3859907094037 ], [ 17.06322757329383, 9.263362534853115 ], [ 17.185649041370141, 9.156547349380673 ], [ 17.104827101289686, 9.069782619971932 ], [ 16.998993767748175, 9.037484849146438 ], [ 16.91910200365453, 9.036141262009608 ], [ 16.568735793222288, 9.136083482166953 ], [ 16.435358920677629, 9.127608547296632 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-LO", "NAME_1": "Logone Occidental" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 16.357430861345165, 9.238712877497278 ], [ 16.420527785329, 9.180835273329194 ], [ 16.489722528272068, 9.021465155392605 ], [ 16.592455274591259, 8.917492173824769 ], [ 16.5971061536261, 8.881111964745344 ], [ 16.490497673728669, 8.732438869758028 ], [ 16.409572380860652, 8.706548977153545 ], [ 16.365027297072061, 8.648568020197956 ], [ 16.145195754380552, 8.597511705400848 ], [ 16.004119093321492, 8.511005357511237 ], [ 15.867951693715668, 8.413543606052542 ], [ 15.85740970219598, 8.366466375822597 ], [ 15.813639763863989, 8.315823473074829 ], [ 15.728166944748693, 8.322696438389926 ], [ 15.664708285558902, 8.276394355415164 ], [ 15.614065382811134, 8.26202830716062 ], [ 15.570812209315989, 8.258152574481699 ], [ 15.558048129717406, 8.278358059277025 ], [ 15.524251743023513, 8.279908351988865 ], [ 15.520065951982076, 8.317012030580713 ], [ 15.481722039040847, 8.347914537425936 ], [ 15.431440870599658, 8.355149237047613 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.264939405828898, 8.502117011490895 ], [ 15.305918817099723, 8.671770738528721 ], [ 15.37542361840525, 8.746236477231605 ], [ 15.372943148807281, 8.86426544779215 ], [ 15.428185256444408, 9.013455308515688 ], [ 15.463893670156722, 9.02673615205174 ], [ 15.518515659270236, 8.994851793275643 ], [ 15.569727003698233, 8.991389471746686 ], [ 15.7552970717025, 9.032213853386622 ], [ 15.818135614167261, 9.025082506552394 ], [ 16.022050815892442, 9.117273261351897 ], [ 16.146952751768083, 9.088541164842866 ], [ 16.204210239211136, 9.155927231756323 ], [ 16.304307488999427, 9.192410792723877 ], [ 16.357430861345165, 9.238712877497278 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-GR", "NAME_1": "Guéra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.265166674902105, 11.493192876350122 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.986685417597926, 12.482442532396931 ], [ 18.075258823036222, 12.569672348899758 ], [ 18.435288526744102, 12.763769029517107 ], [ 19.112611524602187, 12.751728420330323 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.990284050768309, 12.970578111090958 ], [ 20.136425002012231, 12.860455633720505 ], [ 20.170738152643594, 12.854409490705393 ], [ 20.169497918294269, 12.787850246991297 ], [ 20.122472364907708, 12.767903143715102 ], [ 20.048626742929798, 12.668477688394603 ], [ 20.094050326761135, 12.594838771991704 ], [ 20.043355747169983, 12.558871974961676 ], [ 19.926308627641049, 12.423117988304512 ], [ 19.885535922844554, 12.343381252043116 ], [ 19.821457146929788, 12.163185533485546 ], [ 19.820010207005453, 12.087014472439932 ], [ 19.642036573828818, 12.097659816747125 ], [ 19.3895972019493, 12.158948066500045 ], [ 19.219220005398938, 11.999371242988445 ], [ 19.293685744101765, 11.87286733575894 ], [ 19.190797967252365, 11.673654689911984 ], [ 19.061400181073509, 11.61572540890046 ], [ 19.012204217350757, 11.703213608721057 ], [ 18.886527134219875, 11.706365871887442 ], [ 18.867613559717995, 11.692361557939535 ], [ 18.84632287020429, 11.389485988887259 ], [ 18.860430536040383, 11.345354316248688 ], [ 19.136020949407282, 11.184175523181864 ], [ 19.255496859892162, 11.142575995186007 ], [ 19.315441521608932, 11.103611965519804 ], [ 19.360089959084348, 11.102578436745432 ], [ 19.392697788272301, 11.064234523804203 ], [ 19.460807325597557, 11.062580878304857 ], [ 19.628549024717699, 10.991680813019116 ], [ 19.6929378598943, 10.917731838253701 ], [ 19.666892937658929, 10.896027736690655 ], [ 19.677124871715421, 10.705083320139124 ], [ 19.658366326844487, 10.618473619461952 ], [ 19.61428633014998, 10.595374253918692 ], [ 19.580128208250187, 10.541165675955142 ], [ 19.557803989062847, 10.552741197148521 ], [ 19.5337227715886, 10.495380356917906 ], [ 19.464372999914019, 10.482357895800249 ], [ 19.442203810357569, 10.502046617557369 ], [ 19.378590122436208, 10.469128730006844 ], [ 19.336835564809462, 10.419622707921633 ], [ 19.316371697595741, 10.357404283081166 ], [ 19.281593458970917, 10.34923940747268 ], [ 19.302677442909669, 10.299940090063103 ], [ 19.19358849521285, 10.202995103441253 ], [ 19.164132928291963, 10.137521064445536 ], [ 19.111991407877156, 10.120312812286443 ], [ 19.049462924674231, 10.035666816370451 ], [ 18.998871697870527, 10.007038071749605 ], [ 18.795266553608542, 9.994842433831252 ], [ 18.756974318410073, 9.881774399768744 ], [ 18.69020836822159, 9.867304998726695 ], [ 18.653466423936322, 9.900171210333099 ], [ 18.602410109139214, 9.9109715851705 ], [ 18.607060988174055, 10.020784003279118 ], [ 18.500762566639139, 10.10791046789376 ], [ 18.453116895628227, 10.179120592441393 ], [ 18.381596713617398, 10.204803779470808 ], [ 18.349919061315632, 10.236378078985126 ], [ 18.264756300562794, 10.247798570547559 ], [ 18.218505894431473, 10.288571275344054 ], [ 18.156752556685149, 10.311257228838031 ], [ 18.145125360447025, 10.341487942114838 ], [ 18.038826938912109, 10.344691881225344 ], [ 17.931288283027811, 10.382415676542223 ], [ 17.827160271829086, 10.47388296092987 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.295801616351525, 10.584356293508108 ], [ 17.316224909751782, 10.74774264520687 ], [ 17.254955027752317, 11.237901701202532 ], [ 17.265166674902105, 11.493192876350122 ] ] ] } },
{ "type": "Feature", "properties": { "ISO": "TD-BA", "NAME_1": "Batha" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.131698845825042, 13.144004218020257 ], [ 17.202133823117435, 13.250974433123588 ], [ 17.197017856988509, 13.500623277042564 ], [ 17.238152296990904, 13.720609849365019 ], [ 17.551414828831298, 13.820086982428279 ], [ 17.992886590243927, 15.616411445132087 ], [ 18.084922316311861, 15.53166209642859 ], [ 18.209049106730902, 15.457248032770508 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.892418246704722, 15.495075181774212 ], [ 19.046982455975524, 15.614654445945916 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.863780145337444, 15.798674221238286 ], [ 20.000050896831397, 15.783843084990338 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.304321729863943, 13.118321030990785 ], [ 20.260396762800326, 13.043545233925386 ], [ 19.987648553338033, 12.986029364063882 ], [ 19.990284050768309, 12.970578111090958 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.096178419698276, 12.748679510850707 ], [ 18.435288526744102, 12.763769029517107 ], [ 18.075258823036222, 12.569672348899758 ], [ 17.986685417597926, 12.482442532396931 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.038887974300565, 12.829914862980502 ], [ 16.941116163580091, 12.908411363093933 ], [ 16.969848260089123, 12.970578111090958 ], [ 17.072684360095138, 13.063957424295666 ], [ 17.131698845825042, 13.144004218020257 ] ] ] } }
{ "type": "Feature", "properties": { "ISO": "TD-BA", "NAME_1": "Batha" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.072684360095138, 13.063957424295666 ], [ 17.202133823117435, 13.250974433123588 ], [ 17.197017856988509, 13.500623277042564 ], [ 17.238152296990904, 13.720609849365019 ], [ 17.551414828831298, 13.820086982428279 ], [ 17.992886590243927, 15.616411445132087 ], [ 18.084922316311861, 15.53166209642859 ], [ 18.209049106730902, 15.457248032770508 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.892418246704722, 15.495075181774212 ], [ 19.046982455975524, 15.614654445945916 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.863780145337444, 15.798674221238286 ], [ 20.000050896831397, 15.783843084990338 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.304321729863943, 13.118321030990785 ], [ 20.260396762800326, 13.043545233925386 ], [ 19.987648553338033, 12.986029364063882 ], [ 19.990284050768309, 12.970578111090958 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.096178419698276, 12.748679510850707 ], [ 18.435288526744102, 12.763769029517107 ], [ 18.075258823036222, 12.569672348899758 ], [ 17.986685417597926, 12.482442532396931 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.038887974300565, 12.829914862980502 ], [ 16.941116163580091, 12.908411363093933 ], [ 16.969848260089123, 12.970578111090958 ], [ 17.072684360095138, 13.063957424295666 ] ] ] } }
]
}

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