Compare commits

..

23 Commits

Author SHA1 Message Date
Evan Rusackas
26b7b8cdea test(echarts): Tier 3 plugin-level smoke tests for 4 migrated charts
36 tests across 4 charts exercising different defineChart features:

  Pie/Pie.test.ts (11 tests)
    Plugin metadata (name, category, behaviors: Drill*), controlPanel
    auto-generation (Query + Chart Options sections, _glyphArgs),
    custom buildQuery (contribution post-processing, conditional
    orderby), loaders presence.

  Timeseries/Line/Line.test.ts (9 tests)
    Plugin metadata, supportedAnnotationTypes pass-through (Event,
    Formula, Interval, Timeseries), formDataOverrides preservation
    (ExtraControls + getStandardizedControls pattern), thumbnails,
    _glyphArgs, loaders.

  Sunburst/Sunburst.test.ts (8 tests)
    Plugin metadata, Drill behaviors, formDataOverrides preservation,
    additionalControlOverrides merging into controlPanel.controlOverrides,
    _glyphArgs, loaders. Sunburst exercises the complex cross-filter
    useCallback render pattern, which we don't deeply unit-test here
    (would require full Redux + React rendering); plugin-level shape
    is verified.

  BigNumber/BigNumberPeriodOverPeriod.test.ts (8 tests)
    Plugin metadata, additionalSections append-after-Chart-Options
    semantics (Time Comparison section), additional section has its
    own controlSetRows, controlOverrides merging, _glyphArgs, loaders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:01:16 -05:00
Evan Rusackas
2908674e1f test(glyph): add GlyphOptionsPanel component tests
14 tests covering the native renderer:

  - Returns null when the section has no rows or all rows are empty
  - Renders glyph-defined args natively via Control, with value sourced
    from formData (not Redux controls state) and validationErrors still
    sourced from controls
  - Multi-arg rows render every arg
  - visibleWhen: passes isVisible=true/false to Control based on
    evaluateGlyphCondition; absent visibleWhen → isVisible undefined
  - Data args (Metric/Dimension/Temporal) in glyphArgs are NOT rendered
    natively — they fall through (panel only renders Customize-tab
    customize args)
  - Non-glyph items in the row use the renderControl fallback path,
    preserving compatibility with additionalControls.chartOptions
  - Mixed rows render glyph args + non-glyph items side-by-side
  - Section header renders the Chart Options label (or any custom label)

Uses jest.mock to capture props passed to Control so we can assert on
the hybrid-rendering contract without dragging in the full Redux
controls pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:21 -05:00
Evan Rusackas
86f2f056d2 test(glyph-core): add Tier 1 unit test suite (162 tests)
Comprehensive unit coverage of @superset-ui/glyph-core's public API:

  arguments.test.ts (53 tests)
    Every Argument class (Metric, Dimension, Temporal, Select, Text,
    Checkbox, Int, Color, NumberFormat, Currency, TimeFormat,
    ConditionalFormatting, Slider, Bounds, ColorPicker, RadioButton)
    plus all type guards. Verifies static defaults, .with() override
    semantics, and that the base class isn't mutated by subclasses.

  defineChart.test.tsx (34 tests)
    Factory wiring end-to-end: metadata pass-throughs (label,
    canBeAnnotationTypes, useLegacyApi, supportedAnnotationTypes,
    behaviors default, exampleGallery, thumbnailDark, credits),
    arguments → auto Query / Chart Options sections, suppress flag,
    prepend/middle/additional sections ordering, chartOptionsTabOverride,
    controlOverrides + additionalControlOverrides merge, formDataOverrides,
    onInit, _glyphArgs attachment, custom buildQuery + transform via the
    plugin's loaders, and visibleWhen preservation through object-form
    ArgDef. Plus resolveArgClass, getArgVisibleWhen, evaluateGlyphCondition.

  presets.test.ts (27 tests)
    Every reusable preset (HeaderFontSize, SubheaderFontSize, Subtitle,
    ShowLegend, ShowLabels, ShowValue, ShowMetricName, MetricNameFontSize,
    LegendType, LegendOrientation, LegendSort, LabelType, SortByMetric,
    LabelPosition, SimpleLabelType, ValueLabelType, ShowTotal,
    LabelThreshold, CircleShape, DataZoom, ForceTimestampFormatting) and
    the shared option arrays (FONT_SIZE_OPTIONS_*, LEGEND_*_OPTIONS,
    LABEL_TYPE_OPTIONS, SORT_OPTIONS). Verifies extensibility via .with().

  crossFilter.test.ts (19 tests)
    createSelectedValuesMap (empty / single / multi / unknown name),
    isDataPointFiltered (empty filter / matching / mismatched),
    createLabelMap (empty / multi / collision / column subset),
    extractCrossFilterProps (all fields, missing setDataMask default,
    coltypeMapping passthrough, empty filterState).

  generators.test.ts (29 tests)
    getControlConfig for Select/Checkbox/Int/Color/Text (with paramName
    fallback when label is null), generateControlPanel section structure
    (Query / Chart Options auto-population, metric/groupby/x_axis/
    time_grain_sqla rows, adhoc_filters always present, GlyphArgConfig
    visibility wiring, controlOverrides/formDataOverrides/extra rows),
    generateTransformProps value extraction with all defaulting rules,
    Color RGBA↔hex round-trip, Metric/Dimension/Temporal skipping,
    passthrough option, custom transform option. Plus createGlyphPlugin
    bundling.

All 162 tests green. Runs as: npx jest packages/superset-ui-glyph-core/test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 01:10:23 -05:00
Evan Rusackas
79eb91da96 fix(docker): make docker compose up build reliably
Three interlocking fixes so the full dev stack comes up cleanly:

1. Bind-mount Python package metadata (pyproject.toml, setup.py,
   MANIFEST.in, README.md) into /app in docker-compose.yml. Without
   these mounts, docker-bootstrap.sh's `uv pip install -e .` reads the
   stale metadata baked into the image at build time. After bumping
   apache-superset-core's sqlglot pin (>=28.10.0,<29), the bind mount
   ensures the editable install always sees the host's current pins
   instead of the image's older snapshot — which had been causing
   "apache-superset==0.0.0.dev0 depends on sqlglot>=27.15.2,<28" type
   resolver failures.

2. Bump @superset-ui/glyph-core's react peer from ^17.0.2 to ^18.2.0.
   Root and every other workspace package are on react 18; the
   stranded peer caused ERESOLVE during the in-container `npm install`
   (Docker doesn't use --legacy-peer-deps).

3. Restore the legacy-plugin-chart-partition multi-file plugin
   (controlPanel.tsx, transformProps.ts, index.ts) from master. They
   were missing on our branch; webpack resolves @superset-ui/legacy-
   plugin-chart-* via the tsconfig path mapping to plugins/.../src/,
   and without index.ts the partition entrypoint was unreachable.
   MainPreset.ts imports it.

Verified: docker compose down -v && docker compose build --no-cache &&
docker compose up brings the full stack up, /health returns OK, and
nginx serves the frontend on http://localhost/ (HTTP 302 login).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 00:19:40 -05:00
Evan Rusackas
96be1ccb2d fix(deps): drop unused @react-spring/web from root deps
@react-spring/web@^10.0.3 was accidentally introduced via our squash
commit but nothing in the source tree imports from @react-spring/* .
The version conflicted with @visx/xychart's peer dependency on
@react-spring/web@^9.4.5, causing docker compose up to fail with
ERESOLVE in the superset-node frontend container (which runs plain
`npm install`, no --legacy-peer-deps).

Removing the dead dep eliminates the conflict — Docker's npm install
now resolves cleanly without needing --legacy-peer-deps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:56:29 -05:00
Evan Rusackas
f63e369687 chore(glyph): drop dead _glyphVisibleWhen write in defineChart
`renderControl` stopped reading controlConfig._glyphVisibleWhen in
commit 28966ce84c — GlyphOptionsPanel evaluates argDef.visibleWhen
directly via evaluateGlyphCondition(). The write site in defineChart
was never cleaned up. No readers anywhere; safe to remove.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:51:36 -07:00
Evan Rusackas
67231d194e feat(glyph): consolidate plugin-chart-table to defineChart()
Replace src/index.ts with src/index.tsx using defineChart(). Existing
sibling files (buildQuery, controlPanel, transformProps, TableChart,
types, etc.) stay put — index.tsx just composes them.

Adds `canBeAnnotationTypes?: string[]` pass-through on the
defineChart metadata interface so Table can preserve its
['EVENT','INTERVAL'] declaration.

TableChartPlugin is exported as both default and named to match the
prior public API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:20:34 -07:00
Evan Rusackas
816583b602 feat(glyph): consolidate plugin-chart-pivot-table to defineChart()
Move plugin/{buildQuery,controlPanel,transformProps} up to src/ as
siblings and replace the plugin/ entrypoint with a new src/index.tsx
that wraps everything via defineChart(). Thin-shim approach: keeps
the large existing controlPanel.tsx (~490 lines), buildQuery, and
transformProps as their own files and just composes them.

PivotTableChartPlugin is exported as both default and named to match
the prior public API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:16:53 -07:00
Evan Rusackas
49b9afa591 test(deckgl): update Polygon/Scatter tests for consolidated index
After the layer consolidation, Polygon and Scatter export buildQuery
and transformProps as named exports from their index.tsx instead of
from separate files. Update the tests' imports accordingly. Also
export transformProps (was a private function) since the tests rely
on calling it directly.

Drop Multi/controlPanel.test.ts: its assertions tested the structure
of the standalone controlPanel object which no longer exists; the
build itself validates the controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:13:01 -07:00
Evan Rusackas
8efda4f704 feat(glyph): consolidate deckgl Multi layer to defineChart()
Collapse the Multi composer's index.ts + controlPanel.ts into a single
index.tsx. Multi continues to delegate to ../transformProps (the
preset-level transformProps that handles slice composition). The
Multi.tsx component and its imports of each layer's getPoints stay
intact.

This completes the deckgl single-file consolidation: all 10 layers
plus Multi now use defineChart().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:05:22 -07:00
Evan Rusackas
12ebe0f032 feat(glyph): consolidate deckgl Screengrid layer to defineChart()
Collapse multi-file plugin into single index.tsx. Screengrid uses
shared buildSpatialQuery/transformSpatialProps + grid size + default/
fixed/categorical color scheme. Screengrid.tsx component stays as
sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:03:35 -07:00
Evan Rusackas
857ba7f855 feat(glyph): consolidate deckgl Scatter layer to defineChart()
Collapse multi-file plugin into single index.tsx. Scatter keeps its
custom buildQuery (spatial config required, metric-or-fixed radius
handling) and transformProps (radius from metric/fix value, category
column, js columns).

Also wires `onInit?: ControlPanelConfig['onInit']` through to
defineChart so Scatter can clear time_grain_sqla and granularity on
chart initialization. The onInit hook is now an optional metadata-level
override surfaced in the generated ControlPanelConfig.

Scatter.tsx component stays as sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:01:33 -07:00
Evan Rusackas
dd1226f8e5 feat(glyph): consolidate deckgl Polygon layer to defineChart()
Collapse multi-file plugin into single index.tsx. Polygon keeps its
custom buildQuery (line_column required, optional metric/elevation
metric handling, null filter) and transformProps (json/geohash/zipcode
polygon decoders, fixed/metric elevation, reverse_long_lat). Largest
deckgl layer (500+ lines) but follows the same pattern. Polygon.tsx
component stays as sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:56:52 -07:00
Evan Rusackas
46555d12cb feat(glyph): consolidate deckgl Path layer to defineChart()
Collapse multi-file plugin into single index.tsx. Path keeps its
custom buildQuery (line_column required, optional metric/groupby
handling) and transformProps (decoders for json/polyline/geohash line
formats, reverse_long_lat support). Path.tsx component stays as
sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:54:06 -07:00
Evan Rusackas
5b0d895322 feat(glyph): consolidate deckgl Hex layer to defineChart()
Collapse multi-file plugin into single index.tsx. Hex uses shared
buildSpatialQuery/transformSpatialProps + categorical color scheme +
dynamic aggregation function (sum/min/max/mean/median/count/etc).
Hex.tsx component stays as sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:51:46 -07:00
Evan Rusackas
1eae7b4ee5 feat(glyph): consolidate deckgl Heatmap layer to defineChart()
Collapse multi-file plugin into single index.tsx. Heatmap uses shared
buildSpatialQuery/transformSpatialProps, plus intensity/radius_pixels
controls and a linear color scheme palette. Heatmap.tsx component
stays as sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:49:50 -07:00
Evan Rusackas
6a054f9170 feat(glyph): consolidate deckgl Grid layer to defineChart()
Collapse multi-file plugin (buildQuery, controlPanel, transformProps,
index) into single index.tsx. Grid uses shared buildSpatialQuery /
transformSpatialProps. Grid.tsx component stays as sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:47:52 -07:00
Evan Rusackas
865b75c90e feat(glyph): consolidate deckgl Geojson layer to defineChart()
Collapse multi-file plugin (buildQuery, controlPanel, transformProps,
index) into single index.tsx. The Geojson.tsx component stays as
sibling. Largest deckgl controlPanel so far due to extensive label /
icon / JavaScript config controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:46:02 -07:00
Evan Rusackas
6fc18b38fa feat(glyph): consolidate deckgl Contour layer to defineChart()
Collapse the multi-file plugin (buildQuery, controlPanel,
transformProps, index) into a single index.tsx. Contour delegates to
the shared buildSpatialQuery / transformSpatialProps from ../spatialUtils.
The Contour.tsx component, getSafeCellSize helper, and Multi.tsx's
getPoints import stay intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:42:58 -07:00
Evan Rusackas
3f61764543 feat(glyph): consolidate deckgl Arc layer to defineChart()
First of 10 deck.gl layer consolidations: collapses Arc's multi-file
plugin (buildQuery, controlPanel, transformProps, index) into a single
index.tsx using defineChart(). The Arc.tsx component (and its
getPoints / getLayer / getHighlightLayer exports for Multi.tsx) stays
intact as a sibling.

Also wires @superset-ui/glyph-core as a TypeScript project reference
in preset-chart-deckgl/tsconfig.json so the deckgl plugin can resolve
the glyph-core source.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:40:50 -07:00
Evan Rusackas
9dad9cbfc2 feat(glyph): consolidate plugin-chart-word-cloud to defineChart()
Collapse the multi-file plugin/ subdirectory (buildQuery, controlPanel,
transformProps, index) into a single top-level src/index.tsx using
defineChart(). The chart/WordCloud.tsx component stays as a sibling.

  - Move plugin/controls/ up to src/controls/ so the new index.tsx can
    import the local RotationControl and ColorSchemeControl directly.
  - Delete src/index.ts barrel (the new index.tsx is the entrypoint and
    re-exports from ./types).
  - WordCloudChartPlugin is exported both as default and named, matching
    the previous public API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:28:08 -07:00
Evan Rusackas
b2c58a0856 feat(glyph): consolidate nvd3 Bubble/Bullet/Compare to defineChart()
Migrate the remaining 3 nvd3 charts to single-file glyph pattern,
matching TimePivot which already uses defineChart. Each plugin now
has a single index.tsx replacing the prior multi-file
{controlPanel.ts, index.ts} layout. All charts continue to share
../transformProps and ../ReactNVD3 via require().

Also adds `label?: ChartLabel` pass-through to the defineChart
metadata interface so the Deprecated badge survives the consolidation
(Bubble + Compare use it).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:03:22 -07:00
Evan Rusackas
2e16b8266a feat(glyph): single-file chart definition pattern across all plugins
Introduce `defineChart()` — a declarative pattern that bundles metadata,
arguments (control-panel config), buildQuery, transform, and render into
a single chart-plugin file. Migrate every chart plugin to this pattern:

  * plugin-chart-echarts: Pie, Funnel, Gauge, Sankey, Waterfall,
    Histogram, Tree, Bubble, BoxPlot, Sunburst, Radar, Treemap, Graph,
    Heatmap, Gantt, BigNumber (Total, WithTrendline, PoP, Glyph demo),
    MixedTimeseries, and the Timeseries family (Generic, Scatter,
    SmoothLine, Step, Area, Line, Bar)
  * legacy-plugin-chart-*: calendar, horizon, chord, country-map,
    world-map, paired-t-test, parallel-coordinates, partition, rose,
    map-box
  * other plugins: handlebars, word-cloud, pivot-table, table,
    ag-grid-table, cartodiagram
  * legacy-preset-chart-nvd3: Bubble, Bullet, Compare, TimePivot
  * legacy-preset-chart-deckgl: Grid, Hex, Polygon, Scatter (single-file
    defineChart); Arc, Contour, Geojson, Heatmap, Path, Screengrid kept
    on the original multi-file ChartPlugin pattern pending follow-up

Glyph-core lives as @superset-ui/glyph-core (extracted package) and
provides: defineChart, ~14 argument types (Metric, Dimension, Select,
Checkbox, Text, Int, Slider, etc.), reusable presets (ShowLegend,
HeaderFontSize, Subtitle, etc.), cross-filter utilities
(extractCrossFilterProps, createSelectedValuesMap, isDataPointFiltered,
createLabelMap), and visibility-condition helpers
(resolveArgClass, getArgVisibleWhen, evaluateGlyphCondition).

Customize-tab rendering uses a new GlyphOptionsPanel — a native React
renderer that hybrids glyph args with additionalControls, with
inlined sharedControls in the Query section.

Imports are routed through @apache-superset/core subpath entrypoints
(/translation for t, /common for GenericDataType).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:38:18 -07:00
515 changed files with 35535 additions and 49401 deletions

View File

@@ -0,0 +1 @@
{"sessionId":"a0289491-ebb9-4d03-aa1d-023b0219c585","pid":48965,"procStart":"Fri May 1 19:01:07 2026","acquiredAt":1778687635094}

View File

@@ -111,6 +111,8 @@ services:
superset-init-light:
condition: service_completed_successfully
volumes: *superset-volumes
ports:
- "${SUPERSET_PORT:-8088}:8088"
environment:
DATABASE_HOST: db-light
DATABASE_DB: superset_light
@@ -162,7 +164,7 @@ services:
environment:
# set this to false if you have perf issues running the npm i; npm run dev in-docker
# if you do so, you have to run this manually on the host, which should perform better!
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
BUILD_SUPERSET_FRONTEND_IN_DOCKER: false
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"

View File

@@ -34,6 +34,14 @@ x-superset-volumes: &superset-volumes
- superset_home:/app/superset_home
- ./tests:/app/tests
- superset_data:/app/data
# Python package metadata for the editable `uv pip install -e .` that
# docker-bootstrap.sh runs at container start. Without these bind mounts
# the editable install reads stale metadata baked into the image at
# build time and may conflict with apache-superset-core's current pins.
- ./pyproject.toml:/app/pyproject.toml
- ./setup.py:/app/setup.py
- ./MANIFEST.in:/app/MANIFEST.in
- ./README.md:/app/README.md
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`

View File

@@ -71,7 +71,7 @@
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.33",
"antd": "^6.4.2",
"antd": "^6.3.7",
"baseline-browser-mapping": "^2.10.29",
"caniuse-lite": "^1.0.30001792",
"docusaurus-plugin-openapi-docs": "^5.0.2",

View File

@@ -212,7 +212,7 @@
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
"@ant-design/icons@^6.2.3":
"@ant-design/icons@^6.1.1", "@ant-design/icons@^6.2.3":
version "6.2.3"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.3.tgz#66e1c7fdea009b9c3fab6964062bedc76f308ad8"
integrity sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==
@@ -1158,10 +1158,10 @@
dependencies:
core-js-pure "^3.43.0"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4", "@babel/runtime@^7.29.2":
version "7.29.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e"
integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4":
version "7.28.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.28.6":
version "7.28.6"
@@ -2924,13 +2924,13 @@
dependencies:
"@babel/runtime" "^7.24.4"
"@rc-component/cascader@~1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.15.0.tgz#554cba8e01e94a1288547cec96422b2cfc73ff40"
integrity sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==
"@rc-component/cascader@~1.14.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.14.0.tgz#74e1fca58cb14f8f75f6e4bf1debd90534aaea7c"
integrity sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==
dependencies:
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.3.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
@@ -2968,10 +2968,10 @@
dependencies:
"@rc-component/util" "^1.3.0"
"@rc-component/dialog@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.9.0.tgz#3134f8fa8644d9bc228c862668b90de048c7ea1a"
integrity sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==
"@rc-component/dialog@~1.8.4":
version "1.8.4"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.4.tgz#e1f05f311539852f40a5717bc3874ce0af64c6ff"
integrity sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==
dependencies:
"@rc-component/motion" "^1.1.3"
"@rc-component/portal" "^2.1.0"
@@ -3025,30 +3025,30 @@
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/input@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/input/-/input-1.3.0.tgz#a8c113000bbc39089cf75337bec68120115b9e05"
integrity sha512-IUUNOdAuWuEvDEFFgfmwQl818tiDbvXwLgon4HL1q2hJeYkqrRrYwYhJN0zfPHGTDxs3gvyVC/C02D4hWFoIcA==
"@rc-component/input@~1.1.0", "@rc-component/input@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz"
integrity sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==
dependencies:
"@rc-component/resize-observer" "^1.1.1"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/mentions@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.9.0.tgz#1e133d607835854430e264b681b7b32c4b49daa7"
integrity sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==
"@rc-component/mentions@~1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz"
integrity sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==
dependencies:
"@rc-component/input" "~1.3.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/input" "~1.1.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/textarea" "~1.1.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/menu@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/menu/-/menu-1.3.0.tgz#fc70d81ca76ae6013b0d7955f20a2393adef04b3"
integrity sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==
"@rc-component/menu@~1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz"
integrity sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/overflow" "^1.0.0"
@@ -3078,13 +3078,13 @@
dependencies:
"@rc-component/util" "^1.2.0"
"@rc-component/notification@~2.0.6":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@rc-component/notification/-/notification-2.0.7.tgz#f2450a482f87e4698285833c4a8efcac169acabb"
integrity sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==
"@rc-component/notification@~1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz"
integrity sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/util" "^1.11.0"
"@rc-component/util" "^1.2.1"
clsx "^2.1.1"
"@rc-component/overflow@^1.0.0":
@@ -3105,10 +3105,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/picker@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.10.0.tgz#6989f0ae67fca8db00e31f81a8217c8bc370cd34"
integrity sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==
"@rc-component/picker@~1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.9.1.tgz#7ffcb1e4d4655fe2f3d712773e1d3ab9cd5c2a5c"
integrity sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/resize-observer" "^1.0.0"
@@ -3199,10 +3199,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.0.tgz#7a98d68176f23f50a762df464f4c9142e7db3942"
integrity sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w==
"@rc-component/table@~1.9.1":
version "1.9.1"
resolved "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz"
integrity sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==
dependencies:
"@rc-component/context" "^2.0.1"
"@rc-component/resize-observer" "^1.0.0"
@@ -3210,18 +3210,28 @@
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
"@rc-component/tabs@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.9.0.tgz#8f3e3755450e5a90d240d1ed3dc140d520b1fbef"
integrity sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==
"@rc-component/tabs@~1.7.0":
version "1.7.0"
resolved "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz"
integrity sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==
dependencies:
"@rc-component/dropdown" "~1.0.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/motion" "^1.1.3"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/textarea@~1.1.0", "@rc-component/textarea@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz"
integrity sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==
dependencies:
"@rc-component/input" "~1.1.0"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/tooltip@~1.4.0":
version "1.4.0"
resolved "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz"
@@ -3231,30 +3241,30 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/tour@~2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/tour/-/tour-2.4.0.tgz#caf89cf8f2f9fb68f1fb0e0c867610015d01f432"
integrity sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==
"@rc-component/tour@~2.3.0":
version "2.3.0"
resolved "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz"
integrity sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==
dependencies:
"@rc-component/portal" "^2.2.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.9.0.tgz#13ea516478b6cb558e04181abb0a01ae6fbdd31f"
integrity sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==
"@rc-component/tree-select@~1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.8.0.tgz#480e84221befbd1fa93ab2034423e2b064e41981"
integrity sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==
dependencies:
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.3.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/tree@~1.3.0", "@rc-component/tree@~1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.3.1.tgz#6983ca6bd9d5f6d04dd7258d00cb0fe71cdfe661"
integrity sha512-zlL0PW0bTFlveTtLcA01VD/yMWKK73EywItFMgIZUY5sb6tMOAw7zV6qGzqldufqrV93ZWQB4H3NBNoTMCueJA==
"@rc-component/tree@~1.2.0", "@rc-component/tree@~1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.2.4.tgz#cb4f7d818118b3447763e74d3a82fba6454c7317"
integrity sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/util" "^1.8.1"
@@ -3280,10 +3290,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.11.0.tgz#965c8b44a3f57fc96dc14e5072afbe32e422fd4d"
integrity sha512-jHG3/BYgUWiP5c7RZHiaUNToyw1L3nlPSKG2RPu+YoiD9b3ajiJwBWhsjO+ZELmCsKFAjNR5DelbKdlF0e2BDA==
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.10.1.tgz#213c84c77e8b2001095530d3b0dc47c49c34ffe3"
integrity sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==
dependencies:
is-mobile "^5.0.0"
react-is "^18.2.0"
@@ -5491,36 +5501,36 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.4.2.tgz#9fc0fee455a5c56e7ec27855495eefadc8df636a"
integrity sha512-PNJz8Vxc/mC3EsOg/h3e2YuaZduJ1RDp4RmySDuDmKPCxVgyp4Da4kB36o87p9hbLbOWdAWCKQlnyopsN8utKQ==
antd@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.7.tgz#620354ec04135356cbc5ce0a666871ddc73e4117"
integrity sha512-WTHi4bHVNKpYXLHESzU0Tts7rRNQeL84Bph9dfI3Qw7mHbTulExDcYKNHny5CTXcrBBOpraXbU9miBAwUR5vaw==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
"@ant-design/cssinjs-utils" "^2.1.2"
"@ant-design/fast-color" "^3.0.1"
"@ant-design/icons" "^6.2.3"
"@ant-design/icons" "^6.1.1"
"@ant-design/react-slick" "~2.0.0"
"@babel/runtime" "^7.29.2"
"@rc-component/cascader" "~1.15.0"
"@babel/runtime" "^7.28.4"
"@rc-component/cascader" "~1.14.0"
"@rc-component/checkbox" "~2.0.0"
"@rc-component/collapse" "~1.2.0"
"@rc-component/color-picker" "~3.1.1"
"@rc-component/dialog" "~1.9.0"
"@rc-component/dialog" "~1.8.4"
"@rc-component/drawer" "~1.4.2"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/form" "~1.8.1"
"@rc-component/image" "~1.9.0"
"@rc-component/input" "~1.3.0"
"@rc-component/input" "~1.1.2"
"@rc-component/input-number" "~1.6.2"
"@rc-component/mentions" "~1.9.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/mentions" "~1.6.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/motion" "^1.3.2"
"@rc-component/mutate-observer" "^2.0.1"
"@rc-component/notification" "~2.0.6"
"@rc-component/notification" "~1.2.0"
"@rc-component/pagination" "~1.2.0"
"@rc-component/picker" "~1.10.0"
"@rc-component/picker" "~1.9.1"
"@rc-component/progress" "~1.0.2"
"@rc-component/qrcode" "~1.1.1"
"@rc-component/rate" "~1.0.1"
@@ -5530,12 +5540,13 @@ antd@^6.4.2:
"@rc-component/slider" "~1.0.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.10.0"
"@rc-component/tabs" "~1.9.0"
"@rc-component/table" "~1.9.1"
"@rc-component/tabs" "~1.7.0"
"@rc-component/textarea" "~1.1.2"
"@rc-component/tooltip" "~1.4.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.3.1"
"@rc-component/tree-select" "~1.9.0"
"@rc-component/tour" "~2.3.0"
"@rc-component/tree" "~1.2.4"
"@rc-component/tree-select" "~1.8.0"
"@rc-component/trigger" "^3.9.0"
"@rc-component/upload" "~1.1.0"
"@rc-component/util" "^1.10.1"
@@ -7090,7 +7101,12 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
dayjs@^1.11.11, dayjs@^1.11.19:
dayjs@^1.11.11:
version "1.11.13"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
dayjs@^1.11.19:
version "1.11.20"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==

View File

@@ -27,6 +27,11 @@ module.exports = {
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
'^src/(.*)$': '<rootDir>/src/$1',
'^spec/(.*)$': '<rootDir>/spec/$1',
// mapping glyph-core to local package source
'^@superset-ui/glyph-core$':
'<rootDir>/packages/superset-ui-glyph-core/src',
'^@superset-ui/glyph-core/(.*)$':
'<rootDir>/packages/superset-ui-glyph-core/src/$1',
// mapping plugins of superset-ui to source code
'^@superset-ui/([^/]+)/(.*)$':
'<rootDir>/node_modules/@superset-ui/$1/src/$2',

File diff suppressed because it is too large Load Diff

View File

@@ -109,7 +109,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@googleapis/sheets": "^13.0.1",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
@@ -183,7 +185,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.6.2",
"immer": "^11.1.8",
"immer": "^11.1.7",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -199,10 +201,11 @@
"nanoid": "^5.1.11",
"ol": "^10.9.0",
"pretty-ms": "^9.3.0",
"prop-types": "^15.8.1",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^18.2.0",
"react-arborist": "^3.6.1",
"react-arborist": "^3.5.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
@@ -305,7 +308,7 @@
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"babel-jest": "^30.4.1",
"babel-jest": "^30.0.2",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@@ -367,7 +370,7 @@
"terser-webpack-plugin": "^5.6.0",
"ts-jest": "^29.4.9",
"tscw-config": "^1.1.2",
"tsx": "^4.22.0",
"tsx": "^4.21.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
@@ -375,7 +378,7 @@
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.4",
"webpack-dev-server": "^5.2.3",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"

View File

@@ -441,6 +441,8 @@ export interface ControlPanelConfig {
sectionOverrides?: SectionOverrides;
onInit?: (state: ControlStateMapping) => void;
formDataOverrides?: (formData: QueryFormData) => QueryFormData;
/** @internal Raw glyph argument definitions from defineChart() used for native control panel rendering */
_glyphArgs?: unknown;
}
export type ControlOverrides = {

View File

@@ -24,7 +24,7 @@
"lib"
],
"dependencies": {
"@ant-design/icons": "^6.2.3",
"@ant-design/icons": "^6.2.2",
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.2",
"@types/json-bigint": "^1.0.4",

View File

@@ -33,6 +33,14 @@ export enum Behavior {
*/
DrillToDetail = 'DRILL_TO_DETAIL',
DrillBy = 'DRILL_BY',
/**
* Include `ALLOWS_EMPTY_RESULTS` behavior if the chart handles empty/no data
* gracefully (e.g., showing a drop zone for drag-and-drop configuration).
* Charts with this behavior will receive empty data instead of seeing
* the "No results" message.
*/
AllowsEmptyResults = 'ALLOWS_EMPTY_RESULTS',
}
export interface ContextMenuFilters {

View File

@@ -0,0 +1,335 @@
# Glyph Pattern Migration Guide
This guide documents how to migrate traditional Superset chart plugins to the single-file Glyph pattern.
## Overview
The Glyph pattern simplifies chart plugin development by:
- **Arguments define BOTH controls AND render props** - No separate files needed
- **No `controlPanel.ts`** - Generated from argument definitions
- **No `transformProps.ts`** - Arguments are passed directly to render
- **No `buildQuery.ts`** - Inferred from Metric/Dimension/Temporal arguments
- **Single file** - Everything in one place (~200 lines vs 500+ across multiple files)
## Migration Steps
### 1. Analyze the Existing Chart
Identify from the original chart:
- **Metrics/Dimensions**: What data does it query?
- **Controls**: What options does the user configure?
- **Styling**: What visual customizations exist?
- **Rendering**: How is the data displayed?
### 2. Create the Glyph Chart File
Create a new file: `src/BigNumber/BigNumberGlyph/index.tsx`
```typescript
import { t } from '@apache-superset/core';
import { styled } from '@apache-superset/core/ui';
import { Behavior, getNumberFormatter, CurrencyFormatter } from '@superset-ui/core';
import {
defineChart,
Metric,
Select,
Text,
Checkbox,
NumberFormat,
Currency,
TimeFormat,
ConditionalFormatting,
} from '@superset-ui/glyph-core';
```
### 3. Define Arguments (Controls + Props)
**CRITICAL: Use camelCase for argument names!**
Superset converts control names to camelCase in `formData`. If you use snake_case (`show_metric_name`), it won't match the camelCase key in formData (`showMetricName`).
```typescript
arguments: {
// Data arguments
metric: Metric.with({ label: t('Metric') }),
// Visual arguments - USE CAMELCASE!
headerFontSize: Select.with({
label: t('Font Size'),
options: [
{ label: t('Small'), value: 0.2 },
{ label: t('Large'), value: 0.4 },
],
default: 0.4,
}),
showMetricName: Checkbox.with({
label: t('Show Metric Name'),
default: false,
}),
// Declarative visibility (preferred)
metricNameFontSize: {
arg: Select.with({ ... }),
visibleWhen: { showMetricName: true },
},
// Declarative disabled state
subtitleFontSize: {
arg: Select.with({ ... }),
disabledWhen: { subtitle: '' },
},
}
```
### 4. Available Argument Types
| Type | Control Generated | Value Type | Properties |
|------|------------------|------------|------------|
| `Metric` | MetricControl | `{ value, name, formattedValue }` | `label` |
| `Dimension` | GroupByControl | `string[]` | `label` |
| `Temporal` | TemporalControl | `string` | `label` |
| `Select` | SelectControl | `string \| number` | `label`, `description`, `options`, `default` |
| `Text` | TextControl | `string` | `label`, `description`, `default`, `placeholder` |
| `Checkbox` | CheckboxControl | `boolean` | `label`, `description`, `default` |
| `Int` | SliderControl | `number` | `label`, `description`, `default`, `min`, `max`, `step` |
| `Color` | ColorPickerControl | `string` (hex) | `label`, `description`, `default` |
| `NumberFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
| `Currency` | CurrencyControl | `{ symbol?, symbolPosition? }` | `label`, `description`, `default` |
| `TimeFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
| `ConditionalFormatting` | ConditionalFormattingControl | `Rule[]` | `label`, `description` |
### 5. Declarative Visibility & Disabled States
Instead of Redux `mapStateToProps`, use declarative conditions:
```typescript
// Simple equality check - visible when showMetricName is true
metricNameFontSize: {
arg: Select.with({ ... }),
visibleWhen: { showMetricName: true },
},
// Function check - visible when subtitle is not empty
subtitleFontSize: {
arg: Select.with({ ... }),
visibleWhen: { subtitle: (val) => !!val },
},
// Multiple conditions (AND) - visible when both conditions are met
advancedOption: {
arg: Checkbox.with({ ... }),
visibleWhen: {
showMetricName: true,
subtitle: (val) => !!val,
},
},
// Disabled state (control visible but not editable)
formatOption: {
arg: Select.with({ ... }),
disabledWhen: { forceTimestampFormatting: true },
},
```
### 6. Number, Currency, and Time Formatting
Use the built-in format argument types:
```typescript
arguments: {
numberFormat: NumberFormat.with({
label: t('Number Format'),
description: t('D3 format string'),
default: 'SMART_NUMBER',
}),
currencyFormat: Currency.with({
label: t('Currency Format'),
}),
timeFormat: TimeFormat.with({
label: t('Date Format'),
default: 'smart_date',
}),
}
```
Then use them directly in the render function:
```typescript
render: ({ numberFormat, currencyFormat, timeFormat, metric }) => {
const formatter = currencyFormat?.symbol
? new CurrencyFormatter({
currency: { symbol: currencyFormat.symbol, symbolPosition: currencyFormat.symbolPosition ?? 'prefix' },
d3Format: numberFormat,
})
: getNumberFormatter(numberFormat);
return <div>{formatter(metric.value)}</div>;
}
```
### 7. Conditional Formatting (Colors)
Use `ConditionalFormatting` for color-based rules:
```typescript
import { getColorFormatters } from '@superset-ui/chart-controls';
arguments: {
conditionalFormatting: ConditionalFormatting.with({
label: t('Conditional Formatting'),
description: t('Apply conditional color formatting to metric'),
}),
},
render: ({ conditionalFormatting, metric, data, theme }) => {
let numberColor: string | undefined;
if (conditionalFormatting?.length > 0 && metric.value != null) {
const colorFormatters = getColorFormatters(conditionalFormatting, data, theme, false);
if (colorFormatters) {
for (const formatter of colorFormatters) {
const color = formatter.getColorFromValue(metric.value as number);
if (color) {
numberColor = color;
break;
}
}
}
}
return <BigNumberText color={numberColor}>{metric.formattedValue}</BigNumberText>;
}
```
### 8. Styled Components
Use Superset's theme properties with template literal syntax:
```typescript
const Container = styled.div<{ height: number }>`
${({ theme, height }) => `
height: ${height}px;
padding: ${theme.sizeUnit * 4}px;
font-family: ${theme.fontFamily};
color: ${theme.colorText};
`}
`;
```
**Common theme properties:**
| Property | Description |
|----------|-------------|
| `theme.sizeUnit` | Base spacing unit (typically 4px) |
| `theme.fontFamily` | Default font family |
| `theme.fontWeightNormal` | Normal font weight |
| `theme.fontWeightLight` | Light font weight |
| `theme.fontSizeSM` | Small font size |
| `theme.colorText` | Primary text color |
| `theme.colorTextTertiary` | Muted/secondary text color |
| `theme.borderRadius` | Standard border radius |
### 9. Render Function
The render function receives all arguments directly - no formData lookup needed:
```typescript
render: ({
metric,
headerFontSize,
showMetricName,
numberFormat,
currencyFormat,
conditionalFormatting,
height,
data,
theme,
}) => {
// All arguments are directly available!
const formatter = currencyFormat?.symbol
? new CurrencyFormatter({ currency: currencyFormat, d3Format: numberFormat })
: getNumberFormatter(numberFormat);
const formattedValue = metric.value != null
? formatter(metric.value as number)
: t('No data');
return (
<Container height={height}>
{showMetricName && <MetricName>{metric.name}</MetricName>}
<BigNumberText>{formattedValue}</BigNumberText>
</Container>
);
},
```
### 10. Register the Plugin
In `BigNumber/index.ts`:
```typescript
export { default as BigNumberGlyphChartPlugin } from './BigNumberGlyph';
```
In `plugin-chart-echarts/src/index.ts`:
```typescript
export { BigNumberGlyphChartPlugin } from './BigNumber';
```
In `MainPreset.js`:
```typescript
import { BigNumberGlyphChartPlugin } from '@superset-ui/plugin-chart-echarts';
new BigNumberGlyphChartPlugin().configure({ key: 'big_number_glyph' }),
```
## Common Pitfalls
### 1. Snake Case vs Camel Case
- **WRONG**: `show_metric_name` - won't match formData
- **RIGHT**: `showMetricName` - matches Superset's camelCase conversion
### 2. Theme Undefined
- **WRONG**: `theme.gridUnit` - crashes if theme is undefined
- **RIGHT**: `theme?.gridUnit ?? 4` - safe with fallback
### 3. Metric Value Extraction
The Glyph core automatically extracts metric values from query results. The `metric` argument provides:
- `metric.value` - The raw numeric value
- `metric.name` - The metric label/name
- `metric.formattedValue` - Basic string representation
### 4. Visibility vs Legacy Functions
- **Prefer**: `visibleWhen: { showMetricName: true }` - declarative, clean
- **Legacy**: `visibility: ({ controls }) => controls?.showMetricName?.value === true` - still works
## File Structure Comparison
### Traditional (5+ files, ~500 lines)
```
BigNumberTotal/
├── index.ts # Plugin registration
├── controlPanel.ts # Control definitions (~100 lines)
├── transformProps.ts # Data transformation (~150 lines)
├── buildQuery.ts # Query building (~50 lines)
├── BigNumberViz.tsx # React component (~150 lines)
└── types.ts # TypeScript types (~50 lines)
```
### Glyph Pattern (1 file, ~250 lines)
```
BigNumberGlyph/
└── index.tsx # Everything in one file!
```
## Complete Example
See `BigNumber/BigNumberGlyph/index.tsx` for a complete working example with:
- Metric display
- Number/currency/time formatting
- Conditional color formatting
- Declarative visibility
- Subtitle support
- Font size controls

View File

@@ -0,0 +1,38 @@
{
"name": "@superset-ui/glyph-core",
"version": "0.20.3",
"description": "Glyph Core - A declarative visualization plugin framework for Apache Superset",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "https://github.com/apache/superset.git",
"directory": "superset-frontend/packages/superset-ui-glyph-core"
},
"keywords": [
"superset",
"glyph",
"visualization",
"chart"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache/superset/issues"
},
"homepage": "https://github.com/apache/superset#readme",
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
}
}

