Skip to content

Commit

Permalink
feat(partition): stroke configuration and linked label value font for…
Browse files Browse the repository at this point in the history
…mat (#602)

* feat: configurable stroke color
* fix: tapered border must not protrude
* test: improved storybook ht Marco
* chore: minor story improvement
* fix: outerSizeRatio be sectorLineWidth aware
* chore: update for subtle border color change in story
* chore: don't use colon
* feat: linked label value font configuration
* chore: measure/render uniformity; better story
* chore: story pic update
  • Loading branch information
monfera authored Mar 26, 2020
1 parent 43c13f1 commit 7dce0a3
Show file tree
Hide file tree
Showing 31 changed files with 250 additions and 62 deletions.
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.
48 changes: 25 additions & 23 deletions src/chart_types/partition_chart/layout/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ export function percentFormatter(d: number): string {
return `${Math.round(d)}%`;
}

const fontSettings = {
fontFamily: {
dflt: 'Sans-Serif',
type: 'string',
},
fontSize: { dflt: 12, min: 4, max: 32, type: 'number' },
fontStyle: {
dflt: 'normal',
type: 'string',
values: FONT_STYLES,
},
fontVariant: {
dflt: 'normal',
type: 'string',
values: FONT_VARIANTS,
},
fontWeight: { dflt: 400, min: 100, max: 900, type: 'number' },
};

const valueFont = {
type: 'group',
values: {
Expand All @@ -70,17 +89,9 @@ const valueFont = {
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,
},
fontWeight: fontSettings.fontWeight,
fontStyle: fontSettings.fontStyle,
fontVariant: fontSettings.fontVariant,
},
};

Expand Down Expand Up @@ -161,17 +172,7 @@ export const configMetadata = {
values: {
textColor: { dflt: '#000000', type: 'color' },
textInvertible: { dflt: false, type: 'boolean' },
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,
},
...fontSettings,
valueGetter: {
dflt: sumValueGetter,
type: 'function',
Expand All @@ -196,7 +197,7 @@ export const configMetadata = {
reconfigurable: true,
documentation: 'Uses linked labels below this limit of the outer sector arc length (in pixels)',
},
fontSize: { dflt: 12, min: 4, max: 32, type: 'number' },
...fontSettings,
gap: { dflt: 10, min: 6, max: 16, type: 'number' },
spacing: { dflt: 2, min: 0, max: 16, type: 'number' },
horizontalStemLength: { dflt: 10, min: 6, max: 16, type: 'number' },
Expand Down Expand Up @@ -233,6 +234,7 @@ export const configMetadata = {
// other
backgroundColor: { dflt: '#ffffff', type: 'color' },
sectorLineWidth: { dflt: 1, min: 0, max: 4, type: 'number' },
sectorLineStroke: { dflt: 'white', type: 'string' },
colors: { dflt: 'turbo', type: 'palette', values: Object.keys(palettes) },
palettes: { dflt: palettes, type: 'palettes', reconfigurable: false },
};
Expand Down
5 changes: 3 additions & 2 deletions src/chart_types/partition_chart/layout/types/config_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { Distance, Pixels, Radian, Radius, Ratio, SizeRatio, TimeMs } from './geometry_types';
import { Font, FontFamily, PartialFont } from './types';
import { $Values as Values } from 'utility-types';
import { Color, ValueFormatter } from '../../../../utils/commons';
import { Color, StrokeStyle, ValueFormatter } from '../../../../utils/commons';

export const PartitionLayout = Object.freeze({
sunburst: 'sunburst' as 'sunburst',
Expand Down Expand Up @@ -86,9 +86,10 @@ export interface StaticConfig {
// linked labels (primarily: single-line)
linkLabel: LinkLabelConfig;

// other
// global
backgroundColor: Color;
sectorLineWidth: Pixels;
sectorLineStroke: StrokeStyle;
}

export type EasingFunction = (x: Ratio) => Ratio;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export type LinkLabelVM = {
text: string;
valueText: string;
width: Distance;
valueWidth: Distance;
verticalOffset: Distance;
labelFontSpec: Font;
valueFontSpec: Font;
};

export interface RowBox extends Font {
Expand Down Expand Up @@ -65,6 +68,7 @@ export interface RowSet {

export interface QuadViewModel extends ShapeTreeNode {
strokeWidth: number;
strokeStyle: string;
fillColor: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,26 @@ export function linkTextLayout(
const stemToX = x + north * west * cy - west * relativeY;
const stemToY = cy;
const text = rawTextGetter(node);
const { width, emHeightAscent, emHeightDescent } = measure(linkLabel.fontSize, [
{ fontFamily: config.fontFamily, ...linkLabel, text },
])[0];
const valueText = valueFormatter(valueGetter(node));
const labelFontSpec = {
fontStyle: 'normal',
fontVariant: 'normal',
fontFamily: config.fontFamily,
fontWeight: 'normal',
...linkLabel,
text,
};
const valueFontSpec = {
fontStyle: 'normal',
fontVariant: 'normal',
fontFamily: config.fontFamily,
fontWeight: 'normal',
...linkLabel,
...linkLabel.valueFont,
text: valueText,
};
const { width, emHeightAscent, emHeightDescent } = measure(linkLabel.fontSize, [labelFontSpec])[0];
const { width: valueWidth } = measure(linkLabel.fontSize, [valueFontSpec])[0];
return {
link: [
[x0, y0],
Expand All @@ -83,9 +100,12 @@ export function linkTextLayout(
translate: [stemToX + west * (linkLabel.horizontalStemLength + linkLabel.gap), stemToY],
textAlign: side ? 'left' : 'right',
text,
valueText: valueFormatter(valueGetter(node)),
valueText,
width,
valueWidth,
verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle`
labelFontSpec,
valueFontSpec,
};
});
}
10 changes: 7 additions & 3 deletions src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import {
parentAccessor,
sortIndexAccessor,
} from '../utils/group_by_rollup';
import { ValueAccessor, ValueFormatter } from '../../../../utils/commons';
import { StrokeStyle, ValueAccessor, ValueFormatter } from '../../../../utils/commons';
import { percentValueGetter } from '../config/config';

function paddingAccessor(n: ArrayEntry) {
Expand Down Expand Up @@ -99,6 +99,7 @@ export function makeQuadViewModel(
childNodes: ShapeTreeNode[],
layers: Layer[],
sectorLineWidth: Pixels,
sectorLineStroke: StrokeStyle,
): Array<QuadViewModel> {
return childNodes.map((node) => {
const opacityMultiplier = 1; // could alter in the future, eg. in response to interactions
Expand All @@ -109,7 +110,8 @@ export function makeQuadViewModel(
const { r, g, b, opacity } = stringToRGB(shapeFillColor);
const fillColor = argsToRGBString(r, g, b, opacity * opacityMultiplier);
const strokeWidth = sectorLineWidth;
return { strokeWidth, fillColor, ...node };
const strokeStyle = sectorLineStroke;
return { strokeWidth, strokeStyle, fillColor, ...node };
});
}

Expand Down Expand Up @@ -166,6 +168,7 @@ export function shapeViewModel(
specialFirstInnermostSector,
minFontSize,
partitionLayout,
sectorLineWidth,
} = config;

const innerWidth = width * (1 - Math.min(1, margin.left + margin.right));
Expand Down Expand Up @@ -222,7 +225,7 @@ export function shapeViewModel(

// use the smaller of the two sizes, as a circle fits into a square
const circleMaximumSize = Math.min(innerWidth, innerHeight);
const outerRadius: Radius = (outerSizeRatio * circleMaximumSize) / 2;
const outerRadius: Radius = Math.min(outerSizeRatio * circleMaximumSize, circleMaximumSize - sectorLineWidth) / 2;
const innerRadius: Radius = outerRadius - (1 - emptySizeRatio) * outerRadius;
const treeHeight = shownChildNodes.reduce((p: number, n: any) => Math.max(p, entryValue(n.node).depth), 0); // 1: pie, 2: two-ring donut etc.
const ringThickness = (outerRadius - innerRadius) / treeHeight;
Expand All @@ -249,6 +252,7 @@ export function shapeViewModel(
),
layers,
config.sectorLineWidth,
config.sectorLineStroke,
);

// fill text
Expand Down
65 changes: 36 additions & 29 deletions src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
import { TAU } from '../../layout/utils/math';
import { PartitionLayout } from '../../layout/types/config_types';
import { cssFontShorthand } from '../../layout/utils/measure';
import { withContext, renderLayers, clearCanvas } from '../../../../renderers/canvas';
import { clearCanvas, renderLayers, withContext } from '../../../../renderers/canvas';

// the burnout avoidance in the center of the pie
const LINE_WIDTH_MULT = 10; // border can be a maximum 1/LINE_WIDTH_MULT - th of the sector angle, otherwise the border would dominate
Expand Down Expand Up @@ -62,7 +62,7 @@ function renderRowSets(ctx: CanvasRenderingContext2D, rowSets: RowSet[]) {

function renderTaperedBorder(
ctx: CanvasRenderingContext2D,
{ strokeWidth, fillColor, x0, x1, y0px, y1px }: QuadViewModel,
{ strokeWidth, strokeStyle, fillColor, x0, x1, y0px, y1px }: QuadViewModel,
) {
const X0 = x0 - TAU / 4;
const X1 = x1 - TAU / 4;
Expand All @@ -89,18 +89,19 @@ function renderTaperedBorder(
ctx.arc(0, 0, y0px, X1, X0, true);
ctx.stroke();

ctx.fillStyle = 'white';
ctx.fillStyle = strokeStyle;

// each side (radial 'line') is modeled as a pentagon (some lines can be short arcs though)
ctx.beginPath();
const yThreshold = Math.max(TAPER_OFF_LIMIT, (LINE_WIDTH_MULT * strokeWidth) / (X1 - X0));
const beta = strokeWidth / yThreshold; // angle where strokeWidth equals the lineWidthMult limit at a radius of yThreshold
ctx.arc(0, 0, y0px, X0, X0 + beta * (yThreshold / y0px));
ctx.arc(0, 0, yThreshold, X0 + beta, X0 + beta);
ctx.arc(0, 0, Math.min(yThreshold, y1px), X0 + beta, X0 + beta);
ctx.arc(0, 0, y1px, X0 + beta * (yThreshold / y1px), X0, true);
ctx.arc(0, 0, y0px, X0, X0);
ctx.fill();
} else {
ctx.strokeStyle = strokeStyle;
ctx.stroke();
}
}
Expand Down Expand Up @@ -164,28 +165,41 @@ function renderLinkLabels(
ctx: CanvasRenderingContext2D,
linkLabelFontSize: Pixels,
linkLabelLineWidth: Pixels,
fontFamily: string,
linkLabelTextColor: string,
viewModels: LinkLabelVM[],
) {
const labelValueGap = linkLabelFontSize / 2; // one en space
withContext(ctx, (ctx) => {
ctx.lineWidth = linkLabelLineWidth;
ctx.strokeStyle = linkLabelTextColor;
ctx.fillStyle = linkLabelTextColor;
ctx.font = `${400} ${linkLabelFontSize}px ${fontFamily}`;
viewModels.forEach(({ link, translate, textAlign, text, valueText }: LinkLabelVM) => {
ctx.beginPath();
ctx.moveTo(...link[0]);
link.slice(1).forEach((point) => ctx.lineTo(...point));
ctx.stroke();
withContext(ctx, (ctx) => {
ctx.translate(...translate);
ctx.scale(1, -1); // flip for text rendering not to be upside down
ctx.textAlign = textAlign;
// only use a colon if both text and valueText are non-zero length strings
ctx.fillText(text + (text && valueText ? ': ' : '') + valueText, 0, 0);
});
});
viewModels.forEach(
({
link,
translate,
textAlign,
text,
valueText,
width,
labelFontSpec,
valueFontSpec,
valueWidth,
}: LinkLabelVM) => {
ctx.beginPath();
ctx.moveTo(...link[0]);
link.slice(1).forEach((point) => ctx.lineTo(...point));
ctx.stroke();
withContext(ctx, (ctx) => {
ctx.translate(...translate);
ctx.scale(1, -1); // flip for text rendering not to be upside down
ctx.textAlign = textAlign;
ctx.font = `${labelFontSpec.fontStyle} ${labelFontSpec.fontVariant} ${labelFontSpec.fontWeight} ${linkLabelFontSize}px ${labelFontSpec.fontFamily}`;
ctx.fillText(text, textAlign === 'right' ? -valueWidth - labelValueGap : 0, 0);
ctx.font = `${valueFontSpec.fontStyle} ${valueFontSpec.fontVariant} ${valueFontSpec.fontWeight} ${linkLabelFontSize}px ${valueFontSpec.fontFamily}`;
ctx.fillText(valueText, textAlign === 'left' ? width + labelValueGap : 0, 0);
});
},
);
});
}

Expand All @@ -195,7 +209,7 @@ export function renderPartitionCanvas2d(
dpr: number,
{ config, quadViewModel, rowSets, outsideLinksViewModel, linkLabelViewModels, diskCenter }: ShapeViewModel,
) {
const { sectorLineWidth, linkLabel, fontFamily /*, backgroundColor*/ } = config;
const { sectorLineWidth, sectorLineStroke, linkLabel /*, backgroundColor*/ } = config;

const linkLabelTextColor = addOpacity(linkLabel.textColor, linkLabel.textOpacity);

Expand All @@ -219,7 +233,7 @@ export function renderPartitionCanvas2d(
ctx.scale(1, -1);

ctx.lineJoin = 'round';
ctx.strokeStyle = 'white'; // todo make it configurable just like sectorLineWidth
ctx.strokeStyle = sectorLineStroke;
ctx.lineWidth = sectorLineWidth;

// painter's algorithm, like that of SVG: the sequence determines what overdraws what; first element of the array is drawn first
Expand All @@ -245,14 +259,7 @@ export function renderPartitionCanvas2d(

// all the text and link lines for single-row outside texts
(ctx: CanvasRenderingContext2D) =>
renderLinkLabels(
ctx,
linkLabel.fontSize,
linkLabel.lineWidth,
fontFamily,
linkLabelTextColor,
linkLabelViewModels,
),
renderLinkLabels(ctx, linkLabel.fontSize, linkLabel.lineWidth, linkLabelTextColor, linkLabelViewModels),
]);
});
}
1 change: 1 addition & 0 deletions src/utils/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type Datum = any; // unknown;
export type Rotation = 0 | 90 | -90 | 180;
export type Rendering = 'canvas' | 'svg';
export type Color = string;
export type StrokeStyle = Color; // now narrower than string | CanvasGradient | CanvasPattern

export const Position = Object.freeze({
Top: 'top' as 'top',
Expand Down
Loading

0 comments on commit 7dce0a3

Please sign in to comment.