javascript - 如何为具有焦点和上下文的区域图实现 d3 的输入更新退出模式?

标签 javascript d3.js

我有一个带有焦点和上下文部分的区域图。上下文部分允许用户更改焦点部分中显示的时间段。
数据包括一段时间内的人口信息,包括性别。我希望能够在男性、女性和全部之间切换。我已经开始了这项工作,因为我正在获取我需要的数据并将其提供给图表。但是,当我单击另一个选项时,面积图不会退出,因此 Men/Women/All 的图表会堆叠在一起。
谁能帮我解开这个问题?
代码如下,这是一个 fiddle :https://jsfiddle.net/3wtrsegp/

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<style>
    .brush rect.selection {
        fill: none;
        opacity: 1;
    }
    rect.handle{
        fill: #666;
    }

</style>
</head>
<body>

<div id="buttons">
    <button id="All" class="button selected">All</button>
    <button id="Men" class="button">Men</button>
    <button id="Women" class="button">Women</button>
</div>
<div id='ptchart'></div>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var json = [
    {
      "date": "2020-01-02",
      "gender": "Men",
      "value": 4320
    },
    {
      "date": "2020-01-02",
      "gender": "Women",
      "value": 984
    },
    {
      "date": "2020-01-15",
      "gender": "Men",
      "value": 4624
    },
    {
      "date": "2020-01-15",
      "gender": "Women",
      "value": 1005
    },
    {
      "date": "2020-02-03",
      "gender": "Men",
      "value": 5488
    },
    {
      "date": "2020-02-03",
      "gender": "Women",
      "value": 978
    },
    {
      "date": "2020-02-18",
      "gender": "Men",
      "value": 5842
    },
    {
      "date": "2020-02-18",
      "gender": "Women",
      "value": 1006
    },
    {
      "date": "2020-03-02",
      "gender": "Men",
      "value": 6925
    },
    {
      "date": "2020-03-02",
      "gender": "Women",
      "value": 1004
    },
    {
      "date": "2020-03-16",
      "gender": "Men",
      "value": 6132
    },
    {
      "date": "2020-03-16",
      "gender": "Women",
      "value": 948
    },
    {
      "date": "2020-04-01",
      "gender": "Men",
      "value": 5852
    },
    {
      "date": "2020-04-01",
      "gender": "Women",
      "value": 685
    },
    {
      "date": "2020-04-15",
      "gender": "Men",
      "value": 8697
    },
    {
      "date": "2020-04-15",
      "gender": "Women",
      "value": 497
    },
    {
      "date": "2020-05-01",
      "gender": "Men",
      "value": 4547
    },
    {
      "date": "2020-05-01",
      "gender": "Women",
      "value": 468
    }
]

var margin = { top: 30, right: 210, bottom: 110, left: 50 },
    margin2 = { top: 380, right: 200, bottom: 70, left: 50 },
    width = 1000 - margin.left - margin.right,
    height = 440 - margin.top - margin.bottom,
    height2 = 490 - margin2.top - margin2.bottom;

var x = d3.scaleTime().range([0, width]),
    x2 = d3.scaleTime().range([0, width]),
    y = d3.scaleLinear().range([height, 0]),
    y2 = d3.scaleLinear().range([height2, 0]);

var xAxis = d3.axisBottom(x).tickFormat(d3.timeFormat("%b %Y")).ticks(5),
    xAxis2 = d3.axisBottom(x2).ticks(0).tickSize(0),
    yAxis = d3.axisLeft(y);

var brush = d3.brushX()
    .extent([[0, 0], [width, height2]])
    .on("brush", brushed);

var svg = d3.select("#ptchart").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom);

