diff --git a/src/client/views/cube-view/cube-view.tsx b/src/client/views/cube-view/cube-view.tsx index 8bb759753..38378c2f5 100644 --- a/src/client/views/cube-view/cube-view.tsx +++ b/src/client/views/cube-view/cube-view.tsx @@ -36,7 +36,6 @@ import { Stage } from "../../../common/models/stage/stage"; import { TimeShift } from "../../../common/models/time-shift/time-shift"; import { Timekeeper } from "../../../common/models/timekeeper/timekeeper"; import { VisualizationManifest } from "../../../common/models/visualization-manifest/visualization-manifest"; -import { VisualizationProps } from "../../../common/models/visualization-props/visualization-props"; import { VisualizationSettings } from "../../../common/models/visualization-settings/visualization-settings"; import { Binary, Ternary } from "../../../common/utils/functional/functional"; import { Fn } from "../../../common/utils/general/general"; @@ -62,6 +61,7 @@ import { DragManager } from "../../utils/drag-manager/drag-manager"; import * as localStorage from "../../utils/local-storage/local-storage"; import tabularOptions from "../../utils/tabular-options/tabular-options"; import { getVisualizationComponent } from "../../visualizations"; +import { HighlightController } from "../../visualizations/highlight-controller/highlight-controller"; import { CubeContext, CubeContextValue } from "./cube-context"; import { CubeHeaderBar } from "./cube-header-bar/cube-header-bar"; import "./cube-view.scss"; @@ -732,7 +732,7 @@ export class CubeView extends React.Component { private visElement() { const { essence, visualizationStage: stage, lastRefreshRequestTimestamp } = this.state; if (!(essence.visResolve.isReady() && stage)) return null; - const visProps: VisualizationProps = { + const visProps = { refreshRequestTimestamp: lastRefreshRequestTimestamp, essence, clicker: this.clicker, @@ -743,6 +743,9 @@ export class CubeView extends React.Component { } }; - return React.createElement(getVisualizationComponent(essence.visualization), visProps); + return + {highlightProps => + React.createElement(getVisualizationComponent(essence.visualization), { ...visProps, ...highlightProps })} + ; } } diff --git a/src/client/visualizations/bar-chart/bar-chart.tsx b/src/client/visualizations/bar-chart/bar-chart.tsx index 747f9961b..d28adc035 100644 --- a/src/client/visualizations/bar-chart/bar-chart.tsx +++ b/src/client/visualizations/bar-chart/bar-chart.tsx @@ -22,7 +22,13 @@ import * as React from "react"; import { DateRange } from "../../../common/models/date-range/date-range"; import { canBucketByDefault, Dimension } from "../../../common/models/dimension/dimension"; import { findDimensionByName } from "../../../common/models/dimension/dimensions"; -import { FilterClause, FixedTimeFilterClause, NumberFilterClause, StringFilterAction, StringFilterClause } from "../../../common/models/filter-clause/filter-clause"; +import { + FilterClause, + FixedTimeFilterClause, + NumberFilterClause, + StringFilterAction, + StringFilterClause +} from "../../../common/models/filter-clause/filter-clause"; import { Measure } from "../../../common/models/measure/measure"; import { ConcreteSeries, SeriesDerivation } from "../../../common/models/series/concrete-series"; import { Series } from "../../../common/models/series/series"; @@ -46,6 +52,7 @@ import { VisMeasureLabel } from "../../components/vis-measure-label/vis-measure- import { SPLIT, VIS_H_PADDING } from "../../config/constants"; import { classNames, roundToPx } from "../../utils/dom/dom"; import { BaseVisualization, BaseVisualizationState } from "../base-visualization/base-visualization"; +import { hasHighlightOn } from "../highlight-controller/highlight-controller"; import "./bar-chart.scss"; import { BarCoordinates } from "./bar-coordinates"; import { BarChart as ImprovedBarChart } from "./improved-bar-chart/bar-chart"; @@ -252,14 +259,14 @@ export class BarChart extends BaseVisualization { }; onClick = (x: number, y: number) => { - const { essence } = this.props; + const { essence, highlight, dropHighlight, saveHighlight } = this.props; const selectionInfo = this.calculateMousePosition(x, y); if (!selectionInfo) return; if (!selectionInfo.coordinates) { - this.dropHighlight(); + dropHighlight(); this.setState({ selectionInfo: null }); return; } @@ -272,17 +279,16 @@ export class BarChart extends BaseVisualization { const rowHighlight = getFilterFromDatum(splits, path); const currentSeries = series.get(chartIndex).definition; - if (this.highlightOn(currentSeries.key())) { - const delta = this.getHighlightClauses(); - if (rowHighlight.equals(delta)) { - this.dropHighlight(); + if (hasHighlightOn(highlight, currentSeries.key())) { + if (rowHighlight.equals(highlight.clauses)) { + dropHighlight(); this.setState({ selectionInfo: null }); return; } } this.setState({ selectionInfo }); - this.highlight(rowHighlight, series.get(chartIndex).definition.key()); + saveHighlight(rowHighlight, series.get(chartIndex).definition.key()); }; getYExtent(data: Datum[], series: ConcreteSeries): number[] { @@ -393,6 +399,7 @@ export class BarChart extends BaseVisualization { } renderSelectionBubble(hoverInfo: BubbleInfo): JSX.Element { + const { dropHighlight, acceptHighlight } = this.props; const { series, path, chartIndex, segmentLabel, coordinates } = hoverInfo; const chartStage = this.getSingleChartStage(); const leftOffset = this.getBubbleLeftOffset(coordinates.middleX); @@ -403,8 +410,8 @@ export class BarChart extends BaseVisualization { return {segmentValue} ; @@ -413,7 +420,6 @@ export class BarChart extends BaseVisualization { renderHoverBubble(hoverInfo: BubbleInfo): JSX.Element { const chartStage = this.getSingleChartStage(); const { series, path, chartIndex, segmentLabel, coordinates } = hoverInfo; - const { essence } = this.props; const leftOffset = this.getBubbleLeftOffset(coordinates.middleX); const topOffset = this.getBubbleTopOffset(coordinates.y, chartIndex, chartStage); @@ -445,17 +451,17 @@ export class BarChart extends BaseVisualization { } isSelected(path: Datum[], series: Series): boolean { - const { essence } = this.props; + const { essence, highlight } = this.props; const { splits } = essence; - return this.highlightOn(series.key()) && this.getHighlightClauses().equals(getFilterFromDatum(splits, path)); + return hasHighlightOn(highlight, series.key()) && highlight.clauses.equals(getFilterFromDatum(splits, path)); } isFaded(): boolean { - return this.hasHighlight(); + return this.props.highlight !== null; } hasAnySelectionGoingOn(): boolean { - return this.hasHighlight(); + return this.props.highlight !== null; } isHovered(path: Datum[], series: ConcreteSeries): boolean { @@ -901,15 +907,15 @@ export class BarChart extends BaseVisualization { } renderInternals(dataset: Dataset) { - const { essence, stage } = this.props; + const { essence, stage, highlight, saveHighlight, acceptHighlight, dropHighlight } = this.props; const { splits } = essence; const newVersionSupports = or(Predicates.areExactSplitKinds("time"), Predicates.areExactSplitKinds("*", "time")); if (newVersionSupports(essence)) { return ; diff --git a/src/client/visualizations/base-visualization/base-visualization.tsx b/src/client/visualizations/base-visualization/base-visualization.tsx index c836efd2c..09a7d621c 100644 --- a/src/client/visualizations/base-visualization/base-visualization.tsx +++ b/src/client/visualizations/base-visualization/base-visualization.tsx @@ -15,14 +15,21 @@ * limitations under the License. */ -import { List } from "immutable"; import { Dataset, Expression } from "plywood"; import * as React from "react"; import { Essence } from "../../../common/models/essence/essence"; -import { FilterClause } from "../../../common/models/filter-clause/filter-clause"; import { Timekeeper } from "../../../common/models/timekeeper/timekeeper"; import { Visualization } from "../../../common/models/visualization-manifest/visualization-manifest"; -import { DatasetLoad, error, isError, isLoaded, isLoading, loaded, loading, VisualizationProps } from "../../../common/models/visualization-props/visualization-props"; +import { + DatasetLoad, + error, + isError, + isLoaded, + isLoading, + loaded, + loading, + VisualizationProps +} from "../../../common/models/visualization-props/visualization-props"; import { debounceWithPromise } from "../../../common/utils/functional/functional"; import makeQuery from "../../../common/utils/query/visualization-query"; import { Loader } from "../../components/loader/loader"; @@ -163,40 +170,6 @@ export class BaseVisualization extends React.C return null; } - protected getHighlight(): Highlight | null { - return this.state.highlight; - } - - protected hasHighlight(): boolean { - return this.state.highlight !== null; - } - - protected highlightOn(key: string): boolean { - const highlight = this.getHighlight(); - if (!highlight) return false; - return highlight.key === key; - } - - protected getHighlightClauses(): List { - const highlight = this.getHighlight(); - if (!highlight) return null; - return highlight.clauses; - } - - protected dropHighlight = () => this.setState({ highlight: null }); - - protected acceptHighlight = () => { - if (!this.hasHighlight()) return; - const { essence, clicker } = this.props; - clicker.changeFilter(essence.filter.mergeClauses(this.getHighlightClauses())); - this.setState({ highlight: null }); - }; - - protected highlight = (clauses: List, key: string | null = null) => { - const highlight = new Highlight(clauses, key); - this.setState({ highlight }); - }; - deriveDatasetState(dataset: Dataset): Partial { return {}; } diff --git a/src/client/visualizations/heat-map/heat-map.tsx b/src/client/visualizations/heat-map/heat-map.tsx index 97de2afd4..a1e49bc1a 100644 --- a/src/client/visualizations/heat-map/heat-map.tsx +++ b/src/client/visualizations/heat-map/heat-map.tsx @@ -46,7 +46,7 @@ export class HeatMap extends BaseVisualization { } renderInternals() { - const { essence, stage } = this.props; + const { essence, stage, highlight, saveHighlight, acceptHighlight, dropHighlight } = this.props; const { preparedDataset: dataset } = this.state; @@ -59,10 +59,10 @@ export class HeatMap extends BaseVisualization { xScale={x} yScale={y} colorScale={color} - saveHighlight={this.highlight} - highlight={this.getHighlight()} - acceptHighlight={this.acceptHighlight} - dropHighlight={this.dropHighlight} + saveHighlight={saveHighlight} + highlight={highlight} + acceptHighlight={acceptHighlight} + dropHighlight={dropHighlight} essence={essence} /> ; diff --git a/src/client/visualizations/highlight-controller/highlight-controller.tsx b/src/client/visualizations/highlight-controller/highlight-controller.tsx new file mode 100644 index 000000000..f6e0a9b57 --- /dev/null +++ b/src/client/visualizations/highlight-controller/highlight-controller.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2017-2021 Allegro.pl + * + * Licensed 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 { List } from "immutable"; +import * as React from "react"; +import { ReactNode } from "react"; +import { Clicker } from "../../../common/models/clicker/clicker"; +import { Essence } from "../../../common/models/essence/essence"; +import { FilterClause } from "../../../common/models/filter-clause/filter-clause"; +import { Binary, Nullary, Unary } from "../../../common/utils/functional/functional"; +import { Highlight } from "../base-visualization/highlight"; + +interface HighlightProps { + dropHighlight: Nullary; + acceptHighlight: Nullary; + highlight: Highlight | null; + saveHighlight: Binary, string | null, void>; +} + +interface HighlightControllerProps { + essence: Essence; + clicker: Clicker; + children: Unary; +} + +interface HighlightControllerState { + highlight: Highlight | null; +} + +export function hasHighlightOn(highlight: Highlight | null, key: string): boolean { + if (!highlight) return false; + return highlight.key === key; +} + +export class HighlightController extends React.Component { + state: HighlightControllerState = { highlight: null }; + + private dropHighlight = () => this.setState({ highlight: null }); + + private acceptHighlight = () => { + const { highlight } = this.state; + if (highlight === null) return; + const { essence, clicker } = this.props; + clicker.changeFilter(essence.filter.mergeClauses(highlight.clauses)); + this.setState({ highlight: null }); + }; + + private saveHighlight = (clauses: List, key: string | null = null) => { + const highlight = new Highlight(clauses, key); + this.setState({ highlight }); + }; + + render() { + const { children } = this.props; + const { highlight } = this.state; + const highlightProps = { + dropHighlight: this.dropHighlight, + acceptHighlight: this.acceptHighlight, + saveHighlight: this.saveHighlight, + highlight + }; + return children(highlightProps); + } +} diff --git a/src/client/visualizations/line-chart/line-chart.tsx b/src/client/visualizations/line-chart/line-chart.tsx index b9282586d..1fee3c212 100644 --- a/src/client/visualizations/line-chart/line-chart.tsx +++ b/src/client/visualizations/line-chart/line-chart.tsx @@ -36,7 +36,7 @@ export class LineChart extends BaseVisualization { private chartsRef = React.createRef(); protected renderInternals(dataset: Dataset): JSX.Element { - const { essence, timekeeper, stage } = this.props; + const { essence, timekeeper, stage, highlight, dropHighlight, acceptHighlight, saveHighlight } = this.props; const range = calculateXRange(essence, timekeeper, dataset); if (!range) { @@ -52,10 +52,10 @@ export class LineChart extends BaseVisualization { xScale={scale} chartsContainerRef={this.chartsRef} essence={essence} - highlight={this.getHighlight()} - dropHighlight={this.dropHighlight} - acceptHighlight={this.acceptHighlight} - saveHighlight={this.highlight}> + highlight={highlight} + dropHighlight={dropHighlight} + acceptHighlight={acceptHighlight} + saveHighlight={saveHighlight}> {interactions => { return
diff --git a/src/client/visualizations/table/table.tsx b/src/client/visualizations/table/table.tsx index 9a17bc36d..37e7133c6 100644 --- a/src/client/visualizations/table/table.tsx +++ b/src/client/visualizations/table/table.tsx @@ -108,18 +108,18 @@ export class Table extends BaseVisualization { } private highlightRow(datum: Datum) { - const { essence: { splits } } = this.props; + const { essence: { splits }, highlight, saveHighlight, dropHighlight } = this.props; const rowHighlight = getFilterFromDatum(splits, datum); if (!rowHighlight) return; - const alreadyHighlighted = this.hasHighlight() && rowHighlight.equals(this.getHighlightClauses()); + const alreadyHighlighted = highlight !== null && rowHighlight.equals(highlight.clauses); if (alreadyHighlighted) { - this.dropHighlight(); + dropHighlight(); return; } - this.highlight(rowHighlight, null); + saveHighlight(rowHighlight, null); } private calculateMousePosition(x: number, y: number, part: ScrollerPart): PositionHover { @@ -208,17 +208,17 @@ export class Table extends BaseVisualization { } private highlightedRowIndex(flatData?: PseudoDatum[]): number | null { - const { essence } = this.props; + const { essence, highlight } = this.props; if (!flatData) return null; - if (!this.hasHighlight()) return null; + if (highlight === null) return null; const { splits } = essence; - const index = flatData.findIndex(d => this.getHighlightClauses().equals(getFilterFromDatum(splits, d))); + const index = flatData.findIndex(d => highlight.clauses.equals(getFilterFromDatum(splits, d))); if (index >= 0) return index; return null; } protected renderInternals() { - const { essence, stage } = this.props; + const { essence, stage, acceptHighlight, dropHighlight } = this.props; const { flatData, scrollTop, hoverRow, segmentWidth } = this.state; const collapseRows = this.shouldCollapseRows(); @@ -304,8 +304,8 @@ export class Table extends BaseVisualization { title={nestedSplitName(flatData[highlightedRowIndex], essence)} left={stage.x + stage.width / 2} top={stage.y + HEADER_HEIGHT + (highlightedRowIndex * ROW_HEIGHT) - scrollTop - HIGHLIGHT_BUBBLE_V_OFFSET} - acceptHighlight={this.acceptHighlight} - dropHighlight={this.dropHighlight} />} + acceptHighlight={acceptHighlight} + dropHighlight={dropHighlight} />}
; } } diff --git a/src/common/models/visualization-props/visualization-props.ts b/src/common/models/visualization-props/visualization-props.ts index 443483ed9..93e4e6b51 100644 --- a/src/common/models/visualization-props/visualization-props.ts +++ b/src/common/models/visualization-props/visualization-props.ts @@ -15,9 +15,13 @@ * limitations under the License. */ +import { List } from "immutable"; import { Dataset } from "plywood"; +import { Highlight } from "../../../client/visualizations/base-visualization/highlight"; +import { Nullary } from "../../utils/functional/functional"; import { Clicker } from "../clicker/clicker"; import { Essence } from "../essence/essence"; +import { FilterClause } from "../filter-clause/filter-clause"; import { Stage } from "../stage/stage"; import { Timekeeper } from "../timekeeper/timekeeper"; @@ -28,6 +32,10 @@ export interface VisualizationProps { stage: Stage; registerDownloadableDataset?: (dataset: Dataset) => void; refreshRequestTimestamp: number; + dropHighlight: Nullary; + acceptHighlight: Nullary; + highlight: Highlight | null; + saveHighlight: (clauses: List, key?: string) => void; } enum DatasetLoadStatus { LOADED, LOADING, ERROR }