diff --git a/src/client/utils/extent/extent.mocha.ts b/src/client/utils/extent/extent.mocha.ts new file mode 100644 index 000000000..dc48a4c17 --- /dev/null +++ b/src/client/utils/extent/extent.mocha.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2017-2019 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 { expect } from "chai"; +import { Datum } from "plywood"; +import { Measure } from "../../../common/models/measure/measure"; +import { SeriesDerivation } from "../../../common/models/series/concrete-series"; +import { MeasureConcreteSeries } from "../../../common/models/series/measure-concrete-series"; +import { MeasureSeries } from "../../../common/models/series/measure-series"; +import { datumsExtent, Selector, seriesSelectors } from "./extent"; + +describe("extent", () => { + describe("seriesSelectors", () => { + const reference = "count"; + const seriesFixture = new MeasureConcreteSeries( + new MeasureSeries({ reference }), + Measure.fromJS({ title: "Count", name: reference, formula: "$main.count()" })); + + const datumFixture = { + [seriesFixture.plywoodKey()]: 42, + [seriesFixture.plywoodKey(SeriesDerivation.PREVIOUS)]: 101 + } as Datum; + + describe("hasComparison is false", () => { + it("should return one selector", () => { + const selectors = seriesSelectors(seriesFixture, false); + expect(selectors).to.have.length(1); + }); + + it("should return selector which pick current value", () => { + const [selector] = seriesSelectors(seriesFixture, false); + expect(selector(datumFixture)).to.be.equal(42); + }); + }); + + describe("hasComparison is true", () => { + it("should return two selectors", () => { + const selectors = seriesSelectors(seriesFixture, true); + expect(selectors).to.have.length(2); + }); + + it("should return selector which pick current value", () => { + const [selector] = seriesSelectors(seriesFixture, true); + expect(selector(datumFixture)).to.be.equal(42); + }); + + it("should return selector which pick previous value", () => { + const [, selector] = seriesSelectors(seriesFixture, true); + expect(selector(datumFixture)).to.be.equal(101); + }); + }); + }); + + describe("datumsExtent", () => { + const fooSelector: Selector = d => d.foo as number; + const barSelector: Selector = d => d.bar as number; + + const datumsFixture = [ + { foo: 0, bar: 100 }, + { foo: 1, bar: -200 }, + { foo: 3, bar: 4 } + ]; + + it("should pick extent by one selector", () => { + const selectors = [fooSelector]; + expect(datumsExtent(datumsFixture, selectors)).to.be.deep.equal([0, 3]); + }); + + it("should pick extent by two selectors", () => { + const selectors = [fooSelector, barSelector]; + expect(datumsExtent(datumsFixture, selectors)).to.be.deep.equal([-200, 100]); + }); + }); +}); diff --git a/src/client/utils/extent/extent.ts b/src/client/utils/extent/extent.ts new file mode 100644 index 000000000..5963fc5c9 --- /dev/null +++ b/src/client/utils/extent/extent.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2019 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 { Datum } from "plywood"; +import { ConcreteSeries, SeriesDerivation } from "../../../common/models/series/concrete-series"; +import { Unary } from "../../../common/utils/functional/functional"; +import { readNumber } from "../../../common/utils/general/general"; + +export type Selector = Unary; +export type Extent = [number, number]; + +export function seriesSelectors(series: ConcreteSeries, hasComparison: boolean): Selector[] { + const get = (d: Datum) => readNumber(series.selectValue(d)); + if (!hasComparison) return [get]; + return [ + get, + (d: Datum) => readNumber(series.selectValue(d, SeriesDerivation.PREVIOUS)) + ]; +} + +export function datumsExtent(datums: Datum[], selectors: Selector[]): Extent { + return selectors.reduce((acc, selector) => { + const extent = d3.extent(datums, selector); + return d3.extent([...extent, ...acc]); + }, [0, 0]) as Extent; +} diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/bars/bar.tsx b/src/client/visualizations/bar-chart/improved-bar-chart/bars/bar.tsx index ae4031f4b..33c8522bb 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/bars/bar.tsx +++ b/src/client/visualizations/bar-chart/improved-bar-chart/bars/bar.tsx @@ -16,28 +16,31 @@ import { Datum } from "plywood"; import * as React from "react"; +import { ConcreteSeries, SeriesDerivation } from "../../../../../common/models/series/concrete-series"; import { Unary } from "../../../../../common/utils/functional/functional"; import { LinearScale } from "../../../../utils/linear-scale/linear-scale"; import { DomainValue } from "../utils/x-domain"; import { XScale } from "../utils/x-scale"; -interface BarProps { +export const TOP_PADDING = 5; + +interface SingleBarProps { datum: Datum; yScale: LinearScale; xScale: XScale; - getY: Unary; + series: ConcreteSeries; getX: Unary; maxHeight: number; } -export const BAR_PADDING = 3; +const SIDE_PADDING = 5; -export const Bar: React.SFC = props => { - const { datum, xScale, yScale, getX, getY, maxHeight } = props; +const SingleBar: React.SFC = props => { + const { datum, xScale, yScale, getX, series, maxHeight } = props; const x = getX(datum); - const xPos = xScale.calculate(x) + BAR_PADDING; - const width = xScale.rangeBand() - (2 * BAR_PADDING); - const y = getY(datum); + const xPos = xScale.calculate(x) + SIDE_PADDING; + const width = xScale.rangeBand() - (2 * SIDE_PADDING); + const y = series.selectValue(datum); const yPos = yScale(y); const height = maxHeight - yPos; @@ -48,3 +51,58 @@ export const Bar: React.SFC = props => { width={width} height={height} />; }; + +interface TimeShiftBarProps { + datum: Datum; + yScale: LinearScale; + xScale: XScale; + series: ConcreteSeries; + getX: Unary; + maxHeight: number; +} + +const TimeShiftBar: React.SFC = props => { + const { datum, xScale, yScale, getX, series, maxHeight } = props; + const x = getX(datum); + const xStart = xScale.calculate(x); + const rangeBand = xScale.rangeBand(); + const fullWidth = rangeBand - 2 * SIDE_PADDING; + const barWidth = fullWidth * 2 / 3; + + const yCurrent = series.selectValue(datum); + const yPrevious = series.selectValue(datum, SeriesDerivation.PREVIOUS); + const yCurrentStart = yScale(yCurrent); + const yPreviousStart = yScale(yPrevious); + + return + + + ; +}; + +interface BarProps { + datum: Datum; + yScale: LinearScale; + xScale: XScale; + series: ConcreteSeries; + getX: Unary; + showPrevious: boolean; + maxHeight: number; +} + +export const Bar: React.SFC = props => { + const { showPrevious, ...otherProps } = props; + return showPrevious ? + : + ; +}; diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.scss b/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.scss index 68bdbe028..4132b374d 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.scss +++ b/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.scss @@ -21,6 +21,11 @@ @include css-variable(fill, brand); } + .bar-chart-bar-previous { + @include css-variable(fill, main-time-area); + } + + .bar-chart-total { position: absolute; top: 15px; diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.tsx b/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.tsx index 3bc97b8a1..e1c475bc0 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.tsx +++ b/src/client/visualizations/bar-chart/improved-bar-chart/bars/bars.tsx @@ -29,6 +29,7 @@ import { Interaction } from "../interactions/interaction"; import { calculateChartStage } from "../utils/layout"; import { firstSplitRef } from "../utils/splits"; import { xGetter, XScale } from "../utils/x-scale"; +import { yExtent } from "../utils/y-extent"; import { Background } from "./background"; import { Bar } from "./bar"; import "./bars.scss"; @@ -56,12 +57,9 @@ export class Bars extends React.Component { const chartStage = calculateChartStage(stage); const firstSplitReference = firstSplitRef(essence); const getX = xGetter(firstSplitReference); - const getY = (datum: Datum) => series.selectValue(datum); - // TODO: move outside line chart const datums = selectFirstSplitDatums(dataset); - - const yExtent = d3.extent(datums, getY); - const yScale = getScale(yExtent, chartStage.height); + const extent = yExtent(datums, series, essence); + const yScale = getScale(extent, chartStage.height); return
{ datum={datum} yScale={yScale} xScale={xScale} - getY={getY} + series={series} + showPrevious={essence.hasComparison()} getX={getX} maxHeight={chartStage.height} />)} diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/foreground/foreground.tsx b/src/client/visualizations/bar-chart/improved-bar-chart/foreground/foreground.tsx index 4831d0c1d..91172124a 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/foreground/foreground.tsx +++ b/src/client/visualizations/bar-chart/improved-bar-chart/foreground/foreground.tsx @@ -58,6 +58,7 @@ export const Foreground: React.SFC = props => { rect={rect} /> ; stage: Stage; + showPrevious: boolean; +} + +function getYValue(datum: Datum, series: ConcreteSeries, includePrevious: boolean): number { + if (!includePrevious) { + return series.selectValue(datum); + } + return Math.max(series.selectValue(datum), series.selectValue(datum, SeriesDerivation.PREVIOUS)); } export const HighlightOverlay: React.SFC = props => { - const { stage, yScale, series, xScale, interaction: { datum }, getX } = props; + const { stage, yScale, series, xScale, showPrevious, interaction: { datum }, getX } = props; const xValue = getX(datum); const left = xScale.calculate(xValue); const right = left + xScale.rangeBand(); - const yValue = series.selectValue(datum); - const top = yScale(yValue) + stage.y - BAR_PADDING; + const yValue = getYValue(datum, series, showPrevious); + const top = yScale(yValue) + stage.y - TOP_PADDING; return ; }; diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.mocha.ts b/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.mocha.ts index 0e2d212e5..145fb2681 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.mocha.ts +++ b/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.mocha.ts @@ -31,7 +31,7 @@ describe("calculateSegmentStage", () => { }); it("should set width domain size * minimal bar width if big enough", () => { - const minimalBarWidth = 20; + const minimalBarWidth = 30; const domainSize = 2000; const stage = calculateSegmentStage(bodyStage, domainSize, 1); expect(stage.width).to.be.equal(domainSize * minimalBarWidth); diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.ts b/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.ts index 87b8d0bdf..e5dd2c176 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.ts +++ b/src/client/visualizations/bar-chart/improved-bar-chart/utils/calculate-segment-stage.ts @@ -16,7 +16,7 @@ import { Stage } from "../../../../../common/models/stage/stage"; -const BAR_MIN_WIDTH = 20; +const BAR_MIN_WIDTH = 30; const MIN_CHART_HEIGHT = 200; export function calculateSegmentStage(bodyStage: Stage, domainSize: number, seriesCount: number): Stage { diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/utils/y-extent.ts b/src/client/visualizations/bar-chart/improved-bar-chart/utils/y-extent.ts new file mode 100644 index 000000000..e2245148b --- /dev/null +++ b/src/client/visualizations/bar-chart/improved-bar-chart/utils/y-extent.ts @@ -0,0 +1,24 @@ +/* + * 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 { Datum } from "plywood"; +import { Essence } from "../../../../../common/models/essence/essence"; +import { ConcreteSeries } from "../../../../../common/models/series/concrete-series"; +import { datumsExtent, Extent, seriesSelectors } from "../../../../utils/extent/extent"; + +export function yExtent(datums: Datum[], series: ConcreteSeries, essence: Essence): Extent { + return datumsExtent(datums, seriesSelectors(series, essence.hasComparison())); +} diff --git a/src/client/visualizations/line-chart/utils/extent.ts b/src/client/visualizations/line-chart/utils/extent.ts index 1de1395d5..7a592f7a2 100644 --- a/src/client/visualizations/line-chart/utils/extent.ts +++ b/src/client/visualizations/line-chart/utils/extent.ts @@ -15,33 +15,14 @@ */ import * as d3 from "d3"; -import { Dataset, Datum } from "plywood"; +import { Dataset } from "plywood"; import { Essence } from "../../../../common/models/essence/essence"; -import { ConcreteSeries, SeriesDerivation } from "../../../../common/models/series/concrete-series"; -import { flatMap, Unary } from "../../../../common/utils/functional/functional"; -import { readNumber } from "../../../../common/utils/general/general"; +import { ConcreteSeries } from "../../../../common/models/series/concrete-series"; +import { flatMap } from "../../../../common/utils/functional/functional"; import { selectSplitDataset } from "../../../utils/dataset/selectors/selectors"; +import { datumsExtent, Extent, seriesSelectors } from "../../../utils/extent/extent"; import { hasNominalSplit } from "./splits"; -type Getter = Unary; -type Extent = [number, number]; - -function seriesSelectors(series: ConcreteSeries, hasComparison: boolean): Getter[] { - const get = (d: Datum) => readNumber(series.selectValue(d)); - if (!hasComparison) return [get]; - return [ - get, - (d: Datum) => readNumber(series.selectValue(d, SeriesDerivation.PREVIOUS)) - ]; -} - -function datumsExtent(datums: Datum[], getters: Getter[]): Extent { - return getters.reduce((acc, getter) => { - const extent = d3.extent(datums, getter); - return d3.extent([...extent, ...acc]); - }, [0, 0]) as Extent; -} - export function extentAcrossSeries(dataset: Dataset, essence: Essence): Extent { const hasComparison = essence.hasComparison(); const series = essence.getConcreteSeries().toArray();