import * as echarts from 'echarts/lib/echarts'; import { getLayoutRect } from 'echarts/lib/util/layout'; import graphicGL from '../../util/graphicGL'; import ViewGL from '../../core/ViewGL'; import Lines2DGeometry from '../../util/geometry/Lines2D'; import retrieve from '../../util/retrieve'; import ForceAtlas2GPU from './ForceAtlas2GPU'; import ForceAtlas2 from './ForceAtlas2'; import requestAnimationFrame from 'zrender/lib/animation/requestAnimationFrame'; import glmatrix from 'claygl/src/dep/glmatrix'; import { getItemVisualColor, getItemVisualOpacity } from '../../util/visual'; var vec2 = glmatrix.vec2; import Roam2DControl from '../../util/Roam2DControl'; import PointsBuilder from '../common/PointsBuilder'; import lines2DGLSL from '../../util/shader/lines2D.glsl.js'; graphicGL.Shader.import(lines2DGLSL); var globalLayoutId = 1; export default echarts.ChartView.extend({ type: 'graphGL', __ecgl__: true, init: function (ecModel, api) { this.groupGL = new graphicGL.Node(); this.viewGL = new ViewGL('orthographic'); this.viewGL.camera.left = this.viewGL.camera.right = 0; this.viewGL.add(this.groupGL); this._pointsBuilder = new PointsBuilder(true, api); // Mesh used during force directed layout. this._forceEdgesMesh = new graphicGL.Mesh({ material: new graphicGL.Material({ shader: graphicGL.createShader('ecgl.forceAtlas2.edges'), transparent: true, depthMask: false, depthTest: false }), $ignorePicking: true, geometry: new graphicGL.Geometry({ attributes: { node: new graphicGL.Geometry.Attribute('node', 'float', 2), color: new graphicGL.Geometry.Attribute('color', 'float', 4, 'COLOR') }, dynamic: true, mainAttribute: 'node' }), renderOrder: -1, mode: graphicGL.Mesh.LINES }); // Mesh used after force directed layout. this._edgesMesh = new graphicGL.Mesh({ material: new graphicGL.Material({ shader: graphicGL.createShader('ecgl.meshLines2D'), transparent: true, depthMask: false, depthTest: false }), $ignorePicking: true, geometry: new Lines2DGeometry({ useNativeLine: false, dynamic: true }), renderOrder: -1, culling: false }); this._layoutId = 0; this._control = new Roam2DControl({ zr: api.getZr(), viewGL: this.viewGL }); this._control.setTarget(this.groupGL); this._control.init(); this._clickHandler = this._clickHandler.bind(this); }, render: function (seriesModel, ecModel, api) { this.groupGL.add(this._pointsBuilder.rootNode); this._model = seriesModel; this._api = api; this._initLayout(seriesModel, ecModel, api); this._pointsBuilder.update(seriesModel, ecModel, api); if (!(this._forceLayoutInstance instanceof ForceAtlas2GPU)) { this.groupGL.remove(this._forceEdgesMesh); } this._updateCamera(seriesModel, api); this._control.off('update'); this._control.on('update', function () { api.dispatchAction({ type: 'graphGLRoam', seriesId: seriesModel.id, zoom: this._control.getZoom(), offset: this._control.getOffset() }); this._pointsBuilder.updateView(this.viewGL.camera); }, this); this._control.setZoom(retrieve.firstNotNull(seriesModel.get('zoom'), 1)); this._control.setOffset(seriesModel.get('offset') || [0, 0]); var mesh = this._pointsBuilder.getPointsMesh(); mesh.off('mousemove', this._mousemoveHandler); mesh.off('mouseout', this._mouseOutHandler, this); api.getZr().off('click', this._clickHandler); this._pointsBuilder.highlightOnMouseover = true; if (seriesModel.get('focusNodeAdjacency')) { var focusNodeAdjacencyOn = seriesModel.get('focusNodeAdjacencyOn'); if (focusNodeAdjacencyOn === 'click') { // Remove default emphasis effect api.getZr().on('click', this._clickHandler); } else if (focusNodeAdjacencyOn === 'mouseover') { mesh.on('mousemove', this._mousemoveHandler, this); mesh.on('mouseout', this._mouseOutHandler, this); this._pointsBuilder.highlightOnMouseover = false; } else { if (process.env.NODE_ENV !== 'production') { console.warn('Unkown focusNodeAdjacencyOn value \s' + focusNodeAdjacencyOn); } } } // Reset this._lastMouseOverDataIndex = -1; }, _clickHandler: function (e) { if (this._layouting) { return; } var dataIndex = this._pointsBuilder.getPointsMesh().dataIndex; if (dataIndex >= 0) { this._api.dispatchAction({ type: 'graphGLFocusNodeAdjacency', seriesId: this._model.id, dataIndex: dataIndex }); } else { this._api.dispatchAction({ type: 'graphGLUnfocusNodeAdjacency', seriesId: this._model.id }); } }, _mousemoveHandler: function (e) { if (this._layouting) { return; } var dataIndex = this._pointsBuilder.getPointsMesh().dataIndex; if (dataIndex >= 0) { if (dataIndex !== this._lastMouseOverDataIndex) { this._api.dispatchAction({ type: 'graphGLFocusNodeAdjacency', seriesId: this._model.id, dataIndex: dataIndex }); } } else { this._mouseOutHandler(e); } this._lastMouseOverDataIndex = dataIndex; }, _mouseOutHandler: function (e) { if (this._layouting) { return; } this._api.dispatchAction({ type: 'graphGLUnfocusNodeAdjacency', seriesId: this._model.id }); this._lastMouseOverDataIndex = -1; }, _updateForceEdgesGeometry: function (edges, seriesModel) { var geometry = this._forceEdgesMesh.geometry; var edgeData = seriesModel.getEdgeData(); var offset = 0; var layoutInstance = this._forceLayoutInstance; var vertexCount = edgeData.count() * 2; geometry.attributes.node.init(vertexCount); geometry.attributes.color.init(vertexCount); edgeData.each(function (idx) { var edge = edges[idx]; geometry.attributes.node.set(offset, layoutInstance.getNodeUV(edge.node1)); geometry.attributes.node.set(offset + 1, layoutInstance.getNodeUV(edge.node2)); var color = getItemVisualColor(edgeData, edge.dataIndex); var colorArr = graphicGL.parseColor(color); colorArr[3] *= retrieve.firstNotNull(getItemVisualOpacity(edgeData, edge.dataIndex), 1); geometry.attributes.color.set(offset, colorArr); geometry.attributes.color.set(offset + 1, colorArr); offset += 2; }); geometry.dirty(); }, _updateMeshLinesGeometry: function () { var edgeData = this._model.getEdgeData(); var geometry = this._edgesMesh.geometry; var edgeData = this._model.getEdgeData(); var points = this._model.getData().getLayout('points'); geometry.resetOffset(); geometry.setVertexCount(edgeData.count() * geometry.getLineVertexCount()); geometry.setTriangleCount(edgeData.count() * geometry.getLineTriangleCount()); var p0 = []; var p1 = []; var lineWidthQuery = ['lineStyle', 'width']; this._originalEdgeColors = new Float32Array(edgeData.count() * 4); this._edgeIndicesMap = new Float32Array(edgeData.count()); edgeData.each(function (idx) { var edge = edgeData.graph.getEdgeByIndex(idx); var idx1 = edge.node1.dataIndex * 2; var idx2 = edge.node2.dataIndex * 2; p0[0] = points[idx1]; p0[1] = points[idx1 + 1]; p1[0] = points[idx2]; p1[1] = points[idx2 + 1]; var color = getItemVisualColor(edgeData, edge.dataIndex); var colorArr = graphicGL.parseColor(color); colorArr[3] *= retrieve.firstNotNull(getItemVisualOpacity(edgeData, edge.dataIndex), 1); var itemModel = edgeData.getItemModel(edge.dataIndex); var lineWidth = retrieve.firstNotNull(itemModel.get(lineWidthQuery), 1) * this._api.getDevicePixelRatio(); geometry.addLine(p0, p1, colorArr, lineWidth); for (var k = 0; k < 4; k++) { this._originalEdgeColors[edge.dataIndex * 4 + k] = colorArr[k]; } this._edgeIndicesMap[edge.dataIndex] = idx; }, this); geometry.dirty(); }, _updateForceNodesGeometry: function (nodeData) { var pointsMesh = this._pointsBuilder.getPointsMesh(); var pos = []; for (var i = 0; i < nodeData.count(); i++) { this._forceLayoutInstance.getNodeUV(i, pos); pointsMesh.geometry.attributes.position.set(i, pos); } pointsMesh.geometry.dirty('position'); }, _initLayout: function (seriesModel, ecModel, api) { var layout = seriesModel.get('layout'); var graph = seriesModel.getGraph(); var boxLayoutOption = seriesModel.getBoxLayoutParams(); var viewport = getLayoutRect(boxLayoutOption, { width: api.getWidth(), height: api.getHeight() }); if (layout === 'force') { if (process.env.NODE_ENV !== 'production') { console.warn('Currently only forceAtlas2 layout supported.'); } layout = 'forceAtlas2'; } // Stop previous layout this.stopLayout(seriesModel, ecModel, api, { beforeLayout: true }); var nodeData = seriesModel.getData(); var edgeData = seriesModel.getData(); if (layout === 'forceAtlas2') { var layoutModel = seriesModel.getModel('forceAtlas2'); var layoutInstance = this._forceLayoutInstance; var nodes = []; var edges = []; var nodeDataExtent = nodeData.getDataExtent('value'); var edgeDataExtent = edgeData.getDataExtent('value'); var edgeWeightRange = retrieve.firstNotNull(layoutModel.get('edgeWeight'), 1.0); var nodeWeightRange = retrieve.firstNotNull(layoutModel.get('nodeWeight'), 1.0); if (typeof edgeWeightRange === 'number') { edgeWeightRange = [edgeWeightRange, edgeWeightRange]; } if (typeof nodeWeightRange === 'number') { nodeWeightRange = [nodeWeightRange, nodeWeightRange]; } var offset = 0; var nodesIndicesMap = {}; var layoutPoints = new Float32Array(nodeData.count() * 2); graph.eachNode(function (node) { var dataIndex = node.dataIndex; var value = nodeData.get('value', dataIndex); var x; var y; if (nodeData.hasItemOption) { var itemModel = nodeData.getItemModel(dataIndex); x = itemModel.get('x'); y = itemModel.get('y'); } if (x == null) { // Random in rectangle x = viewport.x + Math.random() * viewport.width; y = viewport.y + Math.random() * viewport.height; } layoutPoints[offset * 2] = x; layoutPoints[offset * 2 + 1] = y; nodesIndicesMap[node.id] = offset++; var mass = echarts.number.linearMap(value, nodeDataExtent, nodeWeightRange); if (isNaN(mass)) { if (!isNaN(nodeWeightRange[0])) { mass = nodeWeightRange[0]; } else { mass = 1; } } nodes.push({ x: x, y: y, mass: mass, size: nodeData.getItemVisual(dataIndex, 'symbolSize') }); }); nodeData.setLayout('points', layoutPoints); graph.eachEdge(function (edge) { var dataIndex = edge.dataIndex; var value = nodeData.get('value', dataIndex); var weight = echarts.number.linearMap(value, edgeDataExtent, edgeWeightRange); if (isNaN(weight)) { if (!isNaN(edgeWeightRange[0])) { weight = edgeWeightRange[0]; } else { weight = 1; } } edges.push({ node1: nodesIndicesMap[edge.node1.id], node2: nodesIndicesMap[edge.node2.id], weight: weight, dataIndex: dataIndex }); }); if (!layoutInstance) { var isGPU = layoutModel.get('GPU'); if (this._forceLayoutInstance) { if (isGPU && !(this._forceLayoutInstance instanceof ForceAtlas2GPU) || !isGPU && !(this._forceLayoutInstance instanceof ForceAtlas2)) { // Mark to dispose this._forceLayoutInstanceToDispose = this._forceLayoutInstance; } } layoutInstance = this._forceLayoutInstance = isGPU ? new ForceAtlas2GPU() : new ForceAtlas2(); } layoutInstance.initData(nodes, edges); layoutInstance.updateOption(layoutModel.option); // Update lines geometry after first layout; this._updateForceEdgesGeometry(layoutInstance.getEdges(), seriesModel); this._updatePositionTexture(); api.dispatchAction({ type: 'graphGLStartLayout', from: this.uid }); } else { var layoutPoints = new Float32Array(nodeData.count() * 2); var offset = 0; graph.eachNode(function (node) { var dataIndex = node.dataIndex; var x; var y; if (nodeData.hasItemOption) { var itemModel = nodeData.getItemModel(dataIndex); x = itemModel.get('x'); y = itemModel.get('y'); } layoutPoints[offset++] = x; layoutPoints[offset++] = y; }); nodeData.setLayout('points', layoutPoints); this._updateAfterLayout(seriesModel, ecModel, api); } }, _updatePositionTexture: function () { var positionTex = this._forceLayoutInstance.getNodePositionTexture(); this._pointsBuilder.setPositionTexture(positionTex); this._forceEdgesMesh.material.set('positionTex', positionTex); }, startLayout: function (seriesModel, ecModel, api, payload) { if (payload && payload.from != null && payload.from !== this.uid) { return; } var viewGL = this.viewGL; var api = this._api; var layoutInstance = this._forceLayoutInstance; var data = this._model.getData(); var layoutModel = this._model.getModel('forceAtlas2'); if (!layoutInstance) { if (process.env.NODE_ENV !== 'production') { console.error('None layout don\'t have startLayout action'); } return; } this.groupGL.remove(this._edgesMesh); this.groupGL.add(this._forceEdgesMesh); if (!this._forceLayoutInstance) { return; } this._updateForceNodesGeometry(seriesModel.getData()); this._pointsBuilder.hideLabels(); var self = this; var layoutId = this._layoutId = globalLayoutId++; var maxSteps = layoutModel.getShallow('maxSteps'); var steps = layoutModel.getShallow('steps'); var stepsCount = 0; var syncStepCount = Math.max(steps * 2, 20); var doLayout = function (layoutId) { if (layoutId !== self._layoutId) { return; } if (layoutInstance.isFinished(maxSteps)) { api.dispatchAction({ type: 'graphGLStopLayout', from: self.uid }); api.dispatchAction({ type: 'graphGLFinishLayout', points: data.getLayout('points'), from: self.uid }); return; } layoutInstance.update(viewGL.layer.renderer, steps, function () { self._updatePositionTexture(); // PENDING Performance. stepsCount += steps; // Sync posiiton every 20 steps. if (stepsCount >= syncStepCount) { self._syncNodePosition(seriesModel); stepsCount = 0; } // Position texture will been swapped. set every time. api.getZr().refresh(); requestAnimationFrame(function () { doLayout(layoutId); }); }); }; requestAnimationFrame(function () { if (self._forceLayoutInstanceToDispose) { self._forceLayoutInstanceToDispose.dispose(viewGL.layer.renderer); self._forceLayoutInstanceToDispose = null; } doLayout(layoutId); }); this._layouting = true; }, stopLayout: function (seriesModel, ecModel, api, payload) { if (payload && payload.from != null && payload.from !== this.uid) { return; } this._layoutId = 0; this.groupGL.remove(this._forceEdgesMesh); this.groupGL.add(this._edgesMesh); if (!this._forceLayoutInstance) { return; } if (!this.viewGL.layer) { return; } if (!(payload && payload.beforeLayout)) { this._syncNodePosition(seriesModel); this._updateAfterLayout(seriesModel, ecModel, api); } this._api.getZr().refresh(); this._layouting = false; }, _syncNodePosition: function (seriesModel) { var points = this._forceLayoutInstance.getNodePosition(this.viewGL.layer.renderer); seriesModel.getData().setLayout('points', points); seriesModel.setNodePosition(points); }, _updateAfterLayout: function (seriesModel, ecModel, api) { this._updateMeshLinesGeometry(); this._pointsBuilder.removePositionTexture(); this._pointsBuilder.updateLayout(seriesModel, ecModel, api); this._pointsBuilder.updateView(this.viewGL.camera); this._pointsBuilder.updateLabels(); this._pointsBuilder.showLabels(); }, focusNodeAdjacency: function (seriesModel, ecModel, api, payload) { var data = this._model.getData(); this._downplayAll(); var dataIndex = payload.dataIndex; var graph = data.graph; var focusNodes = []; var node = graph.getNodeByIndex(dataIndex); focusNodes.push(node); node.edges.forEach(function (edge) { if (edge.dataIndex < 0) { return; } edge.node1 !== node && focusNodes.push(edge.node1); edge.node2 !== node && focusNodes.push(edge.node2); }, this); this._pointsBuilder.fadeOutAll(0.05); this._fadeOutEdgesAll(0.05); focusNodes.forEach(function (node) { this._pointsBuilder.highlight(data, node.dataIndex); }, this); this._pointsBuilder.updateLabels(focusNodes.map(function (node) { return node.dataIndex; })); var focusEdges = []; node.edges.forEach(function (edge) { if (edge.dataIndex >= 0) { this._highlightEdge(edge.dataIndex); focusEdges.push(edge); } }, this); this._focusNodes = focusNodes; this._focusEdges = focusEdges; }, unfocusNodeAdjacency: function (seriesModel, ecModel, api, payload) { this._downplayAll(); this._pointsBuilder.fadeInAll(); this._fadeInEdgesAll(); this._pointsBuilder.updateLabels(); }, _highlightEdge: function (dataIndex) { var itemModel = this._model.getEdgeData().getItemModel(dataIndex); var emphasisColor = graphicGL.parseColor(itemModel.get('emphasis.lineStyle.color') || itemModel.get('lineStyle.color')); var emphasisOpacity = retrieve.firstNotNull(itemModel.get('emphasis.lineStyle.opacity'), itemModel.get('lineStyle.opacity'), 1); emphasisColor[3] *= emphasisOpacity; this._edgesMesh.geometry.setItemColor(this._edgeIndicesMap[dataIndex], emphasisColor); }, _downplayAll: function () { if (this._focusNodes) { this._focusNodes.forEach(function (node) { this._pointsBuilder.downplay(this._model.getData(), node.dataIndex); }, this); } if (this._focusEdges) { this._focusEdges.forEach(function (edge) { this._downplayEdge(edge.dataIndex); }, this); } }, _downplayEdge: function (dataIndex) { var color = this._getColor(dataIndex, []); this._edgesMesh.geometry.setItemColor(this._edgeIndicesMap[dataIndex], color); }, _setEdgeFade: function () { var color = []; return function (dataIndex, percent) { this._getColor(dataIndex, color); color[3] *= percent; this._edgesMesh.geometry.setItemColor(this._edgeIndicesMap[dataIndex], color); }; }(), _getColor: function (dataIndex, out) { for (var i = 0; i < 4; i++) { out[i] = this._originalEdgeColors[dataIndex * 4 + i]; } return out; }, _fadeOutEdgesAll: function (percent) { var graph = this._model.getData().graph; graph.eachEdge(function (edge) { this._setEdgeFade(edge.dataIndex, percent); }, this); }, _fadeInEdgesAll: function () { this._fadeOutEdgesAll(1); }, _updateCamera: function (seriesModel, api) { this.viewGL.setViewport(0, 0, api.getWidth(), api.getHeight(), api.getDevicePixelRatio()); var camera = this.viewGL.camera; var nodeData = seriesModel.getData(); var points = nodeData.getLayout('points'); var min = vec2.create(Infinity, Infinity); var max = vec2.create(-Infinity, -Infinity); var pt = []; for (var i = 0; i < points.length;) { pt[0] = points[i++]; pt[1] = points[i++]; vec2.min(min, min, pt); vec2.max(max, max, pt); } var cy = (max[1] + min[1]) / 2; var cx = (max[0] + min[0]) / 2; // Only fit the camera when graph is not in the center. // PENDING if (cx > camera.left && cx < camera.right && cy < camera.bottom && cy > camera.top) { return; } // Scale a bit var width = Math.max(max[0] - min[0], 10); // Keep aspect var height = width / api.getWidth() * api.getHeight(); width *= 1.4; height *= 1.4; min[0] -= width * 0.2; camera.left = min[0]; camera.top = cy - height / 2; camera.bottom = cy + height / 2; camera.right = width + min[0]; camera.near = 0; camera.far = 100; }, dispose: function () { var renderer = this.viewGL.layer.renderer; if (this._forceLayoutInstance) { this._forceLayoutInstance.dispose(renderer); } this.groupGL.removeAll(); // Stop layout. this._layoutId = -1; this._pointsBuilder.dispose(); }, remove: function () { this.groupGL.removeAll(); this._control.dispose(); } });