Skip to content

Commit

Permalink
feat(partitions): Small multiples legends (opensearch-project#1094)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: the `flatLegend` (true) option yields alphabetical, formatted name based sorting for unique name/color occurrences, to make it easy for the user to look up names in the legend as it's alphabetically sorted
  • Loading branch information
monfera authored Mar 30, 2021
1 parent 363ed25 commit 02e45d6
Show file tree
Hide file tree
Showing 19 changed files with 192 additions and 143 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ export interface PartitionSmallMultiplesModel extends SmallMultiplesIndices {
innerColumnCount: number;
innerRowIndex: number;
innerColumnIndex: number;
marginLeftPx: Pixels;
marginTopPx: Pixels;
panelInnerWidth: Pixels;
panelInnerHeight: Pixels;
}

/** @internal */
Expand Down Expand Up @@ -159,6 +163,10 @@ export const nullPartitionSmallMultiplesModel = (partitionLayout: PartitionLayou
innerColumnCount: 0,
innerRowIndex: 0,
innerColumnIndex: 0,
marginLeftPx: 0,
marginTopPx: 0,
panelInnerWidth: 0,
panelInnerHeight: 0,
partitionLayout,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ const defaultStrategy: LegendStrategy = LegendStrategy.Key;
/** @internal */
export function highlightedGeoms(
legendStrategy: LegendStrategy | undefined,
flatLegend: boolean | undefined,
quadViewModel: QuadViewModel[],
highlightedLegendItemPath: LegendPath,
) {
const pickedLogic: LegendStrategy = legendStrategy ?? defaultStrategy;
const pickedLogic: LegendStrategy = flatLegend ? LegendStrategy.Key : legendStrategy ?? defaultStrategy;
return quadViewModel.filter(legendStrategies[pickedLogic](highlightedLegendItemPath));
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { CategoryKey } from '../../../../common/category';
import { map } from '../../../../common/iterables';
import { LegendItem } from '../../../../common/legend';
import { LegendPositionConfig } from '../../../../specs/settings';
import { identity } from '../../../../utils/common';
import { isHierarchicalLegend } from '../../../../utils/legend';
import { Layer } from '../../specs';
import { QuadViewModel } from '../types/viewmodel_types';
Expand All @@ -30,7 +29,12 @@ function makeKey(...keyParts: CategoryKey[]): string {
return keyParts.join('---');
}

function compareTreePaths({ path: a }: QuadViewModel, { path: b }: QuadViewModel): number {
function compareTreePaths(
{ index: oiA, innerIndex: iiA, path: a }: QuadViewModel,
{ index: oiB, innerIndex: iiB, path: b }: QuadViewModel,
): number {
if (oiA !== oiB) return oiA - oiB;
if (iiA !== iiB) return iiA - iiB;
for (let i = 0; i < Math.min(a.length, b.length); i++) {
const diff = a[i].index - b[i].index;
if (diff) {
Expand All @@ -52,10 +56,21 @@ export function getLegendItems(
const uniqueNames = new Set(map(({ dataName, fillColor }) => makeKey(dataName, fillColor), quadViewModel));
const useHierarchicalLegend = isHierarchicalLegend(flatLegend, legendPosition);

const formattedLabel = ({ dataName, depth }: QuadViewModel) => {
const formatter = layers[depth - 1]?.nodeLabel;
return formatter ? formatter(dataName) : dataName;
};

function compareNames(aItem: QuadViewModel, bItem: QuadViewModel): number {
const a = formattedLabel(aItem);
const b = formattedLabel(bItem);
return a < b ? -1 : a > b ? 1 : 0;
}

const excluded: Set<string> = new Set();
const items = quadViewModel.filter(({ depth, dataName, fillColor }) => {
if (legendMaxDepth != null) {
return depth <= legendMaxDepth;
if (legendMaxDepth !== null && depth > legendMaxDepth) {
return false;
}
if (!useHierarchicalLegend) {
const key = makeKey(dataName, fillColor);
Expand All @@ -67,13 +82,13 @@ export function getLegendItems(
return true;
});

items.sort(compareTreePaths);
items.sort(flatLegend ? compareNames : compareTreePaths);

return items.map<LegendItem>(({ dataName, fillColor, depth, path }) => {
const formatter = layers[depth - 1]?.nodeLabel ?? identity;
return items.map<LegendItem>((item) => {
const { dataName, fillColor, depth, path } = item;
return {
color: fillColor,
label: formatter(dataName),
label: formattedLabel(item),
childId: dataName,
depth: useHierarchicalLegend ? depth - 1 : 0,
path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ import {
Pixels,
PointTuple,
Radius,
Ratio,
trueBearingToStandardPositionAngle,
} from '../../../../common/geometry';
import { Part, TextMeasure } from '../../../../common/text_utils';
import { SmallMultiplesStyle, RelativeBandsPadding } from '../../../../specs';
import { SmallMultiplesStyle } from '../../../../specs';
import { StrokeStyle, ValueFormatter, Color, RecursivePartial } from '../../../../utils/common';
import { Layer } from '../../specs';
import { config as defaultConfig, MODEL_KEY, percentValueGetter } from '../config';
Expand Down Expand Up @@ -262,15 +261,6 @@ const rawChildNodes = (
/** @internal */
export type PanelPlacement = PartitionSmallMultiplesModel;

function getInterMarginSize(size: Pixels, startMargin: Ratio, endMargin: Ratio) {
return size * (1 - Math.min(1, startMargin + endMargin));
}

function bandwidth(range: Pixels, bandCount: number, { outer, inner }: RelativeBandsPadding) {
// same convention as d3.scaleBand https://observablehq.com/@d3/d3-scaleband
return range / (2 * outer + bandCount + bandCount * inner - inner);
}

/**
* Todo move it to config
* @internal
Expand All @@ -295,7 +285,6 @@ export function shapeViewModel(
const {
width,
height,
margin,
emptySizeRatio,
outerSizeRatio,
fillOutside,
Expand All @@ -307,21 +296,7 @@ export function shapeViewModel(
sectorLineWidth,
} = config;

const innerWidth = getInterMarginSize(width, margin.left, margin.right);
const innerHeight = getInterMarginSize(height, margin.top, margin.bottom);

const panelInnerWidth = bandwidth(innerWidth, panel.innerColumnCount, smallMultiplesStyle.horizontalPanelPadding);

const panelInnerHeight = bandwidth(innerHeight, panel.innerRowCount, smallMultiplesStyle.verticalPanelPadding);

const marginLeftPx =
width * margin.left +
panelInnerWidth * smallMultiplesStyle.horizontalPanelPadding.outer +
panel.innerColumnIndex * (panelInnerWidth * (1 + smallMultiplesStyle.horizontalPanelPadding.inner));
const marginTopPx =
height * margin.top +
panelInnerHeight * smallMultiplesStyle.verticalPanelPadding.outer +
panel.innerRowIndex * (panelInnerHeight * (1 + smallMultiplesStyle.verticalPanelPadding.inner));
const { marginLeftPx, marginTopPx, panelInnerWidth, panelInnerHeight } = panel;

const treemapLayout = isTreemap(partitionLayout);
const sunburstLayout = isSunburst(partitionLayout);
Expand Down Expand Up @@ -494,6 +469,10 @@ export function shapeViewModel(
innerColumnCount: panel.innerColumnCount,
innerRowIndex: panel.innerRowIndex,
innerColumnIndex: panel.innerColumnIndex,
marginLeftPx: panel.marginLeftPx,
marginTopPx: panel.marginTopPx,
panelInnerWidth: panel.panelInnerWidth,
panelInnerHeight: panel.panelInnerHeight,

config,
layers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,19 @@ export class HighlighterComponent extends React.Component<HighlighterProps> {
renderAsMask() {
const {
chartId,
canvasDimension: { width, height },
canvasDimension: { width },
highlightSets,
} = this.props;

const maskId = (ind: number, ind2: number) => `echHighlighterMask__${chartId}__${ind}__${ind2}`;

const someGeometriesHighlighted = highlightSets.some(({ geometries }) => geometries.length > 0);
const renderedHighlightSet = someGeometriesHighlighted ? highlightSets : [];

return (
<>
<defs>
{highlightSets
{renderedHighlightSet
.filter(({ geometries }) => geometries.length > 0)
.map(
({
Expand All @@ -167,17 +170,17 @@ export class HighlighterComponent extends React.Component<HighlighterProps> {
index,
innerIndex,
partitionLayout,
top: topRatio,
left: leftRatio,
width: widthRatio,
height: heightRatio,
marginLeftPx,
marginTopPx,
panelInnerWidth,
panelInnerHeight,
}) => (
<mask key={`${index}__${innerIndex}`} id={maskId(index, innerIndex)}>
<mask key={maskId(index, innerIndex)} id={maskId(index, innerIndex)}>
<rect
x={width * leftRatio}
y={height * topRatio}
width={width * widthRatio}
height={height * heightRatio}
x={marginLeftPx}
y={marginTopPx}
width={panelInnerWidth}
height={panelInnerHeight}
fill="white"
/>
<g transform={`translate(${diskCenter.x}, ${diskCenter.y})`}>
Expand All @@ -187,41 +190,39 @@ export class HighlighterComponent extends React.Component<HighlighterProps> {
),
)}
</defs>
{highlightSets
.filter(({ geometries }) => geometries.length > 0)
.map(
({
diskCenter,
outerRadius,
index,
innerIndex,
partitionLayout,
top: topRatio,
left: leftRatio,
width: widthRatio,
height: heightRatio,
}) =>
isSunburst(partitionLayout) ? (
<circle
key={`${index}__${innerIndex}`}
cx={diskCenter.x}
cy={diskCenter.y}
r={outerRadius}
mask={`url(#${maskId(index, innerIndex)})`}
className="echHighlighter__mask"
/>
) : (
<rect
key={`${index}__${innerIndex}`}
x={width * leftRatio}
y={height * topRatio}
width={width * widthRatio}
height={height * heightRatio}
mask={`url(#${maskId(index, innerIndex)})`}
className="echHighlighter__mask"
/>
),
)}
{renderedHighlightSet.map(
({
diskCenter,
outerRadius,
index,
innerIndex,
partitionLayout,
marginLeftPx,
marginTopPx,
panelInnerWidth,
panelInnerHeight,
}) =>
isSunburst(partitionLayout) ? (
<circle
key={`${index}__${innerIndex}`}
cx={diskCenter.x}
cy={diskCenter.y}
r={outerRadius}
mask={`url(#${maskId(index, innerIndex)})`}
className="echHighlighter__mask"
/>
) : (
<rect
key={`${index}__${innerIndex}`}
x={marginLeftPx}
y={marginTopPx}
width={panelInnerWidth}
height={panelInnerHeight}
mask={`url(#${maskId(index, innerIndex)})`}
className="echHighlighter__mask"
/>
),
)}
</>
);
}
Expand Down Expand Up @@ -285,23 +286,12 @@ export const DEFAULT_PROPS: HighlighterProps = {
/** @internal */
export function highlightSetMapper(geometries: QuadViewModel[], foci: IndexedContinuousDomainFocus[]) {
return (vm: ShapeViewModel): HighlightSet => {
const { index } = vm;
const { innerIndex } = vm;
return {
index: vm.index,
innerIndex: vm.innerIndex,
panelTitle: vm.panelTitle,
partitionLayout: vm.partitionLayout,
width: vm.width,
height: vm.height,
top: vm.top,
left: vm.left,
diskCenter: vm.diskCenter,
outerRadius: vm.outerRadius,
geometries: geometries.filter(({ index: i, innerIndex: ii }) => vm.index === i && vm.innerIndex === ii),
geometriesFoci: foci.filter(({ index: i, innerIndex: ii }) => vm.index === i && vm.innerIndex === ii),
innerRowIndex: vm.innerRowIndex,
innerColumnIndex: vm.innerColumnIndex,
innerRowCount: vm.innerRowCount,
innerColumnCount: vm.innerColumnCount,
...vm,
geometries: geometries.filter(({ index: i, innerIndex: ii }) => index === i && innerIndex === ii),
geometriesFoci: foci.filter(({ index: i, innerIndex: ii }) => index === i && innerIndex === ii),
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ const hoverMapStateToProps = (state: GlobalChartState): HighlighterProps => {
const geometriesFoci = partitionDrilldownFocus(state);
const pickedGeometries = getPickedShapes(state);

const highlightSets = allGeometries.map(highlightSetMapper(pickedGeometries, geometriesFoci));

return {
chartId,
initialized: true,
renderAsOverlay: true,
canvasDimension,
highlightSets: allGeometries.map(highlightSetMapper(pickedGeometries, geometriesFoci)),
highlightSets,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ const legendMapStateToProps = (state: GlobalChartState): HighlighterProps => {
const geometries = legendHoverHighlightNodes(state);
const geometriesFoci = partitionDrilldownFocus(state);
const canvasDimension = getChartContainerDimensionsSelector(state);
const multiGeometries = partitionMultiGeometries(state);
const highlightMapper = highlightSetMapper(geometries, geometriesFoci);
const highlightSets = multiGeometries.map(highlightMapper);

return {
chartId,
initialized: true,
renderAsOverlay: false,
canvasDimension,
highlightSets: partitionMultiGeometries(state).map(highlightSetMapper(geometries, geometriesFoci)),
highlightSets,
};
};

Expand Down
Loading

0 comments on commit 02e45d6

Please sign in to comment.