@@ -9,7 +9,21 @@ import { isStackChart } from '../dataInsight/utils';
99import { Factory } from '../../core/factory' ;
1010import type { BaseAtomConstructor } from '../../types' ;
1111import 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+ }
1222
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+ }
1327export class SpecInsightAtom extends BaseAtom < SpecInsightCtx , SpecInsightOptions > {
1428 name = AtomName . SPEC_INSIGHT ;
1529
@@ -267,14 +281,189 @@ export class SpecInsightAtom extends BaseAtom<SpecInsightCtx, SpecInsightOptions
267281 } ) ;
268282 }
269283
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+
270458 protected runBeforeLLM ( ) : SpecInsightCtx {
271459 const { spec, insights, chartType } = this . context ;
272460 const newSpec = merge ( { } , spec ) ;
273461 const { cell, transpose } = getCellFromSpec ( spec , chartType ) ;
274462 const pointIndexMap : Record < string , boolean > = { } ;
275463 const isStack = isStackChart ( spec , chartType , cell ) ;
276464 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 ;
278467 const direction = transpose
279468 ? Number ( value ) >= 0
280469 ? 'right'
@@ -329,6 +518,26 @@ export class SpecInsightAtom extends BaseAtom<SpecInsightCtx, SpecInsightOptions
329518 ? `+${ ( info . change * 100 ) . toFixed ( 1 ) } %`
330519 : `${ ( info . change * 100 ) . toFixed ( 1 ) } %`
331520 } ) ;
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 ;
332541 }
333542 } ) ;
334543 this . updateContext ( {
0 commit comments