-
Notifications
You must be signed in to change notification settings - Fork 172
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
12b4bc5
commit fa3dc82
Showing
18 changed files
with
499 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> ; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
Oops, something went wrong.