Skip to content

Commit

Permalink
Scatterplot/ Add basic chart with points (#831)
Browse files Browse the repository at this point in the history
  • Loading branch information
kzadurska authored and adrianmroz-allegro committed Feb 18, 2022
1 parent 240380f commit 13ffe98
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 3 deletions.
45 changes: 45 additions & 0 deletions src/client/visualizations/scatterplot/point.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2017-2022 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 { Datum } from "plywood";
import { ConcreteSeries } from "../../../common/models/series/concrete-series";
import "./scatterplot.scss";

import { LinearScale } from "../../utils/linear-scale/linear-scale";

interface PointProps {
datum: Datum;
xScale: LinearScale;
yScale: LinearScale;
xSeries: ConcreteSeries;
ySeries: ConcreteSeries;
}

const POINT_RADIUS = 3;

export const Point: React.SFC<PointProps> = ({ datum, xScale, yScale, xSeries, ySeries }) => {
const xValue = xSeries.selectValue(datum);
const yValue = ySeries.selectValue(datum);

return (<circle
cx={xScale(xValue)}
cy={yScale(yValue)}
r={POINT_RADIUS}
className="point"
/>
);
};
36 changes: 36 additions & 0 deletions src/client/visualizations/scatterplot/scatterplot.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,47 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@import '../../imports';

.scatterplot {
.scatterplot-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
position: relative;
}

.axis {
pointer-events: none;

line.border {
stroke: $gray;
}

line.tick {
stroke: $gray;
}

.label {
@include css-variable(fill, text-medium);
font-size: 12px;
font-weight: 400;

&.axis-label-x {
text-anchor: middle;
}
}
}

.point {
@include css-variable(stroke, brand);
@include css-variable(fill, item-dimension);
}

.axis-title {
position: absolute;
font-weight: $bold;
}
}
82 changes: 79 additions & 3 deletions src/client/visualizations/scatterplot/scatterplot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,55 @@ import {
VisualizationProps
} from "../../views/cube-view/center-panel/center-panel";

import * as d3 from "d3";
import { Datum } from "plywood";
import { ConcreteSeries } from "../../../common/models/series/concrete-series";
import { selectFirstSplitDatums } from "../../utils/dataset/selectors/selectors";
import "./scatterplot.scss";

