From 6d0319f1b0898360c004ec34844ac0b441d08b38 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Fri, 4 Sep 2020 16:40:54 -0500 Subject: [PATCH] feat(brush): histogram brushing last values and rounding (#801) --- api/charts.api.md | 2 + ...istogram-visually-looks-correct-1-snap.png | Bin 0 -> 13397 bytes .../state/selectors/on_brush_end_caller.ts | 26 ++++++- src/specs/settings.tsx | 24 ++++++ .../interactions/10_brush_selection_bar.tsx | 70 ++++++++++++------ .../10a_brush_selection_bar_hist.tsx | 56 ++++++++++++++ stories/interactions/12_brush_time_hist.tsx | 21 +++++- stories/interactions/interactions.stories.tsx | 1 + 8 files changed, 171 insertions(+), 29 deletions(-) create mode 100644 integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-brush-selection-tool-on-bar-chart-histogram-visually-looks-correct-1-snap.png create mode 100644 stories/interactions/10a_brush_selection_bar_hist.tsx diff --git a/api/charts.api.md b/api/charts.api.md index be941bc314..568bb4bdc9 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -1292,6 +1292,7 @@ export const Settings: React.FunctionComponent; // @public export interface SettingsSpec extends Spec { + allowBrushingLastHistogramBucket?: boolean; // (undocumented) animateData: boolean; baseTheme?: Theme; @@ -1339,6 +1340,7 @@ export interface SettingsSpec extends Spec { resizeDebounce?: number; // (undocumented) rotation: Rotation; + roundHistogramBrushValues?: boolean; // (undocumented) showLegend: boolean; showLegendExtra: boolean; diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-brush-selection-tool-on-bar-chart-histogram-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-brush-selection-tool-on-bar-chart-histogram-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..4356c792c033172d8ece5cdf97d4f4a4d29f14e3 GIT binary patch literal 13397 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 void { ], ( lastDrag, - { onBrushEnd, rotation, brushAxis, minBrushDelta }, + { + onBrushEnd, + rotation, + brushAxis, + minBrushDelta, + roundHistogramBrushValues, + allowBrushingLastHistogramBucket, + }, computedScales, { chartDimensions }, histogramMode, @@ -111,6 +118,8 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void { histogramMode, xScale, minBrushDelta, + roundHistogramBrushValues, + allowBrushingLastHistogramBucket, ); } if (brushAxis === BrushAxis.Y || brushAxis === BrushAxis.Both) { @@ -140,6 +149,8 @@ function getXBrushExtent( histogramMode: boolean, xScale: Scale, minBrushDelta?: number, + roundHistogramBrushValues?: boolean, + allowBrushingLastHistogramBucket?: boolean, ): [number, number] | undefined { let startPos = getLeftPoint(chartDimensions, lastDrag.start.position); let endPos = getLeftPoint(chartDimensions, lastDrag.end.position); @@ -163,10 +174,17 @@ function getXBrushExtent( } const offset = histogramMode ? 0 : -(xScale.bandwidth + xScale.bandwidthPadding) / 2; - const minPosScaled = xScale.invert(minPos + offset); - const maxPosScaled = xScale.invert(maxPos + offset); + const invertValue = roundHistogramBrushValues + ? (value: number) => xScale.invertWithStep(value, xScale.domain)?.value + : (value: number) => xScale.invert(value); + const minPosScaled = invertValue(minPos + offset); + const maxPosScaled = invertValue(maxPos + offset); + + const maxDomainValue = xScale.domain[1] + (allowBrushingLastHistogramBucket ? xScale.minInterval : 0); + const minValue = minValueWithLowerLimit(minPosScaled, maxPosScaled, xScale.domain[0]); - const maxValue = maxValueWithUpperLimit(minPosScaled, maxPosScaled, xScale.domain[1]); + const maxValue = maxValueWithUpperLimit(minPosScaled, maxPosScaled, maxDomainValue); + return [minValue, maxValue]; } diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 7d31bba51c..b99423cd44 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -333,6 +333,30 @@ export interface SettingsSpec extends Spec { * @defaultValue 2 */ minBrushDelta?: number; + /** + * Boolean to round brushed values to nearest step bounds. + * + * e.g. + * A brush selection range of [1.23, 3.6] with a domain of [1, 2, 3, 4]. + * + * - when true returns [1, 3] + * - when false returns [1.23, 3.6] + * + * @defaultValue false + */ + roundHistogramBrushValues?: boolean; + /** + * Boolean to allow brushing on last bucket even when outside domain or limit to end of domain. + * + * e.g. + * A brush selection range of [1.23, 3.6] with a domain of [1, 2, 3] + * + * - when true returns [1.23, 3.6] + * - when false returns [1.23, 3] + * + * @defaultValue false + */ + allowBrushingLastHistogramBucket?: boolean; } export type DefaultSettingsProps = diff --git a/stories/interactions/10_brush_selection_bar.tsx b/stories/interactions/10_brush_selection_bar.tsx index 061166d2da..ca309b727b 100644 --- a/stories/interactions/10_brush_selection_bar.tsx +++ b/stories/interactions/10_brush_selection_bar.tsx @@ -21,27 +21,55 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '../../src'; +import { isVerticalRotation } from '../../src/chart_types/xy_chart/state/utils/common'; import { getChartRotationKnob } from '../utils/knobs'; -export const Example = () => ( - - - - Number(d).toFixed(2)} /> - - Number(d).toFixed(2)} /> +export const Example = () => { + const rotation = getChartRotationKnob(); + const isVertical = isVerticalRotation(rotation); - - -); + return ( + + + Number(d).toFixed(2) : undefined} + /> + Number(d).toFixed(2) : undefined} + /> + Number(d).toFixed(2) : undefined} + /> + Number(d).toFixed(2) : undefined} + /> + + + + ); +}; diff --git a/stories/interactions/10a_brush_selection_bar_hist.tsx b/stories/interactions/10a_brush_selection_bar_hist.tsx new file mode 100644 index 0000000000..d62858e590 --- /dev/null +++ b/stories/interactions/10a_brush_selection_bar_hist.tsx @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { action } from '@storybook/addon-actions'; +import { boolean } from '@storybook/addon-knobs'; +import React from 'react'; + +import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '../../src'; +import { getChartRotationKnob } from '../utils/knobs'; + +export const Example = () => ( + + + + Number(d).toFixed(2)} /> + + Number(d).toFixed(2)} /> + + + +); diff --git a/stories/interactions/12_brush_time_hist.tsx b/stories/interactions/12_brush_time_hist.tsx index 306fea162d..8d23bbf309 100644 --- a/stories/interactions/12_brush_time_hist.tsx +++ b/stories/interactions/12_brush_time_hist.tsx @@ -23,14 +23,19 @@ import { DateTime } from 'luxon'; import React from 'react'; import { Axis, Chart, niceTimeFormatter, Position, ScaleType, Settings, HistogramBarSeries } from '../../src'; +import { isVerticalRotation } from '../../src/chart_types/xy_chart/state/utils/common'; import { getChartRotationKnob } from '../utils/knobs'; export const Example = () => { + const rotation = getChartRotationKnob(); + const isVertical = isVerticalRotation(rotation); const now = DateTime.fromISO('2019-01-11T00:00:00.000') .setZone('utc+1') .toMillis(); const oneDay = 1000 * 60 * 60 * 24; - const formatter = niceTimeFormatter([now, now + oneDay * 5]); + const dateFormatter = niceTimeFormatter([now, now + oneDay * 5]); + const numberFormatter = (d: any) => Number(d).toFixed(2); + return ( { if (!x) { return; } - action('onBrushEnd')(formatter(x[0]), formatter(x[1])); + action('onBrushEnd')(dateFormatter(x[0]), dateFormatter(x[1])); }} onElementClick={action('onElementClick')} rotation={getChartRotationKnob()} + roundHistogramBrushValues={boolean('roundHistogramBrushValues', false)} + allowBrushingLastHistogramBucket={boolean('allowBrushingLastHistogramBucket', false)} + /> + - - Number(d).toFixed(2)} /> +