Compare commits

...

1 Commits

Author SHA1 Message Date
Evan Rusackas
eb36d7b043 feat(glyph): single-file chart definition pattern across all plugins
Introduces @superset-ui/glyph-core's defineChart() and migrates ECharts,
deckgl, nvd3, legacy, and other plugins from the multi-file pattern
(controlPanel/transformProps/buildQuery/types/component) to single-file
chart definitions. Includes the SIP design doc, GlyphOptionsPanel explore
UI, and the supporting test suites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:21:13 -07:00
486 changed files with 30639 additions and 47212 deletions

View File

@@ -14,6 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Install pre-push hooks too, not just pre-commit:
# pre-commit install --hook-type pre-push
# (or this default applies automatically on `pre-commit install`)
default_install_hook_types: [pre-commit, pre-push]
repos:
- repo: https://github.com/MarcoGorelli/auto-walrus
rev: 0.3.4
@@ -94,6 +98,19 @@ repos:
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
exclude: ^superset-frontend/cypress-base\/
require_serial: true
- id: full-type-check-frontend
name: Full Type-Check (Frontend, pre-push)
# The pre-commit (per-file) variant catches most issues but `tsc <file>`
# bypasses some project-wide invariants. This runs the same check CI
# runs (`npm run type` -> `tsc --noEmit -p tsconfig.json`) so cross-file
# type errors are surfaced before they reach origin.
entry: bash -c 'cd superset-frontend && npm run type'
language: system
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
exclude: ^superset-frontend/cypress-base\/
pass_filenames: false
stages: [pre-push]
require_serial: true
# blacklist unsafe functions like make_url (see #19526)
- repo: https://github.com/skorokithakis/blacklist-pre-commit-hook
rev: e2f070289d8eddcaec0b580d3bde29437e7c8221

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

@@ -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',

View File

@@ -51,6 +51,7 @@
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
"@superset-ui/glyph-core": "file:./packages/superset-ui-glyph-core",
"@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar",
"@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord",
"@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map",
@@ -10148,6 +10149,10 @@
"resolved": "packages/generator-superset",
"link": true
},
"node_modules/@superset-ui/glyph-core": {
"resolved": "packages/superset-ui-glyph-core",
"link": true
},
"node_modules/@superset-ui/legacy-plugin-chart-calendar": {
"resolved": "plugins/legacy-plugin-chart-calendar",
"link": true
@@ -10595,9 +10600,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -10614,9 +10616,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -10633,9 +10632,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -10652,9 +10648,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -10671,9 +10664,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -10690,9 +10680,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -26127,6 +26114,21 @@
}
}
},
"node_modules/jsdom/node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/jsdom/node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
@@ -43249,6 +43251,21 @@
}
}
},
"node_modules/whatwg-url/node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/whatwg-url/node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
@@ -44369,7 +44386,7 @@
"cross-env": "^10.1.0",
"fs-extra": "^11.3.5",
"jest": "^30.4.2",
"yeoman-test": "^11.5.2"
"yeoman-test": "^11.5.3"
},
"engines": {
"node": ">= 18.0.0",
@@ -44694,6 +44711,17 @@
"dev": true,
"license": "MIT"
},
"packages/superset-ui-glyph-core": {
"name": "@superset-ui/glyph-core",
"version": "0.20.3",
"license": "Apache-2.0",
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
}
},
"packages/superset-ui-switchboard": {
"name": "@superset-ui/switchboard",
"version": "0.20.3",
@@ -44714,6 +44742,7 @@
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"react": "^18.3.0"
}
},
@@ -44750,7 +44779,8 @@
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*"
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*"
}
},
"plugins/legacy-plugin-chart-chord/node_modules/react": {
@@ -44775,6 +44805,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"react": "^18.3.0"
}
},
@@ -44803,6 +44834,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"react": "^18.3.0"
}
},
@@ -44831,6 +44863,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"react": "^18.3.0"
}
},
@@ -44846,6 +44879,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"react": "^18.3.0"
}
},
@@ -44862,6 +44896,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"react": "^18.3.0",
@@ -44900,6 +44935,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"react": "^18.3.0"
}
},
@@ -44986,6 +45022,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"dayjs": "^1.11.21",
"react": "^18.3.0"
}
@@ -45061,6 +45098,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",
@@ -45089,6 +45127,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"dayjs": "^1.11.21",
"echarts": "*",
"memoize-one": "*",
@@ -45137,6 +45176,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"ace-builds": "^1.4.14",
"dayjs": "^1.11.21",
"handlebars": "^4.7.8",
@@ -45321,6 +45361,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"dayjs": "^1.11.21",
"mapbox-gl": ">=1.0.0",
"react": "^18.3.0",

View File

@@ -134,6 +134,7 @@
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
"@superset-ui/glyph-core": "file:./packages/superset-ui-glyph-core",
"@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar",
"@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord",
"@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map",

