This is an automated email from the ASF dual-hosted git repository. sushuang pushed a commit to branch custom-enhance in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit ba37b28240bb657a05488a80a96e4c37b2d80b91 Author: sushuang <[email protected]> AuthorDate: Thu Sep 20 02:53:39 2018 +0800 Try to support animation from and custom-series animation. --- src/chart/custom.js | 133 ++++++++++++++++++++++++-------------- src/chart/line/LineView.js | 16 ++--- src/component/axis/AxisBuilder.js | 34 ++++++++++ src/export.js | 2 +- src/processor/dataStack.js | 4 +- src/util/graphic.js | 44 ++++++++++--- test/custom-feature.html | 101 ++++++++++++++++++++++++++++- test/tmp-base.html | 4 +- 8 files changed, 266 insertions(+), 72 deletions(-) diff --git a/src/chart/custom.js b/src/chart/custom.js index 98f7ead..ce7ae3a 100644 --- a/src/chart/custom.js +++ b/src/chart/custom.js @@ -42,6 +42,10 @@ var LABEL_EMPHASIS = ['emphasis', 'label']; // which will cause weird udpate animation. var GROUP_DIFF_PREFIX = 'e\0\0'; +var IMAGE_TRANSITION_STYLES = ['x', 'y', 'width', 'height', 'opacity']; +var TEXT_TRANSITION_STYLES = ['x', 'y', 'opacity']; +var PATH_TRANSITION_STYLES = ['opacity', 'lineWidth']; + /** * To reduce total package size of each coordinate systems, the modules `prepareCustom` * of each coordinate systems are not required by each coordinate systems directly, but @@ -252,54 +256,78 @@ function createEl(elOption) { } function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot) { - var transitionProps = {}; - var elOptionStyle = elOption.style || {}; - - elOption.shape && (transitionProps.shape = zrUtil.clone(elOption.shape)); - elOption.position && (transitionProps.position = elOption.position.slice()); - elOption.scale && (transitionProps.scale = elOption.scale.slice()); - elOption.origin && (transitionProps.origin = elOption.origin.slice()); - elOption.rotation && (transitionProps.rotation = elOption.rotation); - - if (el.type === 'image' && elOption.style) { - var targetStyle = transitionProps.style = {}; - zrUtil.each(['x', 'y', 'width', 'height'], function (prop) { - prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit); - }); + var elType = el.type; + var isGroup = el.isGroup; + var isImage = elType === 'image'; + var isText = elType === 'text'; + var notCopy = elOption.copy === false; + + var userInitState = elOption.initState; + var styleToBeSet = elOption.style; + + // Make `elOptionValid` and `initStateValid`. + var elOptionValid = fetchDisplayableProps(elOption, notCopy); + var initStateValid = isInit && userInitState && fetchDisplayableProps(userInitState, notCopy); + + // Prepare `styleTransValues`. + var styleTransValues; + if (isInit && !userInitState && !isGroup) { + // Default init animation. + styleTransValues = {opacity: 0}; + } + else { + var styleTransCandidates = isInit ? (userInitState && userInitState.style) : styleToBeSet; + // Enable style transition. + var styleTransProps = isImage + ? IMAGE_TRANSITION_STYLES + : isText + ? TEXT_TRANSITION_STYLES + : !isGroup + ? PATH_TRANSITION_STYLES + : null; + if (styleTransProps && styleTransCandidates) { + for (var i = 0; i < styleTransProps.length; i++) { + var prop = styleTransProps[i]; + if (styleTransCandidates[prop] != null) { + // Condider animation performance, do not give until necessary. + (styleTransValues = styleTransValues || {})[prop] = styleTransCandidates[prop]; + !isInit && (styleToBeSet[prop] = el.style[prop]); + } + } + } } - if (el.type === 'text' && elOption.style) { - var targetStyle = transitionProps.style = {}; - zrUtil.each(['x', 'y'], function (prop) { - prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit); - }); - // Compatible with previous: both support - // textFill and fill, textStroke and stroke in 'text' element. - !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && ( - elOptionStyle.textFill = elOptionStyle.fill + // Compatible with previous: both support + // textFill and fill, textStroke and stroke in 'text' element. + if (isText) { + !styleToBeSet.hasOwnProperty('textFill') && styleToBeSet.fill && ( + styleToBeSet.textFill = styleToBeSet.fill ); - !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && ( - elOptionStyle.textStroke = elOptionStyle.stroke + !styleToBeSet.hasOwnProperty('textStroke') && styleToBeSet.stroke && ( + styleToBeSet.textStroke = styleToBeSet.stroke ); } - if (el.type !== 'group') { - el.useStyle(elOptionStyle); - - // Init animation. - if (isInit) { - el.style.opacity = 0; - var targetOpacity = elOptionStyle.opacity; - targetOpacity == null && (targetOpacity = 1); - graphicUtil.initProps(el, {style: {opacity: targetOpacity}}, animatableModel, dataIndex); - } + if (!isGroup) { + // Follow the convention and consider performance, merge by default. + // If `el.merge` is false, here the `el` is a new element just created. + (!isInit && elOption.mergeStyle === false) + ? el.useStyle(styleToBeSet || {}) + : (styleToBeSet && el.setStyle(styleToBeSet)); } if (isInit) { - el.attr(transitionProps); + elOptionValid && el.attr(elOptionValid); + styleTransValues && ((initStateValid = initStateValid || {}).style = styleTransValues); + // When "init", the current props represents the final state, while the `initState` is a subset of + // props. When "update", the current props represents the current state, while the final state is + // a subset of props. So we do not use `graphicUtil.initProps`. See the comment of + // `graphicUtil.initFromProps` for more details. + initStateValid && graphicUtil.initFromProps(el, initStateValid, animatableModel, dataIndex); } else { - graphicUtil.updateProps(el, transitionProps, animatableModel, dataIndex); + styleTransValues && ((elOptionValid = elOptionValid || {}).style = styleTransValues); + elOptionValid && graphicUtil.updateProps(el, elOptionValid, animatableModel, dataIndex); } // Merge by default. @@ -313,6 +341,7 @@ function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot // Update them only when user specified, otherwise, remain. elOption.hasOwnProperty('info') && el.attr('info', elOption.info); + // Set `styleEmphasis`. // If `elOption.styleEmphasis` is `false`, remove hover style. The // logic is ensured by `graphicUtil.setElementHoverStyle`. var styleEmphasis = elOption.styleEmphasis; @@ -330,11 +359,19 @@ function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot isRoot && graphicUtil.setAsHoverStyleTrigger(el, !disableStyleEmphasis); } -function prepareStyleTransition(prop, targetStyle, elOptionStyle, oldElStyle, isInit) { - if (elOptionStyle[prop] != null && !isInit) { - targetStyle[prop] = elOptionStyle[prop]; - elOptionStyle[prop] = oldElStyle[prop]; - } +function fetchDisplayableProps(option, notCopy) { + // Condider performance of `graphic.initFromProps/updateProps`, do not give until necessary. + var props; + var v; + // Consider performance of large shape, enable user to disable copy. + (v = option.shape) && ((props = props || {}).shape = notCopy ? v : zrUtil.clone(v)); + // Transform attributes will be copied inside `el.attr()` and `graphic.updateProps()`. + (v = option.position) && ((props = props || {}).position = v); + (v = option.scale) && ((props = props || {}).scale = v); + (v = option.origin) && ((props = props || {}).origin = v); + (v = option.rotation) != null && ((props = props || {}).rotation = v); + + return props; } function makeRenderItem(customSeries, data, ecModel, api) { @@ -620,25 +657,25 @@ function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, } // Usage: -// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that +// (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 +// (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 +// (3) If `elOption.mergeChildren` is `false`, the existing children will be // replaced totally. // (4) If `!elOption.children`, following the "merge" principle, nothing will happen. // // 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). // User can remove a single child by set its `ignore` as `true` or replace -// it by another element, where its `$merge` can be set as `true` if necessary. +// it by another element, where its `merge` can be set as `true` if necessary. function mergeChildren(el, dataIndex, elOption, animatableModel, data) { var newChildren = elOption.children; var newLen = newChildren ? newChildren.length : 0; - var mergeChildren = elOption.$mergeChildren; + var mergeChildren = elOption.mergeChildren; // `diffChildrenByName` has been deprecated. var byName = mergeChildren === 'byName' || elOption.diffChildrenByName; var notMerge = mergeChildren === false; @@ -678,7 +715,7 @@ function mergeChildren(el, dataIndex, elOption, animatableModel, data) { if (__DEV__) { zrUtil.assert( !notMerge || el.childCount() === index, - 'MUST NOT contain empty item in children array when `group.$mergeChildren` is `false`.' + 'MUST NOT contain empty item in children array when `group.mergeChildren` is `false`.' ); } } diff --git a/src/chart/line/LineView.js b/src/chart/line/LineView.js index 501f026..97a59ed 100644 --- a/src/chart/line/LineView.js +++ b/src/chart/line/LineView.js @@ -733,7 +733,7 @@ export default ChartView.extend({ shape: { points: next } - }, seriesModel); + }, seriesModel, null, {during: duringAnimation}); if (polygon) { polygon.setShape({ @@ -758,19 +758,17 @@ export default ChartView.extend({ if (el) { updatedDataInfo.push({ el: el, - ptIdx: i // Index of points + ptIdx: i // Index of points }); } } } - if (polyline.animators && polyline.animators.length) { - polyline.animators[0].during(function () { - for (var i = 0; i < updatedDataInfo.length; i++) { - var el = updatedDataInfo[i].el; - el.attr('position', polyline.shape.__points[updatedDataInfo[i].ptIdx]); - } - }); + function duringAnimation() { + for (var i = 0; i < updatedDataInfo.length; i++) { + var el = updatedDataInfo[i].el; + el.attr('position', polyline.shape.__points[updatedDataInfo[i].ptIdx]); + } } }, diff --git a/src/component/axis/AxisBuilder.js b/src/component/axis/AxisBuilder.js index 1f0ff7e..5a5254d 100644 --- a/src/component/axis/AxisBuilder.js +++ b/src/component/axis/AxisBuilder.js @@ -612,6 +612,40 @@ function buildAxisTick(axisBuilder, axisModel, opt) { z2: 2, silent: true })); + +// if (window.__basesss == null) { +// window.__basesss = 1; +// } +// (function () { +// var uuid = window.__basesss++; +// Object.defineProperty(tickEl.shape, 'uuid', { +// get() { +// return uuid; +// }, +// set() { +// debugger; +// } +// }); +// })(); + +// Object.defineProperty(tickEl.shape, 'aaaa', { +// get() { +// return 1; +// }, +// enumerable: true, +// set(val) { +// console.log(val, '???????????????????????'); +// debugger; +// } +// }); +// if (!window.__xxx) { +// console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); +// window.__xxx = tickEl; +// // tickEl.shape.aaaa = 1; +// console.log(tickEl.shape.aaaa); +// console.log(tickEl.shape.uuid, '=++++++++++++++'); +// } + axisBuilder.group.add(tickEl); tickEls.push(tickEl); } diff --git a/src/export.js b/src/export.js index dded75c..0542ab0 100644 --- a/src/export.js +++ b/src/export.js @@ -69,7 +69,7 @@ zrUtil.each( 'extendShape', 'extendPath', 'makePath', 'makeImage', 'mergePath', 'resizePath', 'createIcon', 'setHoverStyle', 'setLabelStyle', 'setTextStyle', 'setText', - 'getFont', 'updateProps', 'initProps', 'getTransform', + 'getFont', 'updateProps', 'initProps', 'initFromProps', 'getTransform', 'clipPointsByRect', 'clipRectByRect', 'Group', 'Image', diff --git a/src/processor/dataStack.js b/src/processor/dataStack.js index 22b9202..9ca423e 100644 --- a/src/processor/dataStack.js +++ b/src/processor/dataStack.js @@ -19,11 +19,11 @@ import {createHashMap, each} from 'zrender/src/core/util'; -// (1) [Caution]: the logic is correct based on the premises: +// (1) [Caution]: the logic is correct based on the promises: // data processing stage is blocked in stream. // See <module:echarts/stream/Scheduler#performDataProcessorTasks> // (2) Only register once when import repeatly. -// Should be executed before after series filtered and before stack calculation. +// Should be executed after series filtered and before stack calculation. export default function (ecModel) { var stackInfoMap = createHashMap(); ecModel.eachSeries(function (seriesModel) { diff --git a/src/util/graphic.js b/src/util/graphic.js index f44b844..71befb7 100644 --- a/src/util/graphic.js +++ b/src/util/graphic.js @@ -886,7 +886,7 @@ export function getFont(opt, ecModel) { ].join(' ')); } -function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) { +function animateOrSetProps(isUpdate, isFrom, el, props, animatableModel, dataIndex, cb) { if (typeof dataIndex === 'function') { cb = dataIndex; dataIndex = null; @@ -914,11 +914,14 @@ function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) } duration > 0 - ? el.animateTo(props, duration, animationDelay || 0, animationEasing, cb, !!cb) + ? el[isFrom ? 'animateFrom' : 'animateTo']( + props, duration, animationDelay || 0, animationEasing, cb, !!cb + ) + // Consider that the new `props` maybe different from the last `props`, + // forward to last to ensure the last settings work. : (el.stopAnimation(), el.attr(props), cb && cb()); } - else { - el.stopAnimation(); + else if (!isFrom) { el.attr(props); cb && cb(); } @@ -936,7 +939,7 @@ function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) * @param {Object} props * @param {module:echarts/model/Model} [animatableModel] * @param {number} [dataIndex] - * @param {Function} [cb] + * @param {Function|Object} [cb] Function: done, Object: {done: function, during: function} * @example * graphic.updateProps(el, { * position: [100, 100] @@ -947,7 +950,7 @@ function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) * }, seriesModel, function () { console.log('Animation done!'); }); */ export function updateProps(el, props, animatableModel, dataIndex, cb) { - animateOrSetProps(true, el, props, animatableModel, dataIndex, cb); + animateOrSetProps(true, false, el, props, animatableModel, dataIndex, cb); } /** @@ -962,10 +965,34 @@ export function updateProps(el, props, animatableModel, dataIndex, cb) { * @param {Object} props * @param {module:echarts/model/Model} [animatableModel] * @param {number} [dataIndex] - * @param {Function} cb + * @param {Function|Object} [cb] Function: done, Object: {done: function, during: function} */ export function initProps(el, props, animatableModel, dataIndex, cb) { - animateOrSetProps(false, el, props, animatableModel, dataIndex, cb); + animateOrSetProps(false, false, el, props, animatableModel, dataIndex, cb); +} + +/** + * Animate from the given props to el current state with "init" animation settings. + * The rest definitions are the same as `initPorps`. + * + * Consider that only props that existing in `initState` can be animated, and sometimes + * the target state is the complete set of props but the initial state is just a subset, + * using `initFromProps` is simpler. For example: + * + * Use `initProps` to make init animation: + * ```js + * var {targetStateNotInInitState, targetStateInInitState} = split(targetState, initState); + * el.attr(targetStateNotInInitState); + * graphicUtil.initProps(elInInitState, targetStateInInitState); + * ``` + * + * Use `initFromProps` to make init animation. + * ```js + * graphicUtil.initFromProps(elInTargetState, initState, ...)`; + * ``` + */ +export function initFromProps(el, props, animatableModel, dataIndex, cb) { + animateOrSetProps(false, true, el, props, animatableModel, dataIndex, cb); } /** @@ -1066,6 +1093,7 @@ export function groupTransition(g1, g2, animatableModel, cb) { if (!el.isGroup && el.anid) { var oldEl = elMap1[el.anid]; if (oldEl) { + // ??? animateFrom var newProp = getAnimatableProps(el); el.attr(getAnimatableProps(oldEl)); updateProps(el, newProp, animatableModel, el.dataIndex); diff --git a/test/custom-feature.html b/test/custom-feature.html index 7c568b1..c27d75b 100644 --- a/test/custom-feature.html +++ b/test/custom-feature.html @@ -42,7 +42,7 @@ under the License. <div id="main0"></div> <div id="main2"></div> <div id="main3"></div> - <!-- <div id="main1"></div> --> + <div id="main4"></div> <script> @@ -453,5 +453,104 @@ under the License. + + + + + <script> + + require(['echarts'/*, 'map/js/china' */], function (echarts) { + + var animationDuration = 5000; + var animationDurationUpdate = 4000; + var option = { + xAxis: {}, + yAxis: {}, + dataZoom: [ + { + }, + // { + // type: 'inside' + // } + ], + animationDuration: animationDuration, + animationDurationUpdate: animationDurationUpdate, + // animation: false, + series: [{ + type: 'custom', + renderItem: function (params, api) { + return { + type: 'group', + position: api.coord([api.value(0), api.value(1)]), + children: [{ + type: 'rect', + shape: { + x: -50, + y: 50, + width: 100, + height: 150, + r: 10 + }, + style: { + fill: 'rgba(102,241,98,0.9)' + } + }, { + type: 'circle', + shape: { + cx: -50, + cy: 50, + r: 30 + }, + style: { + text: 'data', + textFill: '#fff', + fill: 'blue' + } + }] + }; + }, + data: [[121, 333], [29, 312]] + }] + }; + + var chart = testHelper.create(echarts, 'main4', { + title: [ + 'Style merge', + '(1) dataZoom hide a data item, and then show it, ensure the fade in animation normal.', + '(2) click button to setOption merge.' + ], + option: option, + info: { + animationDuration: animationDuration, + animationDurationUpdate: animationDurationUpdate + }, + buttons: [{ + text: 'merge style: border become blue, but background not changed', + onclick: function () { + chart.setOption({ + type: 'custom', + renderItem: function (params, api) { + return { + type: 'group', + children: [{ + type: 'rect', + style: { + stroke: 'red', + lineWidth: 5 + } + }] + }; + } + }); + } + }] + }); + }); + + </script> + + + + </body> </html> \ No newline at end of file diff --git a/test/tmp-base.html b/test/tmp-base.html index 3f3693c..dbb80e6 100644 --- a/test/tmp-base.html +++ b/test/tmp-base.html @@ -48,9 +48,7 @@ under the License. var myChart; var option; - require([ - 'echarts'/*, 'map/js/china' */ - ], function (echarts) { + require(['echarts'/*, 'map/js/china' */], function (echarts) { // $.getJSON('./data/nutrients.json', function (data) { // }); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
