如何在D3.js多折线图中默认隐藏两条折线并支持单独显示
修复D3.js折线图默认隐藏分组的单独控制问题
你遇到的问题根源是:初始绘制时未给默认隐藏的折线设置display: none,且图例矩形的初始状态未匹配隐藏逻辑。当点击其中一个分组时,所有折线的路径被重新生成,未被操作的隐藏分组会意外显示。以下是具体修复方案:
关键修改点
- 初始绘制时设置折线的display状态:直接生成所有折线路径,用
display属性控制初始可见性,确保默认隐藏的折线一开始就不可见。 - 图例矩形初始状态匹配隐藏逻辑:默认隐藏的分组对应图例矩形填充设为透明,与交互后的状态保持一致。
- 保持分组状态独立控制:每个分组的显示/隐藏仅由自身的
clicked属性决定,互不干扰。
修改后的完整代码
JavaScript
let timeW = 960, timeH = 450 let timeMargin = { top: 20, right: 300, bottom: 80, left: 60 }, timeWidth = timeW - timeMargin.left - timeMargin.right, timeHeight = timeH - timeMargin.top - timeMargin.bottom; var x2 = d3.scaleTime().range([0, timeWidth]), y2 = d3.scaleLinear().range([timeHeight, 0]); var xAxis = d3.axisBottom(x2), yAxis = d3.axisLeft(y2); var line = d3.line() .x(function(d) { return x2(d.date); }) .y(function(d) { return y2(d.value); }); const seriesColors = ['#ff3300', 'royalblue', 'green', 'turquoise', 'navy'] var color = d3.scaleOrdinal() .range(seriesColors); const parseDate = d3.timeParse("%Y%m%d"); d3.csv("https://raw.githubusercontent.com/sprucegoose1/sample-data/main/age.csv").then(function(data) { var long_data = []; data.forEach(function(row) { row.date = parseDate(row.Date) let tableKeys = data.columns.slice(1); Object.keys(row).forEach(function(colname) { if (colname !== "date" && colname !== "Date") { long_data.push({ "date": row["date"], "value": +row[colname], "bucket": colname }); } }); }); data.sort((a, b) => a.date - b.date) let dataNest = d3.group(long_data, d => d.bucket) let tableKeys = data.columns.slice(1); let grouped_data_array = Array.from(dataNest) grouped_data_array.forEach(d => { d[0] == "<18" || d[0] == "56+" ? d.clicked = true : d.clicked = false }) drawChart(long_data, grouped_data_array, tableKeys) }) function drawChart(data, dataNest, tableKeys) { d3.select("#timeseries").remove() let timeseries = d3.select("#chart").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 focus = timeseries.append("g") .attr("class", "focus") .attr("transform", "translate(" + timeMargin.left + "," + timeMargin.top + ")"); x2.domain(d3.extent(data, function(d) { return d.date; })); y2.domain([0, d3.max(data, function(d) { return d.value; })]); focus .selectAll("path") .data(dataNest) .enter().append("path") .attr('class', d => 'groups a' + d[0].replaceAll("+", "").replaceAll("<", "").replaceAll(" ", "_")) .attr("d", d => line(d[1])) .style("stroke", d => color(d[0])) .style("stroke-width", 1) .style('fill', 'none') .style("display", d => d.clicked ? "none" : "block") .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); var gXAxis = focus.append("g") .attr("class", "axis axis--x") .attr("transform", "translate(0," + timeHeight + ")") .call(xAxis); var gYAxis = focus.append("g") .attr("class", "axis axis--y") .call(yAxis); let clickedItems = ["<18", "56+"] var legend = timeseries.append('g') .attr('class', 'legend') .attr("transform", "translate(" + (timeW - timeMargin.right + 10) + "," + timeMargin.top + ")") var legendGroups = legend .selectAll(".legendGroup") .data(dataNest, d => d[0]) var enterGroups = legendGroups .enter() .append("g") .attr("class", d => "legendGroup " + d[0].replaceAll(" ", "_").replace("<", "")) .on("click", function(event, d) { d.clicked = !d.clicked; // 切换图例矩形填充 d3.select('rect.a' + d[0].replaceAll(" ", "_").replace("+", "").replace("<", "")).attr("fill", d.clicked ? "transparent" : color(d[0])); // 切换折线显示 d3.select("path.a" + d[0].replaceAll(" ", "_").replace("+", "").replace("<", "")).style("display", d.clicked ? "none" : "block"); // 切换表格行显示(如果有) d3.select(".row.a" + d[0].replaceAll(" ", "_").replace("+", "").replace("<", "")).style("display", d.clicked ? "none" : "block"); let i = d3.selectAll(".legend-rect").nodes().indexOf(this); d3.selectAll(".group" + i).style("display", d.clicked ? "none" : "block"); // 更新clickedItems数组 d.clicked ? clickedItems.push(d[0]) : clickedItems.splice(clickedItems.indexOf(d[0]), 1); // 重新计算坐标轴范围 const visibleData = data.filter(d => clickedItems.indexOf(d.bucket) === -1); x2.domain(d3.extent(visibleData, d => d.date)); y2.domain([0, d3.max(visibleData, d => d.value)]); gYAxis.call(yAxis) gXAxis.call(xAxis) // 重新绘制所有折线 focus.selectAll("path.groups") .transition() .attr("d", d => line(d[1])) }) legendGroups .exit() .remove(); enterGroups .append("rect") .attr("width", 10) .attr("height", 10) .attr("fill", d => d.clicked ? "transparent" : color(d[0])) .attr("stroke", d => color(d[0])) .attr("stroke-width", 2) .attr('stroke-opacity', 1) .attr("x", 30) .attr("y", (d, i) => i * 20) .attr("class", d => "a" + d[0].replace("+", "").replace("<", "") + ' legend-rect') enterGroups .append("text") .attr('class', 'legend-text') .text(d => d[0]) .attr("x", 45) .attr("y", (d, i) => 10 + i * 20) enterGroups .append("foreignObject") .attr("x", 15) .attr("y", (d, i) => i * 19) .attr("width", 12) .attr("height", 14) .attr("id", (d, i) => 'a' + i) .append("xhtml:tree") .html(d => d[0] == "56+" || d[0] == "<18" ? "<input type='checkbox' class='check'>" : "<input type='checkbox' checked class='check'>") .attr('class', 'checkcontainer') }
CSS
#chart { height: 450px; width: 760px; } .check { width: 11px; height: 12px; filter: grayscale(1); margin: 0; margin-top: -1px !important; }
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> <div id="chart"></div>
效果说明
- 初始加载时,
<18和56+分组的折线默认隐藏,图例矩形为透明,复选框未勾选。 - 点击任意一个隐藏分组的图例,仅该分组的折线会显示/隐藏,互不影响。
- 坐标轴范围会自动适配当前可见的数据,保证图表显示始终合理。
内容的提问来源于stack exchange,提问作者sprucegoose
相关产品推荐
相关产品推荐