svg.append("defs").append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

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

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

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

    let data = json
    updateChart(data)

    d3.selectAll(".button").on("click", function () {
        var section = d3.select(this).attr("id");
        
        if (section == 'All') { data = json } else {
            data = json.filter(d => d.gender == section)
        }

        updateChart(data);
    })

    updateChart(json);


    function updateChart(selectedData) {

        const data = d3.nest()
                .key(d => d.date)
                .rollup(v => d3.sum(v, d => d.value))
                .entries(selectedData);

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

            x.domain(d3.extent(data, d => d.date));
            y.domain([0, d3.max(data, d => d.value) + 500]);
            x2.domain(x.domain());
            y2.domain(y.domain());

        var area = d3.area()
            .x(function (d) { return x(d.date); })
            .y0(height)
            .y1(function (d) { return y(d.value); })

        var line = d3.line()
            .x(d => x(d.date))
            .y(d => y(d.value))

        
        // This is what the area looked like before I started to try and get the enter/update/exit pattern working:

        // var area = focus.append("g");
        // area.attr("clip-path", "url(#clip)");
        // area.selectAll('path')
        //     .data([data])
        //     .enter().append("path")
        //     .attr("class", "area")
        //     .attr("d", area)
        //     .attr('fill', '#eeeeee')

        var area = focus.append("g").selectAll('path')
        .data([data])

        area.enter().append("path")
            .attr("class", "area")
            .attr('fill', '#eeeeee')
        
        area.transition()
            .attr("d", area)
            .attr("clip-path", "url(#clip)");
        
        area.exit().remove()


        var arealine = focus.append("g");
        arealine.attr("clip-path", "url(#clip)");
        arealine.selectAll('path')
            .data([data])
            .enter().append("path")
            .attr("class", "line")
            .attr("d", line)
            .attr('fill', 'none')
            .attr('stroke', '#333')

        var capacityline = focus.append('g')
        capacityline.selectAll('line')
            .data(data)
            .enter().append('line')
            .attr('class', 'capacityline')
            .attr('x1', 0)
            .attr('y1', d => y(12500))
            .attr('x2', width)
            .attr('y2', d => y(12500))
            .attr('stroke-width', '1px')
            .attr('stroke', '#4D4E56')
            .attr('stroke-dasharray', 4)

        var stooltip = d3.select("body").append("div")
            .attr("class", "tooltip");

        svg.selectAll(".axis").remove();

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

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

            var area2 = d3.area()
            .x(d => x2(d.date))
            .y0(height2)
            .y1(d => y2(d.value))

        var line2 = d3.line()
            .x(d => x2(d.date))
            .y(d => y2(d.value))

        var area = context.append("g");
        area.attr("clip-path", "url(#clip)");
        area.selectAll('path')
            .data([data])
            .enter().append("path")
            .attr("class", "area")
            .attr("d", area2)
            .attr('fill', '#eeeeee')

        var arealine = context.append("g");
        arealine.attr("clip-path", "url(#clip)");
        arealine.selectAll('path')
            .data([data])
            .enter().append("path")
            .attr("class", "line")
            .attr("d", line2)
            .attr('fill', 'none')
            .attr('stroke', '#333')

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

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


}

function brushed() {
    var area = d3.area()
        .x(function (d) { return x(d.date); })
        .y0(height)
        .y1(function (d) { return y(d.value); })

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

    var selection = d3.event.selection;
    x.domain(selection.map(x2.invert, x2));
    focus.selectAll(".area")
        .attr("d", area)
    focus.selectAll(".line")
        .attr("d", line)

    focus.select(".axis--x").call(xAxis);
}

</script>
</body>
</html>

最佳答案

您的直接问题很容易错过:
您创建新父级的每次更新g元素并在这些新的 g 上执行进入/更新/退出循环要素:

 var area = focus.append("g").selectAll('path')
新追加的g上面的元素将是空的,因为它还没有子元素;因此,输入选择将为数据数组中的每个项目包含一个元素。因为您永远不会删除或重新选择旧的 g元素,它们只是坐在那里,不受新选择和任何后续进入/更新/退出周期的影响。结果是每次我们运行更新函数时,我们只是对新数据进行分层。
家长 g元素应该附加在更新函数之外:它们不需要更新:有一个的地方仍然会有一个,并且父级的属性保持不变。父 g 元素独立于数据,它们只包含表示数据的子元素。我们应该在更新函数之外设置一次父 g 元素,以及所有其他需要设置一次的东西。然后我们可以使用这些父项来设置我们的进入/更新/退出周期,因为我们将在每次更新时选择相同父项中的元素(而不是每次更新都使用新的空父项)。
下面我取了你图表的一个子集(为了便于演示)并附加了你所有的父 g只需要附加一次并在调用更新函数之前附加它们的元素。我还使用了您的区域和线生成器,并将它们从更新功能中删除:它们在这里也不会改变。这也适用于工具提示 div 和轴本身:父 g 在更新函数之外附加一次,但更新函数在这些 g 元素上调用轴生成器。
顺便说一句,您的代码中有很多重叠的变量名称,我可能会更改名称以避免这种情况。我在输入和更新行时使用了合并方法。

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<style>
    .brush rect.selection {
        fill: none;
        opacity: 1;
    }
    rect.handle{
        fill: #666;
    }

