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)
------------------------------

Reply via email to