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(partition): treemap group text in grooves #652

Merged
merged 11 commits into from
Apr 30, 2020
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,
monfera marked this conversation as resolved.
Show resolved Hide resolved
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,
nickofthyme marked this conversation as resolved.
Show resolved Hide resolved
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