import { VennGraphOptions } from '../../models'; import AtomComponent from '../../atomComponent'; import { merge } from '../../utils'; import { venn, scaleSolution, circlePath, intersectionAreaPath, computeTextCentres } from './vennJs/index'; import SvgPath from './svgpath2canvasJs'; import { SeriesCategory } from '../../constants'; export default class Venn extends AtomComponent { private static readonly defaultOptions: VennGraphOptions = { type: 'venn', data: [] }; protected seriesCategory = SeriesCategory.SINGLE; private options: VennGraphOptions; private color: string; constructor(id: string, chart, options: VennGraphOptions) { super({ id, chart }); this.options = this.processOptions(Venn.defaultOptions, options); } public render() { const { data } = this.options; this.drawSets(data); } private drawSets(sets) { const { canvas } = this; const { ctx, size: { width, height }, } = canvas; const INNER_PADDING = 30; const MAGIC_NUMBER_FOR_LAYOUT = 45; const colors = this.computeStrokeColor(); const solution = venn(sets); const circles = scaleSolution(solution, width + MAGIC_NUMBER_FOR_LAYOUT, height + MAGIC_NUMBER_FOR_LAYOUT, INNER_PADDING); const textCenters = computeTextCentres(circles, sets); const insectInfos = []; sets.forEach((set, i) => { const { sets: innerSets } = set; const id = innerSets.join(','); ctx.beginPath(); let path: string; let textCenter: any; if (innerSets.length === 1) { const circle = circles[id]; path = circlePath(circle.x, circle.y, circle.radius); } else { const setCircles = innerSets.map(set => circles[set]); path = intersectionAreaPath(setCircles); textCenter = textCenters[id]; insectInfos.push({ set, textCenter, color: colors[i] }); } const color = colors[i]; ctx.strokeStyle = color; ctx.fillStyle = color; // 多边形区域 const sp = new SvgPath(path); sp.save().beginPath() .to(ctx) .stroke() .fill(); }); // 相交区域标识 insectInfos.forEach(({ textCenter }, index) => { const { x, y } = textCenter; // 背景 this.painter.draw( 'Circle', { x, y, r: 6, fillStyle: '#000', strokeStyle: '#000', }, false, ); // 序号 【REMINDER: 由于依赖库计算出的坐标是canvas坐标,故此处无法使用painter绘制】 ctx.font = '10px PingFang SC serif'; ctx.fillStyle = '#fff'; ctx.textBaseline = 'middle'; ctx.fillText(`${index + 1}`, x - 3, y); }); // 图例 this.drawVennLegend(sets, insectInfos); } // Venn图例数据组装 private processValue4Legend(rawSets, rawInsectInfos) { const setsInfos = rawInsectInfos.map(({ set }) => ({ ...set })); return setsInfos.map((setsInfo) => { const { name, size, sets } = setsInfo; const setsWithPerc = sets.map((set) => { const total = rawSets.reduce((acc, { sets, size }) => { if (sets.includes(set)) { return acc + size; } return acc; }, 0); const perc = `${((size / total) * 100).toFixed(1)}%`; return { name: set, val: perc, }; }); return { name, size, setsWithPerc, }; }); } // Venn 图例定制化 故在组件中实现 private drawVennLegend(sets, rawInsectInfos) { const insectInfos = this.processValue4Legend(sets, rawInsectInfos); const { canvas: { size }, } = this; const DOT_RADIUS = 6; const INTERVAL = 4; const LINE_HEIGHT = 30; const LINE_WIDTH = size.width; let areaTop = 0; insectInfos.forEach((legendInfo, index) => { const { size, setsWithPerc } = legendInfo; const innerPadding = 20; // 总量 this.painter.draw('Circle', { x: innerPadding, y: areaTop, r: DOT_RADIUS, fillStyle: '#000', strokeStyle: '#000', }); this.painter.draw('Text', { text: `${index + 1}`, x: innerPadding - 3, y: areaTop, fontStyle: '#fff', fontSize: 10, fontFamily: 'PingFang SC serif', textBaseline: 'middle', textAlign: 'left', fontWeight: '400', }); this.painter.draw('Text', { text: `${(size / 10000).toFixed(1)}万`, x: innerPadding + 2 * INTERVAL + INTERVAL, y: areaTop, fontStyle: '#4B5B76', fontSize: 10, fontFamily: 'PingFang SC serif', textBaseline: 'middle', }); areaTop -= LINE_HEIGHT; setsWithPerc.forEach(({ name, val }, itemIndex) => { // 集合具体信息列表 const itemLeft = (LINE_WIDTH / 2) * (itemIndex % 2); const itemTop = areaTop - LINE_HEIGHT * Math.floor(itemIndex / 2); const colors = this.computeStrokeColor(); const colorIndex = sets.findIndex(set => set.sets.length === 1 && name.includes(set.name)); const itemColor = colors[colorIndex]; this.painter.draw('Circle', { x: itemLeft + innerPadding, y: itemTop, r: DOT_RADIUS, fillStyle: itemColor, strokeStyle: itemColor, }); this.painter.draw('Text', { text: name, x: itemLeft + innerPadding + 2 * INTERVAL + INTERVAL, y: itemTop, fontStyle: '#4B5B76', fontSize: 10, fontFamily: 'PingFang SC serif', textBaseline: 'middle', textAlign: 'left', }); this.painter.draw('Text', { text: val, x: (LINE_WIDTH / 2) * ((itemIndex % 2) + 1) - 29, y: itemTop, fontStyle: '#4B5B76', fontSize: 10, fontFamily: 'PingFang SC serif', textBaseline: 'middle', textAlign: 'right', }); }); areaTop -= LINE_HEIGHT * Math.ceil(setsWithPerc.length / 2); }); } private processStyle({ itemStyle: rawItemStyle, lineStyle, areaStyle }) { return { itemStyle: { ...rawItemStyle, }, lineStyle, areaStyle, }; } private processOptions(defaultOptions, userOptions) { // tvision-token建议配色 // const { SEQUENTIAL_COLORS } = ThemeLight; // [ // SEQUENTIAL_COLORS.BLUE[7], // SEQUENTIAL_COLORS.GREEN[4], // SEQUENTIAL_COLORS.ORANGE[4], // SEQUENTIAL_COLORS.CYAN[5], // SEQUENTIAL_COLORS.RED[5], // SEQUENTIAL_COLORS.YELLOW[5], // SEQUENTIAL_COLORS.PINK[5], // ]; // t4建议配色 【REMINDER: venn特殊场景色轮定制化】 const vennColorPalette = ['#006EFF', '#0DB9D0', '#6DCF5D', '#7269F0', '#37BF86', '#FF9E00', '#FFC900', '#ECE118', '#DB4B8E', '#F54E4E']; const options = merge(defaultOptions, userOptions); // 颜色处理 const style = this.processStyle(options); return { ...options, ...style, colors: vennColorPalette, }; } private computeStrokeColor() { const { colors = [] } = this?.options; return colors; } }