Skip to content

Commit f458bc9

Browse files
fix: position tooltip within chart with single value xScale (#259)
1 parent 8e7400c commit f458bc9

File tree

11 files changed

+190
-1
lines changed

11 files changed

+190
-1
lines changed

src/lib/utils/interactions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ export function isCrosshairTooltipType(type: TooltipType) {
7272
export function isFollowTooltipType(type: TooltipType) {
7373
return type === TooltipType.Follow;
7474
}
75+
export function isNoneTooltipType(type: TooltipType) {
76+
return type === TooltipType.None;
77+
}
7578

7679
export function areIndexedGeometryArraysEquals(arr1: IndexedGeometry[], arr2: IndexedGeometry[]) {
7780
if (arr1.length !== arr2.length) {

src/lib/utils/scales/scale_band.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,14 @@ describe('Scale Band', () => {
9797
expect(scale.invert(99.99999)).toBe('d');
9898
expect(scale.invert(100)).toBe('d');
9999
});
100+
describe('isSingleValue', () => {
101+
it('should return true for single value scale', () => {
102+
const scale = new ScaleBand(['a'], [0, 100]);
103+
expect(scale.isSingleValue()).toBe(true);
104+
});
105+
it('should return false for multi value scale', () => {
106+
const scale = new ScaleBand(['a', 'b'], [0, 100]);
107+
expect(scale.isSingleValue()).toBe(false);
108+
});
109+
});
100110
});

src/lib/utils/scales/scale_band.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export class ScaleBand implements Scale {
6262
invertWithStep(value: any) {
6363
return this.invertedScale(value);
6464
}
65+
isSingleValue() {
66+
return this.domain.length < 2;
67+
}
6568
}
6669

6770
export function isOrdinalScale(scale: Scale): scale is ScaleBand {

src/lib/utils/scales/scale_continuous.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ describe('Scale Continuous', () => {
140140
expect(scaleLinear.invertWithStep(90, data)).toBe(90);
141141
});
142142

143+
describe('isSingleValue', () => {
144+
test('should return true for domain with fewer than 2 values', () => {
145+
const scale = new ScaleContinuous(ScaleType.Linear, [], [0, 100]);
146+
expect(scale.isSingleValue()).toBe(true);
147+
});
148+
test('should return true for domain with equal min and max values', () => {
149+
const scale = new ScaleContinuous(ScaleType.Linear, [1, 1], [0, 100]);
150+
expect(scale.isSingleValue()).toBe(true);
151+
});
152+
test('should return false for domain with differing min and max values', () => {
153+
const scale = new ScaleContinuous(ScaleType.Linear, [1, 2], [0, 100]);
154+
expect(scale.isSingleValue()).toBe(false);
155+
});
156+
});
157+
143158
describe('time ticks', () => {
144159
const timezonesToTest = ['Asia/Tokyo', 'Europe/Berlin', 'UTC', 'America/New_York', 'America/Los_Angeles'];
145160

src/lib/utils/scales/scale_continuous.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,15 @@ export class ScaleContinuous implements Scale {
201201
}
202202
return prevValue;
203203
}
204+
isSingleValue() {
205+
if (this.domain.length < 2) {
206+
return true;
207+
}
208+
209+
const min = this.domain[0];
210+
const max = this.domain[this.domain.length - 1];
211+
return max === min;
212+
}
204213
}
205214

206215
export function isContinuousScale(scale: Scale): scale is ScaleContinuous {

src/lib/utils/scales/scales.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Scale {
55
scale: (value: any) => number;
66
invert: (value: number) => any;
77
invertWithStep: (value: number, data: any[]) => any;
8+
isSingleValue: () => boolean;
89
bandwidth: number;
910
minInterval: number;
1011
type: ScaleType;

src/state/chart_state.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,4 +885,22 @@ describe('Chart Store', () => {
885885
expect(store.isCrosshairCursorVisible.get()).toBe(false);
886886
});
887887
});
888+
test('should set tooltip type to follow when single value x scale', () => {
889+
const singleValueSpec: BarSeriesSpec = {
890+
id: SPEC_ID,
891+
groupId: GROUP_ID,
892+
seriesType: 'bar',
893+
yScaleToDataExtent: false,
894+
data: [{ x: 1, y: 1, g: 0 }],
895+
xAccessor: 'x',
896+
yAccessors: ['y'],
897+
xScaleType: ScaleType.Linear,
898+
yScaleType: ScaleType.Linear,
899+
hideInLegend: false,
900+
};
901+
902+
store.addSeriesSpec(singleValueSpec);
903+
store.computeChart();
904+
expect(store.tooltipType.get()).toBe(TooltipType.Follow);
905+
});
888906
});

src/state/chart_state.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
getValidYPosition,
5757
isCrosshairTooltipType,
5858
isFollowTooltipType,
59+
isNoneTooltipType,
5960
TooltipType,
6061
TooltipValue,
6162
TooltipValueFormatter,
@@ -301,11 +302,14 @@ export class ChartStore {
301302
const updatedCursorLine = getCursorLinePosition(this.chartRotation, this.chartDimensions, this.cursorPosition);
302303
Object.assign(this.cursorLinePosition, updatedCursorLine);
303304

305+
const isSingleValueXScale = this.xScale.isSingleValue();
306+
304307
this.tooltipPosition.transform = getTooltipPosition(
305308
this.chartDimensions,
306309
this.chartRotation,
307310
this.cursorBandPosition,
308311
this.cursorPosition,
312+
isSingleValueXScale,
309313
);
310314

311315
// get the elements on at this cursor position
@@ -873,6 +877,12 @@ export class ChartStore {
873877
// console.log({ seriesGeometries });
874878
this.geometries = seriesGeometries.geometries;
875879
this.xScale = seriesGeometries.scales.xScale;
880+
881+
const isSingleValueXScale = this.xScale.isSingleValue();
882+
if (isSingleValueXScale && !isNoneTooltipType(this.tooltipType.get())) {
883+
this.tooltipType.set(TooltipType.Follow);
884+
}
885+
876886
this.yScales = seriesGeometries.scales.yScales;
877887
this.geometriesIndex = seriesGeometries.geometriesIndex;
878888
this.geometriesIndexKeys = [...this.geometriesIndex.keys()].sort(compareByValueAsc);

src/state/crosshair_utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,22 @@ export function getTooltipPosition(
122122
chartRotation: Rotation,
123123
cursorBandPosition: Dimensions,
124124
cursorPosition: { x: number; y: number },
125+
isSingleValueXScale: boolean,
125126
): string {
126127
const isHorizontalRotated = isHorizontalRotation(chartRotation);
127128
const hPosition = getHorizontalTooltipPosition(
128129
cursorPosition.x,
129130
cursorBandPosition,
130131
chartDimensions,
131132
isHorizontalRotated,
133+
isSingleValueXScale,
132134
);
133135
const vPosition = getVerticalTooltipPosition(
134136
cursorPosition.y,
135137
cursorBandPosition,
136138
chartDimensions,
137139
isHorizontalRotated,
140+
isSingleValueXScale,
138141
);
139142
const xTranslation = `translateX(${hPosition.position}px) translateX(-${hPosition.offset}%)`;
140143
const yTranslation = `translateY(${vPosition.position}px) translateY(-${vPosition.offset}%)`;
@@ -146,9 +149,17 @@ export function getHorizontalTooltipPosition(
146149
cursorBandPosition: Dimensions,
147150
chartDimensions: Dimensions,
148151
isHorizontalRotated: boolean,
152+
isSingleValueXScale: boolean,
149153
padding: number = 20,
150154
): { offset: number; position: number } {
151155
if (isHorizontalRotated) {
156+
if (isSingleValueXScale) {
157+
return {
158+
offset: 0,
159+
position: cursorBandPosition.left,
160+
};
161+
}
162+
152163
if (cursorXPosition <= chartDimensions.width / 2) {
153164
return {
154165
offset: 0,
@@ -180,6 +191,7 @@ export function getVerticalTooltipPosition(
180191
cursorBandPosition: Dimensions,
181192
chartDimensions: Dimensions,
182193
isHorizontalRotated: boolean,
194+
isSingleValueXScale: boolean,
183195
padding: number = 20,
184196
): {
185197
offset: number;
@@ -198,6 +210,12 @@ export function getVerticalTooltipPosition(
198210
};
199211
}
200212
} else {
213+
if (isSingleValueXScale) {
214+
return {
215+
offset: 0,
216+
position: cursorBandPosition.top,
217+
};
218+
}
201219
if (cursorYPosition <= chartDimensions.height / 2) {
202220
return {
203221
offset: 0,

src/state/test/interactions.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ScaleContinuous } from '../../lib/utils/scales/scale_continuous';
88
import { ScaleType } from '../../lib/utils/scales/scales';
99
import { ChartStore } from '../chart_state';
1010
import { computeSeriesDomains } from '../utils';
11+
import { ScaleBand } from '../../lib/utils/scales/scale_band';
1112

1213
const SPEC_ID = getSpecId('spec_1');
1314
const GROUP_ID = getGroupId('group_1');
@@ -369,4 +370,26 @@ function mouseOverTestSuite(scaleType: ScaleType) {
369370
expect(onOverListener.mock.calls[0][0]).toEqual([indexedGeom2Blue.value]);
370371
expect(onOutListener).toBeCalledTimes(0);
371372
});
373+
374+
describe('can position tooltip within chart when xScale is a single value scale', () => {
375+
beforeEach(() => {
376+
const singleValueScale =
377+
store.xScale!.type === ScaleType.Ordinal
378+
? new ScaleBand(['a'], [0, 0])
379+
: new ScaleContinuous(ScaleType.Linear, [1, 1], [0, 0]);
380+
store.xScale = singleValueScale;
381+
});
382+
test('horizontal chart rotation', () => {
383+
store.setCursorPosition(chartLeft + 99, chartTop + 99);
384+
const expectedTransform = `translateX(${chartLeft}px) translateX(-0%) translateY(109px) translateY(-100%)`;
385+
expect(store.tooltipPosition.transform).toBe(expectedTransform);
386+
});
387+
388+
test('vertical chart rotation', () => {
389+
store.chartRotation = 90;
390+
store.setCursorPosition(chartLeft + 99, chartTop + 99);
391+
const expectedTransform = `translateX(109px) translateX(-100%) translateY(${chartTop}px) translateY(-0%)`;
392+
expect(store.tooltipPosition.transform).toBe(expectedTransform);
393+
});
394+
});
372395
}

0 commit comments

Comments
 (0)