Skip to content

Commit

Permalink
WIP: basic marimekko
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianmroz-allegro committed Jan 6, 2023
1 parent c9900be commit 97d77cd
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 7 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const <visualizationName>: React.FunctionComponent<ChartProps> = () => {
</div>;
};
export function <visualizationName>Visualization(props: VisualizationProps) {
export default function <visualizationName>Visualization(props: VisualizationProps) {
return <React.Fragment>
<DefaultVisualizationControls {...props} />
<ChartPanel {...props} queryFactory={makeQuery} chartComponent={<visualizationName>}/>
Expand Down
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 @@ -95,6 +95,8 @@ export class VisSelectorMenu extends React.Component<VisSelectorMenuProps, VisSe
return null;
case "bar-chart":
return null;
case "marimekko":
return null;
case "line-chart":
const LineChartSettingsComponent = settingsComponent(visualization.name);
return <LineChartSettingsComponent onChange={this.changeSettings as Unary<ImmutableRecord<LineChartSettings>, void>}
Expand Down
21 changes: 21 additions & 0 deletions src/client/icons/vis-marimekko.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 @@ -24,6 +24,7 @@ interface SettingsComponents {
"bar-chart": null;
"line-chart": typeof LineChartSettingsComponent;
"heatmap": null;
"marimekko": null;
"grid": null;
"totals": null;
"scatterplot": typeof ScatterplotSettingsComponent;
Expand All @@ -33,6 +34,7 @@ const Components: SettingsComponents = {
"bar-chart": null,
"line-chart": LineChartSettingsComponent,
"heatmap": null,
"marimekko": null,
"grid": null,
"totals": null,
"table": TableSettingsComponent,
Expand Down
1 change: 1 addition & 0 deletions src/client/visualizations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const VISUALIZATIONS = {
"line-chart": () => import(/* webpackChunkName: "line-chart" */ "./line-chart/line-chart"),
"bar-chart": () => import(/* webpackChunkName: "bar-chart" */ "./bar-chart/bar-chart"),
"heatmap": () => import(/* webpackChunkName: "heatmap" */ "./heat-map/heat-map"),
"marimekko": () => import(/* webpackChunkName: "marimekko" */ "./marimekko/marimekko"),
"grid": () => import(/* webpackChunkName: "grid" */ "./grid/grid"),
"scatterplot": () => import(/* webpackChunkName: "scatterplot" */ "./scatterplot/scatterplot")
};
Expand Down
204 changes: 204 additions & 0 deletions src/client/visualizations/marimekko/marimekko.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* 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 d3 from "d3";
import { sum } from "d3";
import { Dataset, Datum } from "plywood";
import React from "react";
import { ChartProps } from "../../../common/models/chart-props/chart-props";
import { findDimensionByName } from "../../../common/models/dimension/dimensions";
import { Essence } from "../../../common/models/essence/essence";
import { percentFormatter } from "../../../common/models/series/series-format";
import { Stage } from "../../../common/models/stage/stage";
import { flatMap } from "../../../common/utils/functional/functional";
import { mapValues } from "../../../common/utils/object/object";
import makeQuery from "../../../common/utils/query/visualization-query";
import { LegendSpot } from "../../components/pinboard-panel/pinboard-panel";
import { selectFirstSplitDatums, selectSplitDatums } from "../../utils/dataset/selectors/selectors";
import {
ChartPanel,
DefaultVisualizationControls,
VisualizationProps
} from "../../views/cube-view/center-panel/center-panel";
import { useSettingsContext } from "../../views/cube-view/settings-context";
import { Legend } from "../line-chart/legend/legend"; // import from different viz

function prepareData(data: Dataset, essence: Essence) {
const series = essence.getConcreteSeries().first();
const xSplit = essence.splits.getSplit(1);

const ySplit = essence.splits.getSplit(0);

const dataset = selectFirstSplitDatums(data);

const baseYs = dataset.map(datum => ySplit.selectValue(datum));

const xs: Record<string, Datum[]> = {};

dataset.forEach(datum => {
const splitDatums = selectSplitDatums(datum);
const yValue = ySplit.selectValue(datum);
const y = {
[ySplit.reference]: yValue
};
splitDatums.forEach(splitDatum => {
const x = String(xSplit.selectValue(splitDatum));
if (xs[x] === undefined) {
xs[x] = [];
}
xs[x].push({ ...splitDatum, ...y });
});
});

const xs2 = mapValues(xs, ys => {
const x = d3.sum(ys, datum => series.selectValue(datum));
return {
x,
ys
};
});

function stackYs(ys: Datum[]): Array<{ name: string, y: number, y0: number }> {
const sorted = flatMap(baseYs, y => {
const found = ys.find(datum => ySplit.selectValue(datum) === y);
return found ? [found] : [];
});
return sorted.map((datum, index, coll) => {
const name = String(ySplit.selectValue(datum));
const y = series.selectValue(datum);
const y0 = sum(coll.slice(0, index), datum => series.selectValue(datum));

return {
name,
y,
y0
};
});
}

const xs3 = Object.entries(xs2)
.map(([name, value]) => ({ name, value }))
.sort(({ value: a }, { value: b }) => b.x - a.x)
.map(({ value, name }, index, coll) => {
const { x } = value;
const x0 = sum(coll.slice(0, index), ({ value: { x } }) => x);
const ys = stackYs(value.ys);
return { name, value: { x, x0, ys } };
});

return xs3;
}

const Marimekko: React.FunctionComponent<ChartProps> = props => {
const { stage, data: dataset, essence } = props;
const { dataCube: { dimensions } } = essence;
const { customization } = useSettingsContext();
const colors = customization.visualizationColors.series;
const chartStage = new Stage({
x: 10,
y: 20,
height: stage.height - 30,
width: stage.width - 20
});

const series = essence.getConcreteSeries().first();

const ySplit = essence.splits.getSplit(0);
const yDimension = findDimensionByName(dimensions, ySplit.reference);
const colorValues = selectFirstSplitDatums(dataset).map(datum => String(ySplit.selectValue(datum)));

const colorScale = d3.scaleOrdinal<string>()
.range(colors)
.domain(colorValues);

const data = prepareData(dataset, essence);

const total = sum(data, datum => datum.value.x);
const xScale = d3.scaleLinear()
.range([0, chartStage.width])
.domain([0, total]);

// TODO: magic 30!
const stackHeight = chartStage.height - 30;

return <div className="marimekko-root">
<LegendSpot>
<Legend values={colorValues} title={ySplit.getTitle(yDimension)} />
</LegendSpot>
<svg viewBox={`0 0 ${stage.width} ${stage.height}`}>
<g transform={chartStage.getTransform()}>
{data.map(datum => {
const { name, value: { x, x0, ys } } = datum;
const xpx = xScale(x0);

const yScale = d3.scaleLinear()
.range([0, stackHeight])
.domain([0, x]);

return <g transform={`translate(${xpx}, 0)`} key={name}>
<text x={5} y={20}>
{name}: {series.formatter()(x)} ({percentFormatter(x / total)})
</text>
<g transform="translate(0, 30)">
{ys.map(datum => {
const { name, y, y0 } = datum;
const ypx = yScale(y0);
const height = yScale(y);

const width = xScale(x);
return <g transform={`translate(0, ${ypx})`} key={name}>
<rect x={0}
y={0}
width={width}
height={height}
fill={colorScale(name)}
opacity={0.7}
stroke="none"/>
<text x={5} y={20}>{name}: {series.formatter()(y)} ({percentFormatter(y / x)})</text>
{ypx === yScale(0) ? null : <line
x1={0}
x2={width}
y1={0.5}
y2={0.5}
stroke="white"
strokeWidth={2}
/>}
</g>;
})}
{xpx === 0 ? null :
<line
x1={0.5}
x2={0.5}
y1={0}
y2={stackHeight}
stroke="white"
strokeWidth={2}
/>}
</g>
</g>;
})}
</g>
</svg>

</div>;
};

export default function marimekkoVisualization(props: VisualizationProps) {
return <React.Fragment>
<DefaultVisualizationControls {...props} />
<ChartPanel {...props} queryFactory={makeQuery} chartComponent={Marimekko}/>
</React.Fragment>;
}
2 changes: 1 addition & 1 deletion src/common/models/series/series-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function formatFnFactory(format: string): (n: number) => string {
export const exactFormat = "0,0";
const exactFormatter = formatFnFactory(exactFormat);
export const percentFormat = "0[.]00%";
const percentFormatter = formatFnFactory(percentFormat);
export const percentFormatter = formatFnFactory(percentFormat);
export const measureDefaultFormat = "0,0.0 a";
export const defaultFormatter = formatFnFactory(measureDefaultFormat);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class Resolve {
}
}

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

export class VisualizationManifest<T extends object = {}> {
constructor(
Expand Down
9 changes: 5 additions & 4 deletions src/common/visualization-manifests/heat-map/heat-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ const rulesEvaluator = visualizationDependentEvaluatorBuilder
})
.build();

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

const suggestAddingSplits = ({ dataCube, splits }: ActionVariables) =>
export const suggestAddingSplits = ({ dataCube, splits }: ActionVariables) =>
allDimensions(dataCube.dimensions)
.filter(dimension => !splits.hasSplitOn(dimension))
.slice(0, 2)
Expand All @@ -76,7 +76,7 @@ const suggestAddingSplits = ({ dataCube, splits }: ActionVariables) =>
}
}));