</style>
</head>
<body>

<div id="buttons">
    <button id="All" class="button selected">All</button>
    <button id="Men" class="button">Men</button>
    <button id="Women" class="button">Women</button>
</div>
<div id='ptchart'></div>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var json = [
    {
      "date": "2020-01-02",
      "gender": "Men",
      "value": 4320
    },
    {
      "date": "2020-01-02",
      "gender": "Women",
      "value": 984
    },
    {
      "date": "2020-01-15",
      "gender": "Men",
      "value": 4624
    },
    {
      "date": "2020-01-15",
      "gender": "Women",
      "value": 1005
    },
    {
      "date": "2020-02-03",
      "gender": "Men",
      "value": 5488
    },
    {
      "date": "2020-02-03",
      "gender": "Women",
      "value": 978
    },
    {
      "date": "2020-02-18",
      "gender": "Men",
      "value": 5842
    },
    {
      "date": "2020-02-18",
      "gender": "Women",
      "value": 1006
    },
    {
      "date": "2020-03-02",
      "gender": "Men",
      "value": 6925
    },
    {
      "date": "2020-03-02",
      "gender": "Women",
      "value": 1004
    },
    {
      "date": "2020-03-16",
      "gender": "Men",
      "value": 6132
    },
    {
      "date": "2020-03-16",
      "gender": "Women",
      "value": 948
    },
    {
      "date": "2020-04-01",
      "gender": "Men",
      "value": 5852
    },
    {
      "date": "2020-04-01",
      "gender": "Women",
      "value": 685
    },
    {
      "date": "2020-04-15",
      "gender": "Men",
      "value": 8697
    },
    {
      "date": "2020-04-15",
      "gender": "Women",
      "value": 497
    },
    {
      "date": "2020-05-01",
      "gender": "Men",
      "value": 4547
    },
    {
      "date": "2020-05-01",
      "gender": "Women",
      "value": 468
    }
]

var margin = { top: 30, right: 210, bottom: 110, left: 50 },
    width = 1000 - margin.left - margin.right,
    height = 440 - margin.top - margin.bottom;

var x = d3.scaleTime().range([0, width]),
    y = d3.scaleLinear().range([height, 0]);

var xAxis = d3.axisBottom(x).tickFormat(d3.timeFormat("%b %Y")).ticks(5),
    yAxis = d3.axisLeft(y);

var svg = d3.select("#ptchart").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom);

svg.append("defs").append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

// Add all the `g` parent elements now:
var focus = svg.append("g")
    .attr("class", "focus")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
var areaG = focus.append("g");
var lineG = focus.append("g")
                 .attr("clip-path", "url(#clip)");

var stooltip = d3.select("body").append("div")
    .attr("class", "tooltip");
            
var xAxisG = focus.append("g")
            .attr("class", "axis axis--x")
            .attr("transform", "translate(0," + height + ")")           

var yAxisG = focus.append("g")
             .attr("class", "axis axis--y")         
            
    
// area and line generators:
var area = d3.area()
    .x(function (d) { return x(d.date); })
    .y0(height)
    .y1(function (d) { return y(d.value); })

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


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

let data = json
updateChart(data)

    d3.selectAll(".button").on("click", function () {
        var section = d3.select(this).attr("id");
        
        if (section == 'All') { data = json } else {
            data = json.filter(d => d.gender == section)
        }

        updateChart(data);
    })

    updateChart(json);


function updateChart(selectedData) {
    
    // Manage data
    const data = d3.nest()
        .key(d => d.date)
        .rollup(v => d3.sum(v, d => d.value))
        .entries(selectedData);

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

    // Calculate new scale values:
    x.domain(d3.extent(data, d => d.date));
    y.domain([0, d3.max(data, d => d.value) + 500]);

    // Main chart:
    // Do the areas:
    var areaPaths = areaG.selectAll('path')
        .data([data])

        areaPaths.enter().append("path")
            .attr("class", "area")
            .attr('fill', '#eeeeee')
        
        areaPaths.transition()
            .attr("d", area)
            .attr("clip-path", "url(#clip)");
        
        areaPaths.exit().remove()

    // Do the lines:
    var linePaths = lineG.selectAll('path')
            .data([data]);
         
            // Update/exit
            linePaths.enter().append("path")
            .merge(linePaths)       
            .attr("class", "line")
            .attr("d", line)
            .attr('fill', 'none')
            .attr('stroke', '#333')
            
   linePaths.exit().remove();

    // Update the axes:
    xAxisG.call(xAxis);
    yAxisG.call(yAxis);

}

