未验证 提交 04e86855 编写于 作者: S sushuang 提交者: GitHub

Merge pull request #9783 from apache/feature-dataZoomAction

feat(dataZoom): enhance dispatchAction for dataZoom. Normalize illega…
......@@ -20,6 +20,7 @@
import * as zrUtil from 'zrender/src/core/util';
import * as numberUtil from '../../util/number';
import * as helper from './helper';
import sliderMove from '../helper/sliderMove';
var each = zrUtil.each;
var asc = numberUtil.asc;
......@@ -194,48 +195,43 @@ AxisProxy.prototype = {
var scale = axisModel.axis.scale;
var rangePropMode = this._dataZoomModel.getRangePropMode();
var percentExtent = [0, 100];
var percentWindow = [
opt.start,
opt.end
];
var percentWindow = [];
var valueWindow = [];
var hasPropModeValue;
each(['startValue', 'endValue'], function (prop) {
valueWindow.push(opt[prop] != null ? scale.parse(opt[prop]) : null);
});
// Normalize bound.
each([0, 1], function (idx) {
var boundValue = valueWindow[idx];
var boundPercent = percentWindow[idx];
each(['start', 'end'], function (prop, idx) {
var boundPercent = opt[prop];
var boundValue = opt[prop + 'Value'];
// Notice: dataZoom is based either on `percentProp` ('start', 'end') or
// on `valueProp` ('startValue', 'endValue'). The former one is suitable
// for cases that a dataZoom component controls multiple axes with different
// unit or extent, and the latter one is suitable for accurate zoom by pixel
// (e.g., in dataZoomSelect). `valueProp` can be calculated from `percentProp`,
// but it is awkward that `percentProp` can not be obtained from `valueProp`
// accurately (because all of values that are overflow the `dataExtent` will
// be calculated to percent '100%'). So we have to use
// `dataZoom.getRangePropMode()` to mark which prop is used.
// `rangePropMode` is updated only when setOption or dispatchAction, otherwise
// it remains its original value.
// on `valueProp` ('startValue', 'endValue'). (They are based on the data extent
// but not min/max of axis, which will be calculated by data window then).
// The former one is suitable for cases that a dataZoom component controls multiple
// axes with different unit or extent, and the latter one is suitable for accurate
// zoom by pixel (e.g., in dataZoomSelect).
// we use `getRangePropMode()` to mark which prop is used. `rangePropMode` is updated
// only when setOption or dispatchAction, otherwise it remains its original value.
// (Why not only record `percentProp` and always map to `valueProp`? Because
// the map `valueProp` -> `percentProp` -> `valueProp` probably not the original
// `valueProp`. consider two axes constrolled by one dataZoom. They have different
// data extent. All of values that are overflow the `dataExtent` will be calculated
// to percent '100%').
if (rangePropMode[idx] === 'percent') {
if (boundPercent == null) {
boundPercent = percentExtent[idx];
}
boundPercent == null && (boundPercent = percentExtent[idx]);
// Use scale.parse to math round for category or time axis.
boundValue = scale.parse(numberUtil.linearMap(
boundPercent, percentExtent, dataExtent, true
boundPercent, percentExtent, dataExtent
));
}
else {
hasPropModeValue = true;
boundValue = boundValue == null ? dataExtent[idx] : scale.parse(boundValue);
// Calculating `percent` from `value` may be not accurate, because
// This calculation can not be inversed, because all of values that
// are overflow the `dataExtent` will be calculated to percent '100%'
boundPercent = numberUtil.linearMap(
boundValue, dataExtent, percentExtent, true
boundValue, dataExtent, percentExtent
);
}
......@@ -245,9 +241,31 @@ AxisProxy.prototype = {
percentWindow[idx] = boundPercent;
});
asc(valueWindow);
asc(percentWindow);
// The windows from user calling of `dispatchAction` might be out of the extent,
// or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we dont restrict window
// by `zoomLock` here, because we see `zoomLock` just as a interaction constraint,
// where API is able to initialize/modify the window size even though `zoomLock`
// specified.
var spans = this._minMaxSpan;
hasPropModeValue
? restrictSet(valueWindow, percentWindow, dataExtent, percentExtent, false)
: restrictSet(percentWindow, valueWindow, percentExtent, dataExtent, true);
function restrictSet(fromWindow, toWindow, fromExtent, toExtent, toValue) {
var suffix = toValue ? 'Span' : 'ValueSpan';
sliderMove(0, fromWindow, fromExtent, 'all', spans['min' + suffix], spans['max' + suffix]);
for (var i = 0; i < 2; i++) {
toWindow[i] = numberUtil.linearMap(fromWindow[i], fromExtent, toExtent, true);
toValue && (toWindow[i] = scale.parse(toWindow[i]));
}
}
return {
valueWindow: asc(valueWindow),
percentWindow: asc(percentWindow)
valueWindow: valueWindow,
percentWindow: percentWindow
};
},
......@@ -277,13 +295,14 @@ AxisProxy.prototype = {
// }
// }, this);
// `calculateDataWindow` uses min/maxSpan.
setMinMaxSpan(this);
var dataWindow = this.calculateDataWindow(dataZoomModel.option);
this._valueWindow = dataWindow.valueWindow;
this._percentWindow = dataWindow.percentWindow;
setMinMaxSpan(this);
// Update axis setting then.
setAxisModel(this);
},
......@@ -492,24 +511,27 @@ function setAxisModel(axisProxy, isRestore) {
function setMinMaxSpan(axisProxy) {
var minMaxSpan = axisProxy._minMaxSpan = {};
var dataZoomModel = axisProxy._dataZoomModel;
var dataExtent = axisProxy._dataExtent;
each(['min', 'max'], function (minMax) {
minMaxSpan[minMax + 'Span'] = dataZoomModel.get(minMax + 'Span');
// minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
var percentSpan = dataZoomModel.get(minMax + 'Span');
var valueSpan = dataZoomModel.get(minMax + 'ValueSpan');
valueSpan != null && (valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan));
// minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
if (valueSpan != null) {
minMaxSpan[minMax + 'ValueSpan'] = valueSpan;
valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan);
if (valueSpan != null) {
var dataExtent = axisProxy._dataExtent;
minMaxSpan[minMax + 'Span'] = numberUtil.linearMap(
dataExtent[0] + valueSpan, dataExtent, [0, 100], true
);
}
percentSpan = numberUtil.linearMap(
dataExtent[0] + valueSpan, dataExtent, [0, 100], true
);
}
else if (percentSpan != null) {
valueSpan = numberUtil.linearMap(
percentSpan, [0, 100], dataExtent, true
) - dataExtent[0];
}
minMaxSpan[minMax + 'Span'] = percentSpan;
minMaxSpan[minMax + 'ValueSpan'] = valueSpan;
});
}
......
......@@ -29,8 +29,7 @@
* handleEnds will be modified in this method.
* @param {Array.<number>} extent handleEnds is restricted by extent.
* extent[0] should less or equals than extent[1].
* @param {number|string} handleIndex Can be 'all', means that both move the two handleEnds,
* where the input minSpan and maxSpan will not work.
* @param {number|string} handleIndex Can be 'all', means that both move the two handleEnds.
* @param {number} [minSpan] The range of dataZoom can not be smaller than that.
* If not set, handle0 and cross handle1. If set as a non-negative
* number (including `0`), handles will push each other when reaching
......@@ -39,9 +38,6 @@
* @return {Array.<number>} The input handleEnds.
*/
export default function (delta, handleEnds, extent, handleIndex, minSpan, maxSpan) {
// Normalize firstly.
handleEnds[0] = restrict(handleEnds[0], extent);
handleEnds[1] = restrict(handleEnds[1], extent);
delta = delta || 0;
......@@ -55,10 +51,15 @@ export default function (delta, handleEnds, extent, handleIndex, minSpan, maxSpa
maxSpan = Math.max(maxSpan, minSpan != null ? minSpan : 0);
}
if (handleIndex === 'all') {
minSpan = maxSpan = Math.abs(handleEnds[1] - handleEnds[0]);
var handleSpan = Math.abs(handleEnds[1] - handleEnds[0]);
handleSpan = restrict(handleSpan, [0, extentSpan]);
minSpan = maxSpan = restrict(handleSpan, [minSpan, maxSpan]);
handleIndex = 0;
}
handleEnds[0] = restrict(handleEnds[0], extent);
handleEnds[1] = restrict(handleEnds[1], extent);
var originalDistSign = getSpanSign(handleEnds, handleIndex);
handleEnds[handleIndex] += delta;
......@@ -95,5 +96,8 @@ function getSpanSign(handleEnds, handleIndex) {
}
function restrict(value, extend) {
return Math.min(extend[1], Math.max(extend[0], value));
}
\ No newline at end of file
return Math.min(
extend[1] != null ? extend[1] : Infinity,
Math.max(extend[0] != null ? extend[0] : -Infinity, value)
);
}
......@@ -132,6 +132,13 @@ export function round(x, precision, returnStr) {
return returnStr ? x : +x;
}
/**
* asc sort arr.
* The input arr will be modified.
*
* @param {Array} arr
* @return {Array} The input arr.
*/
export function asc(arr) {
arr.sort(function (a, b) {
return a - b;
......
......@@ -23,6 +23,7 @@ under the License.
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="../dist/echarts.js"></script>
<script src="lib/testHelper.js"></script>
<script src="lib/facePrint.js"></script>
<script src="lib/jquery.min.js"></script>
<link rel="stylesheet" href="lib/reset.css" />
......@@ -39,34 +40,126 @@ under the License.
margin: 0;
}
.chart {
height: 450px;
height: 400px;
}
.by-dispatch-action {
padding: 5px;
}
</style>
<div class="by-dispatch-action">
by dispatchAction:
<button id="change-start-value">Change Start Value</button>
<button id="change-end-value">Change Start Value</button>
<button id="first-focus">First Focus</button>
</div>
<div id="main0"></div>
<div id="main2"></div>
<div id="main3"></div>
<div id="main4"></div>
<div id="main">
<div class="chart" id="chart"></div>
</div>
<script>
function makeButtons(ctx) {
function printStates() {
var option = ctx.chart.getOption();
var dzs = option.dataZoom;
var info = [{
type: dzs[0].type,
start: dzs[0].start,
end: dzs[0].end,
startValue: dzs[0].startValue,
endValue: dzs[0].endValue
}, {
type: dzs[1].type,
start: dzs[1].start,
end: dzs[1].end,
startValue: dzs[1].startValue,
endValue: dzs[1].endValue
}]
alert(
'!!! ' + ctx.hint + ' !!!\n' + JSON.stringify(info, null, 2)
);
}
function dispatchPercent(start, end) {
ctx.chart.dispatchAction({
type: 'dataZoom',
batch: [
{id: 'dz-in', start: start, end: end},
{id: 'dz-s', start: start, end: end}
]
});
}
function dispatchValue(startValue, endValue) {
ctx.chart.dispatchAction({
type: 'dataZoom',
batch: [
{id: 'dz-in', startValue: startValue, endValue: endValue},
{id: 'dz-s', startValue: startValue, endValue: endValue}
]
});
}
var buttons = [];
echarts.util.each(ctx.percentButttons, function (item) {
buttons.push({
text: item.text,
onclick: function () {
dispatchPercent(item.start, item.end);
printStates();
}
})
});
echarts.util.each(ctx.valueButtons, function (item) {
buttons.push({
text: item.text,
onclick: function () {
dispatchValue(item.startValue, item.endValue);
printStates();
}
})
});
return buttons;
}
function getDefaultPercentButtons() {
return [{
text: '{start: -5, end: 5} over min',
start: -5,
end: 5
}, {
text: '{start: 95, end: 105} over max',
start: 95,
end: 105
}, {
text: '{start: 40, end: 60}',
start: 40,
end: 60
}, {
text: '{start: 40, end: 90} (over maxSpan)',
start: 40,
end: 90
}, {
text: '{start: 40, end: 41} (over minSpan)',
start: 40,
end: 41
}];
}
</script>
$.getJSON('./data/ec-star.json', function (data) {
$('#first-focus').on('click', firstFocus);
$('#change-start-value').on('click', changeStartValue);
$('#change-end-value').on('click', changeEndValue);
var myChart = echarts.init(document.getElementById('chart'));
<script>
$.getJSON('./data/ec-star.json', function (data) {
var minStartValue = '2013-06-06';
var maxEndValue = '2017-10-17';
......@@ -167,44 +260,399 @@ under the License.
}]
};
myChart.setOption(option);
var myChart = testHelper.create(echarts, 'main0', {
option: option,
height: 300,
buttons: [{
text: 'Change Start Value',
onclick: function (phase) {
currStartValue = '2014-09-01';
myChart.dispatchAction({
type: 'dataZoom',
id: 'dz',
startValue: currStartValue
});
}
}, {
text: 'Change End Value',
onclick: function (phase) {
currEndValue = '2017-02-05';
myChart.dispatchAction({
type: 'dataZoom',
id: 'dz',
endValue: currEndValue
});
}
}, {
text: 'firstFocus',
onclick: function (phase) {
currStartValue = minStartValue;
currEndValue = '2013-11-06';
myChart.dispatchAction({
type: 'dataZoom',
id: 'dz',
startValue: currStartValue,
endValue: currEndValue
});
}
}]
});
});
</script>
$(window).resize(function() {
myChart.resize();
})
// Control -------------------------
function changeStartValue(phase) {
currStartValue = '2014-09-01';
myChart.dispatchAction({
type: 'dataZoom',
id: 'dz',
startValue: currStartValue
});
<script>
var option;
var base = +new Date(1990, 9, 3);
var oneDay = 24 * 3600 * 1000;
var startValue = Math.random() * 300;
var data1 = [];
var category = [];
var specialNormal = [];
var specialLong = [];
var specialShort = [];
for (var i = 0; i < 100; i++) {
var now = new Date(base += oneDay);
var cat = [now.getFullYear(), now.getMonth() + 1, now.getDate()].join('-');
category.push(cat);
value = Math.round((Math.random() - 0.5) * 20 + (!i ? startValue : data1[i - 1]));
data1.push(value);
if (i === 40) {
specialNormal[0] = cat;
specialLong[0] = cat;
specialShort[0] = cat;
}
else if (i === 42) {
specialShort[1] = cat;
}
else if (i === 55) {
specialNormal[1] = cat;
}
else if (i === 85) {
specialLong[1] = cat;
}
}
function changeEndValue(phase) {
currEndValue = '2017-02-05';
myChart.dispatchAction({
type: 'dataZoom',
id: 'dz',
endValue: currEndValue
});
var option = {
tooltip: {
trigger: 'axis'
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
xAxis: [{
type: 'category',
boundaryGap: false,
data: category
}],
yAxis: [{
type: 'value',
boundaryGap: [0, '100%']
}],
dataZoom: [{
type: 'inside',
id: 'dz-in',
start: 0,
end: 10,
minSpan: 5,
maxSpan: 30,
xAxisIndex: 0
}, {
id: 'dz-s',
start: 0,
end: 10,
minSpan: 5,
maxSpan: 30,
xAxisIndex: 0
}],
series: [{
type:'line',
symbolSize: 10,
data: data1
}]
};
function getBtnLabel(special, extra) {
extra = extra || '';
return 'startValue: "' + special[0] + ', endValue: "' + special[1] + '" ' + extra;
}
function firstFocus(phase) {
currStartValue = minStartValue;
currEndValue = '2013-11-06';
myChart.dispatchAction({
type: 'dataZoom',
id: 'dz',
startValue: currStartValue,
endValue: currEndValue
});
var ctx = {
hint: 'category axis value should be integer',
percentButttons: getDefaultPercentButtons(),
valueButtons: [{
text: getBtnLabel(specialNormal),
startValue: specialNormal[0],
endValue: specialNormal[1]
}, {
text: getBtnLabel(specialLong, 'over maxSpan'),
startValue: specialLong[0],
endValue: specialLong[1]
}, {
text: getBtnLabel(specialShort, 'over minSpan'),
startValue: specialShort[0],
endValue: specialShort[1]
}, {
}]
};
ctx.chart = testHelper.create(echarts, 'main2', {
option: option,
title: [
'(category axis) dispatchAction: {type: "dataZoom"}',
'range should not out of bound',
'minSpan: 5, maxSpan: 30'
],
height: 200,
buttons: makeButtons(ctx)
});
</script>
<script>
var option;
var base = +new Date(1990, 9, 3);
var oneDay = 24 * 3600 * 1000;
var startValue = Math.random() * 300;
var data2 = [];
var specialNormal = [];
var specialShort = [];
var specialLong = [];
for (var i = 0; i < 100; i++) {
var now = new Date(base += oneDay);
value = Math.round((Math.random() - 0.5) * 20 + (!i ? startValue : data2[i - 1][1]));
data2.push([now, value]);
if (i === 30) {
specialNormal[0] = +now;
specialShort[0] = +now;
specialLong[0] = +now;
}
else if (i === 32) {
specialShort[1] = +now;
}
else if (i === 38) {
specialNormal[1] = +now;
}
else if (i === 70) {
specialLong[1] = +now;
}
}
});
var option = {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
}
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
xAxis: [{
type: 'time'
}],
yAxis: [{
}],
dataZoom: [{
type: 'inside',
id: 'dz-in',
maxSpan: 30,
minSpan: 5,
start: 0,
end: 10
}, {
id: 'dz-s',
maxSpan: 30,
minSpan: 5,
start: 0,
end: 10
}],
series: [{
type:'line',
symbolSize: 10,
data: data2
}]
};
function fmt2Str(dt) {
return echarts.format.formatTime('yyyy-MM-dd', +dt)
}
function getBtnLabel(special, extra) {
extra = extra || '';
return 'startValue: "' + fmt2Str(special[0]) + '"('
+ special[0] + '), endValue: "' + fmt2Str(special[1])
+ '"(' + special[1] + ') ' + extra;
}
var ctx = {
hint: 'time axis value should be integer',
percentButttons: getDefaultPercentButtons(),
valueButtons: [{
text: getBtnLabel(specialNormal),
startValue: fmt2Str(specialNormal[0]),
endValue: fmt2Str(specialNormal[1])
}, {
text: getBtnLabel(specialShort, 'over minSpan'),
startValue: fmt2Str(specialShort[0]),
endValue: fmt2Str(specialShort[1])
}, {
text: getBtnLabel(specialLong, 'over maxSpan'),
startValue: fmt2Str(specialLong[0]),
endValue: fmt2Str(specialLong[1])
}]
};
ctx.chart = testHelper.create(echarts, 'main3', {
option: option,
title: [
'(time axis) dispatchAction: {type: "dataZoom"}',
'range should not out of bound',
'maxSpan: 30, minSpan: 5',
],
height: 200,
buttons: makeButtons(ctx)
});
</script>
<script>
var option;
var base = +new Date(1990, 9, 3);
var oneDay = 24 * 3600 * 1000;
var startValue = Math.random() * 300;
var data1 = [];
var category = []
for (var i = 0; i < 1000; i++) {
var now = new Date(base += oneDay);
category.push([now.getFullYear(), now.getMonth() + 1, now.getDate()].join('-'));
value = Math.round((Math.random() - 0.5) * 20 + (!i ? startValue : data1[i - 1]));
data1.push(value);
}
var option = {
tooltip: {
trigger: 'axis'
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
xAxis: [{
type: 'category',
boundaryGap: false,
data: category
}],
yAxis: [{
type: 'value',
boundaryGap: [0, '100%']
}],
dataZoom: [{
type: 'inside',
id: 'dz-in',
start: 0,
end: 10,
minValueSpan: 50,
maxValueSpan: 300,
xAxisIndex: 0
}, {
id: 'dz-s',
start: 0,
end: 10,
minValueSpan: 50,
maxValueSpan: 300,
xAxisIndex: 0
}],
series: [{
type:'line',
symbolSize: 10,
data: data1
}]
};
var ctx = {
hint: 'category axis value should be integer',
percentButttons: getDefaultPercentButtons(),
valueButtons: [{
text: 'startValue: 30, endValue: 500, over maxValueSpan',
startValue: 30,
endValue: 500
}, {
text: 'startValue: 30, endValue: 32, over minValueSpan',
startValue: 30,
endValue: 32
}]
};
ctx.chart = testHelper.create(echarts, 'main4', {
title: [
'(category axis)',
'dispatchAction: {type: "dataZoom"}',
'range should not out of bound',
'minValueSpan: 50, maxValueSpan: 300'
],
option: option,
height: 200,
buttons: makeButtons(ctx)
});
</script>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册