Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ export interface ArcSeriesStyle {

// @public (undocumented)
export interface ArcStyle {
dimmed?: {
opacity: number;
} | {
fill: Color | ColorVariant;
stroke?: Color | ColorVariant;
};
fill?: Color | ColorVariant;
opacity: number;
stroke?: Color | ColorVariant;
Expand Down Expand Up @@ -2644,6 +2650,14 @@ export interface RectBorderStyle {

// @public (undocumented)
export interface RectStyle {
dimmed?: {
opacity: number;
} | {
fill: Color | ColorVariant;
texture?: {
opacity: number;
};
};
fill?: Color | ColorVariant;
opacity: number;
texture?: TexturedStyles;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
* Side Public License, v 1.
*/

import type { AnimationState } from './partition';
import { colorToRgba, RGBATupleToString } from '../../../../common/color_library_wrappers';
import type { Color } from '../../../../common/colors';
import { TAU } from '../../../../common/constants';
import type { Pixels } from '../../../../common/geometry';
import { cssFontShorthand, HorizontalAlignment } from '../../../../common/text_utils';
import { renderLayers, withContext } from '../../../../renderers/canvas';
import { MIN_STROKE_WIDTH } from '../../../../renderers/canvas/primitives/line';
import type { LegendPath } from '../../../../state/actions/legend';
import type { ArcSeriesStyle } from '../../../../utils/themes/theme';
import type {
LinkLabelVM,
OutsideLinksViewModel,
Expand All @@ -22,6 +25,8 @@ import type {
ShapeViewModel,
TextRow,
} from '../../layout/types/viewmodel_types';
import type { LegendStrategy } from '../../layout/utils/highlighted_geoms';
import { highlightedGeoms } from '../../layout/utils/highlighted_geoms';
import type { LinkLabelsViewModelSpec } from '../../layout/viewmodel/link_text_layout';
import { isSunburst } from '../../layout/viewmodel/viewmodel';

Expand Down Expand Up @@ -149,22 +154,53 @@ function renderTaperedBorder(
}
}

function renderSectors(ctx: CanvasRenderingContext2D, quadViewModel: QuadViewModel[]) {
function renderSectors(
ctx: CanvasRenderingContext2D,
quadViewModel: QuadViewModel[],
highlightedQuadSet: Set<QuadViewModel>,
arcSeriesStyle: ArcSeriesStyle,
) {
withContext(ctx, () => {
ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y
quadViewModel.forEach((quad: QuadViewModel) => {
if (quad.x0 !== quad.x1) renderTaperedBorder(ctx, quad);
// Apply dimmed colors for unhighlighted quads
const isUnhighlighted = highlightedQuadSet.size > 0 && !highlightedQuadSet.has(quad);
const dimmed = arcSeriesStyle.arc.dimmed;
const dimmedFill = dimmed && 'fill' in dimmed ? dimmed.fill : undefined;
const useDimmedColor = isUnhighlighted && dimmedFill;
Comment on lines +167 to +170
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract this into a function and reuse it?
Probably we have probably a function that is used everytime we need to pick up the a dimmed or non-dimmed color and reuse it elsewhere also on area/line/point charts


if (useDimmedColor) {
// Temporarily override fillColor for dimmed state
const originalFillColor = quad.fillColor;
quad.fillColor = dimmedFill;
if (quad.x0 !== quad.x1) renderTaperedBorder(ctx, quad);
quad.fillColor = originalFillColor; // Restore
Comment on lines +175 to +177
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we need to update the color just for that call, I prefer instead passing just the required properties (in case also by revisiting the renderTaperedBorder arguments to accept just the required props and passing them, rather then modifing and restore that value.

} else {
if (quad.x0 !== quad.x1) renderTaperedBorder(ctx, quad);
}
});
});
}

function renderRectangles(ctx: CanvasRenderingContext2D, quadViewModel: QuadViewModel[]) {
function renderRectangles(
ctx: CanvasRenderingContext2D,
quadViewModel: QuadViewModel[],
highlightedQuadSet: Set<QuadViewModel>,
arcSeriesStyle: ArcSeriesStyle,
) {
withContext(ctx, () => {
ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y
quadViewModel.forEach(({ strokeWidth, fillColor, x0, x1, y0px, y1px }) => {
quadViewModel.forEach((quad) => {
const { strokeWidth, fillColor, x0, x1, y0px, y1px } = quad;
// only draw a shape if it would show up at all
if (x1 - x0 >= 1 && y1px - y0px >= 1) {
ctx.fillStyle = fillColor;
// Apply dimmed colors for unhighlighted quads
const isUnhighlighted = highlightedQuadSet.size > 0 && !highlightedQuadSet.has(quad);
const dimmed = arcSeriesStyle.arc.dimmed;
const dimmedFill = dimmed && 'fill' in dimmed ? dimmed.fill : undefined;
const useDimmedColor = isUnhighlighted && dimmedFill;

ctx.fillStyle = useDimmedColor ? dimmedFill : fillColor;
ctx.beginPath();
ctx.moveTo(x0, y0px);
ctx.lineTo(x0, y1px);
Expand Down Expand Up @@ -277,6 +313,12 @@ export function renderPartitionCanvas2d(
panel,
chartDimensions,
}: ShapeViewModel,
_focus: unknown,
_animationState: AnimationState,
Comment on lines +316 to +317
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see where this function get called, we probably need to fix there and call the relative functions with just the minimal amount of arguments, living these arguments there doesn't have much sense

highlightedLegendPath: LegendPath,
legendStrategy: LegendStrategy | undefined,
flatLegend: boolean | undefined,
arcSeriesStyle: ArcSeriesStyle,
) {
const { sectorLineWidth, sectorLineStroke, linkLabel } = style;

Expand Down Expand Up @@ -322,13 +364,24 @@ export function renderPartitionCanvas2d(
ctx.strokeStyle = sectorLineStroke;
ctx.lineWidth = sectorLineWidth;

// Calculate which quads are highlighted for legend dimming
const highlightedQuadSet = new Set<QuadViewModel>();
if (highlightedLegendPath.length > 0) {
// Use highlightedGeoms to determine which quads match the legend path
const highlighted = highlightedGeoms(legendStrategy, flatLegend, quadViewModel, highlightedLegendPath);
highlighted.forEach((quad) => highlightedQuadSet.add(quad));
}

// painter's algorithm, like that of SVG: the sequence determines what overdraws what; first element of the array is drawn first
// (of course, with SVG, it's for ambiguous situations only, eg. when 3D transforms with different Z values aren't used, but
// unlike SVG and esp. WebGL, Canvas2d doesn't support the 3rd dimension well, see ctx.transform / ctx.setTransform).
// The layers are callbacks, because of the need to not bake in the `ctx`, it feels more composable and uncoupled this way.
renderLayers(ctx, [
// bottom layer: sectors (pie slices, ring sectors etc.)
() => (isSunburst(layout) ? renderSectors(ctx, quadViewModel) : renderRectangles(ctx, quadViewModel)),
() =>
isSunburst(layout)
? renderSectors(ctx, quadViewModel, highlightedQuadSet, arcSeriesStyle)
: renderRectangles(ctx, quadViewModel, highlightedQuadSet, arcSeriesStyle),

// all the fill-based, potentially multirow text, whether inside or outside the sector
() => renderRowSets(ctx, rowSets, linkLineColor),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ScreenReaderSummary } from '../../../../components/accessibility';
import { clearCanvas } from '../../../../renderers/canvas';
import type { SettingsSpec } from '../../../../specs/settings';
import { onChartRendered } from '../../../../state/actions/chart';
import type { LegendPath } from '../../../../state/actions/legend';
import type { GlobalChartState } from '../../../../state/chart_state';
import type { A11ySettings } from '../../../../state/selectors/get_accessibility_config';
import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config';
Expand All @@ -29,6 +30,7 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import type { Dimensions } from '../../../../utils/dimensions';
import type { ArcSeriesStyle } from '../../../../utils/themes/theme';
import { MODEL_KEY } from '../../layout/config';
import type { QuadViewModel, ShapeViewModel, SmallMultiplesDescriptors } from '../../layout/types/viewmodel_types';
import { hasMostlyRTLLabels, nullShapeViewModel } from '../../layout/types/viewmodel_types';
Expand Down Expand Up @@ -58,6 +60,10 @@ interface ReactiveChartStateProps {
a11ySettings: A11ySettings;
debug: SettingsSpec['debug'];
background: Color;
highlightedLegendPath: LegendPath;
legendStrategy: SettingsSpec['legendStrategy'];
flatLegend: SettingsSpec['flatLegend'];
arcSeriesStyle: ArcSeriesStyle;
}

interface ReactiveChartDispatchProps {
Expand Down Expand Up @@ -187,7 +193,17 @@ class PartitionComponent extends React.Component<PartitionProps> {
: isWaffle(geometries.layout)
? renderWrappedPartitionCanvas2d
: renderPartitionCanvas2d;
renderer(ctx, devicePixelRatio, geometries, focus, this.animationState);
renderer(
ctx,
devicePixelRatio,
geometries,
focus,
this.animationState,
props.highlightedLegendPath,
props.legendStrategy,
props.flatLegend,
props.arcSeriesStyle,
);
});
}
}
Expand Down Expand Up @@ -216,13 +232,19 @@ const DEFAULT_PROPS: ReactiveChartStateProps = {
a11ySettings: DEFAULT_A11Y_SETTINGS,
debug: false,
background: Colors.Transparent.keyword,
highlightedLegendPath: [],
legendStrategy: undefined,
flatLegend: undefined,
arcSeriesStyle: { arc: { visible: true, stroke: Colors.Black.keyword, strokeWidth: 1, opacity: 1 } },
};

const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) {
return DEFAULT_PROPS;
}
const multiGeometries = partitionMultiGeometries(state);
const settings = getSettingsSpecSelector(state);
const theme = getChartThemeSelector(state);

return {
isRTL: hasMostlyRTLLabels(multiGeometries),
Expand All @@ -232,8 +254,12 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
chartDimensions: getChartContainerDimensionsSelector(state),
geometriesFoci: partitionDrilldownFocus(state),
a11ySettings: getA11ySettingsSelector(state),
debug: getSettingsSpecSelector(state).debug,
background: getChartThemeSelector(state).background.color,
debug: settings.debug,
background: theme.background.color,
highlightedLegendPath: state.interactions.highlightedLegendPath,
legendStrategy: settings.legendStrategy,
flatLegend: settings.flatLegend,
arcSeriesStyle: theme.arcSeriesStyle,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import type { RefObject } from 'react';
import React from 'react';

import { HighlighterFromHover } from './highlighter_hover';
import { HighlighterFromLegend } from './highlighter_legend';
import { Tooltip } from '../../../../components/tooltip/tooltip';
import type { BackwardRef, ChartRenderer } from '../../../../state/internal_chart_renderer';
import { Partition } from '../canvas/partition';

/** @internal */
/**
* Partition chart renderer
* - HighlighterFromHover: SVG overlay for direct slice hover (immediate visual feedback)
* - Canvas dimming: Used for legend hover (consistent with bar/line/area charts)
* @internal
*/
export const chartRenderer: ChartRenderer = (
containerRef: BackwardRef,
forwardStageRef: RefObject<HTMLCanvasElement>,
Expand All @@ -24,6 +28,5 @@ export const chartRenderer: ChartRenderer = (
<Tooltip getChartContainerRef={containerRef} />
<Partition forwardStageRef={forwardStageRef} />
<HighlighterFromHover />
<HighlighterFromLegend />
</>
);
11 changes: 10 additions & 1 deletion packages/charts/src/chart_types/xy_chart/renderer/canvas/bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,16 @@ export function renderBars(
const { x, y, width, height, color, seriesStyle: style, seriesIdentifier } = barGeometry;
const rect = { x, y, width, height };
const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem);
const barStyle = buildBarStyle(ctx, imgCanvas, color, style.rect, style.rectBorder, geometryStateStyle, rect);
const barStyle = buildBarStyle(
ctx,
imgCanvas,
color,
style.rect,
style.rectBorder,
geometryStateStyle,
sharedStyle,
rect,
);
renderRect(ctx, rect, barStyle.fill, barStyle.stroke);
}),
{ area: getPanelClipping(panel, rotation), shouldClip: true },
Expand Down
Loading