Copilot commented on code in PR #36103:
URL: https://github.com/apache/superset/pull/36103#discussion_r2535114631
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -53,24 +98,208 @@ function CountryMap(element, props) {
country,
linearColorScheme,
numberFormat,
+ customColorRules = [],
+ customColorScale = [],
+ minColor,
+ maxColor,
colorScheme,
sliceId,
} = props;
+ const minColorHexRaw = rgbaToHex(minColor) || '#f7fbff';
+ const maxColorHexRaw = rgbaToHex(maxColor) || '#08306b';
+ const minColorHex = normalizeColorKeyword(minColorHexRaw);
+ const maxColorHex = normalizeColorKeyword(maxColorHexRaw);
+
const container = element;
const format = getNumberFormatter(numberFormat);
+ const normalizedScale = normalizeScale(customColorScale);
+ const normalizedScaleWithColors = Array.isArray(normalizedScale)
+ ? normalizedScale.map(e => {
+ if (!e || typeof e !== 'object') return e;
+ return { ...e, color: normalizeColorKeyword(e.color) };
+ })
+ : [];
+
const linearColorScale = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(data, v => v.metric));
const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ // Parse metrics to numbers safely
+ const parsedData = Array.isArray(data)
+ ? data.map(r => ({ ...r, metric: safeNumber(r.metric) }))
+ : [];
+
+ // numeric values only
+ const numericValues = parsedData
+ .map(r => r.metric)
+ .filter(v => Number.isFinite(v));
+
+ let minValue = 0;
+ let maxValue = 1;
+ if (numericValues.length > 0) {
+ const extent = d3Extent(numericValues);
+ minValue = extent[0];
+ maxValue = extent[1];
+ }
+
+ const valueRange = maxValue - minValue;
+ const valueRangeNonZero = valueRange === 0 ? 1 : valueRange;
+
+ /** -------------------------
+ * 1) Custom conditional rules
+ * ------------------------- */
+ const getColorFromRules = value => {
+ if (!Array.isArray(customColorRules) || !Number.isFinite(value))
+ return null;
+ for (const rule of customColorRules) {
+ if (
+ rule &&
+ typeof rule.color === 'string' &&
+ (('min' in rule && 'max' in rule) || 'value' in rule)
+ ) {
+ if ('value' in rule && Number(rule.value) === value) {
+ // 🆕 normalize possible keyword in conditional rules as well
+ return normalizeColorKeyword(rule.color);
+ }
+ if ('min' in rule && 'max' in rule) {
+ const minR = safeNumber(rule.min);
+ const maxR = safeNumber(rule.max);
+ if (
+ Number.isFinite(minR) &&
+ Number.isFinite(maxR) &&
+ value >= minR &&
+ value <= maxR
+ ) {
+ return normalizeColorKeyword(rule.color);
+ }
+ }
+ }
+ }
+ return null;
+ };
+
+ /** -------------------------
+ * 2) Custom color scale (by %)
+ * ------------------------- */
+ let percentColorScale = null;
+ if (
+ Array.isArray(normalizedScaleWithColors) &&
+ normalizedScaleWithColors.length >= 2
+ ) {
+ const sorted = normalizedScaleWithColors
+ .filter(
+ e => e && typeof e.percent === 'number' && typeof e.color === 'string',
+ )
+ .slice()
+ .sort((a, b) => a.percent - b.percent);
+
+ if (sorted.length >= 2) {
+ const domainPerc = sorted.map(e => e.percent);
+ const rangeColors = sorted.map(e => e.color);
+ percentColorScale = d3.scale
+ .linear()
+ .domain(domainPerc)
+ .range(rangeColors)
+ .clamp(true)
+ .interpolate(d3.interpolateRgb);
+ }
+ }
+
+ /** -------------------------
+ * 3) Linear palette from registry (SI défini)
+ * ------------------------- */
+ let linearPaletteScale = null;
+ if (linearColorScheme) {
+ try {
+ const seq = getSequentialSchemeRegistry().get(linearColorScheme);
+ if (seq && typeof seq.createLinearScale === 'function') {
+ linearPaletteScale = seq.createLinearScale([minValue, maxValue]);
+ } else if (seq && Array.isArray(seq.colors) && seq.colors.length >= 2) {
+ linearPaletteScale = d3.scale
+ .linear()
+ .domain([minValue, maxValue])
+ .range([seq.colors[0], seq.colors[seq.colors.length - 1]])
+ .interpolate(d3.interpolateRgb);
+ }
+ } catch {
+ linearPaletteScale = null;
+ }
+ }
+
+ /** -------------------------
+ * 4) Gradient fallback (minColor → maxColor) avec HEX
+ * ------------------------- */
+ let gradientColorScale;
+ if (minValue === maxValue) {
+ gradientColorScale = () => minColorHex || maxColorHex || '#ddd';
+ } else {
+ gradientColorScale = d3.scale
+ .linear()
+ .domain([minValue, maxValue])
+ .range([minColorHex, maxColorHex])
+ .interpolate(d3.interpolateRgb);
+ }
+
+ /** -------------------------
+ * Build final color (priority)
+ * rules > customScale > linearPalette > gradient
+ * ------------------------- */
const colorMap = {};
- data.forEach(d => {
- colorMap[d.country_id] = colorScheme
- ? colorScale(d.country_id, sliceId)
- : linearColorScale(d.metric);
+ parsedData.forEach(r => {
+ const iso = r.country_id;
+ const value = r.metric;
+ if (!iso) return;
+ if (!Number.isFinite(value)) {
+ colorMap[iso] = 'none';
+ return;
+ }
+
+ const ruleColor = getColorFromRules(value);
+ if (ruleColor) {
+ colorMap[iso] = ruleColor;
+ return;
+ }
+
+ if (percentColorScale) {
+ const percentNormalized = ((value - minValue) / valueRangeNonZero) * 100;
+ const p = Math.max(0, Math.min(100, percentNormalized));
+ try {
+ colorMap[iso] = percentColorScale(p);
+ return;
+ } catch {
+ // continue regardless of error
Review Comment:
The percentage calculation on line 266 uses `valueRangeNonZero` to avoid
division by zero, but the logic for handling this edge case could lead to
incorrect results. When `minValue === maxValue`, all values will be normalized
to 0% (since `(value - minValue) / 1 * 100 = 0`), but they should probably all
map to 100% or 50% to indicate they're at the singular data point.
Consider handling this case explicitly:
```javascript
if (valueRange === 0) {
colorMap[iso] = percentColorScale(50); // or 100, depending on desired
behavior
return;
}
```
```suggestion
if (minValue === maxValue) {
// All values are the same; map to central color (e.g., 50%)
try {
colorMap[iso] = percentColorScale(50);
return;
} catch {
// continue regardless of error
}
} else {
const percentNormalized = ((value - minValue) / valueRangeNonZero) *
100;
const p = Math.max(0, Math.min(100, percentNormalized));
try {
colorMap[iso] = percentColorScale(p);
return;
} catch {
// continue regardless of error
}
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -27,6 +27,15 @@ import {
} from '@superset-ui/core';
import countries, { countryOptions } from './countries';
+function normalizeColorKeyword(color) {
+ if (!color && color !== '') return '#000000';
+ const c = String(color).trim().toLowerCase();
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c)) return c;
+ if (/^[a-z]+$/.test(c)) return c;
+
+ return '#000000';
+}
Review Comment:
The `normalizeColorKeyword` function doesn't validate or normalize other
valid CSS color formats like `rgb()`, `rgba()`, `hsl()`, `hsla()`, or named
colors beyond simple keywords. The function name suggests it normalizes color
keywords, but the current implementation will return '#000000' for valid CSS
colors that don't match the two regex patterns, which could lead to unexpected
behavior when users provide valid CSS colors in their custom configuration.
Consider either:
1. Renaming the function to reflect its limited scope (e.g.,
`validateBasicColor`)
2. Expanding validation to handle more CSS color formats
3. Adding documentation that only hex colors and simple lowercase color
names are supported
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -27,6 +27,15 @@ import {
} from '@superset-ui/core';
import countries, { countryOptions } from './countries';
+function normalizeColorKeyword(color) {
+ if (!color && color !== '') return '#000000';
Review Comment:
The `normalizeColorKeyword` function has a logical error in the first
condition. The check `!color && color !== ''` will never be true for an empty
string because empty string is falsy. This means empty strings will not be
normalized to '#000000' as intended.
Consider changing to: `if (!color || color === '') return '#000000';` or
simply `if (!color) return '#000000';` since empty string is falsy.
```suggestion
if (!color) return '#000000';
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/controlPanel.ts:
##########
@@ -69,6 +69,43 @@ const config: ControlPanelConfig = {
},
],
['linear_color_scheme'],
+ [
+ {
+ name: 'customColorScale',
+ config: {
+ type: 'TextAreaControl',
+ label: t('Custom Color Scale (by %)'),
+ description: t(
+ "Custom configuration that overrides the linear color scheme
color codes and thresholds. Thresholds are defined in percentage, and color
codes accept any valid CSS value. Copy-paste and edit following sample
configuration to define your own thresholds and colors :\n\n" +
+ "[\n" +
+ " { \"percent\": 0, \"color\": \"#600000\" },\n" +
+ " { \"percent\": 0.01, \"color\": \"#A00000\" },\n" +
+ " { \"percent\": 20, \"color\": \"#E52B50\" },\n" +
+ " { \"percent\": 35, \"color\": \"#FFA500\" },\n" +
+ " { \"percent\": 50, \"color\": \"#FFFF99\" },\n" +
+ " { \"percent\": 65, \"color\": \"#9ACD32\" },\n" +
+ " { \"percent\": 80, \"color\": \"#3CB371\" },\n" +
+ " { \"percent\": 99.99, \"color\": \"#228B22\" },\n" +
+ " { \"percent\": 100, \"color\": \"#006400\" }\n" +
+ "]"
+ ),
+ default: `[
+ { "percent": 0, "color": "#600000" },
+ { "percent": 0.01, "color": "#A00000" },
+ { "percent": 20, "color": "#E52B50" },
+ { "percent": 35, "color": "#FFA500" },
+ { "percent": 50, "color": "#FFFF99" },
+ { "percent": 65, "color": "#9ACD32" },
+ { "percent": 80, "color": "#3CB371" },
+ { "percent": 99.99, "color": "#228B22" },
+ { "percent": 100, "color": "#006400" }
+]`,
+ language: 'json',
+ rows: 10,
+ renderTrigger: true,
+ },
+ },
Review Comment:
[nitpick] Missing input validation for the JSON in the TextAreaControl.
Users could enter malformed JSON or data that doesn't match the expected schema
(objects without 'percent' or 'color' fields, incorrect types, etc.). While
there is try-catch in `transformProps.js`, invalid data that successfully
parses as JSON will silently fail or produce unexpected results.
Consider adding validation logic or using a more structured control (like a
JSON editor with schema validation) to provide immediate feedback to users when
they enter invalid configurations.
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -41,10 +50,46 @@ const propTypes = {
linearColorScheme: PropTypes.string,
mapBaseUrl: PropTypes.string,
numberFormat: PropTypes.string,
+ sliceId: PropTypes.number,
+ customColorRules: PropTypes.array,
+ minColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ maxColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ customColorScale: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
Review Comment:
The PropType for `customColorScale` is defined as
`PropTypes.oneOfType([PropTypes.array, PropTypes.string])` but based on the
parsing logic in `transformProps.js`, it should always be an array by the time
it reaches this component. The string type appears unnecessary and creates
confusion about the expected data type.
Consider changing to `PropTypes.array` only, or add a comment explaining why
it can be a string.
```suggestion
customColorScale: PropTypes.array,
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -53,24 +98,208 @@ function CountryMap(element, props) {
country,
linearColorScheme,
numberFormat,
+ customColorRules = [],
+ customColorScale = [],
+ minColor,
+ maxColor,
colorScheme,
sliceId,
} = props;
+ const minColorHexRaw = rgbaToHex(minColor) || '#f7fbff';
+ const maxColorHexRaw = rgbaToHex(maxColor) || '#08306b';
+ const minColorHex = normalizeColorKeyword(minColorHexRaw);
+ const maxColorHex = normalizeColorKeyword(maxColorHexRaw);
+
const container = element;
const format = getNumberFormatter(numberFormat);
+ const normalizedScale = normalizeScale(customColorScale);
+ const normalizedScaleWithColors = Array.isArray(normalizedScale)
+ ? normalizedScale.map(e => {
+ if (!e || typeof e !== 'object') return e;
+ return { ...e, color: normalizeColorKeyword(e.color) };
+ })
+ : [];
+
const linearColorScale = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(data, v => v.metric));
const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ // Parse metrics to numbers safely
+ const parsedData = Array.isArray(data)
+ ? data.map(r => ({ ...r, metric: safeNumber(r.metric) }))
+ : [];
+
+ // numeric values only
+ const numericValues = parsedData
+ .map(r => r.metric)
+ .filter(v => Number.isFinite(v));
+
+ let minValue = 0;
+ let maxValue = 1;
+ if (numericValues.length > 0) {
+ const extent = d3Extent(numericValues);
+ minValue = extent[0];
+ maxValue = extent[1];
+ }
+
+ const valueRange = maxValue - minValue;
+ const valueRangeNonZero = valueRange === 0 ? 1 : valueRange;
+
+ /** -------------------------
+ * 1) Custom conditional rules
+ * ------------------------- */
+ const getColorFromRules = value => {
+ if (!Array.isArray(customColorRules) || !Number.isFinite(value))
+ return null;
+ for (const rule of customColorRules) {
+ if (
+ rule &&
+ typeof rule.color === 'string' &&
+ (('min' in rule && 'max' in rule) || 'value' in rule)
+ ) {
+ if ('value' in rule && Number(rule.value) === value) {
+ // 🆕 normalize possible keyword in conditional rules as well
+ return normalizeColorKeyword(rule.color);
+ }
+ if ('min' in rule && 'max' in rule) {
+ const minR = safeNumber(rule.min);
+ const maxR = safeNumber(rule.max);
+ if (
+ Number.isFinite(minR) &&
+ Number.isFinite(maxR) &&
+ value >= minR &&
+ value <= maxR
+ ) {
+ return normalizeColorKeyword(rule.color);
+ }
+ }
+ }
+ }
Review Comment:
[nitpick] The color mapping logic iterates through `customColorRules` array
for every data point on line 156. If there are many rules and many data points,
this could lead to O(n*m) complexity. Consider optimizing by:
1. Pre-processing rules into a more efficient data structure (e.g., sorted
array for binary search, or hash map for exact value matches)
2. Early termination when a rule matches (which is already done with
`return`)
However, this is likely acceptable for typical use cases with small numbers
of rules.
```suggestion
// Preprocess customColorRules for efficient lookup
let valueRuleMap = {};
let rangeRules = [];
if (Array.isArray(customColorRules)) {
for (const rule of customColorRules) {
if (
rule &&
typeof rule.color === 'string' &&
(('min' in rule && 'max' in rule) || 'value' in rule)
) {
if ('value' in rule && Number.isFinite(Number(rule.value))) {
valueRuleMap[Number(rule.value)] =
normalizeColorKeyword(rule.color);
} else if ('min' in rule && 'max' in rule) {
const minR = safeNumber(rule.min);
const maxR = safeNumber(rule.max);
if (Number.isFinite(minR) && Number.isFinite(maxR)) {
rangeRules.push({
min: minR,
max: maxR,
color: normalizeColorKeyword(rule.color),
});
}
}
}
}
// Sort rangeRules by min for possible future optimizations
rangeRules.sort((a, b) => a.min - b.min);
}
const getColorFromRules = value => {
if (!Number.isFinite(value)) return null;
// Check for exact value match first
if (Object.prototype.hasOwnProperty.call(valueRuleMap, value)) {
return valueRuleMap[value];
}
// Check range rules
for (const rule of rangeRules) {
if (value >= rule.min && value <= rule.max) {
return rule.color;
}
}
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -53,24 +98,208 @@ function CountryMap(element, props) {
country,
linearColorScheme,
numberFormat,
+ customColorRules = [],
+ customColorScale = [],
+ minColor,
+ maxColor,
colorScheme,
sliceId,
} = props;
+ const minColorHexRaw = rgbaToHex(minColor) || '#f7fbff';
+ const maxColorHexRaw = rgbaToHex(maxColor) || '#08306b';
+ const minColorHex = normalizeColorKeyword(minColorHexRaw);
+ const maxColorHex = normalizeColorKeyword(maxColorHexRaw);
+
const container = element;
const format = getNumberFormatter(numberFormat);
+ const normalizedScale = normalizeScale(customColorScale);
+ const normalizedScaleWithColors = Array.isArray(normalizedScale)
+ ? normalizedScale.map(e => {
+ if (!e || typeof e !== 'object') return e;
+ return { ...e, color: normalizeColorKeyword(e.color) };
+ })
+ : [];
+
const linearColorScale = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(data, v => v.metric));
const colorScale = CategoricalColorNamespace.getScale(colorScheme);
Review Comment:
The variable `colorScale` is created on line 127 but is replaced by
`fallbackCategorical` on line 291, making this initial assignment unused.
Consider removing line 127 to improve code clarity.
```suggestion
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js:
##########
@@ -24,8 +24,25 @@ export default function transformProps(chartProps) {
selectCountry,
colorScheme,
sliceId,
+ customColorRules,
Review Comment:
The variable name `customColorRules` is extracted from `formData` but this
feature is not actually implemented or used anywhere in the visible changes.
The parsing logic is added in `transformProps.js` and the prop is defined in
PropTypes, but there's no corresponding control in `controlPanel.ts` and no
documentation on how to use it. This creates an incomplete API surface.
Either add the corresponding control and documentation for this feature, or
remove it if it's not part of this PR's scope.
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js:
##########
@@ -24,8 +24,25 @@ export default function transformProps(chartProps) {
selectCountry,
colorScheme,
sliceId,
+ customColorRules,
+ customColorScale,
} = formData;
+ let parsedColorRules = [];
+ let parsedColorScale = [];
+
+ try {
+ parsedColorRules = customColorRules ? JSON.parse(customColorRules) : [];
+ } catch (error) {
+ console.warn('Invalid JSON in customColorRules:', error);
+ }
+
+ try {
+ parsedColorScale = customColorScale ? JSON.parse(customColorScale) : [];
+ } catch (error) {
+ console.warn('Invalid JSON in customColorScale:', error);
Review Comment:
The `console.warn` messages for JSON parsing errors will only show generic
error messages. This makes it difficult for users to debug their JSON
configuration issues. Consider providing more specific error messages that
include what went wrong and where.
Example:
```javascript
console.warn('Invalid JSON in customColorScale. Please check your
configuration syntax:', error.message);
```
```suggestion
console.warn(
'Invalid JSON in customColorScale. Please check your configuration
syntax. Error:',
error && error.message ? error.message : error,
'Input:',
customColorScale,
);
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -53,24 +98,208 @@ function CountryMap(element, props) {
country,
linearColorScheme,
numberFormat,
+ customColorRules = [],
+ customColorScale = [],
+ minColor,
+ maxColor,
colorScheme,
sliceId,
} = props;
+ const minColorHexRaw = rgbaToHex(minColor) || '#f7fbff';
+ const maxColorHexRaw = rgbaToHex(maxColor) || '#08306b';
+ const minColorHex = normalizeColorKeyword(minColorHexRaw);
+ const maxColorHex = normalizeColorKeyword(maxColorHexRaw);
+
const container = element;
const format = getNumberFormatter(numberFormat);
+ const normalizedScale = normalizeScale(customColorScale);
+ const normalizedScaleWithColors = Array.isArray(normalizedScale)
+ ? normalizedScale.map(e => {
+ if (!e || typeof e !== 'object') return e;
+ return { ...e, color: normalizeColorKeyword(e.color) };
+ })
+ : [];
+
const linearColorScale = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(data, v => v.metric));
const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ // Parse metrics to numbers safely
+ const parsedData = Array.isArray(data)
+ ? data.map(r => ({ ...r, metric: safeNumber(r.metric) }))
+ : [];
+
+ // numeric values only
+ const numericValues = parsedData
+ .map(r => r.metric)
+ .filter(v => Number.isFinite(v));
+
+ let minValue = 0;
+ let maxValue = 1;
+ if (numericValues.length > 0) {
+ const extent = d3Extent(numericValues);
+ minValue = extent[0];
+ maxValue = extent[1];
+ }
+
+ const valueRange = maxValue - minValue;
+ const valueRangeNonZero = valueRange === 0 ? 1 : valueRange;
+
+ /** -------------------------
+ * 1) Custom conditional rules
+ * ------------------------- */
+ const getColorFromRules = value => {
+ if (!Array.isArray(customColorRules) || !Number.isFinite(value))
+ return null;
+ for (const rule of customColorRules) {
+ if (
+ rule &&
+ typeof rule.color === 'string' &&
+ (('min' in rule && 'max' in rule) || 'value' in rule)
+ ) {
+ if ('value' in rule && Number(rule.value) === value) {
+ // 🆕 normalize possible keyword in conditional rules as well
+ return normalizeColorKeyword(rule.color);
+ }
+ if ('min' in rule && 'max' in rule) {
+ const minR = safeNumber(rule.min);
+ const maxR = safeNumber(rule.max);
+ if (
+ Number.isFinite(minR) &&
+ Number.isFinite(maxR) &&
+ value >= minR &&
+ value <= maxR
+ ) {
+ return normalizeColorKeyword(rule.color);
+ }
+ }
+ }
+ }
+ return null;
+ };
+
+ /** -------------------------
+ * 2) Custom color scale (by %)
+ * ------------------------- */
+ let percentColorScale = null;
+ if (
+ Array.isArray(normalizedScaleWithColors) &&
+ normalizedScaleWithColors.length >= 2
+ ) {
+ const sorted = normalizedScaleWithColors
+ .filter(
+ e => e && typeof e.percent === 'number' && typeof e.color === 'string',
+ )
+ .slice()
+ .sort((a, b) => a.percent - b.percent);
+
+ if (sorted.length >= 2) {
+ const domainPerc = sorted.map(e => e.percent);
+ const rangeColors = sorted.map(e => e.color);
+ percentColorScale = d3.scale
+ .linear()
+ .domain(domainPerc)
+ .range(rangeColors)
+ .clamp(true)
+ .interpolate(d3.interpolateRgb);
+ }
+ }
+
+ /** -------------------------
+ * 3) Linear palette from registry (SI défini)
Review Comment:
Typo in French comment: "SI défini" should be "if defined" in English to
maintain consistency with the rest of the codebase.
```suggestion
* 3) Linear palette from registry (if defined)
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -27,6 +27,15 @@ import {
} from '@superset-ui/core';
import countries, { countryOptions } from './countries';
+function normalizeColorKeyword(color) {
+ if (!color && color !== '') return '#000000';
+ const c = String(color).trim().toLowerCase();
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c)) return c;
+ if (/^[a-z]+$/.test(c)) return c;
Review Comment:
The regex pattern `/^[a-z]+$/` only matches lowercase color keywords, but
CSS color keywords are case-insensitive and could be provided in uppercase or
mixed case (e.g., "Red", "BLUE"). This will cause valid CSS color names to fall
through to the default '#000000'.
Consider making the pattern case-insensitive: `/^[a-z]+$/i` or converting
the color to lowercase before testing: `if (/^[a-z]+$/.test(c.toLowerCase()))`
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -53,24 +98,208 @@ function CountryMap(element, props) {
country,
linearColorScheme,
numberFormat,
+ customColorRules = [],
+ customColorScale = [],
+ minColor,
+ maxColor,
colorScheme,
sliceId,
} = props;
+ const minColorHexRaw = rgbaToHex(minColor) || '#f7fbff';
+ const maxColorHexRaw = rgbaToHex(maxColor) || '#08306b';
+ const minColorHex = normalizeColorKeyword(minColorHexRaw);
+ const maxColorHex = normalizeColorKeyword(maxColorHexRaw);
+
const container = element;
const format = getNumberFormatter(numberFormat);
+ const normalizedScale = normalizeScale(customColorScale);
+ const normalizedScaleWithColors = Array.isArray(normalizedScale)
+ ? normalizedScale.map(e => {
+ if (!e || typeof e !== 'object') return e;
+ return { ...e, color: normalizeColorKeyword(e.color) };
+ })
+ : [];
+
const linearColorScale = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(data, v => v.metric));
Review Comment:
The variable `linearColorScale` is created on line 124-126 but is never used
in the updated code. The new implementation uses `linearPaletteScale` instead.
This unused variable should be removed to avoid confusion and maintain code
cleanliness.
```suggestion
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js:
##########
@@ -41,10 +50,46 @@ const propTypes = {
linearColorScheme: PropTypes.string,
mapBaseUrl: PropTypes.string,
numberFormat: PropTypes.string,
+ sliceId: PropTypes.number,
+ customColorRules: PropTypes.array,
+ minColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ maxColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ customColorScale: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
};
const maps = {};
+function safeNumber(v) {
+ if (v === null || v === undefined || v === '') return NaN;
+ const n = Number(v);
+ return Number.isFinite(n) ? n : NaN;
+}
+
+function rgbaToHex(rgba) {
+ if (typeof rgba === 'string') return rgba;
+ if (Array.isArray(rgba)) return rgbaToHex(rgba[0]);
+ if (!rgba || typeof rgba !== 'object') return null;
+ const { r, g, b } = rgba;
+ if (r === undefined || g === undefined || b === undefined) return null;
+ const toHex = n => {
+ const hex = Math.round(n).toString(16);
+ return hex.length === 1 ? '0' + hex : hex;
+ };
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+}
+
+function normalizeScale(scale) {
+ if (Array.isArray(scale)) return scale;
+ if (typeof scale === 'string') {
+ try {
+ return JSON.parse(scale);
+ } catch {
+ return [];
+ }
+ }
+ return [];
+}
Review Comment:
The `normalizeScale` function has duplicate JSON parsing logic that already
exists in `transformProps.js`. The `customColorScale` prop should already be an
array by the time it reaches `CountryMap.js`. This redundant parsing suggests
either:
1. The prop type is incorrectly defined (should be array-only, not
`PropTypes.oneOfType([PropTypes.array, PropTypes.string])`)
2. There's a misunderstanding of the data flow
Consider removing this duplicate parsing logic and relying on the parsing
done in `transformProps.js`, or clarify why both are needed.
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx:
##########
@@ -23,8 +23,11 @@ import Component from './CountryMap';
const ReactComponent = reactify(Component);
const CountryMap = ({ className, ...otherProps }) => (
- <div className={className}>
- <ReactComponent {...otherProps} />
+ <div className={className} >
+ <ReactComponent
+ {...otherProps}
+ />
+
Review Comment:
[nitpick] Trailing whitespace on line 30. This should be removed to maintain
code cleanliness.
```suggestion
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js:
##########
@@ -24,8 +24,25 @@ export default function transformProps(chartProps) {
selectCountry,
colorScheme,
sliceId,
+ customColorRules,
+ customColorScale,
} = formData;
+ let parsedColorRules = [];
+ let parsedColorScale = [];
+
+ try {
+ parsedColorRules = customColorRules ? JSON.parse(customColorRules) : [];
+ } catch (error) {
+ console.warn('Invalid JSON in customColorRules:', error);
+ }
+
+ try {
+ parsedColorScale = customColorScale ? JSON.parse(customColorScale) : [];
+ } catch (error) {
+ console.warn('Invalid JSON in customColorScale:', error);
Review Comment:
The `console.warn` messages for JSON parsing errors will only show generic
error messages. This makes it difficult for users to debug their JSON
configuration issues. Consider providing more specific error messages that
include what went wrong and where.
Example:
```javascript
console.warn('Invalid JSON in customColorRules. Please check your
configuration syntax:', error.message);
```
```suggestion
console.warn(
'Invalid JSON in customColorRules. Please check your configuration
syntax:',
error && error.message ? error.message : error
);
}
try {
parsedColorScale = customColorScale ? JSON.parse(customColorScale) : [];
} catch (error) {
console.warn(
'Invalid JSON in customColorScale. Please check your configuration
syntax:',
error && error.message ? error.message : error
);
```
##########
superset-frontend/plugins/legacy-plugin-chart-country-map/src/controlPanel.ts:
##########
@@ -69,6 +69,43 @@ const config: ControlPanelConfig = {
},
],
['linear_color_scheme'],
+ [
+ {
+ name: 'customColorScale',
+ config: {
+ type: 'TextAreaControl',
+ label: t('Custom Color Scale (by %)'),
+ description: t(
+ "Custom configuration that overrides the linear color scheme
color codes and thresholds. Thresholds are defined in percentage, and color
codes accept any valid CSS value. Copy-paste and edit following sample
configuration to define your own thresholds and colors :\n\n" +
+ "[\n" +
+ " { \"percent\": 0, \"color\": \"#600000\" },\n" +
+ " { \"percent\": 0.01, \"color\": \"#A00000\" },\n" +
+ " { \"percent\": 20, \"color\": \"#E52B50\" },\n" +
+ " { \"percent\": 35, \"color\": \"#FFA500\" },\n" +
+ " { \"percent\": 50, \"color\": \"#FFFF99\" },\n" +
+ " { \"percent\": 65, \"color\": \"#9ACD32\" },\n" +
+ " { \"percent\": 80, \"color\": \"#3CB371\" },\n" +
+ " { \"percent\": 99.99, \"color\": \"#228B22\" },\n" +
+ " { \"percent\": 100, \"color\": \"#006400\" }\n" +
+ "]"
+ ),
Review Comment:
[nitpick] The description string concatenation on lines 79-91 is hard to
read and maintain. Consider using a template literal instead:
```javascript
description: t(
`Custom configuration that overrides the linear color scheme color codes
and thresholds. Thresholds are defined in percentage, and color codes accept
any valid CSS value. Copy-paste and edit following sample configuration to
define your own thresholds and colors :
[
{ "percent": 0, "color": "#600000" },
{ "percent": 0.01, "color": "#A00000" },
...
]`
)
```
```suggestion
`Custom configuration that overrides the linear color scheme
color codes and thresholds. Thresholds are defined in percentage, and color
codes accept any valid CSS value. Copy-paste and edit following sample
configuration to define your own thresholds and colors :
[
{ "percent": 0, "color": "#600000" },
{ "percent": 0.01, "color": "#A00000" },
{ "percent": 20, "color": "#E52B50" },
{ "percent": 35, "color": "#FFA500" },
{ "percent": 50, "color": "#FFFF99" },
{ "percent": 65, "color": "#9ACD32" },
{ "percent": 80, "color": "#3CB371" },
{ "percent": 99.99, "color": "#228B22" },
{ "percent": 100, "color": "#006400" }
]`
),
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]