Skip to content

Commit 14cd5d5

Browse files
authored
fix(react-charting): Use schema colorscale for Grouped Vertical Bar chart (#34530)
1 parent 19a140f commit 14cd5d5

File tree

6 files changed

+124
-31
lines changed

6 files changed

+124
-31
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix(chart-utilities): Exporting ColorAxis type",
4+
"packageName": "@fluentui/chart-utilities",
5+
"email": "120183316+srmukher@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix(react-charting): Use colorscale for Grouped Vertical Bar chart",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "120183316+srmukher@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/chart-utilities/etc/chart-utilities.api.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,29 @@ export type Calendar = 'gregorian' | 'chinese' | 'coptic' | 'discworld' | 'ethio
166166
// @public (undocumented)
167167
export type Color = string | number | Array<string | number | undefined | null> | Array<Array<string | number | undefined | null>>;
168168

169+
// @public (undocumented)
170+
export interface ColorAxis {
171+
// (undocumented)
172+
cmax?: number;
173+
// (undocumented)
174+
cmin?: number;
175+
// (undocumented)
176+
colorbar?: {
177+
title?: string | {
178+
text: string;
179+
};
180+
thickness?: number;
181+
len?: number;
182+
outlinewidth?: number;
183+
};
184+
// (undocumented)
185+
colorscale?: Array<[number, string]>;
186+
// (undocumented)
187+
reversescale?: boolean;
188+
// (undocumented)
189+
showscale?: boolean;
190+
}
191+
169192
// @public (undocumented)
170193
export interface ColorBar {
171194
// (undocumented)

packages/charts/chart-utilities/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {
66
AxisType,
77
Calendar,
88
Color,
9+
ColorAxis,
910
ColorBar,
1011
ColorScale,
1112
Config,

packages/charts/react-charting/src/components/DeclarativeChart/PlotlySchemaAdapter.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,16 @@ export const transformPlotlyJsonToVSBCProps = (
404404
};
405405
};
406406

407+
const createColorScale = (colorscale: Array<[number, string]>, domain: [number, number]) => {
408+
const [dMin, dMax] = domain;
409+
410+
// Normalize colorscale domain to actual data domain
411+
const scaleDomain = colorscale.map(([pos]) => dMin + pos * (dMax - dMin));
412+
const scaleColors = colorscale.map(item => item[1]);
413+
414+
return d3ScaleLinear<string>().domain(scaleDomain).range(scaleColors);
415+
};
416+
407417
export const transformPlotlyJsonToGVBCProps = (
408418
input: PlotlySchema,
409419
colorMap: React.MutableRefObject<Map<string, string>>,
@@ -413,8 +423,22 @@ export const transformPlotlyJsonToGVBCProps = (
413423
const mapXToDataPoints: Record<string, IGroupedVerticalBarChartData> = {};
414424
const secondaryYAxisValues = getSecondaryYAxisValues(input.data, input.layout, 0, 0);
415425
const { legends, hideLegend } = getLegendProps(input.data, input.layout);
416-
426+
let colorScale: ((value: number) => string) | undefined = undefined;
417427
input.data.forEach((series: Partial<PlotData>, index1: number) => {
428+
if (
429+
input.layout?.coloraxis?.colorscale?.length &&
430+
isArrayOrTypedArray(series.marker?.color) &&
431+
(series.marker?.color as Color[]).length > 0 &&
432+
typeof (series.marker?.color as Color[])?.[0] === 'number'
433+
) {
434+
const scale = input.layout.coloraxis.colorscale as Array<[number, string]>;
435+
const colorValues = series.marker?.color as number[];
436+
const [dMin, dMax] = [
437+
input.layout.coloraxis?.cmin ?? Math.min(...colorValues),
438+
input.layout.coloraxis?.cmax ?? Math.max(...colorValues),
439+
];
440+
colorScale = createColorScale(scale, [dMin, dMax]);
441+
}
418442
// extract colors for each series only once
419443
const extractedColors = extractColor(
420444
input.layout?.template?.layout?.colorway,
@@ -434,8 +458,13 @@ export const transformPlotlyJsonToGVBCProps = (
434458

435459
if (series.type === 'bar') {
436460
const legend: string = legends[index1];
437-
// resolve color for each legend's bars from the extracted colors
438-
const color = resolveColor(extractedColors, index1, legend, colorMap, isDarkTheme);
461+
// resolve color for each legend's bars from the colorscale or extracted colors
462+
const color = colorScale
463+
? colorScale(
464+
isArrayOrTypedArray(series.marker?.color) ? ((series.marker?.color as Color[])?.[xIndex] as number) : 0,
465+
)
466+
: resolveColor(extractedColors, index1, legend, colorMap, isDarkTheme);
467+
439468
mapXToDataPoints[x].series.push({
440469
key: legend,
441470
data: series.y![xIndex] as number,

packages/charts/react-charting/src/components/GroupedVerticalBarChart/GroupedVerticalBarChart.base.tsx

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { max as d3Max, min as d3Min } from 'd3-array';
33
import { select as d3Select } from 'd3-selection';
44
import { Axis as D3Axis } from 'd3-axis';
5-
import { ScaleLinear, scaleBand as d3ScaleBand } from 'd3-scale';
5+
import { scaleLinear as d3ScaleLinear, ScaleLinear, scaleBand as d3ScaleBand } from 'd3-scale';
66
import {
77
classNamesFunction,
88
getId,
@@ -51,6 +51,7 @@ import {
5151
import { IChart, IImageExportOptions } from '../../types/index';
5252
import { toImage } from '../../utilities/image-export-utils';
5353
import { ILegendContainer } from '../Legends/index';
54+
import { rgb } from 'd3-color';
5455

5556
const COMPONENT_NAME = 'GROUPED VERTICAL BAR CHART';
5657
const getClassNames = classNamesFunction<IGroupedVerticalBarChartStyleProps, IGroupedVerticalBarChartStyles>();
@@ -437,21 +438,6 @@ export class GroupedVerticalBarChartBase
437438
const xPoint = xScale1(legendTitle) + (xScale1.bandwidth() - this._barWidth) / 2;
438439
const isLegendActive = this._legendHighlighted(legendTitle) || this._noLegendHighlighted();
439440
const barOpacity = isLegendActive ? '' : '0.1';
440-
const gradientId = getId('GVBC_Gradient') + `_${singleSet.indexNum}_${legendIndex}`;
441-
let startColor = barPoints[0].color;
442-
let endColor = startColor;
443-
444-
if (this.props.enableGradient) {
445-
startColor = barPoints[0].gradient![0];
446-
endColor = barPoints[0].gradient![1];
447-
448-
singleGroup.push(
449-
<linearGradient key={gradientId} id={gradientId} x1="0%" y1="100%" x2="0%" y2="0%">
450-
<stop offset="0" stopColor={startColor} />
451-
<stop offset="100%" stopColor={endColor} />
452-
</linearGradient>,
453-
);
454-
}
455441

456442
let barTotalValue = 0;
457443
const yBaseline = yBarScale(this.Y_ORIGIN);
@@ -464,8 +450,25 @@ export class GroupedVerticalBarChartBase
464450
// Not rendering data with 0.
465451
return;
466452
}
453+
const gradientId = getId('GVBC_Gradient') + `_${singleSet.indexNum}_${legendIndex}_${pointIndex}`;
454+
if (this.props.enableGradient) {
455+
const startColor = pointData.gradient?.[0] || pointData.color;
456+
const endColor = pointData.gradient?.[1] || pointData.color;
457+
458+
singleGroup.push(
459+
<defs key={`defs_${gradientId}`}>
460+
<linearGradient id={gradientId} x1="0%" y1="100%" x2="0%" y2="0%">
461+
<stop offset="0%" stopColor={startColor} />
462+
<stop offset="100%" stopColor={endColor} />
463+
</linearGradient>
464+
</defs>,
465+
);
466+
}
467+
467468
const barGap = (VERTICAL_BAR_GAP / 2) * (pointIndex > 0 ? 2 : 0);
468469
const height = Math.max(yBarScale(this.Y_ORIGIN) - yBarScale(Math.abs(pointData.data)), MIN_BAR_HEIGHT);
470+
const pointColor = pointData.color; // Use the color of the current point
471+
469472
if (pointData.data >= this.Y_ORIGIN) {
470473
yPositiveStart -= height + barGap;
471474
yPoint = yPositiveStart;
@@ -484,7 +487,7 @@ export class GroupedVerticalBarChartBase
484487
y={yPoint}
485488
data-is-focusable={!this.props.hideTooltip && isLegendActive}
486489
opacity={barOpacity}
487-
fill={this.props.enableGradient ? `url(#${gradientId})` : startColor}
490+
fill={this.props.enableGradient ? `url(#${gradientId})` : pointColor}
488491
rx={this.props.roundCorners ? 3 : 0}
489492
onMouseOver={this._onBarHover.bind(this, pointData, singleSet)}
490493
onMouseMove={this._onBarHover.bind(this, pointData, singleSet)}
@@ -782,6 +785,13 @@ export class GroupedVerticalBarChartBase
782785
);
783786
};
784787

788+
// Lighten/Darken a color by a given percentage using d3-scale
789+
private _adjustColor = (color: string, percentage: number, lightenColor: boolean, isDarkTheme: boolean): string => {
790+
const targetColor = lightenColor ? (isDarkTheme ? '#000000' : '#ffffff') : isDarkTheme ? '#ffffff' : '#000000';
791+
const colorInterpolator = d3ScaleLinear<string>().domain([0, 1]).range([color, targetColor]);
792+
return rgb(colorInterpolator(percentage)).formatRgb();
793+
};
794+
785795
private _addDefaultColors = (data?: IGroupedVerticalBarChartData[]): IGroupedVerticalBarChartData[] => {
786796
this._legendColorMap = {};
787797
let colorIndex = 0;
@@ -792,26 +802,42 @@ export class GroupedVerticalBarChartBase
792802
...point,
793803
series:
794804
point.series?.map(seriesPoint => {
795-
if (!this._legendColorMap[seriesPoint.legend]) {
796-
let startColor = seriesPoint.color
797-
? seriesPoint.color
798-
: getNextColor(colorIndex, 0, this.props.theme?.isInverted);
799-
let endColor = startColor;
800-
801-
if (this.props.enableGradient) {
805+
let startColor = seriesPoint.color
806+
? seriesPoint.color
807+
: getNextColor(colorIndex, 0, this.props.theme?.isInverted);
808+
let endColor = startColor;
809+
810+
if (this.props.enableGradient) {
811+
if (seriesPoint.color) {
812+
// Generate gradient colors based on seriesPoint.color
813+
startColor = this._adjustColor(
814+
seriesPoint.color || endColor,
815+
0.2,
816+
false,
817+
this.props.theme?.isInverted!,
818+
);
819+
endColor = this._adjustColor(
820+
seriesPoint.color || startColor,
821+
0.2,
822+
true,
823+
this.props.theme?.isInverted!,
824+
);
825+
} else {
802826
const nextGradient = getNextGradient(colorIndex, 0, this.props.theme?.isInverted);
803827
startColor = seriesPoint.gradient?.[0] || nextGradient[0];
804828
endColor = seriesPoint.gradient?.[1] || nextGradient[1];
805829
}
806-
830+
}
831+
const pointGradient: [string, string] = [startColor, endColor];
832+
if (!this._legendColorMap[seriesPoint.legend]) {
807833
this._legendColorMap[seriesPoint.legend] = [startColor, endColor];
808-
colorIndex += 1;
809834
}
835+
colorIndex += 1;
810836

811837
return {
812838
...seriesPoint,
813-
color: this._legendColorMap[seriesPoint.legend][0],
814-
...(this.props.enableGradient ? { gradient: this._legendColorMap[seriesPoint.legend] } : {}),
839+
color: seriesPoint.color ?? this._legendColorMap[seriesPoint.legend][0],
840+
...(this.props.enableGradient ? { gradient: pointGradient } : {}),
815841
};
816842
}) ?? [],
817843
};

0 commit comments

Comments
 (0)