diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.tsx
similarity index 81%
rename from superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts
rename to superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.tsx
index 9425f1211701..a4d7d689e7c0 100644
--- a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.tsx
@@ -21,6 +21,7 @@ import {
ControlPanelConfig,
getStandardizedControls,
} from '@superset-ui/chart-controls';
+import { RotationControl, ColorSchemeControl } from './controls';
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -63,25 +64,14 @@ const config: ControlPanelConfig = {
},
},
],
+ [],
[
- {
- name: 'rotation',
- config: {
- type: 'SelectControl',
- label: t('Word Rotation'),
- choices: [
- ['random', t('random')],
- ['flat', t('flat')],
- ['square', t('square')],
- ],
- renderTrigger: true,
- default: 'square',
- clearable: false,
- description: t('Rotation to apply to words in the cloud'),
- },
- },
+ ,
],
- ['color_scheme'],
],
},
],
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl.tsx
new file mode 100644
index 000000000000..f9f1859b1ffd
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl.tsx
@@ -0,0 +1,62 @@
+/**
+ * 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 { getCategoricalSchemeRegistry } from '@superset-ui/core';
+import InternalColorSchemeControl from './ColorSchemeControl/index';
+import { ColorSchemes } from './ColorSchemeControl/index';
+// NOTE: We copied the Explore ColorSchemeControl into this plugin to avoid
+// pulling the entire frontend src tree into this package’s tsconfig (importing
+// from src/ was dragging in fixtures, tests, and other plugins). Keep this copy
+// in sync with upstream changes, and consider moving it into a shared package
+// once the control-panel refactor settles so all consumers can reuse it.
+import { ControlComponentProps } from '@superset-ui/chart-controls';
+
+type ColorSchemeControlWrapperProps = ControlComponentProps & {
+ clearable?: boolean;
+};
+
+export default function ColorSchemeControlWrapper({
+ name = 'color_scheme',
+ value,
+ onChange,
+ clearable = true,
+ label,
+ description,
+ ...rest
+}: ColorSchemeControlWrapperProps) {
+ const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
+ const choices = categoricalSchemeRegistry.keys().map(s => [s, s]);
+ const schemes = categoricalSchemeRegistry.getMap() as ColorSchemes;
+
+ return (
+
+ );
+}
+
+ColorSchemeControlWrapper.displayName = 'ColorSchemeControlWrapper';
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/ColorSchemeLabel.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/ColorSchemeLabel.tsx
new file mode 100644
index 000000000000..135d7d1134b3
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/ColorSchemeLabel.tsx
@@ -0,0 +1,126 @@
+/**
+ * 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 { css, SupersetTheme } from '@apache-superset/core/ui';
+import { useRef, useState } from 'react';
+import { Tooltip } from '@superset-ui/core/components';
+
+type ColorSchemeLabelProps = {
+ colors: string[];
+ id: string;
+ label: string;
+};
+
+export default function ColorSchemeLabel(props: ColorSchemeLabelProps) {
+ const { id, label, colors } = props;
+ const [showTooltip, setShowTooltip] = useState(false);
+ const labelNameRef = useRef(null);
+ const labelsColorRef = useRef(null);
+ const handleShowTooltip = () => {
+ const labelNameElement = labelNameRef.current;
+ const labelsColorElement = labelsColorRef.current;
+ if (
+ labelNameElement &&
+ labelsColorElement &&
+ (labelNameElement.scrollWidth > labelNameElement.offsetWidth ||
+ labelNameElement.scrollHeight > labelNameElement.offsetHeight ||
+ labelsColorElement.scrollWidth > labelsColorElement.offsetWidth ||
+ labelsColorElement.scrollHeight > labelsColorElement.offsetHeight)
+ ) {
+ setShowTooltip(true);
+ }
+ };
+ const handleHideTooltip = () => {
+ setShowTooltip(false);
+ };
+
+ const colorsList = () =>
+ colors.map((color: string, i: number) => (
+ css`
+ padding-left: ${theme.sizeUnit / 2}px;
+ :before {
+ content: '';
+ display: inline-block;
+ background-color: ${color};
+ border: 1px solid ${color === 'white' ? 'black' : color};
+ width: 9px;
+ height: 10px;
+ }
+ `}
+ />
+ ));
+
+ const tooltipContent = () => (
+ <>
+ {label}
+
{colorsList()}
+ >
+ );
+
+ return (
+
+
+ css`
+ min-width: 125px;
+ padding-right: ${theme.sizeUnit * 2}px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ `}
+ >
+ {label}
+
+ css`
+ flex: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ padding-right: ${theme.sizeUnit}px;
+ `}
+ >
+ {colorsList()}
+
+
+
+ );
+}
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/index.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/index.tsx
new file mode 100644
index 000000000000..22983ca28629
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/index.tsx
@@ -0,0 +1,333 @@
+/**
+ * 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 { useMemo, ReactNode } from 'react';
+
+import {
+ ColorScheme,
+ ColorSchemeGroup,
+ SequentialScheme,
+ t,
+ getLabelsColorMap,
+ CategoricalColorNamespace,
+} from '@superset-ui/core';
+import { css, useTheme } from '@apache-superset/core/ui';
+import { sortBy } from 'lodash';
+import { ControlHeader } from '@superset-ui/chart-controls';
+import {
+ Tooltip,
+ Select,
+ type SelectOptionsType,
+} from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import ColorSchemeLabel from './ColorSchemeLabel';
+
+const getColorNamespace = (namespace?: string) => namespace || undefined;
+
+export type OptionData = SelectOptionsType[number]['options'][number] & {
+ searchText?: string;
+};
+
+export interface ColorSchemes {
+ [key: string]: ColorScheme;
+}
+
+export interface ColorSchemeControlProps {
+ hasCustomLabelsColor: boolean;
+ hasDashboardColorScheme?: boolean;
+ hasSharedLabelsColor?: boolean;
+ sharedLabelsColors?: string[];
+ mapLabelsColors?: Record;
+ colorNamespace?: string;
+ chartId?: number;
+ dashboardId?: number;
+ label?: string;
+ name: string;
+ onChange?: (value: string) => void;
+ value: string;
+ clearable: boolean;
+ defaultScheme?: string;
+ choices: string[][] | (() => string[][]);
+ schemes: ColorSchemes | (() => ColorSchemes);
+ isLinear?: boolean;
+ description?: string;
+ hovered?: boolean;
+}
+
+const CUSTOM_LABEL_ALERT = t(
+ `The colors of this chart might be overridden by custom label colors of the related dashboard.
+ Check the JSON metadata in the Advanced settings.`,
+);
+
+const DASHBOARD_ALERT = t(
+ `The color scheme is determined by the related dashboard.
+ Edit the color scheme in the dashboard properties.`,
+);
+
+const DASHBOARD_CONTEXT_ALERT = t(
+ `You are viewing this chart in a dashboard context with labels shared across multiple charts.
+ The color scheme selection is disabled.`,
+);
+
+const DASHBOARD_CONTEXT_TOOLTIP = t(
+ `You are viewing this chart in the context of a dashboard that is directly affecting its colors.
+ To edit the color scheme, open this chart outside of the dashboard.`,
+);
+
+const Label = ({
+ label,
+ dashboardId,
+ hasSharedLabelsColor,
+ hasCustomLabelsColor,
+ hasDashboardColorScheme,
+}: Pick<
+ ColorSchemeControlProps,
+ | 'label'
+ | 'dashboardId'
+ | 'hasCustomLabelsColor'
+ | 'hasSharedLabelsColor'
+ | 'hasDashboardColorScheme'
+>) => {
+ const theme = useTheme();
+ if (hasSharedLabelsColor || hasCustomLabelsColor || hasDashboardColorScheme) {
+ const alertTitle =
+ hasCustomLabelsColor && !hasSharedLabelsColor
+ ? CUSTOM_LABEL_ALERT
+ : dashboardId && hasDashboardColorScheme
+ ? DASHBOARD_ALERT
+ : DASHBOARD_CONTEXT_ALERT;
+ return (
+ <>
+ {label}{' '}
+
+
+
+ >
+ );
+ }
+ return <>{label}>;
+};
+
+const ColorSchemeControl = ({
+ hasCustomLabelsColor = false,
+ hasDashboardColorScheme = false,
+ mapLabelsColors = {},
+ sharedLabelsColors = [],
+ dashboardId,
+ colorNamespace,
+ chartId,
+ label = t('Color scheme'),
+ onChange = () => {},
+ value,
+ clearable = false,
+ defaultScheme,
+ choices = [],
+ schemes = {},
+ isLinear,
+ ...rest
+}: ColorSchemeControlProps) => {
+ const countSharedLabelsColor = sharedLabelsColors.length;
+ const colorMapInstance = getLabelsColorMap();
+ const chartLabels = chartId
+ ? colorMapInstance.chartsLabelsMap.get(chartId)?.labels || []
+ : [];
+ const hasSharedLabelsColor = !!(
+ dashboardId &&
+ countSharedLabelsColor > 0 &&
+ chartLabels.some(label => sharedLabelsColors.includes(label))
+ );
+ const hasDashboardScheme = dashboardId && hasDashboardColorScheme;
+ const showDashboardLockedOption = hasDashboardScheme || hasSharedLabelsColor;
+ const theme = useTheme();
+ const currentScheme = useMemo(() => {
+ if (showDashboardLockedOption) {
+ return 'dashboard';
+ }
+ let result = value || defaultScheme;
+ if (result === 'SUPERSET_DEFAULT') {
+ const schemesObject = typeof schemes === 'function' ? schemes() : schemes;
+ result = schemesObject?.SUPERSET_DEFAULT?.id;
+ }
+ return result;
+ }, [defaultScheme, schemes, showDashboardLockedOption, value]);
+
+ const options = useMemo(() => {
+ if (showDashboardLockedOption) {
+ return [
+ {
+ value: 'dashboard',
+ label: (
+
+ {t('Dashboard scheme')}
+
+ ),
+ },
+ ];
+ }
+ const schemesObject = typeof schemes === 'function' ? schemes() : schemes;
+ const controlChoices = typeof choices === 'function' ? choices() : choices;
+ const allColorOptions: string[] = [];
+ const filteredColorOptions = controlChoices.filter(o => {
+ const option = o[0];
+ const isValidColorOption =
+ option !== 'SUPERSET_DEFAULT' && !allColorOptions.includes(option);
+ allColorOptions.push(option);
+ return isValidColorOption;
+ });
+
+ const groups = filteredColorOptions.reduce(
+ (acc, [value]) => {
+ const currentScheme = schemesObject[value];
+
+ // For categorical scheme, display all the colors
+ // For sequential scheme, show 10 or interpolate to 10.
+ // Sequential schemes usually have at most 10 colors.
+ let colors: string[] = [];
+ if (currentScheme) {
+ colors = isLinear
+ ? (currentScheme as SequentialScheme).getColors(10)
+ : currentScheme.colors;
+ }
+ const option = {
+ label: (
+
+ ) as ReactNode,
+ value,
+ searchText: currentScheme.label,
+ };
+ acc[currentScheme.group ?? ColorSchemeGroup.Other].options.push(option);
+ return acc;
+ },
+ {
+ [ColorSchemeGroup.Custom]: {
+ title: ColorSchemeGroup.Custom,
+ label: t('Custom color palettes'),
+ options: [] as OptionData[],
+ },
+ [ColorSchemeGroup.Featured]: {
+ title: ColorSchemeGroup.Featured,
+ label: t('Featured color palettes'),
+ options: [] as OptionData[],
+ },
+ [ColorSchemeGroup.Other]: {
+ title: ColorSchemeGroup.Other,
+ label: t('Other color palettes'),
+ options: [] as OptionData[],
+ },
+ },
+ );
+ const nonEmptyGroups = Object.values(groups)
+ .filter(group => group.options.length > 0)
+ .map(group => ({
+ ...group,
+ options: sortBy(group.options, opt => opt.label),
+ }));
+
+ // if there are no featured or custom color schemes, return the ungrouped options
+ if (
+ nonEmptyGroups.length === 1 &&
+ nonEmptyGroups[0].title === ColorSchemeGroup.Other
+ ) {
+ return nonEmptyGroups[0].options.map(opt => ({
+ value: opt.value,
+ label: opt.customLabel || opt.label,
+ }));
+ }
+ return nonEmptyGroups.map(group => ({
+ label: group.label,
+ options: group.options.map(opt => ({
+ value: opt.value,
+ label: opt.customLabel || opt.label,
+ searchText: opt.searchText,
+ })),
+ }));
+ }, [choices, hasDashboardScheme, hasSharedLabelsColor, isLinear, schemes]);
+
+ // We can't pass on change directly because it receives a second
+ // parameter and it would be interpreted as the error parameter
+ const handleOnChange = (value: string) => {
+ if (chartId) {
+ colorMapInstance.setOwnColorScheme(chartId, value);
+ if (dashboardId) {
+ const colorNameSpace = getColorNamespace(colorNamespace);
+ const categoricalNamespace =
+ CategoricalColorNamespace.getNamespace(colorNameSpace);
+
+ const sharedLabelsSet = new Set(sharedLabelsColors);
+ // reset colors except shared and custom labels to keep dashboard consistency
+ const resettableLabels = Object.keys(mapLabelsColors).filter(
+ l => !sharedLabelsSet.has(l),
+ );
+ categoricalNamespace.resetColorsForLabels(resettableLabels);
+ }
+ }
+
+ onChange(value);
+ };
+
+ return (
+ <>
+
+ }
+ />
+