Skip to content

Commit

Permalink
[REF] color: simplify color scale usage
Browse files Browse the repository at this point in the history
This commit simplifies the use of color scales in the conditional format
plugin and export the color scales to an helper.

Now we have a simple `getColorScale` helper, that take as argument
a number of value/color pairs (the thresholds) and return a function
that computes a color based on a value and the provided value/color
pairs.

All the complexity of the color scale computation is now hidden in
the helper, which simplify the use of color scale a lot and allows us
to use them in other places.

Task: 3265268
Part-of: #5096
Signed-off-by: Lucas Lefèvre (lul) <[email protected]>
  • Loading branch information
hokolomopo authored and LucasLefevre committed Dec 11, 2024
1 parent a6bb4c9 commit 1fca760
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
GRAY_200,
GRAY_300,
} from "../../../../constants";
import { colorNumberString, isColorValid, rangeReference } from "../../../../helpers";
import {
colorNumberString,
colorToNumber,
isColorValid,
rangeReference,
} from "../../../../helpers";
import { canonicalizeCFRule } from "../../../../helpers/locale";
import { cycleFixedReference } from "../../../../helpers/reference_type";
import { _t } from "../../../../translation";
Expand Down Expand Up @@ -441,7 +446,7 @@ export class ConditionalFormattingEditor extends Component<Props, SpreadsheetChi

const point = this.state.rules.colorScale[target];
if (point) {
point.color = Number.parseInt(color.slice(1), 16);
point.color = colorToNumber(color);
}
this.updateConditionalFormat({ rule: this.state.rules.colorScale });
this.closeMenus();
Expand Down
90 changes: 90 additions & 0 deletions src/helpers/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export function colorNumberString(color: number): Color {
return toHex(color.toString(16).padStart(6, "0"));
}

export function colorToNumber(color: Color | number): number {
if (typeof color === "number") {
return color;
}
return Number.parseInt(toHex(color).slice(1), 16);
}

/**
* Converts any CSS color value to a standardized hex6 value.
* Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
Expand Down Expand Up @@ -554,3 +561,86 @@ export class AlternatingColorGenerator extends ColorGenerator {
);
}
}

type ColorScaleThreshold = {
min: number;
max: number;
minColor: number;
maxColor: number;
colorDiff: [number, number, number];
};

/**
* Returns a function that maps a value to a color using a color scale defined by the given
* color/threshold values pairs.
*/
export function getColorScale(
colorScalePoints: { value: number; color: number | Color }[]
): (value: number) => Color {
if (colorScalePoints.length < 2) {
throw new Error("Color scale must have at least 2 points");
}
const sortedColorScalePoints = [...colorScalePoints.sort((a, b) => a.value - b.value)];
const thresholds: ColorScaleThreshold[] = [];
for (let i = 1; i < sortedColorScalePoints.length; i++) {
const minColor = colorToNumber(sortedColorScalePoints[i - 1].color);
const maxColor = colorToNumber(sortedColorScalePoints[i].color);
thresholds.push({
min: sortedColorScalePoints[i - 1].value,
max: sortedColorScalePoints[i].value,
minColor,
maxColor,
colorDiff: computeColorDiffUnits(
sortedColorScalePoints[i - 1].value,
sortedColorScalePoints[i].value,
minColor,
maxColor
),
});
}

return (value: number) => {
if (value < thresholds[0].min) {
return colorNumberString(thresholds[0].minColor);
}
for (const threshold of thresholds) {
if (value >= threshold.min && value <= threshold.max) {
return colorNumberString(
colorCell(value, threshold.min, threshold.minColor, threshold.colorDiff)
);
}
}
return colorNumberString(thresholds[thresholds.length - 1].maxColor);
};
}

function computeColorDiffUnits(
minValue: number,
maxValue: number,
minColor: number,
maxColor: number
): [number, number, number] {
const deltaValue = maxValue - minValue;

const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
const deltaColorB = (minColor % 256) - (maxColor % 256);

const colorDiffUnitR = deltaColorR / deltaValue;
const colorDiffUnitG = deltaColorG / deltaValue;
const colorDiffUnitB = deltaColorB / deltaValue;
return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
}

