Skip to content

Commit

Permalink
fix(partition): rtl text label support (elastic#1433)
Browse files Browse the repository at this point in the history
support rtl text labels in xy and partition charts, including mixed ltr/rtl.
# Conflicts:
#	packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts
#	packages/charts/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts
#	packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts
#	packages/charts/src/chart_types/xy_chart/utils/axis_utils.test.ts
#	packages/charts/src/chart_types/xy_chart/utils/axis_utils.ts
  • Loading branch information
nickofthyme committed Nov 15, 2021
1 parent 671fdcd commit 1ccca38
Show file tree
Hide file tree
Showing 65 changed files with 701 additions and 145 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.
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.
37 changes: 30 additions & 7 deletions integration/tests/test_cases_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Side Public License, v 1.
*/

import { PartitionLayout, SeriesType } from '../../packages/charts/src';
import { eachRotation } from '../helpers';
import { common } from '../page_objects';

Expand All @@ -25,12 +26,34 @@ describe('Test cases stories', () => {
{ waitSelector: '.echReactiveChart_noResults' },
);
});
});

describe('annotation marker rotation', () => {
eachRotation.it(async (rotation) => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/iframe.html?id=test-cases--no-axes-annotation-bug-fix&knob-horizontal marker position=undefined&knob-vertical marker position=undefined&knob-chartRotation=${rotation}`,
);
}, 'should render marker with annotations with %s degree rotations');
describe('annotation marker rotation', () => {
eachRotation.it(async (rotation) => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/iframe.html?id=test-cases--no-axes-annotation-bug-fix&knob-horizontal marker position=undefined&knob-vertical marker position=undefined&knob-chartRotation=${rotation}`,
);
}, 'should render marker with annotations with %s degree rotations');
});

