Skip to content

Commit

Permalink
feat(bar_chart): add Alignment offset to value labels (#784)
Browse files Browse the repository at this point in the history
New parameters to better control value labels horizontal and vertical positioning/alignment are introduced to better control the visual outlook of the charts.
  • Loading branch information
dej611 authored Oct 13, 2020
1 parent f173b49 commit 363aeb4
Show file tree
Hide file tree
Showing 41 changed files with 249 additions and 49 deletions.
4 changes: 4 additions & 0 deletions api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ export interface DisplayValueSpec {
export type DisplayValueStyle = TextStyle & {
offsetX: number;
offsetY: number;
alignment?: {
horizontal: Exclude<HorizontalAlignment, 'far' | 'near'>;
vertical: Exclude<VerticalAlignment, 'far' | 'near'>;
};
};

// @public (undocumented)
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.
27 changes: 27 additions & 0 deletions integration/tests/bar_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { DisplayValueStyle, HorizontalAlignment, Rotation, VerticalAlignment } from '../../src';
import { common } from '../page_objects';

describe('Bar series stories', () => {
Expand Down Expand Up @@ -159,4 +160,30 @@ describe('Bar series stories', () => {
});
});
});

describe('value labels positioning', () => {
describe.each<[string, Rotation]>([
['0', 0],
['90', 90],
['180', 180],
['negative 90', -90],
])('rotation - %s', (_, rotation) => {
describe.each<NonNullable<DisplayValueStyle['alignment']>['vertical']>([
VerticalAlignment.Middle,
VerticalAlignment.Top,
VerticalAlignment.Bottom,
])('Vertical Alignment - %s', (verticalAlignment) => {
describe.each<NonNullable<DisplayValueStyle['alignment']>['horizontal']>([
HorizontalAlignment.Left,
HorizontalAlignment.Center,
HorizontalAlignment.Right,
])('Horizontal Alignment - %s', (horizontalAlignment) => {
const url = `http://localhost:9001/?path=/story/bar-chart--with-value-label&knob-chartRotation=${rotation}&knob-Horizontal alignment=${horizontalAlignment}&knob-Vertical alignment=${verticalAlignment}`;
it('place the value labels on the correct area', async () => {
await common.expectChartAtUrlToMatchScreenshot(url);
});
});
});
});
});
});
241 changes: 192 additions & 49 deletions src/chart_types/xy_chart/renderer/canvas/values/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
*/

import { Rect } from '../../../../../geoms/types';
import { Rotation } from '../../../../../utils/commons';
import { Rotation, VerticalAlignment, HorizontalAlignment } from '../../../../../utils/commons';
import { Dimensions } from '../../../../../utils/dimensions';
import { BarGeometry } from '../../../../../utils/geometry';
import { Point } from '../../../../../utils/point';
import { Theme } from '../../../../../utils/themes/theme';
import { Theme, TextAlignment } from '../../../../../utils/themes/theme';
import { Font, FontStyle, TextBaseline, TextAlign } from '../../../../partition_chart/layout/types/types';
import { renderText, wrapLines } from '../primitives/text';
import { renderDebugRect } from '../utils/debug';
Expand All @@ -35,10 +35,17 @@ interface BarValuesProps {
bars: BarGeometry[];
}

const CHART_DIRECTION: Record<string, Rotation> = {
BottomUp: 0,
TopToBottom: 180,
LeftToRight: 90,
RightToLeft: -90,
};