function colorCell(
value: number,
minValue: number,
minColor: number,
colorDiffUnit: [number, number, number]
) {
const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
return (r << 16) | (g << 8) | b;
}
99 changes: 6 additions & 93 deletions src/plugins/ui_core_views/evaluation_conditional_format.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { compile } from "../../formulas";
import { parseLiteral } from "../../helpers/cells";
import { colorNumberString, isInside, percentile } from "../../helpers/index";
import { colorNumberString, getColorScale, isInside, percentile } from "../../helpers/index";
import { clip, largeMax, largeMin, lazy } from "../../helpers/misc";
import { _t } from "../../translation";
import {
Expand Down Expand Up @@ -338,112 +338,25 @@ export class EvaluationConditionalFormatPlugin extends UIPlugin {
return;
}
const zone: Zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
const colorCellArgs: {
minValue: number;
minColor: number;
colorDiffUnit: [number, number, number];
}[] = [];
const colorThresholds = [{ value: minValue, color: rule.minimum.color }];
if (rule.midpoint && midValue) {
colorCellArgs.push({
minValue,
minColor: rule.minimum.color,
colorDiffUnit: this.computeColorDiffUnits(
minValue,
midValue,
rule.minimum.color,
rule.midpoint.color
),
});
colorCellArgs.push({
minValue: midValue,
minColor: rule.midpoint.color,
colorDiffUnit: this.computeColorDiffUnits(
midValue,
maxValue,
rule.midpoint.color,
rule.maximum.color
),
});
} else {
colorCellArgs.push({
minValue,
minColor: rule.minimum.color,
colorDiffUnit: this.computeColorDiffUnits(
minValue,
maxValue,
rule.minimum.color,
rule.maximum.color
),
});
colorThresholds.push({ value: midValue, color: rule.midpoint.color });
}
colorThresholds.push({ value: maxValue, color: rule.maximum.color });
const colorScale = getColorScale(colorThresholds);
for (let row = zone.top; row <= zone.bottom; row++) {
for (let col = zone.left; col <= zone.right; col++) {
const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
if (cell.type === CellValueType.number) {
const value = clip(cell.value, minValue, maxValue);
let color;
if (colorCellArgs.length === 2 && midValue) {
color =
value <= midValue
? this.colorCell(
value,
colorCellArgs[0].minValue,
colorCellArgs[0].minColor,
colorCellArgs[0].colorDiffUnit
)
: this.colorCell(
value,
colorCellArgs[1].minValue,
colorCellArgs[1].minColor,
colorCellArgs[1].colorDiffUnit
);
} else {
color = this.colorCell(
value,
colorCellArgs[0].minValue,
colorCellArgs[0].minColor,
colorCellArgs[0].colorDiffUnit
);
}
if (!computedStyle[col]) computedStyle[col] = [];
computedStyle[col][row] = computedStyle[col]?.[row] || {};
computedStyle[col][row]!.fillColor = colorNumberString(color);
computedStyle[col][row]!.fillColor = colorScale(value);
}
}
}
}

private computeColorDiffUnits(
minValue: number,
maxValue: number,
minColor: number,
maxColor: number
): [number, number, number] {
const deltaValue = maxValue - minValue;

const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
const deltaColorB = (minColor % 256) - (maxColor % 256);

const colorDiffUnitR = deltaColorR / deltaValue;
const colorDiffUnitG = deltaColorG / deltaValue;
const colorDiffUnitB = deltaColorB / deltaValue;
return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
}

private colorCell(
value: number,
minValue: number,
minColor: number,
colorDiffUnit: [number, number, number]
) {
const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
return (r << 16) | (g << 8) | b;
}

/**
* Execute the predicate to know if a conditional formatting rule should be applied to a cell
*/
Expand Down

0 comments on commit 1fca760

Please sign in to comment.