feat: 实现2D图表鼠标移动时右侧四个图表跟着动,实现2D图表右侧的控制颜色按钮,实现右侧四个图表的鼠标移动时显示竖线功能

This commit is contained in:
Xu Zhimeng 2023-07-04 19:46:38 +08:00
parent e1a2e89c2b
commit 22fa4c54b2
11 changed files with 4654 additions and 262 deletions

View File

@ -20,10 +20,27 @@ export default {
mounted() {
this.chart = echarts.init(this.$refs.containerRef)
this.chart.setOption(this.option)
this.initEventListener()
},
methods: {
initEventListener() {
const zr = this.getZRender()
zr.on('mousemove', (params) => {
this.$emit('zr:mousemove', params)
})
},
resize() {
this.chart && this.chart.resize()
},
// echart
getChartInstance() {
return this.chart
},
getZRender() {
return this.chart.getZr()
}
},
watch : {

20
src/utils/chartHelper.js Normal file
View File

@ -0,0 +1,20 @@
/**
* 根据位置获取这个点在图表的哪个轴线上
* @param offsetX
* @param offsetY
*/
export function getXAxisAndYAxisByPosition(chart, offsetX, offsetY, seriesIndex = 0) {
const pointInPixel = [offsetX, offsetY]
if (
chart.containPixel(
{
seriesIndex: 0
},
pointInPixel
)
) {
const [xAxis, yAxis] = chart.convertFromPixel({ seriesIndex }, pointInPixel)
return [xAxis, yAxis]
}
return null
}

View File

@ -5,7 +5,7 @@
<template slot="title">
Beta-Gamma Spectrum: Sample
</template>
<beta-gamma-spectrum-chart ref="scatterChartRef" />
<beta-gamma-spectrum-chart ref="betaGammaChartRef" :data="twoDData" @positionChange="handlePositionChange" />
</beta-gamma-chart-container>
</div>
<div class="beta-and-gamma-spectrum">
@ -64,6 +64,9 @@ import BetaGammaChartContainer from './components/BetaGammaChartContainer.vue'
import BetaGammaSpectrumChart from './components/BetaGammaSpectrumChart.vue'
import ResultDisplay from './components/ResultDisplay.vue'
import SpectrumLineChart from './components/SpectrumLineChart.vue'
import twoDData from './data.json'
export default {
components: { BetaGammaChartContainer, SpectrumLineChart, ResultDisplay, BetaGammaSpectrumChart },
data() {
@ -97,16 +100,36 @@ export default {
uncertainty: '+/-0.01988',
mdc: '0.03464'
}
]
],
twoDData: {}
}
},
created() {
this.getData()
},
methods: {
resize() {
this.$refs.scatterChartRef && this.$refs.scatterChartRef.resize()
this.$refs.betaGammaChartRef && this.$refs.betaGammaChartRef.resize()
this.$refs.lineChart1Ref && this.$refs.lineChart1Ref.resize()
this.$refs.lineChart2Ref && this.$refs.lineChart2Ref.resize()
this.$refs.lineChart3Ref && this.$refs.lineChart3Ref.resize()
this.$refs.lineChart4Ref && this.$refs.lineChart4Ref.resize()
},
async getData() {
await 0
this.twoDData = twoDData
},
// 2d
handlePositionChange([xAxis, yAxis]) {
// Gamma Spectrumbata-gammagamma channely
this.$refs.lineChart1Ref.setLinePosition(yAxis)
this.$refs.lineChart2Ref.setLinePosition(yAxis)
// Beta Spectrumbata-gammabata channelx
this.$refs.lineChart3Ref.setLinePosition(xAxis)
this.$refs.lineChart4Ref.setLinePosition(xAxis)
}
}
}

View File

