Skip to content

Commit 852adaa

Browse files
authored
feat: conditional formatting improvements in tables (apache#34330)
1 parent 1f482b4 commit 852adaa

File tree

12 files changed

+571
-76
lines changed

12 files changed

+571
-76
lines changed

superset-frontend/packages/superset-ui-chart-controls/src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,10 @@ export enum Comparator {
458458
BetweenOrEqual = '≤ x ≤',
459459
BetweenOrLeftEqual = '≤ x <',
460460
BetweenOrRightEqual = '< x ≤',
461+
BeginsWith = 'begins with',
462+
EndsWith = 'ends with',
463+
Containing = 'containing',
464+
NotContaining = 'not containing',
461465
}
462466

463467
export const MultipleValueComparators = [
@@ -469,7 +473,7 @@ export const MultipleValueComparators = [
469473

470474
export type ConditionalFormattingConfig = {
471475
operator?: Comparator;
472-
targetValue?: number;
476+
targetValue?: number | string;
473477
targetValueLeft?: number;
474478
targetValueRight?: number;
475479
column?: string;
@@ -478,7 +482,7 @@ export type ConditionalFormattingConfig = {
478482

479483
export type ColorFormatters = {
480484
column: string;
481-
getColorFromValue: (value: number) => string | undefined;
485+
getColorFromValue: (value: number | string) => string | undefined;
482486
}[];
483487

484488
export default {};

superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,18 @@ const MIN_OPACITY_BOUNDED = 0.05;
3232
const MIN_OPACITY_UNBOUNDED = 0;
3333
const MAX_OPACITY = 1;
3434
export const getOpacity = (
35-
value: number,
36-
cutoffPoint: number,
37-
extremeValue: number,
35+
value: number | string,
36+
cutoffPoint: number | string,
37+
extremeValue: number | string,
3838
minOpacity = MIN_OPACITY_BOUNDED,
3939
maxOpacity = MAX_OPACITY,
4040
) => {
41-
if (extremeValue === cutoffPoint) {
41+
if (
42+
extremeValue === cutoffPoint ||
43+
typeof cutoffPoint !== 'number' ||
44+
typeof extremeValue !== 'number' ||
45+
typeof value !== 'number'
46+
) {
4247
return maxOpacity;
4348
}
4449
return Math.min(
@@ -61,16 +66,16 @@ export const getColorFunction = (
6166
targetValueRight,
6267
colorScheme,
6368
}: ConditionalFormattingConfig,
64-
columnValues: number[],
69+
columnValues: number[] | string[],
6570
alpha?: boolean,
6671
) => {
6772
let minOpacity = MIN_OPACITY_BOUNDED;
6873
const maxOpacity = MAX_OPACITY;
6974

7075
let comparatorFunction: (
71-
value: number,
72-
allValues: number[],
73-
) => false | { cutoffValue: number; extremeValue: number };
76+
value: number | string,
77+
allValues: number[] | string[],
78+
) => false | { cutoffValue: number | string; extremeValue: number | string };
7479
if (operator === undefined || colorScheme === undefined) {
7580
return () => undefined;
7681
}
@@ -90,7 +95,10 @@ export const getColorFunction = (
9095
switch (operator) {
9196
case Comparator.None:
9297
minOpacity = MIN_OPACITY_UNBOUNDED;
93-
comparatorFunction = (value: number, allValues: number[]) => {
98+
comparatorFunction = (value: number | string, allValues: number[]) => {
99+
if (typeof value !== 'number') {
100+
return { cutoffValue: value!, extremeValue: value! };
101+
}
94102
const cutoffValue = Math.min(...allValues);
95103
const extremeValue = Math.max(...allValues);
96104
return value >= cutoffValue && value <= extremeValue
@@ -100,49 +108,65 @@ export const getColorFunction = (
100108
break;
101109
case Comparator.GreaterThan:
102110
comparatorFunction = (value: number, allValues: number[]) =>
103-
value > targetValue!
104-
? { cutoffValue: targetValue!, extremeValue: Math.max(...allValues) }
111+
typeof targetValue === 'number' && value > targetValue!
112+
? {
113+
cutoffValue: targetValue!,
114+
extremeValue: Math.max(...allValues),
115+
}
105116
: false;
106117
break;
107118
case Comparator.LessThan:
108119
comparatorFunction = (value: number, allValues: number[]) =>
109-
value < targetValue!
110-
? { cutoffValue: targetValue!, extremeValue: Math.min(...allValues) }
120+
typeof targetValue === 'number' && value < targetValue!
121+
? {
122+
cutoffValue: targetValue!,
123+
extremeValue: Math.min(...allValues),
124+
}
111125
: false;
112126
break;
113127
case Comparator.GreaterOrEqual:
114128
comparatorFunction = (value: number, allValues: number[]) =>
115-
value >= targetValue!
116-
? { cutoffValue: targetValue!, extremeValue: Math.max(...allValues) }
129+
typeof targetValue === 'number' && value >= targetValue!
130+
? {
131+
cutoffValue: targetValue!,
132+
extremeValue: Math.max(...allValues),
133+
}
117134
: false;
118135
break;
119136
case Comparator.LessOrEqual:
120137
comparatorFunction = (value: number, allValues: number[]) =>
121-
value <= targetValue!
122-
? { cutoffValue: targetValue!, extremeValue: Math.min(...allValues) }
138+
typeof targetValue === 'number' && value <= targetValue!
139+
? {
140+
cutoffValue: targetValue!,
141+
extremeValue: Math.min(...allValues),
142+
}
123143
: false;
124144
break;
125145
case Comparator.Equal:
126-
comparatorFunction = (value: number) =>
146+
comparatorFunction = (value: number | string) =>
127147
value === targetValue!
128148
? { cutoffValue: targetValue!, extremeValue: targetValue! }
129149
: false;
130150
break;
131151
case Comparator.NotEqual:
132152
comparatorFunction = (value: number, allValues: number[]) => {
133-
if (value === targetValue!) {
134-
return false;
153+
if (typeof targetValue === 'number') {
154+
if (value === targetValue!) {
155+
return false;
156+
}
157+
const max = Math.max(...allValues);
158+
const min = Math.min(...allValues);
159+
return {
160+
cutoffValue: targetValue!,
161+
extremeValue:
162+
Math.abs(targetValue! - min) > Math.abs(max - targetValue!)
163+
? min
164+
: max,
165+
};
135166
}
136-
const max = Math.max(...allValues);
137-
const min = Math.min(...allValues);
138-
return {
139-
cutoffValue: targetValue!,
140-
extremeValue:
141-
Math.abs(targetValue! - min) > Math.abs(max - targetValue!)
142-
? min
143-
: max,
144-
};
167+
return false;
145168
};
169+
146170
break;
147171
case Comparator.Between:
148172
comparatorFunction = (value: number) =>
@@ -168,12 +192,38 @@ export const getColorFunction = (
168192
? { cutoffValue: targetValueLeft!, extremeValue: targetValueRight! }
169193
: false;
170194
break;
195+
case Comparator.BeginsWith:
196+
comparatorFunction = (value: string) =>
197+
isString(value) && value?.startsWith(targetValue as string)
198+
? { cutoffValue: targetValue!, extremeValue: targetValue! }
199+
: false;
200+
break;
201+
case Comparator.EndsWith:
202+
comparatorFunction = (value: string) =>
203+
isString(value) && value?.endsWith(targetValue as string)
204+
? { cutoffValue: targetValue!, extremeValue: targetValue! }
205+
: false;
206+
break;
207+
case Comparator.Containing:
208+
comparatorFunction = (value: string) =>
209+
isString(value) &&
210+
value?.toLowerCase().includes((targetValue as string).toLowerCase())
211+
? { cutoffValue: targetValue!, extremeValue: targetValue! }
212+
: false;
213+
break;
214+
case Comparator.NotContaining:
215+
comparatorFunction = (value: string) =>
216+
isString(value) &&
217+
!value?.toLowerCase().includes((targetValue as string).toLowerCase())
218+
? { cutoffValue: targetValue!, extremeValue: targetValue! }
219+
: false;
220+
break;
171221
default:
172222
comparatorFunction = () => false;
173223
break;
174224
}
175225

176-
return (value: number) => {
226+
return (value: number | string) => {
177227
const compareResult = comparatorFunction(value, columnValues);
178228
if (compareResult === false) return undefined;
179229
const { cutoffValue, extremeValue } = compareResult;
@@ -218,3 +268,7 @@ export const getColorFormatters = memoizeOne(
218268
[],
219269
) ?? [],
220270
);
271+
272+
function isString(value: unknown) {
273+
return typeof value === 'string';
274+
}

superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const mockData = [
3232
];
3333
const countValues = mockData.map(row => row.count);
3434

35+
const strData = [{ name: 'Brian' }, { name: 'Carlos' }, { name: 'Diana' }];
36+
const strValues = strData.map(row => row.name);
37+
3538
describe('round', () => {
3639
it('round', () => {
3740
expect(round(1)).toEqual(1);
@@ -339,6 +342,90 @@ describe('getColorFunction()', () => {
339342
expect(colorFunction(50)).toBeUndefined();
340343
expect(colorFunction(100)).toBeUndefined();
341344
});
345+
346+
it('getColorFunction BeginsWith', () => {
347+
const colorFunction = getColorFunction(
348+
{
349+
operator: Comparator.BeginsWith,
350+
targetValue: 'C',
351+
colorScheme: '#FF0000',
352+
column: 'name',
353+
},
354+
strValues,
355+
);
356+
expect(colorFunction('Brian')).toBeUndefined();
357+
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
358+
});
359+
360+
it('getColorFunction EndsWith', () => {
361+
const colorFunction = getColorFunction(
362+
{
363+
operator: Comparator.EndsWith,
364+
targetValue: 'n',
365+
colorScheme: '#FF0000',
366+
column: 'name',
367+
},
368+
strValues,
369+
);
370+
expect(colorFunction('Carlos')).toBeUndefined();
371+
expect(colorFunction('Brian')).toEqual('#FF0000FF');
372+
});
373+
374+
it('getColorFunction Containing', () => {
375+
const colorFunction = getColorFunction(
376+
{
377+
operator: Comparator.Containing,
378+
targetValue: 'o',
379+
colorScheme: '#FF0000',
380+
column: 'name',
381+
},
382+
strValues,
383+
);
384+
expect(colorFunction('Diana')).toBeUndefined();
385+
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
386+
});
387+
388+
it('getColorFunction NotContaining', () => {
389+
const colorFunction = getColorFunction(
390+
{
391+
operator: Comparator.NotContaining,
392+
targetValue: 'i',
393+
colorScheme: '#FF0000',
394+
column: 'name',
395+
},
396+
strValues,
397+
);
398+
expect(colorFunction('Diana')).toBeUndefined();
399+
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
400+
});
401+
402+
it('getColorFunction Equal', () => {
403+
const colorFunction = getColorFunction(
404+
{
405+
operator: Comparator.Equal,
406+
targetValue: 'Diana',
407+
colorScheme: '#FF0000',
408+
column: 'name',
409+
},
410+
strValues,
411+
);
412+
expect(colorFunction('Carlos')).toBeUndefined();
413+
expect(colorFunction('Diana')).toEqual('#FF0000FF');
414+
});
415+
416+
it('getColorFunction None', () => {
417+
const colorFunction = getColorFunction(
418+
{
419+
operator: Comparator.None,
420+
colorScheme: '#FF0000',
421+
column: 'name',
422+
},
423+
strValues,
424+
);
425+
expect(colorFunction('Diana')).toEqual('#FF0000FF');
426+
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
427+
expect(colorFunction('Brian')).toEqual('#FF0000FF');
428+
});
342429
});
343430

344431
describe('getColorFormatters()', () => {
@@ -388,4 +475,47 @@ describe('getColorFormatters()', () => {
388475
const colorFormatters = getColorFormatters(undefined, mockData);
389476
expect(colorFormatters.length).toEqual(0);
390477
});
478+
479+
it('correct column string config', () => {
480+
const columnConfigString = [
481+
{
482+
operator: Comparator.BeginsWith,
483+
targetValue: 'D',
484+
colorScheme: '#FF0000',
485+
column: 'name',
486+
},
487+
{
488+
operator: Comparator.EndsWith,
489+
targetValue: 'n',
490+
colorScheme: '#FF0000',
491+
column: 'name',
492+
},
493+
{
494+
operator: Comparator.Containing,
495+
targetValue: 'o',
496+
colorScheme: '#FF0000',
497+
column: 'name',
498+
},
499+
{
500+
operator: Comparator.NotContaining,
501+
targetValue: 'i',
502+
colorScheme: '#FF0000',
503+
column: 'name',
504+
},
505+
];
506+
const colorFormatters = getColorFormatters(columnConfigString, strData);
507+
expect(colorFormatters.length).toEqual(4);
508+
509+
expect(colorFormatters[0].column).toEqual('name');
510+
expect(colorFormatters[0].getColorFromValue('Diana')).toEqual('#FF0000FF');
511+
512+
expect(colorFormatters[1].column).toEqual('name');
513+
expect(colorFormatters[1].getColorFromValue('Brian')).toEqual('#FF0000FF');
514+
515+
expect(colorFormatters[2].column).toEqual('name');
516+
expect(colorFormatters[2].getColorFromValue('Carlos')).toEqual('#FF0000FF');
517+
518+
expect(colorFormatters[3].column).toEqual('name');
519+
expect(colorFormatters[3].getColorFromValue('Carlos')).toEqual('#FF0000FF');
520+
});
391521
});

superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,8 @@ const config: ControlPanelConfig = {
722722
label: Array.isArray(verboseMap)
723723
? colname
724724
: (verboseMap[colname] ?? colname),
725+
dataType:
726+
colnames && coltypes[colnames?.indexOf(colname)],
725727
}))
726728
: [];
727729
const columnOptions = explore?.controls?.time_compare?.value

superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export default {
110110
(Array.isArray(verboseMap)
111111
? verboseMap[colname as number]
112112
: verboseMap[colname as string]) ?? colname,
113+
dataType:
114+
colnames && coltypes[colnames?.indexOf(colname)],
113115
}))
114116
: [];
115117
return {

0 commit comments

Comments
 (0)