fix(Dashboard): Retain colors when color scheme not set (#30646)
This commit is contained in:
parent
3c326598a8
commit
90572be95a
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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'> = {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ export interface Dataset {
|
|||
}
|
||||
|
||||
export interface ControlPanelState {
|
||||
slice: {
|
||||
slice_id: number;
|
||||
};
|
||||
form_data: QueryFormData;
|
||||
datasource: Dataset | QueryResponse | null;
|
||||
controls: ControlStateMapping;
|
||||
|
|
|
|||
|
|
@ -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 || [],
|
||||
};
|
||||
};
|
||||
|
|
@ -27,3 +27,4 @@ export * from './defineSavedMetrics';
|
|||
export * from './getStandardizedControls';
|
||||
export * from './getTemporalColumns';
|
||||
export { default as displayTimeRelatedControls } from './displayTimeRelatedControls';
|
||||
export * from './colorControls';
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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})`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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":{}}',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 || '{}'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' }),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ describe('controlUtils', () => {
|
|||
controls: {},
|
||||
form_data: { datasource: '1__table', viz_type: 'table' },
|
||||
common: {},
|
||||
slice: {
|
||||
slice_id: 1,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue