Published
Edited
Mar 11, 2021
1 star
Insert cell
Insert cell
chart = {
const div = d3.create('div');
// 左图的画布,zoom 事件需要在这里响应
const svgChart = div.append('svg')
.attr('width', config.viewWidth)
.attr('height', config.viewHeight);
const stageChartWrapper = svgChart.append('g') // 左图的内容的容器,和 brush 联动,保持内容的 translate 属性
// 左图的内容
const stageChart = stageChartWrapper.append('g')
.attr("transform", `translate(${config.margin}, ${config.margin})`) // 如果没有 wrapper,这一行就需要在 brush move 完以后再设置左图的 margin
.selectAll('path') // 画图
.data(data)
.join('path')
.attr('d', lineGen)

// 小地图的容器
const svgMinimap = div.append('svg')
.attr('width', config.viewWidth) // 和左图一样,其实可以设置的更小一些
.attr('height', config.viewHeight)
// .attr('width', minimapScaleX(config.minimapScale)(config._width))
// .attr('height', minimapScaleY(config.minimapScale)(config._height))
.attr('viewBox', [0, 0, config._width, config._height].join(' ')) // 让小地图内部可以显示完整图片内容
.attr('preserveAspectRatio', 'xMidYMid meet');
// 小地图的粉色背景
svgMinimap.append('rect')
.attr('width', config._width) // 需要和小地图容器的 viewBox 属性一致
.attr('height', config._height)
.attr('fill', 'pink');
// 小地图的内容
const stageMinimap = svgMinimap.append('g')
.attr("transform", `translate(${config.margin}, ${config.margin})`) // 和 stateChartWrapper 一致
.selectAll('path') // 绘图也和左图一致
.data(data)
.join('path')
.attr('d', lineGen)
// 小地图的刷子
const stageBrush = svgMinimap.append('g');
// 左图的 zoom behavior
const zoom = d3.zoom()
.scaleExtent([config.minimapScale, 1]) // 缩小时刚好充满容器,放大时 1:1
.translateExtent([[0, 0], [config._width, config._height]]) // 拖动时不允许移出容器边界
// 小地图的 brush behavior
const brush = d3.brush()
.extent([[0, 0], [config._width, config._height]]) // 设定最大大小,其实已经受到左图 zoom extent 的限制了

function onBrush() {
// prevent zoom invoked event
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom")
return null;
// console.log('onBrush', d3.event);
if (Array.isArray(d3.event.selection)) {
const [[brushX, brushY], [brushX2, brushY2]] = d3.event.selection; // brush 左上角、右下角的坐标
const zoomScale = d3.zoomTransform(stageChartWrapper.node()).k; // 获取此时左图的缩放比例,为 1 表示不缩放
// console.log(svgChart)
// console.log(zoomScale)
const scaleX = minimapScaleX(zoomScale); // 一个线性变换函数,将实际坐标(也就是 brush 的坐标)换算成缩放后的坐标(也就是左图的坐标)
const scaleY = minimapScaleY(zoomScale);

// 容器设置 transform 属性,内容设置 translate 偏移
svgChart.call(
zoom.transform,
d3.zoomIdentity.translate(scaleX(-brushX), scaleY(-brushY)).scale(zoomScale)
);
stageChartWrapper.attr("transform", `translate(${scaleX(-brushX)}, ${scaleY(-brushY)}) scale(${zoomScale})`);
}
}
function onZoom() {
//prevent brush invoked event
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") // 如果把这一行注释掉,在拖动 brush 的时候也会有 zoom 事件,即:event.type == zoom, event.sourceEvent.type == brush
return null;

// console.log('onZoom', d3.event);

const t = d3.event.transform;
stageChartWrapper.attr("transform", t); // 左图响应 zoom 事件

// brush 同步变更大小
const scaleX = minimapScaleX(t.k);
const scaleY = minimapScaleY(t.k);
brush.move(stageBrush, [
[scaleX.invert(-t.x), scaleY.invert(-t.y)],
[
scaleX.invert(-t.x + config.viewWidth),
scaleY.invert(-t.y + config.viewHeight)
]
]);
}
brush.on('brush', onBrush);
zoom.on('zoom', onZoom);
svgChart.call(zoom);
stageBrush.call(brush).call(brush.move, [ // 设置 brush 的初始大小
[0, 0],
[config.viewWidth, config.viewHeight]
]);

// svgMinimap.selectAll('.handle').remove();
// svgMinimap.selectAll('.overlay').remove();

return div.node();
}
Insert cell
html`
<style>
svg path {
stroke: black;
stroke-width: 1px;
fill: none;
}

</style>`
Insert cell
lineGen = d3
.line()
.x((d, i) => lineScaleX(i)) // 对每组数据映射值到横坐标范围
.y(lineScaleY)
.curve(d3.curveBasis)
Insert cell
maxValueY = d3.max(data, d => d3.max(d))
Insert cell
maxValueX = d3.max(data, d => d.length - 1)
Insert cell
lineScaleX = d3
.scaleLinear()
.domain([0, maxValueX])
.range([0, config.clippedWidth])
Insert cell
Insert cell
minimapScaleX = zoomScale => d3.scaleLinear(
[0, config._width],
[0, config._width * zoomScale]
)
Insert cell
minimapScaleY = zoomScale =>
d3.scaleLinear([0, config._height], [0, config._height * zoomScale])
Insert cell
config = ({
..._config,
clippedWidth: _config._width - _config.margin * 2,
clippedHeight: _config._height - _config.margin * 2,
minimapScale: _config.viewWidth / _config._width,
viewHeight: _config._height * (_config.viewWidth / _config._width)
})
Insert cell
_config = ({
margin: 20,
_width: 1000, // svg 内容大小
_height: 1000,
viewWidth: 300, // svg 容器区域大小
})
Insert cell
ttrans = (x, y, k) => ['transform', trans(x, y, k)]
Insert cell
trans = (x, y, k) => {
const coord2d = `translate(${x}, ${y})`;
if (!k) return coord2d;
return coord2d + ` scale(${k})`;
}
Insert cell
d3 = require('d3@5.15.0')
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more