dxcr-waterfall
Version:
waterfall
1,112 lines (993 loc) • 32.1 kB
JavaScript
/*
* @Description:
* @Author: Dxcr
* @Date: 2024-06-26 15:06:00
* @LastEditTime: 2024-07-09 15:51:29
* @LastEditors: Dxcr
*/
import * as THREE from "three";
import DxcrWebWorker from "dxcr-webworker";
import { ThreejsUtill } from "dxcr-three";
import VisualMap from "./visualMap";
import Stats from "three/addons/libs/stats.module.js";
import dayjs from "dayjs";
import colors from "./colors";
class Waterfall {
// 组件
dom;
scene;
camera;
renderer;
stats;
webWorker;
// 数据处理
geometry;
material;
attributesUpdateFlag = false;
countBuffer = 0;
//
canvas;
chartContext;
canvasTexture;
chartMesh;
// 坐标轴
yAxis;
yAxisData = [];
font;
tickNum = 0;
scrollCount = 0;
xAxis;
xAxisData = [];
xAxisOptions = {
color: 0xffffff,
showCount: 10,
};
yAxisOptions = {
color: 0xffffff,
showCount: 10,
};
//坐标轴指示器
xAxisPointer;
yAxisPointer;
xAxisPointerText;
yAxisPointerText;
currentCoordinate;
//图表
rawData = [];
chartData = [];
rawDataCount;
//选中区域
range = [0, 1];
rangeArea = [0, 0];
rangeAreaMesh;
// 参数
dataCount = 1000;
keepCount = 500;
visualMapOptions = {
height: 100,
width: 20,
paddingLeft: 20,
paddingRight: 10,
show: false,
};
minValue = 0;
maxValue = 1000;
paddingLeft = 0;
paddingRight = 10;
paddingTop = 10;
paddingBottom = 0;
chartWidth = 1000;
chartHeight = 500;
axisWidth = 120;
axisHeight = 20;
tickInterval = 20;
subTickInterval = 5;
debugMode = false;
width = 500;
height = 500;
constructor(dom, options = {}) {
this.dom = dom;
this.dom.style.position = "relative";
// this.width =
// this.axisWidth +
// this.chartWidth +
// this.paddingLeft +
// this.paddingRight +
// 1;
// this.height =
// this.axisHeight +
// this.chartHeight +
// 1 +
// this.paddingBottom +
// this.paddingTop;
this.paddingTop = options.paddingTop || this.paddingTop;
this.paddingBottom = options.paddingBottom || this.paddingBottom;
this.paddingLeft = options.paddingLeft || this.paddingLeft;
this.paddingRight = options.paddingRight || this.paddingRight;
this.width = dom.clientWidth;
this.height = dom.clientHeight;
this.chartWidth =
this.width - this.axisWidth - this.paddingLeft - this.paddingRight;
this.chartHeight =
this.height - this.axisHeight - this.paddingTop - this.paddingBottom;
this.dataCount = options.dataCount || this.dataCount;
this.keepCount = options.keepCount || this.keepCount;
// this.chartWidth = options.chartWidth || this.chartWidth;
// this.chartHeight = options.chartHeight || this.chartHeight;
this.debugMode = options.debugMode || this.debugMode;
this.rawDataCount = options.rawDataCount || this.dataCount;
this.minValue = options.minValue || this.minValue;
this.maxValue = options.maxValue || this.maxValue;
this.xAxisOptions = Object.assign(this.xAxisOptions, options.xAxisOptions);
this.yAxisOptions = Object.assign(this.yAxisOptions, options.yAxisOptions);
this.visualMapOptions = Object.assign(this.visualMapOptions, options.visualMapOptions);
this.init();
}
init() {
if (this.visualMapOptions.show) {
this.chartWidth -=
this.visualMapOptions.width +
this.visualMapOptions.paddingLeft +
this.visualMapOptions.paddingRight;
this.initVisualMap();
}
this.initThree();
this.initWebWorker();
if (this.debugMode) {
console.log("waterfall - Init");
this.initStats();
}
// this.handleVisibilityChange();
}
initWebWorker() {
let c = this;
this.webWorker = new DxcrWebWorker(
"/dxcr-waterfall/Worker/worker.js",
{ type: "module" },
{
onMessage: (event) => {
// console.log('WebWorker Received:', event.data)
let returnObject = event.data;
let type = returnObject.type;
let data = returnObject.data;
if (type == 1) {
c.updateDataAll(data);
} else {
c.updateData(data, returnObject.index);
}
},
onError: (event) => console.error("WebWorker error:", event),
}
);
}
initVisualMap() {
const visualMap = new VisualMap(this.dom);
this.visualMap = visualMap
// 将 RGB 数组转换为 CSS 颜色字符串
const rgbToCssColor = (rgb) =>
`rgb(${rgb.map((val) => Math.round(val * 255)).join(", ")})`;
// 创建颜色渐变字符串
const gradient = colors.map(rgbToCssColor);
visualMap.updateVisualMap(gradient, this.minValue, this.maxValue);
visualMap.visualMapDom.style.position = "absolute";
visualMap.visualMapDom.style.bottom = "20px";
visualMap.visualMapDom.style.right = "20px";
visualMap.bindLabelDoubleClick((type, data) => {
if (type == 0) {
this.minValue = data;
} else {
this.maxValue = data;
}
this.setData(this.rawData);
});
}
addData(data, index) {
this.webWorker.postMessage({
data: data,
index: index,
type: 0,
minValue: this.minValue,
maxValue: this.maxValue,
dataCount: this.dataCount,
});
}
setData(data) {
this.rawData = data;
this.webWorker.postMessage({
data: data,
type: 1,
minValue: this.minValue,
maxValue: this.maxValue,
dataCount: this.dataCount,
});
}
setColorValue(minValue, maxValue) {
this.minValue = minValue || this.minValue;
this.maxValue = maxValue || this.maxValue;
const rgbToCssColor = (rgb) =>
`rgb(${rgb.map((val) => Math.round(val * 255)).join(", ")})`;
const gradient = colors.map(rgbToCssColor);
this.visualMap.updateVisualMap(gradient, this.minValue, this.maxValue);
}
clearData() {
this.chartData = [];
this.updateChartAll(this.chartData);
}
handleVisibilityChange() {
const c = this;
// 处理页面可见性变化
document.addEventListener("visibilitychange", function () {
if (document.visibilityState === "visible") {
// 强制更新所有几何体属性
c.geometry.attributes.position.updateRange.offset = 0;
c.geometry.attributes.position.updateRange.count = -1;
c.geometry.attributes.position.needsUpdate = true;
c.geometry.attributes.color.updateRange.offset = 0;
c.geometry.attributes.color.updateRange.count = -1;
c.geometry.attributes.color.needsUpdate = true;
}
});
}
resizeChart() {
this.range = [0, 1];
this.scaleChart(this.range);
}
scaleChart(range) {
let startUV = range[0] > range[1] ? range[1] : range[0];
let endUV = range[0] > range[1] ? range[0] : range[1];
//改变uv坐标
let uv = this.geometry.attributes.uv.array;
for (let i = 0; i < uv.length; i += 2) {
if (i < uv.length / 2) {
uv[i] = startUV;
} else {
uv[i] = endUV;
}
}
this.geometry.attributes.uv.needsUpdate = true;
let start = Math.floor(startUV * this.rawDataCount);
let end = Math.floor(endUV * this.rawDataCount);
this.xAxisData = this.generateXDatas(start, end);
this.updateXAxis();
}
scaleDatas(datas, range) {
let count = datas[0].length;
let startRange = range[0] || 0;
let endRange = range[1] || count;
const sliceData = datas.map((data) => {
return data.slice(startRange, endRange);
});
return sliceData;
}
scaleData(data, range) {
let count = data.length;
let startRange = range[0] || 0;
let endRange = range[1] || count;
return data.slice(startRange, endRange);
}
initXAxis() {
this.xAxisData = this.generateXDatas();
this.updateXAxis();
}
updateData(dataColors, index) {
// console.log("dataColors", dataColors);
let rollIndex = index % this.keepCount;
this.chartData[rollIndex] = dataColors;
this.updateChart(dataColors, rollIndex);
this.updateYAxis(index);
if (index >= this.keepCount) {
this.scroll();
}
}
updateDataAll(datasArray) {
this.clearTick();
this.updateAllYAxis();
this.chartData = datasArray;
this.updateChartAll(datasArray);
}
updateChartAll(datasArray) {
let c = this;
this.clearChart();
let y = 0;
let width = this.chartWidth / this.dataCount;
let height = this.chartHeight / this.keepCount;
for (let i = 0; i < datasArray.length; i++) {
let colors = datasArray[i];
let x = 0;
for (let j = 0; j < colors.length; j += 3) {
const cssColor = `rgb(${Math.round(colors[j] * 255)},${Math.round(
colors[j + 1] * 255
)},${Math.round(colors[j + 2] * 255)})`;
this.chartContext.fillStyle = cssColor;
// this.chartContext.fillStyle = "rgb(64, 150, 255)";
this.chartContext.fillRect(
x,
this.canvas.height - 1 - y,
width,
height
);
x += width;
}
y += height;
}
// 创建纹理
const texture = new THREE.CanvasTexture(this.canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
this.material.map = texture;
this.material.needsUpdate = true;
}
updateChart(colors, index) {
let c = this;
// 绘制纹理图内容
let width = this.chartWidth / this.dataCount;
let height = this.chartHeight / this.keepCount;
let x = 0;
let y = this.canvas.height - height - index * height;
this.clearChart(0, y, this.canvas.width, height);
for (let i = 0; i < colors.length; i += 3) {
const cssColor = `rgb(${Math.round(colors[i] * 255)}, ${Math.round(
colors[i + 1] * 255
)}, ${Math.round(colors[i + 2] * 255)})`;
this.chartContext.fillStyle = cssColor;
this.chartContext.fillRect(x, y, width, height);
x += width;
}
// 创建纹理
const texture = new THREE.CanvasTexture(this.canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
this.material.map = texture;
this.material.needsUpdate = true;
}
updateChartGradient(colors, index) {
// 创建线性渐变色对象
let gradient = this.chartContext.createLinearGradient(
0,
0,
this.canvas.width,
0
);
// 绘制纹理图内容
for (let i = 0; i < colors.length; i += 3) {
// 设置渐变色的起止颜色
// 假设colors数组是颜色值的线性索引或直接存储颜色值
const cssColor = `rgb(${Math.round(colors[i] * 255)}, ${Math.round(
colors[i + 1] * 255
)}, ${Math.round(colors[i + 2] * 255)})`;
gradient.addColorStop(i / (colors.length - 3), cssColor);
}
// 使用渐变色填充矩形
this.chartContext.fillStyle = gradient;
this.chartContext.fillRect(
0,
this.canvas.height - index,
this.canvas.width,
1
);
// 创建纹理
const texture = new THREE.CanvasTexture(this.canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
this.material.map = texture;
this.material.needsUpdate = true;
}
clearChart(
x = 0,
y = 0,
width = this.canvas.width,
height = this.canvas.height
) {
this.chartContext.clearRect(x, y, width, height);
}
scroll() {
let c = this;
// 关键代码 更新了边界球体 解决了超出一定范围不显示的bug
if (this.scrollCount % (this.chartHeight / 2) < 1) {
this.geometry.computeBoundingSphere();
this.yAxis.geometry.computeBoundingSphere();
}
this.scrollCount++;
this.updateAxis(this.yAxis);
this.rollChart(this.scrollCount);
}
rollChart(index) {
let texture = this.material.map;
texture.offset.y = (index * (1 / this.keepCount)) % 1;
}
initThree() {
// 创建场景和相机
this.scene = new THREE.Scene();
// 宽高要+1 防止第一条数据丢失
this.camera = new THREE.OrthographicCamera(
-this.width / 2, // 左
this.width / 2, // 右
this.height / 2, // 上
-this.height / 2, // 下
1,
1000
);
this.camera.position.set(this.width / 2, this.height / 2, 100);
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xb9d3ff, 0.0);
this.renderer = renderer;
renderer.setSize(this.width, this.height);
this.dom.appendChild(renderer.domElement);
// 创建几何体
let geometry = new THREE.BufferGeometry();
this.geometry = geometry;
//类型化数组创建顶点数据
const vertices = new Float32Array([
0,
0,
0, //顶点1坐标
0,
this.chartHeight,
0, //顶点2坐标
this.chartWidth,
this.chartHeight,
0, //顶点3坐标
this.chartWidth,
0,
0, //顶点4坐标
]);
// 创建索引数组
const indices = new Uint16Array([
0,
1,
2, // 三角形1
0,
2,
3, // 三角形2
]);
// 创建属性缓冲区对象
const attribue = new THREE.BufferAttribute(vertices, 3);
geometry.setAttribute("position", attribue);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// 创建纹理坐标
const uv = new Float32Array([
0,
0, // 顶点1的纹理坐标
0,
1, // 顶点2的纹理坐标
1,
1, // 顶点3的纹理坐标
1,
0, // 顶点4的纹理坐标
]);
geometry.setAttribute("uv", new THREE.BufferAttribute(uv, 2));
const material = new THREE.MeshBasicMaterial({
color: "#ffffff", //材质颜色
map: null,
// side: THREE.FrontSide, //默认只有正面可见
side: THREE.DoubleSide, //两面可见
// side: THREE.BackSide, //设置只有背面可见
});
// material.onBeforeCompile = function (shader, renderer) {
// console.log('shader',shader)
// // shader.uniforms.diffuse.value = null
// console.log('renderer',renderer)
// };
this.material = material;
const chartMesh = new THREE.Mesh(geometry, material);
chartMesh.name = "chart";
this.chartMesh = chartMesh;
chartMesh.position.set(this.axisWidth, this.axisHeight, 0);
this.scene.add(chartMesh);
this.initTicks();
this.initXAxis();
this.initCanvas();
this.initAxisPointer();
this.initRangeArea();
this.addEvent();
// 渲染
this.animate();
}
addEvent() {
let c = this;
// 监听鼠标按下事件
let mouseDown = false;
let startUV = new THREE.Vector2();
let endUV = new THREE.Vector2();
// 创建射线投射器
let dom = this.renderer.domElement;
this.renderer.domElement.addEventListener("mousedown", (event) => {
mouseDown = false;
// 将鼠标位置转换为NDC坐标
let mouse = new THREE.Vector2();
mouse.x = (event.offsetX / dom.clientWidth) * 2 - 1;
mouse.y = -(event.offsetY / dom.clientHeight) * 2 + 1;
// 使用射线投射器和相机来检测点击的对象
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, c.camera);
let intersects = raycaster.intersectObjects(c.scene.children, true);
if (intersects.length > 0) {
// 获取第一个交点的对象
let clickedObject = intersects[0].object;
// console.log("Clicked object:", clickedObject);
let name = clickedObject.name;
if (name == "chart") {
let uv = intersects[0].uv;
startUV = uv;
c.range[0] = startUV.x;
let point = intersects[0].point;
c.rangeArea[0] = point.x;
mouseDown = true;
}
}
});
// 监听鼠标移动事件
this.renderer.domElement.addEventListener("mousemove", (event) => {
// console.log("mousemove");
// 将鼠标位置转换为NDC坐标
let mouse = new THREE.Vector2();
mouse.x = (event.offsetX / dom.clientWidth) * 2 - 1;
mouse.y = -(event.offsetY / dom.clientHeight) * 2 + 1;
// 使用射线投射器和相机来检测点击的对象
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, c.camera);
let intersects = raycaster.intersectObjects(c.scene.children, true);
if (intersects.length > 0) {
// 获取第一个交点的对象
let clickedObject = intersects[0].object;
let name = clickedObject.name;
if (name == "chart") {
if (mouseDown) {
let uv = intersects[0].uv;
endUV = uv;
c.range[1] = endUV.x;
let point = intersects[0].point;
c.rangeArea[1] = point.x;
c.updateRangeArea();
}
let clickedPoint = intersects[0].point;
let x = clickedPoint.x - clickedObject.position.x;
let y = clickedPoint.y - clickedObject.position.y;
c.currentCoordinate = { x, y };
}
}
});
// 监听鼠标释放事件
this.renderer.domElement.addEventListener("mouseup", (event) => {
if (mouseDown) {
// 将鼠标位置转换为NDC坐标
let mouse = new THREE.Vector2();
mouse.x = (event.offsetX / dom.clientWidth) * 2 - 1;
mouse.y = -(event.offsetY / dom.clientHeight) * 2 + 1;
// 使用射线投射器和相机来检测点击的对象
let raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, c.camera);
let intersects = raycaster.intersectObjects(c.scene.children, true);
if (intersects.length > 0) {
// 获取第一个交点的对象
let clickedObject = intersects[0].object;
// console.log("Clicked object:", clickedObject);
let name = clickedObject.name;
if (name == "chart") {
//缩放
let uv = intersects[0].uv;
endUV = uv;
//缩放矩阵
if (startUV.x != endUV.x) {
c.range[1] = endUV.x;
//执行放大
c.scaleChart(c.range);
}
c.showRangeArea(false);
}
}
mouseDown = false;
}
});
this.renderer.domElement.addEventListener("dblclick", () => {
//还原矩阵
c.resizeChart();
});
// 监听鼠标移出事件
this.renderer.domElement.addEventListener("mouseout", function (event) {
c.currentCoordinate = null;
});
}
initCanvas() {
this.canvas = document.createElement("canvas");
// this.dom.appendChild(this.canvas);
this.canvas.width = this.chartWidth;
this.canvas.height = this.chartHeight;
this.chartContext = this.canvas.getContext("2d");
this.canvasTexture = new THREE.CanvasTexture(this.canvas);
this.canvasTexture.wrapS = THREE.RepeatWrapping;
this.canvasTexture.wrapT = THREE.RepeatWrapping;
this.canvasTexture.magFilter = THREE.NearestFilter;
this.canvasTexture.minFilter = THREE.NearestFilter;
this.material.map = this.canvasTexture;
this.material.needsUpdate = true;
}
animate() {
ThreejsUtill.animateDelta((deltaTime) => {
if (this.stats) {
this.stats.update();
}
this.renderer.render(this.scene, this.camera);
this.attributesUpdateFlag = false;
});
}
initStats() {
// 创建stats对象
this.stats = new Stats();
// Stats.domElement:web页面上输出计算结果,一个div元素
this.dom.appendChild(this.stats.domElement);
this.stats.domElement.style.position = "absolute";
this.stats.domElement.style.bottom = "0px";
this.stats.domElement.style.top = "auto";
}
createAxis(length, color) {
const material = new THREE.LineBasicMaterial({
color: color,
linewidth: 10,
});
const points = [];
points.push(new THREE.Vector3(0, 0, 0));
points.push(new THREE.Vector3(length, 0, 0));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geometry, material);
}
createTick(position, color, length = 10) {
const material = new THREE.LineBasicMaterial({ color: color });
const points = [];
points.push(new THREE.Vector3(position, 0, 0));
points.push(new THREE.Vector3(position, length, 0));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geometry, material);
}
addTicks(axis, texts, color, tickInterval, subTickNum) {
let arr = [];
for (let i = 0; i < texts.length; i++) {
let text = texts[i];
let tickPosition = i * tickInterval;
if (i == 0 && this.xAxis == axis) {
tickPosition += 1;
}
if (i % subTickNum == 0) {
let tick = this.addTick(axis, tickPosition, text, color, 6, -6);
arr.push(tick);
} else {
let tick = this.addTick(axis, tickPosition, text, color, 6, -6);
arr.push(tick);
}
}
return arr;
}
addTick(axis, tickPosition, text, color, length, textDistance) {
const tick = this.createTick(tickPosition, color, length);
axis.add(tick);
const sprite = this.createTextSprite(text, 12, "#ffffff", null, [2, 4]);
sprite.position.x += tickPosition;
sprite.position.y += textDistance;
tick.add(sprite);
return tick;
}
removeTick() {
let tick = this.yAxis.children[0];
this.yAxis.remove(tick);
}
initTicks() {
const length = this.chartHeight;
this.yAxis = this.createAxis(length, 0xffffff);
this.yAxis.rotation.z = Math.PI / 2;
this.yAxis.position.x = this.axisWidth + 1;
this.yAxis.position.y = this.axisHeight;
this.scene.add(this.yAxis);
// 添加X轴
let xAxisOptions = this.xAxisOptions;
const xAxisLength = this.chartWidth;
let xAxis = this.createAxis(xAxisLength, xAxisOptions.color); // 使用相同的颜色创建X轴
this.xAxis = xAxis;
this.xAxis.position.x = this.axisWidth; // 调整X轴的位置
this.xAxis.position.y = this.axisHeight; // 调整X轴的位置
this.scene.add(this.xAxis);
}
setYData(data) {
this.yAxisData = data;
}
setXData(data) {
this.xAxisData = data;
}
updateXAxis() {
let xDatas = [];
let showCount = this.xAxisOptions.showCount;
for (let i = 0; i <= showCount; i++) {
xDatas.push(
this.xAxisData[
Math.floor(((this.xAxisData.length - 1) / showCount) * i)
]
);
}
this.xAxis.children = [];
let ticks = this.addTicks(
this.xAxis,
xDatas,
"#ffffff",
this.chartWidth / (xDatas.length - 1),
0
);
for (const tick of ticks) {
tick.position.y = -6;
}
}
//生成x轴文字
generateXDatas(start = 0, end = this.rawDataCount) {
let datas = [];
for (let i = start; i <= end; i++) {
datas.push(i);
}
return datas;
}
initRangeArea() {
const rangeAreaMesh = this.createRangeArea({
width: this.chartWidth,
height: this.chartHeight,
});
rangeAreaMesh.position.x = this.chartWidth / 2;
rangeAreaMesh.position.y = this.chartHeight / 2;
this.chartMesh.add(rangeAreaMesh);
rangeAreaMesh.visible = false;
this.rangeAreaMesh = rangeAreaMesh;
}
updateRangeArea() {
let AreaX = this.rangeArea[1] - this.rangeArea[0];
let scaleX = AreaX / this.chartWidth;
this.rangeAreaMesh.scale.x = scaleX;
this.rangeAreaMesh.position.x =
this.rangeArea[0] - this.chartMesh.position.x + AreaX / 2;
this.rangeAreaMesh.visible = true;
}
showRangeArea(isShow = true) {
this.rangeAreaMesh.visible = isShow;
this.rangeAreaMesh.visible = isShow;
}
createRangeArea({ width, height }) {
let material = new THREE.MeshBasicMaterial({
color: 0xcccccc, // 设置材质颜色
transparent: true, // 启用透明度
opacity: 0.4, // 设置透明度为50%
});
let geometry = new THREE.PlaneGeometry(width, height);
let mesh = new THREE.Mesh(geometry, material);
return mesh;
}
showAxisPointer(isShow = true) {
this.xAxisPointer.visible = isShow;
this.yAxisPointer.visible = isShow;
}
updateAxisPointer() {
if (this.currentCoordinate) {
this.showAxisPointer();
const { x, y } = this.currentCoordinate;
this.xAxisPointer.position.x = x;
this.yAxisPointer.position.y = y;
this.updateAxisPointerText(x, y);
} else {
this.showAxisPointer(false);
}
}
updateAxisPointerText(x, y) {
let xIndex = Math.ceil((x / this.chartWidth) * this.rawDataCount);
let yIndex = Math.floor((y / this.chartHeight) * this.chartHeight);
xIndex = Math.min(xIndex, this.xAxisData.length - 1);
yIndex = Math.min(yIndex, this.yAxisData.length - 1);
let xText = this.xAxisData[xIndex] ?? "";
let yText = this.yAxisData[yIndex] ?? "";
const texture = this.createTextTexture(
xText,
12,
"#ffffff",
"#333333",
[2, 4]
);
this.xAxisPointerText.material.map = texture;
this.xAxisPointerText.scale.set(
texture.image.width,
texture.image.height,
1
);
const texture1 = this.createTextTexture(
yText,
12,
"#ffffff",
"#333333",
[2, 4]
);
this.yAxisPointerText.material.map = texture1;
this.yAxisPointerText.scale.set(
texture1.image.width,
texture1.image.height,
1
);
}
initAxisPointer() {
let c = this;
let xAxisPointer = this.createXAxisPointer();
this.xAxisPointer = xAxisPointer;
xAxisPointer.visible = false;
this.chartMesh.add(xAxisPointer);
let yAxisPointer = this.createYAxisPointer();
this.yAxisPointer = yAxisPointer;
yAxisPointer.visible = false;
this.chartMesh.add(yAxisPointer);
const xAxisPointerText = this.createTextSprite(
"",
12,
"#ffffff",
"#333333",
[2, 4]
);
xAxisPointerText.center = [0.5, 1];
this.xAxisPointerText = xAxisPointerText;
xAxisPointerText.position.y = -4;
xAxisPointerText.position.z = 1;
this.xAxisPointer.add(xAxisPointerText);
const yAxisPointerText = this.createTextSprite(
"",
12,
"#ffffff",
"#333333",
[2, 4]
);
yAxisPointerText.center = [1, 0.5];
this.yAxisPointerText = yAxisPointerText;
yAxisPointerText.position.x = -4;
yAxisPointerText.position.z = 1;
this.yAxisPointer.add(yAxisPointerText);
ThreejsUtill.animate(() => {
c.updateAxisPointer();
});
}
createXAxisPointer() {
const material = new THREE.LineBasicMaterial({
color: "#ffffff",
linewidth: 10,
});
const points = [];
points.push(new THREE.Vector3(0, 0, 0));
points.push(new THREE.Vector3(0, this.chartHeight, 0));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, material);
return line;
}
createYAxisPointer() {
const material = new THREE.LineBasicMaterial({
color: "#ffffff",
linewidth: 10,
});
const points = [];
points.push(new THREE.Vector3(0, 0, 0));
points.push(new THREE.Vector3(this.chartWidth, 0, 0));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, material);
this.yAxisPointer = line;
return line;
}
async initFont() {
this.font = await ThreejsUtill.loadFont(
"/fonts/Noto_Sans_SC/Noto Sans SC Light_Regular.json"
);
return this.font;
}
createTextTexture(
text,
fontSize,
fontColor,
backgroundColor,
padding = [0, 0]
) {
let paddingV = padding[0];
let paddingH = padding[1];
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.font = `${fontSize}px 宋体`;
// 设置画布尺寸
const width = context.measureText(text).width;
canvas.height = fontSize + paddingV * 2;
canvas.width = width + paddingH * 2;
//设置背景色
if (backgroundColor) {
context.fillStyle = backgroundColor;
context.fillRect(0, 0, canvas.width, canvas.height);
}
//写字
context.font = `${fontSize}px 宋体`;
context.fillStyle = fontColor;
// 绘制文字
context.fillText(text, paddingH, canvas.height / 2 + fontSize / 2 - 2);
// 创建纹理
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
return texture;
}
createTextSprite(
text,
fontSize,
fontColor,
backgroundColor,
padding = [0, 0]
) {
const texture = this.createTextTexture(
text,
fontSize,
fontColor,
backgroundColor,
padding
);
const material = new THREE.SpriteMaterial({
map: texture,
});
const sprite = new THREE.Sprite(material);
// 根据字体大小调整精灵的比例
sprite.scale.set(texture.image.width, texture.image.height, 1);
return sprite;
}
updateAxis(yAxis) {
let height = this.chartHeight / this.keepCount;
// 改变原有坐标轴的点 后一个点坐标+1
const position = yAxis.geometry.attributes.position;
const numPoints = position.count; // 坐标轴线上的点的总数
// 更新坐标轴线上每个点的位置
for (let i = 0; i < numPoints; i++) {
// 将每个点的 x 坐标增加 1
position.setX(i, position.getX(i) + height);
}
position.needsUpdate = true;
yAxis.position.y -= height;
}
updateYAxis(index, data) {
data = data || dayjs().format("YYYY-MM-DD HH:mm:ss");
this.yAxisData[index] = data;
let y = index * (this.chartHeight / this.keepCount);
if (index % this.tickInterval == 0) {
this.addTick(this.yAxis, y, data, 0xffffff, 6, 70);
this.tickNum++;
//删除老的
if (this.tickNum > this.keepCount / this.tickInterval) {
this.removeTick();
this.tickNum--;
}
}
}
updateAllYAxis() {
let datas = [];
let showCount = this.yAxisOptions.showCount;
for (let i = 0; i <= showCount; i++) {
datas.push(
this.yAxisData[
Math.floor(((this.yAxisData.length - 1) / showCount) * i)
]
);
}
this.yAxis.children = [];
let ticks = this.addTicks(
this.yAxis,
datas,
"#ffffff",
this.chartHeight / (datas.length - 1),
0
);
for (const tick of ticks) {
for (const children of tick.children) {
children.position.y += 70;
}
}
}
clearTick() {
this.yAxis.children = [];
this.tickNum = 0;
}
}
export default Waterfall;