import React, { useEffect, useState, useMemo, useCallback, useContext } from 'react'; import { observer } from 'mobx-react-lite'; import { runInAction, set } from 'mobx'; import { useTranslation } from 'react-i18next'; import { useVizStore } from '../../store'; import { GLOBAL_CONFIG } from '../../config'; import { IConfigScale, IVisualConfig, IVisualLayout, IThemeKey } from '../../interfaces'; import Toggle from '../toggle'; import { ColorSchemes, extractRGBA } from './colorScheme'; import { DomainScale, RangeScale } from './range-scale'; import { ConfigItemContainer, ConfigItemContent, ConfigItemDescription, ConfigItemHeader, ConfigItemTitle } from './config-item'; import { KVTuple } from '../../models/utils'; import { isNotEmpty } from '../../utils'; import { timezones } from './timezone'; import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { Dialog, DialogContent, DialogFooter, DialogNormalContent } from '../ui/dialog'; import Combobox from '../dropdownSelect/combobox'; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu'; import { vegaThemeContext } from '../../store/theme'; import { ColorPickerComponent } from './color-picker'; import { Switch } from '../ui/switch'; function SettingRow(props: { title: React.ReactNode; description?: React.ReactNode; htmlFor?: string; children?: React.ReactNode }) { return (
{props.description &&

{props.description}

}
{props.children}
); } function useDomainScale() { const [enableMinDomain, setEnableMinDomain] = useState(false); const [enableMaxDomain, setEnableMaxDomain] = useState(false); const [domainMin, setDomainMin] = useState(0); const [domainMax, setDomainMax] = useState(100); const [enableType, setEnableType] = useState(false); const [type, setType] = useState<'linear' | 'log' | 'pow' | 'sqrt' | 'symlog'>('linear'); const setValue = useCallback((value: IConfigScale) => { setEnableMaxDomain(isNotEmpty(value.domainMax)); setEnableMinDomain(isNotEmpty(value.domainMin)); setEnableType(isNotEmpty(value.type)); setDomainMin(value.domainMin ?? 0); setDomainMax(value.domainMax ?? 100); setType(value.type ?? 'linear'); }, []); const value = useMemo( () => ({ ...(enableType ? { type } : {}), ...(enableMaxDomain ? { domainMax } : {}), ...(enableMinDomain ? { domainMin } : {}), }), [enableMaxDomain && domainMax, enableMinDomain && domainMin, enableType && type] ); return { value, setValue, enableMaxDomain, enableMinDomain, domainMax, domainMin, setEnableMinDomain, setEnableMaxDomain, setDomainMin, setDomainMax, enableType, type, setType, setEnableType, }; } function useScale(minRange: number, maxRange: number, defaultMinRange?: number, defaultMaxRange?: number) { const [enableMinDomain, setEnableMinDomain] = useState(false); const [enableMaxDomain, setEnableMaxDomain] = useState(false); const [enableRange, setEnableRange] = useState(false); const [domainMin, setDomainMin] = useState(0); const [domainMax, setDomainMax] = useState(100); const [rangeMin, setRangeMin] = useState(defaultMinRange ?? minRange); const [rangeMax, setRangeMax] = useState(defaultMaxRange ?? maxRange); const [enableType, setEnableType] = useState(false); const [type, setType] = useState<'linear' | 'log' | 'pow' | 'sqrt' | 'symlog'>('linear'); const setValue = useCallback( (value: IConfigScale) => { setEnableMaxDomain(isNotEmpty(value.domainMax)); setEnableMinDomain(isNotEmpty(value.domainMin)); setEnableRange(isNotEmpty(value.rangeMax) || isNotEmpty(value.rangeMin)); setEnableType(isNotEmpty(value.type)); setDomainMin(value.domainMin ?? 0); setDomainMax(value.domainMax ?? 100); setRangeMax(value.rangeMax ?? defaultMaxRange ?? maxRange); setRangeMin(value.rangeMin ?? defaultMinRange ?? minRange); setType(value.type ?? 'linear'); }, [defaultMaxRange, defaultMinRange, maxRange, minRange] ); const value = useMemo( () => ({ ...(enableType ? { type } : {}), ...(enableMaxDomain ? { domainMax } : {}), ...(enableMinDomain ? { domainMin } : {}), ...(enableRange ? { rangeMax, rangeMin } : {}), }), [enableMaxDomain && domainMax, enableMinDomain && domainMin, enableRange && rangeMax, enableRange && rangeMin, enableType && type] ); return { type, setType, enableType, setEnableType, value, setValue, enableMaxDomain, enableMinDomain, enableRange, rangeMax, rangeMin, domainMax, domainMin, setEnableMinDomain, setEnableMaxDomain, setEnableRange, setDomainMin, setDomainMax, setRangeMin, setRangeMax, }; } const VisualConfigPanel: React.FC = () => { const vizStore = useVizStore(); const { layout, showVisualConfigPanel, config } = vizStore; const { t } = useTranslation(); const formatConfigList: (keyof IVisualConfig['format'])[] = ['numberFormat', 'timeFormat', 'normalizedNumberFormat']; const [format, setFormat] = useState({ numberFormat: layout.format.numberFormat, timeFormat: layout.format.timeFormat, normalizedNumberFormat: layout.format.normalizedNumberFormat, }); const [resolve, setResolve] = useState({ x: layout.resolve.x, y: layout.resolve.y, color: layout.resolve.color, opacity: layout.resolve.opacity, shape: layout.resolve.shape, size: layout.resolve.size, }); const [zeroScale, setZeroScale] = useState(layout.zeroScale); const [svg, setSvg] = useState(layout.useSvg ?? false); const [scaleIncludeUnmatchedChoropleth, setScaleIncludeUnmatchedChoropleth] = useState(layout.scaleIncludeUnmatchedChoropleth ?? false); const [showAllGeoshapeInChoropleth, setShowAllGeoshapeInChoropleth] = useState(layout.showAllGeoshapeInChoropleth ?? false); const [renderer, setRenderer] = useState<'vega-lite' | 'observable-plot'>(layout.renderer ?? 'vega-lite'); const [background, setBackground] = useState({ r: 255, g: 255, b: 255, a: 0 }); const [defaultColor, setDefaultColor] = useState({ r: 91, g: 143, b: 249, a: 1 }); const [primaryColorEdited, setPrimaryColorEdited] = useState(false); const [backgroundEdited, setBackgroundEdited] = useState(false); const [displayColorPicker, setDisplayColorPicker] = useState(false); const [displayBackgroundPicker, setDisplayBackgroundPicker] = useState(false); const [colorPalette, setColorPalette] = useState(''); const { vizThemeConfig, setVizThemeConfig } = useContext(vegaThemeContext); const [themeKey, setThemeKey] = useState( typeof vizThemeConfig === 'string' ? vizThemeConfig : 'vega' ); const [geoMapTileUrl, setGeoMapTileUrl] = useState(undefined); const [displayOffset, setDisplayOffset] = useState(undefined); const [displayOffsetEdited, setDisplayOffsetEdited] = useState(false); const [enabledScales, setEnabledScales] = useState([]); const columnValue = useDomainScale(); const rowValue = useDomainScale(); const colorValue = useDomainScale(); const thetaValue = useDomainScale(); const radiusValue = useDomainScale(); const opacityValue = useScale(0, 1, 0.3, 0.8); const sizeValue = useScale(0, 100); const scalesSet = new Set(enabledScales); useEffect(() => { setZeroScale(layout.zeroScale); setSvg(layout.useSvg ?? false); setRenderer(layout.renderer ?? 'vega-lite'); setBackground( extractRGBA( { r: 255, g: 255, b: 255, a: 0, }, layout.background ) ); setResolve(layout.resolve); setDefaultColor(extractRGBA({ r: 91, g: 143, b: 249, a: 1 }, layout.primaryColor)); setPrimaryColorEdited(false); setBackgroundEdited(false); setScaleIncludeUnmatchedChoropleth(layout.scaleIncludeUnmatchedChoropleth ?? false); setFormat({ numberFormat: layout.format.numberFormat, timeFormat: layout.format.timeFormat, normalizedNumberFormat: layout.format.normalizedNumberFormat, }); setColorPalette(layout.colorPalette ?? ''); setThemeKey(typeof vizThemeConfig === 'string' ? vizThemeConfig : 'vega'); const enabledScales = Object.entries(layout.scale ?? {}) .filter(([_k, scale]) => Object.entries(scale).filter(([_k, v]) => !!v).length > 0) .map(([k]) => k); setEnabledScales(enabledScales.length === 0 ? ['opacity', 'size'] : enabledScales); columnValue.setValue(layout.scale?.column ?? {}); rowValue.setValue(layout.scale?.row ?? {}); colorValue.setValue(layout.scale?.color ?? {}); thetaValue.setValue(layout.scale?.theta ?? {}); radiusValue.setValue(layout.scale?.radius ?? {}); opacityValue.setValue(layout.scale?.opacity ?? {}); sizeValue.setValue(layout.scale?.size ?? {}); setGeoMapTileUrl(layout.geoMapTileUrl); setDisplayOffset(config.timezoneDisplayOffset); setDisplayOffsetEdited(false); }, [showVisualConfigPanel, vizThemeConfig]); return ( { vizStore.setShowVisualConfigPanel(false); }} > { setDisplayColorPicker(false); setDisplayBackgroundPicker(false); }} >
{t('config.colors')} {t('config.colors_desc')}
setThemeKey(v as IThemeKey)} options={[ { value: 'vega', label: 'vega' }, { value: 'g2', label: 'g2' }, { value: 'streamlit', label: 'streamlit' }, { value: 'danqing', label: 'danqing' }, { value: 'sodapop', label: 'sodapop' }, ]} /> ({ value: scheme.name, label: ( <>
{scheme.name}
{scheme.value.map((c, index) => { return
; })}
), })).concat({ value: '_none', label: <>{t('config.default_color_palette')}, })} />
{t('config.scale')} {t('config.scale_desc')}
{['row', 'column', 'color', 'theta', 'radius', 'opacity', 'size'].map((scale) => ( { if (e) { setEnabledScales((s) => [...s, scale]); } else { setEnabledScales((s) => s.filter((x) => x !== scale)); } }} > {t(`config.${scale}`)} ))}
{( [ ['column', ], ['row', ], ['color', ], ['theta', ], ['radius', ], ['opacity', ], ['size', ], ] as [string, React.ReactNode][] ) .filter(([key]) => scalesSet.has(key)) .map(([key, control]) => (

{t(`config.${key}`)}

{t(`config.scale_${key}_desc`)}

{control}
))} {enabledScales.length === 0 &&

{t('config.scale_empty')}

}
{t('config.format')} {t('config.format_desc')} {t(`config.formatGuidesDocs`)}:{' '} {t(`config.readHere`)} .
{formatConfigList.map((fc) => ( { setFormat((f) => ({ ...f, [fc]: e.target.value, })); }} /> ))}
{t('config.independence')} {t('config.independence_desc')}
{[ ...GLOBAL_CONFIG.POSITION_CHANNEL_CONFIG_LIST.map((pc) => [pc, t(`config.${pc}`)] as const), ...GLOBAL_CONFIG.NON_POSITION_CHANNEL_CONFIG_LIST.map((npc) => [npc, t(`constant.draggable_key.${npc}`)] as const), ].map(([channel, label]) => ( { setResolve((r) => ({ ...r, [channel]: e, })); }} /> ))}
{t('config.misc')}
{/* Map Configuration Group */}

