diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-bar-clicks-and-hovers-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-bar-clicks-and-hovers-visually-looks-correct-1-snap.png index f29728bbd7..7f0a7461e7 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-bar-clicks-and-hovers-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-bar-clicks-and-hovers-visually-looks-correct-1-snap.png differ diff --git a/src/chart_types/xy_chart/rendering/bars.ts b/src/chart_types/xy_chart/rendering/bars.ts index 5dabc645ab..5bf35fb0e9 100644 --- a/src/chart_types/xy_chart/rendering/bars.ts +++ b/src/chart_types/xy_chart/rendering/bars.ts @@ -39,7 +39,7 @@ export function renderBars( sharedSeriesStyle: BarSeriesStyle, displayValueSettings?: DisplayValueSpec, styleAccessor?: BarStyleAccessor, - minBarHeight?: number, + minBarHeight: number = 0, stackMode?: StackMode, chartRotation?: number, ): { @@ -54,7 +54,6 @@ export function renderBars( // default padding to 1 for now const padding = 1; const { fontSize, fontFamily } = sharedSeriesStyle.displayValue; - const absMinHeight = minBarHeight && Math.abs(minBarHeight); dataSeries.data.forEach((datum) => { const { y0, y1, initialY1, filled } = datum; @@ -78,20 +77,16 @@ export function renderBars( } } else { y = yScale.scale(y1); - if (yScale.isInverted) { - // use always zero as baseline if y0 is null - y0Scaled = y0 === null ? yScale.scale(0) : yScale.scale(y0); - } else { - y0Scaled = y0 === null ? yScale.scale(0) : yScale.scale(y0); - } + // use always zero as baseline if y0 is null + y0Scaled = y0 === null ? yScale.scale(0) : yScale.scale(y0); } if (y === null || y0Scaled === null) { return; } - let height = y0Scaled - y; - // handle minBarHeight adjustment + const absMinHeight = Math.abs(minBarHeight); + let height = y0Scaled - y; if (absMinHeight !== undefined && height !== 0 && Math.abs(height) < absMinHeight) { const heightDelta = absMinHeight - Math.abs(height); if (height < 0) { @@ -102,6 +97,9 @@ export function renderBars( y -= heightDelta; } } + const isUpsideDown = height < 0; + height = Math.abs(height); + y = isUpsideDown ? y - height : y; const xScaled = xScale.scale(datum.x); diff --git a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts index 2486c4c6e9..14702b121e 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts @@ -209,164 +209,139 @@ describe('Rendering bars', () => { expect(geometries.bars[0].value[0].displayValue?.width).toBe(50); }); }); - // describe('Multi series bar chart - ordinal', () => { - // const spec1Id = 'bar1'; - // const spec2Id = 'bar2'; - // const barSeriesSpec1: BarSeriesSpec = { - // chartType: ChartType.XYAxis, - // specType: SpecType.Series, - // id: spec1Id, - // groupId: GROUP_ID, - // seriesType: SeriesType.Bar, - // data: [ - // [0, 10], - // [1, 5], - // ], - // xAccessor: 0, - // yAccessors: [1], - // xScaleType: ScaleType.Ordinal, - // yScaleType: ScaleType.Linear, - // }; - // const barSeriesSpec2: BarSeriesSpec = { - // chartType: ChartType.XYAxis, - // specType: SpecType.Series, - // id: spec2Id, - // groupId: GROUP_ID, - // seriesType: SeriesType.Bar, - // data: [ - // [0, 20], - // [1, 10], - // ], - // xAccessor: 0, - // yAccessors: [1], - // xScaleType: ScaleType.Ordinal, - // yScaleType: ScaleType.Linear, - // }; - // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; - // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - // const xScale = computeXScale({ - // xDomain: barSeriesDomains.xDomain, - // totalBarsInCluster: barSeriesMap.length, - // range: [0, 100], - // }); - // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - // const getBarGeometry = MockBarGeometry.fromBaseline( - // { - // x: 0, - // y: 0, - // width: 50, - // height: 100, - // color: 'red', - // value: { - // accessor: 'y1', - // x: 0, - // y: 10, - // mark: null, - // }, - // seriesIdentifier: { - // specId: spec1Id, - // key: 'spec{bar1}yAccessor{1}splitAccessors{}', - // yAccessor: 1, - // splitAccessors: new Map(), - // seriesKeys: [1], - // }, - // }, - // 'displayValue', - // ); - // - // test('can render first spec bars', () => { - // const { barGeometries } = renderBars( - // 0, - // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - // xScale, - // yScales.get(GROUP_ID)!, - // 'red', - // LIGHT_THEME.barSeriesStyle, - // ); - // expect(barGeometries.length).toEqual(2); - // expect(barGeometries[0]).toEqual( - // getBarGeometry({ - // x: 0, - // y: 50, - // width: 25, - // height: 50, - // value: { - // x: 0, - // y: 10, - // }, - // }), - // ); - // expect(barGeometries[1]).toEqual( - // getBarGeometry({ - // x: 50, - // y: 75, - // width: 25, - // height: 25, - // value: { - // x: 1, - // y: 5, - // }, - // }), - // ); - // }); - // test('can render second spec bars', () => { - // const { barGeometries } = renderBars( - // 1, - // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - // xScale, - // yScales.get(GROUP_ID)!, - // 'blue', - // LIGHT_THEME.barSeriesStyle, - // ); - // const getBarGeometry = MockBarGeometry.fromBaseline( - // { - // x: 0, - // y: 0, - // width: 50, - // height: 100, - // color: 'blue', - // value: { - // accessor: 'y1', - // x: 0, - // y: 10, - // }, - // seriesIdentifier: { - // specId: spec2Id, - // key: 'spec{bar2}yAccessor{1}splitAccessors{}', - // yAccessor: 1, - // splitAccessors: new Map(), - // seriesKeys: [1], - // }, - // }, - // 'displayValue', - // ); - // expect(barGeometries.length).toEqual(2); - // expect(barGeometries[0]).toEqual( - // getBarGeometry({ - // x: 25, - // y: 0, - // width: 25, - // height: 100, - // value: { - // x: 0, - // y: 20, - // }, - // }), - // ); - // expect(barGeometries[1]).toEqual( - // getBarGeometry({ - // x: 75, - // y: 50, - // width: 25, - // height: 50, - // value: { - // x: 1, - // y: 10, - // }, - // }), - // ); - // }); - // }); + describe('Multi series bar chart - ordinal', () => { + const spec1Id = 'bar1'; + const spec2Id = 'bar2'; + const barSeriesSpec1 = MockSeriesSpec.bar({ + id: spec1Id, + groupId: GROUP_ID, + data: [ + [0, 10], + [1, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const barSeriesSpec2 = MockSeriesSpec.bar({ + id: spec2Id, + groupId: GROUP_ID, + data: [ + [0, 20], + [1, 10], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }); + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + barSeriesSpec1, + barSeriesSpec2, + MockGlobalSpec.settingsNoMargins({ theme: { colors: { vizColors: ['red', 'blue'] } } }), + ], + store, + ); + + const getBarGeometry = MockBarGeometry.fromBaseline( + { + x: 0, + y: 0, + width: 50, + height: 100, + color: 'red', + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec1), + }, + 'displayValue', + ); + const { + geometries: { bars }, + } = computeSeriesGeometriesSelector(store.getState()); + + test('can render first spec bars', () => { + expect(bars[0].value.length).toEqual(2); + expect(bars[0].value[0]).toEqual( + getBarGeometry({ + x: 0, + y: 50, + width: 25, + height: 50, + value: { + x: 0, + y: 10, + datum: [0, 10], + }, + }), + ); + expect(bars[0].value[1]).toEqual( + getBarGeometry({ + x: 50, + y: 75, + width: 25, + height: 25, + value: { + x: 1, + y: 5, + datum: [1, 5], + }, + }), + ); + }); + test('can render second spec bars', () => { + const getBarGeometry = MockBarGeometry.fromBaseline( + { + x: 0, + y: 0, + width: 50, + height: 100, + color: 'blue', + value: { + accessor: 'y1', + x: 0, + y: 10, + }, + seriesIdentifier: MockSeriesIdentifier.fromSpec(barSeriesSpec2), + }, + 'displayValue', + ); + expect(bars[1].value.length).toEqual(2); + expect(bars[1].value[0]).toEqual( + getBarGeometry({ + x: 25, + y: 0, + width: 25, + height: 100, + value: { + x: 0, + y: 20, + datum: [0, 20], + }, + }), + ); + expect(bars[1].value[1]).toEqual( + getBarGeometry({ + x: 75, + y: 50, + width: 25, + height: 50, + value: { + x: 1, + y: 10, + datum: [1, 10], + }, + }), + ); + }); + }); // describe('Single series bar chart - linear', () => { // const barSeriesSpec: BarSeriesSpec = { // chartType: ChartType.XYAxis, diff --git a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts index 2645047ecb..f0b3134c27 100644 --- a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts @@ -24,10 +24,11 @@ import { Store } from 'redux'; import { ChartType } from '../..'; import { Rect } from '../../../geoms/types'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs'; import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; import { SettingsSpec, XScaleType, XYBrushArea } from '../../../specs'; -import { SpecType, DEFAULT_SETTINGS_SPEC, TooltipType, BrushAxis } from '../../../specs/constants'; +import { SpecType, TooltipType, BrushAxis } from '../../../specs/constants'; import { onExternalPointerEvent } from '../../../state/actions/events'; import { onPointerMove, onMouseDown, onMouseUp } from '../../../state/actions/mouse'; import { GlobalChartState } from '../../../state/chart_state'; @@ -44,6 +45,7 @@ import { } from './selectors/get_tooltip_values_highlighted_geoms'; import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; import { createOnBrushEndCaller } from './selectors/on_brush_end_caller'; +import { createOnClickCaller } from './selectors/on_click_caller'; import { createOnElementOutCaller } from './selectors/on_element_out_caller'; import { createOnElementOverCaller } from './selectors/on_element_over_caller'; import { createOnPointerMoveCaller } from './selectors/on_pointer_move_caller'; @@ -51,12 +53,9 @@ import { createOnPointerMoveCaller } from './selectors/on_pointer_move_caller'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; -const ordinalBarSeries: BarSeriesSpec = { - chartType: ChartType.XYAxis, - specType: SpecType.Series, +const ordinalBarSeries = MockSeriesSpec.bar({ id: SPEC_ID, groupId: GROUP_ID, - seriesType: SeriesType.Bar, data: [ [0, 10], [1, 5], @@ -66,8 +65,8 @@ const ordinalBarSeries: BarSeriesSpec = { xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, hideInLegend: false, -}; -const linearBarSeries: BarSeriesSpec = { +}); +const linearBarSeries = MockSeriesSpec.bar({ chartType: ChartType.XYAxis, specType: SpecType.Series, id: SPEC_ID, @@ -82,11 +81,10 @@ const linearBarSeries: BarSeriesSpec = { xScaleType: ScaleType.Linear, yScaleType: ScaleType.Linear, hideInLegend: false, -}; +}); const chartTop = 10; const chartLeft = 10; -const settingSpec: SettingsSpec = { - ...DEFAULT_SETTINGS_SPEC, +const settingSpec = MockGlobalSpec.settings({ tooltip: { type: TooltipType.VerticalCursor, }, @@ -98,7 +96,7 @@ const settingSpec: SettingsSpec = { barsPadding: 0, }, }, -}; +}); function initStore(spec: BasicSeriesSpec) { const store = MockStore.default({ width: 100, height: 100, top: chartTop, left: chartLeft }, 'chartId'); @@ -1067,3 +1065,49 @@ function mouseOverTestSuite(scaleType: XScaleType) { }); }); } + +describe('Negative bars click and hover', () => { + let store: Store; + let onElementClick: jest.Mock; + beforeEach(() => { + store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }, 'chartId'); + onElementClick = jest.fn((): void => undefined); + const onElementClickCaller = createOnClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + MockStore.addSpecs( + [ + MockGlobalSpec.settingsNoMargins({ + onElementClick, + }), + MockSeriesSpec.bar({ + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, -10], + [2, 10], + ], + }), + ], + store, + ); + }); + + test('highlight negative bars', () => { + store.dispatch(onPointerMove({ x: 50, y: 75 }, 0)); + const highlightedGeoms = getHighlightedGeomsSelector(store.getState()); + expect(highlightedGeoms.length).toBe(1); + expect(highlightedGeoms[0].value.datum).toEqual([1, -10]); + }); + test('click negative bars', () => { + store.dispatch(onPointerMove({ x: 50, y: 75 }, 0)); + store.dispatch(onMouseDown({ x: 50, y: 75 }, 100)); + store.dispatch(onMouseUp({ x: 50, y: 75 }, 200)); + + expect(onElementClick).toBeCalled(); + const callArgs = onElementClick.mock.calls[0][0]; + expect(callArgs[0][0].datum).toEqual([1, -10]); + }); +}); diff --git a/stories/interactions/1_bar_clicks.tsx b/stories/interactions/1_bar_clicks.tsx index 5ae5bf0c75..f06e100fb4 100644 --- a/stories/interactions/1_bar_clicks.tsx +++ b/stories/interactions/1_bar_clicks.tsx @@ -70,7 +70,7 @@ export const Example = () => { data={[ { x: 0, y: 2, obj: { from: 10, to: 20 }, sObj: 'from 10 to 20' }, { x: 1, y: 7, obj: { from: 20, to: 30 }, sObj: 'from 20 to 30' }, - { x: 2, y: 3, obj: { from: 30, to: 40 }, sObj: 'from 30 to 40' }, + { x: 2, y: -3, obj: { from: 30, to: 40 }, sObj: 'from 30 to 40' }, { x: 3, y: 6, obj: { from: 40, to: 50 }, sObj: 'from 40 to 50' }, ]} />