Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(partitions): Small multiples legends #1094

Merged
merged 10 commits into from
Mar 30, 2021
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));
}
31 changes: 23 additions & 8 deletions src/chart_types/partition_chart/layout/utils/legend.ts
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
33 changes: 6 additions & 27 deletions src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts
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
114 changes: 52 additions & 62 deletions src/chart_types/partition_chart/renderer/dom/highlighter.tsx
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