From e3b6dd3ff72f6a30fc04b469bfed1678cb18f4bb Mon Sep 17 00:00:00 2001 From: pah100 Date: Wed, 18 Nov 2015 21:56:14 +0800 Subject: [PATCH] add parallel --- src/chart/parallel.js | 6 + src/chart/parallel/ParallelSeries.js | 71 +++++ src/chart/parallel/ParallelView.js | 200 ++++++++++++++ src/component/axis/AxisBuilder.js | 367 +++++++++++++++++++++++++ src/component/axis/ParallelAxisView.js | 36 +++ src/component/parallel.js | 15 + src/component/parallelAxis.js | 6 + src/coord/parallel/AxisModel.js | 58 ++++ src/coord/parallel/Parallel.js | 227 +++++++++++++++ src/coord/parallel/ParallelAxis.js | 43 +++ src/coord/parallel/ParallelModel.js | 127 +++++++++ src/coord/parallel/parallelCreator.js | 35 +++ src/data/List.js | 51 +++- src/preprocessor/parallel.js | 60 ++++ test/parallel-aqi.html | 154 +++++++++++ 15 files changed, 1448 insertions(+), 8 deletions(-) create mode 100644 src/chart/parallel.js create mode 100644 src/chart/parallel/ParallelSeries.js create mode 100644 src/chart/parallel/ParallelView.js create mode 100644 src/component/axis/AxisBuilder.js create mode 100644 src/component/axis/ParallelAxisView.js create mode 100644 src/component/parallel.js create mode 100644 src/component/parallelAxis.js create mode 100644 src/coord/parallel/AxisModel.js create mode 100644 src/coord/parallel/Parallel.js create mode 100644 src/coord/parallel/ParallelAxis.js create mode 100644 src/coord/parallel/ParallelModel.js create mode 100644 src/coord/parallel/parallelCreator.js create mode 100644 src/preprocessor/parallel.js create mode 100644 test/parallel-aqi.html diff --git a/src/chart/parallel.js b/src/chart/parallel.js new file mode 100644 index 000000000..788dd297e --- /dev/null +++ b/src/chart/parallel.js @@ -0,0 +1,6 @@ +define(function (require) { + + require('./parallel/ParallelSeries'); + require('./parallel/ParallelView'); + +}); \ No newline at end of file diff --git a/src/chart/parallel/ParallelSeries.js b/src/chart/parallel/ParallelSeries.js new file mode 100644 index 000000000..f910e1293 --- /dev/null +++ b/src/chart/parallel/ParallelSeries.js @@ -0,0 +1,71 @@ +define(function(require) { + + var createListFromArray = require('../helper/createListFromArray'); + var SeriesModel = require('../../model/Series'); + + return SeriesModel.extend({ + + type: 'series.parallel', + + dependencies: ['parallel'], + + getInitialData: function (option, ecModel) { + return createListFromArray(option.data, this, ecModel); + }, + + defaultOption: { + zlevel: 0, // 一级层叠 + z: 2, // 二级层叠 + + coordinateSystem: 'parallel', + parallelIndex: 0, + + label: { + normal: { + show: false + // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 + // position: 默认自适应,水平布局为'top',垂直布局为'right',可选为 + // 'inside'|'left'|'right'|'top'|'bottom' + // textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE + }, + emphasis: { + show: false + // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 + // position: 默认自适应,水平布局为'top',垂直布局为'right',可选为 + // 'inside'|'left'|'right'|'top'|'bottom' + // textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE + } + }, + itemStyle: { + normal: { + // color: 各异 + }, + emphasis: { + // color: 各异, + } + }, + lineStyle: { + normal: { + width: 2, + type: 'solid', + shadowColor: 'rgba(0,0,0,0)', //默认透明 + shadowBlur: 0, + shadowOffsetX: 0, + shadowOffsetY: 0 + } + }, + // areaStyle: { + + // }, + // smooth: false, + // 拐点图形类型 + symbol: 'emptyCircle', + // 拐点图形大小 + symbolSize: 4, + // 拐点图形旋转控制 + // symbolRotate: null, + // 标志图形默认只有主轴显示(随主轴标签间隔隐藏策略) + showAllSymbol: false + } + }); +}); \ No newline at end of file diff --git a/src/chart/parallel/ParallelView.js b/src/chart/parallel/ParallelView.js new file mode 100644 index 000000000..06224face --- /dev/null +++ b/src/chart/parallel/ParallelView.js @@ -0,0 +1,200 @@ +define(function (require) { + + var graphic = require('../../util/graphic'); + var zrUtil = require('zrender/core/util'); + + var ParallelView = require('../../view/Chart').extend({ + + type: 'pie', + + init: function () { + this._dataGroup = new graphic.Group(); + }, + + render: function (seriesModel, ecModel, api, payload) { + + var data = seriesModel.getData(); + var oldData = this._data; + var dataGroup = this._dataGroup; + var group = this.group; + var coordSys = seriesModel.coordinateSystem; + var dimensions = coordSys.dimensions; + + var hasAnimation = ecModel.get('animation'); + var isFirstRender = !oldData; + + var lineStyleModel = seriesModel.getModel('lineStyle.normal'); + var lineStyle = lineStyleModel.getLineStyle(); + + // var onSectorClick = zrUtil.curry( + // updateDataSelected, this.uid, seriesModel, hasAnimation, api + // ); + + // var selectedMode = seriesModel.get('selectedMode'); + + data.diff(oldData) + .add(function (dataIndex) { + var values = data.getValues(dimensions, dataIndex); + + var els = createEls( + dataGroup, values, dimensions, coordSys, + lineStyle, hasAnimation && !isFirstRender + ); + + data.setItemGraphicEl(dataIndex, els); + }) + .update(function (newDataIndex, oldDataIndex) { + // var els = oldData.getItemGraphicEl(oldDataIndex); + // var values = data.getValues(dimensions, newDataIndex); + + // updateEls( + // els, dataGroup, values, dimensions, coordSys, lineStyle + // ); + + // api.updateGraphicEl(sector, { + // points: layout + // }); + // api.updateGraphicEl(labelLine, { + // shape: { + // points: labelLayout.linePoints + // } + // }); + // api.updateGraphicEl(labelText, { + // style: { + // x: labelLayout.x, + // y: labelLayout.y + // }, + // rotation: labelLayout.rotation + // }); + + // // Set none animating style + // labelText.setStyle({ + // textAlign: labelLayout.textAlign, + // textBaseline: labelLayout.textBaseline, + // textFont: labelLayout.font + // }); + + // sectorGroup.add(sector); + // data.setItemGraphicEl(newDataIndex, sector); + + // group.add(labelLine); + // group.add(labelText); + }) + .remove(function (idx) { + // var sector = oldData.getItemGraphicEl(idx); + // sectorGroup.remove(sector); + }) + .execute(); + + // Make sure data els is on top of labels + group.remove(dataGroup); + group.add(dataGroup); + + this._updateAll(data, seriesModel); + + this._data = data; + }, + + _updateAll: function (data, seriesModel, hasAnimation) { + // var selectedOffset = seriesModel.get('selectedOffset'); + // data.eachItemGraphicEl(function (sector, idx) { + // var itemModel = data.getItemModel(idx); + // var itemStyleModel = itemModel.getModel('itemStyle'); + // var visualColor = data.getItemVisual(idx, 'color'); + + // sector.setStyle( + // zrUtil.extend( + // { + // fill: visualColor + // }, + // itemStyleModel.getModel('normal').getItemStyle() + // ) + // ); + // graphic.setHoverStyle( + // sector, + // itemStyleModel.getModel('emphasis').getItemStyle() + // ); + + // // Set label style + // var labelText = sector.__labelText; + // var labelLine = sector.__labelLine; + // var labelModel = itemModel.getModel('label.normal'); + // var textStyleModel = labelModel.getModel('textStyle'); + // var labelPosition = labelModel.get('position'); + // var isLabelInside = labelPosition === 'inside'; + // labelText.setStyle({ + // fill: textStyleModel.get('color') + // || isLabelInside ? '#fff' : visualColor, + // text: seriesModel.getFormattedLabel(idx, 'normal') + // || data.getName(idx), + // textFont: textStyleModel.getFont() + // }); + // labelText.attr('ignore', !labelModel.get('show')); + // // Default use item visual color + // labelLine.attr('ignore', !itemModel.get('labelLine.show')); + // labelLine.setStyle({ + // stroke: visualColor + // }); + // labelLine.setStyle(itemModel.getModel('labelLine').getLineStyle()); + + // toggleItemSelected( + // sector, + // data.getItemLayout(idx), + // itemModel.get('selected'), + // selectedOffset, + // hasAnimation + // ); + // }); + }, + + dispose: function () {} + }); + + function createEls(dataGroup, values, dimensions, coordSys, lineStyle) { + // FIXME + // init animation + + var els = []; + for (var i = 0, len = dimensions.length - 1; i < len; i++) { + var points = [ + coordSys.dataToPoint(values[i], dimensions[i]), + coordSys.dataToPoint(values[i + 1], dimensions[i + 1]) + ]; + dataGroup.add(els[i] = new graphic.Polyline({ + points: points, + style: lineStyle + })); + } + return els; + } + + // function updateEls(els, values, dimensions, coordSys) { + // // FIXME + // // update animation + + // var els = []; + // for (var i = 0, len = dimensions.length - 1; i < len; i++) { + // var points = [ + // coordSys.dataToPoint(values[i], dimensions[i]), + // coordSys.dataToPoint(values[i + 1], dimensions[i + 1]) + // ]; + // dataGroup.add(els[i] = new graphic.Polyline({ + // points: points, + // style: lineStyle + // })); + // api.updateGraphicEl(sector, { + // points: layout + // }); + // } + + + // return els; + // } + + + function getElProp() { + + } + + return ParallelView; +}); \ No newline at end of file diff --git a/src/component/axis/AxisBuilder.js b/src/component/axis/AxisBuilder.js new file mode 100644 index 000000000..a24e92ca7 --- /dev/null +++ b/src/component/axis/AxisBuilder.js @@ -0,0 +1,367 @@ +define(function (require) { + + var zrUtil = require('zrender/core/util'); + var graphic = require('../../util/graphic'); + + var EPSILON = 1e-4; + var PI2 = Math.PI * 2; + var PI = Math.PI; + + /** + * @param {module:zrender/container/Group} group + * @param {Object} axisModel + * @param {Object} opt Standard axis parameters. + * @param {Array.} opt.position [x, y] + * @param {number} opt.rotation by radian + * @param {number} opt.tickDirection 1 or -1 + * @param {number} opt.labelDirection 1 or -1 + * @param {string} [opt.axisName] default get from axisModel. + * @param {number} [opt.lableRotation] by degree, default get from axisModel. + * @param {number} [opt.lableInterval] Default label interval when label + * interval from model is null or 'auto'. + * @param {number} [opt.isCartesian=false] + * @param {number} [opt.z2=0] + */ + var AxisBuilder = function (axisModel, opt) { + + /** + * @readOnly + */ + this.opt = opt; + + /** + * @readOnly + */ + this.axisModel = axisModel; + + /** + * @readOnly + */ + this.group = new graphic.Group({ + position: opt.position.slice(), + rotation: opt.rotation, + z2: opt.z2 + }); + }; + + AxisBuilder.prototype = { + + constructor: AxisBuilder, + + hasBuilder: function (name) { + return !!builders[name]; + }, + + add: function (name) { + builders[name].call(this); + }, + + getGroup: function () { + return this.group; + }, + + /** + * @inner + */ + _getExtent: function () { + var opt = this.opt; + var extent = this.axisModel.axis.getExtent(); + + opt.offset = 0; + + // FIXME + // 修正axisExtent不统一 + if (opt.isCartesian) { + var min = Math.min(extent[0], extent[1]); + var max = Math.max(extent[0], extent[1]); + opt.offset = min; + extent = [0, max - opt.offset]; + } + + return extent; + } + + }; + + var builders = { + + /** + * @private + */ + axisLine: function () { + var axisModel = this.axisModel; + var extent = this._getExtent(); + + this.group.add(new graphic.Line({ + shape: { + x1: extent[0], + y1: 0, + x2: extent[1], + y2: 0 + }, + style: zrUtil.extend( + {lineCap: 'round'}, + axisModel.getModel('axisLine.lineStyle').getLineStyle() + ), + silent: true, + z2: 1 + })); + }, + + /** + * @private + */ + axisTick: function () { + var axisModel = this.axisModel; + var axis = axisModel.axis; + var tickModel = axisModel.getModel('axisTick'); + var opt = this.opt; + + var lineStyleModel = tickModel.getModel('lineStyle'); + var tickLen = tickModel.get('length'); + var tickInterval = getInterval(tickModel, opt); + var ticksCoords = axis.getTicksCoords(); + var tickLines = []; + + for (var i = 0; i < ticksCoords.length; i++) { + // Only ordinal scale support tick interval + if (ifIgnoreOnTick(axis, i, tickInterval)) { + // ??? 检查 计算正确?(因为offset) + continue; + } + + var tickCoord = ticksCoords[i] - opt.offset; + + // Tick line + tickLines.push(new graphic.Line(graphic.subPixelOptimizeLine({ + shape: { + x1: tickCoord, + y1: 0, + x2: tickCoord, + y2: opt.tickDirection * tickLen + }, + style: { + lineWidth: lineStyleModel.get('width') + }, + silent: true + }))); + } + + this.group.add(graphic.mergePath(tickLines, { + style: lineStyleModel.getLineStyle(), + silent: true + })); + }, + + /** + * @param {module:echarts/coord/cartesian/AxisModel} axisModel + * @param {module:echarts/coord/cartesian/GridModel} gridModel + * @private + */ + axisLabel: function () { + var opt = this.opt; + var axisModel = this.axisModel; + var axis = axisModel.axis; + var labelModel = axisModel.getModel('axisLabel'); + var textStyleModel = labelModel.getModel('textStyle'); + var labelMargin = labelModel.get('margin'); + var ticks = axis.scale.getTicks(); + var labels = axisModel.formatLabels(axis.scale.getTicksLabels()); + + // Special label rotate. + var labelRotation = opt.labelRotation; + if (labelRotation == null) { + labelRotation = labelModel.get('rotate') || 0; + } + // To radian. + labelRotation = labelRotation * PI / 180; + + var labelLayout = innerTextLayout(opt, labelRotation); + + for (var i = 0; i < ticks.length; i++) { + if (ifIgnoreOnTick(axis, i, opt.labelInterval)) { + continue; + } + + var tickCoord = axis.dataToCoord(ticks[i]) - opt.offset; + var pos = [tickCoord, opt.labelDirection * labelMargin]; + + this.group.add(new graphic.Text({ + style: { + x: pos[0], + y: pos[1], + text: labels[i], + textAlign: labelLayout.textAlign, + textBaseline: labelLayout.textBaseline, + textFont: textStyleModel.getFont(), + fill: textStyleModel.get('color') + }, + rotation: labelLayout.rotation, + origin: pos, + silent: true + })); + } + }, + + /** + * @private + */ + axisName: function () { + var opt = this.opt; + var axisModel = this.axisModel; + + var name = this.opt.name; + // If name is '', do not get name from axisMode. + if (name == null) { + name = axisModel.get('name'); + } + + if (!name) { + return; + } + + var nameLocation = axisModel.get('nameLocation'); + var textStyleModel = axisModel.getModel('nameTextStyle'); + var gap = axisModel.get('nameGap') || 0; + + var position = opt.position; + var extent = this._getExtent(); + var textX = nameLocation == 'start' + ? position[0] - gap + : position[0] + extent[1] + gap; + var textY = position[1]; + + var labelLayout; + + if (nameLocation === 'middle') { + labelLayout = innerTextLayout(opt, opt.rotation); + } + else { + labelLayout = endTextLayout(opt, nameLocation); + } + + this.group.add(new graphic.Text({ + style: { + text: name, + textFont: textStyleModel.getFont(), + fill: textStyleModel.get('color') + || axisModel.get('axisLine.lineStyle.color'), + textAlign: labelLayout.textAlign, + textBaseline: labelLayout.textBaseline + }, + position: [textX, textY], + silent: true, + z2: 1 + })); + } + + }; + + /** + * @inner + */ + function innerTextLayout(opt, textRotation) { + var labelDirection = opt.labelDirection; + var rotationDiff = remRadian(textRotation - opt.rotation); + var textAlign; + var textBaseline; + + if (isAroundZero(rotationDiff)) { // Label is parallel with axis line. + textBaseline = labelDirection > 0 ? 'top' : 'bottom'; + textAlign = 'center'; + } + else if (isAroundZero(rotationDiff - PI)) { // Label is inverse parallel with axis line. + textBaseline = labelDirection > 0 ? 'bottom' : 'top'; + textAlign = 'center'; + } + else { + textBaseline = 'middle'; + + if (rotationDiff > 0 && rotationDiff < PI) { + textAlign = labelDirection > 0 ? 'right' : 'left'; + } + else { + textAlign = labelDirection > 0 ? 'left' : 'right'; + } + } + + return { + rotation: rotationDiff, + textAlign: textAlign, + textBaseline: textBaseline + }; + } + + /** + * @inner + */ + function endTextLayout(opt, textPosition) { + var rotationDiff = remRadian(-opt.rotation); + var textAlign; + var textBaseline; + + if (isAroundZero(rotationDiff - PI / 2)) { + textBaseline = textPosition === 'start' ? 'top' : 'bottom'; + textAlign = 'center'; + } + else if (isAroundZero(rotationDiff - PI * 1.5)) { + textBaseline = textPosition === 'start' ? 'bottom' : 'top'; + textAlign = 'center'; + } + else { + textBaseline = 'middle'; + + if (rotationDiff < PI * 1.5 && rotationDiff > PI / 2) { + textAlign = textPosition === 'start' ? 'left' : 'right'; + } + else { + textAlign = textPosition === 'start' ? 'right' : 'left'; + } + } + + return { + rotation: rotationDiff, + textAlign: textAlign, + textBaseline: textBaseline + }; + } + + /** + * @inner + */ + function ifIgnoreOnTick(axis, i, interval) { + return axis.scale.type === 'ordinal' + && (typeof interval === 'function') + && !interval(i, axis.scale.getItem(i)) + || i % (interval + 1); + } + + /** + * @inner + */ + function getInterval(model, opt) { + var interval = model.get('interval'); + if (interval == null || interval == 'auto') { + interval = opt.labelInterval; + } + return interval; + } + + /** + * @inner + */ + function isAroundZero(val) { + return val > -EPSILON && val < EPSILON; + } + + /** + * @inner + */ + function remRadian(radian) { + // To 0 - 2 * PI, considering negative radian. + return (radian % PI2 + PI2) % PI2; + } + + return AxisBuilder; + +}); \ No newline at end of file diff --git a/src/component/axis/ParallelAxisView.js b/src/component/axis/ParallelAxisView.js new file mode 100644 index 000000000..326c92ea3 --- /dev/null +++ b/src/component/axis/ParallelAxisView.js @@ -0,0 +1,36 @@ +define(function (require) { + + var zrUtil = require('zrender/core/util'); + var AxisBuilder = require('./AxisBuilder'); + + var elementList = ['axisLine', 'axisLabel', 'axisTick', 'axisName']; + + var AxisView = require('../../echarts').extendComponentView({ + + type: 'parallelAxis', + + render: function (axisModel, ecModel) { + + this.group.removeAll(); + + if (!axisModel.get('show')) { + return; + } + + var coordSys = ecModel.getComponent('parallel', axisModel.get('parallelIndex')); + + var axisBuilder = new AxisBuilder(coordSys.getAxisLayout(axisModel.axis.dim)); + + zrUtil.each(elementList, function (name) { + if (axisModel.get(name +'.show')) { + axisBuilder.add(name); + } + }, this); + + this.group.add(axisBuilder.getGroup()); + } + + }); + + return AxisView; +}); \ No newline at end of file diff --git a/src/component/parallel.js b/src/component/parallel.js new file mode 100644 index 000000000..93fa447d8 --- /dev/null +++ b/src/component/parallel.js @@ -0,0 +1,15 @@ +define(function(require) { + + require('../coord/parallel/parallelCreator'); + require('./parallelAxis'); + + var echarts = require('../echarts'); + + // Parallel view + echarts.extendComponentView({ + type: 'parallel' + }); + + echarts.registerPreprocessor(require('../preprocessor/parallel')); + +}); \ No newline at end of file diff --git a/src/component/parallelAxis.js b/src/component/parallelAxis.js new file mode 100644 index 000000000..aa2647639 --- /dev/null +++ b/src/component/parallelAxis.js @@ -0,0 +1,6 @@ +define(function(require) { + + require('../coord/parallel/parallelCreator'); + + require('./axis/ParallelAxisView'); +}); \ No newline at end of file diff --git a/src/coord/parallel/AxisModel.js b/src/coord/parallel/AxisModel.js new file mode 100644 index 000000000..83bdaa042 --- /dev/null +++ b/src/coord/parallel/AxisModel.js @@ -0,0 +1,58 @@ +define(function(require) { + + var ComponentModel = require('../../model/Component'); + var defaultOption = require('../axisDefault'); + var zrUtil = require('zrender/core/util'); + + function mergeDefault(axisOption, ecModel) { + var axisType = axisOption.type + 'Axis'; + + zrUtil.merge(axisOption, ecModel.get(axisType)); + zrUtil.merge(axisOption, ecModel.getTheme().get(axisType)); + zrUtil.merge(axisOption, defaultOption[axisType]); + } + + var AxisModel = ComponentModel.extend({ + + type: 'parallelAxis', + + /** + * @type {module:echarts/coord/parallel/Axis} + */ + axis: null, + + defaultOption: { + + type: 'value', + + parallelIndex: null + }, + + init: function (axisOption, parentModel, ecModel) { + + zrUtil.merge(axisOption, this.getDefaultOption(), false); + + mergeDefault(axisOption, ecModel); + }, + + /** + * @public + * @param {boolean} needs Whether axis needs cross zero. + */ + setNeedsCrossZero: function (needs) { + this.option.scale = !needs; + }, + + /** + * @public + */ + setParallelIndex: function (parallelIndex) { + this.option.parallelIndex = parallelIndex; + } + + }); + + zrUtil.merge(AxisModel.prototype, require('../axisModelCommonMixin')); + + return AxisModel; +}); \ No newline at end of file diff --git a/src/coord/parallel/Parallel.js b/src/coord/parallel/Parallel.js new file mode 100644 index 000000000..8b6fe5783 --- /dev/null +++ b/src/coord/parallel/Parallel.js @@ -0,0 +1,227 @@ +/** + * Parallel Coordinates + * + */ +define(function(require) { + + var layout = require('../../util/layout'); + var axisHelper = require('../../coord/axisHelper'); + var zrUtil = require('zrender/core/util'); + var ParallelAxis = require('./ParallelAxis'); + var matrix = require('zrender/core/matrix'); + var vector = require('zrender/core/vector'); + + var each = zrUtil.each; + + var PI = Math.PI; + + function Parallel(parallelModel, ecModel, api) { + + /** + * key: dimension + * @type {Object.} + * @private + */ + this._axesMap = {}; + + /** + * key: dimension + * value: {position: [], rotation, } + * @type {Object.} + * @private + */ + this._axesLayout = {}; + + /** + * Always follow axis order. + * @type {Array.} + * @readOnly + */ + this.dimensions = parallelModel.get('dimensions'); + + /** + * @type {module:zrender/core/BoundingRect} + */ + this._rect; + + this._init(parallelModel, ecModel, api); + + this.resize(parallelModel, api); + } + + Parallel.prototype = { + + type: 'parallel', + + constructor: Parallel, + + /** + * Initialize cartesian coordinate systems + * @private + */ + _init: function (parallelModel, ecModel, api) { + + var dimensions = parallelModel.get('dimensions'); + var axisIndices = parallelModel.get('parallelAxisIndex'); + + each(axisIndices, function (axisIndex, idx) { + + var axisModel = ecModel.getComponent('parallelAxis', axisIndex); + var parallelIndex = axisModel.get('parallelIndex'); + + if (ecModel.getComponent('parallel', parallelIndex) !== parallelModel) { + // FIXME + // api.log('Axis should not be shared among coordinate systems!'); + return; + } + + var dim = dimensions[idx]; + + var axis = this._axesMap[dim] = new ParallelAxis( + dim, + axisHelper.createScaleByModel(axisModel), + [0, 0], + axisModel.get('type') + ); + + var isCategory = axis.type === 'category'; + axis.onBand = isCategory && axisModel.get('boundaryGap'); + axis.inverse = axisModel.get('inverse'); + + // Inject axis into axisModel + axisModel.axis = axis; + + // Inject axisModel into axis + axis.model = axisModel; + + axisHelper.niceScaleExtent(axis, axisModel); + + }, this); + }, + + /** + * Resize the parallel coordinate system. + * @param {module:echarts/coord/parallel/ParallelModel} parallelModel + * @param {module:echarts/ExtensionAPI} api + */ + resize: function (parallelModel, api) { + this._rect = layout.parsePositionInfo( + { + x: parallelModel.get('x'), + y: parallelModel.get('y'), + x2: parallelModel.get('x2'), + y2: parallelModel.get('y2'), + width: parallelModel.get('width'), + height: parallelModel.get('height') + }, + { + width: api.getWidth(), + height: api.getHeight() + } + ); + + this._layoutAxes(parallelModel); + }, + + /** + * @private + */ + _layoutAxes: function (parallelModel) { + var rect = this._rect; + var layout = parallelModel.get('layout'); + var axes = this._axesMap; + var dimensions = this.dimensions; + + var size = [rect.width, rect.height]; + var sizeIdx = layout === 'horizontal' ? 0 : 1; + var layoutLength = size[sizeIdx]; + var axisLength = size[1 - sizeIdx]; + + var axisExtent = [0, axisLength]; + each(axes, function (axis) { + var idx = axis.inverse ? 1 : 0; + axis.setExtent(axisExtent[idx], axisExtent[1 - idx]); + }); + + each(dimensions, function (dim, idx) { + var axis = axes[dim]; + var pos = layoutLength * idx / (dimensions.length - 1); + var inverse = axis.inverse ? 'inverse' : 'forward'; + + var positionTable = { + horizontal: { + x: pos, + y: {forward: layoutLength, inverse: 0} + }, + vertical: { + x: {forward: 0, inverse: layoutLength}, + y: pos + } + }; + var rotationTable = { + horizontal: {forward: PI / 2, inverse: PI * 1.5}, + vertical: {forward: 0, inverse: PI} + }; + + var position = [ + positionTable[layout]['x'][inverse] + rect.x, + positionTable[layout]['y'][inverse] + rect.y + ]; + + var rotation = rotationTable[layout][inverse]; + var transform = matrix.create(); + matrix.rotate(transform, transform, rotation); + matrix.translate(transform, transform, position); + + // TODO + // tick等排布信息。 + + // TODO + // 根据axis order 更新 dimensions顺序。 + + this._axesLayout[dim] = { + position: position, + rotation: rotation, + transform: transform + }; + }, this); + }, + + /** + * Convert a dim value of a single item of series data to Point. + * @param {*} value + * @param {string} dim + * @return {Array} + */ + dataToPoint: function (value, dim) { + return this.axisCoordToPoint( + this._axesMap[dim].dataToCoord(value), + dim + ); + }, + + /** + * Convert coords of each axis to Point. + * Return point. For example: [10, 20] + * @param {Array.} coords + * @param {string} dim + * @return {Array.} + */ + axisCoordToPoint: function (coord, dim) { + var axisLayout = this._axesLayout[dim]; + var point = [0, coord]; + vector.applyTransform(point, point, axisLayout.transform); + return point; + }, + + /** + * Get axis layout. + */ + getAxisLayout: function (dim) { + return zrUtil.clone(this._axesLayout[dim], true); + } + + }; + + return Parallel; +}); \ No newline at end of file diff --git a/src/coord/parallel/ParallelAxis.js b/src/coord/parallel/ParallelAxis.js new file mode 100644 index 000000000..b340a500d --- /dev/null +++ b/src/coord/parallel/ParallelAxis.js @@ -0,0 +1,43 @@ +define(function (require) { + + var zrUtil = require('zrender/core/util'); + var Axis = require('../Axis'); + + /** + * @constructor module:echarts/coord/parallel/ParallelAxis + * @extends {module:echarts/coord/Axis} + * @param {string} dim + * @param {*} scale + * @param {Array.} coordExtent + * @param {string} axisType + */ + var ParallelAxis = function (dim, scale, coordExtent, axisType) { + + Axis.call(this, dim, scale, coordExtent); + + /** + * Axis type + * - 'category' + * - 'value' + * - 'time' + * - 'log' + * @type {string} + */ + this.type = axisType || 'value'; + }; + + ParallelAxis.prototype = { + + constructor: ParallelAxis, + + /** + * Axis model + * @param {module:echarts/coord/parallel/AxisModel} + */ + model: null + }; + + zrUtil.inherits(ParallelAxis, Axis); + + return ParallelAxis; +}); \ No newline at end of file diff --git a/src/coord/parallel/ParallelModel.js b/src/coord/parallel/ParallelModel.js new file mode 100644 index 000000000..eda4506c1 --- /dev/null +++ b/src/coord/parallel/ParallelModel.js @@ -0,0 +1,127 @@ +define(function(require) { + + var zrUtil = require('zrender/core/util'); + + require('./AxisModel'); + + require('../../echarts').extendComponentModel({ + + type: 'parallel', + + /** + * @type {module:echarts/coord/parallel/Parallel} + */ + coordinateSystem: null, + + defaultOption: { + show: false, + + dimensions: 5, // {number} 表示 dim数,如设为 3 会自动转化成 ['dim0', 'dim1', 'dim2'] + // {Array.} 表示哪些dim,如 ['dim3', 'dim2'] + parallelAxisIndex: null, // {Array.} 表示引用哪些axis,如 [2, 1, 4] + // {Object} 表示 mapping,如{dim1: 3, dim3: 1, others: 0},others不设则自动取0 + + zlevel: 0, // 一级层叠 + z: 0, // 二级层叠 + x: 80, + y: 60, + x2: 80, + y2: 60, + + layout: 'horizontal', // 'horizontal' or 'vertical' + + // width: {totalWidth} - x - x2, + // height: {totalHeight} - y - y2, + backgroundColor: 'rgba(0,0,0,0)', + borderWidth: 0, + borderColor: '#ccc' + }, + + /** + * @override + */ + mergeOption: function (newOption) { + var thisOption = this.option; + + newOption && zrUtil.merge(thisOption, newOption); + + var parallelAxisIndex = thisOption.parallelAxisIndex; + var dimensions = thisOption.dimensions; + + dimensions = completeDimensions(dimensions); + parallelAxisIndex = completeParallelAxisIndexByMapping( + parallelAxisIndex, dimensions + ); + parallelAxisIndex = completeParallelAxisIndexWhenNone( + parallelAxisIndex, dimensions + ); + + thisOption.dimensions = dimensions; + thisOption.parallelAxisIndex = parallelAxisIndex; + } + + }); + + function completeDimensions(dimensions) { + // If dimensions is not array, represents dimension count. + // Generate dimensions by dimension count. + + if (!zrUtil.isArray(dimensions)) { + var count = dimensions; + dimensions = []; + for (var i = 0; i < count; i++) { + dimensions.push('dim' + i); + } + } + + return dimensions; + } + + function completeParallelAxisIndexByMapping(parallelAxisIndex, dimensions) { + // If parallelAxisIndex is {}, represents mapping. + // like: {dim1: 3, dim3: 1, others: 0} + // Generate parallelAxisIndex by mapping. + + if (zrUtil.isObject(parallelAxisIndex) + && !zrUtil.isArray(parallelAxisIndex) + ) { + var mapping = parallelAxisIndex; + parallelAxisIndex = []; + + var otherAxisIndex = 0; // Others default 0. + zrUtil.each(mapping, function (axisIndex, dim) { + var dimIndex = zrUtil.indexOf(dimensions, dim); + if (dimIndex >= 0) { + parallelAxisIndex[dimIndex] = dim; + } + else if (dim === 'others') { + otherAxisIndex = axisIndex; + } + }); + + // Complete others. + zrUtil.each(parallelAxisIndex, function (axisIndex, idx) { + if (axisIndex == null) { + parallelAxisIndex[idx] = otherAxisIndex; + } + }); + } + + return parallelAxisIndex; + } + + function completeParallelAxisIndexWhenNone(parallelAxisIndex, dimensions) { + if (!zrUtil.isObject(parallelAxisIndex) + || !zrUtil.isArray(parallelAxisIndex) + ) { + parallelAxisIndex = []; + } + + if (parallelAxisIndex.length !== dimensions.length) { + // TODO + } + + return parallelAxisIndex; + } + +}); \ No newline at end of file diff --git a/src/coord/parallel/parallelCreator.js b/src/coord/parallel/parallelCreator.js new file mode 100644 index 000000000..d93371dd0 --- /dev/null +++ b/src/coord/parallel/parallelCreator.js @@ -0,0 +1,35 @@ +/** + * Parallel coordinate system creater. + */ +define(function(require) { + + var Parallel = require('./Parallel'); + + function create(ecModel, api) { + var coordSysList = []; + + ecModel.eachComponent('parallel', function (parallelModel, idx) { + var coordSys = new Parallel(parallelModel, ecModel, api); + + coordSys.name = 'parallel_' + idx; + coordSys.resize(parallelModel, api); + + parallelModel.coordinateSystem = coordSys; + + coordSysList.push(coordSys); + }); + + // Inject the coordinateSystems into seriesModel + ecModel.eachSeries(function (seriesModel) { + if (seriesModel.get('coordinateSystem') === 'parallel') { + var parallelIndex = seriesModel.get('parallelIndex'); + seriesModel.coordinateSystem = coordSysList[parallelIndex]; + } + }); + + return coordSysList; + } + + require('../../CoordinateSystem').register('parallel', {create: create}); + +}); \ No newline at end of file diff --git a/src/data/List.js b/src/data/List.js index d6f0fcd9c..03b0ab296 100644 --- a/src/data/List.js +++ b/src/data/List.js @@ -12,10 +12,10 @@ define(function (require) { ? Array : global.Int32Array; var dataCtors = { - float: Float32Array, - int: Int32Array, + 'float': Float32Array, + 'int': Int32Array, // Ordinal data type can be string or int - ordinal: Array, + 'ordinal': Array, 'number': Array }; @@ -346,6 +346,29 @@ define(function (require) { return value; }; + /** + * Get value for multi dimensions. + * @param {Array.} [dimensions] If ignored, using all dimensions. + * @param {number} idx + * @param {boolean} stack + * @return {number} + */ + listProto.getValues = function (dimensions, idx, stack) { + var values = []; + + if (!zrUtil.isArray(dimensions)) { + stack = idx; + idx = dimensions; + dimensions = this.dimensions; + } + + for (var i = 0, len = dimensions.length; i < len; i++) { + values.push(this.get(dimensions[i], idx, stack)); + } + + return values; + }; + /** * If value is NaN. Inlcuding '-' * @param {string} dim @@ -869,20 +892,32 @@ define(function (require) { }; /** * @param {number} idx - * @param {module:zrender/Element} el + * @param {module:zrender/Element|Array.} el */ listProto.setItemGraphicEl = function (idx, el) { var hostModel = this.hostModel; + + if (zrUtil.isArray(el)) { + zrUtil.each(el, function (singleEl) { + addIndexToGraphicEl(singleEl, idx, hostModel); + }); + } + else { + addIndexToGraphicEl(el, idx, hostModel); + } + + this._graphicEls[idx] = el; + }; + + function addIndexToGraphicEl(el, dataIndex, hostModel) { // Add data index and series index for indexing the data by element // Useful in tooltip - el.dataIndex = idx; + el.dataIndex = dataIndex; el.seriesIndex = hostModel && hostModel.seriesIndex; if (el.type === 'group') { el.traverse(setItemDataAndSeriesIndex, el); } - - this._graphicEls[idx] = el; - }; + } /** * @param {number} idx diff --git a/src/preprocessor/parallel.js b/src/preprocessor/parallel.js new file mode 100644 index 000000000..e74975e5c --- /dev/null +++ b/src/preprocessor/parallel.js @@ -0,0 +1,60 @@ +define(function (require) { + + var zrUtil = require('zrender/core/util'); + + // TODO + // + // parallelAxis: [ // 根据dim需求自动补全 parallelAxis,自动赋值已有axisoption。 + // // 找出所有引用axis的 parallel option,来决定如何补全。 + // // 但是这步在这里做比较难,在后面做比较麻烦。 + // { + // axisLine: [], + // axisLabel: [] + // } + // ], + // parallel: [ // 如果没有写parallel则自动创建。FIXME 是不是应该强制用户写,自动创建埋bug? + // { + // dimensions: 3 // number表示 count, 根据dimensionCount创建 []。 + // ['dim1', 'dim3'], // + // + // parallelAxisIndex: [3, 1], // TODO 如果不设置则根据parallelAxisMap创建此项, + // // 如果没有parallelAxisMap则顺序引用。 + // // TODO 如果设置了 parallelAxisIndex 则此项无效。 + // { // 根据parallelAxisMap创建 [] + // dim1: 3 + // dim3: 1 + // others: 0 // 不配other也取parallelAxis[0]。 + // } + // } + // ], + // series: [ + // { + // parallelIndex: 0, // 缺省则0 + // data: [ + // [22, 23, 34, 6, 19], + // [22, 23, 34, 6, 19] + // ] + // } + // ] + + return function (option) { + + // Create a parallel coordinate if not exists. + + if (option.parallel) { + return; + } + + var hasParallelSeries = false; + + zrUtil.each(option.series, function (seriesOpt) { + if (seriesOpt && seriesOpt.type === 'parallel') { + hasParallelSeries = true; + } + }); + + if (hasParallelSeries) { + option.parallel = [{}]; + } + }; +}); \ No newline at end of file diff --git a/test/parallel-aqi.html b/test/parallel-aqi.html new file mode 100644 index 000000000..7edfd3bd7 --- /dev/null +++ b/test/parallel-aqi.html @@ -0,0 +1,154 @@ + + + + + + + + + +
+ + + + \ No newline at end of file -- GitLab