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;
3032const ARC_TEXT_GAP_FROM_IMAGE = 10 ;
3133const ARC_TITLE_FONT_SIZE = 32 ;
3234const 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 自动截断)
3638const ARC_TEXT_BOX_HEIGHT = 240 ;
3739const ARC_TITLE_TO_CONTENT_GAP = 4 ;
3840// 引导线与 title/content 之间的水平间距
@@ -45,9 +47,21 @@ const ARC_TITLE_IMAGE_MAX_WIDTH = 900;
4547const ARC_TITLE_IMAGE_HEIGHT_RATIO = 0.34 ;
4648// 弧线最高/最低点距离 titleImage 顶部/底部的距离
4749const ARC_GAP_FROM_TITLE_IMAGE = 200 ;
50+ const ARC_FIT_MARGIN = 8 ;
4851
4952const 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' ,
0 commit comments