You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在D3.js聚焦上下文多线图中限制画笔对齐离散日期?

D3.js聚焦-上下文多线图:实现画笔吸附离散日期需求

我基于D3.js实现了带有聚焦区(focus)和上下文区(context)的多线图,可通过上下文区的画笔(brush)选择不同日期范围。但我的数据仅包含每月1日的日期,当前画笔允许在任意日期间选择,我需要让画笔吸附到数据中的离散日期上,这样后续才能方便地将选中数据导出到表格。之前找到过一个基于D3v3的示例,但无法适配我的D3v7项目,寻求技术解决方案。


HTML代码

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<div id="timeseries-container"></div>

CSS代码

.line {
    stroke: #333;
    fill: none;
    clip-path: url(#clip);
  }

  .zoom {
    cursor: move;
    fill: none;
    pointer-events: all;
  }

  .handle {
    fill: #4D4E56;
    stroke: #fff;
    stroke-opacity: 0.5;
    stroke-width: 1.25px;
  }

JavaScript代码

let timeW = 960,
    timeH = 500

  let timeMargin = { top: 20, right: 250, bottom: 130, left: 60 },
    timeMargin2 = { top: 410, right: 250, bottom: 30, left: 60 },
    timeWidth = timeW - timeMargin.left - timeMargin.right,
    timeHeight = timeH - timeMargin.top - timeMargin.bottom,
    timeHeight2 = timeH - timeMargin2.top - timeMargin2.bottom;

  var parseDate = d3.timeParse("%Y-%m-%d");

  let timeseries = d3.select("#timeseries-container").append('svg')
    .attr('id', 'timeseries')
    .attr("width", timeWidth + timeMargin.left + timeMargin.right)
    .attr("height", timeHeight + timeMargin.top + timeMargin.bottom)

  var graph = timeseries.append('g').attr('transform', 'translate(' + timeMargin.left + ',' + timeMargin.top + ')');

  var x2 = d3.scaleTime().range([0, timeWidth]),
    x3 = d3.scaleTime().range([0, timeWidth]),
    y2 = d3.scaleLinear().range([timeHeight, 0]),
    y3 = d3.scaleLinear().range([timeHeight2, 0]);

  var xAxis = d3.axisBottom(x2),
    xAxis2 = d3.axisBottom(x3),
    yAxis = d3.axisLeft(y2);

  var brush = d3.brushX()
    .extent([[0, 0], [timeWidth, timeHeight2]])
    .on("brush end", brushed);

  var zoom = d3.zoom()
    .scaleExtent([1, Infinity])
    .translateExtent([[0, 0], [timeWidth, timeHeight]])
    .extent([[0, 0], [timeWidth, timeHeight]])
    .on("zoom", zoomed);

  var line = d3.line()
    .x(function (d) { return x2(d.date); })
    .y(function (d) { return y2(d.value); });

  var line2 = d3.line()
    .x(function (d) { return x3(d.date); })
    .y(function (d) { return y3(d.value); });

  timeseries.append("defs").append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", timeWidth)
    .attr("height", timeHeight);

  var focus = timeseries.append("g")
    .attr("class", "focus")
    .attr("transform", "translate(" + timeMargin.left + "," + timeMargin.top + ")");

  var context = timeseries.append("g")
    .attr("class", "context")
    .attr("transform", "translate(" + timeMargin2.left + "," + timeMargin2.top + ")");

  d3.csv("https://raw.githubusercontent.com/sprucegoose1/sample-data/main/sample.csv").then(function (data) {

    data.forEach((d) => {
      d.date = parseDate(d.date);
      d.value = +d.value;
    })

    x2.domain(d3.extent(data, function (d) { return d.date; }));
    y2.domain([0, d3.max(data, function (d) { return d.value; })]);
    x3.domain(x2.domain());
    y3.domain(y2.domain());

    const dataNest = d3.group(data, d => d.state)

    const seriesColors = ["#003F5C", "#38CFE8"]

    var color = d3.scaleOrdinal()
      .range(seriesColors);

    focus
      .selectAll("path")
      .data(dataNest)
      .enter().append("path")
      .attr('class', 'groups')
      .attr("d", d => {
        d.line = this;
        return line(d[1]);
      })
      .style("stroke", d => color(d[0]))
      .style("stroke-width", 1)
      .style('fill', 'none')
      .attr("clip-path", "url(#clip)")

    focus.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + timeHeight + ")")
      .call(xAxis);

    focus.append("g")
      .attr("class", "axis axis--y")
      .call(yAxis);

    context
      .selectAll("path")
      .data(dataNest)
      .enter().append("path")
      .attr('class', 'groups')
      .attr("d", d => {
        d.line = this;
        return line2(d[1]);
      })
      .style("stroke", d => color(d[0]))
      .style("stroke-width", 1)
      .style('fill', 'none')
      .attr("clip-path", "url(#clip)")

    context.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + timeHeight2 + ")")
      .call(xAxis2);

    context.append("g")
      .attr("class", "brush")
      .call(brush)
      .call(brush.move, x2.range());

  });

  let zoombrush;
  function brushed(event) {
    var s = event.selection || x2.range();
    if (s[1] === s[0]) s[1] += .5;

    if (event.mode === "move") {
      var extentlength = Math.round(s[1] - s[0])
      event.target.extent([Math.round(s[0] + 0.5) - 0.5,
      Math.round(s[0] + 0.5) - 0.5 + extentlength])
    } else {
      event.target.extent([Math.round(s[0] + 0.5) - 0.5,
      Math.round(s[1] + 0.5) - 0.5])
    }
    event.target(d3.select(this))

    x2.domain(s.map(x3.invert, x3));
    focus.selectAll(".groups")
      .attr("d", d => {
        d.line = this;
        return line(d[1]);
      })

    focus.select(".axis--x").call(xAxis);
    timeseries.select(".zoom").call(zoom.transform, d3.zoomIdentity
      .scale(timeWidth / (s[1] - s[0]))
      .translate(-s[0], 0));
    zoombrush = 0;
  }
  brush.on('end', (event, d) => {
  })

  function zoomed(event) {
    if (zoombrush) return; // ignore zoom-by-brush
    zoombrush = 1;

    var t = event.transform;
    x.domain(t.rescaleX(x2).domain());
    focus.select(".groups").attr("d", line);
    focus.select(".axis--x").call(xAxis);
    context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
    zoombrush = 0;
  }

内容的提问来源于stack exchange,提问作者sprucegoose

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 04:45:14