blob: be06ecaa15b6ee16e58bea93029b4af84dd5ca6c [file] [log] [blame]
/**
* 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 {
FeatureFlag,
isFeatureEnabled,
t,
validateNonEmpty,
validateMapboxStylesUrl,
getCategoricalSchemeRegistry,
getSequentialSchemeRegistry,
SequentialScheme,
} from '@superset-ui/core';
import {
ControlPanelState,
CustomControlItem,
D3_FORMAT_OPTIONS,
getColorControlsProps,
sharedControls,
} from '@superset-ui/chart-controls';
import { columnChoices, PRIMARY_COLOR } from './controls';
import {
COLOR_SCHEME_TYPES,
ColorSchemeType,
isColorSchemeTypeVisible,
} from './utils';
import { TooltipTemplateControl } from './TooltipTemplateControl';
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const sequentialSchemeRegistry = getSequentialSchemeRegistry();
export const DEFAULT_DECKGL_COLOR = { r: 158, g: 158, b: 158, a: 1 };
let deckglTiles: string[][];
export const DEFAULT_DECKGL_TILES = [
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
[
'tile://https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
'Dark (OSM)',
],
['mapbox://styles/mapbox/streets-v9', 'Streets (Mapbox)'],
['mapbox://styles/mapbox/dark-v9', 'Dark (Mapbox)'],
['mapbox://styles/mapbox/light-v9', 'Light (Mapbox)'],
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets (Mapbox)'],
['mapbox://styles/mapbox/satellite-v9', 'Satellite (Mapbox)'],
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors (Mapbox)'],
];
const getDeckGLTiles = () => {
if (!deckglTiles) {
const appContainer = document.getElementById('app');
const { common } = JSON.parse(
appContainer?.getAttribute('data-bootstrap') || '{}',
);
deckglTiles = common?.deckgl_tiles ?? DEFAULT_DECKGL_TILES;
}
return deckglTiles;
};
const DEFAULT_VIEWPORT = {
longitude: 6.85236157047845,
latitude: 31.222656842808707,
zoom: 1,
bearing: 0,
pitch: 0,
};
const sandboxUrl =
'https://github.com/apache/superset/' +
'blob/master/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/sandbox.ts';
const jsFunctionInfo = (
<div>
{t(
'For more information about objects are in context in the scope of this function, refer to the',
)}
<a href={sandboxUrl}>{t(" source code of Superset's sandboxed parser")}.</a>
.
</div>
);
function jsFunctionControl(
label: string,
description: string,
extraDescr = null,
height = 100,
defaultText = '',
) {
return {
type: 'TextAreaControl',
language: 'javascript',
label,
description,
height,
default: defaultText,
aboveEditorSection: (
<div>
<p>{description}</p>
<p>{jsFunctionInfo}</p>
{extraDescr}
</div>
),
warning: !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)
? t(
'This functionality is disabled in your environment for security reasons.',
)
: null,
readOnly: !isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
};
}
export const filterNulls = {
name: 'filter_nulls',
config: {
type: 'CheckboxControl',
label: t('Ignore null locations'),
default: true,
description: t('Whether to ignore locations that are null'),
},
};
export const autozoom = {
name: 'autozoom',
config: {
type: 'CheckboxControl',
label: t('Auto Zoom'),
default: true,
renderTrigger: true,
description: t(
'When checked, the map will zoom to your data after each query',
),
},
};
export const dimension: CustomControlItem = {
name: 'dimension',
config: {
...sharedControls.groupby,
label: t('Dimension'),
description: t('Select a dimension'),
multi: false,
default: null,
},
};
export const jsColumns = {
name: 'js_columns',
config: {
...sharedControls.groupby,
label: t('Extra data for JS'),
default: [],
description: t(
'List of extra columns made available in JavaScript functions',
),
},
};
export const jsDataMutator = {
name: 'js_data_mutator',
config: jsFunctionControl(
t('JavaScript data interceptor'),
t(
'Define a javascript function that receives the data array used in the visualization ' +
'and is expected to return a modified version of that array. This can be used ' +
'to alter properties of the data, filter, or enrich the array.',
),
),
};
export const jsTooltip = {
name: 'js_tooltip',
config: jsFunctionControl(
t('JavaScript tooltip generator'),
t(
'Define a function that receives the input and outputs the content for a tooltip',
),
),
};
export const jsOnclickHref = {
name: 'js_onclick_href',
config: jsFunctionControl(
t('JavaScript onClick href'),
t('Define a function that returns a URL to navigate to when user clicks'),
),
};
export const legendFormat = {
name: 'legend_format',
config: {
label: t('Legend Format'),
description: t('Choose the format for legend values'),
type: 'SelectControl',
clearable: false,
default: D3_FORMAT_OPTIONS[0][0],
choices: D3_FORMAT_OPTIONS,
renderTrigger: true,
freeForm: true,
},
};
export const legendPosition = {
name: 'legend_position',
config: {
label: t('Legend Position'),
description: t('Choose the position of the legend'),
type: 'SelectControl',
clearable: false,
default: 'tr',
choices: [
[null, t('None')],
['tl', t('Top left')],
['tr', t('Top right')],
['bl', t('Bottom left')],
['br', t('Bottom right')],
],
renderTrigger: true,
},
};
export const lineColumn = {
name: 'line_column',
config: {
type: 'SelectControl',
label: t('Lines column'),
default: null,
description: t('The database columns that contains lines information'),
mapStateToProps: (state: ControlPanelState) => ({
choices: columnChoices(state.datasource),
}),
validators: [validateNonEmpty],
},
};
export const lineWidth = {
name: 'line_width',
config: {
type: 'TextControl',
label: t('Line width'),
renderTrigger: true,
isInt: true,
default: 1,
description: t('The width of the lines'),
},
};
export const fillColorPicker: CustomControlItem = {
name: 'fill_color_picker',
config: {
label: t('Fill Color'),
description: t(
' Set the opacity to 0 if you do not want to override the color specified in the GeoJSON',
),
type: 'ColorPickerControl',
default: PRIMARY_COLOR,
renderTrigger: true,
visibility: ({ controls }) =>
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.fixed_color),
},
};
export const strokeColorPicker: CustomControlItem = {
name: 'stroke_color_picker',
config: {
label: t('Stroke Color'),
description: t(
' Set the opacity to 0 if you do not want to override the color specified in the GeoJSON',
),
type: 'ColorPickerControl',
default: PRIMARY_COLOR,
renderTrigger: true,
visibility: ({ controls }) =>
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.fixed_color),
},
};
export const filled = {
name: 'filled',
config: {
type: 'CheckboxControl',
label: t('Filled'),
renderTrigger: true,
description: t('Whether to fill the objects'),
default: true,
},
};
export const stroked = {
name: 'stroked',
config: {
type: 'CheckboxControl',
label: t('Stroked'),
renderTrigger: true,
description: t('Whether to display the stroke'),
default: false,
},
};
export const extruded = {
name: 'extruded',
config: {
type: 'CheckboxControl',
label: t('Extruded'),
renderTrigger: true,
default: true,
description: t('Whether to make the grid 3D'),
},
};
export const gridSize = {
name: 'grid_size',
config: {
type: 'TextControl',
label: t('Grid Size'),
renderTrigger: true,
default: 20,
isInt: true,
description: t('Defines the grid size in pixels'),
},
};
export const viewport = {
name: 'viewport',
config: {
type: 'ViewportControl',
label: t('Viewport'),
renderTrigger: false,
description: t('Parameters related to the view and perspective on the map'),
default: DEFAULT_VIEWPORT,
dontRefreshOnChange: true,
},
};
export const spatial = {
name: 'spatial',
config: {
type: 'SpatialControl',
label: t('Longitude & Latitude'),
validators: [validateNonEmpty],
description: t('Point to your spatial columns'),
mapStateToProps: (state: ControlPanelState) => ({
choices: columnChoices(state.datasource),
}),
},
};
export const pointRadiusFixed = {
name: 'point_radius_fixed',
config: {
type: 'FixedOrMetricControl',
label: t('Point Size'),
default: { type: 'fix', value: 1000 },
description: t('Fixed point radius'),
mapStateToProps: (state: ControlPanelState) => ({
datasource: state.datasource,
}),
},
};
export const multiplier = {
name: 'multiplier',
config: {
type: 'TextControl',
label: t('Multiplier'),
isFloat: true,
renderTrigger: true,
default: 1,
description: t('Factor to multiply the metric by'),
},
};
export const lineType = {
name: 'line_type',
config: {
type: 'SelectControl',
label: t('Lines encoding'),
clearable: false,
default: 'json',
description: t('The encoding format of the lines'),
choices: [
['polyline', t('Polyline')],
['json', t('JSON')],
['geohash', t('geohash (square)')],
],
},
};
export const reverseLongLat = {
name: 'reverse_long_lat',
config: {
type: 'CheckboxControl',
label: t('Reverse Lat & Long'),
default: false,
},
};
export const mapboxStyle = {
name: 'mapbox_style',
config: {
type: 'SelectControl',
label: t('Map Style'),
clearable: false,
renderTrigger: true,
freeForm: true,
validators: [validateMapboxStylesUrl],
choices: getDeckGLTiles(),
default: getDeckGLTiles()[0][0],
description: t(
'Base layer map style. See Mapbox documentation: %s',
'Mapbox base layer map style (see Mapbox documentation: %s) or tile server URL.',
'https://docs.mapbox.com/help/glossary/style-url/',
),
},
};
export const geojsonColumn = {
name: 'geojson',
config: {
type: 'SelectControl',
label: t('GeoJson Column'),
validators: [validateNonEmpty],
description: t('Select the geojson column'),
mapStateToProps: (state: ControlPanelState) => ({
choices: columnChoices(state.datasource),
}),
},
};
const extractMetricsFromFormData = (formData: any) => {
const metrics = new Set<string>();
if (formData.metrics) {
(Array.isArray(formData.metrics)
? formData.metrics
: [formData.metrics]
).forEach((metric: any) => metrics.add(metric));
}
if (formData.point_radius_fixed?.value) {
metrics.add(formData.point_radius_fixed.value);
}
Object.entries(formData).forEach(([, value]) => {
if (!value || typeof value !== 'object') return;
if ((value as any).type === 'metric' && (value as any).value) {
metrics.add((value as any).value);
}
});
return Array.from(metrics).filter(metric => metric != null);
};
export const tooltipContents = {
name: 'tooltip_contents',
config: {
type: 'DndColumnMetricSelect',
label: t('Tooltip contents'),
multi: true,
freeForm: true,
clearable: true,
default: [],
description: t(
'Drag columns and metrics here to customize tooltip content. Order matters - items will appear in the same order in tooltips. Click the button to manually select columns and metrics.',
),
ghostButtonText: t('Drop columns/metrics here or click'),
disabledTabs: new Set(['saved', 'sqlExpression']),
mapStateToProps: (state: any) => {
const { datasource, form_data: formData } = state;
const selectedMetrics = formData
? extractMetricsFromFormData(formData)
: [];
return {
columns: datasource?.columns || [],
savedMetrics: datasource?.metrics || [],
datasource,
selectedMetrics,
disabledTabs: new Set(['saved', 'sqlExpression']),
formData,
};
},
},
};
export const tooltipTemplate = {
name: 'tooltip_template',
config: {
type: TooltipTemplateControl,
label: t('Customize tooltips template'),
debounceDelay: 30,
default: '',
description: '',
placeholder: '',
mapStateToProps: (state: any, control: any) => ({
value: control.value,
}),
},
};
export const deckGLCategoricalColorSchemeTypeSelect: CustomControlItem = {
name: 'color_scheme_type',
config: {
type: 'SelectControl',
label: t('Color Scheme Type'),
description: t('Select the type of color scheme to use.'),
clearable: false,
renderTrigger: true,
validators: [],
choices: [
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
[COLOR_SCHEME_TYPES.categorical_palette, t('Categorical palette')],
[COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')],
],
default: COLOR_SCHEME_TYPES.categorical_palette,
},
};
export const deckGLFixedColor: CustomControlItem = {
name: 'color_picker',
config: {
type: 'ColorPickerControl',
label: t('Fixed Color'),
default: PRIMARY_COLOR,
renderTrigger: true,
description: t('Select the fixed color'),
visibility: ({ controls }) =>
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.fixed_color),
},
};
export const deckGLCategoricalColor: CustomControlItem = {
name: dimension.name,
config: {
...dimension.config,
label: t('Categorical Color'),
description: t(
'Pick a dimension from which categorical colors are defined',
),
visibility: ({ controls }) =>
isColorSchemeTypeVisible(
controls,
COLOR_SCHEME_TYPES.categorical_palette,
),
},
};
export const deckGLCategoricalColorSchemeSelect: CustomControlItem = {
name: 'color_scheme',
config: {
type: 'ColorSchemeControl',
label: t('Color Scheme'),
default: categoricalSchemeRegistry.getDefaultKey(),
renderTrigger: true,
choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]),
description: t('The color scheme for rendering chart'),
schemes: () => categoricalSchemeRegistry.getMap(),
visibility: ({ controls }) =>
isColorSchemeTypeVisible(
controls,
COLOR_SCHEME_TYPES.categorical_palette,
),
},
};
export const deckGLLinearColorSchemeSelect: CustomControlItem = {
name: 'linear_color_scheme',
config: {
type: 'ColorSchemeControl',
label: t('Linear Color Scheme'),
choices: () =>
(sequentialSchemeRegistry.values() as SequentialScheme[]).map(value => [
value.id,
value.label,
]),
default: sequentialSchemeRegistry.getDefaultKey(),
clearable: false,
description: t('Select a linear color scheme'),
renderTrigger: true,
schemes: () => sequentialSchemeRegistry.getMap(),
isLinear: true,
mapStateToProps: state => getColorControlsProps(state),
visibility: ({ controls }) =>
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.linear_palette),
},
};
export const deckGLColorBreakpointsSelect: CustomControlItem = {
name: 'color_breakpoints',
config: {
label: t('Color breakpoints'),
type: 'ColorBreakpointsControl',
description: t('Define color breakpoints for the data'),
renderTrigger: true,
visibility: ({ controls }) =>
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.color_breakpoints),
},
};
export const breakpointsDefaultColor: CustomControlItem = {
name: 'default_breakpoint_color',
config: {
label: t('Default color'),
type: 'ColorPickerControl',
description: t(
"The color used when a value doesn't match any defined breakpoints.",
),
default: DEFAULT_DECKGL_COLOR,
renderTrigger: true,
visibility: ({ controls }) =>
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.color_breakpoints),
},
};
export const deckGLCategoricalColorSchemeControls = [
[deckGLCategoricalColorSchemeTypeSelect],
[deckGLFixedColor],
[deckGLCategoricalColor],
[deckGLCategoricalColorSchemeSelect],
[deckGLColorBreakpointsSelect],
];
export const generateDeckGLColorSchemeControls = ({
defaultSchemeType,
disableCategoricalColumn = false,
}: {
defaultSchemeType?: ColorSchemeType;
disableCategoricalColumn?: boolean;
}) => [
[
{
name: 'color_scheme_type',
config: {
type: 'SelectControl',
label: t('Color Scheme Type'),
description: t('Select the type of color scheme to use.'),
clearable: false,
renderTrigger: true,
validators: [],
choices: [
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
[COLOR_SCHEME_TYPES.categorical_palette, t('Categorical palette')],
[COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')],
],
default: defaultSchemeType || COLOR_SCHEME_TYPES.categorical_palette,
},
},
],
[deckGLFixedColor],
disableCategoricalColumn ? [] : [deckGLCategoricalColor],
[deckGLCategoricalColorSchemeSelect],
[breakpointsDefaultColor],
[deckGLColorBreakpointsSelect],
];