是否存在一些用于构建带有组(如http://projects.delimited.io/experiments/force-bubbles/)但具有巨大数据集的强制布局气泡图的算法。我的主要问题是选择正确的空格,在本示例中,它们使用一些静态偏移量。但是,如果您的小组中有1万个气泡,而有些气泡只有100个,它们通常会碰撞并覆盖标题。也许您可以使用类似https://en.m.wikipedia.org/wiki/Circle_packing_in_a_circle的方法来决定,但不确定如何处理不同的大小
答案 0 :(得分:1)
请考虑以下因素:
SVG可以显示有限数量的元素(建议限制为10K)
人眼可以识别屏幕上的数百个视觉项目(并专注于十二个,而不是更多)
我建议使用Google Maps方法,即定义虚拟空间,并根据视口位置和缩放级别仅显示其中的相关部分。
const space = {
top: -25000,
left: -25000,
right: 25000,
bottom: 25000
};
const generateRandomCircles = count => {
let circles = [];
const spaceWidth = space.right - space.left;
const spaceHeight = space.bottom - space.top;
for (let id = 1; id <= count; id++) {
const x = space.left + Math.random() * spaceWidth;
const y = space.top + Math.random() * spaceHeight;
const r = 30 + Math.random() * 70;
const c = () => 128 + Math.floor(Math.random() * 128);
const color = `rgb(${c()},${c()},${c()})`;
const circle = {id, x, y, r, color};
circles.push(circle);
}
return circles.sort((a, b) => b.r - a.r);
};
const svg = d3.select('svg')
const width = parseInt(svg.attr('width'));
const height = parseInt(svg.attr('height'));
const circles = generateRandomCircles(50000);
const virtualToScreen = (point, transform) => {
const x = (point.x + width/2) * transform.k + transform.x;
const y = (point.y + height/2) * transform.k + transform.y;
return {x, y};
};
const screenToVirtual = (point, transform) => {
const x = (point.x - transform.x) / transform.k - width/2;
const y = (point.y - transform.y) / transform.k - height/2;
return {x, y};
};
const drawMarker = (pos, label, isVertical, showLabel=true) => {
svg.append('line')
.classed('marker-line', true)
.attr('x1', isVertical ? pos : 0)
.attr('x2', isVertical ? pos : width)
.attr('y1', isVertical ? 0 : pos)
.attr('y2', isVertical ? height : pos)
.style('stroke', '#789');
if(showLabel) {
svg.append('text')
.classed('marker-label', true)
.text(label)
.attr('x', isVertical ? pos : 3)
.attr('y', isVertical ? 14 : pos + 4)
.attr('text-anchor', isVertical ? 'middle' : 'start')
.style('fill', '#def');
svg.append('text')
.classed('marker-label', true)
.text(label)
.attr('x', isVertical ? pos : width - 3)
.attr('y', isVertical ? height - 4 : pos + 4)
.attr('text-anchor', isVertical ? 'middle' : 'end')
.style('fill', '#def');
}
};
const scaleFactors = () => {
let factors = [1];
let multiplier = 1;
for (let index = 0; index < 20; index++) {
multiplier *= index % 3 == 1 ? 2.5 : 2;
factors.push(multiplier);
}
return factors.reverse();
}
const updateMarkers = transform => {
svg.selectAll('.marker-label,.marker-line').remove();
const factor = width / 5 / transform.k;
const step = scaleFactors()
.find(step => step < factor) || 1;
const stl = {x: 0, y: 0};
const sbr = {x: width, y: height};
const vtl = screenToVirtual(stl, transform);
const vbr = screenToVirtual(sbr, transform);
const fromX = Math.ceil(vtl.x / step) * step;
const toX = Math.floor(vbr.x / step) * step;
for (let x = fromX; x <= toX; x += step) {
const pos = virtualToScreen({x, y: 0}, transform);
drawMarker(pos.x, x, true, x > fromX && x < toX);
}
const fromY = Math.ceil(vtl.y / step) * step;
const toY = Math.floor(vbr.y / step) * step;
for (let y = fromY; y <= toY; y += step) {
const pos = virtualToScreen({x: 0, y}, transform);
drawMarker(pos.y, y, false);
}
};
const updateView = transform => {
const mapper = circle => {
const point = virtualToScreen(circle, transform);
const r = circle.r * transform.k;
return {...circle, ...point, r};
};
const circleIsVisible = circle =>
(circle.x + circle.r > 0 &&
circle.x - circle.r < width &&
circle.y + circle.r > 0 &&
circle.y - circle.r < height &&
circle.r > 2);
const start = performance.now();
const filtered = circles.map(mapper)
.filter(circleIsVisible);
const all = svg.selectAll('circle')
.data(filtered, d => d.id)
const added = all.enter()
.append('circle')
.style('fill', d => d.color)
.style('opacity', 0.5)
added.merge(all)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r);
all.exit().remove();
updateMarkers(transform);
};
const delta = () => {
return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1) / 5000;
}
const onZoom = () => updateView(d3.event.transform);
const svgZoom = d3.zoom()
.wheelDelta(delta)
.clickDistance(10)
.scaleExtent([0.1, 10])
.on('zoom', onZoom);
svg.call(svgZoom);
updateView({x: 0, y: 0, k: 1});
html, body {
margin: 0;
padding: 0;
}
svg {
background-color: #123;
font-family: Calibri;
font-size: 12pt;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width='500' height='300'>
</svg>