describe('RTL support', () => {
describe.each([SeriesType.Bar, PartitionLayout.treemap, PartitionLayout.sunburst])('%s chart type', (type) => {
describe.each(['HE', 'AR'])('lang %s', (lang) => {
describe.each(['rtl', 'ltr', 'mostly-rtl', 'mostly-ltr'])('%s text', (charSet) => {
it('should render with correct direction', async () => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/test-cases--rtl-text&globals=background:white;theme:light&knob-Chart type=${type}&knob-character set=${charSet}&knob-show legend=true&knob-clockwiseSectors=true&knob-rtl language=${lang}`,
);
});

if (type === PartitionLayout.sunburst) {
it('should render with correct direction - clockwiseSectors', async () => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/test-cases--rtl-text&globals=background:white;theme:light&knob-Chart type=${type}&knob-character set=${charSet}&knob-show legend=true&knob-clockwiseSectors=false&knob-rtl language=${lang}`,
);
});
}
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function shapeViewModel(
const boxedYValues = yValues.map<Box & { value: NonNullable<PrimitiveValue> }>((value) => ({
text: config.yAxisLabel.formatter(value),
value,
isValue: false,
...config.yAxisLabel,
}));

Expand Down Expand Up @@ -150,6 +151,7 @@ export function shapeViewModel(
return {
text: formatter(value, { timeZone: config.timeZone }),
value,
isValue: false,
...config.xAxisLabel,
x: chartDimensions.left + (scaleCallback(value) || 0),
y: cellHeight * pageSize + config.xAxisLabel.fontSize / 2 + config.xAxisLabel.padding,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const computeChartDimensionsSelector = createCustomCachedSelector(
return {
text: String(value),
value,
isValue: false,
...config.yAxisLabel,
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
Radian,
SizeRatio,
} from '../../../../common/geometry';
import { Font, VerticalAlignments } from '../../../../common/text_utils';
import { Font, HorizontalAlignment, VerticalAlignments } from '../../../../common/text_utils';
import { GroupByAccessor } from '../../../../specs';
import { LegendPath } from '../../../../state/actions/legend';
import { ContinuousDomainFocus } from '../../renderer/canvas/partition';
Expand All @@ -30,6 +30,7 @@ import { Config, PartitionLayout } from './config_types';

/** @internal */
export type LinkLabelVM = {
isRTL: boolean;
linkLabels: PointTuples;
translate: PointTuple;
textAlign: CanvasTextAlign;
Expand All @@ -44,6 +45,7 @@ export interface RowBox extends Font {
text: string;
width: Distance;
wordBeginning: Distance;
isValue?: boolean;
}

interface RowAnchor {
Expand Down Expand Up @@ -71,7 +73,8 @@ export interface RowSet {
fontSize: number;
rotation: Radian;
verticalAlignment: VerticalAlignments;
leftAlign: boolean; // might be generalized into horizontalAlign - if needed
horizontalAlignment: HorizontalAlignment;
isRTL: boolean;
container?: any;
clipText?: boolean;
}
Expand Down Expand Up @@ -193,6 +196,14 @@ export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointO
outerRadius: 0,
});

/** @internal */
export const hasMostlyRTLLabels = (geoms: ShapeViewModel[]): boolean =>
geoms.reduce(
(surplus: number, { rowSets }) =>
surplus + rowSets.reduce((excess: number, { isRTL }) => excess + (isRTL ? 1 : -1), 0),
0,
) > 0;

/** @public */
export type TreeLevel = number;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ import {
wrapToTau,
} from '../../../../common/geometry';
import { logarithm } from '../../../../common/math';
import { Box, Font, PartialFont, TextMeasure, VerticalAlignments } from '../../../../common/text_utils';
import {
Box,
Font,
HorizontalAlignment,
PartialFont,
TextMeasure,
VerticalAlignments,
} from '../../../../common/text_utils';
import { integerSnap, monotonicHillClimb } from '../../../../solvers/monotonic_hill_climb';
import { ValueFormatter } from '../../../../utils/common';
import { getOppositeAlignment, isRTLString, ValueFormatter } from '../../../../utils/common';
import { Layer } from '../../specs';
import { Config, Padding } from '../types/config_types';
import {
Expand Down Expand Up @@ -140,6 +147,7 @@ export const getRectangleRowGeometry: GetShapeRowGeometry<RectangleConstruction>
fontSize,
overhang,
);

return {
rowAnchorX: cx + left / 2 - right / 2,
rowAnchorY,
Expand All @@ -163,8 +171,9 @@ function identityRowSet(): RowSet {
fontSize: NaN,
fillTextColor: '',
rotation: NaN,
isRTL: false,
verticalAlignment: VerticalAlignments.middle,
leftAlign: false,
horizontalAlignment: HorizontalAlignment.center,
};
}

Expand All @@ -179,12 +188,11 @@ function getAllBoxes(
return rawTextGetter(node)
.split(' ')
.filter(Boolean)
.map((text) => ({ text, ...sizeInvariantFontShorthand }))
.map<Box>((text) => ({ text, ...sizeInvariantFontShorthand, isValue: false }))
.concat(
valueFormatter(valueGetter(node))
.split(' ')
[valueFormatter(valueGetter(node))]
.filter(Boolean)
.map((text) => ({ text, ...sizeInvariantFontShorthand, ...valueFont })),
.map<Box>((text) => ({ text, ...sizeInvariantFontShorthand, ...valueFont, isValue: true })),
);
}

Expand Down Expand Up @@ -224,6 +232,7 @@ function fill<C>(
leftAlign: boolean,
middleAlign: boolean,
) {
const horizontalAlignment = leftAlign ? HorizontalAlignment.left : HorizontalAlignment.center;
const { maxRowCount, fillLabel } = config;
return (allFontSizes: Pixels[][], textFillOrigin: PointTuple, node: QuadViewModel): RowSet => {
const container = shapeConstructor(node);
Expand Down Expand Up @@ -257,6 +266,7 @@ function fill<C>(
fontFamily,
textColor: node.textColor,
};
const isRtlString = isRTLString(rawTextGetter(node));
const allBoxes = getAllBoxes(rawTextGetter, valueGetter, valueFormatter, sizeInvariantFont, valueFont, node);
const [cx, cy] = textFillOrigin;

Expand All @@ -268,14 +278,15 @@ function fill<C>(
measure,
rotation,
verticalAlignment,
leftAlign,
horizontalAlignment,
container,
getShapeRowGeometry,
cx,
cy,
padding,
node,
clipText,
isRtlString,
),
fillTextColor: node.textColor,
};
Expand All @@ -287,7 +298,7 @@ function tryFontSize<C>(
measure: TextMeasure,
rotation: Radian,
verticalAlignment: VerticalAlignments,
leftAlign: boolean,
horizontalAlignment: HorizontalAlignment,
container: C,
getShapeRowGeometry: GetShapeRowGeometry<C>,
cx: Coordinate,
Expand All @@ -296,8 +307,10 @@ function tryFontSize<C>(
node: ShapeTreeNode,
boxes: Box[],
maxRowCount: number,
clipText?: boolean,
clipText: boolean,
isRTL: boolean,
) {
const adjustedHorizontalAlignment = isRTL ? getOppositeAlignment(horizontalAlignment) : horizontalAlignment;
return function tryFontSizeFn(initialRowSet: RowSet, fontSize: Pixels): { rowSet: RowSet; completed: boolean } {
let rowSet: RowSet = initialRowSet;

Expand Down Expand Up @@ -327,8 +340,9 @@ function tryFontSize<C>(
fillTextColor: '',
rotation,
verticalAlignment,
leftAlign,
horizontalAlignment: adjustedHorizontalAlignment,
clipText,
isRTL,
rows: [...new Array(targetRowCount)].map(() => ({
rowWords: [],
rowAnchorX: NaN,
Expand Down Expand Up @@ -372,7 +386,6 @@ function tryFontSize<C>(
while (measuredBoxes.length > 0 && rowHasRoom) {
// adding box to row
const [currentBox] = measuredBoxes;

const wordBeginning = currentRowLength;
currentRowLength += currentBox.width + wordSpacing;

Expand Down Expand Up @@ -402,20 +415,21 @@ function getRowSet<C>(
measure: TextMeasure,
rotation: Radian,
verticalAlignment: VerticalAlignments,
leftAlign: boolean,
horizontalAlignment: HorizontalAlignment,
container: C,
getShapeRowGeometry: GetShapeRowGeometry<C>,
cx: Coordinate,
cy: Coordinate,
padding: Padding,
node: ShapeTreeNode,
clipText: boolean,
) {
isRtl: boolean,
): RowSet {
const tryFunction = tryFontSize(
measure,
rotation,
verticalAlignment,
leftAlign,
horizontalAlignment,
container,
getShapeRowGeometry,
cx,
Expand All @@ -425,6 +439,7 @@ function getRowSet<C>(
boxes,
maxRowCount,
clipText,
isRtl,
);

// find largest fitting font size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
trueBearingToStandardPositionAngle,
} from '../../../../common/geometry';
import { cutToLength, fitText, Font, measureOneBoxWidth, TextMeasure } from '../../../../common/text_utils';
import { ValueFormatter } from '../../../../utils/common';
import { isRTLString, ValueFormatter } from '../../../../utils/common';
import { Logger } from '../../../../utils/logger';
import { Point } from '../../../../utils/point';
import { Config, LinkLabelConfig } from '../types/config_types';
Expand Down Expand Up @@ -167,13 +167,20 @@ function nodeToLinkLabel({
[stemToX + west * linkLabel.horizontalStemLength, stemToY],
];

const rawLabelText = rawTextGetter(node);
const isRTL = isRTLString(rawLabelText);

// value text is simple: the full, formatted value is always shown, not truncated
const valueText = valueFormatter(valueGetter(node));
const valueWidth = measureOneBoxWidth(measure, linkLabel.fontSize, { ...valueFont, text: valueText });
const valueWidth = measureOneBoxWidth(measure, linkLabel.fontSize, {
...valueFont,
text: valueText,
isValue: false,
});
const widthAdjustment = valueWidth + 2 * linkLabel.fontSize; // gap between label and value, plus possibly 2em wide ellipsis

// label text removes space allotted for value and gaps, then tries to fit as much as possible
const labelText = cutToLength(rawTextGetter(node), maxTextLength);
const labelText = cutToLength(rawLabelText, maxTextLength);
const allottedLabelWidth = Math.max(
0,
rightSide ? rectWidth - diskCenter.x - translateX - widthAdjustment : diskCenter.x + translateX - widthAdjustment,
Expand All @@ -184,6 +191,7 @@ function nodeToLinkLabel({
: { text: '', width: 0 };

return {
isRTL,
linkLabels,
translate,
text,
Expand Down
Loading

0 comments on commit 1ccca38

Please sign in to comment.