Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AG-13216 Gauges pickFocus implementation #3324

Merged
merged 9 commits into from
Jan 7, 2025
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.
Loading