View File

@@ -415,6 +415,8 @@ export interface ControlPanelSectionConfig {
props: ControlPanelsContainerProps,
controlData: AnyDict,
) => boolean;
/** @internal Marks the auto-generated glyph "Chart Options" section */
_glyphChartOptions?: boolean;
}
export interface StandardizedControls {
@@ -447,6 +449,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

@@ -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,267 @@
<!--
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.
-->
# [SIP] Proposal for single-file chart plugins via `defineChart()` (the "Glyph" pattern)
> Draft — to be filed per the [SIP process](https://github.com/apache/superset/issues/5602).
> The SIP will be numbered by a committer upon acceptance.
### Motivation
Building a Superset visualization plugin today requires authoring and keeping in
sync five or more files per chart — `index.ts`, `controlPanel.ts`,
`transformProps.ts`, `buildQuery.ts`, `types.ts`, plus the component — wired
together through several layers of incidental complexity:
- **String-based control references.** Controls are referenced by name strings
(`'metric'`, `'groupby'`, `'y_axis_format'`) that are resolved at runtime
through registries and `expandControlConfig`. Typos fail silently; renames
require grep-driven archaeology.
- **Array-of-arrays layouts.** Control panel layout is expressed as
`controlSetRows: [[control], [control, control], [<JSX/>]]` — positional
nesting that conflates layout with configuration and is hostile to both
reading and tooling.
- **Triplicated knowledge.** The same fact (e.g. "this chart has a `showLegend`
boolean defaulting to true") is restated in the control panel (control
config), in transformProps (formData extraction + defaulting), and in the
component's prop types. The three routinely drift.
- **No type safety across the seam.** `transformProps` output and component
props are connected only by convention; mismatches surface as silently
`undefined` props at runtime, not compile errors.
The result is a high floor for contributing a chart, and a maintenance burden
proportional to (charts × files × duplicated facts).
### Proposed Change
A new workspace package, **`@superset-ui/glyph-core`**, provides a
**`defineChart()`** function that defines a complete chart plugin in a single
file. The core idea: **argument definitions are the single source of truth**
for the control panel, the query, the transform, and the render props.
```tsx
export default defineChart({
metadata: { name: t('My Chart'), thumbnail, behaviors: [Behavior.InteractiveChart] },
arguments: {
metric: Metric.with({ label: t('Metric') }),
groupby: Dimension.with({ label: t('Dimension') }),
showLegend: ShowLegend, // reusable preset
legendType: { arg: LegendType, visibleWhen: { showLegend: true } },
},
transform: (chartProps, argValues) => ({ echartOptions: build(chartProps) }),
render: props => <Echart {...props} />,
});
```
Key elements:
- **Argument classes** (`Metric`, `Dimension`, `Temporal`, `Select`, `Checkbox`,
`Int`, `Slider`, `Color`, `ColorPicker`, `RadioButton`, `Bounds`, `Text`,
`NumberFormat`, `Currency`, `TimeFormat`, `ConditionalFormatting`) declare
what a chart needs; glyph-core generates the matching control configs,
formData extraction with defaults, and typed render props.
- **Declarative conditions** — `visibleWhen` / `disabledWhen` replace
imperative `visibility:` functions reaching into Redux controls state, with a
single shared evaluator (`evaluateGlyphCondition`) usable from both the
legacy pipeline and native rendering.
- **Generated `buildQuery`** for the common case (built on the core
`getXAxisColumn`/`normalizeTimeColumn` helpers so time grain, adhoc x-axis
columns, and the `DTTM_ALIAS` fallback behave identically to hand-written
queries); charts with post-processing pass their own `buildQuery`.
- **Generated `transformProps`** maps formData → typed render props (metric
label resolution via core `getMetricLabel`); charts with heavy transforms
supply a `transform` whose return value is merged into render props.
- **Escape hatches for incremental migration** — `additionalControls`,
`prependSections` / `middleSections` / `additionalSections`,
`controlOverrides`, `formDataOverrides`, `onInit`, and
`chartOptionsTabOverride` let complex charts keep hand-crafted sections while
adopting the pattern for everything else.
- **Reusable presets** (`presets.ts`) — `ShowLegend`, `LegendType`,
`LegendOrientation`, `HeaderFontSize`, `Subtitle`, etc. — shared,
pre-translated argument configurations that keep label strings and defaults
consistent across chart families.
- **Native Customize-tab rendering** — `GlyphOptionsPanel` renders glyph
arguments directly from formData (value + visibility), bypassing the string
expansion pipeline. The generated "Chart Options" section is identified by a
structural `_glyphChartOptions` marker (never by label matching). Controls
whose config requires `mapStateToProps` (e.g. ConditionalFormatting) and JSX
rows (sub-section headers) route through the existing legacy renderer for
full compatibility.
### Current Status (what has been done)
All work lives on `feat/glyph-single-file`. **Every chart family in the main
preset has been consolidated** to `defineChart()`:
- **plugin-chart-echarts**: Pie, Funnel, Gauge, Sankey, Waterfall, Histogram,
Tree, Bubble, BoxPlot, Sunburst, Radar, Treemap, Graph, Heatmap, Gantt,
BigNumber, BigNumberWithTrendline, BigNumberTotal, BigNumberPeriodOverPeriod,
Timeseries (Generic, Area, Bar, Line, Scatter, SmoothLine, Step),
MixedTimeseries
- **preset-chart-deckgl**: all 11 layers (Arc, Contour, Geojson, Grid, Heatmap,
Hex, Path, Polygon, Scatter, Screengrid, Multi)
- **legacy plugins**: calendar, horizon, chord, country-map, world-map,
paired-t-test, parallel-coordinates, partition, rose
- **legacy-preset-chart-nvd3**: Bubble, Bullet, Compare, TimePivot
- **others**: table, pivot-table, ag-grid-table (light-touch), word-cloud,
point-cluster-map, handlebars, cartodiagram
Net effect: **13,000 lines** (+60k/73k across 482 files).
**Test coverage added**: a 168-test suite spanning glyph-core unit tests
(arguments, defineChart, presets, crossFilter — including regression tests for
every audit finding below), `GlyphOptionsPanel` component tests, plugin-level
smoke tests for migrated charts, and a Playwright E2E test for the Customize
tab.
**A full-branch audit (2026-06-11)** ran a seven-angle review over the diff;
17 findings were confirmed and 14 fixed in-tree, notably:
- `behaviors` no longer defaults to `[Behavior.InteractiveChart]` — charts must
opt in, matching `ChartMetadata`'s own default (21 charts had acquired dead
cross-filter UI).
- The real `BigNumberTotalChartPlugin` is registered under
`big_number_total` again (a demo plugin had displaced it, breaking saved
charts' formats and subheaders).
- `@superset-ui/glyph-core` added to root `package.json` so webpack's
workspace-alias loop resolves live `src/` (fresh builds previously failed).
- Explore's initial-query effect is one-shot again — filling in the last
invalid control no longer auto-fires a query for any chart type.
- `ChartDefinition.metadata` can now express `queryObjectCount`,
`dynamicQueryObjectCount`, `parseMethod`, `suppressContextMenu`, and
`enableNoResults` (Mixed Chart's second Results tab returned).
- `disabledWhen` reads controls from the correct `mapStateToProps` argument
(the API was previously non-functional).
- Generated queries carry `timeGrain` on the x-axis column; generated metric
extraction uses `getMetricLabel` and no longer guesses among multiple numeric
columns.
- `NumberFormat`/`TimeFormat` options derive from chart-controls'
`D3_FORMAT_OPTIONS` (translated, with previews) instead of stale copies.
- Dead parallel implementation (`generators.ts`, 419 lines + tests + orphaned
types) deleted; condition evaluation deduplicated to a single function.
- `scripts/check-custom-rules.js` suppression matching is line-based (real
ESLint semantics) — one disable comment can no longer silence an entire
subtree; the leaks it had hidden were fixed with properly scoped disables.
Branch health: full-monorepo `tsc --noEmit` clean, all 24 packages build, all
tests and the complete pre-commit suite (mypy, ruff, pylint, oxlint, prettier,
frontend type-check, custom rules) pass.
### New or Changed Public Interfaces
- **New package** `@superset-ui/glyph-core` (workspace package; public API:
`defineChart`, argument classes, presets, `evaluateGlyphCondition`,
`getGlyphControlConfig`, `resolveArgClass`, cross-filter utilities).
- **`@superset-ui/chart-controls`**: `ControlPanelConfig._glyphArgs` and
`ControlPanelSectionConfig._glyphChartOptions``@internal` structural
markers consumed by explore.
- **`@superset-ui/core`**: `Behavior.AllowsEmptyResults` added to the
`Behavior` enum (no producer yet — see Remaining Work).
- **Explore**: new `GlyphOptionsPanel` component; `ControlPanelsContainer`,
`ExploreChartPanel`, `ExploreViewContainer`, `ChartRenderer`, and
`getSectionsToRender` learned the glyph path alongside the legacy one.
- **REST API**: `ChartDataExtrasSchema` accepts an optional
`allow_empty_query` boolean (`load_default=False`); `get_sqla_query` honors
it for validation while refusing to compile a zero-column SELECT.
- No new CLI surface, no deployment changes.
### New dependencies
None. `@superset-ui/glyph-core` is a new in-repo workspace package depending
only on existing workspace packages (`@superset-ui/core`,
`@superset-ui/chart-controls`). No new npm or PyPI dependencies.
### Migration Plan and Compatibility
- **No database migrations.** Saved charts keep their `viz_type` keys and
form data; `defineChart` plugins register under the same keys.
- **Form-data compatibility** is preserved through generated controls using the
same shared control names (`metric`, `groupby`, `adhoc_filters`, `x_axis`,
`time_grain_sqla`) and per-chart transforms reading legacy keys. One known
gap remains (camelCase arg keys vs. saved snake_case keys — see Remaining
Work item 2).
- **Incremental by design**: the legacy `controlPanel` pipeline still works
untouched; glyph charts coexist with non-migrated charts. Removal of legacy
controlPanel support is an explicit later phase, gated on the roadmap below.
### Remaining Work / Roadmap
Ordered by priority; items 13 should land before this branch merges or
immediately after.
1. **Restore code-splitting (perf regression).**
`loadChart: () => Promise.resolve(Component)` makes every migrated
renderer — including maplibre-gl (~800 KB) and deck.gl — eager in the
initial bundle, where master lazy-loaded them via
`loadChart: () => import('./Chart')`. Add lazy-render support to
`defineChart` (e.g. `render: () => import('./Render')` or a
`loadRender` field) and move heavy renderers back behind dynamic imports.
2. **Per-arg `formDataKey` aliases (saved-chart compatibility).**
Some migrated charts renamed keys (e.g. BigNumberTotal's `yAxisFormat` /
`subtitle` vs. saved `y_axis_format` / `subheader`). Rendering works via
camelCase conversion and transform fallbacks, but explore controls show
defaults for saved charts and write parallel keys. A
`formDataKey: 'y_axis_format'` alias on argument definitions fixes this
generally — and is the foundation for settings transfer between viz types
(arguments sharing a semantic key carry over on viz-type switch).
3. **Typed escape hatch for legacy transforms.**
~14 plugins (table, pivot-table, ag-grid, all deckgl layers,
point-cluster-map) use `transform: p => transformProps(p as any) as any`
plus `render: props => <Comp {...(props as any)} />`. This double-cast class
is what previously hid a silent render-props bug. Provide a
`defineChart<Props>` overload (typed transform fully replaces render props)
or a `wrapLegacyTransform(transformProps, Component)` helper so the next
shape mismatch is a compile error.
4. **Collapse the dual control-panel pipeline.**
`defineChart` currently emits a full legacy `ControlPanelConfig` *and*
native `_glyphArgs`; visibility/disabled/validation exist in both paths
with different data sources (Redux controls vs. formData). Make the native
definition canonical and derive the legacy config as a thin adapter — this
is the enabler for the planned consistent section/area rendering schema.
5. **Extract shared timeseries transform logic.**
The six Timeseries-family charts each inline ~1,200 lines of largely
duplicated transform code. Hoist a shared `timeseriesTransform` into
glyph-core (or a glyph-echarts helper) before the copies drift.
6. **Resolve the `allow_empty_query` plumbing.**
Backend accepts the flag and `Behavior.AllowsEmptyResults` exists, but
nothing produces either. Either wire the drag-and-drop empty-chart flow
end-to-end (derive the extras flag from chart metadata in one place) or
drop the speculative plumbing until the feature lands.
7. **Finish the migration and remove legacy controlPanel support** once 14
are in place, per the original plan: every plugin on `defineChart`, the
string-expansion pipeline (`expandControlConfig`, array-of-arrays sections)
deleted, and chart wrappers reduced to enable animation/realtime rendering.
### Rejected Alternatives
- **`createGlyphPlugin()` builder API** (a Map-based generator predating
`defineChart`): shipped briefly as `generators.ts`, never adopted by any
chart, drifted from the real implementation, and was deleted during the
audit in favor of the single `defineChart()` entry point.
- **JSON/YAML chart manifests**: a pure-data format can describe controls but
not transforms or renderers, forcing a second mechanism anyway; a typed
TypeScript API keeps definition, transform, and render in one
compiler-checked file.
- **Fixing the legacy controlPanel format in place** (e.g. typed control
references over the existing array-of-arrays): preserves the triplication of
knowledge across controlPanel/transformProps/component and the runtime
string-resolution layer that motivates this SIP.
- **Big-bang removal of the legacy pipeline**: rejected in favor of
coexistence + escape hatches; third-party plugins and complex charts need a
migration window.

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,621 @@
/**
* 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 {
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
} from '@superset-ui/chart-controls';
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 — derived from the canonical list in
// @superset-ui/chart-controls so glyph charts offer the same formats
// (with previews and translations) as legacy charts, and new formats
// propagate automatically.
static readonly FORMAT_OPTIONS: SelectOption[] = D3_FORMAT_OPTIONS.map(
([value, label]) => ({ value, label }),
);
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 — derived from the canonical list in
// @superset-ui/chart-controls (see NumberFormat.FORMAT_OPTIONS).
static readonly FORMAT_OPTIONS: SelectOption[] = D3_TIME_FORMAT_OPTIONS.map(
([value, label]) => ({ value, label }),
);
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,70 @@
/**
* 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 './defineChart';
export * from './presets';
export * from './crossFilter';

View File

@@ -0,0 +1,408 @@
/**
* 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({
// Strings match plugin-chart-echarts' legend controls so existing i18n
// catalogs apply and legend UX stays consistent across chart families.
label: t('Show legend'),
description: t('Whether to display a legend for the chart'),
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('Type'),
description: t('Legend type'),
options: LEGEND_TYPE_OPTIONS,
default: 'scroll',
});
/**
* Legend orientation selector
* Position the legend relative to the chart
*/
export const LegendOrientation = Select.with({
label: t('Orientation'),
description: t('Legend Orientation'),
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,257 @@
/**
* 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.
*/
/**
* 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',
}
/**
* 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)
>;

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,789 @@
/**
* 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 {
Bounds,
Checkbox,
ColorPicker,
defineChart,
Dimension,
evaluateGlyphCondition,
getArgVisibleWhen,
getGlyphControlConfig,
Metric,
RadioButton,
resolveArgClass,
Select,
Temporal,
Text,
} from '@superset-ui/glyph-core';
import type { ChartDefinition } from '@superset-ui/glyph-core/defineChart';
// 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 behaviors to [] when omitted (matching ChartMetadata)', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
// Charts must opt in to InteractiveChart — defaulting it on would enroll
// non-interactive charts in dashboard cross-filter UI and configuration.
expect(metadata.behaviors).toEqual([]);
});
test('metadata forwards queryObjectCount and related fields', () => {
const Plugin = defineChart({
metadata: {
name: 'Test',
thumbnail: MIN_THUMBNAIL,
queryObjectCount: 2,
suppressContextMenu: true,
enableNoResults: false,
},
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
expect(metadata.queryObjectCount).toBe(2);
expect(metadata.suppressContextMenu).toBe(true);
expect(metadata.enableNoResults).toBe(false);
});
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',
})) as unknown as ChartDefinition<
Record<string, never>
>['formDataOverrides'];
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 });
});
});
describe('defineChart - getGlyphControlConfig argument class coverage', () => {
test('RadioButton maps to RadioButtonControl (not SelectControl)', () => {
const Choice = RadioButton.with({
label: 'Mode',
default: 'a',
options: [
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
],
});
const config = getGlyphControlConfig(Choice, 'mode');
expect(config.type).toBe('RadioButtonControl');
expect(config.default).toBe('a');
expect(config.options).toEqual([
['a', 'A'],
['b', 'B'],
]);
});
test('ColorPicker keeps its RGBA object default (no hex coercion)', () => {
const red = { r: 255, g: 0, b: 0, a: 1 };
const Picker = ColorPicker.with({ label: 'Stroke', default: red });
const config = getGlyphControlConfig(Picker, 'stroke');
expect(config.type).toBe('ColorPickerControl');
expect(config.default).toEqual(red);
});
test('Bounds maps to BoundsControl (not TextControl)', () => {
const Range = Bounds.with({ label: 'Y bounds', default: [0, 100] });
const config = getGlyphControlConfig(Range, 'y_bounds');
expect(config.type).toBe('BoundsControl');
expect(config.default).toEqual([0, 100]);
});
test('Select honors clearable from the argument class', () => {
const config = getGlyphControlConfig(
Select.with({ label: 'S', options: [{ label: 'A', value: 'a' }] }),
's',
);
expect(config.clearable).toBe(false);
});
});
describe('defineChart - disabledWhen wiring', () => {
test('mapStateToProps reads controls from the state (first) argument', () => {
const Plugin = defineChart({
metadata: { name: 'D', thumbnail: MIN_THUMBNAIL },
arguments: {
subtitle: Text.with({ label: 'Subtitle', default: '' }),
subtitleFontSize: {
arg: Select.with({
label: 'Size',
default: 'm',
options: [{ label: 'M', value: 'm' }],
}),
disabledWhen: { subtitle: (val: unknown) => !val },
},
},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
controlSetRows: Array<
Array<{ name: string; config: Record<string, Function> }>
>;
}>;
const control = sections
.flatMap(s => s.controlSetRows)
.flat()
.find(c => c?.name === 'subtitleFontSize');
expect(control).toBeDefined();
const { mapStateToProps, shouldMapStateToProps } = control!.config;
expect(shouldMapStateToProps()).toBe(true);
// Both real call sites pass the controls-bearing state as the FIRST arg
const disabledResult = mapStateToProps(
{ controls: { subtitle: { value: '' } } },
{ value: 'm' },
);
expect(disabledResult.disabled).toBe(true);
const enabledResult = mapStateToProps(
{ controls: { subtitle: { value: 'hello' } } },
{ value: 'm' },
);
expect(enabledResult.disabled).toBe(false);
});
});
describe('defineChart - generated buildQuery temporal axis', () => {
test('x_axis column is normalized to BASE_AXIS with timeGrain', async () => {
const Plugin = defineChart({
metadata: { name: 'T', thumbnail: MIN_THUMBNAIL },
arguments: { time: Temporal, metric: Metric },
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const p = new Plugin();
const loader = (p as unknown as { loadBuildQuery: () => Promise<Function> })
.loadBuildQuery;
const buildQuery = await loader();
const queryContext = buildQuery({
datasource: '1__table',
viz_type: 'test',
x_axis: 'ds',
time_grain_sqla: 'P1D',
metrics: ['count'],
});
const axisColumn = queryContext.queries[0].columns.find(
(col: { columnType?: string }) =>
typeof col === 'object' && col?.columnType === 'BASE_AXIS',
);
expect(axisColumn).toBeDefined();
expect(axisColumn.sqlExpression).toBe('ds');
// The grain selected in the control panel must reach the query
expect(axisColumn.timeGrain).toBe('P1D');
});
});
describe('defineChart - generated metric extraction', () => {
async function getTransformProps(
PluginClass: ReturnType<typeof defineChart>,
) {
const p = new PluginClass();
const loader = (
p as unknown as { loadTransformProps: () => Promise<Function> }
).loadTransformProps;
return loader();
}
const MetricPlugin = defineChart({
metadata: { name: 'M', thumbnail: MIN_THUMBNAIL },
arguments: { metric: Metric },
render: () => null as unknown as React.ReactElement,
});
test('label-less SQL adhoc metrics resolve via sqlExpression', async () => {
const transformProps = await getTransformProps(MetricPlugin);
const props = transformProps({
width: 1,
height: 1,
formData: {
metric: { expressionType: 'SQL', sqlExpression: 'SUM(x)/SUM(y)' },
},
queriesData: [{ data: [{ 'SUM(x)/SUM(y)': 42 }] }],
});
expect(props.metric.name).toBe('SUM(x)/SUM(y)');
expect(props.metric.value).toBe(42);
});
test('does NOT guess among multiple numeric columns on label miss', async () => {
const transformProps = await getTransformProps(MetricPlugin);
const props = transformProps({
width: 1,
height: 1,
formData: { metric: 'count' },
queriesData: [{ data: [{ year: 2024, total: 9 }] }],
});
// Guessing could render a numeric dimension (e.g. a year) as the metric
expect(props.metric.value).toBeUndefined();
});
test('falls back to the only numeric column when unambiguous', async () => {
const transformProps = await getTransformProps(MetricPlugin);
const props = transformProps({
width: 1,
height: 1,
formData: { metric: 'count' },
queriesData: [{ data: [{ name: 'a', total: 9 }] }],
});
expect(props.metric.value).toBe(9);
expect(props.metric.name).toBe('total');
});
});
describe('defineChart - Chart Options section marker', () => {
test('the generated section carries the structural _glyphChartOptions flag', () => {
const Plugin = defineChart({
metadata: { name: 'S', thumbnail: MIN_THUMBNAIL },
arguments: { title: Text.with({ label: 'Title', default: '' }) },
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { controlPanel } = instantiate(Plugin);
const sections = controlPanel.controlPanelSections as Array<{
label?: string;
_glyphChartOptions?: boolean;
}>;
const chartOptions = sections.find(s => s?._glyphChartOptions);
expect(chartOptions).toBeDefined();
expect(chartOptions?.label).toBe('Chart Options');
});
});

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

@@ -0,0 +1,91 @@
/**
* 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.
*/
/**
* Smoke test: Glyph-defined chart's Customize tab renders via GlyphOptionsPanel.
*
* Glyph charts (those built with defineChart) replace the legacy controlPanel.ts
* file with declarative arg definitions. Their Customize tab is rendered by
* GlyphOptionsPanel (a native React renderer) instead of the legacy
* ControlSetRow pipeline. This test verifies that path actually works end-to-end:
* open the Pie chart explore page, switch to Customize, and confirm a known
* glyph control (Show legend) is rendered.
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import { ExplorePage } from '../../pages/ExplorePage';
import { apiPostChart } from '../../helpers/api/chart';
import { getDatasetByName } from '../../helpers/api/dataset';
const test = testWithAssets;
test('Pie chart Customize tab renders glyph controls', async ({
page,
testAssets,
}) => {
// 1. Create a real Pie chart via API so we have something to load.
const dataset = await getDatasetByName(page, 'members_channels_2');
if (!dataset) {
throw new Error(
'members_channels_2 dataset not found — run Superset with --load-examples',
);
}
const name = `glyph_pie_${Date.now()}_${test.info().parallelIndex}`;
const response = await apiPostChart(page, {
slice_name: name,
datasource_id: dataset.id,
datasource_type: 'table',
viz_type: 'pie',
params: JSON.stringify({
viz_type: 'pie',
groupby: ['channel'],
metric: {
aggregate: 'COUNT',
column: null,
expressionType: 'SIMPLE',
label: 'count',
},
adhoc_filters: [],
row_limit: 100,
show_legend: true,
}),
});
if (!response.ok()) {
throw new Error(`Failed to create Pie chart: ${response.status()}`);
}
const body = await response.json();
const id = body.result?.id ?? body.id;
testAssets.trackChart(id);
// 2. Open the Explore page for this chart.
await page.goto(`/explore/?slice_id=${id}`);
const explore = new ExplorePage(page);
await explore.waitForPageLoad();
// 3. Switch to the Customize tab. This is what triggers GlyphOptionsPanel
// to render — without it we'd be looking at the Data tab.
await page.getByRole('tab', { name: 'Customize' }).click();
// 4. The Customize tab should show at least one Chart Options collapse header.
// Both the section label and a known glyph arg (Show legend) belong to Pie,
// so we assert on the arg label to prove the native glyph renderer ran.
await expect(page.getByText('Show legend').first()).toBeVisible();
});

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.3.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.3.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.3.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.3.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.3.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.3.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

@@ -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 } 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'),
description: t(
'A polar coordinate chart where the circle is broken into wedges of equal angle, and the value represented by any wedge is illustrated by its area, rather than its radius or sweep angle.',
),
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
],
name: t('Nightingale Rose Chart'),
tags: [
t('Legacy'),
t('Advanced-Analytics'),
t('Circular'),
t('Multi-Layers'),
t('Pattern'),
t('Time'),
t('Trend'),
],
thumbnail,
thumbnailDark,
useLegacyApi: true,
});
export default class RoseChartPlugin extends ChartPlugin {
constructor() {
super({
loadChart: () => import('./ReactRose'),
metadata,
transformProps,
controlPanel,
});
}
}

View File

@@ -17,8 +17,8 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { ChartProps } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlSubSectionHeader,
D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS,
@@ -26,93 +26,127 @@ import {
sections,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import ReactRose from './ReactRose';
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';
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
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'),
},
type RoseExtra = {
colorScheme: string;
dateTimeFormat: string;
numberFormat: string;
useAreaProportions: boolean;
useRichTooltip: boolean;
sliceId: number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<any, RoseExtra>({
metadata: {
category: t('Ranking'),
description: t(
'A polar coordinate chart where the circle is broken into wedges of equal angle, and the value represented by any wedge is illustrated by its area, rather than its radius or sweep angle.',
),
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
],
name: t('Nightingale Rose Chart'),
tags: [
t('Legacy'),
t('Advanced-Analytics'),
t('Circular'),
t('Multi-Layers'),
t('Pattern'),
t('Time'),
t('Trend'),
],
thumbnail,
thumbnailDark,
useLegacyApi: true,
},
arguments: {},
prependSections: [sections.legacyTimeseriesTime],
additionalControls: {
queryBefore: [['metrics'], ['adhoc_filters'], ['groupby']],
query: [
['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: [
['color_scheme'],
[
{
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,
},
['row_limit', null],
],
chartOptions: [
['color_scheme'],
[
{
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,
},
{
name: 'date_time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Date Time Format'),
renderTrigger: true,
default: 'smart_date',
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
},
{
name: 'date_time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Date Time Format'),
renderTrigger: true,
default: 'smart_date',
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
],
[
{
name: 'rich_tooltip',
config: {
type: 'CheckboxControl',
label: t('Rich Tooltip'),
renderTrigger: true,
default: true,
description: t(
'The rich tooltip shows a list of all series for that point in time',
),
},
},
{
name: 'rose_area_proportion',
config: {
type: 'CheckboxControl',
label: t('Use Area Proportions'),
description: t(
'Check if the Rose Chart should use segment area instead of ' +
'segment radius for proportioning',
),
default: false,
renderTrigger: true,
},
},
],
},
],
},
[
{
name: 'rich_tooltip',
config: {
type: 'CheckboxControl',
label: t('Rich Tooltip'),
renderTrigger: true,
default: true,
description: t(
'The rich tooltip shows a list of all series for that point in time',
),
},
},
{
name: 'rose_area_proportion',
config: {
type: 'CheckboxControl',
label: t('Use Area Proportions'),
description: t(
'Check if the Rose Chart should use segment area instead of ' +
'segment radius for proportioning',
),
default: false,
renderTrigger: true,
},
},
],
],
},
additionalSections: [
{
label: t('Advanced Analytics'),
tabOverride: 'data',
@@ -232,6 +266,7 @@ const config: ControlPanelConfig = {
},
},
],
// eslint-disable-next-line react/jsx-key
[<ControlSubSectionHeader>{t('Resample')}</ControlSubSectionHeader>],
[
{
@@ -279,6 +314,50 @@ const config: ControlPanelConfig = {
groupby: getStandardizedControls().popAllColumns(),
metrics: getStandardizedControls().popAllMetrics(),
}),
};
transform: (chartProps: ChartProps) => {
const { width, height, formData, queriesData } = chartProps;
const {
colorScheme,
dateTimeFormat,
numberFormat,
richTooltip,
roseAreaProportion,
sliceId,
} = formData as Record<string, unknown>;
export default config;
return {
width,
height,
data: queriesData[0].data,
colorScheme: (colorScheme as string) ?? '',
dateTimeFormat: (dateTimeFormat as string) ?? 'smart_date',
numberFormat: (numberFormat as string) ?? 'SMART_NUMBER',
useAreaProportions: (roseAreaProportion as boolean) ?? false,
useRichTooltip: (richTooltip as boolean) ?? true,
sliceId: (sliceId as number) ?? 0,
};
},
render: ({
width,
height,
data,
colorScheme,
dateTimeFormat,
numberFormat,
useAreaProportions,
useRichTooltip,
sliceId,
}) => (
<ReactRose
width={width}
height={height}
data={data}
colorScheme={colorScheme}
dateTimeFormat={dateTimeFormat}
numberFormat={numberFormat}
useAreaProportions={useAreaProportions}
useRichTooltip={useRichTooltip}
sliceId={sliceId}
/>
),
});

View File

@@ -1,43 +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 {
colorScheme,
dateTimeFormat,
numberFormat,
richTooltip,
roseAreaProportion,
sliceId,
} = formData;
return {
width,
height,
data: queriesData[0].data,
colorScheme,
dateTimeFormat,
numberFormat,
useAreaProportions: roseAreaProportion,
useRichTooltip: richTooltip,
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

@@ -38,6 +38,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.3.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.21",
"react": "^18.3.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

@@ -1,87 +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 { Behavior, 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/Table.jpg';
import example1Dark from './images/Table-dark.jpg';
import example2 from './images/Table2.jpg';
import example2Dark from './images/Table2-dark.jpg';
import example3 from './images/Table3.jpg';
import example3Dark from './images/Table3-dark.jpg';
import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import { TableChartFormData, TableChartProps } from './types';
// must export something for the module to be exist in dev mode
export { default as __hack__ } from './types';
export * from './types';
export {
convertAgGridStateToOwnState,
convertSortModel,
convertColumnState,
convertFilterModel,
} from './stateConversion';
const metadata = new ChartMetadata({
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
category: t('Table'),
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
description: t(
'Classic row-by-column spreadsheet like view of a dataset. Use tables to showcase a view into the underlying data or to show aggregated metrics.',
),
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
{ url: example3, urlDark: example3Dark },
],
name: t('Table V2'),
tags: [
t('Additive'),
t('Business'),
t('Pattern'),
t('Featured'),
t('Report'),
t('Sequential'),
t('Tabular'),
],
thumbnail,
thumbnailDark,
});
export default class AgGridTableChartPlugin extends ChartPlugin<
TableChartFormData,
TableChartProps
> {
constructor() {
super({
loadChart: () => import('./AgGridTableChart'),
metadata,
transformProps,
controlPanel,
buildQuery,
});
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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 } from '@superset-ui/core';
import { ControlPanelSectionConfig } from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import AgGridTableChart from './AgGridTableChart';
import transformProps from './transformProps';
import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import { TableChartProps } from './types';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/Table.jpg';
import example1Dark from './images/Table-dark.jpg';
import example2 from './images/Table2.jpg';
import example2Dark from './images/Table2-dark.jpg';
import example3 from './images/Table3.jpg';
import example3Dark from './images/Table3-dark.jpg';
// must export something for the module to be exist in dev mode
export { default as __hack__ } from './types';
export * from './types';
export {
convertAgGridStateToOwnState,
convertSortModel,
convertColumnState,
convertFilterModel,
} from './stateConversion';
const AgGridTableChartPlugin = defineChart<Record<string, never>, TableChartProps>({
metadata: {
name: t('Table V2'),
description: t(
'Classic row-by-column spreadsheet like view of a dataset. Use tables to showcase a view into the underlying data or to show aggregated metrics.',
),
category: t('Table'),
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
tags: [
t('Additive'),
t('Business'),
t('Pattern'),
t('Featured'),
t('Report'),
t('Sequential'),
t('Tabular'),
],
thumbnail,
thumbnailDark,
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
{ url: example3, urlDark: example3Dark },
],
},
arguments: {},
suppressQuerySection: true,
prependSections: (controlPanel.controlPanelSections?.filter(
(s): s is ControlPanelSectionConfig => s !== null && s !== undefined,
) ?? []) as ControlPanelSectionConfig[],
additionalControlOverrides: controlPanel.controlOverrides as Record<
string,
Record<string, unknown>
>,
formDataOverrides: controlPanel.formDataOverrides,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
buildQuery: (formData: any) => buildQuery(formData),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transform: chartProps => transformProps(chartProps as any) as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render: props => <AgGridTableChart {...(props as any)} />,
});
export { AgGridTableChartPlugin };
export default AgGridTableChartPlugin;

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;

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