Skip to content

Commit b7758db

Browse files
committed
feat: enhance padding and fontsize
1 parent d05ce0e commit b7758db

9 files changed

Lines changed: 250 additions & 112 deletions

File tree

packages/vchart-extension/__tests__/runtime/browser/test-page/storyline.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,11 @@ const createPortraitSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
151151
// arc:弧形布局,通过 direction 切换 dome('up')/ bowl('down')
152152
const createArcSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
153153
type: 'storyline',
154-
padding: [50, 20, 100, 20],
154+
// padding: 0,
155155
width: WIDTH,
156156
height: HEIGHT,
157157
data: buildData(layout),
158-
layout: { type: 'arc', direction: 'down' },
158+
layout: { type: 'arc', direction: 'up' },
159159
titleImage,
160160
themeColor
161161
});
@@ -165,7 +165,7 @@ const createClockSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
165165
type: 'storyline',
166166
height: HEIGHT,
167167
width: WIDTH,
168-
padding: [20, 20, 50, 20],
168+
// padding: [20, 20, 50, 20],
169169
layout: 'clock',
170170
titleImage,
171171
themeColor,
@@ -227,7 +227,7 @@ const createClockSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
227227

228228
const createDefaultSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
229229
type: 'storyline',
230-
padding: 20,
230+
// padding: 20,
231231
data: buildData(layout),
232232
layout,
233233
titleImage,
@@ -250,7 +250,7 @@ const createDefaultSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
250250
// wing:椭圆弧时间线(参考残奥历史信息图),通过 layout.direction 切换左/右翅膀
251251
const createWingSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
252252
type: 'storyline',
253-
padding: [40, 40, 40, 40],
253+
// padding: [40, 40, 40, 40],
254254
height: WIDTH,
255255
width: HEIGHT,
256256
data: buildData(layout),
@@ -265,7 +265,7 @@ const createLadderSpec = (layout: StorylineLayoutType): IStorylineSpec => ({
265265
type: 'storyline',
266266
width: 1600,
267267
height: 900,
268-
padding: 20,
268+
// padding: 20,
269269
layout: { type: 'ladder', direction: 'down', headline: 'ladder' },
270270
themeColor: '#C8102E',
271271
background: 'transparent',
@@ -376,7 +376,7 @@ const run = () => {
376376
window.vchart = cs;
377377
};
378378

379-
select.value = 'clock';
379+
select.value = 'arc';
380380
render(select.value as StorylineLayoutType);
381381

382382
select.addEventListener('change', () => {

packages/vchart-extension/src/charts/storyline/layouts/arc.ts

Lines changed: 117 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
type ICustomMarkSpec,
66
type LayoutContext,
77
type StorylinePoint,
8-
buildRichContent,
8+
BLOCK_TITLE_MAX_LINES,
9+
buildPlainContent,
10+
getBlockTitleHeight,
911
getImageBackgroundStyle,
1012
getRegionGeometry,
1113
getThemeColor,
@@ -30,9 +32,9 @@ const ARC_BLOCK_IMAGE_HALO_PADDING = 6;
3032
const ARC_TEXT_GAP_FROM_IMAGE = 10;
3133
const ARC_TITLE_FONT_SIZE = 32;
3234
const ARC_TITLE_LINE_HEIGHT = 34;
33-
const ARC_CONTENT_LINE_HEIGHT = 26;
34-
const ARC_CONTENT_FONT_SIZE = 20;
35-
// title + content 区域总高度(默认 240px,溢出由富文本 heightLimit + ellipsis 自动截断)
35+
const ARC_CONTENT_LINE_HEIGHT = 24;
36+
const ARC_CONTENT_FONT_SIZE = 18;
37+
// title + content 区域总高度(默认 240px,溢出由 heightLimit + ellipsis 自动截断)
3638
const ARC_TEXT_BOX_HEIGHT = 240;
3739
const ARC_TITLE_TO_CONTENT_GAP = 4;
3840
// 引导线与 title/content 之间的水平间距
@@ -45,9 +47,21 @@ const ARC_TITLE_IMAGE_MAX_WIDTH = 900;
4547
const ARC_TITLE_IMAGE_HEIGHT_RATIO = 0.34;
4648
// 弧线最高/最低点距离 titleImage 顶部/底部的距离
4749
const ARC_GAP_FROM_TITLE_IMAGE = 200;
50+
const ARC_FIT_MARGIN = 8;
4851

4952
const isDownArc = (spec: IStorylineSpec) => normalizeLayout(spec.layout).direction === 'down';
5053

54+
type ArcGeometry = {
55+
cx: number;
56+
cy: number;
57+
rx: number;
58+
ry: number;
59+
startAngle: number;
60+
endAngle: number;
61+
centerTop: number;
62+
centerBottom: number;
63+
};
64+
5165
/**
5266
* 计算 arc 布局 titleImage 的 box:水平居中。
5367
* - up(dome):垂直贴底(位于 inner 区域底部)
@@ -84,7 +98,7 @@ const getArcTitleImageRect = (spec: IStorylineSpec, ctx: LayoutContext) => {
8498
* → ry = (GAP + titleImageHeight) / (1 - sin(startAngle))
8599
* cy = titleImageTop - ry * sin(startAngle)
86100
*/
87-
const getArcGeometry = (spec: IStorylineSpec, ctx: LayoutContext) => {
101+
const getBaseArcGeometry = (spec: IStorylineSpec, ctx: LayoutContext): ArcGeometry => {
88102
const { width, startX } = getRegionGeometry(ctx);
89103
// width 已经是 VChart 减去 spec.padding 后的 region 宽度
90104
const innerWidth = Math.max(width, 1);
@@ -130,8 +144,7 @@ const getArcGeometry = (spec: IStorylineSpec, ctx: LayoutContext) => {
130144
* 同时让 block 沿弧线径向向外偏移 imageHeight/2,
131145
* 使 image 内边贴在弧线上,image + text 整体位于弧线外侧。
132146
*/
133-
const getArcBlockCenter = (spec: IStorylineSpec, ctx: LayoutContext, index: number): StorylinePoint => {
134-
const arc = getArcGeometry(spec, ctx);
147+
const getArcBlockCenterByGeometry = (spec: IStorylineSpec, arc: ArcGeometry, index: number): StorylinePoint => {
135148
const count = spec.data?.length ?? 0;
136149
if (count <= 0) {
137150
return { x: arc.cx, y: arc.cy };
@@ -151,6 +164,90 @@ const getArcBlockCenter = (spec: IStorylineSpec, ctx: LayoutContext, index: numb
151164
return { x: px + nx * offset, y: py + ny * offset };
152165
};
153166

167+
const getArcBlockBounds = (spec: IStorylineSpec, arc: ArcGeometry, index: number) => {
168+
const center = getArcBlockCenterByGeometry(spec, arc, index);
169+
const metrics = getArcBlockMetrics(spec, index);
170+
const halo = shouldShowImageBackground(spec) ? ARC_BLOCK_IMAGE_HALO_PADDING + ARC_BLOCK_IMAGE_BORDER : 0;
171+
const minX = Math.min(metrics.imageBox.x - halo, metrics.textBox.x, metrics.contentBox.x);
172+
const maxX = Math.max(
173+
metrics.imageBox.x + metrics.imageBox.width + halo,
174+
metrics.textBox.x + metrics.textBox.width,
175+
metrics.contentBox.x + metrics.contentBox.width
176+
);
177+
const minY = Math.min(metrics.imageBox.y - halo, metrics.textBox.y, metrics.contentBox.y);
178+
const maxY = Math.max(
179+
metrics.imageBox.y + metrics.imageBox.height + halo,
180+
metrics.textBox.y + metrics.textBox.height,
181+
metrics.contentBox.y + metrics.contentBox.height
182+
);
183+
return {
184+
left: center.x + minX,
185+
right: center.x + maxX,
186+
top: center.y + minY,
187+
bottom: center.y + maxY
188+
};
189+
};
190+
191+
const getArcBlocksBounds = (spec: IStorylineSpec, arc: ArcGeometry) => {
192+
const count = spec.data?.length ?? 0;
193+
if (!count) {
194+
return { left: arc.cx, right: arc.cx, top: arc.cy, bottom: arc.cy };
195+
}
196+
return Array.from({ length: count }, (_, index) => getArcBlockBounds(spec, arc, index)).reduce(
197+
(bounds, blockBounds) => ({
198+
left: Math.min(bounds.left, blockBounds.left),
199+
right: Math.max(bounds.right, blockBounds.right),
200+
top: Math.min(bounds.top, blockBounds.top),
201+
bottom: Math.max(bounds.bottom, blockBounds.bottom)
202+
}),
203+
{
204+
left: Number.POSITIVE_INFINITY,
205+
right: Number.NEGATIVE_INFINITY,
206+
top: Number.POSITIVE_INFINITY,
207+
bottom: Number.NEGATIVE_INFINITY
208+
}
209+
);
210+
};
211+
212+
const getArcGeometry = (spec: IStorylineSpec, ctx: LayoutContext): ArcGeometry => {
213+
const arc = getBaseArcGeometry(spec, ctx);
214+
const region = getRegionGeometry(ctx);
215+
const bounds = getArcBlocksBounds(spec, arc);
216+
const fit = {
217+
left: region.startX + ARC_FIT_MARGIN,
218+
right: region.startX + region.width - ARC_FIT_MARGIN,
219+
top: region.startY + ARC_FIT_MARGIN,
220+
bottom: region.startY + region.height - ARC_FIT_MARGIN
221+
};
222+
let shiftX = 0;
223+
let shiftY = 0;
224+
const boundsWidth = bounds.right - bounds.left;
225+
const boundsHeight = bounds.bottom - bounds.top;
226+
const fitWidth = fit.right - fit.left;
227+
const fitHeight = fit.bottom - fit.top;
228+
229+
if (boundsWidth > fitWidth) {
230+
shiftX = (fit.left + fit.right - bounds.left - bounds.right) / 2;
231+
} else if (bounds.left < fit.left) {
232+
shiftX = fit.left - bounds.left;
233+
} else if (bounds.right > fit.right) {
234+
shiftX = fit.right - bounds.right;
235+
}
236+
237+
if (boundsHeight > fitHeight) {
238+
shiftY = (fit.top + fit.bottom - bounds.top - bounds.bottom) / 2;
239+
} else if (bounds.top < fit.top) {
240+
shiftY = fit.top - bounds.top;
241+
} else if (bounds.bottom > fit.bottom) {
242+
shiftY = fit.bottom - bounds.bottom;
243+
}
244+
245+
return shiftX || shiftY ? { ...arc, cx: arc.cx + shiftX, cy: arc.cy + shiftY } : arc;
246+
};
247+
248+
const getArcBlockCenter = (spec: IStorylineSpec, ctx: LayoutContext, index: number): StorylinePoint =>
249+
getArcBlockCenterByGeometry(spec, getArcGeometry(spec, ctx), index);
250+
154251
/**
155252
* 贯穿所有 block 的弧线 mark(path 通过沿椭圆采样实现,与 arc block 的弧形布局完全重合)
156253
*
@@ -265,9 +362,10 @@ const getArcBlockMetrics = (spec: IStorylineSpec, index: number = 0) => {
265362
[8, 40]
266363
);
267364
const titleLineHeight = resolveAdaptiveLineHeight(titleFontSize, spec.title?.style as any, ARC_TITLE_LINE_HEIGHT);
365+
const titleHeight = getBlockTitleHeight(titleLineHeight, spec.data?.[index]?.title);
268366
// text 区域总高度固定为 ARC_TEXT_BOX_HEIGHT,content 占除 title 与间距外的全部高度
269367
const textHeight = ARC_TEXT_BOX_HEIGHT;
270-
const contentHeight = Math.max(textHeight - titleLineHeight - titleToContentGap, contentLineHeight);
368+
const contentHeight = Math.max(textHeight - titleHeight - titleToContentGap, contentLineHeight);
271369

272370
// 前 1/2 为左侧(奇数 count 时中间块也算左侧),右侧为后 1/2;
273371
// 左侧 title/content 右对齐(贴引导线),右侧 title/content 左对齐(贴引导线)
@@ -293,7 +391,7 @@ const getArcBlockMetrics = (spec: IStorylineSpec, index: number = 0) => {
293391
};
294392
const contentBox = {
295393
x: textBox.x,
296-
y: textBox.y + titleLineHeight + titleToContentGap,
394+
y: textBox.y + titleHeight + titleToContentGap,
297395
width: textBox.width,
298396
height: contentHeight
299397
};
@@ -419,6 +517,9 @@ export const buildArcBlockMark = (
419517
y: metrics.textBox.y,
420518
text: block.title,
421519
maxLineWidth: metrics.textBox.width,
520+
height: metrics.titleLineHeight * BLOCK_TITLE_MAX_LINES,
521+
heightLimit: metrics.titleLineHeight * BLOCK_TITLE_MAX_LINES,
522+
lineClamp: BLOCK_TITLE_MAX_LINES,
422523
fontSize: metrics.titleFontSize,
423524
lineHeight: metrics.titleLineHeight,
424525
fontWeight: 'bold',
@@ -428,6 +529,9 @@ export const buildArcBlockMark = (
428529
lineJoin: 'round',
429530
textAlign: metrics.textAlign,
430531
textBaseline: 'top',
532+
whiteSpace: 'normal',
533+
wordBreak: 'break-word',
534+
ellipsis: '...',
431535
...spec.title?.style
432536
}
433537
} as ICustomMarkSpec<'text'>)
@@ -439,22 +543,19 @@ export const buildArcBlockMark = (
439543
interactive: false,
440544
zIndex: LayoutZIndex.Mark + 4,
441545
...spec.content,
442-
textType: 'rich',
443546
style: {
444547
x: metrics.contentBox.x,
445548
y: metrics.contentBox.y,
446549
width: metrics.contentBox.width,
447550
height: metrics.contentBox.height,
448551
maxLineWidth: metrics.contentBox.width,
449552
heightLimit: metrics.contentBox.height,
450-
text: buildRichContent(contentText, spec, {
451-
fontSize: metrics.contentFontSize,
452-
lineHeight: metrics.contentLineHeight,
453-
fill: '#596173',
454-
align: metrics.textAlign
455-
}),
553+
text: buildPlainContent(contentText),
554+
fontSize: metrics.contentFontSize,
555+
lineHeight: metrics.contentLineHeight,
456556
textAlign: metrics.textAlign,
457557
textBaseline: 'top',
558+
whiteSpace: 'normal',
458559
wordBreak: 'break-word',
459560
ellipsis: '...',
460561
fill: '#596173',

packages/vchart-extension/src/charts/storyline/layouts/clock.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { IStorylineBlock, IStorylineSpec } from '../interface';
44
import {
55
type ICustomMarkSpec,
66
type LayoutContext,
7-
buildRichContent,
7+
BLOCK_TITLE_MAX_LINES,
8+
buildPlainContent,
89
getImageBackgroundStyle,
910
getRegionGeometry,
1011
getThemeColor,
@@ -53,8 +54,9 @@ const CLOCK_ORBIT_DASH = [4, 4];
5354
// ===== 文字 =====
5455
const CLOCK_TITLE_FONT_SIZE = 22;
5556
const CLOCK_TITLE_LINE_HEIGHT = 28;
56-
const CLOCK_CONTENT_FONT_SIZE = 16;
57-
const CLOCK_CONTENT_LINE_HEIGHT = 22;
57+
const CLOCK_CONTENT_FONT_SIZE = 14;
58+
const CLOCK_CONTENT_LINE_HEIGHT = 20;
59+
const CLOCK_CONTENT_LINES = 4;
5860

5961
// ===== 几何 =====
6062

@@ -78,7 +80,7 @@ const getClockGeometry = (spec: IStorylineSpec, ctx: LayoutContext): ClockGeomet
7880
// - content 在 anchor 远离圆心一侧,需要预留 content 的高度
7981
// - 水平方向:text 从 0.92R 向外延伸 CLOCK_TEXT_MAX_WIDTH
8082
const textReserveX = CLOCK_TEXT_MAX_WIDTH;
81-
const textReserveY = 4 + CLOCK_CONTENT_LINE_HEIGHT * 4;
83+
const textReserveY = 4 + CLOCK_CONTENT_LINE_HEIGHT * CLOCK_CONTENT_LINES;
8284
const rMaxX = (innerWidth / 2 - textReserveX) / CLOCK_TEXT_INNER_RATIO;
8385
const rMaxY = (innerHeight / 2 - textReserveY) / CLOCK_TEXT_INNER_RATIO;
8486
const R = Math.max(Math.min(rMaxX, rMaxY), 1);
@@ -383,7 +385,7 @@ export const buildClockBlockMark = (
383385
lineWidth: 1.5
384386
}
385387
} as ICustomMarkSpec<'symbol'>),
386-
// title:文字段的第一行
388+
// title:最多两行
387389
block.title
388390
? ({
389391
type: 'text',
@@ -393,9 +395,12 @@ export const buildClockBlockMark = (
393395
style: {
394396
x: (_d: unknown, ctx: LayoutContext) => getClockTextRect(spec, ctx, index).x,
395397
y: (_d: unknown, ctx: LayoutContext) =>
396-
getClockTextRect(spec, ctx, index).anchorY - getTitleLineHeight(ctx),
398+
getClockTextRect(spec, ctx, index).anchorY - getTitleLineHeight(ctx) * BLOCK_TITLE_MAX_LINES,
397399
text: block.title,
398400
maxLineWidth: (_d: unknown, ctx: LayoutContext) => getClockTextRect(spec, ctx, index).width,
401+
height: (_d: unknown, ctx: LayoutContext) => getTitleLineHeight(ctx) * BLOCK_TITLE_MAX_LINES,
402+
heightLimit: (_d: unknown, ctx: LayoutContext) => getTitleLineHeight(ctx) * BLOCK_TITLE_MAX_LINES,
403+
lineClamp: BLOCK_TITLE_MAX_LINES,
399404
fontSize: (_d: unknown, ctx: LayoutContext) => getTitleFontSize(ctx),
400405
lineHeight: (_d: unknown, ctx: LayoutContext) => getTitleLineHeight(ctx),
401406
fontWeight: 'bold',
@@ -406,30 +411,35 @@ export const buildClockBlockMark = (
406411
textAlign: (_d: unknown, ctx: LayoutContext) =>
407412
getClockTextRect(spec, ctx, index).onLeft ? 'right' : 'left',
408413
textBaseline: 'top',
414+
whiteSpace: 'normal',
415+
wordBreak: 'break-word',
416+
ellipsis: '...',
409417
...spec.title?.style
410418
}
411419
} as ICustomMarkSpec<'text'>)
412420
: null,
413-
// content:富文本,title 下方
421+
// content:普通文本,title 下方
414422
contentText.length
415423
? ({
416424
type: 'text',
417425
name: `storyline-clock-content-${index}`,
418426
interactive: false,
419427
...spec.content,
420-
textType: 'rich',
421428
style: {
422429
x: (_d: unknown, ctx: LayoutContext) => getClockTextRect(spec, ctx, index).x,
423430
y: (_d: unknown, ctx: LayoutContext) => getClockTextRect(spec, ctx, index).anchorY + 4,
424431
width: (_d: unknown, ctx: LayoutContext) => getClockTextRect(spec, ctx, index).width,
432+
height: CLOCK_CONTENT_LINE_HEIGHT * CLOCK_CONTENT_LINES,
425433
maxLineWidth: (_d: unknown, ctx: LayoutContext) => getClockTextRect(spec, ctx, index).width,
426-
text: buildRichContent(contentText, spec),
434+
heightLimit: CLOCK_CONTENT_LINE_HEIGHT * CLOCK_CONTENT_LINES,
435+
text: buildPlainContent(contentText),
427436
fontSize: CLOCK_CONTENT_FONT_SIZE,
428437
lineHeight: CLOCK_CONTENT_LINE_HEIGHT,
429438
fill: '#3a3f4d',
430439
textAlign: (_d: unknown, ctx: LayoutContext) =>
431440
getClockTextRect(spec, ctx, index).onLeft ? 'right' : 'left',
432441
textBaseline: 'top',
442+
whiteSpace: 'normal',
433443
wordBreak: 'break-word',
434444
...spec.content?.style
435445
}

0 commit comments

Comments
 (0)