@ -6,15 +6,18 @@
:key="item"
:class="active == index ? 'active' : ''"
@click="handleChange(index)"
>{{ item }}</span
>
{{ item }}
</span>
<span @click="handleUnzoom">Unzoom</span>
</div>
<div class="beta-gamma-spectrum-chart-main">
<!-- 2D 图表 -->
<div class="_2d-chart" v-if="active == 0">
<custom-chart ref="chartRef" :option="twoDOption" />
<custom-chart ref="chartRef" :option="twoDOption" @zr:mousemove="handleMouseMove" />
<div class="bar">
<div>256</div>
<color-palette v-model="currCount" :maxValue="4" />
<div>{{ currCount + 1 }}</div>
<div class="bar-main"></div>
<div>0</div>
</div>
@ -34,7 +37,10 @@
<script>
import CustomChart from '@/components/CustomChart/index.vue'
import Custom3DChart from '@/components/Custom3DChart/index.vue'
const buttons = ['2D', '3D Surface', '3D Scatter', 'Unzoom']
import ColorPalette from './ColorPalette.vue'
import { getXAxisAndYAxisByPosition } from '@/utils/chartHelper.js'
const buttons = ['2D', '3D Surface', '3D Scatter']
// 2D
const twoDOption = {
@ -45,8 +51,11 @@ const twoDOption = {
bottom: 45
},
tooltip: {
trigger: 'axis',
showContent: false,
trigger: 'item',
formatter: params => {
const [b, g, c] = params.value
return `Beta: ${b}<br>Gamma: ${g}<br>Count: ${c}`
},
axisPointer: {
animation: false,
type: 'cross',
@ -105,21 +114,16 @@ const twoDOption = {
max: 256,
interval: 64
},
series: [
{
xAxisIndex: 0,
yAxisIndex: 0,
type: 'scatter',
symbolSize: 2.5,
itemStyle: {
color: '#fff'
},
data: new Array(1256).fill(0).map(() => [parseInt(Math.random() * 256), parseInt(Math.random() * 256)])
},
{
type: 'line'
series: {
xAxisIndex: 0,
yAxisIndex: 0,
type: 'scatter',
symbolSize: 5,
data: [],
itemStyle: {
color: '#fff'
}
]
}
}
//3D Surface
@ -255,28 +259,93 @@ const threeDScatterOption = {
}
export default {
props: {
data: {
type: Object,
default: () => ({})
}
},
components: {
CustomChart,
Custom3DChart
Custom3DChart,
ColorPalette
},
data() {
this.buttons = buttons
return {
active: 1,
active: 0,
maxCount: 15, // count
currCount: 15,
twoDOption,
threeDSurfaceOption,
threeDScatterOption
}
},
methods: {
// Beta-Gamma Spectrum: Sample
handleChange(index) {
this.active = index
},
// unzoom
handleUnzoom() {
console.log('%c [ handleUnzoom ]-309', 'font-size:13px; background:pink; color:#bf2c9f;')
},
resize() {
this.$refs.chartRef && this.$refs.chartRef.resize()
this.$refs._3dSurfaceRef && this.$refs._3dSurfaceRef.resize()
this.$refs._3dScannerRef && this.$refs._3dScannerRef.resize()
},
handleMouseMove(param) {
const { offsetX, offsetY } = param
const point = getXAxisAndYAxisByPosition(this.$refs.chartRef.getChartInstance(), offsetX, offsetY)
this.$emit('positionChange', point ? [point[0].toFixed(), point[1].toFixed()] : [null, null])
},
//
interpolateColor(color1, color2, percentage) {
const r = color1.r + (color2.r - color1.r) * percentage
const g = color1.g + (color2.g - color1.g) * percentage
const b = color1.b + (color2.b - color1.b) * percentage
return { r, g, b }
}
},
watch: {
data: {
handler(newVal) {
const data = []
Object.entries(newVal).forEach(([_, v]) => {
v.forEach(({ b, g, c }) => {
data.push({
value: [b, g, c]
})
})
})
this.twoDOption.series.data = data
},
immediate: true
},
currCount: {
handler(val) {
if (val <= this.maxCount) {
const { r, g, b } = this.interpolateColor(
{ r: 255, g: 0, b: 0 },
{ r: 255, g: 255, b: 255 },
val / this.maxCount
)
this.twoDOption.series.itemStyle.color = `rgb(${r}, ${g}, ${b})`
} else {
this.twoDOption.series.itemStyle.color = '#fff'
}
},
immediate: true
}
}
}
@ -338,7 +407,7 @@ export default {
&-main {
display: inline-block;
width: 14px;
height: calc(100% - 42px);
height: calc(100% - 70px);
background: linear-gradient(to bottom, #ff0000 0, #fff 100%);
}
}

View File

@ -0,0 +1,132 @@
<template>
<div
ref="containerElemRef"
class="color-palette"
:style="{ width: circleWidth + 'px', height: circleWidth + 'px' }"
@click="handleClick"
>
<span
ref="dotElemRef"
class="dot"
:style="{
width: dotWidth + 'px',
height: dotWidth + 'px',
left: this.dotPosition.x,
top: this.dotPosition.y
}"
></span>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 1
},
maxValue: {
type: Number,
default: 0
},
circleWidth: {
type: Number,
default: 26
},
dotWidth: {
type: Number,
default: 5
}
},
data() {
return {
dotPosition: {
x: 0,
y: 0
}
}
},
methods: {
handleClick({ offsetX, offsetY }) {
const { degree, radian } = this.getDegree([offsetX, offsetY])
for (let index = 0; index < this.range; index++) {
if (degree >= this.perDegree * index && degree < this.perDegree * (index + 1)) {
this.$emit('input', index)
break
}
}
this.setDotPosition(radian)
},
setDotPosition(radian) {
const circleRadius = this.circleWidth / 2 //
const dotRadius = circleRadius - this.dotWidth //
this.dotPosition = {
x: circleRadius - dotRadius * Math.sin(radian) - this.dotWidth / 2 + 'px',
y: circleRadius + dotRadius * Math.cos(radian) - this.dotWidth / 2 + 'px'
}
},
//
/**
* 根据圆心和某个点计算从圆心到该点的角度
*/
getDegree(point) {
// x y
const circleRadius = this.circleWidth / 2 //
const [pointX, pointY] = point
const deltaX = circleRadius - pointX
const deltaY = pointY - circleRadius
// 使
const radian = Math.atan2(deltaX, deltaY)
let degree = radian * (180 / Math.PI)
if (degree < 0) {
degree = 360 + degree
}
return {
radian,
degree
}
}
},
watch: {
value: {
handler(newVal) {
const degree = newVal * this.perDegree
const radian = (degree * Math.PI) / 180 //
this.setDotPosition(radian)
},
immediate: true
}
},
computed: {
range() {
return this.maxValue > 50 ? this.maxValue : 50
},
perDegree() {
return 360 / this.range
}
}
}
</script>
<style lang="less" scoped>
.color-palette {
border-radius: 50%;
background-color: #ff5858;
margin: 0 auto;
position: relative;
.dot {
position: absolute;
pointer-events: none;
border-radius: 50%;
background-color: #f00;
}
}
</style>

