716 lines
21 KiB
JavaScript
716 lines
21 KiB
JavaScript
|
|
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();
|
||
|
|
}
|
||
|
|
});
|