feat: improve color consistency (save all labels) (#19038)

This commit is contained in:
Stephen Liu 2022-03-21 15:20:04 +08:00 committed by GitHub
parent e1d0b83885
commit dc575080d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 690 additions and 137 deletions

2
.gitignore vendored
View File

@ -108,3 +108,5 @@ release.json
messages.mo messages.mo
docker/requirements-local.txt docker/requirements-local.txt
cache/

View File

@ -136,6 +136,7 @@
"rison": "^0.1.1", "rison": "^0.1.1",
"scroll-into-view-if-needed": "^2.2.28", "scroll-into-view-if-needed": "^2.2.28",
"shortid": "^2.2.6", "shortid": "^2.2.6",
"tinycolor2": "^1.4.2",
"urijs": "^1.19.8", "urijs": "^1.19.8",
"use-immer": "^0.6.0", "use-immer": "^0.6.0",
"use-query-params": "^1.1.9", "use-query-params": "^1.1.9",
@ -201,6 +202,7 @@
"@types/rison": "0.0.6", "@types/rison": "0.0.6",
"@types/shortid": "^0.0.29", "@types/shortid": "^0.0.29",
"@types/sinon": "^9.0.5", "@types/sinon": "^9.0.5",
"@types/tinycolor2": "^1.4.3",
"@types/yargs": "12 - 15", "@types/yargs": "12 - 15",
"@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^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", "resolved": "https://registry.npmjs.org/@types/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
"integrity": "sha512-AQ6zewa0ucLJvtUi5HsErbOFKAcQfRLt9zFLlUOvcXBy2G36a+ZDpCHSGdzJVUD8aNURtIjh9aSjCStNMRCcRQ==" "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": { "node_modules/@types/uglify-js": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz",
@ -53688,9 +53695,9 @@
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
}, },
"node_modules/tinycolor2": { "node_modules/tinycolor2": {
"version": "1.4.1", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": { "engines": {
"node": "*" "node": "*"
} }
@ -58702,6 +58709,7 @@
"@types/prop-types": "^15.7.2", "@types/prop-types": "^15.7.2",
"@types/rison": "0.0.6", "@types/rison": "0.0.6",
"@types/seedrandom": "^2.4.28", "@types/seedrandom": "^2.4.28",
"@types/tinycolor2": "^1.4.3",
"@vx/responsive": "^0.0.199", "@vx/responsive": "^0.0.199",
"csstype": "^2.6.4", "csstype": "^2.6.4",
"d3-format": "^1.3.2", "d3-format": "^1.3.2",
@ -75807,6 +75815,7 @@
"@types/prop-types": "^15.7.2", "@types/prop-types": "^15.7.2",
"@types/rison": "0.0.6", "@types/rison": "0.0.6",
"@types/seedrandom": "^2.4.28", "@types/seedrandom": "^2.4.28",
"@types/tinycolor2": "^1.4.3",
"@vx/responsive": "^0.0.199", "@vx/responsive": "^0.0.199",
"csstype": "^2.6.4", "csstype": "^2.6.4",
"d3-format": "^1.3.2", "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", "resolved": "https://registry.npmjs.org/@types/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
"integrity": "sha512-AQ6zewa0ucLJvtUi5HsErbOFKAcQfRLt9zFLlUOvcXBy2G36a+ZDpCHSGdzJVUD8aNURtIjh9aSjCStNMRCcRQ==" "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": { "@types/uglify-js": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz",
@ -102080,9 +102094,9 @@
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
}, },
"tinycolor2": { "tinycolor2": {
"version": "1.4.1", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="
}, },
"tinyqueue": { "tinyqueue": {
"version": "2.0.3", "version": "2.0.3",

View File

@ -196,6 +196,7 @@
"rison": "^0.1.1", "rison": "^0.1.1",
"scroll-into-view-if-needed": "^2.2.28", "scroll-into-view-if-needed": "^2.2.28",
"shortid": "^2.2.6", "shortid": "^2.2.6",
"tinycolor2": "^1.4.2",
"urijs": "^1.19.8", "urijs": "^1.19.8",
"use-immer": "^0.6.0", "use-immer": "^0.6.0",
"use-query-params": "^1.1.9", "use-query-params": "^1.1.9",
@ -261,6 +262,7 @@
"@types/rison": "0.0.6", "@types/rison": "0.0.6",
"@types/shortid": "^0.0.29", "@types/shortid": "^0.0.29",
"@types/sinon": "^9.0.5", "@types/sinon": "^9.0.5",
"@types/tinycolor2": "^1.4.3",
"@types/yargs": "12 - 15", "@types/yargs": "12 - 15",
"@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0", "@typescript-eslint/parser": "^5.3.0",

View File

@ -205,6 +205,9 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
renderTrigger: true, renderTrigger: true,
schemes: () => sequentialSchemeRegistry.getMap(), schemes: () => sequentialSchemeRegistry.getMap(),
isLinear: true, isLinear: true,
mapStateToProps: state => ({
dashboardId: state?.form_data?.dashboardId,
}),
}; };
const secondary_metric: SharedControlConfig<'MetricsControl'> = { const secondary_metric: SharedControlConfig<'MetricsControl'> = {

View File

@ -42,6 +42,7 @@
"@types/math-expression-evaluator": "^1.2.1", "@types/math-expression-evaluator": "^1.2.1",
"@types/rison": "0.0.6", "@types/rison": "0.0.6",
"@types/seedrandom": "^2.4.28", "@types/seedrandom": "^2.4.28",
"@types/tinycolor2": "^1.4.3",
"@types/fetch-mock": "^7.3.3", "@types/fetch-mock": "^7.3.3",
"@types/enzyme": "^3.10.5", "@types/enzyme": "^3.10.5",
"@types/prop-types": "^15.7.2", "@types/prop-types": "^15.7.2",

View File

@ -22,12 +22,12 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
import { ExtensibleFunction } from '../models'; import { ExtensibleFunction } from '../models';
import { ColorsLookup } from './types'; import { ColorsLookup } from './types';
import stringifyAndTrim from './stringifyAndTrim'; import stringifyAndTrim from './stringifyAndTrim';
import getSharedLabelColor from './SharedLabelColorSingleton';
// Use type augmentation to correct the fact that // Use type augmentation to correct the fact that
// an instance of CategoricalScale is also a function // an instance of CategoricalScale is also a function
interface CategoricalColorScale { interface CategoricalColorScale {
(x: { toString(): string }): string; (x: { toString(): string }, y?: number): string;
} }
class CategoricalColorScale extends ExtensibleFunction { class CategoricalColorScale extends ExtensibleFunction {
@ -46,7 +46,7 @@ class CategoricalColorScale extends ExtensibleFunction {
* (usually CategoricalColorNamespace) and supersede this.forcedColors * (usually CategoricalColorNamespace) and supersede this.forcedColors
*/ */
constructor(colors: string[], parentForcedColors?: ColorsLookup) { constructor(colors: string[], parentForcedColors?: ColorsLookup) {
super((value: string) => this.getColor(value)); super((value: string, sliceId?: number) => this.getColor(value, sliceId));
this.colors = colors; this.colors = colors;
this.scale = scaleOrdinal<{ toString(): string }, string>(); this.scale = scaleOrdinal<{ toString(): string }, string>();
@ -55,20 +55,27 @@ class CategoricalColorScale extends ExtensibleFunction {
this.forcedColors = {}; this.forcedColors = {};
} }
getColor(value?: string) { getColor(value?: string, sliceId?: number) {
const cleanedValue = stringifyAndTrim(value); const cleanedValue = stringifyAndTrim(value);
const sharedLabelColor = getSharedLabelColor();
const parentColor = const parentColor =
this.parentForcedColors && this.parentForcedColors[cleanedValue]; this.parentForcedColors && this.parentForcedColors[cleanedValue];
if (parentColor) { if (parentColor) {
sharedLabelColor.addSlice(cleanedValue, parentColor, sliceId);
return parentColor; return parentColor;
} }
const forcedColor = this.forcedColors[cleanedValue]; const forcedColor = this.forcedColors[cleanedValue];
if (forcedColor) { if (forcedColor) {
sharedLabelColor.addSlice(cleanedValue, forcedColor, sliceId);
return forcedColor; return forcedColor;
} }
return this.scale(cleanedValue); const color = this.scale(cleanedValue);
sharedLabelColor.addSlice(cleanedValue, color, sliceId);
return color;
} }
/** /**

View File

@ -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<number, Record<string, string | undefined>>;
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<string>();
const result = new Set<string>();
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;

View File

@ -32,5 +32,9 @@ export * from './SequentialScheme';
export { default as ColorSchemeRegistry } from './ColorSchemeRegistry'; export { default as ColorSchemeRegistry } from './ColorSchemeRegistry';
export * from './colorSchemes'; export * from './colorSchemes';
export * from './utils'; export * from './utils';
export {
default as getSharedLabelColor,
SharedLabelColor,
} from './SharedLabelColorSingleton';
export const BRAND_COLOR = '#00A699'; export const BRAND_COLOR = '#00A699';

View File

@ -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({});
});
});
});

View File

@ -36,7 +36,7 @@ const propTypes = {
}; };
function Chord(element, props) { function Chord(element, props) {
const { data, width, height, numberFormat, colorScheme } = props; const { data, width, height, numberFormat, colorScheme, sliceId } = props;
element.innerHTML = ''; element.innerHTML = '';
@ -93,7 +93,7 @@ function Chord(element, props) {
.append('path') .append('path')
.attr('id', (d, i) => `group${i}`) .attr('id', (d, i) => `group${i}`)
.attr('d', arc) .attr('d', arc)
.style('fill', (d, i) => colorFn(nodes[i])); .style('fill', (d, i) => colorFn(nodes[i], sliceId));
// Add a text label. // Add a text label.
const groupText = group.append('text').attr('x', 6).attr('dy', 15); const groupText = group.append('text').attr('x', 6).attr('dy', 15);
@ -121,7 +121,7 @@ function Chord(element, props) {
.on('mouseover', d => { .on('mouseover', d => {
chord.classed('fade', p => p !== 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); .attr('d', path);
// Add an elaborate mouseover title for each chord. // Add an elaborate mouseover title for each chord.

View File

@ -18,7 +18,7 @@
*/ */
export default function transformProps(chartProps) { export default function transformProps(chartProps) {
const { width, height, formData, queriesData } = chartProps; const { width, height, formData, queriesData } = chartProps;
const { yAxisFormat, colorScheme } = formData; const { yAxisFormat, colorScheme, sliceId } = formData;
return { return {
colorScheme, colorScheme,
@ -26,5 +26,6 @@ export default function transformProps(chartProps) {
height, height,
numberFormat: yAxisFormat, numberFormat: yAxisFormat,
width, width,
sliceId,
}; };
} }

View File

@ -23,6 +23,7 @@ import { extent as d3Extent } from 'd3-array';
import { import {
getNumberFormatter, getNumberFormatter,
getSequentialSchemeRegistry, getSequentialSchemeRegistry,
CategoricalColorNamespace,
} from '@superset-ui/core'; } from '@superset-ui/core';
import countries, { countryOptions } from './countries'; import countries, { countryOptions } from './countries';
import './CountryMap.css'; import './CountryMap.css';
@ -45,17 +46,29 @@ const propTypes = {
const maps = {}; const maps = {};
function CountryMap(element, props) { function CountryMap(element, props) {
const { data, width, height, country, linearColorScheme, numberFormat } = const {
props; data,
width,
height,
country,
linearColorScheme,
numberFormat,
colorScheme,
sliceId,
} = props;
const container = element; const container = element;
const format = getNumberFormatter(numberFormat); const format = getNumberFormatter(numberFormat);
const colorScale = getSequentialSchemeRegistry() const linearColorScale = getSequentialSchemeRegistry()
.get(linearColorScheme) .get(linearColorScheme)
.createLinearScale(d3Extent(data, v => v.metric)); .createLinearScale(d3Extent(data, v => v.metric));
const colorScale = CategoricalColorNamespace.getScale(colorScheme);
const colorMap = {}; const colorMap = {};
data.forEach(d => { 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'; const colorFn = d => colorMap[d.properties.ISO] || 'none';

View File

@ -18,7 +18,13 @@
*/ */
export default function transformProps(chartProps) { export default function transformProps(chartProps) {
const { width, height, formData, queriesData } = chartProps; const { width, height, formData, queriesData } = chartProps;
const { linearColorScheme, numberFormat, selectCountry } = formData; const {
linearColorScheme,
numberFormat,
selectCountry,
colorScheme,
sliceId,
} = formData;
return { return {
width, width,
@ -27,5 +33,7 @@ export default function transformProps(chartProps) {
country: selectCountry ? String(selectCountry).toLowerCase() : null, country: selectCountry ? String(selectCountry).toLowerCase() : null,
linearColorScheme, linearColorScheme,
numberFormat, numberFormat,
colorScheme,
sliceId,
}; };
} }

View File

@ -71,13 +71,14 @@ class CustomHistogram extends React.PureComponent {
xAxisLabel, xAxisLabel,
yAxisLabel, yAxisLabel,
showLegend, showLegend,
sliceId,
} = this.props; } = this.props;
const colorFn = CategoricalColorNamespace.getScale(colorScheme); const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const keys = data.map(d => d.key); const keys = data.map(d => d.key);
const colorScale = scaleOrdinal({ const colorScale = scaleOrdinal({
domain: keys, domain: keys,
range: keys.map(x => colorFn(x)), range: keys.map(x => colorFn(x, sliceId)),
}); });
return ( return (

View File

@ -27,6 +27,7 @@ export default function transformProps(chartProps) {
xAxisLabel, xAxisLabel,
yAxisLabel, yAxisLabel,
showLegend, showLegend,
sliceId,
} = formData; } = formData;
return { return {
@ -41,5 +42,6 @@ export default function transformProps(chartProps) {
xAxisLabel, xAxisLabel,
yAxisLabel, yAxisLabel,
showLegend, showLegend,
sliceId,
}; };
} }

View File

@ -119,6 +119,7 @@ function Icicle(element, props) {
partitionThreshold, partitionThreshold,
useRichTooltip, useRichTooltip,
timeSeriesOption = 'not_time', timeSeriesOption = 'not_time',
sliceId,
} = props; } = props;
const div = d3.select(element); const div = d3.select(element);
@ -385,7 +386,7 @@ function Icicle(element, props) {
// Apply color scheme // Apply color scheme
g.selectAll('rect').style('fill', d => { g.selectAll('rect').style('fill', d => {
d.color = colorFn(d.name); d.color = colorFn(d.name, sliceId);
return d.color; return d.color;
}); });

View File

@ -30,6 +30,7 @@ export default function transformProps(chartProps) {
partitionThreshold, partitionThreshold,
richTooltip, richTooltip,
timeSeriesOption, timeSeriesOption,
sliceId,
} = formData; } = formData;
const { verboseMap } = datasource; const { verboseMap } = datasource;
@ -48,5 +49,6 @@ export default function transformProps(chartProps) {
timeSeriesOption, timeSeriesOption,
useLogScale: logScale, useLogScale: logScale,
useRichTooltip: richTooltip, useRichTooltip: richTooltip,
sliceId,
}; };
} }

View File

@ -76,6 +76,7 @@ function Rose(element, props) {
numberFormat, numberFormat,
useRichTooltip, useRichTooltip,
useAreaProportions, useAreaProportions,
sliceId,
} = props; } = props;
const div = d3.select(element); const div = d3.select(element);
@ -120,10 +121,10 @@ function Rose(element, props) {
.map(v => ({ .map(v => ({
key: v.name, key: v.name,
value: v.value, value: v.value,
color: colorFn(v.name), color: colorFn(v.name, sliceId),
highlight: v.id === d.arcId, 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 { return {
key: 'Date', 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); legendWrap.datum(legendData(datum)).call(legend);
tooltip.headerFormatter(timeFormat).valueFormatter(format); tooltip.headerFormatter(timeFormat).valueFormatter(format);
@ -378,7 +379,7 @@ function Rose(element, props) {
const arcs = ae const arcs = ae
.append('path') .append('path')
.attr('class', 'arc') .attr('class', 'arc')
.attr('fill', d => colorFn(d.name)) .attr('fill', d => colorFn(d.name, sliceId))
.attr('d', arc); .attr('d', arc);
function mousemove() { function mousemove() {

View File

@ -24,6 +24,7 @@ export default function transformProps(chartProps) {
numberFormat, numberFormat,
richTooltip, richTooltip,
roseAreaProportion, roseAreaProportion,
sliceId,
} = formData; } = formData;
return { return {
@ -35,5 +36,6 @@ export default function transformProps(chartProps) {
numberFormat, numberFormat,
useAreaProportions: roseAreaProportion, useAreaProportions: roseAreaProportion,
useRichTooltip: richTooltip, useRichTooltip: richTooltip,
sliceId,
}; };
} }

View File

@ -84,7 +84,7 @@ function computeGraph(links) {
} }
function SankeyLoop(element, props) { function SankeyLoop(element, props) {
const { data, width, height, colorScheme } = props; const { data, width, height, colorScheme, sliceId } = props;
const color = CategoricalColorNamespace.getScale(colorScheme); const color = CategoricalColorNamespace.getScale(colorScheme);
const margin = { ...defaultMargin, ...props.margin }; const margin = { ...defaultMargin, ...props.margin };
const innerWidth = width - margin.left - margin.right; const innerWidth = width - margin.left - margin.right;
@ -109,7 +109,7 @@ function SankeyLoop(element, props) {
value / sValue, value / sValue,
)})`, )})`,
) )
.linkColor(d => color(d.source.name)); .linkColor(d => color(d.source.name, sliceId));
const div = select(element); const div = select(element);
div.selectAll('*').remove(); div.selectAll('*').remove();

View File

@ -18,7 +18,7 @@
*/ */
export default function transformProps(chartProps) { export default function transformProps(chartProps) {
const { width, height, formData, queriesData, margin } = chartProps; const { width, height, formData, queriesData, margin } = chartProps;
const { colorScheme } = formData; const { colorScheme, sliceId } = formData;
return { return {
width, width,
@ -26,5 +26,6 @@ export default function transformProps(chartProps) {
data: queriesData[0].data, data: queriesData[0].data,
colorScheme, colorScheme,
margin, margin,
sliceId,
}; };
} }

View File

@ -44,7 +44,7 @@ const propTypes = {
const formatNumber = getNumberFormatter(NumberFormats.FLOAT); const formatNumber = getNumberFormatter(NumberFormats.FLOAT);
function Sankey(element, props) { function Sankey(element, props) {
const { data, width, height, colorScheme } = props; const { data, width, height, colorScheme, sliceId } = props;
const div = d3.select(element); const div = d3.select(element);
div.classed(`superset-legacy-chart-sankey`, true); div.classed(`superset-legacy-chart-sankey`, true);
const margin = { const margin = {
@ -219,7 +219,7 @@ function Sankey(element, props) {
.attr('width', sankey.nodeWidth()) .attr('width', sankey.nodeWidth())
.style('fill', d => { .style('fill', d => {
const name = d.name || 'N/A'; const name = d.name || 'N/A';
d.color = colorFn(name.replace(/ .*/, '')); d.color = colorFn(name, sliceId);
return d.color; return d.color;
}) })

View File

@ -20,7 +20,7 @@ import { getLabelFontSize } from './utils';
export default function transformProps(chartProps) { export default function transformProps(chartProps) {
const { width, height, formData, queriesData } = chartProps; const { width, height, formData, queriesData } = chartProps;
const { colorScheme } = formData; const { colorScheme, sliceId } = formData;
return { return {
width, width,
@ -28,5 +28,6 @@ export default function transformProps(chartProps) {
data: queriesData[0].data, data: queriesData[0].data,
colorScheme, colorScheme,
fontSize: getLabelFontSize(width), fontSize: getLabelFontSize(width),
sliceId,
}; };
} }

View File

@ -170,6 +170,7 @@ function Sunburst(element, props) {
linearColorScheme, linearColorScheme,
metrics, metrics,
numberFormat, numberFormat,
sliceId,
} = props; } = props;
const responsiveClass = getResponsiveContainerClass(width); const responsiveClass = getResponsiveContainerClass(width);
const isSmallWidth = responsiveClass === 's'; const isSmallWidth = responsiveClass === 's';
@ -287,7 +288,7 @@ function Sunburst(element, props) {
.attr('points', breadcrumbPoints) .attr('points', breadcrumbPoints)
.style('fill', d => .style('fill', d =>
colorByCategory colorByCategory
? categoricalColorScale(d.name) ? categoricalColorScale(d.name, sliceId)
: linearColorScale(d.m2 / d.m1), : 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 // Make text white or black based on the lightness of the background
const col = d3.hsl( const col = d3.hsl(
colorByCategory colorByCategory
? categoricalColorScale(d.name) ? categoricalColorScale(d.name, sliceId)
: linearColorScale(d.m2 / d.m1), : 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. // 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 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; colorByCategory = false;
const ext = d3.extent(nodes, d => d.m2 / d.m1); const ext = d3.extent(nodes, d => d.m2 / d.m1);
linearColorScale = getSequentialSchemeRegistry() linearColorScale = getSequentialSchemeRegistry()
@ -507,7 +508,7 @@ function Sunburst(element, props) {
.attr('fill-rule', 'evenodd') .attr('fill-rule', 'evenodd')
.style('fill', d => .style('fill', d =>
colorByCategory colorByCategory
? categoricalColorScale(d.name) ? categoricalColorScale(d.name, sliceId)
: linearColorScale(d.m2 / d.m1), : linearColorScale(d.m2 / d.m1),
) )
.style('opacity', 1) .style('opacity', 1)

View File

@ -18,7 +18,8 @@
*/ */
export default function transformProps(chartProps) { export default function transformProps(chartProps) {
const { width, height, formData, queriesData, datasource } = chartProps; const { width, height, formData, queriesData, datasource } = chartProps;
const { colorScheme, linearColorScheme, metric, secondaryMetric } = formData; const { colorScheme, linearColorScheme, metric, secondaryMetric, sliceId } =
formData;
const returnProps = { const returnProps = {
width, width,
@ -27,6 +28,7 @@ export default function transformProps(chartProps) {
colorScheme, colorScheme,
linearColorScheme, linearColorScheme,
metrics: [metric, secondaryMetric], metrics: [metric, secondaryMetric],
sliceId,
}; };
if (datasource && datasource.metrics) { if (datasource && datasource.metrics) {

View File

@ -87,6 +87,7 @@ function Treemap(element, props) {
numberFormat, numberFormat,
colorScheme, colorScheme,
treemapRatio, treemapRatio,
sliceId,
} = props; } = props;
const div = d3Select(element); const div = d3Select(element);
div.classed('superset-legacy-chart-treemap', true); div.classed('superset-legacy-chart-treemap', true);
@ -138,7 +139,7 @@ function Treemap(element, props) {
.attr('id', d => `rect-${d.data.name}`) .attr('id', d => `rect-${d.data.name}`)
.attr('width', d => d.x1 - d.x0) .attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0) .attr('height', d => d.y1 - d.y0)
.style('fill', d => colorFn(d.depth)); .style('fill', d => colorFn(d.depth, sliceId));
cell cell
.append('clipPath') .append('clipPath')

View File

@ -18,7 +18,7 @@
*/ */
export default function transformProps(chartProps) { export default function transformProps(chartProps) {
const { width, height, formData, queriesData } = chartProps; const { width, height, formData, queriesData } = chartProps;
const { colorScheme, treemapRatio } = formData; const { colorScheme, treemapRatio, sliceId } = formData;
let { numberFormat } = formData; let { numberFormat } = formData;
if (!numberFormat && chartProps.datasource && chartProps.datasource.metrics) { if (!numberFormat && chartProps.datasource && chartProps.datasource.metrics) {
@ -39,5 +39,6 @@ export default function transformProps(chartProps) {
colorScheme, colorScheme,
numberFormat, numberFormat,
treemapRatio, treemapRatio,
sliceId,
}; };
} }

View File

@ -23,6 +23,7 @@ import { extent as d3Extent } from 'd3-array';
import { import {
getNumberFormatter, getNumberFormatter,
getSequentialSchemeRegistry, getSequentialSchemeRegistry,
CategoricalColorNamespace,
} from '@superset-ui/core'; } from '@superset-ui/core';
import Datamap from 'datamaps/dist/datamaps.world.min'; import Datamap from 'datamaps/dist/datamaps.world.min';
@ -55,6 +56,8 @@ function WorldMap(element, props) {
showBubbles, showBubbles,
linearColorScheme, linearColorScheme,
color, color,
colorScheme,
sliceId,
} = props; } = props;
const div = d3.select(element); const div = d3.select(element);
div.classed('superset-legacy-chart-world-map', true); div.classed('superset-legacy-chart-world-map', true);
@ -69,15 +72,24 @@ function WorldMap(element, props) {
.domain([extRadius[0], extRadius[1]]) .domain([extRadius[0], extRadius[1]])
.range([1, maxBubbleSize]); .range([1, maxBubbleSize]);
const colorScale = getSequentialSchemeRegistry() const linearColorScale = getSequentialSchemeRegistry()
.get(linearColorScheme) .get(linearColorScheme)
.createLinearScale(d3Extent(filteredData, d => d.m1)); .createLinearScale(d3Extent(filteredData, d => d.m1));
const processedData = filteredData.map(d => ({ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
...d,
radius: radiusScale(Math.sqrt(d.m2)), const processedData = filteredData.map(d => {
fillColor: colorScale(d.m1), 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 = {}; const mapData = {};
processedData.forEach(d => { processedData.forEach(d => {

View File

@ -106,6 +106,7 @@ const config: ControlPanelConfig = {
}, },
], ],
['color_picker'], ['color_picker'],
['color_scheme'],
['linear_color_scheme'], ['linear_color_scheme'],
], ],
}, },
@ -126,6 +127,9 @@ const config: ControlPanelConfig = {
color_picker: { color_picker: {
label: t('Bubble Color'), label: t('Bubble Color'),
}, },
color_scheme: {
label: t('Categorical Color Scheme'),
},
linear_color_scheme: { linear_color_scheme: {
label: t('Country Color Scheme'), label: t('Country Color Scheme'),
}, },

View File

@ -20,8 +20,14 @@ import { rgb } from 'd3-color';
export default function transformProps(chartProps) { export default function transformProps(chartProps) {
const { width, height, formData, queriesData } = chartProps; const { width, height, formData, queriesData } = chartProps;
const { maxBubbleSize, showBubbles, linearColorScheme, colorPicker } = const {
formData; maxBubbleSize,
showBubbles,
linearColorScheme,
colorPicker,
colorScheme,
sliceId,
} = formData;
const { r, g, b } = colorPicker; const { r, g, b } = colorPicker;
return { return {
@ -32,5 +38,7 @@ export default function transformProps(chartProps) {
showBubbles, showBubbles,
linearColorScheme, linearColorScheme,
color: rgb(r, g, b).hex(), color: rgb(r, g, b).hex(),
colorScheme,
sliceId,
}; };
} }

View File

@ -46,7 +46,7 @@ function getCategories(fd, data) {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) { if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color; let color;
if (fd.dimension) { if (fd.dimension) {
color = hexToRGB(colorFn(d.cat_color), c.a * 255); color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
} else { } else {
color = fixedColor; color = fixedColor;
} }
@ -212,7 +212,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
return data.map(d => { return data.map(d => {
let color; let color;
if (fd.dimension) { 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 }; return { ...d, color };
} }

View File

@ -313,6 +313,7 @@ function nvd3Vis(element, props) {
yAxis2ShowMinMax = false, yAxis2ShowMinMax = false,
yField, yField,
yIsLogScale, yIsLogScale,
sliceId,
} = props; } = props;
const isExplore = document.querySelector('#explorer-container') !== null; const isExplore = document.querySelector('#explorer-container') !== null;
@ -670,7 +671,9 @@ function nvd3Vis(element, props) {
); );
} else if (vizType !== 'bullet') { } else if (vizType !== 'bullet') {
const colorFn = getScale(colorScheme); 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) { if (isVizTypes(['line', 'area', 'bar', 'dist_bar']) && useRichTooltip) {

View File

@ -94,6 +94,7 @@ export default function transformProps(chartProps) {
yAxisShowminmax, yAxisShowminmax,
yAxis2Showminmax, yAxis2Showminmax,
yLogScale, yLogScale,
sliceId,
} = formData; } = formData;
let { let {
@ -195,5 +196,6 @@ export default function transformProps(chartProps) {
yAxis2ShowMinMax: yAxis2Showminmax, yAxis2ShowMinMax: yAxis2Showminmax,
yField: y, yField: y,
yIsLogScale: yLogScale, yIsLogScale: yLogScale,
sliceId,
}; };
} }

View File

@ -63,6 +63,7 @@ export default function transformProps(
xAxisTitleMargin, xAxisTitleMargin,
yAxisTitleMargin, yAxisTitleMargin,
yAxisTitlePosition, yAxisTitlePosition,
sliceId,
} = formData as BoxPlotQueryFormData; } = formData as BoxPlotQueryFormData;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat); const numberFormatter = getNumberFormatter(numberFormat);
@ -98,9 +99,9 @@ export default function transformProps(
datum[`${metric}__outliers`], datum[`${metric}__outliers`],
], ],
itemStyle: { itemStyle: {
color: colorFn(groupbyLabel), color: colorFn(groupbyLabel, sliceId),
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6, opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
borderColor: colorFn(groupbyLabel), borderColor: colorFn(groupbyLabel, sliceId),
}, },
}; };
}); });
@ -138,7 +139,7 @@ export default function transformProps(
}, },
}, },
itemStyle: { itemStyle: {
color: colorFn(groupbyLabel), color: colorFn(groupbyLabel, sliceId),
opacity: isFiltered opacity: isFiltered
? OpacityEnum.SemiTransparent ? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent, : OpacityEnum.NonTransparent,

View File

@ -103,6 +103,7 @@ export default function transformProps(
showLabels, showLabels,
showLegend, showLegend,
emitFilter, emitFilter,
sliceId,
}: EchartsFunnelFormData = { }: EchartsFunnelFormData = {
...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_LEGEND_FORM_DATA,
...DEFAULT_FUNNEL_FORM_DATA, ...DEFAULT_FUNNEL_FORM_DATA,
@ -145,7 +146,7 @@ export default function transformProps(
value: datum[metricLabel], value: datum[metricLabel],
name, name,
itemStyle: { itemStyle: {
color: colorFn(name), color: colorFn(name, sliceId),
opacity: isFiltered opacity: isFiltered
? OpacityEnum.SemiTransparent ? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent, : OpacityEnum.NonTransparent,

View File

@ -107,6 +107,7 @@ export default function transformProps(
intervalColorIndices, intervalColorIndices,
valueFormatter, valueFormatter,
emitFilter, emitFilter,
sliceId,
}: EchartsGaugeFormData = { ...DEFAULT_GAUGE_FORM_DATA, ...formData }; }: EchartsGaugeFormData = { ...DEFAULT_GAUGE_FORM_DATA, ...formData };
const data = (queriesData[0]?.data || []) as DataRecord[]; const data = (queriesData[0]?.data || []) as DataRecord[];
const numberFormatter = getNumberFormatter(numberFormat); const numberFormatter = getNumberFormatter(numberFormat);
@ -147,7 +148,7 @@ export default function transformProps(
value: data_point[getMetricLabel(metric as QueryFormMetric)] as number, value: data_point[getMetricLabel(metric as QueryFormMetric)] as number,
name, name,
itemStyle: { itemStyle: {
color: colorFn(index), color: colorFn(index, sliceId),
}, },
title: { title: {
offsetCenter: [ offsetCenter: [
@ -175,7 +176,7 @@ export default function transformProps(
item = { item = {
...item, ...item,
itemStyle: { itemStyle: {
color: colorFn(index), color: colorFn(index, sliceId),
opacity: OpacityEnum.SemiTransparent, opacity: OpacityEnum.SemiTransparent,
}, },
detail: { detail: {

View File

@ -184,6 +184,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
baseEdgeWidth, baseEdgeWidth,
baseNodeSize, baseNodeSize,
edgeSymbol, edgeSymbol,
sliceId,
}: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData }; }: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData };
const metricLabel = getMetricLabel(metric); const metricLabel = getMetricLabel(metric);
@ -264,7 +265,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
type: 'graph', type: 'graph',
categories: categoryList.map(c => ({ categories: categoryList.map(c => ({
name: c, name: c,
itemStyle: { color: colorFn(c) }, itemStyle: { color: colorFn(c, sliceId) },
})), })),
layout, layout,
force: { force: {

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { QueryFormData } from '@superset-ui/core';
import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries'; import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries';
import { SeriesTooltipOption } from 'echarts/types/src/util/types'; import { SeriesTooltipOption } from 'echarts/types/src/util/types';
import { import {
@ -27,32 +28,34 @@ import {
export type EdgeSymbol = 'none' | 'circle' | 'arrow'; export type EdgeSymbol = 'none' | 'circle' | 'arrow';
export type EchartsGraphFormData = EchartsLegendFormData & { export type EchartsGraphFormData = QueryFormData &
source: string; EchartsLegendFormData & {
target: string; source: string;
sourceCategory?: string; target: string;
targetCategory?: string; sourceCategory?: string;
colorScheme?: string; targetCategory?: string;
metric?: string; colorScheme?: string;
layout?: 'none' | 'circular' | 'force'; metric?: string;
roam: boolean | 'scale' | 'move'; layout?: 'none' | 'circular' | 'force';
draggable: boolean; roam: boolean | 'scale' | 'move';
selectedMode?: boolean | 'multiple' | 'single'; draggable: boolean;
showSymbolThreshold: number; selectedMode?: boolean | 'multiple' | 'single';
repulsion: number; showSymbolThreshold: number;
gravity: number; repulsion: number;
baseNodeSize: number; gravity: number;
baseEdgeWidth: number; baseNodeSize: number;
edgeLength: number; baseEdgeWidth: number;
edgeSymbol: string; edgeLength: number;
friction: number; edgeSymbol: string;
}; friction: number;
};
export type EChartGraphNode = Omit<GraphNodeItemOption, 'value'> & { export type EChartGraphNode = Omit<GraphNodeItemOption, 'value'> & {
value: number; value: number;
tooltip?: Pick<SeriesTooltipOption, 'formatter'>; tooltip?: Pick<SeriesTooltipOption, 'formatter'>;
}; };
// @ts-ignore
export const DEFAULT_FORM_DATA: EchartsGraphFormData = { export const DEFAULT_FORM_DATA: EchartsGraphFormData = {
...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_LEGEND_FORM_DATA,
source: '', source: '',

View File

@ -128,6 +128,7 @@ export default function transformProps(
xAxisTitleMargin, xAxisTitleMargin,
yAxisTitleMargin, yAxisTitleMargin,
yAxisTitlePosition, yAxisTitlePosition,
sliceId,
}: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; }: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
@ -177,6 +178,7 @@ export default function transformProps(
yAxisIndex, yAxisIndex,
filterState, filterState,
seriesKey: entry.name, seriesKey: entry.name,
sliceId,
}); });
if (transformedSeries) series.push(transformedSeries); if (transformedSeries) series.push(transformedSeries);
}); });
@ -195,6 +197,7 @@ export default function transformProps(
seriesKey: primarySeries.has(entry.name as string) seriesKey: primarySeries.has(entry.name as string)
? `${entry.name} (1)` ? `${entry.name} (1)`
: entry.name, : entry.name,
sliceId,
}); });
if (transformedSeries) series.push(transformedSeries); if (transformedSeries) series.push(transformedSeries);
}); });
@ -203,7 +206,9 @@ export default function transformProps(
.filter((layer: AnnotationLayer) => layer.show) .filter((layer: AnnotationLayer) => layer.show)
.forEach((layer: AnnotationLayer) => { .forEach((layer: AnnotationLayer) => {
if (isFormulaAnnotationLayer(layer)) if (isFormulaAnnotationLayer(layer))
series.push(transformFormulaAnnotation(layer, data1, colorScale)); series.push(
transformFormulaAnnotation(layer, data1, colorScale, sliceId),
);
else if (isIntervalAnnotationLayer(layer)) { else if (isIntervalAnnotationLayer(layer)) {
series.push( series.push(
...transformIntervalAnnotation( ...transformIntervalAnnotation(
@ -211,11 +216,18 @@ export default function transformProps(
data1, data1,
annotationData, annotationData,
colorScale, colorScale,
sliceId,
), ),
); );
} else if (isEventAnnotationLayer(layer)) { } else if (isEventAnnotationLayer(layer)) {
series.push( series.push(
...transformEventAnnotation(layer, data1, annotationData, colorScale), ...transformEventAnnotation(
layer,
data1,
annotationData,
colorScale,
sliceId,
),
); );
} else if (isTimeseriesAnnotationLayer(layer)) { } else if (isTimeseriesAnnotationLayer(layer)) {
series.push( series.push(

View File

@ -109,6 +109,7 @@ export default function transformProps(
showLegend, showLegend,
showLabelsThreshold, showLabelsThreshold,
emitFilter, emitFilter,
sliceId,
}: EchartsPieFormData = { }: EchartsPieFormData = {
...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_LEGEND_FORM_DATA,
...DEFAULT_PIE_FORM_DATA, ...DEFAULT_PIE_FORM_DATA,
@ -162,7 +163,7 @@ export default function transformProps(
value: datum[metricLabel], value: datum[metricLabel],
name, name,
itemStyle: { itemStyle: {
color: colorFn(name), color: colorFn(name, sliceId),
opacity: isFiltered opacity: isFiltered
? OpacityEnum.SemiTransparent ? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent, : OpacityEnum.NonTransparent,

View File

@ -91,6 +91,7 @@ export default function transformProps(
showLegend, showLegend,
isCircle, isCircle,
columnConfig, columnConfig,
sliceId,
}: EchartsRadarFormData = { }: EchartsRadarFormData = {
...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_LEGEND_FORM_DATA,
...DEFAULT_RADAR_FORM_DATA, ...DEFAULT_RADAR_FORM_DATA,
@ -154,7 +155,7 @@ export default function transformProps(
value: metricLabels.map(metricLabel => datum[metricLabel]), value: metricLabels.map(metricLabel => datum[metricLabel]),
name: joinedName, name: joinedName,
itemStyle: { itemStyle: {
color: colorFn(joinedName), color: colorFn(joinedName, sliceId),
opacity: isFiltered opacity: isFiltered
? OpacityEnum.Transparent ? OpacityEnum.Transparent
: OpacityEnum.NonTransparent, : OpacityEnum.NonTransparent,

View File

@ -125,6 +125,7 @@ export default function transformProps(
xAxisTitleMargin, xAxisTitleMargin,
yAxisTitleMargin, yAxisTitleMargin,
yAxisTitlePosition, yAxisTitlePosition,
sliceId,
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
@ -198,6 +199,7 @@ export default function transformProps(
showValueIndexes, showValueIndexes,
thresholdValues, thresholdValues,
richTooltip, richTooltip,
sliceId,
}); });
if (transformedSeries) series.push(transformedSeries); if (transformedSeries) series.push(transformedSeries);
}); });
@ -217,7 +219,9 @@ export default function transformProps(
.filter((layer: AnnotationLayer) => layer.show) .filter((layer: AnnotationLayer) => layer.show)
.forEach((layer: AnnotationLayer) => { .forEach((layer: AnnotationLayer) => {
if (isFormulaAnnotationLayer(layer)) if (isFormulaAnnotationLayer(layer))
series.push(transformFormulaAnnotation(layer, data, colorScale)); series.push(
transformFormulaAnnotation(layer, data, colorScale, sliceId),
);
else if (isIntervalAnnotationLayer(layer)) { else if (isIntervalAnnotationLayer(layer)) {
series.push( series.push(
...transformIntervalAnnotation( ...transformIntervalAnnotation(
@ -225,11 +229,18 @@ export default function transformProps(
data, data,
annotationData, annotationData,
colorScale, colorScale,
sliceId,
), ),
); );
} else if (isEventAnnotationLayer(layer)) { } else if (isEventAnnotationLayer(layer)) {
series.push( series.push(
...transformEventAnnotation(layer, data, annotationData, colorScale), ...transformEventAnnotation(
layer,
data,
annotationData,
colorScale,
sliceId,
),
); );
} else if (isTimeseriesAnnotationLayer(layer)) { } else if (isTimeseriesAnnotationLayer(layer)) {
series.push( series.push(

View File

@ -84,6 +84,7 @@ export function transformSeries(
thresholdValues?: number[]; thresholdValues?: number[];
richTooltip?: boolean; richTooltip?: boolean;
seriesKey?: OptionName; seriesKey?: OptionName;
sliceId?: number;
}, },
): SeriesOption | undefined { ): SeriesOption | undefined {
const { name } = series; const { name } = series;
@ -105,6 +106,7 @@ export function transformSeries(
thresholdValues = [], thresholdValues = [],
richTooltip, richTooltip,
seriesKey, seriesKey,
sliceId,
} = opts; } = opts;
const contexts = seriesContexts[name || ''] || []; const contexts = seriesContexts[name || ''] || [];
const hasForecast = const hasForecast =
@ -151,7 +153,7 @@ export function transformSeries(
} }
// forcing the colorScale to return a different color for same metrics across different queries // forcing the colorScale to return a different color for same metrics across different queries
const itemStyle = { const itemStyle = {
color: colorScale(seriesKey || forecastSeries.name), color: colorScale(seriesKey || forecastSeries.name, sliceId),
opacity, opacity,
}; };
let emphasis = {}; let emphasis = {};
@ -244,13 +246,14 @@ export function transformFormulaAnnotation(
layer: FormulaAnnotationLayer, layer: FormulaAnnotationLayer,
data: TimeseriesDataRecord[], data: TimeseriesDataRecord[],
colorScale: CategoricalColorScale, colorScale: CategoricalColorScale,
sliceId?: number,
): SeriesOption { ): SeriesOption {
const { name, color, opacity, width, style } = layer; const { name, color, opacity, width, style } = layer;
return { return {
name, name,
id: name, id: name,
itemStyle: { itemStyle: {
color: color || colorScale(name), color: color || colorScale(name, sliceId),
}, },
lineStyle: { lineStyle: {
opacity: parseAnnotationOpacity(opacity), opacity: parseAnnotationOpacity(opacity),
@ -269,6 +272,7 @@ export function transformIntervalAnnotation(
data: TimeseriesDataRecord[], data: TimeseriesDataRecord[],
annotationData: AnnotationData, annotationData: AnnotationData,
colorScale: CategoricalColorScale, colorScale: CategoricalColorScale,
sliceId?: number,
): SeriesOption[] { ): SeriesOption[] {
const series: SeriesOption[] = []; const series: SeriesOption[] = [];
const annotations = extractRecordAnnotations(layer, annotationData); const annotations = extractRecordAnnotations(layer, annotationData);
@ -323,7 +327,7 @@ export function transformIntervalAnnotation(
markArea: { markArea: {
silent: false, silent: false,
itemStyle: { itemStyle: {
color: color || colorScale(name), color: color || colorScale(name, sliceId),
opacity: parseAnnotationOpacity(opacity || AnnotationOpacity.Medium), opacity: parseAnnotationOpacity(opacity || AnnotationOpacity.Medium),
emphasis: { emphasis: {
opacity: 0.8, opacity: 0.8,
@ -342,6 +346,7 @@ export function transformEventAnnotation(
data: TimeseriesDataRecord[], data: TimeseriesDataRecord[],
annotationData: AnnotationData, annotationData: AnnotationData,
colorScale: CategoricalColorScale, colorScale: CategoricalColorScale,
sliceId?: number,
): SeriesOption[] { ): SeriesOption[] {
const series: SeriesOption[] = []; const series: SeriesOption[] = [];
const annotations = extractRecordAnnotations(layer, annotationData); const annotations = extractRecordAnnotations(layer, annotationData);
@ -359,7 +364,7 @@ export function transformEventAnnotation(
const lineStyle: LineStyleOption & DefaultStatesMixin['emphasis'] = { const lineStyle: LineStyleOption & DefaultStatesMixin['emphasis'] = {
width, width,
type: style as ZRLineType, type: style as ZRLineType,
color: color || colorScale(name), color: color || colorScale(name, sliceId),
opacity: parseAnnotationOpacity(opacity), opacity: parseAnnotationOpacity(opacity),
emphasis: { emphasis: {
width: width ? width + 1 : width, width: width ? width + 1 : width,

View File

@ -127,6 +127,7 @@ export default function transformProps(
showUpperLabels, showUpperLabels,
dashboardId, dashboardId,
emitFilter, emitFilter,
sliceId,
}: EchartsTreemapFormData = { }: EchartsTreemapFormData = {
...DEFAULT_TREEMAP_FORM_DATA, ...DEFAULT_TREEMAP_FORM_DATA,
...formData, ...formData,
@ -223,7 +224,7 @@ export default function transformProps(
colorSaturation: COLOR_SATURATION, colorSaturation: COLOR_SATURATION,
itemStyle: { itemStyle: {
borderColor: BORDER_COLOR, borderColor: BORDER_COLOR,
color: colorFn(`${child.name}`), color: colorFn(`${child.name}`, sliceId),
borderWidth: BORDER_WIDTH, borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH, gapWidth: GAP_WIDTH,
}, },
@ -259,7 +260,7 @@ export default function transformProps(
show: false, show: false,
}, },
itemStyle: { itemStyle: {
color: CategoricalColorNamespace.getColor(), color: '#1FA8C9',
}, },
}, },
]; ];

View File

@ -25,7 +25,12 @@ import {
DeriveEncoding, DeriveEncoding,
Encoder, Encoder,
} from 'encodable'; } from 'encodable';
import { SupersetThemeProps, withTheme, seedRandom } from '@superset-ui/core'; import {
SupersetThemeProps,
withTheme,
seedRandom,
CategoricalColorScale,
} from '@superset-ui/core';
export const ROTATION = { export const ROTATION = {
flat: () => 0, flat: () => 0,
@ -58,6 +63,7 @@ export interface WordCloudProps extends WordCloudVisualProps {
data: PlainObject[]; data: PlainObject[];
height: number; height: number;
width: number; width: number;
sliceId: number;
} }
export interface WordCloudState { export interface WordCloudState {
@ -210,12 +216,15 @@ class WordCloud extends React.PureComponent<
render() { render() {
const { scaleFactor } = this.state; const { scaleFactor } = this.state;
const { width, height, encoding } = this.props; const { width, height, encoding, sliceId } = this.props;
const { words } = this.state; const { words } = this.state;
const encoder = this.createEncoder(encoding); const encoder = this.createEncoder(encoding);
encoder.channels.color.setDomainFromDataset(words); encoder.channels.color.setDomainFromDataset(words);
const { getValueFromDatum } = encoder.channels.color;
const colorFn = encoder.channels.color.scale as CategoricalColorScale;
const viewBoxWidth = width * scaleFactor; const viewBoxWidth = width * scaleFactor;
const viewBoxHeight = height * scaleFactor; const viewBoxHeight = height * scaleFactor;
@ -234,7 +243,7 @@ class WordCloud extends React.PureComponent<
fontSize={`${w.size}px`} fontSize={`${w.size}px`}
fontWeight={w.weight} fontWeight={w.weight}
fontFamily={w.font} fontFamily={w.font}
fill={encoder.channels.color.encodeDatum(w, '')} fill={colorFn(getValueFromDatum(w) as string, sliceId)}
textAnchor="middle" textAnchor="middle"
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`} transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
> >

View File

@ -43,6 +43,7 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps {
series, series,
sizeFrom = 0, sizeFrom = 0,
sizeTo, sizeTo,
sliceId,
} = formData as LegacyWordCloudFormData; } = formData as LegacyWordCloudFormData;
const metricLabel = getMetricLabel(metric); const metricLabel = getMetricLabel(metric);
@ -77,5 +78,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps {
height, height,
rotation, rotation,
width, width,
sliceId,
}; };
} }

View File

@ -23,7 +23,7 @@ import { WordCloudFormData } from '../types';
export default function transformProps(chartProps: ChartProps): WordCloudProps { export default function transformProps(chartProps: ChartProps): WordCloudProps {
const { width, height, formData, queriesData } = chartProps; const { width, height, formData, queriesData } = chartProps;
const { encoding, rotation } = formData as WordCloudFormData; const { encoding, rotation, sliceId } = formData as WordCloudFormData;
return { return {
data: queriesData[0].data, data: queriesData[0].data,
@ -31,5 +31,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps {
height, height,
rotation, rotation,
width, width,
sliceId,
}; };
} }

View File

@ -47,6 +47,7 @@ const propTypes = {
// and merged with extra filter that current dashboard applying // and merged with extra filter that current dashboard applying
formData: PropTypes.object.isRequired, formData: PropTypes.object.isRequired,
labelColors: PropTypes.object, labelColors: PropTypes.object,
sharedLabelColors: PropTypes.object,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
setControlValue: PropTypes.func, setControlValue: PropTypes.func,
@ -70,6 +71,7 @@ const propTypes = {
onFilterMenuOpen: PropTypes.func, onFilterMenuOpen: PropTypes.func,
onFilterMenuClose: PropTypes.func, onFilterMenuClose: PropTypes.func,
ownState: PropTypes.object, ownState: PropTypes.object,
postTransformProps: PropTypes.func,
}; };
const BLANK = {}; const BLANK = {};

View File

@ -31,6 +31,7 @@ const propTypes = {
initialValues: PropTypes.object, initialValues: PropTypes.object,
formData: PropTypes.object.isRequired, formData: PropTypes.object.isRequired,
labelColors: PropTypes.object, labelColors: PropTypes.object,
sharedLabelColors: PropTypes.object,
height: PropTypes.number, height: PropTypes.number,
width: PropTypes.number, width: PropTypes.number,
setControlValue: PropTypes.func, setControlValue: PropTypes.func,
@ -48,6 +49,7 @@ const propTypes = {
onFilterMenuOpen: PropTypes.func, onFilterMenuOpen: PropTypes.func,
onFilterMenuClose: PropTypes.func, onFilterMenuClose: PropTypes.func,
ownState: PropTypes.object, ownState: PropTypes.object,
postTransformProps: PropTypes.func,
source: PropTypes.oneOf(['dashboard', 'explore']), source: PropTypes.oneOf(['dashboard', 'explore']),
}; };
@ -107,6 +109,7 @@ class ChartRenderer extends React.Component {
nextProps.width !== this.props.width || nextProps.width !== this.props.width ||
nextProps.triggerRender || nextProps.triggerRender ||
nextProps.labelColors !== this.props.labelColors || nextProps.labelColors !== this.props.labelColors ||
nextProps.sharedLabelColors !== this.props.sharedLabelColors ||
nextProps.formData.color_scheme !== this.props.formData.color_scheme || nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
nextProps.cacheBusterProp !== this.props.cacheBusterProp nextProps.cacheBusterProp !== this.props.cacheBusterProp
); );
@ -192,6 +195,7 @@ class ChartRenderer extends React.Component {
filterState, filterState,
formData, formData,
queriesResponse, queriesResponse,
postTransformProps,
} = this.props; } = this.props;
// It's bad practice to use unprefixed `vizType` as classnames for chart // It's bad practice to use unprefixed `vizType` as classnames for chart
@ -260,6 +264,7 @@ class ChartRenderer extends React.Component {
onRenderSuccess={this.handleRenderSuccess} onRenderSuccess={this.handleRenderSuccess}
onRenderFailure={this.handleRenderFailure} onRenderFailure={this.handleRenderFailure}
noResults={noResultsComponent} noResults={noResultsComponent}
postTransformProps={postTransformProps}
/> />
); );
} }

View File

@ -23,6 +23,21 @@ import { ChartConfiguration, DashboardInfo } from '../reducers/types';
export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED'; export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED';
export function updateColorSchema(
metadata: Record<string, any>,
labelColors: Record<string, string>,
) {
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 // updates partially changed dashboard info
export function dashboardInfoChanged(newInfo: { metadata: any }) { export function dashboardInfoChanged(newInfo: { metadata: any }) {
const { metadata } = newInfo; const { metadata } = newInfo;
@ -33,14 +48,12 @@ export function dashboardInfoChanged(newInfo: { metadata: any }) {
categoricalNamespace.resetColors(); categoricalNamespace.resetColors();
if (metadata?.shared_label_colors) {
updateColorSchema(metadata, metadata?.shared_label_colors);
}
if (metadata?.label_colors) { if (metadata?.label_colors) {
const labelColors = metadata.label_colors; updateColorSchema(metadata, metadata?.label_colors);
const colorMap = isString(labelColors)
? JSON.parse(labelColors)
: labelColors;
Object.keys(colorMap).forEach(label => {
categoricalNamespace.setColor(label, colorMap[label]);
});
} }
return { type: DASHBOARD_INFO_UPDATED, newInfo }; return { type: DASHBOARD_INFO_UPDATED, newInfo };

View File

@ -47,17 +47,19 @@ function setUnsavedChangesAfterAction(action) {
dispatch(result); dispatch(result);
} }
const { dashboardLayout, dashboardState } = getState();
const isComponentLevelEvent = const isComponentLevelEvent =
result.type === UPDATE_COMPONENTS && result.type === UPDATE_COMPONENTS &&
result.payload && result.payload &&
result.payload.nextComponents; result.payload.nextComponents;
// trigger dashboardFilters state update if dashboard layout is changed. // trigger dashboardFilters state update if dashboard layout is changed.
if (!isComponentLevelEvent) { if (!isComponentLevelEvent) {
const components = getState().dashboardLayout.present; const components = dashboardLayout.present;
dispatch(updateLayoutComponents(components)); dispatch(updateLayoutComponents(components));
} }
if (!getState().dashboardState.hasUnsavedChanges) { if (!dashboardState.hasUnsavedChanges) {
dispatch(setUnsavedChanges(true)); dispatch(setUnsavedChanges(true));
} }
}; };

View File

@ -18,7 +18,12 @@
*/ */
/* eslint camelcase: 0 */ /* eslint camelcase: 0 */
import { ActionCreators as UndoActionCreators } from 'redux-undo'; 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 { import {
addChart, addChart,
removeChart, removeChart,
@ -67,6 +72,11 @@ export function removeSlice(sliceId) {
return { type: REMOVE_SLICE, 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'; const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR'; export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
export function toggleFaveStar(isStarred) { export function toggleFaveStar(isStarred) {
@ -232,6 +242,7 @@ export function saveDashboardRequest(data, id, saveType) {
color_scheme: data.metadata?.color_scheme || '', color_scheme: data.metadata?.color_scheme || '',
expanded_slices: data.metadata?.expanded_slices || {}, expanded_slices: data.metadata?.expanded_slices || {},
label_colors: data.metadata?.label_colors || {}, label_colors: data.metadata?.label_colors || {},
shared_label_colors: data.metadata?.shared_label_colors || {},
refresh_frequency: data.metadata?.refresh_frequency || 0, refresh_frequency: data.metadata?.refresh_frequency || 0,
timed_refresh_immune_slices: timed_refresh_immune_slices:
data.metadata?.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) { export function removeSliceFromDashboard(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const sliceEntity = getState().sliceEntities.slices[id]; const sliceEntity = getState().sliceEntities.slices[id];
@ -504,6 +537,20 @@ export function removeSliceFromDashboard(id) {
dispatch(removeSlice(id)); dispatch(removeSlice(id));
dispatch(removeChart(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,
}),
);
}; };
} }

View File

@ -39,7 +39,11 @@ describe('dashboardState actions', () => {
sliceIds: [filterId], sliceIds: [filterId],
hasUnsavedChanges: true, hasUnsavedChanges: true,
}, },
dashboardInfo: {}, dashboardInfo: {
metadata: {
color_scheme: 'supersetColors',
},
},
sliceEntities, sliceEntities,
dashboardFilters: emptyFilters, dashboardFilters: emptyFilters,
dashboardLayout: { dashboardLayout: {
@ -116,6 +120,6 @@ describe('dashboardState actions', () => {
const removeFilter = dispatch.getCall(0).args[0]; const removeFilter = dispatch.getCall(0).args[0];
removeFilter(dispatch, getState); removeFilter(dispatch, getState);
expect(dispatch.getCall(3).args[0].type).toBe(REMOVE_FILTER); expect(dispatch.getCall(4).args[0].type).toBe(REMOVE_FILTER);
}); });
}); });

View File

@ -17,12 +17,7 @@
* under the License. * under the License.
*/ */
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import { isString } from 'lodash'; import { Behavior, getChartMetadataRegistry } from '@superset-ui/core';
import {
Behavior,
CategoricalColorNamespace,
getChartMetadataRegistry,
} from '@superset-ui/core';
import { chart } from 'src/components/Chart/chartReducer'; import { chart } from 'src/components/Chart/chartReducer';
import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; 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 { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
import extractUrlParams from '../util/extractUrlParams'; import extractUrlParams from '../util/extractUrlParams';
import getNativeFilterConfig from '../util/filterboxMigrationHelper'; import getNativeFilterConfig from '../util/filterboxMigrationHelper';
import { updateColorSchema } from './dashboardInfo';
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; 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 // Priming the color palette with user's label-color mapping provided in
// the dashboard's JSON metadata // the dashboard's JSON metadata
if (metadata?.label_colors) { if (metadata?.label_colors) {
const namespace = metadata.color_namespace; updateColorSchema(metadata, metadata?.label_colors);
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]);
});
} }
// dashboard layout // dashboard layout

View File

@ -20,7 +20,7 @@
import moment from 'moment'; import moment from 'moment';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 ButtonGroup from 'src/components/ButtonGroup';
import { import {
@ -356,6 +356,15 @@ class Header extends React.PureComponent {
? currentRefreshFrequency ? currentRefreshFrequency
: dashboardInfo.metadata?.refresh_frequency; : 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 = { const data = {
certified_by: dashboardInfo.certified_by, certified_by: dashboardInfo.certified_by,
certification_details: dashboardInfo.certification_details, certification_details: dashboardInfo.certification_details,
@ -367,11 +376,11 @@ class Header extends React.PureComponent {
slug, slug,
metadata: { metadata: {
...dashboardInfo?.metadata, ...dashboardInfo?.metadata,
color_namespace: color_namespace: currentColorNamespace,
dashboardInfo?.metadata?.color_namespace || colorNamespace, color_scheme: currentColorScheme,
color_scheme: dashboardInfo?.metadata?.color_scheme || colorScheme,
positions, positions,
refresh_frequency: refreshFrequency, refresh_frequency: refreshFrequency,
shared_label_colors: currentSharedLabelColors,
}, },
}; };

View File

@ -29,6 +29,7 @@ import {
SupersetClient, SupersetClient,
getCategoricalSchemeRegistry, getCategoricalSchemeRegistry,
ensureIsArray, ensureIsArray,
getSharedLabelColor,
} from '@superset-ui/core'; } from '@superset-ui/core';
import Modal from 'src/components/Modal'; import Modal from 'src/components/Modal';
@ -169,7 +170,11 @@ const PropertiesModal = ({
if (metadata?.positions) { if (metadata?.positions) {
delete 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], [form],
); );
@ -282,12 +287,25 @@ const PropertiesModal = ({
form.getFieldsValue(); form.getFieldsValue();
let currentColorScheme = colorScheme; let currentColorScheme = colorScheme;
let colorNamespace = ''; let colorNamespace = '';
let currentJsonMetadata = jsonMetadata;
// color scheme in json metadata has precedence over selection // color scheme in json metadata has precedence over selection
if (jsonMetadata?.length) { if (currentJsonMetadata?.length) {
const metadata = JSON.parse(jsonMetadata); const metadata = JSON.parse(currentJsonMetadata);
currentColorScheme = metadata?.color_scheme || colorScheme; currentColorScheme = metadata?.color_scheme || colorScheme;
colorNamespace = metadata?.color_namespace || ''; 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, { onColorSchemeChange(currentColorScheme, {
@ -304,7 +322,7 @@ const PropertiesModal = ({
id: dashboardId, id: dashboardId,
title, title,
slug, slug,
jsonMetadata, jsonMetadata: currentJsonMetadata,
owners, owners,
colorScheme: currentColorScheme, colorScheme: currentColorScheme,
colorNamespace, colorNamespace,
@ -323,7 +341,7 @@ const PropertiesModal = ({
body: JSON.stringify({ body: JSON.stringify({
dashboard_title: title, dashboard_title: title,
slug: slug || null, slug: slug || null,
json_metadata: jsonMetadata || null, json_metadata: currentJsonMetadata || null,
owners: (owners || []).map(o => o.id), owners: (owners || []).map(o => o.id),
certified_by: certifiedBy || null, certified_by: certifiedBy || null,
certification_details: certification_details:

View File

@ -56,6 +56,7 @@ const propTypes = {
chart: chartPropShape.isRequired, chart: chartPropShape.isRequired,
formData: PropTypes.object.isRequired, formData: PropTypes.object.isRequired,
labelColors: PropTypes.object, labelColors: PropTypes.object,
sharedLabelColors: PropTypes.object,
datasource: PropTypes.object, datasource: PropTypes.object,
slice: slicePropShape.isRequired, slice: slicePropShape.isRequired,
sliceName: PropTypes.string.isRequired, sliceName: PropTypes.string.isRequired,
@ -81,6 +82,7 @@ const propTypes = {
addDangerToast: PropTypes.func.isRequired, addDangerToast: PropTypes.func.isRequired,
ownState: PropTypes.object, ownState: PropTypes.object,
filterState: PropTypes.object, filterState: PropTypes.object,
postTransformProps: PropTypes.func,
}; };
const defaultProps = { const defaultProps = {
@ -319,6 +321,7 @@ export default class Chart extends React.Component {
filters, filters,
formData, formData,
labelColors, labelColors,
sharedLabelColors,
updateSliceName, updateSliceName,
sliceName, sliceName,
toggleExpandSlice, toggleExpandSlice,
@ -334,6 +337,7 @@ export default class Chart extends React.Component {
handleToggleFullSize, handleToggleFullSize,
isFullSize, isFullSize,
filterboxMigrationState, filterboxMigrationState,
postTransformProps,
} = this.props; } = this.props;
const { width } = this.state; const { width } = this.state;
@ -449,6 +453,7 @@ export default class Chart extends React.Component {
initialValues={initialValues} initialValues={initialValues}
formData={formData} formData={formData}
labelColors={labelColors} labelColors={labelColors}
sharedLabelColors={sharedLabelColors}
ownState={ownState} ownState={ownState}
filterState={filterState} filterState={filterState}
queriesResponse={chart.queriesResponse} queriesResponse={chart.queriesResponse}
@ -457,6 +462,7 @@ export default class Chart extends React.Component {
vizType={slice.viz_type} vizType={slice.viz_type}
isDeactivatedViz={isDeactivatedViz} isDeactivatedViz={isDeactivatedViz}
filterboxMigrationState={filterboxMigrationState} filterboxMigrationState={filterboxMigrationState}
postTransformProps={postTransformProps}
/> />
</div> </div>
</div> </div>

View File

@ -69,6 +69,7 @@ const propTypes = {
updateComponents: PropTypes.func.isRequired, updateComponents: PropTypes.func.isRequired,
handleComponentDrop: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired,
setFullSizeChartId: PropTypes.func.isRequired, setFullSizeChartId: PropTypes.func.isRequired,
postAddSliceFromDashboard: PropTypes.func,
}; };
const defaultProps = { const defaultProps = {
@ -197,6 +198,7 @@ class ChartHolder extends React.Component {
this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this); this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this);
this.handleToggleFullSize = this.handleToggleFullSize.bind(this); this.handleToggleFullSize = this.handleToggleFullSize.bind(this);
this.handlePostTransformProps = this.handlePostTransformProps.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -251,6 +253,11 @@ class ChartHolder extends React.Component {
setFullSizeChartId(isFullSize ? null : chartId); setFullSizeChartId(isFullSize ? null : chartId);
} }
handlePostTransformProps(props) {
this.props.postAddSliceFromDashboard();
return props;
}
render() { render() {
const { isFocused } = this.state; const { isFocused } = this.state;
const { const {
@ -364,6 +371,7 @@ class ChartHolder extends React.Component {
isComponentVisible={isComponentVisible} isComponentVisible={isComponentVisible}
handleToggleFullSize={this.handleToggleFullSize} handleToggleFullSize={this.handleToggleFullSize}
isFullSize={isFullSize} isFullSize={isFullSize}
postTransformProps={this.handlePostTransformProps}
/> />
{editMode && ( {editMode && (
<HoverMenu position="top"> <HoverMenu position="top">

View File

@ -62,6 +62,7 @@ function mapStateToProps(
PLACEHOLDER_DATASOURCE; PLACEHOLDER_DATASOURCE;
const { colorScheme, colorNamespace } = dashboardState; const { colorScheme, colorNamespace } = dashboardState;
const labelColors = dashboardInfo?.metadata?.label_colors || {}; const labelColors = dashboardInfo?.metadata?.label_colors || {};
const sharedLabelColors = dashboardInfo?.metadata?.shared_label_colors || {};
// note: this method caches filters if possible to prevent render cascades // note: this method caches filters if possible to prevent render cascades
const formData = getFormDataWithExtraFilters({ const formData = getFormDataWithExtraFilters({
layout: dashboardLayout.present, layout: dashboardLayout.present,
@ -76,6 +77,7 @@ function mapStateToProps(
nativeFilters, nativeFilters,
dataMask, dataMask,
labelColors, labelColors,
sharedLabelColors,
}); });
formData.dashboardId = dashboardInfo.id; formData.dashboardId = dashboardInfo.id;
@ -84,6 +86,7 @@ function mapStateToProps(
chart, chart,
datasource, datasource,
labelColors, labelColors,
sharedLabelColors,
slice: sliceEntities.slices[id], slice: sliceEntities.slices[id],
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
filters: getActiveFilters() || EMPTY_OBJECT, filters: getActiveFilters() || EMPTY_OBJECT,

View File

@ -37,6 +37,7 @@ import {
setDirectPathToChild, setDirectPathToChild,
setActiveTabs, setActiveTabs,
setFullSizeChartId, setFullSizeChartId,
postAddSliceFromDashboard,
} from 'src/dashboard/actions/dashboardState'; } from 'src/dashboard/actions/dashboardState';
const propTypes = { const propTypes = {
@ -111,6 +112,7 @@ function mapDispatchToProps(dispatch) {
setFullSizeChartId, setFullSizeChartId,
setActiveTabs, setActiveTabs,
logEvent, logEvent,
postAddSliceFromDashboard,
}, },
dispatch, dispatch,
); );

View File

@ -17,7 +17,14 @@
* under the License. * under the License.
*/ */
import React, { FC, useRef, useEffect, useState } from 'react'; 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 { useDispatch, useSelector } from 'react-redux';
import { Global } from '@emotion/react'; import { Global } from '@emotion/react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -222,6 +229,18 @@ const DashboardPage: FC = () => {
return () => {}; return () => {};
}, [css]); }, [css]);
useEffect(
() => () => {
// clean up label color
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
metadata?.color_namespace,
);
categoricalNamespace.resetColors();
getSharedLabelColor().clear();
},
[metadata?.color_namespace],
);
useEffect(() => { useEffect(() => {
if (datasetsApiError) { if (datasetsApiError) {
addDangerToast( addDangerToast(

View File

@ -39,6 +39,7 @@ import {
UNSET_FOCUSED_FILTER_FIELD, UNSET_FOCUSED_FILTER_FIELD,
SET_ACTIVE_TABS, SET_ACTIVE_TABS,
SET_FULL_SIZE_CHART_ID, SET_FULL_SIZE_CHART_ID,
RESET_SLICE,
ON_FILTERS_REFRESH, ON_FILTERS_REFRESH,
ON_FILTERS_REFRESH_SUCCESS, ON_FILTERS_REFRESH_SUCCESS,
} from '../actions/dashboardState'; } from '../actions/dashboardState';
@ -58,6 +59,7 @@ export default function dashboardStateReducer(state = {}, action) {
return { return {
...state, ...state,
sliceIds: Array.from(updatedSliceIds), sliceIds: Array.from(updatedSliceIds),
updateSlice: true,
}; };
}, },
[REMOVE_SLICE]() { [REMOVE_SLICE]() {
@ -70,6 +72,12 @@ export default function dashboardStateReducer(state = {}, action) {
sliceIds: Array.from(updatedSliceIds), sliceIds: Array.from(updatedSliceIds),
}; };
}, },
[RESET_SLICE]() {
return {
...state,
updateSlice: false,
};
},
[TOGGLE_FAVE_STAR]() { [TOGGLE_FAVE_STAR]() {
return { ...state, isStarred: action.isStarred }; return { ...state, isStarred: action.isStarred };
}, },
@ -116,6 +124,7 @@ export default function dashboardStateReducer(state = {}, action) {
maxUndoHistoryExceeded: false, maxUndoHistoryExceeded: false,
editMode: false, editMode: false,
updatedColorScheme: false, updatedColorScheme: false,
updateSlice: false,
// server-side returns last_modified_time for latest change // server-side returns last_modified_time for latest change
lastModifiedTime: action.lastModifiedTime, lastModifiedTime: action.lastModifiedTime,
}; };

View File

@ -28,6 +28,7 @@ import {
TOGGLE_EXPAND_SLICE, TOGGLE_EXPAND_SLICE,
TOGGLE_FAVE_STAR, TOGGLE_FAVE_STAR,
UNSET_FOCUSED_FILTER_FIELD, UNSET_FOCUSED_FILTER_FIELD,
RESET_SLICE,
} from 'src/dashboard/actions/dashboardState'; } from 'src/dashboard/actions/dashboardState';
import dashboardStateReducer from 'src/dashboard/reducers/dashboardState'; import dashboardStateReducer from 'src/dashboard/reducers/dashboardState';
@ -43,7 +44,7 @@ describe('dashboardState reducer', () => {
{ sliceIds: [1] }, { sliceIds: [1] },
{ type: ADD_SLICE, slice: { slice_id: 2 } }, { type: ADD_SLICE, slice: { slice_id: 2 } },
), ),
).toEqual({ sliceIds: [1, 2] }); ).toEqual({ sliceIds: [1, 2], updateSlice: true });
}); });
it('should remove a slice', () => { it('should remove a slice', () => {
@ -55,6 +56,12 @@ describe('dashboardState reducer', () => {
).toEqual({ sliceIds: [1], filters: {} }); ).toEqual({ sliceIds: [1], filters: {} });
}); });
it('should reset updateSlice', () => {
expect(
dashboardStateReducer({ updateSlice: true }, { type: RESET_SLICE }),
).toEqual({ updateSlice: false });
});
it('should toggle fav star', () => { it('should toggle fav star', () => {
expect( expect(
dashboardStateReducer( dashboardStateReducer(

View File

@ -46,6 +46,7 @@ export interface GetFormDataWithExtraFiltersArguments {
dataMask: DataMaskStateWithId; dataMask: DataMaskStateWithId;
nativeFilters: NativeFiltersState; nativeFilters: NativeFiltersState;
labelColors?: Record<string, string>; labelColors?: Record<string, string>;
sharedLabelColors?: Record<string, string>;
} }
// this function merge chart's formData with dashboard filters value, // this function merge chart's formData with dashboard filters value,
@ -63,6 +64,7 @@ export default function getFormDataWithExtraFilters({
layout, layout,
dataMask, dataMask,
labelColors, labelColors,
sharedLabelColors,
}: GetFormDataWithExtraFiltersArguments) { }: GetFormDataWithExtraFiltersArguments) {
// if dashboard metadata + filters have not changed, use cache if possible // if dashboard metadata + filters have not changed, use cache if possible
const cachedFormData = cachedFormdataByChart[sliceId]; const cachedFormData = cachedFormdataByChart[sliceId];
@ -77,6 +79,9 @@ export default function getFormDataWithExtraFilters({
areObjectsEqual(cachedFormData?.label_colors, labelColors, { areObjectsEqual(cachedFormData?.label_colors, labelColors, {
ignoreUndefined: true, ignoreUndefined: true,
}) && }) &&
areObjectsEqual(cachedFormData?.shared_label_colors, sharedLabelColors, {
ignoreUndefined: true,
}) &&
!!cachedFormData && !!cachedFormData &&
areObjectsEqual(cachedFormData?.dataMask, dataMask, { areObjectsEqual(cachedFormData?.dataMask, dataMask, {
ignoreUndefined: true, ignoreUndefined: true,
@ -108,6 +113,7 @@ export default function getFormDataWithExtraFilters({
const formData = { const formData = {
...chart.formData, ...chart.formData,
label_colors: labelColors, label_colors: labelColors,
shared_label_colors: sharedLabelColors,
...(colorScheme && { color_scheme: colorScheme }), ...(colorScheme && { color_scheme: colorScheme }),
extra_filters: getEffectiveExtraFilters(filters), extra_filters: getEffectiveExtraFilters(filters),
...extraData, ...extraData,

View File

@ -156,13 +156,23 @@ export class ExploreChartHeader extends React.PureComponent {
if (dashboard && dashboard.json_metadata) { if (dashboard && dashboard.json_metadata) {
// setting the chart to use the dashboard custom label colors if any // setting the chart to use the dashboard custom label colors if any
const labelColors = const metadata = JSON.parse(dashboard.json_metadata);
JSON.parse(dashboard.json_metadata).label_colors || {}; const sharedLabelColors = metadata.shared_label_colors || {};
const customLabelColors = metadata.label_colors || {};
const mergedLabelColors = {
...sharedLabelColors,
...customLabelColors,
};
const categoricalNamespace = const categoricalNamespace =
CategoricalColorNamespace.getNamespace(); CategoricalColorNamespace.getNamespace();
Object.keys(labelColors).forEach(label => { Object.keys(mergedLabelColors).forEach(label => {
categoricalNamespace.setColor(label, labelColors[label]); categoricalNamespace.setColor(
label,
mergedLabelColors[label],
metadata.color_scheme,
);
}); });
} }
} }

View File

@ -265,6 +265,7 @@ class DashboardDAO(BaseDAO):
md["refresh_frequency"] = data.get("refresh_frequency", 0) md["refresh_frequency"] = data.get("refresh_frequency", 0)
md["color_scheme"] = data.get("color_scheme", "") md["color_scheme"] = data.get("color_scheme", "")
md["label_colors"] = data.get("label_colors", {}) md["label_colors"] = data.get("label_colors", {})
md["shared_label_colors"] = data.get("shared_label_colors", {})
dashboard.json_metadata = json.dumps(md) dashboard.json_metadata = json.dumps(md)

View File

@ -128,6 +128,7 @@ class DashboardJSONMetadataSchema(Schema):
color_namespace = fields.Str(allow_none=True) color_namespace = fields.Str(allow_none=True)
positions = fields.Dict(allow_none=True) positions = fields.Dict(allow_none=True)
label_colors = fields.Dict() label_colors = fields.Dict()
shared_label_colors = fields.Dict()
# used for v0 import/export # used for v0 import/export
import_time = fields.Integer() import_time = fields.Integer()
remote_id = fields.Integer() remote_id = fields.Integer()

View File

@ -72,7 +72,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
"slug": "slug1_changed", "slug": "slug1_changed",
"position_json": '{"b": "B"}', "position_json": '{"b": "B"}',
"css": "css_changed", "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, "published": False,
} }