/** @internal */
export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesProps) {
const { bars, debug, chartRotation, chartDimensions, theme } = props;
const { fontFamily, fontStyle, fill, fontSize } = theme.barSeriesStyle.displayValue;
const { fontFamily, fontStyle, fill, fontSize, alignment } = theme.barSeriesStyle.displayValue;
const barsLength = bars.length;
for (let i = 0; i < barsLength; i++) {
const { displayValue } = bars[i];
Expand All @@ -65,6 +72,7 @@ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesP
displayValue,
chartRotation,
theme.barSeriesStyle.displayValue,
alignment,
);

if (displayValue.isValueContainedInElement) {
Expand All @@ -80,13 +88,13 @@ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesP
const { width, height } = textLines;
const linesLength = textLines.lines.length;

for (let i = 0; i < linesLength; i++) {
const text = textLines.lines[i];
const origin = repositionTextLine({ x, y }, chartRotation, i, linesLength, { height, width });
for (let j = 0; j < linesLength; j++) {
const textLine = textLines.lines[j];
const origin = repositionTextLine({ x, y }, chartRotation, j, linesLength, { height, width });
renderText(
ctx,
origin,
text,
textLine,
{
...font,
fill,
Expand Down Expand Up @@ -132,57 +140,192 @@ function repositionTextLine(
return { x: lineX, y: lineY };
}

function positionText(
function computeHorizontalOffset(
geom: BarGeometry,
valueBox: { width: number; height: number },
chartRotation: Rotation,
offsets: { offsetX: number; offsetY: number },
{ horizontal }: Partial<TextAlignment> = {},
) {
const { offsetX, offsetY } = offsets;
let baseline: TextBaseline = 'top';
let align: TextAlign = 'center';

let x = geom.x + geom.width / 2 - offsetX;
let y = geom.y - offsetY;
const rect: Rect = {
x: x - valueBox.width / 2,
y,
width: valueBox.width,
height: valueBox.height,
};
if (chartRotation === 180) {
baseline = 'bottom';
x = geom.x + geom.width / 2 + offsetX;
y = geom.y + offsetY;
rect.x = x - valueBox.width / 2;
rect.y = y;
}
if (chartRotation === 90) {
x = geom.x - offsetY;
y = geom.y + offsetX;
align = 'right';
rect.x = x;
rect.y = y;
rect.width = valueBox.height;
rect.height = valueBox.width;
switch (chartRotation) {
case CHART_DIRECTION.LeftToRight: {
if (horizontal === HorizontalAlignment.Left) {
return geom.height - valueBox.width;
}
if (horizontal === HorizontalAlignment.Center) {
return geom.height / 2 - valueBox.width / 2;
}
break;
}
case CHART_DIRECTION.RightToLeft: {
if (horizontal === HorizontalAlignment.Right) {
return geom.height - valueBox.width;
}
if (horizontal === HorizontalAlignment.Center) {
return geom.height / 2 - valueBox.width / 2;
}
break;
}
case CHART_DIRECTION.TopToBottom: {
if (horizontal === HorizontalAlignment.Left) {
return geom.width / 2 - valueBox.width / 2;
}
if (horizontal === HorizontalAlignment.Right) {
return -geom.width / 2 + valueBox.width / 2;
}
break;
}
case CHART_DIRECTION.BottomUp:
default: {
if (horizontal === HorizontalAlignment.Left) {
return -geom.width / 2 + valueBox.width / 2;
}
if (horizontal === HorizontalAlignment.Right) {
return geom.width / 2 - valueBox.width / 2;
}
}
}
if (chartRotation === -90) {
x = geom.x + geom.width + offsetY;
y = geom.y - offsetX;
align = 'left';
rect.x = x - valueBox.height;
rect.y = y;
rect.width = valueBox.height;
rect.height = valueBox.width;
return 0;
}

function computeVerticalOffset(
geom: BarGeometry,
valueBox: { width: number; height: number },
chartRotation: Rotation,
{ vertical }: Partial<TextAlignment> = {},
) {
switch (chartRotation) {
case CHART_DIRECTION.LeftToRight: {
if (vertical === VerticalAlignment.Bottom) {
return geom.width - valueBox.height;
}
if (vertical === VerticalAlignment.Middle) {
return geom.width / 2 - valueBox.height / 2;
}
break;
}
case CHART_DIRECTION.RightToLeft: {
if (vertical === VerticalAlignment.Bottom) {
return -geom.width + valueBox.height;
}
if (vertical === VerticalAlignment.Middle) {
return -geom.width / 2 + valueBox.height / 2;
}
break;
}
case CHART_DIRECTION.TopToBottom: {
if (vertical === VerticalAlignment.Top) {
return geom.height - valueBox.height;
}
if (vertical === VerticalAlignment.Middle) {
return geom.height / 2 - valueBox.height / 2;
}
break;
}
case CHART_DIRECTION.BottomUp:
default: {
if (vertical === VerticalAlignment.Bottom) {
return geom.height - valueBox.height;
}
if (vertical === VerticalAlignment.Middle) {
return geom.height / 2 - valueBox.height / 2;
}
}
}
return 0;
}

function computeAlignmentOffset(
geom: BarGeometry,
valueBox: { width: number; height: number },
chartRotation: Rotation,
textAlignment: Partial<TextAlignment> = {},
) {
return {
x,
y,
align,
baseline,
rect,
alignmentOffsetX: computeHorizontalOffset(geom, valueBox, chartRotation, textAlignment),
alignmentOffsetY: computeVerticalOffset(geom, valueBox, chartRotation, textAlignment),
};
}

function positionText(
geom: BarGeometry,
valueBox: { width: number; height: number },
chartRotation: Rotation,
offsets: { offsetX: number; offsetY: number },
alignment?: TextAlignment,
): { x: number; y: number; align: TextAlign; baseline: TextBaseline; rect: Rect } {
const { offsetX, offsetY } = offsets;

const { alignmentOffsetX, alignmentOffsetY } = computeAlignmentOffset(geom, valueBox, chartRotation, alignment);

switch (chartRotation) {
case CHART_DIRECTION.TopToBottom: {
const x = geom.x + geom.width / 2 - offsetX + alignmentOffsetX;
const y = geom.y + offsetY + alignmentOffsetY;
return {
x,
y,
align: 'center',
baseline: 'bottom',
rect: {
x: x - valueBox.width / 2,
y,
width: valueBox.width,
height: valueBox.height,
},
};
}
case CHART_DIRECTION.RightToLeft: {
const x = geom.x + geom.width + offsetY + alignmentOffsetY;
const y = geom.y - offsetX + alignmentOffsetX;
return {
x,
y,
align: 'left',
baseline: 'top',
rect: {
x: x - valueBox.height,
y,
width: valueBox.height,
height: valueBox.width,
},
};
}
case CHART_DIRECTION.LeftToRight: {
const x = geom.x - offsetY + alignmentOffsetY;
const y = geom.y + offsetX + alignmentOffsetX;
return {
x,
y,
align: 'right',
baseline: 'top',
rect: {
x,
y,
width: valueBox.height,
height: valueBox.width,
},
};
}
case CHART_DIRECTION.BottomUp:
default: {
const x = geom.x + geom.width / 2 - offsetX + alignmentOffsetX;
const y = geom.y - offsetY + alignmentOffsetY;
return {
x,
y,
align: 'center',
baseline: 'top',
rect: {
x: x - valueBox.width / 2,
y,
width: valueBox.width,
height: valueBox.height,
},
};
}
}
}

function isOverflow(rect: Rect, chartDimensions: Dimensions, chartRotation: Rotation) {
let cWidth = chartDimensions.width;
let cHeight = chartDimensions.height;
Expand Down
4 changes: 4 additions & 0 deletions src/utils/themes/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ export type PartialTheme = RecursivePartial<Theme>;
export type DisplayValueStyle = TextStyle & {
offsetX: number;
offsetY: number;
alignment?: {
horizontal: Exclude<HorizontalAlignment, 'far' | 'near'>;
vertical: Exclude<VerticalAlignment, 'far' | 'near'>;
};
};

export interface PointStyle {
Expand Down
22 changes: 22 additions & 0 deletions stories/bar/2_label_value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ export const Example = () => {
fill: color('value color', '#000'),
offsetX: number('offsetX', 0),
offsetY: number('offsetY', 0),
alignment: {
horizontal: select(
'Horizontal alignment',
{
Default: undefined,
Left: 'left',
Center: 'center',
Right: 'right',
},
undefined,
),
vertical: select(
'Vertical alignment',
{
Default: undefined,
Top: 'top',
Middle: 'middle',
Bottom: 'bottom',
},
undefined,
),
},
},
},
};
Expand Down

0 comments on commit 363aeb4

Please sign in to comment.