mirror of https://github.com/apache/superset.git
feat: improve color consistency (save all labels) (#19038)
This commit is contained in:
parent
e1d0b83885
commit
dc575080d7
|
@ -108,3 +108,5 @@ release.json
|
|||
messages.mo
|
||||
|
||||
docker/requirements-local.txt
|
||||
|
||||
cache/
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'> = {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 => ({
|
||||
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: colorScale(d.m1),
|
||||
}));
|
||||
fillColor: color,
|
||||
};
|
||||
});
|
||||
|
||||
const mapData = {};
|
||||
processedData.forEach(d => {
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,7 +28,8 @@ import {
|
|||
|
||||
export type EdgeSymbol = 'none' | 'circle' | 'arrow';
|
||||
|
||||
export type EchartsGraphFormData = EchartsLegendFormData & {
|
||||
export type EchartsGraphFormData = QueryFormData &
|
||||
EchartsLegendFormData & {
|
||||
source: string;
|
||||
target: string;
|
||||
sourceCategory?: string;
|
||||
|
@ -46,13 +48,14 @@ export type EchartsGraphFormData = EchartsLegendFormData & {
|
|||
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: '',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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})`}
|
||||
>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue