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

spr*_*ose 5 javascript d3.js

我有一个带有焦点和上下文部分的面积图。上下文部分允许用户更改焦点部分中显示的时间段。

这些数据包括随时间变化的人口信息,并且包括性别。我希望能够在“男性”、“女性”和“所有人”之间切换。我开始了这项工作,因为我正在获取所需的数据并将其输入图表。但是,当我单击另一个选项时,面积图不会退出,因此“男性/女性/所有人”的图表彼此堆叠在一起。

谁能帮我解决这个问题吗?

代码如下,这是一个小提琴: 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>
Run Code Online (Sandbox Code Playgroud)

And*_*eid 3

您眼前的问题很容易被忽视:

每次更新都会创建新的父g元素并对这些新g元素执行进入/更新/退出循环:

 var area = focus.append("g").selectAll('path')
Run Code Online (Sandbox Code Playgroud)

上面新添加的g元素将为空,因为它还没有子元素;因此,输入选择将包含数据数组中每一项的一个元素。由于您从不删除或重新选择旧g元素,因此它们只是坐在那里,不受新选择和任何后续输入/更新/退出周期的影响。结果是我们每次运行更新函数时都会对新数据进行分层。

g元素应该附加在更新函数之外:它们不需要更新:有一个的地方仍然会有一个,并且父元素的属性保持不变。父 g 元素独立于数据,它们仅包含表示数据的子元素。我们应该在更新函数之外设置一次父 g 元素,以及需要设置一次的所有其他内容。然后我们可以使用这些父项来设置我们的输入/更新/退出周期,因为我们将在每次更新时选择相同父项中的元素(而不是每次更新时使用新的空父项)。

下面我取了您的图表的一个子集(为了便于演示)并附加了您的所有父级g需要附加一次的父元素,并在调用更新函数之前附加它们。我还获取了您的区域和线生成器,并将它们从更新功能中删除:它们在这里也没有改变。这也适用于工具提示 div 和轴本身:父级 g 在更新函数外部附加一次,但更新函数调用这些 g 元素上的轴生成器。

顺便说一句,您的代码中有很多重叠的变量名称,我可能已经更改了名称以避免这种情况。我在输入和更新行时使用了合并方法。

 var area = focus.append("g").selectAll('path')
Run Code Online (Sandbox Code Playgroud)

但一个更根本的问题是,您所做的大部分事情都不需要输入/更新/退出周期。这对于容量线来说最为明显:它独立于数据(其属性是硬编码的)。而且,行数固定为一 - 无需退出行或区域,并且一旦附加,则无需输入行。

输入/更新/退出循环的目的是确保选择中的元素数量与数据数组中的项目数量相匹配。如果我们总是有相同数量的元素,那么输入/更新/退出就太过分了。我们可以简化代码,以便您的更新函数仅更新现有元素。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 },
    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>
Run Code Online (Sandbox Code Playgroud)

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