{t('config.map_settings')}

{t('config.map_settings_desc')}

{t('config.customTile_desc')}

{ setGeoMapTileUrl(e ? 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' : undefined); }} />
{isNotEmpty(geoMapTileUrl) && ( { setGeoMapTileUrl(e.target.value); }} className="mt-4 max-w-md" placeholder="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> )}
{/* Choropleth Settings Group */}

{t('config.choropleth_settings')}

{t('config.choropleth_settings_desc')}

{t('config.include_unmatched_desc')}

{ setScaleIncludeUnmatchedChoropleth(en); }} />

{t('config.include_shapes_desc')}

{ setShowAllGeoshapeInChoropleth(en); }} />
{/* Visualization Settings Group */}

{t('config.visualization_settings')}

{t('config.visualization_settings_desc')}

{t('config.zeroScale_desc')}

{ setZeroScale(en); }} />

{t('config.svg_desc')}

{ setSvg(en); }} />

Choose between VegaLite and Observable Plot renderers

setRenderer(value as 'vega-lite' | 'observable-plot')} options={GLOBAL_CONFIG.RENDERER_TYPES.map((type) => ({ value: type, label: type === 'vega-lite' ? 'VegaLite' : 'Observable Plot', }))} />
{/* Timezone Settings Group */}

{t('config.timezone_settings')}

{t('config.timezone_settings_desc')}

{t('config.customOffset_desc')}

{ setDisplayOffsetEdited(true); setDisplayOffset(e ? new Date().getTimezoneOffset() : undefined); }} />
{isNotEmpty(displayOffset) && ( { setDisplayOffsetEdited(true); setDisplayOffset(parseInt(e)); }} options={timezones.map((tz) => ({ value: `${tz.value}`, label: {tz.name}, }))} /> )}
); }; export default observer(VisualConfigPanel);