</script>
</body>
</html>

但一个更根本的问题是,您所做的大部分工作都不需要进入/更新/退出周期。这对于容量线最为明显:它独立于数据(其属性是硬编码的)。而且,行数固定为一 - 无需退出行或区域,一旦附加,无需进入行。
进入/更新/退出循环的目的是确保选择中的元素数量与数据数组中的项目数量相匹配。如果我们总是有相同数量的元素,那么进入/更新/退出就大材小用了。我们可以简化代码,以便您的更新函数仅更新现有元素。而不是只附加一个父 g我们最初也可以附加一个子行。这样我们的更新函数更简洁,代码整体更简单(仅追加,此处没有显式输入/更新/退出),同时仍保留 D3 的功能和精神:
我修改了容量线的属性,使其出现在可视范围内

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<style>
    .brush rect.selection {
        fill: none;
        opacity: 1;
    }
    rect.handle{
        fill: #666;
    }

</style>
</head>
<body>

<div id="buttons">
    <button id="All" class="button selected">All</button>
    <button id="Men" class="button">Men</button>
    <button id="Women" class="button">Women</button>
</div>
<div id='ptchart'></div>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var json = [
    {
      "date": "2020-01-02",
      "gender": "Men",
      "value": 4320
    },
    {
      "date": "2020-01-02",
      "gender": "Women",
      "value": 984
    },
    {
      "date": "2020-01-15",
      "gender": "Men",
      "value": 4624
    },
    {
      "date": "2020-01-15",
      "gender": "Women",
      "value": 1005
    },
    {
      "date": "2020-02-03",
      "gender": "Men",
      "value": 5488
    },
    {
      "date": "2020-02-03",
      "gender": "Women",
      "value": 978
    },
    {
      "date": "2020-02-18",
      "gender": "Men",
      "value": 5842
    },
    {
      "date": "2020-02-18",
      "gender": "Women",
      "value": 1006
    },
    {
      "date": "2020-03-02",
      "gender": "Men",
      "value": 6925
    },
    {
      "date": "2020-03-02",
      "gender": "Women",
      "value": 1004
    },
    {
      "date": "2020-03-16",
      "gender": "Men",
      "value": 6132
    },
    {
      "date": "2020-03-16",
      "gender": "Women",
      "value": 948
    },
    {
      "date": "2020-04-01",
      "gender": "Men",
      "value": 5852
    },
    {
      "date": "2020-04-01",
      "gender": "Women",
      "value": 685
    },
    {
      "date": "2020-04-15",
      "gender": "Men",
      "value": 8697
    },
    {
      "date": "2020-04-15",
      "gender": "Women",
      "value": 497
    },
    {
      "date": "2020-05-01",
      "gender": "Men",
      "value": 4547
    },
    {
      "date": "2020-05-01",
      "gender": "Women",
      "value": 468
    }
]

var margin = { top: 30, right: 210, bottom: 110, left: 50 },
    margin2 = { top: 380, right: 200, bottom: 70, left: 50 },
    width = 1000 - margin.left - margin.right,
    height = 440 - margin.top - margin.bottom,
    height2 = 490 - margin2.top - margin2.bottom;

var x = d3.scaleTime().range([0, width]),
    x2 = d3.scaleTime().range([0, width]),
    y = d3.scaleLinear().range([height, 0]),
    y2 = d3.scaleLinear().range([height2, 0]);

var xAxis = d3.axisBottom(x).tickFormat(d3.timeFormat("%b %Y")).ticks(5),
    xAxis2 = d3.axisBottom(x2).ticks(12).tickSize(0),
    yAxis = d3.axisLeft(y);

var brush = d3.brushX()
    .extent([[0, 0], [width, height2]])
    .on("brush", brushed);

var svg = d3.select("#ptchart").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom);

svg.append("defs").append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

// Set up chart elements:
var focus = svg.append("g")
    .attr("class", "focus")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
// Set up main chart elements:  
var mainArea =  focus.append("g")
    .attr("clip-path", "url(#clip)")
    .append("path")
    .attr("class", "area")
    .attr('fill', '#eeeeee')
    
