Skip to content

Commit daddc7f

Browse files
authored
feat: 增加labelAuto功能 (#2097)
* feat: 增加labelAuto功能 * feat: 增加旋转transformOrigin * chore: 修复单测 * chore: 修复单测 * chore: 修改cr意见
1 parent a4752a6 commit daddc7f

12 files changed

+383
-16
lines changed

packages/f2/src/components/axis/rect/bottom.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ export default (props: RectProps<'bottom'>, context) => {
8888
) : null}
8989
{label
9090
? filterTicks.map((tick, index) => {
91-
const { points, text, tickValue, labelStyle } = tick;
91+
const { points, text, tickValue, labelStyle, visible = true } = tick;
92+
if (!visible) return null;
9293
const { x, y } = points[0];
9394
const { align = 'center' } = labelStyle || label || {};
9495
const textAttrs: TextStyleProps = {

packages/f2/src/components/axis/rect/top.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ export default (props: RectProps, context) => {
8888
) : null}
8989
{label
9090
? ticks.map((tick, _index) => {
91-
const { tickValue, points, text, labelStyle } = tick;
91+
const { tickValue, points, text, labelStyle, visible } = tick;
92+
if (!visible) return null;
9293
const end = points[points.length - 1];
9394
return (
9495
<text

packages/f2/src/components/axis/types.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ interface Point {
4848

4949
export interface Tick {
5050
value: number;
51-
points: Point[];
51+
points?: Point[];
5252
text: string;
5353
tickValue: string | number;
5454
labelStyle?: Text;
5555
gridStyle?: LineStyleProps;
5656
gridPoints?: Point[];
57+
visible?: boolean;
58+
labelWidth?: number;
5759
}
5860

5961
type PolarCord = Pick<Coord, 'center'>;
@@ -130,4 +132,14 @@ export interface AxisProps<
130132
style?: StyleProps;
131133
// 网格线类型
132134
grid?: 'arc' | 'line';
135+
/**
136+
* 是否自动旋转标签以防止重叠
137+
* @default false
138+
*/
139+
labelAutoRotate?: boolean;
140+
/**
141+
* 是否自动隐藏标签
142+
* @default false
143+
*/
144+
labelAutoHide?: boolean;
133145
}

packages/f2/src/components/axis/withAxis.tsx

Lines changed: 186 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default (View) => {
2727
IProps extends AxisProps<TRecord> = AxisProps<TRecord>
2828
> extends Component<IProps & ChartChildProps, {}> {
2929
axisStyle: Style = {};
30+
ticks: Tick[];
3031

3132
constructor(props: IProps & ChartChildProps) {
3233
super(props);
@@ -223,22 +224,66 @@ export default (View) => {
223224
});
224225
}
225226

226-
measureLayout(): PositionLayout | PositionLayout[] {
227+
calculateLabelOverflow(lastTick, label): number {
228+
if (!lastTick || !label) {
229+
return 0;
230+
}
231+
const { props, context } = this;
232+
const { measureText } = context;
233+
const { coord } = props;
234+
const { labelStyle = {}, text } = lastTick;
235+
236+
const tickBBox = measureText(labelStyle.text || text, { ...label, ...labelStyle });
237+
238+
const lastTickPoint = coord.convertPoint({
239+
x: lastTick.value,
240+
y: 0,
241+
});
242+
243+
let labelRightEdge = lastTickPoint.x;
244+
const align = label.align || 'center';
245+
246+
if (align === 'center') {
247+
labelRightEdge += tickBBox.width / 2;
248+
} else if (align === 'left' || align === 'start') {
249+
labelRightEdge += tickBBox.width;
250+
}
251+
252+
return labelRightEdge > coord.right ? labelRightEdge - coord.right : 0;
253+
}
254+
255+
_getXTicksDistance(ticks) {
256+
const { props } = this;
257+
const { coord } = props;
258+
259+
const firstPoint = coord.convertPoint({
260+
x: ticks[0].value,
261+
y: 0,
262+
});
263+
264+
const secondPoint = coord.convertPoint({
265+
x: ticks[1].value,
266+
y: 0,
267+
});
268+
return Math.abs(secondPoint.x - firstPoint.x);
269+
}
270+
271+
measureLayout(ticks): PositionLayout | PositionLayout[] {
227272
const { props, context } = this;
228-
const { visible, coord, style } = props;
273+
const { visible, coord, style, labelAutoRotate = false, labelAutoHide = false } = props;
229274
if (visible === false) {
230275
return null;
231276
}
232277
const { width: customWidth, height: customHeight } = style || {};
233278

234-
const ticks = this.getTicks();
235279
const bbox = this.getMaxBBox(ticks, this.axisStyle);
236280

237281
const { isPolar } = coord;
238282
const dimType = this._getDimType();
239-
// const { width, height } = bbox;
283+
240284
const width = isNil(customWidth) ? bbox.width : context.px2hd(customWidth);
241285
const height = isNil(customHeight) ? bbox.height : context.px2hd(customHeight);
286+
242287
if (isPolar) {
243288
// 机坐标系的 y 不占位置
244289
if (dimType === 'y') {
@@ -258,31 +303,164 @@ export default (View) => {
258303

259304
// 直角坐标系下
260305
const position = this._getPosition();
306+
307+
if ((labelAutoRotate || labelAutoHide) && dimType === 'x') {
308+
const { label } = this.axisStyle;
309+
const lastTick = ticks[ticks.length - 1];
310+
311+
const overflowWidth = this.calculateLabelOverflow(lastTick, label);
312+
313+
return [
314+
{
315+
position,
316+
width,
317+
height,
318+
},
319+
{
320+
position: 'right',
321+
width: overflowWidth,
322+
height: 0,
323+
},
324+
];
325+
}
326+
261327
return {
262328
position,
263329
width,
264330
height,
265331
};
266332
}
267333

334+
findSuitableRotation(ticks) {
335+
const { context } = this;
336+
const { measureText } = context;
337+
338+
const averageSpace = this._getXTicksDistance([ticks[0], ticks[1]]);
339+
const { label } = this.axisStyle;
340+
const { labelStyle = {}, text } = ticks[0];
341+
const bbox = measureText(labelStyle.text || text, { ...label, ...labelStyle });
342+
const labelHeight = bbox.height;
343+
344+
// 安全距离
345+
const safetyDistance = 2;
346+
347+
const availableSpace = labelHeight + safetyDistance;
348+
349+
const cosValue = availableSpace / averageSpace;
350+
351+
const clampedCosValue = Math.max(-1, Math.min(1, cosValue));
352+
353+
const theoreticalAngle = (Math.acos(clampedCosValue) * 180) / Math.PI;
354+
355+
const ceiledAngle = Math.ceil(theoreticalAngle);
356+
357+
if (ceiledAngle > 0 && ceiledAngle <= 90) {
358+
this.axisStyle.label.align = 'start';
359+
this.axisStyle.label.transform = `rotate(${ceiledAngle}deg)`;
360+
this.axisStyle.label.transformOrigin = `0 50%`;
361+
}
362+
}
363+
364+
hasOverlapAtSeq(ticks, step) {
365+
const safetyMargin = 2;
366+
const XDistance = this._getXTicksDistance([ticks[0], ticks[step]]);
367+
368+
let prevIdx = 0;
369+
for (let currIdx = step; currIdx <= ticks.length - 1; currIdx += step) {
370+
const minDistance =
371+
(ticks[prevIdx].labelWidth + ticks[currIdx].labelWidth) / 2 + safetyMargin;
372+
373+
if (XDistance < minDistance) {
374+
return true;
375+
}
376+
377+
prevIdx = currIdx;
378+
}
379+
380+
return false;
381+
}
382+
383+
findLabelsToHide(ticks) {
384+
const { props, context } = this;
385+
const { coord } = props;
386+
const { measureText } = context;
387+
388+
const tickCount = ticks.length;
389+
390+
const { label } = this.axisStyle;
391+
392+
let maxLabelWidth = 0;
393+
for (let i = 0; i < tickCount; i++) {
394+
const tick = ticks[i];
395+
const { labelStyle = {}, text } = tick;
396+
const bbox = measureText(labelStyle.text || text, { ...label, ...labelStyle });
397+
tick.labelWidth = bbox.width;
398+
maxLabelWidth = Math.max(maxLabelWidth, bbox.width);
399+
}
400+
401+
const initialSeq = Math.floor(maxLabelWidth / (coord.width / (tickCount - 1)));
402+
403+
const range = tickCount - 1;
404+
const maxSeq = Math.floor(range / 2);
405+
406+
let finalSeq = initialSeq;
407+
408+
while (finalSeq <= maxSeq && range % finalSeq !== 0) {
409+
finalSeq++;
410+
}
411+
412+
while (finalSeq <= maxSeq && this.hasOverlapAtSeq(ticks, finalSeq)) {
413+
finalSeq++;
414+
while (finalSeq <= maxSeq && range % finalSeq !== 0) {
415+
finalSeq++;
416+
}
417+
}
418+
419+
if (finalSeq > maxSeq || finalSeq === 1) {
420+
return;
421+
}
422+
423+
ticks.forEach((tick) => {
424+
tick.visible = false;
425+
});
426+
427+
for (let i = 0; i <= range; i += finalSeq) {
428+
ticks[i].visible = true;
429+
}
430+
}
431+
268432
// 主要是计算coord的布局
269433
updateCoord() {
270434
const { props } = this;
271-
const { chart } = props;
272-
const layout = this.measureLayout();
435+
const { chart, labelAutoRotate = false, labelAutoHide = false } = props;
436+
const dimType = this._getDimType();
437+
const ticks = this.getTicks();
438+
439+
if (labelAutoRotate && dimType === 'x') {
440+
this.findSuitableRotation(ticks);
441+
this.ticks = ticks;
442+
}
443+
if (labelAutoHide && dimType === 'x') {
444+
this.findLabelsToHide(ticks);
445+
this.ticks = ticks;
446+
}
447+
448+
const layout = this.measureLayout(ticks);
449+
273450
chart.updateCoordFor(this, layout);
274451
}
275452

276453
render() {
277454
const { props, axisStyle } = this;
278455
const { visible, coord } = props;
456+
const dimType = this._getDimType();
457+
279458
if (visible === false) {
280459
return null;
281460
}
282461

283-
const ticks = this.getTicks();
462+
const ticks = this.ticks ? this.ticks : this.getTicks();
284463
const position = this._getPosition();
285-
const dimType = this._getDimType();
286464

287465
return (
288466
<View
54.3 KB
Loading
54.6 KB
Loading
37.4 KB
Loading
16 KB
Loading
10.7 KB
Loading

0 commit comments

Comments
 (0)