View File

@@ -0,0 +1,646 @@
/**
* 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 {
ColumnType,
SelectOptions,
SelectOption,
TextOptions,
CheckboxOptions,
IntOptions,
ColorOptions,
MetricOptions,
DimensionOptions,
NumberFormatOptions,
CurrencyOptions,
CurrencyValue,
TimeFormatOptions,
ConditionalFormattingOptions,
ConditionalFormattingRule,
SliderOptions,
BoundsOptions,
BoundsValue,
ColorPickerOptions,
RadioButtonOptions,
RadioOption,
RgbaColor,
} from './types';
/**
* Base Argument class - all argument types extend from this.
*
* Arguments define:
* 1. What the chart needs (semantically)
* 2. How to render controls in the control panel
* 3. Default values and validation
*/
export class Argument {
static label: string | null = null;
static description: string | null = null;
static columnType: ColumnType = ColumnType.Argument;
static controlType: string = 'TextControl';
value: unknown;
constructor(value: unknown) {
this.value = value;
}
}
/**
* Metric - represents a numeric aggregation (SUM, COUNT, AVG, etc.)
*
* Maps to Superset's MetricsControl in the query section.
*/
export class Metric extends Argument {
static override label: string | null = 'Metric';
static override description: string | null =
'A numeric aggregation (SUM, COUNT, AVG, etc.)';
static override columnType = ColumnType.Metric;
static override controlType = 'MetricsControl';
static multi = false;
static with(options: MetricOptions): typeof Metric {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override multi = options.multi ?? Base.multi;
};
}
}
/**
* Dimension - represents a categorical column for grouping data
*
* Maps to Superset's GroupByControl in the query section.
*/
export class Dimension extends Argument {
static override label: string | null = 'Dimension';
static override description: string | null =
'A categorical column for grouping data';
static override columnType = ColumnType.Dimension;
static override controlType = 'GroupByControl';
static multi = true;
static with(options: DimensionOptions): typeof Dimension {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override multi = options.multi ?? Base.multi;
};
}
}
/**
* Temporal - represents a time column
*
* Maps to Superset's temporal controls (x_axis, time_grain_sqla).
*/
export class Temporal extends Argument {
static override label: string | null = 'Time Column';
static override description: string | null =
'A temporal column for time series data';
static override columnType = ColumnType.Temporal;
static override controlType = 'TemporalControl';
static with(options: {
label?: string;
description?: string;
}): typeof Temporal {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
};
}
}
/**
* Select - dropdown selection from predefined options
*
* Maps to Superset's SelectControl.
*/
export class Select extends Argument {
static override label: string | null = 'Select';
static override description: string | null = 'Choose from options';
static override controlType = 'SelectControl';
static default: string | number = '';
static options: SelectOption[] = [];
static clearable = false;
static with(options: SelectOptions): typeof Select {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
static override options = options.options ?? Base.options;
};
}
}
/**
* Text - free-form text input
*
* Maps to Superset's TextControl.
*/
export class Text extends Argument {
static override label: string | null = 'Text';
static override description: string | null = 'Text input';
static override controlType = 'TextControl';
static default: string = '';
static placeholder: string = '';
static with(options: TextOptions): typeof Text {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
static override placeholder = options.placeholder ?? Base.placeholder;
};
}
}
/**
* Checkbox - boolean toggle
*
* Maps to Superset's CheckboxControl.
*/
export class Checkbox extends Argument {
static override label: string | null = 'Checkbox';
static override description: string | null = 'Toggle option';
static override controlType = 'CheckboxControl';
static default: boolean = false;
static with(options: CheckboxOptions): typeof Checkbox {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
};
}
}
/**
* Int - numeric input with slider
*
* Maps to Superset's SliderControl.
*/
export class Int extends Argument {
static override label: string | null = 'Integer';
static override description: string | null = 'A numeric value';
static override controlType = 'SliderControl';
static default: number = 0;
static min: number = 0;
static max: number = 100;
static step: number = 1;
static with(options: IntOptions): typeof Int {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
static override min = options.min ?? Base.min;
static override max = options.max ?? Base.max;
static override step = options.step ?? Base.step;
};
}
}
/**
* Color - color picker
*
* Maps to Superset's ColorPickerControl.
*/
export class Color extends Argument {
static override label: string | null = 'Color';
static override description: string | null = 'A color value';
static override controlType = 'ColorPickerControl';
// eslint-disable-next-line theme-colors/no-literal-colors
static default: string = '#000000';
static with(options: ColorOptions): typeof Color {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
};
}
}
/**
* NumberFormat - D3 number format string selection
*
* Maps to Superset's SelectControl with D3 format options.
* Allows freeform input for custom formats.
*/
export class NumberFormat extends Argument {
static override label: string | null = 'Number Format';
static override description: string | null =
'D3 format string for number display (e.g., ".2f", ".1%", ",.0f")';
static override controlType = 'NumberFormatControl';
static default: string = 'SMART_NUMBER';
// Standard D3 format options
static readonly FORMAT_OPTIONS: SelectOption[] = [
{ label: 'Adaptive formatting', value: 'SMART_NUMBER' },
{ label: 'Original value', value: '~g' },
{ label: '12,345.432', value: ',.3f' },
{ label: '12,345.43', value: ',.2f' },
{ label: '12,345.4', value: ',.1f' },
{ label: '12,345', value: ',.0f' },
{ label: '12345.432', value: '.3f' },
{ label: '12345.43', value: '.2f' },
{ label: '12345.4', value: '.1f' },
{ label: '12345', value: '.0f' },
{ label: '12K', value: '.0s' },
{ label: '12.3K', value: '.1s' },
{ label: '12.35K', value: '.2s' },
{ label: '12.346K', value: '.3s' },
{ label: '1234543.21%', value: '.2%' },
{ label: '1234543%', value: '.0%' },
{ label: '12.34%', value: '.2r' },
{ label: '+12,345.4', value: '+,.1f' },
{ label: '$12,345.43', value: '$,.2f' },
{ label: 'Duration (1m 6s)', value: 'DURATION' },
{ label: 'Duration (1ms 400µs)', value: 'DURATION_SUB' },
];
static with(options: NumberFormatOptions): typeof NumberFormat {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
};
}
}
/**
* Currency - currency format with symbol and position
*
* Maps to Superset's CurrencyControl.
* Value is { symbol: 'USD', symbolPosition: 'prefix' | 'suffix' }
*/
export class Currency extends Argument {
static override label: string | null = 'Currency Format';
static override description: string | null =
'Currency symbol and position for formatting';
static override controlType = 'CurrencyControl';
static default: CurrencyValue = {};
static with(options: CurrencyOptions): typeof Currency {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
};
}
}
/**
* TimeFormat - D3 time format string selection
*
* Maps to Superset's SelectControl with D3 time format options.
* Allows freeform input for custom formats.
*/
export class TimeFormat extends Argument {
static override label: string | null = 'Time Format';
static override description: string | null =
'D3 time format string (e.g., "%Y-%m-%d", "%H:%M:%S")';
static override controlType = 'TimeFormatControl';
static default: string = 'smart_date';
// Standard D3 time format options
static readonly FORMAT_OPTIONS: SelectOption[] = [
{ label: 'Adaptive formatting', value: 'smart_date' },
{ label: '%d/%m/%Y | 14/01/2019', value: '%d/%m/%Y' },
{ label: '%m/%d/%Y | 01/14/2019', value: '%m/%d/%Y' },
{ label: '%d.%m.%Y | 14.01.2019', value: '%d.%m.%Y' },
{ label: '%Y-%m-%d | 2019-01-14', value: '%Y-%m-%d' },
{
label: '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10',
value: '%Y-%m-%d %H:%M:%S',
},
{
label: '%d-%m-%Y %H:%M:%S | 14-01-2019 01:32:10',
value: '%d-%m-%Y %H:%M:%S',
},
{ label: '%H:%M:%S | 01:32:10', value: '%H:%M:%S' },
];
static with(options: TimeFormatOptions): typeof TimeFormat {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
};
}
}
/**
* ConditionalFormatting - apply color rules based on metric values
*
* This is a special argument type that encapsulates the complex
* mapStateToProps logic needed for conditional formatting controls.
* The control automatically receives numeric column options from the chart response.
*/
export class ConditionalFormatting extends Argument {
static override label: string | null = 'Conditional Formatting';
static override description: string | null =
'Apply conditional color formatting to metric values';
static override controlType = 'ConditionalFormattingControl';
static default: ConditionalFormattingRule[] = [];
static with(
options: ConditionalFormattingOptions,
): typeof ConditionalFormatting {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
};
}
}
/**
* Slider - continuous floating point values with min/max/step
*
* Similar to Int but for float values.
* Maps to Superset's SliderControl.
*/
export class Slider extends Argument {
static override label: string | null = 'Slider';
static override description: string | null = 'A continuous numeric value';
static override controlType = 'SliderControl';
static default: number = 0;
static min: number = 0;
static max: number = 1;
static step: number = 0.1;
static with(options: SliderOptions): typeof Slider {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
static override min = options.min ?? Base.min;
static override max = options.max ?? Base.max;
static override step = options.step ?? Base.step;
};
}
}
/**
* Bounds - min/max value pairs
*
* Used for axis bounds, value ranges, etc.
* Maps to Superset's BoundsControl.
*/
export class Bounds extends Argument {
static override label: string | null = 'Bounds';
static override description: string | null = 'Min and max value bounds';
static override controlType = 'BoundsControl';
static default: BoundsValue = [null, null];
static with(options: BoundsOptions): typeof Bounds {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
};
}
}
/**
* ColorPicker - RGBA color selection
*
* Different from Color (which uses hex strings).
* Maps to Superset's ColorPickerControl with RGBA format.
*/
export class ColorPicker extends Argument {
static override label: string | null = 'Color';
static override description: string | null = 'Select a color';
static override controlType = 'ColorPickerControl';
static default: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
static with(options: ColorPickerOptions): typeof ColorPicker {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
};
}
}
/**
* RadioButton - mutually exclusive options
*
* Use for small sets of exclusive choices (2-4 options).
* Maps to Superset's RadioButtonControl.
*/
export class RadioButton extends Argument {
static override label: string | null = 'Option';
static override description: string | null = 'Select one option';
static override controlType = 'RadioButtonControl';
static default: string | boolean = '';
static options: RadioOption[] = [];
static with(options: RadioButtonOptions): typeof RadioButton {
const Base = this;
return class extends Base {
static override label = options.label ?? Base.label;
static override description = options.description ?? Base.description;
static override default = options.default ?? Base.default;
static override options = options.options;
};
}
}
/**
* Type guard to check if an argument class is a ConditionalFormatting type
*/
export function isConditionalFormattingArg(
argClass: typeof Argument,
): argClass is typeof ConditionalFormatting {
return argClass.controlType === 'ConditionalFormattingControl';
}
/**
* Type guard to check if an argument class is a TimeFormat type
*/
export function isTimeFormatArg(
argClass: typeof Argument,
): argClass is typeof TimeFormat {
return argClass.controlType === 'TimeFormatControl';
}
/**
* Type guard to check if an argument class is a NumberFormat type
*/
export function isNumberFormatArg(
argClass: typeof Argument,
): argClass is typeof NumberFormat {
return argClass.controlType === 'NumberFormatControl';
}
/**
* Type guard to check if an argument class is a Currency type
*/
export function isCurrencyArg(
argClass: typeof Argument,
): argClass is typeof Currency {
return argClass.controlType === 'CurrencyControl';
}
/**
* Type guard to check if an argument class is a Select type
*/
export function isSelectArg(
argClass: typeof Argument,
): argClass is typeof Select {
return (
'options' in argClass && Array.isArray((argClass as typeof Select).options)
);
}
/**
* Type guard to check if an argument class is a Checkbox type
*/
export function isCheckboxArg(
argClass: typeof Argument,
): argClass is typeof Checkbox {
return (
'default' in argClass &&
typeof (argClass as typeof Checkbox).default === 'boolean'
);
}
/**
* Type guard to check if an argument class is a Text type
*/
export function isTextArg(argClass: typeof Argument): argClass is typeof Text {
return (
argClass.controlType === 'TextControl' ||
(argClass.prototype instanceof Text &&
!isSelectArg(argClass) &&
!isCheckboxArg(argClass))
);
}
/**
* Type guard to check if an argument class is an Int type
*/
export function isIntArg(argClass: typeof Argument): argClass is typeof Int {
return 'min' in argClass && 'max' in argClass;
}
/**
* Type guard to check if an argument class is a Color type
*/
export function isColorArg(
argClass: typeof Argument,
): argClass is typeof Color {
return (
argClass.controlType === 'ColorPickerControl' ||
argClass.prototype instanceof Color
);
}
/**
* Type guard to check if an argument class is a Metric type
*/
export function isMetricArg(
argClass: typeof Argument,
): argClass is typeof Metric {
return argClass.columnType === ColumnType.Metric;
}
/**
* Type guard to check if an argument class is a Dimension type
*/
export function isDimensionArg(
argClass: typeof Argument,
): argClass is typeof Dimension {
return argClass.columnType === ColumnType.Dimension;
}
/**
* Type guard to check if an argument class is a Temporal type
*/
export function isTemporalArg(
argClass: typeof Argument,
): argClass is typeof Temporal {
return argClass.columnType === ColumnType.Temporal;
}
/**
* Type guard to check if an argument class is a Slider type
*/
export function isSliderArg(
argClass: typeof Argument,
): argClass is typeof Slider {
return (
argClass.controlType === 'SliderControl' &&
'step' in argClass &&
typeof (argClass as typeof Slider).step === 'number'
);
}
/**
* Type guard to check if an argument class is a Bounds type
*/
export function isBoundsArg(
argClass: typeof Argument,
): argClass is typeof Bounds {
return argClass.controlType === 'BoundsControl';
}
/**
* Type guard to check if an argument class is a ColorPicker type
*/
export function isColorPickerArg(
argClass: typeof Argument,
): argClass is typeof ColorPicker {
return (
argClass.controlType === 'ColorPickerControl' &&
'default' in argClass &&
typeof (argClass as typeof ColorPicker).default === 'object' &&
'r' in ((argClass as typeof ColorPicker).default as object)
);
}
/**
* Type guard to check if an argument class is a RadioButton type
*/
export function isRadioButtonArg(
argClass: typeof Argument,
): argClass is typeof RadioButton {
return argClass.controlType === 'RadioButtonControl';
}

