diff --git a/.gitignore b/.gitignore index 81c44731de5..a23cbb9ba5a 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,5 @@ release.json messages.mo docker/requirements-local.txt + +cache/ diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 24c235692a7..e54aa9df3ec 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -136,6 +136,7 @@ "rison": "^0.1.1", "scroll-into-view-if-needed": "^2.2.28", "shortid": "^2.2.6", + "tinycolor2": "^1.4.2", "urijs": "^1.19.8", "use-immer": "^0.6.0", "use-query-params": "^1.1.9", @@ -201,6 +202,7 @@ "@types/rison": "0.0.6", "@types/shortid": "^0.0.29", "@types/sinon": "^9.0.5", + "@types/tinycolor2": "^1.4.3", "@types/yargs": "12 - 15", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", @@ -22525,6 +22527,11 @@ "resolved": "https://registry.npmjs.org/@types/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", "integrity": "sha512-AQ6zewa0ucLJvtUi5HsErbOFKAcQfRLt9zFLlUOvcXBy2G36a+ZDpCHSGdzJVUD8aNURtIjh9aSjCStNMRCcRQ==" }, + "node_modules/@types/tinycolor2": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.3.tgz", + "integrity": "sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==" + }, "node_modules/@types/uglify-js": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", @@ -53688,9 +53695,9 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "node_modules/tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", + "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==", "engines": { "node": "*" } @@ -58702,6 +58709,7 @@ "@types/prop-types": "^15.7.2", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", + "@types/tinycolor2": "^1.4.3", "@vx/responsive": "^0.0.199", "csstype": "^2.6.4", "d3-format": "^1.3.2", @@ -75807,6 +75815,7 @@ "@types/prop-types": "^15.7.2", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", + "@types/tinycolor2": "^1.4.3", "@vx/responsive": "^0.0.199", "csstype": "^2.6.4", "d3-format": "^1.3.2", @@ -77790,6 +77799,11 @@ "resolved": "https://registry.npmjs.org/@types/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", "integrity": "sha512-AQ6zewa0ucLJvtUi5HsErbOFKAcQfRLt9zFLlUOvcXBy2G36a+ZDpCHSGdzJVUD8aNURtIjh9aSjCStNMRCcRQ==" }, + "@types/tinycolor2": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.3.tgz", + "integrity": "sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==" + }, "@types/uglify-js": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", @@ -102080,9 +102094,9 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", + "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" }, "tinyqueue": { "version": "2.0.3", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index d8161d0d7d6..31a95ca672a 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -196,6 +196,7 @@ "rison": "^0.1.1", "scroll-into-view-if-needed": "^2.2.28", "shortid": "^2.2.6", + "tinycolor2": "^1.4.2", "urijs": "^1.19.8", "use-immer": "^0.6.0", "use-query-params": "^1.1.9", @@ -261,6 +262,7 @@ "@types/rison": "0.0.6", "@types/shortid": "^0.0.29", "@types/sinon": "^9.0.5", + "@types/tinycolor2": "^1.4.3", "@types/yargs": "12 - 15", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index aee3717938c..ff15a6f4a92 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -205,6 +205,9 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = { renderTrigger: true, schemes: () => sequentialSchemeRegistry.getMap(), isLinear: true, + mapStateToProps: state => ({ + dashboardId: state?.form_data?.dashboardId, + }), }; const secondary_metric: SharedControlConfig<'MetricsControl'> = { diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index 0b9713bc434..28937d298ad 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -42,6 +42,7 @@ "@types/math-expression-evaluator": "^1.2.1", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", + "@types/tinycolor2": "^1.4.3", "@types/fetch-mock": "^7.3.3", "@types/enzyme": "^3.10.5", "@types/prop-types": "^15.7.2", diff --git a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts index 63b2cb55f67..d34960dac09 100644 --- a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts +++ b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts @@ -22,12 +22,12 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale'; import { ExtensibleFunction } from '../models'; import { ColorsLookup } from './types'; import stringifyAndTrim from './stringifyAndTrim'; +import getSharedLabelColor from './SharedLabelColorSingleton'; // Use type augmentation to correct the fact that // an instance of CategoricalScale is also a function - interface CategoricalColorScale { - (x: { toString(): string }): string; + (x: { toString(): string }, y?: number): string; } class CategoricalColorScale extends ExtensibleFunction { @@ -46,7 +46,7 @@ class CategoricalColorScale extends ExtensibleFunction { * (usually CategoricalColorNamespace) and supersede this.forcedColors */ constructor(colors: string[], parentForcedColors?: ColorsLookup) { - super((value: string) => this.getColor(value)); + super((value: string, sliceId?: number) => this.getColor(value, sliceId)); this.colors = colors; this.scale = scaleOrdinal<{ toString(): string }, string>(); @@ -55,20 +55,27 @@ class CategoricalColorScale extends ExtensibleFunction { this.forcedColors = {}; } - getColor(value?: string) { + getColor(value?: string, sliceId?: number) { const cleanedValue = stringifyAndTrim(value); + const sharedLabelColor = getSharedLabelColor(); + const parentColor = this.parentForcedColors && this.parentForcedColors[cleanedValue]; if (parentColor) { + sharedLabelColor.addSlice(cleanedValue, parentColor, sliceId); return parentColor; } const forcedColor = this.forcedColors[cleanedValue]; if (forcedColor) { + sharedLabelColor.addSlice(cleanedValue, forcedColor, sliceId); return forcedColor; } - return this.scale(cleanedValue); + const color = this.scale(cleanedValue); + sharedLabelColor.addSlice(cleanedValue, color, sliceId); + + return color; } /** diff --git a/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts b/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts new file mode 100644 index 00000000000..227b565276a --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts @@ -0,0 +1,130 @@ +/* + * 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 tinycolor from 'tinycolor2'; +import { CategoricalColorNamespace } from '.'; +import makeSingleton from '../utils/makeSingleton'; + +export class SharedLabelColor { + sliceLabelColorMap: Record>; + + constructor() { + // { sliceId1: { label1: color1 }, sliceId2: { label2: color2 } } + this.sliceLabelColorMap = {}; + } + + getColorMap( + colorNamespace?: string, + colorScheme?: string, + updateColorScheme?: boolean, + ) { + if (colorScheme) { + const categoricalNamespace = + CategoricalColorNamespace.getNamespace(colorNamespace); + const colors = categoricalNamespace.getScale(colorScheme).range(); + const sharedLabels = this.getSharedLabels(); + const generatedColors: tinycolor.Instance[] = []; + let sharedLabelMap; + + if (sharedLabels.length) { + const multiple = Math.ceil(sharedLabels.length / colors.length); + const ext = 5; + const analogousColors = colors.map(color => { + const result = tinycolor(color).analogous(multiple + ext); + return result.slice(ext); + }); + + // [[A, AA, AAA], [B, BB, BBB]] => [A, B, AA, BB, AAA, BBB] + while (analogousColors[analogousColors.length - 1]?.length) { + analogousColors.forEach(colors => + generatedColors.push(colors.shift() as tinycolor.Instance), + ); + } + sharedLabelMap = sharedLabels.reduce( + (res, label, index) => ({ + ...res, + [label.toString()]: generatedColors[index]?.toHexString(), + }), + {}, + ); + } + + const labelMap = Object.keys(this.sliceLabelColorMap).reduce( + (res, sliceId) => { + const colorScale = categoricalNamespace.getScale(colorScheme); + return { + ...res, + ...Object.keys(this.sliceLabelColorMap[sliceId]).reduce( + (res, label) => ({ + ...res, + [label]: updateColorScheme + ? colorScale(label) + : this.sliceLabelColorMap[sliceId][label], + }), + {}, + ), + }; + }, + {}, + ); + + return { + ...labelMap, + ...sharedLabelMap, + }; + } + return undefined; + } + + addSlice(label: string, color: string, sliceId?: number) { + if (!sliceId) return; + this.sliceLabelColorMap[sliceId] = { + ...this.sliceLabelColorMap[sliceId], + [label]: color, + }; + } + + removeSlice(sliceId: number) { + delete this.sliceLabelColorMap[sliceId]; + } + + clear() { + this.sliceLabelColorMap = {}; + } + + getSharedLabels() { + const tempLabels = new Set(); + const result = new Set(); + Object.keys(this.sliceLabelColorMap).forEach(sliceId => { + const colorMap = this.sliceLabelColorMap[sliceId]; + Object.keys(colorMap).forEach(label => { + if (tempLabels.has(label) && !result.has(label)) { + result.add(label); + } else { + tempLabels.add(label); + } + }); + }); + return [...result]; + } +} + +const getInstance = makeSingleton(SharedLabelColor); + +export default getInstance; diff --git a/superset-frontend/packages/superset-ui-core/src/color/index.ts b/superset-frontend/packages/superset-ui-core/src/color/index.ts index 0f7ce6194c6..e1cde3ba3e2 100644 --- a/superset-frontend/packages/superset-ui-core/src/color/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/color/index.ts @@ -32,5 +32,9 @@ export * from './SequentialScheme'; export { default as ColorSchemeRegistry } from './ColorSchemeRegistry'; export * from './colorSchemes'; export * from './utils'; +export { + default as getSharedLabelColor, + SharedLabelColor, +} from './SharedLabelColorSingleton'; export const BRAND_COLOR = '#00A699'; diff --git a/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts b/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts new file mode 100644 index 00000000000..560bd56e680 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { + CategoricalScheme, + getCategoricalSchemeRegistry, + getSharedLabelColor, + SharedLabelColor, +} from '@superset-ui/core'; + +describe('SharedLabelColor', () => { + beforeAll(() => { + getCategoricalSchemeRegistry() + .registerValue( + 'testColors', + new CategoricalScheme({ + id: 'testColors', + colors: ['red', 'green', 'blue'], + }), + ) + .registerValue( + 'testColors2', + new CategoricalScheme({ + id: 'testColors2', + colors: ['yellow', 'green', 'blue'], + }), + ); + }); + + beforeEach(() => { + getSharedLabelColor().clear(); + }); + + it('has default value out-of-the-box', () => { + expect(getSharedLabelColor()).toBeInstanceOf(SharedLabelColor); + }); + + describe('.addSlice(value, color, sliceId)', () => { + it('should add to valueSliceMap when first adding label', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + expect(sharedLabelColor.sliceLabelColorMap).toHaveProperty('1', { + a: 'red', + }); + }); + + it('do nothing when sliceId is undefined', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red'); + expect(sharedLabelColor.sliceLabelColorMap).toEqual({}); + }); + }); + + describe('.remove(sliceId)', () => { + it('should remove sliceId', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.removeSlice(1); + expect(sharedLabelColor.sliceLabelColorMap).toEqual({}); + }); + }); + + describe('.getColorMap(namespace, scheme, updateColorScheme)', () => { + it('return undefined when scheme is undefined', () => { + const sharedLabelColor = getSharedLabelColor(); + const colorMap = sharedLabelColor.getColorMap(); + expect(colorMap).toBeUndefined(); + }); + + it('return undefined value if pass updateColorScheme', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.addSlice('b', 'blue', 2); + const colorMap = sharedLabelColor.getColorMap('', 'testColors2', true); + expect(colorMap).toEqual({ a: 'yellow', b: 'yellow' }); + }); + + it('return color value if not pass updateColorScheme', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.addSlice('b', 'blue', 2); + const colorMap = sharedLabelColor.getColorMap('', 'testColors'); + expect(colorMap).toEqual({ a: 'red', b: 'blue' }); + }); + + it('return color value if shared label exit', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.addSlice('a', 'blue', 2); + const colorMap = sharedLabelColor.getColorMap('', 'testColors'); + expect(colorMap).not.toEqual({}); + }); + }); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js index d36083e6cb4..d0aed798de9 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js @@ -36,7 +36,7 @@ const propTypes = { }; function Chord(element, props) { - const { data, width, height, numberFormat, colorScheme } = props; + const { data, width, height, numberFormat, colorScheme, sliceId } = props; element.innerHTML = ''; @@ -93,7 +93,7 @@ function Chord(element, props) { .append('path') .attr('id', (d, i) => `group${i}`) .attr('d', arc) - .style('fill', (d, i) => colorFn(nodes[i])); + .style('fill', (d, i) => colorFn(nodes[i], sliceId)); // Add a text label. const groupText = group.append('text').attr('x', 6).attr('dy', 15); @@ -121,7 +121,7 @@ function Chord(element, props) { .on('mouseover', d => { chord.classed('fade', p => p !== d); }) - .style('fill', d => colorFn(nodes[d.source.index])) + .style('fill', d => colorFn(nodes[d.source.index], sliceId)) .attr('d', path); // Add an elaborate mouseover title for each chord. diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js index 4c9d09517f6..7503ff4ea1f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js @@ -18,7 +18,7 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { yAxisFormat, colorScheme } = formData; + const { yAxisFormat, colorScheme, sliceId } = formData; return { colorScheme, @@ -26,5 +26,6 @@ export default function transformProps(chartProps) { height, numberFormat: yAxisFormat, width, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js index ef265691999..d363e8a5980 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js @@ -23,6 +23,7 @@ import { extent as d3Extent } from 'd3-array'; import { getNumberFormatter, getSequentialSchemeRegistry, + CategoricalColorNamespace, } from '@superset-ui/core'; import countries, { countryOptions } from './countries'; import './CountryMap.css'; @@ -45,17 +46,29 @@ const propTypes = { const maps = {}; function CountryMap(element, props) { - const { data, width, height, country, linearColorScheme, numberFormat } = - props; + const { + data, + width, + height, + country, + linearColorScheme, + numberFormat, + colorScheme, + sliceId, + } = props; const container = element; const format = getNumberFormatter(numberFormat); - const colorScale = getSequentialSchemeRegistry() + const linearColorScale = getSequentialSchemeRegistry() .get(linearColorScheme) .createLinearScale(d3Extent(data, v => v.metric)); + const colorScale = CategoricalColorNamespace.getScale(colorScheme); + const colorMap = {}; data.forEach(d => { - colorMap[d.country_id] = colorScale(d.metric); + colorMap[d.country_id] = colorScheme + ? colorScale(d.country_id, sliceId) + : linearColorScale(d.metric); }); const colorFn = d => colorMap[d.properties.ISO] || 'none'; diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js index 4120f621f1a..8789c3d2f34 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js @@ -18,7 +18,13 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { linearColorScheme, numberFormat, selectCountry } = formData; + const { + linearColorScheme, + numberFormat, + selectCountry, + colorScheme, + sliceId, + } = formData; return { width, @@ -27,5 +33,7 @@ export default function transformProps(chartProps) { country: selectCountry ? String(selectCountry).toLowerCase() : null, linearColorScheme, numberFormat, + colorScheme, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx index 518272afdff..2c072677486 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx @@ -71,13 +71,14 @@ class CustomHistogram extends React.PureComponent { xAxisLabel, yAxisLabel, showLegend, + sliceId, } = this.props; const colorFn = CategoricalColorNamespace.getScale(colorScheme); const keys = data.map(d => d.key); const colorScale = scaleOrdinal({ domain: keys, - range: keys.map(x => colorFn(x)), + range: keys.map(x => colorFn(x, sliceId)), }); return ( diff --git a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js index 4a5782c7172..1de22324049 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js @@ -27,6 +27,7 @@ export default function transformProps(chartProps) { xAxisLabel, yAxisLabel, showLegend, + sliceId, } = formData; return { @@ -41,5 +42,6 @@ export default function transformProps(chartProps) { xAxisLabel, yAxisLabel, showLegend, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js index 224ff85af7d..5355530cd53 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js @@ -119,6 +119,7 @@ function Icicle(element, props) { partitionThreshold, useRichTooltip, timeSeriesOption = 'not_time', + sliceId, } = props; const div = d3.select(element); @@ -385,7 +386,7 @@ function Icicle(element, props) { // Apply color scheme g.selectAll('rect').style('fill', d => { - d.color = colorFn(d.name); + d.color = colorFn(d.name, sliceId); return d.color; }); diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js index d69de4ed52f..da58cd61608 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js @@ -30,6 +30,7 @@ export default function transformProps(chartProps) { partitionThreshold, richTooltip, timeSeriesOption, + sliceId, } = formData; const { verboseMap } = datasource; @@ -48,5 +49,6 @@ export default function transformProps(chartProps) { timeSeriesOption, useLogScale: logScale, useRichTooltip: richTooltip, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js index 7ad4fd508ea..4d7ef2b8ed9 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js @@ -76,6 +76,7 @@ function Rose(element, props) { numberFormat, useRichTooltip, useAreaProportions, + sliceId, } = props; const div = d3.select(element); @@ -120,10 +121,10 @@ function Rose(element, props) { .map(v => ({ key: v.name, value: v.value, - color: colorFn(v.name), + color: colorFn(v.name, sliceId), highlight: v.id === d.arcId, })) - : [{ key: d.name, value: d.val, color: colorFn(d.name) }]; + : [{ key: d.name, value: d.val, color: colorFn(d.name, sliceId) }]; return { key: 'Date', @@ -132,7 +133,7 @@ function Rose(element, props) { }; } - legend.width(width).color(d => colorFn(d.key)); + legend.width(width).color(d => colorFn(d.key, sliceId)); legendWrap.datum(legendData(datum)).call(legend); tooltip.headerFormatter(timeFormat).valueFormatter(format); @@ -378,7 +379,7 @@ function Rose(element, props) { const arcs = ae .append('path') .attr('class', 'arc') - .attr('fill', d => colorFn(d.name)) + .attr('fill', d => colorFn(d.name, sliceId)) .attr('d', arc); function mousemove() { diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js index 1e5407b16d9..b907e40ecff 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js @@ -24,6 +24,7 @@ export default function transformProps(chartProps) { numberFormat, richTooltip, roseAreaProportion, + sliceId, } = formData; return { @@ -35,5 +36,6 @@ export default function transformProps(chartProps) { numberFormat, useAreaProportions: roseAreaProportion, useRichTooltip: richTooltip, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js index bddf570026b..4d6f059fdeb 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js @@ -84,7 +84,7 @@ function computeGraph(links) { } function SankeyLoop(element, props) { - const { data, width, height, colorScheme } = props; + const { data, width, height, colorScheme, sliceId } = props; const color = CategoricalColorNamespace.getScale(colorScheme); const margin = { ...defaultMargin, ...props.margin }; const innerWidth = width - margin.left - margin.right; @@ -109,7 +109,7 @@ function SankeyLoop(element, props) { value / sValue, )})`, ) - .linkColor(d => color(d.source.name)); + .linkColor(d => color(d.source.name, sliceId)); const div = select(element); div.selectAll('*').remove(); diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js index b62639a87fe..76c0c220a76 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js @@ -18,7 +18,7 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData, margin } = chartProps; - const { colorScheme } = formData; + const { colorScheme, sliceId } = formData; return { width, @@ -26,5 +26,6 @@ export default function transformProps(chartProps) { data: queriesData[0].data, colorScheme, margin, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js index b847f754133..d8c8f61e441 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js @@ -44,7 +44,7 @@ const propTypes = { const formatNumber = getNumberFormatter(NumberFormats.FLOAT); function Sankey(element, props) { - const { data, width, height, colorScheme } = props; + const { data, width, height, colorScheme, sliceId } = props; const div = d3.select(element); div.classed(`superset-legacy-chart-sankey`, true); const margin = { @@ -219,7 +219,7 @@ function Sankey(element, props) { .attr('width', sankey.nodeWidth()) .style('fill', d => { const name = d.name || 'N/A'; - d.color = colorFn(name.replace(/ .*/, '')); + d.color = colorFn(name, sliceId); return d.color; }) diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js index 5297994fb95..b8e9f05b284 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js @@ -20,7 +20,7 @@ import { getLabelFontSize } from './utils'; export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { colorScheme } = formData; + const { colorScheme, sliceId } = formData; return { width, @@ -28,5 +28,6 @@ export default function transformProps(chartProps) { data: queriesData[0].data, colorScheme, fontSize: getLabelFontSize(width), + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js index 75bebdaa140..2a9cc56f51f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js @@ -170,6 +170,7 @@ function Sunburst(element, props) { linearColorScheme, metrics, numberFormat, + sliceId, } = props; const responsiveClass = getResponsiveContainerClass(width); const isSmallWidth = responsiveClass === 's'; @@ -287,7 +288,7 @@ function Sunburst(element, props) { .attr('points', breadcrumbPoints) .style('fill', d => colorByCategory - ? categoricalColorScale(d.name) + ? categoricalColorScale(d.name, sliceId) : linearColorScale(d.m2 / d.m1), ); @@ -300,7 +301,7 @@ function Sunburst(element, props) { // Make text white or black based on the lightness of the background const col = d3.hsl( colorByCategory - ? categoricalColorScale(d.name) + ? categoricalColorScale(d.name, sliceId) : linearColorScale(d.m2 / d.m1), ); @@ -489,7 +490,7 @@ function Sunburst(element, props) { // For efficiency, filter nodes to keep only those large enough to see. const nodes = partition.nodes(root).filter(d => d.dx > 0.005); // 0.005 radians = 0.29 degrees - if (metrics[0] !== metrics[1] && metrics[1]) { + if (metrics[0] !== metrics[1] && metrics[1] && !colorScheme) { colorByCategory = false; const ext = d3.extent(nodes, d => d.m2 / d.m1); linearColorScale = getSequentialSchemeRegistry() @@ -507,7 +508,7 @@ function Sunburst(element, props) { .attr('fill-rule', 'evenodd') .style('fill', d => colorByCategory - ? categoricalColorScale(d.name) + ? categoricalColorScale(d.name, sliceId) : linearColorScale(d.m2 / d.m1), ) .style('opacity', 1) diff --git a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js index 9952fc4992e..92c4d99f00e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js @@ -18,7 +18,8 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData, datasource } = chartProps; - const { colorScheme, linearColorScheme, metric, secondaryMetric } = formData; + const { colorScheme, linearColorScheme, metric, secondaryMetric, sliceId } = + formData; const returnProps = { width, @@ -27,6 +28,7 @@ export default function transformProps(chartProps) { colorScheme, linearColorScheme, metrics: [metric, secondaryMetric], + sliceId, }; if (datasource && datasource.metrics) { diff --git a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js index a155a050b36..f218218ec8b 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js @@ -87,6 +87,7 @@ function Treemap(element, props) { numberFormat, colorScheme, treemapRatio, + sliceId, } = props; const div = d3Select(element); div.classed('superset-legacy-chart-treemap', true); @@ -138,7 +139,7 @@ function Treemap(element, props) { .attr('id', d => `rect-${d.data.name}`) .attr('width', d => d.x1 - d.x0) .attr('height', d => d.y1 - d.y0) - .style('fill', d => colorFn(d.depth)); + .style('fill', d => colorFn(d.depth, sliceId)); cell .append('clipPath') diff --git a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js index adba34c09b3..bbc577cb3db 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js @@ -18,7 +18,7 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { colorScheme, treemapRatio } = formData; + const { colorScheme, treemapRatio, sliceId } = formData; let { numberFormat } = formData; if (!numberFormat && chartProps.datasource && chartProps.datasource.metrics) { @@ -39,5 +39,6 @@ export default function transformProps(chartProps) { colorScheme, numberFormat, treemapRatio, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js index c7253e10d0e..0c81e985601 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js @@ -23,6 +23,7 @@ import { extent as d3Extent } from 'd3-array'; import { getNumberFormatter, getSequentialSchemeRegistry, + CategoricalColorNamespace, } from '@superset-ui/core'; import Datamap from 'datamaps/dist/datamaps.world.min'; @@ -55,6 +56,8 @@ function WorldMap(element, props) { showBubbles, linearColorScheme, color, + colorScheme, + sliceId, } = props; const div = d3.select(element); div.classed('superset-legacy-chart-world-map', true); @@ -69,15 +72,24 @@ function WorldMap(element, props) { .domain([extRadius[0], extRadius[1]]) .range([1, maxBubbleSize]); - const colorScale = getSequentialSchemeRegistry() + const linearColorScale = getSequentialSchemeRegistry() .get(linearColorScheme) .createLinearScale(d3Extent(filteredData, d => d.m1)); - const processedData = filteredData.map(d => ({ - ...d, - radius: radiusScale(Math.sqrt(d.m2)), - fillColor: colorScale(d.m1), - })); + const colorScale = CategoricalColorNamespace.getScale(colorScheme); + + const processedData = filteredData.map(d => { + let color = linearColorScale(d.m1); + if (colorScheme) { + // use color scheme instead + color = colorScale(d.name, sliceId); + } + return { + ...d, + radius: radiusScale(Math.sqrt(d.m2)), + fillColor: color, + }; + }); const mapData = {}; processedData.forEach(d => { diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts index ec8aafc7b87..91664290dcb 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts @@ -106,6 +106,7 @@ const config: ControlPanelConfig = { }, ], ['color_picker'], + ['color_scheme'], ['linear_color_scheme'], ], }, @@ -126,6 +127,9 @@ const config: ControlPanelConfig = { color_picker: { label: t('Bubble Color'), }, + color_scheme: { + label: t('Categorical Color Scheme'), + }, linear_color_scheme: { label: t('Country Color Scheme'), }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js index 464dd53afa4..3838ebfa5c1 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js @@ -20,8 +20,14 @@ import { rgb } from 'd3-color'; export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { maxBubbleSize, showBubbles, linearColorScheme, colorPicker } = - formData; + const { + maxBubbleSize, + showBubbles, + linearColorScheme, + colorPicker, + colorScheme, + sliceId, + } = formData; const { r, g, b } = colorPicker; return { @@ -32,5 +38,7 @@ export default function transformProps(chartProps) { showBubbles, linearColorScheme, color: rgb(r, g, b).hex(), + colorScheme, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx index 7523c2e4ba9..64bfc0244a8 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx @@ -46,7 +46,7 @@ function getCategories(fd, data) { if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) { let color; if (fd.dimension) { - color = hexToRGB(colorFn(d.cat_color), c.a * 255); + color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255); } else { color = fixedColor; } @@ -212,7 +212,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent { return data.map(d => { let color; if (fd.dimension) { - color = hexToRGB(colorFn(d.cat_color), c.a * 255); + color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255); return { ...d, color }; } diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js index 7f3d4d08d4f..4d130d2139d 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js @@ -313,6 +313,7 @@ function nvd3Vis(element, props) { yAxis2ShowMinMax = false, yField, yIsLogScale, + sliceId, } = props; const isExplore = document.querySelector('#explorer-container') !== null; @@ -670,7 +671,9 @@ function nvd3Vis(element, props) { ); } else if (vizType !== 'bullet') { const colorFn = getScale(colorScheme); - chart.color(d => d.color || colorFn(cleanColorInput(d[colorKey]))); + chart.color( + d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId), + ); } if (isVizTypes(['line', 'area', 'bar', 'dist_bar']) && useRichTooltip) { diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js index 454314c502f..7fc8669e6da 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js @@ -94,6 +94,7 @@ export default function transformProps(chartProps) { yAxisShowminmax, yAxis2Showminmax, yLogScale, + sliceId, } = formData; let { @@ -195,5 +196,6 @@ export default function transformProps(chartProps) { yAxis2ShowMinMax: yAxis2Showminmax, yField: y, yIsLogScale: yLogScale, + sliceId, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts index 8e89914b4d8..6f79ec6e274 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts @@ -63,6 +63,7 @@ export default function transformProps( xAxisTitleMargin, yAxisTitleMargin, yAxisTitlePosition, + sliceId, } = formData as BoxPlotQueryFormData; const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); const numberFormatter = getNumberFormatter(numberFormat); @@ -98,9 +99,9 @@ export default function transformProps( datum[`${metric}__outliers`], ], itemStyle: { - color: colorFn(groupbyLabel), + color: colorFn(groupbyLabel, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6, - borderColor: colorFn(groupbyLabel), + borderColor: colorFn(groupbyLabel, sliceId), }, }; }); @@ -138,7 +139,7 @@ export default function transformProps( }, }, itemStyle: { - color: colorFn(groupbyLabel), + color: colorFn(groupbyLabel, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts index 3f3d84816d2..88a0fe75c21 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts @@ -103,6 +103,7 @@ export default function transformProps( showLabels, showLegend, emitFilter, + sliceId, }: EchartsFunnelFormData = { ...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_FUNNEL_FORM_DATA, @@ -145,7 +146,7 @@ export default function transformProps( value: datum[metricLabel], name, itemStyle: { - color: colorFn(name), + color: colorFn(name, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts index beecb475ace..0486ddf6729 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts @@ -107,6 +107,7 @@ export default function transformProps( intervalColorIndices, valueFormatter, emitFilter, + sliceId, }: EchartsGaugeFormData = { ...DEFAULT_GAUGE_FORM_DATA, ...formData }; const data = (queriesData[0]?.data || []) as DataRecord[]; const numberFormatter = getNumberFormatter(numberFormat); @@ -147,7 +148,7 @@ export default function transformProps( value: data_point[getMetricLabel(metric as QueryFormMetric)] as number, name, itemStyle: { - color: colorFn(index), + color: colorFn(index, sliceId), }, title: { offsetCenter: [ @@ -175,7 +176,7 @@ export default function transformProps( item = { ...item, itemStyle: { - color: colorFn(index), + color: colorFn(index, sliceId), opacity: OpacityEnum.SemiTransparent, }, detail: { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts index ea9c6f15249..905ecbfa8eb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts @@ -184,6 +184,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps { baseEdgeWidth, baseNodeSize, edgeSymbol, + sliceId, }: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData }; const metricLabel = getMetricLabel(metric); @@ -264,7 +265,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps { type: 'graph', categories: categoryList.map(c => ({ name: c, - itemStyle: { color: colorFn(c) }, + itemStyle: { color: colorFn(c, sliceId) }, })), layout, force: { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts index 76be9bb1a4e..9cb35c13044 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { QueryFormData } from '@superset-ui/core'; import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries'; import { SeriesTooltipOption } from 'echarts/types/src/util/types'; import { @@ -27,32 +28,34 @@ import { export type EdgeSymbol = 'none' | 'circle' | 'arrow'; -export type EchartsGraphFormData = EchartsLegendFormData & { - source: string; - target: string; - sourceCategory?: string; - targetCategory?: string; - colorScheme?: string; - metric?: string; - layout?: 'none' | 'circular' | 'force'; - roam: boolean | 'scale' | 'move'; - draggable: boolean; - selectedMode?: boolean | 'multiple' | 'single'; - showSymbolThreshold: number; - repulsion: number; - gravity: number; - baseNodeSize: number; - baseEdgeWidth: number; - edgeLength: number; - edgeSymbol: string; - friction: number; -}; +export type EchartsGraphFormData = QueryFormData & + EchartsLegendFormData & { + source: string; + target: string; + sourceCategory?: string; + targetCategory?: string; + colorScheme?: string; + metric?: string; + layout?: 'none' | 'circular' | 'force'; + roam: boolean | 'scale' | 'move'; + draggable: boolean; + selectedMode?: boolean | 'multiple' | 'single'; + showSymbolThreshold: number; + repulsion: number; + gravity: number; + baseNodeSize: number; + baseEdgeWidth: number; + edgeLength: number; + edgeSymbol: string; + friction: number; + }; export type EChartGraphNode = Omit & { value: number; tooltip?: Pick; }; +// @ts-ignore export const DEFAULT_FORM_DATA: EchartsGraphFormData = { ...DEFAULT_LEGEND_FORM_DATA, source: '', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index c3d6e979a96..8ac08e39257 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -128,6 +128,7 @@ export default function transformProps( xAxisTitleMargin, yAxisTitleMargin, yAxisTitlePosition, + sliceId, }: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); @@ -177,6 +178,7 @@ export default function transformProps( yAxisIndex, filterState, seriesKey: entry.name, + sliceId, }); if (transformedSeries) series.push(transformedSeries); }); @@ -195,6 +197,7 @@ export default function transformProps( seriesKey: primarySeries.has(entry.name as string) ? `${entry.name} (1)` : entry.name, + sliceId, }); if (transformedSeries) series.push(transformedSeries); }); @@ -203,7 +206,9 @@ export default function transformProps( .filter((layer: AnnotationLayer) => layer.show) .forEach((layer: AnnotationLayer) => { if (isFormulaAnnotationLayer(layer)) - series.push(transformFormulaAnnotation(layer, data1, colorScale)); + series.push( + transformFormulaAnnotation(layer, data1, colorScale, sliceId), + ); else if (isIntervalAnnotationLayer(layer)) { series.push( ...transformIntervalAnnotation( @@ -211,11 +216,18 @@ export default function transformProps( data1, annotationData, colorScale, + sliceId, ), ); } else if (isEventAnnotationLayer(layer)) { series.push( - ...transformEventAnnotation(layer, data1, annotationData, colorScale), + ...transformEventAnnotation( + layer, + data1, + annotationData, + colorScale, + sliceId, + ), ); } else if (isTimeseriesAnnotationLayer(layer)) { series.push( diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts index a70855fa432..237f4ae001f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts @@ -109,6 +109,7 @@ export default function transformProps( showLegend, showLabelsThreshold, emitFilter, + sliceId, }: EchartsPieFormData = { ...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_PIE_FORM_DATA, @@ -162,7 +163,7 @@ export default function transformProps( value: datum[metricLabel], name, itemStyle: { - color: colorFn(name), + color: colorFn(name, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts index cd981a21a9b..b668e340350 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts @@ -91,6 +91,7 @@ export default function transformProps( showLegend, isCircle, columnConfig, + sliceId, }: EchartsRadarFormData = { ...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_RADAR_FORM_DATA, @@ -154,7 +155,7 @@ export default function transformProps( value: metricLabels.map(metricLabel => datum[metricLabel]), name: joinedName, itemStyle: { - color: colorFn(joinedName), + color: colorFn(joinedName, sliceId), opacity: isFiltered ? OpacityEnum.Transparent : OpacityEnum.NonTransparent, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index b3a3d58c399..1a2200db220 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -125,6 +125,7 @@ export default function transformProps( xAxisTitleMargin, yAxisTitleMargin, yAxisTitlePosition, + sliceId, }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); @@ -198,6 +199,7 @@ export default function transformProps( showValueIndexes, thresholdValues, richTooltip, + sliceId, }); if (transformedSeries) series.push(transformedSeries); }); @@ -217,7 +219,9 @@ export default function transformProps( .filter((layer: AnnotationLayer) => layer.show) .forEach((layer: AnnotationLayer) => { if (isFormulaAnnotationLayer(layer)) - series.push(transformFormulaAnnotation(layer, data, colorScale)); + series.push( + transformFormulaAnnotation(layer, data, colorScale, sliceId), + ); else if (isIntervalAnnotationLayer(layer)) { series.push( ...transformIntervalAnnotation( @@ -225,11 +229,18 @@ export default function transformProps( data, annotationData, colorScale, + sliceId, ), ); } else if (isEventAnnotationLayer(layer)) { series.push( - ...transformEventAnnotation(layer, data, annotationData, colorScale), + ...transformEventAnnotation( + layer, + data, + annotationData, + colorScale, + sliceId, + ), ); } else if (isTimeseriesAnnotationLayer(layer)) { series.push( diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts index c357b2a40ee..7ce72695be2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -84,6 +84,7 @@ export function transformSeries( thresholdValues?: number[]; richTooltip?: boolean; seriesKey?: OptionName; + sliceId?: number; }, ): SeriesOption | undefined { const { name } = series; @@ -105,6 +106,7 @@ export function transformSeries( thresholdValues = [], richTooltip, seriesKey, + sliceId, } = opts; const contexts = seriesContexts[name || ''] || []; const hasForecast = @@ -151,7 +153,7 @@ export function transformSeries( } // forcing the colorScale to return a different color for same metrics across different queries const itemStyle = { - color: colorScale(seriesKey || forecastSeries.name), + color: colorScale(seriesKey || forecastSeries.name, sliceId), opacity, }; let emphasis = {}; @@ -244,13 +246,14 @@ export function transformFormulaAnnotation( layer: FormulaAnnotationLayer, data: TimeseriesDataRecord[], colorScale: CategoricalColorScale, + sliceId?: number, ): SeriesOption { const { name, color, opacity, width, style } = layer; return { name, id: name, itemStyle: { - color: color || colorScale(name), + color: color || colorScale(name, sliceId), }, lineStyle: { opacity: parseAnnotationOpacity(opacity), @@ -269,6 +272,7 @@ export function transformIntervalAnnotation( data: TimeseriesDataRecord[], annotationData: AnnotationData, colorScale: CategoricalColorScale, + sliceId?: number, ): SeriesOption[] { const series: SeriesOption[] = []; const annotations = extractRecordAnnotations(layer, annotationData); @@ -323,7 +327,7 @@ export function transformIntervalAnnotation( markArea: { silent: false, itemStyle: { - color: color || colorScale(name), + color: color || colorScale(name, sliceId), opacity: parseAnnotationOpacity(opacity || AnnotationOpacity.Medium), emphasis: { opacity: 0.8, @@ -342,6 +346,7 @@ export function transformEventAnnotation( data: TimeseriesDataRecord[], annotationData: AnnotationData, colorScale: CategoricalColorScale, + sliceId?: number, ): SeriesOption[] { const series: SeriesOption[] = []; const annotations = extractRecordAnnotations(layer, annotationData); @@ -359,7 +364,7 @@ export function transformEventAnnotation( const lineStyle: LineStyleOption & DefaultStatesMixin['emphasis'] = { width, type: style as ZRLineType, - color: color || colorScale(name), + color: color || colorScale(name, sliceId), opacity: parseAnnotationOpacity(opacity), emphasis: { width: width ? width + 1 : width, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts index 2face71250d..25f3910b92c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts @@ -127,6 +127,7 @@ export default function transformProps( showUpperLabels, dashboardId, emitFilter, + sliceId, }: EchartsTreemapFormData = { ...DEFAULT_TREEMAP_FORM_DATA, ...formData, @@ -223,7 +224,7 @@ export default function transformProps( colorSaturation: COLOR_SATURATION, itemStyle: { borderColor: BORDER_COLOR, - color: colorFn(`${child.name}`), + color: colorFn(`${child.name}`, sliceId), borderWidth: BORDER_WIDTH, gapWidth: GAP_WIDTH, }, @@ -259,7 +260,7 @@ export default function transformProps( show: false, }, itemStyle: { - color: CategoricalColorNamespace.getColor(), + color: '#1FA8C9', }, }, ]; diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx index 4a9683a6dd6..5d8ae0105e1 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx @@ -25,7 +25,12 @@ import { DeriveEncoding, Encoder, } from 'encodable'; -import { SupersetThemeProps, withTheme, seedRandom } from '@superset-ui/core'; +import { + SupersetThemeProps, + withTheme, + seedRandom, + CategoricalColorScale, +} from '@superset-ui/core'; export const ROTATION = { flat: () => 0, @@ -58,6 +63,7 @@ export interface WordCloudProps extends WordCloudVisualProps { data: PlainObject[]; height: number; width: number; + sliceId: number; } export interface WordCloudState { @@ -210,12 +216,15 @@ class WordCloud extends React.PureComponent< render() { const { scaleFactor } = this.state; - const { width, height, encoding } = this.props; + const { width, height, encoding, sliceId } = this.props; const { words } = this.state; const encoder = this.createEncoder(encoding); encoder.channels.color.setDomainFromDataset(words); + const { getValueFromDatum } = encoder.channels.color; + const colorFn = encoder.channels.color.scale as CategoricalColorScale; + const viewBoxWidth = width * scaleFactor; const viewBoxHeight = height * scaleFactor; @@ -234,7 +243,7 @@ class WordCloud extends React.PureComponent< fontSize={`${w.size}px`} fontWeight={w.weight} fontFamily={w.font} - fill={encoder.channels.color.encodeDatum(w, '')} + fill={colorFn(getValueFromDatum(w) as string, sliceId)} textAnchor="middle" transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`} > diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts index 095714086bc..aec557d616d 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts @@ -43,6 +43,7 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps { series, sizeFrom = 0, sizeTo, + sliceId, } = formData as LegacyWordCloudFormData; const metricLabel = getMetricLabel(metric); @@ -77,5 +78,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps { height, rotation, width, + sliceId, }; } diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts index 02265298e97..4ed6f6405c9 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts @@ -23,7 +23,7 @@ import { WordCloudFormData } from '../types'; export default function transformProps(chartProps: ChartProps): WordCloudProps { const { width, height, formData, queriesData } = chartProps; - const { encoding, rotation } = formData as WordCloudFormData; + const { encoding, rotation, sliceId } = formData as WordCloudFormData; return { data: queriesData[0].data, @@ -31,5 +31,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps { height, rotation, width, + sliceId, }; } diff --git a/superset-frontend/src/components/Chart/Chart.jsx b/superset-frontend/src/components/Chart/Chart.jsx index 89fdd545461..ab4fde84d27 100644 --- a/superset-frontend/src/components/Chart/Chart.jsx +++ b/superset-frontend/src/components/Chart/Chart.jsx @@ -47,6 +47,7 @@ const propTypes = { // and merged with extra filter that current dashboard applying formData: PropTypes.object.isRequired, labelColors: PropTypes.object, + sharedLabelColors: PropTypes.object, width: PropTypes.number, height: PropTypes.number, setControlValue: PropTypes.func, @@ -70,6 +71,7 @@ const propTypes = { onFilterMenuOpen: PropTypes.func, onFilterMenuClose: PropTypes.func, ownState: PropTypes.object, + postTransformProps: PropTypes.func, }; const BLANK = {}; diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx index 3c634ea32df..b814b6fde6d 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx @@ -31,6 +31,7 @@ const propTypes = { initialValues: PropTypes.object, formData: PropTypes.object.isRequired, labelColors: PropTypes.object, + sharedLabelColors: PropTypes.object, height: PropTypes.number, width: PropTypes.number, setControlValue: PropTypes.func, @@ -48,6 +49,7 @@ const propTypes = { onFilterMenuOpen: PropTypes.func, onFilterMenuClose: PropTypes.func, ownState: PropTypes.object, + postTransformProps: PropTypes.func, source: PropTypes.oneOf(['dashboard', 'explore']), }; @@ -107,6 +109,7 @@ class ChartRenderer extends React.Component { nextProps.width !== this.props.width || nextProps.triggerRender || nextProps.labelColors !== this.props.labelColors || + nextProps.sharedLabelColors !== this.props.sharedLabelColors || nextProps.formData.color_scheme !== this.props.formData.color_scheme || nextProps.cacheBusterProp !== this.props.cacheBusterProp ); @@ -192,6 +195,7 @@ class ChartRenderer extends React.Component { filterState, formData, queriesResponse, + postTransformProps, } = this.props; // It's bad practice to use unprefixed `vizType` as classnames for chart @@ -260,6 +264,7 @@ class ChartRenderer extends React.Component { onRenderSuccess={this.handleRenderSuccess} onRenderFailure={this.handleRenderFailure} noResults={noResultsComponent} + postTransformProps={postTransformProps} /> ); } diff --git a/superset-frontend/src/dashboard/actions/dashboardInfo.ts b/superset-frontend/src/dashboard/actions/dashboardInfo.ts index 7b1b0017baa..9a769101cfd 100644 --- a/superset-frontend/src/dashboard/actions/dashboardInfo.ts +++ b/superset-frontend/src/dashboard/actions/dashboardInfo.ts @@ -23,6 +23,21 @@ import { ChartConfiguration, DashboardInfo } from '../reducers/types'; export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED'; +export function updateColorSchema( + metadata: Record, + labelColors: Record, +) { + const categoricalNamespace = CategoricalColorNamespace.getNamespace( + metadata?.color_namespace, + ); + const colorMap = isString(labelColors) + ? JSON.parse(labelColors) + : labelColors; + Object.keys(colorMap).forEach(label => { + categoricalNamespace.setColor(label, colorMap[label]); + }); +} + // updates partially changed dashboard info export function dashboardInfoChanged(newInfo: { metadata: any }) { const { metadata } = newInfo; @@ -33,14 +48,12 @@ export function dashboardInfoChanged(newInfo: { metadata: any }) { categoricalNamespace.resetColors(); + if (metadata?.shared_label_colors) { + updateColorSchema(metadata, metadata?.shared_label_colors); + } + if (metadata?.label_colors) { - const labelColors = metadata.label_colors; - const colorMap = isString(labelColors) - ? JSON.parse(labelColors) - : labelColors; - Object.keys(colorMap).forEach(label => { - categoricalNamespace.setColor(label, colorMap[label]); - }); + updateColorSchema(metadata, metadata?.label_colors); } return { type: DASHBOARD_INFO_UPDATED, newInfo }; diff --git a/superset-frontend/src/dashboard/actions/dashboardLayout.js b/superset-frontend/src/dashboard/actions/dashboardLayout.js index 1fe988849d6..e0cbe7aa00c 100644 --- a/superset-frontend/src/dashboard/actions/dashboardLayout.js +++ b/superset-frontend/src/dashboard/actions/dashboardLayout.js @@ -47,17 +47,19 @@ function setUnsavedChangesAfterAction(action) { dispatch(result); } + const { dashboardLayout, dashboardState } = getState(); + const isComponentLevelEvent = result.type === UPDATE_COMPONENTS && result.payload && result.payload.nextComponents; // trigger dashboardFilters state update if dashboard layout is changed. if (!isComponentLevelEvent) { - const components = getState().dashboardLayout.present; + const components = dashboardLayout.present; dispatch(updateLayoutComponents(components)); } - if (!getState().dashboardState.hasUnsavedChanges) { + if (!dashboardState.hasUnsavedChanges) { dispatch(setUnsavedChanges(true)); } }; diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 0afe42e063f..839b5feb7a1 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -18,7 +18,12 @@ */ /* eslint camelcase: 0 */ import { ActionCreators as UndoActionCreators } from 'redux-undo'; -import { ensureIsArray, t, SupersetClient } from '@superset-ui/core'; +import { + ensureIsArray, + t, + SupersetClient, + getSharedLabelColor, +} from '@superset-ui/core'; import { addChart, removeChart, @@ -67,6 +72,11 @@ export function removeSlice(sliceId) { return { type: REMOVE_SLICE, sliceId }; } +export const RESET_SLICE = 'RESET_SLICE'; +export function resetSlice() { + return { type: RESET_SLICE }; +} + const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard'; export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR'; export function toggleFaveStar(isStarred) { @@ -232,6 +242,7 @@ export function saveDashboardRequest(data, id, saveType) { color_scheme: data.metadata?.color_scheme || '', expanded_slices: data.metadata?.expanded_slices || {}, label_colors: data.metadata?.label_colors || {}, + shared_label_colors: data.metadata?.shared_label_colors || {}, refresh_frequency: data.metadata?.refresh_frequency || 0, timed_refresh_immune_slices: data.metadata?.timed_refresh_immune_slices || [], @@ -495,6 +506,28 @@ export function addSliceToDashboard(id, component) { }; } +export function postAddSliceFromDashboard() { + return (dispatch, getState) => { + const { + dashboardInfo: { metadata }, + dashboardState, + } = getState(); + + if (dashboardState?.updateSlice && dashboardState?.editMode) { + metadata.shared_label_colors = getSharedLabelColor().getColorMap( + metadata?.color_namespace, + metadata?.color_scheme, + ); + dispatch( + dashboardInfoChanged({ + metadata, + }), + ); + dispatch(resetSlice()); + } + }; +} + export function removeSliceFromDashboard(id) { return (dispatch, getState) => { const sliceEntity = getState().sliceEntities.slices[id]; @@ -504,6 +537,20 @@ export function removeSliceFromDashboard(id) { dispatch(removeSlice(id)); dispatch(removeChart(id)); + + const { + dashboardInfo: { metadata }, + } = getState(); + getSharedLabelColor().removeSlice(id); + metadata.shared_label_colors = getSharedLabelColor().getColorMap( + metadata?.color_namespace, + metadata?.color_scheme, + ); + dispatch( + dashboardInfoChanged({ + metadata, + }), + ); }; } diff --git a/superset-frontend/src/dashboard/actions/dashboardState.test.js b/superset-frontend/src/dashboard/actions/dashboardState.test.js index 295dc8cfc11..f5fa60c08d5 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.test.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.test.js @@ -39,7 +39,11 @@ describe('dashboardState actions', () => { sliceIds: [filterId], hasUnsavedChanges: true, }, - dashboardInfo: {}, + dashboardInfo: { + metadata: { + color_scheme: 'supersetColors', + }, + }, sliceEntities, dashboardFilters: emptyFilters, dashboardLayout: { @@ -116,6 +120,6 @@ describe('dashboardState actions', () => { const removeFilter = dispatch.getCall(0).args[0]; removeFilter(dispatch, getState); - expect(dispatch.getCall(3).args[0].type).toBe(REMOVE_FILTER); + expect(dispatch.getCall(4).args[0].type).toBe(REMOVE_FILTER); }); }); diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 4c8a978e963..f02d6f26484 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -17,12 +17,7 @@ * under the License. */ /* eslint-disable camelcase */ -import { isString } from 'lodash'; -import { - Behavior, - CategoricalColorNamespace, - getChartMetadataRegistry, -} from '@superset-ui/core'; +import { Behavior, getChartMetadataRegistry } from '@superset-ui/core'; import { chart } from 'src/components/Chart/chartReducer'; import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; @@ -59,6 +54,7 @@ import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants'; import { FeatureFlag, isFeatureEnabled } from '../../featureFlags'; import extractUrlParams from '../util/extractUrlParams'; import getNativeFilterConfig from '../util/filterboxMigrationHelper'; +import { updateColorSchema } from './dashboardInfo'; export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; @@ -92,19 +88,14 @@ export const hydrateDashboard = // } + if (metadata?.shared_label_colors) { + updateColorSchema(metadata, metadata?.shared_label_colors); + } + // Priming the color palette with user's label-color mapping provided in // the dashboard's JSON metadata if (metadata?.label_colors) { - const namespace = metadata.color_namespace; - const colorMap = isString(metadata.label_colors) - ? JSON.parse(metadata.label_colors) - : metadata.label_colors; - const categoricalNamespace = - CategoricalColorNamespace.getNamespace(namespace); - - Object.keys(colorMap).forEach(label => { - categoricalNamespace.setColor(label, colorMap[label]); - }); + updateColorSchema(metadata, metadata?.label_colors); } // dashboard layout diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index a67061832dd..89b1b9bee67 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -20,7 +20,7 @@ import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; -import { styled, t } from '@superset-ui/core'; +import { styled, t, getSharedLabelColor } from '@superset-ui/core'; import ButtonGroup from 'src/components/ButtonGroup'; import { @@ -356,6 +356,15 @@ class Header extends React.PureComponent { ? currentRefreshFrequency : dashboardInfo.metadata?.refresh_frequency; + const currentColorScheme = + dashboardInfo?.metadata?.color_scheme || colorScheme; + const currentColorNamespace = + dashboardInfo?.metadata?.color_namespace || colorNamespace; + const currentSharedLabelColors = getSharedLabelColor().getColorMap( + currentColorNamespace, + currentColorScheme, + ); + const data = { certified_by: dashboardInfo.certified_by, certification_details: dashboardInfo.certification_details, @@ -367,11 +376,11 @@ class Header extends React.PureComponent { slug, metadata: { ...dashboardInfo?.metadata, - color_namespace: - dashboardInfo?.metadata?.color_namespace || colorNamespace, - color_scheme: dashboardInfo?.metadata?.color_scheme || colorScheme, + color_namespace: currentColorNamespace, + color_scheme: currentColorScheme, positions, refresh_frequency: refreshFrequency, + shared_label_colors: currentSharedLabelColors, }, }; diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx index a18cb40ead8..67c86cb1fc7 100644 --- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx +++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx @@ -29,6 +29,7 @@ import { SupersetClient, getCategoricalSchemeRegistry, ensureIsArray, + getSharedLabelColor, } from '@superset-ui/core'; import Modal from 'src/components/Modal'; @@ -169,7 +170,11 @@ const PropertiesModal = ({ if (metadata?.positions) { delete metadata.positions; } - setJsonMetadata(metadata ? jsonStringify(metadata) : ''); + const metaDataCopy = { ...metadata }; + if (metaDataCopy?.shared_label_colors) { + delete metaDataCopy.shared_label_colors; + } + setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : ''); }, [form], ); @@ -282,12 +287,25 @@ const PropertiesModal = ({ form.getFieldsValue(); let currentColorScheme = colorScheme; let colorNamespace = ''; + let currentJsonMetadata = jsonMetadata; // color scheme in json metadata has precedence over selection - if (jsonMetadata?.length) { - const metadata = JSON.parse(jsonMetadata); + if (currentJsonMetadata?.length) { + const metadata = JSON.parse(currentJsonMetadata); currentColorScheme = metadata?.color_scheme || colorScheme; colorNamespace = metadata?.color_namespace || ''; + + // filter shared_label_color from user input + if (metadata?.shared_label_colors) { + delete metadata.shared_label_colors; + } + const colorMap = getSharedLabelColor().getColorMap( + colorNamespace, + currentColorScheme, + true, + ); + metadata.shared_label_colors = colorMap; + currentJsonMetadata = jsonStringify(metadata); } onColorSchemeChange(currentColorScheme, { @@ -304,7 +322,7 @@ const PropertiesModal = ({ id: dashboardId, title, slug, - jsonMetadata, + jsonMetadata: currentJsonMetadata, owners, colorScheme: currentColorScheme, colorNamespace, @@ -323,7 +341,7 @@ const PropertiesModal = ({ body: JSON.stringify({ dashboard_title: title, slug: slug || null, - json_metadata: jsonMetadata || null, + json_metadata: currentJsonMetadata || null, owners: (owners || []).map(o => o.id), certified_by: certifiedBy || null, certification_details: diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 239bb508deb..821b311b48f 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -56,6 +56,7 @@ const propTypes = { chart: chartPropShape.isRequired, formData: PropTypes.object.isRequired, labelColors: PropTypes.object, + sharedLabelColors: PropTypes.object, datasource: PropTypes.object, slice: slicePropShape.isRequired, sliceName: PropTypes.string.isRequired, @@ -81,6 +82,7 @@ const propTypes = { addDangerToast: PropTypes.func.isRequired, ownState: PropTypes.object, filterState: PropTypes.object, + postTransformProps: PropTypes.func, }; const defaultProps = { @@ -319,6 +321,7 @@ export default class Chart extends React.Component { filters, formData, labelColors, + sharedLabelColors, updateSliceName, sliceName, toggleExpandSlice, @@ -334,6 +337,7 @@ export default class Chart extends React.Component { handleToggleFullSize, isFullSize, filterboxMigrationState, + postTransformProps, } = this.props; const { width } = this.state; @@ -449,6 +453,7 @@ export default class Chart extends React.Component { initialValues={initialValues} formData={formData} labelColors={labelColors} + sharedLabelColors={sharedLabelColors} ownState={ownState} filterState={filterState} queriesResponse={chart.queriesResponse} @@ -457,6 +462,7 @@ export default class Chart extends React.Component { vizType={slice.viz_type} isDeactivatedViz={isDeactivatedViz} filterboxMigrationState={filterboxMigrationState} + postTransformProps={postTransformProps} /> diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx index 465d646e7bd..95fb967c777 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -69,6 +69,7 @@ const propTypes = { updateComponents: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired, setFullSizeChartId: PropTypes.func.isRequired, + postAddSliceFromDashboard: PropTypes.func, }; const defaultProps = { @@ -197,6 +198,7 @@ class ChartHolder extends React.Component { this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this); this.handleToggleFullSize = this.handleToggleFullSize.bind(this); + this.handlePostTransformProps = this.handlePostTransformProps.bind(this); } componentDidMount() { @@ -251,6 +253,11 @@ class ChartHolder extends React.Component { setFullSizeChartId(isFullSize ? null : chartId); } + handlePostTransformProps(props) { + this.props.postAddSliceFromDashboard(); + return props; + } + render() { const { isFocused } = this.state; const { @@ -364,6 +371,7 @@ class ChartHolder extends React.Component { isComponentVisible={isComponentVisible} handleToggleFullSize={this.handleToggleFullSize} isFullSize={isFullSize} + postTransformProps={this.handlePostTransformProps} /> {editMode && ( diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 06d3f56e34f..96e053e8ed6 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -62,6 +62,7 @@ function mapStateToProps( PLACEHOLDER_DATASOURCE; const { colorScheme, colorNamespace } = dashboardState; const labelColors = dashboardInfo?.metadata?.label_colors || {}; + const sharedLabelColors = dashboardInfo?.metadata?.shared_label_colors || {}; // note: this method caches filters if possible to prevent render cascades const formData = getFormDataWithExtraFilters({ layout: dashboardLayout.present, @@ -76,6 +77,7 @@ function mapStateToProps( nativeFilters, dataMask, labelColors, + sharedLabelColors, }); formData.dashboardId = dashboardInfo.id; @@ -84,6 +86,7 @@ function mapStateToProps( chart, datasource, labelColors, + sharedLabelColors, slice: sliceEntities.slices[id], timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, filters: getActiveFilters() || EMPTY_OBJECT, diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx index 08b7ed9f82d..23298d8bf96 100644 --- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx @@ -37,6 +37,7 @@ import { setDirectPathToChild, setActiveTabs, setFullSizeChartId, + postAddSliceFromDashboard, } from 'src/dashboard/actions/dashboardState'; const propTypes = { @@ -111,6 +112,7 @@ function mapDispatchToProps(dispatch) { setFullSizeChartId, setActiveTabs, logEvent, + postAddSliceFromDashboard, }, dispatch, ); diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index e5fff328724..cd677252750 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -17,7 +17,14 @@ * under the License. */ import React, { FC, useRef, useEffect, useState } from 'react'; -import { FeatureFlag, isFeatureEnabled, t, useTheme } from '@superset-ui/core'; +import { + CategoricalColorNamespace, + FeatureFlag, + getSharedLabelColor, + isFeatureEnabled, + t, + useTheme, +} from '@superset-ui/core'; import { useDispatch, useSelector } from 'react-redux'; import { Global } from '@emotion/react'; import { useParams } from 'react-router-dom'; @@ -222,6 +229,18 @@ const DashboardPage: FC = () => { return () => {}; }, [css]); + useEffect( + () => () => { + // clean up label color + const categoricalNamespace = CategoricalColorNamespace.getNamespace( + metadata?.color_namespace, + ); + categoricalNamespace.resetColors(); + getSharedLabelColor().clear(); + }, + [metadata?.color_namespace], + ); + useEffect(() => { if (datasetsApiError) { addDangerToast( diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 64c794af933..21d70b4f53b 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -39,6 +39,7 @@ import { UNSET_FOCUSED_FILTER_FIELD, SET_ACTIVE_TABS, SET_FULL_SIZE_CHART_ID, + RESET_SLICE, ON_FILTERS_REFRESH, ON_FILTERS_REFRESH_SUCCESS, } from '../actions/dashboardState'; @@ -58,6 +59,7 @@ export default function dashboardStateReducer(state = {}, action) { return { ...state, sliceIds: Array.from(updatedSliceIds), + updateSlice: true, }; }, [REMOVE_SLICE]() { @@ -70,6 +72,12 @@ export default function dashboardStateReducer(state = {}, action) { sliceIds: Array.from(updatedSliceIds), }; }, + [RESET_SLICE]() { + return { + ...state, + updateSlice: false, + }; + }, [TOGGLE_FAVE_STAR]() { return { ...state, isStarred: action.isStarred }; }, @@ -116,6 +124,7 @@ export default function dashboardStateReducer(state = {}, action) { maxUndoHistoryExceeded: false, editMode: false, updatedColorScheme: false, + updateSlice: false, // server-side returns last_modified_time for latest change lastModifiedTime: action.lastModifiedTime, }; diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.test.js b/superset-frontend/src/dashboard/reducers/dashboardState.test.js index 39798ecf139..de3ecf72ff3 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.test.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.test.js @@ -28,6 +28,7 @@ import { TOGGLE_EXPAND_SLICE, TOGGLE_FAVE_STAR, UNSET_FOCUSED_FILTER_FIELD, + RESET_SLICE, } from 'src/dashboard/actions/dashboardState'; import dashboardStateReducer from 'src/dashboard/reducers/dashboardState'; @@ -43,7 +44,7 @@ describe('dashboardState reducer', () => { { sliceIds: [1] }, { type: ADD_SLICE, slice: { slice_id: 2 } }, ), - ).toEqual({ sliceIds: [1, 2] }); + ).toEqual({ sliceIds: [1, 2], updateSlice: true }); }); it('should remove a slice', () => { @@ -55,6 +56,12 @@ describe('dashboardState reducer', () => { ).toEqual({ sliceIds: [1], filters: {} }); }); + it('should reset updateSlice', () => { + expect( + dashboardStateReducer({ updateSlice: true }, { type: RESET_SLICE }), + ).toEqual({ updateSlice: false }); + }); + it('should toggle fav star', () => { expect( dashboardStateReducer( diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index 90022a9dce3..54e0417b277 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -46,6 +46,7 @@ export interface GetFormDataWithExtraFiltersArguments { dataMask: DataMaskStateWithId; nativeFilters: NativeFiltersState; labelColors?: Record; + sharedLabelColors?: Record; } // this function merge chart's formData with dashboard filters value, @@ -63,6 +64,7 @@ export default function getFormDataWithExtraFilters({ layout, dataMask, labelColors, + sharedLabelColors, }: GetFormDataWithExtraFiltersArguments) { // if dashboard metadata + filters have not changed, use cache if possible const cachedFormData = cachedFormdataByChart[sliceId]; @@ -77,6 +79,9 @@ export default function getFormDataWithExtraFilters({ areObjectsEqual(cachedFormData?.label_colors, labelColors, { ignoreUndefined: true, }) && + areObjectsEqual(cachedFormData?.shared_label_colors, sharedLabelColors, { + ignoreUndefined: true, + }) && !!cachedFormData && areObjectsEqual(cachedFormData?.dataMask, dataMask, { ignoreUndefined: true, @@ -108,6 +113,7 @@ export default function getFormDataWithExtraFilters({ const formData = { ...chart.formData, label_colors: labelColors, + shared_label_colors: sharedLabelColors, ...(colorScheme && { color_scheme: colorScheme }), extra_filters: getEffectiveExtraFilters(filters), ...extraData, diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx index bb1fde9c30e..21605c553df 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx @@ -156,13 +156,23 @@ export class ExploreChartHeader extends React.PureComponent { if (dashboard && dashboard.json_metadata) { // setting the chart to use the dashboard custom label colors if any - const labelColors = - JSON.parse(dashboard.json_metadata).label_colors || {}; + const metadata = JSON.parse(dashboard.json_metadata); + const sharedLabelColors = metadata.shared_label_colors || {}; + const customLabelColors = metadata.label_colors || {}; + const mergedLabelColors = { + ...sharedLabelColors, + ...customLabelColors, + }; + const categoricalNamespace = CategoricalColorNamespace.getNamespace(); - Object.keys(labelColors).forEach(label => { - categoricalNamespace.setColor(label, labelColors[label]); + Object.keys(mergedLabelColors).forEach(label => { + categoricalNamespace.setColor( + label, + mergedLabelColors[label], + metadata.color_scheme, + ); }); } } diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index 0443763cb37..ce6e30f8d4b 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -265,6 +265,7 @@ class DashboardDAO(BaseDAO): md["refresh_frequency"] = data.get("refresh_frequency", 0) md["color_scheme"] = data.get("color_scheme", "") md["label_colors"] = data.get("label_colors", {}) + md["shared_label_colors"] = data.get("shared_label_colors", {}) dashboard.json_metadata = json.dumps(md) diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py index b1831fdcbbe..661c61e1c24 100644 --- a/superset/dashboards/schemas.py +++ b/superset/dashboards/schemas.py @@ -128,6 +128,7 @@ class DashboardJSONMetadataSchema(Schema): color_namespace = fields.Str(allow_none=True) positions = fields.Dict(allow_none=True) label_colors = fields.Dict() + shared_label_colors = fields.Dict() # used for v0 import/export import_time = fields.Integer() remote_id = fields.Integer() diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index 2ed627a2572..8669da99f4a 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -72,7 +72,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi "slug": "slug1_changed", "position_json": '{"b": "B"}', "css": "css_changed", - "json_metadata": '{"refresh_frequency": 30, "timed_refresh_immune_slices": [], "expanded_slices": {}, "color_scheme": "", "label_colors": {}}', + "json_metadata": '{"refresh_frequency": 30, "timed_refresh_immune_slices": [], "expanded_slices": {}, "color_scheme": "", "label_colors": {}, "shared_label_colors": {}}', "published": False, }