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

feat: 相关性分析

上级 02b21257
...@@ -451,6 +451,7 @@ const setTips = (rect) => { ...@@ -451,6 +451,7 @@ const setTips = (rect) => {
.style('width', '80px') .style('width', '80px')
.style('text-align', 'center') .style('text-align', 'center')
.style('height', '20px') .style('height', '20px')
.style('line-height', '20px')
.style('padding', '5px'); .style('padding', '5px');
rect.on('mouseover', (event, d) => { rect.on('mouseover', (event, d) => {
......
...@@ -27,7 +27,6 @@ import { ...@@ -27,7 +27,6 @@ import {
setSilhouetteHeatmap, setSilhouetteHeatmap,
setSilhouetteLegend setSilhouetteLegend
} from '../utils/index'; } from '../utils/index';
import { config } from 'plotly.js-dist';
const chartRef = ref(null); const chartRef = ref(null);
......
import hcluster from './hcluster'; import hcluster from '../../../utils/hcluster';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm'; import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
// root config // root config
...@@ -107,6 +107,7 @@ export const setTips = (rect) => { ...@@ -107,6 +107,7 @@ export const setTips = (rect) => {
.style('width', '200px') .style('width', '200px')
.style('text-align', 'center') .style('text-align', 'center')
.style('height', '20px') .style('height', '20px')
.style('line-height', '20px')
.style('padding', '5px'); .style('padding', '5px');
rect.on('mouseover', (event, d) => { rect.on('mouseover', (event, d) => {
......
import distance from 'distancejs';
import extend from 'extend';
/**
* hcluster
* @returns
*/
export default function () {
var data,
clusters,
clustersGivenK,
treeRoot,
posKey = 'position',
distanceName = 'angular',
distanceFn = distance.angular,
linkage = 'avg',
verbose = false;
//
// simple constructor
function clust() { }
//
// getters, setters a la D3
// return data or set data and build tree
clust.data = function (value) {
if (!arguments.length) return data;
// dataset will be mutated
data = value;
clust._buildTree();
return clust;
};
clust.posKey = function (value) {
if (!arguments.length) return posKey;
posKey = value;
return clust;
};
clust.linkage = function (value) {
if (!arguments.length) return linkage;
linkage = value;
return clust;
};
clust.verbose = function (value) {
if (!arguments.length) return verbose;
verbose = value;
return clust;
};
clust.distance = function (value) {
if (!arguments.length) return distanceName;
distanceName = value;
distanceFn = {
angular: distance.angular,
euclidean: distance.euclidean
}[value] || distance.angular;
return clust;
}
//
// get tree properties
clust.orderedNodes = function () {
if (!treeRoot) throw new Error('Need to passin data and build tree first.');
return treeRoot.indexes.map(function (ndx) {
return data[ndx];
});
};
clust.tree = function () {
if (!treeRoot) throw new Error('Need to passin data and build tree first.');
return treeRoot;
};
clust.getClusters = function (n) {
if (!treeRoot) throw new Error('Need to passin data and build tree first.');
if (n > data.length) throw new Error('n must be less than the size of the dataset');
return clustersGivenK[data.length - n]
.map(function (indexes) {
return indexes.map(function (ndx) { return data[ndx]; });
});
};
//
// math, matrix utility fn's
// return unique pairs of indexes on n x n matrix above the diagonal
clust._squareMatrixPairs = function (n) {
var pairs = [];
for (var row = 0; row < n; row++) {
for (var col = row + 1; col < n; col++) {
pairs.push([row, col]);
}
}
return pairs;
};
// average distance between set of cluster indexes
clust._avgDistance = function (setA, setB) {
var distance = 0;
for (var ndxA = 0; ndxA < setA.length; ndxA++) {
for (var ndxB = 0; ndxB < setB.length; ndxB++) {
distance += data[setA[ndxA]]._distances[setB[ndxB]];
}
}
return distance / setA.length / setB.length;
};
// min distance between set of cluster indexes
clust._minDistance = function (setA, setB) {
var distances = [];
for (var ndxA = 0; ndxA < setA.length; ndxA++) {
for (var ndxB = 0; ndxB < setB.length; ndxB++) {
distances.push(data[setA[ndxA]]._distances[setB[ndxB]]);
}
}
return distances.sort()[0];
};
// max distance between set of cluster indexes
clust._maxDistance = function (setA, setB) {
var distances = [];
for (var ndxA = 0; ndxA < setA.length; ndxA++) {
for (var ndxB = 0; ndxB < setB.length; ndxB++) {
distances.push(data[setA[ndxA]]._distances[setB[ndxB]]);
}
}
return distances.sort()[distances.length - 1];
};
//
// tree construction
//
clust._buildTree = function () {
if (!data || !data.length) throw new Error('Need `data` to build tree');
//
var node, clusterPairs, nearestPair, newCluster;
clusters = [];
clustersGivenK = [];
treeRoot = {};
// calculate distances and build single datum clusters
data.forEach(function (d, ndx) {
d._distances = data.map(function (compareTo) {
return distanceFn(d[posKey], compareTo[posKey]);
});
clusters.push(extend(d, {
height: 0,
indexes: [ndx]
}));
});
// for tree of n leafs, n-1 linkages
for (var iter = 0; iter < data.length - 1; iter++) {
verbose && console.log(iter + ': ' +
clusters.map(function (c) { return c.indexes; }).join('|'));
// find closest pair of clusters, pair[2] is distance
clusterPairs = clust._squareMatrixPairs(clusters.length);
clusterPairs.forEach(function (pair) {
pair[2] = clust['_' + linkage + 'Distance'](
clusters[pair[0]].indexes,
clusters[pair[1]].indexes);
});
nearestPair = clusterPairs
.reduce(function (pairA, pairB) { return pairA[2] <= pairB[2] ? pairA : pairB; },
[0, 0, Infinity]);
newCluster = {
name: 'Node ' + iter,
height: nearestPair[2],
indexes: clusters[nearestPair[0]].indexes.concat(clusters[nearestPair[1]].indexes),
children: [clusters[nearestPair[0]], clusters[nearestPair[1]]],
};
verbose && console.log(newCluster);
clustersGivenK.push(clusters.map(function (c) { return c.indexes; }));
// remove merged nodes and push new node
clusters.splice(Math.max(nearestPair[0], nearestPair[1]), 1);
clusters.splice(Math.min(nearestPair[0], nearestPair[1]), 1);
clusters.push(newCluster);
}
treeRoot = clusters[0];
// clust._rebalanceTree(treeRoot);
};
// TODO: better rebalancing algo? ... this is just for presentation
// rebalance after tree is built (b/c it is top down operation)
// clust._rebalanceTree = function(node) {
// if(node.parent && node.parent.children && node.parent.children.length &&
// node.children && node.children.length) {
// var rightDistance = clust['_'+linkage+'Distance'](
// node.parent.children[1].indexes,
// node.children[0].indexes);
// var leftDistance = clust['_'+linkage+'Distance'](
// node.parent.children[1].indexes,
// node.children[1].indexes);
// // switch order of node.children
// if(leftDistance > rightDistance) {
// node.children = [ node.children[1], node.children[0] ];
// node.indexes = node.children[0].indexes.concat(node.children[1].indexes);
// }
// }
// if(node.children) {
// clust._rebalanceTree(node.children[0]);
// clust._rebalanceTree(node.children[1]);
// }
// };
return clust;
}
\ No newline at end of file
import hcluster from './hcluster'; import hcluster from '../../../utils/hcluster';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm'; import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
// root config // root config
...@@ -107,6 +107,7 @@ export const setTips = (rect) => { ...@@ -107,6 +107,7 @@ export const setTips = (rect) => {
.style('width', '200px') .style('width', '200px')
.style('text-align', 'center') .style('text-align', 'center')
.style('height', '20px') .style('height', '20px')
.style('line-height', '20px')
.style('padding', '5px'); .style('padding', '5px');
rect.on('mouseover', (event, d) => { rect.on('mouseover', (event, d) => {
......
<template> <template>
<div ref="chartRef"></div> <div ref="chartRef" class="chart-box">
<div class="tip"></div>
</div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
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 PubSub from 'pubsub-js';
import Plotly from 'plotly.js-dist'; import {
setTips,
setMaxMinNumber,
setLegend,
setScale,
setAxis,
setRect,
setTopLeftColor,
getRootConfig,
setTree,
setLabel
} from '../utils/index';
// 根节点实例
const chartRef = ref(null); const chartRef = ref(null);
// 配置项数据 // 配置项数据
const options = ref({}); const options = ref({});
// 图表实例
const svgInstance = ref(null);
// pubsub实例 // pubsub实例
const pubSubInstance = ref(null); const pubSubInstance = ref(null);
// const mackData = [ // 生成图表
// { const generateChart = (data) => {
// z: [ const ret = options.value;
// [1, null, 30, 50, 1],
// [20, 1, 60, 80, 30],
// [30, 60, 1, -10, 20]
// ],
// x: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
// y: ['Morning', 'Afternoon', 'Evening'],
// type: 'heatmap',
// hoverongaps: false
// }
// ];
// 配色
const generateColorConfig = (color) => {
if (!color) return false;
let colors = [];
if (Array.isArray(color)) {
colors = color;
} else {
colors = color.split(',');
}
return colors;
};
// 生成数据 let width = Number(ret.chart_width) || 1000;
const generateData = () => { let height = Number(ret.chart_height) || 700;
let z = [], let marginTop = Number(ret.margin_top) || 120;
x = [], let marginRight = Number(ret.margin_Right) || 120;
y = []; let marginBottom = Number(ret.margin_Bottom) || 60;
let marginLeft = Number(ret.margin_Left) || 120;
let legendPosition = ret.legend_position?.split(',') || [0, 0];
let legendData = [0.8, 0.85, 0.9, 0.95, 1];
let dendWidth = 100;
let heatWidth = width - marginLeft - marginRight;
let colorList = [
['#3DD378', '#EC0B88', '#444444', '#5E35B1', '#F9791E', '#C6C6C6', '#F7DC05', '#3D98D3'],
['#017A4A', '#FFCE4E', '#3D98D3', '#FF363C', '#794924', '#8CDB5E', '#D6D6D6', '#FB8C00']
];
chartData.forEach((r) => { const titleConfig = {
z.push([r.CD4N_1, r.CD4N_2, r.CD4N_3, r.CD4P_1, r.CD4P_2, r.CD4P_3]); 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'
};
x = Object.keys(chartData[0]); const svg = d3.create('svg').attr('width', width).attr('height', height);
y = Object.keys(chartData[0]);
let configObj = {
return [ ...ret,
{ svg,
z, width,
x, height,
y, marginTop,
type: 'heatmap', marginRight,
hoverongaps: false marginBottom,
} marginLeft,
]; dendWidth,
titleConfig,
legendData,
colorList,
heatWidth,
legendPosition
};
setChart(configObj, data);
svgInstance.value = svg.node();
// Append the SVG element.
chartRef.value.appendChild(svgInstance.value);
}; };
// 生成图表 const setChart = (config, data) => {
const generateChart = (data) => { const { maxNumber, minNumber } = setMaxMinNumber(data);
const ret = options.value; const color = d3.scaleSequential([minNumber, maxNumber], d3.interpolateMagma);
//图例位置数组
const legend_position = ret.legend_position?.split(',') || '0,0'; const categories = data.map((d) => d.index);
const { x, y } = setScale(config, categories);
const color_ls = generateColorConfig(ret.data_color_list);
const newArr = data.map((d) => Object.keys(d).map((e) => d[e])).map((item) => ({ index: item[0], values: item.slice(1) }));
const layout = { const { root } = getRootConfig(config, newArr);
width: ret.chart_width,
height: ret.chart_height, let configObj = {
showlegend: ret.show_legend, ...config,
legend: { root,
x: Number(legend_position[0]), // 设置图例水平位置,范围从0到1,0表示左侧,1表示右侧,0.5表示水平居中 categories,
y: Number(legend_position[1]), // 设置图例垂直位置,范围从0到1,0表示底部,1表示顶部,0.5表示垂直居中 color,
font: { x,
family: 'Arial, sans-serif', // 设置图例字体样式 y
size: 16, // 设置图例字体大小(以像素为单位)
color: 'green' // 设置图例字体颜色
}
},
title: {
text: ret.title,
font: {
family: ret.title_font_family, // 设置字体家族
size: ret.title_font_size, // 设置字体大小(以像素为单位)
color: ret.title_font_color, // 设置字体颜色
weight: ret.title_font_weight //字体粗细
},
// xanchor: 'center', // 设置标题在水平中央
// yanchor: 'center' // 设置标题在垂直顶部
x: 0.5, // 设置水平位置,0表示左侧,0.5表示居中,1表示右侧
y: 0.99 // 设置垂直位置,0表示底部,0.5表示居中,1表示顶部
},
margin: {
t: Number(ret.margin_top),
b: Number(ret.margin_bottom),
l: Number(ret.margin_left),
r: Number(ret.margin_right)
},
xaxis: {
visible: ret.show_x_axis,
title: {
text: ret.show_x_title ? ret.x_title : '',
font: {
family: ret.xtitle_font_family, // 设置字体家族
size: Number(ret.xtitle_font_size), // 设置字体大小(以像素为单位)
color: ret.xtitle_font_color, // 设置字体颜色
weight: 'bold' //字体粗细
},
// xanchor: 'left', // 设置标题在水平中央
// yanchor: 'center', // 设置标题在垂直顶部
x: 0.1
},
showgrid: ret.show_x_gridLine, // 显示 x 轴网格线
showline: ret.show_x_line, // 显示 x 轴线
showticklabels: true,
ticks: ret.show_x_tickMarks ? 'outside' : '' // 将刻度线朝外显示\inside 向内
},
yaxis: {
visible: ret.show_y_axis,
title: {
text: ret.show_y_title ? ret.y_title : '',
font: {
family: ret.ytitle_font_family, // 设置字体家族
size: Number(ret.ytitle_font_size), // 设置字体大小(以像素为单位)
color: ret.ytitle_font_color, // 设置字体颜色
weight: ret.ytitle_font_weight //字体粗细
},
xanchor: 'center', // 设置标题在水平中央
yanchor: 'center' // 设置标题在垂直顶部
},
showgrid: ret.show_y_gridLine, // 隐藏 y 轴网格线
showline: ret.show_y_line, // 隐藏 y 轴线
showticklabels: true, // 显示 x 轴上的刻度标签
ticks: ret.show_y_tickMarks ? 'outside' : '' // 将刻度线朝外显示/inside 向内
}
}; };
Plotly.newPlot(chartRef.value, data, layout, { displayModeBar: false, showSendToCloud: true }); // rects
const rects = setRect(configObj, data);
// tips
setTips(rects);
// axis
setAxis(configObj);
// topLeftColor
setTopLeftColor(configObj, data);
// legend
config.show_legend && setLegend(configObj);
// tree
setTree(configObj);
// label
setLabel(configObj);
}; };
onMounted(() => { onMounted(() => {
const mackData = generateData(chartData); const chartData = chartMockData;
pubSubInstance.value = PubSub.subscribe('options', (m, data) => { pubSubInstance.value = PubSub.subscribe('options', (m, data) => {
options.value = data; options.value = data;
generateChart(mackData); chartRef.value.removeChild(svgInstance.value);
generateChart(chartData);
}); });
generateChart(mackData); generateChart(chartData);
}); });
onUnmounted(() => { onUnmounted(() => {
...@@ -164,4 +144,8 @@ onUnmounted(() => { ...@@ -164,4 +144,8 @@ onUnmounted(() => {
}); });
</script> </script>
<style scoped></style> <style scoped>
.chart-box {
position: relative;
}
</style>
...@@ -26,6 +26,10 @@ ...@@ -26,6 +26,10 @@
<input type="text" v-model="dynamic_parameter.margin_left" /> <input type="text" v-model="dynamic_parameter.margin_left" />
</div> </div>
<h3 class="type content">文本</h3> <h3 class="type content">文本</h3>
<div class="form-item">
<span>标题:</span>
<input type="text" v-model="dynamic_parameter.title" />
</div>
<div class="form-item"> <div class="form-item">
<span>字体:</span> <span>字体:</span>
<input type="text" v-model="dynamic_parameter.title_font_family" /> <input type="text" v-model="dynamic_parameter.title_font_family" />
...@@ -55,14 +59,6 @@ ...@@ -55,14 +59,6 @@
<span>显示刻度线:</span> <span>显示刻度线:</span>
<input type="checkbox" v-model="dynamic_parameter.show_x_tickMarks" /> <input type="checkbox" v-model="dynamic_parameter.show_x_tickMarks" />
</div> </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"> <div class="form-item">
<span>刻度线长度:</span> <span>刻度线长度:</span>
<input type="text" v-model="dynamic_parameter.x_tickMarks_length" /> <input type="text" v-model="dynamic_parameter.x_tickMarks_length" />
...@@ -87,10 +83,6 @@ ...@@ -87,10 +83,6 @@
<span>斜体:</span> <span>斜体:</span>
<input type="checkbox" v-model="dynamic_parameter.xtitle_font_style" /> <input type="checkbox" v-model="dynamic_parameter.xtitle_font_style" />
</div> </div>
<div class="form-item">
<span>x轴标题:</span>
<input type="text" v-model="dynamic_parameter.x_title" />
</div>
<h3 class="type content">Y轴配置</h3> <h3 class="type content">Y轴配置</h3>
<div class="form-item"> <div class="form-item">
<span>显示坐标轴:</span> <span>显示坐标轴:</span>
...@@ -100,14 +92,6 @@ ...@@ -100,14 +92,6 @@
<span>显示刻度线:</span> <span>显示刻度线:</span>
<input type="checkbox" v-model="dynamic_parameter.show_y_tickMarks" /> <input type="checkbox" v-model="dynamic_parameter.show_y_tickMarks" />
</div> </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"> <div class="form-item">
<span>刻度线长度:</span> <span>刻度线长度:</span>
<input type="text" v-model="dynamic_parameter.y_tickMarks_length" /> <input type="text" v-model="dynamic_parameter.y_tickMarks_length" />
...@@ -132,10 +116,6 @@ ...@@ -132,10 +116,6 @@
<span>斜体:</span> <span>斜体:</span>
<input type="checkbox" v-model="dynamic_parameter.ytitle_font_style" /> <input type="checkbox" v-model="dynamic_parameter.ytitle_font_style" />
</div> </div>
<div class="form-item">
<span>x轴标题:</span>
<input type="text" v-model="dynamic_parameter.y_title" />
</div>
<h3 class="type content">图例</h3> <h3 class="type content">图例</h3>
<div class="form-item"> <div class="form-item">
<span>显示图例:</span> <span>显示图例:</span>
...@@ -158,30 +138,30 @@ import PubSub from 'pubsub-js'; ...@@ -158,30 +138,30 @@ import PubSub from 'pubsub-js';
//动态图参数,固定参数 //动态图参数,固定参数
const dynamic_parameter = reactive({ const dynamic_parameter = reactive({
//长宽间距 //长宽间距
chart_width: 800, chart_width: 1000,
chart_height: 600, chart_height: 700,
margin_top: 60, margin_top: 180,
margin_right: 60, margin_right: 120,
margin_bottom: 60, margin_bottom: 60,
margin_left: 60, margin_left: 120,
//标题配置 //标题配置
title: '', title: '相关性分析',
title_font_family: 'Arial', title_font_family: 'Arial',
title_font_weight: true, title_font_weight: true,
title_font_size: 20, title_font_size: 20,
title_font_color: '#333333', title_font_color: '#333333',
title_font_style: true, title_font_style: false,
//横轴标题 //横轴标题
xtitle_font_family: 'Arial', xtitle_font_family: 'Arial',
xtitle_font_weight: false, xtitle_font_weight: false,
xtitle_font_style: false, xtitle_font_style: false,
xtitle_font_size: 16, xtitle_font_size: 11,
xtitle_font_color: '#333333', xtitle_font_color: '#333333',
//纵轴标题 //纵轴标题
ytitle_font_family: 'Arial', ytitle_font_family: 'Arial',
ytitle_font_weight: false, ytitle_font_weight: false,
ytitle_font_style: false, ytitle_font_style: false,
ytitle_font_size: 16, ytitle_font_size: 11,
ytitle_font_color: '#333333', ytitle_font_color: '#333333',
//颜色设置 //颜色设置
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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
}
}
// 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', '80px')
.style('height', '25px')
.style('line-height', '25px')
.style('text-align', 'center')
.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 + 55 + 'px')
.html(`${d.count}`);
})
.on('mouseleave', function () {
tips.style('visibility', 'hidden');
});
};
// legend
export const setLegend = (config) => {
config.svg.append('g')
.attr('transform', `translate(${(config.width - 100) * config.legendPosition[0]}, ${(config.height - 100) * config.legendPosition[1]})`)
.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)))
.call((g) =>
g
.append('text')
.attr('x', 45)
.attr('y', 11)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
};
// topLeft Color
export const setTopLeftColor = (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]);
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]);
const y = d3
.scaleBand()
.domain([...categories].reverse())
.range([config.height - config.marginBottom, config.marginTop]);
return { x, y }
}
// axis
export const setAxis = (config) => {
const xAxis = (g) =>
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('.domain').remove())
.selectAll('text')
.style('text-anchor', 'middle');
const yAxis = (g) =>
g
.attr('transform', `translate(${config.width - config.marginRight}, 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.axisRight(config.y).tickSize(Number(config.show_y_tickMarks ? config.y_tickMarks_length : 0)))
.call((g) => g.selectAll('.domain').remove())
.selectAll('text')
.style('text-anchor', 'start')
config.show_x_axis && config.svg.append('g').call(xAxis);
config.show_y_axis && config.svg.append('g').call(yAxis);
}
// rect
export const setRect = (config, data) => {
const rects = config.svg
.append('g')
.selectAll('g')
.data(data)
.enter()
.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');
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));
return rects
}
// 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,
}
}
// 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}, ${60})`)
.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}
`
);
};
// 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(`${config.titleConfig.label}`);
};
...@@ -26,6 +26,10 @@ ...@@ -26,6 +26,10 @@
<input type="text" v-model="dynamic_parameter.margin_left" /> <input type="text" v-model="dynamic_parameter.margin_left" />
</div> </div>
<h3 class="type content">文本</h3> <h3 class="type content">文本</h3>
<div class="form-item">
<span>标题:</span>
<input type="text" v-model="dynamic_parameter.title" />
</div>
<div class="form-item"> <div class="form-item">
<span>字体:</span> <span>字体:</span>
<input type="text" v-model="dynamic_parameter.title_font_family" /> <input type="text" v-model="dynamic_parameter.title_font_family" />
...@@ -76,7 +80,7 @@ const dynamic_parameter = reactive({ ...@@ -76,7 +80,7 @@ const dynamic_parameter = reactive({
margin_bottom: 60, margin_bottom: 60,
margin_left: 60, margin_left: 60,
//标题配置 //标题配置
title: '', title: 'group',
title_font_family: 'Arial', title_font_family: 'Arial',
title_font_weight: true, title_font_weight: true,
title_font_size: 30, title_font_size: 30,
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { chartMockData } from './mock.js'; import { chartMockData } from './mock.js';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm'; import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
import { setTips, setMaxMinNumber, coordinateScale, setColor, setCluster, setLegend, setLabel } from './utils'; import { setTips, setMaxMinNumber, setLegend, setScale, setAxis, setRect, setTopLeftColor, getRootConfig, setTree } from './utils';
const chartRef = ref(null); const chartRef = ref(null);
...@@ -19,20 +19,20 @@ const options = ref({}); ...@@ -19,20 +19,20 @@ const options = ref({});
const generateChart = (data) => { const generateChart = (data) => {
const ret = options.value; const ret = options.value;
let width = 500; let width = 1000;
let height = 1200; let height = 700;
let marginTop = 30; let marginTop = 120;
let marginRight = 30; let marginRight = 120;
let marginBottom = 30; let marginBottom = 60;
let marginLeft = 30; let marginLeft = 120;
let rectHeight = 300; let dendWidth = 100;
let legendData = [0.8, 0.85, 0.9, 0.95, 1];
let colorList = [ let colorList = [
['#E53935', '#1E88E5'], ['#3DD378', '#EC0B88', '#444444', '#5E35B1', '#F9791E', '#C6C6C6', '#F7DC05', '#3D98D3'],
['#AA40FC', '#FFBB78'], ['#017A4A', '#FFCE4E', '#3D98D3', '#FF363C', '#794924', '#8CDB5E', '#D6D6D6', '#FB8C00']
['#17BECF', '#E377C2']
]; ];
let titles = ['UMAP1', 'UMAP2']; let legendPosition = [0, 0];
let labels = ['main_cluster_id_0.4', 'celltype', 'sample_id']; let heatWidth = width - marginLeft - marginRight;
const titleConfig = { const titleConfig = {
label: !!ret.title ? ret.title : null, label: !!ret.title ? ret.title : null,
...@@ -53,59 +53,60 @@ const generateChart = (data) => { ...@@ -53,59 +53,60 @@ const generateChart = (data) => {
marginRight, marginRight,
marginBottom, marginBottom,
marginLeft, marginLeft,
rectHeight, dendWidth,
titleConfig, titleConfig,
titles, legendData,
labels, colorList,
colorList heatWidth,
legendPosition
}; };
for (let i = 0; i < 3; i++) { setChart(configObj, data);
setChart(configObj, data, i);
}
// setEl // setEl
chartRef.value.append(svg.node()); chartRef.value.append(svg.node());
}; };
const setChart = (config, data, index) => { const setChart = (config, data) => {
let position = { const categories = data.map((d) => d.index);
rect: [config.marginLeft, (config.rectHeight + 80) * index + config.marginTop], const newArr = data.map((d) => Object.keys(d).map((e) => d[e])).map((item) => ({ index: item[0], values: item.slice(1) }));
cluster: [0, (config.rectHeight + 80) * index], const { maxNumber, minNumber } = setMaxMinNumber(data);
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); const color = d3.scaleSequential([minNumber, maxNumber], d3.interpolateMagma);
let configObj = {
...config,
categories,
color
};
let label = config.labels[index]; const { x, y } = setScale(config, categories);
let colors = config.colorList[index]; const { root } = getRootConfig(config, newArr);
let configObj = { ...config, paddedMin, paddedMax, position, label, colors }; configObj = {
...configObj,
root,
x,
y
};
const { x, y } = coordinateScale(configObj, data); // rects
const rects = setRect(configObj, data);
const color = setColor(configObj); // tips
setTips(rects);
configObj = { ...configObj, x, y, color }; // axis
setAxis(configObj);
const cluster = setCluster(configObj, data); // topLeftColor
setTopLeftColor(configObj, data);
setTips(cluster); // legend
setLegend(configObj);
// label // tree
setLabel(configObj); setTree(configObj);
}; };
onMounted(() => { onMounted(() => {
......
差异被折叠。
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论