Skip to content

Commit 35593e4

Browse files
WIP: basic marimekko
1 parent c9900be commit 35593e4

File tree

12 files changed

+291
-8
lines changed

12 files changed

+291
-8
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const <visualizationName>: React.FunctionComponent<ChartProps> = () => {
5151
</div>;
5252
};
5353
54-
export function <visualizationName>Visualization(props: VisualizationProps) {
54+
export default function <visualizationName>Visualization(props: VisualizationProps) {
5555
return <React.Fragment>
5656
<DefaultVisualizationControls {...props} />
5757
<ChartPanel {...props} queryFactory={makeQuery} chartComponent={<visualizationName>}/>

src/client/components/vis-selector/vis-selector-menu.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export class VisSelectorMenu extends React.Component<VisSelectorMenuProps, VisSe
9595
return null;
9696
case "bar-chart":
9797
return null;
98+
case "marimekko":
99+
return null;
98100
case "line-chart":
99101
const LineChartSettingsComponent = settingsComponent(visualization.name);
100102
return <LineChartSettingsComponent onChange={this.changeSettings as Unary<ImmutableRecord<LineChartSettings>, void>}

src/client/icons/vis-marimekko.svg

Lines changed: 21 additions & 0 deletions
Loading

src/client/views/cube-view/cube-view.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,8 @@ export class CubeView extends React.Component<CubeViewProps, CubeViewState> {
509509
return menuStage.within({
510510
left,
511511
right,
512-
top: CONTROL_PANEL_HEIGHT
512+
top: CONTROL_PANEL_HEIGHT,
513+
bottom: 20 // TODO: from .cube-view .center-panel margins
513514
});
514515
}
515516

src/client/visualization-settings/settings-component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface SettingsComponents {
2424
"bar-chart": null;
2525
"line-chart": typeof LineChartSettingsComponent;
2626
"heatmap": null;
27+
"marimekko": null;
2728
"grid": null;
2829
"totals": null;
2930
"scatterplot": typeof ScatterplotSettingsComponent;
@@ -33,6 +34,7 @@ const Components: SettingsComponents = {
3334
"bar-chart": null,
3435
"line-chart": LineChartSettingsComponent,
3536
"heatmap": null,
37+
"marimekko": null,
3638
"grid": null,
3739
"totals": null,
3840
"table": TableSettingsComponent,

src/client/visualizations/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const VISUALIZATIONS = {
2424
"line-chart": () => import(/* webpackChunkName: "line-chart" */ "./line-chart/line-chart"),
2525
"bar-chart": () => import(/* webpackChunkName: "bar-chart" */ "./bar-chart/bar-chart"),
2626
"heatmap": () => import(/* webpackChunkName: "heatmap" */ "./heat-map/heat-map"),
27+
"marimekko": () => import(/* webpackChunkName: "marimekko" */ "./marimekko/marimekko"),
2728
"grid": () => import(/* webpackChunkName: "grid" */ "./grid/grid"),
2829
"scatterplot": () => import(/* webpackChunkName: "scatterplot" */ "./scatterplot/scatterplot")
2930
};
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2017-2022 Allegro.pl
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as d3 from "d3";
18+
import { sum } from "d3";
19+
import { Dataset, Datum } from "plywood";
20+
import React from "react";
21+
import { ChartProps } from "../../../common/models/chart-props/chart-props";
22+
import { findDimensionByName } from "../../../common/models/dimension/dimensions";
23+
import { Essence } from "../../../common/models/essence/essence";
24+
import { percentFormatter } from "../../../common/models/series/series-format";
25+
import { Stage } from "../../../common/models/stage/stage";
26+
import { flatMap } from "../../../common/utils/functional/functional";
27+
import { mapValues } from "../../../common/utils/object/object";
28+
import makeQuery from "../../../common/utils/query/visualization-query";
29+
import { LegendSpot } from "../../components/pinboard-panel/pinboard-panel";
30+
import { selectFirstSplitDatums, selectSplitDatums } from "../../utils/dataset/selectors/selectors";
31+
import {
32+
ChartPanel,
33+
DefaultVisualizationControls,
34+
VisualizationProps
35+
} from "../../views/cube-view/center-panel/center-panel";
36+
import { useSettingsContext } from "../../views/cube-view/settings-context";
37+
import { Legend } from "../line-chart/legend/legend"; // import from different viz
38+
39+
function prepareData(data: Dataset, essence: Essence) {
40+
const series = essence.getConcreteSeries().first();
41+
const xSplit = essence.splits.getSplit(1);
42+
43+
const ySplit = essence.splits.getSplit(0);
44+
45+
const dataset = selectFirstSplitDatums(data);
46+
47+
const baseYs = dataset.map(datum => ySplit.selectValue(datum));
48+
49+
const xs: Record<string, Datum[]> = {};
50+
51+
dataset.forEach(datum => {
52+
const splitDatums = selectSplitDatums(datum);
53+
const yValue = ySplit.selectValue(datum);
54+
const y = {
55+
[ySplit.reference]: yValue
56+
};
57+
splitDatums.forEach(splitDatum => {
58+
const x = String(xSplit.selectValue(splitDatum));
59+
if (xs[x] === undefined) {
60+
xs[x] = [];
61+
}
62+
xs[x].push({ ...splitDatum, ...y });
63+
});
64+
});
65+
66+
const xs2 = mapValues(xs, ys => {
67+
const x = d3.sum(ys, datum => series.selectValue(datum));
68+
return {
69+
x,
70+
ys
71+
};
72+
});
73+
74+
function stackYs(ys: Datum[]): Array<{ name: string, y: number, y0: number }> {
75+
const sorted = flatMap(baseYs, y => {
76+
const found = ys.find(datum => ySplit.selectValue(datum) === y);
77+
return found ? [found] : [];
78+
});
79+
return sorted.map((datum, index, coll) => {
80+
const name = String(ySplit.selectValue(datum));
81+
const y = series.selectValue(datum);
82+
const y0 = sum(coll.slice(0, index), datum => series.selectValue(datum));
83+
84+
return {
85+
name,
86+
y,
87+
y0
88+
};
89+
});
90+
}
91+
92+
const xs3 = Object.entries(xs2)
93+
.map(([name, value]) => ({ name, value }))
94+
.sort(({ value: a }, { value: b }) => b.x - a.x)
95+
.map(({ value, name }, index, coll) => {
96+
const { x } = value;
97+
const x0 = sum(coll.slice(0, index), ({ value: { x } }) => x);
98+
const ys = stackYs(value.ys);
99+
return { name, value: { x, x0, ys } };
100+
});
101+
102+
return xs3;
103+
}
104+
105+
const Marimekko: React.FunctionComponent<ChartProps> = props => {
106+
const { stage, data: dataset, essence } = props;
107+
const { dataCube: { dimensions } } = essence;
108+
const { customization } = useSettingsContext();
109+
const colors = customization.visualizationColors.series;
110+
const chartStage = new Stage({
111+
x: 10,
112+
y: 20,
113+
height: stage.height - 30,
114+
width: stage.width - 20
115+
});
116+
117+
const series = essence.getConcreteSeries().first();
118+
119+
const ySplit = essence.splits.getSplit(0);
120+
const yDimension = findDimensionByName(dimensions, ySplit.reference);
121+
const colorValues = selectFirstSplitDatums(dataset).map(datum => String(ySplit.selectValue(datum)));
122+
123+
const colorScale = d3.scaleOrdinal<string>()
124+
.range(colors)
125+
.domain(colorValues);
126+
127+
const data = prepareData(dataset, essence);
128+
129+
const total = sum(data, datum => datum.value.x);
130+
const xScale = d3.scaleLinear()
131+
.range([0, chartStage.width])
132+
.domain([0, total]);
133+
134+
// TODO: magic 30!
135+
const stackHeight = chartStage.height - 30;
136+
137+
return <div className="marimekko-root">
138+
<LegendSpot>
139+
<Legend values={colorValues} title={ySplit.getTitle(yDimension)} />
140+
</LegendSpot>
141+
<svg viewBox={`0 0 ${stage.width} ${stage.height}`}>
142+
<g transform={chartStage.getTransform()}>
143+
{data.map(datum => {
144+
const { name, value: { x, x0, ys } } = datum;
145+
const xpx = xScale(x0);
146+
147+
const yScale = d3.scaleLinear()
148+
.range([0, stackHeight])
149+
.domain([0, x]);
150+
151+
return <g transform={`translate(${xpx}, 0)`} key={name}>
152+
<text x={5} y={20}>
153+
{name}: {series.formatter()(x)} ({percentFormatter(x / total)})
154+
</text>
155+
<g transform="translate(0, 30)">
156+
{ys.map(datum => {
157+
const { name, y, y0 } = datum;
158+
const ypx = yScale(y0);
159+
const height = yScale(y);
160+
161+
const width = xScale(x);
162+
return <g transform={`translate(0, ${ypx})`} key={name}>
163+
<rect x={0}
164+
y={0}
165+
width={width}
166+
height={height}
167+
fill={colorScale(name)}
168+
opacity={0.7}
169+
stroke="none"/>
170+
<text x={5} y={20}>{name}: {series.formatter()(y)} ({percentFormatter(y / x)})</text>
171+
{ypx === yScale(0) ? null : <line
172+
x1={0}
173+
x2={width}
174+
y1={0.5}
175+
y2={0.5}
176+
stroke="white"
177+
strokeWidth={2}
178+
/>}
179+
</g>;
180+
})}
181+
{xpx === 0 ? null :
182+
<line
183+
x1={0.5}
184+
x2={0.5}
185+
y1={0}
186+
y2={stackHeight}
187+
stroke="white"
188+
strokeWidth={2}
189+
/>}
190+
</g>
191+
</g>;
192+
})}
193+
</g>
194+
</svg>
195+
196+
</div>;
197+
};
198+
199+
export default function marimekkoVisualization(props: VisualizationProps) {
200+
return <React.Fragment>
201+
<DefaultVisualizationControls {...props} />
202+
<ChartPanel {...props} queryFactory={makeQuery} chartComponent={Marimekko}/>
203+
</React.Fragment>;
204+
}

src/common/models/series/series-format.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function formatFnFactory(format: string): (n: number) => string {
5353
export const exactFormat = "0,0";
5454
const exactFormatter = formatFnFactory(exactFormat);
5555
export const percentFormat = "0[.]00%";
56-
const percentFormatter = formatFnFactory(percentFormat);
56+
export const percentFormatter = formatFnFactory(percentFormat);
5757
export const measureDefaultFormat = "0,0.0 a";
5858
export const defaultFormatter = formatFnFactory(measureDefaultFormat);
5959

src/common/models/visualization-manifest/visualization-manifest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class Resolve {
9090
}
9191
}
9292

93-
export type Visualization = "heatmap" | "table" | "totals" | "bar-chart" | "line-chart" | "grid" | "scatterplot";
93+
export type Visualization = "heatmap" | "table" | "totals" | "bar-chart" | "line-chart" | "grid" | "scatterplot" | "marimekko";
9494

9595
export class VisualizationManifest<T extends object = {}> {
9696
constructor(

src/common/visualization-manifests/heat-map/heat-map.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ const rulesEvaluator = visualizationDependentEvaluatorBuilder
6060
})
6161
.build();
6262

63-
const suggestRemovingSplits = ({ splits }: ActionVariables) => [{
63+
export const suggestRemovingSplits = ({ splits }: ActionVariables) => [{
6464
description: splits.length() === 3 ? "Remove last split" : `Remove last ${splits.length() - 2} splits`,
6565
adjustment: { splits: splits.slice(0, 2) }
6666
}];
6767

68-
const suggestAddingSplits = ({ dataCube, splits }: ActionVariables) =>
68+
export const suggestAddingSplits = ({ dataCube, splits }: ActionVariables) =>
6969
allDimensions(dataCube.dimensions)
7070
.filter(dimension => !splits.hasSplitOn(dimension))
7171
.slice(0, 2)
@@ -76,7 +76,7 @@ const suggestAddingSplits = ({ dataCube, splits }: ActionVariables) =>
7676
}
7777
}));
7878

79-
const suggestAddingMeasure = ({ dataCube, series }: ActionVariables) => {
79+
export const suggestAddingMeasure = ({ dataCube, series }: ActionVariables) => {
8080
const firstMeasure = allMeasures(dataCube.measures)[0];
8181
return [{
8282
description: `Add measure ${firstMeasure.title}`,
@@ -86,7 +86,8 @@ const suggestAddingMeasure = ({ dataCube, series }: ActionVariables) => {
8686
}];
8787
};
8888

89-
const suggestRemovingMeasures = ({ series }: ActionVariables) => [{
89+
// TODO: Move these exports to commons
90+
export const suggestRemovingMeasures = ({ series }: ActionVariables) => [{
9091
description: series.count() === 2 ? "Remove last measure" : `Remove last ${series.count() - 1} measures`,
9192
adjustment: {
9293
series: series.takeFirst()

0 commit comments

Comments
 (0)