Skip to content

Commit

Permalink
Merge pull request #3324 from ag-grid/AG-13216/gauges_pickFocus
Browse files Browse the repository at this point in the history
AG-13216 Gauges `pickFocus` implementation
  • Loading branch information
olegat authored Jan 7, 2025
2 parents 4329d61 + 17d3ae6 commit e9bce73
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 78 deletions.
18 changes: 11 additions & 7 deletions packages/ag-charts-community/src/chart/series/seriesAreaManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,8 @@ export class SeriesAreaManager extends BaseManager {
}

private handleSeriesFocus(otherIndexDelta: number, datumIndexDelta: number, refresh = false) {
if (this.chart.chartType === 'hierarchy') {
this.handleHierarchySeriesFocus(otherIndexDelta, datumIndexDelta, refresh);
if (this.chart.chartType === 'hierarchy' || this.chart.chartType === 'gauge') {
this.handleSoloSeriesFocus(otherIndexDelta, datumIndexDelta, refresh);
return;
}
const { focus, seriesRect } = this;
Expand All @@ -457,10 +457,11 @@ export class SeriesAreaManager extends BaseManager {
this.updatePickedFocus(pick, refresh);
}

private handleHierarchySeriesFocus(otherIndexDelta: number, datumIndexDelta: number, refresh: boolean) {
// Hierarchial charts (treemap, sunburst) can only have 1 series. So we'll repurpose the focus.seriesIndex
// value to control the focused depth. This allows the hierarchial charts to piggy-back on the base keyboard
// handling implementation.
private handleSoloSeriesFocus(otherIndexDelta: number, datumIndexDelta: number, refresh: boolean) {
// Some chart type (treemap, sunburst, gauges) can only have 1 series. So we'll repurpose the focus.seriesIndex
// value. Hierarchial charts use arrowup/down to change depth and gauges use arrowup/down to change datum type
// (bar/needle, targets). This allows the hierarchial and gauge charts to piggy-backon the base keyboard handling
// implementation.
this.focus.series = this.focus.sortedSeries[0];
const {
focus: { series, seriesIndex: otherIndex, datumIndex },
Expand All @@ -475,7 +476,10 @@ export class SeriesAreaManager extends BaseManager {
const { focus, hoverRect } = this;
if (pick === undefined || focus.series === undefined || hoverRect === undefined) return;

const { datum, datumIndex } = pick;
const { datum, datumIndex, otherIndex } = pick;
if (otherIndex !== undefined) {
focus.seriesIndex = otherIndex;
}
focus.datumIndex = datumIndex;
focus.datum = datum;

Expand Down
37 changes: 37 additions & 0 deletions packages/ag-charts-enterprise/src/series/gauge-util/focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { _ModuleSupport } from 'ag-charts-community';

const { clamp } = _ModuleSupport;
type SeriesNodeDatum = _ModuleSupport.SeriesNodeDatum<unknown>;
type SelectionNode = { node: _ModuleSupport.Path; datum: SeriesNodeDatum };
type PickFocusInputs = _ModuleSupport.PickFocusInputs;
type PickFocusOutputs = _ModuleSupport.PickFocusOutputs;
type GaugeSeriesProperties = {
contextNodeData?: {
nodeData: SeriesNodeDatum[];
targetData: SeriesNodeDatum[];
};
datumSelection: Iterable<SelectionNode>;
targetSelection: Iterable<SelectionNode>;
};

export function pickGaugeFocus(self: GaugeSeriesProperties, opts: PickFocusInputs): PickFocusOutputs | undefined {
const others = [
{ data: self.contextNodeData?.nodeData, selection: self.datumSelection },
{ data: self.contextNodeData?.targetData, selection: self.targetSelection },
].filter((v) => v.data && v.data.length > 0);
const otherIndex = clamp(0, opts.otherIndex + opts.otherIndexDelta, others.length - 1);
if (others.length === 0) return;

const { data, selection } = others[otherIndex];
if (data == null || data.length === 0) return;

const datumIndex = clamp(0, opts.datumIndex, data.length - 1);
const datum = data[datumIndex];

for (const node of selection) {
if (node.datum === datum) {
const bounds = node.node;
return { bounds, showFocusBox: true, clipFocusBox: true, datum, datumIndex, otherIndex };
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
_ModuleSupport,
} from 'ag-charts-community';

import { pickGaugeFocus } from '../gauge-util/focus';
import { fadeInFns, formatLabel, getLabelText } from '../gauge-util/label';
import { lineMarker } from '../gauge-util/lineMarker';
import { type UnknownGaugeNodeDatum, parseUnknownGaugeNodeDatum } from '../gauge-util/properties';
Expand Down Expand Up @@ -179,11 +180,11 @@ export class LinearGaugeSeries
this.scaleGroup,
() => this.nodeFactory()
);
private datumSelection: _ModuleSupport.Selection<_ModuleSupport.Rect, LinearGaugeNodeDatum> = Selection.select(
public datumSelection: _ModuleSupport.Selection<_ModuleSupport.Rect, LinearGaugeNodeDatum> = Selection.select(
this.itemGroup,
() => this.nodeFactory()
);
private targetSelection: _ModuleSupport.Selection<_ModuleSupport.Marker, LinearGaugeTargetDatum> = Selection.select(
public targetSelection: _ModuleSupport.Selection<_ModuleSupport.Marker, LinearGaugeTargetDatum> = Selection.select(
this.itemTargetGroup,
() => this.markerFactory()
);
Expand Down Expand Up @@ -1112,19 +1113,7 @@ export class LinearGaugeSeries
}

override pickFocus(opts: _ModuleSupport.PickFocusInputs): _ModuleSupport.PickFocusOutputs | undefined {
const targetData = this.contextNodeData?.targetData;
if (targetData == null || targetData.length === 0) return;

const datumIndex = Math.min(Math.max(opts.datumIndex, 0), targetData.length - 1);

const datum = targetData[datumIndex];

for (const node of this.targetSelection) {
if (node.datum === datum) {
const bounds = node.node;
return { bounds, showFocusBox: true, clipFocusBox: true, datum, datumIndex };
}
}
return pickGaugeFocus(this, opts);
}

getCaptionText(): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
_ModuleSupport,
} from 'ag-charts-community';

import { pickGaugeFocus } from '../gauge-util/focus';
import { fadeInFns, formatLabel, getLabelText } from '../gauge-util/label';
import { lineMarker } from '../gauge-util/lineMarker';
import { type UnknownGaugeNodeDatum, parseUnknownGaugeNodeDatum } from '../gauge-util/properties';
Expand Down Expand Up @@ -169,15 +170,15 @@ export class RadialGaugeSeries
this.scaleGroup,
() => this.nodeFactory()
);
private datumSelection: _ModuleSupport.Selection<_ModuleSupport.Sector, RadialGaugeNodeDatum> = Selection.select(
public datumSelection: _ModuleSupport.Selection<_ModuleSupport.Sector, RadialGaugeNodeDatum> = Selection.select(
this.itemGroup,
() => this.nodeFactory()
);
private needleSelection: _ModuleSupport.Selection<RadialGaugeNeedle, RadialGaugeNeedleDatum> = Selection.select(
this.itemNeedleGroup,
RadialGaugeNeedle
);
private targetSelection: _ModuleSupport.Selection<_ModuleSupport.Marker, RadialGaugeTargetDatum> = Selection.select(
public targetSelection: _ModuleSupport.Selection<_ModuleSupport.Marker, RadialGaugeTargetDatum> = Selection.select(
this.itemTargetGroup,
() => this.markerFactory()
);
Expand Down Expand Up @@ -456,7 +457,7 @@ export class RadialGaugeSeries
const maxTicks = Math.ceil(normalizeAngle360Inclusive(containerEndAngle - containerStartAngle) * radius);
let segments = segmentation.enabled ? segmentation.interval.getSegments(angleAxis.scale, maxTicks) : undefined;

const barFill = bar.fill ?? this.createConicGradient(bar.fills, bar.fillMode);
const barFill = !bar.enabled ? 'rgba(0,0,0,0)' : bar.fill ?? this.createConicGradient(bar.fills, bar.fillMode);
const scaleFill =
scale.fill ??
(bar.enabled && scale.fills.length === 0 ? scale.defaultFill : undefined) ??
Expand All @@ -468,26 +469,24 @@ export class RadialGaugeSeries
const appliedCornerRadius = Math.min(cornerRadius, (outerRadius - innerRadius) / 2);
const angleInset = appliedCornerRadius / ((innerRadius + outerRadius) / 2);

if (bar.enabled) {
nodeData.push({
series: this,
itemId: `value`,
datum,
datumIndex: { type: NodeDataType.Node },
type: NodeDataType.Node,
centerX,
centerY,
outerRadius,
innerRadius,
startAngle: containerStartAngle - angleInset,
endAngle: containerEndAngle + angleInset,
clipStartAngle: undefined,
clipEndAngle: undefined,
startCornerRadius: cornerRadius,
endCornerRadius: cornerRadius,
fill: barFill,
});
}
nodeData.push({
series: this,
itemId: `value`,
datum,
datumIndex: { type: NodeDataType.Node },
type: NodeDataType.Node,
centerX,
centerY,
outerRadius,
innerRadius,
startAngle: containerStartAngle - angleInset,
endAngle: containerEndAngle + angleInset,
clipStartAngle: undefined,
clipEndAngle: undefined,
startCornerRadius: cornerRadius,
endCornerRadius: cornerRadius,
fill: barFill,
});

scaleData.push({
series: this,
Expand Down Expand Up @@ -521,26 +520,24 @@ export class RadialGaugeSeries
const itemStartAngle = angleScale.convert(segmentStart);
const itemEndAngle = angleScale.convert(segmentEnd);

if (bar.enabled) {
nodeData.push({
series: this,
itemId: `value-${i}`,
datum,
datumIndex: { type: NodeDataType.Node },
type: NodeDataType.Node,
centerX,
centerY,
outerRadius,
innerRadius,
startAngle: itemStartAngle,
endAngle: itemEndAngle,
clipStartAngle: containerStartAngle,
clipEndAngle: containerEndAngle,
startCornerRadius: cornersOnAllItems || isStart ? cornerRadius : 0,
endCornerRadius: cornersOnAllItems || isEnd ? cornerRadius : 0,
fill: barFill,
});
}
nodeData.push({
series: this,
itemId: `value-${i}`,
datum,
datumIndex: { type: NodeDataType.Node },
type: NodeDataType.Node,
centerX,
centerY,
outerRadius,
innerRadius,
startAngle: itemStartAngle,
endAngle: itemEndAngle,
clipStartAngle: containerStartAngle,
clipEndAngle: containerEndAngle,
startCornerRadius: cornersOnAllItems || isStart ? cornerRadius : 0,
endCornerRadius: cornersOnAllItems || isEnd ? cornerRadius : 0,
fill: barFill,
});

scaleData.push({
series: this,
Expand Down Expand Up @@ -1162,19 +1159,7 @@ export class RadialGaugeSeries
}

override pickFocus(opts: _ModuleSupport.PickFocusInputs): _ModuleSupport.PickFocusOutputs | undefined {
const targetData = this.contextNodeData?.targetData;
if (targetData == null || targetData.length === 0) return;

const datumIndex = Math.min(Math.max(opts.datumIndex, 0), targetData.length - 1);

const datum = targetData[datumIndex];

for (const node of this.targetSelection) {
if (node.datum === datum) {
const bounds = node.node;
return { bounds, showFocusBox: true, clipFocusBox: true, datum, datumIndex };
}
}
return pickGaugeFocus(this, opts);
}

getCaptionText(): string {
Expand Down
68 changes: 68 additions & 0 deletions packages/ag-charts-website/e2e/keyboard-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,72 @@ test.describe('keyboard-nav', () => {
await page.keyboard.press('ArrowDown');
await expect(page.locator(SELECTORS.canvasCenter)).toHaveScreenshot(`sankey-link-highlight.png`);
});

test('gauge chart', async ({ page }) => {
const { url } = toExamplePageUrl('linear-gauge', 'custom-targets', 'vanilla');

await gotoExample(page, url);

await page.locator(SELECTORS.canvasCenter).first().click();

await page.keyboard.press('ArrowUp'); // should make the focus indicator appear
await page.keyboard.press('ArrowUp'); // should have no effect
await expect(page.locator(SELECTORS.canvasCenter)).toHaveScreenshot(`linear-gauge-bar-highlight.png`);

await page.keyboard.press('ArrowDown');
await expect(page.locator(SELECTORS.canvasCenter)).toHaveScreenshot(`linear-gauge-target0-highlight.png`);

await page.keyboard.press('ArrowRight');
await page.keyboard.press('ArrowRight');
await expect(page.locator(SELECTORS.canvasCenter)).toHaveScreenshot(`linear-gauge-target2-highlight.png`);

await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft'); // should have no effect
await expect(page.locator(SELECTORS.canvasCenter)).toHaveScreenshot(`linear-gauge-target0-highlight.png`);

await page.keyboard.press('ArrowDown'); // should have no effect
await page.keyboard.press('ArrowUp');
await expect(page.locator(SELECTORS.canvasCenter)).toHaveScreenshot(`linear-gauge-bar-highlight.png`);
});

test('radial gauge chart with needle', async ({ page }) => {
const { url } = toExamplePageUrl('radial-gauge', 'needle', 'vanilla');

await gotoExample(page, url);

const canvas = page.locator(SELECTORS.canvasCenter).first();
const hideNeedle = page.getByText('Hide Needle').first();
const showNeedle = page.getByText('Show Needle').first();
const hideBar = page.getByText('Hide Bar').first();
const showBar = page.getByText('Show Bar').first();

await canvas.click();
await page.keyboard.press('ArrowLeft');
await expect(canvas).toHaveScreenshot(`radial-gauge-showNeedle-hideBar.png`);

await hideNeedle.click();
await hideBar.click();
await canvas.click();
await page.keyboard.press('ArrowLeft');
await expect(canvas).toHaveScreenshot(`radial-gauge-hideNeedle-hideBar.png`);

await hideNeedle.click();
await showBar.click();
await canvas.click();
await page.keyboard.press('ArrowLeft');
await expect(canvas).toHaveScreenshot(`radial-gauge-hideNeedle-showBar.png`);

await showNeedle.click();
await hideBar.click();
await canvas.click();
await page.keyboard.press('ArrowLeft');
await expect(canvas).toHaveScreenshot(`radial-gauge-showNeedle-hideBar.png`);

await showNeedle.click();
await showBar.click();
await canvas.click();
await page.keyboard.press('ArrowLeft');
await expect(canvas).toHaveScreenshot(`radial-gauge-showNeedle-showBar.png`);
});
});
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit e9bce73

Please sign in to comment.