Transition Transition animation will enhance the customization power and would be one of the infrastructures for storytelling capability.
------------------------------ Framework of transition related API Via setOption: // Forward chart.setOption(optionA); chart.setOption(optionB); chart.setOption(optionC); chart.setOption(optionD); // Backward chart.setOption(optionD); chart.setOption(optionC); chart.setOption(optionB); chart.setOption(optionA); // Note that this sentences above are just illustrative.// in practice those sentences should commonly be// triggered by user behaviors rather than called// synchronously. Note that if intending to keep some of the component in merge mode, the capacity of deleting part of the components (or hide components) should be implemented: Probably: chart.setOption({ // Full option }); chart.setOption({ series: [ {id: 'ser0'}, {id: 'ser1'}, ] // Only replace series }, {replace: 'series'}); chart.setOption({ dataset: { ... }, series: [ {id: 'ser0'}, {id: 'ser1'}, ] // Only replace series and dataset }, {replace: ['series', 'dataset']}); ------------------------------ Transition casesTERMS I am not sure whether these terms below are appropriate. Before better one are promoted, we use them in this thread. - Transform/Transformation: means transit a data item to another data item one by one. - Division/Divide: means split a data item to some of data items, which might belong to different new series. - Assembly/Assemble: the opposite of division. Case OTHERS These cases below are correspondingly clear so we do not need to make further discussion. - Use dispatchAction to trigger "dataZoom" or "geoRoam" with animation. - Dynamic data as we already have. Case TRANSFORMATION_A var optionA = { dataset: {...}, xAxis: {id: 'A'}, yAxis: {id: 'A'}, series: [{ id: 'serA', xAxisId: 'A', yAxisId: 'A', type: 'scatter' }] };var optionB = { xAxis: {id: 'B'}, yAxis: {id: 'B'}, series: [{ id: 'serB', xAxisId: 'B', yAxisId: 'B', type: 'bar', }] };var optionC = { xAxis: {id: 'C'}, yAxis: {id: 'C'}, series: [{ id: 'serC', xAxisId: 'C', yAxisId: 'C', type: 'line' }] }; If we want the transition to be: Use the same data, but transit the elements to another coordinate system. From scatter to bar, can be morph. >From bar to line, can be fade out/in. Also consider, the case: a series change its coordinate system from cartesian to polar, where morph may need to be applied. Case TRANSFORMATION_B var optionA = { dataset: { source: [ // dim1 dim2 dim3 ['2020-03-01', 32, 4413, 0.17 ], ['2020-03-02', 42, 1423, 0.47 ], ['2020-03-03', 62, 5467, 0.87 ], ['2020-03-04', 12, 1498, 0.17 ], ['2020-03-05', 52, 2435, 0.57 ], ] }, xAxis: {}, yAxis: {}, series: [{ type: 'scatter', coordinateSystem: 'cartesian', encode: {x: 0, y: 1} }] };var optionB = { series: [{ // Update the series: encode.y is modified to `2`. encode: {x: 0, y: 2} }] }; If we want the transition to be: The the same coordinate system. But map a different dimension to yAxis. The scatter points should be transformed to the new location in the same coordinate system and the yAxis should be transformed properly. Case DIVISION_ASSEMBLY_A var optionA = { dataset: { id: 'dsA', source: [ // Date Month HP Category ['2020-03-29', 3, 32, 'Rice' ], // item0 ['2020-03-30', 3, 42, 'Pizza' ], // item1 ['2020-03-31', 3, 62, 'Noodles' ], // item2 ['2020-04-01', 4, 18, 'Rice' ], // item3 ['2020-04-02', 4, 52, 'Pizza' ], // item4 ] }, series: [ {id: 'ser0', type: 'bar', datasetId: 'dsA', encode: {x: 0, y: 2}} ] };var optionB = { dataset: { id: 'dsB', source: [ // HP Category [ 50, 'Rice' ], // dsA.item0 + dsA.item3 [ 94, 'Pizza' ], // dsA.item1 + dsA.item4 [ 62, 'Noodles' ], // dsA.item2 ] }, series: [ {id: 'ser1', type: 'bar', datasetId: 'dsB', encode: {x: 1, y: 0}} ] }; If we want the transition to be: Bars of "ser0" are assembled by "Category" and transit to "ser1". Or event, consider there is: var optionC = { dataset: { id: 'dsB', source: [ // Month HP [ 3, 136, ], // dsA.item0 + dsA.item1 + dsA.item2 [ 4, 70, ], // dsA.item3 + dsA.item4 ] }, series: [ {id: 'ser2', type: 'bar', datasetId: 'dsC', encode: {x: 1, y: 0}} ] }; Can we make transition from optionB to optionC, where both division and assembly happen? Case DIVISION_ASSEMBLY_B var optionA = { dataset: { source: [ // X Y1=Y2+Y3 Y2 Y3 ['2020-03-01', 32, 12, 20 ], ['2020-03-02', 42, 22, 20 ], ['2020-03-03', 62, 30, 32 ], ['2020-03-04', 18, 2, 16 ], ['2020-03-05', 52, 12, 40 ], ] }, series: [ {id: 'ser0', type: 'bar', encode: {x: 0, y: 1}} ] };var optionB = { series: [ {id: 'ser1', type: 'bar', encode: {x: 0, y: 2}}, {id: 'ser2', type: 'bar', encode: {x: 0, y: 3}} ] }; If we want the transition to be: Each bar of "ser0" is divided to a bar of "ser1" and a bar of "ser2". Case DIVISION_ASSEMBLY_C var optionA = { dataset: [{ id: 'dsA', source: [ // X Y_ser0 Y_ser1 Y_ser2 ['2020-03-01', 32, 44, 17 ], ['2020-03-02', 42, 14, 47 ], ['2020-03-03', 62, 54, 87 ], ['2020-03-04', 12, 14, 17 ], ['2020-03-05', 52, 24, 57 ], ] }, { id: 'dsB', source: [ ['Breakfast', 200], // sum of "dsA" dimension 1. ['Lunch', 150], // sum of "dsA" dimension 2. ['Supper', 225] // sum of "dsA" dimension 3. ] }], series: [{ // Also consider the type can be 'line' with `areaStyle: {}`, // where there is only one polygon for a series. id: 'ser0', type: 'bar', stack: 's', datasetId: 'dsA', name: 'Breakfast', encode: {x: 0, y: 1} }, { id: 'ser1', type: 'bar', stack: 's', datasetId: 'dsA', name: 'Lunch', encode: {x: 0, y: 2} }, { id: 'ser2', type: 'bar', stack: 's', datasetId: 'dsA', name: 'Supper', encode: {x: 0, y: 3} }] };var optionB = { series: [{ id: 'ser3', type: 'bar', name: 'sum', datasetId: 'dsB', encode: {x: 0, y: 1} }] }; If we want the transition to be: bars from "ser0" are assembled to "ser3" bar0, bars from "ser1" are assembled to "ser3" bar1, bars from "ser2" are assembled to "ser3" bar2, or vice versa (division). That is, the entire series of "ser0"/"ser1"/"ser2" are mapped to an datum of "ser3". Proposed transition API How to describe this mapping relationship in option? The key point is how to map the old graphic element to the new one. But follow the conventional principle, we should better firstly consider to describe the mapping in data rather than expose the concept "graphic element" to users. Moreover, since the description of transition might be volatile rather than persistent (only works while the setOption being called), probably we should not put the "transition description" in optoin. Instead, put it as one extra optional param of setOption, which has the same life-cycle as an Payload object. (But I am not totally sure about that, whether put the "transition" setting in option or in setOption param?) A proposed transition API to cover the cases above would be: chart.setOption( newOption, { // Transition mapping rules. Can be // a single rule (object) or rules (Array<object>). // For a single series, only one rule (from top to bottom) // can be accepted. transition: [ { from: {seriesId: 'ser0', dimension: 1}, to: [ {seriesId: 'ser1', dimension: 1}, {seriesId: 'ser2', dimension: 2}, {seriesId: 'ser3', dimension: 3} ] }, ... ], // If `true`, transit from "to" to "from". transitionBackward: true } ); Take the cases above as examples: // Case TRANSFORMATION_A chart.setOption( newOption, { replace: ['series', 'xAxis', 'yAxis', 'grid'], transition: { // By default, mapping by index. from: {seriesId: 'serA'}, to: {seriesId: 'serB'} } } );// Case DIVISION_ASSEMBLY_A chart.setOption( newOption, { replace: 'series', transition: { // Use the category dimension as the mapping key. from: {seriesId: 'ser0', mapOnDimension: 3}, to: {seriesId: 'ser1', mapOnDimension: 1} } } );// Case DIVISION_ASSEMBLY_B chart.setOption( newOption, { replace: 'series', transition: { // Both use dimension 0 (date string) as the mapping key. from: {seriesId: 'ser0', mapOnDimension: 0}, to: [ {seriesId: 'ser1', mapOnDimension: 0}, {seriesId: 'ser2', mapOnDimension: 0} ] } } );// Case DIVISION_ASSEMBLY_C// Theoretically DIVISION_ASSEMBLY_C is the same as DIVISION_ASSEMBLY_A// We can add three extra dimensions to dataset with all the values being series names:var optionA = { dataset: [{ id: 'dsA', source: [ // X SeriesName0 Y_ser0 SeriesName1 Y_ser1 SeriesName2 Y_ser2 ['2020-03-01', 'Breakfast', 32, 'Lunch', 44, 'Supper', 17 ], ['2020-03-02', 'Breakfast', 42, 'Lunch', 14, 'Supper', 47 ], ['2020-03-03', 'Breakfast', 62, 'Lunch', 54, 'Supper', 87 ], ['2020-03-04', 'Breakfast', 12, 'Lunch', 14, 'Supper', 17 ], ['2020-03-05', 'Breakfast', 52, 'Lunch', 24, 'Supper', 57 ], ] }, { id: 'dsB', source: [ ['Breakfast', 200], // sum of "dsA" dimension 2. ['Lunch', 150], // sum of "dsA" dimension 4. ['Supper', 225] // sum of "dsA" dimension 6. ] }], ... };// Add specify transition rules: chart.setOption( optionB, { replace: ['series', 'xAxis', 'yAxis', 'grid'], transition: { from: [ {seriesId: 'ser0', mapOnDimension: 1}, {seriesId: 'ser1', mapOnDimension: 3}, {seriesId: 'ser2', mapOnDimension: 5} ], // If `mapOnDimension` provided, use the values of that // dimension as the key to make transition mapping. // In this case, the value of dimension 0 in "ser3" // are "Breakfast", "Lunch", "Supper". They are mapped // to the series name of "ser0"/"ser1"/"ser2". to: {seriesId: 'ser3', mapOnDimension: 0} } } ); Implementation of "Internal-Series-Transition" Transition inside a single series has been implemented via data.diff and graphic.initProps/graphic.updateProps. But consider the cases TRANSFORMATION_A and TRANSFORMATION_B, the original updateProps should better be enhanced to support morph. Implementation of "Cross-Series-Transition" The implementation of "Cross-Series-Transition", which is probably not only necessary for "Division"/"Assembly" but also useful in other transform requirements above, might be more complicated. I am not sure about the detail of this implementation. Before we make further discussion, we assume that these design is appropriate: "Cross-Series-Transition" can only happen on "create new series". The "transition target" is a new series, and the "transition source" is an existing series, or an series having just been deleted (the latter one might be more common). First of all, consider "delete series", we need to introduce an unified mechanism to temporarily store the "previous series model" (just deleted) or, more specifically, the "previous data with graphic elements". Currently these "previous data" are stored on each series view, which is not able to "cross series". Secondly, we may need to introduce one extra stage before the render stage, say prepareCrossTransition. The stage prepareCrossTransition travels and recognizes the "transition rules" given in the params of setOption calling, finds a proper rule for each series, and detects mappings for each data item based on the key get from mapOnDimension, similar as what DataDiffer.ts did. After these mappings established, we can known how many pieces a single graphic element will be "divided to" or "assembled from". Then for each old graphic element related to any mapping: (A) If it will be "divided", we use a algorithm (say, splitAlgorithm) to calculate the polygons it can be split, and get sourcePolygons. (B) If it will not be "divided", we trade itself as the sourcePolygons. The result of the mappings and sourcePolygons are finally stored in a data structure, say crossTransitionManager. In the chartView.render, it receives new parameters indicating whether this series needs to "Crose-Series-Transition". If not, do things as usually. If so, for each element to be created, instead of calling graphic.initProps, we initialize a morph transition animation for the creating procedure. Firstly we retrieve the sourcePolygons from crossTransitionManager. And then, (A) If the element should be "assembled", we use splitAlgorithm to generate targetPolygons for each sourcePolygon, and start morph transition animations for each sourcePolygon-targetPolygon tuple. (B) If the element should not be "assembled", we trade the element itself as the targetPolygon, and start a morph transition animation for the sourcePolygon-targetPolygon tuple. ------------------------------ Transition on custom series Having the transition framework established, the transition description for "custom series" is similar. And we can customize more in graphic element definitions, since "custom series" has exposed the graphic element definitions to users. custom animation should be at least applied on - properties in "shape": e.g., shape.x, shape.height, shape.points, ... - "transform" propeties: x, y, scaleX, scaleY, originX, originY, rotation . - numeric "style" properties: style.x, style.y, style.opacity. - text, which we will discussed in the next section. Entering transition and leaving transition of custom series For example, if we want to implement the "enter animation" as: "the radius increases from 0 and the opacity increase from 0", the proposed option can be as follows: renderItem: function () { return { type: 'group', children: [{ type: 'circle', shape: { cx: 100, cy: 100, // Means that the final radius is 50 r: 50 }, style: { // Means that the final opacity is 1 opacity: 1, fill: 'red' }, // Set all "enter transition related props" here. // Notice that only needed props needs to set here. // Then the enter animation will be performed from // these props. // The arguments of the enter animation will follow the // settings like `animationEasing`, `animationDuration` // on seriesModel, as other series did. enterTransition: { // The initial radius is 0 shape: { r: 0 }, // The initial opacify is 0 style: { opacity: 0 } }, // Set all of the "out props" here. It works when the element // will be disappeared. // In most case, simply set opacity: 0 as follows is enough. leaveTransition: { style: { opacity: 0 } } }] }; } [[PENDING]] Do we need to support setting style.opcity on a group, which can be adopted on every descendants in this group. This probably enhance the animation performance for custom series. Updating transition of custom series As we already have (what scatter series did), data item mappings can be made automatically by dataItem.name or index. But we should better automatically make transition animation when updating, because echarts does not known which properties changed. This work should better be left to users. chart.setOtion({ dataset: { source: [ ['2020-02-02', 12, 553], ['2020-02-03', 55, 172], ['2020-02-04', 16, 812], ['2020-02-05', 94, 756], ] }, series: { type: 'custom', renderItem: function (params, api) { var pos = api.coord([api.value(0), api.value(1)]); return { type: 'group', children: [{ type: 'rect', shape: {x: -20, y: -20, width: 40, height: 40}, // Instead of setting `x`/`y` on the root level of this `rect`, // we set them in `updateTransition`, which means if update // transition occurs, auto make transition animation (additive // animation) targeting to these `x`/`y`, otherwise set this // `x`/`y` directly. The behavior is like what `graphic.updateProps` // did. updateTransition: { x: pos[0], y: pos[1], } }] }; }, // Mapping can be established by itemName. // If itemName not specified, mapping can be established by index, // which makes sense when dataZoom exists. encode: {x: 1, y: 2, itemName: 0} } });// Now update chart.setOption({ dataset: { source: [ // ['2020-02-02', 12, 553], // remove one ['2020-02-03', 55, 172], ['2020-02-04', 16, 812], ['2020-02-05', 94, 756], ['2020-02-06', 71, 318], // add one ] } }); Cross-Series-Transition of custom series This kind of API will be discuss later, after the final morph API (division/assembly) decided. Thanks, ------------------------------ Su Shuang (100pah) ------------------------------