提交 bff4fbe7 authored 作者: 祖铭松's avatar 祖铭松

feat: PCA

上级 ad01d3f6
......@@ -40,7 +40,7 @@ const generateChart = (data) => {
const titleConfig = {
label: !!ret.title ? ret.title : null,
color: ret.title_font_color || '#333333',
fontSize: ret.title_font_size || 30,
fontSize: ret.title_font_size || 20,
fontStyle: ret.title_font_style ? 'oblique' : 'normal',
fontFamily: ret.title_font_family || 'Arial',
fontWeight: ret.title_font_weight ? 'bold' : 'normal'
......
<template>
<div ref="chartRef"></div>
<div ref="chartRef" class="chart-box">
<div class="tip"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as Plot from '@observablehq/plot';
// import * as d3 from 'd3';
import { chartData } from '../mock';
import { chartMockData } from '../mock.js';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
import PubSub from 'pubsub-js';
import { setTips, setColor, setLegend, setScale, setAxis, setPoint, setLabel, setElliptic } from '../utils/index';
// 根节点实例
const chartRef = ref(null);
// 配置项数据
const options = ref({});
// 图表实例
const svgInstance = ref(null);
// pubsub实例
const pubSubInstance = ref(null);
// plot实例
const plotInstance = ref(null);
// 生成图表
const generateChart = (data) => {
const ret = options.value;
// console.log(ret);
// 点,线,阴影颜色设置
let point_color = '#FF5733';
let line_color = '#39FDBE';
let shadow_color = '#D6D6D6';
if (ret.data_color_list?.length) {
point_color = ret.data_color_list[0];
line_color = ret.data_color_list[1];
shadow_color = ret.data_color_list[2];
}
plotInstance.value = Plot.plot({
width: ret.chart_width || 800,
height: ret.chart_height || 600,
marginTop: ret.margin_top || 60,
marginRight: ret.margin_right || 60,
marginBottom: ret.margin_bottom || 60,
marginLeft: ret.margin_left || 60,
color: {
legend: ret.show_legend
},
x: {
grid: ret.show_x_gridLine,
domain: [-100, 100]
},
y: {
grid: ret.show_y_gridLine,
domain: [-100, 100]
},
marks: [
Plot.frame(),
Plot.ruleX([0], { strokeDasharray: '5' }),
Plot.ruleY([0], { strokeDasharray: '5' }),
Plot.axisX({
interval: 50,
label: !!ret.x_title ? ret.x_title : null,
stroke: ret.x_tickMarks_color || 'currentColor',
tickSize: ret.show_x_tickMarks ? ret.x_tickMarks_length : 0,
fill: ret.xtitle_font_color || 'currentColor',
fontSize: ret.xtitle_font_size || 16,
fontStyle: ret.xtitle_font_style ? 'oblique' : 'normal',
fontFamily: ret.xtitle_font_family || 'Arial',
fontWeight: ret.xtitle_font_weight ? 'bold' : 'normal'
}),
Plot.axisY({
interval: 50,
label: !!ret.y_title ? ret.y_title : null,
stroke: ret.y_tickMarks_color || 'currentColor',
tickSize: ret.show_x_tickMarks ? ret.y_tickMarks_length : 0,
fill: ret.ytitle_font_color || 'currentColor',
fontSize: ret.ytitle_font_size || 16,
fontStyle: ret.ytitle_font_style ? 'oblique' : 'normal',
fontFamily: ret.ytitle_font_family || 'Arial',
fontWeight: ret.ytitle_font_weight ? 'bold' : 'normal'
}),
Plot.dot(data, { x: 'x', y: 'y', fill: point_color, r: Number(ret.point_radius) || 5, tip: true }),
// Plot.linearRegressionX(data, {
// x: 'x',
// y: 'y',
// ci: 0.5,
// stroke: line_color,
// fill: shadow_color,
// fillOpacity: ret.shadow_opacity || 0.5
// }),
Plot.text([ret.title || ''], {
x: 1,
fill: ret.title_font_color || 'currentColor',
let width = Number(ret.chart_width) || 700;
let height = Number(ret.chart_height) || 800;
let marginTop = Number(ret.margin_top) || 120;
let marginRight = Number(ret.margin_Right) || 120;
let marginBottom = Number(ret.margin_Bottom) || 120;
let marginLeft = Number(ret.margin_Left) || 120;
let legendPosition = ret.legend_position?.split(',') || [0.5, 1];
let colors = ret.data_color_list || ['#155C9C', '#44A79D'];
let legendData = ['AD', 'Non_AD'];
// 标题配置
const titleConfig = {
label: !!ret.title ? ret.title : null,
color: ret.title_font_color || '#333333',
fontSize: ret.title_font_size || 20,
fontStyle: ret.title_font_style ? 'oblique' : 'normal',
fontFamily: ret.title_font_family || 'Arial',
fontWeight: ret.title_font_weight ? 'bold' : 'normal',
frameAnchor: 'top',
textAnchor: 'start'
})
]
});
chartRef.value.appendChild(plotInstance.value);
fontWeight: ret.title_font_weight ? 'bold' : 'normal'
};
// 大椭圆
let ellipticOne = {
rx: (width - marginLeft - marginRight) / 2 - 20,
ry: (height - marginTop - marginBottom) / 2 - 20,
cx: (marginLeft + width - marginLeft - marginRight) / 2 + 60,
cy: (marginTop + height - marginTop - marginBottom) / 2 + 60
};
// 椭圆 AD
let ellipticAD = {
rx: 80,
ry: 350,
cx: (marginLeft + width - marginLeft - marginRight) / 2 + 60,
cy: (marginTop + height - marginTop - marginBottom) / 2 + 60,
rotate: 10,
fill: colors[0],
fillOpacity: 0.2
};
// 椭圆 Non_AD
let ellipticNon_AD = {
rx: 350,
ry: 60,
cx: (marginLeft + width - marginLeft - marginRight) / 2 + 60,
cy: (marginTop + height - marginTop - marginBottom) / 2 + 20,
rotate: 20,
fill: colors[1],
fillOpacity: 0.2
};
const svg = d3.create('svg').attr('width', width).attr('height', height);
let configObj = {
...ret,
svg,
width,
height,
marginTop,
marginRight,
marginBottom,
marginLeft,
titleConfig,
colors,
legendData,
legendPosition,
ellipticOne,
ellipticAD,
ellipticNon_AD
};
setChart(configObj, data);
svgInstance.value = svg.node();
// Append the SVG element.
chartRef.value.appendChild(svgInstance.value);
};
const setChart = (config, data) => {
const color = setColor(config);
const { x, y } = setScale(config, data);
let configObj = {
...config,
color,
x,
y
};
// rect && elliptic
setElliptic(configObj);
// rects
const points = setPoint(configObj, data);
// tips
setTips(points);
// axis
setAxis(configObj);
// legend
config.show_legend && setLegend(configObj);
// label
setLabel(configObj);
};
onMounted(() => {
const chartData = chartMockData;
pubSubInstance.value = PubSub.subscribe('options', (m, data) => {
options.value = data;
chartRef.value.removeChild(plotInstance.value);
chartRef.value.removeChild(svgInstance.value);
generateChart(chartData);
});
generateChart(chartData);
......@@ -117,4 +149,12 @@ onUnmounted(() => {
});
</script>
<style scoped></style>
<style scoped>
.chart-box {
position: relative;
}
.main {
width: 500px;
overflow: hidden;
}
</style>
......@@ -26,6 +26,10 @@
<input type="text" v-model="dynamic_parameter.margin_left" />
</div>
<h3 class="type content">文本</h3>
<div class="form-item">
<span>标题:</span>
<input type="text" v-model="dynamic_parameter.title" />
</div>
<div class="form-item">
<span>字体:</span>
<input type="text" v-model="dynamic_parameter.title_font_family" />
......@@ -55,10 +59,6 @@
<span>显示刻度线:</span>
<input type="checkbox" v-model="dynamic_parameter.show_x_tickMarks" />
</div>
<div class="form-item">
<span>显示网格:</span>
<input type="checkbox" v-model="dynamic_parameter.show_x_gridLine" />
</div>
<div class="form-item">
<span>刻度线颜色:</span>
<input type="text" v-model="dynamic_parameter.x_tickMarks_color" />
......@@ -88,7 +88,11 @@
<input type="checkbox" v-model="dynamic_parameter.xtitle_font_style" />
</div>
<div class="form-item">
<span>x轴标题:</span>
<span>标题展示:</span>
<input type="checkbox" v-model="dynamic_parameter.show_x_title" />
</div>
<div class="form-item">
<span>标题:</span>
<input type="text" v-model="dynamic_parameter.x_title" />
</div>
<h3 class="type content">Y轴配置</h3>
......@@ -100,10 +104,6 @@
<span>显示刻度线:</span>
<input type="checkbox" v-model="dynamic_parameter.show_y_tickMarks" />
</div>
<div class="form-item">
<span>显示网格:</span>
<input type="checkbox" v-model="dynamic_parameter.show_y_gridLine" />
</div>
<div class="form-item">
<span>刻度线颜色:</span>
<input type="text" v-model="dynamic_parameter.y_tickMarks_color" />
......@@ -133,23 +133,22 @@
<input type="checkbox" v-model="dynamic_parameter.ytitle_font_style" />
</div>
<div class="form-item">
<span>x轴标题:</span>
<input type="text" v-model="dynamic_parameter.y_title" />
</div>
<h3 class="type content">图表配置</h3>
<div class="form-item">
<span>散点图半径:</span>
<input type="text" v-model="dynamic_parameter.point_radius" />
<span>标题展示:</span>
<input type="checkbox" v-model="dynamic_parameter.show_y_title" />
</div>
<div class="form-item">
<span>阴影透明度:</span>
<input type="text" v-model="dynamic_parameter.shadow_opacity" />
<span>标题:</span>
<input type="text" v-model="dynamic_parameter.y_title" />
</div>
<h3 class="type content">图例</h3>
<div class="form-item">
<span>显示图例:</span>
<input type="checkbox" v-model="dynamic_parameter.show_legend" />
</div>
<div class="form-item">
<span>图例位置:</span>
<input type="text" v-model="dynamic_parameter.legend_position" />
</div>
<div class="btn">
<button @click="submit">提交</button>
</div>
......@@ -165,39 +164,39 @@ const dynamic_parameter = reactive({
//长宽间距
chart_width: 700,
chart_height: 800,
margin_top: 60,
margin_right: 60,
margin_bottom: 60,
margin_left: 60,
margin_top: 120,
margin_right: 120,
margin_bottom: 120,
margin_left: 120,
//标题配置
title: '',
title: 'Principal',
title_font_family: 'Arial',
title_font_weight: true,
title_font_size: 20,
title_font_color: '#333333',
title_font_style: true,
title_font_style: false,
//横轴标题
xtitle_font_family: 'Arial',
xtitle_font_weight: false,
xtitle_font_style: false,
xtitle_font_size: 16,
xtitle_font_size: 14,
xtitle_font_color: '#333333',
//纵轴标题
ytitle_font_family: 'Arial',
ytitle_font_weight: false,
ytitle_font_style: false,
ytitle_font_size: 16,
ytitle_font_size: 14,
ytitle_font_color: '#333333',
//颜色设置
data_color_list: ['#FF5733', '#39FDBE', '#D6D6D6'],
data_color_list: ['#155C9C', '#44A79D'],
point_radius: 5, // 散点图半径
shadow_opacity: 0.5, // 阴影透明度 0~1
//图例
show_legend: true,
legend_position: '1,1',
legend_position: '0.5,1',
//坐标轴
show_x_axis: true,
show_x_line: true,
......@@ -240,7 +239,7 @@ onMounted(() => {
<style scoped>
.form-item span {
display: inline-block;
width: 100px;
width: 120px;
text-align: left;
}
.btn {
......
export const chartData = [
{ type: 'type1', x: '-30', y: '-90' },
{ type: 'type1', x: '-30', y: '10' },
{ type: 'type1', x: '-15', y: '20' },
{ type: 'type1', x: '15', y: '18' },
export const chartMockData = [
{
"name": "AD",
"PC1": -13.6199,
"PC2": 19.2571
},
{
"name": "AD",
"PC1": -26.9176,
"PC2": -83.0864
},
{
"name": "AD",
"PC1": 10.7576,
"PC2": 16.4817
},
{
"name": "AD",
"PC1": -26.8031,
"PC2": 10.3311
},
{
"name": "Non_AD",
"PC1": -31.6168,
"PC2": 11.5461
},
{
"name": "Non_AD",
"PC1": 122.4475,
"PC2": -8.1394
},
{
"name": "Non_AD",
"PC1": -5.0412,
"PC2": 19.3962
},
{
"name": "Non_AD",
"PC1": -29.2065,
"PC2": 14.2135
}
]
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
// color config
export const setColor = (config) => {
let colors = ['#2265A1', '#44A79D'];
if (Array.isArray(config.colors)) {
colors = !!config.colors.length ? config.colors : colors;
} else if (!!config.colors) {
colors = config.colors.split(',');
}
return d3.scaleOrdinal(colors).domain(config.legendData);
};
// tips
export const setTips = (rect) => {
const tips = d3
.select('.tip')
.style('visibility', 'hidden')
.style('position', 'relative')
.style('background-color', 'rgba(0, 0, 0, .8)')
.style('color', '#fff')
.style('border-radius', '5px')
.style('width', '120px')
.style('height', '65px')
.style('text-align', 'left')
.style('padding', '5px 10px')
rect.on('mouseover', (event, d) => {
tips.style('visibility', 'visible');
})
.on('mousemove', function (event, d) {
tips.style('visibility', 'visible')
.style('left', event.offsetX + 15 + 'px')
.style('top', event.offsetY + 55 + 'px')
.html(`
<div>${d.name}</div>
<div>x: ${d.PC1}</div>
<div>y: ${d.PC2}</div>
`);
})
.on('mouseleave', function () {
tips.style('visibility', 'hidden');
});
};
// legend
export const setLegend = (config) => {
const g = config.svg.append('g')
.attr('transform', `translate(${(config.width - 210) * config.legendPosition[0]}, ${(config.height - 35) * config.legendPosition[1]})`)
g.append('rect').attr('width', 200)
.attr('height', 30)
.attr('stroke', 'black')
.attr("stroke-width", 2)
.attr('fill', 'transparent')
g.append('g')
.attr('transform', `translate(0, 15)`)
.selectAll()
.data(config.legendData)
.join('g')
.attr('transform', (d, i) => `translate(${i * 90 + 20}, 0)`)
.call((g) =>
g.append("path")
.attr("class", "dot")
.attr("opacity", 1)
.attr("stroke-opacity", 1)
.attr("stroke-width", 2)
.attr("stroke", (d) => config.color(d))
.attr("fill", 'transparent')
.attr("fill-opacity", 1)
.attr("d", (d) => setSymbol(d)))
.call((g) =>
g
.append('text')
.attr('x', 15)
.attr('y', 0)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
};
// scale
export const setScale = (config, data) => {
const xMax = d3.max(data, (d) => d.PC1)
const xMin = d3.min(data, (d) => d.PC1)
const yMax = d3.min(data, (d) => d.PC2)
const yMin = d3.min(data, (d) => d.PC2)
const xDomain = (Math.abs(xMax) >= Math.abs(xMin) ? Math.abs(xMax) : Math.abs(xMin)) + 10
const yDomain = (Math.abs(yMax) >= Math.abs(yMin) ? Math.abs(yMax) : Math.abs(yMin)) + 10
const x = d3
.scaleLinear()
.domain([-xDomain, xDomain])
.range([config.marginLeft, config.width - config.marginRight])
const y = d3
.scaleLinear()
.domain([-yDomain, yDomain])
.range([config.height - config.marginBottom, config.marginTop])
return { x, y }
}
// axis
export const setAxis = (config) => {
config.show_x_axis && config.svg.append('g')
.attr('transform', `translate(0, ${config.height - config.marginBottom})`)
.style('font', `${config.xtitle_font_size}px ${config.xtitle_font_family}`)
.style('color', `${config.xtitle_font_color}`)
.style('font-weight', `${config.xtitle_font_weight ? 'bold' : 'normal'}`)
.style('font-style', `${config.xtitle_font_style ? 'oblique' : 'normal'}`)
.call(d3.axisBottom(config.x).tickSize(Number(config.show_x_tickMarks ? config.x_tickMarks_length : 0)))
.call(g => g.selectAll("line").clone()
.attr("display", d => d == 0 ? 'block' : 'none')
.attr("stroke-dasharray", '3')
.attr("y2", -(config.height - config.marginBottom - config.marginTop))
)
.call((g) => g.selectAll('.domain').remove())
.selectAll('text')
.style('text-anchor', 'middle');
config.show_x_title && config.svg.append('g')
.attr('transform', `translate(${config.width / 2}, ${config.height - config.marginBottom + 50})`)
.append('text')
.style('text-anchor', 'middle')
.text(config.x_title)
config.show_y_axis && config.svg.append('g')
.attr('transform', `translate(${config.marginLeft}, 0)`)
.style('font', `${config.ytitle_font_size}px ${config.ytitle_font_family}`)
.style('color', `${config.ytitle_font_color}`)
.style('font-weight', `${config.ytitle_font_weight ? 'bold' : 'normal'}`)
.style('font-style', `${config.ytitle_font_style ? 'oblique' : 'normal'}`)
.call(d3.axisLeft(config.y).tickSize(Number(config.show_y_tickMarks ? config.y_tickMarks_length : 0)))
.call(g => g.selectAll("line").clone()
.attr("display", d => d == 0 ? 'block' : 'none')
.attr("stroke-dasharray", '3')
.attr("x2", config.width - config.marginLeft - config.marginRight))
.call((g) => g.selectAll('.domain').remove())
config.show_y_title && config.svg.append('g')
.attr('transform', `translate(${config.marginLeft - 50}, ${config.height / 2})`)
.append('text')
.style('text-anchor', 'middle')
.attr('transform', `rotate(-90)`)
.text(config.y_title)
}
// symbols
export const setSymbol = (type) => {
let symbol = d3.symbol().size(30)
return type == 'AD' ? symbol.type(d3.symbols[0])() : symbol.type(d3.symbols[5])()
}
// rect && elliptic
export const setElliptic = (config) => {
let rects = config.svg
.append('g')
.attr('max-width', config.width - config.marginLeft - config.marginRight)
.attr('height', config.height - config.marginTop - config.marginBottom);
rects
.append('rect')
.attr('x', `${config.marginLeft}`)
.attr('y', `${config.marginTop}`)
.attr('width', `${config.width - config.marginLeft - config.marginRight}`)
.attr('height', `${config.height - config.marginTop - config.marginBottom}`)
.attr('stroke', '#000000')
.attr('stroke-width', 2)
.attr('fill', 'white');
rects
.append('ellipse')
.attr('cx', `${config.ellipticOne.cx}`)
.attr('cy', `${config.ellipticOne.cy}`)
.attr('rx', `${config.ellipticOne.rx}`)
.attr('ry', `${config.ellipticOne.ry}`)
.attr('stroke', '#ADADAD')
.attr('stroke-width', 3)
.attr('fill', 'transparent');
rects
.append('ellipse')
.attr('cx', `${config.ellipticAD.cx}`)
.attr('cy', `${config.ellipticAD.cy}`)
.attr('rx', `${config.ellipticAD.rx}`)
.attr('ry', `${config.ellipticAD.ry}`)
.attr('transform-origin', 'center')
.attr('transform', `rotate(${config.ellipticAD.rotate})`)
.attr('fill', config.ellipticAD.fill)
.attr('fill-opacity', config.ellipticAD.fillOpacity);
rects
.append('ellipse')
.attr('cx', `${config.ellipticNon_AD.cx}`)
.attr('cy', `${config.ellipticNon_AD.cy}`)
.attr('rx', `${config.ellipticNon_AD.rx}`)
.attr('ry', `${config.ellipticNon_AD.ry}`)
.attr('transform-origin', 'center')
.attr('transform', `rotate(${config.ellipticNon_AD.rotate})`)
.attr('fill', config.ellipticNon_AD.fill)
.attr('fill-opacity', config.ellipticNon_AD.fillOpacity);
}
// point
export const setPoint = (config, data) => {
return config.svg
.append("g")
.selectAll("path")
.data(data)
.join("path")
.attr("class", "dot")
.attr("opacity", 1)
.attr("stroke-opacity", 1)
.attr("stroke-width", 2)
.attr("stroke", (d) => config.color(d.name))
.attr("fill", 'transparent')
.attr("fill-opacity", 1)
.attr("d", (d) => setSymbol(d.name))
.attr("transform", d => `translate(${config.x(d.PC1)}, ${config.y(d.PC2)})`)
}
// title
export const setLabel = (config) => {
config.svg.append('text')
.attr('x', config.width / 2)
.attr('y', config.marginTop - 50)
.attr('fill', config.titleConfig.color)
.attr('font-size', `${config.titleConfig.fontSize}px`)
.attr('font-family', `${config.titleConfig.fontFamily}`)
.attr('font-style', `${config.titleConfig.fontStyle}`)
.attr('font-weight', `${config.titleConfig.fontWeight}`)
.style('text-anchor', 'middle')
.text(`${config.titleConfig.label}`);
};
......@@ -8,7 +8,7 @@
import { ref, onMounted } from 'vue';
import { chartMockData } from './mock.js';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
import { setTips, setMaxMinNumber, setLegend, setScale, setAxis, setRect, setTopLeftColor, getRootConfig, setTree } from './utils';
import { setTips, setColor, setLegend, setScale, setAxis, setPoint, setLabel, setElliptic } from './utils';
const chartRef = ref(null);
......@@ -19,21 +19,17 @@ const options = ref({});
const generateChart = (data) => {
const ret = options.value;
let width = 1000;
let height = 700;
let width = 700;
let height = 800;
let marginTop = 120;
let marginRight = 120;
let marginBottom = 60;
let marginBottom = 120;
let marginLeft = 120;
let dendWidth = 100;
let legendData = [0.8, 0.85, 0.9, 0.95, 1];
let colorList = [
['#3DD378', '#EC0B88', '#444444', '#5E35B1', '#F9791E', '#C6C6C6', '#F7DC05', '#3D98D3'],
['#017A4A', '#FFCE4E', '#3D98D3', '#FF363C', '#794924', '#8CDB5E', '#D6D6D6', '#FB8C00']
];
let legendPosition = [0, 0];
let heatWidth = width - marginLeft - marginRight;
let legendData = ['AD', 'Non_AD'];
let legendPosition = [0.5, 1];
let colors = ['#155C9C', '#44A79D'];
// 标题配置
const titleConfig = {
label: !!ret.title ? ret.title : null,
color: ret.title_font_color || '#333333',
......@@ -43,6 +39,32 @@ const generateChart = (data) => {
fontWeight: ret.title_font_weight ? 'bold' : 'normal'
};
// 大椭圆
let ellipticOne = {
rx: (width - marginLeft - marginRight) / 2 - 20,
ry: (height - marginTop - marginBottom) / 2 - 20,
cx: (marginLeft + width - marginLeft - marginRight) / 2 + 60,
cy: (marginTop + height - marginTop - marginBottom) / 2 + 60
};
// 椭圆 AD
let ellipticAD = {
rx: 80,
ry: 350,
cx: (marginLeft + width - marginLeft - marginRight) / 2 + 60,
cy: (marginTop + height - marginTop - marginBottom) / 2 + 60,
rotate: 10
};
// 椭圆 Non_AD
let ellipticNon_AD = {
rx: 350,
ry: 60,
cx: (marginLeft + width - marginLeft - marginRight) / 2 + 60,
cy: (marginTop + height - marginTop - marginBottom) / 2 + 20,
rotate: 20
};
const svg = d3.create('svg').attr('width', width).attr('height', height);
let configObj = {
......@@ -53,12 +75,13 @@ const generateChart = (data) => {
marginRight,
marginBottom,
marginLeft,
dendWidth,
titleConfig,
colors,
legendData,
colorList,
heatWidth,
legendPosition
legendPosition,
ellipticOne,
ellipticAD,
ellipticNon_AD
};
setChart(configObj, data);
......@@ -68,45 +91,33 @@ const generateChart = (data) => {
};
const setChart = (config, data) => {
const categories = data.map((d) => d.index);
const newArr = data.map((d) => Object.keys(d).map((e) => d[e])).map((item) => ({ index: item[0], values: item.slice(1) }));
const { maxNumber, minNumber } = setMaxMinNumber(data);
const color = setColor(config);
const { x, y } = setScale(config, data);
const color = d3.scaleSequential([minNumber, maxNumber], d3.interpolateMagma);
let configObj = {
...config,
categories,
color
};
const { x, y } = setScale(config, categories);
const { root } = getRootConfig(config, newArr);
configObj = {
...configObj,
root,
color,
x,
y
};
// elliptic
setElliptic(configObj);
// rects
const rects = setRect(configObj, data);
const points = setPoint(configObj, data);
// tips
setTips(rects);
setTips(points);
// axis
setAxis(configObj);
// topLeftColor
setTopLeftColor(configObj, data);
// legend
setLegend(configObj);
// tree
setTree(configObj);
// label
setLabel(configObj);
};
onMounted(() => {
......@@ -119,4 +130,8 @@ onMounted(() => {
.chart-box {
position: relative;
}
.main {
width: 500px;
overflow: hidden;
}
</style>
export const chartMockData = [
{
"index": "AD1",
"AD1": "1",
"AD2": "0.84",
"AD3": "0.83",
"AD4": "0.9",
"Non_AD_5": "0.95",
"Non_AD_6": "0.81",
"Non_AD_7": "0.98",
"Non_AD_8": "1.0",
"name": "AD",
"x": -13.6199,
"y": 19.2571
},
{
"index": "AD2",
"AD1": "0.8",
"AD2": "0.9",
"AD3": "0.85",
"AD4": "0.95",
"Non_AD_5": "1",
"Non_AD_6": "0.92",
"Non_AD_7": "0.91",
"Non_AD_8": "0.81",
"name": "AD",
"x": -26.9176,
"y": -83.0864
},
{
"index": "AD3",
"AD1": "1",
"AD2": "0.99",
"AD3": "0.91",
"AD4": "0.98",
"Non_AD_5": "0.95",
"Non_AD_6": "0.93",
"Non_AD_7": "0.94",
"Non_AD_8": "1.0",
"name": "AD",
"x": 10.7576,
"y": 16.4817
},
{
"index": "AD4",
"AD1": "0.99",
"AD2": "0.81",
"AD3": "0.82",
"AD4": "0.93",
"Non_AD_5": "0.99",
"Non_AD_6": "0.98",
"Non_AD_7": "0.91",
"Non_AD_8": "0.97",
"name": "AD",
"x": -26.8031,
"y": 10.3311
},
{
"index": "Non_AD_5",
"AD1": "0.99",
"AD2": "0.98",
"AD3": "0.97",
"AD4": "0.95",
"Non_AD_5": "0.91",
"Non_AD_6": "0.99",
"Non_AD_7": "0.98",
"Non_AD_8": "0.95",
"name": "Non_AD",
"x": -31.6168,
"y": 11.5461
},
{
"index": "Non_AD_6",
"AD1": "0.98",
"AD2": "0.97",
"AD3": "0.92",
"AD4": "0.9",
"Non_AD_5": "0.95",
"Non_AD_6": "0.99",
"Non_AD_7": "0.89",
"Non_AD_8": "0.87",
"name": "Non_AD",
"x": 122.4475,
"y": -8.1394
},
{
"index": "Non_AD_7",
"AD1": "0.99",
"AD2": "0.98",
"AD3": "0.89",
"AD4": "0.91",
"Non_AD_5": "0.92",
"Non_AD_6": "0.89",
"Non_AD_7": "0.92",
"Non_AD_8": "1",
"name": "Non_AD",
"x": -5.0412,
"y": 19.3962
},
{
"index": "Non_AD_8",
"AD1": "0.96",
"AD2": "0.99",
"AD3": "0.97",
"AD4": "0.94",
"Non_AD_5": "0.94",
"Non_AD_6": "0.89",
"Non_AD_7": "0.91",
"Non_AD_8": "1",
"name": "Non_AD",
"x": -29.2065,
"y": 14.2135
}
]
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
import hcluster from '../../utils/hcluster';
// max min
export const setMaxMinNumber = (data) => {
const newArr = data.map((d) => Object.keys(d).map((e) => d[e])).map((f) => f.slice(1))
let maxNumArr = [],
minNumArr = [],
maxNumber = 0,
minNumber = 0;
for (let i = 0; i < newArr.length; i++) {
maxNumArr.push(d3.max(newArr[i]));
minNumArr.push(d3.min(newArr[i]));
}
// 最大值 最小值
maxNumber = d3.max(maxNumArr);
minNumber = d3.min(minNumArr);
return {
maxNumber,
minNumber
}
}
// scale
export const coordinateScale = (config, data) => {
const x = d3
.scaleLinear()
.domain([config.paddedMin, config.paddedMax])
.range([config.marginLeft, config.width - config.marginRight]);
const y = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.y))
.range([config.rectHeight, config.marginTop + 30]);
return {
x, y
// color config
export const setColor = (config) => {
let colors = ['#2265A1', '#44A79D'];
if (Array.isArray(config.colors)) {
colors = !!config.colors.length ? config.colors : colors;
} else if (!!config.colors) {
colors = config.colors.split(',');
}
}
return d3.scaleOrdinal(colors).domain(config.legendData);
};
// tips
export const setTips = (rect) => {
......@@ -48,11 +20,10 @@ export const setTips = (rect) => {
.style('background-color', 'rgba(0, 0, 0, .8)')
.style('color', '#fff')
.style('border-radius', '5px')
.style('width', '80px')
.style('height', '25px')
.style('line-height', '25px')
.style('text-align', 'center')
.style('padding', '5px')
.style('width', '120px')
.style('height', '65px')
.style('text-align', 'left')
.style('padding', '5px 10px')
rect.on('mouseover', (event, d) => {
tips.style('visibility', 'visible');
......@@ -61,7 +32,11 @@ export const setTips = (rect) => {
tips.style('visibility', 'visible')
.style('left', event.offsetX + 15 + 'px')
.style('top', event.offsetY + 55 + 'px')
.html(`${d.count}`);
.html(`
<div>${d.name}</div>
<div>x: ${d.x}</div>
<div>y: ${d.y}</div>
`);
})
.on('mouseleave', function () {
tips.style('visibility', 'hidden');
......@@ -71,173 +46,189 @@ export const setTips = (rect) => {
// legend
export const setLegend = (config) => {
config.svg.append('g')
.attr('transform', `translate(${(config.width - 100) * config.legendPosition[0]}, ${(config.height - 100) * config.legendPosition[1]})`)
const g = config.svg.append('g')
.attr('transform', `translate(${(config.width - 210) * config.legendPosition[0]}, ${(config.height - 35) * config.legendPosition[1]})`)
g.append('rect').attr('width', 200)
.attr('height', 30)
.attr('stroke', 'black')
.attr("stroke-width", 2)
.attr('fill', 'transparent')
g.append('g')
.attr('transform', `translate(0, 15)`)
.selectAll()
.data(config.legendData)
.join('g')
.attr('transform', (d, i, nodes) => `translate(0, ${(nodes.length - i) * 20 - 20})`)
.call((g) => g.append('rect').attr('width', 40).attr('height', 20).attr('fill', d => config.color(d)))
.attr('transform', (d, i) => `translate(${i * 90 + 20}, 0)`)
.call((g) =>
g.append("path")
.attr("class", "dot")
.attr("opacity", 1)
.attr("stroke-opacity", 1)
.attr("stroke-width", 2)
.attr("stroke", (d) => config.color(d))
.attr("fill", 'transparent')
.attr("fill-opacity", 1)
.attr("d", (d) => setSymbol(d)))
.call((g) =>
g
.append('text')
.attr('x', 45)
.attr('y', 11)
.attr('x', 15)
.attr('y', 0)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
};
// topLeft Color
export const setTopLeftColor = (config, data) => {
// scale
export const setScale = (config, data) => {
config.svg
.append('g')
.attr('transform', `translate(${config.marginLeft}, ${config.marginTop - 20})`)
.selectAll()
.data(data)
.join('g')
.append('rect')
.attr('x', (d, i) => config.x.bandwidth() * i)
.attr('y', 0)
.attr('width', config.x.bandwidth())
.attr('height', 20)
.style('fill', (d, i) => config.colorList[0][i]);
const xMax = d3.max(data, (d) => d.x)
const xMin = d3.min(data, (d) => d.x)
const yMax = d3.min(data, (d) => d.y)
const yMin = d3.min(data, (d) => d.y)
const xDomain = (Math.abs(xMax) >= Math.abs(xMin) ? Math.abs(xMax) : Math.abs(xMin)) + 10
const yDomain = (Math.abs(yMax) >= Math.abs(yMin) ? Math.abs(yMax) : Math.abs(yMin)) + 10
config.svg
.append('g')
.attr('transform', `translate(${config.marginLeft - 20}, ${config.marginTop})`)
.selectAll()
.data(data)
.join('g')
.append('rect')
.attr('x', (d, i) => 0)
.attr('y', (d, i) => config.y.bandwidth() * i)
.attr('width', 20)
.attr('height', config.y.bandwidth())
.style('fill', (d, i) => config.colorList[1][i]);
}
// scale
export const setScale = (config, categories) => {
const x = d3
.scaleBand()
.domain(categories)
.range([config.marginLeft, config.width - config.marginRight]);
.scaleLinear()
.domain([-xDomain, xDomain])
.range([config.marginLeft, config.width - config.marginRight])
const y = d3
.scaleBand()
.domain([...categories].reverse())
.range([config.height - config.marginBottom, config.marginTop]);
.scaleLinear()
.domain([-yDomain, yDomain])
.range([config.height - config.marginBottom, config.marginTop])
return { x, y }
}
// axis
export const setAxis = (config) => {
const xAxis = (g) =>
g
config.svg.append('g')
.attr('transform', `translate(0, ${config.height - config.marginBottom})`)
// .style('font', '12px verdana')
.style('font', '12px verdana')
.call(d3.axisBottom(config.x).tickSizeOuter(0))
.call(g => g.selectAll("line").clone()
.attr("display", d => d == 0 ? 'block' : 'none')
.attr("stroke-dasharray", '3')
.attr("y2", -(config.height - config.marginBottom - config.marginTop))
)
.call((g) => g.selectAll('.domain').remove())
.selectAll('text')
.style('text-anchor', 'middle');
const yAxis = (g) =>
g
.attr('transform', `translate(${config.width - config.marginRight + 5}, 0)`)
// .style('font', '12px verdana')
config.svg.append('g')
.attr('transform', `translate(${config.width / 2}, ${config.height - config.marginBottom + 50})`)
.append('text')
.text('PC1 - Proportaion of variance: 0.4')
.style('text-anchor', 'middle')
config.svg.append('g')
.attr('transform', `translate(${config.marginLeft}, 0)`)
.style('font', '12px verdana')
.call(d3.axisLeft(config.y).ticks(null, 's'))
.call(g => g.selectAll("line").clone()
.attr("display", d => d == 0 ? 'block' : 'none')
.attr("stroke-dasharray", '3')
.attr("x2", config.width - config.marginLeft - config.marginRight))
.call((g) => g.selectAll('.domain').remove())
.selectAll('text')
.style('text-anchor', 'start')
.attr('transform', `translate(10, 0)`);
config.svg.append('g').call(xAxis);
config.svg.append('g')
.attr('transform', `translate(${config.marginLeft - 50}, ${config.height / 2})`)
.append('text')
.text('PC2 - Proportaion of variance: 0.18')
.style('text-anchor', 'middle')
.attr('transform', `rotate(-90)`)
}
config.svg.append('g').call(yAxis);
// symbols
export const setSymbol = (type) => {
let symbol = d3.symbol().size(30)
return type == 'AD' ? symbol.type(d3.symbols[0])() : symbol.type(d3.symbols[5])()
}
// rect
export const setRect = (config, data) => {
const rects = config.svg
.append('g')
.selectAll('g')
.data(data)
.enter()
// elliptic
export const setElliptic = (config) => {
let rects = config.svg
.append('g')
.attr('class', (d, i) => `${i} bar`)
.selectAll('g')
.data((d) =>
config.categories.map((e) => {
return { xval: d.index, yval: e, count: d[e] };
})
)
.enter()
.append('g');
.attr('max-width', config.width - config.marginLeft - config.marginRight)
.attr('height', config.height - config.marginTop - config.marginBottom);
rects
.append('rect')
.attr('x', (d) => config.x(d.xval))
.attr('y', (d) => config.y(d.yval))
.attr('width', config.x.bandwidth())
.attr('height', config.y.bandwidth())
.style('fill', (d) => config.color(d.count));
.attr('x', `${config.marginLeft}`)
.attr('y', `${config.marginTop}`)
.attr('width', `${config.width - config.marginLeft - config.marginRight}`)
.attr('height', `${config.height - config.marginTop - config.marginBottom}`)
.attr('stroke', '#000000')
.attr('stroke-width', 2)
.attr('fill', 'white');
return rects
}
rects
.append('ellipse')
.attr('cx', `${config.ellipticOne.cx}`)
.attr('cy', `${config.ellipticOne.cy}`)
.attr('rx', `${config.ellipticOne.rx}`)
.attr('ry', `${config.ellipticOne.ry}`)
.attr('stroke', '#ADADAD')
.attr('stroke-width', 3)
.attr('fill', 'transparent');
// root config
export const getRootConfig = (config, chartData) => {
const normalizedData = () => {
let minmaxes = d3
.range(chartData[0].values.length)
.map((d) => [d3.min(chartData.map((el) => el.values[d])), d3.max(chartData.map((el) => el.values[d]))]);
return chartData.map((el) => ({
...el,
normalized: el.values.map((n, i) => (n - minmaxes[i][0]) / (minmaxes[i][1] - minmaxes[i][0]))
}));
};
const clustering = hcluster().distance('angular').linkage('avg').posKey('normalized').data(normalizedData());
const tree = (data) => {
const root = d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height));
root.dx = 0;
root.dy = config.dendWidth / (root.height + 1);
return d3
.cluster()
.size([config.heatWidth, config.dendWidth])
.separation(() => 2)(root);
};
const root = tree(clustering.tree());
return {
root,
}
rects
.append('ellipse')
.attr('cx', `${config.ellipticAD.cx}`)
.attr('cy', `${config.ellipticAD.cy}`)
.attr('rx', `${config.ellipticAD.rx}`)
.attr('ry', `${config.ellipticAD.ry}`)
.attr('transform-origin', 'center')
.attr('transform', `rotate(${config.ellipticAD.rotate})`)
.attr('fill', config.colors[0])
.attr('fill-opacity', 0.2);
rects
.append('ellipse')
.attr('cx', `${config.ellipticNon_AD.cx}`)
.attr('cy', `${config.ellipticNon_AD.cy}`)
.attr('rx', `${config.ellipticNon_AD.rx}`)
.attr('ry', `${config.ellipticNon_AD.ry}`)
.attr('transform-origin', 'center')
.attr('transform', `rotate(${config.ellipticNon_AD.rotate})`)
.attr('fill', config.colors[1])
.attr('fill-opacity', 0.2);
}
// tree
export const setTree = (config) => {
// top tree
config.svg.append('g')
.attr('fill', 'none')
.attr('stroke', '#555')
.attr('stroke-width', 1)
.attr('transform', `translate(${config.dendWidth + 20}, 0)`)
.selectAll('path')
.data(config.root.links())
.join('path')
.attr(
'd',
(d) => `
M${d.target.x},${d.target.y}
L${d.target.x},${d.source.y}
${d.source.x},${d.source.y}
${d.source.x},${d.source.y}
`
);
// point
export const setPoint = (config, data) => {
return config.svg
.append("g")
.selectAll("path")
.data(data)
.join("path")
.attr("class", "dot")
.attr("opacity", 1)
.attr("stroke-opacity", 1)
.attr("stroke-width", 2)
.attr("stroke", (d) => config.color(d.name))
.attr("fill", 'transparent')
.attr("fill-opacity", 1)
.attr("d", (d) => setSymbol(d.name))
.attr("transform", d => `translate(${config.x(d.x)}, ${config.y(d.y)})`)
}
// title
export const setLabel = (config) => {
config.svg.append('text')
.attr('x', config.width / 2)
.attr('y', 30)
.attr('fill', config.titleConfig.color)
.attr('font-size', `${config.titleConfig.fontSize}px`)
.attr('font-family', `${config.titleConfig.fontFamily}`)
.attr('font-style', `${config.titleConfig.fontStyle}`)
.attr('font-weight', `${config.titleConfig.fontWeight}`)
.style('text-anchor', 'middle')
.text('Principal');
};
......@@ -21,7 +21,7 @@ import ClusterHeatmapSecond from '../../components/ClusterHeatmapSecond/index.vu
import CircularHeatmap from '../../components/CircularHeatmap/index.vue';
import Test from '../../components/Test/index.vue';
const chartCurrent = ref(6);
const chartCurrent = ref(0);
const chartList = shallowRef([
{ title: '线性拟合图', value: 1, comName: LinearFitting },
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论