Skip to content

Commit

Permalink
feat: text improvements (#524)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: renames in `Partition` charts— `Layers`: `fillLabel.formatter`->`fillLabel.valueFormatter`; type `FillLabel`-> `FillLabelConfig`

Non-breaking changes:

* feat: the values in linked labels are rendered, just like they have been in the sectors (formerly, the value could optionally be put in the link label accessor itself)

* feat: font styling is possible separately for values: `valueFormatter` configs

* test: opacity decrease example; coloring examples

* feat: hierarchical data (`parent`, `sortIndex`) is made available to accessors (see stories, helpful with eg. coloring)

* refactor: tighter types; other code improvements
  • Loading branch information
monfera authored Jan 29, 2020
1 parent 1111671 commit 6e61700
Show file tree
Hide file tree
Showing 42 changed files with 608 additions and 255 deletions.
2 changes: 1 addition & 1 deletion .playground/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class Playground extends React.Component<{}, { isSunburstShown: boolean }
{
groupByRollup: (d: Datum) => d.id,
nodeLabel: (d: Datum) => d,
fillLabel: { formatter: (d: Datum) => `${d} pct` },
fillLabel: { valueFormatter: (d: Datum) => `${d} pct` },
},
]}
/>
Expand Down
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 31 additions & 4 deletions src/chart_types/partition_chart/layout/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { palettes } from '../../../../mocks/hierarchical/palettes';
import { Config, PartitionLayout, Numeric } from '../types/config_types';
import { GOLDEN_RATIO, TAU } from '../utils/math';
import { FONT_STYLES, FONT_VARIANTS } from '../types/types';

const log10 = Math.log(10);
function significantDigitCount(d: number): number {
Expand All @@ -24,6 +25,30 @@ function defaultFormatter(d: any): string {
: String(d);
}

const valueFont = {
type: 'group',
values: {
/*
// Object.assign interprets the extant `undefined` as legit, so commenting it out till moving away from Object.assign in `const valueFont = ...`
fontFamily: {
dflt: undefined,
type: 'string',
},
*/
fontWeight: { dflt: 400, min: 100, max: 900, type: 'number' },
fontStyle: {
dflt: 'normal',
type: 'string',
values: FONT_STYLES,
},
fontVariant: {
dflt: 'normal',
type: 'string',
values: FONT_VARIANTS,
},
},
};

export const configMetadata = {
// shape geometry
width: { dflt: 300, min: 0, max: 1024, type: 'number', reconfigurable: false },
Expand Down Expand Up @@ -100,21 +125,22 @@ export const configMetadata = {
values: {
textColor: { dflt: '#000000', type: 'color' },
textInvertible: { dflt: false, type: 'boolean' },
textWeight: { dflt: 400, min: 100, max: 900, type: 'number' },
fontWeight: { dflt: 400, min: 100, max: 900, type: 'number' },
fontStyle: {
dflt: 'normal',
type: 'string',
values: ['normal', 'italic', 'oblique', 'inherit', 'initial', 'unset'],
values: FONT_STYLES,
},
fontVariant: {
dflt: 'normal',
type: 'string',
values: ['normal', 'small-caps'],
values: FONT_VARIANTS,
},
formatter: {
valueFormatter: {
dflt: defaultFormatter,
type: 'function',
},
valueFont,
},
},

Expand Down Expand Up @@ -160,6 +186,7 @@ export const configMetadata = {
type: 'number',
reconfigurable: false, // currently only tau / 8 is reliable
},
valueFont,
},
},

Expand Down
53 changes: 26 additions & 27 deletions src/chart_types/partition_chart/layout/types/config_types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Distance, Pixels, Radian, Radius, Ratio, SizeRatio, TimeMs } from './geometry_types';
import { Color, FontWeight } from './types';
import { Color, Font, FontFamily, PartialFont } from './types';
import { $Values as Values } from 'utility-types';

