From bc4ee6c952ca25ab48a6b72545300d368535ed33 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 11 Jul 2022 17:29:40 +0800 Subject: [PATCH 1/7] fix(custom): fix elements may not be removed after updates #17333 --- src/chart/custom/CustomView.ts | 23 +-- test/custom-update.html | 181 ++++++++++++++++++++++++ test/runTest/actions/__meta__.json | 1 + test/runTest/actions/custom-update.json | 1 + 4 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 test/custom-update.html create mode 100644 test/runTest/actions/custom-update.json diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 1568078267..b415e391ad 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -1316,14 +1316,21 @@ function mergeChildren( // might be better performance. let index = 0; for (; index < newLen; index++) { - newChildren[index] && doCreateOrUpdateEl( - api, - el.childAt(index), - dataIndex, - newChildren[index] as CustomElementOption, - seriesModel, - el - ); + const newChild = newChildren[index]; + if (newChild) { + doCreateOrUpdateEl( + api, + el.childAt(index), + dataIndex, + newChild as CustomElementOption, + seriesModel, + el + ); + } + else { + // The element is being null after updating, remove the old element + el.remove(el.childAt(index)); + } } for (let i = el.childCount() - 1; i >= index; i--) { // Do not support leave elements that are not mentioned in the latest diff --git a/test/custom-update.html b/test/custom-update.html new file mode 100644 index 0000000000..ca91a3cc9c --- /dev/null +++ b/test/custom-update.html @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index baadde9800..69339014dc 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -63,6 +63,7 @@ "custom-large": 1, "custom-shape-morphing": 1, "custom-text-content": 6, + "custom-update": 1, "dataSelect": 7, "dataset-case": 6, "dataZoom-action": 4, diff --git a/test/runTest/actions/custom-update.json b/test/runTest/actions/custom-update.json new file mode 100644 index 0000000000..bfc1d94cb4 --- /dev/null +++ b/test/runTest/actions/custom-update.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":420,"x":14,"y":161},{"type":"mousemove","time":620,"x":27,"y":69},{"type":"mousemove","time":825,"x":28,"y":53},{"type":"mousemove","time":1036,"x":37,"y":74},{"type":"mousedown","time":1162,"x":37,"y":76},{"type":"mousemove","time":1237,"x":37,"y":76},{"type":"mouseup","time":1311,"x":37,"y":76},{"time":1312,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1657531346385}] \ No newline at end of file From 22c82182347f3a450c50ed2b1e1c7679793cb299 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Tue, 12 Jul 2022 16:44:27 +0800 Subject: [PATCH 2/7] fix(custom): apply leave transition and add more comments --- src/chart/custom/CustomView.ts | 40 ++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index b415e391ad..96a54d4734 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -99,6 +99,7 @@ import { applyKeyframeAnimation, stopPreviousKeyframeAnimationAndRestore } from '../../animation/customGraphicKeyframeAnimation'; +import { SeriesModel } from '../../echarts.all'; const EMPHASIS = 'emphasis' as const; const NORMAL = 'normal' as const; @@ -1264,16 +1265,21 @@ function retrieveStyleOptionOnState( // Usage: -// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that -// the existing children will not be removed, and enables the feature that -// update some of the props of some of the children simply by construct +// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates +// that the existing children will not be removed, and enables the feature +// that update some of the props of some of the children simply by construct // the returned children of `renderItem` like: // `var children = group.children = []; children[3] = {opacity: 0.5};` // (2) If `elOption.$mergeChildren` is `'byName'`, add/update/remove children // by child.name. But that might be lower performance. // (3) If `elOption.$mergeChildren` is `false`, the existing children will be // replaced totally. -// (4) If `!elOption.children`, following the "merge" principle, nothing will happen. +// (4) If `!elOption.children`, following the "merge" principle, nothing will +// happen. +// (5) If `elOption.$mergeChildren` is not `false` neither `'byName'` and the +// `el` is a group, and if any of the new child is null, it means to remove +// the element at the same index, if exists. On the other hand, if the new +// child is and empty object `{}`, it means to keep the element not changed. // // For implementation simpleness, do not provide a direct way to remove sinlge // child (otherwise the total indicies of the children array have to be modified). @@ -1317,10 +1323,11 @@ function mergeChildren( let index = 0; for (; index < newLen; index++) { const newChild = newChildren[index]; + const oldChild = el.childAt(index); if (newChild) { doCreateOrUpdateEl( api, - el.childAt(index), + oldChild, dataIndex, newChild as CustomElementOption, seriesModel, @@ -1328,19 +1335,30 @@ function mergeChildren( ); } else { - // The element is being null after updating, remove the old element - el.remove(el.childAt(index)); + removeChildFromGroup(el, oldChild, seriesModel); } } for (let i = el.childCount() - 1; i >= index; i--) { - // Do not support leave elements that are not mentioned in the latest - // `renderItem` return. Otherwise users may not have a clear and simple - // concept that how to control all of the elements. const child = el.childAt(i); - child && applyLeaveTransition(child, customInnerStore(el).option, seriesModel); + removeChildFromGroup(el, child, seriesModel); } } +function removeChildFromGroup( + group: graphicUtil.Group, + child: Element, + seriesModel: SeriesModel +) { + // Do not support leave elements that are not mentioned in the latest + // `renderItem` return. Otherwise users may not have a clear and simple + // concept that how to control all of the elements. + child && applyLeaveTransition( + child, + customInnerStore(group).option, + seriesModel + ); +} + type DiffGroupContext = { api: ExtensionAPI; oldChildren: Element[]; From ee3b8357c812cb48b201633448a098255cf5b670 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Tue, 12 Jul 2022 16:45:15 +0800 Subject: [PATCH 3/7] test(custom): add a test case with {} that should preserve child --- test/runTest/actions/__meta__.json | 2 +- test/runTest/actions/custom-update.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index 69339014dc..de4ac84d96 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -63,7 +63,7 @@ "custom-large": 1, "custom-shape-morphing": 1, "custom-text-content": 6, - "custom-update": 1, + "custom-update": 2, "dataSelect": 7, "dataset-case": 6, "dataZoom-action": 4, diff --git a/test/runTest/actions/custom-update.json b/test/runTest/actions/custom-update.json index bfc1d94cb4..bb04426472 100644 --- a/test/runTest/actions/custom-update.json +++ b/test/runTest/actions/custom-update.json @@ -1 +1 @@ -[{"name":"Action 1","ops":[{"type":"mousemove","time":420,"x":14,"y":161},{"type":"mousemove","time":620,"x":27,"y":69},{"type":"mousemove","time":825,"x":28,"y":53},{"type":"mousemove","time":1036,"x":37,"y":74},{"type":"mousedown","time":1162,"x":37,"y":76},{"type":"mousemove","time":1237,"x":37,"y":76},{"type":"mouseup","time":1311,"x":37,"y":76},{"time":1312,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1657531346385}] \ No newline at end of file +[{"name":"Action 1","ops":[{"type":"mousemove","time":420,"x":14,"y":161},{"type":"mousemove","time":620,"x":27,"y":69},{"type":"mousemove","time":825,"x":28,"y":53},{"type":"mousemove","time":1036,"x":37,"y":74},{"type":"mousedown","time":1162,"x":37,"y":76},{"type":"mousemove","time":1237,"x":37,"y":76},{"type":"mouseup","time":1311,"x":37,"y":76},{"time":1312,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1657531346385},{"name":"Action 2","ops":[{"type":"mousemove","time":743,"x":610,"y":251},{"type":"mousemove","time":943,"x":337,"y":165},{"type":"mousemove","time":1143,"x":109,"y":156},{"type":"mousemove","time":1343,"x":53,"y":176},{"type":"mousemove","time":1543,"x":47,"y":178},{"type":"mousedown","time":1548,"x":47,"y":178},{"type":"mouseup","time":1680,"x":47,"y":178},{"time":1681,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2109,"x":48,"y":178},{"type":"mousemove","time":2309,"x":113,"y":178},{"type":"mousemove","time":2515,"x":114,"y":178}],"scrollY":434,"scrollX":0,"timestamp":1657615363601}] \ No newline at end of file From fc43e0ac14d7d4eddcc960826f5d463912ee801a Mon Sep 17 00:00:00 2001 From: Ovilia Date: Wed, 13 Jul 2022 17:06:47 +0800 Subject: [PATCH 4/7] test(custom): add a test case --- test/custom-update.html | 242 +++++++++++++++++++++------------------- 1 file changed, 125 insertions(+), 117 deletions(-) diff --git a/test/custom-update.html b/test/custom-update.html index ca91a3cc9c..65f8cbc5aa 100644 --- a/test/custom-update.html +++ b/test/custom-update.html @@ -38,6 +38,7 @@
+
@@ -50,129 +51,136 @@ // 'map/js/china', // './data/nutrients.json' ], function (echarts) { - var option; - var cellSize = [50, 51]; - var isFirst = true; - - option = { - calendar: [ - { - orient: 'vertical', - cellSize: cellSize, - yearLabel: { - show: false - }, - monthLabel: { - show: false - }, - range: ['2022-1'] - } - ], - series: [ - { - type: 'custom', - universalTransition: { - enabled: false - }, - name: 'a', - coordinateSystem: 'calendar', - animation: 0, - data: [ - ['2022-01-16', 'e', false] + function createCase(isTestingNull) { + var option; + var cellSize = [50, 51]; + var isFirst = true; + + option = { + calendar: [ + { + orient: 'vertical', + cellSize: cellSize, + yearLabel: { + show: false + }, + monthLabel: { + show: false + }, + range: ['2022-1'] + } ], - renderItem: (params, api) => { - const date = api.value(0); - const cellPoint = api.coord(date); - const xPos = cellPoint[0] - cellSize[0] / 2; - const yPos = cellPoint[1] - cellSize[1] / 2; - const name = api.value(1); - const cellWidth = params.coordSys.cellWidth; - const position = [xPos, yPos]; - const rect = { - type: 'rect', - shape: { - x: 0, - y: 0, - width: cellWidth, - height: 15 - }, - position, - style: { - fill: isFirst ? '#eee' : '#6ee' - }, - textContent: { - style: { - text: name, - fill: '#888', - overflow: 'truncate', - width: cellWidth, - height: 13, - y: 1 - } - }, - textConfig: { - position: 'insideLeft', - distance: 2 - } - }; - - const borderLeft = api.value(2) - ? null - : { - type: 'rect', - shape: { - x: -20, - y: 0, - width: 20, - height: 15 - }, - position, - style: { - fill: 'red' - } - }; - - const group = { - type: 'group', - children: [rect, borderLeft] - }; - return group; - }, - silent: true, - z: 2, - legendHoverLink: true, - clip: false, - label: {}, - emphasis: { label: {} } - } - ] - }; - - var chart = testHelper.create(echarts, 'main0', { - title: [ - 'The red bar should not be rendered after "Update"' - ], - option: option, - // height: 300, - buttons: [{text: 'Update', onclick: function () { - isFirst = false; - chart.setOption({ - calendar: { - range: ['2022-02'] - }, - series: [ + series: [ { type: 'custom', + universalTransition: { + enabled: false + }, name: 'a', + coordinateSystem: 'calendar', + animation: 0, data: [ - ['2022-02-13', 'e', true] - ] + ['2022-01-16', 'e', false] + ], + renderItem: (params, api) => { + const date = api.value(0); + const cellPoint = api.coord(date); + const xPos = cellPoint[0] - cellSize[0] / 2; + const yPos = cellPoint[1] - cellSize[1] / 2; + const name = api.value(1); + const cellWidth = params.coordSys.cellWidth; + const position = [xPos, yPos]; + const rect = { + type: 'rect', + shape: { + x: 0, + y: 0, + width: cellWidth, + height: 15 + }, + position, + style: { + fill: isFirst ? '#eee' : '#6ee' + }, + textContent: { + style: { + text: name, + fill: '#888', + overflow: 'truncate', + width: cellWidth, + height: 13, + y: 1 + } + }, + textConfig: { + position: 'insideLeft', + distance: 2 + }, + name: 'rect' + }; + + const borderLeft = api.value(2) + ? (isTestingNull ? null : {}) + : { + type: 'rect', + shape: { + x: -20, + y: 0, + width: 20, + height: 15 + }, + position, + style: { + fill: 'red' + }, + name: 'bar' + }; + + const group = { + type: 'group', + children: [rect, borderLeft] + }; + return group; + }, + silent: true, + z: 2, + legendHoverLink: true, + clip: false, + label: {}, + emphasis: { label: {} } } - ] - }); - }}], - // recordCanvas: true, - }); + ] + }; + + var rendered = isTestingNull ? ' NOT' : ''; + var chart = testHelper.create(echarts, 'main' + (isTestingNull ? '1' : '0'), { + title: [ + 'Update group with ' + (isTestingNull ? 'null' : '{}') + ' as new child', + 'The red bar should be' + rendered + ' rendered after "Update"' + ], + option: option, + // height: 300, + buttons: [{text: 'Update', onclick: function () { + isFirst = false; + chart.setOption({ + calendar: { + range: ['2022-02'] + }, + series: [ + { + type: 'custom', + data: [ + ['2022-02-13', 'e', true] + ] + } + ] + }); + }}] + }); + } + + createCase(false); + createCase(true); }); From 44e650e3b3647706d1749d84d24c21a96a410a49 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Thu, 14 Jul 2022 15:17:13 +0800 Subject: [PATCH 5/7] fix(custom): fix the case for element after the null child --- src/chart/custom/CustomView.ts | 11 +++++++- test/custom-update.html | 47 +++++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 96a54d4734..60de188fd1 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -1321,9 +1321,14 @@ function mergeChildren( // Mapping children of a group simply by index, which // might be better performance. let index = 0; + let oldRemovedCount = 0; for (; index < newLen; index++) { const newChild = newChildren[index]; - const oldChild = el.childAt(index); + // The index of oldChild may change if previous child not exists + // in newChildren and have no leave animation. By subtracting + // oldRemovedCount, we make sure to compare the children before + // removing from the parent. + const oldChild = el.childAt(index - oldRemovedCount); if (newChild) { doCreateOrUpdateEl( api, @@ -1335,7 +1340,11 @@ function mergeChildren( ); } else { + const childCount = el.childCount(); removeChildFromGroup(el, oldChild, seriesModel); + // If there is not leave animation, the child is removed from + // the parent directly, so we should update the index. + oldRemovedCount += childCount - el.childCount(); } } for (let i = el.childCount() - 1; i >= index; i--) { diff --git a/test/custom-update.html b/test/custom-update.html index 65f8cbc5aa..680020ee04 100644 --- a/test/custom-update.html +++ b/test/custom-update.html @@ -39,6 +39,8 @@
+
+
@@ -51,7 +53,7 @@ // 'map/js/china', // './data/nutrients.json' ], function (echarts) { - function createCase(isTestingNull) { + function createCase(isTestingNull, hasAnimation) { var option; var cellSize = [50, 51]; var isFirst = true; @@ -78,7 +80,7 @@ }, name: 'a', coordinateSystem: 'calendar', - animation: 0, + animation: hasAnimation ? 1000 : 0, data: [ ['2022-01-16', 'e', false] ], @@ -136,9 +138,36 @@ name: 'bar' }; + const right = api.value(2) + ? { + type: 'rect', + shape: { + x: cellWidth, + y: 0, + width: 20, + height: 15 + }, + position, + style: { + fill: 'blue' + } + } + : { + type: 'circle', + shape: { + cx: cellWidth + 20, + cy: 20, + r: 15 + }, + position, + style: { + fill: 'red' + } + }; + const group = { type: 'group', - children: [rect, borderLeft] + children: [rect, borderLeft, right] }; return group; }, @@ -153,10 +182,12 @@ }; var rendered = isTestingNull ? ' NOT' : ''; - var chart = testHelper.create(echarts, 'main' + (isTestingNull ? '1' : '0'), { + var chartId = (isTestingNull ? 2 : 0) + (hasAnimation ? 1 : 0); + var chart = testHelper.create(echarts, 'main' + chartId, { title: [ 'Update group with ' + (isTestingNull ? 'null' : '{}') + ' as new child', - 'The red bar should be' + rendered + ' rendered after "Update"' + 'Animation: ' + (hasAnimation ? 'true' : 'false'), + 'The red shapes should be' + rendered + ' rendered after "Update"' ], option: option, // height: 300, @@ -179,8 +210,10 @@ }); } - createCase(false); - createCase(true); + createCase(false, false); + createCase(true, false); + createCase(false, true); + createCase(true, true); }); From 8fc48c727663b0514352ac8b3b70e4f1eeed3988 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 25 Jul 2022 15:58:52 +0800 Subject: [PATCH 6/7] fix(custom): ignore element when renderItem returns a group with null elements --- src/chart/custom/CustomView.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index 60de188fd1..215087b72b 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -1321,15 +1321,15 @@ function mergeChildren( // Mapping children of a group simply by index, which // might be better performance. let index = 0; - let oldRemovedCount = 0; for (; index < newLen; index++) { const newChild = newChildren[index]; - // The index of oldChild may change if previous child not exists - // in newChildren and have no leave animation. By subtracting - // oldRemovedCount, we make sure to compare the children before - // removing from the parent. - const oldChild = el.childAt(index - oldRemovedCount); + const oldChild = el.childAt(index); if (newChild) { + if (newChild.ignore == null) { + // The old child is set to be ignored if null (see comments + // below). So we need to set ignore to be false back. + newChild.ignore = false; + } doCreateOrUpdateEl( api, oldChild, @@ -1340,11 +1340,11 @@ function mergeChildren( ); } else { - const childCount = el.childCount(); - removeChildFromGroup(el, oldChild, seriesModel); - // If there is not leave animation, the child is removed from - // the parent directly, so we should update the index. - oldRemovedCount += childCount - el.childCount(); + // If the new element option is null, it means to remove the old + // element. But we cannot really remove the element from the group + // directly, because the element order may not be stable when this + // element is added back. So we set the element to be ignored. + oldChild.ignore = true; } } for (let i = el.childCount() - 1; i >= index; i--) { From 09e0c801a5d2e01b6301cec3a636015eec8bd7d4 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 25 Jul 2022 16:07:30 +0800 Subject: [PATCH 7/7] test(custom): update test case for custom update --- test/custom-update.html | 29 +++++++++++++++---------- test/runTest/actions/__meta__.json | 2 +- test/runTest/actions/custom-update.json | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/test/custom-update.html b/test/custom-update.html index 680020ee04..2015c4bbb6 100644 --- a/test/custom-update.html +++ b/test/custom-update.html @@ -57,6 +57,7 @@ var option; var cellSize = [50, 51]; var isFirst = true; + var clickTimes = 0; option = { calendar: [ @@ -80,7 +81,7 @@ }, name: 'a', coordinateSystem: 'calendar', - animation: hasAnimation ? 1000 : 0, + animation: hasAnimation ? 3000 : 0, data: [ ['2022-01-16', 'e', false] ], @@ -102,7 +103,7 @@ }, position, style: { - fill: isFirst ? '#eee' : '#6ee' + fill: api.value(2) ? '#eee' : '#6ee' }, textContent: { style: { @@ -140,16 +141,16 @@ const right = api.value(2) ? { - type: 'rect', + type: 'circle', shape: { - x: cellWidth, - y: 0, - width: 20, - height: 15 + cx: cellWidth + 20, + cy: 20, + r: 30 }, position, style: { - fill: 'blue' + fill: 'transparent', + stroke: 'blue', } } : { @@ -193,19 +194,25 @@ // height: 300, buttons: [{text: 'Update', onclick: function () { isFirst = false; + var testId = clickTimes % 2; chart.setOption({ calendar: { - range: ['2022-02'] - }, + range: testId === 0 ? ['2022-02'] : ['2022-01'] + }, series: [ { type: 'custom', data: [ - ['2022-02-13', 'e', true] + [ + testId === 0 ? '2022-02-13' : '2022-01-16' , + 'e', + testId === 0 + ] ] } ] }); + ++clickTimes; }}] }); } diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index de4ac84d96..7e5334a6b6 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -63,7 +63,7 @@ "custom-large": 1, "custom-shape-morphing": 1, "custom-text-content": 6, - "custom-update": 2, + "custom-update": 4, "dataSelect": 7, "dataset-case": 6, "dataZoom-action": 4, diff --git a/test/runTest/actions/custom-update.json b/test/runTest/actions/custom-update.json index bb04426472..dbacd6ff19 100644 --- a/test/runTest/actions/custom-update.json +++ b/test/runTest/actions/custom-update.json @@ -1 +1 @@ -[{"name":"Action 1","ops":[{"type":"mousemove","time":420,"x":14,"y":161},{"type":"mousemove","time":620,"x":27,"y":69},{"type":"mousemove","time":825,"x":28,"y":53},{"type":"mousemove","time":1036,"x":37,"y":74},{"type":"mousedown","time":1162,"x":37,"y":76},{"type":"mousemove","time":1237,"x":37,"y":76},{"type":"mouseup","time":1311,"x":37,"y":76},{"time":1312,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1657531346385},{"name":"Action 2","ops":[{"type":"mousemove","time":743,"x":610,"y":251},{"type":"mousemove","time":943,"x":337,"y":165},{"type":"mousemove","time":1143,"x":109,"y":156},{"type":"mousemove","time":1343,"x":53,"y":176},{"type":"mousemove","time":1543,"x":47,"y":178},{"type":"mousedown","time":1548,"x":47,"y":178},{"type":"mouseup","time":1680,"x":47,"y":178},{"time":1681,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2109,"x":48,"y":178},{"type":"mousemove","time":2309,"x":113,"y":178},{"type":"mousemove","time":2515,"x":114,"y":178}],"scrollY":434,"scrollX":0,"timestamp":1657615363601}] \ No newline at end of file +[{"name":"Action 1","ops":[{"type":"mousemove","time":678,"x":793,"y":218},{"type":"mousemove","time":878,"x":436,"y":281},{"type":"mousemove","time":1079,"x":285,"y":329},{"type":"mousemove","time":1285,"x":283,"y":329},{"type":"mousemove","time":1362,"x":282,"y":329},{"type":"mousemove","time":1562,"x":239,"y":316},{"type":"mousemove","time":1765,"x":176,"y":325},{"type":"mousemove","time":1979,"x":158,"y":325},{"type":"mousemove","time":2180,"x":72,"y":178},{"type":"mousemove","time":2386,"x":43,"y":114},{"type":"mousemove","time":2580,"x":43,"y":114},{"type":"mousedown","time":2591,"x":43,"y":114},{"type":"mouseup","time":2723,"x":43,"y":114},{"time":2724,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":4303,"x":43,"y":114},{"type":"mouseup","time":4435,"x":43,"y":114},{"time":4436,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":6044,"x":43,"y":114},{"type":"mouseup","time":6177,"x":43,"y":114},{"time":6178,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1658736074386},{"name":"Action 2","ops":[{"type":"mousemove","time":477,"x":484,"y":279},{"type":"mousemove","time":680,"x":226,"y":164},{"type":"mousemove","time":893,"x":119,"y":145},{"type":"mousemove","time":1094,"x":59,"y":132},{"type":"mousemove","time":1301,"x":51,"y":129},{"type":"mousedown","time":1423,"x":51,"y":129},{"type":"mouseup","time":1547,"x":51,"y":129},{"time":1548,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":2550,"x":51,"y":129},{"type":"mouseup","time":2684,"x":51,"y":129},{"time":2685,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":3923,"x":51,"y":129},{"type":"mouseup","time":4056,"x":51,"y":129},{"time":4057,"delay":400,"type":"screenshot-auto"}],"scrollY":530,"scrollX":0,"timestamp":1658736087421},{"name":"Action 3","ops":[{"type":"mousemove","time":399,"x":153,"y":147},{"type":"mousemove","time":600,"x":77,"y":88},{"type":"mousemove","time":805,"x":48,"y":69},{"type":"mousedown","time":931,"x":45,"y":66},{"type":"mousemove","time":1021,"x":45,"y":66},{"type":"mouseup","time":1062,"x":45,"y":66},{"time":1063,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":2000,"x":45,"y":66},{"type":"mouseup","time":2122,"x":45,"y":66},{"time":2123,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2934,"x":45,"y":66},{"type":"mousedown","time":2988,"x":45,"y":67},{"type":"mousemove","time":3136,"x":45,"y":67},{"type":"mouseup","time":3146,"x":45,"y":67},{"time":3147,"delay":400,"type":"screenshot-auto"}],"scrollY":1128,"scrollX":0,"timestamp":1658736095915},{"name":"Action 4","ops":[{"type":"mousemove","time":166,"x":266,"y":215},{"type":"mousemove","time":370,"x":79,"y":198},{"type":"mousemove","time":582,"x":55,"y":185},{"type":"mousedown","time":751,"x":52,"y":184},{"type":"mousemove","time":786,"x":52,"y":184},{"type":"mouseup","time":851,"x":52,"y":184},{"time":852,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":1708,"x":52,"y":184},{"type":"mousemove","time":1715,"x":52,"y":184},{"type":"mouseup","time":1854,"x":52,"y":184},{"time":1855,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2698,"x":52,"y":184},{"type":"mousedown","time":2732,"x":52,"y":185},{"type":"mouseup","time":2874,"x":52,"y":185},{"time":2875,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2903,"x":52,"y":185}],"scrollY":1548,"scrollX":0,"timestamp":1658736103250}] \ No newline at end of file