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
docker/requirements-local.txt
cache/

View File

@ -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",

View File

@ -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",

View File

@ -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'> = {

View File

@ -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",

View File

@ -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;
}
/**

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 * from './colorSchemes';
export * from './utils';
export {
default as getSharedLabelColor,
SharedLabelColor,
} from './SharedLabelColorSingleton';
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) {
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.

View File

@ -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,
};
}

View File

@ -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';

View File

@ -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,
};
}

View File

@ -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 (

View File

@ -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,
};
}

View File

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

View File

@ -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,
};
}

View File

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

View File

@ -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,
};
}

View File

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

View File

@ -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,
};
}

View File

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

View File

@ -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,
};
}

View File

@ -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)

View File

@ -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) {

View File

@ -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')

View File

@ -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,
};
}

View File

@ -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 => {

View File

@ -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'),
},

View File

@ -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,
};
}

View File

@ -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 };
}

View File

@ -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) {

View File

@ -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,
};
}

View File

@ -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,

View File

@ -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,

View File

@ -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: {

View File

@ -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: {

View File

@ -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<GraphNodeItemOption, 'value'> & {
value: number;
tooltip?: Pick<SeriesTooltipOption, 'formatter'>;
};
// @ts-ignore
export const DEFAULT_FORM_DATA: EchartsGraphFormData = {
...DEFAULT_LEGEND_FORM_DATA,
source: '',

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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(

View File

@ -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,

View File

@ -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',
},
},
];

View File

@ -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})`}
>

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

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

View File

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

View File

@ -23,6 +23,21 @@ import { ChartConfiguration, DashboardInfo } from '../reducers/types';
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
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 };

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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,
},
};

View File

@ -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:

View File

@ -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}
/>
</div>
</div>

View File

@ -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 && (
<HoverMenu position="top">

View File

@ -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,

View File

@ -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,
);

View File

@ -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(

View File

@ -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,
};

View File

@ -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(

View File

@ -46,6 +46,7 @@ export interface GetFormDataWithExtraFiltersArguments {
dataMask: DataMaskStateWithId;
nativeFilters: NativeFiltersState;
labelColors?: Record<string, string>;
sharedLabelColors?: Record<string, string>;
}
// 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,

View File

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

View File

@ -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)

View File

@ -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()

View File

@ -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,
}