View File

@ -0,0 +1,128 @@
<template>
<a-modal v-model="visible" :width="1400" title="Load Data From File">
<a-table :data-source="list" :columns="columns" :pagination="false">
<template slot="sampleData">
<a-input type="file" @change="handleFileChange($event, 'sampleData')"></a-input>
</template>
<template slot="gasBkData">
<a-input type="file" @change="handleFileChange($event, 'gasBkData')"></a-input>
</template>
<template slot="detBkData">
<a-input type="file" @change="handleFileChange($event, 'detBkData')"></a-input>
</template>
<template slot="qcData">
<a-input type="file" @change="handleFileChange($event, 'qcData')"></a-input>
</template>
<template slot="status" slot-scope="text">
<span class="status"></span>
</template>
</a-table>
<!-- 底部按钮 -->
<template slot="footer">
<a-space>
<a-button type="primary" @click="handleReset">Reset</a-button>
<a-button type="primary" @click="handleLoad">Load</a-button>
<a-button type="primary" @click="handleCancel">Cancel</a-button>
</a-space>
</template>
<!-- 底部按钮结束 -->
</a-modal>
</template>
<script>
const columns = [
{
title: 'SampleData',
dataIndex: 'sampleData',
scopedSlots: {
customRender: 'sampleData'
}
},
{
title: 'GasBkData',
dataIndex: 'gasBkData',
scopedSlots: {
customRender: 'gasBkData'
}
},
{
title: 'DetBkData',
dataIndex: 'detBkData',
scopedSlots: {
customRender: 'detBkData'
}
},
{
title: 'QCData',
dataIndex: 'qcData',
scopedSlots: {
customRender: 'qcData'
}
},
{
title: 'Status',
align: 'center',
scopedSlots: {
customRender: 'status'
}
}
]
export default {
props: {
value: {
type: Boolean
}
},
data() {
this.columns = columns
return {
list: this.getInitialList()
}
},
methods: {
getInitialList() {
return new Array(10).fill(0).map(() => ({
sampleData: '',
gasBkData: '',
detBkData: '',
qcData: ''
}))
},
handleFileChange(fileInfo, key) {
console.log('%c [ fileInfo, key ]-86', 'font-size:13px; background:pink; color:#bf2c9f;', fileInfo, key)
},
handleReset() {
this.list = this.getInitialList()
},
handleLoad() {},
handleCancel() {}
},
computed: {
visible: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
}
}
</script>
<style lang="less" scoped>
.status {
display: inline-block;
width: 25px;
height: 25px;
border-radius: 50%;
background-color: #00e170;
}
</style>

