548 lines
16 KiB
JavaScript
548 lines
16 KiB
JavaScript
import * as echarts from 'echarts/lib/echarts';
|
|
import graphicGL from '../../util/graphicGL';
|
|
import spriteUtil from '../../util/sprite';
|
|
import PointsMesh from './PointsMesh';
|
|
import LabelsBuilder from '../../component/common/LabelsBuilder';
|
|
import Matrix4 from 'claygl/src/math/Matrix4';
|
|
import retrieve from '../../util/retrieve';
|
|
import { getItemVisualColor, getItemVisualOpacity } from '../../util/visual';
|
|
import { getVisualColor, getVisualOpacity } from '../../util/visual';
|
|
var SDF_RANGE = 20;
|
|
var Z_2D = -10;
|
|
|
|
function isSymbolSizeSame(a, b) {
|
|
return a && b && a[0] === b[0] && a[1] === b[1];
|
|
} // TODO gl_PointSize has max value.
|
|
|
|
|
|
function PointsBuilder(is2D, api) {
|
|
this.rootNode = new graphicGL.Node();
|
|
/**
|
|
* @type {boolean}
|
|
*/
|
|
|
|
this.is2D = is2D;
|
|
this._labelsBuilder = new LabelsBuilder(256, 256, api); // Give a large render order.
|
|
|
|
this._labelsBuilder.getMesh().renderOrder = 100;
|
|
this.rootNode.add(this._labelsBuilder.getMesh());
|
|
this._api = api;
|
|
this._spriteImageCanvas = document.createElement('canvas');
|
|
this._startDataIndex = 0;
|
|
this._endDataIndex = 0;
|
|
this._sizeScale = 1;
|
|
}
|
|
|
|
PointsBuilder.prototype = {
|
|
constructor: PointsBuilder,
|
|
|
|
/**
|
|
* If highlight on over
|
|
*/
|
|
highlightOnMouseover: true,
|
|
update: function (seriesModel, ecModel, api, start, end) {
|
|
// Swap barMesh
|
|
var tmp = this._prevMesh;
|
|
this._prevMesh = this._mesh;
|
|
this._mesh = tmp;
|
|
var data = seriesModel.getData();
|
|
|
|
if (start == null) {
|
|
start = 0;
|
|
}
|
|
|
|
if (end == null) {
|
|
end = data.count();
|
|
}
|
|
|
|
this._startDataIndex = start;
|
|
this._endDataIndex = end - 1;
|
|
|
|
if (!this._mesh) {
|
|
var material = this._prevMesh && this._prevMesh.material;
|
|
this._mesh = new PointsMesh({
|
|
// Render after axes
|
|
renderOrder: 10,
|
|
// FIXME
|
|
frustumCulling: false
|
|
});
|
|
|
|
if (material) {
|
|
this._mesh.material = material;
|
|
}
|
|
}
|
|
|
|
var material = this._mesh.material;
|
|
var geometry = this._mesh.geometry;
|
|
var attributes = geometry.attributes;
|
|
this.rootNode.remove(this._prevMesh);
|
|
this.rootNode.add(this._mesh);
|
|
|
|
this._setPositionTextureToMesh(this._mesh, this._positionTexture);
|
|
|
|
var symbolInfo = this._getSymbolInfo(seriesModel, start, end);
|
|
|
|
var dpr = api.getDevicePixelRatio(); // TODO image symbol
|
|
|
|
var itemStyle = seriesModel.getModel('itemStyle').getItemStyle();
|
|
var largeMode = seriesModel.get('large');
|
|
var pointSizeScale = 1;
|
|
|
|
if (symbolInfo.maxSize > 2) {
|
|
pointSizeScale = this._updateSymbolSprite(seriesModel, itemStyle, symbolInfo, dpr);
|
|
material.enableTexture('sprite');
|
|
} else {
|
|
material.disableTexture('sprite');
|
|
}
|
|
|
|
attributes.position.init(end - start);
|
|
var rgbaArr = [];
|
|
|
|
if (largeMode) {
|
|
material.undefine('VERTEX_SIZE');
|
|
material.undefine('VERTEX_COLOR');
|
|
var color = getVisualColor(data);
|
|
var opacity = getVisualOpacity(data);
|
|
graphicGL.parseColor(color, rgbaArr);
|
|
rgbaArr[3] *= opacity;
|
|
material.set({
|
|
color: rgbaArr,
|
|
'u_Size': symbolInfo.maxSize * this._sizeScale
|
|
});
|
|
} else {
|
|
material.set({
|
|
color: [1, 1, 1, 1]
|
|
});
|
|
material.define('VERTEX_SIZE');
|
|
material.define('VERTEX_COLOR');
|
|
attributes.size.init(end - start);
|
|
attributes.color.init(end - start);
|
|
this._originalOpacity = new Float32Array(end - start);
|
|
}
|
|
|
|
var points = data.getLayout('points');
|
|
var positionArr = attributes.position.value;
|
|
var hasTransparentPoint = false;
|
|
|
|
for (var i = 0; i < end - start; i++) {
|
|
var i3 = i * 3;
|
|
var i2 = i * 2;
|
|
|
|
if (this.is2D) {
|
|
positionArr[i3] = points[i2];
|
|
positionArr[i3 + 1] = points[i2 + 1];
|
|
positionArr[i3 + 2] = Z_2D;
|
|
} else {
|
|
positionArr[i3] = points[i3];
|
|
positionArr[i3 + 1] = points[i3 + 1];
|
|
positionArr[i3 + 2] = points[i3 + 2];
|
|
}
|
|
|
|
if (!largeMode) {
|
|
var color = getItemVisualColor(data, i);
|
|
var opacity = getItemVisualOpacity(data, i);
|
|
graphicGL.parseColor(color, rgbaArr);
|
|
rgbaArr[3] *= opacity;
|
|
attributes.color.set(i, rgbaArr);
|
|
|
|
if (rgbaArr[3] < 0.99) {
|
|
hasTransparentPoint = true;
|
|
}
|
|
|
|
var symbolSize = data.getItemVisual(i, 'symbolSize');
|
|
symbolSize = symbolSize instanceof Array ? Math.max(symbolSize[0], symbolSize[1]) : symbolSize; // NaN pointSize may have strange result.
|
|
|
|
if (isNaN(symbolSize)) {
|
|
symbolSize = 0;
|
|
} // Scale point size because canvas has margin.
|
|
|
|
|
|
attributes.size.value[i] = symbolSize * pointSizeScale * this._sizeScale; // Save the original opacity for recover from fadeIn.
|
|
|
|
this._originalOpacity[i] = rgbaArr[3];
|
|
}
|
|
}
|
|
|
|
this._mesh.sizeScale = pointSizeScale;
|
|
geometry.updateBoundingBox();
|
|
geometry.dirty(); // Update material.
|
|
|
|
this._updateMaterial(seriesModel, itemStyle);
|
|
|
|
var coordSys = seriesModel.coordinateSystem;
|
|
|
|
if (coordSys && coordSys.viewGL) {
|
|
var methodName = coordSys.viewGL.isLinearSpace() ? 'define' : 'undefine';
|
|
material[methodName]('fragment', 'SRGB_DECODE');
|
|
}
|
|
|
|
if (!largeMode) {
|
|
this._updateLabelBuilder(seriesModel, start, end);
|
|
}
|
|
|
|
this._updateHandler(seriesModel, ecModel, api);
|
|
|
|
this._updateAnimation(seriesModel);
|
|
|
|
this._api = api;
|
|
},
|
|
getPointsMesh: function () {
|
|
return this._mesh;
|
|
},
|
|
updateLabels: function (highlightDataIndices) {
|
|
this._labelsBuilder.updateLabels(highlightDataIndices);
|
|
},
|
|
hideLabels: function () {
|
|
this.rootNode.remove(this._labelsBuilder.getMesh());
|
|
},
|
|
showLabels: function () {
|
|
this.rootNode.add(this._labelsBuilder.getMesh());
|
|
},
|
|
dispose: function () {
|
|
this._labelsBuilder.dispose();
|
|
},
|
|
_updateSymbolSprite: function (seriesModel, itemStyle, symbolInfo, dpr) {
|
|
symbolInfo.maxSize = Math.min(symbolInfo.maxSize * 2, 200);
|
|
var symbolSize = [];
|
|
|
|
if (symbolInfo.aspect > 1) {
|
|
symbolSize[0] = symbolInfo.maxSize;
|
|
symbolSize[1] = symbolInfo.maxSize / symbolInfo.aspect;
|
|
} else {
|
|
symbolSize[1] = symbolInfo.maxSize;
|
|
symbolSize[0] = symbolInfo.maxSize * symbolInfo.aspect;
|
|
} // In case invalid data.
|
|
|
|
|
|
symbolSize[0] = symbolSize[0] || 1;
|
|
symbolSize[1] = symbolSize[1] || 1;
|
|
|
|
if (this._symbolType !== symbolInfo.type || !isSymbolSizeSame(this._symbolSize, symbolSize) || this._lineWidth !== itemStyle.lineWidth) {
|
|
spriteUtil.createSymbolSprite(symbolInfo.type, symbolSize, {
|
|
fill: '#fff',
|
|
lineWidth: itemStyle.lineWidth,
|
|
stroke: 'transparent',
|
|
shadowColor: 'transparent',
|
|
minMargin: Math.min(symbolSize[0] / 2, 10)
|
|
}, this._spriteImageCanvas);
|
|
spriteUtil.createSDFFromCanvas(this._spriteImageCanvas, Math.min(this._spriteImageCanvas.width, 32), SDF_RANGE, this._mesh.material.get('sprite').image);
|
|
this._symbolType = symbolInfo.type;
|
|
this._symbolSize = symbolSize;
|
|
this._lineWidth = itemStyle.lineWidth;
|
|
}
|
|
|
|
return this._spriteImageCanvas.width / symbolInfo.maxSize * dpr;
|
|
},
|
|
_updateMaterial: function (seriesModel, itemStyle) {
|
|
var blendFunc = seriesModel.get('blendMode') === 'lighter' ? graphicGL.additiveBlend : null;
|
|
var material = this._mesh.material;
|
|
material.blend = blendFunc;
|
|
material.set('lineWidth', itemStyle.lineWidth / SDF_RANGE);
|
|
var strokeColor = graphicGL.parseColor(itemStyle.stroke);
|
|
material.set('strokeColor', strokeColor); // Because of symbol texture, we always needs it be transparent.
|
|
|
|
material.transparent = true;
|
|
material.depthMask = false;
|
|
material.depthTest = !this.is2D;
|
|
material.sortVertices = !this.is2D;
|
|
},
|
|
_updateLabelBuilder: function (seriesModel, start, end) {
|
|
var data = seriesModel.getData();
|
|
var geometry = this._mesh.geometry;
|
|
var positionArr = geometry.attributes.position.value;
|
|
var start = this._startDataIndex;
|
|
var pointSizeScale = this._mesh.sizeScale;
|
|
|
|
this._labelsBuilder.updateData(data, start, end);
|
|
|
|
this._labelsBuilder.getLabelPosition = function (dataIndex, positionDesc, distance) {
|
|
var idx3 = (dataIndex - start) * 3;
|
|
return [positionArr[idx3], positionArr[idx3 + 1], positionArr[idx3 + 2]];
|
|
};
|
|
|
|
this._labelsBuilder.getLabelDistance = function (dataIndex, positionDesc, distance) {
|
|
var size = geometry.attributes.size.get(dataIndex - start) / pointSizeScale;
|
|
return size / 2 + distance;
|
|
};
|
|
|
|
this._labelsBuilder.updateLabels();
|
|
},
|
|
_updateAnimation: function (seriesModel) {
|
|
graphicGL.updateVertexAnimation([['prevPosition', 'position'], ['prevSize', 'size']], this._prevMesh, this._mesh, seriesModel);
|
|
},
|
|
_updateHandler: function (seriesModel, ecModel, api) {
|
|
var data = seriesModel.getData();
|
|
var pointsMesh = this._mesh;
|
|
var self = this;
|
|
var lastDataIndex = -1;
|
|
var isCartesian3D = seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'cartesian3D';
|
|
var grid3DModel;
|
|
|
|
if (isCartesian3D) {
|
|
grid3DModel = seriesModel.coordinateSystem.model;
|
|
}
|
|
|
|
pointsMesh.seriesIndex = seriesModel.seriesIndex;
|
|
pointsMesh.off('mousemove');
|
|
pointsMesh.off('mouseout');
|
|
pointsMesh.on('mousemove', function (e) {
|
|
var dataIndex = e.vertexIndex + self._startDataIndex;
|
|
|
|
if (dataIndex !== lastDataIndex) {
|
|
if (this.highlightOnMouseover) {
|
|
this.downplay(data, lastDataIndex);
|
|
this.highlight(data, dataIndex);
|
|
|
|
this._labelsBuilder.updateLabels([dataIndex]);
|
|
}
|
|
|
|
if (isCartesian3D) {
|
|
api.dispatchAction({
|
|
type: 'grid3DShowAxisPointer',
|
|
value: [data.get(seriesModel.coordDimToDataDim('x')[0], dataIndex), data.get(seriesModel.coordDimToDataDim('y')[0], dataIndex), data.get(seriesModel.coordDimToDataDim('z')[0], dataIndex)],
|
|
grid3DIndex: grid3DModel.componentIndex
|
|
});
|
|
}
|
|
}
|
|
|
|
pointsMesh.dataIndex = dataIndex;
|
|
lastDataIndex = dataIndex;
|
|
}, this);
|
|
pointsMesh.on('mouseout', function (e) {
|
|
var dataIndex = e.vertexIndex + self._startDataIndex;
|
|
|
|
if (this.highlightOnMouseover) {
|
|
this.downplay(data, dataIndex);
|
|
|
|
this._labelsBuilder.updateLabels();
|
|
}
|
|
|
|
lastDataIndex = -1;
|
|
pointsMesh.dataIndex = -1;
|
|
|
|
if (isCartesian3D) {
|
|
api.dispatchAction({
|
|
type: 'grid3DHideAxisPointer',
|
|
grid3DIndex: grid3DModel.componentIndex
|
|
});
|
|
}
|
|
}, this);
|
|
},
|
|
updateLayout: function (seriesModel, ecModel, api) {
|
|
var data = seriesModel.getData();
|
|
|
|
if (!this._mesh) {
|
|
return;
|
|
}
|
|
|
|
var positionArr = this._mesh.geometry.attributes.position.value;
|
|
var points = data.getLayout('points');
|
|
|
|
if (this.is2D) {
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
var i3 = i * 3;
|
|
var i2 = i * 2;
|
|
positionArr[i3] = points[i2];
|
|
positionArr[i3 + 1] = points[i2 + 1];
|
|
positionArr[i3 + 2] = Z_2D;
|
|
}
|
|
} else {
|
|
for (var i = 0; i < points.length; i++) {
|
|
positionArr[i] = points[i];
|
|
}
|
|
}
|
|
|
|
this._mesh.geometry.dirty();
|
|
|
|
api.getZr().refresh();
|
|
},
|
|
updateView: function (camera) {
|
|
if (!this._mesh) {
|
|
return;
|
|
}
|
|
|
|
var worldViewProjection = new Matrix4();
|
|
Matrix4.mul(worldViewProjection, camera.viewMatrix, this._mesh.worldTransform);
|
|
Matrix4.mul(worldViewProjection, camera.projectionMatrix, worldViewProjection);
|
|
|
|
this._mesh.updateNDCPosition(worldViewProjection, this.is2D, this._api);
|
|
},
|
|
highlight: function (data, dataIndex) {
|
|
if (dataIndex > this._endDataIndex || dataIndex < this._startDataIndex) {
|
|
return;
|
|
}
|
|
|
|
var itemModel = data.getItemModel(dataIndex);
|
|
var emphasisItemStyleModel = itemModel.getModel('emphasis.itemStyle');
|
|
var emphasisColor = emphasisItemStyleModel.get('color');
|
|
var emphasisOpacity = emphasisItemStyleModel.get('opacity');
|
|
|
|
if (emphasisColor == null) {
|
|
var color = getItemVisualColor(data, dataIndex);
|
|
emphasisColor = echarts.color.lift(color, -0.4);
|
|
}
|
|
|
|
if (emphasisOpacity == null) {
|
|
emphasisOpacity = getItemVisualOpacity(data, dataIndex);
|
|
}
|
|
|
|
var colorArr = graphicGL.parseColor(emphasisColor);
|
|
colorArr[3] *= emphasisOpacity;
|
|
|
|
this._mesh.geometry.attributes.color.set(dataIndex - this._startDataIndex, colorArr);
|
|
|
|
this._mesh.geometry.dirtyAttribute('color');
|
|
|
|
this._api.getZr().refresh();
|
|
},
|
|
downplay: function (data, dataIndex) {
|
|
if (dataIndex > this._endDataIndex || dataIndex < this._startDataIndex) {
|
|
return;
|
|
}
|
|
|
|
var color = getItemVisualColor(data, dataIndex);
|
|
var opacity = getItemVisualOpacity(data, dataIndex);
|
|
var colorArr = graphicGL.parseColor(color);
|
|
colorArr[3] *= opacity;
|
|
|
|
this._mesh.geometry.attributes.color.set(dataIndex - this._startDataIndex, colorArr);
|
|
|
|
this._mesh.geometry.dirtyAttribute('color');
|
|
|
|
this._api.getZr().refresh();
|
|
},
|
|
fadeOutAll: function (fadeOutPercent) {
|
|
if (this._originalOpacity) {
|
|
var geo = this._mesh.geometry;
|
|
|
|
for (var i = 0; i < geo.vertexCount; i++) {
|
|
var fadeOutOpacity = this._originalOpacity[i] * fadeOutPercent;
|
|
geo.attributes.color.value[i * 4 + 3] = fadeOutOpacity;
|
|
}
|
|
|
|
geo.dirtyAttribute('color');
|
|
|
|
this._api.getZr().refresh();
|
|
}
|
|
},
|
|
fadeInAll: function () {
|
|
this.fadeOutAll(1);
|
|
},
|
|
setPositionTexture: function (texture) {
|
|
if (this._mesh) {
|
|
this._setPositionTextureToMesh(this._mesh, texture);
|
|
}
|
|
|
|
this._positionTexture = texture;
|
|
},
|
|
removePositionTexture: function () {
|
|
this._positionTexture = null;
|
|
|
|
if (this._mesh) {
|
|
this._setPositionTextureToMesh(this._mesh, null);
|
|
}
|
|
},
|
|
setSizeScale: function (sizeScale) {
|
|
if (sizeScale !== this._sizeScale) {
|
|
if (this._mesh) {
|
|
var originalSize = this._mesh.material.get('u_Size');
|
|
|
|
this._mesh.material.set('u_Size', originalSize / this._sizeScale * sizeScale);
|
|
|
|
var attributes = this._mesh.geometry.attributes;
|
|
|
|
if (attributes.size.value) {
|
|
for (var i = 0; i < attributes.size.value.length; i++) {
|
|
attributes.size.value[i] = attributes.size.value[i] / this._sizeScale * sizeScale;
|
|
}
|
|
}
|
|
}
|
|
|
|
this._sizeScale = sizeScale;
|
|
}
|
|
},
|
|
_setPositionTextureToMesh: function (mesh, texture) {
|
|
if (texture) {
|
|
mesh.material.set('positionTexture', texture);
|
|
}
|
|
|
|
mesh.material[texture ? 'enableTexture' : 'disableTexture']('positionTexture');
|
|
},
|
|
_getSymbolInfo: function (seriesModel, start, end) {
|
|
if (seriesModel.get('large')) {
|
|
var symbolSize = retrieve.firstNotNull(seriesModel.get('symbolSize'), 1);
|
|
var maxSymbolSize;
|
|
var symbolAspect;
|
|
|
|
if (symbolSize instanceof Array) {
|
|
maxSymbolSize = Math.max(symbolSize[0], symbolSize[1]);
|
|
symbolAspect = symbolSize[0] / symbolSize[1];
|
|
} else {
|
|
maxSymbolSize = symbolSize;
|
|
symbolAspect = 1;
|
|
}
|
|
|
|
return {
|
|
maxSize: symbolSize,
|
|
type: seriesModel.get('symbol'),
|
|
aspect: symbolAspect
|
|
};
|
|
}
|
|
|
|
var data = seriesModel.getData();
|
|
var symbolAspect;
|
|
var differentSymbolAspect = false;
|
|
var symbolType = data.getItemVisual(0, 'symbol') || 'circle';
|
|
var differentSymbolType = false;
|
|
var maxSymbolSize = 0;
|
|
|
|
for (var idx = start; idx < end; idx++) {
|
|
var symbolSize = data.getItemVisual(idx, 'symbolSize');
|
|
var currentSymbolType = data.getItemVisual(idx, 'symbol');
|
|
var currentSymbolAspect;
|
|
|
|
if (!(symbolSize instanceof Array)) {
|
|
// Ignore NaN value.
|
|
if (isNaN(symbolSize)) {
|
|
continue;
|
|
}
|
|
|
|
currentSymbolAspect = 1;
|
|
maxSymbolSize = Math.max(symbolSize, maxSymbolSize);
|
|
} else {
|
|
currentSymbolAspect = symbolSize[0] / symbolSize[1];
|
|
maxSymbolSize = Math.max(Math.max(symbolSize[0], symbolSize[1]), maxSymbolSize);
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (symbolAspect != null && Math.abs(currentSymbolAspect - symbolAspect) > 0.05) {
|
|
differentSymbolAspect = true;
|
|
}
|
|
|
|
if (currentSymbolType !== symbolType) {
|
|
differentSymbolType = true;
|
|
}
|
|
}
|
|
|
|
symbolType = currentSymbolType;
|
|
symbolAspect = currentSymbolAspect;
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (differentSymbolAspect) {
|
|
console.warn('Different symbol width / height ratio will be ignored.');
|
|
}
|
|
|
|
if (differentSymbolType) {
|
|
console.warn('Different symbol type will be ignored.');
|
|
}
|
|
}
|
|
|
|
return {
|
|
maxSize: maxSymbolSize,
|
|
type: symbolType,
|
|
aspect: symbolAspect
|
|
};
|
|
}
|
|
};
|
|
export default PointsBuilder; |