const suggestAddingMeasure = ({ dataCube, series }: ActionVariables) => {
export const suggestAddingMeasure = ({ dataCube, series }: ActionVariables) => {
const firstMeasure = allMeasures(dataCube.measures)[0];
return [{
description: `Add measure ${firstMeasure.title}`,
Expand All @@ -86,7 +86,8 @@ const suggestAddingMeasure = ({ dataCube, series }: ActionVariables) => {
}];
};

const suggestRemovingMeasures = ({ series }: ActionVariables) => [{
// TODO: Move these exports to commons
export const suggestRemovingMeasures = ({ series }: ActionVariables) => [{
description: series.count() === 2 ? "Remove last measure" : `Remove last ${series.count() - 1} measures`,
adjustment: {
series: series.takeFirst()
Expand Down
2 changes: 2 additions & 0 deletions src/common/visualization-manifests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { BAR_CHART_MANIFEST } from "./bar-chart/bar-chart";
import { GRID_MANIFEST } from "./grid/grid";
import { HEAT_MAP_MANIFEST } from "./heat-map/heat-map";
import { LINE_CHART_MANIFEST } from "./line-chart/line-chart";
import { MARIMEKKO_MANIFEST } from "./marimekko/marimekko";
import { SCATTERPLOT_MANIFEST } from "./scatterplot/scatterplot";
import { TABLE_MANIFEST } from "./table/table";
import { TOTALS_MANIFEST } from "./totals/totals";
Expand All @@ -31,6 +32,7 @@ export const MANIFESTS: VisualizationManifest[] = [
LINE_CHART_MANIFEST as unknown as VisualizationManifest,
BAR_CHART_MANIFEST,
HEAT_MAP_MANIFEST,
MARIMEKKO_MANIFEST,
TABLE_MANIFEST as unknown as VisualizationManifest,
SCATTERPLOT_MANIFEST as unknown as VisualizationManifest
];
Expand Down
49 changes: 49 additions & 0 deletions src/common/visualization-manifests/marimekko/marimekko.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 { Resolve, VisualizationManifest } from "../../models/visualization-manifest/visualization-manifest";
import { emptySettingsConfig } from "../../models/visualization-settings/empty-settings-config";
import { Predicates } from "../../utils/rules/predicates";
import { visualizationDependentEvaluatorBuilder } from "../../utils/rules/visualization-dependent-evaluator";
import {
suggestAddingMeasure,
suggestAddingSplits,
suggestRemovingMeasures,
suggestRemovingSplits
} from "../heat-map/heat-map";

const rulesEvaluator = visualizationDependentEvaluatorBuilder
.when(Predicates.numberOfSplitsIsNot(2))
.then(variables => Resolve.manual(
3,
"Marimekko needs exactly 2 splits",
variables.splits.length() > 2 ? suggestRemovingSplits(variables) : suggestAddingSplits(variables)
))
.when(Predicates.numberOfSeriesIsNot(1))
.then(variables => Resolve.manual(
3,
"Marimekko needs exactly 1 measure",
variables.series.series.size === 0 ? suggestAddingMeasure(variables) : suggestRemovingMeasures(variables)
))
.otherwise(({ isSelectedVisualization }) => Resolve.ready(isSelectedVisualization ? 10 : 3))
.build();

export const MARIMEKKO_MANIFEST = new VisualizationManifest(
"marimekko",
"Marimekko",
rulesEvaluator,
emptySettingsConfig
);

0 comments on commit 97d77cd

Please sign in to comment.