diff --git a/packages/osd-charts/src/lib/utils/interactions.ts b/packages/osd-charts/src/lib/utils/interactions.ts index 481505c11ea6..b8626228297c 100644 --- a/packages/osd-charts/src/lib/utils/interactions.ts +++ b/packages/osd-charts/src/lib/utils/interactions.ts @@ -72,6 +72,9 @@ export function isCrosshairTooltipType(type: TooltipType) { export function isFollowTooltipType(type: TooltipType) { return type === TooltipType.Follow; } +export function isNoneTooltipType(type: TooltipType) { + return type === TooltipType.None; +} export function areIndexedGeometryArraysEquals(arr1: IndexedGeometry[], arr2: IndexedGeometry[]) { if (arr1.length !== arr2.length) { diff --git a/packages/osd-charts/src/lib/utils/scales/scale_band.test.ts b/packages/osd-charts/src/lib/utils/scales/scale_band.test.ts index 261ec51bc5e2..afeb7d90eda9 100644 --- a/packages/osd-charts/src/lib/utils/scales/scale_band.test.ts +++ b/packages/osd-charts/src/lib/utils/scales/scale_band.test.ts @@ -97,4 +97,14 @@ describe('Scale Band', () => { expect(scale.invert(99.99999)).toBe('d'); expect(scale.invert(100)).toBe('d'); }); + describe('isSingleValue', () => { + it('should return true for single value scale', () => { + const scale = new ScaleBand(['a'], [0, 100]); + expect(scale.isSingleValue()).toBe(true); + }); + it('should return false for multi value scale', () => { + const scale = new ScaleBand(['a', 'b'], [0, 100]); + expect(scale.isSingleValue()).toBe(false); + }); + }); }); diff --git a/packages/osd-charts/src/lib/utils/scales/scale_band.ts b/packages/osd-charts/src/lib/utils/scales/scale_band.ts index 28035f9edb80..bb2fef0b159c 100644 --- a/packages/osd-charts/src/lib/utils/scales/scale_band.ts +++ b/packages/osd-charts/src/lib/utils/scales/scale_band.ts @@ -62,6 +62,9 @@ export class ScaleBand implements Scale { invertWithStep(value: any) { return this.invertedScale(value); } + isSingleValue() { + return this.domain.length < 2; + } } export function isOrdinalScale(scale: Scale): scale is ScaleBand { diff --git a/packages/osd-charts/src/lib/utils/scales/scale_continuous.test.ts b/packages/osd-charts/src/lib/utils/scales/scale_continuous.test.ts index 6375c21ef665..11e19f4aa872 100644 --- a/packages/osd-charts/src/lib/utils/scales/scale_continuous.test.ts +++ b/packages/osd-charts/src/lib/utils/scales/scale_continuous.test.ts @@ -140,6 +140,21 @@ describe('Scale Continuous', () => { expect(scaleLinear.invertWithStep(90, data)).toBe(90); }); + describe('isSingleValue', () => { + test('should return true for domain with fewer than 2 values', () => { + const scale = new ScaleContinuous(ScaleType.Linear, [], [0, 100]); + expect(scale.isSingleValue()).toBe(true); + }); + test('should return true for domain with equal min and max values', () => { + const scale = new ScaleContinuous(ScaleType.Linear, [1, 1], [0, 100]); + expect(scale.isSingleValue()).toBe(true); + }); + test('should return false for domain with differing min and max values', () => { + const scale = new ScaleContinuous(ScaleType.Linear, [1, 2], [0, 100]); + expect(scale.isSingleValue()).toBe(false); + }); + }); + describe('time ticks', () => { const timezonesToTest = ['Asia/Tokyo', 'Europe/Berlin', 'UTC', 'America/New_York', 'America/Los_Angeles']; diff --git a/packages/osd-charts/src/lib/utils/scales/scale_continuous.ts b/packages/osd-charts/src/lib/utils/scales/scale_continuous.ts index e335cf7cea66..dde9f6a56198 100644 --- a/packages/osd-charts/src/lib/utils/scales/scale_continuous.ts +++ b/packages/osd-charts/src/lib/utils/scales/scale_continuous.ts @@ -201,6 +201,15 @@ export class ScaleContinuous implements Scale { } return prevValue; } + isSingleValue() { + if (this.domain.length < 2) { + return true; + } + + const min = this.domain[0]; + const max = this.domain[this.domain.length - 1]; + return max === min; + } } export function isContinuousScale(scale: Scale): scale is ScaleContinuous { diff --git a/packages/osd-charts/src/lib/utils/scales/scales.ts b/packages/osd-charts/src/lib/utils/scales/scales.ts index 8c8ef0d92f55..cc37a4ad36f8 100644 --- a/packages/osd-charts/src/lib/utils/scales/scales.ts +++ b/packages/osd-charts/src/lib/utils/scales/scales.ts @@ -5,6 +5,7 @@ export interface Scale { scale: (value: any) => number; invert: (value: number) => any; invertWithStep: (value: number, data: any[]) => any; + isSingleValue: () => boolean; bandwidth: number; minInterval: number; type: ScaleType; diff --git a/packages/osd-charts/src/state/chart_state.test.ts b/packages/osd-charts/src/state/chart_state.test.ts index afc9b4a87248..1a0f3db4f328 100644 --- a/packages/osd-charts/src/state/chart_state.test.ts +++ b/packages/osd-charts/src/state/chart_state.test.ts @@ -885,4 +885,22 @@ describe('Chart Store', () => { expect(store.isCrosshairCursorVisible.get()).toBe(false); }); }); + test('should set tooltip type to follow when single value x scale', () => { + const singleValueSpec: BarSeriesSpec = { + id: SPEC_ID, + groupId: GROUP_ID, + seriesType: 'bar', + yScaleToDataExtent: false, + data: [{ x: 1, y: 1, g: 0 }], + xAccessor: 'x', + yAccessors: ['y'], + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + hideInLegend: false, + }; + + store.addSeriesSpec(singleValueSpec); + store.computeChart(); + expect(store.tooltipType.get()).toBe(TooltipType.Follow); + }); }); diff --git a/packages/osd-charts/src/state/chart_state.ts b/packages/osd-charts/src/state/chart_state.ts index 55d2d73e861d..5dde2c88b393 100644 --- a/packages/osd-charts/src/state/chart_state.ts +++ b/packages/osd-charts/src/state/chart_state.ts @@ -56,6 +56,7 @@ import { getValidYPosition, isCrosshairTooltipType, isFollowTooltipType, + isNoneTooltipType, TooltipType, TooltipValue, TooltipValueFormatter, @@ -301,11 +302,14 @@ export class ChartStore { const updatedCursorLine = getCursorLinePosition(this.chartRotation, this.chartDimensions, this.cursorPosition); Object.assign(this.cursorLinePosition, updatedCursorLine); + const isSingleValueXScale = this.xScale.isSingleValue(); + this.tooltipPosition.transform = getTooltipPosition( this.chartDimensions, this.chartRotation, this.cursorBandPosition, this.cursorPosition, + isSingleValueXScale, ); // get the elements on at this cursor position @@ -873,6 +877,12 @@ export class ChartStore { // console.log({ seriesGeometries }); this.geometries = seriesGeometries.geometries; this.xScale = seriesGeometries.scales.xScale; + + const isSingleValueXScale = this.xScale.isSingleValue(); + if (isSingleValueXScale && !isNoneTooltipType(this.tooltipType.get())) { + this.tooltipType.set(TooltipType.Follow); + } + this.yScales = seriesGeometries.scales.yScales; this.geometriesIndex = seriesGeometries.geometriesIndex; this.geometriesIndexKeys = [...this.geometriesIndex.keys()].sort(compareByValueAsc); diff --git a/packages/osd-charts/src/state/crosshair_utils.ts b/packages/osd-charts/src/state/crosshair_utils.ts index ec1c564429ca..e05a6b5e1ebc 100644 --- a/packages/osd-charts/src/state/crosshair_utils.ts +++ b/packages/osd-charts/src/state/crosshair_utils.ts @@ -122,6 +122,7 @@ export function getTooltipPosition( chartRotation: Rotation, cursorBandPosition: Dimensions, cursorPosition: { x: number; y: number }, + isSingleValueXScale: boolean, ): string { const isHorizontalRotated = isHorizontalRotation(chartRotation); const hPosition = getHorizontalTooltipPosition( @@ -129,12 +130,14 @@ export function getTooltipPosition( cursorBandPosition, chartDimensions, isHorizontalRotated, + isSingleValueXScale, ); const vPosition = getVerticalTooltipPosition( cursorPosition.y, cursorBandPosition, chartDimensions, isHorizontalRotated, + isSingleValueXScale, ); const xTranslation = `translateX(${hPosition.position}px) translateX(-${hPosition.offset}%)`; const yTranslation = `translateY(${vPosition.position}px) translateY(-${vPosition.offset}%)`; @@ -146,9 +149,17 @@ export function getHorizontalTooltipPosition( cursorBandPosition: Dimensions, chartDimensions: Dimensions, isHorizontalRotated: boolean, + isSingleValueXScale: boolean, padding: number = 20, ): { offset: number; position: number } { if (isHorizontalRotated) { + if (isSingleValueXScale) { + return { + offset: 0, + position: cursorBandPosition.left, + }; + } + if (cursorXPosition <= chartDimensions.width / 2) { return { offset: 0, @@ -180,6 +191,7 @@ export function getVerticalTooltipPosition( cursorBandPosition: Dimensions, chartDimensions: Dimensions, isHorizontalRotated: boolean, + isSingleValueXScale: boolean, padding: number = 20, ): { offset: number; @@ -198,6 +210,12 @@ export function getVerticalTooltipPosition( }; } } else { + if (isSingleValueXScale) { + return { + offset: 0, + position: cursorBandPosition.top, + }; + } if (cursorYPosition <= chartDimensions.height / 2) { return { offset: 0, diff --git a/packages/osd-charts/src/state/test/interactions.test.ts b/packages/osd-charts/src/state/test/interactions.test.ts index 514ea7e25473..a430cd744cfc 100644 --- a/packages/osd-charts/src/state/test/interactions.test.ts +++ b/packages/osd-charts/src/state/test/interactions.test.ts @@ -8,6 +8,7 @@ import { ScaleContinuous } from '../../lib/utils/scales/scale_continuous'; import { ScaleType } from '../../lib/utils/scales/scales'; import { ChartStore } from '../chart_state'; import { computeSeriesDomains } from '../utils'; +import { ScaleBand } from '../../lib/utils/scales/scale_band'; const SPEC_ID = getSpecId('spec_1'); const GROUP_ID = getGroupId('group_1'); @@ -369,4 +370,26 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(onOverListener.mock.calls[0][0]).toEqual([indexedGeom2Blue.value]); expect(onOutListener).toBeCalledTimes(0); }); + + describe('can position tooltip within chart when xScale is a single value scale', () => { + beforeEach(() => { + const singleValueScale = + store.xScale!.type === ScaleType.Ordinal + ? new ScaleBand(['a'], [0, 0]) + : new ScaleContinuous(ScaleType.Linear, [1, 1], [0, 0]); + store.xScale = singleValueScale; + }); + test('horizontal chart rotation', () => { + store.setCursorPosition(chartLeft + 99, chartTop + 99); + const expectedTransform = `translateX(${chartLeft}px) translateX(-0%) translateY(109px) translateY(-100%)`; + expect(store.tooltipPosition.transform).toBe(expectedTransform); + }); + + test('vertical chart rotation', () => { + store.chartRotation = 90; + store.setCursorPosition(chartLeft + 99, chartTop + 99); + const expectedTransform = `translateX(109px) translateX(-100%) translateY(${chartTop}px) translateY(-0%)`; + expect(store.tooltipPosition.transform).toBe(expectedTransform); + }); + }); } diff --git a/packages/osd-charts/stories/bar_chart.tsx b/packages/osd-charts/stories/bar_chart.tsx index 33a147da8d9b..743ac4ffbbaf 100644 --- a/packages/osd-charts/stories/bar_chart.tsx +++ b/packages/osd-charts/stories/bar_chart.tsx @@ -886,9 +886,39 @@ storiesOf('Bar Chart', module) ); }) - .add('single data chart', () => { + .add('single data chart [linear]', () => { + const hasCustomDomain = boolean('has custom domain', false); + const xDomain = hasCustomDomain + ? { + min: 0, + } + : undefined; + + const chartRotation = select( + 'chartRotation', + { + '0 deg': 0, + '90 deg': 90, + '-90 deg': -90, + '180 deg': 180, + }, + 0, + ); + + const theme = { + scales: { + barsPadding: number('bars padding', 0.25, { + range: true, + min: 0, + max: 1, + step: 0.1, + }), + }, + }; + return ( + ); }) + .add('single data chart [ordinal]', () => { + const hasCustomDomain = boolean('has custom domain', false); + const xDomain = hasCustomDomain ? ['a', 'b'] : undefined; + + const chartRotation = select( + 'chartRotation', + { + '0 deg': 0, + '90 deg': 90, + '-90 deg': -90, + '180 deg': 180, + }, + 0, + ); + + const theme = { + scales: { + barsPadding: number('bars padding', 0.25, { + range: true, + min: 0, + max: 1, + step: 0.1, + }), + }, + }; + + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); + }) .add('single data clusterd chart', () => { return (