View File

@ -1,76 +0,0 @@
<template>
<a-modal v-model="visible" title="Select File" :footer="null">
<a-table :data-source="list" :columns="columns" :loading="loading" :pagination="false">
<template slot="operator" slot-scope="record">
<a-icon type="check" style="cursor: pointer;" @click="handleSelect(record)"></a-icon>
</template>
</a-table>
</a-modal>
</template>
<script>
const columns = [
{
title: 'File Name',
dataIndex: 'fileName',
ellipsis: true
},
{
title: 'Action',
width: 80,
align: 'center',
scopedSlots: {
customRender: 'operator'
}
}
]
export default {
props: {
value: {
type: Boolean
}
},
data() {
this.columns = columns
return {
loading: false,
list: []
}
},
methods: {
async getList() {
try {
this.loading = true
const res = await ''
console.log('%c [ res ]-13', 'font-size:13px; background:pink; color:#bf2c9f;', res)
this.loading = false
this.list = [{ fileName: '文件1' }, { fileName: '文件2' }]
} catch (error) {
console.error(error)
}
},
//
handleSelect(fileInfo) {
console.log('%c [ ]-56', 'font-size:13px; background:pink; color:#bf2c9f;', fileInfo)
this.$emit('select', fileInfo)
this.visible = false
}
},
computed: {
visible: {
get() {
if (this.value) {
this.getList()
}
return this.value
},
set(val) {
this.$emit('input', val)
}
}
}
}
</script>
<style></style>

View File

