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

feat: Feature Plot

上级 ce04e1f5
<template>
<div ref="chartRef" class="chart-box">
<div class="tip"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
import { chartMockData } from '../utils/mock';
import { setTips, setMaxMinNumber, coordinateScale, setColor, setCluster, setLegend, setLabel } from '../utils/index';
import PubSub from 'pubsub-js';
// 根节点实例
const chartRef = ref(null);
// 配置项数据
const options = ref({});
// pubsub实例
const pubSubInstance = ref(null);
// 图表实例
const svgInstance = ref(null);
// 生成图表
const generateChart = () => {
const ret = options.value;
let width = Number(ret.chart_width) || 500;
let height = Number(ret.chart_height) || 1200;
let marginTop = Number(ret.margin_top) || 30;
let marginRight = Number(ret.margin_right) || 30;
let marginBottom = Number(ret.margin_bottom) || 30;
let marginLeft = Number(ret.margin_left) || 30;
let radius = Number(ret.point_radius) || 4;
let rectHeight = 300;
let colorList = [
['#E53935', '#1E88E5'],
['#AA40FC', '#FFBB78'],
['#17BECF', '#E377C2']
];
let titles = ['UMAP1', 'UMAP2'];
let labels = ['main_cluster_id_0.4', 'celltype', 'sample_id'];
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'
};
const svg = d3.create('svg').attr('width', width).attr('height', height);
let configObj = {
svg,
width,
height,
marginTop,
marginRight,
marginBottom,
marginLeft,
radius,
rectHeight,
titleConfig,
titles,
labels,
colorList
};
let chartArr = [chartMockData, chartMockData, chartMockData];
for (let i = 0; i < chartArr.length; i++) {
setChart(configObj, chartArr[i], i);
}
svgInstance.value = svg.node();
// Append the SVG element.
chartRef.value.appendChild(svgInstance.value);
};
const setChart = (config, data, index) => {
let position = {
rect: [config.marginLeft, (config.rectHeight + 80) * index + config.marginTop],
cluster: [0, (config.rectHeight + 80) * index],
titleTop: [config.width / 2, (config.rectHeight + 80) * index + 20],
titleBottom: [config.width / 2, (config.rectHeight + 80) * index + config.rectHeight + 50],
titleLeft: [-config.rectHeight / 2 - (config.rectHeight + 80) * index, 20]
};
config.svg
.append('rect')
.attr('x', position.rect[0])
.attr('y', position.rect[1])
.attr('width', config.width - config.marginTop - config.marginRight)
.attr('height', config.rectHeight)
.attr('stroke', '#111111')
.attr('fill', 'white');
const { paddedMin, paddedMax } = setMaxMinNumber(data);
let label = config.labels[index];
let colors = config.colorList[index];
let configObj = { ...config, paddedMin, paddedMax, position, label, colors };
const { x, y } = coordinateScale(configObj, data);
const color = setColor(configObj);
configObj = { ...configObj, x, y, color };
const cluster = setCluster(configObj, data);
setTips(cluster);
// label
setLabel(configObj);
};
onMounted(() => {
pubSubInstance.value = PubSub.subscribe('options', (m, data) => {
options.value = data;
chartRef.value.removeChild(svgInstance.value);
generateChart();
});
generateChart();
});
onUnmounted(() => {
PubSub.unsubscribe(pubSubInstance.value);
});
</script>
<style scoped>
.chart-box {
position: relative;
}
</style>
<template>
<div class="form-container">
<h3 class="type content">画布</h3>
<div class="form-item">
<span>宽:</span>
<input type="text" v-model="dynamic_parameter.chart_width" />
</div>
<div class="form-item">
<span>高:</span>
<input type="text" v-model="dynamic_parameter.chart_height" />
</div>
<div class="form-item">
<span>上边距:</span>
<input type="text" v-model="dynamic_parameter.margin_top" />
</div>
<div class="form-item">
<span>右边距:</span>
<input type="text" v-model="dynamic_parameter.margin_right" />
</div>
<div class="form-item">
<span>下边距:</span>
<input type="text" v-model="dynamic_parameter.margin_bottom" />
</div>
<div class="form-item">
<span>左边距:</span>
<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_font_family" />
</div>
<div class="form-item">
<span>字号:</span>
<input type="text" v-model="dynamic_parameter.title_font_size" />
</div>
<div class="form-item">
<span>字体颜色:</span>
<input type="text" v-model="dynamic_parameter.title_font_color" />
</div>
<div class="form-item">
<span>字体加粗:</span>
<input type="checkbox" v-model="dynamic_parameter.title_font_weight" />
</div>
<div class="form-item">
<span>斜体:</span>
<input type="checkbox" v-model="dynamic_parameter.title_font_style" />
</div>
<h3 class="type content">图表配置</h3>
<div class="form-item">
<span>散点半径:</span>
<input type="text" v-model="dynamic_parameter.point_radius" />
</div>
<!-- <h3 class="type content">图例</h3>
<div class="form-item">
<span>显示图例:</span>
<input type="checkbox" v-model="dynamic_parameter.show_legend" />
</div> -->
<div class="btn">
<button @click="submit">提交</button>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted } from 'vue';
import PubSub from 'pubsub-js';
//动态图参数,固定参数
const dynamic_parameter = reactive({
//长宽间距
chart_width: 500,
chart_height: 1200,
margin_top: 30,
margin_right: 30,
margin_bottom: 30,
margin_left: 30,
//标题配置
title: '',
title_font_family: 'Arial',
title_font_weight: false,
title_font_size: 20,
title_font_color: '#333333',
title_font_style: false,
//横轴标题
xtitle_font_family: 'Arial',
xtitle_font_weight: false,
xtitle_font_style: false,
xtitle_font_size: 16,
xtitle_font_color: '#333333',
//纵轴标题
ytitle_font_family: 'Arial',
ytitle_font_weight: false,
ytitle_font_style: false,
ytitle_font_size: 16,
ytitle_font_color: '#333333',
//颜色设置
data_color_list: ['#E53935', '#1E88E5'],
point_radius: 4, // 散点图半径
//图例
show_legend: true,
legend_position: '1,1',
//坐标轴
show_x_axis: true,
show_x_line: true,
show_x_tickMarks: true,
show_x_gridLine: false,
show_x_title: true,
x_title: '',
x_tickMarks_color: '#333333',
x_tickMarks_length: '5',
show_y_axis: true,
show_y_line: true,
show_y_tickMarks: true,
show_y_gridLine: false,
show_y_title: true,
y_title: '',
y_tickMarks_color: '#333333',
y_tickMarks_length: '5',
//节点相关的参数,可能还会有更多
node_id: null, //节点id
node_color: null, //节点颜色
is_delect_node: false, //是否删除节点
node_search_name: null, //节点名称搜索
node_search_color: '#FF0404', //节点搜索后的颜色
node_edit_name: null, //修改节点名称
node_height: 100, //节点大小
path_id: null
});
const submit = () => {
PubSub.publish('options', dynamic_parameter);
};
onMounted(() => {
submit();
});
</script>
<style scoped>
.form-item span {
display: inline-block;
width: 100px;
text-align: left;
}
.btn {
width: 100%;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
<template>
<div class="chart-container">
<Chart />
<Config />
</div>
</template>
<script setup>
import Chart from './components/chart.vue';
import Config from './components/config.vue';
</script>
<style scoped>
.chart-container {
display: flex;
}
</style>
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
// color config
export const setColor = (config) => {
let colors = ['#E53935', '#1E88E5'];
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 || []);
};
// paddedMin paddedMax
export const setMaxMinNumber = (data) => {
const paddedMin = d3.min(data, (d) => d.x) - d3.min(data, (d) => d.y) * -0.1;
const paddedMax = d3.max(data, (d) => d.x) + d3.max(data, (d) => d.y) * 0.1;
return {
paddedMin,
paddedMax
}
}
// cluster
export const setCluster = (config, data) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.position.cluster[0]}, ${config.position.cluster[1]})`)
.style('isolation', 'isolate')
.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('class', 'points')
.attr('cx', (d) => config.x(d.x))
.attr('cy', (d) => config.y(d.y))
.attr('fill', (d) => config.color(d.group))
.attr('opacity', 0.7)
.attr('r', config.radius);
}
// 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
}
}
// 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', '200px')
.style('height', '45px')
.style('text-align', 'left')
.style('padding', '5px')
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 + 90 + 'px')
.html(`
<div>x: ${d.x}</div>
<div>y: ${d.y}</div>`);
})
.on('mouseleave', function () {
tips.style('visibility', 'hidden');
});
};
// legend
export const setLegend = (config) => {
config.svg.append('g')
.attr('transform', `translate(${config.width - config.marginLeft - 70}, ${100})`)
.selectAll()
.data(config.legendData)
.join('g')
.attr('transform', (d, i) => `translate(0, ${i * 20})`)
.call((g) => g.append('rect').attr('width', 15).attr('height', 15).attr('fill', d => config.color(d)))
.call((g) =>
g
.append('text')
.attr('x', 20)
.attr('y', 8)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
};
// title
export const setLabel = (config) => {
config.svg.append('text')
.attr('x', config.position.titleTop[0])
.attr('y', config.position.titleTop[1])
.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.label);
config.svg.append('text')
.attr('x', config.position.titleBottom[0])
.attr('y', config.position.titleBottom[1])
.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.titles[0]);
config.svg.append('text')
.attr('x', config.position.titleLeft[0])
.attr('y', config.position.titleLeft[1])
.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')
.style('transform', 'rotate(-90deg)')
.text(config.titles[1]);
};
差异被折叠。
<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 * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
import { chartMockData } from '../utils/mock';
import { setTips, setMaxMinNumber, coordinateScale, setColor, setCluster, setLegend, setLabel } from '../utils/index';
import PubSub from 'pubsub-js';
// 根节点实例
......@@ -18,92 +20,88 @@ const options = ref({});
// pubsub实例
const pubSubInstance = ref(null);
// plot实例
const plotInstance = ref(null);
// 图表实例
const svgInstance = 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 || 700,
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,
range: ret.data_color_list || []
},
// x: {
// grid: ret.show_x_gridLine,
// domain: [-100, 100]
// },
// y: {
// grid: ret.show_y_gridLine,
// domain: [-100, 100]
// },
marks: [
Plot.ruleX([0]),
Plot.ruleY([0]),
Plot.axisX({
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({
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',
stroke: 'group',
fill: 'group',
r: Number(ret.point_radius) || 5,
tip: true
}),
Plot.text([ret.title || ''], {
x: 1,
fill: ret.title_font_color || 'currentColor',
fontSize: ret.title_font_size || 20,
let width = Number(ret.chart_width) || 800;
let height = Number(ret.chart_height) || 800;
let marginTop = Number(ret.margin_top) || 60;
let marginRight = Number(ret.margin_right) || 200;
let marginBottom = Number(ret.margin_bottom) || 60;
let marginLeft = Number(ret.margin_left) || 60;
let radius = Number(ret.point_radius) || 4;
let colors = ret.data_color_list || ['#E53935', '#1E88E5'];
let legendData = ['Cancer', 'Non-cance'];
const titleConfig = {
label: !!ret.title ? ret.title : null,
color: ret.title_font_color || '#333333',
fontSize: ret.title_font_size || 30,
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'
};
const svg = d3.create('svg').attr('width', width).attr('height', height);
svg.append('rect')
.attr('x', marginLeft)
.attr('y', marginTop)
.attr('width', width - marginTop - marginRight)
.attr('height', height - marginTop - marginBottom)
.attr('fill', 'white');
const { paddedMin, paddedMax } = setMaxMinNumber(data);
let configObj = {
svg,
width,
height,
marginTop,
marginRight,
marginBottom,
marginLeft,
paddedMin,
paddedMax,
titleConfig,
colors,
radius,
legendData,
...ret
};
const { x, y } = coordinateScale(configObj, data);
const color = setColor(configObj);
configObj = { ...configObj, x, y, color };
const cluster = setCluster(configObj, data);
setTips(cluster);
// legend
configObj.show_legend && setLegend(configObj);
// label
setLabel(configObj);
svgInstance.value = svg.node();
// Append the SVG element.
chartRef.value.appendChild(svgInstance.value);
};
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);
......@@ -114,4 +112,8 @@ onUnmounted(() => {
});
</script>
<style scoped></style>
<style scoped>
.chart-box {
position: relative;
}
</style>
......@@ -46,99 +46,9 @@
<span>斜体:</span>
<input type="checkbox" v-model="dynamic_parameter.title_font_style" />
</div>
<h3 class="type content">X轴配置</h3>
<div class="form-item">
<span>显示坐标轴:</span>
<input type="checkbox" v-model="dynamic_parameter.show_x_axis" />
</div>
<div class="form-item">
<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" />
</div>
<div class="form-item">
<span>刻度线长度:</span>
<input type="text" v-model="dynamic_parameter.x_tickMarks_length" />
</div>
<div class="form-item">
<span>字体:</span>
<input type="text" v-model="dynamic_parameter.xtitle_font_family" />
</div>
<div class="form-item">
<span>字号:</span>
<input type="text" v-model="dynamic_parameter.xtitle_font_size" />
</div>
<div class="form-item">
<span>字体颜色:</span>
<input type="text" v-model="dynamic_parameter.xtitle_font_color" />
</div>
<div class="form-item">
<span>字体加粗:</span>
<input type="checkbox" v-model="dynamic_parameter.xtitle_font_weight" />
</div>
<div class="form-item">
<span>斜体:</span>
<input type="checkbox" v-model="dynamic_parameter.xtitle_font_style" />
</div>
<div class="form-item">
<span>x轴标题:</span>
<input type="text" v-model="dynamic_parameter.x_title" />
</div>
<h3 class="type content">Y轴配置</h3>
<div class="form-item">
<span>显示坐标轴:</span>
<input type="checkbox" v-model="dynamic_parameter.show_y_axis" />
</div>
<div class="form-item">
<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" />
</div>
<div class="form-item">
<span>刻度线长度:</span>
<input type="text" v-model="dynamic_parameter.y_tickMarks_length" />
</div>
<div class="form-item">
<span>字体:</span>
<input type="text" v-model="dynamic_parameter.ytitle_font_family" />
</div>
<div class="form-item">
<span>字号:</span>
<input type="text" v-model="dynamic_parameter.ytitle_font_size" />
</div>
<div class="form-item">
<span>字体颜色:</span>
<input type="text" v-model="dynamic_parameter.ytitle_font_color" />
</div>
<div class="form-item">
<span>字体加粗:</span>
<input type="checkbox" v-model="dynamic_parameter.ytitle_font_weight" />
</div>
<div class="form-item">
<span>斜体:</span>
<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>
<span>散点半径:</span>
<input type="text" v-model="dynamic_parameter.point_radius" />
</div>
<h3 class="type content">图例</h3>
......@@ -160,18 +70,18 @@ import PubSub from 'pubsub-js';
const dynamic_parameter = reactive({
//长宽间距
chart_width: 800,
chart_height: 700,
chart_height: 800,
margin_top: 60,
margin_right: 60,
margin_right: 200,
margin_bottom: 60,
margin_left: 60,
//标题配置
title: '',
title_font_family: 'Arial',
title_font_weight: true,
title_font_size: 20,
title_font_size: 30,
title_font_color: '#333333',
title_font_style: true,
title_font_style: false,
//横轴标题
xtitle_font_family: 'Arial',
xtitle_font_weight: false,
......@@ -186,9 +96,9 @@ const dynamic_parameter = reactive({
ytitle_font_color: '#333333',
//颜色设置
data_color_list: ['#FF5733', '#39FDBE', '#D6D6D6'],
data_color_list: ['#E53935', '#1E88E5'],
point_radius: 5, // 散点图半径
point_radius: 4, // 散点图半径
//图例
show_legend: true,
......
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论