Skip to content

Commit

Permalink
Gird (#712)
Browse files Browse the repository at this point in the history
* assoc util for adding elements to objects

* Table: render measure background whenever scale is provided. Table passes scales only at the bottom level of nested splits.

* Let visualization component override function translating essence to query

* Query function that passes all splits in one object and thus enforces translation to group-by

* Grid component. It is mostly reimplementation of Table.

* Manifest file for Grid. Enforces common sort on measure and same limit value

* Wire up all Grid artifacts

* DruidQueryModal must be aware of visualisation, so it can pick correct query function

* Series are required via visualizationIndependentEvaluator so they should be present in fixtures in unit tests

* Grid should have lower priority than table. At least for now.

* Magic multiplication of limit for Grid query. We add this code only so we can move forward before revamping whole SplitMenu component!
  • Loading branch information
adrianmroz-allegro authored Mar 3, 2021
1 parent 12b4bc5 commit fa3dc82
Show file tree
Hide file tree
Showing 18 changed files with 499 additions and 25 deletions.
2 changes: 2 additions & 0 deletions src/client/components/vis-selector/vis-selector-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export class VisSelectorMenu extends React.Component<VisSelectorMenuProps, VisSe
case "table":
const TableSettingsComponent = settingsComponent(visualization.name);
return <TableSettingsComponent onChange={this.changeSettings} settings={visualizationSettings as ImmutableRecord<TableSettings>} />;
case "grid":
return null;
case "heatmap":
return null;
case "totals":
Expand Down
18 changes: 18 additions & 0 deletions src/client/icons/vis-grid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions src/client/modals/druid-query-modal/druid-query-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import * as React from "react";
import { Essence } from "../../../common/models/essence/essence";
import { Timekeeper } from "../../../common/models/timekeeper/timekeeper";
import { Fn } from "../../../common/utils/general/general";
import makeQuery from "../../../common/utils/query/visualization-query";
import standardQuery from "../../../common/utils/query/visualization-query";
import gridQuery from "../../visualizations/grid/make-query";
import { SourceModal } from "../source-modal/source-modal";

interface DruidQueryModalProps {
Expand All @@ -29,8 +30,9 @@ interface DruidQueryModalProps {
}

export const DruidQueryModal: React.SFC<DruidQueryModalProps> = ({ onClose, timekeeper, essence }) => {
const { dataCube: { attributes, source, options: { customAggregations, customTransforms } } } = essence;
const query = makeQuery(essence, timekeeper);
const { visualization, dataCube: { attributes, source, options: { customAggregations, customTransforms } } } = essence;
const queryFn = visualization.name === "grid" ? gridQuery : standardQuery;
const query = queryFn(essence, timekeeper);
const external = External.fromJS({ engine: "druid", attributes, source, customAggregations, customTransforms });
const plan = query.simulateQueryPlan({ main: external });
const planSource = JSON.stringify(plan, null, 2);
Expand Down
2 changes: 2 additions & 0 deletions src/client/visualization-settings/settings-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ interface SettingsComponents {
"bar-chart": null;
"line-chart": typeof LineChartSettingsComponent;
"heatmap": null;
"grid": null;
"totals": null;
}

const Components: SettingsComponents = {
"bar-chart": null,
"line-chart": LineChartSettingsComponent,
"heatmap": null,
"grid": null,
"totals": null,
"table": TableSettingsComponent
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import { List } from "immutable";
import { Dataset } from "plywood";
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";
Expand Down Expand Up @@ -109,8 +109,12 @@ export class BaseVisualization<S extends BaseVisualizationState> extends React.C
return this.debouncedCallExecutor(essence, timekeeper);
}

protected getQuery(essence: Essence, timekeeper: Timekeeper): Expression {
return makeQuery(essence, timekeeper);
}

private callExecutor = (essence: Essence, timekeeper: Timekeeper): Promise<DatasetLoad | null> =>
essence.dataCube.executor(makeQuery(essence, timekeeper), { timezone: essence.timezone })
essence.dataCube.executor(this.getQuery(essence, timekeeper), { timezone: essence.timezone })
.then((dataset: Dataset) => {
// signal out of order requests with null
if (!this.wasUsedForLastQuery(essence)) return null;
Expand Down
153 changes: 153 additions & 0 deletions src/client/visualizations/grid/grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2017-2018 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, Expression, PseudoDatum } from "plywood";
import * as React from "react";
import { Essence } from "../../../common/models/essence/essence";
import { Timekeeper } from "../../../common/models/timekeeper/timekeeper";
import { Direction, ResizeHandle } from "../../components/resize-handle/resize-handle";
import { Scroller, ScrollerLayout } from "../../components/scroller/scroller";
import { selectFirstSplitDatums } from "../../utils/dataset/selectors/selectors";
import { BaseVisualization, BaseVisualizationState } from "../base-visualization/base-visualization";
import { FlattenedSplits } from "../table/body/splits/flattened-splits";
import { MeasuresHeader } from "../table/header/measures/measures-header";
import { SplitColumnsHeader } from "../table/header/splits/split-columns";
import { HEADER_HEIGHT, ROW_HEIGHT, SPACE_LEFT } from "../table/table";
import { measureColumnsCount } from "../table/utils/measure-columns-count";
import { visibleIndexRange } from "../table/utils/visible-index-range";
import makeQuery from "./make-query";
import { MeasureRows } from "./measure-rows";

interface GridState extends BaseVisualizationState {
segmentWidth: number;
}

const MIN_DIMENSION_WIDTH = 100;
const SEGMENT_WIDTH = 300;
const MEASURE_WIDTH = 130;
const SPACE_RIGHT = 10;

export class Grid extends BaseVisualization<GridState> {
protected innerGridRef = React.createRef<HTMLDivElement>();

protected getQuery(essence: Essence, timekeeper: Timekeeper): Expression {
return makeQuery(essence, timekeeper);
}

getDefaultState(): GridState {
return {
segmentWidth: SEGMENT_WIDTH,
...super.getDefaultState()
};
}

setScroll = (scrollTop: number, scrollLeft: number) => this.setState({ scrollLeft, 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<d3.scale.Linear<number, number>> {
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;
}

protected renderInternals(dataset: Dataset): JSX.Element {
const { essence, stage } = this.props;
const { segmentWidth, scrollTop } = this.state;

const data = selectFirstSplitDatums(dataset);

const columnsCount = measureColumnsCount(essence);
const columnWidth = this.getIdealColumnWidth();
const rowsCount = data.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
};

return <div className="internals table table-inner" ref={this.innerGridRef}>
<ResizeHandle
direction={Direction.LEFT}
onResize={this.setSegmentWidth}
min={SEGMENT_WIDTH}
max={this.maxSegmentWidth()}
value={segmentWidth}
/>
<Scroller
layout={layout}
onScroll={this.setScroll}
topGutter={<MeasuresHeader
cellWidth={columnWidth}
series={essence.getConcreteSeries().toArray()}
commonSort={essence.getCommonSort()}
showPrevious={essence.hasComparison()}/>}

leftGutter={<FlattenedSplits
visibleRowsIndexRange={visibleRowsRange}
essence={essence}
data={data}
segmentWidth={segmentWidth}
highlightedRowIndex={null} />}

topLeftCorner={<SplitColumnsHeader essence={essence}/>}

body={data && <MeasureRows
visibleRowsIndexRange={visibleRowsRange}
essence={essence}
highlightedRowIndex={null}
scales={this.getScalesForColumns(essence, data)}
data={data}
cellWidth={columnWidth}
rowWidth={columnWidth * columnsCount} />}
/>
</div> ;
}
}
101 changes: 101 additions & 0 deletions src/client/visualizations/grid/make-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2017-2018 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 { $, Expression, LimitExpression, ply } from "plywood";
import { DataCube } from "../../../common/models/data-cube/data-cube";
import { Essence } from "../../../common/models/essence/essence";
import { ConcreteSeries } from "../../../common/models/series/concrete-series";
import { Sort } from "../../../common/models/sort/sort";
import { Split, toExpression as splitToExpression } from "../../../common/models/split/split";
import { TimeShiftEnv } from "../../../common/models/time-shift/time-shift-env";
import { Timekeeper } from "../../../common/models/timekeeper/timekeeper";
import { CANONICAL_LENGTH_ID } from "../../../common/utils/canonical-length/query";
import splitCanonicalLength from "../../../common/utils/canonical-length/split-canonical-length";
import timeFilterCanonicalLength from "../../../common/utils/canonical-length/time-filter-canonical-length";
import { assoc, thread } from "../../../common/utils/functional/functional";
import { SPLIT } from "../../config/constants";

const $main = $("main");

function applySeries(series: List<ConcreteSeries>, timeShiftEnv: TimeShiftEnv, nestingLevel = 0) {
return (query: Expression) => {
return series.reduce((query, series) => {
return query.performAction(series.plywoodExpression(nestingLevel, timeShiftEnv));
}, query);
};
}

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;
const limitExpression = new LimitExpression({ value });
return (query: Expression) => query.performAction(limitExpression);
}

function applySort(sort: Sort) {
return (query: Expression) => query.performAction(sort.toExpression());
}

function applyCanonicalLength(splits: List<Split>, dataCube: DataCube) {
return (exp: Expression) => {
const canonicalLength = splits
.map(split => splitCanonicalLength(split, dataCube))
.filter(length => length !== null)
.first();
if (!canonicalLength) return exp;
return exp.apply(CANONICAL_LENGTH_ID, canonicalLength);
};
}

function applySplits(essence: Essence, timeShiftEnv: TimeShiftEnv): Expression {
const { splits: { splits }, dataCube } = essence;
const firstSplit = splits.first();

const splitsMap = splits.reduce<Record<string, Expression>>((map, split) => {
const dimension = dataCube.getDimension(split.reference);
const { name } = dimension;
const expression = splitToExpression(split, dimension, timeShiftEnv);
return assoc(map, name, expression);
}, {});

return thread(
$main.split(splitsMap),
applyCanonicalLength(splits, dataCube),
applySort(firstSplit.sort),
applyLimit(firstSplit),
applySeries(essence.getConcreteSeries(), timeShiftEnv)
);
}

export default function makeQuery(essence: Essence, timekeeper: Timekeeper): Expression {
const { splits, dataCube } = essence;
if (splits.length() > dataCube.getMaxSplits()) throw new Error(`Too many splits in query. DataCube "${dataCube.name}" supports only ${dataCube.getMaxSplits()} splits`);

const hasComparison = essence.hasComparison();
const mainFilter = essence.getEffectiveFilter(timekeeper, { combineWithPrevious: hasComparison });

const timeShiftEnv = essence.getTimeShiftEnv(timekeeper);

const mainExp: Expression = ply()
.apply("main", $main.filter(mainFilter.toExpression(dataCube)))
.apply(CANONICAL_LENGTH_ID, timeFilterCanonicalLength(essence, timekeeper));

const queryWithMeasures = applySeries(essence.getConcreteSeries(), timeShiftEnv)(mainExp);

return queryWithMeasures
.apply(SPLIT, applySplits(essence, timeShiftEnv));
}
Loading

0 comments on commit fa3dc82

Please sign in to comment.