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

WIP cartesian truncated labels #2312

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/charts/src/chart_types/xy_chart/axes/axes_sizes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { SmallMultiplesSpec } from '../../../specs';
import { Position } from '../../../utils/common';
import { innerPad, outerPad, PerSideDistance } from '../../../utils/dimensions';
import { Dimensions, innerPad, outerPad, PerSideDistance } from '../../../utils/dimensions';
import { AxisId } from '../../../utils/ids';
import { AxisStyle, Theme } from '../../../utils/themes/theme';
import { AxesTicksDimensions } from '../state/selectors/compute_axis_ticks_dimensions';
Expand Down Expand Up @@ -37,11 +37,7 @@ const getAxisSizeForLabel = (
const maxAxisGirth = axisDimension + (tickLabel.visible ? allLayersGirth : 0);
// gives space to longer labels: if vertical use half of the label height, if horizontal, use half of the max label (not ideal)
// don't overflow when the multiTimeAxis layer is used.
const maxLabelBoxHalfLength = isVerticalAxis(axisSpec.position)
? maxLabelBboxHeight / 2
: axisSpec.timeAxisLayerCount > 0
? 0
: maxLabelBboxWidth / 2;
const maxLabelBoxHalfLength = isVerticalAxis(axisSpec.position) ? maxLabelBboxHeight / 2 : 0;
return horizontal
? {
top: axisSpec.position === Position.Top ? maxAxisGirth + chartMargins.top : 0,
Expand All @@ -59,12 +55,17 @@ const getAxisSizeForLabel = (

/** @internal */
export function getAxesDimensions(
parentDimensions: Dimensions,
theme: Theme,
axisDimensions: AxesTicksDimensions,
axesStyles: Map<AxisId, AxisStyle | null>,
axisSpecs: AxisSpec[],
smSpec: SmallMultiplesSpec | null,
): PerSideDistance & { margin: { left: number } } {
const verticalAxesCount =
axisSpecs.reduce((count, spec) => {
return count + (isVerticalAxis(spec.position) ? 1 : 0);
}, 0) * 2;
const sizes = [...axisDimensions].reduce(
(acc, [id, tickLabelBounds]) => {
const axisSpec = getSpecsById<AxisSpec>(axisSpecs, id);
Expand All @@ -74,8 +75,8 @@ export function getAxesDimensions(
if (isVerticalAxis(axisSpec.position)) {
acc.axisLabelOverflow.top = Math.max(acc.axisLabelOverflow.top, top);
acc.axisLabelOverflow.bottom = Math.max(acc.axisLabelOverflow.bottom, bottom);
acc.axisMainSize.left += left;
acc.axisMainSize.right += right;
acc.axisMainSize.left += Math.min(left, parentDimensions.width / verticalAxesCount);
acc.axisMainSize.right += Math.min(right, parentDimensions.width / verticalAxesCount);
} else {
// find the max half label size to accommodate the left/right labels
acc.axisMainSize.top += top;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface AxisProps {
debug: boolean;
renderingArea: Dimensions;
layerGirth: number;
maxLabelSize: number;
}

/** @internal */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
*/

import { AxisProps } from '.';
import { measureText } from '../../../../../utils/bbox/canvas_text_bbox_calculator';
import { Position } from '../../../../../utils/common';
import { wrapText } from '../../../../../utils/text/wrap';
import { AxisTick, getTickLabelPosition } from '../../../utils/axis_utils';
import { renderText } from '../primitives/text';
import { renderDebugRectCenterRotated } from '../utils/debug';
Expand All @@ -19,7 +21,7 @@ export function renderTickLabel(
ctx: CanvasRenderingContext2D,
tick: AxisTick,
showTicks: boolean,
{ axisSpec: { position, timeAxisLayerCount }, dimension, size, debug, axisStyle }: AxisProps,
{ axisSpec: { position, timeAxisLayerCount }, dimension, size, debug, axisStyle, maxLabelSize }: AxisProps,
layerGirth: number,
) {
const labelStyle = axisStyle.tickLabel;
Expand All @@ -36,7 +38,22 @@ export function renderTickLabel(
);

const center = { x: tickLabelProps.x + tickLabelProps.offsetX, y: tickLabelProps.y + tickLabelProps.offsetY };

const textMeasure = measureText(ctx);
const wrappedText = wrapText(
tick.label,
{
fontFamily: labelStyle.fontFamily,
fontStyle: labelStyle.fontStyle ?? 'normal',
fontVariant: 'normal',
fontWeight: 'normal',
textColor: labelStyle.fill,
},
labelStyle.fontSize,
maxLabelSize,
1,
textMeasure,
'en', // TODO
);
if (debug) {
const { maxLabelBboxWidth, maxLabelBboxHeight, maxLabelTextWidth: width, maxLabelTextHeight: height } = dimension;
// full text container
Expand All @@ -52,7 +69,7 @@ export function renderTickLabel(
renderText(
ctx,
center,
tick.label,
wrappedText[0] ?? '',
{
fontFamily: labelStyle.fontFamily,
fontStyle: labelStyle.fontStyle ?? 'normal',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function renderPanelSubstrates(ctx: CanvasRenderingContext2D, props: Axes
dimension,
visibleTicks: ticks,
parentSize,
maxLabelSize,
} = geometry;
const axisSpec = getSpecsById<AxisSpec>(axesSpecs, id);

Expand Down Expand Up @@ -105,6 +106,7 @@ export function renderPanelSubstrates(ctx: CanvasRenderingContext2D, props: Axes
debug,
renderingArea,
layerGirth,
maxLabelSize,
},
locale,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import { axisSpecsLookupSelector } from './get_specs';
import { getVisibleTickSetsSelector } from './visible_ticks';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales';
import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions';
import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme';
import { getAxesGeometries } from '../../utils/axis_utils';

/** @internal */
export const computeAxesGeometriesSelector = createCustomCachedSelector(
[
getChartContainerDimensionsSelector,
computeChartDimensionsSelector,
getChartThemeSelector,
axisSpecsLookupSelector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function getVisibleTicks(

const { showOverlappingTicks, showOverlappingLabels, position } = axisSpec;
const requiredSpace = isVerticalAxis(position) ? labelBox.maxLabelBboxHeight / 2 : labelBox.maxLabelBboxWidth / 2;
const bypassOverlapCheck = showOverlappingLabels || isMultilayerTimeAxis;
const bypassOverlapCheck = scale.type === ScaleType.Ordinal || showOverlappingLabels || isMultilayerTimeAxis;
return bypassOverlapCheck
? allTicks
: [...allTicks]
Expand Down
21 changes: 19 additions & 2 deletions packages/charts/src/chart_types/xy_chart/utils/axis_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isHorizontalAxis, isVerticalAxis } from './axis_type_utils';
import { computeXScale, computeYScales } from './scales';
import { SmallMultipleScales, hasSMDomain, getPanelSize } from '../../../common/panel_utils';
import { ScaleBand, ScaleContinuous } from '../../../scales';
import { isContinuousScale } from '../../../scales/types';
import { AxisSpec, SettingsSpec } from '../../../specs';
import {
degToRad,
Expand Down Expand Up @@ -261,13 +262,15 @@ export const getAllAxisLayersGirth = (

/** @internal */
export function getPosition(
parentDimension: Dimensions,
{ chartDimensions }: { chartDimensions: Dimensions },
chartMargins: PerSideDistance,
{ axisTitle, axisPanelTitle, tickLine, tickLabel }: AxisStyle,
{ title, position, hide, timeAxisLayerCount }: AxisSpec,
{ maxLabelBboxHeight, maxLabelBboxWidth }: TickLabelBounds,
smScales: SmallMultipleScales,
{ top: cumTopSum, bottom: cumBottomSum, left: cumLeftSum, right: cumRightSum }: PerSideDistance,
verticalAxisCount: number,
) {
const tickDimension = shouldShowTicks(tickLine, hide) ? tickLine.size + tickLine.padding : 0;
const labelPaddingSum = tickLabel.visible ? innerPad(tickLabel.padding) + outerPad(tickLabel.padding) : 0;
Expand All @@ -276,7 +279,8 @@ export function getPosition(
const scaleBand = vertical ? smScales.vertical : smScales.horizontal;
const panelTitleDimension = hasSMDomain(scaleBand) ? getTitleDimension(axisPanelTitle) : 0;
const maxLabelBboxGirth = tickLabel.visible ? (vertical ? maxLabelBboxWidth : maxLabelBboxHeight) : 0;
const shownLabelSize = getAllAxisLayersGirth(timeAxisLayerCount, maxLabelBboxGirth, !vertical);
const layerGrith = getAllAxisLayersGirth(timeAxisLayerCount, maxLabelBboxGirth, !vertical);
const shownLabelSize = vertical ? Math.min(parentDimension.width / (verticalAxisCount * 2), layerGrith) : layerGrith;
const parallelSize = labelPaddingSum + shownLabelSize + tickDimension + titleDimension + panelTitleDimension;
return {
leftIncrement: position === Position.Left ? parallelSize + chartMargins.left : 0,
Expand Down Expand Up @@ -316,32 +320,39 @@ export interface AxisGeometry {
};
dimension: TickLabelBounds;
visibleTicks: AxisTick[];
maxLabelSize: number;
}

/** @internal */
export function getAxesGeometries(
parentDimension: Dimensions,
chartDims: { chartDimensions: Dimensions; leftMargin: number },
{ chartPaddings, chartMargins, axes: sharedAxesStyle }: Theme,
axisSpecs: Map<AxisId, AxisSpec>,
axesStyles: Map<AxisId, AxisStyle | null>,
smScales: SmallMultipleScales,
visibleTicksSet: Map<AxisId, Projection>,
): AxisGeometry[] {
const verticalAxesCount = [...axisSpecs.values()].reduce((count, spec) => {
return count + (isVerticalAxis(spec.position) ? 1 : 0);
}, 0);
const panel = getPanelSize(smScales);
return [...visibleTicksSet].reduce(
(acc: PerSideDistance & { geoms: AxisGeometry[] }, [axisId, { ticks, labelBox }]: [AxisId, Projection]) => {
(acc: PerSideDistance & { geoms: AxisGeometry[] }, [axisId, { ticks, labelBox, scale }]: [AxisId, Projection]) => {
const axisSpec = axisSpecs.get(axisId);
if (axisSpec) {
const vertical = isVerticalAxis(axisSpec.position);
const axisStyle = axesStyles.get(axisId) ?? sharedAxesStyle;
const { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement } = getPosition(
parentDimension,
chartDims,
chartMargins,
axisStyle,
axisSpec,
labelBox,
smScales,
acc,
verticalAxesCount,
);
acc.top += topIncrement;
acc.bottom += bottomIncrement;
Expand All @@ -357,6 +368,12 @@ export function getAxesGeometries(
width: labelBox.isHidden ? 0 : vertical ? dimensions.width : panel.width,
height: labelBox.isHidden ? 0 : vertical ? panel.height : dimensions.height,
},
maxLabelSize:
vertical && !isContinuousScale(scale)
? parentDimension.width / (verticalAxesCount * 2)
: isContinuousScale(scale)
? Infinity
: scale.step,
});
} else {
throw new Error(`Cannot compute scale for axis spec ${axisId}`); // todo move this feedback as upstream as possible
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface ChartDimensions {
axisSpecs: AxisSpec[],
smSpec: SmallMultiplesSpec | null,
): ChartDimensions {
const axesDimensions = getAxesDimensions(theme, axisTickDimensions, axesStyles, axisSpecs, smSpec);
const axesDimensions = getAxesDimensions(parentDimensions, theme, axisTickDimensions, axesStyles, axisSpecs, smSpec);
const chartWidth = parentDimensions.width - axesDimensions.left - axesDimensions.right;
const chartHeight = parentDimensions.height - axesDimensions.top - axesDimensions.bottom;
const pad = theme.chartPaddings;
Expand Down
136 changes: 136 additions & 0 deletions storybook/stories/bar/a1_horizontal_bars.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { range } from 'lodash';
import React from 'react';

import {
Axis,
BarSeries,
Chart,
GroupBy,
Position,
ScaleType,
Settings,
SmallMultiples,
DataGenerator,
} from '@elastic/charts';
import { getRandomNumberGenerator } from '@elastic/charts/src/mocks/utils';

import { ChartsStory } from '../../types';
import { useBaseTheme } from '../../use_base_theme';
const ng = getRandomNumberGenerator();
const rng = new DataGenerator();
const data = rng.generateSMGroupedSeries(3, 2, () => {
return rng.generateSimpleSeries(10).flatMap((d) =>
range(0, 6, 1).map((y) => {
return {
x: `category-longer-then-${d.x}`,
y: ng(0, 1000),
};
}),
);
});
console.log(data);
export const Example: ChartsStory = (_, { title, description }) => {
return (
<div style={{ border: '1px solid black', position: 'relative', width: '100%', height: '100%' }}>
<div style={{ resize: 'both', border: '1x solid black', overflow: 'hidden', width: '100%', height: '50%' }}>
<Chart title={title} description={description}>
<Settings
baseTheme={useBaseTheme()}
rotation={90}
theme={{
chartMargins: { top: 0, left: 0, right: 0, bottom: 0 },
chartPaddings: { top: 0, left: 0, right: 0, bottom: 0 },
axes: {
axisTitle: {
padding: 0,
},
axisPanelTitle: {
padding: 0,
},
tickLabel: {
padding: 0,
},
tickLine: {
padding: 0,
},
},
}}
/>

<Axis id="left" position={Position.Left} />
<Axis id="right" position={Position.Right} />
<Axis id="bottom" position={Position.Bottom} />
<BarSeries
id="horizontal bar chart"
xScaleType={ScaleType.Ordinal}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={[
{ x: 'artifacts.elastic.co', y: 2, g: 'cluster a' },
{ x: 'www.elastic.co', y: 7, g: 'cluster a' },
{ x: 'cdn.elastic-elastic-elastic.orgcdn.elastic-elastic-elastic.org', y: 3, g: 'cluster a' },
{ x: 'docker.elastic.co', y: 6, g: 'cluster a' },

{ x: 'artifacts.elastic.co', y: 10, g: 'cluster B' },
{ x: 'www.elastic.co', y: 17, g: 'cluster B' },
{ x: 'cdn.elastic-elastic-elastic.orgcdn.elastic-elastic-elastic.org', y: 23, g: 'cluster B' },
{ x: 'docker.elastic.co', y: 8, g: 'cluster B' },
]}
/>
{/* <GroupBy id="g" by={(spec, datum) => datum.g} sort="alphaAsc" />
<SmallMultiples splitHorizontally="g" /> */}
</Chart>
</div>
<div style={{ resize: 'both', overflow: 'hidden', width: '100%', height: '50%' }}>
<Chart title={title} description={description}>
<Settings
baseTheme={useBaseTheme()}
rotation={90}
theme={{
chartMargins: { top: 0, left: 0, right: 0, bottom: 0 },
chartPaddings: { top: 0, left: 0, right: 0, bottom: 0 },
axes: {
axisTitle: {
padding: 0,
},
axisPanelTitle: {
padding: 0,
},
tickLabel: {
padding: 0,
},
tickLine: {
padding: 0,
},
},
}}
/>

<Axis id="bottom" position={Position.Bottom} />
<Axis id="left" position={Position.Left} />
{/* <Axis id="right" position={Position.Right} /> */}
<BarSeries
id="horizontal bar chart"
xScaleType={ScaleType.Ordinal}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={data}
/>
<GroupBy id="h" by={(spec, datum) => datum.h} sort="alphaAsc" />
<GroupBy id="v" by={(spec, datum) => datum.v} sort="alphaAsc" />
<SmallMultiples splitHorizontally="h" splitVertically="v" />
</Chart>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions storybook/stories/bar/bars.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ export { Example as testDualYAxis } from './49_test_dual_axis.story';
export { Example as testUseDefaultGroupDomain } from './56_test_use_dfl_gdomain.story';
export { Example as testRectBorder } from './57_test_rect_border_bars.story';
export { Example as dataValue } from './58_data_values.story';
export { Example as horizontalBarChart } from './a1_horizontal_bars.story';