mirror of
https://github.com/apache/superset.git
synced 2026-05-20 07:15:17 +00:00
Compare commits
23 Commits
enxdev/fea
...
feat/glyph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b7b8cdea | ||
|
|
2908674e1f | ||
|
|
86f2f056d2 | ||
|
|
79eb91da96 | ||
|
|
96be1ccb2d | ||
|
|
f63e369687 | ||
|
|
67231d194e | ||
|
|
816583b602 | ||
|
|
49b9afa591 | ||
|
|
8efda4f704 | ||
|
|
12ebe0f032 | ||
|
|
857ba7f855 | ||
|
|
dd1226f8e5 | ||
|
|
46555d12cb | ||
|
|
5b0d895322 | ||
|
|
1eae7b4ee5 | ||
|
|
6a054f9170 | ||
|
|
865b75c90e | ||
|
|
6fc18b38fa | ||
|
|
3f61764543 | ||
|
|
9dad9cbfc2 | ||
|
|
b2c58a0856 | ||
|
|
2e16b8266a |
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"a0289491-ebb9-4d03-aa1d-023b0219c585","pid":48965,"procStart":"Fri May 1 19:01:07 2026","acquiredAt":1778687635094}
|
||||
@@ -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}"
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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',
|
||||
|
||||
10959
superset-frontend/package-lock.json
generated
10959
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -109,7 +109,9 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/fira-code": "^5.2.7",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@googleapis/sheets": "^13.0.1",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
@@ -199,6 +201,7 @@
|
||||
"nanoid": "^5.1.11",
|
||||
"ol": "^10.9.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -441,6 +441,8 @@ export interface ControlPanelConfig {
|
||||
sectionOverrides?: SectionOverrides;
|
||||
onInit?: (state: ControlStateMapping) => void;
|
||||
formDataOverrides?: (formData: QueryFormData) => QueryFormData;
|
||||
/** @internal Raw glyph argument definitions from defineChart() – used for native control panel rendering */
|
||||
_glyphArgs?: unknown;
|
||||
}
|
||||
|
||||
export type ControlOverrides = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ColumnType,
|
||||
SelectOptions,
|
||||
SelectOption,
|
||||
TextOptions,
|
||||
CheckboxOptions,
|
||||
IntOptions,
|
||||
ColorOptions,
|
||||
MetricOptions,
|
||||
DimensionOptions,
|
||||
NumberFormatOptions,
|
||||
CurrencyOptions,
|
||||
CurrencyValue,
|
||||
TimeFormatOptions,
|
||||
ConditionalFormattingOptions,
|
||||
ConditionalFormattingRule,
|
||||
SliderOptions,
|
||||
BoundsOptions,
|
||||
BoundsValue,
|
||||
ColorPickerOptions,
|
||||
RadioButtonOptions,
|
||||
RadioOption,
|
||||
RgbaColor,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Base Argument class - all argument types extend from this.
|
||||
*
|
||||
* Arguments define:
|
||||
* 1. What the chart needs (semantically)
|
||||
* 2. How to render controls in the control panel
|
||||
* 3. Default values and validation
|
||||
*/
|
||||
export class Argument {
|
||||
static label: string | null = null;
|
||||
static description: string | null = null;
|
||||
static columnType: ColumnType = ColumnType.Argument;
|
||||
static controlType: string = 'TextControl';
|
||||
|
||||
value: unknown;
|
||||
|
||||
constructor(value: unknown) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric - represents a numeric aggregation (SUM, COUNT, AVG, etc.)
|
||||
*
|
||||
* Maps to Superset's MetricsControl in the query section.
|
||||
*/
|
||||
export class Metric extends Argument {
|
||||
static override label: string | null = 'Metric';
|
||||
static override description: string | null =
|
||||
'A numeric aggregation (SUM, COUNT, AVG, etc.)';
|
||||
static override columnType = ColumnType.Metric;
|
||||
static override controlType = 'MetricsControl';
|
||||
static multi = false;
|
||||
|
||||
static with(options: MetricOptions): typeof Metric {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimension - represents a categorical column for grouping data
|
||||
*
|
||||
* Maps to Superset's GroupByControl in the query section.
|
||||
*/
|
||||
export class Dimension extends Argument {
|
||||
static override label: string | null = 'Dimension';
|
||||
static override description: string | null =
|
||||
'A categorical column for grouping data';
|
||||
static override columnType = ColumnType.Dimension;
|
||||
static override controlType = 'GroupByControl';
|
||||
static multi = true;
|
||||
|
||||
static with(options: DimensionOptions): typeof Dimension {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporal - represents a time column
|
||||
*
|
||||
* Maps to Superset's temporal controls (x_axis, time_grain_sqla).
|
||||
*/
|
||||
export class Temporal extends Argument {
|
||||
static override label: string | null = 'Time Column';
|
||||
static override description: string | null =
|
||||
'A temporal column for time series data';
|
||||
static override columnType = ColumnType.Temporal;
|
||||
static override controlType = 'TemporalControl';
|
||||
|
||||
static with(options: {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}): typeof Temporal {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select - dropdown selection from predefined options
|
||||
*
|
||||
* Maps to Superset's SelectControl.
|
||||
*/
|
||||
export class Select extends Argument {
|
||||
static override label: string | null = 'Select';
|
||||
static override description: string | null = 'Choose from options';
|
||||
static override controlType = 'SelectControl';
|
||||
static default: string | number = '';
|
||||
static options: SelectOption[] = [];
|
||||
static clearable = false;
|
||||
|
||||
static with(options: SelectOptions): typeof Select {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options ?? Base.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text - free-form text input
|
||||
*
|
||||
* Maps to Superset's TextControl.
|
||||
*/
|
||||
export class Text extends Argument {
|
||||
static override label: string | null = 'Text';
|
||||
static override description: string | null = 'Text input';
|
||||
static override controlType = 'TextControl';
|
||||
static default: string = '';
|
||||
static placeholder: string = '';
|
||||
|
||||
static with(options: TextOptions): typeof Text {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override placeholder = options.placeholder ?? Base.placeholder;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox - boolean toggle
|
||||
*
|
||||
* Maps to Superset's CheckboxControl.
|
||||
*/
|
||||
export class Checkbox extends Argument {
|
||||
static override label: string | null = 'Checkbox';
|
||||
static override description: string | null = 'Toggle option';
|
||||
static override controlType = 'CheckboxControl';
|
||||
static default: boolean = false;
|
||||
|
||||
static with(options: CheckboxOptions): typeof Checkbox {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Int - numeric input with slider
|
||||
*
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Int extends Argument {
|
||||
static override label: string | null = 'Integer';
|
||||
static override description: string | null = 'A numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 100;
|
||||
static step: number = 1;
|
||||
|
||||
static with(options: IntOptions): typeof Int {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color - color picker
|
||||
*
|
||||
* Maps to Superset's ColorPickerControl.
|
||||
*/
|
||||
export class Color extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'A color value';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
static default: string = '#000000';
|
||||
|
||||
static with(options: ColorOptions): typeof Color {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NumberFormat - D3 number format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class NumberFormat extends Argument {
|
||||
static override label: string | null = 'Number Format';
|
||||
static override description: string | null =
|
||||
'D3 format string for number display (e.g., ".2f", ".1%", ",.0f")';
|
||||
static override controlType = 'NumberFormatControl';
|
||||
static default: string = 'SMART_NUMBER';
|
||||
|
||||
// Standard D3 format options
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = [
|
||||
{ label: 'Adaptive formatting', value: 'SMART_NUMBER' },
|
||||
{ label: 'Original value', value: '~g' },
|
||||
{ label: '12,345.432', value: ',.3f' },
|
||||
{ label: '12,345.43', value: ',.2f' },
|
||||
{ label: '12,345.4', value: ',.1f' },
|
||||
{ label: '12,345', value: ',.0f' },
|
||||
{ label: '12345.432', value: '.3f' },
|
||||
{ label: '12345.43', value: '.2f' },
|
||||
{ label: '12345.4', value: '.1f' },
|
||||
{ label: '12345', value: '.0f' },
|
||||
{ label: '12K', value: '.0s' },
|
||||
{ label: '12.3K', value: '.1s' },
|
||||
{ label: '12.35K', value: '.2s' },
|
||||
{ label: '12.346K', value: '.3s' },
|
||||
{ label: '1234543.21%', value: '.2%' },
|
||||
{ label: '1234543%', value: '.0%' },
|
||||
{ label: '12.34%', value: '.2r' },
|
||||
{ label: '+12,345.4', value: '+,.1f' },
|
||||
{ label: '$12,345.43', value: '$,.2f' },
|
||||
{ label: 'Duration (1m 6s)', value: 'DURATION' },
|
||||
{ label: 'Duration (1ms 400µs)', value: 'DURATION_SUB' },
|
||||
];
|
||||
|
||||
static with(options: NumberFormatOptions): typeof NumberFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency - currency format with symbol and position
|
||||
*
|
||||
* Maps to Superset's CurrencyControl.
|
||||
* Value is { symbol: 'USD', symbolPosition: 'prefix' | 'suffix' }
|
||||
*/
|
||||
export class Currency extends Argument {
|
||||
static override label: string | null = 'Currency Format';
|
||||
static override description: string | null =
|
||||
'Currency symbol and position for formatting';
|
||||
static override controlType = 'CurrencyControl';
|
||||
static default: CurrencyValue = {};
|
||||
|
||||
static with(options: CurrencyOptions): typeof Currency {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TimeFormat - D3 time format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 time format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class TimeFormat extends Argument {
|
||||
static override label: string | null = 'Time Format';
|
||||
static override description: string | null =
|
||||
'D3 time format string (e.g., "%Y-%m-%d", "%H:%M:%S")';
|
||||
static override controlType = 'TimeFormatControl';
|
||||
static default: string = 'smart_date';
|
||||
|
||||
// Standard D3 time format options
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = [
|
||||
{ label: 'Adaptive formatting', value: 'smart_date' },
|
||||
{ label: '%d/%m/%Y | 14/01/2019', value: '%d/%m/%Y' },
|
||||
{ label: '%m/%d/%Y | 01/14/2019', value: '%m/%d/%Y' },
|
||||
{ label: '%d.%m.%Y | 14.01.2019', value: '%d.%m.%Y' },
|
||||
{ label: '%Y-%m-%d | 2019-01-14', value: '%Y-%m-%d' },
|
||||
{
|
||||
label: '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10',
|
||||
value: '%Y-%m-%d %H:%M:%S',
|
||||
},
|
||||
{
|
||||
label: '%d-%m-%Y %H:%M:%S | 14-01-2019 01:32:10',
|
||||
value: '%d-%m-%Y %H:%M:%S',
|
||||
},
|
||||
{ label: '%H:%M:%S | 01:32:10', value: '%H:%M:%S' },
|
||||
];
|
||||
|
||||
static with(options: TimeFormatOptions): typeof TimeFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ConditionalFormatting - apply color rules based on metric values
|
||||
*
|
||||
* This is a special argument type that encapsulates the complex
|
||||
* mapStateToProps logic needed for conditional formatting controls.
|
||||
* The control automatically receives numeric column options from the chart response.
|
||||
*/
|
||||
export class ConditionalFormatting extends Argument {
|
||||
static override label: string | null = 'Conditional Formatting';
|
||||
static override description: string | null =
|
||||
'Apply conditional color formatting to metric values';
|
||||
static override controlType = 'ConditionalFormattingControl';
|
||||
static default: ConditionalFormattingRule[] = [];
|
||||
|
||||
static with(
|
||||
options: ConditionalFormattingOptions,
|
||||
): typeof ConditionalFormatting {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider - continuous floating point values with min/max/step
|
||||
*
|
||||
* Similar to Int but for float values.
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Slider extends Argument {
|
||||
static override label: string | null = 'Slider';
|
||||
static override description: string | null = 'A continuous numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 1;
|
||||
static step: number = 0.1;
|
||||
|
||||
static with(options: SliderOptions): typeof Slider {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds - min/max value pairs
|
||||
*
|
||||
* Used for axis bounds, value ranges, etc.
|
||||
* Maps to Superset's BoundsControl.
|
||||
*/
|
||||
export class Bounds extends Argument {
|
||||
static override label: string | null = 'Bounds';
|
||||
static override description: string | null = 'Min and max value bounds';
|
||||
static override controlType = 'BoundsControl';
|
||||
static default: BoundsValue = [null, null];
|
||||
|
||||
static with(options: BoundsOptions): typeof Bounds {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ColorPicker - RGBA color selection
|
||||
*
|
||||
* Different from Color (which uses hex strings).
|
||||
* Maps to Superset's ColorPickerControl with RGBA format.
|
||||
*/
|
||||
export class ColorPicker extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'Select a color';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
static default: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
|
||||
|
||||
static with(options: ColorPickerOptions): typeof ColorPicker {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RadioButton - mutually exclusive options
|
||||
*
|
||||
* Use for small sets of exclusive choices (2-4 options).
|
||||
* Maps to Superset's RadioButtonControl.
|
||||
*/
|
||||
export class RadioButton extends Argument {
|
||||
static override label: string | null = 'Option';
|
||||
static override description: string | null = 'Select one option';
|
||||
static override controlType = 'RadioButtonControl';
|
||||
static default: string | boolean = '';
|
||||
static options: RadioOption[] = [];
|
||||
|
||||
static with(options: RadioButtonOptions): typeof RadioButton {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ConditionalFormatting type
|
||||
*/
|
||||
export function isConditionalFormattingArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ConditionalFormatting {
|
||||
return argClass.controlType === 'ConditionalFormattingControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a TimeFormat type
|
||||
*/
|
||||
export function isTimeFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof TimeFormat {
|
||||
return argClass.controlType === 'TimeFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a NumberFormat type
|
||||
*/
|
||||
export function isNumberFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof NumberFormat {
|
||||
return argClass.controlType === 'NumberFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Currency type
|
||||
*/
|
||||
export function isCurrencyArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Currency {
|
||||
return argClass.controlType === 'CurrencyControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Select type
|
||||
*/
|
||||
export function isSelectArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Select {
|
||||
return (
|
||||
'options' in argClass && Array.isArray((argClass as typeof Select).options)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Checkbox type
|
||||
*/
|
||||
export function isCheckboxArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Checkbox {
|
||||
return (
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof Checkbox).default === 'boolean'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Text type
|
||||
*/
|
||||
export function isTextArg(argClass: typeof Argument): argClass is typeof Text {
|
||||
return (
|
||||
argClass.controlType === 'TextControl' ||
|
||||
(argClass.prototype instanceof Text &&
|
||||
!isSelectArg(argClass) &&
|
||||
!isCheckboxArg(argClass))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is an Int type
|
||||
*/
|
||||
export function isIntArg(argClass: typeof Argument): argClass is typeof Int {
|
||||
return 'min' in argClass && 'max' in argClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Color type
|
||||
*/
|
||||
export function isColorArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Color {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' ||
|
||||
argClass.prototype instanceof Color
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Metric type
|
||||
*/
|
||||
export function isMetricArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Metric {
|
||||
return argClass.columnType === ColumnType.Metric;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Dimension type
|
||||
*/
|
||||
export function isDimensionArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Dimension {
|
||||
return argClass.columnType === ColumnType.Dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Temporal type
|
||||
*/
|
||||
export function isTemporalArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Temporal {
|
||||
return argClass.columnType === ColumnType.Temporal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Slider type
|
||||
*/
|
||||
export function isSliderArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Slider {
|
||||
return (
|
||||
argClass.controlType === 'SliderControl' &&
|
||||
'step' in argClass &&
|
||||
typeof (argClass as typeof Slider).step === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Bounds type
|
||||
*/
|
||||
export function isBoundsArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Bounds {
|
||||
return argClass.controlType === 'BoundsControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ColorPicker type
|
||||
*/
|
||||
export function isColorPickerArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ColorPicker {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' &&
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof ColorPicker).default === 'object' &&
|
||||
'r' in ((argClass as typeof ColorPicker).default as object)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a RadioButton type
|
||||
*/
|
||||
export function isRadioButtonArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof RadioButton {
|
||||
return argClass.controlType === 'RadioButtonControl';
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import type {
|
||||
ControlPanelConfig,
|
||||
ControlSetRow,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
Argument,
|
||||
Select,
|
||||
Text,
|
||||
Checkbox,
|
||||
Int,
|
||||
Color,
|
||||
isSelectArg,
|
||||
isCheckboxArg,
|
||||
isIntArg,
|
||||
isColorArg,
|
||||
isMetricArg,
|
||||
isDimensionArg,
|
||||
isTemporalArg,
|
||||
} from './arguments';
|
||||
import type { VisibilityFn, RgbaColor } from './types';
|
||||
|
||||
/**
|
||||
* Configuration for a glyph argument with optional visibility control
|
||||
*/
|
||||
export interface GlyphArgConfig {
|
||||
arg: typeof Argument;
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments map - parameter name to argument class or config
|
||||
*/
|
||||
export type GlyphArguments = Map<string, typeof Argument | GlyphArgConfig>;
|
||||
|
||||
/**
|
||||
* Convert hex color string to RGBA object for Superset's ColorPickerControl
|
||||
*/
|
||||
function hexToRgba(hex: string): RgbaColor {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (result && result[1] && result[2] && result[3]) {
|
||||
return {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
a: 1,
|
||||
};
|
||||
}
|
||||
return { r: 0, g: 0, b: 0, a: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGBA object to hex color string
|
||||
*/
|
||||
function rgbaToHex(rgba: RgbaColor): string {
|
||||
const toHex = (n: number) => n.toString(16).padStart(2, '0');
|
||||
return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument class from a config (handles both direct class and config object)
|
||||
*/
|
||||
function getArgClass(
|
||||
argOrConfig: typeof Argument | GlyphArgConfig,
|
||||
): typeof Argument {
|
||||
return 'arg' in argOrConfig ? argOrConfig.arg : argOrConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visibility config if present
|
||||
*/
|
||||
function getVisibilityConfig(argOrConfig: typeof Argument | GlyphArgConfig): {
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
} {
|
||||
if ('arg' in argOrConfig) {
|
||||
return {
|
||||
visibility: argOrConfig.visibility,
|
||||
resetOnHide: argOrConfig.resetOnHide,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Superset control config from a glyph Argument class
|
||||
*/
|
||||
export function getControlConfig(
|
||||
argClass: typeof Argument,
|
||||
paramName: string,
|
||||
): Record<string, unknown> & { type: string } {
|
||||
const label = argClass.label || paramName;
|
||||
const description = argClass.description || '';
|
||||
|
||||
// Select control
|
||||
if (isSelectArg(argClass)) {
|
||||
return {
|
||||
type: 'SelectControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
options: argClass.options,
|
||||
clearable: argClass.clearable ?? false,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Checkbox control
|
||||
if (isCheckboxArg(argClass)) {
|
||||
return {
|
||||
type: 'CheckboxControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Int/Slider control
|
||||
if (isIntArg(argClass)) {
|
||||
return {
|
||||
type: 'SliderControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
min: argClass.min,
|
||||
max: argClass.max,
|
||||
step: argClass.step ?? 1,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Color control
|
||||
if (isColorArg(argClass)) {
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
const hexDefault = argClass.default ?? '#000000';
|
||||
return {
|
||||
type: 'ColorPickerControl',
|
||||
label,
|
||||
description,
|
||||
default: hexToRgba(hexDefault),
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Default to TextControl
|
||||
const textClass = argClass as typeof Text;
|
||||
return {
|
||||
type: 'TextControl',
|
||||
label,
|
||||
description,
|
||||
default: textClass.default ?? '',
|
||||
placeholder: textClass.placeholder ?? '',
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for control panel generation
|
||||
*/
|
||||
export interface ControlPanelOptions {
|
||||
/** Additional control rows for the query section */
|
||||
queryControls?: ControlSetRow[];
|
||||
/** Additional control rows for the chart options section */
|
||||
chartOptionsControls?: ControlSetRow[];
|
||||
/** Control overrides */
|
||||
controlOverrides?: Record<string, Record<string, unknown>>;
|
||||
/** Form data overrides function */
|
||||
formDataOverrides?: (
|
||||
formData: Record<string, unknown>,
|
||||
) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete ControlPanelConfig from glyph arguments
|
||||
*
|
||||
* This is the core function that converts semantic argument definitions
|
||||
* into Superset's control panel format.
|
||||
*/
|
||||
export function generateControlPanel(
|
||||
glyphArguments: GlyphArguments,
|
||||
options: ControlPanelOptions = {},
|
||||
): ControlPanelConfig {
|
||||
const queryControls: ControlSetRow[] = [];
|
||||
const chartOptionsControls: ControlSetRow[] = [];
|
||||
|
||||
// Process each argument
|
||||
for (const [paramName, argOrConfig] of glyphArguments) {
|
||||
const argClass = getArgClass(argOrConfig);
|
||||
const { visibility, resetOnHide } = getVisibilityConfig(argOrConfig);
|
||||
|
||||
// Data arguments go in Query section
|
||||
if (isMetricArg(argClass)) {
|
||||
queryControls.push(['metric']);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDimensionArg(argClass)) {
|
||||
queryControls.push(['groupby']);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTemporalArg(argClass)) {
|
||||
queryControls.push(['x_axis'], ['time_grain_sqla']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Style/visual arguments go in Chart Options section
|
||||
const controlConfig = getControlConfig(argClass, paramName);
|
||||
|
||||
// Add visibility if specified
|
||||
if (visibility) {
|
||||
controlConfig.visibility = visibility;
|
||||
controlConfig.resetOnHide = resetOnHide ?? false;
|
||||
}
|
||||
|
||||
chartOptionsControls.push([
|
||||
{
|
||||
name: paramName,
|
||||
config: controlConfig,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Add adhoc_filters to query section
|
||||
queryControls.push(['adhoc_filters']);
|
||||
|
||||
// Merge with additional controls from options
|
||||
const finalQueryControls = [
|
||||
...queryControls,
|
||||
...(options.queryControls || []),
|
||||
];
|
||||
const finalChartOptionsControls = [
|
||||
...chartOptionsControls,
|
||||
...(options.chartOptionsControls || []),
|
||||
];
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: finalQueryControls,
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: finalChartOptionsControls,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (options.controlOverrides) {
|
||||
config.controlOverrides = options.controlOverrides;
|
||||
}
|
||||
|
||||
if (options.formDataOverrides) {
|
||||
// Type assertion needed because SqlaFormData is more specific than Record<string, unknown>
|
||||
config.formDataOverrides =
|
||||
options.formDataOverrides as ControlPanelConfig['formDataOverrides'];
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for transformProps generation
|
||||
*/
|
||||
export interface TransformPropsOptions<TResult> {
|
||||
/** Custom transformation function that receives extracted values */
|
||||
transform?: (
|
||||
values: Record<string, unknown>,
|
||||
chartProps: ChartProps,
|
||||
) => TResult;
|
||||
/** Additional props to pass through from chartProps */
|
||||
passthrough?: (keyof ChartProps)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transformProps function from glyph arguments
|
||||
*
|
||||
* This extracts values from formData based on argument definitions,
|
||||
* applying type conversions as needed (e.g., RGBA to hex for colors).
|
||||
*/
|
||||
export function generateTransformProps<TResult = Record<string, unknown>>(
|
||||
glyphArguments: GlyphArguments,
|
||||
options: TransformPropsOptions<TResult> = {},
|
||||
): (chartProps: ChartProps) => TResult {
|
||||
return (chartProps: ChartProps) => {
|
||||
const { formData, width, height, queriesData } = chartProps;
|
||||
const values: Record<string, unknown> = {
|
||||
width,
|
||||
height,
|
||||
queriesData,
|
||||
};
|
||||
|
||||
// Add passthrough props
|
||||
if (options.passthrough) {
|
||||
for (const key of options.passthrough) {
|
||||
values[key] = chartProps[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract values from formData based on argument definitions
|
||||
for (const [paramName, argOrConfig] of glyphArguments) {
|
||||
const argClass = getArgClass(argOrConfig);
|
||||
|
||||
// Skip data arguments (metric, dimension, temporal) - these are handled differently
|
||||
if (
|
||||
isMetricArg(argClass) ||
|
||||
isDimensionArg(argClass) ||
|
||||
isTemporalArg(argClass)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get value from formData, using default if not present
|
||||
let value = formData[paramName];
|
||||
|
||||
// Color control: convert RGBA object to hex string
|
||||
if (isColorArg(argClass)) {
|
||||
const colorClass = argClass as typeof Color;
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
const defaultRgba = hexToRgba(colorClass.default ?? '#000000');
|
||||
const colorValue = value ?? defaultRgba;
|
||||
|
||||
if (
|
||||
typeof colorValue === 'object' &&
|
||||
colorValue !== null &&
|
||||
'r' in colorValue
|
||||
) {
|
||||
value = rgbaToHex(colorValue as RgbaColor);
|
||||
} else if (typeof colorValue === 'string') {
|
||||
value = colorValue;
|
||||
} else {
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
value = colorClass.default ?? '#000000';
|
||||
}
|
||||
}
|
||||
// Select control: use default if no value
|
||||
else if (isSelectArg(argClass)) {
|
||||
const selectClass = argClass as typeof Select;
|
||||
value = value ?? selectClass.default;
|
||||
}
|
||||
// Checkbox control: use default if no value
|
||||
else if (isCheckboxArg(argClass)) {
|
||||
const checkboxClass = argClass as typeof Checkbox;
|
||||
value = value ?? checkboxClass.default ?? false;
|
||||
}
|
||||
// Int control: use default if no value
|
||||
else if (isIntArg(argClass)) {
|
||||
const intClass = argClass as typeof Int;
|
||||
value = value ?? intClass.default ?? 0;
|
||||
}
|
||||
// Text control: use default if no value
|
||||
else {
|
||||
const textClass = argClass as typeof Text;
|
||||
value = value ?? textClass.default ?? '';
|
||||
}
|
||||
|
||||
values[paramName] = value;
|
||||
}
|
||||
|
||||
// Apply custom transformation if provided
|
||||
if (options.transform) {
|
||||
return options.transform(values, chartProps);
|
||||
}
|
||||
|
||||
return values as TResult;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined result of creating a glyph plugin
|
||||
*/
|
||||
export interface GlyphPluginDef<TProps> {
|
||||
controlPanel: ControlPanelConfig;
|
||||
transformProps: (chartProps: ChartProps) => TProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create both controlPanel and transformProps from a single argument definition
|
||||
*
|
||||
* This is the main entry point for the single-file viz pattern.
|
||||
*/
|
||||
export function createGlyphPlugin<TProps = Record<string, unknown>>(
|
||||
glyphArguments: GlyphArguments,
|
||||
controlPanelOptions: ControlPanelOptions = {},
|
||||
transformPropsOptions: TransformPropsOptions<TProps> = {},
|
||||
): GlyphPluginDef<TProps> {
|
||||
return {
|
||||
controlPanel: generateControlPanel(glyphArguments, controlPanelOptions),
|
||||
transformProps: generateTransformProps(
|
||||
glyphArguments,
|
||||
transformPropsOptions,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Core - A declarative visualization plugin framework
|
||||
*
|
||||
* This module enables single-file visualization plugins where:
|
||||
* 1. Arguments define both the chart's inputs AND the control panel
|
||||
* 2. transformProps is auto-generated from argument definitions
|
||||
* 3. The chart component is a simple function receiving typed arguments
|
||||
*
|
||||
* Features:
|
||||
* - Single-file chart definitions with defineChart()
|
||||
* - Declarative argument types (Metric, Dimension, Select, Checkbox, etc.)
|
||||
* - Conditional visibility with visibleWhen/disabledWhen
|
||||
* - Cross-filtering support with extractCrossFilterProps() and allEventHandlers()
|
||||
* - Reusable presets (ShowLegend, HeaderFontSize, etc.)
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* export default defineChart({
|
||||
* metadata: {
|
||||
* name: 'My Chart',
|
||||
* thumbnail,
|
||||
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
|
||||
* },
|
||||
* arguments: {
|
||||
* metric: Metric.with({ label: 'Metric' }),
|
||||
* groupby: Dimension.with({ label: 'Breakdowns' }),
|
||||
* fontSize: Select.with({
|
||||
* label: 'Font Size',
|
||||
* options: [{ label: 'Small', value: 0.2 }, { label: 'Large', value: 0.4 }],
|
||||
* default: 0.3,
|
||||
* }),
|
||||
* },
|
||||
* transform: (chartProps, argValues) => {
|
||||
* // Extract cross-filter props for interactive filtering
|
||||
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap, seriesNames);
|
||||
* return { transformedProps: { echartOptions, ...crossFilterProps } };
|
||||
* },
|
||||
* render: ({ transformedProps }) => {
|
||||
* const eventHandlers = allEventHandlers(transformedProps);
|
||||
* return <Echart eventHandlers={eventHandlers} ... />;
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Re-export everything
|
||||
export * from './types';
|
||||
export * from './arguments';
|
||||
export * from './generators';
|
||||
export * from './defineChart';
|
||||
export * from './presets';
|
||||
export * from './crossFilter';
|
||||
406
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
406
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Presets - Reusable argument configurations
|
||||
*
|
||||
* This module contains pre-configured arguments that are commonly
|
||||
* used across multiple visualization types. Charts can import these
|
||||
* directly or use .with() to customize them further.
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* import { HeaderFontSize, Subtitle } from '../../glyph-core/presets';
|
||||
*
|
||||
* arguments: {
|
||||
* headerFontSize: HeaderFontSize,
|
||||
* subtitle: Subtitle,
|
||||
* // Override defaults when needed:
|
||||
* customSize: HeaderFontSize.with({ default: 0.5 }),
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { Select, Text, Checkbox } from './arguments';
|
||||
import { SelectOption } from './types';
|
||||
|
||||
// ============================================================================
|
||||
// Font Size Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Large font size options - for primary/header text elements
|
||||
* Values are multipliers of container height (0.2 = 20% of height)
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_LARGE: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.2 },
|
||||
{ label: t('Small'), value: 0.3 },
|
||||
{ label: t('Normal'), value: 0.4 },
|
||||
{ label: t('Large'), value: 0.5 },
|
||||
{ label: t('Huge'), value: 0.6 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Small font size options - for secondary text elements (subtitles, labels)
|
||||
* Values are multipliers of container height
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_SMALL: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.125 },
|
||||
{ label: t('Small'), value: 0.15 },
|
||||
{ label: t('Normal'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.3 },
|
||||
{ label: t('Huge'), value: 0.4 },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Pre-configured Arguments
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Header/primary font size selector
|
||||
* Used for main display elements like big numbers, titles
|
||||
*/
|
||||
export const HeaderFontSize = Select.with({
|
||||
label: t('Font Size'),
|
||||
description: t('Font size for the primary display element'),
|
||||
options: FONT_SIZE_OPTIONS_LARGE,
|
||||
default: 0.4,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subheader/secondary font size selector
|
||||
* Used for subtitles, labels, secondary text
|
||||
*/
|
||||
export const SubheaderFontSize = Select.with({
|
||||
label: t('Subheader Font Size'),
|
||||
description: t('Font size for secondary text elements'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subtitle text input
|
||||
* Generic subtitle/description field used by many chart types
|
||||
*/
|
||||
export const Subtitle = Text.with({
|
||||
label: t('Subtitle'),
|
||||
description: t('Description text displayed below the main content'),
|
||||
default: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* Show legend toggle
|
||||
* Common toggle for charts with legends
|
||||
*/
|
||||
export const ShowLegend = Checkbox.with({
|
||||
label: t('Show Legend'),
|
||||
description: t('Whether to display the chart legend'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Force timestamp formatting toggle
|
||||
* Used when a value might be a timestamp but isn't auto-detected
|
||||
*/
|
||||
export const ForceTimestampFormatting = Checkbox.with({
|
||||
label: t('Force Date Format'),
|
||||
description: t(
|
||||
'Use date formatting even when the value is not detected as a timestamp',
|
||||
),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Legend Options
|
||||
// ============================================================================
|
||||
|
||||
export const LEGEND_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Scroll'), value: 'scroll' },
|
||||
{ label: t('List'), value: 'plain' },
|
||||
];
|
||||
|
||||
export const LEGEND_ORIENTATION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
];
|
||||
|
||||
export const LEGEND_SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('No sort'), value: '' },
|
||||
{ label: t('Ascending'), value: 'asc' },
|
||||
{ label: t('Descending'), value: 'desc' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Legend type selector
|
||||
* Choose between scrollable or plain list legend
|
||||
*/
|
||||
export const LegendType = Select.with({
|
||||
label: t('Legend Type'),
|
||||
description: t('Type of legend display'),
|
||||
options: LEGEND_TYPE_OPTIONS,
|
||||
default: 'scroll',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend orientation selector
|
||||
* Position the legend relative to the chart
|
||||
*/
|
||||
export const LegendOrientation = Select.with({
|
||||
label: t('Legend Orientation'),
|
||||
description: t('Position of the legend'),
|
||||
options: LEGEND_ORIENTATION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend sort selector
|
||||
* Sort legend items alphabetically
|
||||
*/
|
||||
export const LegendSort = Select.with({
|
||||
label: t('Legend Sort'),
|
||||
description: t('Sort order for legend items'),
|
||||
options: LEGEND_SORT_OPTIONS,
|
||||
default: '',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show labels toggle
|
||||
* Common toggle for chart labels
|
||||
*/
|
||||
export const ShowLabels = Checkbox.with({
|
||||
label: t('Show Labels'),
|
||||
description: t('Whether to display labels on the chart'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Show value toggle
|
||||
* Common toggle for showing values on chart elements
|
||||
*/
|
||||
export const ShowValue = Checkbox.with({
|
||||
label: t('Show Value'),
|
||||
description: t('Whether to display values on the chart'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Metric Name Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show metric name toggle
|
||||
* Used in BigNumber charts to optionally show the metric name
|
||||
*/
|
||||
export const ShowMetricName = Checkbox.with({
|
||||
label: t('Show Metric Name'),
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Metric name font size selector
|
||||
* Typically used with visibility tied to ShowMetricName
|
||||
*/
|
||||
export const MetricNameFontSize = Select.with({
|
||||
label: t('Metric Name Font Size'),
|
||||
description: t('Font size for the metric name'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Type Options (shared by Pie, Funnel, etc.)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Standard label content type options
|
||||
* Used by Pie, Funnel, and other category-based charts
|
||||
*/
|
||||
export const LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Percentage'), value: 'percent' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
{ label: t('Category and Percentage'), value: 'key_percent' },
|
||||
{ label: t('Category, Value and Percentage'), value: 'key_value_percent' },
|
||||
{ label: t('Value and Percentage'), value: 'value_percent' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label type selector for category-based charts
|
||||
*/
|
||||
export const LabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Sort Options
|
||||
// ============================================================================
|
||||
|
||||
export const SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Descending'), value: 'descending' },
|
||||
{ label: t('Ascending'), value: 'ascending' },
|
||||
{ label: t('None'), value: 'none' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Sort by metric toggle
|
||||
* Common for charts that need to sort data by metric value
|
||||
*/
|
||||
export const SortByMetric = Checkbox.with({
|
||||
label: t('Sort by Metric'),
|
||||
description: t('Sort results by the selected metric'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Position Options
|
||||
// ============================================================================
|
||||
|
||||
export const LABEL_POSITION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Inside'), value: 'inside' },
|
||||
{ label: t('Inside Left'), value: 'insideLeft' },
|
||||
{ label: t('Inside Right'), value: 'insideRight' },
|
||||
{ label: t('Inside Top'), value: 'insideTop' },
|
||||
{ label: t('Inside Bottom'), value: 'insideBottom' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label position selector
|
||||
* Position labels relative to chart elements
|
||||
*/
|
||||
export const LabelPosition = Select.with({
|
||||
label: t('Label Position'),
|
||||
description: t('Position of labels on the chart'),
|
||||
options: LABEL_POSITION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Simple Label Type (key/value variants only)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Simple label type options - for charts with fewer label display options
|
||||
* Used by Radar, Sunburst, etc.
|
||||
*/
|
||||
export const SIMPLE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Simple label type selector
|
||||
* For charts that only need key/value/key_value options
|
||||
*/
|
||||
export const SimpleLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: SIMPLE_LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
/**
|
||||
* Value-only label type options - for charts like Radar
|
||||
*/
|
||||
export const VALUE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Value label type selector
|
||||
* For charts that show value or category+value
|
||||
*/
|
||||
export const ValueLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: VALUE_LABEL_TYPE_OPTIONS,
|
||||
default: 'value',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Totals and Aggregates
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show total toggle
|
||||
* For charts that can display aggregate totals
|
||||
*/
|
||||
export const ShowTotal = Checkbox.with({
|
||||
label: t('Show Total'),
|
||||
description: t('Whether to display the aggregate total'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Threshold Controls
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Label percentage threshold
|
||||
* Minimum percentage for showing labels (avoids clutter on small slices)
|
||||
*/
|
||||
export const LabelThreshold = Text.with({
|
||||
label: t('Percentage Threshold'),
|
||||
description: t('Minimum threshold in percentage points for showing labels'),
|
||||
default: '5',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Shape Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Circle shape toggle (used by Radar)
|
||||
*/
|
||||
export const CircleShape = Checkbox.with({
|
||||
label: t('Circle Shape'),
|
||||
description: t('Use circular shape instead of polygon'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Data Zoom
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Enable data zoom toggle
|
||||
* For charts with zoomable data areas
|
||||
*/
|
||||
export const DataZoom = Checkbox.with({
|
||||
label: t('Data Zoom'),
|
||||
description: t('Enable data zooming controls'),
|
||||
default: false,
|
||||
});
|
||||
306
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
306
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Option for Select controls
|
||||
*/
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Select argument type
|
||||
*/
|
||||
export interface SelectOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | number;
|
||||
options?: SelectOption[];
|
||||
clearable?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Text argument type
|
||||
*/
|
||||
export interface TextOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Checkbox argument type
|
||||
*/
|
||||
export interface CheckboxOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Int argument type (slider)
|
||||
*/
|
||||
export interface IntOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Color argument type
|
||||
*/
|
||||
export interface ColorOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Metric argument type
|
||||
*/
|
||||
export interface MetricOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Dimension argument type
|
||||
*/
|
||||
export interface DimensionOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for NumberFormat argument type
|
||||
*/
|
||||
export interface NumberFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency value structure
|
||||
*/
|
||||
export interface CurrencyValue {
|
||||
symbol?: string;
|
||||
symbolPosition?: 'prefix' | 'suffix';
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Currency argument type
|
||||
*/
|
||||
export interface CurrencyOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: CurrencyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for TimeFormat argument type
|
||||
*/
|
||||
export interface TimeFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for ConditionalFormatting argument type
|
||||
*/
|
||||
export interface ConditionalFormattingOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Slider argument type (continuous float values)
|
||||
*/
|
||||
export interface SliderOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Bounds argument type (min/max pairs)
|
||||
*/
|
||||
export interface BoundsOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: [number | null, number | null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds value type - tuple of [min, max] where either can be null
|
||||
*/
|
||||
export type BoundsValue = [number | null, number | null];
|
||||
|
||||
/**
|
||||
* Configuration options for ColorPicker argument type (RGBA colors)
|
||||
*/
|
||||
export interface ColorPickerOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: RgbaColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for RadioButton argument type
|
||||
*/
|
||||
export interface RadioButtonOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | boolean;
|
||||
options: RadioOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Option for RadioButton controls
|
||||
*/
|
||||
export interface RadioOption {
|
||||
label: string;
|
||||
value: string | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional formatting rule value
|
||||
*/
|
||||
export interface ConditionalFormattingRule {
|
||||
column?: string;
|
||||
operator?: '<' | '<=' | '>' | '>=' | '==' | '!=' | 'between';
|
||||
targetValue?: number;
|
||||
targetValueLeft?: number;
|
||||
targetValueRight?: number;
|
||||
colorScheme?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column type enum for data arguments
|
||||
*/
|
||||
export enum ColumnType {
|
||||
Metric = 'metric',
|
||||
Dimension = 'dimension',
|
||||
Temporal = 'temporal',
|
||||
Argument = 'argument',
|
||||
}
|
||||
|
||||
/**
|
||||
* Base argument class interface
|
||||
*/
|
||||
export interface ArgumentClass {
|
||||
label: string | null;
|
||||
description: string | null;
|
||||
columnType?: ColumnType;
|
||||
controlType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RGBA color format used by Superset's ColorPickerControl
|
||||
*/
|
||||
export interface RgbaColor {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility function for conditional control display (legacy)
|
||||
*/
|
||||
export type VisibilityFn = (state: {
|
||||
controls: Record<string, { value: unknown }>;
|
||||
}) => boolean;
|
||||
|
||||
/**
|
||||
* Declarative condition for argument visibility/disabled state.
|
||||
*
|
||||
* Keys are argument names, values define the condition:
|
||||
* - Literal value: equality check (e.g., { showMetricName: true })
|
||||
* - Function: custom check (e.g., { subtitle: (val) => !!val })
|
||||
*
|
||||
* Multiple keys are AND'd together.
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true
|
||||
* visibleWhen: { showMetricName: true }
|
||||
*
|
||||
* @example
|
||||
* // Visible when subtitle is not empty
|
||||
* visibleWhen: { subtitle: (val) => !!val }
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true AND subtitle is not empty
|
||||
* visibleWhen: { showMetricName: true, subtitle: (val) => !!val }
|
||||
*/
|
||||
export type ArgumentCondition = Record<
|
||||
string,
|
||||
unknown | ((value: unknown) => boolean)
|
||||
>;
|
||||
|
||||
/**
|
||||
* Extended control configuration with visibility
|
||||
*/
|
||||
export interface ControlConfig {
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Glyph chart definition
|
||||
*/
|
||||
export interface GlyphChartDef<TArgs extends Record<string, ArgumentClass>> {
|
||||
arguments: TArgs;
|
||||
sections?: {
|
||||
query?: ControlConfig[][];
|
||||
chartOptions?: ControlConfig[][];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of createGlyphPlugin
|
||||
*/
|
||||
export interface GlyphPluginResult<TFormData> {
|
||||
controlPanel: ControlPanelConfig;
|
||||
transformProps: (chartProps: ChartProps) => TFormData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper to extract form data types from argument definitions
|
||||
*/
|
||||
export type ArgumentsToFormData<TArgs extends Record<string, ArgumentClass>> = {
|
||||
[K in keyof TArgs]: TArgs[K] extends { default: infer D } ? D : unknown;
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,563 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
Behavior,
|
||||
ChartLabel,
|
||||
ChartMetadata,
|
||||
ChartPlugin,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
Checkbox,
|
||||
defineChart,
|
||||
Dimension,
|
||||
evaluateGlyphCondition,
|
||||
getArgVisibleWhen,
|
||||
Metric,
|
||||
resolveArgClass,
|
||||
Select,
|
||||
Text,
|
||||
} from '@superset-ui/glyph-core';
|
||||
|
||||
// Helper: instantiate a plugin and reach its controlPanel config.
|
||||
function instantiate(PluginClass: ReturnType<typeof defineChart>) {
|
||||
const plugin = new PluginClass();
|
||||
// ChartPlugin internals expose the panel under .controlPanel
|
||||
// (set via super({ controlPanel }) in defineChart's GlyphChartPlugin).
|
||||
return {
|
||||
plugin,
|
||||
controlPanel: (plugin as unknown as { controlPanel: Record<string, unknown> })
|
||||
.controlPanel,
|
||||
metadata: (plugin as unknown as { metadata: ChartMetadata }).metadata,
|
||||
};
|
||||
}
|
||||
|
||||
const MIN_THUMBNAIL = 'thumb.png';
|
||||
|
||||
describe('resolveArgClass', () => {
|
||||
test('returns the bare class form unchanged', () => {
|
||||
expect(resolveArgClass(Metric)).toBe(Metric);
|
||||
});
|
||||
|
||||
test('unwraps the { arg, visibleWhen } object form', () => {
|
||||
const M = Metric.with({ label: 'Sales' });
|
||||
const argDef = { arg: M, visibleWhen: { show: true } };
|
||||
expect(resolveArgClass(argDef)).toBe(M);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArgVisibleWhen', () => {
|
||||
test('returns undefined for bare class form', () => {
|
||||
expect(getArgVisibleWhen(Metric)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns the condition for object form', () => {
|
||||
const argDef = { arg: Metric, visibleWhen: { show: true } };
|
||||
expect(getArgVisibleWhen(argDef)).toEqual({ show: true });
|
||||
});
|
||||
|
||||
test('returns undefined when object form has no visibleWhen', () => {
|
||||
expect(getArgVisibleWhen({ arg: Metric })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateGlyphCondition', () => {
|
||||
test('returns true for empty condition', () => {
|
||||
expect(evaluateGlyphCondition({}, { foo: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true when equality check matches', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: true })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when equality check fails', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: false })).toBe(false);
|
||||
});
|
||||
|
||||
test('handles missing formData keys as undefined', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, {})).toBe(false);
|
||||
});
|
||||
|
||||
test('supports function-valued conditions', () => {
|
||||
const cond = { subtitle: (val: unknown) => !!val };
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: 'hi' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: '' })).toBe(false);
|
||||
});
|
||||
|
||||
test('requires all keys in the condition to pass (AND semantics)', () => {
|
||||
const cond = { a: true, b: 'x' };
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'x' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'y' })).toBe(false);
|
||||
expect(evaluateGlyphCondition(cond, { a: false, b: 'x' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - basic plugin construction', () => {
|
||||
test('returns a ChartPlugin subclass', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const p = new Plugin();
|
||||
expect(p).toBeInstanceOf(ChartPlugin);
|
||||
});
|
||||
|
||||
test('plugin metadata is a ChartMetadata instance with required fields', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
description: 'A test chart',
|
||||
category: 'Charts',
|
||||
tags: ['test'],
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata).toBeInstanceOf(ChartMetadata);
|
||||
expect(metadata.name).toBe('Test');
|
||||
expect(metadata.description).toBe('A test chart');
|
||||
expect(metadata.category).toBe('Charts');
|
||||
expect(metadata.tags).toEqual(['test']);
|
||||
expect(metadata.thumbnail).toBe(MIN_THUMBNAIL);
|
||||
});
|
||||
|
||||
test('metadata defaults Behavior.InteractiveChart when omitted', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.behaviors).toContain(Behavior.InteractiveChart);
|
||||
});
|
||||
|
||||
test('metadata behaviors override the default when provided', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.behaviors).toEqual([
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
]);
|
||||
});
|
||||
|
||||
test('passes label, canBeAnnotationTypes, useLegacyApi, supportedAnnotationTypes through', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
label: ChartLabel.Deprecated,
|
||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||
useLegacyApi: true,
|
||||
supportedAnnotationTypes: ['FORMULA'],
|
||||
credits: ['https://example.com'],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.label).toBe(ChartLabel.Deprecated);
|
||||
expect(metadata.canBeAnnotationTypes).toEqual(['EVENT', 'INTERVAL']);
|
||||
expect(metadata.useLegacyApi).toBe(true);
|
||||
expect(metadata.supportedAnnotationTypes).toEqual(['FORMULA']);
|
||||
expect(metadata.credits).toEqual(['https://example.com']);
|
||||
});
|
||||
|
||||
test('exampleGallery + thumbnailDark are preserved', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
thumbnailDark: 'thumb-dark.png',
|
||||
exampleGallery: [{ url: 'a.png', urlDark: 'a-dark.png' }],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.thumbnailDark).toBe('thumb-dark.png');
|
||||
expect(metadata.exampleGallery).toEqual([
|
||||
{ url: 'a.png', urlDark: 'a-dark.png' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - controlPanel generation from arguments', () => {
|
||||
test('Query section is auto-generated from Metric/Dimension arguments', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// Query section should be auto-generated
|
||||
expect(sections.some(s => s?.label === 'Query')).toBe(true);
|
||||
});
|
||||
|
||||
test('suppressQuerySection: true skips the auto Query section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
},
|
||||
suppressQuerySection: true,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// The auto-generated Query section is suppressed.
|
||||
// (Charts using suppressQuerySection typically provide their own via
|
||||
// prependSections — see legacy nvd3 / deckgl consolidations.)
|
||||
const autoQuery = sections.find(s => s?.label === 'Query');
|
||||
expect(autoQuery).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Chart Options section is generated when there are non-data args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
|
||||
test('Chart Options section is hidden when there are no customize args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// No Customize-tab content → Chart Options auto-hides.
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - prependSections / middleSections / additionalSections', () => {
|
||||
test('prependSections appears before the auto Query section', () => {
|
||||
const TIME_SECTION = {
|
||||
label: 'Time',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
prependSections: [TIME_SECTION],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const timeIdx = sections.findIndex(s => s?.label === 'Time');
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
expect(timeIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(queryIdx).toBeGreaterThan(timeIdx);
|
||||
});
|
||||
|
||||
test('additionalSections appears after Chart Options', () => {
|
||||
const TIME_COMP = {
|
||||
label: 'Time Comparison',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
additionalSections: [TIME_COMP],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
const timeCompIdx = sections.findIndex(s => s?.label === 'Time Comparison');
|
||||
expect(chartOptsIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(timeCompIdx).toBeGreaterThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('middleSections appears between Query and Chart Options', () => {
|
||||
const MIDDLE = {
|
||||
label: 'Middle',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
middleSections: [MIDDLE],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
const middleIdx = sections.findIndex(s => s?.label === 'Middle');
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
expect(queryIdx).toBeLessThan(middleIdx);
|
||||
expect(middleIdx).toBeLessThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('chartOptionsTabOverride sets tabOverride on the generated section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
chartOptionsTabOverride: 'data',
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
tabOverride?: string;
|
||||
}>;
|
||||
const chartOpts = sections.find(s => s?.label === 'Chart Options');
|
||||
expect(chartOpts?.tabOverride).toBe('data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - overrides + formDataOverrides + onInit', () => {
|
||||
test('additionalControlOverrides land on controlPanel.controlOverrides', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
additionalControlOverrides: {
|
||||
size: { label: 'Custom Size Label' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(
|
||||
(controlPanel.controlOverrides as Record<string, unknown>)?.size,
|
||||
).toEqual({ label: 'Custom Size Label' });
|
||||
});
|
||||
|
||||
test('controlOverrides + additionalControlOverrides merge', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
controlOverrides: {
|
||||
a: { label: 'A' },
|
||||
},
|
||||
additionalControlOverrides: {
|
||||
b: { label: 'B' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const merged = controlPanel.controlOverrides as Record<string, unknown>;
|
||||
expect(merged.a).toEqual({ label: 'A' });
|
||||
expect(merged.b).toEqual({ label: 'B' });
|
||||
});
|
||||
|
||||
test('formDataOverrides is preserved on controlPanel', () => {
|
||||
const fdo = (formData: Record<string, unknown>) => ({
|
||||
...formData,
|
||||
custom: 'extra',
|
||||
});
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
formDataOverrides: fdo,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.formDataOverrides).toBe(fdo);
|
||||
});
|
||||
|
||||
test('onInit is preserved on controlPanel', () => {
|
||||
const onInit = (state: unknown) => state;
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
onInit,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.onInit).toBe(onInit);
|
||||
});
|
||||
|
||||
test('_glyphArgs is attached to the controlPanel for native rendering', () => {
|
||||
const args = {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: args,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toEqual(args);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - custom buildQuery / transform', () => {
|
||||
test('custom buildQuery is invoked via the plugin loader', async () => {
|
||||
const customBuildQuery = jest.fn(() => ({ queries: [{ marker: 1 }] }));
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
buildQuery: customBuildQuery as unknown as never,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
// ChartPlugin stores it as a sanitized loader
|
||||
const loader = (p as unknown as { loadBuildQuery?: () => Promise<Function> })
|
||||
.loadBuildQuery;
|
||||
expect(loader).toBeDefined();
|
||||
const fn = await (loader as () => Promise<Function>)();
|
||||
fn({ viz_type: 'test', datasource: '1__table' });
|
||||
expect(customBuildQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('transform receives chartProps and argValues', async () => {
|
||||
const captured: unknown[] = [];
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
transform: (chartProps, argValues) => {
|
||||
captured.push({ chartProps, argValues });
|
||||
return { transformed: true };
|
||||
},
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
const loader = (
|
||||
p as unknown as { loadTransformProps: () => Promise<Function> }
|
||||
).loadTransformProps;
|
||||
const transformProps = await loader();
|
||||
transformProps({
|
||||
width: 100,
|
||||
height: 100,
|
||||
formData: { metric: 'count' },
|
||||
queriesData: [{ data: [] }],
|
||||
});
|
||||
expect(captured).toHaveLength(1);
|
||||
expect((captured[0] as { chartProps: unknown }).chartProps).toBeDefined();
|
||||
expect((captured[0] as { argValues: unknown }).argValues).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - Text-only argument behavior', () => {
|
||||
test('a Text-only chart still wires up a working plugin', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'TextOnly', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
title: Text.with({ label: 'Title', default: 'Hi' }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel, metadata } = instantiate(Plugin);
|
||||
expect(metadata.name).toBe('TextOnly');
|
||||
// Customize args present → Chart Options shows up
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - visibleWhen with object-form ArgDef', () => {
|
||||
test('attaches a visibility derivation to the underlying control', () => {
|
||||
// Build a plugin where one arg is visibleWhen another is true.
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'V', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
legendPosition: {
|
||||
arg: Select.with({
|
||||
label: 'Position',
|
||||
default: 'right',
|
||||
options: [{ label: 'R', value: 'right' }],
|
||||
}),
|
||||
visibleWhen: { showLegend: true },
|
||||
},
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toBeDefined();
|
||||
const glyphArgs = controlPanel._glyphArgs as Record<string, unknown>;
|
||||
// The visibleWhen is preserved on the glyph args
|
||||
const lp = glyphArgs.legendPosition as { visibleWhen?: unknown };
|
||||
expect(lp.visibleWhen).toEqual({ showLegend: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
Checkbox,
|
||||
Color,
|
||||
createGlyphPlugin,
|
||||
Dimension,
|
||||
generateControlPanel,
|
||||
generateTransformProps,
|
||||
getControlConfig,
|
||||
Int,
|
||||
Metric,
|
||||
Select,
|
||||
Temporal,
|
||||
Text,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import type { GlyphArguments } from '@superset-ui/glyph-core/generators';
|
||||
|
||||
describe('getControlConfig - per argument type', () => {
|
||||
test('Select → SelectControl with options and clearable=false', () => {
|
||||
const S = Select.with({
|
||||
label: 'Choose',
|
||||
default: 'a',
|
||||
options: [{ label: 'A', value: 'a' }],
|
||||
});
|
||||
const cfg = getControlConfig(S, 'myParam');
|
||||
expect(cfg.type).toBe('SelectControl');
|
||||
expect(cfg.label).toBe('Choose');
|
||||
expect(cfg.default).toBe('a');
|
||||
expect(cfg.options).toEqual([{ label: 'A', value: 'a' }]);
|
||||
expect(cfg.clearable).toBe(false);
|
||||
expect(cfg.renderTrigger).toBe(true);
|
||||
});
|
||||
|
||||
test('Checkbox → CheckboxControl with default', () => {
|
||||
const C = Checkbox.with({ label: 'Show', default: true });
|
||||
const cfg = getControlConfig(C, 'show');
|
||||
expect(cfg.type).toBe('CheckboxControl');
|
||||
expect(cfg.label).toBe('Show');
|
||||
expect(cfg.default).toBe(true);
|
||||
expect(cfg.renderTrigger).toBe(true);
|
||||
});
|
||||
|
||||
test('Int → SliderControl with min/max/step', () => {
|
||||
const I = Int.with({ label: 'Limit', default: 50, min: 0, max: 1000, step: 5 });
|
||||
const cfg = getControlConfig(I, 'limit');
|
||||
expect(cfg.type).toBe('SliderControl');
|
||||
expect(cfg.label).toBe('Limit');
|
||||
expect(cfg.default).toBe(50);
|
||||
expect(cfg.min).toBe(0);
|
||||
expect(cfg.max).toBe(1000);
|
||||
expect(cfg.step).toBe(5);
|
||||
});
|
||||
|
||||
test('Color (hex) → ColorPickerControl with RGBA default', () => {
|
||||
const C = Color.with({ label: 'Fill', default: '#ff0000' });
|
||||
const cfg = getControlConfig(C, 'fill');
|
||||
expect(cfg.type).toBe('ColorPickerControl');
|
||||
expect(cfg.label).toBe('Fill');
|
||||
expect(cfg.default).toEqual({ r: 255, g: 0, b: 0, a: 1 });
|
||||
});
|
||||
|
||||
test('Text → TextControl with placeholder', () => {
|
||||
const T = Text.with({
|
||||
label: 'Title',
|
||||
default: 'Untitled',
|
||||
placeholder: 'Enter…',
|
||||
});
|
||||
const cfg = getControlConfig(T, 'title');
|
||||
expect(cfg.type).toBe('TextControl');
|
||||
expect(cfg.label).toBe('Title');
|
||||
expect(cfg.default).toBe('Untitled');
|
||||
expect(cfg.placeholder).toBe('Enter…');
|
||||
});
|
||||
|
||||
test('falls back to paramName when label is unset', () => {
|
||||
// Use the raw Text class (label: null on Argument)
|
||||
class Bare extends Text {
|
||||
static override label = null;
|
||||
}
|
||||
const cfg = getControlConfig(Bare, 'fallback_name');
|
||||
expect(cfg.label).toBe('fallback_name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateControlPanel', () => {
|
||||
test('produces Query and Chart Options sections', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const labels = cp.controlPanelSections.map(s =>
|
||||
s && 'label' in s ? s.label : undefined,
|
||||
);
|
||||
expect(labels).toContain('Query');
|
||||
expect(labels).toContain('Chart Options');
|
||||
});
|
||||
|
||||
test('Metric args produce a [metric] row in Query', () => {
|
||||
const args: GlyphArguments = new Map([['m', Metric]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
expect(querySection).toBeDefined();
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['metric']);
|
||||
});
|
||||
|
||||
test('Dimension args produce a [groupby] row in Query', () => {
|
||||
const args: GlyphArguments = new Map([['d', Dimension]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['groupby']);
|
||||
});
|
||||
|
||||
test('Temporal args produce [x_axis] and [time_grain_sqla] rows in Query', () => {
|
||||
const args: GlyphArguments = new Map([['t', Temporal]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['x_axis']);
|
||||
expect(rows).toContainEqual(['time_grain_sqla']);
|
||||
});
|
||||
|
||||
test('adhoc_filters is always added to Query', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['adhoc_filters']);
|
||||
});
|
||||
|
||||
test('Non-data args become controls in Chart Options', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const rows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toEqual([
|
||||
{
|
||||
name: 'showLegend',
|
||||
config: expect.objectContaining({ type: 'CheckboxControl' }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('GlyphArgConfig with visibility wires onto control', () => {
|
||||
const visibility = jest.fn(() => true);
|
||||
const args: GlyphArguments = new Map([
|
||||
[
|
||||
'subtitleSize',
|
||||
{
|
||||
arg: Select.with({
|
||||
label: 'Size',
|
||||
default: 'm',
|
||||
options: [{ label: 'M', value: 'm' }],
|
||||
}),
|
||||
visibility,
|
||||
resetOnHide: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const row = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows[0];
|
||||
const item = (row as Array<{ config: Record<string, unknown> }>)[0];
|
||||
expect(item.config.visibility).toBe(visibility);
|
||||
expect(item.config.resetOnHide).toBe(true);
|
||||
});
|
||||
|
||||
test('controlOverrides and formDataOverrides options pass through', () => {
|
||||
const fdo = (fd: Record<string, unknown>) => ({ ...fd, x: 1 });
|
||||
const cp = generateControlPanel(new Map(), {
|
||||
controlOverrides: { metric: { label: 'M' } },
|
||||
formDataOverrides: fdo,
|
||||
});
|
||||
expect(cp.controlOverrides).toEqual({ metric: { label: 'M' } });
|
||||
expect(cp.formDataOverrides).toBe(fdo);
|
||||
});
|
||||
|
||||
test('extra queryControls and chartOptionsControls are appended', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const cp = generateControlPanel(args, {
|
||||
queryControls: [['custom_filter']] as never,
|
||||
chartOptionsControls: [['custom_chart_opt']] as never,
|
||||
});
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const queryRows = (querySection as { controlSetRows: unknown[][] })
|
||||
.controlSetRows;
|
||||
expect(queryRows).toContainEqual(['custom_filter']);
|
||||
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const optRows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(optRows).toContainEqual(['custom_chart_opt']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTransformProps', () => {
|
||||
function makeChartProps(formData: Record<string, unknown>): ChartProps {
|
||||
return {
|
||||
width: 400,
|
||||
height: 300,
|
||||
queriesData: [{ data: [] }],
|
||||
formData,
|
||||
} as unknown as ChartProps;
|
||||
}
|
||||
|
||||
test('returns width/height/queriesData passthrough', () => {
|
||||
const transform = generateTransformProps(new Map());
|
||||
const out = transform(makeChartProps({}));
|
||||
expect(out).toMatchObject({ width: 400, height: 300 });
|
||||
});
|
||||
|
||||
test('extracts Select value from formData', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
[
|
||||
'size',
|
||||
Select.with({
|
||||
label: 'Size',
|
||||
default: 'm',
|
||||
options: [
|
||||
{ label: 'S', value: 's' },
|
||||
{ label: 'M', value: 'm' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ size: 's' }));
|
||||
expect((out as { size: unknown }).size).toBe('s');
|
||||
});
|
||||
|
||||
test('Select falls back to default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['size', Select.with({ label: 'Size', default: 'm', options: [] })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { size: unknown }).size).toBe('m');
|
||||
});
|
||||
|
||||
test('Checkbox uses formData value when present', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: false })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ flag: true }));
|
||||
expect((out as { flag: unknown }).flag).toBe(true);
|
||||
});
|
||||
|
||||
test('Checkbox falls back to default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: true })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { flag: unknown }).flag).toBe(true);
|
||||
});
|
||||
|
||||
test('Color: RGBA in formData → hex string', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#000000' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(
|
||||
makeChartProps({ fill: { r: 255, g: 0, b: 0, a: 1 } }),
|
||||
);
|
||||
expect((out as { fill: unknown }).fill).toBe('#ff0000');
|
||||
});
|
||||
|
||||
test('Color: string value in formData passes through', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#000000' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ fill: '#abcdef' }));
|
||||
expect((out as { fill: unknown }).fill).toBe('#abcdef');
|
||||
});
|
||||
|
||||
test('Color: falls back to class default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#112233' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
// default is hex string; transform converts the RGBA-formatted default back to hex
|
||||
expect((out as { fill: unknown }).fill).toBe('#112233');
|
||||
});
|
||||
|
||||
test('Int uses default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['n', Int.with({ label: 'N', default: 7, min: 0, max: 100 })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { n: unknown }).n).toBe(7);
|
||||
});
|
||||
|
||||
test('Text uses default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['s', Text.with({ label: 'S', default: 'hi' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { s: unknown }).s).toBe('hi');
|
||||
});
|
||||
|
||||
test('Metric/Dimension/Temporal args are NOT extracted (handled elsewhere)', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['groupby', Dimension],
|
||||
['t', Temporal],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(
|
||||
makeChartProps({ metric: 'count', groupby: 'a', t: 'date' }),
|
||||
);
|
||||
expect((out as Record<string, unknown>).metric).toBeUndefined();
|
||||
expect((out as Record<string, unknown>).groupby).toBeUndefined();
|
||||
expect((out as Record<string, unknown>).t).toBeUndefined();
|
||||
});
|
||||
|
||||
test('passthrough option copies named ChartProps fields onto the result', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const transform = generateTransformProps(args, {
|
||||
passthrough: ['formData'],
|
||||
});
|
||||
const out = transform(makeChartProps({ marker: 1 })) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(out.formData).toEqual({ marker: 1 });
|
||||
});
|
||||
|
||||
test('custom transform option receives extracted values and chartProps', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: false })],
|
||||
]);
|
||||
const transform = generateTransformProps(args, {
|
||||
transform: (values, chartProps) => ({
|
||||
values,
|
||||
gotChartProps: !!chartProps,
|
||||
}),
|
||||
});
|
||||
const out = transform(makeChartProps({ flag: true })) as {
|
||||
values: { flag: boolean };
|
||||
gotChartProps: boolean;
|
||||
};
|
||||
expect(out.values.flag).toBe(true);
|
||||
expect(out.gotChartProps).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGlyphPlugin', () => {
|
||||
test('returns both controlPanel and transformProps', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['show', Checkbox.with({ label: 'S', default: true })],
|
||||
]);
|
||||
const plugin = createGlyphPlugin(args);
|
||||
expect(plugin.controlPanel).toBeDefined();
|
||||
expect(plugin.controlPanel.controlPanelSections.length).toBe(2);
|
||||
expect(typeof plugin.transformProps).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*"
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/glyph-core": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"dayjs": "^1.11.19",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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} />,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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} />,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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} />,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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} />,
|
||||
});
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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} />,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from '../images/thumbnail.png';
|
||||
import thumbnailDark from '../images/thumbnail-dark.png';
|
||||
import example1 from '../images/example1.png';
|
||||
import example1Dark from '../images/example1-dark.png';
|
||||
import example2 from '../images/example2.png';
|
||||
import example2Dark from '../images/example2-dark.png';
|
||||
import { CartodiagramPluginConstructorOpts } from '../types';
|
||||
import { getLayerConfig } from '../util/controlPanelUtil';
|
||||
|
||||
export default class CartodiagramPlugin extends ChartPlugin {
|
||||
constructor(opts: CartodiagramPluginConstructorOpts) {
|
||||
const metadata = new ChartMetadata({
|
||||
description:
|
||||
'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.',
|
||||
name: t('Cartodiagram'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
tags: [t('Geo'), t('2D'), t('Spatial'), t('Experimental')],
|
||||
category: t('Map'),
|
||||
exampleGallery: [
|
||||
{
|
||||
url: example1,
|
||||
urlDark: example1Dark,
|
||||
caption: t('Pie charts on a map'),
|
||||
},
|
||||
{
|
||||
url: example2,
|
||||
urlDark: example2Dark,
|
||||
caption: t('Line charts on a map'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (opts.defaultLayers) {
|
||||
const layerConfig = getLayerConfig(controlPanel);
|
||||
|
||||
// set defaults for layer config if found
|
||||
if (layerConfig) {
|
||||
layerConfig.config.default = opts.defaultLayers;
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Cannot set defaultLayers. layerConfig not found in control panel. Please check if the path to layerConfig should be adjusted.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('../CartodiagramPlugin'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps, getChartTransformPropsRegistry } from '@superset-ui/core';
|
||||
import {
|
||||
getChartConfigs,
|
||||
parseSelectedChart,
|
||||
} from '../util/transformPropsUtil';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, hooks, theme } = chartProps;
|
||||
const {
|
||||
geomColumn,
|
||||
selectedChart: selectedChartString,
|
||||
chartSize,
|
||||
layerConfigs,
|
||||
mapView,
|
||||
chartBackgroundColor,
|
||||
chartBackgroundBorderRadius,
|
||||
} = formData;
|
||||
const { setControlValue = () => {} } = hooks;
|
||||
const selectedChart = parseSelectedChart(selectedChartString);
|
||||
const transformPropsRegistry = getChartTransformPropsRegistry();
|
||||
const chartTransformer = transformPropsRegistry.get(selectedChart.viz_type);
|
||||
|
||||
const chartConfigs = getChartConfigs(
|
||||
selectedChart,
|
||||
geomColumn,
|
||||
chartProps,
|
||||
chartTransformer,
|
||||
);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
geomColumn,
|
||||
selectedChart,
|
||||
chartConfigs,
|
||||
chartVizType: selectedChart.viz_type,
|
||||
chartSize,
|
||||
layerConfigs,
|
||||
mapView,
|
||||
chartBackgroundColor,
|
||||
chartBackgroundBorderRadius,
|
||||
setControlValue,
|
||||
theme,
|
||||
};
|
||||
}
|
||||
@@ -1,208 +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 {
|
||||
SuperChart,
|
||||
VizType,
|
||||
getChartTransformPropsRegistry,
|
||||
} from '@superset-ui/core';
|
||||
import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram';
|
||||
import {
|
||||
EchartsPieChartPlugin,
|
||||
PieTransformProps,
|
||||
} from '@superset-ui/plugin-chart-echarts';
|
||||
import { withResizableChartDemo, dummyDatasource } from '@storybook-shared';
|
||||
import {
|
||||
defaultLayerConfigs,
|
||||
defaultMapView,
|
||||
defaultChartBackgroundColor,
|
||||
} from './data';
|
||||
|
||||
const VIZ_TYPE = 'cartodiagram';
|
||||
|
||||
// Register the pie chart plugin and its transformProps (same as Pie stories)
|
||||
new EchartsPieChartPlugin().configure({ key: VizType.Pie }).register();
|
||||
getChartTransformPropsRegistry().registerValue(VizType.Pie, PieTransformProps);
|
||||
|
||||
// Register the cartodiagram plugin
|
||||
new CartodiagramPlugin({}).configure({ key: VIZ_TYPE }).register();
|
||||
|
||||
// Sample data: each row has a geom column + the same format as pie chart data
|
||||
// The cartodiagram will group by geom location and create a pie for each
|
||||
const SF_GEOM = '{"type":"Point","coordinates":[-122.4194,37.7749]}';
|
||||
const LA_GEOM = '{"type":"Point","coordinates":[-118.2437,34.0522]}';
|
||||
|
||||
const sampleData = [
|
||||
// San Francisco data
|
||||
{ geom: SF_GEOM, category: 'Tech', 'SUM(revenue)': 1500000 },
|
||||
{ geom: SF_GEOM, category: 'Finance', 'SUM(revenue)': 800000 },
|
||||
{ geom: SF_GEOM, category: 'Healthcare', 'SUM(revenue)': 400000 },
|
||||
// Los Angeles data
|
||||
{ geom: LA_GEOM, category: 'Entertainment', 'SUM(revenue)': 3500000 },
|
||||
{ geom: LA_GEOM, category: 'Tech', 'SUM(revenue)': 2000000 },
|
||||
{ geom: LA_GEOM, category: 'Finance', 'SUM(revenue)': 1500000 },
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins/plugin-chart-cartodiagram',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
donut: false,
|
||||
showLabels: false,
|
||||
colorScheme: 'supersetColors',
|
||||
chartWidth: 100,
|
||||
chartHeight: 100,
|
||||
borderRadius: 8,
|
||||
},
|
||||
argTypes: {
|
||||
donut: {
|
||||
control: 'boolean',
|
||||
description: 'Display pie charts as donuts',
|
||||
},
|
||||
showLabels: {
|
||||
control: 'boolean',
|
||||
description: 'Show labels on embedded charts',
|
||||
},
|
||||
colorScheme: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'supersetColors',
|
||||
'd3Category10',
|
||||
'bnbColors',
|
||||
'googleCategory20c',
|
||||
],
|
||||
},
|
||||
chartWidth: {
|
||||
control: { type: 'range', min: 50, max: 200, step: 10 },
|
||||
description: 'Width of embedded charts in pixels',
|
||||
},
|
||||
chartHeight: {
|
||||
control: { type: 'range', min: 50, max: 200, step: 10 },
|
||||
description: 'Height of embedded charts in pixels',
|
||||
},
|
||||
borderRadius: {
|
||||
control: { type: 'range', min: 0, max: 50, step: 2 },
|
||||
description: 'Border radius of chart containers',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
The Cartodiagram plugin displays embedded charts on a map using OpenLayers.
|
||||
Each GeoJSON point location gets a small chart (pie, bar, etc.) rendered on it.
|
||||
|
||||
### Features
|
||||
- OpenLayers map with configurable base layers (OSM, WMS, WFS, XYZ)
|
||||
- Embedded Superset charts at GeoJSON point locations
|
||||
- Configurable chart size per zoom level
|
||||
- Chart background color and border radius customization
|
||||
|
||||
### How It Works
|
||||
1. Data includes a geometry column with GeoJSON Point strings
|
||||
2. The plugin groups data by location
|
||||
3. For each location, it calls the embedded chart's transformProps
|
||||
4. Mini charts are rendered as OpenLayers overlays
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const BasicMap = ({
|
||||
width,
|
||||
height,
|
||||
donut,
|
||||
showLabels,
|
||||
colorScheme,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
borderRadius,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
donut: boolean;
|
||||
showLabels: boolean;
|
||||
colorScheme: string;
|
||||
chartWidth: number;
|
||||
chartHeight: number;
|
||||
borderRadius: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType={VIZ_TYPE}
|
||||
width={width}
|
||||
height={height}
|
||||
datasource={dummyDatasource}
|
||||
queriesData={[
|
||||
{
|
||||
data: sampleData,
|
||||
colnames: ['geom', 'category', 'SUM(revenue)'],
|
||||
coltypes: [1, 1, 0],
|
||||
},
|
||||
]}
|
||||
formData={{
|
||||
datasource: '1__table',
|
||||
viz_type: VIZ_TYPE,
|
||||
geom_column: 'geom',
|
||||
selected_chart: JSON.stringify({
|
||||
viz_type: VizType.Pie,
|
||||
params: JSON.stringify({
|
||||
groupby: ['category'],
|
||||
metric: 'SUM(revenue)',
|
||||
colorScheme,
|
||||
numberFormat: 'SMART_NUMBER',
|
||||
showLabels,
|
||||
showLegend: false,
|
||||
donut,
|
||||
outerRadius: 70,
|
||||
}),
|
||||
}),
|
||||
chart_size: {
|
||||
type: 'FIXED',
|
||||
configs: { zoom: 6, width: chartWidth, height: chartHeight },
|
||||
values: {
|
||||
6: { width: chartWidth, height: chartHeight },
|
||||
7: { width: chartWidth + 20, height: chartHeight + 20 },
|
||||
8: { width: chartWidth + 40, height: chartHeight + 40 },
|
||||
},
|
||||
},
|
||||
layer_configs: defaultLayerConfigs,
|
||||
map_view: defaultMapView,
|
||||
chart_background_color: defaultChartBackgroundColor,
|
||||
chart_background_border_radius: borderRadius,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
BasicMap.parameters = {
|
||||
initialSize: {
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Shows an OpenLayers map with embedded pie charts at San Francisco and Los Angeles.
|
||||
Each pie chart shows the breakdown of revenue by industry sector for that city.
|
||||
|
||||
The map uses OpenStreetMap tiles as the base layer.
|
||||
`,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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 { CartodiagramPlugin } from '../src';
|
||||
|
||||
/**
|
||||
* The example tests in this file act as a starting point, and
|
||||
* we encourage you to build more. These tests check that the
|
||||
* plugin loads properly, and focus on `transformProps`
|
||||
* to ake sure that data, controls, and props are all
|
||||
* treated correctly (e.g. formData from plugin controls
|
||||
* properly transform the data and/or any resulting props).
|
||||
*/
|
||||
describe('CartodiagramPlugin', () => {
|
||||
test('exists', () => {
|
||||
expect(CartodiagramPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { getChartBuildQueryRegistry } from '@superset-ui/core';
|
||||
import buildQuery from '../../src/plugin/buildQuery';
|
||||
import { buildQuery } from '../../src/index';
|
||||
|
||||
describe('CartodiagramPlugin buildQuery', () => {
|
||||
const selectedChartParams = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user