@@ -9,7 +9,21 @@ import { isStackChart } from '../dataInsight/utils';
9
9
import { Factory } from '../../core/factory' ;
10
10
import type { BaseAtomConstructor } from '../../types' ;
11
11
import type { DataItem } from '@visactor/generate-vchart' ;
12
+ import VChart from '@visactor/vchart' ;
13
+ // 辅助函数:通用值比较
14
+ function compareValues ( a : any , b : any ) : number {
15
+ // 如果是日期字符串,转换为时间戳比较
16
+ if ( isDateString ( a ) && isDateString ( b ) ) {
17
+ return new Date ( a ) . getTime ( ) - new Date ( b ) . getTime ( ) ;
18
+ }
19
+ // 默认按数字或字符串比较
20
+ return a > b ? 1 : a < b ? - 1 : 0 ;
21
+ }
12
22
23
+ // 辅助函数:检测是否是日期字符串
24
+ function isDateString ( value : any ) : boolean {
25
+ return typeof value === 'string' && ( / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / . test ( value ) || / ^ [ A - Z a - z ] { 3 } - \d { 2 } $ / . test ( value ) ) ;
26
+ }
13
27
export class SpecInsightAtom extends BaseAtom < SpecInsightCtx , SpecInsightOptions > {
14
28
name = AtomName . SPEC_INSIGHT ;
15
29
@@ -267,14 +281,189 @@ export class SpecInsightAtom extends BaseAtom<SpecInsightCtx, SpecInsightOptions
267
281
} ) ;
268
282
}
269
283
284
+ /** generate markline of Abnormal trend by coordinates */
285
+ protected getAbnormalTrendMarkLine (
286
+ spec : any ,
287
+ options : {
288
+ series_name : string ;
289
+ text : string ;
290
+ isTransposed : boolean ;
291
+ info : any ;
292
+ }
293
+ ) {
294
+ if ( ! spec . markLine ) {
295
+ spec . markLine = [ ] ;
296
+ }
297
+
298
+ const { series_name, text, isTransposed = false , info } = options ;
299
+
300
+ const datavalues = spec . data [ 0 ] . values ;
301
+ const xField = Array . isArray ( spec . xField ) ? spec . xField [ 0 ] : spec . xField ;
302
+ const yField = Array . isArray ( spec . yField ) ? spec . yField [ 0 ] : spec . yField ;
303
+ const { startDimValue, startValue, endDimValue, endValue } = info ;
304
+
305
+ const coordinates = [
306
+ {
307
+ [ xField ] : startDimValue ,
308
+ [ yField ] : startValue
309
+ } ,
310
+ {
311
+ [ xField ] : endDimValue ,
312
+ [ yField ] : endValue
313
+ }
314
+ ] ;
315
+
316
+ spec . markLine . push ( {
317
+ type : 'type-step' ,
318
+ coordinates,
319
+ connectDirection : 'right' ,
320
+ expandDistance : - 100 ,
321
+ line : {
322
+ multiSegment : true ,
323
+ mainSegmentIndex : 1 ,
324
+ style : [
325
+ {
326
+ lineDash : [ 2 , 2 ] ,
327
+ stroke : '#000' ,
328
+ lineWidth : 2
329
+ } ,
330
+ {
331
+ stroke : '#000' ,
332
+ lineWidth : 2
333
+ } ,
334
+ {
335
+ lineDash : [ 2 , 2 ] ,
336
+ stroke : '#000' ,
337
+ lineWidth : 2
338
+ }
339
+ ]
340
+ } ,
341
+ label : {
342
+ position : 'middle' ,
343
+ text : text ,
344
+ labelBackground : {
345
+ padding : { left : 4 , right : 4 , top : 4 , bottom : 4 } ,
346
+ style : {
347
+ fill : '#fff' ,
348
+ fillOpacity : 1 ,
349
+ stroke : '#000' ,
350
+ lineWidth : 1 ,
351
+ cornerRadius : 4
352
+ }
353
+ } ,
354
+ style : {
355
+ fill : '#000'
356
+ }
357
+ } ,
358
+ endSymbol : {
359
+ size : 12 ,
360
+ refX : - 4
361
+ }
362
+ } ) ;
363
+ }
364
+
365
+ protected getAbnormalBandMarkArea (
366
+ spec : any ,
367
+ options : {
368
+ series_name : string ;
369
+ isTransposed : boolean ;
370
+ info : any ;
371
+ text : string ;
372
+ }
373
+ ) {
374
+ if ( ! spec . markArea ) {
375
+ spec . markArea = [ ] ;
376
+ }
377
+ if ( ! spec . line ) {
378
+ spec . line = { } ;
379
+ }
380
+
381
+ const { series_name, isTransposed = false , info, text } = options ;
382
+
383
+ const timeField = Array . isArray ( spec . xField ) ? spec . xField [ 0 ] : spec . xField ;
384
+
385
+ if ( ! timeField ) {
386
+ console . error ( 'No time field found in spec.xField' ) ;
387
+ return ;
388
+ }
389
+
390
+ const { startValue, endValue } = options . info ;
391
+ const datavalues = spec . data [ 0 ] . values ;
392
+
393
+ // 2. 标记预测数据
394
+ datavalues . forEach ( ( item : any ) => {
395
+ const timeValue = item [ timeField ] ;
396
+ if ( timeValue === undefined ) {
397
+ return ;
398
+ }
399
+
400
+ // 3. 通用比较逻辑(支持字符串、数字或日期)
401
+ if ( compareValues ( timeValue , startValue ) >= 0 && compareValues ( timeValue , endValue ) <= 0 ) {
402
+ item . forecast = true ;
403
+ }
404
+ } ) ;
405
+
406
+ // 添加 lineDash
407
+ if ( ! spec . line . style ) {
408
+ spec . line . style = { } ;
409
+ }
410
+ // spec.line.style.lineDash =
411
+ // `__FUNCTION__: (data => {
412
+ // if (data.forecast) {
413
+ // return [5, 5]; // 预测数据用虚线
414
+ // }
415
+ // return [0]; // 其他数据用实线
416
+ // })`
417
+
418
+ spec . line . style . lineDash = function ( data ) {
419
+ return data . forecast ? [ 5 , 5 ] : [ 0 ] ;
420
+ } ;
421
+
422
+ //console.log("lineDash exists?", typeof spec.line.style.lineDash === 'function'); // 应为 true
423
+
424
+ if ( spec . type === 'waterfall' || spec . type === 'bar' ) {
425
+ spec . markArea . push ( {
426
+ x : startValue ,
427
+ x1 : endValue ,
428
+ label : {
429
+ text : text ,
430
+ position : 'insideTop' ,
431
+ labelBackground : {
432
+ padding : 2 ,
433
+ style : {
434
+ fill : '#E8346D'
435
+ }
436
+ }
437
+ }
438
+ } ) ;
439
+ } else {
440
+ spec . markArea . push ( {
441
+ x : startValue ,
442
+ x1 : endValue ,
443
+ label : {
444
+ text : text ,
445
+ position : 'insideTop' ,
446
+ labelBackground : {
447
+ padding : 2 ,
448
+ style : {
449
+ fill : '#E8346D'
450
+ }
451
+ }
452
+ }
453
+ } ) ;
454
+ }
455
+ //console.log(spec.markArea)
456
+ }
457
+
270
458
protected runBeforeLLM ( ) : SpecInsightCtx {
271
459
const { spec, insights, chartType } = this . context ;
272
460
const newSpec = merge ( { } , spec ) ;
273
461
const { cell, transpose } = getCellFromSpec ( spec , chartType ) ;
274
462
const pointIndexMap : Record < string , boolean > = { } ;
275
463
const isStack = isStackChart ( spec , chartType , cell ) ;
276
464
insights . forEach ( insight => {
277
- const { type, data, value, fieldId, info } = insight ;
465
+ const { type, data, value, fieldId, info, seriesName, textContent } = insight ;
466
+ const series_name = Array . isArray ( seriesName ) ? seriesName [ 0 ] : seriesName ;
278
467
const direction = transpose
279
468
? Number ( value ) >= 0
280
469
? 'right'
@@ -329,6 +518,26 @@ export class SpecInsightAtom extends BaseAtom<SpecInsightCtx, SpecInsightOptions
329
518
? `+${ ( info . change * 100 ) . toFixed ( 1 ) } %`
330
519
: `${ ( info . change * 100 ) . toFixed ( 1 ) } %`
331
520
} ) ;
521
+ break ;
522
+ case InsightType . AbnormalTrend :
523
+ this . getAbnormalTrendMarkLine ( newSpec , {
524
+ series_name : String ( series_name ) ,
525
+ isTransposed : transpose ,
526
+ text :
527
+ value === TrendType . INCREASING
528
+ ? `Abnormal upward trend +${ ( info . change * 100 ) . toFixed ( 1 ) } %`
529
+ : `Abnormal downward trend ${ ( info . change * 100 ) . toFixed ( 1 ) } %` ,
530
+ info : info
531
+ } ) ;
532
+ break ;
533
+ case InsightType . AbnormalBand :
534
+ this . getAbnormalBandMarkArea ( newSpec , {
535
+ series_name : String ( series_name ) ,
536
+ isTransposed : transpose ,
537
+ info : info ,
538
+ text : textContent . plainText
539
+ } ) ;
540
+ break ;
332
541
}
333
542
} ) ;
334
543
this . updateContext ( {
0 commit comments