From 91ba300f49042f072c1747e478f175421ac9ede4 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 4 Aug 2025 14:39:54 +0800 Subject: [PATCH 01/16] feat: extension chart for sequence analysis --- .../browser/test-page/sequence-analysis.ts | 2452 +++++++++++++++++ .../src/charts/sequence-analysis/constant.ts | 3 + .../src/charts/sequence-analysis/interface.ts | 106 + .../sequence-analysis-transformer.ts | 395 +++ .../sequence-analysis/sequence-analysis.ts | 41 + packages/vchart-extension/src/index.ts | 1 + .../data-zoom/data-filter-base-component.ts | 59 +- .../src/component/data-zoom/interface.ts | 13 + .../vchart/src/component/data-zoom/util.ts | 11 +- .../vchart/src/data/transforms/obj-flat.ts | 22 +- packages/vchart/src/series/dot/dot.ts | 48 +- packages/vchart/src/series/dot/interface.ts | 7 + .../vchart/src/series/dot/tooltip-helper.ts | 2 +- packages/vchart/src/series/interface/type.ts | 3 + packages/vchart/src/series/link/constant.ts | 4 +- packages/vchart/src/series/link/interface.ts | 21 + packages/vchart/src/series/link/link.ts | 146 +- 17 files changed, 3264 insertions(+), 70 deletions(-) create mode 100644 packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts create mode 100644 packages/vchart-extension/src/charts/sequence-analysis/constant.ts create mode 100644 packages/vchart-extension/src/charts/sequence-analysis/interface.ts create mode 100644 packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts create mode 100644 packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis.ts diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts new file mode 100644 index 0000000000..08101f121e --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts @@ -0,0 +1,2452 @@ +import { registerSequenceAnalysis } from '../../../../src'; +import { VChart } from '@visactor/vchart'; +import type { + ActionData, + EventData, + ISequenceAnalysisSpec, + PatternData, + UserImageMap +} from '../../../../src/charts/sequence-analysis/interface'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +// import trainingData2 from '../data/sequence-scatter/Training_process2/data.json'; +// import bgimgData from '../data/sequence-scatter/Training_process1/bgimg_data.json'; + +// 模拟绘制点的 EventData 数据 +const eventData: EventData[] = [ + { + user_id: 'user_001', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_001_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_001_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 10 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_001_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_001_4', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_001_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'login', + node_name: 'event_001_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_001_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 11 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_001_8', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 22 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_001_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 3 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_001_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + } + ] + }, + { + user_id: 'user_002', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_002_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_002_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_002_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_002_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 11 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_002_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 22 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_002_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 34 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_002_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_002_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_002_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 12 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_002_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_002_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_002_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 6 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_002_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_002_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + } + ] + }, + { + user_id: 'user_003', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_003_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 15 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_003_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 2 + }, + { + step: 'step_3', + action_type: 'SendGift', + node_name: 'event_003_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_003_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_003_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_6', + action_type: 'follow', + node_name: 'event_003_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_003_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_003_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_003_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 5 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_003_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_003_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_003_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_003_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 11 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_003_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 12 + }, + { + step: 'step_15', + action_type: 'focus', + node_name: 'event_003_15', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 3 + } + ] + }, + { + user_id: 'user_004', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_004_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_004_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 9 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_004_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 7 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_004_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_004_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_004_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_004_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 4 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_004_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'follow', + node_name: 'event_004_9', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_005', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_005_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'follow', + node_name: 'event_005_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 11 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_005_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_005_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_5', + action_type: 'im', + node_name: 'event_005_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_005_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + } + ] + }, + { + user_id: 'user_006', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_006_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 3 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_006_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_006_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_006_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_006_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_006_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_006_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_006_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_006_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 7 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_006_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_006_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_006_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_006_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 3 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_006_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_006_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_007', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_007_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_007_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 6 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_007_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_007_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_007_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + } + ] + }, + { + user_id: 'user_008', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_008_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 23 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_008_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 5 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_008_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_008_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_008_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_008_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 2 + }, + { + step: 'step_7', + action_type: 'follow', + node_name: 'event_008_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'im', + node_name: 'event_008_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 33 + }, + { + step: 'step_9', + action_type: 'comment', + node_name: 'event_008_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 2 + }, + { + step: 'step_10', + action_type: 'focus', + node_name: 'event_008_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + } + ] + }, + { + user_id: 'user_009', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_009_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_009_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 8 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_009_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_009_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_009_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_009_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_009_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_009_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_009_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_009_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_009_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_009_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_009_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_009_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_009_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_010', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_010.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_010_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_010_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 20 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_010_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'comment', + node_name: 'event_010_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_010_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_010_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_010_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_010_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_010_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_010_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_010_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_010_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + } +].reverse(); +const tickData = []; +const timeCountMap = {}; +eventData.forEach(user => { + user.dots.forEach(e => { + timeCountMap[e.time_stamp] = e.action_count; + tickData.push({ + time_stamp: e.time_stamp, + action_count: e.action_count + }); + }); +}); + +// 模拟绘制用户头像的 UserImageMap 数据 +const userImageMap: UserImageMap = { + user_001: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + user_002: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + user_003: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_004: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_005: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_006: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_007: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + user_008: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_009: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' +}; + +const stepMediumMap: Record = { + step_1: { + medium_type: 'IP', + count: 0 + }, + step_2: { + medium_type: 'Device', + count: 1 + }, + step_3: { + medium_type: 'User', + count: 3 + }, + step_4: { + medium_type: 'Liveroom', + count: 3 + }, + step_5: { + medium_type: 'User', + count: 3 + }, + step_6: { + medium_type: 'User', + count: 3 + }, + step_7: { + medium_type: 'User', + count: 3 + }, + step_8: { + medium_type: 'Liveroom', + count: 3 + }, + step_9: { + medium_type: 'Email', + count: 3 + }, + step_10: { + medium_type: 'Tel', + count: 3 + }, + step_11: { + medium_type: 'Medium', + count: 3 + }, + step_12: { + medium_type: 'Medium', + count: 3 + }, + step_13: { + medium_type: 'Medium', + count: 3 + }, + step_14: { + medium_type: 'Medium', + count: 3 + }, + step_15: { + medium_type: 'Medium', + count: 3 + } +}; + +// 模拟绘制 pattern 的 PatternData 数据 +const patternData: PatternData[] = [ + { + from: 'event_002_1', + to: 'event_002_1', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_002_4', + to: 'event_006', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_7', + to: 'event_002_7', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_004_2', + to: 'event_004_2', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_004_4', + to: 'event_004_6', + pattern_type: 'pattern_001' + }, + { + from: 'event_004_7', + to: 'event_004_7', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_004_8', + to: 'event_004_8', + pattern_type: 'pattern_002', + isExtend: 'right' + }, + { + from: 'event_004_10', + to: 'event_004_10', + pattern_type: 'pattern_002' + }, + { + from: 'event_005_3', + to: 'event_005_3', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_005_4', + to: 'event_005_5', + pattern_type: 'pattern_001' + }, + { + from: 'event_005_6', + to: 'event_005_6', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_009_3', + to: 'event_009_3', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_009_4', + to: 'event_009_5', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_6', + to: 'event_009_6', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_010_5', + to: 'event_010_6', + pattern_type: 'pattern_001' + }, + { + from: 'event_010_6', + to: 'event_010_6', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_010_8', + to: 'event_010_8', + pattern_type: 'pattern_002' + }, + { + from: 'event_010_10', + to: 'event_010_10', + pattern_type: 'pattern_002', + isExtend: 'left' + } +]; + +const patternDataCurve: PatternData[] = [ + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_009_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_002' + }, + { + from: 'event_003_10', + to: 'event_002_11', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_005_2', + pattern_type: 'pattern_002' + }, + { + from: 'event_005_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_007_4', + pattern_type: 'pattern_001' + } +]; + +// 模拟绘制箭头的 ActionData 数据 +const actionData: ActionData[] = [ + { + from: 'event_002_2', + to: 'event_003_2', + user_image: userImageMap['user_003'], + user_id: 'user_003' + }, + { + from: 'event_003_4', + to: 'event_004_4', + user_image: userImageMap['user_003'], + user_id: 'user_003' + }, + { + from: 'event_003_5', + to: 'event_004_5', + user_image: userImageMap['user_003'], + user_id: 'user_003' + }, + { + from: 'event_004_5', + to: 'event_005_5', + user_image: userImageMap['user_004'], + user_id: 'user_004' + }, + { + from: 'event_005_5', + to: 'event_006_5', + user_image: userImageMap['user_005'], + user_id: 'user_005' + }, + { + from: 'event_006_5', + to: 'event_007_5', + user_image: userImageMap['user_006'], + user_id: 'user_006' + }, + { + from: 'event_006_7', + to: 'event_007_7', + user_image: userImageMap['user_006'], + user_id: 'user_006' + }, + { + from: 'event_006_8', + to: 'event_007_8', + user_image: userImageMap['user_007'], + user_id: 'user_007' + }, + { + from: 'event_006_9', + to: 'event_007_9', + user_image: userImageMap['user_006'], + user_id: 'user_006' + }, + { + from: 'event_006_10', + to: 'event_007_10', + user_image: userImageMap['user_006'], + user_id: 'user_006' + }, + { + from: 'event_008_2', + to: 'event_009_2', + user_image: userImageMap['user_008'], + user_id: 'user_008' + }, + { + from: 'event_008_3', + to: 'event_009_3', + user_image: userImageMap['user_008'], + user_id: 'user_008' + }, + { + from: 'event_008_10', + to: 'event_009_10', + user_image: userImageMap['user_008'], + user_id: 'user_008' + } +]; + +const spec_1: ISequenceAnalysisSpec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + actionTarget: false, + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; + +const spec_2: ISequenceAnalysisSpec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: true, + actionTarget: true, + eventData, + actionData, + userImageMap, + patternData: patternDataCurve, + stepMediumMap +}; + +const spec_3: ISequenceAnalysisSpec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: true, + actionTarget: true, + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; + +const spec_4: ISequenceAnalysisSpec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: true, + actionTarget: false, + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; + +const spec1 = { + type: 'sequenceAnalysis', + series: [ + { + type: 'link', + dataId: 'patternData', + dotSeriesIndex: 1, + fromField: 'from', + toField: 'to', + seriesField: 'pattern_type', + allowExtend: true, + arrow: { + visible: false + }, + + link: { + state: { + custom_unSelected: { + strokeOpacity: 0.5, + stroke: 'grey' + } + } + } + }, + { + type: 'dot', + xField: 'step', + yField: 'user_id', + titleField: 'user_id', + seriesField: 'action_type', + dataIndex: 0, + highLightSeriesGroup: '', + title: { + visible: false, + style: { + fill: 'rgba(46, 47, 50)' + } + }, + grid: { + style: { + visible: false + } + }, + dot: { + state: { + custom_unSelected: { + fillOpacity: 0.5, + fill: 'grey' + } + } + }, + symbol: { + visible: false + } + } + ], + data: [ + { + id: 'eventData', + values: eventData + }, + { + id: 'actionData', + values: actionData + }, + { + id: 'patternData', + values: patternData + } + ], + axes: [ + { + orient: 'top', + type: 'band', + // bandSize: 800, + label: { + formatMethod: label => { + return { + type: 'rich', + text: [ + { + text: `${(stepMediumMap[label] as any).medium_type}... `, + fontSize: 12 + }, + { + text: `+${(stepMediumMap[label] as any).count}`, + fontSize: 12, + background: 'grey', + backgroundOpacity: 0.4, + backgroundCornerRadius: 5 + } + ] + }; + } + } + }, + { + orient: 'bottom', + // bandSize: 800, + type: 'band', + bandPadding: 0 + }, + { + orient: 'left', + type: 'band', + domainLine: { + visible: false + }, + tick: { + visible: false + }, + label: { + formatMethod: label => { + return { + type: 'rich', + text: [{ image: userImageMap[label], width: 40, height: 40 }] + }; + } + }, + grid: { + visible: true, + style: { + lineDash: [3, 3] + } + } + } + ], + dataZoom: [ + { + orient: 'bottom', + start: 0, + end: 1, + showDetail: true, + axisId: 'step_bottom' + // filterMode: 'axis' + } + ], + scrollBar: [ + { + orient: 'bottom', + auto: true, + axisIndex: 0, + filterMode: 'axis' + } + ] +}; + +const spec2 = { + type: 'sequenceAnalysis', + series: [ + { + type: 'link', + dataId: 'patternData', + dotSeriesIndex: 1, + fromField: 'from', + toField: 'to', + seriesField: 'user_id', + linkType: 'curve', + arrow: { + visible: false + }, + link: { + style: { + lineWidth: 1, + stroke: 'red' + } + } + }, + { + type: 'dot', + // xField: 'step', + xField: 'medium_type', + yField: 'user_id', + seriesField: 'action_type', + titleField: 'user_id', + dataIndex: 0, + highLightSeriesGroup: '', + title: { + visible: false, + style: { + fill: 'rgba(46, 47, 50)' + } + }, + grid: { + style: { + visible: false + } + }, + symbol: { + visible: false + } + } + ], + data: [ + { + id: 'eventData', + values: eventData + }, + { + id: 'actionData', + values: actionData + }, + { + id: 'patternData', + values: patternDataCurve + } + ], + axes: [ + { + orient: 'top', + type: 'band' + // bandSize: 600 + // label: { + // formatMethod: label => { + // return { + // type: 'rich', + // text: [ + // { + // text: `${(stepMediumMap[label] as any).medium}`, + // fontSize: 12 + // }, + // { + // text: ` +${(stepMediumMap[label] as any).count}`, + // fontSize: 12, + // background: 'grey', + // backgroundOpacity: 0.4, + // backgroundCornerRadius: 5 + // } + // ] + // }; + // } + // } + }, + // { + // orient: 'bottom', + // // bandSize: 800, + // type: 'band' + // }, + { + orient: 'left', + type: 'band', + label: { + formatMethod: label => { + return { + type: 'rich', + text: [{ image: userImageMap[label], width: 40, height: 40 }] + }; + } + }, + grid: { + visible: true, + style: { + lineDash: [3, 3] + } + }, + domainLine: { + visible: false + }, + tick: { + visible: false + } + } + ], + tooltip: { + visible: false + }, + animation: false, + dataZoom: [ + { + orient: 'bottom', + filterMode: 'axis' + } + ], + scrollBar: [ + { + orient: 'right', + axisIndex: 1, + filterMode: 'axis', + start: 0, + end: 0.5 + } + ] +}; + +const spec3 = { + type: 'sequenceAnalysis', + series: [ + { + type: 'link', + dataId: 'actionData', + dotSeriesIndex: 1, + fromField: 'from', + toField: 'to', + arrow: { + visible: false + } + }, + { + type: 'dot', + xField: 'time_stamp', + yField: 'user_id', + titleField: 'user_id', + seriesField: 'action_type', + dataIndex: 0, + highLightSeriesGroup: '', + title: { + visible: false, + style: { + fill: 'rgba(46, 47, 50)' + } + }, + grid: { + style: { + visible: false + } + }, + symbol: { + visible: false + } + } + ], + data: [ + { + id: 'eventData', + values: eventData + }, + { + id: 'actionData', + values: actionData + }, + { + id: 'patternData', + values: patternData + } + ], + axes: [ + { + orient: 'top', + // bandSize: 800, + type: 'time', + label: { + dataFilter: (datum, value, context) => { + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.action_count, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp + })); + }, + formatMethod: (label, datum) => { + // console.log('label', datum, label, timeCountMap[label]); + if (datum?.rawValue) { + return timeCountMap[datum.rawValue]; + } + return label; + } + }, + tick: { + dataFilter: (datum, context) => { + // console.log('context', context, datum); + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + const [minRange, maxRange] = context.vchart.getChart().getAllComponents()[0].getScale(0).range(); + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.time_stamp, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp, + point: { x: ((t.time_stamp - min) / (max - min)) * (maxRange - minRange) + minRange, y: 0 } + })); + } + }, + grid: { + visible: false + } + }, + { + orient: 'bottom', + id: 'bottom_time', + // bandSize: 800, + type: 'time', + layers: [ + { + tickStep: 28800, + timeFormat: '%Y%m%d' + }, + { + tickStep: 28800, + timeFormat: '%H:%M' + } + ], + label: { + dataFilter: (datum, value, context) => { + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.time_stamp, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp + })); + } + }, + tick: { + dataFilter: (datum, context) => { + console.log('context', context, datum); + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + const [minRange, maxRange] = context.vchart.getChart().getAllComponents()[0].getScale(0).range(); + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.time_stamp, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp, + point: { x: ((t.time_stamp - min) / (max - min)) * (maxRange - minRange) + minRange, y: 0 } + })); + } + }, + grid: { + visible: false + } + }, + + { + orient: 'left', + type: 'band', + label: { + formatMethod: label => { + return { + type: 'rich', + text: [{ image: userImageMap[label], width: 40, height: 40 }] + }; + } + }, + grid: { + visible: true, + style: { + lineDash: [3, 3] + } + } + } + ], + markLine: tickData.map(t => ({ + zIndex: -1, + x: t.time_stamp, + endSymbol: { + visible: false + }, + label: { + visible: false + } + })), + dataZoom: [ + { + orient: 'bottom', + filterMode: 'axis' + } + ], + scrollBar: [ + { + orient: 'bottom', + auto: true, + axisIndex: 0, + filterMode: 'axis' + } + ] +}; + +const spec4 = { + type: 'sequenceAnalysis', + padding: 60, + series: [ + { + type: 'link', + dataId: 'actionData', + dotSeriesIndex: 1, + fromField: 'from', + toField: 'to', + imageLabelField: 'user_image', + arrow: { + style: { + fill: 'grey' + } + }, + imageLabel: { + style: { + width: 40, + height: 40 + } + }, + link: { + style: { + stroke: 'grey' + } + } + }, + { + type: 'dot', + xField: 'time_stamp', + yField: 'user_id', + titleField: 'user_id', + seriesField: 'action_type', + dataIndex: 0, + highLightSeriesGroup: '', + title: { + visible: false, + style: { + fill: 'rgba(46, 47, 50)' + } + }, + grid: { + style: { + visible: false + } + }, + dot: { + zIndex: 999, + state: { + custom_unSelected: { + fillOpacity: 0.5, + fill: 'grey' + } + } + } + } + ], + data: [ + { + id: 'eventData', + values: eventData + }, + { + id: 'actionData', + values: actionData + }, + { + id: 'patternData', + values: patternData + } + ], + region: [ + { + clip: false + } + ], + axes: [ + { + orient: 'top', + // bandSize: 800, + type: 'time', + id: 'top_time', + label: { + dataFilter: (datum, value, context) => { + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.action_count, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp + })); + }, + formatMethod: (label, datum) => { + // console.log('label', datum, label, timeCountMap[label]); + if (datum?.rawValue) { + return timeCountMap[datum.rawValue]; + } + return label; + } + }, + tick: { + dataFilter: (datum, context) => { + // console.log('context', context, datum); + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + const [minRange, maxRange] = context.vchart.getChart().getAllComponents()[0].getScale(0).range(); + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.time_stamp, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp, + point: { x: ((t.time_stamp - min) / (max - min)) * (maxRange - minRange) + minRange, y: 0 } + })); + } + }, + grid: { + visible: false + }, + hasDimensionTooltip: true + }, + { + orient: 'bottom', + id: 'bottom_time', + // bandSize: 800, + type: 'time', + layers: [ + { + tickStep: 28800, + timeFormat: '%Y%m%d' + }, + { + tickStep: 28800, + timeFormat: '%H:%M' + } + ], + label: { + dataFilter: (datum, value, context) => { + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.time_stamp, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp + })); + } + }, + tick: { + dataFilter: (datum, context) => { + console.log('context', context, datum); + const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + const [minRange, maxRange] = context.vchart.getChart().getAllComponents()[0].getScale(0).range(); + // console.log('datum', datum, min, max); + // return datum; + return tickData.map(t => ({ + id: t.time_stamp, + label: t.time_stamp, + value: (t.time_stamp - min) / (max - min), + rawValue: t.time_stamp, + point: { x: ((t.time_stamp - min) / (max - min)) * (maxRange - minRange) + minRange, y: 0 } + })); + } + }, + grid: { + visible: false + }, + hasDimensionTooltip: true + }, + + { + orient: 'left', + type: 'band', + id: 'left_user', + label: { + formatMethod: label => { + return { + type: 'rich', + text: [{ image: userImageMap[label], width: 40, height: 40 }] + }; + } + }, + grid: { + visible: true, + style: { + lineDash: [3, 3] + } + }, + tick: { + visible: false + }, + domainLine: { + visible: false + } + } + ], + markLine: tickData.map(t => ({ + zIndex: -1, + x: t.time_stamp, + endSymbol: { + visible: false + }, + label: { + visible: false + } + })), + tooltip: { + visible: false + }, + crosshair: { + xField: { + visible: true, + line: { + type: 'rect', + width: 20, + style: { + fill: 'rgb(240,242,245)', + lineWidth: 0 + } + }, + bindingAxesIndex: [0, 1] + }, + yField: { + visible: true, + bindingAxesIndex: [2], + defaultSelect: { + axisIndex: 2, + datum: 40 + }, + line: { + type: 'rect' + }, + label: { + visible: true // Default is false + } + } + }, + dataZoom: [ + { + orient: 'bottom', + filterMode: 'axis' + } + ], + scrollBar: [ + { + orient: 'bottom', + auto: true, + axisIndex: 0, + filterMode: 'axis' + } + ] +}; +const run = () => { + registerSequenceAnalysis(); + const cs = new VChart(spec_1, { + dom: document.getElementById('chart') as HTMLElement, + //theme: 'dark', + onError: err => { + console.error(err); + } + }); + console.time('renderTime'); + + cs.renderSync(); + + // cs.on('pointerenter', e => { + // console.log('pointerenter', e); + // }); + + // 创建一个div,位置可以自定义 + const width = 80; + const height = 60; + const createPoptip = (id: string) => { + const poptip = document.createElement('div'); + poptip.style.position = 'absolute'; + poptip.style.width = `${width}px`; + poptip.style.height = `${height}px`; + poptip.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + poptip.style.borderRadius = '5px'; + poptip.style.color = '#fff'; + poptip.id = id; + poptip.innerHTML = id; + poptip.style.visibility = 'hidden'; + document.getElementById('chart')?.appendChild(poptip); + }; + createPoptip('userPoptip'); + createPoptip('axisPoptip'); + createPoptip('markPoptip'); + + cs.on('pointerover', { nodeName: 'axis-label' }, event => { + // console.log('pointerover', event); + + const { x, y } = event.event.global; + const { _compiledProductId } = event.mark; + let poptipId = ''; + console.log('_compiledProductId', _compiledProductId); + if (_compiledProductId === 'left_user') { + // left_user对应的是spec中配置的轴id, 可以写死 + poptipId = 'userPoptip'; + } else if (_compiledProductId === 'top_time') { + poptipId = 'axisPoptip'; + } + const poptip = document.getElementById(poptipId); + if (poptip) { + poptip.style.visibility = 'visible'; + poptip.style.left = `${x - width / 2}px`; + poptip.style.top = `${y - height}px`; + } + }); + + cs.on('pointerout', { nodeName: 'axis-label' }, event => { + const { _compiledProductId } = event.mark; + let poptipId = ''; + console.log('_compiledProductId', _compiledProductId); + if (_compiledProductId === 'left_user') { + // left_user对应的是spec中配置的轴id, 可以写死 + poptipId = 'userPoptip'; + } else if (_compiledProductId === 'top_time') { + poptipId = 'axisPoptip'; + } + // 获取 poptip 元素 + const poptip = document.getElementById(poptipId); + if (poptip) { + poptip.style.visibility = 'hidden'; + } + }); + + cs.on('pointerover', { markName: 'dot' }, event => { + // console.log('pointerover', event); + + const { x, y } = event.event.global; + const poptipId = 'markPoptip'; + const poptip = document.getElementById(poptipId); + if (poptip) { + poptip.style.visibility = 'visible'; + poptip.style.left = `${x - width / 2}px`; + poptip.style.top = `${y - height}px`; + } + }); + + cs.on('pointerout', { level: 'mark' }, event => { + // console.log('pointerover', event); + + const poptipId = 'markPoptip'; + const poptip = document.getElementById(poptipId); + if (poptip) { + poptip.style.visibility = 'hidden'; + } + }); + + if (document.getElementById('user_001')) { + document.getElementById('user_001').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // 数据 type 不相等的进入这个状态 + return datum.user_id === 'user_001'; + } + } + }); + }; + } + + if (document.getElementById('user_002')) { + document.getElementById('user_002').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // 数据 type 不相等的进入这个状态 + return datum.user_id === 'user_002'; + } + } + }); + }; + } + + if (document.getElementById('action_register')) { + document.getElementById('action_register').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.action_type === 'register'; + } + } + }); + }; + } + + if (document.getElementById('action_login')) { + document.getElementById('action_login').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.action_type === 'login'; + } + } + }); + }; + } + + if (document.getElementById('action_commit')) { + document.getElementById('action_commit').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.action_type === 'commit'; + } + } + }); + }; + } + + if (document.getElementById('pattern_001')) { + document.getElementById('pattern_001').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.pattern_type === 'pattern_001'; + } + } + }); + }; + } + + if (document.getElementById('pattern_002')) { + document.getElementById('pattern_002').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.pattern_type === 'pattern_002'; + } + } + }); + }; + } + + if (document.getElementById('medium_web')) { + document.getElementById('medium_web').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.medium_type === 'web'; + } + } + }); + }; + } + + if (document.getElementById('medium_mobile')) { + document.getElementById('medium_mobile').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.medium_type === 'mobile'; + } + } + }); + }; + } + + if (document.getElementById('medium_IP')) { + document.getElementById('medium_IP').onclick = () => { + cs.clearAllStates(); + cs.updateState({ + // 名称与上方配置要对应 + custom_unSelected: { + filter: datum => { + // console.log('datum', datum); + // 数据 type 不相等的进入这个状态 + return datum.medium_type === 'IP'; + } + } + }); + }; + } + + if ( + document.getElementById('mode') && + document.getElementById('mediumConnection') && + document.getElementById('actionTarget') + ) { + let mode = 'step'; + let mediumConnection = false; + let actionTarget = false; + document.getElementById('mode').onchange = event => { + mode = event.target.value; + console.log('mode', mode, mediumConnection, actionTarget); + updateSequenceSpec(mode, mediumConnection, actionTarget); + }; + document.getElementById('mediumConnection').onchange = event => { + mediumConnection = event.target.checked; + console.log('mediumConnection', mode, mediumConnection, actionTarget); + updateSequenceSpec(mode, mediumConnection, actionTarget); + }; + document.getElementById('actionTarget').onchange = event => { + actionTarget = event.target.checked; + console.log('acitionTarget', mode, mediumConnection, actionTarget); + updateSequenceSpec(mode, mediumConnection, actionTarget); + }; + const updateSequenceSpec = (mode, mediumConnection, actionTarget) => { + if (mode === 'step') { + if (mediumConnection) { + cs.updateSpec(spec_2); + } else { + cs.updateSpec(spec_1); + } + } else { + if (actionTarget) { + cs.updateSpec(spec_3); + } else { + cs.updateSpec(spec_4); + } + } + }; + } + + console.timeEnd('renderTime'); + window['vchart'] = cs; + console.log(cs); +}; +run(); diff --git a/packages/vchart-extension/src/charts/sequence-analysis/constant.ts b/packages/vchart-extension/src/charts/sequence-analysis/constant.ts new file mode 100644 index 0000000000..8dbfdf626b --- /dev/null +++ b/packages/vchart-extension/src/charts/sequence-analysis/constant.ts @@ -0,0 +1,3 @@ +export const DATA_KEY = 'dataKey'; +export const BACKGROUND_KEY = 'background'; +export const EXPAND_RATIO = 0.2; // 拓展比例 diff --git a/packages/vchart-extension/src/charts/sequence-analysis/interface.ts b/packages/vchart-extension/src/charts/sequence-analysis/interface.ts new file mode 100644 index 0000000000..e3dbbc0158 --- /dev/null +++ b/packages/vchart-extension/src/charts/sequence-analysis/interface.ts @@ -0,0 +1,106 @@ +// 绘制点 +export interface EventData { + user_id: string; // 用户id + user_image: string; // 用户头像url + dots: { + // step步骤 + // step模式下以此为维度轴 + step: string; + // 事件类型 + // 颜色维度 + action_type: string; + // 事件唯一标识 + node_name: string; + // 介质类型 + // step + medium connection, 以此为维度轴 + medium_type: string; + // 事件发生时间 + // time模式, 以此为维度轴 + time_stamp: number; + // 事件发生次数 + // time模式, 以此为top轴标识 + action_count: number; + }[]; +} + +// 绘制user头像 +export interface UserImageMap { + [user_id: string]: string; // 用户id -> 用户头像url +} + +// 绘制pattern +export interface PatternData { + // 事件id + from: string; + // 事件id + to: string; + // pattern类型, 颜色维度 + pattern_type: string; + /** + * 是否延伸 2/x + * 默认两边延伸 + */ + isExtend?: 'left' | 'right' | 'all'; +} + +// 绘制连接 +export interface ActionData { + // 事件id + from: string; + // 事件id + to: string; + // 用户id + // time模式开启Action Target是需要使用 + user_id?: string; + // 用户头像url + user_image?: string; +} + +export interface ISequenceAnalysisSpec { + /** + * 图表类型 + */ + type: 'sequenceAnalysis'; + /** + * 色板(看是否需要根据key指定) + */ + colors?: string[]; + /** + * 展示模式 + */ + mode: 'time' | 'step'; + /** + * step模式下, 是否开启介质连接 + * @default false + * @description 关闭时, 使用直接绘制; 开启时, 使用三次贝塞尔曲线绘制 + */ + mediumConnection?: boolean; + /** + * time模式下, 是否开启actionTarget + * @default false + * @description 关闭时, 使用直线连接; 开启时, 连接线中间有用户头像 + */ + actionTarget?: boolean; + /** + * 事件数据 + */ + eventData: EventData[]; + /** + * 模式下的pattern数据 + */ + patternData?: PatternData[]; + /** + * 模式下的action数据 + */ + actionData?: ActionData[]; + /** + * 用户头像数据(从eventData中抽取也可以, 但不建议在前端遍历数据) + */ + userImageMap: UserImageMap; + stepMediumMap: Record; + /** + * @param spec 经过rankingList转化后的vchart原始spec + * @description 获取vchart原始spec, 并根据业务自行添加转换逻辑 + */ + customTransformSpec?: (spec: any) => void; +} diff --git a/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts new file mode 100644 index 0000000000..b2451033aa --- /dev/null +++ b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts @@ -0,0 +1,395 @@ +import { CommonChartSpecTransformer } from '@visactor/vchart'; +import type { EventData } from './interface'; + +export class SequenceAnalysisChartSpecTransformer extends CommonChartSpecTransformer { + transformSpec(spec: any): void { + // 类型配置 + delete spec.type; + spec.type = 'common'; + + // 数据配置 + spec.data = [ + { + id: 'eventData', + values: spec.eventData + }, + { + id: 'actionData', + values: spec.actionData + }, + { + id: 'patternData', + values: spec.patternData + } + ]; + const { eventData } = spec; + // delete spec.eventData; + // delete spec.actionData; + // delete spec.patternData; + + // 系列配置 + const { mode, mediumConnection = false, actionTarget = false, stepMediumMap } = spec; + spec.series = [ + { + type: 'link', + dataId: mode === 'time' ? 'actionData' : 'patternData', + dotSeriesIndex: 1, + fromField: 'from', + toField: 'to', + seriesField: 'pattern_type', + imageLabelField: mode === 'time' && actionTarget ? 'user_image' : '', + allowExtend: mode === 'step' && !mediumConnection, + linkType: mode === 'step' && mediumConnection ? 'curve' : 'line', + arrow: { + style: { + visible: mode === 'time' && actionTarget + // fill: 'grey' + } + }, + link: { + state: { + custom_unSelected: { + strokeOpacity: 0.5, + stroke: 'grey' + } + }, + style: { + // stroke: 'grey', + visible: mode === 'step' || actionTarget + } + }, + imageLabel: { + visible: mode === 'time' && actionTarget, + style: { + width: 40, + height: 40 + } + } + }, + { + type: 'dot', + xField: mode === 'time' ? 'time_stamp' : mediumConnection ? 'medium_type' : 'step', + yField: 'user_id', + titleField: 'user_id', + seriesField: 'action_type', + linearMode: mode === 'time', + dataIndex: 0, + highLightSeriesGroup: '', + title: { + visible: false, + style: { + fill: 'rgba(46, 47, 50)' + } + }, + grid: { + style: { + visible: false + } + }, + dot: { + state: { + custom_unSelected: { + fillOpacity: 0.5, + fill: 'grey' + } + } + }, + symbol: { + visible: false + } + } + ]; + spec.series.forEach((s: any) => { + if (s.type === 'link') { + s.dotSeriesSpec = spec.series[s.dotSeriesIndex]; + } + }); + + // 构建轴标签 + // 时间 => 事件数量 + const tickData: { time_stamp: number; action_count: number }[] = []; + const timeCountMap: Record = {}; + (eventData as EventData[]).forEach(user => { + user.dots.forEach(e => { + if (timeCountMap[e.time_stamp]) { + timeCountMap[e.time_stamp] += e.action_count; + } else { + timeCountMap[e.time_stamp] = e.action_count; + } + }); + }); + Object.keys(timeCountMap) + .sort((a, b) => Number(a) - Number(b)) // 按时间戳排序 + .forEach(key => { + tickData.push({ + time_stamp: Number(key), + action_count: timeCountMap[key] + }); + }); + + // 轴配置 + const axes = []; + // step 模式 + if (mode === 'step') { + if (mediumConnection) { + axes.push({ + orient: 'top', + type: 'band', + id: 'top_medium' + }); + } else { + axes.push({ + orient: 'top', + type: 'band', + id: 'top_medium', + label: { + formatMethod: (label: string) => { + return { + type: 'rich', + text: [ + { + text: `${(stepMediumMap[label] as any).medium_type}... `, + fontSize: 12 + }, + { + text: `+${(stepMediumMap[label] as any).count}`, + fontSize: 12, + background: 'grey', + backgroundOpacity: 0.4, + backgroundCornerRadius: 5 + } + ] + }; + } + } + }); + axes.push({ + orient: 'bottom', + id: 'bottom_step', + type: 'band', + bandPadding: 0 + }); + } + } + // time 模式 + else { + axes.push({ + orient: 'top', + // bandSize: 800, + type: 'time', + id: 'top_count', + label: { + dataFilter: (datum: any) => { + const minData = datum[0].rawValue; + const minValue = datum[0].value; + const maxData = datum[datum.length - 1].rawValue; + const maxValue = datum[datum.length - 1].value; + return tickData + .filter(t => t.time_stamp >= minData && t.time_stamp <= maxData) + .map(t => ({ + id: t.time_stamp, + label: t.action_count, + value: ((t.time_stamp - minData) / (maxData - minData)) * (maxValue - minValue) + minValue, + rawValue: t.time_stamp + })); + }, + formatMethod: (label: string, datum: any) => { + if (datum?.rawValue) { + return timeCountMap[datum.rawValue]; + } + return label; + } + }, + tick: { + visible: false + // dataFilter: (datum, context) => { + // const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + // const [minRange, maxRange] = context.vchart.getChart().getAllComponents()[0].getScale(0)._wholeRange; + // console.log( + // 'datum-----------', + // datum, + // maxRange, + // max, + // context.vchart.getChart().getAllComponents()[0].getScale(0), + // tickData.map(t => ({ + // id: t.time_stamp, + // label: t.time_stamp, + // value: (t.time_stamp - min) / (max - min), + // rawValue: t.time_stamp, + // point: { x: ((t.time_stamp - min) / (max - min)) * (maxRange - minRange) + minRange, y: 0 } + // })) + // ); + // return datum; + // return tickData.map(t => ({ + // id: t.time_stamp, + // label: t.time_stamp, + // value: (t.time_stamp - min) / (max - min), + // rawValue: t.time_stamp, + // point: { x: ((t.time_stamp - min) / (max - min)) * (maxRange - minRange) + minRange, y: 0 } + // })); + // } + }, + grid: { + visible: false + }, + innerOffset: { + left: 50, + right: 50 + } + }); + axes.push({ + orient: 'bottom', + id: 'bottom_time', + type: 'time', + layers: [ + { + tickStep: 28800, + timeFormat: '%Y%m%d' + }, + { + tickStep: 28800, + timeFormat: '%H:%M' + } + ], + label: { + dataFilter: (datum: any) => { + const minData = datum[0].rawValue; + const minValue = datum[0].value; + const maxData = datum[datum.length - 1].rawValue; + const maxValue = datum[datum.length - 1].value; + return tickData + .filter(t => t.time_stamp >= minData && t.time_stamp <= maxData) + .map(t => ({ + id: t.time_stamp, + label: t.time_stamp, + value: ((t.time_stamp - minData) / (maxData - minData)) * (maxValue - minValue) + minValue, + rawValue: t.time_stamp + })); + } + }, + tick: { + visible: false + // dataFilter: (datum, context) => { + // // console.log('context', context, datum); + // const [min, max] = context.vchart.getChart().getAllComponents()[0].getScale(0)._niceDomain; + // const [minRange, maxRange] = context.vchart.getChart().getAllComponents()[0].getScale(0).range(); + // // console.log('datum', datum, min, max); + // // return datum; + // return tickData.map(t => ({ + // id: t.time_stamp, + // label: t.time_stamp, + // value: (t.time_stamp - min) / (max - min), + // rawValue: t.time_stamp, + // point: { x: ((t.time_stamp - min) / (max - min)) * (maxRange - minRange) + minRange, y: 0 } + // })); + // } + }, + grid: { + visible: false + }, + innerOffset: { + left: 50, + right: 50 + } + }); + } + spec.axes = [ + ...axes, + { + orient: 'left', + type: 'band', + domainLine: { + visible: false + }, + tick: { + visible: false + }, + label: { + formatMethod: label => { + return { + type: 'rich', + text: [{ image: spec.userImageMap[label], width: 40, height: 40 }] + }; + } + }, + grid: { + visible: true, + style: { + lineDash: [3, 3] + } + } + } + ]; + + // 缩略轴和滚动条配置 + spec.dataZoom = [ + { + orient: 'bottom', + axisIndex: 0, + filterMode: 'filter' + } + ]; + spec.scrollBar = [ + { + orient: 'right', + axisIndex: spec.axes.length - 1, + filterMode: 'axis', + start: 0, + end: 0.8 + }, + { + orient: 'bottom', + axisIndex: 0, + linkedAxisIndex: mediumConnection && mode === 'step' ? undefined : 1, + filterMode: 'axis', + start: 0, + end: 0.8 + } + ]; + spec.crosshair = { + xField: { + visible: true, + line: { + type: 'rect', + width: 20, + style: { + fill: 'rgb(240,242,245)' + } + }, + bindingAxesIndex: [0, 1] + }, + yField: { + visible: true, + bindingAxesIndex: [spec.axes.length - 1], + line: { + type: 'rect' + } + } + }; + + // markLine配置 + if (mode === 'time') { + spec.markLine = tickData.map(t => ({ + zIndex: -1, + x: t.time_stamp, + endSymbol: { + visible: false + }, + label: { + visible: false + } + })); + } + + // 其他配置 + spec.tooltip = { + visible: false + }; + spec.animation = false; + + // 自定义配置 + spec.customTransformSpec?.(spec); + + super.transformSpec(spec); + } +} diff --git a/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis.ts b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis.ts new file mode 100644 index 0000000000..f97a587b41 --- /dev/null +++ b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis.ts @@ -0,0 +1,41 @@ +import type { ISequenceAnalysisSpec } from './interface'; +import { VChart, BaseChart, CommonChart } from '@visactor/vchart'; +import { SequenceAnalysisChartSpecTransformer } from './sequence-analysis-transformer'; + +export class SequenceAnalysis extends BaseChart> { + type = 'sequenceAnalysis'; + static type = 'sequenceAnalysis'; + static readonly view: string = 'singleDefault'; + + declare _spec: ISequenceAnalysisSpec; + + static readonly transformerConstructor = SequenceAnalysisChartSpecTransformer; + readonly transformerConstructor = SequenceAnalysisChartSpecTransformer; + + init() { + if (!this.isValid()) { + return; + } + super.init(); + } + + protected isValid() { + const { eventData, type } = this._spec; + if (type !== 'sequenceAnalysis') { + this._option.onError?.('Type is required'); + return false; + } + if (!eventData) { + this._option.onError?.('Data is required'); + return false; + } + return true; + } +} + +export const registerSequenceAnalysis = (option?: { VChart?: typeof VChart }) => { + const vchartConstructor = option?.VChart || VChart; + if (vchartConstructor) { + vchartConstructor.useChart([SequenceAnalysis, CommonChart]); + } +}; diff --git a/packages/vchart-extension/src/index.ts b/packages/vchart-extension/src/index.ts index 9e580565f6..e94be657a7 100644 --- a/packages/vchart-extension/src/index.ts +++ b/packages/vchart-extension/src/index.ts @@ -4,6 +4,7 @@ export * from './charts/conversion-funnel'; export * from './charts/sequence-scatter-link/sequence-scatter-link'; export * from './charts/sequence-scatter-kde/sequence-scatter-kde'; export * from './charts/sequence-scatter-pixel/sequence-scatter-pixel'; +export * from './charts/sequence-analysis/sequence-analysis'; export * from './charts/bar-3d'; export * from './charts/funnel-3d'; diff --git a/packages/vchart/src/component/data-zoom/data-filter-base-component.ts b/packages/vchart/src/component/data-zoom/data-filter-base-component.ts index 7f91ad32f3..fc9bd37c2e 100644 --- a/packages/vchart/src/component/data-zoom/data-filter-base-component.ts +++ b/packages/vchart/src/component/data-zoom/data-filter-base-component.ts @@ -75,6 +75,7 @@ export abstract class DataFilterBaseComponent; // 与系列的关联关系 @@ -216,29 +217,38 @@ export abstract class DataFilterBaseComponent; if (reverse) { switch (tag) { case 'startHandler': axis.scaleRangeFactorEnd(newRangeFactor[1]); + linkedAxis?.scaleRangeFactorEnd(newRangeFactor[1]); break; case 'endHandler': axis.scaleRangeFactorStart(newRangeFactor[0]); + linkedAxis?.scaleRangeFactorStart(newRangeFactor[0]); break; - default: + default: // end 保证为准确值 axis.scaleRangeFactorStart(newRangeFactor[0], true); - axis.scaleRangeFactorEnd(newRangeFactor[1]); // end 保证为准确值 + axis.scaleRangeFactorEnd(newRangeFactor[1]); + linkedAxis?.scaleRangeFactorStart(newRangeFactor[0], true); + linkedAxis?.scaleRangeFactorEnd(newRangeFactor[1]); // end 保证为准确值 } } else { switch (tag) { case 'startHandler': axis.scaleRangeFactorStart(newRangeFactor[0]); + linkedAxis?.scaleRangeFactorStart(newRangeFactor[0]); break; case 'endHandler': axis.scaleRangeFactorEnd(newRangeFactor[1]); + linkedAxis?.scaleRangeFactorEnd(newRangeFactor[1]); break; - default: + default: // start 保证为准确值 axis.scaleRangeFactorEnd(newRangeFactor[1], true); - axis.scaleRangeFactorStart(newRangeFactor[0]); // start 保证为准确值 + linkedAxis?.scaleRangeFactorEnd(newRangeFactor[1], true); + axis.scaleRangeFactorStart(newRangeFactor[0]); + linkedAxis?.scaleRangeFactorStart(newRangeFactor[0]); // start 保证为准确值 } } @@ -339,6 +349,11 @@ export abstract class DataFilterBaseComponent { + if (s.type === 'link') { + return; + } // 如果副轴的类型是time或band,则无法进行数据统计 const xAxisHelper = s.coordinate === 'cartesian' ? (s as ICartesianSeries).getXAxisHelper() : s.coordinate === 'polar' - ? (s as IPolarSeries).angleAxisHelper - : null; + ? (s as IPolarSeries).angleAxisHelper + : null; const yAxisHelper = s.coordinate === 'cartesian' ? (s as ICartesianSeries).getYAxisHelper() : s.coordinate === 'polar' - ? (s as IPolarSeries).radiusAxisHelper - : null; + ? (s as IPolarSeries).radiusAxisHelper + : null; if (!xAxisHelper || !yAxisHelper) { return; } @@ -463,13 +482,14 @@ export abstract class DataFilterBaseComponent boolean; + /** + * 仅在画布交互展示组件 + * @since 2.0.3 + */ + autoVisible?: boolean; } export interface IRoamScrollSpec extends IRoamSpec { diff --git a/packages/vchart/src/component/data-zoom/util.ts b/packages/vchart/src/component/data-zoom/util.ts index 81d16cf54c..10cef7ec8e 100644 --- a/packages/vchart/src/component/data-zoom/util.ts +++ b/packages/vchart/src/component/data-zoom/util.ts @@ -1,6 +1,7 @@ import { isArray, last } from '@visactor/vutils'; import { array, isNil } from '../../util'; import type { DataView } from '@visactor/vdataset'; +import { objFlat } from '../../data/transforms/obj-flat'; export interface IDataFilterWithNewDomainOption { getNewDomain: () => any[]; @@ -93,6 +94,7 @@ export interface IDataFilterComputeDomainOption { dataCollection: any[]; stateFields: string[]; valueFields: string[]; + seriesTypes: string[]; isCategoryState?: boolean; method: 'sum'; // todo: 也许可以提供多种数据统计方法 @chensiji }; @@ -103,7 +105,7 @@ export interface IDataFilterComputeDomainOption { } export const dataFilterComputeDomain = (data: Array, op: IDataFilterComputeDomainOption) => { - const { stateFields, valueFields, dataCollection, isCategoryState } = op.input; + const { stateFields, valueFields, dataCollection, isCategoryState, seriesTypes } = op.input; const { stateField, valueField } = op.output; const resultObj: any = {}; const resultData: any[] = []; @@ -125,7 +127,8 @@ export const dataFilterComputeDomain = (data: Array, op: IDataFilterCompute } }); } - dv.latestData.forEach((d: any) => { + const data = seriesTypes[i] === 'dot' ? objFlat(dv.latestData, 'dots') : dv.latestData; + data.forEach((d: any) => { // 针对rangeColumn等xField为数组形式的图表,需要将xField的所有value都记录下来 array(stateFields[i]).forEach(state => { if (!isNil(d[state])) { @@ -146,8 +149,8 @@ export const dataFilterComputeDomain = (data: Array, op: IDataFilterCompute const sortedStateValues = hasLockDomain ? stateValues : isCategoryState === false - ? stateValues.sort((a, b) => a - b) - : Object.keys(resultObj); + ? stateValues.sort((a, b) => a - b) + : Object.keys(resultObj); sortedStateValues.forEach(state => { const res = { [stateField]: state }; diff --git a/packages/vchart/src/data/transforms/obj-flat.ts b/packages/vchart/src/data/transforms/obj-flat.ts index c38cc62778..eae23f5058 100644 --- a/packages/vchart/src/data/transforms/obj-flat.ts +++ b/packages/vchart/src/data/transforms/obj-flat.ts @@ -22,16 +22,26 @@ export const objFlat = (data: Array, op: string) => { } const dataOp = datum[op]; - dataOp?.forEach((d: any, i: number) => { + if (dataOp && dataOp.length > 0) { + dataOp?.forEach((d: any, i: number) => { + result.push( + Object.assign({}, dataKeyObj, d, { + [DEFAULT_DATA_INDEX]: index, + [DEFAULT_DATA_KEY]: `${dataKey}_${i}` + }) + ); + + index++; + }); + } else { result.push( - Object.assign({}, dataKeyObj, d, { + Object.assign({}, dataKeyObj, { [DEFAULT_DATA_INDEX]: index, - [DEFAULT_DATA_KEY]: `${dataKey}_${i}` + [DEFAULT_DATA_KEY]: dataKey }) ); - - index++; - }); + } }); + // console.log('result', result); return result; }; diff --git a/packages/vchart/src/series/dot/dot.ts b/packages/vchart/src/series/dot/dot.ts index bd106bd014..f530c49f4f 100644 --- a/packages/vchart/src/series/dot/dot.ts +++ b/packages/vchart/src/series/dot/dot.ts @@ -115,14 +115,42 @@ export class DotSeries extends Cartes }, false ); + + this.getRawData()?.transform( + { + type: 'objFlat', + options: 'dots', + level: TransformLevel.dotObjFlat + }, + false + ); + } + + protected _statisticViewData() { + super._statisticViewData(); + this._viewDataStatistics?.transform( + { + type: 'objFlat', + options: 'dots', + level: TransformLevel.dotObjFlat + }, + false + ); } getStatisticFields() { - return [{ key: this._fieldY[0], operations: ['values'], customize: this._xDimensionStatisticsDomain }] as { + const result = [{ key: this._fieldY[0], operations: ['values'], customize: this._xDimensionStatisticsDomain }] as { key: string; operations: Array<'max' | 'min' | 'values'>; customize: any[]; }[]; + const { linearMode = true } = this._spec; + if (linearMode) { + (result as any).push({ key: this._fieldX[0], operations: ['min', 'max'] }); + } else { + (result as any).push({ key: this._fieldX[0], operations: ['values'] }); + } + return result; } /** @@ -192,11 +220,11 @@ export class DotSeries extends Cartes this.setMarkStyle( clipMark, { - x: -this._spec.leftAppendPadding, + x: -(this._spec.leftAppendPadding ?? 0), y: 0, // 本应使用this.getLayoutRect().width, 但这该返回值为0。考虑到横向不需要裁剪,故先采用一个较大值 width: 10000, - height: this._spec.clipHeight + height: () => this._spec.clipHeight ?? this._region.getLayoutRect().height }, 'normal', AttributeLevel.Series @@ -209,7 +237,7 @@ export class DotSeries extends Cartes this.setMarkStyle( containerMark, { - x: this._spec.leftAppendPadding + x: this._spec.leftAppendPadding ?? 0 }, 'normal', AttributeLevel.Series @@ -390,8 +418,8 @@ export class DotSeries extends Cartes return this._seriesGroupField ? this.getViewDataStatistics()?.latestData[this._seriesGroupField].values : this._seriesField - ? this.getViewDataStatistics()?.latestData[this._seriesField].values - : []; + ? this.getViewDataStatistics()?.latestData[this._seriesField].values + : []; } /** @@ -413,10 +441,10 @@ export class DotSeries extends Cartes const colorDomain = this._dotTypeField ? this.getViewDataStatistics()?.latestData[this._dotTypeField].values : this._seriesGroupField - ? this.getViewDataStatistics()?.latestData[this._seriesGroupField].values - : this._seriesField - ? this.getViewDataStatistics()?.latestData[this._seriesField].values - : []; + ? this.getViewDataStatistics()?.latestData[this._seriesGroupField].values + : this._seriesField + ? this.getViewDataStatistics()?.latestData[this._seriesField].values + : []; const colorRange = this._getDataScheme(); return new ColorOrdinalScale().domain(colorDomain).range(colorRange); } diff --git a/packages/vchart/src/series/dot/interface.ts b/packages/vchart/src/series/dot/interface.ts index e237b771b2..8e6baccd7b 100644 --- a/packages/vchart/src/series/dot/interface.ts +++ b/packages/vchart/src/series/dot/interface.ts @@ -58,6 +58,13 @@ export interface IDotSeriesSpec extends ICartesianSeriesSpec { * @description 用于标记node name,便于绘制link。 */ name: string; + + /** + * 是否线性维度展开 + * @default true + */ + linearMode?: boolean; + /** * 图元配置 */ diff --git a/packages/vchart/src/series/dot/tooltip-helper.ts b/packages/vchart/src/series/dot/tooltip-helper.ts index b745d83333..e6466b825a 100644 --- a/packages/vchart/src/series/dot/tooltip-helper.ts +++ b/packages/vchart/src/series/dot/tooltip-helper.ts @@ -69,7 +69,7 @@ export class DotSeriesTooltipHelper extends BaseSeriesTooltipHelper implements I const childrenPrev = prev.filter((p: any) => p.key === 'children'); childrenPrev.length > 0 && - childrenPrev[0].value.forEach((element: any) => { + childrenPrev?.[0]?.value?.forEach((element: any) => { let flag = true; for (const key in element) { childrenContent.push({ diff --git a/packages/vchart/src/series/interface/type.ts b/packages/vchart/src/series/interface/type.ts index 5195a0ba56..aab6ffac53 100644 --- a/packages/vchart/src/series/interface/type.ts +++ b/packages/vchart/src/series/interface/type.ts @@ -9,6 +9,7 @@ export enum SeriesTypeEnum { dot = 'dot', geo = 'geo', link = 'link', + curveLink = 'curveLink', map = 'map', pie = 'pie', radar = 'radar', @@ -63,6 +64,8 @@ export const enum SeriesMarkNameEnum { cell = 'cell', cellBackground = 'cellBackground', link = 'link', + curveLink = 'curveLink', + imageLabel = 'imageLabel', arrow = 'arrow', pie = 'pie', labelLine = 'labelLine', diff --git a/packages/vchart/src/series/link/constant.ts b/packages/vchart/src/series/link/constant.ts index c730ec563b..5231a7ed18 100644 --- a/packages/vchart/src/series/link/constant.ts +++ b/packages/vchart/src/series/link/constant.ts @@ -7,5 +7,7 @@ export const linkSeriesMark: SeriesMarkMap = { ...baseSeriesMark, [SeriesMarkNameEnum.group]: { name: SeriesMarkNameEnum.group, type: MarkTypeEnum.group }, [SeriesMarkNameEnum.link]: { name: SeriesMarkNameEnum.link, type: MarkTypeEnum.rule }, - [SeriesMarkNameEnum.arrow]: { name: SeriesMarkNameEnum.arrow, type: MarkTypeEnum.symbol } + [SeriesMarkNameEnum.curveLink]: { name: SeriesMarkNameEnum.curveLink, type: MarkTypeEnum.path }, + [SeriesMarkNameEnum.arrow]: { name: SeriesMarkNameEnum.arrow, type: MarkTypeEnum.symbol }, + [SeriesMarkNameEnum.imageLabel]: { name: SeriesMarkNameEnum.imageLabel, type: MarkTypeEnum.image } }; diff --git a/packages/vchart/src/series/link/interface.ts b/packages/vchart/src/series/link/interface.ts index 839d9ac62b..336d6302df 100644 --- a/packages/vchart/src/series/link/interface.ts +++ b/packages/vchart/src/series/link/interface.ts @@ -35,6 +35,22 @@ export interface ILinkSeriesSpec extends ICartesianSeriesSpec, ILinkSeriesSpecFr * @description 用于区分link的stroke逻辑。如果没有配置,则每条link都使用相同的stroke */ dotTypeField?: string; + + /** + * link类型: 直线 或 曲线 + * @default 'line' + */ + linkType?: LinkType; + + /** + * link的image标签字段 + */ + imageLabelField?: string; + /** + * 是否允许向左右两侧延伸 + * @default false + */ + allowExtend?: boolean; /** * 图元配置 */ @@ -66,3 +82,8 @@ export interface ILinkSeriesTheme extends ICartesianSeriesTheme { [SeriesMarkNameEnum.link]?: Partial>; [SeriesMarkNameEnum.arrow]?: Partial>; } + +export enum LinkType { + line = 'line', + curve = 'curve' +} diff --git a/packages/vchart/src/series/link/link.ts b/packages/vchart/src/series/link/link.ts index f288d7808d..5096453756 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -3,12 +3,12 @@ import { DEFAULT_DATA_SERIES_FIELD } from '../../constant/data'; import { CartesianSeries } from '../cartesian/cartesian'; import type { Datum } from '../../typings'; import { isValid } from '@visactor/vutils'; -import type { IGroupMark, IMark, IRuleMark, ISymbolMark } from '../../mark/interface'; +import type { IGroupMark, IImageMark, IMark, IRuleMark, ISymbolMark } from '../../mark/interface'; import { SeriesTypeEnum } from '../interface/type'; import { registerDataSetInstanceTransform } from '../../data/register'; import type { IDotSeriesSpec } from '../dot/interface'; import { LinkSeriesTooltipHelper } from './tooltip-helper'; -import type { ILinkSeriesSpec } from './interface'; +import { LinkType, type ILinkSeriesSpec } from './interface'; import type { SeriesMarkMap } from '../interface'; import { registerRuleMark } from '../../mark/rule'; import { registerSymbolMark } from '../../mark/symbol'; @@ -27,6 +27,9 @@ export class LinkSeries extends Car static readonly mark: SeriesMarkMap = linkSeriesMark; static readonly builtInTheme = { link }; + // 记录起点和终点相同的曲线数量, 便于给不同曲线设置不同曲率 + private _curveLinkCountMap: Record = {}; + protected _fromField?: string; getFromField() { return this._fromField; @@ -116,14 +119,16 @@ export class LinkSeries extends Car private _containerMark: IGroupMark; private _linkMark: IRuleMark; private _arrowMark: ISymbolMark; + private _imageLabelMark: IImageMark; initMark(): void { + const { linkType = LinkType.line, imageLabelField } = this._spec; this._clipMark = this._createMark(LinkSeries.mark.group) as IGroupMark; this._containerMark = this._createMark(LinkSeries.mark.group, { parent: this._clipMark }) as IGroupMark; - this._linkMark = this._createMark(LinkSeries.mark.link, { + this._linkMark = this._createMark(linkType === LinkType.curve ? LinkSeries.mark.curveLink : LinkSeries.mark.link, { skipBeforeLayouted: false, parent: this._containerMark }) as IRuleMark; @@ -133,6 +138,14 @@ export class LinkSeries extends Car isSeriesMark: true, parent: this._containerMark }) as ISymbolMark; + + if (imageLabelField) { + this._imageLabelMark = this._createMark(LinkSeries.mark.imageLabel, { + skipBeforeLayouted: false, + isSeriesMark: true, + parent: this._containerMark + }) as IImageMark; + } } initMarkStyle(): void { @@ -141,11 +154,11 @@ export class LinkSeries extends Car this.setMarkStyle( clipMark, { - x: -this._spec.leftAppendPadding, + x: -(this._spec.leftAppendPadding ?? 0), y: 0, // 本应使用this.getLayoutRect().width, 但这该返回值为0。考虑到横向不需要裁剪,故先采用一个较大值 width: 10000, - height: this._spec.clipHeight + height: () => this._spec.clipHeight ?? this._region.getLayoutRect().height }, 'normal', AttributeLevel.Series @@ -168,25 +181,60 @@ export class LinkSeries extends Car } const linkMark = this._linkMark; + const { linkType = LinkType.line } = this._spec; if (linkMark) { - this.setMarkStyle( - linkMark, - { - stroke: this.getColorAttribute(), - strokeOpacity: this.dataToOpacity.bind(this), - x: this.dataToPositionXFrom.bind(this), - y: this.dataToPositionYFrom.bind(this), - x1: this.dataToPositionXTo.bind(this), - y1: this.dataToPositionYTo.bind(this) - }, - 'normal', - AttributeLevel.Series - ); + if (linkType === LinkType.curve) { + this.setMarkStyle( + linkMark, + { + stroke: this._spec.link?.style?.stroke ?? this.getColorAttribute(), + strokeOpacity: this.dataToOpacity.bind(this), + path: (datum: Datum) => { + const fromX = this.dataToPositionXFrom(datum); + const fromY = this.dataToPositionYFrom(datum); + const toX = this.dataToPositionXTo(datum); + const toY = this.dataToPositionYTo(datum); + + // 计算曲率 + const curveLinkCount = this._curveLinkCountMap[`${fromX}-${fromY}-${toX}-${toY}`] || 1; + this._curveLinkCountMap[`${fromX}-${fromY}-${toX}-${toY}`] = curveLinkCount + 1; + const curveRatio = 0.5 + curveLinkCount * 0.1; + + // 计算控制点 + const controlX1 = fromX + (toX - fromX) * 0.7; + const controlY1 = fromY; + const controlX2 = toX - (toX - fromX) * curveRatio; + const controlY2 = toY; + + // 生成三次贝塞尔曲线路径 + return `M ${fromX} ${fromY} C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${toX} ${toY}`; + }, + lineWidth: this._spec.link.style?.lineWidth ?? 1 + }, + 'normal', + AttributeLevel.Series + ); + } else { + this.setMarkStyle( + linkMark, + { + stroke: this.getColorAttribute(), + strokeOpacity: this.dataToOpacity.bind(this), + x: this.dataToPositionXFrom.bind(this), + y: this.dataToPositionYFrom.bind(this), + x1: this.dataToPositionXTo.bind(this), + y1: this.dataToPositionYTo.bind(this) + }, + 'normal', + AttributeLevel.Series + ); + } } const arrowMark = this._arrowMark; if (arrowMark) { const arrowSize = (this._theme?.arrow?.style?.size as number) ?? 10; + this.setMarkStyle( arrowMark, { @@ -203,6 +251,30 @@ export class LinkSeries extends Car AttributeLevel.Series ); } + + const imageLabelMark = this._imageLabelMark; + if (imageLabelMark) { + const { style = {} } = this._spec.imageLabel; + const { width = 40, height = 40 } = style; + this.setMarkStyle( + imageLabelMark, + { + x: datum => { + return (this.dataToPositionXFrom(datum) + this.dataToPositionXTo(datum)) / 2 - width / 2; + }, + y: datum => { + return (this.dataToPositionYFrom(datum) + this.dataToPositionYTo(datum)) / 2 - height / 2; + }, + width, + height, + image: datum => { + return datum[this._spec.imageLabelField]; + } + }, + 'normal', + AttributeLevel.Series + ); + } } afterInitMark(): void { @@ -217,9 +289,17 @@ export class LinkSeries extends Car } const { dataToPosition } = this._xAxisHelper; - return dataToPosition(this.getDatumPositionValues(datum, this._fromField + '_xField'), { - bandPosition: this._bandPosition - }); + const allowExtend = this._spec.allowExtend; + const bandSize = this._xAxisHelper?.getBandwidth?.(0) ?? 0; + + const { style = {} } = this._dotSeriesSpec.symbol ?? {}; + const { size: dotSize = 10 } = style; + + return ( + dataToPosition(this.getDatumPositionValues(datum, this._fromField + '_xField'), { + bandPosition: this._bandPosition + }) + (allowExtend && (!datum.isExtend || datum.isExtend === 'left') ? -(bandSize + dotSize) / 2 : 0) + ); } dataToPositionYFrom(datum: Datum): number { @@ -236,9 +316,16 @@ export class LinkSeries extends Car return Number.NaN; } const { dataToPosition } = this._xAxisHelper; - return dataToPosition(this.getDatumPositionValues(datum, this._toField + '_xField'), { - bandPosition: this._bandPosition - }); + const allowExtend = this._spec.allowExtend; + const bandSize = this._xAxisHelper?.getBandwidth?.(0) ?? 0; + const { style = {} } = this._dotSeriesSpec.symbol ?? {}; + const { size: dotSize = 10 } = style; + + return ( + dataToPosition(this.getDatumPositionValues(datum, this._toField + '_xField'), { + bandPosition: this._bandPosition + }) + (allowExtend && (!datum.isExtend || datum.isExtend === 'right') ? (bandSize + dotSize) / 2 : 0) + ); } dataToPositionYTo(datum: Datum): number { @@ -267,9 +354,10 @@ export class LinkSeries extends Car dataToOpacity(datum: Datum): number { if ( - this.isPositionXOuterRange(datum, this._fromField + '_xField') || - this.isPositionXOuterRange(datum, this._toField + '_xField') || - datum[this._fromField] === datum[this._toField] + !this._spec.allowExtend && + (this.isPositionXOuterRange(datum, this._fromField + '_xField') || + this.isPositionXOuterRange(datum, this._toField + '_xField') || + datum[this._fromField] === datum[this._toField]) ) { return 0; } @@ -313,8 +401,8 @@ export class LinkSeries extends Car return this._dotTypeField ? this.getViewDataStatistics()?.latestData[this._dotTypeField].values : this._seriesField - ? this.getViewDataStatistics()?.latestData[this._seriesField].values - : []; + ? this.getViewDataStatistics()?.latestData[this._seriesField].values + : []; } /** From 0fb2d67348a5405a244276ea8782eef68f265de3 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 4 Aug 2025 14:43:22 +0800 Subject: [PATCH 02/16] feat: extension chart for sequence analysis --- packages/vchart/src/series/link/link.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/vchart/src/series/link/link.ts b/packages/vchart/src/series/link/link.ts index 5096453756..6fa2888e3c 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -2,7 +2,7 @@ import { DEFAULT_DATA_SERIES_FIELD } from '../../constant/data'; import { CartesianSeries } from '../cartesian/cartesian'; import type { Datum } from '../../typings'; -import { isValid } from '@visactor/vutils'; +import { array, isValid } from '@visactor/vutils'; import type { IGroupMark, IImageMark, IMark, IRuleMark, ISymbolMark } from '../../mark/interface'; import { SeriesTypeEnum } from '../interface/type'; import { registerDataSetInstanceTransform } from '../../data/register'; @@ -254,7 +254,7 @@ export class LinkSeries extends Car const imageLabelMark = this._imageLabelMark; if (imageLabelMark) { - const { style = {} } = this._spec.imageLabel; + const { style = {} } = (this._spec as ILinkSeriesSpec).imageLabel; const { width = 40, height = 40 } = style; this.setMarkStyle( imageLabelMark, @@ -324,7 +324,10 @@ export class LinkSeries extends Car return ( dataToPosition(this.getDatumPositionValues(datum, this._toField + '_xField'), { bandPosition: this._bandPosition - }) + (allowExtend && (!datum.isExtend || datum.isExtend === 'right') ? (bandSize + dotSize) / 2 : 0) + }) + + (allowExtend && (!datum.isExtend || datum.isExtend === 'right') + ? (array(bandSize)[0] + (dotSize as number)) / 2 + : 0) ); } @@ -401,8 +404,8 @@ export class LinkSeries extends Car return this._dotTypeField ? this.getViewDataStatistics()?.latestData[this._dotTypeField].values : this._seriesField - ? this.getViewDataStatistics()?.latestData[this._seriesField].values - : []; + ? this.getViewDataStatistics()?.latestData[this._seriesField].values + : []; } /** From 7c5e6bfd405f81ce80309be9a5b62e5886cf8e1b Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 4 Aug 2025 14:44:36 +0800 Subject: [PATCH 03/16] feat: extension chart for sequence analysis --- packages/vchart/src/series/link/link.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vchart/src/series/link/link.ts b/packages/vchart/src/series/link/link.ts index 6fa2888e3c..e9d27d4b15 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -298,7 +298,10 @@ export class LinkSeries extends Car return ( dataToPosition(this.getDatumPositionValues(datum, this._fromField + '_xField'), { bandPosition: this._bandPosition - }) + (allowExtend && (!datum.isExtend || datum.isExtend === 'left') ? -(bandSize + dotSize) / 2 : 0) + }) + + (allowExtend && (!datum.isExtend || datum.isExtend === 'left') + ? -(array(bandSize)[0] + (dotSize as number)) / 2 + : 0) ); } From 39bfee3432330cbc771000caf2cf16f336bdf2da Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 4 Aug 2025 14:47:30 +0800 Subject: [PATCH 04/16] feat: extension chart for sequence analysis --- packages/vchart/src/series/link/interface.ts | 7 ++++++- packages/vchart/src/series/link/link.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/vchart/src/series/link/interface.ts b/packages/vchart/src/series/link/interface.ts index 336d6302df..98c073832e 100644 --- a/packages/vchart/src/series/link/interface.ts +++ b/packages/vchart/src/series/link/interface.ts @@ -1,6 +1,6 @@ import type { ICartesianSeriesSpec, ICartesianSeriesTheme } from '../cartesian/interface'; import type { IMarkSpec, IMarkTheme } from '../../typings/spec/common'; -import type { ILineMarkSpec, ISymbolMarkSpec } from '../../typings/visual'; +import type { IImageMarkSpec, ILineMarkSpec, ISymbolMarkSpec } from '../../typings/visual'; import type { SeriesMarkNameEnum } from '../interface/type'; import type { DataView } from '@visactor/vdataset'; import type { IDotSeriesSpec } from '../dot/interface'; @@ -55,6 +55,10 @@ export interface ILinkSeriesSpec extends ICartesianSeriesSpec, ILinkSeriesSpecFr * 图元配置 */ [SeriesMarkNameEnum.link]?: IMarkSpec; + /** + * image标签配置 + */ + [SeriesMarkNameEnum.imageLabel]?: IMarkSpec; } type ILinkSeriesSpecFromDot = { @@ -81,6 +85,7 @@ type ILinkSeriesSpecFromDot = { export interface ILinkSeriesTheme extends ICartesianSeriesTheme { [SeriesMarkNameEnum.link]?: Partial>; [SeriesMarkNameEnum.arrow]?: Partial>; + [SeriesMarkNameEnum.imageLabel]?: Partial>; } export enum LinkType { diff --git a/packages/vchart/src/series/link/link.ts b/packages/vchart/src/series/link/link.ts index e9d27d4b15..c5db8310b3 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -260,10 +260,10 @@ export class LinkSeries extends Car imageLabelMark, { x: datum => { - return (this.dataToPositionXFrom(datum) + this.dataToPositionXTo(datum)) / 2 - width / 2; + return (this.dataToPositionXFrom(datum) + this.dataToPositionXTo(datum)) / 2 - (width as number) / 2; }, y: datum => { - return (this.dataToPositionYFrom(datum) + this.dataToPositionYTo(datum)) / 2 - height / 2; + return (this.dataToPositionYFrom(datum) + this.dataToPositionYTo(datum)) / 2 - (height as number) / 2; }, width, height, From 023e0103b18a9ba9f9b135ff862663d2f907d1a3 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 15:04:08 +0800 Subject: [PATCH 05/16] fix: resolve sequence analysis P1 review findings --- .../sequence-analysis-transformer.ts | 6 +- .../series/sequence-analysis-review.test.ts | 105 ++++++++++++++++++ .../cases/api/update/sequence-link-data.mjs | 104 +++++++++++++++++ .../vchart/__tests__/visual/cases/index.mjs | 5 + packages/vchart/src/series/dot/dot.ts | 21 +--- packages/vchart/src/series/link/link.ts | 4 + 6 files changed, 225 insertions(+), 20 deletions(-) create mode 100644 packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts create mode 100644 packages/vchart/__tests__/visual/cases/api/update/sequence-link-data.mjs diff --git a/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts index b2451033aa..927bb425e2 100644 --- a/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts +++ b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts @@ -52,10 +52,6 @@ export class SequenceAnalysisChartSpecTransformer extends CommonChartSpecTransfo strokeOpacity: 0.5, stroke: 'grey' } - }, - style: { - // stroke: 'grey', - visible: mode === 'step' || actionTarget } }, imageLabel: { @@ -305,7 +301,7 @@ export class SequenceAnalysisChartSpecTransformer extends CommonChartSpecTransfo visible: false }, label: { - formatMethod: label => { + formatMethod: (label: string) => { return { type: 'rich', text: [{ image: spec.userImageMap[label], width: 40, height: 40 }] diff --git a/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts b/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts new file mode 100644 index 0000000000..3ea1c54ab3 --- /dev/null +++ b/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts @@ -0,0 +1,105 @@ +import { DataSet, DataView, dataViewParser } from '@visactor/vdataset'; +import { DotSeries } from '../../../src/series/dot/dot'; +import { LinkSeries, registerLinkSeries } from '../../../src/series/link/link'; +import { registerSequenceChart } from '../../../src/chart/sequence/sequence'; +import { Factory } from '../../../src/core/factory'; +import { factoryRegistry } from '../../../src/core/factory-registry'; +import { linkDotInfo } from '../../../src/data/transforms/link-dot-info'; +// eslint-disable-next-line max-len +import { SequenceAnalysisChartSpecTransformer } from '../../../../vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer'; + +const events = (time: number) => [ + { + user_id: 'U1', + dots: [ + { node_name: 'a', time_stamp: time }, + { node_name: 'b', time_stamp: time + 10 } + ] + }, + { user_id: 'U2', dots: [{ node_name: 'c', time_stamp: time + 20 }] } +]; + +describe('sequence analysis review regressions', () => { + it('preserves user rows and link coordinates after fillData and raw-data updates', () => { + const dataSet = new DataSet(); + dataSet.registerParser('dataview', dataViewParser); + const raw = new DataView(dataSet).parse(events(10)); + const dot: any = new DotSeries( + { type: 'dot', xField: 'time_stamp', yField: 'user_id', animation: false } as any, + { dataSet, getSeriesData: () => raw, eventDispatcher: { dispatch: jest.fn() } } as any + ); + dot._dataSet = dataSet; + dot._fieldX = ['time_stamp']; + dot._fieldY = ['user_id']; + dot.initData(); + const link: any = new LinkSeries( + { dotSeriesIndex: 0 } as any, + { + getChart: () => ({ getSeriesInIndex: () => [dot] }) + } as any + ); + const verify = (time: number) => { + dot.fillData(); + expect(raw.latestData).toHaveLength(2); + expect(raw.latestData[0].dots).toHaveLength(2); + expect(dot.getViewData().latestData).toHaveLength(3); + const links = linkDotInfo([], { + infoKey: 'dots', + fields: () => ({ fromField: 'from', toField: 'to', xField: 'time_stamp', yField: 'user_id' }), + linkData: () => [{ from: 'a', to: 'c' }], + dotData: () => link._getDotData() + }); + expect(links[0]).toMatchObject({ + from_xField: time, + from_yField: 'U1', + to_xField: time + 20, + to_yField: 'U2' + }); + }; + verify(10); + verify(10); + dot.updateRawData(events(40)); + verify(40); + dataSet.destroy(); + }); + + it.each([false, true])('keeps time-mode links visible with actionTarget = %s', actionTarget => { + const spec: any = { + type: 'sequenceAnalysis', + mode: 'time', + actionTarget, + eventData: events(10), + actionData: [{ from: 'a', to: 'c' }], + userImageMap: {}, + stepMediumMap: {} + }; + new SequenceAnalysisChartSpecTransformer({ type: 'sequenceAnalysis' }).transformSpec(spec); + const link = spec.series[0]; + expect(link.linkType).toBe('line'); + expect(link.link.visible).not.toBe(false); + expect(link.link.style?.visible).not.toBe(false); + expect(link.imageLabel.visible).toBe(actionTarget); + expect(link.arrow.style.visible).toBe(actionTarget); + }); + + it.each([registerLinkSeries, registerSequenceChart])('registers curve and image marks on demand (%#)', register => { + const marks = { ...factoryRegistry.marks }; + const graphics = { ...factoryRegistry.graphicComponents }; + try { + delete factoryRegistry.marks.path; + delete factoryRegistry.marks.image; + delete factoryRegistry.graphicComponents.path; + delete factoryRegistry.graphicComponents.image; + register(); + for (const type of ['path', 'image']) { + const mark = Factory.createMark(type, type, {} as any); + expect(mark?.type).toBe(type); + expect(Factory.createGraphicComponent(type, {}).type).toBe(type); + mark.release(); + } + } finally { + Object.assign(factoryRegistry.marks, marks); + Object.assign(factoryRegistry.graphicComponents, graphics); + } + }); +}); diff --git a/packages/vchart/__tests__/visual/cases/api/update/sequence-link-data.mjs b/packages/vchart/__tests__/visual/cases/api/update/sequence-link-data.mjs new file mode 100644 index 0000000000..4c4ebce713 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update/sequence-link-data.mjs @@ -0,0 +1,104 @@ +/** + * 验证目的:dot 原始数据保持逐用户的嵌套结构,更新后 link 仍能定位事件。 + * 图表类型:common(dot + link)。 + * 关键配置:dotSeriesIndex、嵌套 dots、固定线性轴与用户轴。 + * 场景条件:两个用户三个事件,更新事件时间后检查连接端点。 + * 最终检查:原始用户行、展开事件数、更新后的连接数据和有限图元坐标;截图检查连接呈现。 + * 覆盖边界:不覆盖曲线、头像、扩展 transformer 和按需注册,另有单元测试。 + */ +export default { + createSpec() { + // 固定轴范围以便直接对照更新前后的连接位置。 + const dot = { + type: 'dot', + dataId: 'events', + xField: 'time', + yField: 'user', + titleField: 'user', + leftAppendPadding: 0, + title: { visible: false }, + subTitle: { visible: false }, + symbol: { visible: false } + }; + return { + type: 'common', + animation: false, + data: [ + { + id: 'events', + values: [ + { + user: 'U1', + dots: [ + { node_name: 'a', time: 10 }, + { node_name: 'b', time: 20 } + ] + }, + { user: 'U2', dots: [{ node_name: 'c', time: 30 }] } + ] + }, + { id: 'links', values: [{ from: 'a', to: 'c' }] } + ], + series: [ + { + type: 'link', + dataId: 'links', + dotSeriesIndex: 1, + dotSeriesSpec: dot, + fromField: 'from', + toField: 'to', + leftAppendPadding: 0, + link: { style: { stroke: '#5470c6', lineWidth: 2 } }, + arrow: { visible: false } + }, + dot + ], + axes: [ + { type: 'linear', orient: 'bottom', min: 0, max: 80, nice: false }, + { type: 'band', orient: 'left', domain: ['U1', 'U2'] } + ] + }; + }, + async exercise(page) { + // 修改真实数据;未执行动作时最终坐标断言不能通过。 + await page.evaluate(() => { + window.__visualChart.updateFullDataSync([ + { + id: 'events', + values: [ + { + user: 'U1', + dots: [ + { node_name: 'a', time: 40 }, + { node_name: 'b', time: 50 } + ] + }, + { user: 'U2', dots: [{ node_name: 'c', time: 60 }] } + ] + }, + { id: 'links', values: [{ from: 'a', to: 'c' }] } + ]); + }); + }, + async verify(page) { + // 校验数据关系及实际连接图元,避免仅凭配置或截图存在判断成功。 + await page.evaluate(() => { + const series = window.__visualChart.getChart().getAllSeries(); + const dot = series.find(s => s.type === 'dot'); + const link = series.find(s => s.type === 'link'); + const raw = dot.getRawData().latestData; + if (raw.length !== 2 || raw[0].dots.length !== 2 || raw[1].dots.length !== 1) + throw Error('dot 原始数据未保留逐用户结构'); + if (dot.getViewData().latestData.length !== 3) throw Error('dot 视图未展开三个事件'); + const datum = link.getViewData().latestData[0]; + if (datum.from_xField !== 40 || datum.to_xField !== 60 || datum.from_yField !== 'U1' || datum.to_yField !== 'U2') + throw Error('更新后的连接端点数据不正确'); + const mark = link.getMarkInName('link'); + const graphics = mark.getGraphics(); + if (graphics.length !== 1) throw Error('连接图元数量不正确'); + const attrs = graphics[0].attribute; + if (attrs.points.length !== 2 || !attrs.points.every(p => Number.isFinite(p.x) && Number.isFinite(p.y))) + throw Error('连接图元坐标无效'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/index.mjs b/packages/vchart/__tests__/visual/cases/index.mjs index 5c76200eb4..86f11ad71c 100644 --- a/packages/vchart/__tests__/visual/cases/index.mjs +++ b/packages/vchart/__tests__/visual/cases/index.mjs @@ -4,6 +4,11 @@ */ /** 本地用例元数据的唯一清单;模块必须保持浏览器和 Node 均可导入。 */ export const cases = [ + { + id: 'sequence-link-data', + purpose: 'dot 嵌套原始数据与更新后的连接端点', + file: './api/update/sequence-link-data.mjs' + }, { id: 'bar-stack', purpose: '正负值、堆叠与零基准线(bar)', diff --git a/packages/vchart/src/series/dot/dot.ts b/packages/vchart/src/series/dot/dot.ts index c6ff10933b..0cc6ceb61d 100644 --- a/packages/vchart/src/series/dot/dot.ts +++ b/packages/vchart/src/series/dot/dot.ts @@ -141,15 +141,6 @@ export class DotSeries extends Cartes }, false ); - - this.getRawData()?.transform( - { - type: 'objFlat', - options: 'dots', - level: TransformLevel.dotObjFlat - }, - false - ); } protected _statisticViewData() { @@ -444,8 +435,8 @@ export class DotSeries extends Cartes return this._seriesGroupField ? this.getViewDataStatistics()?.latestData[this._seriesGroupField].values : this._seriesField - ? this.getViewDataStatistics()?.latestData[this._seriesField].values - : []; + ? this.getViewDataStatistics()?.latestData[this._seriesField].values + : []; } /** @@ -467,10 +458,10 @@ export class DotSeries extends Cartes const colorDomain = this._dotTypeField ? this.getViewDataStatistics()?.latestData[this._dotTypeField].values : this._seriesGroupField - ? this.getViewDataStatistics()?.latestData[this._seriesGroupField].values - : this._seriesField - ? this.getViewDataStatistics()?.latestData[this._seriesField].values - : []; + ? this.getViewDataStatistics()?.latestData[this._seriesGroupField].values + : this._seriesField + ? this.getViewDataStatistics()?.latestData[this._seriesField].values + : []; const colorRange = this._getDataScheme(); return new ColorOrdinalScale().domain(colorDomain).range(colorRange); } diff --git a/packages/vchart/src/series/link/link.ts b/packages/vchart/src/series/link/link.ts index c5db8310b3..dbf9e331bb 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -12,6 +12,8 @@ import { LinkType, type ILinkSeriesSpec } from './interface'; import type { SeriesMarkMap } from '../interface'; import { registerRuleMark } from '../../mark/rule'; import { registerSymbolMark } from '../../mark/symbol'; +import { registerPathMark } from '../../mark/path'; +import { registerImageMark } from '../../mark/image'; import { linkSeriesMark } from './constant'; import { linkDotInfo } from '../../data/transforms/link-dot-info'; import { Factory } from '../../core/factory'; @@ -459,6 +461,8 @@ export class LinkSeries extends Car export const registerLinkSeries = () => { registerRuleMark(); registerSymbolMark(); + registerPathMark(); + registerImageMark(); registerCartesianBandAxis(); registerCartesianLinearAxis(); Factory.registerSeries(LinkSeries.type, LinkSeries); From e5f19b5545550f7eefac6a6cb005a6280ccae4f5 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 15:29:18 +0800 Subject: [PATCH 06/16] fix: preserve datazoom axis ordering and avoid redundant statistics --- .../component/data-zoom/data-zoom.test.ts | 67 ++++++++++++++++++ .../datazoom/datazoom-numeric-category.mjs | 68 +++++++++++++++++++ .../vchart/__tests__/visual/cases/index.mjs | 5 ++ .../vchart/src/component/data-zoom/util.ts | 3 +- 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-numeric-category.mjs diff --git a/packages/vchart/__tests__/unit/component/data-zoom/data-zoom.test.ts b/packages/vchart/__tests__/unit/component/data-zoom/data-zoom.test.ts index f38451f659..f7b8d3dfc5 100644 --- a/packages/vchart/__tests__/unit/component/data-zoom/data-zoom.test.ts +++ b/packages/vchart/__tests__/unit/component/data-zoom/data-zoom.test.ts @@ -81,4 +81,71 @@ describe('DataZoom preview validity', () => { expect(viewData().map((d: Row) => d.category)).toEqual(['A', 'B', 'C']); expectPreview(); }); + + it('keeps numeric categories in axis order when the same field also has continuous statistics', () => { + chart = new VChart( + { + type: 'bar', + width: 500, + height: 500, + animation: false, + xField: 'value', + yField: 'value', + data: { id: 'data', values: [{ value: 3 }, { value: 1 }, { value: 2 }] }, + axes: [ + { orient: 'bottom', type: 'band' }, + { orient: 'left', type: 'linear' } + ], + dataZoom: [{ orient: 'bottom', filterMode: 'axis' }] + }, + { dom, animation: false } + ); + chart.renderSync(); + const model = zoom('bottom'); + const previewValues = () => + model + .getData() + .getLatestData() + .map((d: { x: number }) => d.x); + expect(previewValues()).toEqual([3, 1, 2]); + model.setStartAndEnd(0, 0.5); + expect(model.stateScale.domain()).toEqual([3, 1, 2]); + model.setStartAndEnd(0, 1); + chart.updateDataSync('data', [{ value: 6 }, { value: 4 }, { value: 5 }]); + expect(previewValues()).toEqual([6, 4, 5]); + expect(model.stateScale.domain()).toEqual([6, 4, 5]); + }); + + it('does not collect discrete values for a continuous DataZoom axis on data updates', () => { + chart = new VChart( + { + type: 'scatter', + width: 500, + height: 500, + animation: false, + xField: 'x', + yField: 'y', + data: { id: 'data', values: [{ x: 1, y: 1 }] }, + axes: [ + { orient: 'bottom', type: 'linear' }, + { orient: 'left', type: 'linear' } + ], + dataZoom: [{ orient: 'bottom' }] + }, + { dom, animation: false } + ); + chart.renderSync(); + const series = chart.getChart().getAllSeries()[0]; + const statistics = jest.spyOn(series, 'getRawDataStatisticsByField'); + chart.updateDataSync( + 'data', + Array.from({ length: 10000 }, (_, x) => ({ x, y: x % 100 })) + ); + const xCalls = statistics.mock.calls.filter(([field]) => String(field) === 'x'); + expect(xCalls.length).toBeGreaterThan(0); + expect(xCalls.every(([, isNumeric]) => isNumeric === true)).toBe(true); + expect(series.getRawDataStatisticsByField('x', true).values).toBeUndefined(); + expect(zoom('bottom').stateScale.domain()).toEqual([0, 9999]); + statistics.mockRestore(); + }); }); diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-numeric-category.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-numeric-category.mjs new file mode 100644 index 0000000000..01db737a93 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-numeric-category.mjs @@ -0,0 +1,68 @@ +import { verifyRendered } from '../../../helpers.mjs'; + +/** + * 验证目的:数字分类轴的预览与缩放范围保持分类顺序,不受同字段连续统计影响。 + * 图表类型:bar。 + * 关键配置:xField 与 yField 使用同一字段,横轴 band、纵轴 linear、filterMode: axis。 + * 场景条件:数字分类按非升序输入,更新数据后通过 API 缩小窗口。 + * 最终检查:更新后的分类域、预览顺序、轴范围因子与实际可见分类;截图检查预览和柱形布局。 + * 覆盖边界:不覆盖鼠标拖动、联动轴和连续轴统计开销,另有交互用例和单元测试。 + */ +export default { + createSpec() { + // 使用同一字段触发分类值与 min/max 共存的合法统计场景。 + return { + type: 'bar', + animation: false, + xField: 'value', + yField: 'value', + data: { id: 'data', values: [{ value: 3 }, { value: 1 }, { value: 2 }] }, + axes: [ + { orient: 'bottom', type: 'band' }, + { orient: 'left', type: 'linear' } + ], + dataZoom: [{ orient: 'bottom', filterMode: 'axis' }] + }; + }, + async exercise(page) { + // 更新真实数据并缩放,最终断言分别检测两项动作是否生效。 + await page.evaluate(() => { + const chart = window.__visualChart; + chart.updateDataSync('data', [{ value: 6 }, { value: 4 }, { value: 5 }]); + chart + .getChart() + .getAllComponents() + .find(c => c.type === 'dataZoom') + .setStartAndEnd(0, 0.5); + }); + }, + async verify(page) { + await verifyRendered(page, 'series'); + await page.evaluate(() => { + const chart = window.__visualChart; + const zoom = chart + .getChart() + .getAllComponents() + .find(c => c.type === 'dataZoom'); + const expected = JSON.stringify([6, 4, 5]); + if (JSON.stringify(zoom.stateScale.domain()) !== expected) throw new Error('更新后的缩放分类域顺序错误'); + if ( + JSON.stringify( + zoom + .getData() + .getLatestData() + .map(d => d.x) + ) !== expected + ) + throw new Error('数字分类轴的预览被按数值重新排序'); + const scale = zoom.relatedAxisComponent.getScale(); + if (JSON.stringify(scale.rangeFactor()) !== JSON.stringify([0, 0.5])) throw new Error('缩放窗口没有更新'); + const range = scale.range(); + const visible = scale.domain().filter(value => { + const position = scale.scale(value); + return position >= Math.min(...range) && position <= Math.max(...range); + }); + if (JSON.stringify(visible) !== JSON.stringify([6, 4])) throw new Error('缩放显示了错误的分类'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/index.mjs b/packages/vchart/__tests__/visual/cases/index.mjs index 86f11ad71c..097e2d491a 100644 --- a/packages/vchart/__tests__/visual/cases/index.mjs +++ b/packages/vchart/__tests__/visual/cases/index.mjs @@ -379,6 +379,11 @@ export const cases = [ purpose: 'dataZoom 预览图的初始范围和布局', file: './components/datazoom/datazoom-preview.mjs' }, + { + id: 'datazoom-numeric-category', + purpose: '数字分类轴在更新和缩放后保持预览及分类顺序', + file: './components/datazoom/datazoom-numeric-category.mjs' + }, { id: 'waterfall-leader-line', purpose: '反向类目轴瀑布图的连接线与变化值堆叠标签', diff --git a/packages/vchart/src/component/data-zoom/util.ts b/packages/vchart/src/component/data-zoom/util.ts index 88155102c9..81bc27f97d 100644 --- a/packages/vchart/src/component/data-zoom/util.ts +++ b/packages/vchart/src/component/data-zoom/util.ts @@ -120,7 +120,8 @@ export const dataFilterComputeDomain = (data: Array, op: IDataFilterCompute return; } const series = seriesCollection[i]; - if (series) { + // 关联轴已确定分类或连续类型;仅无关联轴时通过系列统计推断,避免额外统计和分类重排。 + if (series && isCategoryState === undefined) { const statistics = series.getRawDataStatisticsByField(stateFields[i]); if (isValid(statistics?.max) && isValid(statistics?.min)) { isAllLinearValue = true; From 03e4bba7f375fd5e48039cc66c2f43576a7e9ff5 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 17:01:57 +0800 Subject: [PATCH 07/16] fix: stabilize sequence curves and honor link configuration --- .../browser/test-page/sequence-analysis.ts | 8 +- .../src/charts/sequence-analysis/interface.ts | 3 +- .../sequence-analysis-transformer.ts | 9 +- .../__tests__/unit/series/link-curve.test.ts | 148 ++++++++++++++++++ .../series/sequence-analysis-review.test.ts | 20 +++ .../cases/api/update/sequence-curve-link.mjs | 78 +++++++++ .../vchart/__tests__/visual/cases/index.mjs | 5 + packages/vchart/src/constant/data.ts | 3 + .../src/data/transforms/link-dot-info.ts | 16 ++ packages/vchart/src/series/link/constant.ts | 3 +- packages/vchart/src/series/link/link.ts | 29 ++-- 11 files changed, 295 insertions(+), 27 deletions(-) create mode 100644 packages/vchart/__tests__/unit/series/link-curve.test.ts create mode 100644 packages/vchart/__tests__/visual/cases/api/update/sequence-curve-link.mjs diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts index 08101f121e..d320c507dc 100644 --- a/packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/sequence-analysis.ts @@ -988,7 +988,7 @@ const userImageMap: UserImageMap = { user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' }; -const stepMediumMap: Record = { +const stepMediumMap: ISequenceAnalysisSpec['stepMediumMap'] = { step_1: { medium_type: 'IP', count: 0 @@ -1500,11 +1500,11 @@ const spec1 = { type: 'rich', text: [ { - text: `${(stepMediumMap[label] as any).medium_type}... `, + text: `${stepMediumMap[label].medium_type}... `, fontSize: 12 }, { - text: `+${(stepMediumMap[label] as any).count}`, + text: `+${stepMediumMap[label].count}`, fontSize: 12, background: 'grey', backgroundOpacity: 0.4, @@ -1641,7 +1641,7 @@ const spec2 = { // fontSize: 12 // }, // { - // text: ` +${(stepMediumMap[label] as any).count}`, + // text: ` +${stepMediumMap[label].count}`, // fontSize: 12, // background: 'grey', // backgroundOpacity: 0.4, diff --git a/packages/vchart-extension/src/charts/sequence-analysis/interface.ts b/packages/vchart-extension/src/charts/sequence-analysis/interface.ts index e3dbbc0158..4126b9d3e2 100644 --- a/packages/vchart-extension/src/charts/sequence-analysis/interface.ts +++ b/packages/vchart-extension/src/charts/sequence-analysis/interface.ts @@ -97,7 +97,8 @@ export interface ISequenceAnalysisSpec { * 用户头像数据(从eventData中抽取也可以, 但不建议在前端遍历数据) */ userImageMap: UserImageMap; - stepMediumMap: Record; + /** 步骤对应的介质名称和附加数量,用于非介质连接模式的轴标签。 */ + stepMediumMap: Record; /** * @param spec 经过rankingList转化后的vchart原始spec * @description 获取vchart原始spec, 并根据业务自行添加转换逻辑 diff --git a/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts index 927bb425e2..74d3d275cd 100644 --- a/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts +++ b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts @@ -1,5 +1,5 @@ import { CommonChartSpecTransformer } from '@visactor/vchart'; -import type { EventData } from './interface'; +import type { EventData, ISequenceAnalysisSpec } from './interface'; export class SequenceAnalysisChartSpecTransformer extends CommonChartSpecTransformer { transformSpec(spec: any): void { @@ -28,7 +28,8 @@ export class SequenceAnalysisChartSpecTransformer extends CommonChartSpecTransfo // delete spec.patternData; // 系列配置 - const { mode, mediumConnection = false, actionTarget = false, stepMediumMap } = spec; + const { mode, mediumConnection = false, actionTarget = false } = spec; + const stepMediumMap: ISequenceAnalysisSpec['stepMediumMap'] = spec.stepMediumMap; spec.series = [ { type: 'link', @@ -144,11 +145,11 @@ export class SequenceAnalysisChartSpecTransformer extends CommonChartSpecTransfo type: 'rich', text: [ { - text: `${(stepMediumMap[label] as any).medium_type}... `, + text: `${stepMediumMap[label].medium_type}... `, fontSize: 12 }, { - text: `+${(stepMediumMap[label] as any).count}`, + text: `+${stepMediumMap[label].count}`, fontSize: 12, background: 'grey', backgroundOpacity: 0.4, diff --git a/packages/vchart/__tests__/unit/series/link-curve.test.ts b/packages/vchart/__tests__/unit/series/link-curve.test.ts new file mode 100644 index 0000000000..30953c06a9 --- /dev/null +++ b/packages/vchart/__tests__/unit/series/link-curve.test.ts @@ -0,0 +1,148 @@ +import VChart, { type ICommonChartSpec, type ILinkSeriesSpec } from '../../../src'; +import { LinkType } from '../../../src/series/link/interface'; +import { createDiv, removeDom } from '../../util/dom'; + +const makeSpec = (link?: ILinkSeriesSpec['link']): ICommonChartSpec => { + const dot = { + type: 'dot' as const, + name: 'events', + dataId: 'events', + xField: 'time', + yField: 'user', + titleField: 'user', + leftAppendPadding: 0, + title: { visible: false }, + subTitle: { visible: false }, + symbol: { visible: false } + }; + return { + type: 'common', + width: 600, + height: 400, + data: [ + { + id: 'events', + values: [ + { user: 'U1', dots: [{ node_name: 'a', time: 10 }] }, + { user: 'U2', dots: [{ node_name: 'b', time: 30 }] } + ] + }, + { + id: 'links', + values: [ + { from: 'a', to: 'b' }, + { from: 'a', to: 'b' } + ] + } + ], + series: [ + { + type: 'link', + dataId: 'links', + dotSeriesIndex: 1, + dotSeriesSpec: dot, + fromField: 'from', + toField: 'to', + linkType: LinkType.curve, + leftAppendPadding: 0, + link + }, + dot + ], + axes: [ + { type: 'linear', orient: 'bottom', min: 0, max: 80, nice: false }, + { type: 'band', orient: 'left', domain: ['U1', 'U2'] } + ] + }; +}; + +describe('curve link rendering', () => { + let dom: HTMLElement; + let chart: VChart; + beforeEach(() => { + dom = createDiv(); + }); + afterEach(() => { + chart?.release(); + removeDom(dom); + }); + const series = () => + chart + .getChart() + .getAllSeries() + .find(s => s.type === 'link'); + const mark = () => + series() + .getMarksWithoutRoot() + .find(m => m.type === 'path'); + const paths = () => + series() + .getViewData() + .latestData.map((d: Record) => mark().getAttribute('path', d)); + + it('keeps duplicate curves distinct and stable through reevaluation, resize and data updates', () => { + chart = new VChart(makeSpec(), { dom, animation: false }); + chart.renderSync(); + const initial = paths(); + expect(initial).toHaveLength(2); + expect(initial[0]).not.toBe(initial[1]); + expect(paths()).toEqual(initial); + chart.resize(800, 500); + const resized = paths(); + expect(resized).not.toEqual(initial); + expect(paths()).toEqual(resized); + chart.resize(600, 400); + expect(paths()).toEqual(initial); + chart.updateDataSync('links', [{ from: 'a', to: 'b' }]); + expect(paths()).toEqual([initial[0]]); + chart.updateFullDataSync([ + { + id: 'events', + values: [ + { user: 'U1', dots: [{ node_name: 'a', time: 40 }] }, + { user: 'U2', dots: [{ node_name: 'b', time: 60 }] } + ] + }, + { + id: 'links', + values: [ + { from: 'a', to: 'b' }, + { from: 'a', to: 'b' } + ] + } + ]); + const updated = paths(); + expect(updated).not.toEqual(initial); + expect(updated[0]).not.toBe(updated[1]); + expect(paths()).toEqual(updated); + chart.updateFullDataSync(makeSpec().data as any); + expect(paths()).toEqual(initial); + }); + + it('applies public link visibility, styles and states on initialization and updateSpec', () => { + const config = { + visible: false, + style: { stroke: '#123456', lineWidth: 3 }, + state: { custom_unSelected: { strokeOpacity: 0.25 } } + }; + chart = new VChart(makeSpec(config), { dom, animation: false }); + chart.renderSync(); + const datum = series().getViewData().latestData[0]; + expect(mark().getVisible()).toBe(false); + expect(mark().name).toBe('link'); + expect(mark().getAttribute('stroke', datum)).toBe('#123456'); + expect(mark().getAttribute('strokeOpacity', datum, 'custom_unSelected')).toBe(0.25); + chart.updateSpecSync( + makeSpec({ + visible: true, + style: { stroke: '#654321', lineWidth: 4 }, + state: { custom_unSelected: { strokeOpacity: 0.75 } } + }) + ); + expect(mark().getVisible()).toBe(true); + expect(mark().getAttribute('stroke', datum)).toBe('#654321'); + expect(mark().getAttribute('lineWidth', datum)).toBe(4); + expect(mark().getAttribute('strokeOpacity', datum, 'custom_unSelected')).toBe(0.75); + expect(mark().getGraphics()).toHaveLength(2); + }); +}); diff --git a/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts b/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts index 3ea1c54ab3..35e1fdca82 100644 --- a/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts +++ b/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts @@ -8,6 +8,9 @@ import { linkDotInfo } from '../../../src/data/transforms/link-dot-info'; // eslint-disable-next-line max-len import { SequenceAnalysisChartSpecTransformer } from '../../../../vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer'; +// eslint-disable-next-line max-len +import type { ISequenceAnalysisSpec } from '../../../../vchart-extension/src/charts/sequence-analysis/interface'; + const events = (time: number) => [ { user_id: 'U1', @@ -102,4 +105,21 @@ describe('sequence analysis review regressions', () => { Object.assign(factoryRegistry.graphicComponents, graphics); } }); + + it('accepts typed step medium objects and formats a zero count', () => { + const stepMediumMap: ISequenceAnalysisSpec['stepMediumMap'] = { + step_1: { medium_type: 'IP', count: 0 } + }; + const spec: any = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + eventData: [], + userImageMap: {}, + stepMediumMap + }; + new SequenceAnalysisChartSpecTransformer({ type: 'sequenceAnalysis' }).transformSpec(spec); + const label = spec.axes[0].label.formatMethod('step_1'); + expect(label.text.map((part: { text: string }) => part.text)).toEqual(['IP... ', '+0']); + }); }); diff --git a/packages/vchart/__tests__/visual/cases/api/update/sequence-curve-link.mjs b/packages/vchart/__tests__/visual/cases/api/update/sequence-curve-link.mjs new file mode 100644 index 0000000000..fe80745d9e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update/sequence-curve-link.mjs @@ -0,0 +1,78 @@ +import lineCase from './sequence-link-data.mjs'; + +/** + * 验证目的:曲线连接的公开 link 配置生效,重复端点的曲率在重绘和数据更新后保持确定。 + * 图表类型:common(dot + link)。 + * 关键配置:linkType: curve、link.visible/style/state、重复 from/to。 + * 场景条件:初始隐藏曲线,通过 updateSpec 显示,再更新事件数据。 + * 最终检查:真实图元显隐、样式、状态,更新后端点与两条不同且稳定的曲线路径。 + * 覆盖边界:不覆盖扩展图表时间轴派生组件;旧 develop 不支持曲线,基线语义校验会明确失败。 + */ +export default { + createSpec() { + // 复用逐用户事件数据,增加两条相同端点的连接。 + const spec = lineCase.createSpec(); + spec.series[0].linkType = 'curve'; + spec.series[0].link = { + visible: false, + style: { stroke: '#5470c6', lineWidth: 2 }, + state: { custom_unSelected: { strokeOpacity: 0.25 } } + }; + spec.data[1].values = [ + { from: 'a', to: 'c' }, + { from: 'a', to: 'c' } + ]; + return spec; + }, + async exercise(page) { + // 先检查初始化显隐,再通过公开 API 更新配置和数据。 + await page.evaluate(() => { + const series = window.__visualChart + .getChart() + .getAllSeries() + .find(s => s.type === 'link'); + const mark = series.getMarksWithoutRoot().find(m => m.type === 'path' || m.name === 'link'); + if (mark.getVisible() !== false) throw new Error('初始化未应用 link.visible'); + }); + const spec = this.createSpec(); + spec.series[0].link = { + visible: true, + style: { stroke: '#d65f00', lineWidth: 3 }, + state: { custom_unSelected: { strokeOpacity: 0.75 } } + }; + await page.evaluate(spec => window.__visualChart.updateSpecSync(spec), spec); + await lineCase.exercise(page); + await page.evaluate(() => { + window.__visualChart.updateDataSync('links', [ + { from: 'a', to: 'c' }, + { from: 'a', to: 'c' } + ]); + }); + }, + async verify(page) { + // 核对最终真实数据和图元,既检测动作缺失,也检测属性回调的副作用。 + await page.evaluate(() => { + const series = window.__visualChart + .getChart() + .getAllSeries() + .find(s => s.type === 'link'); + const mark = series.getMarkInName('link'); + if (!mark || mark.type !== 'path') throw new Error('未通过公开 link 配置创建曲线图元'); + if (!mark.getVisible()) throw new Error('updateSpec 后曲线仍不可见'); + const data = series.getViewData().latestData; + if (data.length !== 2 || data.some(d => d.from_xField !== 40 || d.to_xField !== 60)) + throw new Error('连接数量或更新后的端点错误'); + const paths = data.map(d => mark.getAttribute('path', d)); + if (paths[0] === paths[1] || paths.some(p => typeof p !== 'string' || /NaN|undefined/.test(p))) + throw new Error('重复连接的路径无效或未分离'); + if (data.some((d, i) => mark.getAttribute('path', d) !== paths[i])) throw new Error('重复求值改变了曲率'); + if (mark.getAttribute('stroke', data[0]) !== '#d65f00' || mark.getAttribute('lineWidth', data[0]) !== 3) + throw new Error('updateSpec 未应用 link 样式'); + if (mark.getAttribute('strokeOpacity', data[0], 'custom_unSelected') !== 0.75) + throw new Error('updateSpec 未应用 link 状态'); + const graphics = mark.getGraphics(); + if (graphics.length !== 2 || graphics.some((g, i) => g.attribute.path !== paths[i])) + throw new Error('实际绘制路径与数据求值不一致'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/index.mjs b/packages/vchart/__tests__/visual/cases/index.mjs index 097e2d491a..010ca9614a 100644 --- a/packages/vchart/__tests__/visual/cases/index.mjs +++ b/packages/vchart/__tests__/visual/cases/index.mjs @@ -9,6 +9,11 @@ export const cases = [ purpose: 'dot 嵌套原始数据与更新后的连接端点', file: './api/update/sequence-link-data.mjs' }, + { + id: 'sequence-curve-link', + purpose: '曲线连接在配置与数据更新后保持路径稳定并应用公开 link 配置', + file: './api/update/sequence-curve-link.mjs' + }, { id: 'bar-stack', purpose: '正负值、堆叠与零基准线(bar)', diff --git a/packages/vchart/src/constant/data.ts b/packages/vchart/src/constant/data.ts index f77b61fbc7..98431b380a 100644 --- a/packages/vchart/src/constant/data.ts +++ b/packages/vchart/src/constant/data.ts @@ -4,6 +4,9 @@ export const DEFAULT_MEASURE_CANVAS_ID = `${PREFIX}_MEASURE_CANVAS_ID`; export const DEFAULT_DATA_INDEX = `${PREFIX}_DEFAULT_DATA_INDEX`; +// 当前数据集中相同端点连接的曲线序号,由 linkDotInfo 转换生成。 +export const DEFAULT_LINK_CURVE_INDEX = `${PREFIX}_LINK_CURVE_INDEX`; + export const DEFAULT_DATA_KEY = `${PREFIX}_DEFAULT_DATA_KEY`; export const DEFAULT_DATA_SERIES_FIELD = `${PREFIX}_DEFAULT_DATA_SERIES_FIELD`; diff --git a/packages/vchart/src/data/transforms/link-dot-info.ts b/packages/vchart/src/data/transforms/link-dot-info.ts index 0263e1dc71..7ed358642a 100644 --- a/packages/vchart/src/data/transforms/link-dot-info.ts +++ b/packages/vchart/src/data/transforms/link-dot-info.ts @@ -1,7 +1,9 @@ +import { DEFAULT_LINK_CURVE_INDEX } from '../../constant/data'; import type { Datum } from '../../typings'; export interface ILinkDotInfoOpt { infoKey: string; + isCurve?: () => boolean; fields: () => { fromField: string; toField: string; @@ -36,12 +38,26 @@ export const linkDotInfo = (data: Array, op: ILinkDotInfoOpt) => { }); }); + // 仅在曲线数据转换时分配序号,每次重算独立计数,图元属性求值不再修改状态。 + const curveCounts = op.isCurve?.() ? new Map() : undefined; + // 将起始点的node data加入进来 dataLinkObj.forEach((datum: any) => { datum[fromField + '_xField'] = dataLinkDotHash?.[datum[fromField]]?.[xField]; datum[fromField + '_yField'] = dataLinkDotHash?.[datum[fromField]]?.[yField]; datum[toField + '_xField'] = dataLinkDotHash?.[datum[toField]]?.[xField]; datum[toField + '_yField'] = dataLinkDotHash?.[datum[toField]]?.[yField]; + if (curveCounts) { + const key = JSON.stringify([ + datum[fromField + '_xField'], + datum[fromField + '_yField'], + datum[toField + '_xField'], + datum[toField + '_yField'] + ]); + const index = (curveCounts.get(key) ?? 0) + 1; + curveCounts.set(key, index); + datum[DEFAULT_LINK_CURVE_INDEX] = index; + } }); return dataLinkObj; }; diff --git a/packages/vchart/src/series/link/constant.ts b/packages/vchart/src/series/link/constant.ts index 5231a7ed18..749d9bee3d 100644 --- a/packages/vchart/src/series/link/constant.ts +++ b/packages/vchart/src/series/link/constant.ts @@ -6,8 +6,7 @@ import { SeriesMarkNameEnum } from '../interface/type'; export const linkSeriesMark: SeriesMarkMap = { ...baseSeriesMark, [SeriesMarkNameEnum.group]: { name: SeriesMarkNameEnum.group, type: MarkTypeEnum.group }, - [SeriesMarkNameEnum.link]: { name: SeriesMarkNameEnum.link, type: MarkTypeEnum.rule }, - [SeriesMarkNameEnum.curveLink]: { name: SeriesMarkNameEnum.curveLink, type: MarkTypeEnum.path }, + [SeriesMarkNameEnum.link]: { name: SeriesMarkNameEnum.link, type: [MarkTypeEnum.rule, MarkTypeEnum.path] }, [SeriesMarkNameEnum.arrow]: { name: SeriesMarkNameEnum.arrow, type: MarkTypeEnum.symbol }, [SeriesMarkNameEnum.imageLabel]: { name: SeriesMarkNameEnum.imageLabel, type: MarkTypeEnum.image } }; diff --git a/packages/vchart/src/series/link/link.ts b/packages/vchart/src/series/link/link.ts index dbf9e331bb..f50159587b 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -1,9 +1,9 @@ /* eslint-disable no-duplicate-imports */ -import { DEFAULT_DATA_SERIES_FIELD } from '../../constant/data'; +import { DEFAULT_DATA_SERIES_FIELD, DEFAULT_LINK_CURVE_INDEX } from '../../constant/data'; import { CartesianSeries } from '../cartesian/cartesian'; import type { Datum } from '../../typings'; import { array, isValid } from '@visactor/vutils'; -import type { IGroupMark, IImageMark, IMark, IRuleMark, ISymbolMark } from '../../mark/interface'; +import type { IGroupMark, IImageMark, IMark, IPathMark, IRuleMark, ISymbolMark } from '../../mark/interface'; import { SeriesTypeEnum } from '../interface/type'; import { registerDataSetInstanceTransform } from '../../data/register'; import type { IDotSeriesSpec } from '../dot/interface'; @@ -14,6 +14,7 @@ import { registerRuleMark } from '../../mark/rule'; import { registerSymbolMark } from '../../mark/symbol'; import { registerPathMark } from '../../mark/path'; import { registerImageMark } from '../../mark/image'; +import { MarkTypeEnum } from '../../mark/interface/type'; import { linkSeriesMark } from './constant'; import { linkDotInfo } from '../../data/transforms/link-dot-info'; import { Factory } from '../../core/factory'; @@ -29,9 +30,6 @@ export class LinkSeries extends Car static readonly mark: SeriesMarkMap = linkSeriesMark; static readonly builtInTheme = { link }; - // 记录起点和终点相同的曲线数量, 便于给不同曲线设置不同曲率 - private _curveLinkCountMap: Record = {}; - protected _fromField?: string; getFromField() { return this._fromField; @@ -89,6 +87,7 @@ export class LinkSeries extends Car type: 'linkDotInfo', options: { infoKey: 'dots', + isCurve: () => this._spec.linkType === LinkType.curve, fields: () => { return { fromField: this._fromField, @@ -119,7 +118,7 @@ export class LinkSeries extends Car private _clipMark: IGroupMark; private _containerMark: IGroupMark; - private _linkMark: IRuleMark; + private _linkMark: IRuleMark | IPathMark; private _arrowMark: ISymbolMark; private _imageLabelMark: IImageMark; initMark(): void { @@ -130,10 +129,10 @@ export class LinkSeries extends Car parent: this._clipMark }) as IGroupMark; - this._linkMark = this._createMark(linkType === LinkType.curve ? LinkSeries.mark.curveLink : LinkSeries.mark.link, { - skipBeforeLayouted: false, - parent: this._containerMark - }) as IRuleMark; + this._linkMark = this._createMark( + { ...LinkSeries.mark.link, type: linkType === LinkType.curve ? MarkTypeEnum.path : MarkTypeEnum.rule }, + { skipBeforeLayouted: false, parent: this._containerMark } + ) as IRuleMark | IPathMark; this._arrowMark = this._createMark(LinkSeries.mark.arrow, { skipBeforeLayouted: false, @@ -189,7 +188,7 @@ export class LinkSeries extends Car this.setMarkStyle( linkMark, { - stroke: this._spec.link?.style?.stroke ?? this.getColorAttribute(), + stroke: this.getColorAttribute(), strokeOpacity: this.dataToOpacity.bind(this), path: (datum: Datum) => { const fromX = this.dataToPositionXFrom(datum); @@ -197,10 +196,8 @@ export class LinkSeries extends Car const toX = this.dataToPositionXTo(datum); const toY = this.dataToPositionYTo(datum); - // 计算曲率 - const curveLinkCount = this._curveLinkCountMap[`${fromX}-${fromY}-${toX}-${toY}`] || 1; - this._curveLinkCountMap[`${fromX}-${fromY}-${toX}-${toY}`] = curveLinkCount + 1; - const curveRatio = 0.5 + curveLinkCount * 0.1; + // 重复连接的序号在数据转换阶段计算,重复求值和缩放不会改变曲率。 + const curveRatio = 0.5 + datum[DEFAULT_LINK_CURVE_INDEX] * 0.1; // 计算控制点 const controlX1 = fromX + (toX - fromX) * 0.7; @@ -211,7 +208,7 @@ export class LinkSeries extends Car // 生成三次贝塞尔曲线路径 return `M ${fromX} ${fromY} C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${toX} ${toY}`; }, - lineWidth: this._spec.link.style?.lineWidth ?? 1 + lineWidth: 1 }, 'normal', AttributeLevel.Series From 871c1a3ee80ede5eaffbccc7db6afdf233afec35 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 17:31:35 +0800 Subject: [PATCH 08/16] docs: add sequence analysis examples options and tutorial --- .../sequence-analysis-medium.md | 133 +++++++++++++++ .../extension-chart/sequence-analysis-step.md | 133 +++++++++++++++ .../sequence-analysis-target.md | 133 +++++++++++++++ .../extension-chart/sequence-analysis-time.md | 133 +++++++++++++++ docs/assets/examples/menu.json | 28 +++ .../sequence-analysis-medium.md | 133 +++++++++++++++ .../extension-chart/sequence-analysis-step.md | 133 +++++++++++++++ .../sequence-analysis-target.md | 133 +++++++++++++++ .../extension-chart/sequence-analysis-time.md | 133 +++++++++++++++ .../Chart_Extensions/sequence_analysis.md | 158 +++++++++++++++++ docs/assets/guide/menu.json | 7 + .../Chart_Extensions/sequence_analysis.md | 158 +++++++++++++++++ .../option/en/chart/sequence-analysis.md | 159 ++++++++++++++++++ .../data-filter-base-component.md | 11 +- docs/assets/option/en/option.md | 2 + docs/assets/option/en/series/link.md | 34 +++- .../option/zh/chart/sequence-analysis.md | 159 ++++++++++++++++++ .../data-filter-base-component.md | 18 +- docs/assets/option/zh/option.md | 2 + docs/assets/option/zh/series/link.md | 43 ++++- .../preview/sequence-analysis-medium.png | Bin 0 -> 14706 bytes .../vchart/preview/sequence-analysis-step.png | Bin 0 -> 14942 bytes .../preview/sequence-analysis-target.png | Bin 0 -> 20009 bytes .../vchart/preview/sequence-analysis-time.png | Bin 0 -> 18145 bytes 24 files changed, 1827 insertions(+), 16 deletions(-) create mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-medium.md create mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-step.md create mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-target.md create mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-time.md create mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md create mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-step.md create mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-target.md create mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-time.md create mode 100644 docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md create mode 100644 docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md create mode 100644 docs/assets/option/en/chart/sequence-analysis.md create mode 100644 docs/assets/option/zh/chart/sequence-analysis.md create mode 100644 docs/public/vchart/preview/sequence-analysis-medium.png create mode 100644 docs/public/vchart/preview/sequence-analysis-step.png create mode 100644 docs/public/vchart/preview/sequence-analysis-target.png create mode 100644 docs/public/vchart/preview/sequence-analysis-time.png diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md new file mode 100644 index 0000000000..0c44ba70be --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: 'Sequence Analysis: Medium Connections' +keywords: extension, sequenceAnalysis, step +order: 17 +cover: /vchart/preview/sequence-analysis-medium.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis: Medium Connections + +Use medium_type as the horizontal category and draw pattern connections as curves. Two links share the same endpoints to demonstrate distinct, stable curvature. + +## Key Configurations + +- `mode: 'step'`. +- `mediumConnection: true`. +- `from` / `to` in `patternData` reference globally unique `dots.node_name` values. +- Data and avatars are embedded; no external requests are required. + +## Code Demo + +```javascript livedemo +// Use matching VChart and extension versions that include sequenceAnalysis. +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: true, + actionTarget: false, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // Configure the generated VChart spec and show the complete initial range. + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## Related Documentation + +[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md new file mode 100644 index 0000000000..4d59f92156 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: 'Sequence Analysis: Step Mode' +keywords: extension, sequenceAnalysis, step +order: 16 +cover: /vchart/preview/sequence-analysis-step.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis: Step Mode + +Arrange events by step rather than elapsed time. The top axis uses the medium name and additional count from stepMediumMap; the bottom axis shows steps. + +## Key Configurations + +- `mode: 'step'`. +- `mediumConnection: false`. +- `from` / `to` in `patternData` reference globally unique `dots.node_name` values. +- Data and avatars are embedded; no external requests are required. + +## Code Demo + +```javascript livedemo +// Use matching VChart and extension versions that include sequenceAnalysis. +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + actionTarget: false, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // Configure the generated VChart spec and show the complete initial range. + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## Related Documentation + +[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md new file mode 100644 index 0000000000..834f7afac3 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: 'Sequence Analysis: Target Avatars' +keywords: extension, sequenceAnalysis, time +order: 15 +cover: /vchart/preview/sequence-analysis-target.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis: Target Avatars + +Show an avatar at the midpoint of each time-mode connection and an arrow at its target. Avatars come from actionData.user_image; user_id does not resolve an image automatically. + +## Key Configurations + +- `mode: 'time'`. +- `actionTarget: true`. +- `from` / `to` in `actionData` reference globally unique `dots.node_name` values. +- Data and avatars are embedded; no external requests are required. + +## Code Demo + +```javascript livedemo +// Use matching VChart and extension versions that include sequenceAnalysis. +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: false, + actionTarget: true, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // Configure the generated VChart spec and show the complete initial range. + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## Related Documentation + +[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-time.md b/docs/assets/examples/en/extension-chart/sequence-analysis-time.md new file mode 100644 index 0000000000..6733cbe4ac --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-time.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: 'Sequence Analysis: Time Mode' +keywords: extension, sequenceAnalysis, time +order: 14 +cover: /vchart/preview/sequence-analysis-time.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis: Time Mode + +Plot two users on a time axis and connect related events. Connection lines remain visible when target avatars are disabled. + +## Key Configurations + +- `mode: 'time'`. +- `actionTarget: false`. +- `from` / `to` in `actionData` reference globally unique `dots.node_name` values. +- Data and avatars are embedded; no external requests are required. + +## Code Demo + +```javascript livedemo +// Use matching VChart and extension versions that include sequenceAnalysis. +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: false, + actionTarget: false, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // Configure the generated VChart spec and show the complete initial range. + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## Related Documentation + +[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/menu.json b/docs/assets/examples/menu.json index d499c70d49..92b2bf30ce 100644 --- a/docs/assets/examples/menu.json +++ b/docs/assets/examples/menu.json @@ -2117,6 +2117,34 @@ "zh": "翼形叙事图", "en": "Wing Storyline" } + }, + { + "path": "sequence-analysis-time", + "title": { + "zh": "时序分析:时间模式", + "en": "Sequence Analysis: Time Mode" + } + }, + { + "path": "sequence-analysis-target", + "title": { + "zh": "时序分析:目标头像", + "en": "Sequence Analysis: Target Avatars" + } + }, + { + "path": "sequence-analysis-step", + "title": { + "zh": "时序分析:步骤模式", + "en": "Sequence Analysis: Step Mode" + } + }, + { + "path": "sequence-analysis-medium", + "title": { + "zh": "时序分析:介质曲线连接", + "en": "Sequence Analysis: Medium Connections" + } } ] }, diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md new file mode 100644 index 0000000000..c0661101eb --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: '时序分析:介质曲线连接' +keywords: extension, sequenceAnalysis, step +order: 17 +cover: /vchart/preview/sequence-analysis-medium.png +option: sequenceAnalysisChart +--- + +# 时序分析:介质曲线连接 + +以 medium_type 为横轴分类,通过曲线展示模式连接。示例保留两条相同端点的连接,用于观察不同且稳定的曲率。 + +## 关键配置 + +- `mode: 'step'`。 +- `mediumConnection: true`。 +- `patternData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 +- 数据与头像均内置,不依赖外部请求。 + +## 代码演示 + +```javascript livedemo +// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: true, + actionTarget: false, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## 相关文档 + +[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md new file mode 100644 index 0000000000..e803ce46ea --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: '时序分析:步骤模式' +keywords: extension, sequenceAnalysis, step +order: 16 +cover: /vchart/preview/sequence-analysis-step.png +option: sequenceAnalysisChart +--- + +# 时序分析:步骤模式 + +按步骤而非真实时间间隔排列事件。上方标签使用 stepMediumMap 的介质名称和附加数量,下方显示步骤名称。 + +## 关键配置 + +- `mode: 'step'`。 +- `mediumConnection: false`。 +- `patternData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 +- 数据与头像均内置,不依赖外部请求。 + +## 代码演示 + +```javascript livedemo +// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + actionTarget: false, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## 相关文档 + +[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md new file mode 100644 index 0000000000..cc3529a2e0 --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: '时序分析:目标头像' +keywords: extension, sequenceAnalysis, time +order: 15 +cover: /vchart/preview/sequence-analysis-target.png +option: sequenceAnalysisChart +--- + +# 时序分析:目标头像 + +在时间模式的连接中间显示用户头像,并显示目标端箭头。头像来自 actionData.user_image,而不是根据 user_id 自动查找。 + +## 关键配置 + +- `mode: 'time'`。 +- `actionTarget: true`。 +- `actionData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 +- 数据与头像均内置,不依赖外部请求。 + +## 代码演示 + +```javascript livedemo +// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: false, + actionTarget: true, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## 相关文档 + +[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-time.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-time.md new file mode 100644 index 0000000000..37a1b34a07 --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-time.md @@ -0,0 +1,133 @@ +--- +category: examples +group: extension chart +title: '时序分析:时间模式' +keywords: extension, sequenceAnalysis, time +order: 14 +cover: /vchart/preview/sequence-analysis-time.png +option: sequenceAnalysisChart +--- + +# 时序分析:时间模式 + +按事件发生时间展示两个用户的行为,用直线连接相关事件。关闭目标头像时,连接线仍然显示。 + +## 关键配置 + +- `mode: 'time'`。 +- `actionTarget: false`。 +- `actionData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 +- 数据与头像均内置,不依赖外部请求。 + +## 代码演示 + +```javascript livedemo +// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 +// import VChart from '@visactor/vchart'; +// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const avatar = (text, fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + `${text}` + ); +const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; +const day = 24 * 60 * 60 * 1000; +const start = Date.UTC(2026, 0, 1); +const eventData = [ + { + user_id: 'Alice', + user_image: userImageMap.Alice, + dots: [ + { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, + { + node_name: 'a2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 2 * day, + action_count: 4 + }, + { + node_name: 'a3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 4 * day, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: userImageMap.Bob, + dots: [ + { + node_name: 'b1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: start + day, + action_count: 8 + }, + { + node_name: 'b2', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: start + 3 * day, + action_count: 3 + }, + { + node_name: 'b3', + step: 'step_3', + medium_type: 'Account', + action_type: 'order', + time_stamp: start + 5 * day, + action_count: 1 + } + ] + } +]; +const stepMediumMap = { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 }, + step_3: { medium_type: 'Account', count: 2 } +}; + +const spec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: false, + actionTarget: false, + height: 440, + eventData, + actionData: [ + { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, + { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } + ], + patternData: [ + { from: 'a1', to: 'b3', pattern_type: 'shared' }, + { from: 'a1', to: 'b3', pattern_type: 'shared' } + ], + userImageMap, + stepMediumMap, + customTransformSpec(converted) { + // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: CONTAINER_ID }); +vchart.renderSync(); +window.vchart = vchart; +``` + +## 相关文档 + +[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md new file mode 100644 index 0000000000..c5b3830730 --- /dev/null +++ b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -0,0 +1,158 @@ +# Extension Chart: Sequence Analysis + +Sequence Analysis places each user's events on a horizontal row, aligns events by time, step or medium, and draws their relationships. It supports behavior paths, relationships across users and repeated patterns. + +The chart comes from `@visactor/vchart-extension` with type `sequenceAnalysis`. It is distinct from the built-in sequence chart and the sequenceScatterLink extension for training-process analysis. + +![Medium connections](/vchart/preview/sequence-analysis-medium.png) + +## Choose a mode + +| Configuration | Horizontal axis | Connections | Appearance | +| --------------------------------------- | ---------------------- | ------------- | ------------------------------------------------------------------- | +| `mode: 'time', actionTarget: false` | Millisecond timestamps | `actionData` | Straight lines; the top axis shows total event counts per timestamp | +| `mode: 'time', actionTarget: true` | Millisecond timestamps | `actionData` | Lines, midpoint avatars and target arrows | +| `mode: 'step', mediumConnection: false` | `dots.step` | `patternData` | Straight lines; top labels show medium names and additional counts | +| `mode: 'step', mediumConnection: true` | `dots.medium_type` | `patternData` | Cubic Bézier curves; repeated endpoints receive distinct curvature | + +## Register and create a chart + +Install matching VChart and extension versions that include sequenceAnalysis, and register before creating the chart. The page needs a container with id="chart". + +This complete minimal step example replaces image labels with user names through the transform callback, so userImageMap can be empty. The demo pages instead use embedded SVG avatars. + +```js +import VChart from '@visactor/vchart'; +import { registerSequenceAnalysis } from '@visactor/vchart-extension'; + +registerSequenceAnalysis(); +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + height: 440, + eventData: [ + { + user_id: 'Alice', + user_image: '', + dots: [ + { + node_name: 'a1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: 1767225600000, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: '', + dots: [ + { + node_name: 'b1', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: 1767312000000, + action_count: 1 + } + ] + } + ], + patternData: [{ from: 'a1', to: 'b1', pattern_type: 'shared' }], + actionData: [{ from: 'a1', to: 'b1' }], + userImageMap: {}, + stepMediumMap: { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 } + }, + customTransformSpec(converted) { + converted.color = ['#5470c6', '#26a69a']; + converted.axes.find(axis => axis.orient === 'left').label.formatMethod = label => label; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: 'chart' }); +vchart.renderSync(); +``` + +For CDN usage, load the matching build/index.min.js files for both packages. Pass the actual chart constructor used by your page to the registration method: + +```js +VChartExtension.registerSequenceAnalysis({ VChart: VChart.default }); +const vchart = new VChart.default(spec, { dom: 'chart' }); +vchart.renderSync(); +``` + +## Prepare events and links + +Each eventData row contains user_id, user_image and nested dots. Do not flatten dots before passing data to the chart. Each event supplies node_name, step, medium_type, action_type, time_stamp and action_count. See the [options](/vchart/option/sequenceAnalysisChart) for details. + +- node_name must be globally unique across users. Every link's from and to must reference existing events. +- Time mode reads actionData; step mode reads patternData. Use empty arrays for no connections. +- Dot colors are grouped by action_type; pattern links are grouped by pattern_type. +- time_stamp is in milliseconds. The top time axis sums action_count across users at each timestamp, rather than counting rows. +- stepMediumMap values are `{ medium_type, count }` objects. Their count is an application-supplied additional quantity, independent of time-axis totals. Zero is displayed. Cover all steps in ordinary step mode; the other axis modes may use `{}`. + +## User and target avatars + +Default vertical labels read userImageMap[user_id]. The image stored in an event row does not populate this map automatically. + +With actionTarget enabled in time mode, the midpoint avatar reads actionData.user_image. Supplying actionData.user_id alone does not resolve the image. Disabling actionTarget hides avatars and arrows while keeping lines visible. + +Use accessible image URLs or data URLs. The demos embed their images to avoid network dependencies. [Target avatar example](/vchart/demo/extension-chart/sequence-analysis-target). + +## Customize appearance and zoom + +The transformer generates dot/link series, axes, a bottom DataZoom and horizontal/vertical ScrollBars. When two horizontal axes exist, the bottom scrollbar synchronizes their window proportions. Scrollbars initially show `[0, 0.8]`. Animation and tooltip are disabled by default. + +Mutate the generated common-chart configuration in customTransformSpec. A returned object does not replace it: + +```js +customTransformSpec(converted) { + const connection = converted.series.find(series => series.type === 'link'); + connection.link.style = { stroke: '#5470c6', lineWidth: 2 }; + connection.link.state = { custom_unSelected: { strokeOpacity: 0.25 } }; + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { bar.start = 0; bar.end = 1; }); +} +``` + +Straight and curved connections both use link visible/style/state options; no curveLink configuration is needed. The interface's colors field is not currently wired to the palette, so use the standard color option above. + +DataZoom and ScrollBar linkedAxisId / linkedAxisIndex synchronize window proportions only in axis filter mode. They do not copy axis domains. [Linked axis option](/vchart/option/commonChart-scrollBar-linkedAxisIndex). + +## Updates and current limitation + +Keep the original extension spec used to create the chart. The common-chart spec returned by getSpec() is not a substitute. + +Time-mode count labels and markLine configurations are currently generated during spec transformation. updateDataSync('eventData', ...) and updateFullDataSync(...) update events without rebuilding those configurations, so labels or reference lines can remain stale. + +Use a complete updateSpecSync instead. The following also switches the step example to time mode, regenerating count labels and reference lines: + +```js +const nextEvents = spec.eventData.map(user => ({ + ...user, + dots: user.dots.map(event => ({ + ...event, + time_stamp: event.time_stamp + 24 * 60 * 60 * 1000, + action_count: event.action_count + 1 + })) +})); +vchart.updateSpecSync({ ...spec, mode: 'time', eventData: nextEvents }); +``` + +When removing or renaming events, update actionData / patternData in the same complete spec. Also update userImageMap for user changes and stepMediumMap for step changes. + +## Examples and options + +- [Time mode](/vchart/demo/extension-chart/sequence-analysis-time) +- [Target avatars](/vchart/demo/extension-chart/sequence-analysis-target) +- [Step mode](/vchart/demo/extension-chart/sequence-analysis-step) +- [Medium connections](/vchart/demo/extension-chart/sequence-analysis-medium) +- [All options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/guide/menu.json b/docs/assets/guide/menu.json index b3f28bfcc8..b83252e415 100644 --- a/docs/assets/guide/menu.json +++ b/docs/assets/guide/menu.json @@ -961,6 +961,13 @@ "zh": "叙事图", "en": "Storyline" } + }, + { + "path": "sequence_analysis", + "title": { + "zh": "时序分析图", + "en": "Sequence Analysis" + } } ] } diff --git a/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md new file mode 100644 index 0000000000..8cdbe693de --- /dev/null +++ b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -0,0 +1,158 @@ +# 扩展图表:时序分析图 + +时序分析图(Sequence Analysis)把每个用户的事件放在同一条水平线上,通过时间、步骤或介质分类对齐事件,再用连接线表达事件之间的关系。适合展示行为路径、跨用户事件关联和重复模式。 + +它由 `@visactor/vchart-extension` 提供,类型为 `sequenceAnalysis`,与内置 `sequence` 和用于训练过程分析的 `sequenceScatterLink` 是不同图表。 + +![介质曲线连接](/vchart/preview/sequence-analysis-medium.png) + +## 选择展示模式 + +| 配置 | 横轴 | 连接数据 | 呈现 | +| --------------------------------------- | ------------------ | ------------- | ---------------------------------------- | +| `mode: 'time', actionTarget: false` | 毫秒时间戳 | `actionData` | 直线连接;顶部显示各时间点的事件数量总和 | +| `mode: 'time', actionTarget: true` | 毫秒时间戳 | `actionData` | 直线、连接中点头像及目标端箭头 | +| `mode: 'step', mediumConnection: false` | `dots.step` | `patternData` | 直线;顶部显示步骤的介质名称和附加数量 | +| `mode: 'step', mediumConnection: true` | `dots.medium_type` | `patternData` | 三次贝塞尔曲线;重复端点连接使用不同曲率 | + +## 注册并创建图表 + +安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包,在创建图表前注册。页面需要提供 `id="chart"` 的容器。 + +下面是完整的最小步骤示例。这里通过转换回调把纵轴头像改成用户名称,所以 `userImageMap` 可以为空;演示页则使用内置 SVG 头像。 + +```js +import VChart from '@visactor/vchart'; +import { registerSequenceAnalysis } from '@visactor/vchart-extension'; + +registerSequenceAnalysis(); +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + height: 440, + eventData: [ + { + user_id: 'Alice', + user_image: '', + dots: [ + { + node_name: 'a1', + step: 'step_1', + medium_type: 'IP', + action_type: 'visit', + time_stamp: 1767225600000, + action_count: 2 + } + ] + }, + { + user_id: 'Bob', + user_image: '', + dots: [ + { + node_name: 'b1', + step: 'step_2', + medium_type: 'Device', + action_type: 'login', + time_stamp: 1767312000000, + action_count: 1 + } + ] + } + ], + patternData: [{ from: 'a1', to: 'b1', pattern_type: 'shared' }], + actionData: [{ from: 'a1', to: 'b1' }], + userImageMap: {}, + stepMediumMap: { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 1 } + }, + customTransformSpec(converted) { + converted.color = ['#5470c6', '#26a69a']; + converted.axes.find(axis => axis.orient === 'left').label.formatMethod = label => label; + converted.scrollBar.forEach(bar => { + bar.start = 0; + bar.end = 1; + }); + } +}; +const vchart = new VChart(spec, { dom: 'chart' }); +vchart.renderSync(); +``` + +通过 CDN 使用时,加载两个包对应版本的 `build/index.min.js`,并把页面实际使用的 VChart 构造函数传入注册方法: + +```js +VChartExtension.registerSequenceAnalysis({ VChart: VChart.default }); +const vchart = new VChart.default(spec, { dom: 'chart' }); +vchart.renderSync(); +``` + +## 准备事件与连接数据 + +`eventData` 是逐用户数组,每行包含 `user_id`、`user_image` 和嵌套的 `dots`,不要提前把 dots 展平。每个事件提供 `node_name`、`step`、`medium_type`、`action_type`、`time_stamp` 和 `action_count`,完整字段说明见[配置项](/vchart/option/sequenceAnalysisChart)。 + +- `node_name` 必须在所有用户之间全局唯一;所有连接的 `from`、`to` 都必须能找到对应事件。 +- time 模式使用 `actionData`;step 模式使用 `patternData`。没有连接时传入空数组。 +- dot 颜色按 `action_type` 分组,模式连接颜色按 `pattern_type` 分组。 +- `time_stamp` 单位为毫秒。顶部时间轴按时间戳汇总所有用户的 `action_count`,不等于简单的事件行数。 +- `stepMediumMap` 的值是 `{ medium_type, count }` 对象,`count` 是业务提供的附加数量,不是时间轴的 action_count 汇总。零计数也会显示;普通 step 模式需覆盖全部步骤,另外两类横轴可以传 `{}`。 + +## 用户头像与目标头像 + +默认纵轴使用 `userImageMap[user_id]`;事件行里的 user_image 不会自动转为该映射。 + +时间模式开启 actionTarget 后,连接中点使用 `actionData.user_image`。只填写 `actionData.user_id` 不会自动查找头像地址。关闭 actionTarget 只隐藏连接头像和箭头,仍保留连接线。 + +头像可以使用可访问的图片 URL 或 data URL;示例使用内置数据,避免网络依赖。[查看目标头像示例](/vchart/demo/extension-chart/sequence-analysis-target)。 + +## 定制样式与缩放 + +转换器自动生成 dot/link 系列、横纵轴、底部 DataZoom 和横纵 ScrollBar。底部滚动条在存在两根横轴时同步它们的窗口比例;默认滚动条范围为 `[0, 0.8]`。动画和 tooltip 默认关闭。 + +通过 customTransformSpec 原地修改生成的 common 配置。返回值不会替代该配置: + +```js +customTransformSpec(converted) { + const connection = converted.series.find(series => series.type === 'link'); + connection.link.style = { stroke: '#5470c6', lineWidth: 2 }; + connection.link.state = { custom_unSelected: { strokeOpacity: 0.25 } }; + converted.color = ['#5470c6', '#26a69a', '#f6bd16']; + converted.scrollBar.forEach(bar => { bar.start = 0; bar.end = 1; }); +} +``` + +直线和曲线都使用 link 的 visible、style、state,不需要配置 curveLink。接口中的 colors 尚未接入转换逻辑,设置色板请使用上面的标准 color 配置。 + +DataZoom 与 ScrollBar 新增的 linkedAxisId / linkedAxisIndex 仅在 filterMode 为 axis 时同步窗口比例,不复制另一根轴的数据域。[联动轴配置](/vchart/option/commonChart-scrollBar-linkedAxisIndex)。 + +## 更新数据与当前限制 + +保存创建图表时的原始扩展 spec;不要把已经转换成 common 的 getSpec() 结果作为原始输入。 + +当前 time 模式的时间计数标签与 markLine 在 spec 转换阶段生成。单独 updateDataSync('eventData', ...) 或 updateFullDataSync(...) 会更新事件,但不会重建这些派生配置,可能留下旧标签或标记线。 + +使用完整 updateSpecSync 更新。以下代码也展示如何从步骤模式切换到时间模式;该方式会重建计数标签和标记线: + +```js +const nextEvents = spec.eventData.map(user => ({ + ...user, + dots: user.dots.map(event => ({ + ...event, + time_stamp: event.time_stamp + 24 * 60 * 60 * 1000, + action_count: event.action_count + 1 + })) +})); +vchart.updateSpecSync({ ...spec, mode: 'time', eventData: nextEvents }); +``` + +若删除或重命名事件,需在同一次完整 spec 更新中同步 actionData / patternData;用户变化时同时更新 userImageMap,步骤变化时同时更新 stepMediumMap。 + +## 完整示例与配置项 + +- [时间模式](/vchart/demo/extension-chart/sequence-analysis-time) +- [目标头像](/vchart/demo/extension-chart/sequence-analysis-target) +- [步骤模式](/vchart/demo/extension-chart/sequence-analysis-step) +- [介质曲线连接](/vchart/demo/extension-chart/sequence-analysis-medium) +- [全部配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/option/en/chart/sequence-analysis.md b/docs/assets/option/en/chart/sequence-analysis.md new file mode 100644 index 0000000000..b479afb78a --- /dev/null +++ b/docs/assets/option/en/chart/sequence-analysis.md @@ -0,0 +1,159 @@ +{{ target: chart-sequence-analysis }} + +# sequenceAnalysisChart + +A sequence analysis extension that places users on the vertical axis and events on a time, step or medium axis. Register it from `@visactor/vchart-extension` before use. Use matching package versions that include this feature. + +```js +import VChart from '@visactor/vchart'; +import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +registerSequenceAnalysis(); +``` + +[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Time mode](/vchart/demo/extension-chart/sequence-analysis-time) · [Target avatars](/vchart/demo/extension-chart/sequence-analysis-target) · [Step mode](/vchart/demo/extension-chart/sequence-analysis-step) · [Medium connections](/vchart/demo/extension-chart/sequence-analysis-medium) + +The transformer generates a common chart, dot/link series, axes and zoom components from the fields below. You do not need to supply `data`, `series`, `xField` or `yField`. Customize the generated spec with `customTransformSpec`. + +## type(string) = 'sequenceAnalysis' + +Required. Distinct from the built-in `sequence` chart and the `sequenceScatterLink` extension. + +## mode(string) + +Required: `'time'` or `'step'`. + +- `'time'`: uses `dots.time_stamp` horizontally, displays summed action counts at the top and time at the bottom; connections come from `actionData`. +- `'step'`: uses `dots.step` horizontally and `patternData` for connections. With mediumConnection enabled, the horizontal field becomes `dots.medium_type`. + +## mediumConnection(boolean) = false + +Step mode only. When false, use step categories, straight lines and top labels from stepMediumMap. When true, use medium categories and cubic Bézier curves. Repeated links sharing endpoints have distinct curvature. + +## actionTarget(boolean) = false + +Time mode only. False keeps connection lines visible while hiding avatars and arrows. True displays `actionData.user_image` at the midpoint and an arrow at the target. The image is not looked up automatically using user_id. + +## eventData(Array) + +Required array of user rows: `{ user_id, user_image, dots: [...] }`. Preserve this nested structure. + +In the current time mode, count labels and markLine configurations are generated during spec transformation. `updateDataSync('eventData', ...)` and `updateFullDataSync(...)` do not regenerate them. When timestamps or counts change, call `updateSpecSync({ ...spec, eventData: nextEvents })` using your saved complete extension spec, updating links and image maps as needed. Do not substitute the transformed result of getSpec() for the original extension spec. + +### user_id(string) + +Unique user ID and vertical category. + +### user_image(string) + +User image in the event row. Default vertical labels actually use `userImageMap[user_id]`; this field does not populate that map automatically. + +### dots(Array) + +Events for this user. The fields below form the public event structure; supply all of them to support switching modes. + +#### node_name(string) + +Event identifier, globally unique across all users. Link from/to values reference it. + +#### step(string) + +Horizontal category in step mode when mediumConnection is false. + +#### medium_type(string) + +Horizontal category in step mode when mediumConnection is true, for example `'IP'` or `'Device'`. + +#### action_type(string) + +Event category used to group dot colors. + +#### time_stamp(number) + +Unix timestamp in milliseconds, used horizontally in time mode. + +#### action_count(number) + +Event count. The top time axis sums this value across users at each timestamp. + +## actionData(Array) + +Connections for time mode. Use `[]` when there are none. + +### from(string) + +Source event node_name. + +### to(string) + +Target event node_name. Both endpoints must exist in eventData. + +### user_id(string) + +Optional associated user ID. It does not resolve an avatar URL. + +### user_image(string) + +Image URL or data URL used when actionTarget is true. This is independent of userImageMap, which controls vertical labels. + +## patternData(Array) + +Connections for step mode. Use `[]` when there are none. + +### from(string) + +Source event node_name. + +### to(string) + +Target event node_name. + +### pattern_type(string) + +Pattern category used to group link colors. + +### isExtend(string) + +Use `'left'` or `'right'` to select an end. Omit the field to extend both ends; the interface's `'all'` value does not currently enable both ends. Applies only to straight step-mode links, where allowExtend is enabled: extends the source and/or target end toward the outside of its category band without moving events. Curves do not use this setting. + +## userImageMap(Object) + +Maps user IDs to image URLs or data URLs for the default vertical labels. Cover every user in eventData. An empty object is acceptable if customTransformSpec replaces the image labels with text. + +## stepMediumMap(Object) + +Maps step names to `{ medium_type: string; count: number }` objects for top labels in step mode without medium connections. Values are objects, not strings; count may be zero. + +```js +stepMediumMap: { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 2 } +} +``` + +Cover every step category. Time mode and medium-connection mode can use `{}`. The count is supplied by your application, not computed from eventData. + +## colors(Array) + +This field exists in the interface but is not currently mapped to the chart palette. Set the standard color option in customTransformSpec instead: + +```js +customTransformSpec(spec) { + spec.color = ['#5470c6', '#26a69a', '#f6bd16']; +} +``` + +## customTransformSpec(Function) + +Called after conversion to a common chart spec and before common-chart normalization. Mutate the supplied object; no return value is needed. + +The generated components are a bottom DataZoom in filter mode, a right ScrollBar and a bottom ScrollBar in axis mode. Scrollbars start with `[0, 0.8]`. The bottom scrollbar also links the second horizontal axis when present. Animation and tooltip are disabled by default. + +```js +customTransformSpec(spec) { + const link = spec.series.find(series => series.type === 'link'); + link.link.style = { stroke: '#5470c6', lineWidth: 2 }; + // Both straight and curved links use the public link configuration. + link.link.state = { custom_unSelected: { strokeOpacity: 0.25 } }; + spec.scrollBar.forEach(bar => { bar.start = 0; bar.end = 1; }); +} +``` diff --git a/docs/assets/option/en/component/data-filter-component-common/data-filter-base-component.md b/docs/assets/option/en/component/data-filter-component-common/data-filter-base-component.md index bc2ec0184a..b934985be0 100644 --- a/docs/assets/option/en/component/data-filter-component-common/data-filter-base-component.md +++ b/docs/assets/option/en/component/data-filter-component-common/data-filter-base-component.md @@ -35,6 +35,14 @@ Associated axis ID. Associated axis index. +#${prefix} linkedAxisId(string) + +ID of a second linked axis. Only effective in `filterMode: 'axis'`. Copies the primary axis window proportions, not its data domain; use compatible directions and domains. Available for both DataZoom and ScrollBar. Takes precedence over linkedAxisIndex. + +#${prefix} linkedAxisIndex(number) + +Zero-based index of the linked axis in axes. Only effective in `filterMode: 'axis'`. + #${prefix} regionId(string) Associated region ID. @@ -90,7 +98,6 @@ Start and end configuration modes: Only when the mode and configuration match do Whether to automatically indent. - #${prefix} auto(boolean) = false Whether it is in automatic mode. After opening, the component will not cause scaling of the axis, and configurations such as `end`、`roam`、`minSpan`、`maxSpan`、`minValueSpan`、`maxValueSpan` that may cause scaling will be ignored, and the component can automatically disappear. Supported since version `1.4.0`. @@ -114,6 +121,7 @@ The maximum data value used to limit the window size, only takes effect in conti Event trigger delay type, if not configured, it will be regarded as not enabled. Supported since version `1.5.1`. Optional values: + - `'throttle'`: throttling - `'debounce'`: anti-shake @@ -158,6 +166,7 @@ Whether to update the view in real time during interaction, enabled by default. Supports custom domain, used to unify dataZoom domain and associated axis domain(When the brush component turns on `zoomAfterBrush`, the associated dataZoom needs to be consistent with the axis, otherwise the brush range cannot be calculated correctly.). Supported since version 1.10.0. ### updateDataAfterChange(Function) + Customize the callback when datazoom is updated. Supported since version 1.10.0. ```ts diff --git a/docs/assets/option/en/option.md b/docs/assets/option/en/option.md index d0d459c5ce..c2470877d7 100644 --- a/docs/assets/option/en/option.md +++ b/docs/assets/option/en/option.md @@ -69,3 +69,5 @@ {{import: chart-image-cloud}} {{import: chart-storyline}} + +{{import: chart-sequence-analysis}} diff --git a/docs/assets/option/en/series/link.md b/docs/assets/option/en/series/link.md index a25ff70590..360956d0cb 100644 --- a/docs/assets/option/en/series/link.md +++ b/docs/assets/option/en/series/link.md @@ -16,11 +16,11 @@ #${prefix} fromField(string) The starting point field, used to draw the starting point of the link. -Note: The data provided by this field should be consistent with the data provided by the nameField field in the associated dot series, and the positioning of the starting point follows the nodes of the dot series. +Note: The data provided by this field should be consistent with the `node_name` value of an event in the associated dot series, and the positioning of the starting point follows the nodes of the dot series. #${prefix} toField(string) The ending point field, used to draw the ending point of the link. -Note: The data provided by this field should be consistent with the data provided by the nameField field in the associated dot series, and the positioning of the ending point follows the nodes of the dot series. +Note: The data provided by this field should be consistent with the `node_name` value of an event in the associated dot series, and the positioning of the ending point follows the nodes of the dot series. #${prefix} dotSeriesIndex(number) @@ -30,6 +30,34 @@ The associated dot series index. The associated event type. It is used to distinguish the stroke logic of the link. If it is not configured, each link will use the same stroke. +#${prefix} linkType(string) = 'line' + +`'line'` draws straight links; `'curve'` draws cubic Bézier curves. Both use the public link visible/style/state configuration; no curveLink option is required. Repeated links sharing endpoints receive distinct curvature in data order. + +#${prefix} allowExtend(boolean) = false + +Allow links to extend outside category bands. Set the datum's isExtend to `'left'` or `'right'`, or omit it to extend both ends. The interface's `'all'` value does not currently enable both ends. + +#${prefix} imageLabelField(string) + +Field containing the midpoint avatar image URL or data URL. No image mark is created when this field is not specified. + +#${prefix} imageLabel(Object) + +Avatar mark configuration: visible, style and state. style.width and style.height default to 40 pixels each. + +{{ use: common-mark( + prefix = '#' + ${prefix} +) }} + +##${prefix} style(Object) + +Image styles, for example `{ width: 32, height: 32 }`. Unless overridden, the image is centered between the endpoints. + +##${prefix} state(Object) + +{{ use: mark-state-style() }} + #${prefix} link(Object) Line graphic style configuration. @@ -71,4 +99,4 @@ Arrow graphic style configuration. ##${prefix} state(Object) -{{ use: mark-state-style() }} \ No newline at end of file +{{ use: mark-state-style() }} diff --git a/docs/assets/option/zh/chart/sequence-analysis.md b/docs/assets/option/zh/chart/sequence-analysis.md new file mode 100644 index 0000000000..2c7a40c43e --- /dev/null +++ b/docs/assets/option/zh/chart/sequence-analysis.md @@ -0,0 +1,159 @@ +{{ target: chart-sequence-analysis }} + +# sequenceAnalysisChart + +时序分析扩展图表,以用户为纵轴,按时间、步骤或介质展示事件及连接。来自 `@visactor/vchart-extension`,使用前调用 `registerSequenceAnalysis()`;扩展包与 VChart 应使用相同且包含该功能的版本。 + +```js +import VChart from '@visactor/vchart'; +import { registerSequenceAnalysis } from '@visactor/vchart-extension'; +registerSequenceAnalysis(); +``` + +[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [时间模式](/vchart/demo/extension-chart/sequence-analysis-time) · [目标头像](/vchart/demo/extension-chart/sequence-analysis-target) · [步骤模式](/vchart/demo/extension-chart/sequence-analysis-step) · [介质连接](/vchart/demo/extension-chart/sequence-analysis-medium) + +本图表使用以下专用数据字段,转换器会生成底层 common 图表、dot/link 系列、轴及缩放组件,无需自行配置 `data`、`series`、`xField` 或 `yField`。需要修改生成结果时使用 `customTransformSpec`。 + +## type(string) = 'sequenceAnalysis' + +必填,固定为 `'sequenceAnalysis'`。与内置 `sequence` 图表及 `sequenceScatterLink` 扩展图表不同。 + +## mode(string) + +必填,可选 `'time'` 或 `'step'`。 + +- `'time'`:横轴使用 `dots.time_stamp`,顶部显示同一时间点的 `action_count` 总和,底部显示时间;连接来自 `actionData`。 +- `'step'`:默认以 `dots.step` 为横轴,连接来自 `patternData`;开启 `mediumConnection` 后改用 `dots.medium_type`。 + +## mediumConnection(boolean) = false + +仅 step 模式生效。`false` 时使用步骤分类轴和直线,顶部标签来自 `stepMediumMap`;`true` 时使用介质分类轴和三次贝塞尔曲线,同一组端点的重复连接使用不同曲率。 + +## actionTarget(boolean) = false + +仅 time 模式生效。`false` 时保留直线连接,不显示连接头像及箭头;`true` 时在线段中间显示 `actionData.user_image`,并在目标端显示箭头。头像来源不会根据 `user_id` 自动查找。 + +## eventData(Array) + +必填的逐用户事件数组,保留 `{ user_id, user_image, dots: [...] }` 嵌套结构,不要在调用前展平。 + +当前 time 模式的计数标签和 markLine 在 spec 转换时生成。仅调用 `updateDataSync('eventData', ...)` 或 `updateFullDataSync(...)` 不会重建这些派生配置。更改时间戳或计数时,请用保存的完整扩展 spec 调用 `updateSpecSync({ ...spec, eventData: nextEvents })`,并按需一并更新连接及头像映射;不要把转换后的 `getSpec()` 当作原始扩展 spec。 + +### user_id(string) + +用户唯一标识,也是纵轴分类值。 + +### user_image(string) + +事件数据中的用户头像地址。默认纵轴头像实际取自 `userImageMap[user_id]`,不会自动从该字段生成映射。 + +### dots(Array) + +该用户的事件列表。以下字段构成公开事件数据结构;建议在两种模式中均提供完整字段,便于切换。 + +#### node_name(string) + +事件唯一标识,必须在所有用户的 dots 中全局唯一。连接的 `from` / `to` 引用该值。 + +#### step(string) + +步骤分类,step 模式且 `mediumConnection: false` 时用作横轴。 + +#### medium_type(string) + +介质分类,例如 `'IP'`、`'Device'`,step 模式且 `mediumConnection: true` 时用作横轴。 + +#### action_type(string) + +事件类型,用于 dot 系列的颜色分组。 + +#### time_stamp(number) + +事件发生的 Unix 时间戳,单位为毫秒,time 模式用作横轴。 + +#### action_count(number) + +事件数量。time 模式顶部标签按时间戳对所有用户的该值求和。 + +## actionData(Array) + +time 模式的事件连接数组;无连接时传入 `[]`。 + +### from(string) + +起始事件的 `node_name`。 + +### to(string) + +目标事件的 `node_name`。两端必须在 eventData 中存在。 + +### user_id(string) + +可选的连接关联用户标识,不会自动解析头像地址。 + +### user_image(string) + +连接头像地址或 data URL,`actionTarget: true` 时使用。它与纵轴使用的 userImageMap 是两个独立输入。 + +## patternData(Array) + +step 模式的模式连接数组;无连接时传入 `[]`。 + +### from(string) + +起始事件的 `node_name`。 + +### to(string) + +目标事件的 `node_name`。 + +### pattern_type(string) + +模式分类,用于 link 系列的颜色分组。 + +### isExtend(string) + +使用 `'left'` 或 `'right'` 指定延伸侧,省略时向两侧延伸。接口中的 `'all'` 当前未按双侧处理,需要两端延伸时请省略该字段。仅 step 模式的直线连接开启 `allowExtend`:在起点或终点向分类带外侧适当延长;不改变事件的位置。曲线连接不使用此设置。 + +## userImageMap(Object) + +用户 ID 到头像地址的映射,例如 `{ Alice: 'data:image/svg+xml,...' }`,默认纵轴标签从这里读取。应覆盖 eventData 中的所有用户。若通过 customTransformSpec 把纵轴标签改成文字,可传入空对象。 + +## stepMediumMap(Object) + +步骤名称到 `{ medium_type: string; count: number }` 的映射,供 step 模式且 `mediumConnection: false` 的顶部标签使用。值是对象,不是字符串;`count` 可为 0。 + +```js +stepMediumMap: { + step_1: { medium_type: 'IP', count: 0 }, + step_2: { medium_type: 'Device', count: 2 } +} +``` + +应覆盖所有 step 分类;time 模式及介质连接模式可传入 `{}`。此处的 count 是业务提供的附加数量,不会从 eventData 自动统计。 + +## colors(Array) + +当前接口保留该字段,但转换器尚未将其映射到底层色板。请在 `customTransformSpec` 中设置标准 `color` 配置: + +```js +customTransformSpec(spec) { + spec.color = ['#5470c6', '#26a69a', '#f6bd16']; +} +``` + +## customTransformSpec(Function) + +在专用 spec 转成 common 图表配置后、执行 common 图表规范化前调用。原地修改传入对象,无需返回值。 + +默认生成底部 DataZoom(filter 模式)、右侧 ScrollBar,以及底部 ScrollBar(axis 模式)。滚动条初始范围为 `[0, 0.8]`;存在两根横轴时,底部滚动条会联动第二根轴。默认关闭动画和 tooltip。 + +```js +customTransformSpec(spec) { + const link = spec.series.find(series => series.type === 'link'); + link.link.style = { stroke: '#5470c6', lineWidth: 2 }; + // 直线和曲线均通过 link 配置显隐、样式和状态。 + link.link.state = { custom_unSelected: { strokeOpacity: 0.25 } }; + spec.scrollBar.forEach(bar => { bar.start = 0; bar.end = 1; }); +} +``` diff --git a/docs/assets/option/zh/component/data-filter-component-common/data-filter-base-component.md b/docs/assets/option/zh/component/data-filter-component-common/data-filter-base-component.md index 80b62150a0..eef6c89cc9 100644 --- a/docs/assets/option/zh/component/data-filter-component-common/data-filter-base-component.md +++ b/docs/assets/option/zh/component/data-filter-component-common/data-filter-base-component.md @@ -35,6 +35,14 @@ 关联的轴序号。 +#${prefix} linkedAxisId(string) + +联动的第二根轴 ID,仅 `filterMode: 'axis'` 生效。主轴窗口变化时同步相同的范围比例,不复制数据域;请让两根轴使用相容的方向和数据范围。该选项同时适用于 DataZoom 和 ScrollBar。与 linkedAxisIndex 同时配置时优先使用 ID。 + +#${prefix} linkedAxisIndex(number) + +联动轴在 axes 数组中的索引(从 0 开始),仅 `filterMode: 'axis'` 生效。 + #${prefix} regionId(string) 关联的 region ID。 @@ -104,15 +112,16 @@ 用于限制窗口大小的最大值, [0, 1]。自 `1.5.1` 版本开始支持。 #${prefix} minValueSpan(number) -用于限制窗口大小的最小数据值, 仅在continous scale生效,优先级高于minSpan。自 `1.5.1` 版本开始支持。 +用于限制窗口大小的最小数据值, 仅在 continous scale 生效,优先级高于 minSpan。自 `1.5.1` 版本开始支持。 #${prefix} maxValueSpan(number) -用于限制窗口大小的最大数据值, 仅在continous scale生效,优先级高于maxSpan。自 `1.5.1` 版本开始支持。 +用于限制窗口大小的最大数据值, 仅在 continous scale 生效,优先级高于 maxSpan。自 `1.5.1` 版本开始支持。 #${prefix} delayType(string) 事件触发延迟类型, 不配置视作不开启。自 `1.5.1` 版本开始支持。 可选值: + - `'throttle'`: 节流 - `'debounce'`: 防抖 @@ -150,10 +159,11 @@ prefix = '#' + ${prefix} ### customDomain(array) -支持自定义domain, 用于统一dataZoom domain与关联axis domain(当brush组件开启`zoomAfterBrush`时, 关联的dataZoom需要和axis保持一致, 否则刷取范围无法正确计算)。自 1.10.0 版本开始支持。 +支持自定义 domain, 用于统一 dataZoom domain 与关联 axis domain(当 brush 组件开启`zoomAfterBrush`时, 关联的 dataZoom 需要和 axis 保持一致, 否则刷取范围无法正确计算)。自 1.10.0 版本开始支持。 ### updateDataAfterChange(Function) -自定义datazoom更新时的回调。自 1.10.0 版本开始支持。 + +自定义 datazoom 更新时的回调。自 1.10.0 版本开始支持。 ```ts /** diff --git a/docs/assets/option/zh/option.md b/docs/assets/option/zh/option.md index d0d459c5ce..c2470877d7 100644 --- a/docs/assets/option/zh/option.md +++ b/docs/assets/option/zh/option.md @@ -69,3 +69,5 @@ {{import: chart-image-cloud}} {{import: chart-storyline}} + +{{import: chart-sequence-analysis}} diff --git a/docs/assets/option/zh/series/link.md b/docs/assets/option/zh/series/link.md index 87a011a996..01c513d303 100644 --- a/docs/assets/option/zh/series/link.md +++ b/docs/assets/option/zh/series/link.md @@ -2,7 +2,7 @@ -**link 系列**,可用于绘制时序图,需要配合dot 系列使用。**仅适用于直角坐标系**。 +**link 系列**,可用于绘制时序图,需要配合 dot 系列使用。**仅适用于直角坐标系**。 {{ use: common-cartesian-series( prefix = ${prefix}, @@ -15,21 +15,48 @@ #${prefix} fromField(string) -起点字段,用于绘制link的起点。 -注: 该字段提供的数据需要和被关联的dot 系列中的nameField字段提供的数据保持一致,起点的定位跟随dot 系列的节点。 +起点字段,用于绘制 link 的起点。 +注: 该字段提供的数据需要和被关联的 dot 系列事件中的 `node_name` 字段值保持一致,起点的定位跟随 dot 系列的节点。 #${prefix} toField(string) -终点字段,用于绘制link的终点。 -注: 该字段提供的数据需要和被关联的dot 系列中的nameField字段提供的数据保持一致,终点的定位跟随dot 系列的节点。 +终点字段,用于绘制 link 的终点。 +注: 该字段提供的数据需要和被关联的 dot 系列事件中的 `node_name` 字段值保持一致,终点的定位跟随 dot 系列的节点。 #${prefix} dotSeriesIndex(number) -被关联的dot 系列索引。 +被关联的 dot 系列索引。 #${prefix} dotTypeField(string) -被关联的事件类型。用于区分link的stroke逻辑。如果没有配置,则每条link都使用相同的stroke。 +被关联的事件类型。用于区分 link 的 stroke 逻辑。如果没有配置,则每条 link 都使用相同的 stroke。 +#${prefix} linkType(string) = 'line' + +连接线类型:`'line'` 为直线,`'curve'` 为三次贝塞尔曲线。曲线仍使用公开的 `link` 图元配置控制 visible、style 和 state,无需配置 curveLink。相同端点的重复连接会按当前数据顺序分配不同曲率。 + +#${prefix} allowExtend(boolean) = false + +是否允许连接在分类带两侧延伸。开启后可通过数据的 isExtend 指定 `'left'` 或 `'right'`;省略字段时延伸两端。接口中的 `'all'` 当前未按双侧处理。 + +#${prefix} imageLabelField(string) + +连接中点头像对应的数据字段,值为图片地址或 data URL。未设置时不创建头像图元。 + +#${prefix} imageLabel(Object) + +头像图元配置,可设置 visible、style 和 state;style.width / style.height 默认各为 40 像素。 + +{{ use: common-mark( + prefix = '#' + ${prefix} +) }} + +##${prefix} style(Object) + +头像图元样式,如 `{ width: 32, height: 32 }`。未覆盖位置时居中显示在连接两端之间。 + +##${prefix} state(Object) + +{{ use: mark-state-style() }} #${prefix} link(Object) 线图元样式配置。 @@ -72,4 +99,4 @@ arrow 图元样式配置。 ##${prefix} state(Object) -{{ use: mark-state-style() }} \ No newline at end of file +{{ use: mark-state-style() }} diff --git a/docs/public/vchart/preview/sequence-analysis-medium.png b/docs/public/vchart/preview/sequence-analysis-medium.png new file mode 100644 index 0000000000000000000000000000000000000000..1ad1cad235c961f2f27f778c8220828a9c969003 GIT binary patch literal 14706 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 QIXq1&iW&+P@)jTd8;b4z&;S4c literal 0 HcmV?d00001 diff --git a/docs/public/vchart/preview/sequence-analysis-step.png b/docs/public/vchart/preview/sequence-analysis-step.png new file mode 100644 index 0000000000000000000000000000000000000000..5a2eb23a5979d01235407b4815b6fe05b6b88c44 GIT binary patch literal 14942 zcmd^mcUY6pw`ahDf&!v~NVNdcm0k=%LHW{^UP6&h=r#0!il8*<9i&U|y(>xyRch!6 zp%ZI z6LcAtTHH0EoL!$SQZ^B})7xu;!^C5R?OYGsS?e)9)_k3;su2nv4pgyYp*FQ`uIn{l zRn{9Ljb1z;C88XYQpvWAh;VrE_I}IOQmd2!Jsdvvr?G`%Y0?wFYA?bCh=EA6J|i@N zcZlwS=9MqxI%pnA`d$XjEoR9JpdqFFLIj#iSNg$kU*d}vpphgZ768o`$p6u6lH8E) za*VZpvGBKOZt5WYVj9UwhaS(-VU?w;%x0YwUPq-&b6xTA@d&~2t9HhA5T4SB4OLau z8$|_!D_8mh<@umt!NJmHtVDO8C|+K^f^=c$<_>{`WQq7+2#m60r6doy1TiH0({p&q zp7u29`BS$uHE(a@6LX2}RvLYu`MuigAFe?dH?$%N(z@@a6~Ae!#LQ6R=M>~d$5f2@ z?G}$^x;sbM@5cH9`CNlGrXTa9-; zv3;t=;)}kMT5>j_%*4)%amO(Tf4n|4%s)$UabfL~AYH1nK*d34pO?FunRZV!OE_c1 zm>6J%B&}NPgH)P*w%|r<*d%A{2Ki`EagxVg=hFH2B_>KM=4$pqCx#>duD znhByWAZyY-Qs?cwUe9@IvZuIhrotq)PxaVBHKK06*ntOiYE|d_O0c~#AZ+QRXR$JD zDfp&UK{%hT358l7EI(doUv* zB5jnqfI3|>)zy#yTqW{AsUFJS`Jp5A5$wrYVjXUaUAsnH=uNBv5g#9F$!!Ju=0f>zy>t9ve2oOfj5D zR~&5jG(Ytu@`Ns7qIg1+TUV^^1clte#?zu+)?p7rM$r;0(YlnR$(G9nTo@jya=4O^ z1;rL;s$-3gI0@L_vQdqfOSoy5-Ff9ReLOS#kp0fJUi@Np-vLFa#n5WR?+s*%2#3am zvFSY1%u`+`duB?7@;C{%t*enw_AxWftjJko8S=oRDg3Z-@7W0oH_ddqKCs>$JaUlo zN<{6ohS$mV_nET+yKL*wwD2cK>8S16FwP@QeSG{@LP~u?!&m#w8JW#f*}MB~QL2!w zHc=qA^74B}&#e2`Ze^K2nZZ8Y*yf(zKQwb5&-_u}c{zDh{DJTCqv_h7`Q(W+Mvn#B z(TXK0+?3Dh^1gYmuz>}l58Y#pSb=M8SbGO`Msb`Jo8muv7EDesxu4c~)Z2J|RwWjR|Y!|~obrlFOJW{v_c^vZu~+L z`yr^~j}6`%x-bjNEeXf3N(CY7;@Td<&$1$z&WHoa8?eKDbKh)g>+|L1 zKaur~2#>18bTv8{E~l?AtkK;mDr7hC($G{!85#X{C3x)q{K*VZGEnG ziW$$PC$$vl*HP%k{>?lU+Cgj+_eq}aM7>ivKeqG%N}YPaG|0usNg=hipM`{x|5q@v z8(ZB?N;?zO?l#@hhdk_ zBwgymhxhL^aQD8JnXWOkLkY^&Gc`WlZ9@qasnYSWIV$}KYxUr1P zUFHq`xxC8{o+DhRxxP+SdzXJoQ`%~m9$82(dndxh$XxRrt1nO9!NsN3{nHYesd~s; zzH08XecDNP;rq>vDJe{R%qP7|#9Dq}?~ETW%PpP2ik_=uTru@r^N!w5eSSwZ%-_(*Mwj!z1zqx*8uJQX6)VaqnAA{hDY?Sf zECnWZczdqS<4D%%CTyQJC}+GzPcL&>)y2WWwDEHA+V-v_ZqE!X!VW6T{LalsUqc4j zO#fNiqGDq72)>{W+r`e{=e9#P=OU8bFV%K=yeV<{6CcQ(mYcN;ESy{KPHNoohHA4x zRl@qbyzB@IoiiT?`jUI(RNo49h5|>So)e9d9({SI^8RU(Gf_eQt49y(HyOQuAK_bX zi2mdTUM*l-zBuK8gt)BvNT&JFMib`?FHEm^M0v$^|Ju+-uGX)w@9<0+ijxC2&;=cdOHff&=MYb$>2<@;CoW;hxk#vjTyJ%7yEQaf zRxeq5qi^f$6>9M6e<^c>g`M&nTt>#Hm})xPY9s`Yk}*62s_gNlltrM@RY_r-!k`-I zkY;C}S=qQWVjMuNB&zo^G>x~hzB+H&T59yqkJ_T}U+E25qqm5y9yTX7ABSuO9UZNA zXAgx2BaL0G~*REl`!!ud?slBXiw|y(6^_P1zTQQu?UxRdc9g)QKG4_rNi7> z&T8{MFDw#W7Bs#;LQBKBQN-cCj%+?}N8wj!Bz6a)Puv$$k0K9hZ+<<#+)k}*(p8p_ zpBLwGb?eJ-{X%v#U)~@CV zM48x=^ubNjzWDXV^0*Mqv2VYoe6Y{%U8kUsQJE28V`CE*)`Z+}64CRRn#?^7;?CbJ z^+B~@4>ENX_{qbDhmkvHeM5Q6Skw^~I1 z1@w}>5n$i{9u}-B3NK|Jty{5to&K8>Y|2lI#S!!x|(V?hkFFQ;5}=BMSr`SH!DNA30h$d2@! zN9tX2==h?XaL!gQ{QL9zVcgbARe zgQH@+GG+QD0o6)QA`pnXckk+GG1Ajt3;14HS!rbR7up8u8XFsH)_A)7+kbwNv!uvk z^4CdG}bh1ktIhhOS>4|B^px=9sPVCD2}h9 z!fo<^7eh~l$8dc;OFRmpGBx<=-~w3TnDjl{<6@v4n>l3PX--Vuu3r|2)A`(w~_d^&gm`Ww+Xc2*m! zJY8G3jA9ha)(B4rmR9nyVE4uQY0Rv-frlCQiu=IUaXo$=8<}GGiZDmwPmjyXxwJFw zDW0d;aH3Krefmw*>=9cMQS;v=0$j-Y{a!6Q@xlg01n-Tge;+yo6Y1E(|GwN(cWL?D zbuny-?WgpuBwD6uIvUX5!dfChbi9>?FUu=_FkEvM-#{Jz zR;`MUzaqS|xLg9pL5h+E_a)JX6Z4MJH9OB0Z39qd-?o#pB3gRl{eY<$;4b* zXx)xDlxcnkR*1fyJK+IGIKpzQfoJ=n@j z5!&BJen9Fkada}nO|4sMQK>w6)Osx$KX}^{pN3a^ult$zIQ|oqxJJiudA`^HwGv-V z+G|Bcg|9o9l|iasVJ3fn<`>FKLegA4_J!xLBKr{iJo=IE7;cD(2fti8J-eReBvU+- zj6XtYW{#G58fO&dlGORwM5~(67@jRmQkU&@hQl-^-hBG^TtNP?5W8>wuvu!|)=B`) zV0j#G`Vlh6;OrY`U#B6rZ`>kNqNr2fuRX3yBzV)s$rG#;aiHmRJf`^U%h?G9jw{=I zW&gPIe6N`&IQZmZN$JR~FYVq|2{-2rHKqrWh-?46ihX;ZeO{s%QZ&*hmT&^^jo#o> zY3`*x2y|#IWqaQ1#5o~#FuFjPwkKJMHdeFBAn6ZuKi~$Db%9{=w(UfXE$P=rB-^E3LK8u}45(jGY zrZP59N}-+#A9dn3C|fS;sQNa2VqBJvM!Z>mv9Ez{m(NRKT3`Eg_^aiL!O(o8KzYEk z;w=9d|CsF~GOZt`&f_*g=mz78HY53tse3$95)Vfg7&2H~bv*(mfAi4~`h^czM!x-^;<>Sgc?$)Q8!>vzwf|o38jEtCXsq95jJl*a1_Tv3j4)t2m zvqTx&9(kps=~8h!k9G4Kw$GX@9_F)H4psD*6|iU*hBr_02;0@N%X$v$X8XtV^r!za z`vA=k8|VxWkRKrsv8YpESvQAHS}RbwYMdt|8)@&NJB{FOHC(n1D$hb=_!k&ccj_no zFrVQvrb@#LW~)}5+?tJ8#W5VeEMFkWNsx z*B2JSw#b(AvMfO-`5!WFw9YkzOKIn?xss2=YFWC)9%w)%tqzq6vd^Qg4Y434|5q%F zIG44L@{4#SGn80<-HR#O21d zO`B42wzZ=?_X^l0&6lr-_g!e@IO$e!!#k53M+kj`B&YFTnMIr_ZnP@n@kmbbH0(be zIUKHJ%rTaq@Vo1WgZC?bKNYTgt-3b~GzdqJ_TFD_8yo5_bH$vU<}i)1|DitO2NVnl z&N4FYisfAJGt^4{%9s7ssosM8jNg^hg? z|0REcf6s#mUMPieM`@_n7qB+yWF7y!UfuDAlBkU@+SXHLMe~&Ve5>pr^Ak#KrRpkk zOxB$t)V5^jj8DeB(_Ws>6iFSt(=qp}@?vb;{aww?XDWixuCX&rSJ+s;szeTJuZ&+9 zw!Hl!cgODew}fo?_dH-hI1>}-_+Z*H`?>3`9{%W?-N(H5w?ba>pTqVI68dDCE*+&4 zYrTUN7oE6S4y&F`6&uj=S-HqKk%eMyW`u7>=t0>%TwW%)dKRU5nImlc zexF&$nqM{VXh6Ch%S%`l6mOJ;Zc)EElCXc)jxD zN0h!E4y#%mxP0=wr|C-mS3(fF>1VjC_Fc^>w%oHHDkM1X*mpZw{?|jT;DiRQGrXL< zOKxq&jKB6Ci4@5*HZ=_eR)G`EzR-Fk$C9n4==x#vpDB1&L?c#><0EQ~U)wNQ@I?NE9L0`cb38pi zvHM!icLwA?_DWGN^rx%2OsW+RkN6pW7M?|A{7H;o5BY_)u&l#PPqD6Lq};aLX2t+6 z@p$8SG{&QJ8eich%w`9qXtB1AOtu4Qz1o1EPle14OI2hw>TH$cYs<#8G%ZJ1Ugusk zf+F8Gk2^`?^=7V}b(SUg_kbjJK?eQ??uc5(Gu3J~-5gP>^6{BXr3D&!m-AC_>8YO!+`{~62l#~YN!Y1oLh+r-}7&0BY{fI+FAXV07w|8>O5a+<`C>MA>0z`^;V)RRl+- z%GD9&TgvZ?%gxRft4{RqGP*TX@A{`nARpWNN2?0Tw(nNY=xKRLxi*2hYOXcIobxP; zRAl?Ucv?ID#Kz~Vr7OynWFmITuAStAKOnx(~}rNbRK^R+`>mgLTL8DL-F@ zUnxhAZC+VLU==gipJZ)xcH9CJawZ|-5X;Vc*J@py4i}xY3m{KxYAXw z3Jv{K{Gnpr6C(^HV(-4BYjb_W$^YynHD`23O;UPi>1ya^V!m2gPOuzH&CfVAC9na0 z#bk2YSn-pbfOap}SQzCsHrBl0PA64f1A|BnZR>G^WL96r4}=4p&_ABVYtQQ&GweLv zhQzVg4lg)G%c)#X=yu+iCUr6mcQ7#3QBipd5fUa`3h6cqC-In{4M$;Vgb4^YkWQzRf5r0{*=)pzI(fF<0%a+Y7?^ZtI)q z25id2YQuG(+>PmLzsrB~(?-8hJwvDJ7N2|uOzr$yDNtA`h66=>Z zqZN*_p{?@Ka={6wT;E*;E$ihKe@uK;ds`R;B+wE=P3o^CIG!MuJoRpK;A$$GA?dn* zqyP}Z4&T0KSDBSu2v$5-jkqC%FnYwX$P}2xs3^m=Uj00^2&>^K=N6qxbVdCT7X;1- z`0;fc>7TPmDpup;!-vYTU})>4Joz|2g`DJnRU>{R1&Cwzso1Z(JHEc*J^0;Nf5Iy( zqU~aML_w$A=!ixfXHdvV-Y)?JM>NFhIbpEI{M^mCDKtm4Nw#JFCq?WJu$4J? z4vR=eXi08EAf<2Yhg6%>RbBREVTQYR>!4!QVo|Le3m*?}u3CiJ=R#PGcwvGZOA9OEIL=)6wzYpH zJ2zWNvu}29d7)c3T8^qam*9e@Nzt2Bsu-mV2;~F9iM*h^yr=2|xlA80@4J;rA?+0)S^m6FAuiE?&)=j3UAm-G zpL~5IG!(y*_N4esC5uWOQ5K9I_l~xV{*k*=4Cs@DKL7`Rd+n)rgZtdBw6P>S{|duV zpP5zm5LO9QX0f?OMM#WM|F$WqH8=e(Feg3`T4&7cwp=>>*=1V9opMz^4ySWZ#OpLY zsI2;Bq?Dy{wPg4PxJ}a?TF%^p2NhOMuz1F@Sp!{G1A4^QJb)?mp56O#Rse{QHV5Y+q6q9M`E& zf=X>}=Yl};CQV+hBram7e2)Z|*;&)T4|@Uebp$&o*-ds)Du*s7L2T*w4iI~V_+vUM zKmrJU3f_ADS=rrioqyiIAZzb{Rs=%L6SD7$C+S@nq+#>o!DRveI+RJDzj^2qug;Ra zs>ezM86a4bwd+1$rS2HG6YtO!$v?o9MK#W@SJJB2D^%X5CW6rUgXzZLI&};*L)FMbQ0l0kfS<$gjKrs4U24f<>J&@?Lo+ncUQ`(;$+XsfmNy~>p7vu@S^Jy^wq!MqYtNY)PO0POcxC16i z?X$6nkN08 zF5v={gcSu{L_IH08LRU1AejXWa@&lC7ED)gnmP!%9u}~xC4z*T*L;y-k>R1aWkzRk z921>t#Z+@dFPH%u;uD!N;^bH$Rgb{Rl1X-zz+`Fd!?<$QjO2z*%$GxlKnF%w32;*L z9&nf#hTdOq4}rvjjr0{6#cYYqAhy<> zC_+4}i>e7Bq#>spu5DeAFR&JIC$(xdzChPyj15`mp|5V#3-Am<0r`3bb-8 z6KoJ9@g{gC)eG83n$kDWr`4Y2A*aZrINxc;0WS@a&iO(j}B*&K_smKX)QYc0@)0Wd_11k zy?{-VY#_7TG0RC!!m5ExUSBTw@_~Rg?y2{V1{xU@E%(>d#^Eo@g!jpZUBkT!N8 ze|Tp{4GtdESU5Ti`yH>NlKb#`IzgRALw-WBtobk4AQ0WlfbeM{?4MN2s>*$llMhel ztB}CI#u(T5gKd1CW*`-tk&(Fuh+$)OsJy|+2{?G7=^!q<;*bs3jnMZ0N*kEBl~RFk{IIqh%hk`utN}~ zdnIsX1Yf?z&aX*Lx5feD9b}BzYX&mryvPwZ_;zC{=hgS?UuCwTZs z=d0SUN$h&GgS%{2gn*NZPt4pu$(7kd(tv46>wA(Hw1j<7%W8vayPxD(EMF?x#`vJ3 zv$Ee47LSwhy==d)B&ZDV9OW0kLnNRlEdM;+ z{MRO~Nj&6#9#SL%* zlFG?ga*URfQ;dEAoDRfLitu2;E59&pY5-azmlct5!k6a?jspMM%zp!onuThP-ikUb zgq6mkuJpHDw%l^jv#3VyDN=-au z_DUzFaNEsk8k>NNNz9jD-uPapt&`ifxkEAy;9&9-gItqXOVn)wF}{2Qr2TUxB2y-w%PW4< z_mB_QaA9r>8{0GiWV;bt%^6>cz5$h$3v|^l6$hB53znXtX?a9!0o29wfXgTts%ncqvf|5e4b=Gcvnefz`LDk4e_uzb{ zR6mAVxpME?Qu` z5BHgn9cmBLU)r7~!qyHKXNFiGU&3S~i%f0Dw>DeBijLsKpcWg4j7_)%Zk4)q0w8CT zRvGiE{P(+lvYIcDC5v=_q)(Jr&6$6mm|^wT7!32Ztb7kho;Q2d8Gt-Fl;3`k$SUH4 z{<2jzh#Mtk0GPA#E9&aLDSE`WS*$ep^@s`Z^BbdcVK0(5L?q{(k<^$T(Gj?Nh+r$Z z=kZ~3U$a0>MKeCeD9}UD4Jd-5Y2nIJ&jYc2WKOH|F9X=!{Mz;4)?szyu#Q&H|IxeCl|#h ztnJnu_OE-gZUAeUbQQ5e(*Cs~jnZt0x12l9=|%W!@?(&x+66};bkE1D99e_NB-;uH z_B54V%kvdhU90MRv#f7KqB+i7U0QS0>%55C{4JILe58@NECcm3e8j1T+;xffcaOq+6D-MwOo=a0 zlX@>75I@m$!Ttttlf^$5$%OXaJG%Emx8@Vg6r!PNHlEcJICNx3d4Y;72qAUc2DuA! zn+Lq9KBi`_uBF(4lmk@L_JIoj%@53r{;a8~md6cO#%XllK4>1&@DaavRs%6gqW8y- zH&-f-odt3CHA@`iQmj99ZQ<|@smzAhAdhYr7AvVm)6klR?E;OJ`RR>1iQlOYU3_%C zU(P^a)BkyzYDiVKk~!{+r!z>w8sw=NDJgbksdSPShmoK6(D8E9?cdq1ft~Mk2G}+C zNy7w;w8utagMZlWFL=vX^-B!yiB9>MtJiISjJoaCI_$K>+tP!2SJlsZi6;3-`&e&6 z^745%@upgOVLQvs=_8H$J|;doK^A}ZjYd+XOvWI5(qQ5wI6j9v6g!F4 zRH$?+k`w@tuEtUOwm8Y#d)Vp2<*A~rO86mq>f{n-IkBFBc1jA(MFzj3^Axos2BNkC zVEQ_h%hDoTO(%()=i9M@#AA|Hr2TShIg>t8RdKM()SD@mNn-HC?b7;c)JmdaxLw=k zHR-a1?r1VHP>xbiE#NGY;UT&a~yU?E!{K;s3nr~3>TsSzIdds?`M}nOv!@<$Z zAs!hVqh)=ycJ+s8N5Db?;TFJF9@k>3JiAWod~%_@EFct&Fy$U7&xSySU8LF;uAslI z(=s(VB{p>WgJ^rITlM|xD65D0{i&!4x`B#8K1&`huBMNr6OK(k7I*vpb()TT(YXp> z&*ihM$m>t1Qx%MzwsXbn@z4bU;FHSA)7w^(VTJPeLh<1 z4P=uVr*FRFa(Jr3aYMd@Gs@v;W~2ETxHehbW4O13QD>5SJc7VMS$yO6n^9x<_T7An zdkHE6GiP=u7wu`oVEDtbnEI_I-GZJ3k1svB2^|-l^|D^z&x&VkSJypHf|qcq9nCnm z>7G&ZmyBPqL8P;YDgBb&9ESWvak)>0R@QHtY7sr)fVX?3Bp6813KjIH00gzA3)Ir?t z`2EVGb0WJ)i5@yiRVbm?!=%JH)O-B{DOFf}@6Y!Wyg03wIx26^pA3n8o4uDWD+r^MTt8{^_CG&THT8G@;9u`3eT)|;#gC%K3l()# z(AR_>*RtOM04JO(;KfmudkJf zB9YE!J=om(l*=32JwAV64X(8^rqz458TD94^X7e9J+$}X~N!>0OxKd#9J8S5=*O3v&2why0qopGnNL!i5*vTOvB9ABijpEqXlnb$<_ z4JkS3hRd(n&#j)=_6RSh>u3Vo@^ElM{`Q)`hlczU6T?07C&`GNrlKh1V zwf`~Vnp1?9=E`Dbv)h-4Z{95)f!83@KRh&=1+pxfjtoE*Rus!f=k^+Z^S%K>bf1NBmyEClg<4Fr>wdDq1d%jodtXL*cA7qw)F% zc^Vwt+O>LuvZp&B_(4iKrSqd2f}0JF;XnLc+xt@2x7Jfb|7ZgrkDGb@O4Wz}Nm{aR z6+#*%U(O{&tX`eyrlZN$@q~kP1&GGz5s)vVzMUCGLyJ?_;p5T)d_k;@2Cny$nQ%3& z{iJul2|6T$b^EkMz#qOf;K%^l|MJ@B|6dC+2#LD?yH(`>3rRBDS_&j^PBsE42Ucq< z?C$O+{D&ayeOqoV{{OTg~1Z{-eCsYze3dMpyTQs6=Wp5AM zXwR|lFK48Cs^2Vkdt|MGxaD}RgE-yq0U6nEA;}!wVB*qep|{^3onI;t4&`FLD^m72 z;IAV6N4?ezaxMqF zROe7fAa*=$>z}}c z@NucUV1?oY(h;osC?cJv2mmq6=vI&!cE6SWolPp zK!xqm2w%V5f`U~?hKb^e$k{$En_7poXy)Ya=+7s{bu9t_<(qfFCA&pX;+GycIU!; ziAC>t7FIUOFgNvI2yr<8<=3}2#tv3UU~!SAXH%WA`yLzKTeUBK&a52_PdW~^4wT9s zxvSOJpZZbEL?$2(JEfF6a>f{n2A2{R5U8D&ljPnNbJTfzbfV%vuR2c~OB5&ssuD~C zhC?@wb}S+`cD`ct^p02QmKKnw9}_9OXB(@PlrT)F)gP}OE$knUi+kc1=C?f0q1A8; zfYY_kPIQaxcL&y}V`iRCFr!9D4bg$q;g!|dUb;TLE&V^tQ&9V$GB)=amYZ_ohd{me%Sow|?FrUm8olS_%TwIV#J z>MHO@n)20gv^>+V2w2UxsSn4Zx9QjP^pCT5BT#A~q@{BYUO+TIVeG zNQ`<2>8{^-gWc}|GKsAILEQ{vKHq%QZBWB0lR6_ZXkQ!R+0c}Fy5msYezJYC)!SJ` z5YuWn%f^H*i{1Wpn=+=lI-^+n;M;*T7cC0dj^^&e92Uk#x`WSuXexEyDE-ZFNoQl? z_1KqDbDcrP(~d@fMU;0zEvmVJmyL>MF%fMGFq$WL7 zjV%@%*5}t6M5cKo7*0Q-EmFOBn5pO*S6H_#`S@^(>Fi~O2I?RZx8|ZlWKH`TTGoW$ zU}CDX?enWcY-`L`iNaTQY3P_G{up0bW54%gZZs@;eUssHnmft{cryJsV*V%ho*meA z`j+vQ7a*m%!jiwThX*Shv7R`RI#!d`c`f|Jwk;b9FNT=7Bz4Ri`e5S1Eg zY8{tuC^|5H=}Uo%3rMR2_~=`BpO?3|s!BszH7WH1R7i*wj4v^@mOM3PB*aZgKM{(4 zP)SSpYEAd&<+f%Kn14oDnM#(5!;O33@uY4;;45j`!7)NuCAB99x4Hl4UxZbCc=c## za+;ri9_iT-6e5-^^q{A~$ukAa-I84`OO?z)PafnZEG~X>*a=gM*^IKYwdh0@d*Sp3 zZHw*9l+{7t1ci-N?o?ATSNg*w_J2>Yg+63uEjqT)MK<>dm)9Hp4ew@^?o@iGB+H>G zev-_uUYG4Ua$YnyPZc@pvfgnaaGV&4swt4{*jz$Gd2UW^rFdggChTqv(3~$xdNxX6 z`cVG2hdzF!4q~5L9k!_W`$pgL585F1c~Z^}>DX}F12^x>jY`tmr+we%a3p5tQe@~N z%dm-gT`k7MI4yYl#z9A~8xWnZBKxgtO;+hnHIp2}FCW$w3WiMiFNS6xZ2Dt``>H-X zzegAp=h$;|N+bTK?Xwo9f85rPvdnir+aaG5^S=Q_ByAB^Da(Ojfa?D{-{<-2ShL9L zRO%FJYmIyz&a2M*3Kn73fbU9IR}$}ScB!a;%c?%C^H%@(@gsrrC)fnGxyqB^aXIK5V#CKOuD8iqFuUy`^?a1? z%MCEiSQ0V&%Xc`^Ftl`Rg!QX@;ro+$pt>m#xpa48Z#=V1Ph3Od)|HC}u)&i0vbMMc z!WWDs+BPZD)hxsn&e=_-CeBaUCRSaW&n8TNG6^h4fM`Yvwo z`x)=^KJVCL?>)XBd+fb`=v-Vq=RD^b$9W#}o1BaUHYN!s2n52Gd@uSL1VU~CfxtEB zNWc+PHDXB+=n+U#^sRzR()KKd$MDHb$ANVMg=r$ihO{~z6MV?mu0n$zla7Ke$YtSM z&{?UgbwAHt?;A3G)(5NsU&r~^{b|2sWT4ZnX343!4HB40?OnaUysMhzeZaIPk+2rK za^kTv*ik%x8(DF|*~K?Me|C0J=j`Bs4!j=}WBcU3VHEd%05ngZ2_pjy=A$$)(4eBj zf&aacP)vYE7>vXYG-)6(3DC%SgRp^yPWXT8ZOrMFm4&6bs!foD1oaT{C=gr>N>^D~ zjWwYzq93)52^OYu24~yT?d=DqTE(%NYFr{#!otFw1<#jk4n9rp?iIsME*TtnS$rTr zl;tXJ6PZpW={-(s7Cx}2_zN8OZNlnqt=A-> z^B#9m-Z$G5p3h(Lea;I@4RA;qD;T#|MHtXCLKZYG!5n z-I@UDG(uaY%wje1kj^%6!LjT6qqO8cfBAvBy|AqYzQB0H00MWma@wESZmw?K?*%No ze=kx0qIn`a2r*<&SZOMs*|%}VeYjtoZ~mwxY8hYg5V`kaUHVZ;Di|+4_9Nf;tmf9n zBAx-diHXJI5;WIGIzzwNc{UmOp7S&QT(J4(Er5cCI>YCDs=6$J!TmJLhCA`=e#D)H z(}UHlwU`pP&e54}`rAWuO-~VTm{uCB6Zf^b;1w??%?Ap4CLt}y6MRC<36c82^RX-Q z0U_%K80FpFDZAki_XP(V!jF5taAw9K2Rk1z=duZ3aK_;OrEQ zVoro#>)f?4?76wr$nrQRfpp#=RYVP&RUUFDyVmE`DM3cBF42!;+q2dRiQS9hT?ZSh z2OtP&VqixHBdUd=yY)@lTh0xY#Z;$_T5G#(}$ z5}t2}Qz6iK%or|T>~Wzz0NY>7*0%UDZAT4@kW9UihJ@T5&1>c5 z?}doVOIs$-ymt0`fh#lTwSI!+ZOA?%;)u#r__;ncWpcntejbikUENshmrzQ3GJp=k z3H_senk4-ICG>wrLD@`^B;hMM-mBaq`iTWv3lTo+IsU~EK+mA~P1IRt*ViizJf=Ub zTFoZcN}+&49sH6l=jt@GkB(0c2ukuPs|?Q1U9Qd%DBEVkYnD*xoZuXAiRW2s_$|cE z+M2~ja8D|f-Sx^xA>R?Iz_I6Sn0-d*Iw+zRq13N5(dV5!Rtoc0s zjf3Cpj!;B25v7#Ggr?D&&&W>~-WH#d$5g;KCMiit_1xZE_4M?tI+>W5eEu968(T>| zEW?L%uXYWLjADNLh^r0`4qlx&hGqR6fA<`13sgEgk?4t_U|0#roYC9r;4K=dWV@Ns~V;AYCR0UD6gzl=B^&%cl3-?P%X z`0s{YVex((hchtIfL6!&pQ);d%KW!U{AX6kaQ>OEKeqq>2`^U%WQ?%N`DZD_^V4nE z;ZP&~s@>JkRPl>V`7iG}0&(LW<3HKRB)sou(4-T}W&To2^p*2&E?u?F-5}MXGfgPJ z5WP5X8l+?&#|vkTw5EHNbkT-(<^`KOJZ9YC*K(V8-de?nF~2e}l1%+~>HgQi$jr+w zzIJlpKWq(P2|8?x=ao~m-E-c`#w9636ujCc3B7-TLT0!`T4Kn~nT3Y2wA=3K#p28r zOnrW0j`{xVD58f49?jI}45@e0UbP)`7Dw@ivOCtn0<-T=KmB|{hTx3anA;mWv2=?$ zRf?G}KfrQbzSn4SK7v?quL-E@_yMaf@^p{XYA6(zwD^{g$mT`OR36d-oJJ`4;SgUtZ`ahT3lD3NqjjE zxgHfbtfM@pv`Cj%T)#i;{E_^obo*PZ($#3lR!f5?%Kd3Dhti@8Wd8J4av;O}(dC6F z-(QD?#)?F1rkA=&r;DMAdyBSuQX=l^T9zlt^F@VbrltGh5PE^<_piMwHA7KrpTDjn z!E)v-?$R8dvTLZ#&8W?-Dje;Y)fbgRjK|K~d@d2hc6C0KL1h%j$KlAaFoo!~1)!KNA!3F}u!jGQJOipwBbme+fy^7L|>vWjPQ)M6z55W?Q z<@e4WcG2Koitigco;_TQG3YuU&2DzEYe_KorD3=}N*tpJPocVsA9|6v7e??+_+YVl zM=Q{&PfEyGNuvRzA@8o(lQR8rRNbhD_U%Tn^eXSb&-F^(F&S|bG6hG=X>IMExm0xq z(V?aB9>+bU8Rr}LH)>y0h5Ogfjuj?-9vv#H&p0AzY4l`*DJd!XgUFgwf)3-&=l4Di z+zsln->!9p%391$SQyZeFn1`_XI^w18M+Ck=hiyTlUwq@o{$$J*}P%p61t6@)!ew4 zPf-6Dtlb4Mlm>6Hl!BWT6&+fX7-c6mb1EMP;P$yYPC-qiNGCkJ1y~x24kXC zO4U7#lQU@wN+aa1ZCL9qz<&#$?fOOrE(t$vStVzBDNsQOoB1@rDsp<4Nt*`d;Rf?RDK)4>F4 zm#|-HMQiLw?5B#?&L{~IAq24|x%0p98dIK)DwSs!nsZUSQPcRjsslMO4FVDVVCU}B zHk&7yE~OWCH?6e9T;I@W+!Z;J^s^$7*3;Q9lN`?QILXicn0h^!crO?7Qcg$5HrZkh zii>glrg|Sfe~GRZ2_7O48W7Q_n&L*Ir@eWX?Wd^5fiCsrgaWp>WVtu<42MLgvQlq# zKV2ezJehX&xXd=x`Kh(OF*Y-GL}xG~tX+Q>&_Afhr<5#8q<4}xHOj=aMon~^5`$9} zh*0S!4O`5uD0j1Bu%LRt7Q!>+fhD3A)}F-U_P*1QHgmYbpQb}L((o~)&q__^#BI-Z;l$(7Iix*P~iwmSri1u^Vsa z&)BB%3QR_%=+!I^y#+(=G-;(s-QwL>l1{0MZw5wZ8s@va%)VAFwUqM|j^U(uTtmQo z@vO}3M{<4bmUr?$x;oUaItveW)k3}NOnG_xJth;YameGUEu;MT_G;FYw4KAJs>a5O z*zzk#xEcck1J&0bogaGg#q7>nE$;M^y7!-mJV9M}MxT%32Me&hscKA)V$a)B8|JmQms5*eWWOqgxaXH{M=id+qE8e#iw=h_ z_fJ#5b6{ymBuqPt^Xw7^*AQKF*&p=>A-h%{etS*-#^!ek$On4bMVp#5!m1xoxy7Z5 zpVpH=LR;wC+LBzY?YSi><<#S|zBv}7c;(!92@`S1uHNWQeCPP^oqG2Sj08KruOERP zoko4xeR`AZ+sDdo2!KF;gZaM6s)Cr#V1lf=b&3`}fysPj0!qC)i? zoBLOgx1eD#$a2(n<+XL{$O2pdpMVMD`{^7*i`n7s`+=bAojn%m6!!0x3fsHI12AsO z&7XK(t*G0ZEMD%qmtyiNQmeh4Z`O(=Nfe3W74;t@uj(9e5Mn>X+^@JMm{ZrbxSW5R zxX0FA41KK@*4UMJX(qLis@3bZgp)#ZW89pvsyxu(2Rz*Bw&mBjrhOZ{h6s$NyCd3* zm4T*Cly>FUzy}5)Bv4;sC)e_n=QWgxg|W*fKRV5ye$ub=R2xUAr^e%RX74boVj@~P zOB@jeV*OyN&7DB7C4(NM&^HD0G;jH;jJunUh+VDpZE!f;MFBOPI#J}XDyI!!)%48p zJC7~sz9CS=+v&?~9Wa{ph1;oznUP)#?4?wDt~RiPL1u=dWF~B4*^kwHhta@IS(_wN zthWM^nXDXwuLWxD?d-p%H)r$+4>4izjE{kD^FB zdj!%UA59iDG1^Ad>_<5>7K=C=1)vfM)~(PpW#58?wfQ_M1~q(k-@BIAB5AtWMT`~s zSA=AR)u;5kTta`wz1IeVl;mb5{ft;gZe5pFct`~;t9gUZ+BGvTZYCsu8iNob^|(qs z%|^t@j#^fBKVAEKn=yO~6MhZ`R#+vB9(4#_uFu*thZFPg%YnQTOxF7nX$uH8@^J@U zz3f*?NADsp*%y{z}Pr-Br+o6S;4SzPkJ9kf{KE6 z^=6aR;P~!GGS{6)c_iSbcm`$;*`XX>kBT<}lQv^H5!y4{y~C~2Idglvl5V$sBBPF$ zYy;tsL1{RiwJH0VneAQ0o{8z}II++K?&6g9et zt<~wtK3PWk;zqw} zk!Imm(e0#-~$fk@*!;J_>1U#^U7eI32zZAJ;6ldhlw!@gT@d<<2UfLf4A0vL1 zx9UIulB>;Jm0sgOIttnvj`HorM>uGyEs8=WTs;5_J#hV84CLKLMkAbP$-<&pqJISK zUJfdXe*BcxdYgy(3-<;#2r_p&GZo?)ZC5fmLRHs2KpYwP0Db-C-5kF4I`h?z2^bVp zD(1bLNKYmZbUZphkq}2hMpG?CvN`oJYj}HmeUV3iX7^xlnUa!2a3-<5@ao~;5#1u0 zBDb1asL*ci^?rUN3>K}fF-62B=hfGSY^=>{GqT@Q@~k5+PgfT|Oy2*2YpBe_hCN)q zELoC;-CYpv+dSa*duNsN*Cst#BM*yRTEo|V@P>YE~TR-4zc6dRca8-q({{Q5A1slH&OG_^XR?%1>Pk>*UiD|7EX(j^r~_%e{c?r6A6%d` z*&oVPaSZSsAq9pfM-xBNri(&GulC=~<_lf3uGT8!D2-QYv6^~=yl-33*+CPkPH&TI zE$sE}>jA0gTuhSs*|d3f1S~`@EdpxL!qX}upS7n(wABU|o0{&YSQFDrhWV#PMi>** z87~>wa1F5VUnlTFrqnOas&Qs-N2-*^o+9{XA+E!4eA(<-h++Z5&vpM`WdpjTm7-{`yDz#DduO97U}If8;^=bbhpny; zgd&`2>L#{B0uxm7at*k$u=BS?$_&|4#H3;^4;^Qae}+YV+E3kEA)Ep?8tKeIbWj=) zs^(}bTIRc!xOUtEmZ2+PGWH?+xjW`@aPudq*M;I{Ww?bV4+|5WcObd9^lJbw0Z`VO zW(}XBvbs_J3C7`a@0*zuFJjpl9)4yu7EleRdWf+3y}nv#h+)(&9Nu`|B7(YYc2K71 z)@bdb@)giiYGtgin}irE2F3s{6t)><9S+^MnoHT<1gFoj(MZg(+LwGB_^!+N#*-I| zuoxW!Cz4Q?jV()5@amnCT$=HV9?vr?z#JVqzT%FJ z6b1%H3-3oD8aFiCoUU?~9*S7*%M3v4K3L1o5<^9E!^r7PrIhkK^+z79EL@jGCj`Nr z0mMh=dkZ!88m5CGexGSqK-Y2$MLknbk0(JgiJq&@mn`zD*n3 z{dsd$#h-C_u3YM!rL8z01|K}Jp|kdc)Idvb`tz4jK;t4nRFA-Q#MX>rucL>*#6F=+ zeC*J6@hSZQjVX5?5YnnuZ%kyw;s^?kPkjpqJp(=iUWhQ!Uw;wW+f8i<-+{axUG#12q+*KaDve>@jerF(vo?wWegH#!)YO%& zzt>mfdeM4)byuQ)U_Kezw`0#td3{LPA!ySI$DsC=9N;Yb16~cn+Or=F#Pgg(jh)c1 zI{>X;ZhwffZ3)==z(Y(_GBj#9NaK@;y!+XU(DUZY2W|6hMBE*uC!tE!d%{v6avq9rKOv zsk1!2)9W)}JA#>ON(#YY)%yoUHr8d@Wsobvc9)=os-kSk|c=9`P=v>TyW?1l}>rD7nw)O>K}w= zx{NdXCt{%KSFyO)~NP|8UvK{Q}UA(CuC{F3%IOPY`|gdby$!=xMSDT3H^0 z{tdE7qo*h=3T+HF;jsM+_Yl^@O@Y{RZT`LQZiE~En$-mkN`1m$%J}!v9l3ow-^QfT z@cae$do91lF0aVKdiqnc0tz1flKM~L>yAQX?z^Mgh4l=H>L1_LMwVr^thl&13NaF@#k0Rx?^ulA`iplUy`9Adp}osb?Iya( z#X(pfNH(>&_m)Z^U2xV_i@k)OR2_+EVwarmB|(;^yJ-+luP4x}u#y^<`fqepC?cJMRY?38k6(03yLcF~M}%>5~_fKjv9; z*br>Vm7H0TQOm%kY`8j}uP^V71`sG^$i(g4jV3KbyY&0T{#s; zN^3Gg%Dvp20G6OR??O`^C&R)Gx4-qJeuqRH_+Yz3mvIf7dL|&H$kcZR zsovc&RYp`OB{z;+vC9r2_+PzdRiH&nq>O(eWaCY~rqYVf&R#kFTtEtcv=B?X_j1|y zRSHFcB?dP#68l01Fw>OthoL)R!sI!5=Y3<=seI;niQ?2x>5I+Y$XdH|hO&R#4e zwb*Uo_m?Mh{6jU{p&~6tm>)=DV6KQo^(9nLhXg(jE$}Z|4sa``0gR_2_tV5@8EM6e zV(pB!sC0YGtubq6Pe8u9mX_Pl5%Qa*^1XX9dbxWnM-gbv~gX9Q-4hO~T%ralLEnZHAAK4qV zC3{S$;hc2MG+1h{78Pvv-&;FUX7Z;C9WSsae7auDU0;_V2!ZhBa2_&)j*s1l%#92{ zx*B0*5a`_%iL&^@Ns{X}Sgv zdZSavB6Z*t8*Ba-B>rLfhi4jugoK5J-n=VMM=<;YaE64g_R(t(`lm9^#?&wUSfhO5 z9FCTZg-G1&n;W1aF`9+qU@XQN)nN{c89jCiYpXmHt2`^qH%<-@_YXXmzw``z`$|CI zZok)D^Y&6mD5h?IaKxOM3(|5eIe+i@mtLhqRB?lGfcpMhCakYiETE{zJcWT|!ELMU_&-G9AGuu@m zSW`ZQ!!!Y;*E>Qh|2QN*o1pLT`HfXnJNSl|-^(K!11fjo$E^ ziyi6MHgtc?4+U-{4&Tv#|APe}mGA+wp%%uau9(j-GkrGrNq@DyTH>h!Q;xMcn=O=8 zVdG&8MG|Beba9f2{W8_V{`Uyg657=T;>YN#Sm*91NH!)(qKFlWx;rN@zRL=sGp1vX zrDB7^Z|Q@saa^fLQnk?bhJuB#%vY)dF}Do7p;nh^v4Wk|*Ww{rIIgziQ?N%JQQ`9}X-Kj+m$67V z{&*%$l_M2Z=>7&KV@{fNgsUOiUn8d@z*BUflU#XjBu|m?4d3Zowr{R|;}R?5 zUCU34Pp9VkAF(eq5dq36I52T3yswyi=B$HUy?1&L3V}%pncJ`fgWZ|)FN!DyH- zXJ!>kBQXEOA~=Ef9XwYjOzTUSn!Xm`r}pq*f(2!hQjVvK`+Ue_&~kU3Ex=u*kTs;dN5O7J+a)%8{fGu=PE$)hLV&++ zT^2bnquK>UZP%2-`W8^HYfAEGFN<^1^YSB)rP&w@jn~(goA2isC`Y(Y`}n!#+5Emh z!}K@8l}?&->`qh49IUN(cb0CSP*+V+)YM&M3cFV5>oYNsMAu zQr&)cK;bWCZ6X0(gZ(ltJI44OA?^>&gQSTWeQ#$}^nrUHn>Qt{X3_E%|J^rD{Q7hGWS#k}UswTRL;&z1zRIgIR%#Q!%AzIBBGwap)m6Gu zSM6~VkHA{Tk#mCJ!>w1+?A+|&vFabZoCa!TEw^?g0O2&JoP5s683V%R<`z#&zKDLiMnA*rVdVUn2F9Sv__Ue8?He{}W)zG7Ik_@x5#W{cy}qyAgx zM~J^X-MU+czjedvXt)CZmnqLRSRL^e~mu zZtDq*=sOJvKZ+Xe`{0~R&}@|cET;bO3mv$r8OU4)j*nSwCj9(VD^O0)J3}kbwm&iB zQfr%#&mYI@)E^-0zk`kSnohI)5yIHYDjAiXput9XLhD*bh(;J$MtHdSP{&)JzSyFx zv*3qs-<7@ja1{2&t9uvhFhQf*tK4b0R8LnwpGLbE#f01M&RtMmFxoG)uU?49H3tTQ zhYMb;td!3g_r_5bv;sq}1Na0hF6CqvRu)#VfhjDhBp3DWQGRw_L_((R>wBZsl^BWf z#C##g4<&5JIc#N_C#NSYo^rBT>Tz!n+u)#>G=Ygi$pM5=vzF(%C~Zfg7Z6;=z0d#9 zC4PwjtoEh=>G&k#nL-BD`I$Z~M$=nSfA_n?HC@2wmkruZE|x;neP-ZQ>$7C;9-bp_f zX*6`JY7@n240o6F9v?X%Q2e$li$%Z`TP)Sw`~X~^0JAYQFc6J+%Y+)7^wH_Gt1b5N zbSUSw4dh({|4ny=Q)Oj z98Gr0+U&2Zb08hJW`oZB0y8O4uH3q!lTxwzyTkMM;HCpWE-E9^UDe0!XsI3y9REnW zvLYgxX4$$CbRHWydX6S=vE0rXC8=WRq65$CiuH57LT>`sn*O%l{ity#ve84iR!w@P z!`e|obhB(9=!_3J*mXtpZUz}z*y7mi+TZ}Op~nktJ0pPFGcrg8PBFx%R! zqMfj8Hl=I}28O`?2?V`ILm!elr~4+7@9!&Ocm5j{6I;c?DI_l zs4ErX{=GvZ{g)7+bU*mk^tr;gfV<2^8^gm-`*5R0bpn=a*yu;o4p z5u^&AFfver9(5JviQf%=^Ra7C6<=Qv9*xfC9{t{aS?TXOPsw!er9xg0FY&@z%roDVuMgxQYM73Bp<+4G;AmRTbIdAqr3Gx61nyYCZaSo z$KlK!KS>j(469C$9M4X68y=D^02;R>L-=|d3$6XvgJdBpjfnxkr6^8umAo$D{!0)y z9LF`@urrG{Jy3a%YD45NK~hJ)yV3H$zpwh_@4J4q4iI(gol+|XKVFC`|&yqkla#+{d1%n z0Z6^U9wj9S6^I)Ni9M}1W!vmXr#w45v9}M1`;#7R(Sn*Vm*sttZv^(-jXLI2DX2j? zDgU7(FtMd~)SZ?P6SvpuI6!&4NMP56Ah#kAl@0UCR*Q-?+ z80vU?shE@i(9Yi>A<8ZF3%L0t5bGWydJ zpY84KD7D9r3zFXe)(V~Q#yCZY%HlK0lYNl+Cx2vw zzC=$VfcPE;ZPt&C;ncaGKC)&h1cB-koV^Z#s9auioKxTKCn$|IM7R~7yt9x)IgKq| z)Xmt6^9Amz+$+$itE04!IQ5#w8x;_@{5`JdK=&_P5q)sTY^LGpd}!R>>Y`898UmTn z-n-4OZH>KgI%Z-+I}>r!YOolc>tzHgi-191^RWPa)VPQ#ITbYxd}opKxlOU7Q=4f2 zPe)mvdtn8@X{(KuET@&@HQ^kR)RQ(>ilxxG>vd$Chq1EXt|9Ihdv_@pUS}_{K%2rL z-uAq4+Qvn~<5OmlwXS2RgVam$Kfz5@(jTy}kB>agW$SM*f75}5QR9%Y*9WcG4Y%_n z(^YYJBmy>uqI_3ej+($DK!W*UP#DK8fYB-r{B*BjyLl|9e@#fs!Npr>6 zgi_mj4ARFh~M0Hpbj;gk6UIhW%BU`Op5c<7qSw|+LI-J?$+?zZ}^AOLFKix&_D zA^e%vgs16+d(@-B2yN}B_&?a`>XsJhv@djdnYz*j(Q9yAxq5Bkf6kwYnx-sL_!ls5iaRdHi5NbVgp z2wD$yS)|TIYI;SMiP@1w-VqB;kyjn2F90e>sw}MyRU(*aA{Ad3Wc1UM@!=|XUhWJM z&hv`-*4t@sK*g)$XWt z`5%n9k?HMLyTo%4Lf`KFQXlMN&DKT;fQ<%yTbVI$3b?euIliR)pxhWsYq4BpzMLif zkkE##zC4c&NJRe%@1Nzb<$=5oHUbNKxf~DFuWL}SMYgH>FjvJP&vH@2Cs7?)JR?{v zj85DykXtyb2vNOJR9ciWJ9BKo)7Z z;%U+}2ThdDKz0OO9TV$R?R15Riwj)&ig#=xIx#;;igdWH+oTh*y(ps$_7)JhTT4gS z?8UE3U^G?cIsnC8OuwpR20p8b1vg}34H!KSWc`WLzZ+h`}6pBg--Wz|9@I;)!Wv9e^28%adC2z-W?9T9rkMP#|-|^^6pDx zqvM)2?R|EZZ0td`F8~s1IrrgwWi{|K)#O|!g2Lo{q}}J$`SSt7LYzDk&TUcJX2G6 zEmVI#8e7rLWna6Rg;!vLuF^RLVoHs#JlV<6Rsa@ief=R)nD|LGwV^bzW>g~92VPp! z5Gm}sy-O;s6g5EBtEC1=b@tC(+$N#Mkneg{wU;djAe%0o-8-N3gMtLF#T#7*&wO3@ zZmrlIm%K>XA$q!qjQ?f^Y_}YXiG9?t2VqjCX0;0Ji<6xR9wD$h6U5>qb5R zai`qWRs!CJ=IQj{nsutz5gfx8p1iVuG@n8y?HwThd6o>3H^upDLzS27WLog&p*Y4ul6BL{Q=ciz@iULo!b@mshsvEAe|CrV0bagaO)Q|Xq= zqy#EY+leETg2MLJri zcW{ji81ZQisr9P_30s~oH3%~0S`)-dGP*{9Ek;e?9YUpzH5_eN&! z&jV@74Ly5W2gKJI_M_4I>meB5D7U%vKddBRzYMZke7HM;dc($Nzt~@BNh9yvlkFcE zlg!3M^&0Ve7H4I&8}|h(Z#AhjE7w5XYd4^r1565(AZj{R5Hef#+(dmGT-ytMR}t&E z8GRYq&L`7Ek<+3}Xs5CQWGN}*VD_ROO>^5t$Bd@zc4S-DcfA*V$*m#}6?Us}^K7`u z^6Aw8L7|_T*UXm$QV|kK<9BkGyLEb#cBbP7CW&*oSW{|l2Dhhuv{}ZvB}J!$ecD`A z)zy`WRog`>r*8dBu#1g8S=_6wS;|DxyK8xUt9sU$P>{I_E2yI*#XEyf0I(hLVI zTPEm0g!ZHUgW8s_1U3+RiGiyNvYc%s6yxdryPPs}&g;PQ-U=)@M9@3)@{=(u!u zH%(MyY|IUy3GSAuuDWrr-bEV}Q^QeGOj$#p89Y@>oa1Av$SG_qv`s}Mm%izk80)7N z58kR+AsGEOw;Id7|8;QbBU6>WB00i6b<(z>d+05q!%B)Mm1JZ&T=3=;kxz+_-s4Bq zpu{gAo<^7z7WQTj`n)6E{d!|N{o6o@fCPy7_sFs1&qo3@0@exv%xXZcDH9n48}?zKdI}&P2#) zMkI>cPcBdi+`gy2)7J|?pJ@FnQ%#@m7AClqh22;YOKDm`MTj-g+S0~QR%VZSncP3j z#MA{}4hPCi07xFVpX zbq;6RP*xX5W`>t&DNzrbi$69V6H*VMEPR;|8!>YX4E%4EuAcG3#3vd4ph(4srgPaO@9Gf#YmVq54EJpf+*R)^7Rs zg0GuiCWTEvkONUV{dlFlQ1*5NeteYunAufNL4kWKP*v2)(n?zKYqe||ZRG)Yuy#dF zB&MC=U5&b1Czg#~7S1P+f;YbxR$J@JZOj03ZK;TlLu|16WY2hj+QNcU>u5h_4niOJ zOZI3uN;F{e{Xk5V{(uNFI;JpJXgk*@5aM}93L5r49_UB88D=26n@bu(LSF&}1E|++ zf)rrGfy$UeoAIv@sy!3&aSbySD*(i)KiwuQ;Q~;}-+xykOppQ~njVl0pX6~xIt9>( zKz&PZ9IVp>h?P*X)3ZG~VOfm(n)ZpPO0XJG@>56IFSLI;)Xn!8IOwgZs_36N4M0FF zo&pjRLn8J|0~F+pz!^|*uGPyHtEi!OMR8`WlU;}Hk^{g|IbSFSdUxHP)ZK=kzD# z^|O-knJ+spp0uQRnLT890R~E~1q#%@l>{aZo9))oSlRsC1IjZ#DkUY;1t#s)IemQy zN4o!*C}?hrEf6J=YOBhZaMg>`aRHdeSiG;QHsCLVl*+{L!vraOc4?OPhR?ENzqfX@WXOBAg%S#$zG%mH#F37n2YUso3Y z#25eR{Qzrj^nZjQ!ZI3hGFkHfq8`YE|6%0l#WszZEWZY(-Pay_4f&Vk<#e$xPhliJUDnEE8VjeTU z_MTbMB-u5??{;i5n4QEatgJAKf&Rt&utF45f+6ttwg^QP0V@W{;OCI+}$~jEv1O%k)HgYWR83sZCGG zm{l~GiyE^W!#T=ekSY}S@T=qVtC}wqcl#7~pRTg0R~l>9>_sif7t0XnP0%E#SJr_v z$OqGu1x(DZMoriu5NNEXgO-CqjYqTFx-Sb3A!Rh*`PG(n<1J=#zm%0%wd>WGW&LVE zc6P_lZ>FZ1k)rGGLOAX`u9i=oG8>dEK8}nkYtWHyk&-7zmw8N-{M(=3Occa{e3B~oknPVp}7oQ)@FKkk$9 zSOz5#fz?8xvlTloVkhMGMXV^7#W#oUuC+%o`G#`Le|W&IZY~W6#RG!pEBT~FwF=Or zu49kBUMzk64BWL9uZGiUJjqsrNXW8wYF6mS1P7%AvIv#ZOY#f5_#dxV8pFqW&aSR@ zYh=R28}<+Dg^ac(NuwJaR=3JN&E>iZHv^OT`-;+wmJct0Nw&6J7-cOKH6STVnfzJ7ZoBYIkdmM7GX-5l z^^=9-y+_I6#e*iij;RqkBH^%b8?eCbV)kujn5^thYFGqU{@F>2{p@NMPE-gZKvZ7- z1<+tMyr+0uuGM!YJcZ{H048P4!%9cLk2~wBT)MrTqDRHF_O&1lkUaMY9jxTVOIwo& zDq-kjY>KBZB^nHt=X$p>HgdI6Qp!F-H3%qQam4f59R;rhXMG&7L#xdu!>8j>qmi%V zuLDZ=T)!^GI?vMuqO5%4=LxbIyfE$&ST+Ae#xqfQmX??}9XhT>!;*2ND63Kyz7-)7 zVkfGNv(s5XMnUk|rbhI52pE&`aANn&2Yy}EbQhKm3Vz$D+LX!H5!8VmE7RdKx;hG? zE-E(dMK{fF!Ds!+o@3{q)~*Q~qJwJ{#UGM%)<3;JJ zeaMz)gKf5jzTqM^$~AxYVmQ&}CGaQlq+w@_r|pUfu{_@bFOiQDHBSY+@n$%z5qt}= z%gn8P$;du2LqtIGqRyy9TEIE!(Q8PxX{0%d_6=w2MqPd*Qy?D{k` zG;8Sr@cyui?)M8g#H=MJS{V1lns(ZaDh@bA*Qp<*s}qmSH#75cq8vZSM_W(enwk#f zsr9!r(f+IRu^xN}CkI+(OR0yA(h&Dkzd_@0N=;SG!uQz=sCx===j`1 z>XjXj+tstf{4RsYc*t&F`3h+SD2bdJzWFkcbABhm!|CE>l$f#0PK4^|Z}J3nwYqAr zDX20buyV6IxC8x%bqkFOESHLBrYMPrInSyL7kn3D)cajpiwGO`Pa}A_W;S%={6e=-<&%}F~2|ms~JHgZZ zz6)@sFPw&}g0oTtm@WM*4o-LZe{Do!^?wgMUkbztBYRY3FB_vZUt50h^vR&(BRS|{ zibFQgwa;N>3{lpf4%xaitF@>lltlL$ZOpv7kPGZ8j2z4|0_(48jk9|gj8<2Ad{TD< zmGcVncuua%hqvuF_m-c_T(-&{g-;2QQ)xO4y5i0{AEhC{sfBkkH4jf5ey8+W@G#pP zpssiFv3h-tCbdIRsCl3$KuzJfKyaiR5AwRn8Cg`FbVafOqnL1*nOl1uRGzKoGzr)n zCu$$Q5Ned2Jos?rWa{dF4QJ8r7ab=?9Ma86m$Q)aJa4`p))Dc(&fB1%+j%4{-RGl; zw%2{JoXK|IZzqy>EL?<=qexrm7J78mlZt|7(hzXw-)SNu( z74&ynZ%Z4fUCmjWt>+uPw&R`xf{VT0n+KoQ_PF{@Z8$`YJF`?Ydb`!if{8uqvp0}QZ{>m4vM(`f> zUPC{7{5-z5qV+~oKfWCb=eY*npxKXGo;PdsoQ`ZEgv5T~};9N2=$}bH_Br2|U)~;ya^Y{{C0too(1*P`yiq*gSgl zSTS#>*Y}$U_*+jzD{{DLQ^xPhSQHeR!(CldMqD0Co!T14oPF`g{@(b=06n4uMaCjF z)CYV^n;ibvN!ysa%u`p1PK~A2)mLbzDBDwb+~3LhGK!#Y#V?|)ux<4IW=sn*-p6p6O#S=;(iL}k7_biqundKAzlW7PAWwhnfTAN` zAhHz!Ik#O0^qUzwN#WQ|U4E8?xL^0ZQ0^6}jt?4c)-DfA(5AZJPoDRqX_GMv(pTTy zoK$Oh`OB=2>Subl-9OtmEXUf!W2Z2DRk>Dx-3nenO!!sHO`=K9!S4ec&%4m{uY!0v zKpS0I%sSf_)n8{E4aDd_+~TxI;I{fPXrgvAfJ>7a1?lSzet=E3-!*0ZBmn#L;9wUz z=A2l)j{kHX*vMf$CEDqA{WAtyFHt*>@KB_fbZUjAUf!j?Gr^;?;d#$#j!X#}NaKM7 zF-Hb<>jt_x9FP931n9>56Cjq5(GFi|x;4&b7IioO&NaUc9gO8DsDwcC|bFr{F9lQW1C$}Ikx1i`?_{xbZ z0EIL!-+VQIEb!cEn6q|pWacpeiN&p{ib=$^n({mx7*xZ*2Q_bF2It{E+ns{oTBx*i zM(iA3$-}>94vrg#yyF76-77l6XVoNJ()Lp3Qc_#rLe;pqcru=G6~WxxfK5{zqz$85 zacSpd?4_i_L&5J@bilmtUiPO^)6<*FG@x>ERRg{=@YM)(tiiv>3eTitx$p=u1Ut7? z>FHhL9UbEx(;kO`@nqKf2ZLRqz{0URe`{q@ITh4xb|jl$_khB{{h!Hr$osB%&jTO> zMBqXLCDu)l$aS%S7b1Wn)kkUMvhTm9=UvWcqFUPhXfk<-ixor2lLXgHdJi4)4?szF zVfrsNfh;JZI9q@dQzalw?i(B8tMFaF$mr^wXD>q^TJe9GTF{QmkT{Lv_y~D7A7eG_ z+%76RTf#up#NuNGu>HslSEAwaZNW@Hd)8oX>*}fUyj7Q=!ZR-MtQXsTy}9|;KUlI! zxh#W2zik%c5o7@31oR-^F*xkue93`C=HQEB_t&&uBVYSdhw#SIs1?41fi8X4*ilE* z)zqtZZmo`-1|2T($wDXl{1+8%}&27$P0&(!2RAz?s$2gi#0D& z+fg^o?^Tf+cPo$1k?2zWnGay&5G}}!$kCgv^K@ThE#6TNc?UlZKnl{RLF&}PeDoL3 zRpiyJ(T|048_V)&UN_zS92TsrtoX14aZ63L{AY#d{82_fFcAL;KPONi04S5EI{#~Y z;NzAWCSPyN%gnsFzrMb*a_1cZpj1H|5G-7{(9_ei$oa;k$^H*-Y)p3d^y~!6HDq^m zbx9eg^=wd8mv7Y5)&`yKR3Q0R;6HF09LUso@PyGyN5==qJdpau`A_jDO_`G?PfGQ+ zfjgUX;=X&f96GiFqV|9c6%KxC*j>P@HaWAttj^mz+c`P7e6iniQ6UitabZ=UBNkL? zd6vuq9u)KNO^@4S_1F9T{W>P|uY1|DKiuwD=~rIgv$~6egg}P}g!tHYo>OJ|GHdJo zb2|zqg{^-3aXI^EK46aM@sUyk85Q=*v*euog5pozw{G2<^yqQb?S*$U{*?SxTl~HP z)R8XDvNft%FyZj|y?y z6uE3emX@#0DH8D%io>jZ2^w6<6`?=h+vu9t-Fv`Dv)!Ym?kGK7FpZxoK z%NH&Tr>L`IBePO~3aq(|y{>6wq*Hfkv5s z$-1Cp=hm#AQ=24an=Tp)b_KJ7A}EfRhOkr*hqC z)&BUUsJ#5UL4Ng)-P!*(cUb|=mTA2!JU5&5#l(}GQrqk8k37B{yd?^_VuYc=d1LjW z;(rWlFIP(Tx_#cZzQ4S@I%C4Z>S()nd3i0FKqXU)K7aUe)8+ZYbyLL?tARn!{HHei zwUpmxU^Gwme8DGD2|8kIMeyu<_m{skv7g@Q_x|#&FZ(aCs|Gb{=+P8LA=YgGO z577ATtL4B!1ue#?tlZqy`8hTp&wu^&<;w3WPz+dbn5tNbbAg8-zXDfF0fho+a%dUQ wV8j?FmdN<JN8COLAs<%y1QWulAG=h>F%zx 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 GMo}mmH8?Z`Q6%u?xBkwX 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 BuEGES literal 0 HcmV?d00001 From 9abadf5dbf7c3e18de100b5c934737330e40752a Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 17:44:54 +0800 Subject: [PATCH 09/16] docs: reuse local sequence analysis demo in a single example --- .../sequence-analysis-medium.md | 133 -- .../extension-chart/sequence-analysis-step.md | 133 -- .../sequence-analysis-target.md | 133 -- .../extension-chart/sequence-analysis-time.md | 133 -- .../en/extension-chart/sequence-analysis.md | 1368 +++++++++++++++++ docs/assets/examples/menu.json | 27 +- .../sequence-analysis-medium.md | 133 -- .../extension-chart/sequence-analysis-step.md | 133 -- .../sequence-analysis-target.md | 133 -- .../extension-chart/sequence-analysis-time.md | 133 -- .../zh/extension-chart/sequence-analysis.md | 1368 +++++++++++++++++ .../Chart_Extensions/sequence_analysis.md | 11 +- .../Chart_Extensions/sequence_analysis.md | 11 +- .../option/en/chart/sequence-analysis.md | 2 +- .../option/zh/chart/sequence-analysis.md | 2 +- .../preview/sequence-analysis-medium.png | Bin 14706 -> 0 bytes .../vchart/preview/sequence-analysis-step.png | Bin 14942 -> 0 bytes .../preview/sequence-analysis-target.png | Bin 20009 -> 0 bytes .../vchart/preview/sequence-analysis-time.png | Bin 18145 -> 0 bytes .../vchart/preview/sequence-analysis.png | Bin 0 -> 57967 bytes 20 files changed, 2749 insertions(+), 1104 deletions(-) delete mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-medium.md delete mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-step.md delete mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-target.md delete mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-time.md create mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis.md delete mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md delete mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-step.md delete mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-target.md delete mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-time.md create mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis.md delete mode 100644 docs/public/vchart/preview/sequence-analysis-medium.png delete mode 100644 docs/public/vchart/preview/sequence-analysis-step.png delete mode 100644 docs/public/vchart/preview/sequence-analysis-target.png delete mode 100644 docs/public/vchart/preview/sequence-analysis-time.png create mode 100644 docs/public/vchart/preview/sequence-analysis.png diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md deleted file mode 100644 index 0c44ba70be..0000000000 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: 'Sequence Analysis: Medium Connections' -keywords: extension, sequenceAnalysis, step -order: 17 -cover: /vchart/preview/sequence-analysis-medium.png -option: sequenceAnalysisChart ---- - -# Sequence Analysis: Medium Connections - -Use medium_type as the horizontal category and draw pattern connections as curves. Two links share the same endpoints to demonstrate distinct, stable curvature. - -## Key Configurations - -- `mode: 'step'`. -- `mediumConnection: true`. -- `from` / `to` in `patternData` reference globally unique `dots.node_name` values. -- Data and avatars are embedded; no external requests are required. - -## Code Demo - -```javascript livedemo -// Use matching VChart and extension versions that include sequenceAnalysis. -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'step', - mediumConnection: true, - actionTarget: false, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // Configure the generated VChart spec and show the complete initial range. - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## Related Documentation - -[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md deleted file mode 100644 index 4d59f92156..0000000000 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: 'Sequence Analysis: Step Mode' -keywords: extension, sequenceAnalysis, step -order: 16 -cover: /vchart/preview/sequence-analysis-step.png -option: sequenceAnalysisChart ---- - -# Sequence Analysis: Step Mode - -Arrange events by step rather than elapsed time. The top axis uses the medium name and additional count from stepMediumMap; the bottom axis shows steps. - -## Key Configurations - -- `mode: 'step'`. -- `mediumConnection: false`. -- `from` / `to` in `patternData` reference globally unique `dots.node_name` values. -- Data and avatars are embedded; no external requests are required. - -## Code Demo - -```javascript livedemo -// Use matching VChart and extension versions that include sequenceAnalysis. -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'step', - mediumConnection: false, - actionTarget: false, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // Configure the generated VChart spec and show the complete initial range. - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## Related Documentation - -[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md deleted file mode 100644 index 834f7afac3..0000000000 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: 'Sequence Analysis: Target Avatars' -keywords: extension, sequenceAnalysis, time -order: 15 -cover: /vchart/preview/sequence-analysis-target.png -option: sequenceAnalysisChart ---- - -# Sequence Analysis: Target Avatars - -Show an avatar at the midpoint of each time-mode connection and an arrow at its target. Avatars come from actionData.user_image; user_id does not resolve an image automatically. - -## Key Configurations - -- `mode: 'time'`. -- `actionTarget: true`. -- `from` / `to` in `actionData` reference globally unique `dots.node_name` values. -- Data and avatars are embedded; no external requests are required. - -## Code Demo - -```javascript livedemo -// Use matching VChart and extension versions that include sequenceAnalysis. -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'time', - mediumConnection: false, - actionTarget: true, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // Configure the generated VChart spec and show the complete initial range. - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## Related Documentation - -[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-time.md b/docs/assets/examples/en/extension-chart/sequence-analysis-time.md deleted file mode 100644 index 6733cbe4ac..0000000000 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-time.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: 'Sequence Analysis: Time Mode' -keywords: extension, sequenceAnalysis, time -order: 14 -cover: /vchart/preview/sequence-analysis-time.png -option: sequenceAnalysisChart ---- - -# Sequence Analysis: Time Mode - -Plot two users on a time axis and connect related events. Connection lines remain visible when target avatars are disabled. - -## Key Configurations - -- `mode: 'time'`. -- `actionTarget: false`. -- `from` / `to` in `actionData` reference globally unique `dots.node_name` values. -- Data and avatars are embedded; no external requests are required. - -## Code Demo - -```javascript livedemo -// Use matching VChart and extension versions that include sequenceAnalysis. -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'time', - mediumConnection: false, - actionTarget: false, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // Configure the generated VChart spec and show the complete initial range. - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## Related Documentation - -[Sequence analysis tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis.md b/docs/assets/examples/en/extension-chart/sequence-analysis.md new file mode 100644 index 0000000000..70b56449d5 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis.md @@ -0,0 +1,1368 @@ +--- +category: examples +group: extension chart +title: Sequence Analysis +keywords: extension, sequenceAnalysis, time, step +order: 14 +cover: /vchart/preview/sequence-analysis.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis + +Uses the local sequence-analysis demo data and configuration: 10 users and 111 events. Select step, medium curves, time or target avatars in the dropdown. + +Links referencing nonexistent events have been omitted; event data, avatar URLs and chart configuration otherwise follow the local demo. + +## Demo + +```javascript livedemo +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const eventData = [ + { + user_id: 'user_010', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_010.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_010_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_010_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 20 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_010_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'comment', + node_name: 'event_010_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_010_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_010_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_010_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_010_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_010_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_010_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_010_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_010_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_009', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_009_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_009_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 8 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_009_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_009_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_009_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_009_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_009_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_009_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_009_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_009_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_009_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_009_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_009_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_009_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_009_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_008', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_008_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 23 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_008_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 5 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_008_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_008_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_008_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_008_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 2 + }, + { + step: 'step_7', + action_type: 'follow', + node_name: 'event_008_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'im', + node_name: 'event_008_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 33 + }, + { + step: 'step_9', + action_type: 'comment', + node_name: 'event_008_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 2 + }, + { + step: 'step_10', + action_type: 'focus', + node_name: 'event_008_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + } + ] + }, + { + user_id: 'user_007', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_007_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_007_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 6 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_007_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_007_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_007_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + } + ] + }, + { + user_id: 'user_006', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_006_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 3 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_006_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_006_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_006_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_006_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_006_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_006_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_006_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_006_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 7 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_006_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_006_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_006_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_006_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 3 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_006_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_006_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_005', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_005_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'follow', + node_name: 'event_005_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 11 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_005_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_005_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_5', + action_type: 'im', + node_name: 'event_005_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_005_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + } + ] + }, + { + user_id: 'user_004', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_004_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_004_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 9 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_004_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 7 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_004_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_004_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_004_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_004_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 4 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_004_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'follow', + node_name: 'event_004_9', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_003', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_003_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 15 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_003_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 2 + }, + { + step: 'step_3', + action_type: 'SendGift', + node_name: 'event_003_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_003_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_003_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_6', + action_type: 'follow', + node_name: 'event_003_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_003_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_003_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_003_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 5 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_003_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_003_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_003_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_003_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 11 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_003_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 12 + }, + { + step: 'step_15', + action_type: 'focus', + node_name: 'event_003_15', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 3 + } + ] + }, + { + user_id: 'user_002', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_002_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_002_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_002_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_002_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 11 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_002_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 22 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_002_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 34 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_002_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_002_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_002_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 12 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_002_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_002_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_002_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 6 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_002_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_002_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + } + ] + }, + { + user_id: 'user_001', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_001_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_001_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 10 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_001_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_001_4', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_001_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'login', + node_name: 'event_001_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_001_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 11 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_001_8', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 22 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_001_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 3 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_001_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + } + ] + } +]; + +const userImageMap = { + user_001: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + user_002: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + user_003: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_004: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_005: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_006: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_007: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + user_008: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_009: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' +}; + +const stepMediumMap = { + step_1: { + medium_type: 'IP', + count: 0 + }, + step_2: { + medium_type: 'Device', + count: 1 + }, + step_3: { + medium_type: 'User', + count: 3 + }, + step_4: { + medium_type: 'Liveroom', + count: 3 + }, + step_5: { + medium_type: 'User', + count: 3 + }, + step_6: { + medium_type: 'User', + count: 3 + }, + step_7: { + medium_type: 'User', + count: 3 + }, + step_8: { + medium_type: 'Liveroom', + count: 3 + }, + step_9: { + medium_type: 'Email', + count: 3 + }, + step_10: { + medium_type: 'Tel', + count: 3 + }, + step_11: { + medium_type: 'Medium', + count: 3 + }, + step_12: { + medium_type: 'Medium', + count: 3 + }, + step_13: { + medium_type: 'Medium', + count: 3 + }, + step_14: { + medium_type: 'Medium', + count: 3 + }, + step_15: { + medium_type: 'Medium', + count: 3 + } +}; + +const patternData = [ + { + from: 'event_002_1', + to: 'event_002_1', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_002_7', + to: 'event_002_7', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_004_2', + to: 'event_004_2', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_004_4', + to: 'event_004_6', + pattern_type: 'pattern_001' + }, + { + from: 'event_004_7', + to: 'event_004_7', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_004_8', + to: 'event_004_8', + pattern_type: 'pattern_002', + isExtend: 'right' + }, + { + from: 'event_005_3', + to: 'event_005_3', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_005_4', + to: 'event_005_5', + pattern_type: 'pattern_001' + }, + { + from: 'event_005_6', + to: 'event_005_6', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_009_3', + to: 'event_009_3', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_009_4', + to: 'event_009_5', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_6', + to: 'event_009_6', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_010_5', + to: 'event_010_6', + pattern_type: 'pattern_001' + }, + { + from: 'event_010_6', + to: 'event_010_6', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_010_8', + to: 'event_010_8', + pattern_type: 'pattern_002' + }, + { + from: 'event_010_10', + to: 'event_010_10', + pattern_type: 'pattern_002', + isExtend: 'left' + } +]; + +const patternDataCurve = [ + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_009_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_002' + }, + { + from: 'event_003_10', + to: 'event_002_11', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_005_2', + pattern_type: 'pattern_002' + }, + { + from: 'event_005_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_007_4', + pattern_type: 'pattern_001' + } +]; + +const actionData = [ + { + from: 'event_002_2', + to: 'event_003_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_4', + to: 'event_004_4', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_5', + to: 'event_004_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_004_5', + to: 'event_005_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_id: 'user_004' + }, + { + from: 'event_005_5', + to: 'event_006_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_id: 'user_005' + }, + { + from: 'event_006_5', + to: 'event_007_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_id: 'user_006' + }, + { + from: 'event_008_2', + to: 'event_009_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_3', + to: 'event_009_3', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_10', + to: 'event_009_10', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + } +]; + +const baseSpec = { + type: 'sequenceAnalysis', + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; +const specs = [ + { ...baseSpec, mode: 'step', mediumConnection: false, actionTarget: false }, + { ...baseSpec, mode: 'step', mediumConnection: true, actionTarget: true, patternData: patternDataCurve }, + { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: false }, + { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: true } +]; +const container = document.getElementById(CONTAINER_ID); +container.style.height = '660px'; +const modeSelect = document.createElement('select'); +modeSelect.setAttribute('aria-label', 'Display mode'); +modeSelect.style.cssText = 'margin: 8px 0; padding: 6px 12px;'; +['Step mode', 'Medium curves', 'Time mode', 'Time with target avatars'].forEach((label, index) => { + const option = document.createElement('option'); + option.value = String(index); + option.textContent = label; + modeSelect.appendChild(option); +}); +const chartContainer = document.createElement('div'); +chartContainer.style.height = '600px'; +container.replaceChildren(modeSelect, chartContainer); +const vchart = new VChart(specs[0], { dom: chartContainer }); +vchart.renderSync(); +modeSelect.onchange = () => vchart.updateSpecSync(specs[Number(modeSelect.value)]); +window.vchart = vchart; +``` + +[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/menu.json b/docs/assets/examples/menu.json index 92b2bf30ce..d8e51f13e7 100644 --- a/docs/assets/examples/menu.json +++ b/docs/assets/examples/menu.json @@ -2119,31 +2119,10 @@ } }, { - "path": "sequence-analysis-time", + "path": "sequence-analysis", "title": { - "zh": "时序分析:时间模式", - "en": "Sequence Analysis: Time Mode" - } - }, - { - "path": "sequence-analysis-target", - "title": { - "zh": "时序分析:目标头像", - "en": "Sequence Analysis: Target Avatars" - } - }, - { - "path": "sequence-analysis-step", - "title": { - "zh": "时序分析:步骤模式", - "en": "Sequence Analysis: Step Mode" - } - }, - { - "path": "sequence-analysis-medium", - "title": { - "zh": "时序分析:介质曲线连接", - "en": "Sequence Analysis: Medium Connections" + "zh": "时序分析图", + "en": "Sequence Analysis" } } ] diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md deleted file mode 100644 index c0661101eb..0000000000 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: '时序分析:介质曲线连接' -keywords: extension, sequenceAnalysis, step -order: 17 -cover: /vchart/preview/sequence-analysis-medium.png -option: sequenceAnalysisChart ---- - -# 时序分析:介质曲线连接 - -以 medium_type 为横轴分类,通过曲线展示模式连接。示例保留两条相同端点的连接,用于观察不同且稳定的曲率。 - -## 关键配置 - -- `mode: 'step'`。 -- `mediumConnection: true`。 -- `patternData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 -- 数据与头像均内置,不依赖外部请求。 - -## 代码演示 - -```javascript livedemo -// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'step', - mediumConnection: true, - actionTarget: false, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## 相关文档 - -[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md deleted file mode 100644 index e803ce46ea..0000000000 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: '时序分析:步骤模式' -keywords: extension, sequenceAnalysis, step -order: 16 -cover: /vchart/preview/sequence-analysis-step.png -option: sequenceAnalysisChart ---- - -# 时序分析:步骤模式 - -按步骤而非真实时间间隔排列事件。上方标签使用 stepMediumMap 的介质名称和附加数量,下方显示步骤名称。 - -## 关键配置 - -- `mode: 'step'`。 -- `mediumConnection: false`。 -- `patternData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 -- 数据与头像均内置,不依赖外部请求。 - -## 代码演示 - -```javascript livedemo -// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'step', - mediumConnection: false, - actionTarget: false, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## 相关文档 - -[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md deleted file mode 100644 index cc3529a2e0..0000000000 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: '时序分析:目标头像' -keywords: extension, sequenceAnalysis, time -order: 15 -cover: /vchart/preview/sequence-analysis-target.png -option: sequenceAnalysisChart ---- - -# 时序分析:目标头像 - -在时间模式的连接中间显示用户头像,并显示目标端箭头。头像来自 actionData.user_image,而不是根据 user_id 自动查找。 - -## 关键配置 - -- `mode: 'time'`。 -- `actionTarget: true`。 -- `actionData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 -- 数据与头像均内置,不依赖外部请求。 - -## 代码演示 - -```javascript livedemo -// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'time', - mediumConnection: false, - actionTarget: true, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## 相关文档 - -[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-time.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-time.md deleted file mode 100644 index 37a1b34a07..0000000000 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-time.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -category: examples -group: extension chart -title: '时序分析:时间模式' -keywords: extension, sequenceAnalysis, time -order: 14 -cover: /vchart/preview/sequence-analysis-time.png -option: sequenceAnalysisChart ---- - -# 时序分析:时间模式 - -按事件发生时间展示两个用户的行为,用直线连接相关事件。关闭目标头像时,连接线仍然显示。 - -## 关键配置 - -- `mode: 'time'`。 -- `actionTarget: false`。 -- `actionData` 的 `from` / `to` 引用全局唯一的 `dots.node_name`。 -- 数据与头像均内置,不依赖外部请求。 - -## 代码演示 - -```javascript livedemo -// 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包。 -// import VChart from '@visactor/vchart'; -// import { registerSequenceAnalysis } from '@visactor/vchart-extension'; -const { registerSequenceAnalysis } = VChartExtension; -registerSequenceAnalysis({ VChart }); - -const avatar = (text, fill) => - 'data:image/svg+xml;charset=utf-8,' + - encodeURIComponent( - `${text}` - ); -const userImageMap = { Alice: avatar('A', '#5470c6'), Bob: avatar('B', '#26a69a') }; -const day = 24 * 60 * 60 * 1000; -const start = Date.UTC(2026, 0, 1); -const eventData = [ - { - user_id: 'Alice', - user_image: userImageMap.Alice, - dots: [ - { node_name: 'a1', step: 'step_1', medium_type: 'IP', action_type: 'visit', time_stamp: start, action_count: 10 }, - { - node_name: 'a2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 2 * day, - action_count: 4 - }, - { - node_name: 'a3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 4 * day, - action_count: 2 - } - ] - }, - { - user_id: 'Bob', - user_image: userImageMap.Bob, - dots: [ - { - node_name: 'b1', - step: 'step_1', - medium_type: 'IP', - action_type: 'visit', - time_stamp: start + day, - action_count: 8 - }, - { - node_name: 'b2', - step: 'step_2', - medium_type: 'Device', - action_type: 'login', - time_stamp: start + 3 * day, - action_count: 3 - }, - { - node_name: 'b3', - step: 'step_3', - medium_type: 'Account', - action_type: 'order', - time_stamp: start + 5 * day, - action_count: 1 - } - ] - } -]; -const stepMediumMap = { - step_1: { medium_type: 'IP', count: 0 }, - step_2: { medium_type: 'Device', count: 1 }, - step_3: { medium_type: 'Account', count: 2 } -}; - -const spec = { - type: 'sequenceAnalysis', - mode: 'time', - mediumConnection: false, - actionTarget: false, - height: 440, - eventData, - actionData: [ - { from: 'a1', to: 'b2', user_id: 'Bob', user_image: userImageMap.Bob }, - { from: 'b2', to: 'a3', user_id: 'Alice', user_image: userImageMap.Alice } - ], - patternData: [ - { from: 'a1', to: 'b3', pattern_type: 'shared' }, - { from: 'a1', to: 'b3', pattern_type: 'shared' } - ], - userImageMap, - stepMediumMap, - customTransformSpec(converted) { - // 通过转换回调设置普通 VChart 配置,并显示完整初始范围。 - converted.color = ['#5470c6', '#26a69a', '#f6bd16']; - converted.scrollBar.forEach(bar => { - bar.start = 0; - bar.end = 1; - }); - } -}; -const vchart = new VChart(spec, { dom: CONTAINER_ID }); -vchart.renderSync(); -window.vchart = vchart; -``` - -## 相关文档 - -[时序分析教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis.md b/docs/assets/examples/zh/extension-chart/sequence-analysis.md new file mode 100644 index 0000000000..056b589076 --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis.md @@ -0,0 +1,1368 @@ +--- +category: examples +group: extension chart +title: 时序分析图 +keywords: extension, sequenceAnalysis, time, step +order: 14 +cover: /vchart/preview/sequence-analysis.png +option: sequenceAnalysisChart +--- + +# 时序分析图 + +复用本地 sequence-analysis demo 的 10 位用户、111 个事件和连接配置。通过下拉框切换步骤、介质曲线、时间和目标头像四种展示方式。 + +移植时去掉了连接端点不存在的演示数据,其余事件、头像地址和展示配置沿用本地 demo。 + +## 代码演示 + +```javascript livedemo +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const eventData = [ + { + user_id: 'user_010', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_010.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_010_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_010_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 20 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_010_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'comment', + node_name: 'event_010_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_010_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_010_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_010_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_010_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_010_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_010_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_010_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_010_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_009', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_009_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_009_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 8 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_009_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_009_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_009_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_009_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_009_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_009_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_009_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_009_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_009_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_009_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_009_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_009_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_009_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_008', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_008_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 23 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_008_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 5 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_008_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_008_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_008_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_008_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 2 + }, + { + step: 'step_7', + action_type: 'follow', + node_name: 'event_008_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'im', + node_name: 'event_008_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 33 + }, + { + step: 'step_9', + action_type: 'comment', + node_name: 'event_008_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 2 + }, + { + step: 'step_10', + action_type: 'focus', + node_name: 'event_008_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + } + ] + }, + { + user_id: 'user_007', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_007_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_007_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 6 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_007_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_007_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_007_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + } + ] + }, + { + user_id: 'user_006', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_006_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 3 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_006_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_006_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_006_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_006_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_006_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_006_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_006_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_006_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 7 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_006_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_006_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_006_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_006_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 3 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_006_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_006_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_005', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_005_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'follow', + node_name: 'event_005_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 11 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_005_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_005_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_5', + action_type: 'im', + node_name: 'event_005_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_005_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + } + ] + }, + { + user_id: 'user_004', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_004_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_004_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 9 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_004_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 7 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_004_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_004_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_004_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_004_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 4 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_004_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'follow', + node_name: 'event_004_9', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_003', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_003_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 15 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_003_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 2 + }, + { + step: 'step_3', + action_type: 'SendGift', + node_name: 'event_003_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_003_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_003_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_6', + action_type: 'follow', + node_name: 'event_003_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_003_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_003_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_003_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 5 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_003_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_003_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_003_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_003_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 11 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_003_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 12 + }, + { + step: 'step_15', + action_type: 'focus', + node_name: 'event_003_15', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 3 + } + ] + }, + { + user_id: 'user_002', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_002_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_002_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_002_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_002_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 11 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_002_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 22 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_002_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 34 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_002_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_002_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_002_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 12 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_002_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_002_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_002_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 6 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_002_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_002_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + } + ] + }, + { + user_id: 'user_001', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_001_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_001_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 10 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_001_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_001_4', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_001_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'login', + node_name: 'event_001_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_001_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 11 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_001_8', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 22 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_001_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 3 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_001_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + } + ] + } +]; + +const userImageMap = { + user_001: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + user_002: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + user_003: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_004: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_005: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_006: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_007: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + user_008: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_009: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' +}; + +const stepMediumMap = { + step_1: { + medium_type: 'IP', + count: 0 + }, + step_2: { + medium_type: 'Device', + count: 1 + }, + step_3: { + medium_type: 'User', + count: 3 + }, + step_4: { + medium_type: 'Liveroom', + count: 3 + }, + step_5: { + medium_type: 'User', + count: 3 + }, + step_6: { + medium_type: 'User', + count: 3 + }, + step_7: { + medium_type: 'User', + count: 3 + }, + step_8: { + medium_type: 'Liveroom', + count: 3 + }, + step_9: { + medium_type: 'Email', + count: 3 + }, + step_10: { + medium_type: 'Tel', + count: 3 + }, + step_11: { + medium_type: 'Medium', + count: 3 + }, + step_12: { + medium_type: 'Medium', + count: 3 + }, + step_13: { + medium_type: 'Medium', + count: 3 + }, + step_14: { + medium_type: 'Medium', + count: 3 + }, + step_15: { + medium_type: 'Medium', + count: 3 + } +}; + +const patternData = [ + { + from: 'event_002_1', + to: 'event_002_1', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_002_7', + to: 'event_002_7', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_004_2', + to: 'event_004_2', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_004_4', + to: 'event_004_6', + pattern_type: 'pattern_001' + }, + { + from: 'event_004_7', + to: 'event_004_7', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_004_8', + to: 'event_004_8', + pattern_type: 'pattern_002', + isExtend: 'right' + }, + { + from: 'event_005_3', + to: 'event_005_3', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_005_4', + to: 'event_005_5', + pattern_type: 'pattern_001' + }, + { + from: 'event_005_6', + to: 'event_005_6', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_009_3', + to: 'event_009_3', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_009_4', + to: 'event_009_5', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_6', + to: 'event_009_6', + pattern_type: 'pattern_001', + isExtend: 'left' + }, + { + from: 'event_010_5', + to: 'event_010_6', + pattern_type: 'pattern_001' + }, + { + from: 'event_010_6', + to: 'event_010_6', + pattern_type: 'pattern_001', + isExtend: 'right' + }, + { + from: 'event_010_8', + to: 'event_010_8', + pattern_type: 'pattern_002' + }, + { + from: 'event_010_10', + to: 'event_010_10', + pattern_type: 'pattern_002', + isExtend: 'left' + } +]; + +const patternDataCurve = [ + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_009_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_002' + }, + { + from: 'event_003_10', + to: 'event_002_11', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_005_2', + pattern_type: 'pattern_002' + }, + { + from: 'event_005_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_007_4', + pattern_type: 'pattern_001' + } +]; + +const actionData = [ + { + from: 'event_002_2', + to: 'event_003_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_4', + to: 'event_004_4', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_5', + to: 'event_004_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_004_5', + to: 'event_005_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_id: 'user_004' + }, + { + from: 'event_005_5', + to: 'event_006_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_id: 'user_005' + }, + { + from: 'event_006_5', + to: 'event_007_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_id: 'user_006' + }, + { + from: 'event_008_2', + to: 'event_009_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_3', + to: 'event_009_3', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_10', + to: 'event_009_10', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + } +]; + +const baseSpec = { + type: 'sequenceAnalysis', + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; +const specs = [ + { ...baseSpec, mode: 'step', mediumConnection: false, actionTarget: false }, + { ...baseSpec, mode: 'step', mediumConnection: true, actionTarget: true, patternData: patternDataCurve }, + { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: false }, + { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: true } +]; +const container = document.getElementById(CONTAINER_ID); +container.style.height = '660px'; +const modeSelect = document.createElement('select'); +modeSelect.setAttribute('aria-label', '展示模式'); +modeSelect.style.cssText = 'margin: 8px 0; padding: 6px 12px;'; +['步骤模式', '介质曲线连接', '时间模式', '时间模式 + 目标头像'].forEach((label, index) => { + const option = document.createElement('option'); + option.value = String(index); + option.textContent = label; + modeSelect.appendChild(option); +}); +const chartContainer = document.createElement('div'); +chartContainer.style.height = '600px'; +container.replaceChildren(modeSelect, chartContainer); +const vchart = new VChart(specs[0], { dom: chartContainer }); +vchart.renderSync(); +modeSelect.onchange = () => vchart.updateSpecSync(specs[Number(modeSelect.value)]); +window.vchart = vchart; +``` + +[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md index c5b3830730..b2456a2752 100644 --- a/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md +++ b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -4,7 +4,7 @@ Sequence Analysis places each user's events on a horizontal row, aligns events b The chart comes from `@visactor/vchart-extension` with type `sequenceAnalysis`. It is distinct from the built-in sequence chart and the sequenceScatterLink extension for training-process analysis. -![Medium connections](/vchart/preview/sequence-analysis-medium.png) +![Sequence Analysis](/vchart/preview/sequence-analysis.png) ## Choose a mode @@ -19,7 +19,7 @@ The chart comes from `@visactor/vchart-extension` with type `sequenceAnalysis`. Install matching VChart and extension versions that include sequenceAnalysis, and register before creating the chart. The page needs a container with id="chart". -This complete minimal step example replaces image labels with user names through the transform callback, so userImageMap can be empty. The demo pages instead use embedded SVG avatars. +This complete minimal step example replaces image labels with user names through the transform callback, so userImageMap can be empty. The demo page uses the avatar URLs from the local demo. ```js import VChart from '@visactor/vchart'; @@ -105,7 +105,7 @@ Default vertical labels read userImageMap[user_id]. The image stored in an event With actionTarget enabled in time mode, the midpoint avatar reads actionData.user_image. Supplying actionData.user_id alone does not resolve the image. Disabling actionTarget hides avatars and arrows while keeping lines visible. -Use accessible image URLs or data URLs. The demos embed their images to avoid network dependencies. [Target avatar example](/vchart/demo/extension-chart/sequence-analysis-target). +Use accessible image URLs or data URLs. Use the dropdown in the demo to select target avatars. [Interactive example](/vchart/demo/extension-chart/sequence-analysis). ## Customize appearance and zoom @@ -151,8 +151,5 @@ When removing or renaming events, update actionData / patternData in the same co ## Examples and options -- [Time mode](/vchart/demo/extension-chart/sequence-analysis-time) -- [Target avatars](/vchart/demo/extension-chart/sequence-analysis-target) -- [Step mode](/vchart/demo/extension-chart/sequence-analysis-step) -- [Medium connections](/vchart/demo/extension-chart/sequence-analysis-medium) +- [Interactive example with four modes](/vchart/demo/extension-chart/sequence-analysis) - [All options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md index 8cdbe693de..cc7447f67c 100644 --- a/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md +++ b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -4,7 +4,7 @@ 它由 `@visactor/vchart-extension` 提供,类型为 `sequenceAnalysis`,与内置 `sequence` 和用于训练过程分析的 `sequenceScatterLink` 是不同图表。 -![介质曲线连接](/vchart/preview/sequence-analysis-medium.png) +![时序分析图](/vchart/preview/sequence-analysis.png) ## 选择展示模式 @@ -19,7 +19,7 @@ 安装与 VChart 版本一致、且包含 sequenceAnalysis 的扩展包,在创建图表前注册。页面需要提供 `id="chart"` 的容器。 -下面是完整的最小步骤示例。这里通过转换回调把纵轴头像改成用户名称,所以 `userImageMap` 可以为空;演示页则使用内置 SVG 头像。 +下面是完整的最小步骤示例。这里通过转换回调把纵轴头像改成用户名称,所以 `userImageMap` 可以为空;演示页沿用本地 demo 的用户头像地址。 ```js import VChart from '@visactor/vchart'; @@ -105,7 +105,7 @@ vchart.renderSync(); 时间模式开启 actionTarget 后,连接中点使用 `actionData.user_image`。只填写 `actionData.user_id` 不会自动查找头像地址。关闭 actionTarget 只隐藏连接头像和箭头,仍保留连接线。 -头像可以使用可访问的图片 URL 或 data URL;示例使用内置数据,避免网络依赖。[查看目标头像示例](/vchart/demo/extension-chart/sequence-analysis-target)。 +头像可以使用可访问的图片 URL 或 data URL;演示页可通过下拉框切换到目标头像模式。[查看完整示例](/vchart/demo/extension-chart/sequence-analysis)。 ## 定制样式与缩放 @@ -151,8 +151,5 @@ vchart.updateSpecSync({ ...spec, mode: 'time', eventData: nextEvents }); ## 完整示例与配置项 -- [时间模式](/vchart/demo/extension-chart/sequence-analysis-time) -- [目标头像](/vchart/demo/extension-chart/sequence-analysis-target) -- [步骤模式](/vchart/demo/extension-chart/sequence-analysis-step) -- [介质曲线连接](/vchart/demo/extension-chart/sequence-analysis-medium) +- [完整示例(可切换四种模式)](/vchart/demo/extension-chart/sequence-analysis) - [全部配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/option/en/chart/sequence-analysis.md b/docs/assets/option/en/chart/sequence-analysis.md index b479afb78a..dd1fe04025 100644 --- a/docs/assets/option/en/chart/sequence-analysis.md +++ b/docs/assets/option/en/chart/sequence-analysis.md @@ -10,7 +10,7 @@ import { registerSequenceAnalysis } from '@visactor/vchart-extension'; registerSequenceAnalysis(); ``` -[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Time mode](/vchart/demo/extension-chart/sequence-analysis-time) · [Target avatars](/vchart/demo/extension-chart/sequence-analysis-target) · [Step mode](/vchart/demo/extension-chart/sequence-analysis-step) · [Medium connections](/vchart/demo/extension-chart/sequence-analysis-medium) +[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Interactive example](/vchart/demo/extension-chart/sequence-analysis) The transformer generates a common chart, dot/link series, axes and zoom components from the fields below. You do not need to supply `data`, `series`, `xField` or `yField`. Customize the generated spec with `customTransformSpec`. diff --git a/docs/assets/option/zh/chart/sequence-analysis.md b/docs/assets/option/zh/chart/sequence-analysis.md index 2c7a40c43e..66d71063fe 100644 --- a/docs/assets/option/zh/chart/sequence-analysis.md +++ b/docs/assets/option/zh/chart/sequence-analysis.md @@ -10,7 +10,7 @@ import { registerSequenceAnalysis } from '@visactor/vchart-extension'; registerSequenceAnalysis(); ``` -[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [时间模式](/vchart/demo/extension-chart/sequence-analysis-time) · [目标头像](/vchart/demo/extension-chart/sequence-analysis-target) · [步骤模式](/vchart/demo/extension-chart/sequence-analysis-step) · [介质连接](/vchart/demo/extension-chart/sequence-analysis-medium) +[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [完整示例](/vchart/demo/extension-chart/sequence-analysis) 本图表使用以下专用数据字段,转换器会生成底层 common 图表、dot/link 系列、轴及缩放组件,无需自行配置 `data`、`series`、`xField` 或 `yField`。需要修改生成结果时使用 `customTransformSpec`。 diff --git a/docs/public/vchart/preview/sequence-analysis-medium.png b/docs/public/vchart/preview/sequence-analysis-medium.png deleted file mode 100644 index 1ad1cad235c961f2f27f778c8220828a9c969003..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14706 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 QIXq1&iW&+P@)jTd8;b4z&;S4c diff --git a/docs/public/vchart/preview/sequence-analysis-step.png b/docs/public/vchart/preview/sequence-analysis-step.png deleted file mode 100644 index 5a2eb23a5979d01235407b4815b6fe05b6b88c44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14942 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 wV8j?FmdN<JN8COLAs<%y1QWulAG=h>F%zx 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 GMo}mmH8?Z`Q6%u?xBkwX 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 BuEGES diff --git a/docs/public/vchart/preview/sequence-analysis.png b/docs/public/vchart/preview/sequence-analysis.png new file mode 100644 index 0000000000000000000000000000000000000000..1d885c34cfa89d644bc4859e05abf43bd9b3ee4d GIT binary patch literal 57967 zcmd42Rao0g7d~2VDO$96ixzjc7K#%n?oix4IDt|e3KTCAC|Wt@oXYd8e+3|CH+KojZ5%m6hIV-MRCC|IVFz zhEML@)=a+oIC4jynzCn+&y3~{+{yyr!_i$)`0 ztl`pJH4@oy6ekAgp

0-p{M7-pfx|lz&TVvUtiygM$y^D&(CjJU}kD6Dek#x z9q@|VedG0uKb|f9@`)XMs0FW3fw%$#P0csl_kpWwA~4*{Bdy(qF~TRe`4F{W-e2x*2kul+@J5 ze};igdj;{#xc)8V0RkZ|k-V!b@}s0C?-b2SW-3*668 z;WRuGFTfIKD060h`C{-VX{lDybD+R@Bck&TcQPXzvd3FlYwF=c(D%DwJuSSHAEbej zFXy{He#8G#Qs$5`)MX~E3Vl6<4Y#fnlh+(zXz^>59Iy_6&<)5RpGH~}?q0j!^lpxHT8;t^%Q z6MOy6A4jzXjZKq!={!@AqZ-f=D@~skhHL9{&j_`Cjd8nYXlM4wz|#40&7#*B_-<#0 z2~oFIQVJqW#H1x5m4#g6dRss7YBy>R?26ihr9@giE|nG{^^T%}V? zB8-;ig3{y}ra@@tv%LIL3w_X-dSc+=J8rI?tA&uOKiYX`M1};V0Z~>lf`~h{cy;Af z33{siEB zc_;17^6AWYp0+lg>@>Gl6uaj@K%vGHgd=FRwew4hckRRy^dlG*Lh_VYgPK^ ziSc+s5c3ick&qb=gyn`f?B!iAE1I3o6hh7zJNgd74sY1rgf16i!MH~VO}*mA<=>^=_p=8#yJANVOp9Tp+^uO>EtvPLUD4h zCbgLN6vx5U?D8tCczQ~r4ch*5wUNMJyvbeu+;X?fTt_zPt-4;5n~7mF0wH3gZw%O0 z(yZ_Jd9ahM4`UR=2%A=||4i27ka|>PK6qSz6exQsJ+_wv;$6Swyr>iL9fD6!tB{jP zz10#`Is6?Lh-wVLrfU&G+N)dK#H$Z-$O9@~}CA z!ov$kyfiVeLQ_oi5pd{Qt`GIn>QG^FN^gRZZSA8m1~APa%h^I?e`wZFyM9n=UgQQ7 z0V+J>n#|pz-7i1lHmuk`LS0j7%i(@T0m;EF>c!{6% zQ!`)y7vs1F&br;@_*@%#88W09cuB%v(hr^`Y&46KK6(@&miO3DFZgI!HLTl8x+btM z{dmV^n&9B~!}FgS^z+RfM=ho`6LP>Avxas2tC;HCd@`;Epn&EXq-~gbA5iincQZ9P zaFNwV zrFrldBA`pZ6#|-(`!H@@U5B)?GRSTtbxlyecsZ$Yr_Yp-zB7y4xR%Vz$=SDOEvA9D z4x7%h^Lp!cIqz^;N*-*hmbPN$HJcs#dS#RL+w{?uP2XLg*Y_LtGfiu`SC*T7)mVDi z@xiX@R0P9n+s-GSR2oH%+l0D+em2!`7|3$XsGT#>mfPVHF;hMzOD-F}-5{>clG~?& zOTeQk%&~@fMW1?3Nu1i!!}<=yyh-OiR-=f2z`C)+=TMT;2x~MmXX2OdDOrIMM@*&= zN1{}q@V{*;(8{&Q|Vog zd|Vk#XMh^Oz0g7+J3>Uojvtu&KJ(_nsZm$I1ET#QeHX_c@%Vfzv`{e-tX=|-KNfQ9 zI2OOCC(V_*Fk{(7-*fgSukJ}4a-i7QQ8o|8yx6`FN&#^oAHOjLFmr{G2IsVL0&&mR z>Ui>YQMeC+>cs`m-<3|kFvS9E3cs@x(uygc79+{eurmJNtZZ<(y2fd+=@M>>b$j~* z+IGmL_JjEy^-XGtvBuQR@M#AJg3JVFpuSxYgNZIB=cTA`m>e}Bo%H!(#bZ>7_k|o& z+UNwkp`57@h=N`!_+py6hO-{twfA_j{V3Tlid#I#*fVro#L-FniW>2Ce2!#b8M1%B7e9E@)kqr(mMeXlus)XwY}{KXRFY(eK?H!}a%O&tjH9JW|9bKWg&WFE4&8 zKrIPqq#>U2M|;6%i_XH>9s297NT){l2)b;y<6s<3SmYC$N3>ODXFk{HKXQ9sA?h2? zFXIAZyz38jFSahK2-~h`CEO70r~UiL2W%D%qXD?^#8Cduh7dWzS2^2s1=QgmLU3?% z1-SMfN;Hc|_%_vE6{Q>dK|-KN84}U-^vF z)2NQedG<~YmuGuzQITWj();1%4j2fXbU^H`TF^d10&PokXz~OA7ZCHmP>hl*k`O5` zNlZ{o%t1O;R{c?x@Jj9QMIPLPMgjIp-F{H*#7h5JW&P*i*yxb9vM+_64ql*IviwNatyiuTh-;#W1E$QN{gkqa?1*Mc;Zo?$OEb@6}slNz_=Rii_u{)g+;!(XHrsF7m=L>kI|Cn|z~VAW&E#A}^y|(`j45 z6pMK$F7@S&U3MVhn${nWR)s#f80##$3coQaw-PRSif}q?%W{?r=E)#%^P0F=tHprf z7i8G7m?z*>vD zI|MH@SFa)uw5@UUoH&z80>!vXdp_d|zF56y{=>RAfHf{Oc)xGO0U&}*Qu2JWk3rIR zTo*PPFojLUWx3UTxr?D)!?)oX7f2R4l$QF5rTvu@OJ*8M zQQD9RV>6c)(lpUT$R#uJRSR>hY|Hrs9D+@6z_cqny{+f;5x+spQf|VBW1+h233hT?&p03;yw>Yb$}w?jo{3*$@Lb zkmWq?TQBI8a^Op+`&s0XWUvW?HRM&4Wl74K0}TVPk3`%I0C`$a0#xwjy_>z5^{hjU*nKil=HN{1sG6X_zZ=4$8) zv(t8rr(q*+VGr{c2#ll|+kz7S9f$iAi36c3gxB4TndSP|86-fk{D zJ!8@WvJ%cNObI^tK@u^C)N8Hn66X(~t7t$PH$y#Zt&}Z1Za029K(F#eT)(&xpOrfq zRCJr#|85E;8Wf7mRkXMp*TENdm+(r3NkFmnf*S2Kuwt=UsdsS)*_syIemt6OJ{K5G z9wDyZVA^11C7D~GdRn!&r2i#enh@v>2ateZ9Wc{jP*f|*9ELA9gXy|JG?x^A_b-*` zt5~2$1y3BcCc^YN4e@AqxLVB?HJT~}^~XV3=Vo~V6h$>5XD8XLioRd5a-cBABO$Hp zy2UEJrLL|w+P7N@^!{a{`?EZvrUm~G`(>y9!vaoeMTT8u`oh*kcZ;PSUp!V6y3iUI z%&RFX1uYxr>T*yVd2P~+Vpl!qTVzD+I;1E(3r=KjBSVK{SLlv-mYneDbGNU^T0FdX zgg16grIcsYiq%ZU7iPaR=sR6d^SHEI(=UHw0hiu8zIsWYym7$M!BMpDYCL?uFi%P? zQS|f#Uspd}qQvJY*46z{R9^|U5!SSW35K

gXT~|)Wr`AAhX$W}QK)4!Rl;r2iXo$D| z#x36oZYlK=<1H#GC|+md?w6|PH{6z8)A^B3TgImr`%^LRd_QnWNl-K`;h1q@3SjrOYr%|A5yIa|%< zG(=|h03N*NLA6dk78XcUscD@aM8>5^CMoP@EliP@!}Th zoE%O?s}y}hZ%e8iP{f?~A~x&BNB!lBEC zvnFqf8Byn(*S|Apev^1M(RClu6V5Qrk)5t$83X+FkW$jZ-uiN;)BpOSqG@8mg$rL% zBZnI=tK()IC!i>E@oS#XfX>A}L>uF`IBZ_DKpLR)Rbt8fqw8<_kS+NOAFcpG_*k!4 zE;-cPz>gi-8XYW4>Y5@ifAauQv@7La>Hgb>fmCV=v9xI0UV-9Mg(ek?IF@rDW#z37 z3UK-1!}4D>)Gdz0K~_|zbt&j93#5R8+jxVyC{lvo)|9Lirc|gXI@u!o0MAH%+UR&?|tiE`zo{? z&?;t_`)-n?M}ZXQzlkr(LNI42>j|psyx=ohaZgX1V3|LXWe+ko$})A2 z6_%H=RZPMTNW;WcJrK8`fkC-F_vcaTyC&i+PlbkR}KTO(w)gZI@&NFr& zBuOZ9y&V#?s(80EaTjXZZbc$hslI@+$nDGhEuYxS7Q5mphN{yvH#WtTeGzVdg89mV zfP9rV3wTiMUT9Al?02z0?#Ue8q^s2VoIS zw=p3*tA0Pmg6lVVrX$vP}aL zOF^r*$#!Tpfzc^1aCLXV?G=q%$4Pk=q2wW+l%K*-K(^FOULsb%E5c;Ez$mH^BQI7re?$=9_;uh?aH(s_7cZQpX<+Ci`v06*>#A4Zu?Egp8~NpZ$nvZS=1lyeol_TyqC0R4u7^>$| zs`!}QPp(D{T43iII5ZS=EVLeY;(Kro&Yt3+wo3pULUa92iZ{)Iyf6ABT-$v%bf{j^ zfjno9_1!owMNf~#oJ4>-TV#aNhj^x!U17h^H?lXJB%4I)&y7-9!>6TeT^qig{yq}7 z`UyLhH=innV(oG4ud;Ndj&e-*Bc;vz3tVGWfxGcXm^h!^3g2B*)BdK--UeEp9qF?Y z%8b}7H^bXAt$FV!>*J_ORWaB$ZFV^;>3GIzccRn+Ttul*Hh=gKx;TE)g4qneerc$Q zwCfx0Euf}>E@_AWZlT#Ly2QBP2iUP)cO@CACRY<|)!x;_st*in(oZ)^Jul6(%i^TW zM=|#GJ?{<%1wS}z8SD{EA!XocM=UPxp7Kn|lWl_T)LPQI5m8)-mnagn(do0D62&V7 z1_YS-Hr`SdmVD4HxQHZI%S3#CB|c?U?*!NAaMLNZ>E{*#8d3Lzx+Nr^w`=)h&0DP8 zYE@R_X;7h=?;B)e!FM`4X;yh^RX$NsQDI@91WH@!m-zVjQ&Vp5DMIzU)htJMa9%q} z*Nxhga&ea)wZYxEGb92qNHN=YIxf)ZnfZAtJnq9;X^g!rJJ_eyw60g*FqUFJhJh^OUfA^Qg+CWxS%6``t3xE9HAqYkU<_DI+5$Gv~pKbB5FfB21)DkB^VR zR*#^!ZqCo|>f*SdS%)>bV;5aFPG~(X7Jn-v#CPu&Qibl5cdgYsOw`euwOpEYeTav!`W1KtdnZ)28lZ|*fFC?+!p;g6j@ zE!wH`(XFb+%`G;qqHxAbo&kZ#5qlmc`&GLU{<7rg)G)8s_t`5g>AL{vq}y0aEcZ~$ z%%2t8jjUOHHjmE9Jrh)Z%45U#B|AT7IQ=X>D*%@j6nw01h#2-S zbE`z!`Y%@;TOtV{ljPUVCYP7Noi8}+sk^#lzGh^+ip4coRGrz}x70Q?6ho`rIck{{ z8O1PqF7ted2sz|t%Q5~R9|msz%YOYoCe4!Ly8bA=0;dhwyhs}*NR;niU&drwbT@N8 z`jh*luke7h1@Bd?kL~+UCUh53ES|Np-1iTxZJilZxCu?AM4`_{Gp6#Hx>lrBd?`I|s*1r6Hp-F&qAWEMwbU#Y8$_xJ&I_7dH^|I(=3 z`V^saMrbvB!P)F(@Hjm#+pqpY0n!prnke^w zo3t2~8}QSL+4uhO&kZh}3wuPA68X$hSGB~6>Eny&f^Qoz|7OW>l7AHcVW=;;Ct3Dx z_{LKzUO@n1glsGWhbK$M$asjL5w*p0Dg|UlHwxI=?%Fi;#!Uye3iiKFrFkT@gK3+GOsozr_ zu5&?OmHr?sI3!bp_?I(_1RtH&B;08ln4XcrR^P@jG`+g&Pv!9BZ-28hmc7YchTDSj zNT|=nB#sgLfUQ@_o5F~1#SPJBNG#`FP;z#FC7gJ@;x3A$EiX6v)=l(R^zVhgVwfXlUt1{) zQqfV;X{~)KUt2Q~C&wujpv-`;d-&@BhHPtFOoUUE9iV!q+KOR{;wfsS@OnNf99XiF zBo!Skt*($%rWzgr-gsb1OvI?363f4ax^4eV+=EPoSy1O&`NYgZntI>!l>B1+Zl+AN zQ8b$fyFX$aFXfqE=&E1H>+@RCvm&74X4dd!R3k0>HPP3;CXmQ{Cg z>TKgCXT)m`c9_BBIKI*A1MC&e7;NGCHKoOaNc$3tUMs$k7cXKN04xy)AD{huonhPV zAIYn8l)U=`y1bhx z^JfO1UsUkRV3BApJvFtLD8kI`gN1=vs_2_RYilYc6Yh-ks9=+SO-9GDp~#&@e1xG) zhd(9lh)d6kAqj~_j|~`!gKZ62o?~Vv8&2@?nUmQPNIr&sEu$n*j(uBDxSFpftg6aT zX3d{H@|{z2Du+o7CFKyAJ7O(GM_83IzOOAB_RIBQib*&|N_nOIEhahEyJ%V5(!8cO z@UdrpW%ac9J!4Rn^rG@MyKfeg8N(pavx=BN4iC2%!o^P3E1!~M&=@fK&?~K~0L+mk zC&tf^Qu*dm(!qmxX~jI@gyESk97}Acu>K{=tG=~jCV(m$03-a~hYi&D#V7`FJ3e=F zaz2P5hLj1+jUP7iMklQ_kl!Y_g*#1#-W7z%El6L(kJIlzW%}_jt208kv z*t6mKo>f%BXy{&WO}}+P(uWyiW9^JlYcw_07J6#}RrKmM1`Y2bRsXc`p$`y{W&2aBdOJvTYM8he`nXwr$f<^guNMO4Jjj-53r zpVAXCN*oI)8eL~j-iSfKK%FJ*dj`shQsa;KUFofD$qPQ+{&1BYMv}F(Stl;_^xwv+ zdeG<{8YRpXqAX8wauPTs;Z%L;cT{!Hs)=Jk!Lx<%a^t7sMPP}s-o+4I2Nuk6-=@bW zSQ?6zxuBaF9`NNDbI)^a#f}aFdVC0?SasGnei%<@hLIcgR=th`#rybm*38xA0Y>Aj7*V6#M#l)-PYCT3>4N9WNFsTqOt zF$%O!=mDb0UgCh8xghys_Z109)IQ%~n3EDWP;d3@F0t4f@g;fZ@W?3{iG#2Vt5RFx z2{{Rk$b{cIkkO$9bRh%AYU22f(N;FhI(e2P+?!~rp+`YyF6f{S0>o5Sr_F`GiDQjN zgFlUY9H>;2bzqTFQO?Q%Dt?&lKbYKSO$1344hc{v7C-7sJv;uXZpm0~@%yrZURpwu zw~mitGk6-ba-Y^o>Z6kLn!Z)d^^!oRaKF>)Hhh8St;R(Athxp*BnhH+v1J}qBa@ah z=O<1F@@c+l(sTQ6=oUA2D!GsB`I*UOD0dD(?kD_+Re>qqNjK2fpQ%hwNFdP4(mJIb zmJDFzu)|zxcZrb$uq_=J$)YkGr6}zqz-!*55-PE zmW+gndrNtu0!>n7n;qjjoUwTwuPML9>7|53NtbE(x;-3q|eEhryYH< z>KBent3Y;0l7{5znL%U!$z~cPDMX?^WA+J1W^|D0Vf~AfPK_nSJKTUcA0+{>a#?M` z+L{uRq@bX+LE)opn0R2hp%6*;CV@pz7x#gr3&++~ zu$>^)ihm=dY;niuJ5Ap>G^YHUXY3+LJTv^fk7W{%azSQq@+U=fd>hqXrrQK;^3U*n zzl^q?p|^avb8y!jo2IFD)524SW+%j)|zn`mi1^CRzp26JrfEmS08i77aJH14Xi!M zR2+TxxOUmrEX|bOI)Y(W${pGJFafZwQ<6a-Zl*AbQAE12_Y!Q@z>wY7L!P8Vx2O&QSZZ zq7T&vUCzP=TFD_Y$>+3Bk6n%$=d6@rp3fRN!CF>0BTNw;x-oBe%Qnr+%PO@<4X1QU zhbZy%rgdgKISb2cU${$-k)#ygG`Hu>>}?pNFZ)F@3PR7Wf@}IJ&;cwd3G$3tCC032 z-|r38etf~lsmcntX?ZBko4DYi*`!lFmQlW;%%Yn;dXvl3kxt;Y^G;MYHpw6#X4tWv z=ngUJ`60~j_JFYmCc9l*6x`Wunx4&*>G$z>txVT1X9xEr5<~txqk&cO(W6|R+Vo>f zl1!N|mB5){_w9UhUO}3txBtJ#k+$?ottqrHrbs!W&4_1!;TuXs$XnE!sDh_I%B`n zS&~-e*7bxO9o|mZiLGLNGme>TWaZcX6N;*0kLy)Ax`b2;N|JM8(}|}rW3G$7)D_xi zGk#Rtb?P`R4G^0S15X=3J2)MfQWHUUFNZG7i6y}THEG!Cr_3W^>U(;c%kXPl$hDN& zRN&@2B0jeR8p?a)gjV#{I>-nqdfENnD0`7uM_mQdX|E

5UcZj=5}+X2L7 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 literal 0 HcmV?d00001 From 2060c0261b658b95bb58ded2f9368259cf3deabb Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 17:49:35 +0800 Subject: [PATCH 10/16] docs: split sequence analysis into three standalone demos --- .../sequence-analysis-medium.md | 1141 +++++++++++++++++ ...-analysis.md => sequence-analysis-step.md} | 231 +--- .../sequence-analysis-target.md | 1065 +++++++++++++++ docs/assets/examples/menu.json | 20 +- .../sequence-analysis-medium.md | 1141 +++++++++++++++++ ...-analysis.md => sequence-analysis-step.md} | 231 +--- .../sequence-analysis-target.md | 1065 +++++++++++++++ .../Chart_Extensions/sequence_analysis.md | 8 +- .../Chart_Extensions/sequence_analysis.md | 8 +- .../option/en/chart/sequence-analysis.md | 2 +- .../option/zh/chart/sequence-analysis.md | 2 +- .../preview/sequence-analysis-medium.png | Bin 0 -> 70455 bytes ...nalysis.png => sequence-analysis-step.png} | Bin .../preview/sequence-analysis-target.png | Bin 0 -> 71287 bytes 14 files changed, 4467 insertions(+), 447 deletions(-) create mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-medium.md rename docs/assets/examples/en/extension-chart/{sequence-analysis.md => sequence-analysis-step.md} (83%) create mode 100644 docs/assets/examples/en/extension-chart/sequence-analysis-target.md create mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md rename docs/assets/examples/zh/extension-chart/{sequence-analysis.md => sequence-analysis-step.md} (83%) create mode 100644 docs/assets/examples/zh/extension-chart/sequence-analysis-target.md create mode 100644 docs/public/vchart/preview/sequence-analysis-medium.png rename docs/public/vchart/preview/{sequence-analysis.png => sequence-analysis-step.png} (100%) create mode 100644 docs/public/vchart/preview/sequence-analysis-target.png diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md new file mode 100644 index 0000000000..51ee722a73 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md @@ -0,0 +1,1141 @@ +--- +category: examples +group: extension chart +title: 'Sequence Analysis: Medium Curves' +keywords: extension, sequenceAnalysis, medium +order: 15 +cover: /vchart/preview/sequence-analysis-medium.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis: Medium Curves + +Groups the local demo’s user events by medium and draws curved pattern links. + +## Demo + +```javascript livedemo +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const eventData = [ + { + user_id: 'user_010', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_010.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_010_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_010_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 20 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_010_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'comment', + node_name: 'event_010_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_010_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_010_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_010_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_010_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_010_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_010_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_010_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_010_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_009', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_009_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_009_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 8 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_009_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_009_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_009_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_009_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_009_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_009_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_009_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_009_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_009_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_009_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_009_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_009_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_009_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_008', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_008_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 23 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_008_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 5 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_008_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_008_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_008_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_008_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 2 + }, + { + step: 'step_7', + action_type: 'follow', + node_name: 'event_008_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'im', + node_name: 'event_008_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 33 + }, + { + step: 'step_9', + action_type: 'comment', + node_name: 'event_008_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 2 + }, + { + step: 'step_10', + action_type: 'focus', + node_name: 'event_008_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + } + ] + }, + { + user_id: 'user_007', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_007_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_007_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 6 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_007_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_007_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_007_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + } + ] + }, + { + user_id: 'user_006', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_006_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 3 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_006_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_006_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_006_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_006_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_006_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_006_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_006_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_006_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 7 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_006_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_006_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_006_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_006_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 3 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_006_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_006_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_005', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_005_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'follow', + node_name: 'event_005_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 11 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_005_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_005_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_5', + action_type: 'im', + node_name: 'event_005_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_005_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + } + ] + }, + { + user_id: 'user_004', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_004_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_004_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 9 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_004_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 7 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_004_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_004_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_004_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_004_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 4 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_004_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'follow', + node_name: 'event_004_9', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_003', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_003_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 15 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_003_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 2 + }, + { + step: 'step_3', + action_type: 'SendGift', + node_name: 'event_003_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_003_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_003_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_6', + action_type: 'follow', + node_name: 'event_003_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_003_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_003_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_003_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 5 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_003_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_003_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_003_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_003_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 11 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_003_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 12 + }, + { + step: 'step_15', + action_type: 'focus', + node_name: 'event_003_15', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 3 + } + ] + }, + { + user_id: 'user_002', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_002_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_002_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_002_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_002_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 11 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_002_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 22 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_002_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 34 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_002_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_002_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_002_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 12 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_002_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_002_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_002_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 6 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_002_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_002_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + } + ] + }, + { + user_id: 'user_001', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_001_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_001_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 10 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_001_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_001_4', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_001_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'login', + node_name: 'event_001_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_001_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 11 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_001_8', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 22 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_001_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 3 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_001_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + } + ] + } +]; + +const userImageMap = { + user_001: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + user_002: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + user_003: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_004: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_005: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_006: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_007: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + user_008: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_009: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' +}; + +const stepMediumMap = {}; + +const patternData = [ + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_009_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_002' + }, + { + from: 'event_003_10', + to: 'event_002_11', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_005_2', + pattern_type: 'pattern_002' + }, + { + from: 'event_005_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_007_4', + pattern_type: 'pattern_001' + } +]; + +const actionData = []; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: true, + actionTarget: false, + height: 600, + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; +const container = document.getElementById(CONTAINER_ID); +container.style.height = '600px'; +const vchart = new VChart(spec, { dom: container }); +vchart.renderSync(); +window.vchart = vchart; +``` + +[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md similarity index 83% rename from docs/assets/examples/en/extension-chart/sequence-analysis.md rename to docs/assets/examples/en/extension-chart/sequence-analysis-step.md index 70b56449d5..69cc218965 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -1,18 +1,16 @@ --- category: examples group: extension chart -title: Sequence Analysis -keywords: extension, sequenceAnalysis, time, step +title: 'Sequence Analysis: Step Mode' +keywords: extension, sequenceAnalysis, step order: 14 -cover: /vchart/preview/sequence-analysis.png +cover: /vchart/preview/sequence-analysis-step.png option: sequenceAnalysisChart --- -# Sequence Analysis +# Sequence Analysis: Step Mode -Uses the local sequence-analysis demo data and configuration: 10 users and 111 events. Select step, medium curves, time or target avatars in the dropdown. - -Links referencing nonexistent events have been omitted; event data, avatar URLs and chart configuration otherwise follow the local demo. +Displays the local demo’s user events and pattern links by step. ## Demo @@ -1141,227 +1139,24 @@ const patternData = [ } ]; -const patternDataCurve = [ - { - from: 'event_001_1', - to: 'event_002_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_001_1', - to: 'event_002_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_001_1', - to: 'event_002_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_3', - to: 'event_006_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_4', - to: 'event_006_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_4', - to: 'event_004_9', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_4', - to: 'event_004_9', - pattern_type: 'pattern_002' - }, - { - from: 'event_006_4', - to: 'event_004_9', - pattern_type: 'pattern_002' - }, - { - from: 'event_006_1', - to: 'event_009_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_2', - to: 'event_008_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_2', - to: 'event_008_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_2', - to: 'event_008_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_008_3', - to: 'event_009_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_008_3', - to: 'event_009_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_008_3', - to: 'event_009_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_4', - to: 'event_002_9', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_4', - to: 'event_002_9', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_9', - to: 'event_003_10', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_9', - to: 'event_003_10', - pattern_type: 'pattern_002' - }, - { - from: 'event_003_10', - to: 'event_002_11', - pattern_type: 'pattern_002' - }, - { - from: 'event_006_1', - to: 'event_005_2', - pattern_type: 'pattern_002' - }, - { - from: 'event_005_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_3', - to: 'event_007_4', - pattern_type: 'pattern_001' - } -]; - -const actionData = [ - { - from: 'event_002_2', - to: 'event_003_2', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', - user_id: 'user_003' - }, - { - from: 'event_003_4', - to: 'event_004_4', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', - user_id: 'user_003' - }, - { - from: 'event_003_5', - to: 'event_004_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', - user_id: 'user_003' - }, - { - from: 'event_004_5', - to: 'event_005_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', - user_id: 'user_004' - }, - { - from: 'event_005_5', - to: 'event_006_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', - user_id: 'user_005' - }, - { - from: 'event_006_5', - to: 'event_007_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', - user_id: 'user_006' - }, - { - from: 'event_008_2', - to: 'event_009_2', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', - user_id: 'user_008' - }, - { - from: 'event_008_3', - to: 'event_009_3', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', - user_id: 'user_008' - }, - { - from: 'event_008_10', - to: 'event_009_10', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', - user_id: 'user_008' - } -]; +const actionData = []; -const baseSpec = { +const spec = { type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + actionTarget: false, + height: 600, eventData, actionData, userImageMap, patternData, stepMediumMap }; -const specs = [ - { ...baseSpec, mode: 'step', mediumConnection: false, actionTarget: false }, - { ...baseSpec, mode: 'step', mediumConnection: true, actionTarget: true, patternData: patternDataCurve }, - { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: false }, - { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: true } -]; const container = document.getElementById(CONTAINER_ID); -container.style.height = '660px'; -const modeSelect = document.createElement('select'); -modeSelect.setAttribute('aria-label', 'Display mode'); -modeSelect.style.cssText = 'margin: 8px 0; padding: 6px 12px;'; -['Step mode', 'Medium curves', 'Time mode', 'Time with target avatars'].forEach((label, index) => { - const option = document.createElement('option'); - option.value = String(index); - option.textContent = label; - modeSelect.appendChild(option); -}); -const chartContainer = document.createElement('div'); -chartContainer.style.height = '600px'; -container.replaceChildren(modeSelect, chartContainer); -const vchart = new VChart(specs[0], { dom: chartContainer }); +container.style.height = '600px'; +const vchart = new VChart(spec, { dom: container }); vchart.renderSync(); -modeSelect.onchange = () => vchart.updateSpecSync(specs[Number(modeSelect.value)]); window.vchart = vchart; ``` diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md new file mode 100644 index 0000000000..51c7c89eaa --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md @@ -0,0 +1,1065 @@ +--- +category: examples +group: extension chart +title: 'Sequence Analysis: Time with Target Avatars' +keywords: extension, sequenceAnalysis, target +order: 16 +cover: /vchart/preview/sequence-analysis-target.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis: Time with Target Avatars + +Places the local demo’s events on a time axis, with midpoint avatars and target arrows on links. + +## Demo + +```javascript livedemo +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const eventData = [ + { + user_id: 'user_010', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_010.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_010_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_010_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 20 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_010_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'comment', + node_name: 'event_010_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_010_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_010_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_010_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_010_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_010_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_010_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_010_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_010_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_009', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_009_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_009_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 8 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_009_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_009_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_009_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_009_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_009_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_009_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_009_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_009_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_009_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_009_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_009_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_009_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_009_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_008', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_008_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 23 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_008_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 5 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_008_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_008_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_008_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_008_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 2 + }, + { + step: 'step_7', + action_type: 'follow', + node_name: 'event_008_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'im', + node_name: 'event_008_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 33 + }, + { + step: 'step_9', + action_type: 'comment', + node_name: 'event_008_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 2 + }, + { + step: 'step_10', + action_type: 'focus', + node_name: 'event_008_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + } + ] + }, + { + user_id: 'user_007', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_007_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_007_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 6 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_007_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_007_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_007_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + } + ] + }, + { + user_id: 'user_006', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_006_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 3 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_006_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_006_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_006_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_006_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_006_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_006_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_006_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_006_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 7 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_006_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_006_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_006_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_006_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 3 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_006_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_006_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_005', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_005_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'follow', + node_name: 'event_005_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 11 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_005_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_005_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_5', + action_type: 'im', + node_name: 'event_005_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_005_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + } + ] + }, + { + user_id: 'user_004', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_004_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_004_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 9 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_004_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 7 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_004_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_004_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_004_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_004_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 4 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_004_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'follow', + node_name: 'event_004_9', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_003', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_003_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 15 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_003_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 2 + }, + { + step: 'step_3', + action_type: 'SendGift', + node_name: 'event_003_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_003_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_003_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_6', + action_type: 'follow', + node_name: 'event_003_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_003_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_003_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_003_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 5 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_003_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_003_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_003_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_003_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 11 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_003_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 12 + }, + { + step: 'step_15', + action_type: 'focus', + node_name: 'event_003_15', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 3 + } + ] + }, + { + user_id: 'user_002', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_002_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_002_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_002_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_002_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 11 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_002_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 22 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_002_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 34 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_002_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_002_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_002_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 12 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_002_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_002_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_002_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 6 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_002_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_002_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + } + ] + }, + { + user_id: 'user_001', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_001_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_001_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 10 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_001_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_001_4', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_001_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'login', + node_name: 'event_001_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_001_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 11 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_001_8', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 22 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_001_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 3 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_001_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + } + ] + } +]; + +const userImageMap = { + user_001: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + user_002: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + user_003: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_004: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_005: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_006: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_007: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + user_008: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_009: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' +}; + +const stepMediumMap = {}; + +const patternData = []; + +const actionData = [ + { + from: 'event_002_2', + to: 'event_003_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_4', + to: 'event_004_4', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_5', + to: 'event_004_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_004_5', + to: 'event_005_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_id: 'user_004' + }, + { + from: 'event_005_5', + to: 'event_006_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_id: 'user_005' + }, + { + from: 'event_006_5', + to: 'event_007_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_id: 'user_006' + }, + { + from: 'event_008_2', + to: 'event_009_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_3', + to: 'event_009_3', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_10', + to: 'event_009_10', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + } +]; + +const spec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: true, + actionTarget: true, + height: 600, + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; +const container = document.getElementById(CONTAINER_ID); +container.style.height = '600px'; +const vchart = new VChart(spec, { dom: container }); +vchart.renderSync(); +window.vchart = vchart; +``` + +[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/menu.json b/docs/assets/examples/menu.json index d8e51f13e7..4d323dc45b 100644 --- a/docs/assets/examples/menu.json +++ b/docs/assets/examples/menu.json @@ -2119,10 +2119,24 @@ } }, { - "path": "sequence-analysis", + "path": "sequence-analysis-step", "title": { - "zh": "时序分析图", - "en": "Sequence Analysis" + "zh": "时序分析:步骤模式", + "en": "Sequence Analysis: Step Mode" + } + }, + { + "path": "sequence-analysis-medium", + "title": { + "zh": "时序分析:介质曲线连接", + "en": "Sequence Analysis: Medium Curves" + } + }, + { + "path": "sequence-analysis-target", + "title": { + "zh": "时序分析:时间模式 + 目标头像", + "en": "Sequence Analysis: Time with Target Avatars" } } ] diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md new file mode 100644 index 0000000000..ce647dbc42 --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md @@ -0,0 +1,1141 @@ +--- +category: examples +group: extension chart +title: '时序分析:介质曲线连接' +keywords: extension, sequenceAnalysis, medium +order: 15 +cover: /vchart/preview/sequence-analysis-medium.png +option: sequenceAnalysisChart +--- + +# 时序分析:介质曲线连接 + +按介质分类展示本地 demo 的用户事件,通过曲线表达模式连接。 + +## 代码演示 + +```javascript livedemo +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const eventData = [ + { + user_id: 'user_010', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_010.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_010_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_010_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 20 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_010_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'comment', + node_name: 'event_010_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_010_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_010_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_010_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_010_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_010_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_010_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_010_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_010_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_009', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_009_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_009_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 8 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_009_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_009_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_009_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_009_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_009_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_009_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_009_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_009_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_009_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_009_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_009_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_009_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_009_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_008', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_008_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 23 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_008_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 5 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_008_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_008_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_008_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_008_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 2 + }, + { + step: 'step_7', + action_type: 'follow', + node_name: 'event_008_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'im', + node_name: 'event_008_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 33 + }, + { + step: 'step_9', + action_type: 'comment', + node_name: 'event_008_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 2 + }, + { + step: 'step_10', + action_type: 'focus', + node_name: 'event_008_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + } + ] + }, + { + user_id: 'user_007', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_007_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_007_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 6 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_007_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_007_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_007_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + } + ] + }, + { + user_id: 'user_006', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_006_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 3 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_006_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_006_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_006_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_006_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_006_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_006_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_006_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_006_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 7 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_006_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_006_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_006_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_006_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 3 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_006_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_006_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_005', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_005_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'follow', + node_name: 'event_005_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 11 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_005_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_005_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_5', + action_type: 'im', + node_name: 'event_005_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_005_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + } + ] + }, + { + user_id: 'user_004', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_004_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_004_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 9 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_004_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 7 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_004_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_004_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_004_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_004_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 4 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_004_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'follow', + node_name: 'event_004_9', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_003', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_003_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 15 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_003_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 2 + }, + { + step: 'step_3', + action_type: 'SendGift', + node_name: 'event_003_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_003_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_003_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_6', + action_type: 'follow', + node_name: 'event_003_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_003_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_003_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_003_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 5 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_003_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_003_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_003_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_003_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 11 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_003_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 12 + }, + { + step: 'step_15', + action_type: 'focus', + node_name: 'event_003_15', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 3 + } + ] + }, + { + user_id: 'user_002', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_002_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_002_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_002_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_002_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 11 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_002_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 22 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_002_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 34 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_002_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_002_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_002_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 12 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_002_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_002_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_002_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 6 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_002_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_002_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + } + ] + }, + { + user_id: 'user_001', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_001_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_001_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 10 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_001_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_001_4', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_001_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'login', + node_name: 'event_001_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_001_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 11 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_001_8', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 22 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_001_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 3 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_001_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + } + ] + } +]; + +const userImageMap = { + user_001: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + user_002: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + user_003: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_004: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_005: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_006: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_007: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + user_008: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_009: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' +}; + +const stepMediumMap = {}; + +const patternData = [ + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_001_1', + to: 'event_002_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_006_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_4', + to: 'event_004_9', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_009_2', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_2', + to: 'event_008_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_008_3', + to: 'event_009_4', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_009_4', + to: 'event_002_9', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_001' + }, + { + from: 'event_002_9', + to: 'event_003_10', + pattern_type: 'pattern_002' + }, + { + from: 'event_003_10', + to: 'event_002_11', + pattern_type: 'pattern_002' + }, + { + from: 'event_006_1', + to: 'event_005_2', + pattern_type: 'pattern_002' + }, + { + from: 'event_005_2', + to: 'event_006_3', + pattern_type: 'pattern_001' + }, + { + from: 'event_006_3', + to: 'event_007_4', + pattern_type: 'pattern_001' + } +]; + +const actionData = []; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: true, + actionTarget: false, + height: 600, + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; +const container = document.getElementById(CONTAINER_ID); +container.style.height = '600px'; +const vchart = new VChart(spec, { dom: container }); +vchart.renderSync(); +window.vchart = vchart; +``` + +[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md similarity index 83% rename from docs/assets/examples/zh/extension-chart/sequence-analysis.md rename to docs/assets/examples/zh/extension-chart/sequence-analysis-step.md index 056b589076..682495ecea 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -1,18 +1,16 @@ --- category: examples group: extension chart -title: 时序分析图 -keywords: extension, sequenceAnalysis, time, step +title: '时序分析:步骤模式' +keywords: extension, sequenceAnalysis, step order: 14 -cover: /vchart/preview/sequence-analysis.png +cover: /vchart/preview/sequence-analysis-step.png option: sequenceAnalysisChart --- -# 时序分析图 +# 时序分析:步骤模式 -复用本地 sequence-analysis demo 的 10 位用户、111 个事件和连接配置。通过下拉框切换步骤、介质曲线、时间和目标头像四种展示方式。 - -移植时去掉了连接端点不存在的演示数据,其余事件、头像地址和展示配置沿用本地 demo。 +按步骤展示本地 demo 的用户事件与模式连接。 ## 代码演示 @@ -1141,227 +1139,24 @@ const patternData = [ } ]; -const patternDataCurve = [ - { - from: 'event_001_1', - to: 'event_002_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_001_1', - to: 'event_002_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_001_1', - to: 'event_002_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_3', - to: 'event_006_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_4', - to: 'event_006_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_4', - to: 'event_004_9', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_4', - to: 'event_004_9', - pattern_type: 'pattern_002' - }, - { - from: 'event_006_4', - to: 'event_004_9', - pattern_type: 'pattern_002' - }, - { - from: 'event_006_1', - to: 'event_009_2', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_2', - to: 'event_008_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_2', - to: 'event_008_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_2', - to: 'event_008_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_008_3', - to: 'event_009_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_008_3', - to: 'event_009_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_008_3', - to: 'event_009_4', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_4', - to: 'event_002_9', - pattern_type: 'pattern_001' - }, - { - from: 'event_009_4', - to: 'event_002_9', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_9', - to: 'event_003_10', - pattern_type: 'pattern_001' - }, - { - from: 'event_002_9', - to: 'event_003_10', - pattern_type: 'pattern_002' - }, - { - from: 'event_003_10', - to: 'event_002_11', - pattern_type: 'pattern_002' - }, - { - from: 'event_006_1', - to: 'event_005_2', - pattern_type: 'pattern_002' - }, - { - from: 'event_005_2', - to: 'event_006_3', - pattern_type: 'pattern_001' - }, - { - from: 'event_006_3', - to: 'event_007_4', - pattern_type: 'pattern_001' - } -]; - -const actionData = [ - { - from: 'event_002_2', - to: 'event_003_2', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', - user_id: 'user_003' - }, - { - from: 'event_003_4', - to: 'event_004_4', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', - user_id: 'user_003' - }, - { - from: 'event_003_5', - to: 'event_004_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', - user_id: 'user_003' - }, - { - from: 'event_004_5', - to: 'event_005_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', - user_id: 'user_004' - }, - { - from: 'event_005_5', - to: 'event_006_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', - user_id: 'user_005' - }, - { - from: 'event_006_5', - to: 'event_007_5', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', - user_id: 'user_006' - }, - { - from: 'event_008_2', - to: 'event_009_2', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', - user_id: 'user_008' - }, - { - from: 'event_008_3', - to: 'event_009_3', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', - user_id: 'user_008' - }, - { - from: 'event_008_10', - to: 'event_009_10', - user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', - user_id: 'user_008' - } -]; +const actionData = []; -const baseSpec = { +const spec = { type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + actionTarget: false, + height: 600, eventData, actionData, userImageMap, patternData, stepMediumMap }; -const specs = [ - { ...baseSpec, mode: 'step', mediumConnection: false, actionTarget: false }, - { ...baseSpec, mode: 'step', mediumConnection: true, actionTarget: true, patternData: patternDataCurve }, - { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: false }, - { ...baseSpec, mode: 'time', mediumConnection: true, actionTarget: true } -]; const container = document.getElementById(CONTAINER_ID); -container.style.height = '660px'; -const modeSelect = document.createElement('select'); -modeSelect.setAttribute('aria-label', '展示模式'); -modeSelect.style.cssText = 'margin: 8px 0; padding: 6px 12px;'; -['步骤模式', '介质曲线连接', '时间模式', '时间模式 + 目标头像'].forEach((label, index) => { - const option = document.createElement('option'); - option.value = String(index); - option.textContent = label; - modeSelect.appendChild(option); -}); -const chartContainer = document.createElement('div'); -chartContainer.style.height = '600px'; -container.replaceChildren(modeSelect, chartContainer); -const vchart = new VChart(specs[0], { dom: chartContainer }); +container.style.height = '600px'; +const vchart = new VChart(spec, { dom: container }); vchart.renderSync(); -modeSelect.onchange = () => vchart.updateSpecSync(specs[Number(modeSelect.value)]); window.vchart = vchart; ``` diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md new file mode 100644 index 0000000000..2fc2792a0c --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md @@ -0,0 +1,1065 @@ +--- +category: examples +group: extension chart +title: '时序分析:时间模式 + 目标头像' +keywords: extension, sequenceAnalysis, target +order: 16 +cover: /vchart/preview/sequence-analysis-target.png +option: sequenceAnalysisChart +--- + +# 时序分析:时间模式 + 目标头像 + +按时间展示本地 demo 的用户事件,通过连接中点头像和目标端箭头表达事件关系。 + +## 代码演示 + +```javascript livedemo +const { registerSequenceAnalysis } = VChartExtension; +registerSequenceAnalysis({ VChart }); + +const eventData = [ + { + user_id: 'user_010', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_010.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_010_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_010_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 20 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_010_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'comment', + node_name: 'event_010_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_010_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_010_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_010_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_010_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_010_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_010_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_010_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_010_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_009', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_009_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_009_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 8 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_009_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_009_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_009_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_009_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_009_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 20 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_009_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 20 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_009_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 20 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_009_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_009_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_009_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_009_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_009_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_009_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 20 + } + ] + }, + { + user_id: 'user_008', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_008_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 23 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_008_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 5 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_008_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_008_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_008_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_008_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 2 + }, + { + step: 'step_7', + action_type: 'follow', + node_name: 'event_008_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'im', + node_name: 'event_008_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 33 + }, + { + step: 'step_9', + action_type: 'comment', + node_name: 'event_008_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 2 + }, + { + step: 'step_10', + action_type: 'focus', + node_name: 'event_008_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + } + ] + }, + { + user_id: 'user_007', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_007_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_007_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 6 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_007_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_007_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_007_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + } + ] + }, + { + user_id: 'user_006', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_006_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 3 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_006_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_006_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_006_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'SendGift', + node_name: 'event_006_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_006_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_006_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_006_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'im', + node_name: 'event_006_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 7 + }, + { + step: 'step_10', + action_type: 'comment', + node_name: 'event_006_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + }, + { + step: 'step_11', + action_type: 'register', + node_name: 'event_006_11', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'im', + node_name: 'event_006_12', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_006_13', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 3 + }, + { + step: 'step_14', + action_type: 'focus', + node_name: 'event_006_14', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 4 + }, + { + step: 'step_15', + action_type: 'like', + node_name: 'event_006_15', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_005', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_005_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 9 + }, + { + step: 'step_2', + action_type: 'follow', + node_name: 'event_005_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 11 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_005_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_005_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_5', + action_type: 'im', + node_name: 'event_005_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'SendGift', + node_name: 'event_005_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + } + ] + }, + { + user_id: 'user_004', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_004_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_004_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 9 + }, + { + step: 'step_3', + action_type: 'im', + node_name: 'event_004_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 7 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_004_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_004_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_004_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 33 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_004_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 4 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_004_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'follow', + node_name: 'event_004_9', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 8 + } + ] + }, + { + user_id: 'user_003', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_003_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 15 + }, + { + step: 'step_2', + action_type: 'im', + node_name: 'event_003_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 2 + }, + { + step: 'step_3', + action_type: 'SendGift', + node_name: 'event_003_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_4', + action_type: 'follow', + node_name: 'event_003_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_003_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_6', + action_type: 'follow', + node_name: 'event_003_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'SendGift', + node_name: 'event_003_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_003_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 6 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_003_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 5 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_003_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_003_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_003_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_003_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 11 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_003_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 12 + }, + { + step: 'step_15', + action_type: 'focus', + node_name: 'event_003_15', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 3 + } + ] + }, + { + user_id: 'user_002', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_002_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 8 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_002_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 7 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_002_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 9 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_002_4', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 11 + }, + { + step: 'step_5', + action_type: 'follow', + node_name: 'event_002_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 22 + }, + { + step: 'step_6', + action_type: 'im', + node_name: 'event_002_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 34 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_002_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 6 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_002_8', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 9 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_002_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 12 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_002_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 11 + }, + { + step: 'step_11', + action_type: 'follow', + node_name: 'event_002_11', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 5 + }, + { + step: 'step_12', + action_type: 'focus', + node_name: 'event_002_12', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 6 + }, + { + step: 'step_13', + action_type: 'follow', + node_name: 'event_002_13', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 8 + }, + { + step: 'step_14', + action_type: 'im', + node_name: 'event_002_14', + medium_type: 'Medium', + time_stamp: 1751871600000, + action_count: 9 + } + ] + }, + { + user_id: 'user_001', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + dots: [ + { + step: 'step_1', + action_type: 'register', + node_name: 'event_001_1', + medium_type: 'IP', + time_stamp: 1751850000000, + action_count: 20 + }, + { + step: 'step_2', + action_type: 'login', + node_name: 'event_001_2', + medium_type: 'Device', + time_stamp: 1751853600000, + action_count: 10 + }, + { + step: 'step_3', + action_type: 'comment', + node_name: 'event_001_3', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 3 + }, + { + step: 'step_4', + action_type: 'SendGift', + node_name: 'event_001_4', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 2 + }, + { + step: 'step_5', + action_type: 'like', + node_name: 'event_001_5', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 5 + }, + { + step: 'step_6', + action_type: 'login', + node_name: 'event_001_6', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 8 + }, + { + step: 'step_7', + action_type: 'focus', + node_name: 'event_001_7', + medium_type: 'User', + time_stamp: 1751857200000, + action_count: 11 + }, + { + step: 'step_8', + action_type: 'login', + node_name: 'event_001_8', + medium_type: 'Liveroom', + time_stamp: 1751850000000, + action_count: 22 + }, + { + step: 'step_9', + action_type: 'focus', + node_name: 'event_001_9', + medium_type: 'Email', + time_stamp: 1751864400000, + action_count: 3 + }, + { + step: 'step_10', + action_type: 'login', + node_name: 'event_001_10', + medium_type: 'Tel', + time_stamp: 1751868000000, + action_count: 6 + } + ] + } +]; + +const userImageMap = { + user_001: 'https://tosv.byted.org/obj/bit-cloud/user_001.png', + user_002: 'https://tosv.byted.org/obj/bit-cloud/user_002.png', + user_003: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_004: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_005: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_006: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_007: 'https://tosv.byted.org/obj/bit-cloud/user_007.png', + user_008: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_009: 'https://tosv.byted.org/obj/bit-cloud/user_009.png', + user_010: 'https://tosv.byted.org/obj/bit-cloud/user_010.png' +}; + +const stepMediumMap = {}; + +const patternData = []; + +const actionData = [ + { + from: 'event_002_2', + to: 'event_003_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_4', + to: 'event_004_4', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_003_5', + to: 'event_004_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_003.png', + user_id: 'user_003' + }, + { + from: 'event_004_5', + to: 'event_005_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_004.png', + user_id: 'user_004' + }, + { + from: 'event_005_5', + to: 'event_006_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_005.png', + user_id: 'user_005' + }, + { + from: 'event_006_5', + to: 'event_007_5', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_006.png', + user_id: 'user_006' + }, + { + from: 'event_008_2', + to: 'event_009_2', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_3', + to: 'event_009_3', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + }, + { + from: 'event_008_10', + to: 'event_009_10', + user_image: 'https://tosv.byted.org/obj/bit-cloud/user_008.png', + user_id: 'user_008' + } +]; + +const spec = { + type: 'sequenceAnalysis', + mode: 'time', + mediumConnection: true, + actionTarget: true, + height: 600, + eventData, + actionData, + userImageMap, + patternData, + stepMediumMap +}; +const container = document.getElementById(CONTAINER_ID); +container.style.height = '600px'; +const vchart = new VChart(spec, { dom: container }); +vchart.renderSync(); +window.vchart = vchart; +``` + +[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md index b2456a2752..222561f0d9 100644 --- a/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md +++ b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -4,7 +4,7 @@ Sequence Analysis places each user's events on a horizontal row, aligns events b The chart comes from `@visactor/vchart-extension` with type `sequenceAnalysis`. It is distinct from the built-in sequence chart and the sequenceScatterLink extension for training-process analysis. -![Sequence Analysis](/vchart/preview/sequence-analysis.png) +![Sequence Analysis](/vchart/preview/sequence-analysis-step.png) ## Choose a mode @@ -105,7 +105,7 @@ Default vertical labels read userImageMap[user_id]. The image stored in an event With actionTarget enabled in time mode, the midpoint avatar reads actionData.user_image. Supplying actionData.user_id alone does not resolve the image. Disabling actionTarget hides avatars and arrows while keeping lines visible. -Use accessible image URLs or data URLs. Use the dropdown in the demo to select target avatars. [Interactive example](/vchart/demo/extension-chart/sequence-analysis). +Use accessible image URLs or data URLs. [Time with target avatars example](/vchart/demo/extension-chart/sequence-analysis-target). ## Customize appearance and zoom @@ -151,5 +151,7 @@ When removing or renaming events, update actionData / patternData in the same co ## Examples and options -- [Interactive example with four modes](/vchart/demo/extension-chart/sequence-analysis) +- [Step mode](/vchart/demo/extension-chart/sequence-analysis-step) +- [Medium curves](/vchart/demo/extension-chart/sequence-analysis-medium) +- [Time with target avatars](/vchart/demo/extension-chart/sequence-analysis-target) - [All options](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md index cc7447f67c..aefea58b9d 100644 --- a/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md +++ b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -4,7 +4,7 @@ 它由 `@visactor/vchart-extension` 提供,类型为 `sequenceAnalysis`,与内置 `sequence` 和用于训练过程分析的 `sequenceScatterLink` 是不同图表。 -![时序分析图](/vchart/preview/sequence-analysis.png) +![时序分析图](/vchart/preview/sequence-analysis-step.png) ## 选择展示模式 @@ -105,7 +105,7 @@ vchart.renderSync(); 时间模式开启 actionTarget 后,连接中点使用 `actionData.user_image`。只填写 `actionData.user_id` 不会自动查找头像地址。关闭 actionTarget 只隐藏连接头像和箭头,仍保留连接线。 -头像可以使用可访问的图片 URL 或 data URL;演示页可通过下拉框切换到目标头像模式。[查看完整示例](/vchart/demo/extension-chart/sequence-analysis)。 +头像可以使用可访问的图片 URL 或 data URL;[查看时间模式 + 目标头像示例](/vchart/demo/extension-chart/sequence-analysis-target)。 ## 定制样式与缩放 @@ -151,5 +151,7 @@ vchart.updateSpecSync({ ...spec, mode: 'time', eventData: nextEvents }); ## 完整示例与配置项 -- [完整示例(可切换四种模式)](/vchart/demo/extension-chart/sequence-analysis) +- [步骤模式](/vchart/demo/extension-chart/sequence-analysis-step) +- [介质曲线连接](/vchart/demo/extension-chart/sequence-analysis-medium) +- [时间模式 + 目标头像](/vchart/demo/extension-chart/sequence-analysis-target) - [全部配置项](/vchart/option/sequenceAnalysisChart) diff --git a/docs/assets/option/en/chart/sequence-analysis.md b/docs/assets/option/en/chart/sequence-analysis.md index dd1fe04025..9e0d6d7462 100644 --- a/docs/assets/option/en/chart/sequence-analysis.md +++ b/docs/assets/option/en/chart/sequence-analysis.md @@ -10,7 +10,7 @@ import { registerSequenceAnalysis } from '@visactor/vchart-extension'; registerSequenceAnalysis(); ``` -[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Interactive example](/vchart/demo/extension-chart/sequence-analysis) +[Tutorial](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [Step mode](/vchart/demo/extension-chart/sequence-analysis-step) · [Medium curves](/vchart/demo/extension-chart/sequence-analysis-medium) · [Time with target avatars](/vchart/demo/extension-chart/sequence-analysis-target) The transformer generates a common chart, dot/link series, axes and zoom components from the fields below. You do not need to supply `data`, `series`, `xField` or `yField`. Customize the generated spec with `customTransformSpec`. diff --git a/docs/assets/option/zh/chart/sequence-analysis.md b/docs/assets/option/zh/chart/sequence-analysis.md index 66d71063fe..506eefedc6 100644 --- a/docs/assets/option/zh/chart/sequence-analysis.md +++ b/docs/assets/option/zh/chart/sequence-analysis.md @@ -10,7 +10,7 @@ import { registerSequenceAnalysis } from '@visactor/vchart-extension'; registerSequenceAnalysis(); ``` -[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [完整示例](/vchart/demo/extension-chart/sequence-analysis) +[教程](/vchart/guide/tutorial_docs/Chart_Extensions/sequence_analysis) · [步骤模式](/vchart/demo/extension-chart/sequence-analysis-step) · [介质曲线连接](/vchart/demo/extension-chart/sequence-analysis-medium) · [时间模式 + 目标头像](/vchart/demo/extension-chart/sequence-analysis-target) 本图表使用以下专用数据字段,转换器会生成底层 common 图表、dot/link 系列、轴及缩放组件,无需自行配置 `data`、`series`、`xField` 或 `yField`。需要修改生成结果时使用 `customTransformSpec`。 diff --git a/docs/public/vchart/preview/sequence-analysis-medium.png b/docs/public/vchart/preview/sequence-analysis-medium.png new file mode 100644 index 0000000000000000000000000000000000000000..c5833a24f5b995e60d82c6b847e380b6cb60763d GIT binary patch literal 70455 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�Z)u&e|L8YXqsjAFx=xELJjUTMqr@MIY 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 A4gdfE literal 0 HcmV?d00001 From dedf182131c31ec355bf4a94d9656a40ed9897a2 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 17:51:36 +0800 Subject: [PATCH 11/16] docs: explain sequence analysis demo visual encodings --- .../extension-chart/sequence-analysis-medium.md | 14 ++++++++++++++ .../en/extension-chart/sequence-analysis-step.md | 13 +++++++++++++ .../extension-chart/sequence-analysis-target.md | 15 +++++++++++++++ .../extension-chart/sequence-analysis-medium.md | 14 ++++++++++++++ .../zh/extension-chart/sequence-analysis-step.md | 13 +++++++++++++ .../extension-chart/sequence-analysis-target.md | 15 +++++++++++++++ 6 files changed, 84 insertions(+) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md index 51ee722a73..c318dd56f3 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md @@ -12,6 +12,20 @@ option: sequenceAnalysisChart Groups the local demo’s user events by medium and draws curved pattern links. +## How to read the chart + +This example aligns the same user events by medium and uses curves to show pattern relationships across events, users and media. + +- **Vertical axis and left avatars:** each row represents a user (`user_id`), with an avatar from `userImageMap`. +- **Top axis:** `IP`, `Device`, `User`, `Liveroom`, `Email` and `Tel` are event medium categories (`medium_type`). Categories are equally spaced and do not encode chronological order or elapsed time. +- **Colored dots:** each dot is an event record, colored by `action_type`. User and medium determine its position. Events with the same user and medium overlap, so fewer dots may be visible than the total number of events. +- **Curves:** each curve corresponds to a `patternData` record connecting the events referenced by `from` and `to`. A curve across rows relates different users' events; a curve across columns relates different media. Color groups links by `pattern_type`. +- **Nearby curves:** the example includes repeated relationships with identical endpoints, such as three records from `event_001_1` to `event_002_2`. Different curvature separates these records visually; curvature does not encode relationship weight or strength. + +For example, `event_001_1` is in the IP column for `user_001`, and `event_002_2` is in the Device column for `user_002`. The curves between them show the supplied pattern relationships. + +Use the bottom range selector to filter media, the horizontal gray scrollbar to pan across categories, and the right scrollbar to browse users. The initial window shows only part of the data. + ## Demo ```javascript livedemo diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md index 69cc218965..8a602081ac 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -12,6 +12,19 @@ option: sequenceAnalysisChart Displays the local demo’s user events and pattern links by step. +## How to read the chart + +This example shows 111 events from 10 users, organized by behavioral step, together with explicitly supplied pattern relationships. + +- **Vertical axis and left avatars:** each row represents a user (`user_id`), with an avatar from `userImageMap`. Follow a row to inspect that user's events across steps. +- **Bottom axis:** `step_1`, `step_2`, and so on are event categories from `dots.step`. Steps are equally spaced; their distance does not encode elapsed time. +- **Top labels:** each label shows the medium name and additional count supplied in `stepMediumMap`. For example, `+1` in `Device... +1` comes from the step's `count`. This is an application-supplied quantity, not the number of event dots. +- **Colored dots:** each dot is an event record, colored by `action_type`, such as registration, login or comment. User and step determine its position. Multiple events for the same user and step can overlap. +- **Links:** each line corresponds to a `patternData` record whose `from` and `to` reference event `node_name` values. It represents a supplied pattern relationship, colored by `pattern_type`. A link within one row relates events of one user; a link across rows relates events of different users. +- **Short segments beside dots:** this example enables link extension along step bands and includes pattern records that reference the same event at both ends. Such records can appear as short segments beside a dot. Segment length does not encode duration or relationship strength. + +Use the bottom range selector to filter steps, the horizontal gray scrollbar to pan across them, and the right scrollbar to browse users. The initial window shows only part of the steps and users. + ## Demo ```javascript livedemo diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md index 51c7c89eaa..297a1ddec1 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md @@ -12,6 +12,21 @@ option: sequenceAnalysisChart Places the local demo’s events on a time axis, with midpoint avatars and target arrows on links. +## How to read the chart + +This example positions user events by their occurrence time and uses arrows and link avatars to display directed event relationships. + +- **Vertical axis and left avatars:** each row represents a user (`user_id`); the left avatar identifies that user. +- **Bottom axis:** dates and times correspond to the millisecond `time_stamp` values. Horizontal distance reflects elapsed time, and simultaneous events align vertically. +- **Top numbers and vertical dashed lines:** each dashed line marks an event timestamp. The number above it sums `action_count` across all users' events at that timestamp. For example, `141` is an aggregate count, not 141 visible dots. +- **Colored dots:** each dot is an event record, colored by `action_type`. Events for the same user at the same time can overlap. Dot size does not encode `action_count`. +- **Lines and arrows:** each `actionData` record links the event referenced by `from` to the event referenced by `to`, with an arrow at the target. These relationships are explicitly supplied. Simultaneous events can also be connected, producing vertical links. +- **Midpoint avatars:** the image comes from the relationship's `actionData.user_image`. It is configured independently from the vertical-axis avatar and can represent a different user from the target event's row. This example retains the local demo's associated-user settings. + +For example, the link from `event_002_2` to `event_003_2` connects simultaneous events of two users. Its arrow points to the latter event, and the midpoint shows the configured `user_003` avatar. + +Use the bottom range selector to filter a time interval, the horizontal gray scrollbar to pan the time window, and the right scrollbar to browse users. The initial window shows only part of the data. + ## Demo ```javascript livedemo diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md index ce647dbc42..1fdde1fe4a 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md @@ -12,6 +12,20 @@ option: sequenceAnalysisChart 按介质分类展示本地 demo 的用户事件,通过曲线表达模式连接。 +## 如何读图 + +这个示例将同一批用户事件按介质分类对齐,用曲线展示跨事件的模式关联,便于观察不同用户、不同介质之间的联系。 + +- **纵轴与左侧头像**:每一行对应一个用户(`user_id`),头像来自 `userImageMap`。 +- **顶部横轴**:`IP`、`Device`、`User`、`Liveroom`、`Email`、`Tel` 是事件的介质类型(`medium_type`)。横轴按类别等距排列,不表示时间先后或时间间隔。 +- **彩色圆点**:每个点是一条事件记录,颜色按 `action_type` 区分。点的位置由用户和介质类型决定;同一用户在相同介质上的多条事件会落在同一位置,所以可见圆点数量可能少于事件总数。 +- **曲线**:每条曲线对应一条 `patternData`,连接 `from` 和 `to` 指定的两个事件。跨行表示关联发生在不同用户的事件之间,跨列表示两个事件属于不同介质;曲线颜色按 `pattern_type` 区分。 +- **多条相近的曲线**:本例保留了多条起终点相同的关联,例如 `event_001_1` 到 `event_002_2` 的三条记录。图表为它们分配不同曲率,便于分辨重复关联;弯曲程度不表示关联权重或强弱。 + +例如,`event_001_1` 位于用户 `user_001` 的 IP 列,`event_002_2` 位于用户 `user_002` 的 Device 列,两者之间的曲线表示数据中指定的模式关联。 + +底部范围选择器用于筛选介质范围;灰色横向滚动条用于平移可见类别,右侧滚动条用于浏览其他用户。初始窗口只显示部分数据。 + ## 代码演示 ```javascript livedemo diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md index 682495ecea..11b47a800f 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -12,6 +12,19 @@ option: sequenceAnalysisChart 按步骤展示本地 demo 的用户事件与模式连接。 +## 如何读图 + +这个示例用 10 位用户的 111 个事件展示行为步骤,以及数据中指定的模式关联。 + +- **纵轴与左侧头像**:每一行对应一个用户(`user_id`),头像来自 `userImageMap`。沿着同一行可以查看这个用户在各个步骤上的事件。 +- **底部横轴**:`step_1`、`step_2` 等表示事件所属的步骤(`dots.step`)。它是分类轴,相邻步骤等距排列,间距不表示实际耗时。 +- **顶部标签**:展示 `stepMediumMap` 为每个步骤配置的介质名称和附加数量。例如 `Device... +1` 中的 `+1` 来自该步骤的 `count`,是示例预先给定的业务数量,不是图中事件点的个数。 +- **彩色圆点**:每个点是一条事件记录,颜色按 `action_type` 区分,例如注册、登录、评论等。点的位置由用户和步骤共同决定;同一用户在同一步骤的多条事件可能重叠。 +- **连接线**:每条线对应一条 `patternData`,通过 `from` / `to` 指向事件的 `node_name`,表示示例指定的模式关联;线的颜色按 `pattern_type` 区分。同一行上的线关联同一用户的事件,跨行线关联不同用户的事件。 +- **节点旁的短线**:本例开启了沿步骤带的连接延伸,并包含起终点为同一事件的模式记录,因此部分关联呈现为点旁的短线。延伸长度只是绘制样式,不代表持续时间或关联强度。 + +底部范围选择器用于筛选横轴范围;灰色横向滚动条用于平移可见步骤,右侧滚动条用于浏览其他用户。初始窗口只显示部分步骤和用户,可拖动查看完整数据。 + ## 代码演示 ```javascript livedemo diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md index 2fc2792a0c..83809912d2 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md @@ -12,6 +12,21 @@ option: sequenceAnalysisChart 按时间展示本地 demo 的用户事件,通过连接中点头像和目标端箭头表达事件关系。 +## 如何读图 + +这个示例按真实发生时间排列用户事件,并用箭头和连接头像展示事件之间的有向关联。 + +- **纵轴与左侧头像**:每一行对应一个用户(`user_id`),左侧头像用于识别这一行的用户。 +- **底部横轴**:展示事件的日期和时间,位置由毫秒时间戳 `time_stamp` 决定。横向间距反映实际时间间隔;同一时刻的事件在竖直方向对齐。 +- **顶部数字与竖向虚线**:虚线标记有事件发生的时间点,顶部数字是该时刻所有用户事件的 `action_count` 总和。例如顶部的 `141` 表示该时刻累计数量为 141,并不是有 141 个可见圆点。 +- **彩色圆点**:每个点是一条事件记录,颜色按 `action_type` 区分。相同用户在同一时刻的多条事件可能重叠;`action_count` 不通过点的大小编码。 +- **连接线与箭头**:每条连接来自 `actionData`,由 `from` 指定的事件指向 `to` 指定的事件,箭头位于目标端。连接关系由示例数据给定;即使两个事件发生在同一时刻,也可以形成竖向连接。 +- **连接中间的头像**:表示这条关联所配置的用户头像,取自 `actionData.user_image`。它与纵轴头像是独立配置,可能与终点所在行的用户不同;本例沿用本地 demo 的关联用户设置。 + +例如,`event_002_2` 到 `event_003_2` 的连接关联了两个用户同一时刻的事件,箭头指向后者,中间显示这条连接配置的 `user_003` 头像。 + +底部范围选择器用于筛选时间区间;灰色横向滚动条用于平移时间窗口,右侧滚动条用于浏览其他用户。初始窗口只显示部分数据。 + ## 代码演示 ```javascript livedemo From f1c95155d54a79276f2b52dc9390235cdf0339ac Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 17:55:45 +0800 Subject: [PATCH 12/16] docs: clarify the purpose and limits of the sequence step demo --- .../en/extension-chart/sequence-analysis-step.md | 16 +++++++++++++--- .../zh/extension-chart/sequence-analysis-step.md | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md index 8a602081ac..5f02ae9591 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -10,7 +10,17 @@ option: sequenceAnalysisChart # Sequence Analysis: Step Mode -Displays the local demo’s user events and pattern links by step. +This chart demonstrates how to compare multiple users' event histories and highlight selected behavioral segments. Think of it as a user activity comparison table: each row is a user, columns show behavioral steps, and colored dots represent events such as registration, login, follow and private messaging. + +## What this example represents + +Imagine reviewing platform activity logs to understand which steps each user took and where selected behavioral segments appear across users. Aligning the records by step lets you compare them together instead of reading logs one record at a time. + +For example, `user_004` has a follow event at step 4 and a private-message event (`im`) at step 6. The line between them means the input data marks these events as a relationship to inspect together. For `user_005`, follow at step 4 is linked to private messaging at step 5. The chart makes the positions of these similar segments easy to compare. Intermediate events remain visible; a link does not mean that the user skipped those steps. + +Dots are event records; links are additional relationship annotations supplied through `patternData`. The chart displays these annotations and does not automatically discover behavioral patterns in the logs. + +**This dataset demonstrates visualization features.** The local demo does not define business rules for `pattern_001` or `pattern_002`, or a complete metric definition for the top `+N` labels. It cannot establish anomalous activity, a shared IP address or a causal relationship. A business application should supply clearly named relationships with defined criteria and use the chart to display those results. ## How to read the chart @@ -18,9 +28,9 @@ This example shows 111 events from 10 users, organized by behavioral step, toget - **Vertical axis and left avatars:** each row represents a user (`user_id`), with an avatar from `userImageMap`. Follow a row to inspect that user's events across steps. - **Bottom axis:** `step_1`, `step_2`, and so on are event categories from `dots.step`. Steps are equally spaced; their distance does not encode elapsed time. -- **Top labels:** each label shows the medium name and additional count supplied in `stepMediumMap`. For example, `+1` in `Device... +1` comes from the step's `count`. This is an application-supplied quantity, not the number of event dots. +- **Top labels:** each label shows the medium name and additional count supplied in `stepMediumMap`. For example, `+1` in `Device... +1` comes from the step's `count`. This is a supplied additional count without a defined business metric in this demo; it is not the number of event dots. - **Colored dots:** each dot is an event record, colored by `action_type`, such as registration, login or comment. User and step determine its position. Multiple events for the same user and step can overlap. -- **Links:** each line corresponds to a `patternData` record whose `from` and `to` reference event `node_name` values. It represents a supplied pattern relationship, colored by `pattern_type`. A link within one row relates events of one user; a link across rows relates events of different users. +- **Links:** each line corresponds to a `patternData` record whose `from` and `to` reference event `node_name` values. It represents a supplied pattern relationship, colored by `pattern_type`. All links in this step example stay within a single user row and annotate that user’s event segments. - **Short segments beside dots:** this example enables link extension along step bands and includes pattern records that reference the same event at both ends. Such records can appear as short segments beside a dot. Segment length does not encode duration or relationship strength. Use the bottom range selector to filter steps, the horizontal gray scrollbar to pan across them, and the right scrollbar to browse users. The initial window shows only part of the steps and users. diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md index 11b47a800f..5a9df48cae 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -10,7 +10,17 @@ option: sequenceAnalysisChart # 时序分析:步骤模式 -按步骤展示本地 demo 的用户事件与模式连接。 +这张图演示如何把多位用户的行为记录放在一起比较,并标出需要关注的行为片段。可以把它理解为一张“用户行为对照表”:每行是一位用户,从左到右是他的行为步骤,彩色点代表注册、登录、关注、私信等事件。 + +## 这个示例代表什么 + +假设你正在查看一批用户的平台操作记录,希望了解:每个人经过了哪些行为步骤?某类行为片段出现在了哪些用户身上、各自位于什么位置?这张图把这些记录按步骤对齐,方便同时查看,而不用逐条翻阅日志。 + +以本例的 `user_004` 为例:第 4 步是关注(`follow`),第 6 步是私信(`im`),示例在这两个事件之间画了一条线,表示输入数据把它们标记为一组需要一起观察的行为关联。`user_005` 的第 4 步关注和第 5 步私信也被连接起来,因此可以直观看到类似行为片段在不同用户的哪几个步骤出现。第 5 步等中间事件仍保留在图中,连线不表示用户跳过了它们。 + +这里的点是用户的事件记录,线是额外提供的关联标注。连线由 `patternData` 指定,图表负责呈现这些标注,不会自动从日志中识别行为模式。 + +**这份数据的用途是演示可视化能力。** 本地 demo 没有定义 `pattern_001`、`pattern_002` 的具体业务规则,也没有给顶部 `+N` 指定完整的统计口径。因此,不能据此判断某个用户异常、某两人使用同一个 IP,或两个事件存在因果关系。实际用于业务分析时,需要由业务方给出有明确名称和判断依据的关联,再用这张图展示其结果。 ## 如何读图 @@ -18,9 +28,9 @@ option: sequenceAnalysisChart - **纵轴与左侧头像**:每一行对应一个用户(`user_id`),头像来自 `userImageMap`。沿着同一行可以查看这个用户在各个步骤上的事件。 - **底部横轴**:`step_1`、`step_2` 等表示事件所属的步骤(`dots.step`)。它是分类轴,相邻步骤等距排列,间距不表示实际耗时。 -- **顶部标签**:展示 `stepMediumMap` 为每个步骤配置的介质名称和附加数量。例如 `Device... +1` 中的 `+1` 来自该步骤的 `count`,是示例预先给定的业务数量,不是图中事件点的个数。 +- **顶部标签**:展示 `stepMediumMap` 为每个步骤配置的介质名称和附加数量。例如 `Device... +1` 中的 `+1` 来自该步骤的 `count`,是示例预先给定的附加计数,示例未定义其业务统计口径,也不是图中事件点的个数。 - **彩色圆点**:每个点是一条事件记录,颜色按 `action_type` 区分,例如注册、登录、评论等。点的位置由用户和步骤共同决定;同一用户在同一步骤的多条事件可能重叠。 -- **连接线**:每条线对应一条 `patternData`,通过 `from` / `to` 指向事件的 `node_name`,表示示例指定的模式关联;线的颜色按 `pattern_type` 区分。同一行上的线关联同一用户的事件,跨行线关联不同用户的事件。 +- **连接线**:每条线对应一条 `patternData`,通过 `from` / `to` 指向事件的 `node_name`,表示示例指定的模式关联;线的颜色按 `pattern_type` 区分。本步骤示例中的线均位于同一用户行,用于标注该用户的事件片段。 - **节点旁的短线**:本例开启了沿步骤带的连接延伸,并包含起终点为同一事件的模式记录,因此部分关联呈现为点旁的短线。延伸长度只是绘制样式,不代表持续时间或关联强度。 底部范围选择器用于筛选横轴范围;灰色横向滚动条用于平移可见步骤,右侧滚动条用于浏览其他用户。初始窗口只显示部分步骤和用户,可拖动查看完整数据。 From 6419a8e269eef7f0060602e82ab73775e31245fc Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 18:02:47 +0800 Subject: [PATCH 13/16] docs: illustrate sequence analysis with a concrete event record --- .../extension-chart/sequence-analysis-step.md | 41 +++++++++++++++++++ .../extension-chart/sequence-analysis-step.md | 41 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md index 5f02ae9591..278f6def9b 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -22,6 +22,47 @@ Dots are event records; links are additional relationship annotations supplied t **This dataset demonstrates visualization features.** The local demo does not define business rules for `pattern_001` or `pattern_002`, or a complete metric definition for the top `+N` labels. It cannot establish anomalous activity, a shared IP address or a causal relationship. A business application should supply clearly named relationships with defined criteria and use the chart to display those results. +## A concrete record and its scenario + +The following record is taken directly from this example. One possible business interpretation is live-streaming platform activity analysis: **the system records a room-related follow activity for `user_004`, assigns it to step 4, timestamps it at July 7, 2025, 12:00 in Beijing (UTC+8), and gives it an activity count of 7.** This provides a scenario for interpreting the synthetic fields. + +```js +// An event excerpt for user_004 from eventData +{ + user_id: 'user_004', + dots: [ + { + node_name: 'event_004_4', + step: 'step_4', + action_type: 'follow', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + } + ] +} +``` + +| Field | Meaning in this record | Visual representation | +| --------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------ | +| `user_id: 'user_004'` | The user associated with the activity | The row identified by that user's avatar | +| `node_name: 'event_004_4'` | Unique identifier of the event record | Referenced by link data; the identifier is not drawn | +| `step: 'step_4'` | The fourth step assigned by the application | The `step_4` column on the bottom axis | +| `action_type: 'follow'` | A follow activity | Determines the dot's color category | +| `medium_type: 'Liveroom'` | The related object category is a live room | The `Liveroom` column in medium mode; step mode still positions it at `step_4` | +| `time_stamp: 1751860800000` | July 7, 2025, 12:00 in Beijing (UTC+8) | Determines the horizontal position in time mode, but not step mode | +| `action_count: 7` | A follow-activity count of 7 in this record | Draws one dot; time mode adds 7 to the top total for that timestamp | + +The dot at the intersection of the `user_004` row and `step_4` column represents this record. **The count of 7 does not mean seven users or necessarily seven distinct live rooms.** `Liveroom` specifies only an object category. A business application would need additional fields, such as a room ID, to identify the actual room. + +The example also supplies this link: + +```js +{ from: 'event_004_4', to: 'event_004_6', pattern_type: 'pattern_001' } +``` + +`event_004_6` is a private-message record for the same user. The line means **these follow and private-message records are annotated as a relationship to inspect together**. It helps an analyst locate the pair for further log inspection. In this example, `pattern_001` is only a category identifier with no detection rule defined. The link does not establish causation or chronological order. + ## How to read the chart This example shows 111 events from 10 users, organized by behavioral step, together with explicitly supplied pattern relationships. diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md index 5a9df48cae..0cd752a21d 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -22,6 +22,47 @@ option: sequenceAnalysisChart **这份数据的用途是演示可视化能力。** 本地 demo 没有定义 `pattern_001`、`pattern_002` 的具体业务规则,也没有给顶部 `+N` 指定完整的统计口径。因此,不能据此判断某个用户异常、某两人使用同一个 IP,或两个事件存在因果关系。实际用于业务分析时,需要由业务方给出有明确名称和判断依据的关联,再用这张图展示其结果。 +## 用一条数据理解具体场景 + +下面摘取了本例中真实使用的一条数据。可以把它放到“直播平台用户行为分析”的场景中理解:**统计系统为用户 `user_004` 记录了一条与直播间有关的关注行为,归在第 4 个步骤,记录时间为北京时间 2025 年 7 月 7 日 12:00,行为计数为 7。** 这是对模拟字段的一种业务解读。 + +```js +// eventData 中 user_004 的一条事件摘录 +{ + user_id: 'user_004', + dots: [ + { + node_name: 'event_004_4', + step: 'step_4', + action_type: 'follow', + medium_type: 'Liveroom', + time_stamp: 1751860800000, + action_count: 7 + } + ] +} +``` + +| 字段 | 这条记录表达的含义 | 图上如何呈现 | +| --------------------------- | -------------------------------- | ---------------------------------------------------------- | +| `user_id: 'user_004'` | 行为所属的用户 | 位于该用户头像对应的行 | +| `node_name: 'event_004_4'` | 这条事件记录的唯一编号 | 供连接数据引用,不直接显示编号 | +| `step: 'step_4'` | 业务指定的第 4 个步骤 | 位于底部 `step_4` 对应的列 | +| `action_type: 'follow'` | 行为是“关注” | 决定圆点所属的颜色类别 | +| `medium_type: 'Liveroom'` | 这条行为涉及的对象类型是“直播间” | 在介质模式中归入 `Liveroom` 列;步骤模式仍按 `step_4` 定位 | +| `time_stamp: 1751860800000` | 北京时间 2025-07-07 12:00 | 时间模式下决定横坐标,步骤模式下不决定位置 | +| `action_count: 7` | 这条记录携带的关注行为计数为 7 | 仍画一个点;时间模式下将 7 累加到对应时刻的顶部总数 | + +因此,你在 `user_004` 行、`step_4` 列看到的那个点,代表的就是这条记录。**7 是行为计数,不是 7 个用户,也不能据此认定是 7 个不同的直播间。** `Liveroom` 只给出了对象类型;如果业务需要展示具体哪个直播间,还需要在数据中补充直播间 ID 等信息。 + +本例还提供了这样一条连接: + +```js +{ from: 'event_004_4', to: 'event_004_6', pattern_type: 'pattern_001' } +``` + +`event_004_6` 是同一用户的一条私信记录。因此,这条线表达的是:**把这条关注记录和那条私信记录标注为一组需要一起观察的行为关联。** 它可以帮助分析人员定位这组记录并进一步查看日志。`pattern_001` 在本例中只是关联类别的代号,没有定义识别规则;连线也不表示“关注导致私信”或实际发生时间的先后。 + ## 如何读图 这个示例用 10 位用户的 111 个事件展示行为步骤,以及数据中指定的模式关联。 From 79ddd3f8b84609f0c1a7e9ca7af328fa9e697078 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 19:30:35 +0800 Subject: [PATCH 14/16] docs: shorten all sequence demos with concrete data examples --- .../sequence-analysis-medium.md | 18 ++--- .../extension-chart/sequence-analysis-step.md | 68 ++----------------- .../sequence-analysis-target.md | 19 ++---- .../sequence-analysis-medium.md | 18 ++--- .../extension-chart/sequence-analysis-step.md | 68 ++----------------- .../sequence-analysis-target.md | 19 ++---- 6 files changed, 30 insertions(+), 180 deletions(-) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md index c318dd56f3..b7747f5d77 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md @@ -10,21 +10,13 @@ option: sequenceAnalysisChart # Sequence Analysis: Medium Curves -Groups the local demo’s user events by medium and draws curved pattern links. +Group events by the type of object they involve (their medium) and inspect relationships across users and object categories. -## How to read the chart +**Data example:** `event_001_1` is an IP-related registration record for `user_001`; `event_002_2` is a device-related login record for `user_002`. Their curve marks a supplied relationship for joint inspection. These categories alone do not establish a shared IP address or device. -This example aligns the same user events by medium and uses curves to show pattern relationships across events, users and media. - -- **Vertical axis and left avatars:** each row represents a user (`user_id`), with an avatar from `userImageMap`. -- **Top axis:** `IP`, `Device`, `User`, `Liveroom`, `Email` and `Tel` are event medium categories (`medium_type`). Categories are equally spaced and do not encode chronological order or elapsed time. -- **Colored dots:** each dot is an event record, colored by `action_type`. User and medium determine its position. Events with the same user and medium overlap, so fewer dots may be visible than the total number of events. -- **Curves:** each curve corresponds to a `patternData` record connecting the events referenced by `from` and `to`. A curve across rows relates different users' events; a curve across columns relates different media. Color groups links by `pattern_type`. -- **Nearby curves:** the example includes repeated relationships with identical endpoints, such as three records from `event_001_1` to `event_002_2`. Different curvature separates these records visually; curvature does not encode relationship weight or strength. - -For example, `event_001_1` is in the IP column for `user_001`, and `event_002_2` is in the Device column for `user_002`. The curves between them show the supplied pattern relationships. - -Use the bottom range selector to filter media, the horizontal gray scrollbar to pan across categories, and the right scrollbar to browse users. The initial window shows only part of the data. +- **Axes:** each row is a user; columns are medium categories such as IP, device, user and live room, rather than time. +- **Dots and curves:** dot colors group activities; curve colors group relationships. Events for the same user and medium may overlap. Repeated relationships with identical endpoints receive distinct curvature. +- **Controls:** select a range at the bottom, pan across media horizontally, and scroll vertically to see more users. ## Demo diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md index 278f6def9b..b7487d32c2 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -10,71 +10,13 @@ option: sequenceAnalysisChart # Sequence Analysis: Step Mode -This chart demonstrates how to compare multiple users' event histories and highlight selected behavioral segments. Think of it as a user activity comparison table: each row is a user, columns show behavioral steps, and colored dots represent events such as registration, login, follow and private messaging. +Compare users' behavioral steps and locate segments annotated as related. -## What this example represents +**Data example:** `event_004_4` is a room-related follow record for `user_004` at step 4, with a count of 7. Its link to the private-message record `event_004_6` at step 6 marks the pair for inspection together. The relationship is supplied by the example. -Imagine reviewing platform activity logs to understand which steps each user took and where selected behavioral segments appear across users. Aligning the records by step lets you compare them together instead of reading logs one record at a time. - -For example, `user_004` has a follow event at step 4 and a private-message event (`im`) at step 6. The line between them means the input data marks these events as a relationship to inspect together. For `user_005`, follow at step 4 is linked to private messaging at step 5. The chart makes the positions of these similar segments easy to compare. Intermediate events remain visible; a link does not mean that the user skipped those steps. - -Dots are event records; links are additional relationship annotations supplied through `patternData`. The chart displays these annotations and does not automatically discover behavioral patterns in the logs. - -**This dataset demonstrates visualization features.** The local demo does not define business rules for `pattern_001` or `pattern_002`, or a complete metric definition for the top `+N` labels. It cannot establish anomalous activity, a shared IP address or a causal relationship. A business application should supply clearly named relationships with defined criteria and use the chart to display those results. - -## A concrete record and its scenario - -The following record is taken directly from this example. One possible business interpretation is live-streaming platform activity analysis: **the system records a room-related follow activity for `user_004`, assigns it to step 4, timestamps it at July 7, 2025, 12:00 in Beijing (UTC+8), and gives it an activity count of 7.** This provides a scenario for interpreting the synthetic fields. - -```js -// An event excerpt for user_004 from eventData -{ - user_id: 'user_004', - dots: [ - { - node_name: 'event_004_4', - step: 'step_4', - action_type: 'follow', - medium_type: 'Liveroom', - time_stamp: 1751860800000, - action_count: 7 - } - ] -} -``` - -| Field | Meaning in this record | Visual representation | -| --------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------ | -| `user_id: 'user_004'` | The user associated with the activity | The row identified by that user's avatar | -| `node_name: 'event_004_4'` | Unique identifier of the event record | Referenced by link data; the identifier is not drawn | -| `step: 'step_4'` | The fourth step assigned by the application | The `step_4` column on the bottom axis | -| `action_type: 'follow'` | A follow activity | Determines the dot's color category | -| `medium_type: 'Liveroom'` | The related object category is a live room | The `Liveroom` column in medium mode; step mode still positions it at `step_4` | -| `time_stamp: 1751860800000` | July 7, 2025, 12:00 in Beijing (UTC+8) | Determines the horizontal position in time mode, but not step mode | -| `action_count: 7` | A follow-activity count of 7 in this record | Draws one dot; time mode adds 7 to the top total for that timestamp | - -The dot at the intersection of the `user_004` row and `step_4` column represents this record. **The count of 7 does not mean seven users or necessarily seven distinct live rooms.** `Liveroom` specifies only an object category. A business application would need additional fields, such as a room ID, to identify the actual room. - -The example also supplies this link: - -```js -{ from: 'event_004_4', to: 'event_004_6', pattern_type: 'pattern_001' } -``` - -`event_004_6` is a private-message record for the same user. The line means **these follow and private-message records are annotated as a relationship to inspect together**. It helps an analyst locate the pair for further log inspection. In this example, `pattern_001` is only a category identifier with no detection rule defined. The link does not establish causation or chronological order. - -## How to read the chart - -This example shows 111 events from 10 users, organized by behavioral step, together with explicitly supplied pattern relationships. - -- **Vertical axis and left avatars:** each row represents a user (`user_id`), with an avatar from `userImageMap`. Follow a row to inspect that user's events across steps. -- **Bottom axis:** `step_1`, `step_2`, and so on are event categories from `dots.step`. Steps are equally spaced; their distance does not encode elapsed time. -- **Top labels:** each label shows the medium name and additional count supplied in `stepMediumMap`. For example, `+1` in `Device... +1` comes from the step's `count`. This is a supplied additional count without a defined business metric in this demo; it is not the number of event dots. -- **Colored dots:** each dot is an event record, colored by `action_type`, such as registration, login or comment. User and step determine its position. Multiple events for the same user and step can overlap. -- **Links:** each line corresponds to a `patternData` record whose `from` and `to` reference event `node_name` values. It represents a supplied pattern relationship, colored by `pattern_type`. All links in this step example stay within a single user row and annotate that user’s event segments. -- **Short segments beside dots:** this example enables link extension along step bands and includes pattern records that reference the same event at both ends. Such records can appear as short segments beside a dot. Segment length does not encode duration or relationship strength. - -Use the bottom range selector to filter steps, the horizontal gray scrollbar to pan across them, and the right scrollbar to browse users. The initial window shows only part of the steps and users. +- **Axes:** each row is a user; columns are steps. Top labels show each step's configured medium category, such as room or device, and an additional `+N` count. +- **Dots and links:** dots are event records colored by activity; links mark related segments and are colored by relationship category. A count of 7 still produces one dot. +- **Controls:** select a range at the bottom, pan across steps horizontally, and scroll vertically to see more users. ## Demo diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md index 297a1ddec1..8a353cd35d 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md @@ -10,22 +10,13 @@ option: sequenceAnalysisChart # Sequence Analysis: Time with Target Avatars -Places the local demo’s events on a time axis, with midpoint avatars and target arrows on links. +Inspect user activity over time and locate related events and associated users for further investigation. -## How to read the chart +**Data example:** at 10:00 on July 7, 2025 (Beijing, UTC+8), `user_002` has login record `event_002_2` (count 7), and `user_003` has private-message record `event_003_2` (count 2). The example links them, points an arrow to the message record and displays the configured `user_003` avatar at the midpoint. This is a supplied relationship, not an automatically detected real interaction. -This example positions user events by their occurrence time and uses arrows and link avatars to display directed event relationships. - -- **Vertical axis and left avatars:** each row represents a user (`user_id`); the left avatar identifies that user. -- **Bottom axis:** dates and times correspond to the millisecond `time_stamp` values. Horizontal distance reflects elapsed time, and simultaneous events align vertically. -- **Top numbers and vertical dashed lines:** each dashed line marks an event timestamp. The number above it sums `action_count` across all users' events at that timestamp. For example, `141` is an aggregate count, not 141 visible dots. -- **Colored dots:** each dot is an event record, colored by `action_type`. Events for the same user at the same time can overlap. Dot size does not encode `action_count`. -- **Lines and arrows:** each `actionData` record links the event referenced by `from` to the event referenced by `to`, with an arrow at the target. These relationships are explicitly supplied. Simultaneous events can also be connected, producing vertical links. -- **Midpoint avatars:** the image comes from the relationship's `actionData.user_image`. It is configured independently from the vertical-axis avatar and can represent a different user from the target event's row. This example retains the local demo's associated-user settings. - -For example, the link from `event_002_2` to `event_003_2` connects simultaneous events of two users. Its arrow points to the latter event, and the midpoint shows the configured `user_003` avatar. - -Use the bottom range selector to filter a time interval, the horizontal gray scrollbar to pan the time window, and the right scrollbar to browse users. The initial window shows only part of the data. +- **Axes:** each row is a user; horizontal position shows occurrence time. Top numbers sum activity counts across all users at each timestamp. +- **Dots and links:** dot colors group activities; arrows show link direction. Midpoint avatars are configured separately and can differ from the target event's user. +- **Controls:** select a time range at the bottom, pan the time window horizontally, and scroll vertically to see more users. ## Demo diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md index 1fdde1fe4a..a354ce1180 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md @@ -10,21 +10,13 @@ option: sequenceAnalysisChart # 时序分析:介质曲线连接 -按介质分类展示本地 demo 的用户事件,通过曲线表达模式连接。 +用于按事件涉及的对象类型(介质)归类,查看不同用户、不同类型事件之间的关联。 -## 如何读图 +**数据举例**:`event_001_1` 是用户 `user_001` 的注册记录,介质为 IP;`event_002_2` 是用户 `user_002` 的登录记录,介质为设备。连接它们的曲线表示这两条记录被预设为一组关联,方便一起排查。这里只给出对象类型,不能推断两人共用同一个 IP 或设备。 -这个示例将同一批用户事件按介质分类对齐,用曲线展示跨事件的模式关联,便于观察不同用户、不同介质之间的联系。 - -- **纵轴与左侧头像**:每一行对应一个用户(`user_id`),头像来自 `userImageMap`。 -- **顶部横轴**:`IP`、`Device`、`User`、`Liveroom`、`Email`、`Tel` 是事件的介质类型(`medium_type`)。横轴按类别等距排列,不表示时间先后或时间间隔。 -- **彩色圆点**:每个点是一条事件记录,颜色按 `action_type` 区分。点的位置由用户和介质类型决定;同一用户在相同介质上的多条事件会落在同一位置,所以可见圆点数量可能少于事件总数。 -- **曲线**:每条曲线对应一条 `patternData`,连接 `from` 和 `to` 指定的两个事件。跨行表示关联发生在不同用户的事件之间,跨列表示两个事件属于不同介质;曲线颜色按 `pattern_type` 区分。 -- **多条相近的曲线**:本例保留了多条起终点相同的关联,例如 `event_001_1` 到 `event_002_2` 的三条记录。图表为它们分配不同曲率,便于分辨重复关联;弯曲程度不表示关联权重或强弱。 - -例如,`event_001_1` 位于用户 `user_001` 的 IP 列,`event_002_2` 位于用户 `user_002` 的 Device 列,两者之间的曲线表示数据中指定的模式关联。 - -底部范围选择器用于筛选介质范围;灰色横向滚动条用于平移可见类别,右侧滚动条用于浏览其他用户。初始窗口只显示部分数据。 +- **坐标轴**:每行是一位用户,横轴是 IP、设备、用户、直播间等介质类型,不表示时间。 +- **点与线**:点颜色区分行为,线颜色区分关联类别;同一用户、同类介质的事件可能重叠。相同端点的多条关联用不同曲率区分。 +- **操作**:底部选择范围、横向滚动查看介质,右侧滚动查看其他用户。 ## 代码演示 diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md index 0cd752a21d..189a8a573a 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -10,71 +10,13 @@ option: sequenceAnalysisChart # 时序分析:步骤模式 -这张图演示如何把多位用户的行为记录放在一起比较,并标出需要关注的行为片段。可以把它理解为一张“用户行为对照表”:每行是一位用户,从左到右是他的行为步骤,彩色点代表注册、登录、关注、私信等事件。 +用于对照多位用户的行为步骤,查看哪些行为片段被标记为关联。 -## 这个示例代表什么 +**数据举例**:`event_004_4` 表示用户 `user_004` 在第 4 步的一条直播间相关关注记录,计数为 7。它与第 6 步的私信记录 `event_004_6` 相连,表示这两条记录被选为一组,供分析人员一起查看;关联由示例预先给定。 -假设你正在查看一批用户的平台操作记录,希望了解:每个人经过了哪些行为步骤?某类行为片段出现在了哪些用户身上、各自位于什么位置?这张图把这些记录按步骤对齐,方便同时查看,而不用逐条翻阅日志。 - -以本例的 `user_004` 为例:第 4 步是关注(`follow`),第 6 步是私信(`im`),示例在这两个事件之间画了一条线,表示输入数据把它们标记为一组需要一起观察的行为关联。`user_005` 的第 4 步关注和第 5 步私信也被连接起来,因此可以直观看到类似行为片段在不同用户的哪几个步骤出现。第 5 步等中间事件仍保留在图中,连线不表示用户跳过了它们。 - -这里的点是用户的事件记录,线是额外提供的关联标注。连线由 `patternData` 指定,图表负责呈现这些标注,不会自动从日志中识别行为模式。 - -**这份数据的用途是演示可视化能力。** 本地 demo 没有定义 `pattern_001`、`pattern_002` 的具体业务规则,也没有给顶部 `+N` 指定完整的统计口径。因此,不能据此判断某个用户异常、某两人使用同一个 IP,或两个事件存在因果关系。实际用于业务分析时,需要由业务方给出有明确名称和判断依据的关联,再用这张图展示其结果。 - -## 用一条数据理解具体场景 - -下面摘取了本例中真实使用的一条数据。可以把它放到“直播平台用户行为分析”的场景中理解:**统计系统为用户 `user_004` 记录了一条与直播间有关的关注行为,归在第 4 个步骤,记录时间为北京时间 2025 年 7 月 7 日 12:00,行为计数为 7。** 这是对模拟字段的一种业务解读。 - -```js -// eventData 中 user_004 的一条事件摘录 -{ - user_id: 'user_004', - dots: [ - { - node_name: 'event_004_4', - step: 'step_4', - action_type: 'follow', - medium_type: 'Liveroom', - time_stamp: 1751860800000, - action_count: 7 - } - ] -} -``` - -| 字段 | 这条记录表达的含义 | 图上如何呈现 | -| --------------------------- | -------------------------------- | ---------------------------------------------------------- | -| `user_id: 'user_004'` | 行为所属的用户 | 位于该用户头像对应的行 | -| `node_name: 'event_004_4'` | 这条事件记录的唯一编号 | 供连接数据引用,不直接显示编号 | -| `step: 'step_4'` | 业务指定的第 4 个步骤 | 位于底部 `step_4` 对应的列 | -| `action_type: 'follow'` | 行为是“关注” | 决定圆点所属的颜色类别 | -| `medium_type: 'Liveroom'` | 这条行为涉及的对象类型是“直播间” | 在介质模式中归入 `Liveroom` 列;步骤模式仍按 `step_4` 定位 | -| `time_stamp: 1751860800000` | 北京时间 2025-07-07 12:00 | 时间模式下决定横坐标,步骤模式下不决定位置 | -| `action_count: 7` | 这条记录携带的关注行为计数为 7 | 仍画一个点;时间模式下将 7 累加到对应时刻的顶部总数 | - -因此,你在 `user_004` 行、`step_4` 列看到的那个点,代表的就是这条记录。**7 是行为计数,不是 7 个用户,也不能据此认定是 7 个不同的直播间。** `Liveroom` 只给出了对象类型;如果业务需要展示具体哪个直播间,还需要在数据中补充直播间 ID 等信息。 - -本例还提供了这样一条连接: - -```js -{ from: 'event_004_4', to: 'event_004_6', pattern_type: 'pattern_001' } -``` - -`event_004_6` 是同一用户的一条私信记录。因此,这条线表达的是:**把这条关注记录和那条私信记录标注为一组需要一起观察的行为关联。** 它可以帮助分析人员定位这组记录并进一步查看日志。`pattern_001` 在本例中只是关联类别的代号,没有定义识别规则;连线也不表示“关注导致私信”或实际发生时间的先后。 - -## 如何读图 - -这个示例用 10 位用户的 111 个事件展示行为步骤,以及数据中指定的模式关联。 - -- **纵轴与左侧头像**:每一行对应一个用户(`user_id`),头像来自 `userImageMap`。沿着同一行可以查看这个用户在各个步骤上的事件。 -- **底部横轴**:`step_1`、`step_2` 等表示事件所属的步骤(`dots.step`)。它是分类轴,相邻步骤等距排列,间距不表示实际耗时。 -- **顶部标签**:展示 `stepMediumMap` 为每个步骤配置的介质名称和附加数量。例如 `Device... +1` 中的 `+1` 来自该步骤的 `count`,是示例预先给定的附加计数,示例未定义其业务统计口径,也不是图中事件点的个数。 -- **彩色圆点**:每个点是一条事件记录,颜色按 `action_type` 区分,例如注册、登录、评论等。点的位置由用户和步骤共同决定;同一用户在同一步骤的多条事件可能重叠。 -- **连接线**:每条线对应一条 `patternData`,通过 `from` / `to` 指向事件的 `node_name`,表示示例指定的模式关联;线的颜色按 `pattern_type` 区分。本步骤示例中的线均位于同一用户行,用于标注该用户的事件片段。 -- **节点旁的短线**:本例开启了沿步骤带的连接延伸,并包含起终点为同一事件的模式记录,因此部分关联呈现为点旁的短线。延伸长度只是绘制样式,不代表持续时间或关联强度。 - -底部范围选择器用于筛选横轴范围;灰色横向滚动条用于平移可见步骤,右侧滚动条用于浏览其他用户。初始窗口只显示部分步骤和用户,可拖动查看完整数据。 +- **坐标轴**:每行是一位用户,横轴是步骤;顶部显示该步骤配置的介质类型(如直播间、设备)及附加计数 `+N`。 +- **点与线**:点代表事件记录,颜色区分行为类型;线标注关联片段,颜色区分关联类别。计数 7 仍对应一个点。 +- **操作**:底部选择范围、横向滚动查看步骤,右侧滚动查看其他用户。 ## 代码演示 diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md index 83809912d2..ad153fee6c 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md @@ -10,22 +10,13 @@ option: sequenceAnalysisChart # 时序分析:时间模式 + 目标头像 -按时间展示本地 demo 的用户事件,通过连接中点头像和目标端箭头表达事件关系。 +用于沿时间查看多位用户的活动,并定位需要一起检查的事件及其关联用户。 -## 如何读图 +**数据举例**:北京时间 2025-07-07 10:00,`user_002` 有一条登录记录 `event_002_2`(计数 7),`user_003` 有一条私信记录 `event_003_2`(计数 2)。示例将两者相连,箭头指向私信记录,中间显示配置的 `user_003` 头像,表示一组预先指定的关联,并非自动识别的真实互动。 -这个示例按真实发生时间排列用户事件,并用箭头和连接头像展示事件之间的有向关联。 - -- **纵轴与左侧头像**:每一行对应一个用户(`user_id`),左侧头像用于识别这一行的用户。 -- **底部横轴**:展示事件的日期和时间,位置由毫秒时间戳 `time_stamp` 决定。横向间距反映实际时间间隔;同一时刻的事件在竖直方向对齐。 -- **顶部数字与竖向虚线**:虚线标记有事件发生的时间点,顶部数字是该时刻所有用户事件的 `action_count` 总和。例如顶部的 `141` 表示该时刻累计数量为 141,并不是有 141 个可见圆点。 -- **彩色圆点**:每个点是一条事件记录,颜色按 `action_type` 区分。相同用户在同一时刻的多条事件可能重叠;`action_count` 不通过点的大小编码。 -- **连接线与箭头**:每条连接来自 `actionData`,由 `from` 指定的事件指向 `to` 指定的事件,箭头位于目标端。连接关系由示例数据给定;即使两个事件发生在同一时刻,也可以形成竖向连接。 -- **连接中间的头像**:表示这条关联所配置的用户头像,取自 `actionData.user_image`。它与纵轴头像是独立配置,可能与终点所在行的用户不同;本例沿用本地 demo 的关联用户设置。 - -例如,`event_002_2` 到 `event_003_2` 的连接关联了两个用户同一时刻的事件,箭头指向后者,中间显示这条连接配置的 `user_003` 头像。 - -底部范围选择器用于筛选时间区间;灰色横向滚动条用于平移时间窗口,右侧滚动条用于浏览其他用户。初始窗口只显示部分数据。 +- **坐标轴**:每行是一位用户,横轴是发生时间;顶部数字是该时刻所有用户的行为计数之和。 +- **点与线**:点颜色区分行为,箭头表示连接方向;中间头像由连接数据单独指定,可以与终点用户不同。 +- **操作**:底部选择时间范围,横向滚动平移时间窗口,右侧滚动查看其他用户。 ## 代码演示 From 779011a3df0343925113428ae23c6ca0a936d4f4 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 12 Oct 2026 19:44:30 +0800 Subject: [PATCH 15/16] docs: explain sequence design relationships with hypothetical scenarios --- .../examples/en/extension-chart/sequence-analysis-medium.md | 6 ++++-- .../examples/en/extension-chart/sequence-analysis-step.md | 6 ++++-- .../examples/en/extension-chart/sequence-analysis-target.md | 6 ++++-- .../examples/zh/extension-chart/sequence-analysis-medium.md | 6 ++++-- .../examples/zh/extension-chart/sequence-analysis-step.md | 6 ++++-- .../examples/zh/extension-chart/sequence-analysis-target.md | 6 ++++-- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md index b7747f5d77..daa6df6ecf 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md @@ -10,9 +10,11 @@ option: sequenceAnalysisChart # Sequence Analysis: Medium Curves -Group events by the type of object they involve (their medium) and inspect relationships across users and object categories. +**Design meaning: shared media.** Identify whether different users' activities pass through the same specific device, IP address or live room. -**Data example:** `event_001_1` is an IP-related registration record for `user_001`; `event_002_2` is a device-related login record for `user_002`. Their curve marks a supplied relationship for joint inspection. These categories alone do not establish a shared IP address or device. +**Hypothetical scenario:** user A connects through IP₁ and user B through IP₂, but both log in using device D. Their medium paths meet at D, providing a link between the accounts. The shared entity is the actual device D, not merely the Device category. + +**Example data:** a curve links `user_001`'s registration record `event_001_1` (IP category) to `user_002`'s login record `event_002_2` (device category). Only categories and endpoints are supplied, without concrete medium IDs. This demonstrates curve rendering but does not establish which medium the users share. - **Axes:** each row is a user; columns are medium categories such as IP, device, user and live room, rather than time. - **Dots and curves:** dot colors group activities; curve colors group relationships. Events for the same user and medium may overlap. Repeated relationships with identical endpoints receive distinct curvature. diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md index b7487d32c2..8b6c06e7e1 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -10,9 +10,11 @@ option: sequenceAnalysisChart # Sequence Analysis: Step Mode -Compare users' behavioral steps and locate segments annotated as related. +**Design meaning: risk behavior patterns.** Highlight or connect action nodes that match a business-defined risk pattern to compare behavioral segments across users. -**Data example:** `event_004_4` is a room-related follow record for `user_004` at step 4, with a count of 7. Its link to the private-message record `event_004_6` at step 6 marks the pair for inspection together. The relationship is supplied by the example. +**Hypothetical scenario:** a business marks “log in from a new device → change the linked email → send messages in bulk” as a pattern requiring review. Connecting the matching actions shows which users exhibit the pattern and at which steps. This rule is illustrative. + +**Example data:** `user_004` has follow record `event_004_4` at step 4 (count 7), linked to private-message record `event_004_6` at step 6. This demonstrates pattern annotation; no actual risk rule is defined for `pattern_001`. - **Axes:** each row is a user; columns are steps. Top labels show each step's configured medium category, such as room or device, and an additional `+N` count. - **Dots and links:** dots are event records colored by activity; links mark related segments and are colored by relationship category. A count of 7 still produces one dot. diff --git a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md index 8a353cd35d..e6794f47df 100644 --- a/docs/assets/examples/en/extension-chart/sequence-analysis-target.md +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md @@ -10,9 +10,11 @@ option: sequenceAnalysisChart # Sequence Analysis: Time with Target Avatars -Inspect user activity over time and locate related events and associated users for further investigation. +**Design meaning: action targets.** Inspect whether multiple users act on the same object at similar times, such as sending messages to the same user. -**Data example:** at 10:00 on July 7, 2025 (Beijing, UTC+8), `user_002` has login record `event_002_2` (count 7), and `user_003` has private-message record `event_003_2` (count 2). The example links them, points an arrow to the message record and displays the configured `user_003` avatar at the midpoint. This is a supplied relationship, not an automatically detected real interaction. +**Hypothetical scenario:** users A, B and C all message user T within the same minute. The time axis shows temporal concentration, while T's target avatar reveals concentration on the same recipient. T receives the actions rather than sending them. + +**Example data:** at 10:00 on July 7, 2025 (Beijing, UTC+8), `user_002`'s login record `event_002_2` (count 7) links to `user_003`'s private-message record `event_003_2` (count 2), with a configured `user_003` midpoint avatar. These mock records demonstrate arrows and avatars; they do not fully model the hypothetical group-message scenario above. - **Axes:** each row is a user; horizontal position shows occurrence time. Top numbers sum activity counts across all users at each timestamp. - **Dots and links:** dot colors group activities; arrows show link direction. Midpoint avatars are configured separately and can differ from the target event's user. diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md index a354ce1180..55cb964191 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md @@ -10,9 +10,11 @@ option: sequenceAnalysisChart # 时序分析:介质曲线连接 -用于按事件涉及的对象类型(介质)归类,查看不同用户、不同类型事件之间的关联。 +**设计上表达的关系:共享介质关系。** 查看不同用户经过的具体介质是否重合,例如同一台设备、同一个 IP 或同一个直播间。 -**数据举例**:`event_001_1` 是用户 `user_001` 的注册记录,介质为 IP;`event_002_2` 是用户 `user_002` 的登录记录,介质为设备。连接它们的曲线表示这两条记录被预设为一组关联,方便一起排查。这里只给出对象类型,不能推断两人共用同一个 IP 或设备。 +**假设场景**:用户 A 通过 IP₁、用户 B 通过 IP₂,都使用设备 D 登录。两人的介质路径在设备 D 重合,提供了账号之间的关联线索;共同点是同一台设备 D,而不仅是都属于 Device 类型。 + +**本例数据**:曲线连接 `user_001` 的注册记录 `event_001_1`(IP 类)与 `user_002` 的登录记录 `event_002_2`(设备类)。本例仅提供类型和连接端点,未提供具体介质 ID,因此这条线演示连接效果,不能据此确认两人共享哪个介质。 - **坐标轴**:每行是一位用户,横轴是 IP、设备、用户、直播间等介质类型,不表示时间。 - **点与线**:点颜色区分行为,线颜色区分关联类别;同一用户、同类介质的事件可能重叠。相同端点的多条关联用不同曲率区分。 diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md index 189a8a573a..42b8c614ee 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -10,9 +10,11 @@ option: sequenceAnalysisChart # 时序分析:步骤模式 -用于对照多位用户的行为步骤,查看哪些行为片段被标记为关联。 +**设计上表达的关系:风险行为模式。** 将命中某种业务风险特征的动作节点高亮或连接起来,方便比较不同用户的行为片段。 -**数据举例**:`event_004_4` 表示用户 `user_004` 在第 4 步的一条直播间相关关注记录,计数为 7。它与第 6 步的私信记录 `event_004_6` 相连,表示这两条记录被选为一组,供分析人员一起查看;关联由示例预先给定。 +**假设场景**:某业务将“更换设备登录 → 修改绑定邮箱 → 批量发送消息”标记为待复核的行为模式。图中把命中的动作连接起来,分析人员就能找到哪些用户出现了这组行为,以及它们处于哪些步骤。这里是帮助理解的假设规则。 + +**本例数据**:`user_004` 第 4 步的关注记录 `event_004_4`(计数 7)与第 6 步的私信记录 `event_004_6` 被连线标记。本例只演示模式标注,未定义 `pattern_001` 的实际风险规则。 - **坐标轴**:每行是一位用户,横轴是步骤;顶部显示该步骤配置的介质类型(如直播间、设备)及附加计数 `+N`。 - **点与线**:点代表事件记录,颜色区分行为类型;线标注关联片段,颜色区分关联类别。计数 7 仍对应一个点。 diff --git a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md index ad153fee6c..c6bc1057ed 100644 --- a/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md @@ -10,9 +10,11 @@ option: sequenceAnalysisChart # 时序分析:时间模式 + 目标头像 -用于沿时间查看多位用户的活动,并定位需要一起检查的事件及其关联用户。 +**设计上表达的关系:行为作用对象。** 查看多个用户是否在相近时间对同一个对象采取行动,例如向同一用户发送消息。 -**数据举例**:北京时间 2025-07-07 10:00,`user_002` 有一条登录记录 `event_002_2`(计数 7),`user_003` 有一条私信记录 `event_003_2`(计数 2)。示例将两者相连,箭头指向私信记录,中间显示配置的 `user_003` 头像,表示一组预先指定的关联,并非自动识别的真实互动。 +**假设场景**:用户 A、B、C 在同一分钟内都给用户 T 发消息。时间轴展示行为的时间聚集,目标头像标识 T,让分析人员同时看到这些行为也集中作用于同一个对象。T 是行为接收方,不是发送者。 + +**本例数据**:北京时间 2025-07-07 10:00,`user_002` 的登录记录 `event_002_2`(计数 7)连向 `user_003` 的私信记录 `event_003_2`(计数 2),中间显示配置的 `user_003` 头像。这组模拟记录用于演示箭头和头像,不是上述多人向 T 发消息场景的完整数据。 - **坐标轴**:每行是一位用户,横轴是发生时间;顶部数字是该时刻所有用户的行为计数之和。 - **点与线**:点颜色区分行为,箭头表示连接方向;中间头像由连接数据单独指定,可以与终点用户不同。 From 41fb3ddd367e9cd222e6752a0956d588b1df18f6 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 13 Oct 2026 15:51:06 +0800 Subject: [PATCH 16/16] fix: restore sequence dataZoom preview and link filtering --- ...x-sequence-datazoom-filter_2026-10-13.json | 10 ++ .../data-zoom/sequence-analysis.test.ts | 110 ++++++++++++++++-- .../datazoom/sequence-preview-filter.mjs | 103 ++++++++++++++++ .../vchart/__tests__/visual/cases/index.mjs | 5 + .../data-zoom/data-filter-base-component.ts | 46 ++++---- 5 files changed, 242 insertions(+), 32 deletions(-) create mode 100644 common/changes/@visactor/vchart/fix-sequence-datazoom-filter_2026-10-13.json create mode 100644 packages/vchart/__tests__/visual/cases/components/datazoom/sequence-preview-filter.mjs diff --git a/common/changes/@visactor/vchart/fix-sequence-datazoom-filter_2026-10-13.json b/common/changes/@visactor/vchart/fix-sequence-datazoom-filter_2026-10-13.json new file mode 100644 index 0000000000..2a4edabe54 --- /dev/null +++ b/common/changes/@visactor/vchart/fix-sequence-datazoom-filter_2026-10-13.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "fix: restore sequence link filtering in dataZoom and exclude categorical IDs from preview sums", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} diff --git a/packages/vchart/__tests__/unit/component/data-zoom/sequence-analysis.test.ts b/packages/vchart/__tests__/unit/component/data-zoom/sequence-analysis.test.ts index d5852474ac..cb569e3dfc 100644 --- a/packages/vchart/__tests__/unit/component/data-zoom/sequence-analysis.test.ts +++ b/packages/vchart/__tests__/unit/component/data-zoom/sequence-analysis.test.ts @@ -1,13 +1,15 @@ import { DataSet, DataView } from '@visactor/vdataset'; -import { BandScale, type IBandLikeScale } from '@visactor/vscale'; +import { BandScale, LinearScale, type IBandLikeScale } from '@visactor/vscale'; import { DataZoom } from '../../../../src/component/data-zoom/data-zoom/data-zoom'; +import { dataFilterWithNewDomain } from '../../../../src/component/data-zoom/util'; +import { linkDotInfo } from '../../../../src/data/transforms/link-dot-info'; describe('sequence analysis DataZoom', () => { - it('keeps nested dot domains after data updates and excludes link data', () => { + it('keeps nested dot domains and link filter fields without summing numeric-looking user IDs', () => { const dataSet = new DataSet(); - const data = new DataView(dataSet).parse([{ user: 'U1', dots: [{ step: 'A' }, { step: 'B' }] }]); + const data = new DataView(dataSet).parse([{ user: '123456xxxx', dots: [{ step: 'A' }, { step: 'B' }] }]); const xScale = new BandScale().domain(['A', 'B']).range([0, 100]); - const yScale = new BandScale().domain(['U1']).range([0, 100]); + const yScale = new BandScale().domain(['123456xxxx']).range([0, 100]); const series = { id: 1, type: 'dot', @@ -20,27 +22,115 @@ describe('sequence analysis DataZoom', () => { }; const zoom: any = new DataZoom({}, { dataSet } as any); zoom._relatedAxisComponent = { id: 10 }; - zoom._regions = [{ getSeries: () => [{ type: 'link' }, series] }]; + zoom._regions = [ + { getSeries: () => [{ ...series, id: 2, type: 'link', getSpec: () => ({ fromField: 'from' }) }, series] } + ]; zoom._stateField = 'state'; zoom._valueField = 'value'; zoom._stateScale = new BandScale(); zoom._handleChange = jest.fn(); zoom._initData(); + expect(zoom._parseFieldOfSeries({ id: 2 })).toEqual(['from_xField']); expect(zoom._data.getLatestData()).toEqual([ - { state: 'A', value: 1 }, - { state: 'B', value: 1 } + { state: 'A', value: 0 }, + { state: 'B', value: 0 } ]); - data.parse([{ user: 'U1', dots: [{ step: 'B' }, { step: 'C' }] }]); + data.parse([{ user: '123456xxxx', dots: [{ step: 'B' }, { step: 'C' }] }]); zoom.onDataUpdate(); expect(zoom._data.getLatestData()).toEqual([ - { state: 'B', value: 1 }, - { state: 'C', value: 1 } + { state: 'B', value: 0 }, + { state: 'C', value: 0 } ]); expect(zoom.stateScale.domain()).toEqual(['B', 'C']); dataSet.destroy(); }); + it('keeps continuous preview values aligned when a categorical dot series comes first', () => { + const dataSet = new DataSet(); + const events = new DataView(dataSet).parse([{ user: '123456xxxx', dots: [{ time: 10 }, { time: 20 }] }]); + const counts = new DataView(dataSet).parse([ + { time: 10, count: 2 }, + { time: 20, count: 3 } + ]); + const xScale = new LinearScale().domain([0, 30]); + const series = (id: number, type: string, data: DataView, yField: string, yScale: any) => ({ + id, + type, + coordinate: 'cartesian', + getRawData: () => data, + getSpec: () => ({ xField: 'time', yField }), + getXAxisHelper: () => ({ getAxisId: () => 10, getScale: () => xScale }), + getYAxisHelper: () => ({ getAxisId: () => id + 10, getScale: () => yScale }) + }); + const zoom: any = new DataZoom({}, { dataSet } as any); + zoom._relatedAxisComponent = { id: 10 }; + zoom._regions = [ + { + getSeries: () => [ + series(1, 'dot', events, 'user', new BandScale()), + series(2, 'bar', counts, 'count', new LinearScale()) + ] + } + ]; + zoom._stateField = 'state'; + zoom._valueField = 'value'; + zoom._stateScale = new LinearScale(); + zoom._handleChange = jest.fn(); + zoom._initData(); + expect(zoom._data.getLatestData()).toEqual([ + { state: 10, value: 2 }, + { state: 20, value: 3 } + ]); + counts.parse([ + { time: 10, count: 4 }, + { time: 20, count: 5 } + ]); + zoom.onDataUpdate(); + expect(zoom._data.getLatestData()).toEqual([ + { state: 10, value: 4 }, + { state: 20, value: 5 } + ]); + dataSet.destroy(); + }); + + it.each([ + ['from', 'x'], + ['source', 'x'], + ['from', 'y'], + ['source', 'y'] + ])('filters links with fromField = %s on the %s axis', (fromField, dimension) => { + const links = ['a', 'b', 'c'].map(node => ({ [fromField]: node, target: 'c' })); + const data = linkDotInfo([], { + infoKey: 'dots', + fields: () => ({ fromField, toField: 'target', xField: 'time', yField: 'user' }), + linkData: () => links, + dotData: () => [ + { user: 20, dots: [{ node_name: 'a', time: 10 }] }, + { user: 50, dots: [{ node_name: 'b', time: 30 }] }, + { user: 30, dots: [{ node_name: 'c', time: 50 }] } + ] + }); + const series = { + id: 1, + type: 'link', + coordinate: 'cartesian', + getSpec: () => ({ fromField }), + getXAxisHelper: () => ({ getAxisId: () => 10 }), + getYAxisHelper: () => ({ getAxisId: () => 11 }) + }; + const zoom: any = new DataZoom({}, {} as any); + zoom._relatedAxisComponent = { id: dimension === 'x' ? 10 : 11 }; + zoom._regions = [{ getSeries: () => [series] }]; + expect(zoom._collectDataInfo().dataCollection).toEqual([]); + const filtered = dataFilterWithNewDomain(data, { + field: () => zoom._parseFieldOfSeries(series), + getNewDomain: () => [10, 40], + isContinuous: () => true + }); + expect(filtered.map(d => d[fromField])).toEqual(dimension === 'x' ? ['a', 'b'] : ['a', 'c']); + }); + it.each([false, true])('synchronizes linked axes with reversed range = %s', reverse => { const scale = new BandScale().domain(['A', 'B', 'C']).range(reverse ? [100, 0] : [0, 100]); const linkedScale = scale.clone(); diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/sequence-preview-filter.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/sequence-preview-filter.mjs new file mode 100644 index 0000000000..7b41fbe265 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/sequence-preview-filter.mjs @@ -0,0 +1,103 @@ +/** + * 验证目的:时序图 dataZoom 不累加用户 ID,且连线随时间选区过滤。 + * 图表类型:common(bar + link + dot)。 + * 关键配置:嵌套 dots、数字开头的分类 ID、连续时间轴、filter 模式。 + * 场景条件:五个时刻、两个用户,四条同一时刻的连线;缩放到中间三个时刻。 + * 最终检查:缩略图仅汇总柱状图数值,dot 与 link 同步过滤;截图检查预览和连线。 + * 覆盖边界:不覆盖曲线和头像,更新与系列顺序由单元测试覆盖。 + */ +export default { + createSpec() { + // 使用虚构用户和固定时刻复现分类 ID 被当作数值的问题。 + const dot = { + type: 'dot', + dataId: 'events', + xField: 'time', + yField: 'user', + titleField: 'user', + leftAppendPadding: 0, + title: { visible: false }, + subTitle: { visible: false }, + symbol: { visible: false } + }; + return { + type: 'common', + animation: false, + data: [ + { id: 'counts', values: [10, 20, 30, 40, 50].map((time, i) => ({ time, count: i + 1 })) }, + { + id: 'events', + values: ['123456xxxx', '789012xxxx'].map((user, row) => ({ + user, + dots: [10, 20, 30, 40, 50].map(time => ({ node_name: `${row}-${time}`, time })) + })) + }, + { + id: 'links', + values: [10, 20, 30, 40].map(time => ({ from: `0-${time}`, to: `1-${time}` })) + } + ], + series: [ + { + type: 'bar', + dataId: 'counts', + xField: 'time', + yField: 'count', + barWidth: 12, + bar: { style: { fill: '#ddd' } } + }, + { + type: 'link', + dataId: 'links', + dotSeriesIndex: 2, + clipHeight: 600, + dotSeriesSpec: dot, + fromField: 'from', + toField: 'to', + leftAppendPadding: 0, + link: { style: { stroke: '#5470c6', lineWidth: 2 } } + }, + dot + ], + axes: [ + { type: 'linear', orient: 'bottom', min: 0, max: 60, nice: false }, + { type: 'linear', orient: 'left', seriesIndex: [0], min: 0, max: 6, nice: false }, + { type: 'band', orient: 'right', seriesIndex: [1, 2], domain: ['123456xxxx', '789012xxxx'] } + ], + dataZoom: [{ orient: 'top', xAxisIndex: 0, filterMode: 'filter', start: 0, end: 1 }] + }; + }, + async exercise(page) { + // 通过真实组件 API 改变选区,未执行缩放时最终断言应失败。 + await page.evaluate(() => { + const chart = window.__visualChart.getChart(); + const link = chart.getAllSeries().find(s => s.type === 'link'); + if (link.getViewData().latestData.length !== 4) throw Error('初始连线数量不正确'); + chart.getComponentsByKey('dataZoom')[0].setStartAndEnd(1 / 3, 2 / 3); + }); + }, + async verify(page) { + // 检查实际预览数据及过滤后的起点,而非只检查 spec 中的配置。 + await page.evaluate(() => { + const chart = window.__visualChart.getChart(); + const series = chart.getAllSeries(); + const preview = chart.getComponentsByKey('dataZoom')[0].getData().getLatestData(); + if (preview.reduce((sum, d) => sum + d.y, 0) !== 15 || Math.max(...preview.map(d => d.y)) !== 5) + throw Error('预览错误地汇总了用户 ID 或丢失了柱状图数值'); + const dots = series.find(s => s.type === 'dot').getViewData().latestData; + if (dots.length !== 6 || dots.some(d => d.time < 20 || d.time > 40)) throw Error('事件未按选区过滤'); + const link = series.find(s => s.type === 'link'); + const starts = link + .getViewData() + .latestData.map(d => d.from_xField) + .sort((a, b) => a - b); + if (JSON.stringify(starts) !== '[20,30,40]') throw Error('连线未按起点时间过滤'); + const graphics = link.getMarkInName('link').getGraphics(); + if ( + !graphics.length || + graphics.some(g => !g.attribute.points.every(p => Number.isFinite(p.x) && Number.isFinite(p.y))) + ) + throw Error('连线图元坐标无效'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/index.mjs b/packages/vchart/__tests__/visual/cases/index.mjs index 010ca9614a..e581cec529 100644 --- a/packages/vchart/__tests__/visual/cases/index.mjs +++ b/packages/vchart/__tests__/visual/cases/index.mjs @@ -4,6 +4,11 @@ */ /** 本地用例元数据的唯一清单;模块必须保持浏览器和 Node 均可导入。 */ export const cases = [ + { + id: 'sequence-preview-filter', + purpose: '时序图缩略图不累加用户 ID,连线随 dataZoom 时间选区过滤', + file: './components/datazoom/sequence-preview-filter.mjs' + }, { id: 'sequence-link-data', purpose: 'dot 嵌套原始数据与更新后的连接端点', diff --git a/packages/vchart/src/component/data-zoom/data-filter-base-component.ts b/packages/vchart/src/component/data-zoom/data-filter-base-component.ts index 2a11585652..ee568f8197 100644 --- a/packages/vchart/src/component/data-zoom/data-filter-base-component.ts +++ b/packages/vchart/src/component/data-zoom/data-filter-base-component.ts @@ -1,4 +1,5 @@ import type { ICartesianSeries, IPolarSeries, ISeries } from '../../series/interface'; +import type { ILinkSeriesSpec } from '../../series/link/interface'; import { eachSeries } from '../../util/model'; import { BaseComponent } from '../base/base-component'; import type { IEffect, IModelInitOption } from '../../model/interface'; @@ -600,22 +601,19 @@ export abstract class DataFilterBaseComponent { - if (s.type === 'link') { - return; - } // 如果副轴的类型是time或band,则无法进行数据统计 const xAxisHelper = s.coordinate === 'cartesian' ? (s as ICartesianSeries).getXAxisHelper() : s.coordinate === 'polar' - ? (s as IPolarSeries).angleAxisHelper - : null; + ? (s as IPolarSeries).angleAxisHelper + : null; const yAxisHelper = s.coordinate === 'cartesian' ? (s as ICartesianSeries).getYAxisHelper() : s.coordinate === 'polar' - ? (s as IPolarSeries).radiusAxisHelper - : null; + ? (s as IPolarSeries).radiusAxisHelper + : null; if (!xAxisHelper || !yAxisHelper) { return; } @@ -623,10 +621,16 @@ export abstract class DataFilterBaseComponent