UNPKG

light-chart

Version:

Charts for mobile visualization.

850 lines (759 loc) 21 kB
const Base = require('../base'); const Plot = require('./plot'); const Util = require('../util/common'); const Coord = require('../coord/index'); const Geom = require('../geom/base'); const ScaleController = require('./controller/scale'); const AxisController = require('./controller/axis'); const Global = require('../global'); const { Canvas } = require('../graphic/index'); const Helper = require('../util/helper'); function isFullCircle(coord) { const startAngle = coord.startAngle; const endAngle = coord.endAngle; if (!Util.isNil(startAngle) && !Util.isNil(endAngle) && (endAngle - startAngle) < Math.PI * 2) { return false; } return true; } function compare(a, b) { return a - b; } function _isScaleExist(scales, compareScale) { let flag = false; Util.each(scales, scale => { const scaleValues = [].concat(scale.values); const compareScaleValues = [].concat(compareScale.values); if (scale.type === compareScale.type && scale.field === compareScale.field && scaleValues.sort(compare).toString() === compareScaleValues.sort(compare).toString()) { flag = true; return; } }); return flag; } class Chart extends Base { static initPlugins() { return { _plugins: [], _cacheId: 0, register(plugins) { const p = this._plugins; ([]).concat(plugins).forEach(plugin => { if (p.indexOf(plugin) === -1) { p.push(plugin); } }); this._cacheId++; }, unregister(plugins) { const p = this._plugins; ([]).concat(plugins).forEach(plugin => { const idx = p.indexOf(plugin); if (idx !== -1) { p.splice(idx, 1); } }); this._cacheId++; }, clear() { this._plugins = []; this._cacheId++; }, count() { return this._plugins.length; }, getAll() { return this._plugins; }, notify(chart, hook, args) { const descriptors = this.descriptors(chart); const ilen = descriptors.length; let i; let descriptor; let plugin; let params; let method; for (i = 0; i < ilen; ++i) { descriptor = descriptors[i]; plugin = descriptor.plugin; method = plugin[hook]; if (typeof method === 'function') { params = [ chart ].concat(args || []); if (method.apply(plugin, params) === false) { return false; } } } return true; }, descriptors(chart) { const cache = chart._plugins || (chart._plugins = {}); if (cache.id === this._cacheId) { return cache.descriptors; } const plugins = []; const descriptors = []; this._plugins.concat((chart && chart.get('plugins')) || []).forEach(plugin => { const idx = plugins.indexOf(plugin); if (idx !== -1) { return; } plugins.push(plugin); descriptors.push({ plugin }); }); cache.descriptors = descriptors; cache.id = this._cacheId; return descriptors; } }; } getDefaultCfg() { return { /** * the id of canvas * @type {String} */ id: null, /** * padding * @type {Array|Number} */ padding: Global.padding, /** * data * @type {Array} */ data: null, /** * scales of chart * @type {Object} */ scales: {}, /** * @private * geometry instances * @type {Array} */ geoms: null, /** * scale configuration * @type {Object} */ colDefs: null, pixelRatio: Global.pixelRatio, /** * filter options * @type {Object} */ filters: null, appendPadding: Global.appendPadding }; } _syncYScales() { const geoms = this.get('geoms'); const syncScales = []; let min = []; let max = []; Util.each(geoms, geom => { const yScale = geom.getYScale(); if (yScale.isLinear) { syncScales.push(yScale); min.push(yScale.min); max.push(yScale.max); } }); min = Math.min.apply(null, min); max = Math.max.apply(null, max); Util.each(syncScales, scale => { scale.change({ min }); scale.change({ max }); }); } _getFieldsForLegend() { const fields = []; const geoms = this.get('geoms'); Util.each(geoms, geom => { const attrOptions = geom.get('attrOptions'); const attrCfg = attrOptions.color; if (attrCfg && attrCfg.field && Util.isString(attrCfg.field)) { const arr = attrCfg.field.split('*'); Util.each(arr, item => { if (fields.indexOf(item) === -1) { fields.push(item); } }); } }); return fields; } _createScale(field, data) { const scaleController = this.get('scaleController'); return scaleController.createScale(field, data); } _adjustScale() { const self = this; const coord = self.get('coord'); const xScale = self.getXScale(); const yScales = self.getYScales(); let scales = []; xScale && scales.push(xScale); scales = scales.concat(yScales); const inFullCircle = coord.isPolar && isFullCircle(coord); const scaleController = self.get('scaleController'); const colDefs = scaleController.defs; Util.each(scales, function(scale) { if ((scale.isCategory || scale.isIdentity) && scale.values && !(colDefs[scale.field] && colDefs[scale.field].range)) { const count = scale.values.length; let range; if (count === 1) { range = [ 0.5, 1 ]; } else { let widthRatio = 1; let offset = 0; if (inFullCircle) { if (!coord.transposed) { range = [ 0, 1 - 1 / count ]; } else { widthRatio = Global.widthRatio.multiplePie; offset = 1 / count * widthRatio; range = [ offset / 2, 1 - offset / 2 ]; } } else { offset = 1 / count * 1 / 2; range = [ offset, 1 - offset ]; } } scale.range = range; } }); const geoms = this.get('geoms'); for (let i = 0; i < geoms.length; i++) { const geom = geoms[i]; if (geom.get('type') === 'interval') { const yScale = geom.getYScale(); const { field, min, max, type } = yScale; if (!(colDefs[field] && colDefs[field].min) && type !== 'time') { if (min > 0) { yScale.change({ min: 0 }); } else if (max <= 0) { yScale.change({ max: 0 }); } } } } } _removeGeoms() { const geoms = this.get('geoms'); while (geoms.length > 0) { const geom = geoms.shift(); geom.destroy(); } } _clearGeoms() { const geoms = this.get('geoms'); for (let i = 0, length = geoms.length; i < length; i++) { const geom = geoms[i]; geom.clear(); } } _clearInner() { this.set('scales', {}); this.set('legendItems', null); this._clearGeoms(); Chart.plugins.notify(this, 'clearInner'); this.get('axisController') && this.get('axisController').clear(); } _execFilter(data) { const filters = this.get('filters'); if (filters) { data = data.filter(function(obj) { let rst = true; Util.each(filters, function(fn, k) { if (fn) { rst = fn(obj[k], obj); if (!rst) { return false; } } }); return rst; }); } return data; } _initGeoms(geoms) { const coord = this.get('coord'); const data = this.get('filteredData'); const colDefs = this.get('colDefs'); for (let i = 0, length = geoms.length; i < length; i++) { const geom = geoms[i]; geom.set('data', data); geom.set('coord', coord); geom.set('colDefs', colDefs); geom.init(); } } _initCoord() { const plot = this.get('plotRange'); const coordCfg = Util.mix({ type: 'cartesian' }, this.get('coordCfg'), { plot }); const type = coordCfg.type; const C = Coord[Util.upperFirst(type)]; const coord = new C(coordCfg); this.set('coord', coord); } _initLayout() { let padding = this.get('_padding'); if (!padding) { padding = this.get('margin') || this.get('padding'); padding = Util.parsePadding(padding); } const top = padding[0] === 'auto' ? 0 : padding[0]; const right = padding[1] === 'auto' ? 0 : padding[1]; const bottom = padding[2] === 'auto' ? 0 : padding[2]; const left = padding[3] === 'auto' ? 0 : padding[3]; const width = this.get('width'); const height = this.get('height'); const plot = new Plot({ start: { x: left, y: top }, end: { x: width - right, y: height - bottom } }); this.set('plotRange', plot); this.set('plot', plot); } _initCanvas() { const self = this; try { const canvas = new Canvas({ el: self.get('el') || self.get('id'), context: self.get('context'), pixelRatio: self.get('pixelRatio'), width: self.get('width'), height: self.get('height'), fontFamily: Global.fontFamily }); self.set('canvas', canvas); self.set('width', canvas.get('width')); self.set('height', canvas.get('height')); } catch (error) { throw error; } Chart.plugins.notify(self, 'afterCanvasInit'); self._initLayout(); } _initLayers() { const canvas = this.get('canvas'); this.set('backPlot', canvas.addGroup()); this.set('middlePlot', canvas.addGroup({ zIndex: 10 })); this.set('frontPlot', canvas.addGroup({ zIndex: 20 })); } _init() { const self = this; self._initCanvas(); self._initLayers(); self.set('geoms', []); self.set('scaleController', new ScaleController()); self.set('axisController', new AxisController({ frontPlot: self.get('frontPlot').addGroup({ className: 'axisContainer' }), backPlot: self.get('backPlot').addGroup({ className: 'axisContainer' }), chart: self })); Chart.plugins.notify(self, 'init'); } constructor(cfg) { super(cfg); const self = this; Util.each(Geom, function(geomConstructor, className) { const methodName = Util.lowerFirst(className); self[methodName] = function(cfg) { const geom = new geomConstructor(cfg); self.addGeom(geom); return geom; }; }); self._init(); } /** * set data and some scale configuration * @chainable * @param {Array} data the dataset to visualize * @param {Object} colDefs the configuration for scales * @return {Chart} return the chart instance */ source(data, colDefs) { this.set('data', data); if (colDefs) { this.scale(colDefs); } return this; } scale(field, cfg) { const colDefs = this.get('colDefs') || {}; if (Util.isObject(field)) { Util.mix(colDefs, field); } else { colDefs[field] = cfg; } this.set('colDefs', colDefs); const scaleController = this.get('scaleController'); scaleController.defs = colDefs; return this; } /** * configure the axis * @chainable * @param {String|Boolean} field the field name of data * @param {Object} cfg configuration for axis * @return {Chart} return the chart instance */ axis(field, cfg) { const axisController = this.get('axisController'); if (!field) { axisController.axisCfg = null; } else { axisController.axisCfg = axisController.axisCfg || {}; axisController.axisCfg[field] = cfg; } return this; } /** * configure the coordinate * @chainable * @param {String} type set the type of coodinate * @param {Object} cfg configuration for coordinate * @return {Chart} return the chart instance */ coord(type, cfg) { let coordCfg; if (Util.isObject(type)) { coordCfg = type; } else { coordCfg = cfg || {}; coordCfg.type = type || 'cartesian'; } this.set('coordCfg', coordCfg); return this; } filter(field, condition) { const filters = this.get('filters') || {}; filters[field] = condition; this.set('filters', filters); } /** * render the chart * @chainable * @return {Chart} return the chart instance */ render() { const canvas = this.get('canvas'); const geoms = this.get('geoms'); const data = this.get('data') || []; const filteredData = this._execFilter(data); // filter data this.set('filteredData', filteredData); this._initCoord(); // initialization coordinate instance Chart.plugins.notify(this, 'beforeGeomInit'); this._initGeoms(geoms); // init all geometry instances this.get('syncY') && this._syncYScales(); this._adjustScale(); // do some adjust for data Chart.plugins.notify(this, 'beforeGeomDraw'); this._renderAxis(); const middlePlot = this.get('middlePlot'); if (this.get('limitInPlot') && !middlePlot.attr('clip')) { const coord = this.get('coord'); const clip = Helper.getClip(coord); clip.set('canvas', middlePlot.get('canvas')); middlePlot.attr('clip', clip); } for (let i = 0, length = geoms.length; i < length; i++) { const geom = geoms[i]; geom.paint(); } Chart.plugins.notify(this, 'afterGeomDraw'); canvas.sort(); this.get('frontPlot').sort(); Chart.plugins.notify(this, 'beforeCanvasDraw'); canvas.draw(); return this; } /** * clear the chart, include geometris and all the shapes * @chainable * @return {Chart} return the chart */ clear() { Chart.plugins.notify(this, 'clear'); this._removeGeoms(); this._clearInner(); this.set('filters', null); this.set('isUpdate', false); this.set('_padding', null); const canvas = this.get('canvas'); canvas.draw(); return this; } repaint() { this.set('isUpdate', true); Chart.plugins.notify(this, 'repaint'); this._clearInner(); this.render(); } changeData(data) { this.set('data', data); Chart.plugins.notify(this, 'changeData'); this.set('_padding', null); this.repaint(); } changeSize(width, height) { if (width) { this.set('width', width); } else { width = this.get('width'); } if (height) { this.set('height', height); } else { height = this.get('height'); } const canvas = this.get('canvas'); canvas.changeSize(width, height); this._initLayout(); this.repaint(); return this; } destroy() { this.clear(); const canvas = this.get('canvas'); canvas.destroy(); Chart.plugins.notify(this, 'afterCanvasDestroyed'); if (this._interactions) { Util.each(this._interactions, interaction => { interaction.destroy(); }); } super.destroy(); } /** * calculate dataset's position on canvas * @param {Object} record the dataset * @return {Object} return the position */ getPosition(record) { const self = this; const coord = self.get('coord'); const xScale = self.getXScale(); const yScale = self.getYScales()[0]; const xField = xScale.field; const x = xScale.scale(record[xField]); const yField = yScale.field; const y = yScale.scale(record[yField]); return coord.convertPoint({ x, y }); } /** * get the data item of the point * @param {Object} point canvas position * @return {Object} return the data item */ getRecord(point) { const self = this; const coord = self.get('coord'); const xScale = self.getXScale(); const yScale = self.getYScales()[0]; const invertPoint = coord.invertPoint(point); const record = {}; record[xScale.field] = xScale.invert(invertPoint.x); record[yScale.field] = yScale.invert(invertPoint.y); return record; } /** * get the dataset of the point * @param {Object} point canvas position * @return {Array} return the dataset **/ getSnapRecords(point) { const geom = this.get('geoms')[0]; let data = []; if (geom) { // need to judge data = geom.getSnapRecords(point); } return data; } /** * creat scale instances * @param {String} field field name of data * @return {Scale} return the scale */ createScale(field) { let data = this.get('data'); const filteredData = this.get('filteredData'); if (filteredData.length) { const legendFields = this._getFieldsForLegend(); if (legendFields.indexOf(field) === -1) { data = filteredData; } } const scales = this.get('scales'); if (!scales[field]) { scales[field] = this._createScale(field, data); } return scales[field]; } /** * @protected * add geometry instance to geoms * @param {Geom} geom geometry instance */ addGeom(geom) { const geoms = this.get('geoms'); const middlePlot = this.get('middlePlot'); geoms.push(geom); geom.set('chart', this); geom.set('container', middlePlot.addGroup()); } /** * get the scale of x axis * @return {Scale} return the scale */ getXScale() { const self = this; const geoms = self.get('geoms'); const xScale = geoms[0].getXScale(); return xScale; } /** * get the scale of y axis * @return {Array} return the scale */ getYScales() { const geoms = this.get('geoms'); const rst = []; Util.each(geoms, function(geom) { const yScale = geom.getYScale(); if (rst.indexOf(yScale) === -1) { rst.push(yScale); } }); return rst; } getLegendItems() { if (this.get('legendItems')) { return this.get('legendItems'); } const legendItems = {}; const scales = []; const geoms = this.get('geoms'); Util.each(geoms, geom => { const colorAttr = geom.getAttr('color'); if (colorAttr) { const scale = colorAttr.getScale('color'); if (scale.type !== 'identity' && !_isScaleExist(scales, scale)) { scales.push(scale); const field = scale.field; const ticks = scale.getTicks(); const items = []; Util.each(ticks, tick => { const text = tick.text; const name = text; const scaleValue = tick.value; const value = scale.invert(scaleValue); const color = colorAttr.mapping(value).join('') || Global.defaultColor; const marker = { fill: color, radius: 3, symbol: 'circle', stroke: '#fff' }; items.push({ name, // for display dataValue: value, // the origin value checked: true, marker }); }); legendItems[field] = items; } } }); this.set('legendItems', legendItems); return legendItems; } // register the plugins registerPlugins(plugins) { const self = this; let chartPlugins = self.get('plugins') || []; if (!Util.isArray(chartPlugins)) { chartPlugins = [ chartPlugins ]; } ([]).concat(plugins).forEach(plugin => { if (chartPlugins.indexOf(plugin) === -1) { plugin.init && plugin.init(self); // init chartPlugins.push(plugin); } }); Chart.plugins._cacheId++; self.set('plugins', chartPlugins); } _renderAxis() { const axisController = this.get('axisController'); const xScale = this.getXScale(); const yScales = this.getYScales(); const coord = this.get('coord'); Chart.plugins.notify(this, 'beforeRenderAxis'); axisController.createAxis(coord, xScale, yScales); } _isAutoPadding() { if (this.get('_padding')) { return false; } const padding = this.get('padding'); if (Util.isArray(padding)) { return padding.indexOf('auto') !== -1; } return padding === 'auto'; } _updateLayout(padding) { const width = this.get('width'); const height = this.get('height'); const start = { x: padding[3], y: padding[0] }; const end = { x: width - padding[1], y: height - padding[2] }; const plot = this.get('plot'); const coord = this.get('coord'); plot.reset(start, end); coord.reset(plot); } } Chart.plugins = Chart.initPlugins(); module.exports = Chart;