export const PartitionLayout = Object.freeze({
Expand All @@ -9,14 +9,27 @@ export const PartitionLayout = Object.freeze({

export type PartitionLayout = Values<typeof PartitionLayout>; // could use ValuesType<typeof HierarchicalChartTypes>

export interface FillLabel {
interface LabelConfig extends Font {
textColor: Color;
textInvertible: boolean;
textWeight: FontWeight;
fontStyle: string;
fontVariant: string;
fontFamily: string;
formatter: (x: number) => string;
textOpacity: Ratio;
valueFormatter: (x: number) => string;
valueFont: PartialFont;
}

export type FillLabelConfig = LabelConfig;

export interface LinkLabelConfig extends LabelConfig {
fontSize: Pixels; // todo consider putting it in Font
maximumSection: Distance; // use linked labels below this limit
gap: Pixels;
spacing: Pixels;
minimumStemLength: Distance;
stemAngle: Radian;
horizontalStemLength: Distance;
radiusPadding: Distance;
lineWidth: Pixels;
maxCount: number;
}

// todo switch to `io-ts` style, generic way of combining static and runtime type info
Expand All @@ -32,12 +45,12 @@ export interface StaticConfig {
partitionLayout: PartitionLayout;

// general text config
fontFamily: string;
fontFamily: FontFamily;

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

// fill text layout config
circlePadding: Distance;
Expand All @@ -47,26 +60,12 @@ export interface StaticConfig {
maxRowCount: number;
fillOutside: boolean;
radiusOutside: Radius;
fillRectangleWidth: number;
fillRectangleHeight: number;
fillLabel: FillLabel;
fillRectangleWidth: Distance;
fillRectangleHeight: Distance;
fillLabel: FillLabelConfig;

// linked labels (primarily: single-line)
linkLabel: {
maximumSection: number; // use linked labels below this limit
fontSize: Pixels;
gap: Pixels;
spacing: Pixels;
minimumStemLength: Distance;
stemAngle: Radian;
horizontalStemLength: Distance;
radiusPadding: Distance;
lineWidth: Pixels;
maxCount: number;
textColor: Color;
textInvertible: boolean;
textOpacity: number;
};
linkLabel: LinkLabelConfig;

// other
backgroundColor: Color;
Expand Down
33 changes: 31 additions & 2 deletions src/chart_types/partition_chart/layout/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,38 @@ import { ArrayEntry } from '../utils/group_by_rollup';

export type Color = string; // todo refine later (union type)

export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; // the aliases are now excluded: 'normal' | 'bold' | 'lighter' | 'bolder';
export const FONT_VARIANTS = Object.freeze(['normal', 'small-caps'] as const);
export type FontVariant = typeof FONT_VARIANTS[number];

// prettier-ignore
export const FONT_WEIGHTS = Object.freeze([
100, 200, 300, 400, 500, 600, 700, 800, 900,
'normal', 'bold', 'lighter', 'bolder', 'inherit', 'initial', 'unset',
] as const);
export type FontWeight = typeof FONT_WEIGHTS[number];
export type NumericFontWeight = number & typeof FONT_WEIGHTS[number];

export const FONT_STYLES = Object.freeze(['normal', 'italic', 'oblique', 'inherit', 'initial', 'unset'] as const);
export type FontStyle = typeof FONT_STYLES[number];

/** todo consider doing tighter control for permissible font families, eg. as in Kibana Canvas - expression language
* - though the same applies for permissible (eg. known available or loaded) font weights, styles, variants...
*/
export type FontFamily = string;

export interface Font {
fontStyle: FontStyle;
fontVariant: FontVariant;
fontWeight: FontWeight;
fontFamily: FontFamily;
}

export type TextMeasure = (font: string, texts: string[]) => TextMetrics[];
export type PartialFont = Partial<Font>;

export interface Box extends Font {
text: string;
}
export type TextMeasure = (fontSize: number, boxes: Box[]) => TextMetrics[];

/**
* Part-to-whole visualizations such as treemap, sunburst, pie hinge on an aggregation
Expand Down
18 changes: 8 additions & 10 deletions src/chart_types/partition_chart/layout/types/viewmodel_types.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Config } from './config_types';
import { Coordinate, Distance, PointObject, PointTuple, Radian } from './geometry_types';
import { Color, FontWeight } from './types';
import { Color, Font } from './types';
import { config } from '../config/config';
import { ArrayNode } from '../utils/group_by_rollup';

export type LinkLabelVM = {
link: [PointTuple, ...PointTuple[]]; // at least one point
translate: [number, number];
textAlign: CanvasTextAlign;
text: string;
valueText: string;
width: Distance;
verticalOffset: Distance;
};

export interface RowBox {
export interface RowBox extends Font {
text: string;
width: Distance;
verticalOffset: Distance;
Expand All @@ -38,15 +40,11 @@ export interface RowSet {
id: string;
rows: Array<TextRow>;
fillTextColor: string;
fillTextWeight: FontWeight;
fontFamily: string;
fontStyle: string;
fontVariant: string;
fontSize: number;
rotation: Radian;
}

export interface QuadViewModel extends RingSectorGeometry {
export interface QuadViewModel extends ShapeTreeNode {
strokeWidth: number;
fillColor: string;
}
Expand Down Expand Up @@ -92,14 +90,14 @@ interface SectorGeomSpecY {
y1px: Distance;
}

export interface RingSectorGeometry extends AngleFromTo, SectorGeomSpecY {}

export interface ShapeTreeNode extends TreeNode, SectorGeomSpecY {
yMidPx: Distance;
depth: number;
inRingIndex: number;
sortIndex: number;
dataName: any;
value: number;
parent: ArrayNode;
}

export type RawTextGetter = (node: ShapeTreeNode) => string;
export type ValueFormatter = (value: number) => string;
47 changes: 39 additions & 8 deletions src/chart_types/partition_chart/layout/utils/group_by_rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Relation } from '../types/types';
export const AGGREGATE_KEY = 'value'; // todo later switch back to 'aggregate'
export const DEPTH_KEY = 'depth';
export const CHILDREN_KEY = 'children';
export const PARENT_KEY = 'parent';
export const SORT_INDEX_KEY = 'sortIndex';

interface NodeDescriptor {
[AGGREGATE_KEY]: number;
Expand All @@ -11,13 +13,16 @@ interface NodeDescriptor {

export type ArrayEntry = [Key, ArrayNode];
export type HierarchyOfArrays = Array<ArrayEntry>;
interface ArrayNode extends NodeDescriptor {
[CHILDREN_KEY]?: HierarchyOfArrays;
export interface ArrayNode extends NodeDescriptor {
[CHILDREN_KEY]: HierarchyOfArrays;
[PARENT_KEY]: ArrayNode;
[SORT_INDEX_KEY]: number;
}

type HierarchyOfMaps = Map<Key, MapNode>;
interface MapNode extends NodeDescriptor {
[CHILDREN_KEY]?: HierarchyOfMaps;
[PARENT_KEY]?: ArrayNode;
}

export type PrimitiveValue = string | number | null; // there could be more but sufficient for now
Expand All @@ -31,12 +36,18 @@ export const entryValue = ([, value]: ArrayEntry) => value;
export function depthAccessor(n: ArrayEntry) {
return entryValue(n)[DEPTH_KEY];
}
export function aggregateAccessor(n: ArrayEntry) {
export function aggregateAccessor(n: ArrayEntry): number {
return entryValue(n)[AGGREGATE_KEY];
}
export function parentAccessor(n: ArrayEntry): ArrayNode {
return entryValue(n)[PARENT_KEY];
}
export function childrenAccessor(n: ArrayEntry) {
return entryValue(n)[CHILDREN_KEY];
}
export function sortIndexAccessor(n: ArrayEntry) {
return entryValue(n)[SORT_INDEX_KEY];
}
const ascending: Sorter = (a, b) => a - b;
const descending: Sorter = (a, b) => b - a;

Expand Down Expand Up @@ -78,21 +89,41 @@ export function groupByRollup(
return reductionMap;
}

function getRootArrayNode(): ArrayNode {
const children: HierarchyOfArrays = [];
const bootstrap = { [AGGREGATE_KEY]: NaN, [DEPTH_KEY]: NaN, [CHILDREN_KEY]: children };
Object.assign(bootstrap, { [PARENT_KEY]: bootstrap });
const result: ArrayNode = bootstrap as ArrayNode;
return result;
}

export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): HierarchyOfArrays {
const groupByMap = (node: HierarchyOfMaps) =>
const groupByMap = (node: HierarchyOfMaps, parent: ArrayNode) =>
Array.from(
node,
([key, value]: [Key, MapNode]): ArrayEntry => {
const valueElement = value[CHILDREN_KEY];
const resultNode: ArrayNode = {
[AGGREGATE_KEY]: NaN,
[CHILDREN_KEY]: [],
[DEPTH_KEY]: NaN,
[SORT_INDEX_KEY]: NaN,
[PARENT_KEY]: parent,
};
const newValue: ArrayNode = Object.assign(
{},
resultNode,
value,
valueElement && { [CHILDREN_KEY]: groupByMap(valueElement) },
valueElement && { [CHILDREN_KEY]: groupByMap(valueElement, resultNode) },
);
return [key, newValue];
},
).sort(sorter); // with the current algo, decreasing order is important
return groupByMap(root);
)
.sort(sorter)
.map((n: ArrayEntry, i) => {
entryValue(n).sortIndex = i;
return n;
}); // with the current algo, decreasing order is important
return groupByMap(root, getRootArrayNode());
}

export function mapEntryValue(entry: ArrayEntry) {
Expand Down
16 changes: 11 additions & 5 deletions src/chart_types/partition_chart/layout/utils/measure.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { TextMeasure } from '../types/types';
import { Box, Font, TextMeasure } from '../types/types';
import { Pixels } from '../types/geometry_types';

export function cssFontShorthand({ fontStyle, fontVariant, fontWeight, fontFamily }: Font, fontSize: Pixels) {
return `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}px ${fontFamily}`;
}

export function measureText(ctx: CanvasRenderingContext2D): TextMeasure {
return (font: string, texts: string[]): TextMetrics[] => {
ctx.font = font;
return texts.map((text) => ctx.measureText(text));
};
return (fontSize: number, boxes: Box[]): TextMetrics[] =>
boxes.map((box: Box) => {
ctx.font = cssFontShorthand(box, fontSize);
return ctx.measureText(box.text);
});
}
Loading

0 comments on commit 6e61700

Please sign in to comment.