@ -1,156 +0,0 @@
<template>
<a-modal v-model="visible" :width="1400" title="Load Data From File">
<a-table :data-source="list" :columns="columns" :pagination="false">
<template slot="status" slot-scope="text">
<span class="status"></span>
</template>
</a-table>
<!-- 底部按钮 -->
<template slot="footer">
<a-space>
<a-button type="primary" @click="handleReset">Reset</a-button>
<a-button type="primary" @click="handleLoad">Load</a-button>
<a-button type="primary" @click="handleCancel">Cancel</a-button>
</a-space>
</template>
<!-- 底部按钮结束 -->
<ftp-file-modal v-model="ftpFileModalVisible" @select="handleSelect" />
</a-modal>
</template>
<script>
import FtpFileModal from './FtpFileModal.vue'
export default {
components: { FtpFileModal },
props: {
value: {
type: Boolean
}
},
data() {
return {
list: this.getInitialList(),
ftpFileModalVisible: false
}
},
methods: {
getInitialList() {
return new Array(10).fill(0).map(() => ({
sampleData: '',
gasBkData: '',
detBkData: '',
qcData: ''
}))
},
/**
* 从ftp中选择文件
* @param rowData 选中的行的数据
* @param { string } dataKey 选中的数据的那一列的key
*/
selectFileFromFtp(rowData, dataKey) {
this.currRowData = rowData
this.currDataKey = dataKey
this.ftpFileModalVisible = true
},
handleSelect(fileInfo) {
this.currRowData[this.currDataKey] = fileInfo.fileName
},
handleReset() {
this.list = this.getInitialList()
},
handleLoad() {},
handleCancel() {}
},
computed: {
visible: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
columns() {
return [
{
title: 'SampleData',
dataIndex: 'sampleData',
customCell: record => {
return {
on: {
click: () => {
this.selectFileFromFtp(record, 'sampleData')
}
}
}
}
},
{
title: 'GasBkData',
dataIndex: 'gasBkData',
customCell: record => {
return {
on: {
click: () => {
this.selectFileFromFtp(record, 'gasBkData')
}
}
}
}
},
{
title: 'DetBkData',
dataIndex: 'detBkData',
customCell: record => {
return {
on: {
click: () => {
this.selectFileFromFtp(record, 'detBkData')
}
}
}
}
},
{
title: 'QCData',
dataIndex: 'qcData',
customCell: record => {
return {
on: {
click: () => {
this.selectFileFromFtp(record, 'qcData')
}
}
}
}
},
{
title: 'Status',
align: 'center',
scopedSlots: {
customRender: 'status'
}
}
]
}
}
}
</script>
<style lang="less" scoped>
.status {
display: inline-block;
width: 25px;
height: 25px;
border-radius: 50%;
background-color: #00e170;
}
</style>

View File

@ -1,13 +1,20 @@
<template>
<div class="spectrum-line-chart">
<div class="title">{{ title + ' Count'}}</div>
<custom-chart class="spectrum-line-chart-main" ref="chartRef" :option="option" style="height: 100%"></custom-chart>
<div class="title">{{ title + ' Count' }}</div>
<custom-chart
class="spectrum-line-chart-main"
ref="chartRef"
:option="option"
style="height: 100%"
@zr:mousemove="handleMouseMove"
></custom-chart>
</div>
</template>
<script>
import CustomChart from '@/components/CustomChart/index.vue'
import { cloneDeep } from 'lodash'
import { getXAxisAndYAxisByPosition } from '@/utils/chartHelper.js'
const initialOption = {
grid: {
@ -88,7 +95,23 @@ const initialOption = {
symbol: 'none',
data: new Array(256)
.fill(0)
.map((_, index) => [index, (Math.random() < 0.05 ? parseInt(Math.random() * 19644) : parseInt(Math.random() * 800))])
.map((_, index) => [
index,
Math.random() < 0.05 ? parseInt(Math.random() * 19644) : parseInt(Math.random() * 800)
]),
markLine: {
symbol: 'none',
animation: false,
label: {
show: false
},
lineStyle: {
type: 'solid',
color: 'yellow'
},
silent: true,
data: []
}
}
}
@ -118,6 +141,23 @@ export default {
methods: {
resize() {
this.$refs.chartRef && this.$refs.chartRef.resize()
},
// 线
setLinePosition(xAxis) {
setTimeout(() => {
if (xAxis) {
this.option.series.markLine.data = [{ xAxis }]
} else {
this.option.series.markLine.data = []
}
}, 0)
},
handleMouseMove(param) {
const { offsetX, offsetY } = param
const point = getXAxisAndYAxisByPosition(this.$refs.chartRef.getChartInstance(), offsetX, offsetY)
this.setLinePosition(point ? point[0].toFixed() : null)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -92,7 +92,7 @@ import BetaGammaAnalysis from './beta-gamma-analysis.vue'
import Spectra from './components/SubOperators/Spectra.vue'
import SpectraListInMenu from './components/SpectraListInMenu.vue'
import LoadFromDbModal from './components/LoadFromDBModal.vue'
import LoadFromFileModal from './components/LoadFromFileModal/Index.vue'
import LoadFromFileModal from './components/LoadFromFileModal.vue'
//
const ANALYZE_TYPE = {