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: text improvements #524

Merged
merged 10 commits into from
Jan 29, 2020
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;
markov00 marked this conversation as resolved.
Show resolved Hide resolved
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);
markov00 marked this conversation as resolved.
Show resolved Hide resolved
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