Skip to content

Commit

Permalink
feat(partition): treemap group text in grooves (#652)
Browse files Browse the repository at this point in the history
Puts treemap text for groups into top grooves. Treemap texts are left-aligned and brought to top (groove texts are on the bottom). Also, font min/max sizes can be configured per Layer. 
Addresses the treemap part of #599 and contributes to solving a few items in #612

fix #611
  • Loading branch information
monfera authored Apr 30, 2020
1 parent aa6ff42 commit 304dd48
Show file tree
Hide file tree
Showing 30 changed files with 335 additions and 86 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
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.
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.
14 changes: 8 additions & 6 deletions src/chart_types/partition_chart/layout/types/config_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface LabelConfig extends Font {
textOpacity: Ratio;
valueFormatter: ValueFormatter;
valueFont: PartialFont;
padding: number;
}

export type FillLabelConfig = LabelConfig;
Expand All @@ -51,8 +52,14 @@ export interface LinkLabelConfig extends LabelConfig {
maxCount: number;
}

export interface FillFontSizeRange {
minFontSize: Pixels;
maxFontSize: Pixels;
idealFontSizeJump: Ratio;
}

// todo switch to `io-ts` style, generic way of combining static and runtime type info
export interface StaticConfig {
export interface StaticConfig extends FillFontSizeRange {
// shape geometry
width: number;
height: number;
Expand All @@ -66,11 +73,6 @@ export interface StaticConfig {
// general text config
fontFamily: FontFamily;

// fill text config
minFontSize: Pixels;
maxFontSize: Pixels;
idealFontSizeJump: Ratio;

// fill text layout config
circlePadding: Distance;
radialPadding: Distance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Font } from './types';
import { config, ValueGetterName } from '../config/config';
import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup';
import { Color } from '../../../../utils/commons';
import { VerticalAlignments } from '../viewmodel/viewmodel';

export type LinkLabelVM = {
link: [PointTuple, ...PointTuple[]]; // at least one point
Expand Down Expand Up @@ -64,6 +65,9 @@ export interface RowSet {
fillTextColor: string;
fontSize: number;
rotation: Radian;
verticalAlignment: VerticalAlignments;
leftAlign: boolean; // might be generalized into horizontalAlign - if needed
container?: any;
}

export interface QuadViewModel extends ShapeTreeNode {
Expand Down
26 changes: 18 additions & 8 deletions src/chart_types/partition_chart/layout/utils/treemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
import { ArrayEntry, CHILDREN_KEY, entryValue, HierarchyOfArrays } from './group_by_rollup';
import { Part } from '../types/types';
import { GOLDEN_RATIO } from './math';
import { Pixels } from '../types/geometry_types';

const MAX_PADDING_RATIO = 0.0256197; // this limits area distortion to <10% (which occurs due to pixel padding) with very small rectangles
const MAX_U_PADDING_RATIO = 0.0256197; // this limits area distortion to <10% (which occurs due to pixel padding) with very small rectangles
const MAX_TOP_PADDING_RATIO = 0.33; // this limits further area distortion to ~33%

interface LayoutElement {
nodes: HierarchyOfArrays;
Expand Down Expand Up @@ -88,10 +90,15 @@ function vectorNodeCoordinates(vectorLayout: LayoutElement, x0Base: number, y0Ba
});
}

/** @internal */
export const getTopPadding = (requestedTopPadding: number, fullHeight: Pixels) =>
Math.min(requestedTopPadding, fullHeight * MAX_TOP_PADDING_RATIO);

/** @internal */
export function treemap(
nodes: HierarchyOfArrays,
areaAccessor: (e: ArrayEntry) => number,
topPaddingAccessor: (e: ArrayEntry) => number,
paddingAccessor: (e: ArrayEntry) => number,
{ x0, y0, width, height }: { x0: number; y0: number; width: number; height: number },
): Array<Part> {
Expand All @@ -111,20 +118,22 @@ export function treemap(
}
const fullWidth = x1 - x0;
const fullHeight = y1 - y0;
const padding = Math.min(
const uPadding = Math.min(
paddingAccessor(node),
fullWidth * MAX_PADDING_RATIO * 2,
fullHeight * MAX_PADDING_RATIO * 2,
fullWidth * MAX_U_PADDING_RATIO * 2,
fullHeight * MAX_U_PADDING_RATIO * 2,
);
const width = fullWidth - 2 * padding;
const height = fullHeight - 2 * padding;
const topPadding = getTopPadding(topPaddingAccessor(node), fullHeight);
const width = fullWidth - 2 * uPadding;
const height = fullHeight - uPadding - topPadding;
return treemap(
childrenNodes,
(d) => ((width * height) / (fullWidth * fullHeight)) * areaAccessor(d),
topPaddingAccessor,
paddingAccessor,
{
x0: x0 + padding,
y0: y0 + padding,
x0: x0 + uPadding,
y0: y0 + topPadding,
width,
height,
},
Expand All @@ -135,6 +144,7 @@ export function treemap(
treemap(
nodes.slice(vector.length),
areaAccessor,
topPaddingAccessor,
paddingAccessor,
vertical
? { x0, y0: y0 + dependentSize, width, height: height - dependentSize }
Expand Down
124 changes: 84 additions & 40 deletions src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License. */

import { wrapToTau } from '../geometry';
import { Coordinate, Distance, Pixels, Radian, Radius, RingSector } from '../types/geometry_types';
import { Coordinate, Distance, Pixels, Radian, Radius, Ratio, RingSector } from '../types/geometry_types';
import { Config } from '../types/config_types';
import { logarithm, TAU, trueBearingToStandardPositionAngle } from '../utils/math';
import {
Expand All @@ -35,6 +35,7 @@ import { Layer } from '../../specs/index';
import { stringToRGB } from '../utils/d3_utils';
import { colorIsDark } from '../utils/calcs';
import { ValueFormatter } from '../../../../utils/commons';
import { RectangleConstruction, VerticalAlignments } from './viewmodel';

const INFINITY_RADIUS = 1e4; // far enough for a sub-2px precision on a 4k screen, good enough for text bounds; 64 bit floats still work well with it

Expand Down Expand Up @@ -73,16 +74,6 @@ export function nodeId(node: ShapeTreeNode): string {
return `${node.x0}|${node.y0}`;
}

/** @internal */
export function rectangleConstruction(node: ShapeTreeNode) {
return {
x0: node.x0,
y0: node.y0px,
x1: node.x1,
y1: node.y1px,
};
}

/** @internal */
export function ringSectorConstruction(config: Config, innerRadius: Radius, ringThickness: Distance) {
return (ringSector: ShapeTreeNode): RingSector => {
Expand Down Expand Up @@ -181,34 +172,61 @@ export function getSectorRowGeometry(
return { rowCentroidX, rowCentroidY, maximumRowLength };
}

function getVerticalAlignment(
container: RectangleConstruction,
verticalAlignment: VerticalAlignments,
linePitch: Pixels,
totalRowCount: number,
rowIndex: number,
padding: Pixels,
fontSize: Pixels,
overhang: Ratio,
) {
switch (verticalAlignment) {
case VerticalAlignments.top:
return -(container.y0 + linePitch * rowIndex + padding + fontSize * overhang);
case VerticalAlignments.bottom:
return -(container.y1 - linePitch * (totalRowCount - 1 - rowIndex) - fontSize * overhang);
default:
return -((container.y0 + container.y1) / 2 + (linePitch * (rowIndex - totalRowCount)) / 2);
}
}

/** @internal */
export function getRectangleRowGeometry(
container: any,
container: RectangleConstruction,
cx: number,
cy: number,
totalRowCount: number,
linePitch: Pixels,
rowIndex: number,
fontSize: Pixels,
_rotation: Radian,
verticalAlignment: VerticalAlignments,
): RowSpace {
const wordSpacing = getWordSpacing(fontSize);
const x0 = container.x0 + wordSpacing;
const y0 = container.y0 + linePitch / 2;
const x1 = container.x1 - wordSpacing;
const y1 = container.y1 - linePitch / 2;

// prettier-ignore
const offset =
(totalRowCount / 2) * fontSize
+ fontSize / 2
- linePitch * rowIndex

const rowCentroidX = cx;
const rowCentroidY = cy - offset;
const overhang = 0.05;
const padding = fontSize < 6 ? 0 : Math.max(1, Math.min(2, fontSize / 16)); // taper out padding with small fonts
if ((container.y1 - container.y0 - 2 * padding) / totalRowCount < linePitch) {
return {
rowCentroidX: NaN,
rowCentroidY: NaN,
maximumRowLength: 0,
};
}
const rowCentroidY = getVerticalAlignment(
container,
verticalAlignment,
linePitch,
totalRowCount,
rowIndex,
padding,
fontSize,
overhang,
);
return {
rowCentroidX,
rowCentroidY: -rowCentroidY,
maximumRowLength: rowCentroidY - linePitch / 2 < y0 || rowCentroidY + linePitch / 2 > y1 ? 0 : x1 - x0,
rowCentroidX: cx,
rowCentroidY,
maximumRowLength: container.x1 - container.x0 - 2 * padding,
};
}

Expand All @@ -223,6 +241,8 @@ function identityRowSet(): RowSet {
fontSize: NaN,
fillTextColor: '',
rotation: NaN,
verticalAlignment: VerticalAlignments.middle,
leftAlign: false,
};
}

Expand Down Expand Up @@ -251,7 +271,7 @@ function getWordSpacing(fontSize: number) {
function fill(
config: Config,
layers: Layer[],
fontSizes: string | any[],
allFontSizes: string | any[],
measure: TextMeasure,
rawTextGetter: RawTextGetter,
valueGetter: ValueGetterFunction,
Expand All @@ -260,11 +280,19 @@ function fill(
shapeConstructor: (n: ShapeTreeNode) => any,
getShapeRowGeometry: (...args: any[]) => RowSpace,
getRotation: Function,
leftAlign: boolean,
middleAlign: boolean,
) {
return (node: QuadViewModel, index: number) => {
const { maxRowCount, fillLabel } = config;

const layer = layers[node.depth - 1] || {};
const verticalAlignment = middleAlign
? VerticalAlignments.middle
: node.depth < layers.length
? VerticalAlignments.bottom
: VerticalAlignments.top;
const fontSizes = allFontSizes[Math.min(node.depth, allFontSizes.length) - 1];
const { textColor, textInvertible, fontStyle, fontVariant, fontFamily, fontWeight, valueFormatter } = Object.assign(
{ fontFamily: config.fontFamily, fontWeight: 'normal' },
fillLabel,
Expand Down Expand Up @@ -337,13 +365,16 @@ function fill(
: `rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})`
: textColor,
rotation,
verticalAlignment,
leftAlign: leftAlign,
rows: [...Array(targetRowCount)].map(() => ({
rowWords: [],
rowCentroidX: NaN,
rowCentroidY: NaN,
maximumLength: NaN,
length: NaN,
})),
container,
};

let currentRowIndex = 0;
Expand All @@ -361,6 +392,7 @@ function fill(
currentRowIndex,
fontSize,
rotation,
verticalAlignment,
);

currentRow.rowCentroidX = rowCentroidX;
Expand Down Expand Up @@ -430,24 +462,34 @@ export function fillTextLayout(
shapeConstructor: (n: ShapeTreeNode) => any,
getShapeRowGeometry: (...args: any[]) => RowSpace,
getRotation: Function,
leftAlign: boolean,
middleAlign: boolean,
) {
const { minFontSize, maxFontSize, idealFontSizeJump } = config;
const fontSizeMagnification = maxFontSize / minFontSize;
const fontSizeJumpCount = Math.round(logarithm(idealFontSizeJump, fontSizeMagnification));
const realFontSizeJump = Math.pow(fontSizeMagnification, 1 / fontSizeJumpCount);
const fontSizes: Pixels[] = [];
for (let i = 0; i <= fontSizeJumpCount; i++) {
const fontSize = Math.round(minFontSize * Math.pow(realFontSizeJump, i));
if (fontSizes.indexOf(fontSize) === -1) {
fontSizes.push(fontSize);
const allFontSizes: Pixels[][] = [];
for (let l = 0; l <= layers.length; l++) {
// get font size spec from config, which layer.fillLabel properties can override
const { minFontSize, maxFontSize, idealFontSizeJump } = {
...config,
...(l < layers.length && layers[l].fillLabel),
};
const fontSizeMagnification = maxFontSize / minFontSize;
const fontSizeJumpCount = Math.round(logarithm(idealFontSizeJump, fontSizeMagnification));
const realFontSizeJump = Math.pow(fontSizeMagnification, 1 / fontSizeJumpCount);
const fontSizes: Pixels[] = [];
for (let i = 0; i <= fontSizeJumpCount; i++) {
const fontSize = Math.round(minFontSize * Math.pow(realFontSizeJump, i));
if (fontSizes.indexOf(fontSize) === -1) {
fontSizes.push(fontSize);
}
}
allFontSizes.push(fontSizes);
}

return childNodes.map(
fill(
config,
layers,
fontSizes,
allFontSizes,
measure,
rawTextGetter,
valueGetter,
Expand All @@ -456,6 +498,8 @@ export function fillTextLayout(
shapeConstructor,
getShapeRowGeometry,
getRotation,
leftAlign,
middleAlign,
),
);
}
Loading

0 comments on commit 304dd48

Please sign in to comment.