main
parent
ee6c5bf8a5
commit
366ec62cd7
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1775115523115" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5347" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M458.105263 970.105263h-215.578947V264.084211h215.578947V970.105263z m-161.68421-53.894737h107.789473V317.978947h-107.789473V916.210526z" fill="#ffffff" p-id="5348"></path><path d="M296.421053 970.105263h-215.578948V479.663158h215.578948V970.105263z m-161.684211-53.894737h107.789474V533.557895h-107.789474V916.210526zM943.157895 970.105263h-215.578948V479.663158h215.578948V970.105263z m-161.684211-53.894737h107.789474V533.557895h-107.789474V916.210526zM619.789474 970.105263h-215.578948V53.894737h215.578948v916.210526z m-161.684211-53.894737h107.789474V107.789474h-107.789474v808.421052z" fill="#ffffff" p-id="5349"></path><path d="M781.473684 970.105263h-215.578947V264.084211h215.578947V970.105263z m-161.68421-53.894737h107.789473V317.978947h-107.789473V916.210526z" fill="#ffffff" p-id="5350"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,366 @@
|
|||
<template>
|
||||
<div class="histogram">
|
||||
<div ref="chartContainer" style="width: 490px; height: 290px;" v-loading="loading"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts/core';
|
||||
import { LineChart } from 'echarts/charts';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
LegendComponent,
|
||||
DatasetComponent,
|
||||
// 内置数据转换器组件 (filter, sort)
|
||||
TransformComponent
|
||||
} from 'echarts/components';
|
||||
// 标签自动布局、全局过渡动画等特性
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
echarts.use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
DataZoomComponent,
|
||||
LegendComponent,
|
||||
LineChart,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
CanvasRenderer
|
||||
]);
|
||||
import {
|
||||
getRenderingEngine,
|
||||
cache,
|
||||
utilities,
|
||||
Enums,
|
||||
} from '@cornerstonejs/core';
|
||||
export default {
|
||||
name: "histogram",
|
||||
props: {
|
||||
viewportKey: {
|
||||
type: String,
|
||||
default: 'viewport'
|
||||
},
|
||||
activeViewportIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
renderingEngineId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
|
||||
},
|
||||
initChart() {
|
||||
this.chart = echarts.init(this.$refs.chartContainer);
|
||||
var formatTime = echarts.time.format;
|
||||
var _data = this.generateData1();
|
||||
const option = {
|
||||
// Choose axis ticks based on UTC time.
|
||||
useUTC: true,
|
||||
title: {
|
||||
text: 'Intraday Chart with Breaks (Single Day)',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'time',
|
||||
interval: 1000 * 60 * 30,
|
||||
axisLabel: {
|
||||
showMinLabel: true,
|
||||
showMaxLabel: true,
|
||||
formatter: (value, index, extra) => {
|
||||
if (!extra || !extra.break) {
|
||||
// The third parameter is `useUTC: true`.
|
||||
return formatTime(value, '{HH}:{mm}', true);
|
||||
}
|
||||
// Only render the label on break start, but not on break end.
|
||||
if (extra.break.type === 'start') {
|
||||
return (
|
||||
formatTime(extra.break.start, '{HH}:{mm}', true) +
|
||||
'/' +
|
||||
formatTime(extra.break.end, '{HH}:{mm}', true)
|
||||
);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
breakLabelLayout: {
|
||||
// Disable auto move of break labels if overlapping,
|
||||
// and use `axisLabel.formatter` to control the label display.
|
||||
moveOverlap: false
|
||||
},
|
||||
breaks: [
|
||||
{
|
||||
start: _data.breakStart,
|
||||
end: _data.breakEnd,
|
||||
gap: 0
|
||||
}
|
||||
],
|
||||
breakArea: {
|
||||
expandOnClick: false,
|
||||
zigzagAmplitude: 0,
|
||||
zigzagZ: 200
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 'dataMin'
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: 0
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
xAxisIndex: 0
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
symbolSize: 0,
|
||||
data: _data.seriesData
|
||||
}
|
||||
]
|
||||
};
|
||||
this.chart.setOption(option);
|
||||
},
|
||||
generateData1() {
|
||||
var seriesData = [];
|
||||
var time = new Date('2024-04-09T09:30:00Z');
|
||||
var endTime = new Date('2024-04-09T15:00:00Z').getTime();
|
||||
var breakStart = new Date('2024-04-09T11:30:00Z').getTime();
|
||||
var breakEnd = new Date('2024-04-09T13:00:00Z').getTime();
|
||||
for (var val = 1669; time.getTime() <= endTime;) {
|
||||
if (time.getTime() <= breakStart || time.getTime() >= breakEnd) {
|
||||
val =
|
||||
val +
|
||||
Math.floor((Math.random() - 0.5 * Math.sin(val / 1000)) * 20 * 100) /
|
||||
100;
|
||||
val = +val.toFixed(2);
|
||||
seriesData.push([time.getTime(), val]);
|
||||
}
|
||||
time.setMinutes(time.getMinutes() + 1);
|
||||
}
|
||||
return {
|
||||
seriesData: seriesData,
|
||||
breakStart: breakStart,
|
||||
breakEnd: breakEnd
|
||||
};
|
||||
},
|
||||
resize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize()
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
async getCurrentSliceValuesFromVoxelManager(renderingEngineId, viewportId) {
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId);
|
||||
const viewport = renderingEngine.getViewport(viewportId);
|
||||
|
||||
const volumeId = viewport.getVolumeId();
|
||||
if (!volumeId) {
|
||||
throw new Error('No volumeId found on viewport');
|
||||
}
|
||||
|
||||
const volume = cache.getVolume(volumeId);
|
||||
if (!volume) {
|
||||
throw new Error(`Volume not found in cache: ${volumeId}`);
|
||||
}
|
||||
|
||||
if (!volume.load) {
|
||||
await volume.load();
|
||||
}
|
||||
|
||||
const voxelManager = volume.voxelManager;
|
||||
if (!voxelManager) {
|
||||
throw new Error('voxelManager not found on volume');
|
||||
}
|
||||
|
||||
const [dimX, dimY, dimZ] = volume.dimensions;
|
||||
|
||||
const camera = viewport.getCamera();
|
||||
const focalPoint = camera.focalPoint;
|
||||
const orientation = this.guessOrthogonalOrientation(camera.viewPlaneNormal);
|
||||
|
||||
const ijk = utilities.transformWorldToIndex(volume.imageData, focalPoint);
|
||||
const i = this.clamp(Math.round(ijk[0]), 0, dimX - 1);
|
||||
const j = this.clamp(Math.round(ijk[1]), 0, dimY - 1);
|
||||
const k = this.clamp(Math.round(ijk[2]), 0, dimZ - 1);
|
||||
|
||||
const modality =
|
||||
volume.metadata?.Modality ||
|
||||
volume.metadata?.modality ||
|
||||
'UNKNOWN';
|
||||
|
||||
let width, height, values;
|
||||
|
||||
if (orientation === Enums.OrientationAxis.AXIAL) {
|
||||
width = dimX;
|
||||
height = dimY;
|
||||
values = new Float32Array(width * height);
|
||||
|
||||
let idx = 0;
|
||||
for (let y = 0; y < dimY; y++) {
|
||||
for (let x = 0; x < dimX; x++) {
|
||||
const raw = voxelManager.getAtIJK(x, y, k);
|
||||
values[idx++] = this.convertToPhysicalValue(raw, volume.metadata, modality);
|
||||
}
|
||||
}
|
||||
|
||||
return { orientation: 'AXIAL', sliceIndex: k, width, height, values, modality };
|
||||
}
|
||||
|
||||
if (orientation === Enums.OrientationAxis.CORONAL) {
|
||||
width = dimX;
|
||||
height = dimZ;
|
||||
values = new Float32Array(width * height);
|
||||
|
||||
let idx = 0;
|
||||
for (let z = 0; z < dimZ; z++) {
|
||||
for (let x = 0; x < dimX; x++) {
|
||||
const raw = voxelManager.getAtIJK(x, j, z);
|
||||
values[idx++] = this.convertToPhysicalValue(raw, volume.metadata, modality);
|
||||
}
|
||||
}
|
||||
|
||||
return { orientation: 'CORONAL', sliceIndex: j, width, height, values, modality };
|
||||
}
|
||||
|
||||
if (orientation === Enums.OrientationAxis.SAGITTAL) {
|
||||
width = dimY;
|
||||
height = dimZ;
|
||||
values = new Float32Array(width * height);
|
||||
|
||||
let idx = 0;
|
||||
for (let z = 0; z < dimZ; z++) {
|
||||
for (let y = 0; y < dimY; y++) {
|
||||
const raw = voxelManager.getAtIJK(i, y, z);
|
||||
values[idx++] = this.convertToPhysicalValue(raw, volume.metadata, modality);
|
||||
}
|
||||
}
|
||||
|
||||
return { orientation: 'SAGITTAL', sliceIndex: i, width, height, values, modality };
|
||||
}
|
||||
|
||||
throw new Error('Unsupported orientation');
|
||||
},
|
||||
|
||||
convertToPhysicalValue(storedValue, metadata, modality) {
|
||||
if (modality === 'CT') {
|
||||
return this.convertCTToHU(storedValue, metadata);
|
||||
}
|
||||
|
||||
if (modality === 'PT' || modality === 'PET') {
|
||||
return this.convertPETToSUV(storedValue, metadata);
|
||||
}
|
||||
|
||||
return storedValue;
|
||||
},
|
||||
|
||||
convertCTToHU(value, metadata) {
|
||||
const slope = Number(metadata?.RescaleSlope ?? metadata?.rescaleSlope ?? 1);
|
||||
const intercept = Number(
|
||||
metadata?.RescaleIntercept ?? metadata?.rescaleIntercept ?? 0
|
||||
);
|
||||
|
||||
return value * slope + intercept;
|
||||
},
|
||||
|
||||
convertPETToSUV(value, metadata) {
|
||||
// 先尝试判断当前值是否已经是 SUV
|
||||
const units = metadata?.Units || metadata?.units || '';
|
||||
const correctedImage = metadata?.CorrectedImage || metadata?.correctedImage;
|
||||
// 如果数据已明确是 SUV,可以直接返回
|
||||
if (
|
||||
typeof units === 'string' &&
|
||||
units.toUpperCase().includes('SUV')
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 有些数据会先经过Rescale
|
||||
const slope = Number(metadata?.RescaleSlope ?? metadata?.rescaleSlope ?? 1);
|
||||
const intercept = Number(
|
||||
metadata?.RescaleIntercept ?? metadata?.rescaleIntercept ?? 0
|
||||
);
|
||||
const activityConcentration = value * slope + intercept;
|
||||
|
||||
// 如果 metadata 中已有 suvFactor,优先使用
|
||||
const suvFactor = metadata?.suvFactor ?? metadata?.SUVFactor;
|
||||
if (suvFactor != null) {
|
||||
return activityConcentration * Number(suvFactor);
|
||||
}
|
||||
|
||||
// 如果没有足够信息计算 SUV,就退化返回 rescale 后值
|
||||
// 这可能是 Bq/ml,而不是真正SUV
|
||||
return activityConcentration;
|
||||
},
|
||||
|
||||
getValueType(modality, metadata) {
|
||||
if (modality === 'CT') {
|
||||
return 'HU';
|
||||
}
|
||||
|
||||
if (modality === 'PT' || modality === 'PET') {
|
||||
const units = metadata?.Units || metadata?.units || '';
|
||||
if (typeof units === 'string' && units.toUpperCase().includes('SUV')) {
|
||||
return 'SUV';
|
||||
}
|
||||
|
||||
if (metadata?.suvFactor != null || metadata?.SUVFactor != null) {
|
||||
return 'SUV';
|
||||
}
|
||||
|
||||
return 'PET';
|
||||
}
|
||||
|
||||
return 'RAW';
|
||||
},
|
||||
|
||||
clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
},
|
||||
|
||||
guessOrthogonalOrientation(viewPlaneNormal) {
|
||||
const [nx, ny, nz] = viewPlaneNormal.map(Math.abs);
|
||||
|
||||
if (nz >= nx && nz >= ny) {
|
||||
return Enums.OrientationAxis.AXIAL;
|
||||
}
|
||||
|
||||
if (ny >= nx && ny >= nz) {
|
||||
return Enums.OrientationAxis.CORONAL;
|
||||
}
|
||||
|
||||
return Enums.OrientationAxis.SAGITTAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
Loading…
Reference in New Issue