fix(Dashboard): Retain colors when color scheme not set (#30646)

This commit is contained in:
Geido 2024-11-21 19:58:32 +02:00 committed by GitHub
parent 3c326598a8
commit 90572be95a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 1979 additions and 437 deletions

View File

@ -16,10 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SAMPLE_DASHBOARD_1, TABBED_DASHBOARD } from 'cypress/utils/urls';
import {
SAMPLE_DASHBOARD_1,
SUPPORTED_CHARTS_DASHBOARD,
TABBED_DASHBOARD,
} from 'cypress/utils/urls';
import { drag, resize, waitForChartLoad } from 'cypress/utils';
import * as ace from 'brace';
import { interceptGet, interceptUpdate, openTab } from './utils';
import {
interceptExploreUpdate,
interceptGet,
interceptUpdate,
openTab,
} from './utils';
import {
interceptExploreJson,
interceptFiltering as interceptCharts,
@ -42,15 +51,37 @@ function openProperties() {
cy.getBySel('header-actions-menu')
.contains('Edit properties')
.click({ force: true });
cy.wait(500);
cy.get('.ant-modal-body').should('be.visible');
});
}
function openExploreProperties() {
cy.getBySel('actions-trigger').click({ force: true });
cy.get('.ant-dropdown-menu')
.contains('Edit chart properties')
.click({ force: true });
cy.get('.ant-modal-body').should('be.visible');
}
function assertMetadata(text: string) {
const regex = new RegExp(text);
cy.get('#json_metadata')
.should('be.visible')
.then(() => {
const metadata = cy.$$('#json_metadata')[0];
// cypress can read this locally, but not in ci
// so we have to use the ace module directly to fetch the value
expect(ace.edit(metadata).getValue()).to.match(regex);
});
}
function openAdvancedProperties() {
cy.get('.ant-modal-body')
.contains('Advanced')
.should('be.visible')
.click({ force: true });
cy.get('#json_metadata').should('be.visible');
}
function dragComponent(
@ -83,20 +114,36 @@ function visitEdit(sampleDashboard = SAMPLE_DASHBOARD_1) {
cy.visit(sampleDashboard);
cy.wait('@get');
editDashboard();
cy.get('.grid-container').should('exist');
cy.wait('@filtering');
cy.wait(500);
}
function resetTabbedDashboard(go = false) {
function visit(sampleDashboard = SAMPLE_DASHBOARD_1) {
interceptCharts();
interceptGet();
if (sampleDashboard === SAMPLE_DASHBOARD_1) {
cy.createSampleDashboards([0]);
}
cy.visit(sampleDashboard);
cy.wait('@get');
cy.get('.grid-container').should('exist');
cy.wait(500);
}
function resetDashboardColors(dashboard = 'tabbed_dash') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cy.getDashboard('tabbed_dash').then((r: Record<string, any>) => {
cy.getDashboard(dashboard).then((r: Record<string, any>) => {
const jsonMetadata = r?.json_metadata || '{}';
const metadata = JSON.parse(jsonMetadata);
const resetMetadata = JSON.stringify({
...metadata,
color_scheme: '',
label_colors: {},
shared_label_colors: {},
shared_label_colors: [],
map_label_colors: {},
});
cy.updateDashboard(r.id, {
certification_details: r.certification_details,
@ -106,25 +153,35 @@ function resetTabbedDashboard(go = false) {
json_metadata: resetMetadata,
owners: r.owners,
slug: r.slug,
}).then(() => {
if (go) {
visitEdit(TABBED_DASHBOARD);
}
});
});
}
function visitResetTabbedDashboard() {
resetTabbedDashboard(true);
function selectColorScheme(
color: string,
target = 'dashboard-edit-properties-form',
) {
cy.get(`[data-test="${target}"] input[aria-label="Select color scheme"]`)
.first()
.then($input => {
cy.wrap($input).click({ force: true });
cy.wrap($input).type(color.slice(0, 5), { force: true });
});
cy.getBySel(color).click({ force: true });
}
function selectColorScheme(color: string) {
cy.get(
'[data-test="dashboard-edit-properties-form"] [aria-label="Select color scheme"]',
)
.first()
.click();
cy.getBySel(color).click({ force: true });
function saveAndGo(dashboard = 'Tabbed Dashboard') {
interceptExploreUpdate();
cy.getBySel('query-save-button').click();
cy.getBySel('save-modal-body').then($modal => {
cy.wrap($modal)
.find("div[aria-label='Select a dashboard'] .ant-select-selection-item")
.should('have.text', dashboard);
cy.getBySel('save-overwrite-radio').should('not.be.disabled');
cy.getBySel('save-overwrite-radio').click();
cy.get('#btn_modal_save_goto_dash').click();
cy.wait('@chartUpdate');
});
}
function applyChanges() {
@ -137,37 +194,37 @@ function saveChanges() {
cy.wait('@update');
}
function assertMetadata(text: string) {
const regex = new RegExp(text);
cy.get('#json_metadata')
.should('be.visible')
.then(() => {
const metadata = cy.$$('#json_metadata')[0];
// cypress can read this locally, but not in ci
// so we have to use the ace module directly to fetch the value
expect(ace.edit(metadata).getValue()).to.match(regex);
});
}
function clearMetadata() {
cy.get('#json_metadata').then($jsonmetadata => {
cy.wrap($jsonmetadata).find('.ace_content').click();
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
cy.wrap($jsonmetadata)
.find('.ace_text-input')
.type('{selectall} {backspace}', { force: true });
.then($ace => {
cy.wrap($ace).focus();
cy.wrap($ace).should('have.focus');
cy.wrap($ace).type('{selectall}', { force: true });
cy.wrap($ace).type('{backspace}', { force: true });
});
});
}
function writeMetadata(metadata: string) {
cy.get('#json_metadata').then($jsonmetadata =>
cy
.wrap($jsonmetadata)
cy.get('#json_metadata').then($jsonmetadata => {
cy.wrap($jsonmetadata).find('.ace_content').click({ force: true });
cy.wrap($jsonmetadata)
.find('.ace_text-input')
.type(metadata, { parseSpecialCharSequences: false, force: true }),
);
.then($ace => {
cy.wrap($ace).focus();
cy.wrap($ace).should('have.focus');
cy.wrap($ace).type(metadata, {
parseSpecialCharSequences: false,
force: true,
});
});
});
}
function openExplore(chartName: string) {
function openExploreWithDashboardContext(chartName: string) {
interceptExploreJson();
interceptGet();
@ -181,23 +238,351 @@ function openExplore(chartName: string) {
.should('contain', 'Edit chart')
.click();
cy.wait('@getJson');
cy.get('.chart-container').should('exist');
}
function saveExploreColorScheme(
chart = 'Top 10 California Names Timeseries',
colorScheme = 'supersetColors',
) {
interceptExploreUpdate();
openExploreWithDashboardContext(chart);
openTab(0, 1, 'control-tabs');
selectColorScheme(colorScheme, 'control-item');
cy.getBySel('query-save-button').click();
cy.getBySel('save-overwrite-radio').click();
cy.getBySel('btn-modal-save').click();
cy.wait('@chartUpdate');
}
describe('Dashboard edit', () => {
describe('Color consistency', () => {
beforeEach(() => {
visitResetTabbedDashboard();
resetDashboardColors();
});
after(() => {
resetTabbedDashboard();
it('should not allow to change color scheme of a chart when dashboard has one', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
openTab(0, 1, 'control-tabs');
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
});
it('should not allow to change color scheme of a chart when dashboard has no scheme but chart has shared labels', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// open second top tab to catch shared labels
openTab(0, 1);
waitForChartLoad({
name: 'Trends',
viz: 'line',
});
openTab(0, 0);
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
openTab(0, 1, 'control-tabs');
cy.get('[aria-label="Select color scheme"]').should('be.disabled');
});
it('should allow to change color scheme of a chart when dashboard has no scheme but only custom label colors', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openTab(0, 1, 'control-tabs');
selectColorScheme('blueToGreen', 'control-item');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// label Daniel
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(0, 76, 218)');
// label David
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(3)
.should('have.css', 'fill', 'rgb(0, 116, 241)');
});
it('should allow to change color scheme of a chart when dashboard has no scheme and show the change', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
openTab(0, 1, 'control-tabs');
selectColorScheme('blueToGreen', 'control-item');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
saveAndGo();
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// reset original scheme
saveExploreColorScheme();
});
it('should allow to change color scheme of a chart when dashboard has no scheme but custom label colors and show the change', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openTab(0, 1, 'control-tabs');
selectColorScheme('blueToGreen', 'control-item');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
saveAndGo();
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// reset original scheme
saveExploreColorScheme();
});
it('should not change colors on refreshes with no color scheme set', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(69, 78, 124)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(69, 78, 124)');
});
it('should not change colors on refreshes with color scheme set', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
saveChanges();
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(0, 76, 218)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Andrew
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(0, 76, 218)');
});
it('should respect chart color scheme when none is set for the dashboard', () => {
openProperties();
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
applyChanges();
saveChanges();
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
@ -214,7 +599,8 @@ describe('Dashboard edit', () => {
.should('have.css', 'fill', 'rgb(31, 168, 201)');
});
it('should apply same color to same labels with color scheme set', () => {
it('should apply same color to same labels with color scheme set on refresh', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@ -238,17 +624,63 @@ describe('Dashboard edit', () => {
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
it('should apply same color to same labels with no color scheme set', () => {
openProperties();
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
applyChanges();
saveChanges();
it('should apply same color to same labels with no color scheme set on refresh', () => {
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
name: 'Top 10 California Names Timeseries',
viz: 'line',
});
// label Anthony
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
// open 2nd main tab
openTab(0, 1);
waitForChartLoad({ name: 'Trends', viz: 'line' });
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(31, 168, 201)');
visit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
@ -275,6 +707,7 @@ describe('Dashboard edit', () => {
});
it('custom label colors should take the precedence in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@ -305,6 +738,7 @@ describe('Dashboard edit', () => {
});
it('label colors should take the precedence for rendered charts in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
// open the tab first time and let chart load
openTab(1, 1);
waitForChartLoad({
@ -333,6 +767,7 @@ describe('Dashboard edit', () => {
});
it('should re-apply original color after removing custom label color with color scheme set', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@ -375,6 +810,7 @@ describe('Dashboard edit', () => {
});
it('should re-apply original color after removing custom label color with no color scheme set', () => {
visitEdit(TABBED_DASHBOARD);
// open nested tab
openTab(1, 1);
waitForChartLoad({
@ -438,6 +874,7 @@ describe('Dashboard edit', () => {
});
it('should show the same colors in Explore', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
openAdvancedProperties();
clearMetadata();
@ -461,7 +898,7 @@ describe('Dashboard edit', () => {
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
openExplore('Top 10 California Names Timeseries');
openExploreWithDashboardContext('Top 10 California Names Timeseries');
// label Anthony
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
@ -469,7 +906,8 @@ describe('Dashboard edit', () => {
.should('have.css', 'fill', 'rgb(255, 0, 0)');
});
it.skip('should change color scheme multiple times', () => {
it('should change color scheme multiple times', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@ -487,7 +925,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
@ -496,7 +934,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
editDashboard();
openProperties();
@ -507,7 +945,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(41, 105, 107)');
.should('have.css', 'fill', 'rgb(0, 128, 246)');
// open main tab and nested tab
openTab(0, 0);
@ -518,10 +956,11 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(41, 105, 107)');
.should('have.css', 'fill', 'rgb(0, 128, 246)');
});
it.skip('should apply the color scheme across main tabs', () => {
it('should apply the color scheme across main tabs', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@ -533,10 +972,11 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
it.skip('should apply the color scheme across main tabs for rendered charts', () => {
it('should apply the color scheme across main tabs for rendered charts', () => {
visitEdit(TABBED_DASHBOARD);
waitForChartLoad({ name: 'Treemap', viz: 'treemap_v2' });
openProperties();
selectColorScheme('blueToGreen');
@ -549,7 +989,7 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(41, 105, 107)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// change scheme now that charts are rendered across the main tabs
editDashboard();
@ -560,10 +1000,11 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(0, 128, 246)');
});
it.skip('should apply the color scheme in nested tabs', () => {
it('should apply the color scheme in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
openProperties();
selectColorScheme('blueToGreen');
applyChanges();
@ -579,17 +1020,18 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open another nested tab
openTab(2, 1);
waitForChartLoad({ name: 'Growth Rate', viz: 'line' });
cy.get('[data-test-chart-name="Growth Rate"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
it.skip('should apply a valid color scheme for rendered charts in nested tabs', () => {
it('should apply a valid color scheme for rendered charts in nested tabs', () => {
visitEdit(TABBED_DASHBOARD);
// open the tab first time and let chart load
openTab(1, 1);
waitForChartLoad({
@ -611,7 +1053,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(234, 11, 140)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
});
@ -623,9 +1065,10 @@ describe('Dashboard edit', () => {
beforeEach(() => {
cy.createSampleDashboards([0]);
openProperties();
selectColorScheme('supersetColors');
});
it.skip('should accept a valid color scheme', () => {
it('should accept a valid color scheme', () => {
openAdvancedProperties();
clearMetadata();
writeMetadata('{"color_scheme":"lyftColors"}');
@ -636,21 +1079,21 @@ describe('Dashboard edit', () => {
applyChanges();
});
it.skip('should overwrite the color scheme when advanced is closed', () => {
it('should overwrite the color scheme when advanced is closed', () => {
selectColorScheme('blueToGreen');
openAdvancedProperties();
assertMetadata('blueToGreen');
applyChanges();
});
it.skip('should overwrite the color scheme when advanced is open', () => {
it('should overwrite the color scheme when advanced is open', () => {
openAdvancedProperties();
selectColorScheme('modernSunset');
assertMetadata('modernSunset');
applyChanges();
});
it.skip('should not accept an invalid color scheme', () => {
it('should not accept an invalid color scheme', () => {
openAdvancedProperties();
clearMetadata();
// allow console error
@ -716,7 +1159,7 @@ describe('Dashboard edit', () => {
visitEdit();
});
it.skip('should add charts', () => {
it('should add charts', () => {
cy.get('[role="checkbox"]').click();
dragComponent();
cy.getBySel('dashboard-component-chart-holder').should('have.length', 1);
@ -765,7 +1208,7 @@ describe('Dashboard edit', () => {
visitEdit();
});
it.skip('should save', () => {
it('should save', () => {
cy.get('[role="checkbox"]').click();
dragComponent();
cy.getBySel('header-save-button').should('be.enabled');

View File

@ -91,7 +91,7 @@ export function prepareDashboardFilters(
refresh_frequency: 0,
color_scheme: '',
label_colors: {},
shared_label_colors: {},
shared_label_colors: [],
color_scheme_domain: [],
cross_filters_enabled: false,
positions: {

View File

@ -125,7 +125,7 @@ export const valueNativeFilterOptions = [
];
export function interceptGet() {
cy.intercept('/api/v1/dashboard/*').as('get');
cy.intercept('GET', '/api/v1/dashboard/*').as('get');
}
export function interceptFiltering() {
@ -144,6 +144,10 @@ export function interceptUpdate() {
cy.intercept('PUT', `/api/v1/dashboard/*`).as('update');
}
export function interceptExploreUpdate() {
cy.intercept('PUT', `/api/v1/chart/*`).as('chartUpdate');
}
export function interceptPost() {
cy.intercept('POST', `/api/v1/dashboard/`).as('post');
}
@ -524,13 +528,17 @@ export function addCountryNameFilter() {
);
}
export function openTab(tabComponentIndex: number, tabIndex: number) {
return cy
.getBySel('dashboard-component-tabs')
export function openTab(
tabComponentIndex: number,
tabIndex: number,
target = 'dashboard-component-tabs',
) {
cy.getBySel(target)
.eq(tabComponentIndex)
.find('[role="tab"]')
.eq(tabIndex)
.click();
cy.wait(500);
}
export const openTopLevelTab = (tabName: string) => {

View File

@ -50,6 +50,7 @@ import {
import {
formatSelectOptions,
displayTimeRelatedControls,
getColorControlsProps,
D3_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
@ -142,9 +143,7 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
renderTrigger: true,
schemes: () => sequentialSchemeRegistry.getMap(),
isLinear: true,
mapStateToProps: state => ({
dashboardId: state?.form_data?.dashboardId,
}),
mapStateToProps: state => getColorControlsProps(state),
};
const granularity: SharedControlConfig<'SelectControl'> = {
@ -333,9 +332,7 @@ const color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]),
description: t('The color scheme for rendering chart'),
schemes: () => categoricalSchemeRegistry.getMap(),
mapStateToProps: state => ({
dashboardId: state?.form_data?.dashboardId,
}),
mapStateToProps: state => getColorControlsProps(state),
};
const time_shift_color: SharedControlConfig<'CheckboxControl'> = {

View File

@ -87,6 +87,9 @@ export interface Dataset {
}
export interface ControlPanelState {
slice: {
slice_id: number;
};
form_data: QueryFormData;
datasource: Dataset | QueryResponse | null;
controls: ControlStateMapping;

View File

@ -0,0 +1,32 @@
/**
* 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.
*/
export const getColorControlsProps = (state: Record<string, any>) => {
const dashboardId = state?.form_data?.dashboardId;
return {
chartId: state?.slice?.slice_id,
dashboardId,
hasDashboardColorScheme:
!!dashboardId && !!state?.form_data?.dashboard_color_scheme,
hasCustomLabelsColor:
Object.keys(state?.form_data?.label_colors || {}).length > 0,
colorNamespace: state?.form_data?.color_namespace,
mapLabelsColors: state?.form_data?.map_label_colors || {},
sharedLabelsColors: state?.form_data?.shared_label_colors || [],
};
};

View File

@ -27,3 +27,4 @@ export * from './defineSavedMetrics';
export * from './getStandardizedControls';
export * from './getTemporalColumns';
export { default as displayTimeRelatedControls } from './displayTimeRelatedControls';
export * from './colorControls';

View File

@ -0,0 +1,112 @@
/**
* 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 { getColorControlsProps } from '../../src';
describe('getColorControlsProps', () => {
it('should return default values when state is empty', () => {
const state = {};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: undefined,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should return correct values when state has form_data with dashboardId and color scheme', () => {
const state = {
form_data: {
dashboardId: 123,
dashboard_color_scheme: 'blueScheme',
label_colors: {},
},
slice: { slice_id: 456 },
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: 456,
dashboardId: 123,
hasDashboardColorScheme: true,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should detect custom label colors correctly', () => {
const state = {
form_data: {
dashboardId: 123,
label_colors: { label1: '#000000' },
},
slice: { slice_id: 456 },
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: 456,
dashboardId: 123,
hasDashboardColorScheme: false,
hasCustomLabelsColor: true,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
it('should return shared label colors when available', () => {
const state = {
form_data: {
shared_label_colors: ['#FF5733', '#33FF57'],
},
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: undefined,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
sharedLabelsColors: ['#FF5733', '#33FF57'],
colorNamespace: undefined,
mapLabelsColors: {},
});
});
it('should handle missing form_data and slice properties', () => {
const state = {
form_data: {
dashboardId: 789,
},
};
const result = getColorControlsProps(state);
expect(result).toEqual({
chartId: undefined,
dashboardId: 789,
hasDashboardColorScheme: false,
hasCustomLabelsColor: false,
colorNamespace: undefined,
mapLabelsColors: {},
sharedLabelsColors: [],
});
});
});

View File

@ -17,6 +17,7 @@
* under the License.
*/
import { cloneDeep } from 'lodash';
import CategoricalColorScale from './CategoricalColorScale';
import { ColorsLookup } from './types';
import getCategoricalSchemeRegistry from './CategoricalSchemeRegistrySingleton';
@ -37,10 +38,21 @@ export default class CategoricalColorNamespace {
this.forcedItems = {};
}
getScale(schemeId?: string) {
const id = schemeId ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
/**
* A new CategoricalColorScale instance is created for each chart.
*
* @param colorScheme - the color scheme to use
* @returns a new instance of a color scale
*/
getScale(colorScheme?: string) {
const id =
colorScheme ?? getCategoricalSchemeRegistry().getDefaultKey() ?? '';
const scheme = getCategoricalSchemeRegistry().get(id);
return new CategoricalColorScale(scheme?.colors ?? [], this.forcedItems);
return new CategoricalColorScale(
scheme?.colors ?? [],
this.forcedItems,
colorScheme,
);
}
/**
@ -59,6 +71,17 @@ export default class CategoricalColorNamespace {
resetColors() {
this.forcedItems = {};
}
resetColorsForLabels(labels: string[] = []) {
const updatedForcedItems = cloneDeep(this.forcedItems);
labels.forEach(label => {
if (updatedForcedItems.hasOwnProperty(label)) {
delete updatedForcedItems[label];
}
});
this.forcedItems = { ...updatedForcedItems };
}
}
const namespaces: {
@ -80,16 +103,19 @@ export function getNamespace(name: string = DEFAULT_NAMESPACE) {
export function getColor(
value?: string,
schemeId?: string,
colorScheme?: string,
namespace?: string,
) {
return getNamespace(namespace).getScale(schemeId).getColor(value);
return getNamespace(namespace).getScale(colorScheme).getColor(value);
}
/*
Returns a new scale instance within the same namespace.
Especially useful when a chart is booting for the first time
@param scheme - the applied color scheme
@param namespace - the namespace
*/
export function getScale(scheme?: string, namespace?: string) {
return getNamespace(namespace).getScale(scheme);
export function getScale(colorScheme?: string, namespace?: string) {
return getNamespace(namespace).getScale(colorScheme);
}

View File

@ -21,14 +21,16 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
import { ExtensibleFunction } from '../models';
import { ColorsInitLookup, ColorsLookup } from './types';
import stringifyAndTrim from './stringifyAndTrim';
import getLabelsColorMap from './LabelsColorMapSingleton';
import getLabelsColorMap, {
LabelsColorMapSource,
} from './LabelsColorMapSingleton';
import { getAnalogousColors } from './utils';
import { FeatureFlag, isFeatureEnabled } from '../utils';
// Use type augmentation to correct the fact that
// an instance of CategoricalScale is also a function
interface CategoricalColorScale {
(x: { toString(): string }, y?: number, w?: string): string;
(x: { toString(): string }, y?: number): string;
}
class CategoricalColorScale extends ExtensibleFunction {
@ -50,11 +52,16 @@ class CategoricalColorScale extends ExtensibleFunction {
* Constructor
* @param {*} colors an array of colors
* @param {*} forcedColors optional parameter that comes from parent
* (usually CategoricalColorNamespace)
* @param {*} appliedColorScheme the color scheme applied to the chart
*
*/
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
super((value: string, sliceId?: number, colorScheme?: string) =>
this.getColor(value, sliceId, colorScheme),
constructor(
colors: string[],
forcedColors: ColorsInitLookup = {},
appliedColorScheme?: string,
) {
super((value: string, sliceId?: number) =>
this.getColor(value, sliceId, appliedColorScheme),
);
// holds original color scheme colors
this.originColors = colors;
@ -107,15 +114,28 @@ class CategoricalColorScale extends ExtensibleFunction {
*
* @param value the value of a label to get the color for
* @param sliceId the ID of the current chart
* @param colorScheme the original color scheme of the chart
* @param appliedColorScheme the color scheme applied to the chart
* @returns the color or the next available color
*/
getColor(value?: string, sliceId?: number, colorScheme?: string): string {
getColor(
value?: string,
sliceId?: number,
appliedColorScheme?: string,
): string {
const cleanedValue = stringifyAndTrim(value);
// priority: forced color (i.e. custom label colors) > shared color > scale color
// priority: forced color (aka custom label colors) > shared color > scale color
const forcedColor = this.forcedColors?.[cleanedValue];
const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
let color = forcedColor || this.scale(cleanedValue);
const { source } = this.labelsColorMapInstance;
const currentColorMap =
source === LabelsColorMapSource.Dashboard
? this.labelsColorMapInstance.getColorMap()
: this.chartLabelsColorMap;
const isExistingLabel = currentColorMap.has(cleanedValue);
let color =
forcedColor ||
(isExistingLabel
? (currentColorMap.get(cleanedValue) as string)
: this.scale(cleanedValue));
// a forced color will always be used independently of the usage count
if (!forcedColor && !isExistingLabel) {
@ -128,7 +148,7 @@ class CategoricalColorScale extends ExtensibleFunction {
this.isColorUsed(color)
) {
// fallback to least used color
color = this.getNextAvailableColor(color);
color = this.getNextAvailableColor(cleanedValue, color);
}
}
@ -141,7 +161,7 @@ class CategoricalColorScale extends ExtensibleFunction {
cleanedValue,
color,
sliceId,
colorScheme,
appliedColorScheme,
);
}
return color;
@ -164,48 +184,76 @@ class CategoricalColorScale extends ExtensibleFunction {
* @param color the color to check
* @returns the count of the color usage in this slice
*/
getColorUsageCount(currentColor: string): number {
let count = 0;
this.chartLabelsColorMap.forEach(color => {
if (color === currentColor) {
count += 1;
}
});
return count;
getColorUsageCount(color: string): number {
return Array.from(this.chartLabelsColorMap.values()).filter(
value => value === color,
).length;
}
/**
* Lower chances of color collision by returning the least used color
* Checks across colors of current slice within LabelsColorMapSingleton
* Lower chances of color collision by returning the least used color.
* Checks across colors of current slice within chartLabelsColorMap.
*
* @param currentLabel the current label
* @param currentColor the current color
* @returns the least used color that is not the excluded color
* @returns the least used color that is not the current color
*/
getNextAvailableColor(currentColor: string) {
const colorUsageArray = this.colors.map(color => ({
color,
count: this.getColorUsageCount(color),
}));
const currentColorCount = this.getColorUsageCount(currentColor);
const otherColors = colorUsageArray.filter(
colorEntry => colorEntry.color !== currentColor,
);
// all other colors are used as much or more than currentColor
const hasNoneAvailable = otherColors.every(
colorEntry => colorEntry.count >= currentColorCount,
getNextAvailableColor(currentLabel: string, currentColor: string): string {
// Precompute color usage counts for all colors
const colorUsageCounts = new Map(
this.colors.map(color => [color, this.getColorUsageCount(color)]),
);
// fallback to currentColor color
if (!otherColors.length || hasNoneAvailable) {
return currentColor;
// Get an ordered array of labels from the map
const orderedLabels = Array.from(this.chartLabelsColorMap.keys());
const currentLabelIndex = orderedLabels.indexOf(currentLabel);
// Helper to infer "previous" and "next" labels based on index
const getAdjacentLabelsColors = (): string[] => {
const previousLabel =
currentLabelIndex > 0 ? orderedLabels[currentLabelIndex - 1] : null;
const nextLabel =
currentLabelIndex < orderedLabels.length - 1
? orderedLabels[currentLabelIndex + 1]
: null;
const previousColor = previousLabel
? this.chartLabelsColorMap.get(previousLabel)
: null;
const nextColor = nextLabel
? this.chartLabelsColorMap.get(nextLabel)
: null;
return [previousColor, nextColor].filter(color => color) as string[];
};
const adjacentColors = getAdjacentLabelsColors();
// Determine adjusted score (usage count + penalties)
const calculateScore = (color: string): number => {
/* istanbul ignore next */
const usageCount = colorUsageCounts.get(color) || 0;
const adjacencyPenalty = adjacentColors.includes(color) ? 100 : 0;
return usageCount + adjacencyPenalty;
};
// If there is any color that has never been used, prioritize it
const unusedColor = this.colors.find(
color => (colorUsageCounts.get(color) || 0) === 0,
);
if (unusedColor) {
return unusedColor;
}
// Finding the least used color
const leastUsedColor = otherColors.reduce((min, entry) =>
entry.count < min.count ? entry : min,
).color;
// If all colors are used, calculate scores and choose the best one
const otherColors = this.colors.filter(color => color !== currentColor);
return leastUsedColor;
// Find the color with the minimum score, defaulting to currentColor
return otherColors.reduce((bestColor, color) => {
const bestScore = calculateScore(bestColor);
const currentScore = calculateScore(color);
return currentScore < bestScore ? color : bestColor;
}, currentColor);
}
/**

View File

@ -18,6 +18,7 @@
*/
import { makeSingleton } from '../utils';
import CategoricalColorNamespace from './CategoricalColorNamespace';
export enum LabelsColorMapSource {
Dashboard,
@ -25,7 +26,10 @@ export enum LabelsColorMapSource {
}
export class LabelsColorMap {
chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
chartsLabelsMap: Map<
number,
{ labels: string[]; scheme?: string; ownScheme?: string }
>;
colorMap: Map<string, string>;
@ -38,17 +42,38 @@ export class LabelsColorMap {
this.source = LabelsColorMapSource.Dashboard;
}
updateColorMap(categoricalNamespace: any, colorScheme?: string) {
const newColorMap = new Map();
this.colorMap.clear();
/**
* Wipes out the color map and updates it with the new color scheme.
*
* @param categoricalNamespace - the namespace to use for color mapping
* @param colorScheme - color scheme
*/
updateColorMap(
categoricalNamespace: CategoricalColorNamespace,
colorScheme?: string,
merge = false,
) {
const newColorMap = this.colorMap;
if (!merge) {
newColorMap.clear();
}
this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
const { labels, scheme: originalChartColorScheme } = chartConfig;
const currentColorScheme = colorScheme || originalChartColorScheme;
const colorScale = categoricalNamespace.getScale(currentColorScheme);
const { labels, ownScheme } = chartConfig;
const appliedColorScheme = colorScheme || ownScheme;
const colorScale = categoricalNamespace.getScale(appliedColorScheme);
labels.forEach(label => {
const newColor = colorScale.getColor(label, sliceId);
newColorMap.set(label, newColor);
// if merge, apply the scheme only to new labels in the map
if (!merge || !this.colorMap.has(label)) {
const newColor = colorScale.getColor(
label,
sliceId,
appliedColorScheme,
);
newColorMap.set(label, newColor);
}
});
});
this.colorMap = newColorMap;
@ -58,29 +83,63 @@ export class LabelsColorMap {
return this.colorMap;
}
/**
*
* Called individually by each plugin via getColor fn.
*
* @param label - the label name
* @param color - the color
* @param sliceId - the chart id
* @param colorScheme - the color scheme
*
*/
addSlice(
label: string,
color: string,
sliceId: number,
colorScheme?: string,
) {
if (this.source !== LabelsColorMapSource.Dashboard) return;
const chartConfig = this.chartsLabelsMap.get(sliceId) || {
labels: [],
scheme: '',
scheme: undefined,
ownScheme: undefined,
};
const { labels } = chartConfig;
if (!labels.includes(label)) {
labels.push(label);
this.chartsLabelsMap.set(sliceId, {
labels,
scheme: colorScheme,
ownScheme: chartConfig.ownScheme,
});
}
this.colorMap.set(label, color);
if (this.source === LabelsColorMapSource.Dashboard) {
this.colorMap.set(label, color);
}
}
/**
* Used to make sure all slices respect their original scheme.
*
* @param sliceId - the chart id
* @param ownScheme - the color scheme
*/
setOwnColorScheme(sliceId: number, ownScheme: string) {
const chartConfig = this.chartsLabelsMap.get(sliceId);
if (chartConfig) {
this.chartsLabelsMap.set(sliceId, {
...chartConfig,
ownScheme,
});
}
}
/**
* Remove a slice from the color map.
*
* @param sliceId - the chart
*/
removeSlice(sliceId: number) {
if (this.source !== LabelsColorMapSource.Dashboard) return;
@ -96,10 +155,20 @@ export class LabelsColorMap {
this.colorMap = newColorMap;
}
/**
* Clear the shared labels color map.
*/
clear() {
this.chartsLabelsMap.clear();
this.colorMap.clear();
}
/**
* Clears all maps
*/
reset() {
this.clear();
this.chartsLabelsMap.clear();
}
}
const getInstance = makeSingleton(LabelsColorMap);

View File

@ -161,4 +161,63 @@ describe('CategoricalColorNamespace', () => {
expect(color).toBe(color2);
});
});
describe('statis resetColorsForLabels(labels)', () => {
it('removes specified labels from forcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.setColor('label2', 'blue');
namespace.resetColorsForLabels(['label1']);
expect(namespace.forcedItems).toMatchObject({ label2: 'blue' });
});
it('does not modify forcedItems if no labels are provided', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.setColor('label2', 'blue');
namespace.resetColorsForLabels();
expect(namespace.forcedItems).toMatchObject({
label1: 'red',
label2: 'blue',
});
});
it('does nothing if the label is not in forcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
expect(namespace.forcedItems).toMatchObject({ label1: 'red' });
});
it('removes all labels when all are provided', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.setColor('label2', 'blue');
namespace.resetColorsForLabels(['label1', 'label2']);
expect(namespace.forcedItems).toMatchObject({});
});
it('creates a deep copy of forcedItems before modifying', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
const originalForcedItems = namespace.forcedItems;
namespace.resetColorsForLabels(['label1']);
expect(originalForcedItems).not.toBe(namespace.forcedItems);
});
it('removes the label if it exists in updatedForcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.resetColorsForLabels(['label1']);
expect(namespace.forcedItems).toEqual({});
});
it('does nothing for a label not in updatedForcedItems', () => {
const namespace = getNamespace('test-reset-individual');
namespace.setColor('label1', 'red');
namespace.resetColorsForLabels(['label2']); // label2 doesn't exist
expect(namespace.forcedItems).toEqual({ label1: 'red' });
});
});
});

View File

@ -18,7 +18,11 @@
*/
import { ScaleOrdinal } from 'd3-scale';
import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
import {
CategoricalColorScale,
FeatureFlag,
LabelsColorMapSource,
} from '@superset-ui/core';
describe('CategoricalColorScale', () => {
beforeEach(() => {
@ -43,7 +47,6 @@ describe('CategoricalColorScale', () => {
expect(scale).toBeInstanceOf(CategoricalColorScale);
expect(scale.forcedColors).toBe(forcedColors);
});
it('can refer to colors based on their index', () => {
const forcedColors = { pig: 1, horse: 5 };
const scale = new CategoricalColorScale(
@ -67,7 +70,7 @@ describe('CategoricalColorScale', () => {
>;
let getNextAvailableColorSpy: jest.SpyInstance<
string,
[currentColor: string]
[currentLabel: string, currentColor: string]
>;
beforeEach(() => {
@ -83,6 +86,36 @@ describe('CategoricalColorScale', () => {
jest.restoreAllMocks();
});
it('uses labelsColorMapInstance color map when source is Dashboard, otherwise uses chartLabelsColorMap', () => {
const sliceId = 123;
const colorScheme = 'preset';
// Mock chartLabelsColorMap and labelsColorMapInstance's getColorMap
const chartColorMap = new Map([['testValueChart', 'chartColor']]);
const dashboardColorMap = new Map([['testValueDash', 'dashboardColor']]);
scale.chartLabelsColorMap = chartColorMap;
jest
.spyOn(scale.labelsColorMapInstance, 'getColorMap')
.mockReturnValue(dashboardColorMap);
// Test when source is Dashboard
scale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
const colorFromDashboard = scale.getColor(
'testValueDash',
sliceId,
colorScheme,
);
expect(colorFromDashboard).toBe('dashboardColor');
// Test when source is not Dashboard
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
const colorFromChart = scale.getColor(
'testValueChart',
sliceId,
colorScheme,
);
expect(colorFromChart).toBe('chartColor');
});
it('returns same color for same value', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green'], {
pig: 'red',
@ -177,7 +210,10 @@ describe('CategoricalColorScale', () => {
scale.getColor('testValue3');
scale.getColor('testValue4');
expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
expect(getNextAvailableColorSpy).toHaveBeenCalledWith(
'testValue4',
'blue',
);
getNextAvailableColorSpy.mockClear();
@ -289,23 +325,25 @@ describe('CategoricalColorScale', () => {
});
});
describe('.getNextAvailableColor(currentColor)', () => {
describe('.getNextAvailableColor(currentLabel, currentColor)', () => {
it('returns the current color if it is the least used or equally used among colors', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat');
scale.getColor('dog');
// Since 'green' hasn't been used, it's considered the least used.
expect(scale.getNextAvailableColor('blue')).toBe('green');
expect(scale.getNextAvailableColor('fish', 'blue')).toBe('green');
});
it('handles cases where all colors are equally used and returns the current color', () => {
it('returns the least used color among all', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.getColor('fish'); // green
// All colors used once, so the function should return the current color
expect(scale.getNextAvailableColor('red')).toBe('red');
scale.getColor('puppy'); // blue
scale.getColor('teddy'); // red
// All colors used, so the function should return least used
expect(scale.getNextAvailableColor('darling', 'red')).toBe('green');
});
it('returns the least used color accurately even when some colors are used more frequently', () => {
@ -324,7 +362,57 @@ describe('CategoricalColorScale', () => {
scale.getColor('pony'); // green
// Yellow is the least used color, so it should be returned.
expect(scale.getNextAvailableColor('blue')).toBe('yellow');
expect(scale.getNextAvailableColor('pony', 'blue')).toBe('yellow');
});
it('does not return adjacent colors if a non-adjacent color is equally used', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.chartLabelsColorMap.set('label1', 'red'); // Adjacent
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
scale.chartLabelsColorMap.set('label3', 'green'); // Adjacent
// Green and blue are equally used, but green is adjacent and penalized.
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('blue');
});
it('prioritizes a color that has never been used, even if there are adjacent colors', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.chartLabelsColorMap.set('label1', 'red');
scale.chartLabelsColorMap.set('label2', 'blue'); // currentLabel
// Green has never been used, so it is prioritized.
expect(scale.getNextAvailableColor('label2', 'blue')).toBe('green');
});
it('returns the least used or unused color when there are no adjacent labels', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
// No adjacent labels are defined in chartLabelsColorMap.
expect(scale.getNextAvailableColor('label2', 'green')).toBe('green');
});
it('handles colors that have never been used (fallback to usage count 0)', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
// Do not use "green" at all
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
// "green" has never been used, so usageCount for "green" should fallback to 0
expect(scale.getNextAvailableColor('label2', 'red')).toBe('green');
});
it('handles a color with an explicit usage count of 0', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
// Mock or override getColorUsageCount to return 0 for "blue"
jest.spyOn(scale, 'getColorUsageCount').mockImplementation(color => {
if (color === 'blue') return 0; // Explicitly return 0 for "blue"
return 1; // Return 1 for other colors
});
// "blue" should still be a valid option with a usage count of 0
expect(scale.getNextAvailableColor('label1', 'red')).toBe('blue');
});
});

View File

@ -53,7 +53,7 @@ describe('LabelsColorMap', () => {
beforeEach(() => {
getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
getLabelsColorMap().clear();
getLabelsColorMap().reset();
});
it('has default value out-of-the-box', () => {
@ -92,11 +92,17 @@ describe('LabelsColorMap', () => {
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
});
it('should do nothing when source is not dashboard', () => {
it('should set a new color only when source is dashboard', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
labelsColorMap.addSlice('a', 'red', 1);
expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({});
labelsColorMap.source = LabelsColorMapSource.Dashboard;
labelsColorMap.addSlice('a', 'red', 1);
const colorMap2 = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap2)).toEqual({ a: 'red' });
});
});
@ -126,7 +132,7 @@ describe('LabelsColorMap', () => {
});
});
describe('.updateColorMap(namespace, scheme)', () => {
describe('.updateColorMap(namespace, scheme, merge)', () => {
let categoricalNamespace: any;
let mockedNamespace: any;
let labelsColorMap: any;
@ -141,18 +147,24 @@ describe('LabelsColorMap', () => {
};
});
it('should clear color map when not merge', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', false);
expect(labelsColorMap.colorMap).toEqual(new Map([['a', 'mockColor']]));
});
it('should not clear color map when merge', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2', true);
expect(labelsColorMap.colorMap).not.toEqual(new Map());
});
it('should use provided color scheme', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2');
expect(mockedNamespace.getScale).toHaveBeenCalledWith('testColors2');
});
it('should fallback to original chart color scheme if no color scheme is provided', () => {
labelsColorMap.addSlice('a', 'red', 1, 'originalScheme');
labelsColorMap.updateColorMap(mockedNamespace);
expect(mockedNamespace.getScale).toHaveBeenCalledWith('originalScheme');
});
it('should fallback to undefined if no color scheme is provided', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 2);
@ -181,6 +193,23 @@ describe('LabelsColorMap', () => {
});
});
it('should update only new labels in the color map when merge', () => {
labelsColorMap.colorMap = new Map();
labelsColorMap.addSlice('a', 'yellow', 1);
labelsColorMap.addSlice('b', 'green', 1);
labelsColorMap.addSlice('c', 'purple', 1);
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors2', true);
const mergedColorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(mergedColorMap)).toEqual({
a: 'yellow',
b: 'green',
c: 'purple',
});
});
it('should use recycle colors', () => {
window.featureFlags = {
[FeatureFlag.UseAnalagousColors]: false,
@ -231,4 +260,47 @@ describe('LabelsColorMap', () => {
expect(Object.fromEntries(colorMap)).toEqual({});
});
});
describe('setOwnColorScheme(sliceId, ownScheme)', () => {
it('should update the scheme in the config', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
const sliceId = 1;
const initialConfig = { labels: ['initial config'] };
labelsColorMap.chartsLabelsMap = new Map();
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
...initialConfig,
ownScheme: 'newScheme',
});
});
it('should update ownScheme when source is not Explore', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
const sliceId = 1;
const initialConfig = { labels: ['initial config'] };
labelsColorMap.chartsLabelsMap = new Map();
labelsColorMap.chartsLabelsMap.set(sliceId, initialConfig);
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual({
...initialConfig,
ownScheme: 'newScheme',
});
});
it('should do nothing when chart config does not exist', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
const sliceId = 1;
labelsColorMap.setOwnColorScheme(sliceId, 'newScheme');
expect(labelsColorMap.chartsLabelsMap.get(sliceId)).toEqual(undefined);
});
});
});

View File

@ -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], sliceId, colorScheme));
.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], sliceId, colorScheme))
.style('fill', d => colorFn(nodes[d.source.index], sliceId))
.attr('d', path);
// Add an elaborate mouseover title for each chord.

View File

@ -37,6 +37,7 @@ const propTypes = {
width: PropTypes.number,
height: PropTypes.number,
country: PropTypes.string,
colorScheme: PropTypes.string,
linearColorScheme: PropTypes.string,
mapBaseUrl: PropTypes.string,
numberFormat: PropTypes.string,

View File

@ -73,12 +73,11 @@ class CustomHistogram extends PureComponent {
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, sliceId, colorScheme)),
range: keys.map(x => colorFn(x, sliceId)),
});
return (

View File

@ -384,7 +384,7 @@ function Icicle(element, props) {
// Apply color scheme
g.selectAll('rect').style('fill', d => {
d.color = colorFn(d.name, sliceId, colorScheme);
d.color = colorFn(d.name, sliceId);
return d.color;
});

View File

@ -46,6 +46,7 @@ const propTypes = {
numberFormat: PropTypes.string,
useRichTooltip: PropTypes.bool,
useAreaProportions: PropTypes.bool,
colorScheme: PropTypes.string,
};
function copyArc(d) {
@ -120,14 +121,14 @@ function Rose(element, props) {
.map(v => ({
key: v.name,
value: v.value,
color: colorFn(v.name, sliceId, colorScheme),
color: colorFn(v.name, sliceId),
highlight: v.id === d.arcId,
}))
: [
{
key: d.name,
value: d.val,
color: colorFn(d.name, sliceId, colorScheme),
color: colorFn(d.name, sliceId),
},
];
@ -138,7 +139,7 @@ function Rose(element, props) {
};
}
legend.width(width).color(d => colorFn(d.key, sliceId, colorScheme));
legend.width(width).color(d => colorFn(d.key, sliceId));
legendWrap.datum(legendData(datum)).call(legend);
tooltip.headerFormatter(timeFormat).valueFormatter(format);
@ -385,7 +386,7 @@ function Rose(element, props) {
const arcs = ae
.append('path')
.attr('class', 'arc')
.attr('fill', d => colorFn(d.name, sliceId, colorScheme))
.attr('fill', d => colorFn(d.name, sliceId))
.attr('d', arc);
function mousemove() {

View File

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

View File

@ -67,7 +67,6 @@ function Sankey(element, props) {
.attr('height', innerHeight + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const sankey = d3Sankey()
@ -219,7 +218,7 @@ function Sankey(element, props) {
.attr('width', sankey.nodeWidth())
.style('fill', d => {
const name = d.name || 'N/A';
d.color = colorFn(name, sliceId, colorScheme);
d.color = colorFn(name, sliceId);
return d.color;
})

View File

@ -43,6 +43,7 @@ const propTypes = {
showBubbles: PropTypes.bool,
linearColorScheme: PropTypes.string,
color: PropTypes.string,
colorScheme: PropTypes.string,
setDataMask: PropTypes.func,
onContextMenu: PropTypes.func,
emitCrossFilters: PropTypes.bool,
@ -85,24 +86,24 @@ function WorldMap(element, props) {
.range([1, maxBubbleSize]);
let processedData;
let colorScale;
let colorFn;
if (colorBy === ColorBy.Country) {
colorScale = CategoricalColorNamespace.getScale(colorScheme);
colorFn = CategoricalColorNamespace.getScale(colorScheme);
processedData = filteredData.map(d => ({
...d,
radius: radiusScale(Math.sqrt(d.m2)),
fillColor: colorScale(d.name, sliceId),
fillColor: colorFn(d.name, sliceId),
}));
} else {
colorScale = getSequentialSchemeRegistry()
colorFn = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(filteredData, d => d.m1));
processedData = filteredData.map(d => ({
...d,
radius: radiusScale(Math.sqrt(d.m2)),
fillColor: colorScale(d.m1),
fillColor: colorFn(d.m1),
}));
}

View File

@ -52,16 +52,14 @@ const { getScale } = CategoricalColorNamespace;
function getCategories(fd: QueryFormData, data: JsonObject[]) {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const colorFn = getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
const categories = {};
data.forEach(d => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;
if (fd.dimension) {
color = hexToRGB(
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
c.a * 255,
);
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
} else {
color = fixedColor;
}
@ -132,15 +130,13 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
const addColor = useCallback((data: JsonObject[], fd: QueryFormData) => {
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const colorFn = getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
return data.map(d => {
let color;
if (fd.dimension) {
color = hexToRGB(
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
c.a * 255,
);
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
return { ...d, color };
}

View File

@ -55,7 +55,8 @@ export function getLayer(
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
) {
const fd = formData;
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
const colorRange = colorScale
.range()
.map(color => hexToRGB(color)) as Color[];

View File

@ -54,7 +54,8 @@ export function getLayer(
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
) {
const fd = formData;
const colorScale = CategoricalColorNamespace.getScale(fd.color_scheme);
const appliedScheme = fd.color_scheme;
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
const colorRange = colorScale
.range()
.map(color => hexToRGB(color)) as Color[];

View File

@ -658,9 +658,7 @@ function nvd3Vis(element, props) {
} else if (vizType !== 'bullet') {
const colorFn = getScale(colorScheme);
chart.color(
d =>
d.color ||
colorFn(cleanColorInput(d[colorKey]), sliceId, colorScheme),
d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
);
}

View File

@ -109,9 +109,9 @@ export default function transformProps(
datum[`${metric}__outliers`],
],
itemStyle: {
color: colorFn(groupbyLabel, sliceId, colorScheme),
color: colorFn(groupbyLabel, sliceId),
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
borderColor: colorFn(groupbyLabel, sliceId, colorScheme),
borderColor: colorFn(groupbyLabel, sliceId),
},
};
});
@ -150,7 +150,7 @@ export default function transformProps(
},
},
itemStyle: {
color: colorFn(groupbyLabel, sliceId, colorScheme),
color: colorFn(groupbyLabel, sliceId),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@ -108,8 +108,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
legendOrientation,
legendMargin,
legendType,
sliceId,
}: EchartsBubbleFormData = { ...DEFAULT_FORM_DATA, ...formData };
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const legends = new Set<string>();
@ -138,7 +138,10 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
],
],
type: 'scatter',
itemStyle: { color: colorFn(name), opacity },
itemStyle: {
color: colorFn(name, sliceId),
opacity,
},
});
legends.add(name);
});

View File

@ -145,7 +145,6 @@ export default function transformProps(
}, {});
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getValueFormatter(
metric,
@ -175,7 +174,7 @@ export default function transformProps(
value,
name,
itemStyle: {
color: colorFn(name, sliceId, colorScheme),
color: colorFn(name, sliceId),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@ -166,6 +166,7 @@ export default function transformProps(
const name = groupbyLabels
.map(column => `${verboseMap[column] || column}: ${data_point[column]}`)
.join(', ');
const colorLabel = groupbyLabels.map(col => data_point[col] as string);
columnsLabelMap.set(
name,
groupbyLabels.map(col => data_point[col] as string),
@ -174,7 +175,7 @@ export default function transformProps(
value: data_point[metricLabel] as number,
name,
itemStyle: {
color: colorFn(index, sliceId, colorScheme),
color: colorFn(colorLabel, sliceId),
},
title: {
offsetCenter: [
@ -202,7 +203,7 @@ export default function transformProps(
item = {
...item,
itemStyle: {
color: colorFn(index, sliceId, colorScheme),
color: colorFn(index, sliceId),
opacity: OpacityEnum.SemiTransparent,
},
detail: {

View File

@ -297,14 +297,15 @@ export default function transformProps(
});
const categoryList = [...categories];
const series: GraphSeriesOption[] = [
{
zoom: DEFAULT_GRAPH_SERIES_OPTION.zoom,
type: 'graph',
categories: categoryList.map(c => ({
name: c,
itemStyle: { color: colorFn(c, sliceId, colorScheme) },
itemStyle: {
color: colorFn(c, sliceId),
},
})),
layout,
force: {

View File

@ -191,7 +191,6 @@ export default function transformProps(
}, {});
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getValueFormatter(
metric,
@ -223,7 +222,7 @@ export default function transformProps(
value,
name,
itemStyle: {
color: colorFn(name, sliceId, colorScheme),
color: colorFn(name, sliceId),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@ -109,7 +109,6 @@ export default function transformProps(
...formData,
};
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
const formatter = (params: CallbackDataParams) =>
@ -182,7 +181,7 @@ export default function transformProps(
value: metricLabels.map(metricLabel => datum[metricLabel]),
name: joinedName,
itemStyle: {
color: colorFn(joinedName, sliceId, colorScheme),
color: colorFn(joinedName, sliceId),
opacity: isFiltered
? OpacityEnum.Transparent
: OpacityEnum.NonTransparent,

View File

@ -41,7 +41,7 @@ export default function transformProps(
const refs: Refs = {};
const { formData, height, hooks, queriesData, width } = chartProps;
const { onLegendStateChanged } = hooks;
const { colorScheme, metric, source, target } = formData;
const { colorScheme, metric, source, target, sliceId } = formData;
const { data } = queriesData[0];
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const metricLabel = getMetricLabel(metric);
@ -68,7 +68,7 @@ export default function transformProps(
).map(name => ({
name,
itemStyle: {
color: colorFn(name),
color: colorFn(name, sliceId),
},
}));

View File

@ -202,7 +202,6 @@ export default function transformProps(
}
return { ...acc, [entry[0]]: entry[1] };
}, {});
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
const rebasedData = rebaseForecastDatum(data, verboseMap);
let xAxisLabel = getXAxisLabel(chartProps.rawFormData) as string;

View File

@ -176,18 +176,18 @@ export default function transformProps(
let item: TreemapSeriesNodeItemOption = {
name,
value,
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(name, sliceId),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},
};
if (treeNode.children?.length) {
item = {
...item,
children: traverse(treeNode.children, newPath),
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(name, sliceId, colorScheme),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},
};
} else {
const joinedName = newPath.join(',');
@ -217,7 +217,7 @@ export default function transformProps(
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(`${metricLabel}`, sliceId, colorScheme),
color: colorFn(`${metricLabel}`, sliceId),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},

View File

@ -29,7 +29,7 @@ import {
SupersetThemeProps,
withTheme,
seed,
CategoricalColorScale,
CategoricalColorNamespace,
} from '@superset-ui/core';
import { isEqual } from 'lodash';
@ -230,7 +230,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
encoder.channels.color.setDomainFromDataset(words);
const { getValueFromDatum } = encoder.channels.color;
const colorFn = encoder.channels.color.scale as CategoricalColorScale;
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const viewBoxWidth = width * scaleFactor;
const viewBoxHeight = height * scaleFactor;
@ -250,11 +250,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
fontSize={`${w.size}px`}
fontWeight={w.weight}
fontFamily={w.font}
fill={colorFn(
getValueFromDatum(w) as string,
sliceId,
colorScheme,
)}
fill={colorFn(getValueFromDatum(w) as string, sliceId)}
textAnchor="middle"
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
>

View File

@ -26,10 +26,7 @@ export const getDashboardFormData = (overrides: JsonObject = {}) => ({
girl: '#FF69B4',
boy: '#ADD8E6',
},
shared_label_colors: {
boy: '#ADD8E6',
girl: '#FF69B4',
},
shared_label_colors: ['boy', 'girl'],
color_scheme: 'd3Category20b',
extra_filters: [
{

View File

@ -113,6 +113,6 @@ export const overwriteConfirmMetadata = {
slug: null,
owners: [],
json_metadata:
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":{"Male":"#5ac19e","Female":"#1f86c9","<NULL>":"#5AC189","Prefer not to say":"#47457c","No Answer":"#e05043","Yes, an ethnic minority":"#666666","No, not an ethnic minority":"#ffa444","age":"#1FA8C9"},"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":["Male", "Female","<NULL>","Prefer not to say","No Answer","Yes, an ethnic minority","No, not an ethnic minority","age"],"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
},
};

View File

@ -55,6 +55,7 @@ import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import { safeStringify } from 'src/utils/safeStringify';
import { logEvent } from 'src/logger/actions';
import { LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA } from 'src/logger/LogUtils';
import { isEqual } from 'lodash';
import { UPDATE_COMPONENTS_PARENTS_LIST } from './dashboardLayout';
import {
saveChartConfiguration,
@ -68,9 +69,10 @@ import getOverwriteItems from '../util/getOverwriteItems';
import {
applyColors,
isLabelsColorMapSynced,
getLabelsColorMapEntries,
getColorSchemeDomain,
getColorNamespace,
getLabelsColorMapEntries,
getFreshSharedLabels,
} from '../../utils/colorScheme';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
@ -224,6 +226,41 @@ export function saveDashboardFinished() {
return { type: SAVE_DASHBOARD_FINISHED };
}
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE =
'SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE';
export const SET_DASHBOARD_LABELS_COLORMAP_SYNCED =
'SET_DASHBOARD_LABELS_COLORMAP_SYNCED';
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE =
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE';
export const SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED =
'SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED';
export function setDashboardLabelsColorMapSync() {
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE };
}
export function setDashboardLabelsColorMapSynced() {
return { type: SET_DASHBOARD_LABELS_COLORMAP_SYNCED };
}
export function setDashboardSharedLabelsColorsSync() {
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE };
}
export function setDashboardSharedLabelsColorsSynced() {
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED };
}
export const setDashboardMetadata = updatedMetadata => async dispatch => {
dispatch(
dashboardInfoChanged({
metadata: {
...updatedMetadata,
},
}),
);
};
export function saveDashboardRequest(data, id, saveType) {
return (dispatch, getState) => {
dispatch({ type: UPDATE_COMPONENTS_PARENTS_LIST });
@ -254,6 +291,9 @@ export function saveDashboardRequest(data, id, saveType) {
const hasId = item => item.id !== undefined;
const metadataCrossFiltersEnabled = data.metadata?.cross_filters_enabled;
const colorScheme = data.metadata?.color_scheme;
const customLabelsColor = data.metadata?.label_colors || {};
const sharedLabelsColor = data.metadata?.shared_label_colors || [];
// making sure the data is what the backend expects
const cleanedData = {
...data,
@ -270,11 +310,14 @@ export function saveDashboardRequest(data, id, saveType) {
metadata: {
...data.metadata,
color_namespace: getColorNamespace(data.metadata?.color_namespace),
color_scheme: data.metadata?.color_scheme || '',
color_scheme_domain: data.metadata?.color_scheme_domain || [],
color_scheme: colorScheme || '',
color_scheme_domain: colorScheme
? getColorSchemeDomain(colorScheme)
: [],
expanded_slices: data.metadata?.expanded_slices || {},
label_colors: data.metadata?.label_colors || {},
shared_label_colors: data.metadata?.shared_label_colors || {},
label_colors: customLabelsColor,
shared_label_colors: getFreshSharedLabels(sharedLabelsColor),
map_label_colors: getLabelsColorMapEntries(customLabelsColor),
refresh_frequency: data.metadata?.refresh_frequency || 0,
timed_refresh_immune_slices:
data.metadata?.timed_refresh_immune_slices || [],
@ -324,11 +367,7 @@ export function saveDashboardRequest(data, id, saveType) {
// syncing with the backend transformations of the metadata
if (updatedDashboard.json_metadata) {
const metadata = JSON.parse(updatedDashboard.json_metadata);
dispatch(
dashboardInfoChanged({
metadata,
}),
);
dispatch(setDashboardMetadata(metadata));
if (metadata.chart_configuration) {
dispatch({
type: SAVE_CHART_CONFIG_COMPLETE,
@ -677,68 +716,261 @@ export function setDatasetsStatus(status) {
};
}
const updateDashboardMetadata = async (id, metadata, dispatch) => {
await SupersetClient.put({
const storeDashboardMetadata = async (id, metadata) =>
SupersetClient.put({
endpoint: `/api/v1/dashboard/${id}`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ json_metadata: JSON.stringify(metadata) }),
});
dispatch(dashboardInfoChanged({ metadata }));
};
export const updateDashboardLabelsColor = () => async (dispatch, getState) => {
/**
*
* Persists the label colors maps in the dashboard metadata.
* It runs when outdated color info are detected in stored metadata.
*
* @returns void
*/
export const persistDashboardLabelsColor = () => async (dispatch, getState) => {
const {
dashboardInfo: { id, metadata },
dashboardState: { labelsColorMapMustSync, sharedLabelsColorsMustSync },
} = getState();
const categoricalSchemes = getCategoricalSchemeRegistry();
const colorScheme = metadata?.color_scheme;
const colorSchemeRegistry = categoricalSchemes.get(
metadata?.color_scheme,
true,
);
const defaultScheme = categoricalSchemes.defaultKey;
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
const colorSchemeDomain = metadata?.color_scheme_domain || [];
if (labelsColorMapMustSync || sharedLabelsColorsMustSync) {
storeDashboardMetadata(id, metadata);
dispatch(setDashboardLabelsColorMapSynced());
dispatch(setDashboardSharedLabelsColorsSynced());
}
};
/**
* Checks the stored dashboard metadata for inconsistencies.
* Update the current metadata with validated color information.
* It runs only on Dashboard page load.
*
* @param {*} metadata - the stored dashboard metadata
* @returns void
*/
export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
try {
const updatedMetadata = { ...metadata };
let updatedScheme = metadata?.color_scheme;
const customLabelsColor = metadata.label_colors || {};
const sharedLabelsColor = metadata.shared_label_colors || [];
let hasChanged = false;
// backward compatibility of shared_label_colors
const sharedLabels = metadata.shared_label_colors || [];
if (!Array.isArray(sharedLabels) && Object.keys(sharedLabels).length > 0) {
hasChanged = true;
updatedMetadata.shared_label_colors = getFreshSharedLabels(
Object.keys(sharedLabelsColor),
);
}
// backward compatibility of map_label_colors
const hasMapLabelColors =
Object.keys(metadata.map_label_colors || {}).length > 0;
let updatedScheme = metadata.color_scheme;
const categoricalSchemes = getCategoricalSchemeRegistry();
const colorSchemeRegistry = categoricalSchemes.get(updatedScheme, true);
const hasInvalidColorScheme = !!updatedScheme && !colorSchemeRegistry;
// color scheme might not exist any longer
if (hasInvalidColorScheme) {
const defaultScheme = categoricalSchemes.defaultKey;
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
hasChanged = true;
// Color scheme does not exist anymore, fallback to default
if (colorScheme && !colorSchemeRegistry) {
updatedScheme = fallbackScheme;
updatedMetadata.color_scheme = updatedScheme;
updatedMetadata.color_scheme_domain = getColorSchemeDomain(colorScheme);
dispatch(setColorScheme(updatedScheme));
// must re-apply colors from fresh labels color map
applyColors(updatedMetadata, true);
}
// stored labels color map and applied might differ
const isMapSynced = isLabelsColorMapSynced(metadata);
if (!isMapSynced) {
// re-apply a fresh labels color map
applyColors(updatedMetadata, true);
// pull and store the just applied labels color map
updatedMetadata.shared_label_colors = getLabelsColorMapEntries();
}
// the stored color domain registry and fresh might differ at this point
const freshColorSchemeDomain = getColorSchemeDomain(colorScheme);
const isRegistrySynced =
colorSchemeDomain.toString() !== freshColorSchemeDomain.toString();
if (colorScheme && !isRegistrySynced) {
const freshColorSchemeDomain = updatedScheme
? getColorSchemeDomain(updatedScheme)
: [];
const currentColorSchemeDomain = metadata.color_scheme_domain || [];
if (!isEqual(freshColorSchemeDomain, currentColorSchemeDomain)) {
hasChanged = true;
updatedMetadata.color_scheme_domain = freshColorSchemeDomain;
}
if (
(colorScheme && (!colorSchemeRegistry || !isRegistrySynced)) ||
!isMapSynced
) {
await updateDashboardMetadata(id, updatedMetadata, dispatch);
// if color scheme is invalid or map is missing, apply a fresh color map
// if valid, apply the stored map to keep consistency across refreshes
const shouldGoFresh = !hasMapLabelColors || hasInvalidColorScheme;
applyColors(updatedMetadata, shouldGoFresh);
if (shouldGoFresh) {
// a fresh color map has been applied
// needs to be stored for consistency
hasChanged = true;
updatedMetadata.map_label_colors =
getLabelsColorMapEntries(customLabelsColor);
}
} catch (error) {
console.error('Failed to update dashboard color settings:', error);
if (hasChanged) {
dispatch(setDashboardMetadata(updatedMetadata));
dispatch(setDashboardLabelsColorMapSync());
}
} catch (e) {
console.error('Failed to update dashboard color on load:', e);
}
};
/**
*
* Ensure that the stored color map matches fresh map.
*
* @param {*} metadata - the dashboard metadata
* @returns void
*/
export const ensureSyncedLabelsColorMap = metadata => (dispatch, getState) => {
const {
dashboardState: { labelsColorMapMustSync },
} = getState();
const updatedMetadata = { ...metadata };
const customLabelsColor = metadata.label_colors || {};
const isMapSynced = isLabelsColorMapSynced(metadata);
const mustSync = !isMapSynced;
if (mustSync) {
const freshestColorMapEntries = getLabelsColorMapEntries(customLabelsColor);
updatedMetadata.map_label_colors = freshestColorMapEntries;
dispatch(setDashboardMetadata(updatedMetadata));
}
if (mustSync && !labelsColorMapMustSync) {
// prepare to persist the just applied labels color map
dispatch(setDashboardLabelsColorMapSync());
}
if (!mustSync && labelsColorMapMustSync) {
dispatch(setDashboardLabelsColorMapSynced());
}
};
/**
*
* Ensure that the stored shared labels colors match current.
*
* @param {*} metadata - the dashboard metadata
* @returns void
*/
export const ensureSyncedSharedLabelsColors =
metadata => (dispatch, getState) => {
// using a timeout to let the rendered charts finish processing labels
setTimeout(() => {
const {
dashboardState: { sharedLabelsColorsMustSync },
} = getState();
const updatedMetadata = { ...metadata };
const sharedLabelsColors = metadata.shared_label_colors || [];
const freshLabelsColors = getFreshSharedLabels(sharedLabelsColors);
const isSharedLabelsColorsSynced = isEqual(
sharedLabelsColors,
freshLabelsColors,
);
const mustSync = !isSharedLabelsColorsSynced;
if (mustSync) {
updatedMetadata.shared_label_colors = freshLabelsColors;
dispatch(setDashboardMetadata(updatedMetadata));
}
if (mustSync && !sharedLabelsColorsMustSync) {
// prepare to persist the shared labels colors
dispatch(setDashboardSharedLabelsColorsSync());
}
if (!mustSync && sharedLabelsColorsMustSync) {
dispatch(setDashboardSharedLabelsColorsSynced());
}
}, 500);
};
/**
*
* Updates the color map with new labels and colors as they appear.
*
* @param {*} renderedChartIds - the charts that have finished rendering
* @returns void
*/
export const updateDashboardLabelsColor =
renderedChartIds => (dispatch, getState) => {
try {
const {
dashboardInfo: { metadata },
charts,
} = getState();
const colorScheme = metadata.color_scheme;
const labelsColorMapInstance = getLabelsColorMap();
const fullLabelsColors = metadata.map_label_colors || {};
const sharedLabelsColors = metadata.shared_label_colors || [];
const customLabelsColors = metadata.label_colors || {};
const updatedMetadata = { ...metadata };
// for dashboards with no color scheme, the charts should always use their individual schemes
// this logic looks for unique labels (not shared across multiple charts) of each rendered chart
// it applies a new color to those unique labels when the applied scheme is not up to date
// while leaving shared label colors and custom label colors intact for color consistency
const shouldReset = [];
if (renderedChartIds.length > 0) {
const sharedLabelsSet = new Set(sharedLabelsColors);
renderedChartIds.forEach(id => {
const chart = charts[id];
const formData = chart.form_data || chart.latestQueryFormData;
// ensure charts have their original color scheme always available
labelsColorMapInstance.setOwnColorScheme(
formData.slice_id,
formData.color_scheme,
);
// if dashboard has a scheme, charts should ignore individual schemes
// thus following logic is inapplicable if a dashboard color scheme exists
if (colorScheme) return;
const chartColorScheme = formData.color_scheme;
const currentChartConfig = labelsColorMapInstance.chartsLabelsMap.get(
formData.slice_id,
);
const currentChartLabels = currentChartConfig?.labels || [];
const uniqueChartLabels = currentChartLabels.filter(
l =>
!sharedLabelsSet.has(l) && !customLabelsColors.hasOwnProperty(l),
);
// Map unique labels to colors
const uniqueChartLabelsColor = new Set(
uniqueChartLabels.map(l => fullLabelsColors[l]).filter(Boolean),
);
const expectedColorsForChartScheme = new Set(
getColorSchemeDomain(chartColorScheme),
);
// Check if any unique label color is not in the expected colors set
const shouldResetColors = [...uniqueChartLabelsColor].some(
color => !expectedColorsForChartScheme.has(color),
);
// Only push uniqueChartLabels if they require resetting
if (shouldResetColors) shouldReset.push(...uniqueChartLabels);
});
}
// an existing map is available, use mrge option
// to only apply colors to newly found labels
const shouldGoFresh = shouldReset.length > 0 ? shouldReset : false;
const shouldMerge = !shouldGoFresh;
// re-apply the color map first to get fresh maps accordingly
applyColors(updatedMetadata, shouldGoFresh, shouldMerge);
// new data may have appeared in the map (data changes)
// or new slices may have appeared while changing tabs
dispatch(ensureSyncedLabelsColorMap(updatedMetadata));
dispatch(ensureSyncedSharedLabelsColors(updatedMetadata));
} catch (e) {
console.error('Failed to update colors for new charts and labels:', e);
}
};

View File

@ -36,6 +36,8 @@ import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
fetchMock.put('glob:*/api/v1/dashboard/*', {});
jest.mock('src/dashboard/actions/dashboardState', () => ({
...jest.requireActual('src/dashboard/actions/dashboardState'),
fetchFaveStar: jest.fn(),

View File

@ -18,7 +18,7 @@
*/
// ParentSize uses resize observer so the dashboard will update size
// when its container size changes, due to e.g., builder side panel opening
import { FC, useEffect, useMemo, useRef } from 'react';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Filter,
@ -43,12 +43,12 @@ import {
import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
import { setInScopeStatusOfFilters } from 'src/dashboard/actions/nativeFilters';
import { updateDashboardLabelsColor } from 'src/dashboard/actions/dashboardState';
import {
applyColors,
getColorNamespace,
resetColors,
} from 'src/utils/colorScheme';
applyDashboardLabelsColorOnLoad,
updateDashboardLabelsColor,
persistDashboardLabelsColor,
} from 'src/dashboard/actions/dashboardState';
import { getColorNamespace, resetColors } from 'src/utils/colorScheme';
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
import { getRootLevelTabsComponent } from './utils';
@ -88,6 +88,14 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const chartIds = useSelector<RootState, number[]>(state =>
Object.values(state.charts).map(chart => chart.id),
);
const renderedChartIds = useSelector<RootState, number[]>(state =>
Object.values(state.charts)
.filter(chart => chart.chartStatus === 'rendered')
.map(chart => chart.id),
);
const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
useState(false);
const prevRenderedChartIds = useRef<number[]>([]);
const prevTabIndexRef = useRef();
const tabIndex = useMemo(() => {
@ -140,28 +148,65 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const activeKey = min === 0 ? DASHBOARD_GRID_ID : min.toString();
const TOP_OF_PAGE_RANGE = 220;
const onBeforeUnload = useCallback(() => {
dispatch(persistDashboardLabelsColor());
resetColors(getColorNamespace(dashboardInfo?.metadata?.color_namespace));
prevRenderedChartIds.current = [];
}, [dashboardInfo?.metadata?.color_namespace, dispatch]);
useEffect(() => {
// verify freshness of color map on tab change
// and when loading for first time
setTimeout(() => {
dispatch(updateDashboardLabelsColor());
}, 500);
}, [directPathToChild, dispatch]);
// verify freshness of color map
// when charts render to catch new labels
const numRenderedCharts = renderedChartIds.length;
if (
dashboardLabelsColorInitiated &&
dashboardInfo?.id &&
numRenderedCharts > 0 &&
prevRenderedChartIds.current.length < numRenderedCharts
) {
const newRenderedChartIds = renderedChartIds.filter(
id => !prevRenderedChartIds.current.includes(id),
);
prevRenderedChartIds.current = renderedChartIds;
dispatch(updateDashboardLabelsColor(newRenderedChartIds));
}
}, [
dashboardInfo?.id,
renderedChartIds,
dispatch,
dashboardLabelsColorInitiated,
]);
useEffect(() => {
const labelsColorMap = getLabelsColorMap();
const colorNamespace = getColorNamespace(
dashboardInfo?.metadata?.color_namespace,
);
labelsColorMap.source = LabelsColorMapSource.Dashboard;
// apply labels color as dictated by stored metadata
applyColors(dashboardInfo.metadata);
if (dashboardInfo?.id && !dashboardLabelsColorInitiated) {
// apply labels color as dictated by stored metadata (if any)
setDashboardLabelsColorInitiated(true);
dispatch(applyDashboardLabelsColorOnLoad(dashboardInfo.metadata));
}
return () => {
resetColors(getColorNamespace(colorNamespace));
onBeforeUnload();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardInfo.id, dispatch]);
}, [dashboardInfo?.id, dispatch]);
useEffect(() => {
// 'beforeunload' event interferes with Cypress data cleanup process.
// This code prevents 'beforeunload' from triggering in Cypress tests,
// as it is not required for end-to-end testing scenarios.
if (!(window as any).Cypress) {
window.addEventListener('beforeunload', onBeforeUnload);
}
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
};
}, [onBeforeUnload]);
return (
<div className="grid-container" data-test="grid-container">

View File

@ -461,7 +461,6 @@ class Header extends PureComponent {
customCss,
colorNamespace,
dataMask,
setColorScheme,
setUnsavedChanges,
colorScheme,
onUndo,
@ -501,7 +500,6 @@ class Header extends PureComponent {
const handleOnPropertiesChange = updates => {
const { dashboardInfoChanged, dashboardTitleChanged } = this.props;
setColorScheme(updates.colorScheme);
dashboardInfoChanged({
slug: updates.slug,
metadata: JSON.parse(updates.jsonMetadata || '{}'),

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { omit } from 'lodash';
import { Input } from 'src/components/Input';
import { FormItem } from 'src/components/Form';
@ -44,9 +44,19 @@ import withToasts from 'src/components/MessageToasts/withToasts';
import TagType from 'src/types/TagType';
import { fetchTags, OBJECT_TYPES } from 'src/features/tags/tags';
import { loadTags } from 'src/components/Tags/utils';
import { applyColors, getColorNamespace } from 'src/utils/colorScheme';
import {
applyColors,
getColorNamespace,
getLabelsColorMapEntries,
} from 'src/utils/colorScheme';
import getOwnerName from 'src/utils/getOwnerName';
import Owner from 'src/types/Owner';
import { useDispatch } from 'react-redux';
import {
setColorScheme,
setDashboardMetadata,
} from 'src/dashboard/actions/dashboardState';
import { areObjectsEqual } from 'src/reduxUtils';
const StyledFormItem = styled(FormItem)`
margin-bottom: 0;
@ -84,6 +94,7 @@ type DashboardInfo = {
certifiedBy: string;
certificationDetails: string;
isManagedExternally: boolean;
metadata: Record<string, any>;
};
const PropertiesModal = ({
@ -98,10 +109,11 @@ const PropertiesModal = ({
onSubmit = () => {},
show = false,
}: PropertiesModalProps) => {
const dispatch = useDispatch();
const [form] = AntdForm.useForm();
const [isLoading, setIsLoading] = useState(false);
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
const [colorScheme, setColorScheme] = useState(currentColorScheme);
const [colorScheme, setCurrentColorScheme] = useState(currentColorScheme);
const [jsonMetadata, setJsonMetadata] = useState('');
const [dashboardInfo, setDashboardInfo] = useState<DashboardInfo>();
const [owners, setOwners] = useState<Owners>([]);
@ -109,6 +121,7 @@ const PropertiesModal = ({
const saveLabel = onlyApply ? t('Apply') : t('Save');
const [tags, setTags] = useState<TagType[]>([]);
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const originalDashboardMetadata = useRef<Record<string, any>>({});
const tagsAsSelectValues = useMemo(() => {
const selectTags = tags.map((tag: { id: number; name: string }) => ({
@ -182,21 +195,24 @@ const PropertiesModal = ({
certifiedBy: certified_by || '',
certificationDetails: certification_details || '',
isManagedExternally: is_managed_externally || false,
metadata,
};
form.setFieldsValue(dashboardInfo);
setDashboardInfo(dashboardInfo);
setOwners(owners);
setRoles(roles);
setColorScheme(metadata.color_scheme);
setCurrentColorScheme(metadata.color_scheme);
const metaDataCopy = omit(metadata, [
'positions',
'shared_label_colors',
'map_label_colors',
'color_scheme_domain',
]);
setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : '');
originalDashboardMetadata.current = metadata;
},
[form],
);
@ -269,6 +285,8 @@ const PropertiesModal = ({
return parsedRoles;
};
const handleOnCancel = () => onHide();
const onColorSchemeChange = (
colorScheme = '',
{ updateMetadata = true } = {},
@ -287,20 +305,21 @@ const PropertiesModal = ({
throw new Error('A valid color scheme is required');
}
jsonMetadataObj.color_scheme = colorScheme;
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
setCurrentColorScheme(colorScheme);
dispatch(setColorScheme(colorScheme));
// update metadata to match selection
if (updateMetadata) {
jsonMetadataObj.color_scheme = colorScheme;
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
setJsonMetadata(jsonStringify(jsonMetadataObj));
}
setColorScheme(colorScheme);
};
const onFinish = () => {
const { title, slug, certifiedBy, certificationDetails } =
form.getFieldsValue();
let currentColorScheme = colorScheme;
let currentJsonMetadata = jsonMetadata;
// validate currentJsonMetadata
@ -318,29 +337,48 @@ const PropertiesModal = ({
return;
}
const copyMetadata = { ...metadata };
const colorNamespace = getColorNamespace(metadata?.color_namespace);
// color scheme in json metadata has precedence over selection
currentColorScheme = metadata?.color_scheme || colorScheme;
const updatedColorScheme = metadata?.color_scheme || colorScheme;
const shouldGoFresh =
updatedColorScheme !== originalDashboardMetadata.current.color_scheme;
const shouldResetCustomLabels = !areObjectsEqual(
originalDashboardMetadata.current.label_colors || {},
metadata?.label_colors || {},
);
const currentCustomLabels = Object.keys(metadata?.label_colors || {});
const prevCustomLabels = Object.keys(
originalDashboardMetadata.current.label_colors || {},
);
const resettableCustomLabels =
currentCustomLabels.length > 0 ? currentCustomLabels : prevCustomLabels;
const freshCustomLabels =
shouldResetCustomLabels && resettableCustomLabels.length > 0
? resettableCustomLabels
: false;
const jsonMetadataObj = getJsonMetadata();
const customLabelColors = jsonMetadataObj.label_colors || {};
const updatedDashboardMetadata = {
...originalDashboardMetadata.current,
label_colors: customLabelColors,
color_scheme: updatedColorScheme,
};
// remove information from user facing input
if (metadata?.shared_label_colors) {
delete metadata.shared_label_colors;
}
if (metadata?.color_scheme_domain) {
delete metadata.color_scheme_domain;
}
originalDashboardMetadata.current = updatedDashboardMetadata;
applyColors(updatedDashboardMetadata, shouldGoFresh || freshCustomLabels);
dispatch(
setDashboardMetadata({
...updatedDashboardMetadata,
map_label_colors: getLabelsColorMapEntries(customLabelColors),
}),
);
// only apply colors, the user has not saved yet
applyColors(copyMetadata, true);
currentJsonMetadata = jsonStringify(metadata);
onColorSchemeChange(currentColorScheme, {
onColorSchemeChange(updatedColorScheme, {
updateMetadata: false,
});
currentJsonMetadata = jsonStringify(metadata);
const moreOnSubmitProps: { roles?: Roles } = {};
const morePutProps: { roles?: number[]; tags?: (number | undefined)[] } =
{};
@ -557,14 +595,14 @@ const PropertiesModal = ({
return (
<Modal
show={show}
onHide={onHide}
onHide={handleOnCancel}
title={t('Dashboard properties')}
footer={
<>
<Button
htmlType="button"
buttonSize="small"
onClick={onHide}
onClick={handleOnCancel}
data-test="properties-modal-cancel-button"
cta
>

View File

@ -66,8 +66,8 @@ const SyncDashboardState: FC<Props> = ({ dashboardPageId }) => {
>(
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
labelsColor: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
labelsColorMap:
dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
labelsColorMap: dashboardInfo.metadata?.map_label_colors || EMPTY_OBJECT,
sharedLabelsColors: dashboardInfo.metadata?.shared_label_colors || [],
colorScheme: dashboardState?.colorScheme,
chartConfiguration:
dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,

View File

@ -197,11 +197,19 @@ class Chart extends Component {
}
}
} else if (
// chart should re-render if color scheme or label color was changed
// chart should re-render if color scheme or label colors were changed
nextProps.formData?.color_scheme !== this.props.formData?.color_scheme ||
!areObjectsEqual(
nextProps.formData?.label_colors,
this.props.formData?.label_colors,
nextProps.formData?.label_colors || {},
this.props.formData?.label_colors || {},
) ||
!areObjectsEqual(
nextProps.formData?.map_label_colors || {},
this.props.formData?.map_label_colors || {},
) ||
!isEqual(
nextProps.formData?.shared_label_colors || [],
this.props.formData?.shared_label_colors || [],
)
) {
return true;

View File

@ -33,7 +33,7 @@ export const crossFiltersSelector = (props: {
verboseMaps: { [key: string]: Record<string, string> };
}): CrossFilterIndicator[] => {
const { dataMask, chartConfiguration, dashboardLayout, verboseMaps } = props;
const chartsIds = Object.keys(chartConfiguration);
const chartsIds = Object.keys(chartConfiguration || {});
return chartsIds
.map(chartId => {

View File

@ -41,7 +41,8 @@ const initialState: { dashboardInfo: DashboardInfo } = {
color_namespace: '',
color_scheme_domain: [],
label_colors: {},
shared_label_colors: {},
shared_label_colors: [],
map_label_colors: {},
cross_filters_enabled: false,
},
json_metadata: '',

View File

@ -485,7 +485,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
Sports: '#8FD3E4',
Strategy: '#A1A6BD',
},
shared_label_colors: {},
shared_label_colors: [],
color_scheme: 'supersetColors',
extra_filters: [
{
@ -710,7 +710,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
queryFields: {
metric: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
color_scheme: 'supersetColors',
extra_filters: [
{
@ -759,7 +759,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
queryFields: {
metric: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
color_scheme: 'supersetColors',
dashboardId: 9,
applied_time_extras: {},
@ -3046,7 +3046,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@ -3125,7 +3125,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
dashboardId: 9,
applied_time_extras: {},
where: '',
@ -16514,7 +16514,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@ -16661,7 +16661,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
dashboardId: 9,
applied_time_extras: {},
where: '',
@ -17523,7 +17523,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
Sports: '#8FD3E4',
Strategy: '#A1A6BD',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@ -17678,7 +17678,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
extra_filters: [
{
col: '__time_range',
@ -17715,7 +17715,7 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
groupby: 'groupby',
metrics: 'metrics',
},
shared_label_colors: {},
shared_label_colors: [],
dashboardId: 9,
applied_time_extras: {},
where: '',

View File

@ -59,17 +59,24 @@ function mapStateToProps(
const datasource =
(chart && chart.form_data && datasources[chart.form_data.datasource]) ||
PLACEHOLDER_DATASOURCE;
const { colorScheme, colorNamespace, datasetsStatus } = dashboardState;
const {
colorScheme: appliedColorScheme,
colorNamespace,
datasetsStatus,
} = dashboardState;
const labelsColor = dashboardInfo?.metadata?.label_colors || {};
const labelsColorMap = dashboardInfo?.metadata?.shared_label_colors || {};
const labelsColorMap = dashboardInfo?.metadata?.map_label_colors || {};
const sharedLabelsColors = dashboardInfo?.metadata?.shared_label_colors || [];
const ownColorScheme = chart.form_data?.color_scheme;
// note: this method caches filters if possible to prevent render cascades
const formData = getFormDataWithExtraFilters({
chart,
chartConfiguration: dashboardInfo.metadata?.chart_configuration,
charts: chartQueries,
filters: getAppliedFilterValues(id),
colorScheme,
colorNamespace,
colorScheme: appliedColorScheme,
ownColorScheme,
sliceId: id,
nativeFilters: nativeFilters?.filters,
allSliceIds: dashboardState.sliceIds,
@ -77,6 +84,7 @@ function mapStateToProps(
extraControls,
labelsColor,
labelsColorMap,
sharedLabelsColors,
});
formData.dashboardId = dashboardInfo.id;

View File

@ -46,6 +46,10 @@ import {
SET_OVERRIDE_CONFIRM,
SAVE_DASHBOARD_STARTED,
SAVE_DASHBOARD_FINISHED,
SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE,
SET_DASHBOARD_LABELS_COLORMAP_SYNCED,
SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE,
SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED,
} from '../actions/dashboardState';
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
@ -101,6 +105,30 @@ export default function dashboardStateReducer(state = {}, action) {
updatedColorScheme: true,
};
},
[SET_DASHBOARD_LABELS_COLORMAP_SYNCABLE]() {
return {
...state,
labelsColorMapMustSync: true,
};
},
[SET_DASHBOARD_LABELS_COLORMAP_SYNCED]() {
return {
...state,
labelsColorMapMustSync: false,
};
},
[SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE]() {
return {
...state,
sharedLabelsColorsMustSync: true,
};
},
[SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED]() {
return {
...state,
sharedLabelsColorsMustSync: false,
};
},
[TOGGLE_EXPAND_SLICE]() {
const updatedExpandedSlices = { ...state.expandedSlices };
const { sliceId } = action;

View File

@ -52,6 +52,8 @@ export type Chart = ChartState & {
form_data: {
viz_type: string;
datasource: string;
color_scheme: string;
slice_id: number;
};
};
@ -131,7 +133,8 @@ export type DashboardInfo = {
color_namespace: string;
color_scheme_domain: string[];
label_colors: JsonObject;
shared_label_colors: JsonObject;
shared_label_colors: string[];
map_label_colors: JsonObject;
cross_filters_enabled: boolean;
};
crossFiltersEnabled: boolean;

View File

@ -25,6 +25,7 @@ import {
import { ChartConfiguration, ChartQueryPayload } from 'src/dashboard/types';
import { getExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
import { areObjectsEqual } from 'src/reduxUtils';
import { isEqual } from 'lodash';
import getEffectiveExtraFilters from './getEffectiveExtraFilters';
import { getAllActiveFilters } from '../activeAllDashboardFilters';
@ -38,6 +39,7 @@ export interface GetFormDataWithExtraFiltersArguments {
chart: ChartQueryPayload;
filters: DataRecordFilters;
colorScheme?: string;
ownColorScheme?: string;
colorNamespace?: string;
sliceId: number;
dataMask: DataMaskStateWithId;
@ -45,6 +47,7 @@ export interface GetFormDataWithExtraFiltersArguments {
extraControls: Record<string, string | boolean | null>;
labelsColor?: Record<string, string>;
labelsColorMap?: Record<string, string>;
sharedLabelsColors?: string[];
allSliceIds: number[];
}
@ -57,30 +60,32 @@ export default function getFormDataWithExtraFilters({
nativeFilters,
chartConfiguration,
colorScheme,
ownColorScheme,
colorNamespace,
sliceId,
dataMask,
extraControls,
labelsColor,
labelsColorMap,
sharedLabelsColors,
allSliceIds,
}: GetFormDataWithExtraFiltersArguments) {
// if dashboard metadata + filters have not changed, use cache if possible
const cachedFormData = cachedFormdataByChart[sliceId];
if (
cachedFiltersByChart[sliceId] === filters &&
areObjectsEqual(cachedFormData?.color_scheme, colorScheme, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.own_color_scheme, ownColorScheme) &&
areObjectsEqual(cachedFormData?.color_scheme, colorScheme) &&
areObjectsEqual(cachedFormData?.color_namespace, colorNamespace, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.label_colors, labelsColor, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.shared_label_colors, labelsColorMap, {
areObjectsEqual(cachedFormData?.map_label_colors, labelsColorMap, {
ignoreUndefined: true,
}) &&
isEqual(cachedFormData?.shared_label_colors, sharedLabelsColors) &&
!!cachedFormData &&
areObjectsEqual(cachedFormData?.dataMask, dataMask, {
ignoreUndefined: true,
@ -110,9 +115,14 @@ export default function getFormDataWithExtraFilters({
const formData = {
...chart.form_data,
chart_id: chart.id,
label_colors: labelsColor,
shared_label_colors: labelsColorMap,
shared_label_colors: sharedLabelsColors,
map_label_colors: labelsColorMap,
...(colorScheme && { color_scheme: colorScheme }),
...(ownColorScheme && {
own_color_scheme: ownColorScheme,
}),
extra_filters: getEffectiveExtraFilters(filters),
...extraData,
...extraControls,

View File

@ -58,6 +58,7 @@ const CHARTS = {
datasource: '2__table',
viz_type: 'echarts_timeseries_line',
slice_id: 1,
color_scheme: 'supersetColors',
},
chartAlert: null,
chartStatus: 'rendered' as const,
@ -76,6 +77,7 @@ const CHARTS = {
'2': {
id: 2,
form_data: {
color_scheme: 'supersetColors',
datasource: '2__table',
viz_type: 'echarts_timeseries_line',
slice_id: 2,
@ -262,8 +264,10 @@ test('Recalculate charts in global filter scope when charts change', () => {
'3': {
id: 3,
form_data: {
slice_id: 3,
datasource: '3__table',
viz_type: 'echarts_timeseries_line',
color_scheme: 'supersetColors',
},
chartAlert: null,
chartStatus: 'rendered' as const,

View File

@ -39,6 +39,7 @@ const propTypes = {
canOverwrite: PropTypes.bool.isRequired,
canDownload: PropTypes.bool.isRequired,
dashboardId: PropTypes.number,
colorScheme: PropTypes.string,
isStarred: PropTypes.bool.isRequired,
slice: PropTypes.object,
sliceName: PropTypes.string,
@ -68,6 +69,7 @@ const additionalItemsStyles = theme => css`
export const ExploreChartHeader = ({
dashboardId,
colorScheme: dashboardColorScheme,
slice,
actions,
formData,
@ -84,17 +86,15 @@ export const ExploreChartHeader = ({
const dispatch = useDispatch();
const { latestQueryFormData, sliceFormData } = chart;
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
const updateCategoricalNamespace = async () => {
const { dashboards } = metadata || {};
const dashboard =
dashboardId && dashboards && dashboards.find(d => d.id === dashboardId);
if (!dashboard) {
if (!dashboard || !dashboardColorScheme) {
// clean up color namespace and shared color maps
// to avoid colors spill outside of dashboard context
resetColors(metadata?.color_namespace);
return;
}
if (dashboard) {
@ -108,6 +108,7 @@ export const ExploreChartHeader = ({
// setting the chart to use the dashboard custom label colors if any
const dashboardMetadata = JSON.parse(result.json_metadata);
// ensure consistency with the dashboard
applyColors(dashboardMetadata);
} catch (error) {
logging.info(t('Unable to retrieve dashboard colors'));

View File

@ -76,6 +76,9 @@ const propTypes = {
actions: PropTypes.object.isRequired,
datasource_type: PropTypes.string.isRequired,
dashboardId: PropTypes.number,
colorScheme: PropTypes.string,
ownColorScheme: PropTypes.string,
dashboardColorScheme: PropTypes.string,
isDatasourceMetaLoading: PropTypes.bool.isRequired,
chart: chartPropShape.isRequired,
slice: PropTypes.object,
@ -563,6 +566,7 @@ function ExploreViewContainer(props) {
canOverwrite={props.can_overwrite}
canDownload={props.can_download}
dashboardId={props.dashboardId}
colorScheme={props.dashboardColorScheme}
isStarred={props.isStarred}
slice={props.slice}
sliceName={props.sliceName}
@ -741,6 +745,9 @@ function mapStateToProps(state) {
},
);
const chart = charts[slice_id];
const colorScheme = explore.form_data?.color_scheme;
const ownColorScheme = explore.form_data?.own_color_scheme;
const dashboardColorScheme = explore.form_data?.dashboard_color_scheme;
let dashboardId = Number(explore.form_data?.dashboardId);
if (Number.isNaN(dashboardId)) {
@ -753,6 +760,9 @@ function mapStateToProps(state) {
datasource_type: datasource.type,
datasourceId: datasource.datasource_id,
dashboardId,
colorScheme,
ownColorScheme,
dashboardColorScheme,
controls: explore.controls,
can_add: !!explore.can_add,
can_download: !!explore.can_download,

View File

@ -29,6 +29,7 @@ import ColorSchemeControl, { ColorSchemes } from '.';
const defaultProps = () => ({
hasCustomLabelsColor: false,
sharedLabelsColors: [],
label: 'Color scheme',
labelMargin: 0,
name: 'color',
@ -128,8 +129,8 @@ test('displays color scheme options', async () => {
});
});
test('Renders control with dashboard id', () => {
setup({ dashboardId: 1 });
test('Renders control with dashboard id and dashboard color scheme', () => {
setup({ dashboardId: 1, hasDashboardColorScheme: true });
expect(screen.getByText('Dashboard scheme')).toBeInTheDocument();
expect(
screen.getByLabelText('Select color scheme', { selector: 'input' }),

View File

@ -26,6 +26,8 @@ import {
styled,
t,
useTheme,
getLabelsColorMap,
CategoricalColorNamespace,
} from '@superset-ui/core';
import AntdSelect from 'antd/lib/select';
import { isFunction, sortBy } from 'lodash';
@ -35,6 +37,7 @@ import Icons from 'src/components/Icons';
import { SelectOptionsType } from 'src/components/Select/types';
import { StyledSelect } from 'src/components/Select/styles';
import { handleFilterOptionHelper } from 'src/components/Select/utils';
import { getColorNamespace } from 'src/utils/colorScheme';
import ColorSchemeLabel from './ColorSchemeLabel';
const { Option, OptGroup } = AntdSelect;
@ -47,6 +50,12 @@ export interface ColorSchemes {
export interface ColorSchemeControlProps {
hasCustomLabelsColor: boolean;
hasDashboardColorScheme?: boolean;
hasSharedLabelsColor?: boolean;
sharedLabelsColors?: string[];
mapLabelsColors?: Record<string, any>;
colorNamespace?: string;
chartId?: number;
dashboardId?: number;
label: string;
name: string;
@ -64,8 +73,8 @@ const StyledAlert = styled(Icons.AlertSolid)`
`;
const CUSTOM_LABEL_ALERT = t(
`This color scheme is being overridden by custom label colors.
Check the JSON metadata in the Advanced settings`,
`The colors of this chart might be overridden by custom label colors of the related dashboard.
Check the JSON metadata in the Advanced settings.`,
);
const DASHBOARD_ALERT = t(
@ -73,18 +82,38 @@ const DASHBOARD_ALERT = t(
Edit the color scheme in the dashboard properties.`,
);
const DASHBOARD_CONTEXT_ALERT = t(
`You are viewing this chart in a dashboard context with labels shared across multiple charts.
The color scheme selection is disabled.`,
);
const DASHBOARD_CONTEXT_TOOLTIP = t(
`You are viewing this chart in the context of a dashboard that is directly affecting its colors.
To edit the color scheme, open this chart outside of the dashboard.`,
);
const Label = ({
label,
hasCustomLabelsColor,
dashboardId,
hasSharedLabelsColor,
hasCustomLabelsColor,
hasDashboardColorScheme,
}: Pick<
ColorSchemeControlProps,
'label' | 'hasCustomLabelsColor' | 'dashboardId'
| 'label'
| 'dashboardId'
| 'hasCustomLabelsColor'
| 'hasSharedLabelsColor'
| 'hasDashboardColorScheme'
>) => {
if (hasCustomLabelsColor || dashboardId) {
const alertTitle = hasCustomLabelsColor
? CUSTOM_LABEL_ALERT
: DASHBOARD_ALERT;
if (hasSharedLabelsColor || hasCustomLabelsColor || hasDashboardColorScheme) {
const alertTitle =
hasCustomLabelsColor && !hasSharedLabelsColor
? CUSTOM_LABEL_ALERT
: dashboardId && hasDashboardColorScheme
? DASHBOARD_ALERT
: DASHBOARD_CONTEXT_ALERT;
return (
<>
{label}{' '}
@ -99,7 +128,12 @@ const Label = ({
const ColorSchemeControl = ({
hasCustomLabelsColor = false,
hasDashboardColorScheme = false,
mapLabelsColors = {},
sharedLabelsColors = [],
dashboardId,
colorNamespace,
chartId,
label = t('Color scheme'),
onChange = () => {},
value,
@ -110,9 +144,21 @@ const ColorSchemeControl = ({
isLinear,
...rest
}: ColorSchemeControlProps) => {
const countSharedLabelsColor = sharedLabelsColors.length;
const colorMapInstance = getLabelsColorMap();
const chartLabels = chartId
? colorMapInstance.chartsLabelsMap.get(chartId)?.labels || []
: [];
const hasSharedLabelsColor = !!(
dashboardId &&
countSharedLabelsColor > 0 &&
chartLabels.some(label => sharedLabelsColors.includes(label))
);
const hasDashboardScheme = dashboardId && hasDashboardColorScheme;
const showDashboardLockedOption = hasDashboardScheme || hasSharedLabelsColor;
const theme = useTheme();
const currentScheme = useMemo(() => {
if (dashboardId) {
if (showDashboardLockedOption) {
return 'dashboard';
}
let result = value || defaultScheme;
@ -121,13 +167,15 @@ const ColorSchemeControl = ({
result = schemesObject?.SUPERSET_DEFAULT?.id;
}
return result;
}, [dashboardId, defaultScheme, schemes, value]);
}, [defaultScheme, schemes, showDashboardLockedOption, value]);
const options = useMemo(() => {
if (dashboardId) {
if (showDashboardLockedOption) {
return [
<Option value="dashboard" label={t('dashboard')} key="dashboard">
<Tooltip title={DASHBOARD_ALERT}>{t('Dashboard scheme')}</Tooltip>
<Option value="dashboard" label={t('Dashboard')} key="dashboard">
<Tooltip title={DASHBOARD_CONTEXT_TOOLTIP}>
{t('Dashboard scheme')}
</Tooltip>
</Option>,
];
}
@ -218,11 +266,29 @@ const ColorSchemeControl = ({
))}
</OptGroup>
));
}, [choices, dashboardId, isLinear, schemes]);
}, [choices, hasDashboardScheme, hasSharedLabelsColor, isLinear, schemes]);
// We can't pass on change directly because it receives a second
// parameter and it would be interpreted as the error parameter
const handleOnChange = (value: string) => onChange(value);
const handleOnChange = (value: string) => {
if (chartId) {
colorMapInstance.setOwnColorScheme(chartId, value);
if (dashboardId) {
const colorNameSpace = getColorNamespace(colorNamespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const sharedLabelsSet = new Set(sharedLabelsColors);
// reset colors except shared and custom labels to keep dashboard consistency
const resettableLabels = Object.keys(mapLabelsColors).filter(
l => !sharedLabelsSet.has(l),
);
categoricalNamespace.resetColorsForLabels(resettableLabels);
}
}
onChange(value);
};
return (
<>
@ -231,8 +297,10 @@ const ColorSchemeControl = ({
label={
<Label
label={label}
hasCustomLabelsColor={hasCustomLabelsColor}
dashboardId={dashboardId}
hasCustomLabelsColor={hasCustomLabelsColor}
hasDashboardColorScheme={hasDashboardColorScheme}
hasSharedLabelsColor={hasSharedLabelsColor}
/>
}
/>
@ -249,7 +317,7 @@ const ColorSchemeControl = ({
`}
aria-label={t('Select color scheme')}
allowClear={clearable}
disabled={!!dashboardId}
disabled={hasDashboardScheme || hasSharedLabelsColor}
onChange={handleOnChange}
placeholder={t('Select scheme')}
value={currentScheme}

View File

@ -61,6 +61,9 @@ describe('controlUtils', () => {
controls: {},
form_data: { datasource: '1__table', viz_type: 'table' },
common: {},
slice: {
slice_id: 1,
},
};
beforeAll(() => {

View File

@ -151,10 +151,9 @@ const getExpectedResultFormData = (overrides: JsonObject = {}) => ({
girl: '#FF69B4',
boy: '#ADD8E6',
},
shared_label_colors: {
boy: '#ADD8E6',
girl: '#FF69B4',
},
shared_label_colors: ['boy', 'girl'],
own_color_scheme: 'supersetColors',
dashboard_color_scheme: 'd3Category20b',
extra_filters: [
{
col: '__time_range',

View File

@ -218,11 +218,18 @@ export const getFormDataWithDashboardContext = (
{},
);
const ownColorScheme = exploreFormData.color_scheme;
const dashboardColorScheme = dashboardContextFormData.color_scheme;
const appliedColorScheme = dashboardColorScheme || ownColorScheme;
return {
...exploreFormData,
...dashboardContextFormData,
...filterBoxData,
...nativeFiltersData,
...adhocFilters,
own_color_scheme: ownColorScheme,
color_scheme: appliedColorScheme,
dashboard_color_scheme: dashboardColorScheme,
};
};

View File

@ -99,9 +99,10 @@ const getDashboardContextFormData = () => {
if (dashboardContext) {
const sliceId = getUrlParam(URL_PARAMS.sliceId) || 0;
const {
colorScheme,
labelsColor,
labelsColorMap,
colorScheme,
sharedLabelsColors,
chartConfiguration,
nativeFilters,
filterBoxFilters,
@ -113,15 +114,18 @@ const getDashboardContextFormData = () => {
filters: getAppliedFilterValues(sliceId, filterBoxFilters),
nativeFilters,
chartConfiguration,
colorScheme,
dataMask,
colorScheme,
labelsColor,
labelsColorMap,
sharedLabelsColors,
sliceId,
allSliceIds: [sliceId],
extraControls: {},
});
Object.assign(dashboardContextWithFilters, { dashboardId });
Object.assign(dashboardContextWithFilters, {
dashboardId,
});
return dashboardContextWithFilters;
}
return null;

View File

@ -26,6 +26,7 @@ import { ChartConfiguration } from 'src/dashboard/types';
export interface DashboardContextForExplore {
labelsColor: Record<string, string>;
labelsColorMap: Record<string, string>;
sharedLabelsColors: string[];
colorScheme: string;
chartConfiguration: ChartConfiguration;
nativeFilters: PartialFilters;

View File

@ -32,16 +32,60 @@ import {
export const getColorNamespace = (namespace?: string) => namespace || undefined;
/**
* Get the labels color map entries
* Get labels shared across all charts in a dashboard.
* Merges a fresh instance of shared label colors with a stored one.
*
* @returns Record<string, string>
*/
export const getLabelsColorMapEntries = (): Record<string, string> => {
const labelsColorMapInstance = getLabelsColorMap();
const updatedLabelsColorMapEntries = Object.fromEntries(
labelsColorMapInstance.getColorMap(),
export const getFreshSharedLabels = (
currentSharedLabels: string[] = [],
): string[] => {
const { chartsLabelsMap } = getLabelsColorMap();
const allLabels = Array.from(chartsLabelsMap.values()).flatMap(
({ labels }) => labels,
);
return updatedLabelsColorMapEntries;
const duplicates = Array.from(
allLabels.reduce(
(counts, label) => counts.set(label, (counts.get(label) || 0) + 1),
new Map(),
),
)
.filter(([, count]) => count > 1)
.map(([label]) => label);
return Array.from(new Set([...currentSharedLabels, ...duplicates]));
};
export const getSharedLabelsColorMapEntries = (
currentColorMap: Record<string, string>,
sharedLabels: string[],
): Record<string, string> =>
Object.fromEntries(
Object.entries(currentColorMap).filter(([label]) =>
sharedLabels.includes(label),
),
);
/**
* Returns all entries (labels and colors) except custom label colors.
*
* @param customLabelsColor - the custom label colors in label_colors field
* @returns all color entries except custom label colors
*/
export const getLabelsColorMapEntries = (
customLabelsColor: Record<string, string>,
): Record<string, string> => {
const labelsColorMapInstance = getLabelsColorMap();
const allEntries = Object.fromEntries(labelsColorMapInstance.getColorMap());
// custom label colors are applied and stored separetely via label_colors
// removing all instances of custom label colors from the entries
Object.keys(customLabelsColor).forEach(label => {
delete allEntries[label];
});
return allEntries;
};
export const getColorSchemeDomain = (colorScheme: string) =>
@ -56,15 +100,20 @@ export const getColorSchemeDomain = (colorScheme: string) =>
export const isLabelsColorMapSynced = (
metadata: Record<string, any>,
): boolean => {
const currentLabelsColorMap = metadata?.shared_label_colors || {};
const customLabelColors = metadata?.label_colors || {};
const freshLabelsColorMap = getLabelsColorMap().getColorMap();
const isSynced = Array.from(freshLabelsColorMap.entries()).every(
const storedLabelsColorMap = metadata.map_label_colors || {};
const customLabelColors = metadata.label_colors || {};
const freshColorMap = getLabelsColorMap().getColorMap();
const fullFreshColorMap = {
...Object.fromEntries(freshColorMap),
...customLabelColors,
};
const isSynced = Object.entries(fullFreshColorMap).every(
([label, color]) =>
currentLabelsColorMap.hasOwnProperty(label) &&
(currentLabelsColorMap[label] === color ||
customLabelColors[label] !== undefined),
storedLabelsColorMap.hasOwnProperty(label) &&
storedLabelsColorMap[label] === color,
);
return isSynced;
};
@ -79,7 +128,7 @@ export const resetColors = (color_namespace?: string) => {
getColorNamespace(color_namespace),
);
categoricalNamespace.resetColors();
labelsColorMapInstance.clear();
labelsColorMapInstance.reset();
};
/**
@ -92,13 +141,18 @@ export const resetColors = (color_namespace?: string) => {
export const refreshLabelsColorMap = (
namespace?: string,
colorScheme?: string,
merge = false,
) => {
const colorNameSpace = getColorNamespace(namespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const labelsColorMapInstance = getLabelsColorMap();
labelsColorMapInstance.updateColorMap(categoricalNamespace, colorScheme);
labelsColorMapInstance.updateColorMap(
categoricalNamespace,
colorScheme,
merge,
);
};
/**
@ -107,34 +161,78 @@ export const refreshLabelsColorMap = (
*
* @param metadata - the dashboard metadata object
*/
export const applyColors = (metadata: Record<string, any>, fresh = false) => {
export const applyColors = (
metadata: Record<string, any>,
// Create a fresh color map by changing color scheme
fresh: boolean | string[] = false,
// Catch new labels in the color map as they appear
merge = false,
// Apply only label colors that are shared across multiple charts.
shared = false,
) => {
const colorNameSpace = getColorNamespace(metadata?.color_namespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const colorScheme = metadata?.color_scheme;
const customLabelColors = metadata?.label_colors || {};
// when scheme unset, update only custom label colors
const labelsColorMap = metadata?.shared_label_colors || {};
const fullLabelsColor = metadata?.map_label_colors || {};
const sharedLabels = metadata?.shared_label_colors || [];
const customLabelsColor = metadata?.label_colors || {};
const sharedLabelsColor = getSharedLabelsColorMapEntries(
fullLabelsColor,
sharedLabels,
);
// reset forced colors (custom labels + labels color map)
categoricalNamespace.resetColors();
if (fresh && !Array.isArray(fresh)) {
// reset custom label colors
// re-evaluate all other label colors
categoricalNamespace.resetColors();
}
// apply custom label colors first
Object.keys(customLabelColors).forEach(label => {
categoricalNamespace.setColor(label, customLabelColors[label]);
});
if (fresh && Array.isArray(fresh)) {
// when a color scheme is not set for the dashboard
// should only reset colors for charts that have changed scheme
// while keeping colors of existing shared label colors intact
// this is used also to reset custom label colors when added or removed
categoricalNamespace.resetColorsForLabels(fresh);
}
// re-instantiate a fresh labels color map based on current scheme
// will consider also just applied custom label colors
refreshLabelsColorMap(metadata?.color_namespace, colorScheme);
if (fresh || merge) {
// re-instantiate a fresh labels color map based on current scheme
// it consider just applied custom label colors if present and all forced colors
// it will merge with the existing color map new labels only when merge is true
refreshLabelsColorMap(metadata?.color_namespace, colorScheme, merge);
}
// get the fresh map that was just updated or existing
const labelsColorMapEntries = fresh
? getLabelsColorMapEntries()
: labelsColorMap;
let applicableColorMapEntries: Record<string, any> = fullLabelsColor;
if (fresh) {
// requires a new map all together
applicableColorMapEntries = {
...getLabelsColorMapEntries(customLabelsColor),
};
}
if (merge) {
// must only add up newly appearing labels
// without overriding existing ones
applicableColorMapEntries = {
...fullLabelsColor,
...getLabelsColorMapEntries(customLabelsColor),
};
}
if (shared) {
// must apply the colors to only shared labels
applicableColorMapEntries = sharedLabelsColor;
}
applicableColorMapEntries = {
...applicableColorMapEntries,
...customLabelsColor,
};
// apply the final color map
Object.keys(labelsColorMapEntries).forEach(label => {
categoricalNamespace.setColor(label, labelsColorMapEntries[label]);
});
if (applicableColorMapEntries) {
Object.keys(applicableColorMapEntries).forEach(label => {
categoricalNamespace.setColor(label, applicableColorMapEntries[label]);
});
}
};

View File

@ -260,7 +260,8 @@ class DashboardDAO(BaseDAO[Dashboard]):
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", {})
md["shared_label_colors"] = data.get("shared_label_colors", [])
md["map_label_colors"] = data.get("map_label_colors", {})
md["color_scheme_domain"] = data.get("color_scheme_domain", [])
md["cross_filters_enabled"] = data.get("cross_filters_enabled", True)
dashboard.json_metadata = json.dumps(md)

View File

@ -137,7 +137,8 @@ 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()
shared_label_colors = fields.List(fields.Str())
map_label_colors = fields.Dict()
color_scheme_domain = fields.List(fields.Str())
cross_filters_enabled = fields.Boolean(dump_default=True)
# used for v0 import/export

View File

@ -21,7 +21,7 @@ import pandas as pd
from sqlalchemy import DateTime, inspect, String
from sqlalchemy.sql import column
from superset import app, db
from superset import app, db, security_manager
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
from superset.models.core import Database
from superset.models.dashboard import Dashboard
@ -152,6 +152,7 @@ def _add_table_metrics(datasource: SqlaTable) -> None:
def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
owner = security_manager.get_user_by_id(1)
metrics = [
{
"expressionType": "SIMPLE",
@ -193,7 +194,6 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
slice_kwargs = {
"datasource_id": tbl.id,
"datasource_type": DatasourceType.TABLE,
"owners": [],
}
print("Creating some slices")
@ -210,6 +210,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
compare_suffix="over 5Y",
metric=metric,
),
owners=[],
),
Slice(
**slice_kwargs,
@ -218,6 +219,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
params=get_slice_json(
defaults, viz_type="pie", groupby=["gender"], metric=metric
),
owners=[],
),
Slice(
**slice_kwargs,
@ -232,6 +234,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
show_legend=True,
metrics=metrics,
),
owners=[],
),
Slice(
**slice_kwargs,
@ -268,6 +271,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
],
groupby=["state"],
),
owners=[],
),
Slice(
**slice_kwargs,
@ -281,6 +285,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
timeseries_limit_metric=metric,
metrics=[metric],
),
owners=[],
),
Slice(
**slice_kwargs,
@ -297,6 +302,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
adhoc_filters=[gen_filter("gender", "girl")],
metric=metric,
),
owners=[],
),
Slice(
**slice_kwargs,
@ -310,6 +316,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
timeseries_limit_metric=metric,
metrics=[metric],
),
owners=[],
),
Slice(
**slice_kwargs,
@ -326,6 +333,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
adhoc_filters=[gen_filter("gender", "boy")],
metric=metric,
),
owners=[],
),
Slice(
**slice_kwargs,
@ -343,6 +351,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
x_axis_forma="smart_date",
metrics=metrics,
),
owners=[],
),
Slice(
**slice_kwargs,
@ -360,6 +369,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
x_axis_forma="smart_date",
metrics=metrics,
),
owners=[],
),
Slice(
**slice_kwargs,
@ -381,6 +391,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
}
],
),
owners=[],
),
]
misc_slices = [
@ -405,12 +416,14 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
yAxisIndex=0,
yAxisIndexB=1,
),
owners=[],
),
Slice(
**slice_kwargs,
slice_name="Num Births Trend",
viz_type="line",
params=get_slice_json(defaults, viz_type="line", metrics=metrics),
owners=[],
),
Slice(
**slice_kwargs,
@ -433,6 +446,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
}
],
),
owners=[],
),
Slice(
**slice_kwargs,
@ -452,6 +466,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
viz_type="big_number_total",
granularity_sqla="ds",
),
owners=[],
),
Slice(
**slice_kwargs,
@ -484,6 +499,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
},
limit="10",
),
owners=[owner] if owner else [],
),
Slice(
**slice_kwargs,
@ -504,6 +520,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
"label": "SUM(num_california)",
},
),
owners=[],
),
Slice(
**slice_kwargs,
@ -517,6 +534,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
adhoc_filters=[gen_filter("gender", "girl")],
subheader="total female participants",
),
owners=[],
),
Slice(
**slice_kwargs,
@ -529,6 +547,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
groupbyColumns=["state"],
metrics=metrics,
),
owners=[],
),
]
for slc in slices:

View File

@ -81,7 +81,7 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
"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": {}, "shared_label_colors": {}, "color_scheme_domain": [], "cross_filters_enabled": false}',
"json_metadata": '{"refresh_frequency": 30, "timed_refresh_immune_slices": [], "expanded_slices": {}, "color_scheme": "", "label_colors": {}, "shared_label_colors": [], "map_label_colors": {}, "color_scheme_domain": [], "cross_filters_enabled": false}',
"published": False,
}

View File

@ -175,7 +175,8 @@ dashboards_config_1: dict[str, Any] = {
"color_scheme": "supersetColors",
"label_colors": {},
"color_scheme_domain": [],
"shared_label_colors": {},
"shared_label_colors": [],
"map_label_colors": {},
"cross_filters_enabled": False,
},
"version": "1.0.0",
@ -251,7 +252,8 @@ dashboards_config_2: dict[str, Any] = {
"color_scheme": "supersetColors",
"label_colors": {},
"color_scheme_domain": [],
"shared_label_colors": {},
"shared_label_colors": [],
"map_label_colors": {},
},
"version": "1.0.0",
},

View File

@ -750,7 +750,8 @@ def test_query_context_modified_sankey_tampered(mocker: MockerFixture) -> None:
"dashboards": [11],
"extra_form_data": {},
"label_colors": {},
"shared_label_colors": {},
"shared_label_colors": [],
"map_label_colors": {},
"extra_filters": [],
"dashboardId": 11,
"force": False,