var mainLine = focus.append("g")  
    .attr("clip-path", "url(#clip)")
    .append("path")
    .attr("class", "line")
    .attr('fill', 'none')
    .attr('stroke', '#333')    
    
var capacityLine = focus.append("g")
    .append("line")
    .attr('class', 'capacityline')
    .attr('stroke-width', '1px')
    .attr('stroke', '#4D4E56')
    .attr('stroke-dasharray', 4)   

var xAxisG = focus.append("g") 
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")")
    
var yAxisG = focus.append("g")
    .attr("class", "axis axis--y")
        
// Set up brushable chart elements:
var context = svg.append("g")
    .attr("class", "context")
    .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
    
var brushableArea = context.append("g")
    .attr("clip-path", "url(#clip)")
    .append("path")
    .attr("class", "area")
    .attr('fill', '#eeeeee');
    
var brushableLine = context.append("g")
    .attr("clip-path", "url(#clip)")
    .append("path")
    .attr("class", "line")
    .attr('fill', 'none')
    .attr('stroke', '#333')  
    
var brushAxisG = context.append("g")
   .attr("class", "axis axis--x")
   .attr("transform", "translate(0," + height2 + ")")

var brushG = context.append("g")
   .attr("class", "brush")
   
// Set up tooltip div:
var stooltip = d3.select("body").append("div")
    .attr("class", "tooltip");

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

    
// Area and line generators:
var area = d3.area()
  .x(function (d) { return x(d.date); })
  .y0(height)
  .y1(function (d) { return y(d.value); })

var line = d3.line()
  .x(d => x(d.date))
  .y(d => y(d.value))   
  
var area2 = d3.area()
  .x(d => x2(d.date))
  .y0(height2)
  .y1(d => y2(d.value))

var line2 = d3.line()
  .x(d => x2(d.date))
  .y(d => y2(d.value))

updateChart(json)

d3.selectAll(".button").on("click", function () {
    var section = d3.select(this).attr("id");
        
    if (section == 'All') { data = json } else {
       data = json.filter(d => d.gender == section)
     }

    updateChart(data);
})
    
function updateChart(selectedData) {
    const data = d3.nest()
     .key(d => d.date)
     .rollup(v => d3.sum(v, d => d.value))
     .entries(selectedData);

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

    // Update scales:
    x.domain(d3.extent(data, d => d.date));
    y.domain([0, d3.max(data, d => d.value) + 500]);
    x2.domain(x.domain());
    y2.domain(y.domain());
    
    // Update axes:
    xAxisG.call(xAxis);
    yAxisG.call(yAxis);
    
    // Update area and lines of main chart:
    mainArea.datum(data)
      .attr("d", area);
        
    mainLine.datum(data)
      .attr("d",line);

    capacityLine.transition()
      .attr('x1', 0)
      .attr('y1', d => y(1000))
      .attr('x2', width)
      .attr('y2', d => y(1000))

    // Update brushable area:
    brushableArea.datum(data)
      .attr("d", area2);
    
    brushableLine.datum(data)
      .attr("d", line2);

  // Update brush axis:
  brushAxisG.call(xAxis2);

  // Update brush:
  brushG.call(brush)
     .call(brush.move, x.range());
}

function brushed() {

    var selection = d3.event.selection;
    x.domain(selection.map(x2.invert, x2));
    focus.selectAll(".area")
        .attr("d", area)
    focus.selectAll(".line")
        .attr("d", line)

    focus.select(".axis--x").call(xAxis);
}

</script>
</body>
</html>

以上使用.datum()它将提供的值绑定(bind)到所选元素:它相当于您使用 .data([data])在您的原始代码中输入/更新/退出选择。

关于javascript - 如何为具有焦点和上下文的区域图实现 d3 的输入更新退出模式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67426583/

相关文章:

javascript - d3 : Using DataMaps to create a map of the USA

javascript - 如何为图形的整行设置一个标签

javascript - d3.js - 在堆叠图表上移动 y 轴

Javascript 打开新选项卡 cognos authoring 透视模式

javascript - 试图使 tinyMCE 3.x 的宽度 float

javascript - Html5 Canvas : alternative for drawImage()

javascript - d3.js 条件词颜色填充wordcloud

d3.js - 在 webpack 中使用 d3.js 作为外部

javascript - 有没有办法在 Svelte 中将 props 声明为可选

javascript - Vanilla JavaScript 中的准确计时器