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/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..daa6df6ecf --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-medium.md @@ -0,0 +1,1149 @@ +--- +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 + +**Design meaning: shared media.** Identify whether different users' activities pass through the same specific device, IP address or live room. + +**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. +- **Controls:** select a range at the bottom, pan across media horizontally, and scroll vertically to see more users. + +## 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-step.md b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md new file mode 100644 index 0000000000..8b6c06e7e1 --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-step.md @@ -0,0 +1,1171 @@ +--- +category: examples +group: extension chart +title: 'Sequence Analysis: Step Mode' +keywords: extension, sequenceAnalysis, step +order: 14 +cover: /vchart/preview/sequence-analysis-step.png +option: sequenceAnalysisChart +--- + +# Sequence Analysis: Step Mode + +**Design meaning: risk behavior patterns.** Highlight or connect action nodes that match a business-defined risk pattern to compare behavioral segments across users. + +**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. +- **Controls:** select a range at the bottom, pan across steps horizontally, and scroll vertically to see more users. + +## 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 actionData = []; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + 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-target.md b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md new file mode 100644 index 0000000000..e6794f47df --- /dev/null +++ b/docs/assets/examples/en/extension-chart/sequence-analysis-target.md @@ -0,0 +1,1073 @@ +--- +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 + +**Design meaning: action targets.** Inspect whether multiple users act on the same object at similar times, such as sending messages to the same user. + +**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. +- **Controls:** select a time range at the bottom, pan the time window horizontally, and scroll vertically to see more users. + +## 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 d499c70d49..4d323dc45b 100644 --- a/docs/assets/examples/menu.json +++ b/docs/assets/examples/menu.json @@ -2117,6 +2117,27 @@ "zh": "翼形叙事图", "en": "Wing Storyline" } + }, + { + "path": "sequence-analysis-step", + "title": { + "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..55cb964191 --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-medium.md @@ -0,0 +1,1149 @@ +--- +category: examples +group: extension chart +title: '时序分析:介质曲线连接' +keywords: extension, sequenceAnalysis, medium +order: 15 +cover: /vchart/preview/sequence-analysis-medium.png +option: sequenceAnalysisChart +--- + +# 时序分析:介质曲线连接 + +**设计上表达的关系:共享介质关系。** 查看不同用户经过的具体介质是否重合,例如同一台设备、同一个 IP 或同一个直播间。 + +**假设场景**:用户 A 通过 IP₁、用户 B 通过 IP₂,都使用设备 D 登录。两人的介质路径在设备 D 重合,提供了账号之间的关联线索;共同点是同一台设备 D,而不仅是都属于 Device 类型。 + +**本例数据**:曲线连接 `user_001` 的注册记录 `event_001_1`(IP 类)与 `user_002` 的登录记录 `event_002_2`(设备类)。本例仅提供类型和连接端点,未提供具体介质 ID,因此这条线演示连接效果,不能据此确认两人共享哪个介质。 + +- **坐标轴**:每行是一位用户,横轴是 IP、设备、用户、直播间等介质类型,不表示时间。 +- **点与线**:点颜色区分行为,线颜色区分关联类别;同一用户、同类介质的事件可能重叠。相同端点的多条关联用不同曲率区分。 +- **操作**:底部选择范围、横向滚动查看介质,右侧滚动查看其他用户。 + +## 代码演示 + +```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-step.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md new file mode 100644 index 0000000000..42b8c614ee --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-step.md @@ -0,0 +1,1171 @@ +--- +category: examples +group: extension chart +title: '时序分析:步骤模式' +keywords: extension, sequenceAnalysis, step +order: 14 +cover: /vchart/preview/sequence-analysis-step.png +option: sequenceAnalysisChart +--- + +# 时序分析:步骤模式 + +**设计上表达的关系:风险行为模式。** 将命中某种业务风险特征的动作节点高亮或连接起来,方便比较不同用户的行为片段。 + +**假设场景**:某业务将“更换设备登录 → 修改绑定邮箱 → 批量发送消息”标记为待复核的行为模式。图中把命中的动作连接起来,分析人员就能找到哪些用户出现了这组行为,以及它们处于哪些步骤。这里是帮助理解的假设规则。 + +**本例数据**:`user_004` 第 4 步的关注记录 `event_004_4`(计数 7)与第 6 步的私信记录 `event_004_6` 被连线标记。本例只演示模式标注,未定义 `pattern_001` 的实际风险规则。 + +- **坐标轴**:每行是一位用户,横轴是步骤;顶部显示该步骤配置的介质类型(如直播间、设备)及附加计数 `+N`。 +- **点与线**:点代表事件记录,颜色区分行为类型;线标注关联片段,颜色区分关联类别。计数 7 仍对应一个点。 +- **操作**:底部选择范围、横向滚动查看步骤,右侧滚动查看其他用户。 + +## 代码演示 + +```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 actionData = []; + +const spec = { + type: 'sequenceAnalysis', + mode: 'step', + mediumConnection: false, + 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-target.md b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md new file mode 100644 index 0000000000..c6bc1057ed --- /dev/null +++ b/docs/assets/examples/zh/extension-chart/sequence-analysis-target.md @@ -0,0 +1,1073 @@ +--- +category: examples +group: extension chart +title: '时序分析:时间模式 + 目标头像' +keywords: extension, sequenceAnalysis, target +order: 16 +cover: /vchart/preview/sequence-analysis-target.png +option: sequenceAnalysisChart +--- + +# 时序分析:时间模式 + 目标头像 + +**设计上表达的关系:行为作用对象。** 查看多个用户是否在相近时间对同一个对象采取行动,例如向同一用户发送消息。 + +**假设场景**:用户 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 发消息场景的完整数据。 + +- **坐标轴**:每行是一位用户,横轴是发生时间;顶部数字是该时刻所有用户的行为计数之和。 +- **点与线**:点颜色区分行为,箭头表示连接方向;中间头像由连接数据单独指定,可以与终点用户不同。 +- **操作**:底部选择时间范围,横向滚动平移时间窗口,右侧滚动查看其他用户。 + +## 代码演示 + +```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 new file mode 100644 index 0000000000..222561f0d9 --- /dev/null +++ b/docs/assets/guide/en/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -0,0 +1,157 @@ +# 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. + +![Sequence Analysis](/vchart/preview/sequence-analysis-step.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 page uses the avatar URLs from the local demo. + +```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. [Time with target avatars 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 + +- [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/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..aefea58b9d --- /dev/null +++ b/docs/assets/guide/zh/tutorial_docs/Chart_Extensions/sequence_analysis.md @@ -0,0 +1,157 @@ +# 扩展图表:时序分析图 + +时序分析图(Sequence Analysis)把每个用户的事件放在同一条水平线上,通过时间、步骤或介质分类对齐事件,再用连接线表达事件之间的关系。适合展示行为路径、跨用户事件关联和重复模式。 + +它由 `@visactor/vchart-extension` 提供,类型为 `sequenceAnalysis`,与内置 `sequence` 和用于训练过程分析的 `sequenceScatterLink` 是不同图表。 + +![时序分析图](/vchart/preview/sequence-analysis-step.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` 可以为空;演示页沿用本地 demo 的用户头像地址。 + +```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-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 new file mode 100644 index 0000000000..9e0d6d7462 --- /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) · [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`. + +## 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..506eefedc6 --- /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-step) · [介质曲线连接](/vchart/demo/extension-chart/sequence-analysis-medium) · [时间模式 + 目标头像](/vchart/demo/extension-chart/sequence-analysis-target) + +本图表使用以下专用数据字段,转换器会生成底层 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 0000000000..c5833a24f5 Binary files /dev/null and b/docs/public/vchart/preview/sequence-analysis-medium.png differ 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 0000000000..1d885c34cf Binary files /dev/null and b/docs/public/vchart/preview/sequence-analysis-step.png differ diff --git a/docs/public/vchart/preview/sequence-analysis-target.png b/docs/public/vchart/preview/sequence-analysis-target.png new file mode 100644 index 0000000000..75075a56ca Binary files /dev/null and b/docs/public/vchart/preview/sequence-analysis-target.png differ 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..d320c507dc --- /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: ISequenceAnalysisSpec['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 + } +}; + +// 模拟绘制 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].medium_type}... `, + fontSize: 12 + }, + { + text: `+${stepMediumMap[label].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].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..4126b9d3e2 --- /dev/null +++ b/packages/vchart-extension/src/charts/sequence-analysis/interface.ts @@ -0,0 +1,107 @@ +// 绘制点 +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..74d3d275cd --- /dev/null +++ b/packages/vchart-extension/src/charts/sequence-analysis/sequence-analysis-transformer.ts @@ -0,0 +1,392 @@ +import { CommonChartSpecTransformer } from '@visactor/vchart'; +import type { EventData, ISequenceAnalysisSpec } 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 } = spec; + const stepMediumMap: ISequenceAnalysisSpec['stepMediumMap'] = spec.stepMediumMap; + 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' + } + } + }, + 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].medium_type}... `, + fontSize: 12 + }, + { + text: `+${stepMediumMap[label].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: string) => { + 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 d8f80b5d97..bd3d92f18a 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/__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__/unit/component/data-zoom/sequence-analysis.test.ts b/packages/vchart/__tests__/unit/component/data-zoom/sequence-analysis.test.ts new file mode 100644 index 0000000000..cb569e3dfc --- /dev/null +++ b/packages/vchart/__tests__/unit/component/data-zoom/sequence-analysis.test.ts @@ -0,0 +1,154 @@ +import { DataSet, DataView } from '@visactor/vdataset'; +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 and link filter fields without summing numeric-looking user IDs', () => { + const dataSet = new DataSet(); + 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(['123456xxxx']).range([0, 100]); + const series = { + id: 1, + type: 'dot', + coordinate: 'cartesian', + getRawData: () => data, + getRawDataStatisticsByField: (): undefined => undefined, + getSpec: () => ({ xField: 'step', yField: 'user' }), + getXAxisHelper: () => ({ getAxisId: () => 10, getScale: () => xScale }), + getYAxisHelper: () => ({ getAxisId: () => 11, getScale: () => yScale }) + }; + const zoom: any = new DataZoom({}, { dataSet } as any); + zoom._relatedAxisComponent = { id: 10 }; + 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: 0 }, + { state: 'B', value: 0 } + ]); + + data.parse([{ user: '123456xxxx', dots: [{ step: 'B' }, { step: 'C' }] }]); + zoom.onDataUpdate(); + expect(zoom._data.getLatestData()).toEqual([ + { 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(); + const axis = (axisScale: IBandLikeScale) => ({ + getScale: () => axisScale, + getInverse: () => false, + scaleRangeFactorStart: (value: number) => axisScale.rangeFactorStart(value), + scaleRangeFactorEnd: (value: number) => axisScale.rangeFactorEnd(value) + }); + const zoom: any = new DataZoom({}, {} as any); + zoom._relatedAxisComponent = axis(scale); + zoom._linkedAxisComponent = axis(linkedScale); + zoom._isHorizontal = true; + zoom._start = 0.2; + zoom._end = 0.6; + zoom._updateRangeFactor(); + const expected = reverse ? [0.4, 0.8] : [0.2, 0.6]; + expect(scale.rangeFactor()).toEqual(expected); + expect(linkedScale.rangeFactor()).toEqual(expected); + }); +}); 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 new file mode 100644 index 0000000000..35e1fdca82 --- /dev/null +++ b/packages/vchart/__tests__/unit/series/sequence-analysis-review.test.ts @@ -0,0 +1,125 @@ +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'; + +// eslint-disable-next-line max-len +import type { ISequenceAnalysisSpec } from '../../../../vchart-extension/src/charts/sequence-analysis/interface'; + +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); + } + }); + + 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/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/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/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 5c76200eb4..e581cec529 100644 --- a/packages/vchart/__tests__/visual/cases/index.mjs +++ b/packages/vchart/__tests__/visual/cases/index.mjs @@ -4,6 +4,21 @@ */ /** 本地用例元数据的唯一清单;模块必须保持浏览器和 Node 均可导入。 */ export const cases = [ + { + id: 'sequence-preview-filter', + purpose: '时序图缩略图不累加用户 ID,连线随 dataZoom 时间选区过滤', + file: './components/datazoom/sequence-preview-filter.mjs' + }, + { + id: 'sequence-link-data', + 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)', @@ -374,6 +389,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/data-filter-base-component.ts b/packages/vchart/src/component/data-zoom/data-filter-base-component.ts index 9d0d02c90c..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'; @@ -102,6 +103,7 @@ 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: axis.scaleRangeFactorStart(newRangeFactor[0], true); + linkedAxis?.scaleRangeFactorStart(newRangeFactor[0], true); axis.scaleRangeFactorEnd(newRangeFactor[1]); // end 保证为准确值 + linkedAxis?.scaleRangeFactorEnd(newRangeFactor[1]); } } 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: axis.scaleRangeFactorEnd(newRangeFactor[1], true); + linkedAxis?.scaleRangeFactorEnd(newRangeFactor[1], true); axis.scaleRangeFactorStart(newRangeFactor[0]); // start 保证为准确值 + linkedAxis?.scaleRangeFactorStart(newRangeFactor[0]); } } @@ -508,6 +519,12 @@ export abstract class DataFilterBaseComponent, 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; @@ -137,7 +139,8 @@ export const dataFilterComputeDomain = (data: Array, op: IDataFilterCompute } }); } - dv.latestData.forEach((d: any) => { + const seriesData = series?.type === 'dot' ? objFlat(dv.latestData, 'dots') : dv.latestData; + seriesData.forEach((d: any) => { // 针对rangeColumn等xField为数组形式的图表,需要将xField的所有value都记录下来 array(stateFields[i]).forEach(state => { if (!isNil(d[state])) { 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/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 b09e0ea734..0cc6ceb61d 100644 --- a/packages/vchart/src/series/dot/dot.ts +++ b/packages/vchart/src/series/dot/dot.ts @@ -143,12 +143,31 @@ export class DotSeries extends Cartes ); } + 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; } /** @@ -235,7 +254,7 @@ export class DotSeries extends Cartes this.setMarkStyle( containerMark, { - x: this._spec.leftAppendPadding + x: this._spec.leftAppendPadding ?? 0 }, 'normal', AttributeLevel.Series @@ -416,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 + : []; } /** @@ -439,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/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..749d9bee3d 100644 --- a/packages/vchart/src/series/link/constant.ts +++ b/packages/vchart/src/series/link/constant.ts @@ -6,6 +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.arrow]: { name: SeriesMarkNameEnum.arrow, type: MarkTypeEnum.symbol } + [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/interface.ts b/packages/vchart/src/series/link/interface.ts index 839d9ac62b..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'; @@ -35,10 +35,30 @@ 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; /** * 图元配置 */ [SeriesMarkNameEnum.link]?: IMarkSpec; + /** + * image标签配置 + */ + [SeriesMarkNameEnum.imageLabel]?: IMarkSpec; } type ILinkSeriesSpecFromDot = { @@ -65,4 +85,10 @@ type ILinkSeriesSpecFromDot = { export interface ILinkSeriesTheme extends ICartesianSeriesTheme { [SeriesMarkNameEnum.link]?: Partial>; [SeriesMarkNameEnum.arrow]?: Partial>; + [SeriesMarkNameEnum.imageLabel]?: 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..f50159587b 100644 --- a/packages/vchart/src/series/link/link.ts +++ b/packages/vchart/src/series/link/link.ts @@ -1,17 +1,20 @@ /* 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 { isValid } from '@visactor/vutils'; -import type { IGroupMark, IMark, IRuleMark, ISymbolMark } from '../../mark/interface'; +import { array, isValid } from '@visactor/vutils'; +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'; 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'; +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'; @@ -84,6 +87,7 @@ export class LinkSeries extends Car type: 'linkDotInfo', options: { infoKey: 'dots', + isCurve: () => this._spec.linkType === LinkType.curve, fields: () => { return { fromField: this._fromField, @@ -114,25 +118,35 @@ 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 { + 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, { - 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, 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 +155,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 +182,58 @@ 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.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 curveRatio = 0.5 + datum[DEFAULT_LINK_CURVE_INDEX] * 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: 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 +250,30 @@ export class LinkSeries extends Car AttributeLevel.Series ); } + + const imageLabelMark = this._imageLabelMark; + if (imageLabelMark) { + const { style = {} } = (this._spec as ILinkSeriesSpec).imageLabel; + const { width = 40, height = 40 } = style; + this.setMarkStyle( + imageLabelMark, + { + x: datum => { + return (this.dataToPositionXFrom(datum) + this.dataToPositionXTo(datum)) / 2 - (width as number) / 2; + }, + y: datum => { + return (this.dataToPositionYFrom(datum) + this.dataToPositionYTo(datum)) / 2 - (height as number) / 2; + }, + width, + height, + image: datum => { + return datum[this._spec.imageLabelField]; + } + }, + 'normal', + AttributeLevel.Series + ); + } } afterInitMark(): void { @@ -217,9 +288,20 @@ 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') + ? -(array(bandSize)[0] + (dotSize as number)) / 2 + : 0) + ); } dataToPositionYFrom(datum: Datum): number { @@ -236,9 +318,19 @@ 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') + ? (array(bandSize)[0] + (dotSize as number)) / 2 + : 0) + ); } dataToPositionYTo(datum: Datum): number { @@ -267,9 +359,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; } @@ -365,6 +458,8 @@ export class LinkSeries extends Car export const registerLinkSeries = () => { registerRuleMark(); registerSymbolMark(); + registerPathMark(); + registerImageMark(); registerCartesianBandAxis(); registerCartesianLinearAxis(); Factory.registerSeries(LinkSeries.type, LinkSeries);