Skip to content

Commit 598962e

Browse files
authored
feat(react-charting): Support additional modes (#34860)
1 parent 6f18258 commit 598962e

File tree

11 files changed

+124
-61
lines changed

11 files changed

+124
-61
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": "feat(chart-utilities): Support additional modes",
4+
"packageName": "@fluentui/chart-utilities",
5+
"email": "[email protected]",
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": "feat(react-charting): Support additional modes",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1265,7 +1265,7 @@ export interface PlotData {
12651265
// (undocumented)
12661266
marker: Partial<PlotMarker>;
12671267
// (undocumented)
1268-
mode: 'lines' | 'markers' | 'text' | 'lines+markers' | 'text+markers' | 'text+lines' | 'text+lines+markers' | 'none' | 'gauge' | 'number' | 'delta' | 'number+delta' | 'gauge+number' | 'gauge+number+delta' | 'gauge+delta' | 'markers+text' | 'lines+text';
1268+
mode: 'lines' | 'markers' | 'text' | 'lines+markers' | 'text+markers' | 'text+lines' | 'text+lines+markers' | 'none' | 'gauge' | 'number' | 'delta' | 'number+delta' | 'gauge+number' | 'gauge+number+delta' | 'gauge+delta' | 'markers+text' | 'lines+text' | 'lines+markers+text';
12691269
// (undocumented)
12701270
name: string;
12711271
// (undocumented)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1174,7 +1174,8 @@ export interface PlotData {
11741174
| 'gauge+number+delta'
11751175
| 'gauge+delta'
11761176
| 'markers+text'
1177-
| 'lines+text';
1177+
| 'lines+text'
1178+
| 'lines+markers+text';
11781179
histfunc: 'count' | 'sum' | 'avg' | 'min' | 'max';
11791180
histnorm: '' | 'percent' | 'probability' | 'density' | 'probability density';
11801181
hoveron: 'points' | 'fills';

packages/charts/chart-utilities/src/PlotlySchemaConverter.UT.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,24 @@ describe('mapFluentChart UTs', () => {
660660
expect(result.type).toBe('line');
661661
});
662662

663+
test('scatter chart mapping - lines+markers+text', () => {
664+
const input = {
665+
data: [{ type: 'scatter', mode: 'lines+markers+text', x: [1, 2, 3], y: [4, 5, 6] }],
666+
};
667+
const result = mapFluentChart(input);
668+
expect(result.isValid).toBe(true);
669+
expect(result.type).toBe('line');
670+
});
671+
672+
test('scatter chart mapping - text', () => {
673+
const input = {
674+
data: [{ type: 'scatter', mode: 'text', x: [1, 2, 3], y: [4, 5, 6] }],
675+
};
676+
const result = mapFluentChart(input);
677+
expect(result.isValid).toBe(true);
678+
expect(result.type).toBe('scatter');
679+
});
680+
663681
test('histogram chart mapping', () => {
664682
const input = {
665683
data: [{ type: 'histogram', x: [1, 2, 3, 4, 5] }],

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ const validateBarData = (data: Partial<PlotData>) => {
242242
}
243243
};
244244
const isScatterMarkers = (mode: string): boolean => {
245-
return ['markers', 'text+markers', 'markers+text'].includes(mode);
245+
return ['markers', 'text+markers', 'markers+text', 'text'].includes(mode);
246246
};
247247

248248
const validateScatterData = (data: Partial<PlotData>) => {
@@ -258,7 +258,15 @@ const validateScatterData = (data: Partial<PlotData>) => {
258258
throw new Error(`${UNSUPPORTED_MSG_PREFIX} ${data.type}, mode: ${mode}, yAxisType: ${yAxisType}`);
259259
}
260260
} else if (
261-
['lines+markers', 'markers+lines', 'text+lines+markers', 'lines', 'text+lines', 'lines+text'].includes(mode)
261+
[
262+
'lines+markers',
263+
'markers+lines',
264+
'text+lines+markers',
265+
'lines',
266+
'text+lines',
267+
'lines+text',
268+
'lines+markers+text',
269+
].includes(mode)
262270
) {
263271
if (!isNumberArray(data.x) && !isStringArray(data.x) && !isDateArray(data.x)) {
264272
throw new Error(`${UNSUPPORTED_MSG_PREFIX} ${data.type}, mode: ${mode}, xAxisType: ${xAxisType}`);

packages/charts/react-charting/etc/react-charting.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,7 @@ export interface ILineChartLineOptions extends React_2.SVGProps<SVGPathElement>
11381138
curve?: 'linear' | 'natural' | 'step' | 'stepAfter' | 'stepBefore' | CurveFactory;
11391139
lineBorderColor?: string;
11401140
lineBorderWidth?: string | number;
1141-
mode?: 'lines' | 'markers' | 'text' | 'lines+markers' | 'text+markers' | 'text+lines' | 'text+lines+markers' | 'none' | 'gauge' | 'number' | 'delta' | 'number+delta' | 'gauge+number' | 'gauge+number+delta' | 'gauge+delta' | 'markers+text' | 'lines+text';
1141+
mode?: 'lines' | 'markers' | 'text' | 'lines+markers' | 'text+markers' | 'text+lines' | 'text+lines+markers' | 'none' | 'gauge' | 'number' | 'delta' | 'number+delta' | 'gauge+number' | 'gauge+number+delta' | 'gauge+delta' | 'markers+text' | 'lines+text' | 'lines+markers+text';
11421142
strokeDasharray?: string | number;
11431143
strokeDashoffset?: string | number;
11441144
strokeLinecap?: 'butt' | 'round' | 'square' | 'inherit';

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,12 +779,14 @@ const transformPlotlyJsonToScatterTraceProps = (
779779
isDarkTheme?: boolean,
780780
): ILineChartProps | IAreaChartProps | IScatterChartProps => {
781781
const isScatterMarkers = [
782+
'text',
782783
'markers',
783784
'text+markers',
784785
'markers+text',
785786
'lines+markers',
786787
'markers+line',
787788
'text+lines+markers',
789+
'lines+markers+text',
788790
].includes((input.data[0] as PlotData)?.mode);
789791
const isAreaChart = chartType === 'area';
790792
const isScatterChart = chartType === 'scatter';

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

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
domainRangeOfXStringAxis,
1414
findNumericMinMaxOfY,
1515
isScatterPolarSeries,
16+
isTextMode,
1617
YAxisType,
1718
} from '../../utilities/index';
1819
import {
@@ -93,6 +94,7 @@ export const ScatterChartBase: React.FunctionComponent<IScatterChartProps> = Rea
9394
const [refSelected, setRefSelected] = React.useState<string>('');
9495
const prevSelectedLegendsRef = React.useRef<string[] | undefined>(undefined);
9596
const _isScatterPolarRef = React.useRef(false);
97+
const _isTextMode = React.useRef(false);
9698

9799
const classNames = getClassNames(props.styles!, {
98100
theme: props.theme!,
@@ -491,60 +493,64 @@ export const ScatterChartBase: React.FunctionComponent<IScatterChartProps> = Rea
491493

492494
const currentPointHidden = _points.current?.[i]?.hideNonActiveDots && activePoint !== circleId;
493495
const text = _points.current?.[i].data[j]?.text;
494-
pointsForSeries.push(
495-
<>
496-
<circle
497-
id={circleId}
498-
key={circleId}
499-
r={Math.max(circleRadius, 4)}
500-
cx={_xAxisScale.current?.(x) + _xBandwidth.current}
501-
cy={_yAxisScale.current?.(y)}
502-
data-is-focusable={isLegendSelected}
503-
onMouseOver={(event: React.MouseEvent<SVGElement>) =>
504-
_handleHover(
505-
x,
506-
y,
507-
verticaLineHeight,
508-
xAxisCalloutData,
509-
circleId,
510-
xAxisCalloutAccessibilityData,
511-
event,
512-
)
513-
}
514-
onMouseMove={(event: React.MouseEvent<SVGElement>) =>
515-
_handleHover(
516-
x,
517-
y,
518-
verticaLineHeight,
519-
xAxisCalloutData,
520-
circleId,
521-
xAxisCalloutAccessibilityData,
522-
event,
523-
)
524-
}
525-
onMouseOut={_handleMouseOut}
526-
onFocus={() => _handleFocus(seriesId, x, xAxisCalloutData, circleId, xAxisCalloutAccessibilityData)}
527-
onBlur={_handleMouseOut}
528-
{..._getClickHandler(_points.current?.[i]?.data[j]?.onDataPointClick)}
529-
opacity={isLegendSelected && !currentPointHidden ? 1 : 0.1}
530-
fill={_getPointFill(seriesColor, circleId)}
531-
stroke={seriesColor}
532-
role="img"
533-
aria-label={_getAriaLabel(i, j)}
534-
tabIndex={_points.current?.[i]?.legend !== '' ? 0 : undefined}
535-
/>
536-
{!_isScatterPolarRef.current && text && (
537-
<text
538-
key={`${circleId}-label`}
539-
x={_xAxisScale.current?.(x) + _xBandwidth.current}
540-
y={_yAxisScale.current?.(y) + Math.max(circleRadius + 12, 16)}
541-
className={classNames.markerLabel}
542-
>
543-
{text}
544-
</text>
545-
)}
546-
</>,
547-
);
496+
if (!_isTextMode.current) {
497+
pointsForSeries.push(
498+
<>
499+
<circle
500+
id={circleId}
501+
key={circleId}
502+
r={Math.max(circleRadius, 4)}
503+
cx={_xAxisScale.current?.(x) + _xBandwidth.current}
504+
cy={_yAxisScale.current?.(y)}
505+
data-is-focusable={isLegendSelected}
506+
onMouseOver={(event: React.MouseEvent<SVGElement>) =>
507+
_handleHover(
508+
x,
509+
y,
510+
verticaLineHeight,
511+
xAxisCalloutData,
512+
circleId,
513+
xAxisCalloutAccessibilityData,
514+
event,
515+
)
516+
}
517+
onMouseMove={(event: React.MouseEvent<SVGElement>) =>
518+
_handleHover(
519+
x,
520+
y,
521+
verticaLineHeight,
522+
xAxisCalloutData,
523+
circleId,
524+
xAxisCalloutAccessibilityData,
525+
event,
526+
)
527+
}
528+
onMouseOut={_handleMouseOut}
529+
onFocus={() => _handleFocus(seriesId, x, xAxisCalloutData, circleId, xAxisCalloutAccessibilityData)}
530+
onBlur={_handleMouseOut}
531+
{..._getClickHandler(_points.current?.[i]?.data[j]?.onDataPointClick)}
532+
opacity={isLegendSelected && !currentPointHidden ? 1 : 0.1}
533+
fill={_getPointFill(seriesColor, circleId)}
534+
stroke={seriesColor}
535+
role="img"
536+
aria-label={_getAriaLabel(i, j)}
537+
tabIndex={_points.current?.[i]?.legend !== '' ? 0 : undefined}
538+
/>
539+
</>,
540+
);
541+
}
542+
if ((!_isScatterPolarRef.current || _isTextMode) && text) {
543+
pointsForSeries.push(
544+
<text
545+
key={`${circleId}-label`}
546+
x={_xAxisScale.current?.(x) + _xBandwidth.current}
547+
y={_yAxisScale.current?.(y) + Math.max(circleRadius + 12, 16)}
548+
className={classNames.markerLabel}
549+
>
550+
{text}
551+
</text>,
552+
);
553+
}
548554
}
549555

550556
if (_isScatterPolarRef.current) {
@@ -636,6 +642,7 @@ export const ScatterChartBase: React.FunctionComponent<IScatterChartProps> = Rea
636642
_xAxisScale.current = xScale;
637643
_yAxisScale.current = yScale;
638644
_isScatterPolarRef.current = isScatterPolarSeries(_points.current);
645+
_isTextMode.current = isTextMode(_points.current);
639646
renderSeries.current = _createPlot(xElement!, containerHeight!);
640647
},
641648
[renderSeries, _xAxisScale, _yAxisScale, _createPlot],
@@ -706,7 +713,7 @@ export const ScatterChartBase: React.FunctionComponent<IScatterChartProps> = Rea
706713
// reduce computation cost by only creating legendBars
707714
// if when hideLegend is false.
708715
// NOTE: they are rendered only when hideLegend is false in CartesianChart.
709-
if (!props.hideLegend) {
716+
if (!props.hideLegend && !_isTextMode.current) {
710717
legendBars = _createLegends(_points.current!); // ToDo: Memoize legends to improve performance.
711718
}
712719
const calloutProps = {

packages/charts/react-charting/src/types/IDataPoint.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,8 @@ export interface ILineChartLineOptions extends React.SVGProps<SVGPathElement> {
440440
| 'gauge+number+delta'
441441
| 'gauge+delta'
442442
| 'markers+text'
443-
| 'lines+text';
443+
| 'lines+text'
444+
| 'lines+markers+text';
444445
}
445446

446447
/**

0 commit comments

Comments
 (0)