const Scatterplot: React.SFC<ChartProps> = () => {
return <div className="scatterplot-container">
<h2>Scatterplot will be here</h2>
import { Stage } from "../../../common/models/stage/stage";
import { GridLines } from "../../components/grid-lines/grid-lines";
import { pickTicks } from "../../utils/linear-scale/linear-scale";
import { Point } from "./point";
import { XAxis } from "./x-axis";
import { YAxis } from "./y-axis";

const TICK_SIZE = 10;
const MARGIN = 40;
const X_AXIS_HEIGHT = 50;
const Y_AXIS_WIDTH = 50;

const Scatterplot: React.SFC<ChartProps> = ({ data, essence, stage }) => {
const [xSeries, ySeries] = essence.getConcreteSeries().toArray();
const scatterplotData = selectFirstSplitDatums(data);
const xExtent = getExtent(scatterplotData, xSeries);
const yExtent = getExtent(scatterplotData, ySeries);

const plottingStage = calculatePlottingStage(stage);
const yScale = d3.scale.linear().domain(yExtent).nice().range([plottingStage.height, 0]);
const xScale = d3.scale.linear().domain(xExtent).nice().range([0, plottingStage.width]);
const xTicks = pickTicks(xScale, 10);
const yTicks = pickTicks(yScale, 10);

return <div className="scatterplot-container" style={stage.getWidthHeight()}>
<span className="axis-title" style={{ top: 10, left: 10 }}>{xSeries.title()}</span>
<span className="axis-title" style={{ bottom: 145, right: 10 }}>{ySeries.title()}</span>
<svg viewBox={stage.getViewBox()}>
<GridLines orientation={"vertical"} stage={plottingStage} ticks={xTicks} scale={xScale} />
<GridLines orientation={"horizontal"} stage={plottingStage} ticks={yTicks} scale={yScale} />
<XAxis scale={xScale} stage={calculateXAxisStage(plottingStage)} ticks={xTicks} formatter={xSeries.formatter()} tickSize={TICK_SIZE}/>
<YAxis
stage={calculateYAxisStage(plottingStage)}
ticks={yTicks}
tickSize={TICK_SIZE}
scale={yScale}
formatter={ySeries.formatter()} />
<g transform={plottingStage.getTransform()}>
{scatterplotData.map(datum => (
<Point datum={datum} xScale={xScale} yScale={yScale} xSeries={xSeries} ySeries={ySeries} key={`point-${datum.x}-${datum.y}`}/>
))}
</g>
</svg>
</div>;
};

Expand All @@ -37,3 +81,35 @@ export function ScatterplotVisualization(props: VisualizationProps) {
<ChartPanel {...props} queryFactory={makeQuery} chartComponent={Scatterplot}/>
</React.Fragment>;
}

function getExtent(data: Datum[], series: ConcreteSeries): number[] {
const selectValues = (d: Datum) => series.selectValue(d);
return d3.extent(data, selectValues);
}

function calculatePlottingStage(stage: Stage): Stage {
return Stage.fromJS({
x: Y_AXIS_WIDTH + MARGIN,
y: MARGIN,
width: stage.width - Y_AXIS_WIDTH - 2 * MARGIN,
height: stage.height - X_AXIS_HEIGHT - 2 * MARGIN
});
}

function calculateXAxisStage(stage: Stage): Stage {
return Stage.fromJS({
x: Y_AXIS_WIDTH + MARGIN,
y: stage.height + MARGIN,
width: stage.width,
height: X_AXIS_HEIGHT
});
}

function calculateYAxisStage(stage: Stage): Stage {
return Stage.fromJS({
x: MARGIN,
y: MARGIN,
width: Y_AXIS_WIDTH,
height: stage.height
});
}
52 changes: 52 additions & 0 deletions src/client/visualizations/scatterplot/x-axis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2017-2022 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 "./scatterplot.scss";

import { Stage } from "../../../common/models/stage/stage";
import { Unary } from "../../../common/utils/functional/functional";
import { roundToHalfPx } from "../../utils/dom/dom";
import { LinearScale } from "../../utils/linear-scale/linear-scale";

const TEXT_OFFSET_X = 16;

interface XAxisProps {
stage: Stage;
ticks: number[];
tickSize: number;
scale: LinearScale;
formatter: Unary<number, string>;
}

export const XAxis: React.SFC<XAxisProps> = ({ stage, ticks, scale, formatter, tickSize }) => {
const labelY = tickSize + TEXT_OFFSET_X;
const linePositionY = roundToHalfPx(0);
const lines = ticks.map((tick: number) => {
const x = roundToHalfPx(scale(tick));
return <line className="tick" key={String(tick)} x1={x} y1={0} x2={x} y2={tickSize} />;
});
const labels = ticks.map((tick: number) => {
const x = scale(tick);
return <text className="label axis-label-x" key={String(tick)} x={x} y={labelY}>{formatter(tick)}</text>;
});

return (<g className="axis" transform={stage.getTransform()}>
{lines}
{labels}
<line className="border" y1={linePositionY} y2={linePositionY} x1={0} x2={stage.width}/>
</g>);
};
54 changes: 54 additions & 0 deletions src/client/visualizations/scatterplot/y-axis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2017-2022 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 "./scatterplot.scss";

import { Stage } from "../../../common/models/stage/stage";
import { Unary } from "../../../common/utils/functional/functional";
import { roundToHalfPx } from "../../utils/dom/dom";
import { LinearScale } from "../../utils/linear-scale/linear-scale";

const TEXT_OFFSET_Y = 4;

interface YAxisProps {
stage: Stage;
ticks: number[];
tickSize: number;
scale: LinearScale;
formatter: Unary<number, string>;
}

export const YAxis: React.SFC<YAxisProps> = ({ formatter, stage, tickSize, ticks, scale }) => {
const linePositionX = roundToHalfPx(stage.width);

const lines = ticks.map((tick: number) => {
const y = roundToHalfPx(scale(tick));
return <line className="tick" key={String(tick)} x1={stage.width - tickSize} y1={y} x2={stage.width} y2={y} />;
});

const labels = ticks.map((tick: number) => {
const y = scale(tick);
const labelX = y + TEXT_OFFSET_Y;
return <text className="label" key={String(tick)} x={0} y={labelX}>{formatter(tick)}</text>;
});

return <g className="axis" transform={stage.getTransform()}>
<line className="border" x1={linePositionX} y1={0} x2={linePositionX} y2={stage.height} />
{lines}
{labels}
</g>;
};

0 comments on commit 13ffe98

Please sign in to comment.