View File

@@ -0,0 +1,245 @@
/**
* 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.
*/
/**
* Cross-Filter Utilities for Glyph Charts
*
* This module provides helpers for implementing cross-filtering in Glyph charts.
* Cross-filtering allows charts to filter other charts on the dashboard when
* users click on data points.
*
* ## Quick Start
*
* 1. Add behaviors to metadata:
* ```typescript
* metadata: {
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
* }
* ```
*
* 2. Extract cross-filter props in transform:
* ```typescript
* transform: (chartProps) => {
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap);
* return { transformedProps: { ...otherProps, ...crossFilterProps } };
* }
* ```
*
* 3. Use event handlers in render:
* ```typescript
* render: ({ transformedProps }) => {
* const eventHandlers = allEventHandlers(transformedProps);
* return <Echart eventHandlers={eventHandlers} ... />;
* }
* ```
*/
import type {
ChartProps,
FilterState,
QueryFormColumn,
SetDataMaskHook,
ContextMenuFilters,
} from '@superset-ui/core';
/**
* Props needed for cross-filtering in the render component.
* These are typically returned from the transform function and passed to Echart.
*/
export interface CrossFilterRenderProps {
/** Groupby columns used for filtering */
groupby: QueryFormColumn[];
/** Maps series names to their groupby column values */
labelMap: Record<string, string[]>;
/** Callback to emit cross-filter data mask */
setDataMask: SetDataMaskHook;
/** Maps series indices to selected value names */
selectedValues: Record<number, string>;
/** Whether cross-filters are enabled for this chart */
emitCrossFilters?: boolean;
/** Context menu handler for drill actions */
onContextMenu?: (
clientX: number,
clientY: number,
filters?: ContextMenuFilters,
) => void;
/** Column type mapping for formatting */
coltypeMapping?: Record<string, number>;
}
/**
* Create a selectedValues map from filterState.
*
* The selectedValues map is used by the Echart component to track which
* data points are currently selected (for highlighting).
*
* @param filterState - Current filter state from chartProps
* @param seriesNames - Array of series/data point names
* @returns Map of index -> name for selected values
*
* @example
* ```typescript
* const selectedValues = createSelectedValuesMap(
* filterState,
* transformedData.map(d => d.name),
* );
* ```
*/
export function createSelectedValuesMap(
filterState: FilterState | undefined,
seriesNames: string[],
): Record<number, string> {
return (filterState?.selectedValues || []).reduce(
(acc: Record<number, string>, selectedValue: string) => {
const index = seriesNames.findIndex(name => name === selectedValue);
if (index >= 0) {
return { ...acc, [index]: selectedValue };
}
return acc;
},
{},
);
}
/**
* Extract cross-filter related props from ChartProps.
*
* This is a convenience function that extracts all the props needed for
* cross-filtering from the standard ChartProps object.
*
* @param chartProps - The chart props from Superset
* @param groupby - The groupby columns (dimensions) from form data
* @param labelMap - A map from series names to their groupby values
* @param seriesNames - Array of series/data point names for selectedValues mapping
* @param coltypeMapping - Optional column type mapping
*
* @example
* ```typescript
* // In transform function:
* const labelMap = data.reduce((acc, datum) => ({
* ...acc,
* [extractGroupbyLabel({ datum, groupby })]: groupby.map(col => datum[col]),
* }), {});
*
* const crossFilterProps = extractCrossFilterProps(
* chartProps,
* groupby,
* labelMap,
* transformedData.map(d => d.name),
* coltypeMapping,
* );
*
* return {
* transformedProps: {
* echartOptions,
* formData,
* width,
* height,
* refs,
* ...crossFilterProps,
* },
* };
* ```
*/
export function extractCrossFilterProps(
chartProps: ChartProps,
groupby: QueryFormColumn[],
labelMap: Record<string, string[]>,
seriesNames: string[],
coltypeMapping?: Record<string, number>,
): CrossFilterRenderProps {
const { hooks, filterState, emitCrossFilters, formData } = chartProps;
const { setDataMask = () => {}, onContextMenu } = hooks ?? {};
const selectedValues = createSelectedValuesMap(filterState, seriesNames);
return {
groupby,
labelMap,
setDataMask,
selectedValues,
emitCrossFilters,
onContextMenu,
coltypeMapping,
// Also include formData for context menu formatting
formData,
} as CrossFilterRenderProps & { formData: unknown };
}
/**
* Check if a data point is currently filtered (should be dimmed).
*
* Use this in the transform function to apply opacity/styling to
* data points that are not part of the current filter selection.
*
* @param filterState - Current filter state from chartProps
* @param name - The name/label of the data point to check
* @returns true if the data point should be dimmed, false otherwise
*
* @example
* ```typescript
* const isFiltered = isDataPointFiltered(filterState, datum.name);
* const opacity = isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent;
* ```
*/
export function isDataPointFiltered(
filterState: FilterState | undefined,
name: string,
): boolean {
return Boolean(
filterState?.selectedValues &&
filterState.selectedValues.length > 0 &&
!filterState.selectedValues.includes(name),
);
}
/**
* Create a labelMap from data records.
*
* The labelMap maps series names (like "USA" or "2024-01") to their
* corresponding groupby column values. This is needed for the cross-filter
* event handlers to construct proper filter clauses.
*
* @param data - Array of data records
* @param groupbyLabels - Array of groupby column labels
* @param extractLabel - Function to extract the series label from a datum
* @returns Map of label -> groupby values
*
* @example
* ```typescript
* const labelMap = createLabelMap(
* data,
* groupbyLabels,
* datum => extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }),
* );
* ```
*/
export function createLabelMap<T extends Record<string, unknown>>(
data: T[],
groupbyLabels: string[],
extractLabel: (datum: T) => string,
): Record<string, string[]> {
return data.reduce((acc: Record<string, string[]>, datum: T) => {
const label = extractLabel(datum);
return {
...acc,
[label]: groupbyLabels.map(col => datum[col] as string),
};
}, {});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,419 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import type {
ControlPanelConfig,
ControlSetRow,
} from '@superset-ui/chart-controls';
import type { ChartProps } from '@superset-ui/core';
import {
Argument,
Select,
Text,
Checkbox,
Int,
Color,
isSelectArg,
isCheckboxArg,
isIntArg,
isColorArg,
isMetricArg,
isDimensionArg,
isTemporalArg,
} from './arguments';
import type { VisibilityFn, RgbaColor } from './types';
/**
* Configuration for a glyph argument with optional visibility control
*/
export interface GlyphArgConfig {
arg: typeof Argument;
visibility?: VisibilityFn;
resetOnHide?: boolean;
}
/**
* Arguments map - parameter name to argument class or config
*/
export type GlyphArguments = Map<string, typeof Argument | GlyphArgConfig>;
/**
* Convert hex color string to RGBA object for Superset's ColorPickerControl
*/
function hexToRgba(hex: string): RgbaColor {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result && result[1] && result[2] && result[3]) {
return {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: 1,
};
}
return { r: 0, g: 0, b: 0, a: 1 };
}
/**
* Convert RGBA object to hex color string
*/
function rgbaToHex(rgba: RgbaColor): string {
const toHex = (n: number) => n.toString(16).padStart(2, '0');
return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`;
}
/**
* Get the argument class from a config (handles both direct class and config object)
*/
function getArgClass(
argOrConfig: typeof Argument | GlyphArgConfig,
): typeof Argument {
return 'arg' in argOrConfig ? argOrConfig.arg : argOrConfig;
}
/**
* Get visibility config if present
*/
function getVisibilityConfig(argOrConfig: typeof Argument | GlyphArgConfig): {
visibility?: VisibilityFn;
resetOnHide?: boolean;
} {
if ('arg' in argOrConfig) {
return {
visibility: argOrConfig.visibility,
resetOnHide: argOrConfig.resetOnHide,
};
}
return {};
}
/**
* Generate Superset control config from a glyph Argument class
*/
export function getControlConfig(
argClass: typeof Argument,
paramName: string,
): Record<string, unknown> & { type: string } {
const label = argClass.label || paramName;
const description = argClass.description || '';
// Select control
if (isSelectArg(argClass)) {
return {
type: 'SelectControl',
label,
description,
default: argClass.default,
options: argClass.options,
clearable: argClass.clearable ?? false,
renderTrigger: true,
};
}
// Checkbox control
if (isCheckboxArg(argClass)) {
return {
type: 'CheckboxControl',
label,
description,
default: argClass.default,
renderTrigger: true,
};
}
// Int/Slider control
if (isIntArg(argClass)) {
return {
type: 'SliderControl',
label,
description,
default: argClass.default,
min: argClass.min,
max: argClass.max,
step: argClass.step ?? 1,
renderTrigger: true,
};
}
// Color control
if (isColorArg(argClass)) {
// eslint-disable-next-line theme-colors/no-literal-colors
const hexDefault = argClass.default ?? '#000000';
return {
type: 'ColorPickerControl',
label,
description,
default: hexToRgba(hexDefault),
renderTrigger: true,
};
}
// Default to TextControl
const textClass = argClass as typeof Text;
return {
type: 'TextControl',
label,
description,
default: textClass.default ?? '',
placeholder: textClass.placeholder ?? '',
renderTrigger: true,
};
}
/**
* Options for control panel generation
*/
export interface ControlPanelOptions {
/** Additional control rows for the query section */
queryControls?: ControlSetRow[];
/** Additional control rows for the chart options section */
chartOptionsControls?: ControlSetRow[];
/** Control overrides */
controlOverrides?: Record<string, Record<string, unknown>>;
/** Form data overrides function */
formDataOverrides?: (
formData: Record<string, unknown>,
) => Record<string, unknown>;
}
/**
* Generate a complete ControlPanelConfig from glyph arguments
*
* This is the core function that converts semantic argument definitions
* into Superset's control panel format.
*/
export function generateControlPanel(
glyphArguments: GlyphArguments,
options: ControlPanelOptions = {},
): ControlPanelConfig {
const queryControls: ControlSetRow[] = [];
const chartOptionsControls: ControlSetRow[] = [];
// Process each argument
for (const [paramName, argOrConfig] of glyphArguments) {
const argClass = getArgClass(argOrConfig);
const { visibility, resetOnHide } = getVisibilityConfig(argOrConfig);
// Data arguments go in Query section
if (isMetricArg(argClass)) {
queryControls.push(['metric']);
continue;
}
if (isDimensionArg(argClass)) {
queryControls.push(['groupby']);
continue;
}
if (isTemporalArg(argClass)) {
queryControls.push(['x_axis'], ['time_grain_sqla']);
continue;
}
// Style/visual arguments go in Chart Options section
const controlConfig = getControlConfig(argClass, paramName);
// Add visibility if specified
if (visibility) {
controlConfig.visibility = visibility;
controlConfig.resetOnHide = resetOnHide ?? false;
}
chartOptionsControls.push([
{
name: paramName,
config: controlConfig,
},
]);
}
// Add adhoc_filters to query section
queryControls.push(['adhoc_filters']);
// Merge with additional controls from options
const finalQueryControls = [
...queryControls,
...(options.queryControls || []),
];
const finalChartOptionsControls = [
...chartOptionsControls,
...(options.chartOptionsControls || []),
];
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: finalQueryControls,
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: finalChartOptionsControls,
},
],
};
if (options.controlOverrides) {
config.controlOverrides = options.controlOverrides;
}
if (options.formDataOverrides) {
// Type assertion needed because SqlaFormData is more specific than Record<string, unknown>
config.formDataOverrides =
options.formDataOverrides as ControlPanelConfig['formDataOverrides'];
}
return config;
}
/**
* Options for transformProps generation
*/
export interface TransformPropsOptions<TResult> {
/** Custom transformation function that receives extracted values */
transform?: (
values: Record<string, unknown>,
chartProps: ChartProps,
) => TResult;
/** Additional props to pass through from chartProps */
passthrough?: (keyof ChartProps)[];
}
/**
* Generate a transformProps function from glyph arguments
*
* This extracts values from formData based on argument definitions,
* applying type conversions as needed (e.g., RGBA to hex for colors).
*/
export function generateTransformProps<TResult = Record<string, unknown>>(
glyphArguments: GlyphArguments,
options: TransformPropsOptions<TResult> = {},
): (chartProps: ChartProps) => TResult {
return (chartProps: ChartProps) => {
const { formData, width, height, queriesData } = chartProps;
const values: Record<string, unknown> = {
width,
height,
queriesData,
};
// Add passthrough props
if (options.passthrough) {
for (const key of options.passthrough) {
values[key] = chartProps[key];
}
}
// Extract values from formData based on argument definitions
for (const [paramName, argOrConfig] of glyphArguments) {
const argClass = getArgClass(argOrConfig);
// Skip data arguments (metric, dimension, temporal) - these are handled differently
if (
isMetricArg(argClass) ||
isDimensionArg(argClass) ||
isTemporalArg(argClass)
) {
continue;
}
// Get value from formData, using default if not present
let value = formData[paramName];
// Color control: convert RGBA object to hex string
if (isColorArg(argClass)) {
const colorClass = argClass as typeof Color;
// eslint-disable-next-line theme-colors/no-literal-colors
const defaultRgba = hexToRgba(colorClass.default ?? '#000000');
const colorValue = value ?? defaultRgba;
if (
typeof colorValue === 'object' &&
colorValue !== null &&
'r' in colorValue
) {
value = rgbaToHex(colorValue as RgbaColor);
} else if (typeof colorValue === 'string') {
value = colorValue;
} else {
// eslint-disable-next-line theme-colors/no-literal-colors
value = colorClass.default ?? '#000000';
}
}
// Select control: use default if no value
else if (isSelectArg(argClass)) {
const selectClass = argClass as typeof Select;
value = value ?? selectClass.default;
}
// Checkbox control: use default if no value
else if (isCheckboxArg(argClass)) {
const checkboxClass = argClass as typeof Checkbox;
value = value ?? checkboxClass.default ?? false;
}
// Int control: use default if no value
else if (isIntArg(argClass)) {
const intClass = argClass as typeof Int;
value = value ?? intClass.default ?? 0;
}
// Text control: use default if no value
else {
const textClass = argClass as typeof Text;
value = value ?? textClass.default ?? '';
}
values[paramName] = value;
}
// Apply custom transformation if provided
if (options.transform) {
return options.transform(values, chartProps);
}
return values as TResult;
};
}
/**
* Combined result of creating a glyph plugin
*/
export interface GlyphPluginDef<TProps> {
controlPanel: ControlPanelConfig;
transformProps: (chartProps: ChartProps) => TProps;
}
/**
* Create both controlPanel and transformProps from a single argument definition
*
* This is the main entry point for the single-file viz pattern.
*/
export function createGlyphPlugin<TProps = Record<string, unknown>>(
glyphArguments: GlyphArguments,
controlPanelOptions: ControlPanelOptions = {},
transformPropsOptions: TransformPropsOptions<TProps> = {},
): GlyphPluginDef<TProps> {
return {
controlPanel: generateControlPanel(glyphArguments, controlPanelOptions),
transformProps: generateTransformProps(
glyphArguments,
transformPropsOptions,
),
};
}

View File

@@ -0,0 +1,71 @@
/**
* 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.
*/
/**
* Glyph Core - A declarative visualization plugin framework
*
* This module enables single-file visualization plugins where:
* 1. Arguments define both the chart's inputs AND the control panel
* 2. transformProps is auto-generated from argument definitions
* 3. The chart component is a simple function receiving typed arguments
*
* Features:
* - Single-file chart definitions with defineChart()
* - Declarative argument types (Metric, Dimension, Select, Checkbox, etc.)
* - Conditional visibility with visibleWhen/disabledWhen
* - Cross-filtering support with extractCrossFilterProps() and allEventHandlers()
* - Reusable presets (ShowLegend, HeaderFontSize, etc.)
*
* Example usage:
* ```typescript
* export default defineChart({
* metadata: {
* name: 'My Chart',
* thumbnail,
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
* },
* arguments: {
* metric: Metric.with({ label: 'Metric' }),
* groupby: Dimension.with({ label: 'Breakdowns' }),
* fontSize: Select.with({
* label: 'Font Size',
* options: [{ label: 'Small', value: 0.2 }, { label: 'Large', value: 0.4 }],
* default: 0.3,
* }),
* },
* transform: (chartProps, argValues) => {
* // Extract cross-filter props for interactive filtering
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap, seriesNames);
* return { transformedProps: { echartOptions, ...crossFilterProps } };
* },
* render: ({ transformedProps }) => {
* const eventHandlers = allEventHandlers(transformedProps);
* return <Echart eventHandlers={eventHandlers} ... />;
* },
* });
* ```
*/
// Re-export everything
export * from './types';
export * from './arguments';
export * from './generators';
export * from './defineChart';
export * from './presets';
export * from './crossFilter';

View File

@@ -0,0 +1,406 @@
/**
* 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.
*/
/**
* Glyph Presets - Reusable argument configurations
*
* This module contains pre-configured arguments that are commonly
* used across multiple visualization types. Charts can import these
* directly or use .with() to customize them further.
*
* Example usage:
* ```typescript
* import { HeaderFontSize, Subtitle } from '../../glyph-core/presets';
*
* arguments: {
* headerFontSize: HeaderFontSize,
* subtitle: Subtitle,
* // Override defaults when needed:
* customSize: HeaderFontSize.with({ default: 0.5 }),
* }
* ```
*/
import { t } from '@apache-superset/core/translation';
import { Select, Text, Checkbox } from './arguments';
import { SelectOption } from './types';
// ============================================================================
// Font Size Options
// ============================================================================
/**
* Large font size options - for primary/header text elements
* Values are multipliers of container height (0.2 = 20% of height)
*/
export const FONT_SIZE_OPTIONS_LARGE: SelectOption[] = [
{ label: t('Tiny'), value: 0.2 },
{ label: t('Small'), value: 0.3 },
{ label: t('Normal'), value: 0.4 },
{ label: t('Large'), value: 0.5 },
{ label: t('Huge'), value: 0.6 },
];
/**
* Small font size options - for secondary text elements (subtitles, labels)
* Values are multipliers of container height
*/
export const FONT_SIZE_OPTIONS_SMALL: SelectOption[] = [
{ label: t('Tiny'), value: 0.125 },
{ label: t('Small'), value: 0.15 },
{ label: t('Normal'), value: 0.2 },
{ label: t('Large'), value: 0.3 },
{ label: t('Huge'), value: 0.4 },
];
// ============================================================================
// Pre-configured Arguments
// ============================================================================
/**
* Header/primary font size selector
* Used for main display elements like big numbers, titles
*/
export const HeaderFontSize = Select.with({
label: t('Font Size'),
description: t('Font size for the primary display element'),
options: FONT_SIZE_OPTIONS_LARGE,
default: 0.4,
});
/**
* Subheader/secondary font size selector
* Used for subtitles, labels, secondary text
*/
export const SubheaderFontSize = Select.with({
label: t('Subheader Font Size'),
description: t('Font size for secondary text elements'),
options: FONT_SIZE_OPTIONS_SMALL,
default: 0.15,
});
/**
* Subtitle text input
* Generic subtitle/description field used by many chart types
*/
export const Subtitle = Text.with({
label: t('Subtitle'),
description: t('Description text displayed below the main content'),
default: '',
});
/**
* Show legend toggle
* Common toggle for charts with legends
*/
export const ShowLegend = Checkbox.with({
label: t('Show Legend'),
description: t('Whether to display the chart legend'),
default: true,
});
/**
* Force timestamp formatting toggle
* Used when a value might be a timestamp but isn't auto-detected
*/
export const ForceTimestampFormatting = Checkbox.with({
label: t('Force Date Format'),
description: t(
'Use date formatting even when the value is not detected as a timestamp',
),
default: false,
});
// ============================================================================
// Legend Options
// ============================================================================
export const LEGEND_TYPE_OPTIONS: SelectOption[] = [
{ label: t('Scroll'), value: 'scroll' },
{ label: t('List'), value: 'plain' },
];
export const LEGEND_ORIENTATION_OPTIONS: SelectOption[] = [
{ label: t('Top'), value: 'top' },
{ label: t('Bottom'), value: 'bottom' },
{ label: t('Left'), value: 'left' },
{ label: t('Right'), value: 'right' },
];
export const LEGEND_SORT_OPTIONS: SelectOption[] = [
{ label: t('No sort'), value: '' },
{ label: t('Ascending'), value: 'asc' },
{ label: t('Descending'), value: 'desc' },
];
/**
* Legend type selector
* Choose between scrollable or plain list legend
*/
export const LegendType = Select.with({
label: t('Legend Type'),
description: t('Type of legend display'),
options: LEGEND_TYPE_OPTIONS,
default: 'scroll',
});
/**
* Legend orientation selector
* Position the legend relative to the chart
*/
export const LegendOrientation = Select.with({
label: t('Legend Orientation'),
description: t('Position of the legend'),
options: LEGEND_ORIENTATION_OPTIONS,
default: 'top',
});
/**
* Legend sort selector
* Sort legend items alphabetically
*/
export const LegendSort = Select.with({
label: t('Legend Sort'),
description: t('Sort order for legend items'),
options: LEGEND_SORT_OPTIONS,
default: '',
});
// ============================================================================
// Label Presets
// ============================================================================
/**
* Show labels toggle
* Common toggle for chart labels
*/
export const ShowLabels = Checkbox.with({
label: t('Show Labels'),
description: t('Whether to display labels on the chart'),
default: true,
});
/**
* Show value toggle
* Common toggle for showing values on chart elements
*/
export const ShowValue = Checkbox.with({
label: t('Show Value'),
description: t('Whether to display values on the chart'),
default: false,
});
// ============================================================================
// Metric Name Presets
// ============================================================================
/**
* Show metric name toggle
* Used in BigNumber charts to optionally show the metric name
*/
export const ShowMetricName = Checkbox.with({
label: t('Show Metric Name'),
description: t('Whether to display the metric name as a title'),
default: false,
});
/**
* Metric name font size selector
* Typically used with visibility tied to ShowMetricName
*/
export const MetricNameFontSize = Select.with({
label: t('Metric Name Font Size'),
description: t('Font size for the metric name'),
options: FONT_SIZE_OPTIONS_SMALL,
default: 0.15,
});
// ============================================================================
// Label Type Options (shared by Pie, Funnel, etc.)
// ============================================================================
/**
* Standard label content type options
* Used by Pie, Funnel, and other category-based charts
*/
export const LABEL_TYPE_OPTIONS: SelectOption[] = [
{ label: t('Category Name'), value: 'key' },
{ label: t('Value'), value: 'value' },
{ label: t('Percentage'), value: 'percent' },
{ label: t('Category and Value'), value: 'key_value' },
{ label: t('Category and Percentage'), value: 'key_percent' },
{ label: t('Category, Value and Percentage'), value: 'key_value_percent' },
{ label: t('Value and Percentage'), value: 'value_percent' },
];
/**
* Label type selector for category-based charts
*/
export const LabelType = Select.with({
label: t('Label Type'),
description: t('What should be shown on the label?'),
options: LABEL_TYPE_OPTIONS,
default: 'key',
});
// ============================================================================
// Sort Options
// ============================================================================
export const SORT_OPTIONS: SelectOption[] = [
{ label: t('Descending'), value: 'descending' },
{ label: t('Ascending'), value: 'ascending' },
{ label: t('None'), value: 'none' },
];
/**
* Sort by metric toggle
* Common for charts that need to sort data by metric value
*/
export const SortByMetric = Checkbox.with({
label: t('Sort by Metric'),
description: t('Sort results by the selected metric'),
default: true,
});
// ============================================================================
// Label Position Options
// ============================================================================
export const LABEL_POSITION_OPTIONS: SelectOption[] = [
{ label: t('Top'), value: 'top' },
{ label: t('Left'), value: 'left' },
{ label: t('Right'), value: 'right' },
{ label: t('Bottom'), value: 'bottom' },
{ label: t('Inside'), value: 'inside' },
{ label: t('Inside Left'), value: 'insideLeft' },
{ label: t('Inside Right'), value: 'insideRight' },
{ label: t('Inside Top'), value: 'insideTop' },
{ label: t('Inside Bottom'), value: 'insideBottom' },
];
/**
* Label position selector
* Position labels relative to chart elements
*/
export const LabelPosition = Select.with({
label: t('Label Position'),
description: t('Position of labels on the chart'),
options: LABEL_POSITION_OPTIONS,
default: 'top',
});
// ============================================================================
// Simple Label Type (key/value variants only)
// ============================================================================
/**
* Simple label type options - for charts with fewer label display options
* Used by Radar, Sunburst, etc.
*/
export const SIMPLE_LABEL_TYPE_OPTIONS: SelectOption[] = [
{ label: t('Category Name'), value: 'key' },
{ label: t('Value'), value: 'value' },
{ label: t('Category and Value'), value: 'key_value' },
];
/**
* Simple label type selector
* For charts that only need key/value/key_value options
*/
export const SimpleLabelType = Select.with({
label: t('Label Type'),
description: t('What should be shown on the label?'),
options: SIMPLE_LABEL_TYPE_OPTIONS,
default: 'key',
});
/**
* Value-only label type options - for charts like Radar
*/
export const VALUE_LABEL_TYPE_OPTIONS: SelectOption[] = [
{ label: t('Value'), value: 'value' },
{ label: t('Category and Value'), value: 'key_value' },
];
/**
* Value label type selector
* For charts that show value or category+value
*/
export const ValueLabelType = Select.with({
label: t('Label Type'),
description: t('What should be shown on the label?'),
options: VALUE_LABEL_TYPE_OPTIONS,
default: 'value',
});
// ============================================================================
// Totals and Aggregates
// ============================================================================
/**
* Show total toggle
* For charts that can display aggregate totals
*/
export const ShowTotal = Checkbox.with({
label: t('Show Total'),
description: t('Whether to display the aggregate total'),
default: false,
});
// ============================================================================
// Threshold Controls
// ============================================================================
/**
* Label percentage threshold
* Minimum percentage for showing labels (avoids clutter on small slices)
*/
export const LabelThreshold = Text.with({
label: t('Percentage Threshold'),
description: t('Minimum threshold in percentage points for showing labels'),
default: '5',
});
// ============================================================================
// Shape Options
// ============================================================================
/**
* Circle shape toggle (used by Radar)
*/
export const CircleShape = Checkbox.with({
label: t('Circle Shape'),
description: t('Use circular shape instead of polygon'),
default: false,
});
// ============================================================================
// Data Zoom
// ============================================================================
/**
* Enable data zoom toggle
* For charts with zoomable data areas
*/
export const DataZoom = Checkbox.with({
label: t('Data Zoom'),
description: t('Enable data zooming controls'),
default: false,
});

View File

@@ -0,0 +1,306 @@
/**
* 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 type { ControlPanelConfig } from '@superset-ui/chart-controls';
import type { ChartProps } from '@superset-ui/core';
/**
* Option for Select controls
*/
export interface SelectOption {
label: string;
value: string | number;
}
/**
* Configuration options for Select argument type
*/
export interface SelectOptions {
label?: string;
description?: string;
default?: string | number;
options?: SelectOption[];
clearable?: boolean;
renderTrigger?: boolean;
}
/**
* Configuration options for Text argument type
*/
export interface TextOptions {
label?: string;
description?: string;
default?: string;
placeholder?: string;
}
/**
* Configuration options for Checkbox argument type
*/
export interface CheckboxOptions {
label?: string;
description?: string;
default?: boolean;
}
/**
* Configuration options for Int argument type (slider)
*/
export interface IntOptions {
label?: string;
description?: string;
default?: number;
min?: number;
max?: number;
step?: number;
}
/**
* Configuration options for Color argument type
*/
export interface ColorOptions {
label?: string;
description?: string;
default?: string;
}
/**
* Configuration options for Metric argument type
*/
export interface MetricOptions {
label?: string;
description?: string;
multi?: boolean;
}
/**
* Configuration options for Dimension argument type
*/
export interface DimensionOptions {
label?: string;
description?: string;
multi?: boolean;
}
/**
* Configuration options for NumberFormat argument type
*/
export interface NumberFormatOptions {
label?: string;
description?: string;
default?: string;
}
/**
* Currency value structure
*/
export interface CurrencyValue {
symbol?: string;
symbolPosition?: 'prefix' | 'suffix';
}
/**
* Configuration options for Currency argument type
*/
export interface CurrencyOptions {
label?: string;
description?: string;
default?: CurrencyValue;
}
/**
* Configuration options for TimeFormat argument type
*/
export interface TimeFormatOptions {
label?: string;
description?: string;
default?: string;
}
/**
* Configuration options for ConditionalFormatting argument type
*/
export interface ConditionalFormattingOptions {
label?: string;
description?: string;
}
/**
* Configuration options for Slider argument type (continuous float values)
*/
export interface SliderOptions {
label?: string;
description?: string;
default?: number;
min?: number;
max?: number;
step?: number;
}
/**
* Configuration options for Bounds argument type (min/max pairs)
*/
export interface BoundsOptions {
label?: string;
description?: string;
default?: [number | null, number | null];
}
/**
* Bounds value type - tuple of [min, max] where either can be null
*/
export type BoundsValue = [number | null, number | null];
/**
* Configuration options for ColorPicker argument type (RGBA colors)
*/
export interface ColorPickerOptions {
label?: string;
description?: string;
default?: RgbaColor;
}
/**
* Configuration options for RadioButton argument type
*/
export interface RadioButtonOptions {
label?: string;
description?: string;
default?: string | boolean;
options: RadioOption[];
}
/**
* Option for RadioButton controls
*/
export interface RadioOption {
label: string;
value: string | boolean;
}
/**
* Conditional formatting rule value
*/
export interface ConditionalFormattingRule {
column?: string;
operator?: '<' | '<=' | '>' | '>=' | '==' | '!=' | 'between';
targetValue?: number;
targetValueLeft?: number;
targetValueRight?: number;
colorScheme?: string;
}
/**
* Column type enum for data arguments
*/
export enum ColumnType {
Metric = 'metric',
Dimension = 'dimension',
Temporal = 'temporal',
Argument = 'argument',
}
/**
* Base argument class interface
*/
export interface ArgumentClass {
label: string | null;
description: string | null;
columnType?: ColumnType;
controlType?: string;
}
/**
* RGBA color format used by Superset's ColorPickerControl
*/
export interface RgbaColor {
r: number;
g: number;
b: number;
a: number;
}
/**
* Visibility function for conditional control display (legacy)
*/
export type VisibilityFn = (state: {
controls: Record<string, { value: unknown }>;
}) => boolean;
/**
* Declarative condition for argument visibility/disabled state.
*
* Keys are argument names, values define the condition:
* - Literal value: equality check (e.g., { showMetricName: true })
* - Function: custom check (e.g., { subtitle: (val) => !!val })
*
* Multiple keys are AND'd together.
*
* @example
* // Visible when showMetricName is true
* visibleWhen: { showMetricName: true }
*
* @example
* // Visible when subtitle is not empty
* visibleWhen: { subtitle: (val) => !!val }
*
* @example
* // Visible when showMetricName is true AND subtitle is not empty
* visibleWhen: { showMetricName: true, subtitle: (val) => !!val }
*/
export type ArgumentCondition = Record<
string,
unknown | ((value: unknown) => boolean)
>;
/**
* Extended control configuration with visibility
*/
export interface ControlConfig {
name: string;
config: Record<string, unknown>;
visibility?: VisibilityFn;
resetOnHide?: boolean;
}
/**
* Glyph chart definition
*/
export interface GlyphChartDef<TArgs extends Record<string, ArgumentClass>> {
arguments: TArgs;
sections?: {
query?: ControlConfig[][];
chartOptions?: ControlConfig[][];
};
}
/**
* Result of createGlyphPlugin
*/
export interface GlyphPluginResult<TFormData> {
controlPanel: ControlPanelConfig;
transformProps: (chartProps: ChartProps) => TFormData;
}
/**
* Type helper to extract form data types from argument definitions
*/
export type ArgumentsToFormData<TArgs extends Record<string, ArgumentClass>> = {
[K in keyof TArgs]: TArgs[K] extends { default: infer D } ? D : unknown;
};

View File

@@ -0,0 +1,475 @@
/**
* 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 {
Argument,
Bounds,
Checkbox,
Color,
ColorPicker,
ConditionalFormatting,
Currency,
Dimension,
Int,
isBoundsArg,
isCheckboxArg,
isColorArg,
isColorPickerArg,
isConditionalFormattingArg,
isCurrencyArg,
isDimensionArg,
isIntArg,
isMetricArg,
isNumberFormatArg,
isRadioButtonArg,
isSelectArg,
isSliderArg,
isTemporalArg,
isTextArg,
isTimeFormatArg,
Metric,
NumberFormat,
RadioButton,
Select,
Slider,
Temporal,
Text,
TimeFormat,
} from '@superset-ui/glyph-core';
import { ColumnType } from '@superset-ui/glyph-core/types';
describe('Argument base class', () => {
test('stores its constructor value', () => {
const a = new Argument(42);
expect(a.value).toBe(42);
});
test('has expected static defaults', () => {
expect(Argument.label).toBeNull();
expect(Argument.description).toBeNull();
expect(Argument.columnType).toBe(ColumnType.Argument);
expect(Argument.controlType).toBe('TextControl');
});
});
describe('Metric', () => {
test('has expected static metadata', () => {
expect(Metric.label).toBe('Metric');
expect(Metric.columnType).toBe(ColumnType.Metric);
expect(Metric.controlType).toBe('MetricsControl');
expect(Metric.multi).toBe(false);
});
test('.with() overrides label, description, multi', () => {
const M = Metric.with({
label: 'Sales',
description: 'Total sales',
multi: true,
});
expect(M.label).toBe('Sales');
expect(M.description).toBe('Total sales');
expect(M.multi).toBe(true);
// unaltered ancestor metadata still present
expect(M.columnType).toBe(ColumnType.Metric);
expect(M.controlType).toBe('MetricsControl');
});
test('.with() falls back to parent defaults when option omitted', () => {
const M = Metric.with({ label: 'X' });
expect(M.label).toBe('X');
expect(M.multi).toBe(Metric.multi);
expect(M.description).toBe(Metric.description);
});
test('isMetricArg type guard', () => {
expect(isMetricArg(Metric)).toBe(true);
expect(isMetricArg(Metric.with({ label: 'X' }))).toBe(true);
expect(isMetricArg(Dimension)).toBe(false);
expect(isMetricArg(Select)).toBe(false);
});
});
describe('Dimension', () => {
test('has expected static metadata', () => {
expect(Dimension.label).toBe('Dimension');
expect(Dimension.columnType).toBe(ColumnType.Dimension);
expect(Dimension.controlType).toBe('GroupByControl');
expect(Dimension.multi).toBe(true);
});
test('.with() overrides label, description, multi', () => {
const D = Dimension.with({
label: 'Region',
multi: false,
});
expect(D.label).toBe('Region');
expect(D.multi).toBe(false);
});
test('isDimensionArg type guard', () => {
expect(isDimensionArg(Dimension)).toBe(true);
expect(isDimensionArg(Dimension.with({ label: 'X' }))).toBe(true);
expect(isDimensionArg(Metric)).toBe(false);
});
});
describe('Temporal', () => {
test('has expected static metadata', () => {
expect(Temporal.label).toBe('Time Column');
expect(Temporal.columnType).toBe(ColumnType.Temporal);
expect(Temporal.controlType).toBe('TemporalControl');
});
test('.with() overrides label and description', () => {
const T = Temporal.with({ label: 'Order Date' });
expect(T.label).toBe('Order Date');
});
test('isTemporalArg type guard', () => {
expect(isTemporalArg(Temporal)).toBe(true);
expect(isTemporalArg(Metric)).toBe(false);
});
});
describe('Select', () => {
const OPTIONS = [
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
];
test('has expected static defaults', () => {
expect(Select.label).toBe('Select');
expect(Select.controlType).toBe('SelectControl');
expect(Select.options).toEqual([]);
expect(Select.default).toBe('');
});
test('.with() applies label, default, options', () => {
const S = Select.with({
label: 'Choice',
default: 'a',
options: OPTIONS,
});
expect(S.label).toBe('Choice');
expect(S.default).toBe('a');
expect(S.options).toEqual(OPTIONS);
});
test('isSelectArg type guard', () => {
expect(isSelectArg(Select.with({ options: OPTIONS }))).toBe(true);
expect(isSelectArg(Checkbox)).toBe(false);
expect(isSelectArg(Metric)).toBe(false);
});
});
describe('Text', () => {
test('has expected static defaults', () => {
expect(Text.controlType).toBe('TextControl');
expect(Text.default).toBe('');
expect(Text.placeholder).toBe('');
});
test('.with() applies label, default, placeholder', () => {
const T = Text.with({
label: 'Title',
default: 'Untitled',
placeholder: 'Enter title',
});
expect(T.label).toBe('Title');
expect(T.default).toBe('Untitled');
expect(T.placeholder).toBe('Enter title');
});
test('isTextArg type guard accepts Text but not Select/Checkbox', () => {
expect(isTextArg(Text)).toBe(true);
expect(isTextArg(Text.with({ label: 'X' }))).toBe(true);
expect(isTextArg(Checkbox)).toBe(false);
});
});
describe('Checkbox', () => {
test('has expected static defaults', () => {
expect(Checkbox.controlType).toBe('CheckboxControl');
expect(Checkbox.default).toBe(false);
});
test('.with() applies label, description, default', () => {
const C = Checkbox.with({
label: 'Show legend',
default: true,
});
expect(C.label).toBe('Show legend');
expect(C.default).toBe(true);
});
test('isCheckboxArg type guard', () => {
expect(isCheckboxArg(Checkbox)).toBe(true);
expect(isCheckboxArg(Checkbox.with({ default: true }))).toBe(true);
expect(isCheckboxArg(Text)).toBe(false);
});
});
describe('Int', () => {
test('has expected static defaults', () => {
expect(Int.controlType).toBe('SliderControl');
expect(Int.default).toBe(0);
expect(Int.min).toBe(0);
expect(Int.max).toBe(100);
expect(Int.step).toBe(1);
});
test('.with() applies label, default, min, max, step', () => {
const I = Int.with({
label: 'Limit',
default: 50,
min: 10,
max: 1000,
step: 5,
});
expect(I.label).toBe('Limit');
expect(I.default).toBe(50);
expect(I.min).toBe(10);
expect(I.max).toBe(1000);
expect(I.step).toBe(5);
});
test('isIntArg type guard', () => {
expect(isIntArg(Int)).toBe(true);
expect(isIntArg(Slider)).toBe(true); // Slider also has min/max
expect(isIntArg(Checkbox)).toBe(false);
});
});
describe('Color', () => {
test('has expected static defaults', () => {
expect(Color.controlType).toBe('ColorPickerControl');
expect(Color.default).toBe('#000000');
});
test('.with() applies label, default', () => {
const C = Color.with({ label: 'Fill', default: '#ff0000' });
expect(C.label).toBe('Fill');
expect(C.default).toBe('#ff0000');
});
test('isColorArg type guard', () => {
expect(isColorArg(Color)).toBe(true);
expect(isColorArg(Color.with({ default: '#ff0000' }))).toBe(true);
expect(isColorArg(Metric)).toBe(false);
});
});
describe('NumberFormat', () => {
test('has expected static defaults', () => {
expect(NumberFormat.controlType).toBe('NumberFormatControl');
expect(NumberFormat.default).toBe('SMART_NUMBER');
expect(NumberFormat.FORMAT_OPTIONS.length).toBeGreaterThan(10);
expect(
NumberFormat.FORMAT_OPTIONS.some(o => o.value === 'SMART_NUMBER'),
).toBe(true);
});
test('.with() applies label, default', () => {
const N = NumberFormat.with({ label: 'Amount', default: '.2f' });
expect(N.label).toBe('Amount');
expect(N.default).toBe('.2f');
});
test('isNumberFormatArg type guard', () => {
expect(isNumberFormatArg(NumberFormat)).toBe(true);
expect(isNumberFormatArg(TimeFormat)).toBe(false);
});
});
describe('Currency', () => {
test('has expected static defaults', () => {
expect(Currency.controlType).toBe('CurrencyControl');
expect(Currency.default).toEqual({});
});
test('.with() applies label, default', () => {
const C = Currency.with({
label: 'Money',
default: { symbol: 'USD', symbolPosition: 'prefix' },
});
expect(C.label).toBe('Money');
expect(C.default).toEqual({ symbol: 'USD', symbolPosition: 'prefix' });
});
test('isCurrencyArg type guard', () => {
expect(isCurrencyArg(Currency)).toBe(true);
expect(isCurrencyArg(NumberFormat)).toBe(false);
});
});
describe('TimeFormat', () => {
test('has expected static defaults', () => {
expect(TimeFormat.controlType).toBe('TimeFormatControl');
expect(TimeFormat.default).toBe('smart_date');
expect(
TimeFormat.FORMAT_OPTIONS.some(o => o.value === 'smart_date'),
).toBe(true);
});
test('.with() applies label, default', () => {
const T = TimeFormat.with({ label: 'When', default: '%Y-%m-%d' });
expect(T.label).toBe('When');
expect(T.default).toBe('%Y-%m-%d');
});
test('isTimeFormatArg type guard', () => {
expect(isTimeFormatArg(TimeFormat)).toBe(true);
expect(isTimeFormatArg(NumberFormat)).toBe(false);
});
});
describe('ConditionalFormatting', () => {
test('has expected static defaults', () => {
expect(ConditionalFormatting.controlType).toBe(
'ConditionalFormattingControl',
);
expect(ConditionalFormatting.default).toEqual([]);
});
test('.with() applies label and description (not default)', () => {
const CF = ConditionalFormatting.with({ label: 'Format' });
expect(CF.label).toBe('Format');
});
test('isConditionalFormattingArg type guard', () => {
expect(isConditionalFormattingArg(ConditionalFormatting)).toBe(true);
expect(isConditionalFormattingArg(Select)).toBe(false);
});
});
describe('Slider', () => {
test('has expected float-friendly defaults', () => {
expect(Slider.controlType).toBe('SliderControl');
expect(Slider.default).toBe(0);
expect(Slider.min).toBe(0);
expect(Slider.max).toBe(1);
expect(Slider.step).toBe(0.1);
});
test('.with() applies all numeric fields', () => {
const S = Slider.with({
label: 'Opacity',
default: 0.8,
min: 0,
max: 1,
step: 0.05,
});
expect(S.label).toBe('Opacity');
expect(S.default).toBe(0.8);
expect(S.step).toBe(0.05);
});
test('isSliderArg type guard requires float step', () => {
expect(isSliderArg(Slider)).toBe(true);
// Int is also SliderControl + has step but step is integer-valued — still
// numeric so the guard recognizes it (current behavior); document it.
expect(isSliderArg(Int)).toBe(true);
expect(isSliderArg(Checkbox)).toBe(false);
});
});
describe('Bounds', () => {
test('has expected static defaults', () => {
expect(Bounds.controlType).toBe('BoundsControl');
expect(Bounds.default).toEqual([null, null]);
});
test('.with() applies default', () => {
const B = Bounds.with({ label: 'Range', default: [0, 100] });
expect(B.label).toBe('Range');
expect(B.default).toEqual([0, 100]);
});
test('isBoundsArg type guard', () => {
expect(isBoundsArg(Bounds)).toBe(true);
expect(isBoundsArg(Int)).toBe(false);
});
});
describe('ColorPicker', () => {
test('has expected static defaults', () => {
expect(ColorPicker.controlType).toBe('ColorPickerControl');
expect(ColorPicker.default).toEqual({ r: 0, g: 0, b: 0, a: 1 });
});
test('.with() applies default', () => {
const CP = ColorPicker.with({
label: 'Pick',
default: { r: 255, g: 0, b: 0, a: 0.5 },
});
expect(CP.label).toBe('Pick');
expect(CP.default).toEqual({ r: 255, g: 0, b: 0, a: 0.5 });
});
test('isColorPickerArg distinguishes from Color (string)', () => {
expect(isColorPickerArg(ColorPicker)).toBe(true);
expect(isColorPickerArg(Color)).toBe(false);
});
});
describe('RadioButton', () => {
const RADIO_OPTIONS = [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
];
test('has expected static defaults', () => {
expect(RadioButton.controlType).toBe('RadioButtonControl');
expect(RadioButton.default).toBe('');
expect(RadioButton.options).toEqual([]);
});
test('.with() applies all fields', () => {
const RB = RadioButton.with({
label: 'Toggle',
default: true,
options: RADIO_OPTIONS,
});
expect(RB.label).toBe('Toggle');
expect(RB.default).toBe(true);
expect(RB.options).toEqual(RADIO_OPTIONS);
});
test('isRadioButtonArg type guard', () => {
expect(isRadioButtonArg(RadioButton)).toBe(true);
expect(isRadioButtonArg(Select)).toBe(false);
});
});
describe('Argument inheritance via .with()', () => {
test('chained .with() calls compose overrides', () => {
const Base = Select.with({
label: 'Pick one',
options: [{ label: 'A', value: 'a' }],
});
const Tighter = Base.with({ label: 'Pick exactly one' });
expect(Tighter.label).toBe('Pick exactly one');
expect(Tighter.options).toEqual([{ label: 'A', value: 'a' }]);
});
test('original class is unmodified after .with()', () => {
const before = Metric.multi;
Metric.with({ multi: !before });
expect(Metric.multi).toBe(before);
});
});

View File

@@ -0,0 +1,212 @@
/**
* 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 type { ChartProps, FilterState } from '@superset-ui/core';
import {
createLabelMap,
createSelectedValuesMap,
extractCrossFilterProps,
isDataPointFiltered,
} from '@superset-ui/glyph-core';
describe('createSelectedValuesMap', () => {
test('returns empty object when filterState is undefined', () => {
expect(createSelectedValuesMap(undefined, ['a', 'b'])).toEqual({});
});
test('returns empty object when selectedValues is undefined', () => {
expect(
createSelectedValuesMap({} as FilterState, ['a', 'b']),
).toEqual({});
});
test('returns empty object when selectedValues is empty', () => {
expect(
createSelectedValuesMap(
{ selectedValues: [] } as unknown as FilterState,
['a', 'b'],
),
).toEqual({});
});
test('maps selected value to its index in seriesNames', () => {
const result = createSelectedValuesMap(
{ selectedValues: ['b'] } as unknown as FilterState,
['a', 'b', 'c'],
);
expect(result).toEqual({ 1: 'b' });
});
test('maps multiple selected values to their indices', () => {
const result = createSelectedValuesMap(
{ selectedValues: ['a', 'c'] } as unknown as FilterState,
['a', 'b', 'c'],
);
expect(result).toEqual({ 0: 'a', 2: 'c' });
});
test('ignores selected values not in seriesNames', () => {
const result = createSelectedValuesMap(
{ selectedValues: ['x', 'a'] } as unknown as FilterState,
['a', 'b', 'c'],
);
expect(result).toEqual({ 0: 'a' });
});
});
describe('isDataPointFiltered', () => {
test('returns false when no filterState', () => {
expect(isDataPointFiltered(undefined, 'a')).toBe(false);
});
test('returns false when selectedValues is empty', () => {
expect(
isDataPointFiltered(
{ selectedValues: [] } as unknown as FilterState,
'a',
),
).toBe(false);
});
test('returns false when name is in selectedValues', () => {
expect(
isDataPointFiltered(
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
'a',
),
).toBe(false);
});
test('returns true when name is NOT in non-empty selectedValues', () => {
expect(
isDataPointFiltered(
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
'c',
),
).toBe(true);
});
});
describe('createLabelMap', () => {
test('returns empty object for empty data', () => {
expect(createLabelMap([], ['col1'], () => 'label')).toEqual({});
});
test('maps each record to its label and groupby column values', () => {
const data = [
{ country: 'USA', region: 'North' },
{ country: 'Brazil', region: 'South' },
];
const result = createLabelMap(
data,
['country', 'region'],
d => d.country as string,
);
expect(result).toEqual({
USA: ['USA', 'North'],
Brazil: ['Brazil', 'South'],
});
});
test('last record wins when extractLabel collides', () => {
const data = [
{ name: 'X', value: 1 },
{ name: 'X', value: 2 },
];
const result = createLabelMap(data, ['value'], d => d.name as string);
// collision: later entry overwrites
expect(result).toEqual({ X: [2] });
});
test('groupbyLabels controls the columns extracted, not the label', () => {
const data = [{ a: 1, b: 2, c: 3 }];
const result = createLabelMap(data, ['c'], () => 'only-key');
expect(result).toEqual({ 'only-key': [3] });
});
});
describe('extractCrossFilterProps', () => {
const baseChartProps = {
hooks: { setDataMask: jest.fn(), onContextMenu: jest.fn() },
filterState: {
selectedValues: ['USA'],
} as unknown as FilterState,
emitCrossFilters: true,
formData: { viz_type: 'test' },
} as unknown as ChartProps;
test('returns all expected fields', () => {
const result = extractCrossFilterProps(
baseChartProps,
['country'],
{ USA: ['USA'] },
['USA', 'Brazil'],
);
expect(result.groupby).toEqual(['country']);
expect(result.labelMap).toEqual({ USA: ['USA'] });
expect(result.selectedValues).toEqual({ 0: 'USA' });
expect(result.emitCrossFilters).toBe(true);
expect(result.setDataMask).toBe(baseChartProps.hooks!.setDataMask);
expect(result.onContextMenu).toBe(baseChartProps.hooks!.onContextMenu);
});
test('coltypeMapping pass-through when provided', () => {
const result = extractCrossFilterProps(
baseChartProps,
['country'],
{},
[],
{ country: 1 },
);
expect(result.coltypeMapping).toEqual({ country: 1 });
});
test('defaults setDataMask to a no-op when hooks omits it', () => {
const chartProps = {
...baseChartProps,
hooks: {},
} as unknown as ChartProps;
const result = extractCrossFilterProps(chartProps, [], {}, []);
expect(typeof result.setDataMask).toBe('function');
// No throw when invoked
expect(() =>
result.setDataMask({ filterState: {} } as unknown as Parameters<
typeof result.setDataMask
>[0]),
).not.toThrow();
});
test('formData is included in the returned shape (for context menu formatting)', () => {
const result = extractCrossFilterProps(
baseChartProps,
['country'],
{},
[],
) as ReturnType<typeof extractCrossFilterProps> & { formData: unknown };
expect(result.formData).toEqual({ viz_type: 'test' });
});
test('selectedValues is empty when filterState has none', () => {
const chartProps = {
...baseChartProps,
filterState: {} as FilterState,
} as unknown as ChartProps;
const result = extractCrossFilterProps(chartProps, [], {}, ['x']);
expect(result.selectedValues).toEqual({});
});
});

View File

@@ -0,0 +1,563 @@
/**
* 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 {
Behavior,
ChartLabel,
ChartMetadata,
ChartPlugin,
} from '@superset-ui/core';
import {
Checkbox,
defineChart,
Dimension,
evaluateGlyphCondition,
getArgVisibleWhen,
Metric,
resolveArgClass,
Select,
Text,
} from '@superset-ui/glyph-core';
// Helper: instantiate a plugin and reach its controlPanel config.
function instantiate(PluginClass: ReturnType<typeof defineChart>) {
const plugin = new PluginClass();
// ChartPlugin internals expose the panel under .controlPanel
// (set via super({ controlPanel }) in defineChart's GlyphChartPlugin).
return {
plugin,
controlPanel: (plugin as unknown as { controlPanel: Record<string, unknown> })
.controlPanel,
metadata: (plugin as unknown as { metadata: ChartMetadata }).metadata,
};
}
const MIN_THUMBNAIL = 'thumb.png';
describe('resolveArgClass', () => {
test('returns the bare class form unchanged', () => {
expect(resolveArgClass(Metric)).toBe(Metric);
});
test('unwraps the { arg, visibleWhen } object form', () => {
const M = Metric.with({ label: 'Sales' });
const argDef = { arg: M, visibleWhen: { show: true } };
expect(resolveArgClass(argDef)).toBe(M);
});
});
describe('getArgVisibleWhen', () => {
test('returns undefined for bare class form', () => {
expect(getArgVisibleWhen(Metric)).toBeUndefined();
});
test('returns the condition for object form', () => {
const argDef = { arg: Metric, visibleWhen: { show: true } };
expect(getArgVisibleWhen(argDef)).toEqual({ show: true });
});
test('returns undefined when object form has no visibleWhen', () => {
expect(getArgVisibleWhen({ arg: Metric })).toBeUndefined();
});
});
describe('evaluateGlyphCondition', () => {
test('returns true for empty condition', () => {
expect(evaluateGlyphCondition({}, { foo: 1 })).toBe(true);
});
test('returns true when equality check matches', () => {
expect(evaluateGlyphCondition({ show: true }, { show: true })).toBe(true);
});
test('returns false when equality check fails', () => {
expect(evaluateGlyphCondition({ show: true }, { show: false })).toBe(false);
});
test('handles missing formData keys as undefined', () => {
expect(evaluateGlyphCondition({ show: true }, {})).toBe(false);
});
test('supports function-valued conditions', () => {
const cond = { subtitle: (val: unknown) => !!val };
expect(evaluateGlyphCondition(cond, { subtitle: 'hi' })).toBe(true);
expect(evaluateGlyphCondition(cond, { subtitle: '' })).toBe(false);
});
test('requires all keys in the condition to pass (AND semantics)', () => {
const cond = { a: true, b: 'x' };
expect(evaluateGlyphCondition(cond, { a: true, b: 'x' })).toBe(true);
expect(evaluateGlyphCondition(cond, { a: true, b: 'y' })).toBe(false);
expect(evaluateGlyphCondition(cond, { a: false, b: 'x' })).toBe(false);
});
});
describe('defineChart - basic plugin construction', () => {
test('returns a ChartPlugin subclass', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const p = new Plugin();
expect(p).toBeInstanceOf(ChartPlugin);
});
test('plugin metadata is a ChartMetadata instance with required fields', () => {
const Plugin = defineChart({
metadata: {
name: 'Test',
description: 'A test chart',
category: 'Charts',
tags: ['test'],
thumbnail: MIN_THUMBNAIL,
},
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
expect(metadata).toBeInstanceOf(ChartMetadata);
expect(metadata.name).toBe('Test');
expect(metadata.description).toBe('A test chart');
expect(metadata.category).toBe('Charts');
expect(metadata.tags).toEqual(['test']);
expect(metadata.thumbnail).toBe(MIN_THUMBNAIL);
});
test('metadata defaults Behavior.InteractiveChart when omitted', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
expect(metadata.behaviors).toContain(Behavior.InteractiveChart);
});
test('metadata behaviors override the default when provided', () => {
const Plugin = defineChart({
metadata: {
name: 'Test',
thumbnail: MIN_THUMBNAIL,
behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail],
},
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
expect(metadata.behaviors).toEqual([
Behavior.InteractiveChart,
Behavior.DrillToDetail,
]);
});
test('passes label, canBeAnnotationTypes, useLegacyApi, supportedAnnotationTypes through', () => {
const Plugin = defineChart({
metadata: {
name: 'Test',
thumbnail: MIN_THUMBNAIL,
label: ChartLabel.Deprecated,
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
useLegacyApi: true,
supportedAnnotationTypes: ['FORMULA'],
credits: ['https://example.com'],
},
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
expect(metadata.label).toBe(ChartLabel.Deprecated);
expect(metadata.canBeAnnotationTypes).toEqual(['EVENT', 'INTERVAL']);
expect(metadata.useLegacyApi).toBe(true);
expect(metadata.supportedAnnotationTypes).toEqual(['FORMULA']);
expect(metadata.credits).toEqual(['https://example.com']);
});
test('exampleGallery + thumbnailDark are preserved', () => {
const Plugin = defineChart({
metadata: {
name: 'Test',
thumbnail: MIN_THUMBNAIL,
thumbnailDark: 'thumb-dark.png',
exampleGallery: [{ url: 'a.png', urlDark: 'a-dark.png' }],
},
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
expect(metadata.thumbnailDark).toBe('thumb-dark.png');
expect(metadata.exampleGallery).toEqual([
{ url: 'a.png', urlDark: 'a-dark.png' },
]);
});
});
describe('defineChart - controlPanel generation from arguments', () => {
test('Query section is auto-generated from Metric/Dimension arguments', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {
metric: Metric,
groupby: Dimension,
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
// Query section should be auto-generated
expect(sections.some(s => s?.label === 'Query')).toBe(true);
});
test('suppressQuerySection: true skips the auto Query section', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {
metric: Metric,
},
suppressQuerySection: true,
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
// The auto-generated Query section is suppressed.
// (Charts using suppressQuerySection typically provide their own via
// prependSections — see legacy nvd3 / deckgl consolidations.)
const autoQuery = sections.find(s => s?.label === 'Query');
expect(autoQuery).toBeUndefined();
});
test('Chart Options section is generated when there are non-data args', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {
metric: Metric,
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
});
test('Chart Options section is hidden when there are no customize args', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {
metric: Metric,
groupby: Dimension,
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
// No Customize-tab content → Chart Options auto-hides.
expect(sections.some(s => s?.label === 'Chart Options')).toBe(false);
});
});
describe('defineChart - prependSections / middleSections / additionalSections', () => {
test('prependSections appears before the auto Query section', () => {
const TIME_SECTION = {
label: 'Time',
controlSetRows: [],
};
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: { metric: Metric },
prependSections: [TIME_SECTION],
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
const timeIdx = sections.findIndex(s => s?.label === 'Time');
const queryIdx = sections.findIndex(s => s?.label === 'Query');
expect(timeIdx).toBeGreaterThanOrEqual(0);
expect(queryIdx).toBeGreaterThan(timeIdx);
});
test('additionalSections appears after Chart Options', () => {
const TIME_COMP = {
label: 'Time Comparison',
controlSetRows: [],
};
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {
metric: Metric,
showLegend: Checkbox.with({ label: 'Show', default: true }),
},
additionalSections: [TIME_COMP],
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
const timeCompIdx = sections.findIndex(s => s?.label === 'Time Comparison');
expect(chartOptsIdx).toBeGreaterThanOrEqual(0);
expect(timeCompIdx).toBeGreaterThan(chartOptsIdx);
});
test('middleSections appears between Query and Chart Options', () => {
const MIDDLE = {
label: 'Middle',
controlSetRows: [],
};
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {
metric: Metric,
showLegend: Checkbox.with({ label: 'Show', default: true }),
},
middleSections: [MIDDLE],
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
const queryIdx = sections.findIndex(s => s?.label === 'Query');
const middleIdx = sections.findIndex(s => s?.label === 'Middle');
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
expect(queryIdx).toBeLessThan(middleIdx);
expect(middleIdx).toBeLessThan(chartOptsIdx);
});
test('chartOptionsTabOverride sets tabOverride on the generated section', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {
showLegend: Checkbox.with({ label: 'Show', default: true }),
},
chartOptionsTabOverride: 'data',
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
tabOverride?: string;
}>;
const chartOpts = sections.find(s => s?.label === 'Chart Options');
expect(chartOpts?.tabOverride).toBe('data');
});
});
describe('defineChart - overrides + formDataOverrides + onInit', () => {
test('additionalControlOverrides land on controlPanel.controlOverrides', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: { metric: Metric },
additionalControlOverrides: {
size: { label: 'Custom Size Label' },
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
expect(
(controlPanel.controlOverrides as Record<string, unknown>)?.size,
).toEqual({ label: 'Custom Size Label' });
});
test('controlOverrides + additionalControlOverrides merge', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: { metric: Metric },
controlOverrides: {
a: { label: 'A' },
},
additionalControlOverrides: {
b: { label: 'B' },
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const merged = controlPanel.controlOverrides as Record<string, unknown>;
expect(merged.a).toEqual({ label: 'A' });
expect(merged.b).toEqual({ label: 'B' });
});
test('formDataOverrides is preserved on controlPanel', () => {
const fdo = (formData: Record<string, unknown>) => ({
...formData,
custom: 'extra',
});
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {},
formDataOverrides: fdo,
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
expect(controlPanel.formDataOverrides).toBe(fdo);
});
test('onInit is preserved on controlPanel', () => {
const onInit = (state: unknown) => state;
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {},
onInit,
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
expect(controlPanel.onInit).toBe(onInit);
});
test('_glyphArgs is attached to the controlPanel for native rendering', () => {
const args = {
metric: Metric,
showLegend: Checkbox.with({ label: 'Show', default: true }),
};
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: args,
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
expect(controlPanel._glyphArgs).toEqual(args);
});
});
describe('defineChart - custom buildQuery / transform', () => {
test('custom buildQuery is invoked via the plugin loader', async () => {
const customBuildQuery = jest.fn(() => ({ queries: [{ marker: 1 }] }));
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {},
buildQuery: customBuildQuery as unknown as never,
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const p = new Plugin();
// ChartPlugin stores it as a sanitized loader
const loader = (p as unknown as { loadBuildQuery?: () => Promise<Function> })
.loadBuildQuery;
expect(loader).toBeDefined();
const fn = await (loader as () => Promise<Function>)();
fn({ viz_type: 'test', datasource: '1__table' });
expect(customBuildQuery).toHaveBeenCalledTimes(1);
});
test('transform receives chartProps and argValues', async () => {
const captured: unknown[] = [];
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: { metric: Metric },
transform: (chartProps, argValues) => {
captured.push({ chartProps, argValues });
return { transformed: true };
},
render: () => null as unknown as React.ReactElement,
});
const p = new Plugin();
const loader = (
p as unknown as { loadTransformProps: () => Promise<Function> }
).loadTransformProps;
const transformProps = await loader();
transformProps({
width: 100,
height: 100,
formData: { metric: 'count' },
queriesData: [{ data: [] }],
});
expect(captured).toHaveLength(1);
expect((captured[0] as { chartProps: unknown }).chartProps).toBeDefined();
expect((captured[0] as { argValues: unknown }).argValues).toBeDefined();
});
});
describe('defineChart - Text-only argument behavior', () => {
test('a Text-only chart still wires up a working plugin', () => {
const Plugin = defineChart({
metadata: { name: 'TextOnly', thumbnail: MIN_THUMBNAIL },
arguments: {
title: Text.with({ label: 'Title', default: 'Hi' }),
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel, metadata } = instantiate(Plugin);
expect(metadata.name).toBe('TextOnly');
// Customize args present → Chart Options shows up
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
}>;
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
});
});
describe('defineChart - visibleWhen with object-form ArgDef', () => {
test('attaches a visibility derivation to the underlying control', () => {
// Build a plugin where one arg is visibleWhen another is true.
const Plugin = defineChart({
metadata: { name: 'V', thumbnail: MIN_THUMBNAIL },
arguments: {
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
legendPosition: {
arg: Select.with({
label: 'Position',
default: 'right',
options: [{ label: 'R', value: 'right' }],
}),
visibleWhen: { showLegend: true },
},
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
expect(controlPanel._glyphArgs).toBeDefined();
const glyphArgs = controlPanel._glyphArgs as Record<string, unknown>;
// The visibleWhen is preserved on the glyph args
const lp = glyphArgs.legendPosition as { visibleWhen?: unknown };
expect(lp.visibleWhen).toEqual({ showLegend: true });
});
});

View File

@@ -0,0 +1,401 @@
/**
* 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 type { ChartProps } from '@superset-ui/core';
import {
Checkbox,
Color,
createGlyphPlugin,
Dimension,
generateControlPanel,
generateTransformProps,
getControlConfig,
Int,
Metric,
Select,
Temporal,
Text,
} from '@superset-ui/glyph-core';
import type { GlyphArguments } from '@superset-ui/glyph-core/generators';
describe('getControlConfig - per argument type', () => {
test('Select → SelectControl with options and clearable=false', () => {
const S = Select.with({
label: 'Choose',
default: 'a',
options: [{ label: 'A', value: 'a' }],
});
const cfg = getControlConfig(S, 'myParam');
expect(cfg.type).toBe('SelectControl');
expect(cfg.label).toBe('Choose');
expect(cfg.default).toBe('a');
expect(cfg.options).toEqual([{ label: 'A', value: 'a' }]);
expect(cfg.clearable).toBe(false);
expect(cfg.renderTrigger).toBe(true);
});
test('Checkbox → CheckboxControl with default', () => {
const C = Checkbox.with({ label: 'Show', default: true });
const cfg = getControlConfig(C, 'show');
expect(cfg.type).toBe('CheckboxControl');
expect(cfg.label).toBe('Show');
expect(cfg.default).toBe(true);
expect(cfg.renderTrigger).toBe(true);
});
test('Int → SliderControl with min/max/step', () => {
const I = Int.with({ label: 'Limit', default: 50, min: 0, max: 1000, step: 5 });
const cfg = getControlConfig(I, 'limit');
expect(cfg.type).toBe('SliderControl');
expect(cfg.label).toBe('Limit');
expect(cfg.default).toBe(50);
expect(cfg.min).toBe(0);
expect(cfg.max).toBe(1000);
expect(cfg.step).toBe(5);
});
test('Color (hex) → ColorPickerControl with RGBA default', () => {
const C = Color.with({ label: 'Fill', default: '#ff0000' });
const cfg = getControlConfig(C, 'fill');
expect(cfg.type).toBe('ColorPickerControl');
expect(cfg.label).toBe('Fill');
expect(cfg.default).toEqual({ r: 255, g: 0, b: 0, a: 1 });
});
test('Text → TextControl with placeholder', () => {
const T = Text.with({
label: 'Title',
default: 'Untitled',
placeholder: 'Enter…',
});
const cfg = getControlConfig(T, 'title');
expect(cfg.type).toBe('TextControl');
expect(cfg.label).toBe('Title');
expect(cfg.default).toBe('Untitled');
expect(cfg.placeholder).toBe('Enter…');
});
test('falls back to paramName when label is unset', () => {
// Use the raw Text class (label: null on Argument)
class Bare extends Text {
static override label = null;
}
const cfg = getControlConfig(Bare, 'fallback_name');
expect(cfg.label).toBe('fallback_name');
});
});
describe('generateControlPanel', () => {
test('produces Query and Chart Options sections', () => {
const args: GlyphArguments = new Map([
['metric', Metric],
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
]);
const cp = generateControlPanel(args);
const labels = cp.controlPanelSections.map(s =>
s && 'label' in s ? s.label : undefined,
);
expect(labels).toContain('Query');
expect(labels).toContain('Chart Options');
});
test('Metric args produce a [metric] row in Query', () => {
const args: GlyphArguments = new Map([['m', Metric]]);
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
expect(querySection).toBeDefined();
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['metric']);
});
test('Dimension args produce a [groupby] row in Query', () => {
const args: GlyphArguments = new Map([['d', Dimension]]);
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['groupby']);
});
test('Temporal args produce [x_axis] and [time_grain_sqla] rows in Query', () => {
const args: GlyphArguments = new Map([['t', Temporal]]);
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['x_axis']);
expect(rows).toContainEqual(['time_grain_sqla']);
});
test('adhoc_filters is always added to Query', () => {
const args: GlyphArguments = new Map();
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['adhoc_filters']);
});
test('Non-data args become controls in Chart Options', () => {
const args: GlyphArguments = new Map([
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
]);
const cp = generateControlPanel(args);
const chartOpts = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Chart Options',
);
const rows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toHaveLength(1);
expect(rows[0]).toEqual([
{
name: 'showLegend',
config: expect.objectContaining({ type: 'CheckboxControl' }),
},
]);
});
test('GlyphArgConfig with visibility wires onto control', () => {
const visibility = jest.fn(() => true);
const args: GlyphArguments = new Map([
[
'subtitleSize',
{
arg: Select.with({
label: 'Size',
default: 'm',
options: [{ label: 'M', value: 'm' }],
}),
visibility,
resetOnHide: true,
},
],
]);
const cp = generateControlPanel(args);
const chartOpts = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Chart Options',
);
const row = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows[0];
const item = (row as Array<{ config: Record<string, unknown> }>)[0];
expect(item.config.visibility).toBe(visibility);
expect(item.config.resetOnHide).toBe(true);
});
test('controlOverrides and formDataOverrides options pass through', () => {
const fdo = (fd: Record<string, unknown>) => ({ ...fd, x: 1 });
const cp = generateControlPanel(new Map(), {
controlOverrides: { metric: { label: 'M' } },
formDataOverrides: fdo,
});
expect(cp.controlOverrides).toEqual({ metric: { label: 'M' } });
expect(cp.formDataOverrides).toBe(fdo);
});
test('extra queryControls and chartOptionsControls are appended', () => {
const args: GlyphArguments = new Map();
const cp = generateControlPanel(args, {
queryControls: [['custom_filter']] as never,
chartOptionsControls: [['custom_chart_opt']] as never,
});
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const queryRows = (querySection as { controlSetRows: unknown[][] })
.controlSetRows;
expect(queryRows).toContainEqual(['custom_filter']);
const chartOpts = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Chart Options',
);
const optRows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
expect(optRows).toContainEqual(['custom_chart_opt']);
});
});
describe('generateTransformProps', () => {
function makeChartProps(formData: Record<string, unknown>): ChartProps {
return {
width: 400,
height: 300,
queriesData: [{ data: [] }],
formData,
} as unknown as ChartProps;
}
test('returns width/height/queriesData passthrough', () => {
const transform = generateTransformProps(new Map());
const out = transform(makeChartProps({}));
expect(out).toMatchObject({ width: 400, height: 300 });
});
test('extracts Select value from formData', () => {
const args: GlyphArguments = new Map([
[
'size',
Select.with({
label: 'Size',
default: 'm',
options: [
{ label: 'S', value: 's' },
{ label: 'M', value: 'm' },
],
}),
],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({ size: 's' }));
expect((out as { size: unknown }).size).toBe('s');
});
test('Select falls back to default when value missing', () => {
const args: GlyphArguments = new Map([
['size', Select.with({ label: 'Size', default: 'm', options: [] })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { size: unknown }).size).toBe('m');
});
test('Checkbox uses formData value when present', () => {
const args: GlyphArguments = new Map([
['flag', Checkbox.with({ label: 'F', default: false })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({ flag: true }));
expect((out as { flag: unknown }).flag).toBe(true);
});
test('Checkbox falls back to default when value missing', () => {
const args: GlyphArguments = new Map([
['flag', Checkbox.with({ label: 'F', default: true })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { flag: unknown }).flag).toBe(true);
});
test('Color: RGBA in formData → hex string', () => {
const args: GlyphArguments = new Map([
['fill', Color.with({ label: 'Fill', default: '#000000' })],
]);
const transform = generateTransformProps(args);
const out = transform(
makeChartProps({ fill: { r: 255, g: 0, b: 0, a: 1 } }),
);
expect((out as { fill: unknown }).fill).toBe('#ff0000');
});
test('Color: string value in formData passes through', () => {
const args: GlyphArguments = new Map([
['fill', Color.with({ label: 'Fill', default: '#000000' })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({ fill: '#abcdef' }));
expect((out as { fill: unknown }).fill).toBe('#abcdef');
});
test('Color: falls back to class default when value missing', () => {
const args: GlyphArguments = new Map([
['fill', Color.with({ label: 'Fill', default: '#112233' })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
// default is hex string; transform converts the RGBA-formatted default back to hex
expect((out as { fill: unknown }).fill).toBe('#112233');
});
test('Int uses default when value missing', () => {
const args: GlyphArguments = new Map([
['n', Int.with({ label: 'N', default: 7, min: 0, max: 100 })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { n: unknown }).n).toBe(7);
});
test('Text uses default when value missing', () => {
const args: GlyphArguments = new Map([
['s', Text.with({ label: 'S', default: 'hi' })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { s: unknown }).s).toBe('hi');
});
test('Metric/Dimension/Temporal args are NOT extracted (handled elsewhere)', () => {
const args: GlyphArguments = new Map([
['metric', Metric],
['groupby', Dimension],
['t', Temporal],
]);
const transform = generateTransformProps(args);
const out = transform(
makeChartProps({ metric: 'count', groupby: 'a', t: 'date' }),
);
expect((out as Record<string, unknown>).metric).toBeUndefined();
expect((out as Record<string, unknown>).groupby).toBeUndefined();
expect((out as Record<string, unknown>).t).toBeUndefined();
});
test('passthrough option copies named ChartProps fields onto the result', () => {
const args: GlyphArguments = new Map();
const transform = generateTransformProps(args, {
passthrough: ['formData'],
});
const out = transform(makeChartProps({ marker: 1 })) as Record<
string,
unknown
>;
expect(out.formData).toEqual({ marker: 1 });
});
test('custom transform option receives extracted values and chartProps', () => {
const args: GlyphArguments = new Map([
['flag', Checkbox.with({ label: 'F', default: false })],
]);
const transform = generateTransformProps(args, {
transform: (values, chartProps) => ({
values,
gotChartProps: !!chartProps,
}),
});
const out = transform(makeChartProps({ flag: true })) as {
values: { flag: boolean };
gotChartProps: boolean;
};
expect(out.values.flag).toBe(true);
expect(out.gotChartProps).toBe(true);
});
});
describe('createGlyphPlugin', () => {
test('returns both controlPanel and transformProps', () => {
const args: GlyphArguments = new Map([
['metric', Metric],
['show', Checkbox.with({ label: 'S', default: true })],
]);
const plugin = createGlyphPlugin(args);
expect(plugin.controlPanel).toBeDefined();
expect(plugin.controlPanel.controlPanelSections.length).toBe(2);
expect(typeof plugin.transformProps).toBe('function');
});
});

View File

@@ -0,0 +1,203 @@
/**
* 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 {
Checkbox,
CircleShape,
DataZoom,
ForceTimestampFormatting,
HeaderFontSize,
isCheckboxArg,
isSelectArg,
isTextArg,
LabelPosition,
LabelType,
LabelThreshold,
LegendOrientation,
LegendSort,
LegendType,
MetricNameFontSize,
Select,
ShowLabels,
ShowLegend,
ShowMetricName,
ShowTotal,
ShowValue,
SimpleLabelType,
SortByMetric,
Subtitle,
SubheaderFontSize,
Text,
ValueLabelType,
} from '@superset-ui/glyph-core';
import {
FONT_SIZE_OPTIONS_LARGE,
FONT_SIZE_OPTIONS_SMALL,
LABEL_TYPE_OPTIONS,
LEGEND_ORIENTATION_OPTIONS,
LEGEND_SORT_OPTIONS,
LEGEND_TYPE_OPTIONS,
SORT_OPTIONS,
} from '@superset-ui/glyph-core/presets';
describe('Font-size presets', () => {
test('HeaderFontSize is a Select with large font options', () => {
expect(isSelectArg(HeaderFontSize)).toBe(true);
expect((HeaderFontSize as unknown as typeof Select).options).toBe(
FONT_SIZE_OPTIONS_LARGE,
);
});
test('SubheaderFontSize is a Select with small font options', () => {
expect(isSelectArg(SubheaderFontSize)).toBe(true);
expect((SubheaderFontSize as unknown as typeof Select).options).toBe(
FONT_SIZE_OPTIONS_SMALL,
);
});
test('FONT_SIZE_OPTIONS_LARGE and _SMALL are non-empty option arrays', () => {
expect(FONT_SIZE_OPTIONS_LARGE.length).toBeGreaterThan(0);
expect(FONT_SIZE_OPTIONS_SMALL.length).toBeGreaterThan(0);
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('label');
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('value');
});
test('MetricNameFontSize is a Select preset', () => {
expect(isSelectArg(MetricNameFontSize)).toBe(true);
});
});
describe('Text presets', () => {
test('Subtitle is a Text preset', () => {
expect(isTextArg(Subtitle)).toBe(true);
expect(Subtitle.prototype).toBeInstanceOf(Text);
});
test('LabelThreshold is a Text preset', () => {
expect(isTextArg(LabelThreshold)).toBe(true);
});
});
describe('Checkbox presets', () => {
test.each([
['ShowLegend', ShowLegend],
['ShowLabels', ShowLabels],
['ShowValue', ShowValue],
['ShowMetricName', ShowMetricName],
['ShowTotal', ShowTotal],
['SortByMetric', SortByMetric],
['CircleShape', CircleShape],
['DataZoom', DataZoom],
['ForceTimestampFormatting', ForceTimestampFormatting],
])('%s is a Checkbox preset', (_name, preset) => {
expect(isCheckboxArg(preset)).toBe(true);
expect(preset.prototype).toBeInstanceOf(Checkbox);
});
test('Checkbox presets have a label and a description', () => {
[ShowLegend, ShowLabels, ShowValue, ShowMetricName, ShowTotal].forEach(
preset => {
expect(preset.label).toBeTruthy();
expect(preset.description).toBeTruthy();
},
);
});
});
describe('Legend Select presets', () => {
test('LegendType uses LEGEND_TYPE_OPTIONS', () => {
expect(isSelectArg(LegendType)).toBe(true);
expect((LegendType as unknown as typeof Select).options).toBe(
LEGEND_TYPE_OPTIONS,
);
});
test('LegendOrientation uses LEGEND_ORIENTATION_OPTIONS', () => {
expect(isSelectArg(LegendOrientation)).toBe(true);
expect((LegendOrientation as unknown as typeof Select).options).toBe(
LEGEND_ORIENTATION_OPTIONS,
);
});
test('LegendSort uses LEGEND_SORT_OPTIONS', () => {
expect(isSelectArg(LegendSort)).toBe(true);
expect((LegendSort as unknown as typeof Select).options).toBe(
LEGEND_SORT_OPTIONS,
);
});
test('legend option sets are non-empty', () => {
expect(LEGEND_TYPE_OPTIONS.length).toBeGreaterThan(0);
expect(LEGEND_ORIENTATION_OPTIONS.length).toBeGreaterThan(0);
expect(LEGEND_SORT_OPTIONS.length).toBeGreaterThan(0);
});
});
describe('Label / value-label Select presets', () => {
test('LabelType is a Select with LABEL_TYPE_OPTIONS', () => {
expect(isSelectArg(LabelType)).toBe(true);
expect((LabelType as unknown as typeof Select).options).toBe(
LABEL_TYPE_OPTIONS,
);
});
test('SimpleLabelType is a Select preset', () => {
expect(isSelectArg(SimpleLabelType)).toBe(true);
});
test('ValueLabelType is a Select preset', () => {
expect(isSelectArg(ValueLabelType)).toBe(true);
});
test('LabelPosition is a Select preset', () => {
expect(isSelectArg(LabelPosition)).toBe(true);
});
});
describe('Sort options', () => {
test('SORT_OPTIONS is non-empty', () => {
expect(SORT_OPTIONS.length).toBeGreaterThan(0);
expect(SORT_OPTIONS[0]).toHaveProperty('label');
expect(SORT_OPTIONS[0]).toHaveProperty('value');
});
});
describe('Preset extensibility', () => {
test('ShowLegend.with() overrides label while keeping the Checkbox shape', () => {
const Custom = ShowLegend.with({
label: 'Display legend',
default: false,
});
expect(isCheckboxArg(Custom)).toBe(true);
expect(Custom.label).toBe('Display legend');
expect(Custom.default).toBe(false);
});
test('HeaderFontSize.with() overrides label, default keeps options', () => {
const Custom = HeaderFontSize.with({
label: 'Title size',
default: 0.4,
});
expect(isSelectArg(Custom)).toBe(true);
expect(Custom.label).toBe('Title size');
expect(Custom.default).toBe(0.4);
expect((Custom as unknown as typeof Select).options).toBe(
FONT_SIZE_OPTIONS_LARGE,
);
});
});

View File

@@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
"baseUrl": "../..",
// Directory Overrides: Parent config paths are relative to frontend root,
// but packages need paths relative to their own directory
"outDir": "lib",
"rootDir": "src",
"declarationDir": "lib"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
"references": [
{ "path": "../superset-core" },
{ "path": "../superset-ui-core" },
{ "path": "../superset-ui-chart-controls" }
]
}

View File

@@ -32,6 +32,7 @@
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
},

View File

@@ -1,205 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { legacyValidateInteger } from '@superset-ui/core';
import {
ControlPanelConfig,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
getStandardizedControls,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Time'),
expanded: true,
description: t('Time related form attributes'),
controlSetRows: [['granularity_sqla'], ['time_range']],
},
{
label: t('Query'),
expanded: true,
controlSetRows: [
[
{
name: 'domain_granularity',
config: {
type: 'SelectControl',
label: t('Domain'),
default: 'month',
choices: [
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
['year', t('year')],
],
description: t('The time unit used for the grouping of blocks'),
},
},
{
name: 'subdomain_granularity',
config: {
type: 'SelectControl',
label: t('Subdomain'),
default: 'day',
choices: [
['min', t('min')],
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
],
description: t(
'The time unit for each block. Should be a smaller unit than ' +
'domain_granularity. Should be larger or equal to Time Grain',
),
},
},
],
['metrics'],
['adhoc_filters'],
],
},
{
label: t('Chart Options'),
expanded: true,
tabOverride: 'customize',
controlSetRows: [
['linear_color_scheme'],
[
{
name: 'cell_size',
config: {
type: 'TextControl',
isInt: true,
default: 10,
validators: [legacyValidateInteger],
renderTrigger: true,
label: t('Cell Size'),
description: t('The size of the square cell, in pixels'),
},
},
{
name: 'cell_padding',
config: {
type: 'TextControl',
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
default: 2,
label: t('Cell Padding'),
description: t('The distance between cells, in pixels'),
},
},
],
[
{
name: 'cell_radius',
config: {
type: 'TextControl',
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
default: 0,
label: t('Cell Radius'),
description: t('The pixel radius'),
},
},
{
name: 'steps',
config: {
type: 'TextControl',
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
default: 10,
label: t('Color Steps'),
description: t('The number color "steps"'),
},
},
],
[
'y_axis_format',
{
name: 'x_axis_time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Time Format'),
renderTrigger: true,
default: 'smart_date',
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
},
],
[
{
name: 'show_legend',
config: {
type: 'CheckboxControl',
label: t('Legend'),
renderTrigger: true,
default: true,
description: t('Whether to display the legend (toggles)'),
},
},
{
name: 'show_values',
config: {
type: 'CheckboxControl',
label: t('Show Values'),
renderTrigger: true,
default: false,
description: t(
'Whether to display the numerical values within the cells',
),
},
},
],
[
{
name: 'show_metric_name',
config: {
type: 'CheckboxControl',
label: t('Show Metric Names'),
renderTrigger: true,
default: true,
description: t('Whether to display the metric name as a title'),
},
},
null,
],
],
},
],
controlOverrides: {
y_axis_format: {
label: t('Number Format'),
},
},
formDataOverrides: formData => ({
...formData,
metrics: getStandardizedControls().popAllMetrics(),
}),
};
export default config;

View File

@@ -1,58 +0,0 @@
/**
* 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 { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import { t } from '@apache-superset/core/translation';
import transformProps from './transformProps';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import controlPanel from './controlPanel';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
const metadata = new ChartMetadata({
category: t('Correlation'),
credits: ['https://github.com/wa0x6e/cal-heatmap'],
description: t(
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
name: t('Calendar Heatmap'),
tags: [
t('Business'),
t('Comparison'),
t('Intensity'),
t('Pattern'),
t('Report'),
t('Trend'),
],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class CalendarChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactCalendar'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,241 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { getNumberFormatter } from '@superset-ui/core';
import {
D3_FORMAT_DOCS,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import {
defineChart,
Int,
Checkbox,
TimeFormat,
} from '@superset-ui/glyph-core';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import { getFormattedUTCTime } from './utils';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactCalendar = require('./ReactCalendar').default;
type CalendarExtra = {
timeFormatter: (ts: number | string) => string;
valueFormatter: (val: unknown) => string;
verboseMap: Record<string, string>;
domainGranularity: string;
subdomainGranularity: string;
linearColorScheme: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, CalendarExtra>({
metadata: {
name: t('Calendar Heatmap'),
description: t(
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
),
category: t('Correlation'),
credits: ['https://github.com/wa0x6e/cal-heatmap'],
tags: [
t('Business'),
t('Comparison'),
t('Intensity'),
t('Pattern'),
t('Report'),
t('Trend'),
],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
},
arguments: {
cell_size: Int.with({
label: 'Cell Size',
description: 'The size of the square cell, in pixels',
default: 10,
min: 1,
max: 100,
}),
cell_padding: Int.with({
label: 'Cell Padding',
description: 'The distance between cells, in pixels',
default: 2,
min: 0,
max: 20,
}),
cell_radius: Int.with({
label: 'Cell Radius',
description: 'The pixel radius',
default: 0,
min: 0,
max: 50,
}),
steps: Int.with({
label: 'Color Steps',
description: 'The number color "steps"',
default: 10,
min: 1,
max: 50,
}),
x_axis_time_format: TimeFormat.with({
label: 'Time Format',
description: D3_FORMAT_DOCS,
default: 'smart_date',
}),
show_legend: Checkbox.with({
label: 'Legend',
description: 'Whether to display the legend (toggles)',
default: true,
}),
show_values: Checkbox.with({
label: 'Show Values',
description: 'Whether to display the numerical values within the cells',
default: false,
}),
show_metric_name: Checkbox.with({
label: 'Show Metric Names',
description: 'Whether to display the metric name as a title',
default: true,
}),
},
prependSections: [
{
label: t('Time'),
expanded: true,
description: t('Time related form attributes'),
controlSetRows: [['granularity_sqla'], ['time_range']],
},
],
additionalControls: {
queryBefore: [
[
{
name: 'domain_granularity',
config: {
type: 'SelectControl',
label: t('Domain'),
default: 'month',
choices: [
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
['year', t('year')],
],
description: t('The time unit used for the grouping of blocks'),
},
},
{
name: 'subdomain_granularity',
config: {
type: 'SelectControl',
label: t('Subdomain'),
default: 'day',
choices: [
['min', t('min')],
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
],
description: t(
'The time unit for each block. Should be a smaller unit than ' +
'domain_granularity. Should be larger or equal to Time Grain',
),
},
},
],
['metrics'],
],
chartOptions: [['linear_color_scheme'], ['y_axis_format']],
},
chartOptionsTabOverride: 'customize',
additionalControlOverrides: {
y_axis_format: {
label: t('Number Format'),
},
},
formDataOverrides: formData => ({
...formData,
metrics: getStandardizedControls().popAllMetrics(),
}),
transform: (chartProps, { x_axis_time_format }) => {
const { formData, datasource } = chartProps;
const {
domainGranularity,
subdomainGranularity,
linearColorScheme,
yAxisFormat,
} = formData as Record<string, string>;
const verboseMap =
(datasource as { verboseMap?: Record<string, string> })?.verboseMap ?? {};
const timeFormatter = (ts: number | string) =>
getFormattedUTCTime(ts, x_axis_time_format as string);
const valueFormatter = getNumberFormatter(yAxisFormat);
return {
timeFormatter,
valueFormatter: valueFormatter as (val: unknown) => string,
verboseMap,
domainGranularity: domainGranularity ?? 'month',
subdomainGranularity: subdomainGranularity ?? 'day',
linearColorScheme: linearColorScheme ?? '',
};
},
render: ({
height,
data,
cell_size: cellSize,
cell_padding: cellPadding,
cell_radius: cellRadius,
steps,
show_legend: showLegend,
show_values: showValues,
show_metric_name: showMetricName,
timeFormatter,
valueFormatter,
verboseMap,
domainGranularity,
subdomainGranularity,
linearColorScheme,
}) => (
<ReactCalendar
height={height}
data={data}
cellSize={cellSize}
cellPadding={cellPadding}
cellRadius={cellRadius}
steps={steps}
showLegend={showLegend}
showValues={showValues}
showMetricName={showMetricName}
timeFormatter={timeFormatter}
valueFormatter={valueFormatter}
verboseMap={verboseMap}
domainGranularity={domainGranularity}
subdomainGranularity={subdomainGranularity}
linearColorScheme={linearColorScheme}
/>
),
});

View File

@@ -1,62 +0,0 @@
/**
* 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 { ChartProps, getNumberFormatter } from '@superset-ui/core';
import { getFormattedUTCTime } from './utils';
export default function transformProps(chartProps: ChartProps) {
const { height, formData, queriesData, datasource } = chartProps;
const {
cellPadding,
cellRadius,
cellSize,
domainGranularity,
linearColorScheme,
showLegend,
showMetricName,
showValues,
steps,
subdomainGranularity,
xAxisTimeFormat,
yAxisFormat,
} = formData;
const { verboseMap } = datasource;
const timeFormatter = (ts: number | string) =>
getFormattedUTCTime(ts, xAxisTimeFormat);
const valueFormatter = getNumberFormatter(yAxisFormat);
return {
height,
data: queriesData[0].data,
cellPadding,
cellRadius,
cellSize,
domainGranularity,
linearColorScheme,
showLegend,
showMetricName,
showValues,
steps,
subdomainGranularity,
timeFormatter,
valueFormatter,
verboseMap,
};
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}

View File

@@ -36,6 +36,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"@superset-ui/glyph-core": "*"
}
}

View File

@@ -1,76 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
import {
ControlPanelConfig,
getStandardizedControls,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['groupby'],
['columns'],
['metric'],
['adhoc_filters'],
['row_limit'],
['sort_by_metric'],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [['y_axis_format', null], ['color_scheme']],
},
],
controlOverrides: {
y_axis_format: {
label: t('Number format'),
description: t('Choose a number format'),
},
groupby: {
label: t('Source'),
multi: false,
validators: [validateNonEmpty],
description: t('Choose a source'),
},
columns: {
label: t('Target'),
multi: false,
validators: [validateNonEmpty],
description: t('Choose a target'),
},
},
formDataOverrides: formData => {
const groupby = getStandardizedControls()
.popAllColumns()
.filter(col => !ensureIsArray(formData.columns).includes(col));
return {
...formData,
groupby,
metric: getStandardizedControls().shiftMetric(),
};
},
};
export default config;

View File

@@ -1,57 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from './transformProps';
import example from './images/chord.jpg';
import exampleDark from './images/chord-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Flow'),
credits: ['https://github.com/d3/d3-chord'],
description: t(
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
),
exampleGallery: [
{
url: example,
urlDark: exampleDark,
caption: t('Relationships between community channels'),
},
],
name: t('Chord Diagram'),
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class ChordChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactChord'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,120 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
import { getStandardizedControls } from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import example from './images/chord.jpg';
import exampleDark from './images/chord-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactChord = require('./ReactChord').default;
type ChordExtra = {
colorScheme: string;
numberFormat: string;
sliceId: number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, ChordExtra>({
metadata: {
name: t('Chord Diagram'),
description: t(
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
),
category: t('Flow'),
credits: ['https://github.com/d3/d3-chord'],
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
thumbnail,
thumbnailDark,
exampleGallery: [
{
url: example,
urlDark: exampleDark,
caption: t('Relationships between community channels'),
},
],
useLegacyApi: true,
},
arguments: {},
additionalControls: {
queryBefore: [['groupby'], ['columns'], ['metric']],
query: [['row_limit'], ['sort_by_metric']],
chartOptions: [['y_axis_format', null], ['color_scheme']],
},
additionalControlOverrides: {
y_axis_format: {
label: t('Number format'),
description: t('Choose a number format'),
},
groupby: {
label: t('Source'),
multi: false,
validators: [validateNonEmpty],
description: t('Choose a source'),
},
columns: {
label: t('Target'),
multi: false,
validators: [validateNonEmpty],
description: t('Choose a target'),
},
},
formDataOverrides: formData => {
const groupby = getStandardizedControls()
.popAllColumns()
.filter(
(col: string) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!ensureIsArray((formData as any).columns).includes(col),
);
return {
...formData,
groupby,
metric: getStandardizedControls().shiftMetric(),
};
},
transform: chartProps => {
const { formData } = chartProps;
const { yAxisFormat, colorScheme, sliceId } = formData as Record<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>;
return {
colorScheme: colorScheme ?? '',
numberFormat: yAxisFormat ?? '',
sliceId: sliceId ?? 0,
};
},
render: ({ width, height, data, colorScheme, numberFormat, sliceId }) => (
<ReactChord
width={width}
height={height}
data={data}
colorScheme={colorScheme}
numberFormat={numberFormat}
sliceId={sliceId}
/>
),
});

View File

@@ -1,33 +0,0 @@
/**
* 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 { ChartProps } from '@superset-ui/core';
export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, queriesData } = chartProps;
const { yAxisFormat, colorScheme, sliceId } = formData;
return {
colorScheme,
data: queriesData[0].data,
height,
numberFormat: yAxisFormat,
width,
sliceId,
};
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}

View File

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

View File

@@ -1,99 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import {
ControlPanelConfig,
D3_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { countryOptions } from './countries';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
[
{
name: 'select_country',
config: {
type: 'SelectControl',
label: t('Country'),
default: null,
choices: countryOptions,
description: t('Which country to plot the map for?'),
validators: [validateNonEmpty],
},
},
],
['entity'],
['metric'],
['adhoc_filters'],
],
},
{
label: t('Chart Options'),
expanded: true,
tabOverride: 'customize',
controlSetRows: [
[
{
name: 'number_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Number format'),
renderTrigger: true,
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
},
],
['currency_format'],
['linear_color_scheme'],
],
},
],
controlOverrides: {
entity: {
label: t('ISO 3166-2 Codes'),
description: t(
'Column containing ISO 3166-2 codes of region/province/department in your table.',
),
},
metric: {
label: t('Metric'),
description: t('Metric to display bottom title'),
},
linear_color_scheme: {
renderTrigger: false,
},
},
formDataOverrides: formData => ({
...formData,
entity: getStandardizedControls().shiftColumn(),
metric: getStandardizedControls().shiftMetric(),
}),
};
export default config;

View File

@@ -1,65 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from './transformProps';
import exampleUsa from './images/exampleUsa.jpg';
import exampleUsaDark from './images/exampleUsa-dark.jpg';
import exampleGermany from './images/exampleGermany.jpg';
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Map'),
credits: ['https://bl.ocks.org/john-guerra'],
description: t(
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
),
exampleGallery: [
{ url: exampleUsa, urlDark: exampleUsaDark },
{ url: exampleGermany, urlDark: exampleGermanyDark },
],
name: t('Country Map'),
tags: [
t('2D'),
t('Comparison'),
t('Geo'),
t('Range'),
t('Report'),
t('Stacked'),
],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class CountryMapChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactCountryMap'),
metadata,
transformProps,
controlPanel,
});
}
}
export { default as countries } from './countries';

View File

@@ -0,0 +1,170 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import {
D3_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import exampleUsa from './images/exampleUsa.jpg';
import exampleUsaDark from './images/exampleUsa-dark.jpg';
import exampleGermany from './images/exampleGermany.jpg';
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import { countryOptions } from './countries';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactCountryMap = require('./ReactCountryMap').default;
export { default as countries } from './countries';
type CountryMapExtra = {
country: string | null;
linearColorScheme: string;
numberFormat: string;
colorScheme: string;
sliceId: number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, CountryMapExtra>({
metadata: {
name: t('Country Map'),
description: t(
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
),
category: t('Map'),
credits: ['https://bl.ocks.org/john-guerra'],
tags: [
t('2D'),
t('Comparison'),
t('Geo'),
t('Range'),
t('Report'),
t('Stacked'),
],
thumbnail,
thumbnailDark,
exampleGallery: [
{ url: exampleUsa, urlDark: exampleUsaDark },
{ url: exampleGermany, urlDark: exampleGermanyDark },
],
useLegacyApi: true,
},
arguments: {},
additionalControls: {
queryBefore: [
[
{
name: 'select_country',
config: {
type: 'SelectControl',
label: t('Country'),
default: null,
choices: countryOptions,
description: t('Which country to plot the map for?'),
validators: [validateNonEmpty],
},
},
],
['entity'],
['metric'],
],
chartOptions: [
[
{
name: 'number_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Number format'),
renderTrigger: true,
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
},
],
['linear_color_scheme'],
],
},
chartOptionsTabOverride: 'customize',
additionalControlOverrides: {
entity: {
label: t('ISO 3166-2 Codes'),
description: t(
'Column containing ISO 3166-2 codes of region/province/department in your table.',
),
},
metric: {
label: t('Metric'),
description: t('Metric to display bottom title'),
},
linear_color_scheme: {
renderTrigger: false,
},
},
formDataOverrides: formData => ({
...formData,
entity: getStandardizedControls().shiftColumn(),
metric: getStandardizedControls().shiftMetric(),
}),
transform: chartProps => {
const { formData } = chartProps;
const {
linearColorScheme,
numberFormat,
selectCountry,
colorScheme,
sliceId,
} = formData as Record<string, unknown>;
return {
country: selectCountry ? String(selectCountry).toLowerCase() : null,
linearColorScheme: (linearColorScheme as string) ?? '',
numberFormat: (numberFormat as string) ?? '',
colorScheme: (colorScheme as string) ?? '',
sliceId: (sliceId as number) ?? 0,
};
},
render: ({
width,
height,
data,
country,
linearColorScheme,
numberFormat,
colorScheme,
sliceId,
}) => (
<ReactCountryMap
width={width}
height={height}
data={data}
country={country}
linearColorScheme={linearColorScheme}
numberFormat={numberFormat}
colorScheme={colorScheme}
sliceId={sliceId}
/>
),
});

View File

@@ -1,63 +0,0 @@
/**
* 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 { ChartProps, getValueFormatter } from '@superset-ui/core';
export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, queriesData, datasource } = chartProps;
const {
linearColorScheme,
numberFormat,
currencyFormat,
selectCountry,
colorScheme,
sliceId,
metric,
} = formData;
const {
currencyFormats = {},
columnFormats = {},
currencyCodeColumn,
} = datasource;
const { data, detected_currency: detectedCurrency } = queriesData[0];
const formatter = getValueFormatter(
metric,
currencyFormats,
columnFormats,
numberFormat,
currencyFormat,
undefined, // key - not needed for single-metric charts
data,
currencyCodeColumn,
detectedCurrency,
);
return {
width,
height,
data: queriesData[0].data,
country: selectCountry ? String(selectCountry).toLowerCase() : null,
linearColorScheme,
numberFormat, // left for backward compatibility
colorScheme,
sliceId,
formatter,
};
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}

View File

@@ -30,6 +30,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
},

View File

@@ -1,105 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import {
ControlPanelConfig,
formatSelectOptions,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Time'),
expanded: true,
description: t('Time related form attributes'),
controlSetRows: [['granularity_sqla'], ['time_range']],
},
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['adhoc_filters'],
['groupby'],
['limit', 'timeseries_limit_metric'],
['order_desc'],
[
{
name: 'contribution',
config: {
type: 'CheckboxControl',
label: t('Contribution'),
default: false,
description: t('Compute the contribution to the total'),
},
},
],
['row_limit', null],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[
{
name: 'series_height',
config: {
type: 'SelectControl',
renderTrigger: true,
freeForm: true,
label: t('Series Height'),
default: '25',
choices: formatSelectOptions([
'10',
'25',
'40',
'50',
'75',
'100',
'150',
'200',
]),
description: t('Pixel height of each series'),
},
},
{
name: 'horizon_color_scale',
config: {
type: 'SelectControl',
renderTrigger: true,
label: t('Value Domain'),
choices: [
['series', t('series')],
['overall', t('overall')],
['change', t('change')],
],
default: 'series',
description: t(
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
),
},
},
],
],
},
],
};
export default config;

View File

@@ -1,51 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from './transformProps';
import example from './images/Horizon_Chart.jpg';
import exampleDark from './images/Horizon_Chart-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Distribution'),
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
description: t(
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
name: t('Horizon Chart'),
tags: [t('Legacy')],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class HorizonChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./HorizonChart'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,142 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { formatSelectOptions } from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import example from './images/Horizon_Chart.jpg';
import exampleDark from './images/Horizon_Chart-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const HorizonChart = require('./HorizonChart').default;
type HorizonExtra = {
colorScale: string;
seriesHeight: number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, HorizonExtra>({
metadata: {
name: t('Horizon Chart'),
description: t(
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
),
category: t('Distribution'),
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
tags: [t('Legacy')],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
useLegacyApi: true,
},
arguments: {},
prependSections: [
{
label: t('Time'),
expanded: true,
description: t('Time related form attributes'),
controlSetRows: [['granularity_sqla'], ['time_range']],
},
],
additionalControls: {
queryBefore: [['metrics']],
query: [
['groupby'],
['limit', 'timeseries_limit_metric'],
['order_desc'],
[
{
name: 'contribution',
config: {
type: 'CheckboxControl',
label: t('Contribution'),
default: false,
description: t('Compute the contribution to the total'),
},
},
],
['row_limit', null],
],
chartOptions: [
[
{
name: 'series_height',
config: {
type: 'SelectControl',
renderTrigger: true,
freeForm: true,
label: t('Series Height'),
default: '25',
choices: formatSelectOptions([
'10',
'25',
'40',
'50',
'75',
'100',
'150',
'200',
]),
description: t('Pixel height of each series'),
},
},
{
name: 'horizon_color_scale',
config: {
type: 'SelectControl',
renderTrigger: true,
label: t('Value Domain'),
choices: [
['series', t('series')],
['overall', t('overall')],
['change', t('change')],
],
default: 'series',
description: t(
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
),
},
},
],
],
},
transform: chartProps => {
const { formData } = chartProps;
const { horizonColorScale, seriesHeight } = formData as Record<
string,
string
>;
return {
colorScale: horizonColorScale ?? 'series',
seriesHeight: parseInt(seriesHeight ?? '25', 10),
};
},
render: ({ width, height, data, colorScale, seriesHeight }) => (
<HorizonChart
width={width}
height={height}
data={data}
colorScale={colorScale}
seriesHeight={seriesHeight}
/>
),
});

View File

@@ -1,38 +0,0 @@
/**
* 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 { ChartProps } from '@superset-ui/core';
export default function transformProps(chartProps: ChartProps) {
const { height, width, formData, queriesData } = chartProps;
const {
horizon_color_scale: horizonColorScale,
series_height: seriesHeight,
} = formData;
// Only include colorScale if defined, otherwise let defaultProps apply
return {
...(horizonColorScale !== undefined && {
colorScale: horizonColorScale as string,
}),
data: queriesData[0].data,
height,
seriesHeight: parseInt(String(seriesHeight ?? 20), 10),
width,
};
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
declare module "*.png" {
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
declare module "*.jpg" {
const value: any;
export default value;
}

View File

@@ -30,6 +30,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
},

View File

@@ -1,103 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { ControlPanelConfig } from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['adhoc_filters'],
[
{
name: 'groupby',
override: {
validators: [validateNonEmpty],
},
},
],
['limit', 'timeseries_limit_metric'],
['order_desc'],
[
{
name: 'contribution',
config: {
type: 'CheckboxControl',
label: t('Contribution'),
default: false,
description: t('Compute the contribution to the total'),
},
},
],
['row_limit', null],
],
},
{
label: t('Parameters'),
expanded: false,
controlSetRows: [
[
{
name: 'significance_level',
config: {
type: 'TextControl',
label: t('Significance Level'),
default: 0.05,
description: t(
'Threshold alpha level for determining significance',
),
},
},
],
[
{
name: 'pvalue_precision',
config: {
type: 'TextControl',
label: t('p-value precision'),
default: 6,
description: t(
'Number of decimal places with which to display p-values',
),
},
},
],
[
{
name: 'liftvalue_precision',
config: {
type: 'TextControl',
label: t('Lift percent precision'),
default: 4,
description: t(
'Number of decimal places with which to display lift values',
),
},
},
],
],
},
],
};
export default config;

View File

@@ -1,50 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from './transformProps';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Correlation'),
description: t(
'Table that visualizes paired t-tests, which are used to understand statistical differences between groups.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
name: t('Paired t-test Table'),
tags: [t('Legacy'), t('Statistical'), t('Tabular')],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class PairedTTestChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./PairedTTest'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,158 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { defineChart } from '@superset-ui/glyph-core';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const PairedTTest = require('./PairedTTest').default;
type PairedTTestExtra = {
alpha: number;
groups: string[];
liftValPrec: number;
metrics: string[];
pValPrec: number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, PairedTTestExtra>({
metadata: {
name: t('Paired t-test Table'),
description: t(
'Table that visualizes paired t-tests, which are used to understand statistical differences between groups.',
),
category: t('Correlation'),
tags: [t('Legacy'), t('Statistical'), t('Tabular')],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
useLegacyApi: true,
},
arguments: {},
additionalControls: {
queryBefore: [['metrics']],
query: [
[
{
name: 'groupby',
override: {
validators: [validateNonEmpty],
},
},
],
['limit', 'timeseries_limit_metric'],
['order_desc'],
[
{
name: 'contribution',
config: {
type: 'CheckboxControl',
label: t('Contribution'),
default: false,
description: t('Compute the contribution to the total'),
},
},
],
['row_limit', null],
],
},
additionalSections: [
{
label: t('Parameters'),
expanded: false,
controlSetRows: [
[
{
name: 'significance_level',
config: {
type: 'TextControl',
label: t('Significance Level'),
default: 0.05,
description: t(
'Threshold alpha level for determining significance',
),
},
},
],
[
{
name: 'pvalue_precision',
config: {
type: 'TextControl',
label: t('p-value precision'),
default: 6,
description: t(
'Number of decimal places with which to display p-values',
),
},
},
],
[
{
name: 'liftvalue_precision',
config: {
type: 'TextControl',
label: t('Lift percent precision'),
default: 4,
description: t(
'Number of decimal places with which to display lift values',
),
},
},
],
],
},
],
transform: chartProps => {
const { formData } = chartProps;
const {
groupby,
liftvaluePrecision,
metrics,
pvaluePrecision,
significanceLevel,
} = formData as Record<string, unknown>;
return {
alpha: (significanceLevel as number) ?? 0.05,
groups: (groupby as string[]) ?? [],
liftValPrec: parseInt(String(liftvaluePrecision ?? '4'), 10),
metrics: ((metrics as Array<string | { label: string }>) ?? []).map(
metric => (typeof metric === 'string' ? metric : metric.label),
),
pValPrec: parseInt(String(pvaluePrecision ?? '6'), 10),
};
},
render: ({ alpha, groups, liftValPrec, metrics, pValPrec, data }) => (
<PairedTTest
alpha={alpha}
data={data}
groups={groups}
liftValPrec={liftValPrec}
metrics={metrics}
pValPrec={pValPrec}
/>
),
});

View File

@@ -1,42 +0,0 @@
/**
* 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 { ChartProps } from '@superset-ui/core';
export default function transformProps(chartProps: ChartProps) {
const { formData, queriesData } = chartProps;
const {
groupby,
liftvaluePrecision,
metrics,
pvaluePrecision,
significanceLevel,
} = formData;
return {
alpha: significanceLevel,
data: queriesData[0].data,
groups: groupby,
liftValPrec: parseInt(liftvaluePrecision, 10),
metrics: (metrics as (string | { label: string })[]).map(
(metric: string | { label: string }) =>
typeof metric === 'string' ? metric : metric.label,
),
pValPrec: parseInt(pvaluePrecision, 10),
};
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -17,67 +17,12 @@
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}
declare module 'distributions' {
class Studentt {
constructor(degreesOfFreedom: number);
cdf(x: number): number;
}
const dist: {
Studentt: typeof Studentt;
};
export default dist;
}
declare module 'reactable' {
import { ComponentType, ReactNode } from 'react';
interface TableProps {
className?: string;
id?: string;
sortable?: (
| string
| {
column: string;
sortFunction: (a: string, b: string) => number;
}
)[];
children?: ReactNode;
}
interface TrProps {
className?: string;
onClick?: () => void;
children?: ReactNode;
}
interface TdProps {
className?: string;
column?: string;
data?: string | number | boolean;
children?: ReactNode;
}
interface ThProps {
column?: string;
children?: ReactNode;
}
interface TheadProps {
children?: ReactNode;
}
export const Table: ComponentType<TableProps>;
export const Tr: ComponentType<TrProps>;
export const Td: ComponentType<TdProps>;
export const Th: ComponentType<ThProps>;
export const Thead: ComponentType<TheadProps>;
}
declare module 'distributions';
declare module 'reactable';

View File

@@ -35,6 +35,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
}

View File

@@ -1,69 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ControlPanelConfig } from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['series'],
['metrics'],
['secondary_metric'],
['adhoc_filters'],
['limit', 'row_limit'],
['timeseries_limit_metric'],
['order_desc'],
],
},
{
label: t('Options'),
expanded: true,
controlSetRows: [
[
{
name: 'show_datatable',
config: {
type: 'CheckboxControl',
label: t('Data Table'),
default: false,
renderTrigger: true,
description: t('Whether to display the interactive data table'),
},
},
{
name: 'include_series',
config: {
type: 'CheckboxControl',
label: t('Include Series'),
renderTrigger: true,
default: false,
description: t('Include series name as an axis'),
},
},
],
['linear_color_scheme'],
],
},
],
};
export default config;

View File

@@ -1,56 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/example1.jpg';
import example1Dark from './images/example1-dark.jpg';
import example2 from './images/example2.jpg';
import example2Dark from './images/example2-dark.jpg';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Ranking'),
credits: ['https://syntagmatic.github.io/parallel-coordinates'],
description: t(
'Plots the individual metrics for each row in the data vertically and links them together as a line. This chart is useful for comparing multiple metrics across all of the samples or rows in the data.',
),
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
],
name: t('Parallel Coordinates'),
tags: [t('Directional'), t('Legacy'), t('Relational')],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class ParallelCoordinatesChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactParallelCoordinates'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,147 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { defineChart } from '@superset-ui/glyph-core';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/example1.jpg';
import example1Dark from './images/example1-dark.jpg';
import example2 from './images/example2.jpg';
import example2Dark from './images/example2-dark.jpg';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactParallelCoordinates = require('./ReactParallelCoordinates').default;
type ParallelCoordinatesExtra = {
includeSeries: boolean;
linearColorScheme: string;
metrics: string[];
colorMetric: string | undefined;
series: string;
showDatatable: boolean;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, ParallelCoordinatesExtra>({
metadata: {
name: t('Parallel Coordinates'),
description: t(
'Plots the individual metrics for each row in the data vertically and links them together as a line. This chart is useful for comparing multiple metrics across all of the samples or rows in the data.',
),
category: t('Ranking'),
credits: ['https://syntagmatic.github.io/parallel-coordinates'],
tags: [t('Directional'), t('Legacy'), t('Relational')],
thumbnail,
thumbnailDark,
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
],
useLegacyApi: true,
},
arguments: {},
additionalControls: {
queryBefore: [['series'], ['metrics'], ['secondary_metric']],
query: [
['limit', 'row_limit'],
['timeseries_limit_metric'],
['order_desc'],
],
},
middleSections: [
{
label: t('Options'),
expanded: true,
controlSetRows: [
[
{
name: 'show_datatable',
config: {
type: 'CheckboxControl',
label: t('Data Table'),
default: false,
renderTrigger: true,
description: t('Whether to display the interactive data table'),
},
},
{
name: 'include_series',
config: {
type: 'CheckboxControl',
label: t('Include Series'),
renderTrigger: true,
default: false,
description: t('Include series name as an axis'),
},
},
],
['linear_color_scheme'],
],
},
],
transform: chartProps => {
const { formData } = chartProps;
const {
includeSeries,
linearColorScheme,
metrics,
secondaryMetric,
series,
showDatatable,
} = formData as Record<string, unknown>;
return {
includeSeries: (includeSeries as boolean) ?? false,
linearColorScheme: (linearColorScheme as string) ?? '',
metrics: ((metrics as Array<string | { label: string }>) ?? []).map(
m => (m as { label?: string }).label || (m as string),
),
colorMetric:
secondaryMetric && (secondaryMetric as { label?: string }).label
? (secondaryMetric as { label: string }).label
: (secondaryMetric as string | undefined),
series: (series as string) ?? '',
showDatatable: (showDatatable as boolean) ?? false,
};
},
render: ({
width,
height,
data,
includeSeries,
linearColorScheme,
metrics,
colorMetric,
series,
showDatatable,
}) => (
<ReactParallelCoordinates
width={width}
height={height}
data={data}
includeSeries={includeSeries}
linearColorScheme={linearColorScheme}
metrics={metrics}
colorMetric={colorMetric}
series={series}
showDatatable={showDatatable}
/>
),
});

View File

@@ -1,48 +0,0 @@
/**
* 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 { ChartProps } from '@superset-ui/core';
import { isThemeDark } from '@apache-superset/core/theme';
export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, queriesData, theme } = chartProps;
const {
includeSeries,
linearColorScheme,
metrics,
secondaryMetric,
series,
showDatatable,
} = formData;
return {
width,
height,
data: queriesData[0].data,
defaultLineColor: theme.colorTextTertiary,
includeSeries,
isDarkMode: isThemeDark(theme),
linearColorScheme,
metrics: metrics.map((m: { label?: string } | string) =>
typeof m === 'string' ? m : m.label || m,
),
colorMetric: secondaryMetric?.label || secondaryMetric,
series,
showDatatable,
};
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,18 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}
declare module 'd3v3' {
const d3: Record<string, Function>;
export = d3;
}
declare module 'd3v3';

View File

@@ -31,6 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"@superset-ui/glyph-core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"react": "^18.2.0",

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
declare module "*.png" {
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
declare module "*.jpg" {
const value: any;
export default value;
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}

View File

@@ -38,6 +38,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
}

View File

@@ -1,155 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import {
ControlPanelConfig,
formatSelectOptions,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { ColorBy } from './utils';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['entity'],
[
{
name: 'country_fieldtype',
config: {
type: 'SelectControl',
label: t('Country Field Type'),
default: 'cca2',
choices: [
['name', t('Full name')],
['cioc', t('code International Olympic Committee (cioc)')],
['cca2', t('code ISO 3166-1 alpha-2 (cca2)')],
['cca3', t('code ISO 3166-1 alpha-3 (cca3)')],
],
description: t(
'The country code standard that Superset should expect ' +
'to find in the [country] column',
),
},
},
],
['metric'],
['adhoc_filters'],
['row_limit'],
['sort_by_metric'],
],
},
{
label: t('Options'),
expanded: true,
controlSetRows: [
[
{
name: 'show_bubbles',
config: {
type: 'CheckboxControl',
label: t('Show Bubbles'),
default: false,
renderTrigger: true,
description: t('Whether to display bubbles on top of countries'),
},
},
],
['secondary_metric'],
[
{
name: 'max_bubble_size',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Max Bubble Size'),
default: '25',
choices: formatSelectOptions([
'5',
'10',
'15',
'25',
'50',
'75',
'100',
]),
},
},
],
['color_picker'],
[
{
name: 'color_by',
config: {
type: 'RadioButtonControl',
label: t('Color by'),
default: ColorBy.Metric,
options: [
[ColorBy.Metric, t('Metric')],
[ColorBy.Country, t('Country')],
],
description: t(
'Choose whether a country should be shaded by the metric, or assigned a color based on a categorical color palette',
),
},
},
],
['linear_color_scheme'],
['color_scheme'],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [['y_axis_format'], ['currency_format']],
},
],
controlOverrides: {
entity: {
label: t('Country Column'),
description: t('3 letter code of the country'),
},
secondary_metric: {
label: t('Bubble Size'),
description: t('Metric that defines the size of the bubble'),
},
color_picker: {
label: t('Bubble Color'),
},
linear_color_scheme: {
label: t('Country Color Scheme'),
visibility: ({ controls }) =>
Boolean(controls?.color_by.value === ColorBy.Metric),
},
color_scheme: {
label: t('Country Color Scheme'),
visibility: ({ controls }) =>
Boolean(controls?.color_by.value === ColorBy.Country),
},
},
formDataOverrides: formData => ({
...formData,
entity: getStandardizedControls().shiftColumn(),
metric: getStandardizedControls().shiftMetric(),
}),
};
export default config;

View File

@@ -1,71 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/WorldMap1.jpg';
import example1Dark from './images/WorldMap1-dark.jpg';
import example2 from './images/WorldMap2.jpg';
import example2Dark from './images/WorldMap2-dark.jpg';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Map'),
credits: ['http://datamaps.github.io/'],
description: t(
'A map of the world, that can indicate values in different countries.',
),
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
],
name: t('World Map'),
tags: [
t('2D'),
t('Comparison'),
t('Intensity'),
t('Legacy'),
t('Multi-Dimensions'),
t('Multi-Layers'),
t('Multi-Variables'),
t('Scatter'),
t('Featured'),
],
thumbnail,
thumbnailDark,
useLegacyApi: true,
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
});
export default class WorldMapChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactWorldMap'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,266 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { Behavior, getValueFormatter, Currency } from '@superset-ui/core';
import {
formatSelectOptions,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import { rgb } from 'd3-color';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/WorldMap1.jpg';
import example1Dark from './images/WorldMap1-dark.jpg';
import example2 from './images/WorldMap2.jpg';
import example2Dark from './images/WorldMap2-dark.jpg';
import { ColorBy } from './utils';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactWorldMap = require('./ReactWorldMap').default;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type WorldMapExtra = Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, WorldMapExtra>({
metadata: {
name: t('World Map'),
description: t(
'A map of the world, that can indicate values in different countries.',
),
category: t('Map'),
credits: ['http://datamaps.github.io/'],
tags: [
t('2D'),
t('Comparison'),
t('Intensity'),
t('Legacy'),
t('Multi-Dimensions'),
t('Multi-Layers'),
t('Multi-Variables'),
t('Scatter'),
t('Featured'),
],
thumbnail,
thumbnailDark,
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
],
useLegacyApi: true,
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
},
arguments: {},
additionalControls: {
queryBefore: [
['entity'],
[
{
name: 'country_fieldtype',
config: {
type: 'SelectControl',
label: t('Country Field Type'),
default: 'cca2',
choices: [
['name', t('Full name')],
['cioc', t('code International Olympic Committee (cioc)')],
['cca2', t('code ISO 3166-1 alpha-2 (cca2)')],
['cca3', t('code ISO 3166-1 alpha-3 (cca3)')],
],
description: t(
'The country code standard that Superset should expect ' +
'to find in the [country] column',
),
},
},
],
['metric'],
],
query: [['row_limit'], ['sort_by_metric']],
chartOptions: [['y_axis_format'], ['currency_format']],
},
middleSections: [
{
label: t('Options'),
expanded: true,
controlSetRows: [
[
{
name: 'show_bubbles',
config: {
type: 'CheckboxControl',
label: t('Show Bubbles'),
default: false,
renderTrigger: true,
description: t('Whether to display bubbles on top of countries'),
},
},
],
['secondary_metric'],
[
{
name: 'max_bubble_size',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Max Bubble Size'),
default: '25',
choices: formatSelectOptions([
'5',
'10',
'15',
'25',
'50',
'75',
'100',
]),
},
},
],
['color_picker'],
[
{
name: 'color_by',
config: {
type: 'RadioButtonControl',
label: t('Color by'),
default: ColorBy.Metric,
options: [
[ColorBy.Metric, t('Metric')],
[ColorBy.Country, t('Country')],
],
description: t(
'Choose whether a country should be shaded by the metric, or assigned a color based on a categorical color palette',
),
},
},
],
['linear_color_scheme'],
['color_scheme'],
],
},
],
additionalControlOverrides: {
entity: {
label: t('Country Column'),
description: t('3 letter code of the country'),
},
secondary_metric: {
label: t('Bubble Size'),
description: t('Metric that defines the size of the bubble'),
},
color_picker: {
label: t('Bubble Color'),
},
linear_color_scheme: {
label: t('Country Color Scheme'),
visibility: ({
controls,
}: {
controls?: Record<string, { value: unknown }>;
}) => Boolean(controls?.color_by?.value === ColorBy.Metric),
},
color_scheme: {
label: t('Country Color Scheme'),
visibility: ({
controls,
}: {
controls?: Record<string, { value: unknown }>;
}) => Boolean(controls?.color_by?.value === ColorBy.Country),
},
},
formDataOverrides: formData => ({
...formData,
entity: getStandardizedControls().shiftColumn(),
metric: getStandardizedControls().shiftMetric(),
}),
transform: chartProps => {
const {
formData,
hooks,
inContextMenu,
filterState,
emitCrossFilters,
datasource,
} = chartProps;
const { onContextMenu, setDataMask } = hooks as Record<string, unknown>;
const {
countryFieldtype,
entity,
maxBubbleSize,
showBubbles,
linearColorScheme,
colorPicker,
colorBy,
colorScheme,
sliceId,
metric,
yAxisFormat,
currencyFormat,
} = formData as Record<string, unknown>;
const { r, g, b } = (colorPicker ?? { r: 0, g: 0, b: 0 }) as {
r: number;
g: number;
b: number;
};
const { currencyFormats = {}, columnFormats = {} } = (datasource ?? {}) as {
currencyFormats?: Record<string, unknown>;
columnFormats?: Record<string, unknown>;
};
const formatter = getValueFormatter(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metric as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
currencyFormats as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
columnFormats as any,
yAxisFormat as string,
currencyFormat as Currency,
);
return {
countryFieldtype,
entity,
maxBubbleSize: parseInt(String(maxBubbleSize ?? '25'), 10),
showBubbles,
linearColorScheme,
color: rgb(r, g, b).formatHex(),
colorBy,
colorScheme,
sliceId,
onContextMenu,
setDataMask,
inContextMenu,
filterState,
emitCrossFilters,
formatter,
};
},
render: ({ width, height, data, ...extra }) => (
<ReactWorldMap width={width} height={height} data={data} {...extra} />
),
});

View File

@@ -1,89 +0,0 @@
/**
* 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 { rgb } from 'd3-color';
import { ChartProps, getValueFormatter } from '@superset-ui/core';
export default function transformProps(chartProps: ChartProps) {
const {
width,
height,
formData,
queriesData,
hooks,
inContextMenu,
filterState,
emitCrossFilters,
datasource,
} = chartProps;
const { onContextMenu, setDataMask } = hooks;
const {
countryFieldtype,
entity,
maxBubbleSize,
showBubbles,
linearColorScheme,
colorPicker,
colorBy,
colorScheme,
sliceId,
metric,
yAxisFormat,
currencyFormat,
} = formData;
const { r, g, b } = colorPicker;
const {
currencyFormats = {},
columnFormats = {},
currencyCodeColumn,
} = datasource;
const { data, detected_currency: detectedCurrency } = queriesData[0];
const formatter = getValueFormatter(
metric,
currencyFormats,
columnFormats,
yAxisFormat,
currencyFormat,
undefined, // key - not needed for single-metric charts
data,
currencyCodeColumn,
detectedCurrency,
);
return {
countryFieldtype,
entity,
data,
width,
height,
maxBubbleSize: parseInt(maxBubbleSize, 10),
showBubbles,
linearColorScheme,
color: rgb(r, g, b).hex(),
colorBy,
colorScheme,
sliceId,
onContextMenu,
setDataMask,
inContextMenu,
filterState,
emitCrossFilters,
formatter,
};
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -16,13 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}

View File

@@ -42,6 +42,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"dayjs": "^1.11.19",
"react": "^18.2.0"
}

View File

@@ -1,63 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin, ChartLabel } from '@superset-ui/core';
import transformProps from '../transformProps';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Correlation'),
credits: ['http://nvd3.org'],
description: t(
'Visualizes a metric across three dimensions of data in a single chart (X axis, Y axis, and bubble size). Bubbles from the same group can be showcased using bubble color.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
label: ChartLabel.Deprecated,
name: t('Bubble Chart (legacy)'),
tags: [
t('Multi-Dimensions'),
t('Comparison'),
t('Legacy'),
t('Scatter'),
t('Time'),
t('Trend'),
t('nvd3'),
],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
/**
* @deprecated in version 4.0.
*/
export default class BubbleChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('../ReactNVD3'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -17,12 +17,13 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { ChartLabel } from '@superset-ui/core';
import {
ControlPanelConfig,
formatSelectOptions,
D3_FORMAT_OPTIONS,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import {
showLegend,
xAxisLabel,
@@ -36,9 +37,48 @@ import {
leftMargin,
yAxisBounds,
} from '../NVD3Controls';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
const config: ControlPanelConfig = {
controlPanelSections: [
// eslint-disable-next-line @typescript-eslint/no-var-requires
const transformPropsJs = require('../transformProps').default;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactNVD3 = require('../ReactNVD3').default;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NVD3Extra = Record<string, any>;
/**
* @deprecated in version 4.0.
*/
export default defineChart<Record<string, never>, NVD3Extra>({
metadata: {
name: t('Bubble Chart (legacy)'),
description: t(
'Visualizes a metric across three dimensions of data in a single chart (X axis, Y axis, and bubble size). Bubbles from the same group can be showcased using bubble color.',
),
category: t('Correlation'),
credits: ['http://nvd3.org'],
label: ChartLabel.Deprecated,
tags: [
t('Multi-Dimensions'),
t('Comparison'),
t('Legacy'),
t('Scatter'),
t('Time'),
t('Trend'),
t('nvd3'),
],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
useLegacyApi: true,
},
arguments: {},
suppressQuerySection: true,
prependSections: [
{
label: t('Query'),
expanded: true,
@@ -122,7 +162,7 @@ const config: ControlPanelConfig = {
],
},
],
controlOverrides: {
additionalControlOverrides: {
color_scheme: {
renderTrigger: false,
},
@@ -135,6 +175,6 @@ const config: ControlPanelConfig = {
y: getStandardizedControls().shiftMetric(),
size: getStandardizedControls().shiftMetric(),
}),
};
export default config;
transform: chartProps => transformPropsJs(chartProps),
render: props => <ReactNVD3 {...props} />,
});

View File

@@ -1,51 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from '../transformProps';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('KPI'),
credits: ['http://nvd3.org'],
description: t(
'Showcases the progress of a single metric against a given target. The higher the fill, the closer the metric is to the target.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
name: t('Bullet Chart'),
tags: [t('Business'), t('Legacy'), t('Report'), t('nvd3')],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class BulletChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('../ReactNVD3'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -17,10 +17,37 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { ControlPanelConfig } from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
const config: ControlPanelConfig = {
controlPanelSections: [
// eslint-disable-next-line @typescript-eslint/no-var-requires
const transformPropsJs = require('../transformProps').default;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactNVD3 = require('../ReactNVD3').default;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NVD3Extra = Record<string, any>;
export default defineChart<Record<string, never>, NVD3Extra>({
metadata: {
name: t('Bullet Chart'),
description: t(
'Showcases the progress of a single metric against a given target. The higher the fill, the closer the metric is to the target.',
),
category: t('KPI'),
credits: ['http://nvd3.org'],
tags: [t('Business'), t('Legacy'), t('Report'), t('nvd3')],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
useLegacyApi: true,
},
arguments: {},
suppressQuerySection: true,
prependSections: [
{
label: t('Query'),
expanded: true,
@@ -93,6 +120,6 @@ const config: ControlPanelConfig = {
],
},
],
};
export default config;
transform: chartProps => transformPropsJs(chartProps),
render: props => <ReactNVD3 {...props} />,
});

View File

@@ -1,62 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin, ChartLabel } from '@superset-ui/core';
import transformProps from '../transformProps';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Evolution'),
credits: ['http://nvd3.org'],
description: t(
'Visualizes many different time-series objects in a single chart. This chart is being deprecated and we recommend using the Time-series Chart instead.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
label: ChartLabel.Deprecated,
name: t('Time-series Percent Change'),
tags: [
t('Legacy'),
t('Time'),
t('nvd3'),
t('Advanced-Analytics'),
t('Comparison'),
t('Line'),
t('Percentages'),
t('Predictive'),
t('Trend'),
],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class CompareChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('../ReactNVD3'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -17,11 +17,9 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import {
ControlPanelConfig,
getStandardizedControls,
sections,
} from '@superset-ui/chart-controls';
import { ChartLabel } from '@superset-ui/core';
import { getStandardizedControls, sections } from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import {
xAxisLabel,
yAxisLabel,
@@ -35,9 +33,47 @@ import {
leftMargin,
timeSeriesSection,
} from '../NVD3Controls';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
const config: ControlPanelConfig = {
controlPanelSections: [
// eslint-disable-next-line @typescript-eslint/no-var-requires
const transformPropsJs = require('../transformProps').default;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactNVD3 = require('../ReactNVD3').default;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NVD3Extra = Record<string, any>;
export default defineChart<Record<string, never>, NVD3Extra>({
metadata: {
name: t('Time-series Percent Change'),
description: t(
'Visualizes many different time-series objects in a single chart. This chart is being deprecated and we recommend using the Time-series Chart instead.',
),
category: t('Evolution'),
credits: ['http://nvd3.org'],
label: ChartLabel.Deprecated,
tags: [
t('Legacy'),
t('Time'),
t('nvd3'),
t('Advanced-Analytics'),
t('Comparison'),
t('Line'),
t('Percentages'),
t('Predictive'),
t('Trend'),
],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
useLegacyApi: true,
},
arguments: {},
suppressQuerySection: true,
prependSections: [
sections.legacyTimeseriesTime,
timeSeriesSection[0],
{
@@ -71,6 +107,6 @@ const config: ControlPanelConfig = {
groupby: getStandardizedControls().popAllColumns(),
metrics: getStandardizedControls().popAllMetrics(),
}),
};
export default config;
transform: chartProps => transformPropsJs(chartProps),
render: props => <ReactNVD3 {...props} />,
});

View File

@@ -1,51 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from '../transformProps';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Evolution'),
credits: ['http://nvd3.org'],
description: t(
'Compares metrics between different time periods. Displays time series data across multiple periods (like weeks or months) to show period-over-period trends and patterns.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
name: t('Time-series Period Pivot'),
tags: [t('Legacy'), t('Time'), t('nvd3')],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class TimePivotChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('../ReactNVD3'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -18,11 +18,15 @@
*/
import { t } from '@apache-superset/core/translation';
import {
ControlPanelConfig,
D3_FORMAT_OPTIONS,
getStandardizedControls,
sections,
} from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example from './images/example.jpg';
import exampleDark from './images/example-dark.jpg';
import {
lineInterpolation,
showLegend,
@@ -37,8 +41,31 @@ import {
leftMargin,
} from '../NVD3Controls';
const config: ControlPanelConfig = {
controlPanelSections: [
// eslint-disable-next-line @typescript-eslint/no-var-requires
const transformPropsJs = require('../transformProps').default;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactNVD3 = require('../ReactNVD3').default;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NVD3Extra = Record<string, any>;
export default defineChart<Record<string, never>, NVD3Extra>({
metadata: {
name: t('Time-series Period Pivot'),
description: t(
'Compares metrics between different time periods. Displays time series data across multiple periods (like weeks or months) to show period-over-period trends and patterns.',
),
category: t('Evolution'),
credits: ['http://nvd3.org'],
tags: [t('Legacy'), t('Time'), t('nvd3')],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
useLegacyApi: true,
},
arguments: {},
suppressQuerySection: true,
prependSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
@@ -119,15 +146,16 @@ const config: ControlPanelConfig = {
],
},
],
controlOverrides: {
additionalControlOverrides: {
metric: {
clearable: false,
},
},
formDataOverrides: formData => ({
...formData,
metric: getStandardizedControls().shiftMetric,
metric: getStandardizedControls().shiftMetric(),
}),
};
export default config;
transform: chartProps => transformPropsJs(chartProps),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render: (props: any) => <ReactNVD3 {...props} />,
});

View File

@@ -16,6 +16,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -17,33 +17,10 @@
* under the License.
*/
declare module '*.png' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpg' {
const value: string;
const value: any;
export default value;
}
declare module '*.jpeg' {
const value: string;
export default value;
}
declare module 'd3' {
const d3: Record<string, unknown>;
export default d3;
}
declare module 'nvd3-fork' {
const nv: Record<string, unknown>;
export default nv;
}
declare module 'nvd3-fork/build/nv.d3.css';
declare module 'd3-tip' {
const d3tip: () => Record<string, unknown>;
export default d3tip;
}

View File

@@ -20,6 +20,7 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
]
}

View File

@@ -23,7 +23,7 @@
"homepage": "https://github.com/apache-superset/superset-ui#readme",
"contributors": [
"terrestris GmbH & Co. KG <info@terrestris.de> (https://www.terrestris.de)",
"meggsimum - Büro für Geoinformatik <info@meggsimum.de> (https://meggsimum.de)"
"meggsimum - B\u00fcro f\u00fcr Geoinformatik <info@meggsimum.de> (https://meggsimum.de)"
],
"publishConfig": {
"access": "public"
@@ -39,6 +39,7 @@
"@reduxjs/toolkit": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@types/react-redux": "*",
"geostyler": "^18.3.1",
"geostyler-data": "^1.0.0",

View File

@@ -1,59 +0,0 @@
/**
* 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 { createRef, useState } from 'react';
import { styled, useTheme } from '@apache-superset/core/theme';
import OlMap from 'ol/Map';
import {
CartodiagramPluginProps,
CartodiagramPluginStylesProps,
} from './types';
import OlChartMap from './components/OlChartMap';
import 'ol/ol.css';
// The following Styles component is a <div> element, which has been styled using Emotion
// For docs, visit https://emotion.sh/docs/styled
// Theming variables are provided for your use via a ThemeProvider
// imported from @superset-ui/core. For variables available, please visit
// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/style/index.ts
const Styles = styled.div<CartodiagramPluginStylesProps>`
height: ${({ height }) => height}px;
width: ${({ width }) => width}px;
`;
export default function CartodiagramPlugin(props: CartodiagramPluginProps) {
const { height, width } = props;
const theme = useTheme();
const rootElem = createRef<HTMLDivElement>();
const [mapId] = useState(
`cartodiagram-plugin-${Math.floor(Math.random() * 1000000)}`,
);
const [olMap] = useState(new OlMap({}));
return (
<Styles ref={rootElem} height={height} width={width} theme={theme}>
<OlChartMap mapId={mapId} olMap={olMap} {...props} />
</Styles>
);
}

View File

@@ -1,20 +0,0 @@
/**
* 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.
*/
// eslint-disable-next-line import/prefer-default-export
export { default as CartodiagramPlugin } from './plugin';

View File

@@ -0,0 +1,393 @@
/**
* 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 { createRef, useState } from 'react';
import { t } from '@apache-superset/core/translation';
import { styled, useTheme } from '@apache-superset/core/theme';
import OlMap from 'ol/Map';
import {
validateNonEmpty,
ChartPlugin,
QueryFormData,
getChartBuildQueryRegistry,
getChartTransformPropsRegistry,
} from '@superset-ui/core';
import { defineChart } from '@superset-ui/glyph-core';
import {
CartodiagramPluginConstructorOpts,
CartodiagramPluginProps,
CartodiagramPluginStylesProps,
LayerConf,
} from './types';
import OlChartMap from './components/OlChartMap';
import { parseSelectedChart, getChartConfigs } from './util/transformPropsUtil';
import { selectedChartMutator } from './util/controlPanelUtil';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from './util/zoomUtil';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/example1.png';
import example1Dark from './images/example1-dark.png';
import example2 from './images/example2.png';
import example2Dark from './images/example2-dark.png';
import 'ol/ol.css';
// ── CartodiagramPlugin component ──────────────────────────────────────────────
const Styles = styled.div<CartodiagramPluginStylesProps>`
height: ${({ height }) => height}px;
width: ${({ width }) => width}px;
`;
function CartodiagramPlugin(props: CartodiagramPluginProps) {
const { height, width } = props;
const theme = useTheme();
const rootElem = createRef<HTMLDivElement>();
const [mapId] = useState(
`cartodiagram-plugin-${Math.floor(Math.random() * 1000000)}`,
);
const [olMap] = useState(new OlMap({}));
return (
<Styles ref={rootElem} height={height} width={width} theme={theme}>
<OlChartMap mapId={mapId} olMap={olMap} {...props} />
</Styles>
);
}
// ── buildQuery ────────────────────────────────────────────────────────────────
export function buildQuery(formData: QueryFormData) {
const {
selected_chart: selectedChartString,
geom_column: geometryColumn,
extra_form_data: extraFormData,
} = formData;
const selectedChart = JSON.parse(selectedChartString);
const vizType = selectedChart.viz_type;
const chartFormData = JSON.parse(selectedChart.params);
// Pass extra_form_data to chartFormData so that
// dashboard filters will also be applied to the charts
// on the map.
chartFormData.extra_form_data = {
...chartFormData.extra_form_data,
...extraFormData,
};
// adapt groupby property to ensure geometry column always exists
// and is always at first position
let { groupby } = chartFormData;
if (!groupby) {
groupby = [];
}
// add geometry column at the first place
groupby?.unshift(geometryColumn);
chartFormData.groupby = groupby;
// TODO: find way to import correct type "InclusiveLoaderResult"
const buildQueryRegistry = getChartBuildQueryRegistry();
const chartQueryBuilder = buildQueryRegistry.get(vizType) as any;
const chartQuery = chartQueryBuilder(chartFormData);
return chartQuery;
}
// ── Plugin definition ─────────────────────────────────────────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CartodiagramExtra = Record<string, any>;
// Standalone transformProps exported for testing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function transformProps(chartProps: any) {
const { formData, hooks, width, height, queriesData } = chartProps;
const {
geomColumn,
selectedChart: selectedChartString,
chartSize,
layerConfigs,
mapView,
chartBackgroundColor,
chartBackgroundBorderRadius,
} = formData as Record<string, unknown>;
const { setControlValue = () => {} } = (hooks ?? {}) as {
setControlValue?: (key: string, value: unknown) => void;
};
const selectedChart = parseSelectedChart(selectedChartString as string);
const transformPropsRegistry = getChartTransformPropsRegistry();
const chartTransformer = transformPropsRegistry.get(selectedChart.viz_type);
const chartConfigs = getChartConfigs(
selectedChart,
geomColumn as string,
chartProps,
chartTransformer,
);
return {
width,
height,
queriesData,
geomColumn,
selectedChart,
chartConfigs,
chartVizType: selectedChart.viz_type,
chartSize,
layerConfigs,
mapView,
chartBackgroundColor,
chartBackgroundBorderRadius,
setControlValue,
};
}
export function createCartodiagramPlugin(
opts: CartodiagramPluginConstructorOpts = {},
): new () => ChartPlugin {
const layerConfigsDefault: LayerConf[] = opts.defaultLayers ?? [];
return defineChart<Record<string, never>, CartodiagramExtra>({
metadata: {
name: t('Cartodiagram'),
description:
'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.',
category: t('Map'),
tags: [t('Geo'), t('2D'), t('Spatial'), t('Experimental')],
thumbnail,
thumbnailDark,
exampleGallery: [
{
url: example1,
urlDark: example1Dark,
caption: t('Pie charts on a map'),
},
{
url: example2,
urlDark: example2Dark,
caption: t('Line charts on a map'),
},
],
},
arguments: {},
buildQuery,
suppressQuerySection: true,
prependSections: [
{
label: t('Configuration'),
expanded: true,
controlSetRows: [
[
{
name: 'selected_chart',
config: {
type: 'SelectAsyncControl',
mutator: selectedChartMutator,
multi: false,
label: t('Chart'),
validators: [validateNonEmpty],
description: t('Choose a chart for displaying on the map'),
placeholder: t('Select chart'),
onAsyncErrorMessage: t('Error while fetching charts'),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mapStateToProps: (state: any) => {
if (state?.datasource?.id) {
const { id: datasourceId } = state.datasource;
const query = {
columns: ['id', 'slice_name', 'params', 'viz_type'],
filters: [
{
col: 'datasource_id',
opr: 'eq',
value: datasourceId,
},
],
page: 0,
page_size: 999,
};
return {
dataEndpoint: `/api/v1/chart/?q=${JSON.stringify(query)}`,
};
}
return {};
},
},
},
],
[
{
name: 'geom_column',
config: {
type: 'SelectControl',
label: t('Geometry Column'),
renderTrigger: false,
description: t('The name of the geometry column'),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mapStateToProps: (state: any) => ({
choices: state.datasource?.columns?.map(
(c: { column_name: string }) => [
c.column_name,
c.column_name,
],
),
}),
validators: [validateNonEmpty],
},
},
],
],
},
{
label: t('Map Options'),
expanded: true,
controlSetRows: [
[
{
name: 'map_view',
config: {
type: 'MapViewControl',
renderTrigger: true,
description: t(
'The extent of the map on application start. FIT DATA automatically sets the extent so that all data points are included in the viewport. CUSTOM allows users to define the extent manually.',
),
label: t('Extent'),
dontRefreshOnChange: true,
default: {
mode: 'FIT_DATA',
},
},
},
],
[
{
name: 'layer_configs',
config: {
type: 'LayerConfigsControl',
renderTrigger: true,
label: t('Layers'),
default: layerConfigsDefault,
description: t('The configuration for the map layers'),
},
},
],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[
{
name: 'chart_background_color',
config: {
label: t('Background Color'),
description: t('The background color of the charts.'),
type: 'ColorPickerControl',
default: { r: 255, g: 255, b: 255, a: 0.2 },
renderTrigger: true,
},
},
],
[
{
name: 'chart_background_border_radius',
config: {
label: t('Corner Radius'),
description: t('The corner radius of the chart background'),
type: 'SliderControl',
default: 10,
min: 0,
step: 1,
max: 100,
renderTrigger: true,
},
},
],
[
{
name: 'chart_size',
config: {
type: 'ZoomConfigControl',
renderTrigger: true,
default: {
type: 'FIXED',
configs: {
zoom: 6,
width: 100,
height: 100,
slope: 30,
exponent: 2,
},
values: {
...Array.from(
{ length: MAX_ZOOM_LEVEL - MIN_ZOOM_LEVEL + 1 },
() => ({ width: 100, height: 100 }),
),
},
},
label: t('Chart size'),
description: t('Configure the chart size for each zoom level'),
},
},
],
],
},
],
transform: chartProps => {
const { formData, hooks } = chartProps;
const {
geomColumn,
selectedChart: selectedChartString,
chartSize,
layerConfigs,
mapView,
chartBackgroundColor,
chartBackgroundBorderRadius,
} = formData as Record<string, unknown>;
const { setControlValue = () => {} } = (hooks ?? {}) as {
setControlValue?: (key: string, value: unknown) => void;
};
const selectedChart = parseSelectedChart(selectedChartString as string);
const transformPropsRegistry = getChartTransformPropsRegistry();
const chartTransformer = transformPropsRegistry.get(
selectedChart.viz_type,
);
const chartConfigs = getChartConfigs(
selectedChart,
geomColumn as string,
chartProps,
chartTransformer,
);
return {
geomColumn,
selectedChart,
chartConfigs,
chartVizType: selectedChart.viz_type,
chartSize,
layerConfigs,
mapView,
chartBackgroundColor,
chartBackgroundBorderRadius,
setControlValue,
};
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render: (props: any) => <CartodiagramPlugin {...props} />,
});
}

View File

@@ -1,54 +0,0 @@
/**
* 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 { QueryFormData, getChartBuildQueryRegistry } from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
const {
selected_chart: selectedChartString,
geom_column: geometryColumn,
extra_form_data: extraFormData,
} = formData;
const selectedChart = JSON.parse(selectedChartString);
const vizType = selectedChart.viz_type;
const chartFormData = JSON.parse(selectedChart.params);
// Pass extra_form_data to chartFormData so that
// dashboard filters will also be applied to the charts
// on the map.
chartFormData.extra_form_data = {
...chartFormData.extra_form_data,
...extraFormData,
};
// adapt groupby property to ensure geometry column always exists
// and is always at first position
let { groupby } = chartFormData;
if (!groupby) {
groupby = [];
}
// add geometry column at the first place
groupby?.unshift(geometryColumn);
chartFormData.groupby = groupby;
// TODO: find way to import correct type "InclusiveLoaderResult"
const buildQueryRegistry = getChartBuildQueryRegistry();
const chartQueryBuilder = buildQueryRegistry.get(vizType) as any;
const chartQuery = chartQueryBuilder(chartFormData);
return chartQuery;
}

View File

@@ -1,194 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { ControlPanelConfig } from '@superset-ui/chart-controls';
import { selectedChartMutator } from '../util/controlPanelUtil';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../util/zoomUtil';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Configuration'),
expanded: true,
controlSetRows: [
[
{
name: 'selected_chart',
config: {
type: 'SelectAsyncControl',
mutator: selectedChartMutator,
multi: false,
label: t('Chart'),
validators: [validateNonEmpty],
description: t('Choose a chart for displaying on the map'),
placeholder: t('Select chart'),
onAsyncErrorMessage: t('Error while fetching charts'),
mapStateToProps: state => {
if (state?.datasource?.id) {
const datasourceId = state.datasource.id;
const query = {
columns: ['id', 'slice_name', 'params', 'viz_type'],
filters: [
{
col: 'datasource_id',
opr: 'eq',
value: datasourceId,
},
],
page: 0,
// TODO check why we only retrieve 100 items, even though there are more
page_size: 999,
};
const dataEndpoint = `/api/v1/chart/?q=${JSON.stringify(
query,
)}`;
return { dataEndpoint };
}
// could not extract datasource from map
return {};
},
},
},
],
[
{
name: 'geom_column',
config: {
type: 'SelectControl',
label: t('Geometry Column'),
renderTrigger: false,
description: t('The name of the geometry column'),
mapStateToProps: state => ({
choices: state.datasource?.columns.map(c => [
c.column_name,
c.column_name,
]),
}),
validators: [validateNonEmpty],
},
},
],
],
},
{
label: t('Map Options'),
expanded: true,
controlSetRows: [
[
{
name: 'map_view',
config: {
type: 'MapViewControl',
renderTrigger: true,
description: t(
'The extent of the map on application start. FIT DATA automatically sets the extent so that all data points are included in the viewport. CUSTOM allows users to define the extent manually.',
),
label: t('Extent'),
dontRefreshOnChange: true,
default: {
mode: 'FIT_DATA',
},
},
},
],
[
{
// name is referenced in 'index.ts' for setting default value
name: 'layer_configs',
config: {
type: 'LayerConfigsControl',
renderTrigger: true,
label: t('Layers'),
default: [],
description: t('The configuration for the map layers'),
},
},
],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[
{
name: 'chart_background_color',
config: {
label: t('Background Color'),
description: t('The background color of the charts.'),
type: 'ColorPickerControl',
default: { r: 255, g: 255, b: 255, a: 0.2 },
renderTrigger: true,
},
},
],
[
{
name: 'chart_background_border_radius',
config: {
label: t('Corner Radius'),
description: t('The corner radius of the chart background'),
type: 'SliderControl',
default: 10,
min: 0,
step: 1,
max: 100,
renderTrigger: true,
},
},
],
[
{
name: 'chart_size',
config: {
type: 'ZoomConfigControl',
// set this to true, if we are able to render it fast
renderTrigger: true,
default: {
type: 'FIXED',
configs: {
zoom: 6,
width: 100,
height: 100,
slope: 30,
exponent: 2,
},
// create an object with keys MIN_ZOOM_LEVEL - MAX_ZOOM_LEVEL
// that all contain the same initial value
values: {
...Array.from(
{ length: MAX_ZOOM_LEVEL - MIN_ZOOM_LEVEL + 1 },
() => ({ width: 100, height: 100 }),
),
},
},
label: t('Chart size'),
description: t('Configure the chart size for each zoom level'),
},
},
],
],
},
],
};
export default config;

View File

@@ -1,79 +0,0 @@
/**
* 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 { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import thumbnail from '../images/thumbnail.png';
import thumbnailDark from '../images/thumbnail-dark.png';
import example1 from '../images/example1.png';
import example1Dark from '../images/example1-dark.png';
import example2 from '../images/example2.png';
import example2Dark from '../images/example2-dark.png';
import { CartodiagramPluginConstructorOpts } from '../types';
import { getLayerConfig } from '../util/controlPanelUtil';
export default class CartodiagramPlugin extends ChartPlugin {
constructor(opts: CartodiagramPluginConstructorOpts) {
const metadata = new ChartMetadata({
description:
'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.',
name: t('Cartodiagram'),
thumbnail,
thumbnailDark,
tags: [t('Geo'), t('2D'), t('Spatial'), t('Experimental')],
category: t('Map'),
exampleGallery: [
{
url: example1,
urlDark: example1Dark,
caption: t('Pie charts on a map'),
},
{
url: example2,
urlDark: example2Dark,
caption: t('Line charts on a map'),
},
],
});
if (opts.defaultLayers) {
const layerConfig = getLayerConfig(controlPanel);
// set defaults for layer config if found
if (layerConfig) {
layerConfig.config.default = opts.defaultLayers;
} else {
// eslint-disable-next-line no-console
console.warn(
'Cannot set defaultLayers. layerConfig not found in control panel. Please check if the path to layerConfig should be adjusted.',
);
}
}
super({
buildQuery,
controlPanel,
loadChart: () => import('../CartodiagramPlugin'),
metadata,
transformProps,
});
}
}

View File

@@ -1,63 +0,0 @@
/**
* 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 { ChartProps, getChartTransformPropsRegistry } from '@superset-ui/core';
import {
getChartConfigs,
parseSelectedChart,
} from '../util/transformPropsUtil';
export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, hooks, theme } = chartProps;
const {
geomColumn,
selectedChart: selectedChartString,
chartSize,
layerConfigs,
mapView,
chartBackgroundColor,
chartBackgroundBorderRadius,
} = formData;
const { setControlValue = () => {} } = hooks;
const selectedChart = parseSelectedChart(selectedChartString);
const transformPropsRegistry = getChartTransformPropsRegistry();
const chartTransformer = transformPropsRegistry.get(selectedChart.viz_type);
const chartConfigs = getChartConfigs(
selectedChart,
geomColumn,
chartProps,
chartTransformer,
);
return {
width,
height,
geomColumn,
selectedChart,
chartConfigs,
chartVizType: selectedChart.viz_type,
chartSize,
layerConfigs,
mapView,
chartBackgroundColor,
chartBackgroundBorderRadius,
setControlValue,
theme,
};
}

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