From 8abd037090116e9e4f9da5f345ef7762dcfe9841 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Mon, 4 Oct 2021 18:10:36 +0200 Subject: [PATCH 01/13] Add VisualizationControls and use it in Grid component. --- .../components/split-tile/split-tile.tsx | 14 ++++-- .../components/split-tile/split-tiles-row.tsx | 13 +++++- .../components/split-tile/split-tiles.tsx | 19 +++++++- .../cube-view/center-panel/center-panel.tsx | 19 ++++++-- .../visualizations/bar-chart/bar-chart.tsx | 4 +- src/client/visualizations/grid/grid.tsx | 5 ++- .../grid/visualization-controls.tsx | 43 +++++++++++++++++++ .../visualizations/heat-map/heat-map.tsx | 4 +- .../visualizations/line-chart/line-chart.tsx | 4 +- src/client/visualizations/table/table.tsx | 4 +- src/client/visualizations/totals/totals.tsx | 4 +- 11 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 src/client/visualizations/grid/visualization-controls.tsx diff --git a/src/client/components/split-tile/split-tile.tsx b/src/client/components/split-tile/split-tile.tsx index 15af9b6e0..cfc818be9 100644 --- a/src/client/components/split-tile/split-tile.tsx +++ b/src/client/components/split-tile/split-tile.tsx @@ -23,11 +23,11 @@ import { Stage } from "../../../common/models/stage/stage"; import { Binary, Ternary, Unary } from "../../../common/utils/functional/functional"; import { Fn } from "../../../common/utils/general/general"; import { classNames } from "../../utils/dom/dom"; -import { SplitMenu } from "../split-menu/split-menu"; +import { SplitMenu, SplitMenuProps } from "../split-menu/split-menu"; import { SvgIcon } from "../svg-icon/svg-icon"; import { WithRef } from "../with-ref/with-ref"; -interface SplitTileProps { +export interface SplitTileBaseProps { essence: Essence; split: Split; dimension: Dimension; @@ -41,10 +41,18 @@ interface SplitTileProps { containerStage: Stage; } +interface SplitTileProps extends SplitTileBaseProps { + splitMenuComponent: React.ComponentType; +} + const SPLIT_CLASS_NAME = "split"; +export const DefaultSplitTile: React.SFC = props => { + return ; +}; + export const SplitTile: React.SFC = props => { - const { essence, open, split, dimension, style, removeSplit, updateSplit, openMenu, closeMenu, dragStart, containerStage } = props; + const { splitMenuComponent: SplitMenu, essence, open, split, dimension, style, removeSplit, updateSplit, openMenu, closeMenu, dragStart, containerStage } = props; const title = split.getTitle(dimension); diff --git a/src/client/components/split-tile/split-tiles-row.tsx b/src/client/components/split-tile/split-tiles-row.tsx index ea9a82bb9..7dec5d732 100644 --- a/src/client/components/split-tile/split-tiles-row.tsx +++ b/src/client/components/split-tile/split-tiles-row.tsx @@ -28,21 +28,29 @@ import { DraggedElementType, DragManager } from "../../utils/drag-manager/drag-m import { getMaxItems } from "../../utils/pill-tile/pill-tile"; import { DragIndicator } from "../drag-indicator/drag-indicator"; import { AddSplit } from "./add-split"; +import { DefaultSplitTile, SplitTileBaseProps } from "./split-tile"; import "./split-tile.scss"; import { SplitTiles } from "./split-tiles"; -interface SplitTilesRowProps { +export interface SplitTilesRowBaseProps { clicker: Clicker; essence: Essence; menuStage: Stage; } +interface SplitTilesRowProps extends SplitTilesRowBaseProps { + splitTileComponent: React.ComponentType; +} + interface SplitTilesRowState { dragPosition?: DragPosition; openedSplit?: Split; overflowOpen?: boolean; } +export const DefaultSplitTilesRow: React.SFC = props => + ; + export class SplitTilesRow extends React.Component { private items = React.createRef(); @@ -189,13 +197,14 @@ export class SplitTilesRow extends React.Component
{STRINGS.split}
; } export const SplitTiles: React.SFC = props => { - const { overflowOpen, closeOverflowMenu, openOverflowMenu, essence, maxItems, removeSplit, updateSplit, openedSplit, openMenu, closeMenu, dragStart, menuStage } = props; + const { + splitTileComponent: SplitTile, + overflowOpen, + closeOverflowMenu, + openOverflowMenu, + essence, + maxItems, + removeSplit, + updateSplit, + openedSplit, + openMenu, + closeMenu, + dragStart, + menuStage + } = props; const splits = essence.splits.splits.toArray(); diff --git a/src/client/views/cube-view/center-panel/center-panel.tsx b/src/client/views/cube-view/center-panel/center-panel.tsx index 69627a51e..297306b34 100644 --- a/src/client/views/cube-view/center-panel/center-panel.tsx +++ b/src/client/views/cube-view/center-panel/center-panel.tsx @@ -30,14 +30,18 @@ import { DropIndicator } from "../../../components/drop-indicator/drop-indicator import { FilterTilesRow } from "../../../components/filter-tile/filter-tiles-row"; import { ManualFallback } from "../../../components/manual-fallback/manual-fallback"; import { SeriesTilesRow } from "../../../components/series-tile/series-tiles-row"; -import { SplitTilesRow } from "../../../components/split-tile/split-tiles-row"; +import { + DefaultSplitTilesRow, + SplitTilesRow, + SplitTilesRowBaseProps +} from "../../../components/split-tile/split-tiles-row"; import { VisSelector } from "../../../components/vis-selector/vis-selector"; import { classNames } from "../../../utils/dom/dom"; import { DataProvider } from "../../../visualizations/data-provider/data-provider"; import { HighlightController } from "../../../visualizations/highlight-controller/highlight-controller"; import { PartialFilter, PartialSeries } from "../partial-tiles-provider"; -interface VisualizationControlsProps { +export interface VisualizationControlsBaseProps { essence: Essence; clicker: Clicker; stage: Stage; @@ -50,8 +54,17 @@ interface VisualizationControlsProps { removeTile: Fn; } +interface VisualizationControlsProps extends VisualizationControlsBaseProps { + splitTilesRow: React.ComponentType; +} + +export const DefaultVisualizationControls: React.SFC = props => { + return ; +}; + export const VisualizationControls: React.SFC = props => { const { + splitTilesRow: SplitTilesRow, addSeries, addFilter, clicker, @@ -177,5 +190,5 @@ function ChartWrapper(props: ChartWrapperProps) { ; } -type VisualizationPanelProps = ChartPanelProps & VisualizationControlsProps; +type VisualizationPanelProps = ChartPanelProps & VisualizationControlsBaseProps; export type VisualizationProps = Omit; diff --git a/src/client/visualizations/bar-chart/bar-chart.tsx b/src/client/visualizations/bar-chart/bar-chart.tsx index 4c98a9031..df5b228c2 100644 --- a/src/client/visualizations/bar-chart/bar-chart.tsx +++ b/src/client/visualizations/bar-chart/bar-chart.tsx @@ -51,7 +51,7 @@ import { VerticalAxis } from "../../components/vertical-axis/vertical-axis"; import { VisMeasureLabel } from "../../components/vis-measure-label/vis-measure-label"; import { SPLIT, VIS_H_PADDING } from "../../config/constants"; import { classNames, roundToPx } from "../../utils/dom/dom"; -import { ChartPanel, VisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; +import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import { hasHighlightOn } from "../highlight-controller/highlight-controller"; import "./bar-chart.scss"; import { BarCoordinates } from "./bar-coordinates"; @@ -158,7 +158,7 @@ function padDataset(originalDataset: Dataset, dimension: Dimension, measures: Me export function BarChartVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/grid/grid.tsx b/src/client/visualizations/grid/grid.tsx index 9e65505ac..5128bc0d7 100644 --- a/src/client/visualizations/grid/grid.tsx +++ b/src/client/visualizations/grid/grid.tsx @@ -36,9 +36,10 @@ import { FlattenedSplits } from "../../components/tabular-scroller/splits/flatte import { measureColumnsCount } from "../../components/tabular-scroller/utils/measure-columns-count"; import { visibleIndexRange } from "../../components/tabular-scroller/visible-rows/visible-index-range"; import { selectFirstSplitDatums } from "../../utils/dataset/selectors/selectors"; -import { ChartPanel, VisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; +import { ChartPanel, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import "./grid.scss"; import { MeasureRows } from "./measure-rows"; +import { GridVisualizationControls } from "./visualization-controls"; interface GridState { segmentWidth: number; @@ -47,7 +48,7 @@ interface GridState { export function GridVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/grid/visualization-controls.tsx b/src/client/visualizations/grid/visualization-controls.tsx new file mode 100644 index 000000000..851eed2fd --- /dev/null +++ b/src/client/visualizations/grid/visualization-controls.tsx @@ -0,0 +1,43 @@ +/* + * 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 * as React from "react"; +import { SplitMenu, SplitMenuProps } from "../../components/split-menu/split-menu"; +import { SplitTile, SplitTileBaseProps } from "../../components/split-tile/split-tile"; +import { SplitTilesRow, SplitTilesRowBaseProps } from "../../components/split-tile/split-tiles-row"; +import { + VisualizationControls, + VisualizationControlsBaseProps +} from "../../views/cube-view/center-panel/center-panel"; + +export const GridVisualizationControls: React.SFC = props => { + return ; +}; + +function GridSplitTilesRow(props: SplitTilesRowBaseProps) { + return ; +} + +function GridSplitTile(props: SplitTileBaseProps) { + return ; +} + +// TODO: Really implement this menu! +function GridSplitMenu(props: SplitMenuProps) { + return +
GRIIIIIID!
+ +
; +} diff --git a/src/client/visualizations/heat-map/heat-map.tsx b/src/client/visualizations/heat-map/heat-map.tsx index 74b53d8cb..240b8bb26 100644 --- a/src/client/visualizations/heat-map/heat-map.tsx +++ b/src/client/visualizations/heat-map/heat-map.tsx @@ -30,14 +30,14 @@ import { Split } from "../../../common/models/split/split"; import { HEAT_MAP_MANIFEST } from "../../../common/visualization-manifests/heat-map/heat-map"; import { SPLIT } from "../../config/constants"; import { fillDatasetWithMissingValues } from "../../utils/dataset/sparse-dataset/dataset"; -import { ChartPanel, VisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; +import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import "./heat-map.scss"; import { LabelledHeatmap, TILE_SIZE } from "./labeled-heatmap"; import scales from "./utils/scales"; export function HeatMapVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/line-chart/line-chart.tsx b/src/client/visualizations/line-chart/line-chart.tsx index 985f6dab5..0021569d5 100644 --- a/src/client/visualizations/line-chart/line-chart.tsx +++ b/src/client/visualizations/line-chart/line-chart.tsx @@ -19,7 +19,7 @@ import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; import { LINE_CHART_MANIFEST } from "../../../common/visualization-manifests/line-chart/line-chart"; import { MessageCard } from "../../components/message-card/message-card"; -import { ChartPanel, VisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; +import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import { Charts } from "./charts/charts"; import { InteractionController } from "./interactions/interaction-controller"; import "./line-chart.scss"; @@ -32,7 +32,7 @@ const X_AXIS_HEIGHT = 30; export function LineChartVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/table/table.tsx b/src/client/visualizations/table/table.tsx index a69b94459..fd482b7a7 100644 --- a/src/client/visualizations/table/table.tsx +++ b/src/client/visualizations/table/table.tsx @@ -20,14 +20,14 @@ import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; import { ImmutableRecord } from "../../../common/utils/immutable-utils/immutable-utils"; import { TableSettings } from "../../../common/visualization-manifests/table/settings"; -import { ChartPanel, VisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; +import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import { InteractionController } from "./interactions/interaction-controller"; import { ScrolledTable } from "./scrolled-table/scrolled-table"; import "./table.scss"; export function TableVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/totals/totals.tsx b/src/client/visualizations/totals/totals.tsx index 32db6b717..06856b072 100644 --- a/src/client/visualizations/totals/totals.tsx +++ b/src/client/visualizations/totals/totals.tsx @@ -17,7 +17,7 @@ import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; -import { ChartPanel, VisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; +import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import { Total } from "./total"; import "./totals.scss"; @@ -37,7 +37,7 @@ const BigNumbers: React.SFC = ({ essence, data }) => { export function TotalsVisualization(props: VisualizationProps) { return - + ; } From d986aaf6150073a53922bc8c7dde56db28532a98 Mon Sep 17 00:00:00 2001 From: adrianmroz <78143552+adrianmroz-allegro@users.noreply.github.com> Date: Thu, 7 Oct 2021 10:03:39 +0200 Subject: [PATCH 02/13] Parametrize DataProvider with query factory function (#802) --- .../cube-view/center-panel/center-panel.tsx | 11 ++++-- .../visualizations/bar-chart/bar-chart.tsx | 3 +- .../data-provider/data-provider.tsx | 34 ++++++++----------- src/client/visualizations/grid/grid.tsx | 3 +- .../visualizations/heat-map/heat-map.tsx | 3 +- .../visualizations/line-chart/line-chart.tsx | 3 +- src/client/visualizations/table/table.tsx | 3 +- src/client/visualizations/totals/totals.tsx | 3 +- 8 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/client/views/cube-view/center-panel/center-panel.tsx b/src/client/views/cube-view/center-panel/center-panel.tsx index 297306b34..4243abb38 100644 --- a/src/client/views/cube-view/center-panel/center-panel.tsx +++ b/src/client/views/cube-view/center-panel/center-panel.tsx @@ -37,7 +37,7 @@ import { } from "../../../components/split-tile/split-tiles-row"; import { VisSelector } from "../../../components/vis-selector/vis-selector"; import { classNames } from "../../../utils/dom/dom"; -import { DataProvider } from "../../../visualizations/data-provider/data-provider"; +import { DataProvider, QueryFactory } from "../../../visualizations/data-provider/data-provider"; import { HighlightController } from "../../../visualizations/highlight-controller/highlight-controller"; import { PartialFilter, PartialSeries } from "../partial-tiles-provider"; @@ -106,6 +106,7 @@ interface ChartPanelProps { clicker: Clicker; stage: Stage; chartComponent: React.ComponentType; + queryFactory: QueryFactory; timekeeper: Timekeeper; lastRefreshRequestTimestamp: number; dragEnter: Unary, void>; @@ -118,6 +119,7 @@ interface ChartPanelProps { export const ChartPanel: React.SFC = props => { const { chartComponent, + queryFactory, essence, clicker, timekeeper, @@ -136,6 +138,7 @@ export const ChartPanel: React.SFC = props => {
; function ChartWrapper(props: ChartWrapperProps) { - const { chartComponent: ChartComponent, essence, clicker, timekeeper, stage, lastRefreshRequestTimestamp } = props; + const { chartComponent: ChartComponent, queryFactory, essence, clicker, timekeeper, stage, lastRefreshRequestTimestamp } = props; if (essence.visResolve.isManual()) { return ; } @@ -174,6 +178,7 @@ function ChartWrapper(props: ChartWrapperProps) { {highlightProps => @@ -191,4 +196,4 @@ function ChartWrapper(props: ChartWrapperProps) { } type VisualizationPanelProps = ChartPanelProps & VisualizationControlsBaseProps; -export type VisualizationProps = Omit; +export type VisualizationProps = Omit; diff --git a/src/client/visualizations/bar-chart/bar-chart.tsx b/src/client/visualizations/bar-chart/bar-chart.tsx index df5b228c2..02c4a63e3 100644 --- a/src/client/visualizations/bar-chart/bar-chart.tsx +++ b/src/client/visualizations/bar-chart/bar-chart.tsx @@ -39,6 +39,7 @@ import { Splits } from "../../../common/models/splits/splits"; import { Stage } from "../../../common/models/stage/stage"; import { formatValue } from "../../../common/utils/formatter/formatter"; import { or } from "../../../common/utils/functional/functional"; +import makeQuery from "../../../common/utils/query/visualization-query"; import { Predicates } from "../../../common/utils/rules/predicates"; import { BAR_CHART_MANIFEST } from "../../../common/visualization-manifests/bar-chart/bar-chart"; import { BucketMarks } from "../../components/bucket-marks/bucket-marks"; @@ -159,7 +160,7 @@ function padDataset(originalDataset: Dataset, dimension: Dimension, measures: Me export function BarChartVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/data-provider/data-provider.tsx b/src/client/visualizations/data-provider/data-provider.tsx index cb38c7173..e048add65 100644 --- a/src/client/visualizations/data-provider/data-provider.tsx +++ b/src/client/visualizations/data-provider/data-provider.tsx @@ -16,7 +16,6 @@ import { Dataset, Expression } from "plywood"; import * as React from "react"; -import { ReactNode } from "react"; import { DatasetRequest, DatasetRequestStatus, @@ -29,20 +28,21 @@ import { import { Essence } from "../../../common/models/essence/essence"; import { Stage } from "../../../common/models/stage/stage"; import { Timekeeper } from "../../../common/models/timekeeper/timekeeper"; -import { debounceWithPromise, Unary } from "../../../common/utils/functional/functional"; -import visualizationQuery from "../../../common/utils/query/visualization-query"; +import { Binary, debounceWithPromise, Unary } from "../../../common/utils/functional/functional"; import { Loader } from "../../components/loader/loader"; import { QueryError } from "../../components/query-error/query-error"; import { reportError } from "../../utils/error-reporter/error-reporter"; import { DownloadableDataset, DownloadableDatasetContext } from "../../views/cube-view/downloadable-dataset-context"; -import gridQuery from "../grid/make-query"; + +export type QueryFactory = Binary; interface DataProviderProps { refreshRequestTimestamp: number; essence: Essence; timekeeper: Timekeeper; stage: Stage; - children: Unary; + queryFactory: QueryFactory; + children: Unary; } interface DataProviderState { @@ -58,8 +58,8 @@ export class DataProvider extends React.Component { // TODO: encode it better // null is here when we get out of order request, so we just ignore it @@ -92,17 +92,13 @@ export class DataProvider extends React.Component { + private fetchData(essence: Essence, timekeeper: Timekeeper, queryFactory: QueryFactory): Promise { this.lastQueryEssence = essence; - return this.debouncedCallExecutor(essence, timekeeper); - } - - protected getQuery(essence: Essence, timekeeper: Timekeeper): Expression { - return essence.visualization.name === "grid" ? gridQuery(essence, timekeeper) : visualizationQuery(essence, timekeeper); + return this.debouncedCallExecutor(essence, timekeeper, queryFactory); } - private callExecutor = (essence: Essence, timekeeper: Timekeeper): Promise => - essence.dataCube.executor(this.getQuery(essence, timekeeper), { timezone: essence.timezone }) + private callExecutor = (essence: Essence, timekeeper: Timekeeper, queryFactory: QueryFactory): Promise => + essence.dataCube.executor(queryFactory(essence, timekeeper), { timezone: essence.timezone }) .then((dataset: Dataset) => { // signal out of order requests with null if (!this.wasUsedForLastQuery(essence)) return null; diff --git a/src/client/visualizations/grid/grid.tsx b/src/client/visualizations/grid/grid.tsx index 5128bc0d7..c89b7680e 100644 --- a/src/client/visualizations/grid/grid.tsx +++ b/src/client/visualizations/grid/grid.tsx @@ -38,6 +38,7 @@ import { visibleIndexRange } from "../../components/tabular-scroller/visible-row import { selectFirstSplitDatums } from "../../utils/dataset/selectors/selectors"; import { ChartPanel, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import "./grid.scss"; +import makeQuery from "./make-query"; import { MeasureRows } from "./measure-rows"; import { GridVisualizationControls } from "./visualization-controls"; @@ -49,7 +50,7 @@ interface GridState { export function GridVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/heat-map/heat-map.tsx b/src/client/visualizations/heat-map/heat-map.tsx index 240b8bb26..da7ac3637 100644 --- a/src/client/visualizations/heat-map/heat-map.tsx +++ b/src/client/visualizations/heat-map/heat-map.tsx @@ -27,6 +27,7 @@ import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; import { ConcreteSeries } from "../../../common/models/series/concrete-series"; import { Split } from "../../../common/models/split/split"; +import makeQuery from "../../../common/utils/query/visualization-query"; import { HEAT_MAP_MANIFEST } from "../../../common/visualization-manifests/heat-map/heat-map"; import { SPLIT } from "../../config/constants"; import { fillDatasetWithMissingValues } from "../../utils/dataset/sparse-dataset/dataset"; @@ -38,7 +39,7 @@ import scales from "./utils/scales"; export function HeatMapVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/line-chart/line-chart.tsx b/src/client/visualizations/line-chart/line-chart.tsx index 0021569d5..a8183d7c7 100644 --- a/src/client/visualizations/line-chart/line-chart.tsx +++ b/src/client/visualizations/line-chart/line-chart.tsx @@ -17,6 +17,7 @@ import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; +import makeQuery from "../../../common/utils/query/visualization-query"; import { LINE_CHART_MANIFEST } from "../../../common/visualization-manifests/line-chart/line-chart"; import { MessageCard } from "../../components/message-card/message-card"; import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; @@ -33,7 +34,7 @@ const X_AXIS_HEIGHT = 30; export function LineChartVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/table/table.tsx b/src/client/visualizations/table/table.tsx index fd482b7a7..e0419d4d0 100644 --- a/src/client/visualizations/table/table.tsx +++ b/src/client/visualizations/table/table.tsx @@ -19,6 +19,7 @@ import { FlattenOptions, PseudoDatum } from "plywood"; import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; import { ImmutableRecord } from "../../../common/utils/immutable-utils/immutable-utils"; +import makeQuery from "../../../common/utils/query/visualization-query"; import { TableSettings } from "../../../common/visualization-manifests/table/settings"; import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import { InteractionController } from "./interactions/interaction-controller"; @@ -28,7 +29,7 @@ import "./table.scss"; export function TableVisualization(props: VisualizationProps) { return - + ; } diff --git a/src/client/visualizations/totals/totals.tsx b/src/client/visualizations/totals/totals.tsx index 06856b072..9b78079ca 100644 --- a/src/client/visualizations/totals/totals.tsx +++ b/src/client/visualizations/totals/totals.tsx @@ -17,6 +17,7 @@ import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; +import makeQuery from "../../../common/utils/query/visualization-query"; import { ChartPanel, DefaultVisualizationControls, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import { Total } from "./total"; import "./totals.scss"; @@ -38,6 +39,6 @@ const BigNumbers: React.SFC = ({ essence, data }) => { export function TotalsVisualization(props: VisualizationProps) { return - + ; } From 3762bc761431fb6762b75700e4991a08c8afa3d4 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Tue, 5 Oct 2021 22:13:32 +0200 Subject: [PATCH 03/13] Parametrize DataProvider with query factory function --- src/common/visualization-manifests/grid/grid.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/visualization-manifests/grid/grid.ts b/src/common/visualization-manifests/grid/grid.ts index 7da48f5e3..89738ae7e 100644 --- a/src/common/visualization-manifests/grid/grid.ts +++ b/src/common/visualization-manifests/grid/grid.ts @@ -22,6 +22,8 @@ import { Actions } from "../../utils/rules/actions"; import { Predicates } from "../../utils/rules/predicates"; import { visualizationDependentEvaluatorBuilder } from "../../utils/rules/visualization-dependent-evaluator"; +export const GRID_LIMITS = [50, 100, 200, 500, 1000]; + const rulesEvaluator = visualizationDependentEvaluatorBuilder .when(Predicates.noSplits()) .then(Actions.manualDimensionSelection("The Grid requires at least one split")) @@ -32,7 +34,7 @@ const rulesEvaluator = visualizationDependentEvaluatorBuilder .otherwise(({ isSelectedVisualization, splits, series }) => { const firstSeries = series.series.first(); const { limit: firstLimit, sort: firstSort } = splits.getSplit(0); - const safeFirstLimit = isFiniteNumber(firstLimit) ? firstLimit : 50; + const safeFirstLimit = isFiniteNumber(firstLimit) ? firstLimit : GRID_LIMITS[0]; const sort = firstSort instanceof SeriesSort ? firstSort : new SeriesSort({ reference: firstSeries.reference }); From 231484674ff16c70fdc6bc5976d84c746eacccf3 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Thu, 7 Oct 2021 13:18:37 +0200 Subject: [PATCH 04/13] SplitMenuBase --- .../components/split-menu/split-menu-base.tsx | 106 +++++++++++++++++ .../components/split-menu/split-menu.tsx | 110 +++++------------- 2 files changed, 137 insertions(+), 79 deletions(-) create mode 100644 src/client/components/split-menu/split-menu-base.tsx diff --git a/src/client/components/split-menu/split-menu-base.tsx b/src/client/components/split-menu/split-menu-base.tsx new file mode 100644 index 000000000..42e4bd626 --- /dev/null +++ b/src/client/components/split-menu/split-menu-base.tsx @@ -0,0 +1,106 @@ +/* + * 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 * as React from "react"; +import { Dimension } from "../../../common/models/dimension/dimension"; +import { coerceGranularity, isGranularityValid } from "../../../common/models/granularity/granularity"; +import { Sort } from "../../../common/models/sort/sort"; +import { Split } from "../../../common/models/split/split"; +import { Stage } from "../../../common/models/stage/stage"; +import { Fn } from "../../../common/utils/general/general"; +import { STRINGS } from "../../config/constants"; +import { enterKey } from "../../utils/dom/dom"; +import { BubbleMenu } from "../bubble-menu/bubble-menu"; +import { Button } from "../button/button"; + +interface SplitAssembly { + dimension: Dimension; + split: Split; + granularity?: string; + limit?: number; + sort?: Sort; +} + +export function validateSplit(splitAssembly: SplitAssembly): boolean { + const { granularity, split, dimension: { kind } } = splitAssembly; + if (!isGranularityValid(kind, granularity)) { + return false; + } + const newSplit = createSplit(splitAssembly); + return !split.equals(newSplit); +} + +export function createSplit({ + split: { type, reference }, + limit, + granularity, + sort, + dimension: { kind } + }: SplitAssembly): Split { + const bucket = coerceGranularity(granularity, kind); + return new Split({ type, reference, limit, sort, bucket }); +} + +interface SplitMenuBaseProps { + openOn: Element; + containerStage: Stage; + onClose: Fn; + onSave: Fn; + dimension: Dimension; + isValid: boolean; +} + +export class SplitMenuBase extends React.Component { + + componentDidMount() { + window.addEventListener("keydown", this.globalKeyDownListener); + } + + componentWillUnmount() { + window.removeEventListener("keydown", this.globalKeyDownListener); + } + + globalKeyDownListener = (e: KeyboardEvent) => enterKey(e) && this.onOkClick(); + + onCancelClick = () => this.props.onClose(); + + onOkClick = () => { + const { isValid, onSave, onClose } = this.props; + if (!isValid) return; + onSave(); + onClose(); + }; + + render() { + const { containerStage, openOn, dimension, onClose, children, isValid } = this.props; + if (!dimension) return null; + + return + {children} +
+
+
; + } +} diff --git a/src/client/components/split-menu/split-menu.tsx b/src/client/components/split-menu/split-menu.tsx index 1ee3a7a77..6e18b58cc 100644 --- a/src/client/components/split-menu/split-menu.tsx +++ b/src/client/components/split-menu/split-menu.tsx @@ -15,24 +15,20 @@ * limitations under the License. */ -import { Duration } from "chronoshift"; import * as React from "react"; import { Dimension, isContinuous } from "../../../common/models/dimension/dimension"; import { Essence } from "../../../common/models/essence/essence"; -import { granularityToString, isGranularityValid } from "../../../common/models/granularity/granularity"; +import { granularityToString } from "../../../common/models/granularity/granularity"; import { DimensionSortOn, SortOn } from "../../../common/models/sort-on/sort-on"; import { Sort } from "../../../common/models/sort/sort"; -import { Bucket, Split } from "../../../common/models/split/split"; +import { Split } from "../../../common/models/split/split"; import { Stage } from "../../../common/models/stage/stage"; import { Binary } from "../../../common/utils/functional/functional"; import { Fn } from "../../../common/utils/general/general"; -import { STRINGS } from "../../config/constants"; -import { enterKey } from "../../utils/dom/dom"; -import { BubbleMenu } from "../bubble-menu/bubble-menu"; -import { Button } from "../button/button"; import { GranularityPicker } from "./granularity-picker"; import { LimitDropdown } from "./limit-dropdown"; import { SortDropdown } from "./sort-dropdown"; +import { createSplit, SplitMenuBase, validateSplit } from "./split-menu-base"; import "./split-menu.scss"; export interface SplitMenuProps { @@ -46,7 +42,6 @@ export interface SplitMenuProps { } export interface SplitMenuState { - reference?: string; granularity?: string; sort?: Sort; limit?: number; @@ -58,112 +53,69 @@ export class SplitMenu extends React.Component { componentWillMount() { const { split } = this.props; - const { bucket, reference, sort, limit } = split; + const { bucket, sort, limit } = split; this.setState({ - reference, sort, limit, granularity: bucket && granularityToString(bucket) }); } - componentDidMount() { - window.addEventListener("keydown", this.globalKeyDownListener); - } - - componentWillUnmount() { - window.removeEventListener("keydown", this.globalKeyDownListener); - } - - globalKeyDownListener = (e: KeyboardEvent) => enterKey(e) && this.onOkClick(); - saveGranularity = (granularity: string) => this.setState({ granularity }); saveSort = (sort: Sort) => this.setState({ sort }); saveLimit = (limit: number) => this.setState({ limit }); - onCancelClick = () => this.props.onClose(); - - onOkClick = () => { - if (!this.validate()) return; - const { split, saveSplit, onClose } = this.props; - const newSplit = this.constructSplitCombine(); - saveSplit(split, newSplit); - onClose(); + saveSplit = () => { + const { split, saveSplit } = this.props; + saveSplit(split, this.createSplit()); }; - private constructGranularity(): Bucket { - const { dimension: { kind } } = this.props; - const { granularity } = this.state; - if (kind === "time") { - return Duration.fromJS(granularity); - } - if (kind === "number") { - return parseInt(granularity, 10); - } - return null; - } - - private constructSplitCombine(): Split { - const { split: { type } } = this.props; - const { limit, sort, reference } = this.state; - const bucket = this.constructGranularity(); - return new Split({ type, reference, limit, sort, bucket }); + private createSplit(): Split { + const { split, dimension } = this.props; + const { limit, sort, granularity } = this.state; + return createSplit({ split, dimension, limit, sort, granularity }); } validate() { - const { dimension: { kind }, split: originalSplit } = this.props; - if (!isGranularityValid(kind, this.state.granularity)) { - return false; - } - const newSplit: Split = this.constructSplitCombine(); - return !originalSplit.equals(newSplit); + const { dimension, split } = this.props; + const { limit, sort, granularity } = this.state; + return validateSplit({ split, dimension, limit, sort, granularity }); } - renderSortDropdown() { - const { essence, dimension } = this.props; - const { sort } = this.state; + render() { + const { essence, containerStage, openOn, dimension, onClose } = this.props; + const { granularity, sort, limit } = this.state; + const seriesSortOns = essence.seriesSortOns(true).toArray(); const options = [new DimensionSortOn(dimension), ...seriesSortOns]; const selected = SortOn.fromSort(sort, essence); - return ; - } - - render() { - const { containerStage, openOn, dimension, onClose } = this.props; - const { granularity, limit } = this.state; - if (!dimension) return null; - return + onSave={this.saveSplit} + dimension={dimension} + isValid={this.validate()}> - {this.renderSortDropdown()} + -
-
-
; + ; } } From d43df41b8cf31e325b78ca8900e9af138661f707 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Thu, 7 Oct 2021 13:19:03 +0200 Subject: [PATCH 05/13] Split controls for Grid --- .../components/split-tile/split-tile.tsx | 2 +- src/client/utils/styles/_tiles-item.scss | 4 +- .../visualizations/grid/grid-split-menu.tsx | 163 ++++++++++++++++++ .../visualizations/grid/grid-split-tile.tsx | 70 ++++++++ .../grid/visualization-controls.tsx | 26 +-- src/common/models/granularity/granularity.ts | 16 +- .../visualization-manifests/grid/grid.ts | 18 +- 7 files changed, 262 insertions(+), 37 deletions(-) create mode 100644 src/client/visualizations/grid/grid-split-menu.tsx create mode 100644 src/client/visualizations/grid/grid-split-tile.tsx diff --git a/src/client/components/split-tile/split-tile.tsx b/src/client/components/split-tile/split-tile.tsx index cfc818be9..5064ae490 100644 --- a/src/client/components/split-tile/split-tile.tsx +++ b/src/client/components/split-tile/split-tile.tsx @@ -45,7 +45,7 @@ interface SplitTileProps extends SplitTileBaseProps { splitMenuComponent: React.ComponentType; } -const SPLIT_CLASS_NAME = "split"; +export const SPLIT_CLASS_NAME = "split"; export const DefaultSplitTile: React.SFC = props => { return ; diff --git a/src/client/utils/styles/_tiles-item.scss b/src/client/utils/styles/_tiles-item.scss index fd823ce7d..a7878636f 100644 --- a/src/client/utils/styles/_tiles-item.scss +++ b/src/client/utils/styles/_tiles-item.scss @@ -30,7 +30,7 @@ @include css-variable(background-color, item-dimension); @include css-variable(color, item-dimension-text); - &:hover, &.selected { + &:hover:not(.disabled), &.selected { @include css-variable(background-color, item-dimension-hover); } } @@ -39,7 +39,7 @@ @include css-variable(background-color, item-measure); @include css-variable(color, item-measure-text); - &:hover, &.selected { + &:hover:not(.disabled), &.selected { @include css-variable(background-color, item-measure-hover); } } diff --git a/src/client/visualizations/grid/grid-split-menu.tsx b/src/client/visualizations/grid/grid-split-menu.tsx new file mode 100644 index 000000000..683816f6d --- /dev/null +++ b/src/client/visualizations/grid/grid-split-menu.tsx @@ -0,0 +1,163 @@ +/* + * 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 * as React from "react"; +import { isContinuous } from "../../../common/models/dimension/dimension"; +import { findDimensionByName } from "../../../common/models/dimension/dimensions"; +import { granularityToString } from "../../../common/models/granularity/granularity"; +import { DimensionSortOn, SortOn } from "../../../common/models/sort-on/sort-on"; +import { Sort } from "../../../common/models/sort/sort"; +import { Split } from "../../../common/models/split/split"; +import { GRID_LIMITS } from "../../../common/visualization-manifests/grid/grid"; +import { GranularityPicker } from "../../components/split-menu/granularity-picker"; +import { LimitDropdown } from "../../components/split-menu/limit-dropdown"; +import { SortDropdown } from "../../components/split-menu/sort-dropdown"; +import { SplitMenuProps } from "../../components/split-menu/split-menu"; +import { createSplit, SplitMenuBase, validateSplit } from "../../components/split-menu/split-menu-base"; + +export const GridSplitMenu: React.SFC = props => { + const { essence, split, dimension } = props; + const controlSplit = split.equals(essence.splits.getSplit(0)); + if (controlSplit) { + return ; + } + if (isContinuous(dimension)) { + return ; + } + return null; +}; + +interface SplitGranularityMenuState { + granularity: string; +} + +class SplitGranularityMenu extends React.Component { + + state: SplitGranularityMenuState = { + granularity: granularityToString(this.props.split.bucket) + }; + + saveGranularity = (granularity: string) => this.setState({ granularity }); + + saveSplit = () => { + const { split, saveSplit } = this.props; + saveSplit(split, this.createSplit()); + }; + + private createSplit(): Split { + const { split, dimension } = this.props; + const { granularity } = this.state; + return createSplit({ split, dimension, granularity }); + } + + validate() { + const { dimension, split } = this.props; + const { granularity } = this.state; + return validateSplit({ split, dimension, granularity }); + } + + render() { + const { containerStage, dimension, onClose, openOn } = this.props; + const { granularity } = this.state; + return + + ; + } +} + +interface GridControlMenuProps { + granularity?: string; + sort?: Sort; + limit?: number; +} + +class GridControlMenu extends React.Component { + + state: GridControlMenuProps = this.initState(); + + private initState(): GridControlMenuProps { + const { split: { bucket, sort, limit } } = this.props; + return { + sort, + limit, + granularity: bucket && granularityToString(bucket) + }; + } + + saveGranularity = (granularity: string) => this.setState({ granularity }); + + saveSort = (sort: Sort) => this.setState({ sort }); + + saveLimit = (limit: number) => this.setState({ limit }); + + saveSplit = () => { + const { split, saveSplit } = this.props; + saveSplit(split, this.createSplit()); + }; + + private createSplit(): Split { + const { split, dimension } = this.props; + const { limit, sort, granularity } = this.state; + return createSplit({ split, dimension, limit, sort, granularity }); + } + + validate() { + const { dimension, split } = this.props; + const { limit, sort, granularity } = this.state; + return validateSplit({ split, dimension, limit, sort, granularity }); + } + + render() { + const { containerStage, dimension, onClose, essence, openOn } = this.props; + const { granularity, sort, limit } = this.state; + const { dimensions } = essence.dataCube; + const sortOptions = [ + ...essence.splits.splits.toArray().map(({ reference }) => new DimensionSortOn(findDimensionByName(dimensions, reference))), + ...essence.seriesSortOns(true).toArray() + ]; + return + + + + ; + } +} diff --git a/src/client/visualizations/grid/grid-split-tile.tsx b/src/client/visualizations/grid/grid-split-tile.tsx new file mode 100644 index 000000000..d9af60a88 --- /dev/null +++ b/src/client/visualizations/grid/grid-split-tile.tsx @@ -0,0 +1,70 @@ +/* + * 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 * as React from "react"; +import { isContinuous } from "../../../common/models/dimension/dimension"; +import { SPLIT_CLASS_NAME, SplitTileBaseProps } from "../../components/split-tile/split-tile"; +import { SvgIcon } from "../../components/svg-icon/svg-icon"; +import { WithRef } from "../../components/with-ref/with-ref"; +import { classNames } from "../../utils/dom/dom"; +import { GridSplitMenu } from "./grid-split-menu"; + +export const GridSplitTile: React.SFC = props => { + const { essence, open: isOpened, split, dimension, style, removeSplit, updateSplit, openMenu, closeMenu, dragStart, containerStage } = props; + + const enabled = split.equals(essence.splits.getSplit(0)) || isContinuous(dimension); + + const title = split.getTitle(dimension); + + const remove = (e: React.MouseEvent) => { + e.stopPropagation(); + removeSplit(split); + }; + + const open = () => { + if (!enabled) return; + openMenu(split); + }; + + return + {({ ref: openOn, setRef }) => +
dragStart(dimension.title, split, e)} + style={style} + title={title} + > +
{title}
+
+ +
+
+ {enabled && isOpened && openOn && } +
} +
; +}; diff --git a/src/client/visualizations/grid/visualization-controls.tsx b/src/client/visualizations/grid/visualization-controls.tsx index 851eed2fd..bd72eaf92 100644 --- a/src/client/visualizations/grid/visualization-controls.tsx +++ b/src/client/visualizations/grid/visualization-controls.tsx @@ -14,30 +14,14 @@ * limitations under the License. */ import * as React from "react"; -import { SplitMenu, SplitMenuProps } from "../../components/split-menu/split-menu"; -import { SplitTile, SplitTileBaseProps } from "../../components/split-tile/split-tile"; import { SplitTilesRow, SplitTilesRowBaseProps } from "../../components/split-tile/split-tiles-row"; -import { - VisualizationControls, - VisualizationControlsBaseProps -} from "../../views/cube-view/center-panel/center-panel"; - -export const GridVisualizationControls: React.SFC = props => { - return ; -}; +import { VisualizationControls, VisualizationControlsBaseProps } from "../../views/cube-view/center-panel/center-panel"; +import { GridSplitTile } from "./grid-split-tile"; function GridSplitTilesRow(props: SplitTilesRowBaseProps) { return ; } -function GridSplitTile(props: SplitTileBaseProps) { - return ; -} - -// TODO: Really implement this menu! -function GridSplitMenu(props: SplitMenuProps) { - return -
GRIIIIIID!
- -
; -} +export const GridVisualizationControls: React.SFC = props => { + return ; +}; diff --git a/src/common/models/granularity/granularity.ts b/src/common/models/granularity/granularity.ts index 73cf7eed7..221c3ff01 100644 --- a/src/common/models/granularity/granularity.ts +++ b/src/common/models/granularity/granularity.ts @@ -26,8 +26,9 @@ import { getNumberOfWholeDigits, isDecimalInteger, toSignificantDigits -} from "../../../common/utils/general/general"; +} from "../../utils/general/general"; import { isFloorableDuration, isValidDuration } from "../../utils/plywood/duration"; +import { DimensionKind } from "../dimension/dimension"; import { Bucket } from "../split/split"; const MENU_LENGTH = 5; @@ -215,6 +216,19 @@ export function granularityFromJS(input: GranularityJS): Bucket { throw new Error("input should be number or Duration"); } +export function coerceGranularity(granularity: string, kind: DimensionKind): Bucket | null { + switch (kind) { + case "string": + return null; + case "boolean": + return null; + case "time": + return Duration.fromJS(granularity); + case "number": + return parseInt(granularity, 10); + } +} + export function granularityToString(input: Bucket): string { return input.toString(); } diff --git a/src/common/visualization-manifests/grid/grid.ts b/src/common/visualization-manifests/grid/grid.ts index 89738ae7e..44a1dbca7 100644 --- a/src/common/visualization-manifests/grid/grid.ts +++ b/src/common/visualization-manifests/grid/grid.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { SeriesSort } from "../../models/sort/sort"; import { Resolve, VisualizationManifest } from "../../models/visualization-manifest/visualization-manifest"; import { emptySettingsConfig } from "../../models/visualization-settings/empty-settings-config"; -import { isFiniteNumber } from "../../utils/general/general"; import { Actions } from "../../utils/rules/actions"; import { Predicates } from "../../utils/rules/predicates"; import { visualizationDependentEvaluatorBuilder } from "../../utils/rules/visualization-dependent-evaluator"; @@ -31,16 +29,12 @@ const rulesEvaluator = visualizationDependentEvaluatorBuilder .when(Predicates.noSelectedMeasures()) .then(Actions.manualMeasuresSelection()) - .otherwise(({ isSelectedVisualization, splits, series }) => { - const firstSeries = series.series.first(); - const { limit: firstLimit, sort: firstSort } = splits.getSplit(0); - const safeFirstLimit = isFiniteNumber(firstLimit) ? firstLimit : GRID_LIMITS[0]; - const sort = firstSort instanceof SeriesSort - ? firstSort - : new SeriesSort({ reference: firstSeries.reference }); - const newSplits = splits.update("splits", splits => - splits.map(split => - split.changeLimit(safeFirstLimit).changeSort(sort))); + .otherwise(({ isSelectedVisualization, splits }) => { + const firstSplit = splits.getSplit(0); + const { limit: firstLimit } = firstSplit; + const safeFirstLimit = GRID_LIMITS.indexOf(firstLimit) === -1 ? GRID_LIMITS[0] : firstLimit; + + const newSplits = splits.replace(firstSplit, firstSplit.changeLimit(safeFirstLimit)); if (splits.equals(newSplits)) { return Resolve.ready(isSelectedVisualization ? 10 : 4); From a3445761bfbc27d887f258c4a66356968980a78e Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Thu, 7 Oct 2021 18:10:52 +0200 Subject: [PATCH 06/13] Use real limits in grid queries --- src/client/visualizations/grid/make-query.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/visualizations/grid/make-query.ts b/src/client/visualizations/grid/make-query.ts index eed67da81..3c94dda90 100644 --- a/src/client/visualizations/grid/make-query.ts +++ b/src/client/visualizations/grid/make-query.ts @@ -40,9 +40,7 @@ function applySeries(series: List, timeShiftEnv: TimeShiftEnv, n }; } -function applyLimit({ limit }: Split) { - // TODO: this calculation is for evaluation purpose. We should add custom split values for Grid and remove this multiplication! - const value = limit * 10; +function applyLimit({ limit: value }: Split) { const limitExpression = new LimitExpression({ value }); return (query: Expression) => query.performAction(limitExpression); } From b6a44f3a610f78af67b24733c894ca225f9ba122 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Tue, 12 Oct 2021 12:55:49 +0200 Subject: [PATCH 07/13] Fix key on React.Fragment for efficient array diff Rename commonSort prop to sort. --- .../header/measures/measures-header.tsx | 12 ++++++------ .../table/scrolled-table/scrolled-table.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/components/tabular-scroller/header/measures/measures-header.tsx b/src/client/components/tabular-scroller/header/measures/measures-header.tsx index db4772cad..f40102003 100644 --- a/src/client/components/tabular-scroller/header/measures/measures-header.tsx +++ b/src/client/components/tabular-scroller/header/measures/measures-header.tsx @@ -22,7 +22,7 @@ import { MeasureHeaderCell } from "./measure-header-cell"; interface MeasuresHeaderProps { cellWidth: number; series: ConcreteSeries[]; - commonSort: Sort; + sort: Sort; showPrevious: boolean; } @@ -32,7 +32,7 @@ function sortDirection(commonSort: Sort, series: ConcreteSeries, period = Series } export const MeasuresHeader: React.SFC = props => { - const { cellWidth, series, commonSort, showPrevious } = props; + const { cellWidth, series, sort, showPrevious } = props; return {series.map(serie => { @@ -40,25 +40,25 @@ export const MeasuresHeader: React.SFC = props => { key={serie.reactKey()} width={cellWidth} title={serie.title()} - sort={sortDirection(commonSort, serie)} />; + sort={sortDirection(sort, serie)} />; if (!showPrevious) { return currentMeasure; } - return + return {currentMeasure} + sort={sortDirection(sort, serie, SeriesDerivation.PREVIOUS)} /> + sort={sortDirection(sort, serie, SeriesDerivation.DELTA)} /> ; })} ; diff --git a/src/client/visualizations/table/scrolled-table/scrolled-table.tsx b/src/client/visualizations/table/scrolled-table/scrolled-table.tsx index 96e4e3b87..144f273ce 100644 --- a/src/client/visualizations/table/scrolled-table/scrolled-table.tsx +++ b/src/client/visualizations/table/scrolled-table/scrolled-table.tsx @@ -112,7 +112,7 @@ export const ScrolledTable: React.SFC = props => { } From 292057345e232ea0ced7250dc430847bc4708a84 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Tue, 12 Oct 2021 12:56:54 +0200 Subject: [PATCH 08/13] Remove "sort by each dimension" feature on table --- .../table/interactions/interaction-controller.tsx | 10 ---------- .../table/utils/calculate-hover-position.ts | 8 ++------ 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/client/visualizations/table/interactions/interaction-controller.tsx b/src/client/visualizations/table/interactions/interaction-controller.tsx index aced745e5..04dde3c56 100644 --- a/src/client/visualizations/table/interactions/interaction-controller.tsx +++ b/src/client/visualizations/table/interactions/interaction-controller.tsx @@ -81,11 +81,6 @@ export class InteractionController extends React.Component Date: Tue, 12 Oct 2021 12:58:33 +0200 Subject: [PATCH 09/13] Flatten props on split header components. --- .../tabular-scroller/header/splits/split-columns.tsx | 12 +++++++++--- .../table/header/splits/combined-splits-title.tsx | 11 ++++++----- .../table/header/splits/splits-header.tsx | 5 +++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/client/components/tabular-scroller/header/splits/split-columns.tsx b/src/client/components/tabular-scroller/header/splits/split-columns.tsx index 6cf637570..9236e23ce 100644 --- a/src/client/components/tabular-scroller/header/splits/split-columns.tsx +++ b/src/client/components/tabular-scroller/header/splits/split-columns.tsx @@ -15,21 +15,27 @@ */ import * as React from "react"; +import { ClientDataCube } from "../../../../../common/models/data-cube/data-cube"; import { findDimensionByName } from "../../../../../common/models/dimension/dimensions"; -import { Essence } from "../../../../../common/models/essence/essence"; +import { DimensionSort, Sort, SortDirection } from "../../../../../common/models/sort/sort"; +import { Split } from "../../../../../common/models/split/split"; +import { Splits } from "../../../../../common/models/splits/splits"; import { Corner } from "../../corner/corner"; import "./split-columns.scss"; interface SplitColumnsHeader { - essence: Essence; + dataCube: ClientDataCube; + sort?: Sort; + splits: Splits; } export const SplitColumnsHeader: React.SFC = ({ essence }) => { const { splits: { splits }, dataCube } = essence; +export const SplitColumnsHeader: React.SFC = ({ sort, splits, dataCube }) => { return
- {splits.toArray().map(split => { + {splits.splits.toArray().map(split => { const { reference } = split; const title = findDimensionByName(dataCube.dimensions, reference).title; return {title}; diff --git a/src/client/visualizations/table/header/splits/combined-splits-title.tsx b/src/client/visualizations/table/header/splits/combined-splits-title.tsx index 0f018fcaf..f0614b00f 100644 --- a/src/client/visualizations/table/header/splits/combined-splits-title.tsx +++ b/src/client/visualizations/table/header/splits/combined-splits-title.tsx @@ -15,16 +15,17 @@ */ import * as React from "react"; +import { ClientDataCube } from "../../../../../common/models/data-cube/data-cube"; import { findDimensionByName } from "../../../../../common/models/dimension/dimensions"; -import { Essence } from "../../../../../common/models/essence/essence"; +import { Splits } from "../../../../../common/models/splits/splits"; import { Corner } from "../../../../components/tabular-scroller/corner/corner"; interface CombinedSplitsTitle { - essence: Essence; + dataCube: ClientDataCube; + splits: Splits; } -export const CombinedSplitsTitle: React.SFC = ({ essence }) => { - const { splits, dataCube } = essence; - const title = splits.splits.map(split => findDimensionByName(dataCube.dimensions, split.reference).title).join(", "); +export const CombinedSplitsTitle: React.SFC = ({ dataCube, splits: { splits } }) => { + const title = splits.map(split => findDimensionByName(dataCube.dimensions, split.reference).title).join(", "); return {title}; }; diff --git a/src/client/visualizations/table/header/splits/splits-header.tsx b/src/client/visualizations/table/header/splits/splits-header.tsx index 277c4f62d..cffaea56c 100644 --- a/src/client/visualizations/table/header/splits/splits-header.tsx +++ b/src/client/visualizations/table/header/splits/splits-header.tsx @@ -25,7 +25,8 @@ interface SplitHeaderProps { } export const SplitsHeader: React.SFC = ({ essence, collapseRows }) => { + const { dataCube, splits } = essence; return collapseRows ? - : - ; + : + ; }; From 23ae352265d6d69e80cc028c72c7f125a0ea295a Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Tue, 12 Oct 2021 12:58:48 +0200 Subject: [PATCH 10/13] Add 10 000 limit for grid --- src/common/visualization-manifests/grid/grid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/visualization-manifests/grid/grid.ts b/src/common/visualization-manifests/grid/grid.ts index 44a1dbca7..6ab9b9a14 100644 --- a/src/common/visualization-manifests/grid/grid.ts +++ b/src/common/visualization-manifests/grid/grid.ts @@ -20,7 +20,7 @@ import { Actions } from "../../utils/rules/actions"; import { Predicates } from "../../utils/rules/predicates"; import { visualizationDependentEvaluatorBuilder } from "../../utils/rules/visualization-dependent-evaluator"; -export const GRID_LIMITS = [50, 100, 200, 500, 1000]; +export const GRID_LIMITS = [50, 100, 200, 500, 1000, 10000]; const rulesEvaluator = visualizationDependentEvaluatorBuilder .when(Predicates.noSplits()) From b1a59ef83e49139fb66aa11de92f166e372b614d Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Tue, 12 Oct 2021 12:59:15 +0200 Subject: [PATCH 11/13] Show sort indicator on split columns --- .../header/splits/split-columns.scss | 9 +++++++++ .../header/splits/split-columns.tsx | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/client/components/tabular-scroller/header/splits/split-columns.scss b/src/client/components/tabular-scroller/header/splits/split-columns.scss index ae0150b5f..be2c9b064 100644 --- a/src/client/components/tabular-scroller/header/splits/split-columns.scss +++ b/src/client/components/tabular-scroller/header/splits/split-columns.scss @@ -21,7 +21,16 @@ } .header-split-column { + flex: 1; + display: flex; +} + +.header-split-column-title { @include ellipsis; padding-left: 6px; flex: 1; } + +.header-split-column-sort-icon { + flex: 1; +} diff --git a/src/client/components/tabular-scroller/header/splits/split-columns.tsx b/src/client/components/tabular-scroller/header/splits/split-columns.tsx index 9236e23ce..f83ad019e 100644 --- a/src/client/components/tabular-scroller/header/splits/split-columns.tsx +++ b/src/client/components/tabular-scroller/header/splits/split-columns.tsx @@ -21,6 +21,7 @@ import { DimensionSort, Sort, SortDirection } from "../../../../../common/models import { Split } from "../../../../../common/models/split/split"; import { Splits } from "../../../../../common/models/splits/splits"; import { Corner } from "../../corner/corner"; +import { SortIcon } from "../../sort-icon/sort-icon"; import "./split-columns.scss"; interface SplitColumnsHeader { @@ -29,8 +30,10 @@ interface SplitColumnsHeader { splits: Splits; } -export const SplitColumnsHeader: React.SFC = ({ essence }) => { - const { splits: { splits }, dataCube } = essence; +function sortDirection(split: Split, sort: Sort): SortDirection | null { + const isCurrentSort = sort instanceof DimensionSort && split.reference === sort.reference; + return isCurrentSort ? sort.direction : null; +} export const SplitColumnsHeader: React.SFC = ({ sort, splits, dataCube }) => { return @@ -38,7 +41,13 @@ export const SplitColumnsHeader: React.SFC = ({ sort, splits {splits.splits.toArray().map(split => { const { reference } = split; const title = findDimensionByName(dataCube.dimensions, reference).title; - return {title}; + const direction = sortDirection(split, sort); + return
+
{title}
+ {direction &&
+ +
} +
; })}
; From fc17da4d0573838e4e9f5dcf765c1002689cab08 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Tue, 12 Oct 2021 12:59:59 +0200 Subject: [PATCH 12/13] mainSplit util for grid - it selects split where are defined sort and limit --- .../visualizations/grid/grid-split-menu.tsx | 3 ++- .../visualizations/grid/grid-split-tile.tsx | 3 ++- .../visualizations/grid/utils/main-split.ts | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/client/visualizations/grid/utils/main-split.ts diff --git a/src/client/visualizations/grid/grid-split-menu.tsx b/src/client/visualizations/grid/grid-split-menu.tsx index 683816f6d..9441bca58 100644 --- a/src/client/visualizations/grid/grid-split-menu.tsx +++ b/src/client/visualizations/grid/grid-split-menu.tsx @@ -27,10 +27,11 @@ import { LimitDropdown } from "../../components/split-menu/limit-dropdown"; import { SortDropdown } from "../../components/split-menu/sort-dropdown"; import { SplitMenuProps } from "../../components/split-menu/split-menu"; import { createSplit, SplitMenuBase, validateSplit } from "../../components/split-menu/split-menu-base"; +import { mainSplit } from "./utils/main-split"; export const GridSplitMenu: React.SFC = props => { const { essence, split, dimension } = props; - const controlSplit = split.equals(essence.splits.getSplit(0)); + const controlSplit = split.equals(mainSplit(essence)); if (controlSplit) { return ; } diff --git a/src/client/visualizations/grid/grid-split-tile.tsx b/src/client/visualizations/grid/grid-split-tile.tsx index d9af60a88..9e42abf47 100644 --- a/src/client/visualizations/grid/grid-split-tile.tsx +++ b/src/client/visualizations/grid/grid-split-tile.tsx @@ -21,11 +21,12 @@ import { SvgIcon } from "../../components/svg-icon/svg-icon"; import { WithRef } from "../../components/with-ref/with-ref"; import { classNames } from "../../utils/dom/dom"; import { GridSplitMenu } from "./grid-split-menu"; +import { mainSplit } from "./utils/main-split"; export const GridSplitTile: React.SFC = props => { const { essence, open: isOpened, split, dimension, style, removeSplit, updateSplit, openMenu, closeMenu, dragStart, containerStage } = props; - const enabled = split.equals(essence.splits.getSplit(0)) || isContinuous(dimension); + const enabled = split.equals(mainSplit(essence)) || isContinuous(dimension); const title = split.getTitle(dimension); diff --git a/src/client/visualizations/grid/utils/main-split.ts b/src/client/visualizations/grid/utils/main-split.ts new file mode 100644 index 000000000..2b93a148e --- /dev/null +++ b/src/client/visualizations/grid/utils/main-split.ts @@ -0,0 +1,22 @@ +/* + * 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 { Essence } from "../../../../common/models/essence/essence"; +import { Split } from "../../../../common/models/split/split"; + +export function mainSplit(essence: Essence): Split { + return essence.splits.getSplit(0); +} From 50803c323f0c2528a0887d2be1adcb0afd8da9d1 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Tue, 12 Oct 2021 13:00:41 +0200 Subject: [PATCH 13/13] Split grid into visual component and interaction controller. --- src/client/visualizations/grid/grid.tsx | 153 ++++-------------- .../grid/interaction-controller.tsx | 137 ++++++++++++++++ .../visualizations/grid/scrolled-grid.tsx | 134 +++++++++++++++ .../grid/utils/hover-position.ts | 71 ++++++++ 4 files changed, 375 insertions(+), 120 deletions(-) create mode 100644 src/client/visualizations/grid/interaction-controller.tsx create mode 100644 src/client/visualizations/grid/scrolled-grid.tsx create mode 100644 src/client/visualizations/grid/utils/hover-position.ts diff --git a/src/client/visualizations/grid/grid.tsx b/src/client/visualizations/grid/grid.tsx index c89b7680e..3267ad482 100644 --- a/src/client/visualizations/grid/grid.tsx +++ b/src/client/visualizations/grid/grid.tsx @@ -14,39 +14,16 @@ * limitations under the License. */ -import * as d3 from "d3"; -import { Datum, PseudoDatum } from "plywood"; import * as React from "react"; import { ChartProps } from "../../../common/models/chart-props/chart-props"; -import { Essence } from "../../../common/models/essence/essence"; -import { Direction, ResizeHandle } from "../../components/resize-handle/resize-handle"; -import { Scroller, ScrollerLayout } from "../../components/scroller/scroller"; -import { - HEADER_HEIGHT, - MEASURE_WIDTH, - MIN_DIMENSION_WIDTH, - ROW_HEIGHT, - SEGMENT_WIDTH, - SPACE_LEFT, - SPACE_RIGHT -} from "../../components/tabular-scroller/dimensions"; -import { MeasuresHeader } from "../../components/tabular-scroller/header/measures/measures-header"; -import { SplitColumnsHeader } from "../../components/tabular-scroller/header/splits/split-columns"; -import { FlattenedSplits } from "../../components/tabular-scroller/splits/flattened-splits"; -import { measureColumnsCount } from "../../components/tabular-scroller/utils/measure-columns-count"; -import { visibleIndexRange } from "../../components/tabular-scroller/visible-rows/visible-index-range"; -import { selectFirstSplitDatums } from "../../utils/dataset/selectors/selectors"; +import { MIN_DIMENSION_WIDTH } from "../../components/tabular-scroller/dimensions"; import { ChartPanel, VisualizationProps } from "../../views/cube-view/center-panel/center-panel"; import "./grid.scss"; +import { InteractionController } from "./interaction-controller"; import makeQuery from "./make-query"; -import { MeasureRows } from "./measure-rows"; +import { ScrolledGrid } from "./scrolled-grid"; import { GridVisualizationControls } from "./visualization-controls"; -interface GridState { - segmentWidth: number; - scrollTop: number; -} - export function GridVisualization(props: VisualizationProps) { return @@ -54,107 +31,43 @@ export function GridVisualization(props: VisualizationProps) { ; } -class Grid extends React.Component { +class Grid extends React.Component { private innerGridRef = React.createRef(); - state: GridState = { - segmentWidth: SEGMENT_WIDTH, - scrollTop: 0 - }; - - setScroll = (scrollTop: number) => this.setState({ scrollTop }); - - setSegmentWidth = (segmentWidth: number) => this.setState({ segmentWidth }); - - private getIdealColumnWidth(): number { - const availableWidth = this.props.stage.width - SPACE_LEFT - this.getSegmentWidth(); - const count = measureColumnsCount(this.props.essence); - - return count * MEASURE_WIDTH >= availableWidth ? MEASURE_WIDTH : availableWidth / count; - } - - private getScalesForColumns(essence: Essence, flatData: PseudoDatum[]): Array> { - const concreteSeries = essence.getConcreteSeries().toArray(); - - return concreteSeries.map(series => { - const measureValues = flatData - .map((d: Datum) => series.selectValue(d)); - - return d3.scale.linear() - // Ensure that 0 is in there - .domain(d3.extent([0, ...measureValues])) - .range([0, 100]); - }); - } - - maxSegmentWidth(): number { - if (this.innerGridRef.current) { - return this.innerGridRef.current.clientWidth - MIN_DIMENSION_WIDTH; - } - - return SEGMENT_WIDTH; - } - - getSegmentWidth(): number { - const { segmentWidth } = this.state; - return segmentWidth || SEGMENT_WIDTH; + availableWidth(): number | undefined { + if (!this.innerGridRef.current) return undefined; + return this.innerGridRef.current.clientWidth - MIN_DIMENSION_WIDTH; } render(): JSX.Element { - const { essence, stage, data } = this.props; - const { segmentWidth, scrollTop } = this.state; - - const datums = selectFirstSplitDatums(data); - - const columnsCount = measureColumnsCount(essence); - const columnWidth = this.getIdealColumnWidth(); - const rowsCount = datums.length; - const visibleRowsRange = visibleIndexRange(rowsCount, stage.height, scrollTop); - - const layout: ScrollerLayout = { - bodyWidth: columnWidth * columnsCount + SPACE_RIGHT, - bodyHeight: rowsCount * ROW_HEIGHT, - bottom: 0, - left: this.getSegmentWidth(), - right: 0, - top: HEADER_HEIGHT - }; + const { essence, stage, clicker, data } = this.props; return
- - } - - leftGutter={} - - topLeftCorner={} - - body={datums && } - /> + + {({ + segmentWidth, + columnWidth, + scrollTop, + setSegmentWidth, + setScrollTop, + handleClick + }) => + } +
; } } diff --git a/src/client/visualizations/grid/interaction-controller.tsx b/src/client/visualizations/grid/interaction-controller.tsx new file mode 100644 index 000000000..007a8be78 --- /dev/null +++ b/src/client/visualizations/grid/interaction-controller.tsx @@ -0,0 +1,137 @@ +/* + * 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 * as React from "react"; +import { Clicker } from "../../../common/models/clicker/clicker"; +import { Dimension } from "../../../common/models/dimension/dimension"; +import { Essence, VisStrategy } from "../../../common/models/essence/essence"; +import { SeriesDerivation } from "../../../common/models/series/concrete-series"; +import { Series } from "../../../common/models/series/series"; +import { DimensionSort, SeriesSort, Sort, SortDirection } from "../../../common/models/sort/sort"; +import { Stage } from "../../../common/models/stage/stage"; +import { Binary, Ternary, Unary } from "../../../common/utils/functional/functional"; +import { ScrollerPart } from "../../components/scroller/scroller"; +import { MEASURE_WIDTH, SEGMENT_WIDTH, SPACE_LEFT } from "../../components/tabular-scroller/dimensions"; +import { measureColumnsCount } from "../../components/tabular-scroller/utils/measure-columns-count"; +import { Position, seriesPosition, splitPosition } from "./utils/hover-position"; +import { mainSplit } from "./utils/main-split"; + +interface InteractionsProps { + handleClick: Ternary; + setScrollTop: Binary; + setSegmentWidth: Unary; + columnWidth: number; + segmentWidth: number; + scrollTop: number; +} + +interface InteractionControllerProps { + essence: Essence; + clicker: Clicker; + stage: Stage; + children: Unary; +} + +interface InteractionControllerState { + segmentWidth: number; + scrollTop: number; +} + +export class InteractionController extends React.Component { + + state: InteractionControllerState = { + segmentWidth: SEGMENT_WIDTH, + scrollTop: 0 + }; + + setSegmentWidth = (segmentWidth: number) => this.setState({ segmentWidth }); + + getSegmentWidth(): number { + const { segmentWidth } = this.state; + return segmentWidth || SEGMENT_WIDTH; + } + + setScrollTop = (scrollTop: number) => this.setState({ scrollTop }); + + private getIdealColumnWidth(): number { + const availableWidth = this.props.stage.width - SPACE_LEFT - this.getSegmentWidth(); + const count = measureColumnsCount(this.props.essence); + + return count * MEASURE_WIDTH >= availableWidth ? MEASURE_WIDTH : availableWidth / count; + } + + private setSortToSeries(series: Series, period: SeriesDerivation) { + const sort = new SeriesSort({ reference: series.key(), period, direction: SortDirection.descending }); + this.setSort(sort); + } + + private setSortToDimension(dimension: Dimension) { + const sort = new DimensionSort({ reference: dimension.name, direction: SortDirection.descending }); + this.setSort(sort); + } + + private setSort(sort: Sort) { + const { clicker, essence } = this.props; + const { splits } = essence; + const split = mainSplit(essence); + const newSort = split.sort.equals(sort) + // NOTE: this type assertion is needed, because set method on DimensionSort and SeriesSort has overspecialised return type + ? (sort as DimensionSort).set("direction", SortDirection.ascending) + : sort; + clicker.changeSplits(splits.replace(split, split.changeSort(newSort)), VisStrategy.KeepAlways); + } + + calculatePosition(x: number, y: number, part: ScrollerPart): Position { + switch (part) { + case "top-left-corner": + return splitPosition(x, this.props.essence, this.getSegmentWidth()); + case "top-gutter": + return seriesPosition(x, this.props.essence, this.getSegmentWidth(), this.getIdealColumnWidth()); + default: + return { element: "whitespace" }; + } + } + + handleClick = (x: number, y: number, part: ScrollerPart) => { + const position = this.calculatePosition(x, y, part); + + switch (position.element) { + case "dimension": + this.setSortToDimension(position.dimension); + break; + case "series": + this.setSortToSeries(position.series, position.period); + break; + } + } + + render() { + const { children } = this.props; + const { scrollTop, segmentWidth } = this.state; + + return + {children({ + columnWidth: this.getIdealColumnWidth(), + scrollTop, + segmentWidth, + handleClick: this.handleClick, + setScrollTop: this.setScrollTop, + setSegmentWidth: this.setSegmentWidth + })} + ; + } + +} diff --git a/src/client/visualizations/grid/scrolled-grid.tsx b/src/client/visualizations/grid/scrolled-grid.tsx new file mode 100644 index 000000000..79bdc5cbf --- /dev/null +++ b/src/client/visualizations/grid/scrolled-grid.tsx @@ -0,0 +1,134 @@ +/* + * 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 * as d3 from "d3"; +import { Dataset, Datum, PseudoDatum } from "plywood"; +import * as React from "react"; +import { Essence } from "../../../common/models/essence/essence"; +import { Stage } from "../../../common/models/stage/stage"; +import { Binary, Ternary, Unary } from "../../../common/utils/functional/functional"; +import { Direction, ResizeHandle } from "../../components/resize-handle/resize-handle"; +import { Scroller, ScrollerLayout, ScrollerPart } from "../../components/scroller/scroller"; +import { HEADER_HEIGHT, ROW_HEIGHT, SEGMENT_WIDTH, SPACE_RIGHT } from "../../components/tabular-scroller/dimensions"; +import { MeasuresHeader } from "../../components/tabular-scroller/header/measures/measures-header"; +import { SplitColumnsHeader } from "../../components/tabular-scroller/header/splits/split-columns"; +import { FlattenedSplits } from "../../components/tabular-scroller/splits/flattened-splits"; +import { measureColumnsCount } from "../../components/tabular-scroller/utils/measure-columns-count"; +import { visibleIndexRange } from "../../components/tabular-scroller/visible-rows/visible-index-range"; +import { selectFirstSplitDatums } from "../../utils/dataset/selectors/selectors"; +import { MeasureRows } from "./measure-rows"; +import { mainSplit } from "./utils/main-split"; + +interface ScrolledGridProps { + essence: Essence; + data: Dataset; + stage: Stage; + handleClick: Ternary; + setScrollTop: Binary; + setSegmentWidth: Unary; + columnWidth: number; + segmentWidth: number; + availableWidth?: number; + scrollTop: number; +} + +function getScalesForColumns(essence: Essence, flatData: PseudoDatum[]): Array> { + const concreteSeries = essence.getConcreteSeries().toArray(); + + return concreteSeries.map(series => { + const measureValues = flatData + .map((d: Datum) => series.selectValue(d)); + + return d3.scale.linear() + // Ensure that 0 is in there + .domain(d3.extent([0, ...measureValues])) + .range([0, 100]); + }); +} + +export const ScrolledGrid: React.SFC = props => { + const { + essence, + data, + scrollTop, + segmentWidth, + setScrollTop, + setSegmentWidth, + handleClick, + columnWidth, + stage, + availableWidth + } = props; + + const datums = selectFirstSplitDatums(data); + const rowsCount = datums.length; + const visibleRowsRange = visibleIndexRange(rowsCount, stage.height, scrollTop); + const columnsCount = measureColumnsCount(essence); + const maxSegmentWidth = availableWidth || SEGMENT_WIDTH; + const mainSort = mainSplit(essence).sort; + + const layout: ScrollerLayout = { + bodyWidth: columnWidth * columnsCount + SPACE_RIGHT, + bodyHeight: rowsCount * ROW_HEIGHT, + bottom: 0, + left: segmentWidth, + right: 0, + top: HEADER_HEIGHT + }; + + const { dataCube, splits } = essence; + + return + + } + + leftGutter={} + + topLeftCorner={} + + body={datums && } + /> + ; +}; diff --git a/src/client/visualizations/grid/utils/hover-position.ts b/src/client/visualizations/grid/utils/hover-position.ts new file mode 100644 index 000000000..523d316fd --- /dev/null +++ b/src/client/visualizations/grid/utils/hover-position.ts @@ -0,0 +1,71 @@ +/* + * 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 { Dimension } from "../../../../common/models/dimension/dimension"; +import { findDimensionByName } from "../../../../common/models/dimension/dimensions"; +import { Essence } from "../../../../common/models/essence/essence"; +import { SeriesDerivation } from "../../../../common/models/series/concrete-series"; +import { Series } from "../../../../common/models/series/series"; +import { integerDivision } from "../../../../common/utils/general/general"; + +interface WhiteSpace { + element: "whitespace"; +} + +interface SeriesPosition { + element: "series"; + series: Series; + period: SeriesDerivation; +} + +interface DimensionPosition { + element: "dimension"; + dimension: Dimension; +} + +export type Position = WhiteSpace | SeriesPosition | DimensionPosition; + +export function splitPosition(x: number, essence: Essence, segmentWidth: number): Position { + const splitCount = essence.splits.length(); + const splitColumnWidth = segmentWidth / splitCount; + const splitIndex = Math.floor(x / splitColumnWidth); + const split = essence.splits.getSplit(splitIndex); + const dimension = findDimensionByName(essence.dataCube.dimensions, split.reference); + if (!dimension) { + return { element: "whitespace" }; + } + return { element: "dimension", dimension }; +} + +function indexToPeriod(index: number): SeriesDerivation { + return [SeriesDerivation.CURRENT, SeriesDerivation.PREVIOUS, SeriesDerivation.DELTA][index % 3]; +} + +export function seriesPosition(x: number, essence: Essence, segmentWidth: number, columnWidth: number): Position { + const seriesList = essence.series.series; + const xOffset = x - segmentWidth; + const seriesIndex = Math.floor(xOffset / columnWidth); + if (essence.hasComparison()) { + const nominalIndex = integerDivision(seriesIndex, 3); + const series = seriesList.get(nominalIndex); + if (!series) return { element: "whitespace" }; + const period = indexToPeriod(seriesIndex); + return { element: "series", series, period }; + } + const series = seriesList.get(seriesIndex); + if (!series) return { element: "whitespace" }; + return { element: "series", series, period: SeriesDerivation.CURRENT }; +}