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

feat: 聚类热图

上级 84bed51a
...@@ -15,7 +15,7 @@ const chartRef = ref(null); ...@@ -15,7 +15,7 @@ const chartRef = ref(null);
// 配置项数据 // 配置项数据
const options = ref({}); const options = ref({});
// 图例实例 // 图表实例
const svgInstance = ref(null); const svgInstance = ref(null);
// pubsub实例 // pubsub实例
......
<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, chartData1 } 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 * as d3 from 'd3';
import * as uv from 'unipept-visualizations';
// 根节点实例 import {
getRootConfig,
setColor,
setTips,
setMaxMinNumber,
setHeatmap,
setTree,
setLegend,
setBasisLegend,
setConsensusLegend,
setAxisTitle,
setLabel,
setBasisHeatmap,
setConsensusHeatmap,
setSilhouetteHeatmap,
setSilhouetteLegend
} 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 generateData = () => { const generateChart = (chartData) => {
const items = []; const ret = options.value;
const rows = [];
const columns = Object.keys(chartData1[0]).filter((k) => k !== 'Family'); let width = Number(ret.chart_width) || 1000;
let height = Number(ret.chart_height) || 1000;
for (const row of chartData1) { let margin = 10;
const newRow = []; let radius = 3;
for (const [key, value] of Object.entries(row)) { let dendWidth = 100;
if (key === 'Family') {
rows.push(value); let xAxisLength = chartData[0].values.length;
} else { const heatWidth = 2 * radius * xAxisLength;
newRow.push(Number.parseFloat(value)); const heatHeight = 2 * radius * chartData.length;
}
} const titleConfig = {
items.push(newRow); label: !!ret.title ? ret.title : null,
} color: ret.title_font_color || '#333333',
fontSize: ret.title_font_size || 20,
return { fontStyle: ret.title_font_style ? 'oblique' : 'normal',
items, fontFamily: ret.title_font_family || 'Arial',
rows, fontWeight: ret.title_font_weight ? 'bold' : 'normal'
columns
}; };
const svg = d3
.create('svg')
.attr('width', width)
.attr('height', height + 100)
.attr('viewBox', [0, 0, width, height])
.attr('style', 'max-width: 100%; height: auto; font: 10px sans-serif;');
// 最大值 最小值
const { maxNumber, minNumber } = setMaxMinNumber(chartData);
// color config
const colorConfig = setColor(minNumber, maxNumber);
let configObj = {
svg,
width,
height,
radius,
margin,
maxNumber,
minNumber,
dendWidth,
titleConfig,
xAxisLength,
heatWidth,
heatHeight,
color: colorConfig,
...ret
};
const rootConfig = getRootConfig(configObj, chartData);
configObj = { ...configObj, ...rootConfig };
// heatmap
const heatmapRect = setHeatmap(configObj);
setTips(heatmapRect);
// Tree
setTree(configObj);
// axisLabel
setAxisTitle(configObj);
// legend
ret.show_legend && setLegend(configObj);
// label
setLabel(configObj);
// Basis
generageBasis(configObj);
// Consensus
generageConsensus(configObj);
// Silhouette
generageSilhouette(configObj);
svgInstance.value = svg.node();
// Append the SVG element.
chartRef.value.appendChild(svgInstance.value);
}; };
// 生成图表 // Basis
const generateChart = (data) => { const generageBasis = (config) => {
const { items, rows, columns } = data; const chartData = chartMockData.slice(0, 2);
// max/min number
new uv.Heatmap(chartRef.value, items, rows, columns, { const { maxNumber, minNumber } = setMaxMinNumber(chartData);
width: 1200, // root config
height: 800, const { root, orderingx } = getRootConfig(config, chartData);
dendrogramEnabled: true, // color config
squarePadding: 0 const colors = ['#9D9FFF', '#F57BFF', '#3AB1FF', '#D38BFF', '#00C745', '#F4952C'];
}); const color = setColor(minNumber, maxNumber, colors);
const configObj = { ...config, root, orderingx, maxNumber, minNumber, color };
const heatmapRect = setBasisHeatmap(configObj);
// tips
setTips(heatmapRect);
// legend
config.show_legend && setBasisLegend(configObj);
}; };
onMounted(() => { // Consensus
const mackData = generateData(); const generageConsensus = (config) => {
const chartData = chartMockData.slice(2, 4);
// max/min number
const { maxNumber, minNumber } = setMaxMinNumber(chartData);
// root config
const { root, orderingx } = getRootConfig(config, chartData);
// color config
const colors = ['#C3AD00', '#6EC000', '#00CC7B', '#00CDC9', '#FF7B9C', '#FF876E'];
const color = setColor(minNumber, maxNumber, colors);
const configObj = { ...config, root, orderingx, maxNumber, minNumber, color };
const heatmapRect = setConsensusHeatmap(configObj);
// tips
setTips(heatmapRect);
// legend
config.show_legend && setConsensusLegend(configObj);
};
// Silhouette
const generageSilhouette = (config) => {
const chartData = chartMockData.slice(4, 6);
// max/min number
const { maxNumber, minNumber } = setMaxMinNumber(chartData, false);
// root config
const { root, orderingx } = getRootConfig(config, chartData);
// color config
const colors = ['#C3AD00', '#6EC000', '#00CC7B', '#00CDC9', '#FF7B9C', '#FF876E'];
const color = setColor(minNumber, maxNumber, colors);
const configObj = { ...config, root, orderingx, maxNumber, minNumber, color };
const heatmapRect = setSilhouetteHeatmap(configObj);
// tips
setTips(heatmapRect);
// legend
config.show_legend && setSilhouetteLegend(configObj);
};
onMounted(() => {
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(() => {
...@@ -70,4 +191,8 @@ onUnmounted(() => { ...@@ -70,4 +191,8 @@ onUnmounted(() => {
}); });
</script> </script>
<style scoped></style> <style scoped>
.chart-box {
position: relative;
}
</style>
...@@ -9,23 +9,11 @@ ...@@ -9,23 +9,11 @@
<span>高:</span> <span>高:</span>
<input type="text" v-model="dynamic_parameter.chart_height" /> <input type="text" v-model="dynamic_parameter.chart_height" />
</div> </div>
<h3 class="type content">文本</h3>
<div class="form-item"> <div class="form-item">
<span>上边距:</span> <span>标题:</span>
<input type="text" v-model="dynamic_parameter.margin_top" /> <input type="text" v-model="dynamic_parameter.title" />
</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> </div>
<h3 class="type content">文本</h3>
<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" />
...@@ -46,105 +34,12 @@ ...@@ -46,105 +34,12 @@
<span>斜体:</span> <span>斜体:</span>
<input type="checkbox" v-model="dynamic_parameter.title_font_style" /> <input type="checkbox" v-model="dynamic_parameter.title_font_style" />
</div> </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> <h3 class="type content">图例</h3>
<div class="form-item"> <div class="form-item">
<span>显示图例:</span> <span>显示图例:</span>
<input type="checkbox" v-model="dynamic_parameter.show_legend" /> <input type="checkbox" v-model="dynamic_parameter.show_legend" />
</div> </div>
<div class="form-item">
<span>图例位置:</span>
<input type="text" v-model="dynamic_parameter.legend_position" />
</div>
<div class="btn"> <div class="btn">
<button @click="submit">提交</button> <button @click="submit">提交</button>
</div> </div>
...@@ -158,19 +53,19 @@ import PubSub from 'pubsub-js'; ...@@ -158,19 +53,19 @@ import PubSub from 'pubsub-js';
//动态图参数,固定参数 //动态图参数,固定参数
const dynamic_parameter = reactive({ const dynamic_parameter = reactive({
//长宽间距 //长宽间距
chart_width: 800, chart_width: 1000,
chart_height: 600, chart_height: 1000,
margin_top: 60, margin_top: 60,
margin_right: 60, margin_right: 60,
margin_bottom: 60, margin_bottom: 60,
margin_left: 60, margin_left: 60,
//标题配置 //标题配置
title: '', title: 'Consensus matrix',
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,
......
<template> <template>
<div class="chart-container"> <div class="chart-container">
<Chart /> <Chart />
<!-- <Config /> --> <Config />
</div> </div>
</template> </template>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import distance from 'distancejs' import distance from 'distancejs';
import extend from 'extend' import extend from 'extend';
// hcluster /**
var hcluster = function () { * hcluster
* @returns
*/
export default function () {
var data, var data,
clusters, clusters,
clustersGivenK, clustersGivenK,
...@@ -207,7 +210,4 @@ var hcluster = function () { ...@@ -207,7 +210,4 @@ var hcluster = function () {
// }; // };
return clust; return clust;
}; }
\ No newline at end of file
// module.exports = hcluster;
export default hcluster
import hcluster from './hcluster';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
// 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 verticalData = () => {
return d3.range(chartData[0].values.length).map((d) => ({ normalized: normalizedData().map((el) => el.normalized[d]) }));
};
const clustering = hcluster().distance('angular').linkage('avg').posKey('normalized').data(normalizedData());
const verticalClustering = hcluster().distance('angular').linkage('avg').posKey('normalized').data(verticalData());
const tree = (data) => {
const root = d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height));
root.dx = config.radius;
root.dy = config.dendWidth / (root.height + 1);
return d3
.cluster()
.size([config.heatHeight, config.dendWidth])
.separation(() => 2)(root);
};
const vertTree = (data) => {
const root = d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height));
root.dy = config.radius;
root.dx = config.dendWidth / (root.height + 1);
return d3
.cluster()
.size([config.heatWidth, config.dendWidth])
.separation(() => 2)(root);
};
const root = tree(clustering.tree());
const vertRoot = vertTree(verticalClustering.tree());
const orderingx = vertRoot.leaves().map((el) => el.data.indexes[0]);
return {
root,
vertRoot,
orderingx
}
}
// color config
export const setColor = (min, max, color) => {
let colors = [
'#313695',
'#436FB1',
'#6BA2CB',
'#9BCCE2',
'#F0F9D8',
'#F0F9D8',
'#FEF0A9',
'#FDCD7E',
'#FA9C58',
'#EE613D',
'#D22B26',
'#A50026'
];
if (Array.isArray(color)) {
colors = !!color.length ? color : colors;
} else if (!!color) {
colors = color.split(',');
}
return () => d3.scaleQuantize().domain([min, max]).range(colors);
};
// maxnumber minNumber
export const setMaxMinNumber = (chartData, isInteger = true) => {
let maxNumArr = [],
minNumArr = [],
maxNumber = 0,
minNumber = 0;
for (let i = 0; i < chartData.length; i++) {
maxNumArr.push(d3.max(chartData[i].values));
minNumArr.push(d3.min(chartData[i].values));
}
// 最大值 最小值
maxNumber = isInteger ? Math.round(d3.max(maxNumArr)) : d3.max(maxNumArr);
minNumber = isInteger ? Math.round(d3.min(minNumArr)) : d3.min(minNumArr);
return {
maxNumber,
minNumber
}
}
// 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('text-align', 'center')
.style('height', '20px')
.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 + 50 + 'px')
.html(`${d}`);
})
.on('mouseleave', function () {
tips.style('visibility', 'hidden');
});
};
// heatmap
export const setHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin + config.dendWidth * 2})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${d.x - config.radius})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', config.color());
};
// Basis heatmap
export const setBasisHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${130 + config.radius * 2 * i})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', config.color());
};
// Consensus heatmap
export const setConsensusHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${150 + config.radius * 2 * i})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', config.color());
};
// Silhouette heatmap
export const setSilhouetteHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin * 3})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${150 + config.radius * 2 * i})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', d => d3.interpolateGreens(d));
};
// tree
export const setTree = (config) => {
// left tree
config.svg.append('g')
.attr('fill', 'none')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', `translate(${config.margin}, ${config.margin + config.dendWidth * 2})`)
.selectAll('path')
.data(config.root.links())
.join('path')
.attr(
'd',
(d) => `
M${d.target.y},${d.target.x}
L${d.source.y},${d.target.x}
${d.source.y},${d.source.x}
${d.source.y},${d.source.x}
`
);
// top tree
config.svg.append('g')
.attr('fill', 'none')
.attr('stroke', '#555')
.attr('stroke-width', 1)
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin * 3})`)
.selectAll('path')
.data(config.vertRoot.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}
`
);
};
// legend
export const setLegend = (config) => {
const legendData = [...Array.from({ length: config.maxNumber }, (item, i) => i)];
config.svg.append('g')
.selectAll()
.data(legendData)
.join('g')
.attr('transform', (d, i, nodes) => `translate(${config.dendWidth + config.xAxisLength * (config.radius * 2) + 80}, ${config.height / 3 + (nodes.length / 2 - i - 1) * 18 - 60})`)
.call((g) => g.append('rect').attr('width', 18).attr('height', 18).attr('fill', config.color()))
.call((g) =>
g
.append('text')
.attr('x', 21)
.attr('y', 10)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
};
// Basis legend
export const setBasisLegend = (config) => {
const legendData = [...Array.from({ length: config.maxNumber }, (item, i) => i)];
const textX = config.dendWidth + config.xAxisLength * (config.radius * 2) + 180
config.svg.append('text')
.attr('x', textX)
.attr('y', config.height / 3 - 110)
.attr('fill', '#333333')
.attr('font-size', '20px')
.attr('font-family', `${config.titleConfig.fontFamily}`)
.attr('font-style', 'normal')
.attr('font-weight', 'bold')
.style('text-anchor', 'middle')
.text('basis');
config.svg.append('g')
.selectAll()
.data(legendData)
.join('g')
.attr('transform', (d, i, nodes) => `translate(${config.dendWidth + config.xAxisLength * (config.radius * 2) + 160}, ${config.height / 3 + (nodes.length / 2 - i - 1) * 21 - 50})`)
.call((g) => g.append('rect').attr('width', 15).attr('height', 15).attr('fill', config.color()))
.call((g) =>
g
.append('text')
.attr('x', 20)
.attr('y', 9)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
}
// Consensus legend
export const setConsensusLegend = (config) => {
const legendData = [...Array.from({ length: config.maxNumber }, (item, i) => i)];
const textX = config.dendWidth + config.xAxisLength * (config.radius * 2) + 205
config.svg.append('text')
.attr('x', textX)
.attr('y', config.height / 3 + 50)
.attr('fill', '#333333')
.attr('font-size', '20px')
.attr('font-family', `${config.titleConfig.fontFamily}`)
.attr('font-style', 'normal')
.attr('font-weight', 'bold')
.style('text-anchor', 'middle')
.text('consensus');
config.svg.append('g')
.selectAll()
.data(legendData)
.join('g')
.attr('transform', (d, i, nodes) => `translate(${config.dendWidth + config.xAxisLength * (config.radius * 2) + 160}, ${config.height / 3 + (nodes.length / 2 - i - 1) * 21 + 110})`)
.call((g) => g.append('rect').attr('width', 15).attr('height', 15).attr('fill', config.color()))
.call((g) =>
g
.append('text')
.attr('x', 20)
.attr('y', 9)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
}
// Silhouette legend
export const setSilhouetteLegend = (config) => {
const textX = config.dendWidth + config.xAxisLength * (config.radius * 2) + 200
const legendData = [config.maxNumber, config.minNumber]
config.svg.append('text')
.attr('x', textX)
.attr('y', config.height / 3 + 210)
.attr('fill', '#333333')
.attr('font-size', '20px')
.attr('font-family', `${config.titleConfig.fontFamily}`)
.attr('font-style', 'normal')
.attr('font-weight', 'bold')
.style('text-anchor', 'middle')
.text('silhouette');
config.svg.append('g')
.selectAll()
.data(legendData)
.join('text')
.attr('x', textX + 20)
.attr('y', (d, i) => config.height / 3 + i * 40 + 240)
.attr('fill', config.titleConfig.color)
.attr('font-style', `${config.titleConfig.fontStyle}`)
.attr('font-weight', `${config.titleConfig.fontWeight}`)
.style('font', '13px sans-serif')
.style('text-anchor', 'middle')
.text((d) => d);
const gradient = config.svg
.append('defs')
.append('linearGradient')
.attr('id', 'mylLnearGradient')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '0%')
.attr('y2', '100%')
.attr('spreadMethod', 'pad');
gradient
.append('stop')
.attr('offset', '0%')
.attr('stop-color', 'rgb(0,128,0,1)')
.attr('stop-opacity', 1);
gradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', 'rgb(0,128,0,0.1)')
.attr('stop-opacity', 1);
config.svg.append('rect')
.attr('width', '15')
.attr('height', '60')
.attr("fill", "url(#mylLnearGradient)")
.attr("x", `${config.dendWidth + config.xAxisLength * (config.radius * 2) + 160}`)
.attr("y", `${config.height / 3 + 225}`)
}
// 标签
export const setAxisTitle = (config) => {
const labelList = config.root.leaves().map((item) => item.data.id);
config.svg.append('g')
.selectAll('g')
.data(labelList)
.join('g')
.attr(
'transform',
(d, i) =>
`translate(${config.margin + config.dendWidth + config.xAxisLength * 2 * config.radius + 4},${config.margin + config.dendWidth * 2 + i * 2 * config.radius + 4
})`
)
.call((g) =>
g
.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('dy', '0.35em')
.style('font', `6px sans-serif`)
.style('fill', `#000`)
.text((d) => d)
);
};
// 标题
export const setLabel = (config) => {
config.svg.append('text')
.attr('x', config.width / 3)
.attr('y', 0)
.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);
};
...@@ -6,9 +6,25 @@ ...@@ -6,9 +6,25 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { chartMackData } 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 hcluster from './hcluster'; import {
getRootConfig,
setColor,
setTips,
setMaxMinNumber,
setHeatmap,
setTree,
setLegend,
setBasisLegend,
setConsensusLegend,
setAxisTitle,
setLabel,
setBasisHeatmap,
setConsensusHeatmap,
setSilhouetteHeatmap,
setSilhouetteLegend
} from './utils';
const chartRef = ref(null); const chartRef = ref(null);
...@@ -18,16 +34,16 @@ const options = ref({}); ...@@ -18,16 +34,16 @@ const options = ref({});
// 生成图表 // 生成图表
const generateChart = (chartData) => { const generateChart = (chartData) => {
const ret = options.value; const ret = options.value;
// return;
let width = 700; let width = 1000;
let height = 800; let height = 1000;
let margin = 10; let margin = 10;
let radius = 3; let radius = 3;
let dendWidth = 150; let dendWidth = 100;
let heatWidth = 462;
let heatHeight = 600; let xAxisLength = chartData[0].values.length;
let n = 100; const heatWidth = 2 * radius * xAxisLength;
let nonMeasurements = ['MouseID', 'Genotype', 'Treatment', 'Behavior', 'class']; const heatHeight = 2 * radius * chartData.length;
const titleConfig = { const titleConfig = {
label: !!ret.title ? ret.title : null, label: !!ret.title ? ret.title : null,
...@@ -37,240 +53,120 @@ const generateChart = (chartData) => { ...@@ -37,240 +53,120 @@ const generateChart = (chartData) => {
fontFamily: ret.title_font_family || 'Arial', fontFamily: ret.title_font_family || 'Arial',
fontWeight: ret.title_font_weight ? 'bold' : 'normal' fontWeight: ret.title_font_weight ? 'bold' : 'normal'
}; };
let maxNumArr = [];
for (let i = 0; i < chartData.length; i++) {
maxNumArr.push(d3.max(chartData[i].values));
}
// 最大值
const maxNumber = Math.round(d3.max(maxNumArr));
const color_ls = setColorConfig();
const color = (max, range_count, d) => {
return d3.scaleQuantize().domain([0, max]).range(color_ls);
};
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 verticalData = () => {
return d3.range(chartData[0].values.length).map((d) => ({ normalized: normalizedData().map((el) => el.normalized[d]) }));
};
const clustering = hcluster().distance('angular').linkage('avg').posKey('normalized').data(normalizedData());
const verticalClustering = hcluster().distance('angular').linkage('avg').posKey('normalized').data(verticalData());
const tree = (data) => {
const root = d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height));
root.dx = radius;
root.dy = dendWidth / (root.height + 1);
return d3
.cluster()
.size([heatHeight, dendWidth])
.separation(() => 2)(root);
};
const vertTree = (data) => {
const root = d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height));
root.dy = radius;
root.dx = dendWidth / (root.height + 1);
return d3
.cluster()
.size([heatWidth, dendWidth])
.separation(() => 2)(root);
};
const root = tree(clustering.tree());
const vertRoot = vertTree(verticalClustering.tree());
const svg = d3 const svg = d3
.create('svg') .create('svg')
.attr('width', width) .attr('width', width)
.attr('height', height + 50) .attr('height', height + 100)
.attr('viewBox', [0, 0, width, height]) .attr('viewBox', [0, 0, width, height])
.attr('style', 'max-width: 100%; height: auto; font: 10px sans-serif;'); .attr('style', 'max-width: 100%; height: auto; font: 10px sans-serif;');
// left tree // 最大值 最小值
svg.append('g') const { maxNumber, minNumber } = setMaxMinNumber(chartData);
.attr('fill', 'none')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', `translate(${margin}, ${margin + dendWidth})`)
.selectAll('path')
.data(root.links())
.join('path')
.attr(
'd',
(d) => `
M${d.target.y},${d.target.x}
L${d.source.y},${d.target.x}
${d.source.y},${d.source.x}
${d.source.y},${d.source.x}
`
);
// top tree
svg.append('g')
.attr('fill', 'none')
.attr('stroke', '#555')
.attr('stroke-width', 1)
.attr('transform', `translate(${margin + dendWidth}, ${margin})`)
.selectAll('path')
.data(vertRoot.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}
`
);
let orderingx = vertRoot.leaves().map((el) => el.data.indexes[0]);
let fcolors = color(maxNumber, orderingx.length);
const rects = svg
.append('g')
.attr('transform', `translate(${margin + dendWidth}, ${margin + dendWidth})`)
.selectAll('g')
.data(root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${d.x - radius})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * radius)
.attr('y', 0)
.attr('width', 2 * radius)
.attr('height', 2 * radius)
.attr('fill', (d) => fcolors(d));
setTips(rects); // color config
const colorConfig = setColor(minNumber, maxNumber);
const configObj = { let configObj = {
svg,
width, width,
height, height,
radius, radius,
margin, margin,
maxNumber,
minNumber,
dendWidth, dendWidth,
titleConfig, titleConfig,
rootData: root.leaves(), xAxisLength,
vertRootLength: orderingx.length heatWidth,
heatHeight,
color: colorConfig
}; };
const rootConfig = getRootConfig(configObj, chartData);
configObj = { ...configObj, ...rootConfig };
// heatmap
const heatmapRect = setHeatmap(configObj);
setTips(heatmapRect);
// Tree
setTree(configObj);
// axisLabel // axisLabel
setAxisTitle(configObj, svg); setAxisTitle(configObj);
// label // legend
setLabel(configObj, svg); setLegend(configObj);
chartRef.value.append(svg.node()); // label
}; setLabel(configObj);
// 标签 // Basis
const setAxisTitle = (config, svg) => { generageBasis(configObj);
const labelList = config.rootData.map((item) => item.data.id); // Consensus
generageConsensus(configObj);
// Silhouette
generageSilhouette(configObj);
svg.append('g') // setEl
.selectAll('g') chartRef.value.append(svg.node());
.data(labelList)
.join('g')
.attr(
'transform',
(d, i) =>
`translate(${config.margin + config.dendWidth + config.vertRootLength * 2 * config.radius + 3},${
config.margin + config.dendWidth + i * 2 * config.radius + 3
})`
)
.call((g) =>
g
.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('dy', '0.35em')
.style('font', `6px sans-serif`)
.style('fill', `#000`)
.text((d) => d)
);
return svg.node();
}; };
// 标题 // Basis
const setLabel = (config, svg) => { const generageBasis = (config) => {
svg.append('text') const chartData = chartMockData.slice(0, 2);
.attr('x', 200) // max/min number
.attr('y', 0) const { maxNumber, minNumber } = setMaxMinNumber(chartData);
.attr('fill', config.titleConfig.color) // root config
.attr('font-size', `${config.titleConfig.fontSize}px`) const { root, orderingx } = getRootConfig(config, chartData);
.attr('font-family', `${config.titleConfig.fontFamily}`) // color config
.attr('font-style', `${config.titleConfig.fontStyle}`) const colors = ['#9D9FFF', '#F57BFF', '#3AB1FF', '#D38BFF', '#00C745', '#F4952C'];
.attr('font-weight', `${config.titleConfig.fontWeight}`) const color = setColor(minNumber, maxNumber, colors);
.style('text-anchor', 'middle') const configObj = { ...config, root, orderingx, maxNumber, minNumber, color };
.text('热图热图热图热图热图热图热图热图热图'); const heatmapRect = setBasisHeatmap(configObj);
// tips
setTips(heatmapRect);
// legend
setBasisLegend(configObj);
}; };
// 提示 // Consensus
const setTips = (rect) => { const generageConsensus = (config) => {
const tips = d3 const chartData = chartMockData.slice(2, 4);
.select('.tip') // max/min number
.style('visibility', 'hidden') const { maxNumber, minNumber } = setMaxMinNumber(chartData);
.style('position', 'relative') // root config
.style('background-color', 'rgba(0, 0, 0, .8)') const { root, orderingx } = getRootConfig(config, chartData);
.style('color', '#fff') // color config
.style('border-radius', '5px') const colors = ['#C3AD00', '#6EC000', '#00CC7B', '#00CDC9', '#FF7B9C', '#FF876E'];
.style('width', '200px') const color = setColor(minNumber, maxNumber, colors);
.style('text-align', 'center') const configObj = { ...config, root, orderingx, maxNumber, minNumber, color };
.style('height', '20px') const heatmapRect = setConsensusHeatmap(configObj);
.style('padding', '5px'); // tips
setTips(heatmapRect);
rect.on('mouseover', (event, d) => { // legend
tips.style('visibility', 'visible'); setConsensusLegend(configObj);
})
.on('mousemove', function (event, d) {
tips.style('visibility', 'visible')
.style('left', event.offsetX + 15 + 'px')
.style('top', event.offsetY + 50 + 'px')
.html(`${d}`);
})
.on('mouseleave', function () {
tips.style('visibility', 'hidden');
});
}; };
const setColorConfig = (color) => { // Silhouette
let colors = [ const generageSilhouette = (config) => {
'#313695', const chartData = chartMockData.slice(4, 6);
'#436FB1', // max/min number
'#6BA2CB', const { maxNumber, minNumber } = setMaxMinNumber(chartData, false);
'#9BCCE2', // root config
'#F0F9D8', const { root, orderingx } = getRootConfig(config, chartData);
'#F0F9D8', // color config
'#FEF0A9', const colors = ['#C3AD00', '#6EC000', '#00CC7B', '#00CDC9', '#FF7B9C', '#FF876E'];
'#FDCD7E', const color = setColor(minNumber, maxNumber, colors);
'#FA9C58', const configObj = { ...config, root, orderingx, maxNumber, minNumber, color };
'#EE613D', const heatmapRect = setSilhouetteHeatmap(configObj);
'#D22B26', // tips
'#A50026' setTips(heatmapRect);
]; // legend
if (Array.isArray(color)) { setSilhouetteLegend(configObj);
colors = !!color.length ? color : colors;
} else if (!!color) {
colors = color.split(',');
}
return colors;
}; };
onMounted(() => { onMounted(() => {
const chartData = [...chartMackData]; const chartData = chartMockData;
generateChart(chartData); generateChart(chartData);
}); });
</script> </script>
......
export const chartMackData = [ export const chartMockData = [
{ {
"id": "3478_7", "id": "3478_7",
"cls": "c-CS-s", "cls": "c-CS-s",
...@@ -338,35 +338,35 @@ export const chartMackData = [ ...@@ -338,35 +338,35 @@ export const chartMackData = [
0.627634368, 0.627634368,
0.78077067, 0.78077067,
0.331712184, 0.331712184,
2.299912707, 0.299912707,
3.990896621, 0.990896621,
0.211497693, 0.211497693,
0.161740865, 0.161740865,
2.580995137, 0.580995137,
0.208380097, 0.208380097,
1.391445317, 0.391445317,
0.624392069, 0.624392069,
0.297293927, 0.297293927,
0.325227584, 0.325227584,
0.280209502, 0.280209502,
0.747474747, 0.747474747,
0.597580746, 0.597580746,
1.540965208, 0.540965208,
1.976929792, 0.976929792,
0.442823295, 0.442823295,
0.639481232, 0.639481232,
0.400922808, 0.400922808,
0.401421624, 0.401421624,
0.15538097, 0.15538097,
1.306646714, 0.306646714,
2.87916199, 0.87916199,
1.298540965, 0.298540965,
0.2410525, 0.2410525,
0.309265494, 0.309265494,
0.751964085, 0.751964085,
0.153510413, 0.153510413,
0.496071829, 0.496071829,
2.208005986, 0.208005986,
0.323606435, 0.323606435,
0.384836014, 0.384836014,
0.291682255, 0.291682255,
...@@ -381,10 +381,10 @@ export const chartMackData = [ ...@@ -381,10 +381,10 @@ export const chartMackData = [
0.23615096, 0.23615096,
0.984845141, 0.984845141,
0.175163687, 0.175163687,
2.266460678, 0.266460678,
0.3534172, 0.3534172,
0.615684544, 0.615684544,
1.58471272, 0.58471272,
0.319208416, 0.319208416,
0.158316781, 0.158316781,
0.2088575, 0.2088575,
...@@ -397,10 +397,10 @@ export const chartMackData = [ ...@@ -397,10 +397,10 @@ export const chartMackData = [
0.133083205, 0.133083205,
0.521003458, 0.521003458,
0.335172515, 0.335172515,
1.650849702, 0.650849702,
2.406238505, 0.406238505,
0.15699257, 0.15699257,
1.332744795, 0.332744795,
0.936143603, 0.936143603,
0.233502538, 0.233502538,
0.156109762, 0.156109762,
...@@ -411,7 +411,7 @@ export const chartMackData = [ ...@@ -411,7 +411,7 @@ export const chartMackData = [
0.187596557, 0.187596557,
0.171264621, 0.171264621,
0.204958434, 0.204958434,
1.80004414 0.80004414
] ]
}, },
{ {
...@@ -421,35 +421,35 @@ export const chartMackData = [ ...@@ -421,35 +421,35 @@ export const chartMackData = [
0.715018037, 0.715018037,
0.925954054, 0.925954054,
0.338902601, 0.338902601,
2.373457376, 0.373457376,
3.55686349, 0.55686349,
0.250806911, 0.250806911,
0.194797798, 0.194797798,
5.544902221, 0.544902221,
0.223087146, 0.223087146,
1.599012721, 0.599012721,
0.936016708, 0.936016708,
0.32789064, 0.32789064,
0.420732865, 0.420732865,
0.271122081, 0.271122081,
0.832921967, 0.832921967,
0.645528764, 0.645528764,
1.50806911, 0.50806911,
2.932409341, 0.932409341,
0.632618189, 0.632618189,
0.588380482, 0.588380482,
0.601480919, 0.601480919,
0.362825138, 0.362825138,
0.185114866, 0.185114866,
1.134042149, 0.134042149,
1.986709702, 0.986709702,
1.402885893, 0.402885893,
0.270172774, 0.270172774,
0.274729448, 0.274729448,
0.745965445, 0.745965445,
0.1815075, 0.1815075,
0.505031327, 0.505031327,
2.080121511, 0.080121511,
0.332067591, 0.332067591,
0.459464591, 0.459464591,
0.386937536, 0.386937536,
...@@ -464,10 +464,10 @@ export const chartMackData = [ ...@@ -464,10 +464,10 @@ export const chartMackData = [
0.181637914, 0.181637914,
0.949038271, 0.949038271,
0.165377751, 0.165377751,
3.10668253, 0.10668253,
0.30705929, 0.30705929,
0.627900059, 0.627900059,
1.073765616, 0.073765616,
0.311620067, 0.311620067,
0.155463018, 0.155463018,
0.168451319, 0.168451319,
...@@ -480,10 +480,10 @@ export const chartMackData = [ ...@@ -480,10 +480,10 @@ export const chartMackData = [
0.125024787, 0.125024787,
0.464207813, 0.464207813,
0.282272457, 0.282272457,
1.528356137, 0.528356137,
2.087051358, 0.087051358,
0.149910767, 0.149910767,
1.187487607, 0.187487607,
0.938132064, 0.938132064,
0.211580408, 0.211580408,
0.172912949, 0.172912949,
...@@ -494,7 +494,7 @@ export const chartMackData = [ ...@@ -494,7 +494,7 @@ export const chartMackData = [
0.186496133, 0.186496133,
0.183819155, 0.183819155,
0.202260559, 0.202260559,
1.579317866 0.579317866
] ]
}, },
{ {
...@@ -8299,4 +8299,5 @@ export const chartMackData = [ ...@@ -8299,4 +8299,5 @@ export const chartMackData = [
1.144377729 1.144377729
] ]
} }
] ]
\ No newline at end of file
import distance from 'distancejs';
import extend from 'extend';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
// hcluster
export const hcluster = () => {
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;
};
// 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 verticalData = () => {
return d3.range(chartData[0].values.length).map((d) => ({ normalized: normalizedData().map((el) => el.normalized[d]) }));
};
const clustering = hcluster().distance('angular').linkage('avg').posKey('normalized').data(normalizedData());
const verticalClustering = hcluster().distance('angular').linkage('avg').posKey('normalized').data(verticalData());
const tree = (data) => {
const root = d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height));
root.dx = config.radius;
root.dy = config.dendWidth / (root.height + 1);
return d3
.cluster()
.size([config.heatHeight, config.dendWidth])
.separation(() => 2)(root);
};
const vertTree = (data) => {
const root = d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height));
root.dy = config.radius;
root.dx = config.dendWidth / (root.height + 1);
return d3
.cluster()
.size([config.heatWidth, config.dendWidth])
.separation(() => 2)(root);
};
const root = tree(clustering.tree());
const vertRoot = vertTree(verticalClustering.tree());
const orderingx = vertRoot.leaves().map((el) => el.data.indexes[0]);
return {
root,
vertRoot,
orderingx
}
}
// color config
export const setColor = (min, max, color) => {
let colors = [
'#313695',
'#436FB1',
'#6BA2CB',
'#9BCCE2',
'#F0F9D8',
'#F0F9D8',
'#FEF0A9',
'#FDCD7E',
'#FA9C58',
'#EE613D',
'#D22B26',
'#A50026'
];
if (Array.isArray(color)) {
colors = !!color.length ? color : colors;
} else if (!!color) {
colors = color.split(',');
}
return () => d3.scaleQuantize().domain([min, max]).range(colors);
};
// maxnumber minNumber
export const setMaxMinNumber = (chartData, isInteger = true) => {
let maxNumArr = [],
minNumArr = [],
maxNumber = 0,
minNumber = 0;
for (let i = 0; i < chartData.length; i++) {
maxNumArr.push(d3.max(chartData[i].values));
minNumArr.push(d3.min(chartData[i].values));
}
// 最大值 最小值
maxNumber = isInteger ? Math.round(d3.max(maxNumArr)) : d3.max(maxNumArr);
minNumber = isInteger ? Math.round(d3.min(minNumArr)) : d3.min(minNumArr);
return {
maxNumber,
minNumber
}
}
// 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('text-align', 'center')
.style('height', '20px')
.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 + 50 + 'px')
.html(`${d}`);
})
.on('mouseleave', function () {
tips.style('visibility', 'hidden');
});
};
// heatmap
export const setHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin + config.dendWidth * 2})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${d.x - config.radius})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', config.color());
};
// Basis heatmap
export const setBasisHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${130 + config.radius * 2 * i})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', config.color());
};
// Consensus heatmap
export const setConsensusHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${150 + config.radius * 2 * i})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', config.color());
};
// Silhouette heatmap
export const setSilhouetteHeatmap = (config) => {
return config.svg
.append('g')
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin * 3})`)
.selectAll('g')
.data(config.root.leaves())
.join('g')
.attr('transform', (d, i) => `translate(0, ${150 + config.radius * 2 * i})`)
.selectAll('rect')
.data((d) => d.data.values.map((e, i) => d.data.values[config.orderingx[i]]))
.join('rect')
.attr('x', (d, i) => i * 2 * config.radius)
.attr('y', 0)
.attr('width', 2 * config.radius)
.attr('height', 2 * config.radius)
.attr('fill', d => d3.interpolateGreens(d));
};
// tree
export const setTree = (config) => {
// left tree
config.svg.append('g')
.attr('fill', 'none')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', `translate(${config.margin}, ${config.margin + config.dendWidth * 2})`)
.selectAll('path')
.data(config.root.links())
.join('path')
.attr(
'd',
(d) => `
M${d.target.y},${d.target.x}
L${d.source.y},${d.target.x}
${d.source.y},${d.source.x}
${d.source.y},${d.source.x}
`
);
// top tree
config.svg.append('g')
.attr('fill', 'none')
.attr('stroke', '#555')
.attr('stroke-width', 1)
.attr('transform', `translate(${config.margin + config.dendWidth}, ${config.margin * 3})`)
.selectAll('path')
.data(config.vertRoot.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}
`
);
};
// legend
export const setLegend = (config) => {
const legendData = [...Array.from({ length: config.maxNumber }, (item, i) => i)];
config.svg.append('g')
.selectAll()
.data(legendData)
.join('g')
.attr('transform', (d, i, nodes) => `translate(${config.dendWidth + config.xAxisLength * (config.radius * 2) + 80}, ${config.height / 3 + (nodes.length / 2 - i - 1) * 18 - 60})`)
.call((g) => g.append('rect').attr('width', 18).attr('height', 18).attr('fill', config.color()))
.call((g) =>
g
.append('text')
.attr('x', 20)
.attr('y', 10)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
};
// Basis legend
export const setBasisLegend = (config) => {
const legendData = [...Array.from({ length: config.maxNumber }, (item, i) => i)];
const textX = config.dendWidth + config.xAxisLength * (config.radius * 2) + 180
config.svg.append('text')
.attr('x', textX)
.attr('y', config.height / 3 - 110)
.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('basis');
config.svg.append('g')
.selectAll()
.data(legendData)
.join('g')
.attr('transform', (d, i, nodes) => `translate(${config.dendWidth + config.xAxisLength * (config.radius * 2) + 160}, ${config.height / 3 + (nodes.length / 2 - i - 1) * 21 - 50})`)
.call((g) => g.append('rect').attr('width', 15).attr('height', 15).attr('fill', config.color()))
.call((g) =>
g
.append('text')
.attr('x', 20)
.attr('y', 9)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
}
// Consensus legend
export const setConsensusLegend = (config) => {
const legendData = [...Array.from({ length: config.maxNumber }, (item, i) => i)];
const textX = config.dendWidth + config.xAxisLength * (config.radius * 2) + 205
config.svg.append('text')
.attr('x', textX)
.attr('y', config.height / 3 + 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('consensus');
config.svg.append('g')
.selectAll()
.data(legendData)
.join('g')
.attr('transform', (d, i, nodes) => `translate(${config.dendWidth + config.xAxisLength * (config.radius * 2) + 160}, ${config.height / 3 + (nodes.length / 2 - i - 1) * 21 + 110})`)
.call((g) => g.append('rect').attr('width', 15).attr('height', 15).attr('fill', config.color()))
.call((g) =>
g
.append('text')
.attr('x', 20)
.attr('y', 9)
.attr('dy', '0.3em')
.style('font', '13px sans-serif')
.text((d) => d)
);
}
// Silhouette legend
export const setSilhouetteLegend = (config) => {
const textX = config.dendWidth + config.xAxisLength * (config.radius * 2) + 200
const legendData = [config.maxNumber, config.minNumber]
config.svg.append('text')
.attr('x', textX)
.attr('y', config.height / 3 + 210)
.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('silhouette');
config.svg.append('g')
.selectAll()
.data(legendData)
.join('text')
.attr('x', textX + 20)
.attr('y', (d, i) => config.height / 3 + i * 40 + 240)
.attr('fill', config.titleConfig.color)
.attr('font-style', `${config.titleConfig.fontStyle}`)
.attr('font-weight', `${config.titleConfig.fontWeight}`)
.style('font', '13px sans-serif')
.style('text-anchor', 'middle')
.text((d) => d);
const gradient = config.svg
.append('defs')
.append('linearGradient')
.attr('id', 'mylLnearGradient')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '0%')
.attr('y2', '100%')
.attr('spreadMethod', 'pad');
gradient
.append('stop')
.attr('offset', '0%')
.attr('stop-color', 'rgb(0,128,0,1)')
.attr('stop-opacity', 1);
gradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', 'rgb(0,128,0,0.1)')
.attr('stop-opacity', 1);
config.svg.append('rect')
.attr('width', '15')
.attr('height', '60')
.attr("fill", "url(#mylLnearGradient)")
.attr("x", `${config.dendWidth + config.xAxisLength * (config.radius * 2) + 160}`)
.attr("y", `${config.height / 3 + 225}`)
}
// 标签
export const setAxisTitle = (config) => {
const labelList = config.root.leaves().map((item) => item.data.id);
config.svg.append('g')
.selectAll('g')
.data(labelList)
.join('g')
.attr(
'transform',
(d, i) =>
`translate(${config.margin + config.dendWidth + config.xAxisLength * 2 * config.radius + 4},${config.margin + config.dendWidth * 2 + i * 2 * config.radius + 4
})`
)
.call((g) =>
g
.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('dy', '0.35em')
.style('font', `6px sans-serif`)
.style('fill', `#000`)
.text((d) => d)
);
};
// 标题
export const setLabel = (config) => {
config.svg.append('text')
.attr('x', config.width / 3)
.attr('y', 0)
.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('Consensus matrix');
};
...@@ -19,7 +19,7 @@ import ClusterHeatmap from '../../components/ClusterHeatmap/index.vue'; ...@@ -19,7 +19,7 @@ import ClusterHeatmap from '../../components/ClusterHeatmap/index.vue';
import CircularHeatmap from '../../components/CircularHeatmap/index.vue'; import CircularHeatmap from '../../components/CircularHeatmap/index.vue';
import Test from '../../components/Test/index.vue'; import Test from '../../components/Test/index.vue';
const chartCurrent = ref(0); const chartCurrent = ref(6);
const chartList = shallowRef([ const chartList = shallowRef([
{ title: '线性拟合图', value: 1, comName: LinearFitting }, { title: '线性拟合图', value: 1, comName: LinearFitting },
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论