d3.js:enter-update-exit模式-旧点未删除

时间:2018-08-01 00:50:56

标签: javascript d3.js

我正尝试在下面的这张图上执行enter-update-exit模式(该模式是在SO的某种非常有用的ppl的巨大帮助下构建的,但不幸的是,我现在再次陷入困境。我无法使该模式起作用,但是我确定我捡到了正确的对象(在下面的代码中命名为heatDotsGroup)。

但是我可以检查Chrome的开发人员工具,该对象包含节点(椭圆),但是该模式不起作用,因此显然我做错了。

有什么想法吗?非常感谢!

function heatmap(dataset) {
    
    var svg = d3.select("#chart")
        .select("svg")
    
    var xLabels = [],
        yLabels = [];
    for (i = 0; i < dataset.length; i++) {
        if (i==0){
            xLabels.push(dataset[i].xLabel);
            var j = 0;
            while (dataset[j+1].xLabel == dataset[j].xLabel){
                yLabels.push(dataset[j].yLabel);
                j++;
            }
            yLabels.push(dataset[j].yLabel);
        } else {
            if (dataset[i-1].xLabel == dataset[i].xLabel){
                //do nothing
            } else {
                xLabels.push(dataset[i].xLabel);                    
            }
        }
    };

    var margin = {top: 0, right: 25,
                  bottom: 60, left: 75};  

    var width = +svg.attr("width") - margin.left - margin.right,
        height = +svg.attr("height") - margin.top - margin.bottom;

    var dotSpacing = 0,
        dotWidth = width/(2*(xLabels.length+1)),
        dotHeight = height/(2*yLabels.length);

    var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
        days = daysRange[1] - daysRange[0];
    
    var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
        hours = hoursRange[1] - hoursRange[0];    
    
    var tRange = d3.extent(dataset, function (d) {return d.val}),
        tMin = tRange[0],
        tMax = tRange[1];

    var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
    
    // the scale
    var scale = {
        x: d3.scaleLinear()
           .range([-1, width]),
        y: d3.scaleLinear()
           .range([height, 0]),
    };
    
    var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
        yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
    
    var axis = {
        x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
        y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
    };


    function updateScales(data){
        scale.x.domain([0, d3.max(data, d => d.xKey)]),
        scale.y.domain([ 0, d3.max(data, d => d.yKey)])
    }

    var colorScale = d3.scaleQuantile()
        .domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
        .range(colors);

    var zoom = d3.zoom()
        .scaleExtent([1, dotHeight])
        .on("zoom", zoomed);

    var tooltip = d3.select("body").append("div")
        .attr("id", "tooltip")
        .style("opacity", 0);

    // SVG canvas
    svg = d3.select("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .call(zoom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Clip path
    svg.append("clipPath")
        .attr("id", "clip")
        .append("rect")
        .attr("width", width)
        .attr("height", height+dotHeight);


    // Heatmap dots
    var heatDotsGroup = svg.append("g")
        .attr("clip-path", "url(#clip)")
        .append("g");
        

    //Create X axis
    var renderXAxis = svg.append("g")
        .attr("class", "x axis")
        //.attr("transform", "translate(0," + scale.y(-0.5) + ")")
        //.call(axis.x)

    //Create Y axis
    var renderYAxis = svg.append("g")
        .attr("class", "y axis")
        .call(axis.y);


    function zoomed() {
        d3.event.transform.y = 0;
        d3.event.transform.x = Math.min(d3.event.transform.x, 5);
        d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
        // console.log(d3.event.transform)

        // update: rescale x axis
        renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));

        // Make sure that only the x axis is zoomed
        heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
    }
    
    svg.call(renderPlot, dataset)
    
    function renderPlot(selection, dataset){
        
        //Do the axes
        updateScales(dataset)
        selection.select('.y.axis').call(axis.y)
        selection.select('.x.axis')
                .attr("transform", "translate(0," + scale.y(-0.5) + ")")
                .call(axis.x)
           
        
        // Do the chart
        const update = heatDotsGroup.selectAll("ellipse")
        .data(dataset);
        
        update
        .enter()
        .append("ellipse")
        .attr("cx", function (d) {return scale.x(d.xKey) - xBand.bandwidth();})
        .attr("cy", function (d) {return scale.y(d.yKey) + yBand.bandwidth();})
        .attr("rx", dotWidth)
        .attr("ry", dotHeight)
        .attr("fill", function (d) {
            return colorScale(d.val);}
            )
        .merge(update).transition().duration(800);   
        
        update.exit().remove();
        
    }


};
#clickMe{
    height:50px;
    width:150px;
    background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    
    <title>Heatmap Chart</title>

    <!-- Reference style.css -->
    <!--    <link rel="stylesheet" type="text/css" href="style.css">-->

    <!-- Reference minified version of D3 -->
    <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
    <script src='heatmap_v4.js' type='text/javascript'></script>
</head>

<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />

    <div id='chart'>
    <svg width="700" height="500">
      <g class="focus">
        <g class="xaxis"></g>
        <g class="yaxis"></g>
      </g>
    </svg>
   </div>
    
   
    <script>
        function run() {
            var dataset = [];
                for (let i = 1; i < 360; i++) { //360
                    for (j = 1; j < 7; j++) {  //75
                        dataset.push({
                            xKey: i,
                            xLabel: "xMark " + i,
                            yKey: j,
                            yLabel: "yMark " + j,
                            val: Math.random() * 25,
                        })
                        }
                    };

                    heatmap(dataset)
        }

        $(document).ready(function() {});
    </script>
</body>

</html>

2 个答案:

答案 0 :(得分:1)

问题在于,每次运行输入/退出/更新周期时,您不会使用相同的选择。按下按钮后,您:

  1. 生成新数据
  2. 运行热图功能
  3. heatmap函数选择svg,并附加一个名为heatDotsGroup的新鲜g
  4. 调用update函数并将新创建的g作为选择传递
  5. 由于新的g为空,因此输入循环会附加所有内容。

因此,退出和udpate周期均为空。试试:

console.log(update.size(),update.exit().size()) // *Without any merge* 

您应该看到每个更新都为空。这是因为每次都输入所有元素,因此每次更新都会增加省略号的数量。

我已经从heatmap函数中提取了一堆变量声明和追加语句,这些东西只需要运行一次(我可以走得更远,但我只做了最少的事情)。我还合并了您的更新并在设置属性之前输入选择(因为我们要在更新时设置新属性)。下面的代码片段将演示此更改。

在代码段中,按下按钮会发生以下情况:

  1. 生成新数据
  2. 运行热图功能
  3. 热图功能会选择现有选项,并且不会添加任何新内容
  4. 将调用update函数,并传递包含任何现有节点的先前update / enter / exit周期所使用的选择。
  5. 更新功能根据现有节点的需要输入/退出/更新元素。

这是基于上述内容的有效版本:

// Things to set/append once:
var svg = d3.select("#chart")
  .select("svg")

var margin = {top: 0, right: 25,bottom: 60, left: 75};  
var width = +svg.attr("width") - margin.left - margin.right,
  height = +svg.attr("height") - margin.top - margin.bottom;
  
svg = svg.attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var clip = svg.append("clipPath")
        .attr("id", "clip")
        .append("rect")


var heatDotsGroup = svg.append("g")
  .attr("clip-path", "url(#clip)")
  .append("g");

var xAxis = svg.append("g").attr("class", "x axis");
var yAxis = svg.append("g").attr("class", "y axis")
     

        
function heatmap(dataset) {
    
    
    var xLabels = [],
        yLabels = [];
    for (i = 0; i < dataset.length; i++) {
        if (i==0){
            xLabels.push(dataset[i].xLabel);
            var j = 0;
            while (dataset[j+1].xLabel == dataset[j].xLabel){
                yLabels.push(dataset[j].yLabel);
                j++;
            }
            yLabels.push(dataset[j].yLabel);
        } else {
            if (dataset[i-1].xLabel == dataset[i].xLabel){
                //do nothing
            } else {
                xLabels.push(dataset[i].xLabel);                    
            }
        }
    };



    var dotSpacing = 0,
        dotWidth = width/(2*(xLabels.length+1)),
        dotHeight = height/(2*yLabels.length);

    var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
        days = daysRange[1] - daysRange[0];
    
    var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
        hours = hoursRange[1] - hoursRange[0];    
    
    var tRange = d3.extent(dataset, function (d) {return d.val}),
        tMin = tRange[0],
        tMax = tRange[1];

    var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
    
    // the scale
    var scale = {
        x: d3.scaleLinear()
           .range([-1, width]),
        y: d3.scaleLinear()
           .range([height, 0]),
    };
    
    var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
        yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
    
    var axis = {
        x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
        y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
    };


    function updateScales(data){
        scale.x.domain([0, d3.max(data, d => d.xKey)]),
        scale.y.domain([ 0, d3.max(data, d => d.yKey)])
    }

    var colorScale = d3.scaleQuantile()
        .domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
        .range(colors);

    var zoom = d3.zoom()
        .scaleExtent([1, dotHeight])
        .on("zoom", zoomed);

    var tooltip = d3.select("body").append("div")
        .attr("id", "tooltip")
        .style("opacity", 0);

    // SVG canvas
    svg.call(zoom);


    // Clip path
  clip.attr("width", width)
        .attr("height", height+dotHeight);



        

    //Create X axis
    var renderXAxis = xAxis 
        //.attr("transform", "translate(0," + scale.y(-0.5) + ")")
        //.call(axis.x)

    //Create Y axis
    var renderYAxis = yAxis.call(axis.y);


    function zoomed() {
        d3.event.transform.y = 0;
        d3.event.transform.x = Math.min(d3.event.transform.x, 5);
        d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
        // console.log(d3.event.transform)

        // update: rescale x axis
        renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));

        // Make sure that only the x axis is zoomed
        heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
    }
    
    svg.call(renderPlot, dataset)
    
    function renderPlot(selection, dataset){
        
        //Do the axes
        updateScales(dataset)
        selection.select('.y.axis').call(axis.y)
        selection.select('.x.axis')
                .attr("transform", "translate(0," + scale.y(-0.5) + ")")
                .call(axis.x)
           
        
        // Do the chart
        const update = heatDotsGroup.selectAll("ellipse")
        .data(dataset);
        
        update
        .enter()
        .append("ellipse")
        .merge(update)
        .attr("cx", function (d) {return scale.x(d.xKey) - xBand.bandwidth();})
        .attr("cy", function (d) {return scale.y(d.yKey) + yBand.bandwidth();})
        .attr("rx", dotWidth)
        .attr("ry", dotHeight)
        .attr("fill", function (d) {
            return colorScale(d.val);}
            )
         
        
        update.exit().remove();
        
    }


};
#clickMe{
    height:50px;
    width:150px;
    background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    
    <title>Heatmap Chart</title>

    <!-- Reference style.css -->
    <!--    <link rel="stylesheet" type="text/css" href="style.css">-->

    <!-- Reference minified version of D3 -->
    <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
    <script src='heatmap_v4.js' type='text/javascript'></script>
</head>

<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />

    <div id='chart'>
    <svg width="700" height="500">
      <g class="focus">
        <g class="xaxis"></g>
        <g class="yaxis"></g>
      </g>
    </svg>
   </div>
    
   
    <script>
        function run() {
            var dataset = [];
                for (let i = 1; i < 360; i++) { //360
                    for (j = 1; j < 7; j++) {  //75
                        dataset.push({
                            xKey: i,
                            xLabel: "xMark " + i,
                            yKey: j,
                            yLabel: "yMark " + j,
                            val: Math.random() * 25,
                        })
                        }
                    };

                    heatmap(dataset)
        }

        $(document).ready(function() {});
    </script>
</body>

</html>

由于数据数组的大小固定,因此此处的退出选择仍然为空。 D3假定新数据替换了旧数据,但是它不知道新数据应表示为新元素,除非我们当然指定一个关键功能,如现在已删除的注释中所述。这可能是或可能不是您想要的功能。

答案 1 :(得分:1)

我的方法与安德鲁略有不同。

当您有多个图表时,一堆全局变量会变得混乱。

当您单击按钮时:

  1. 致电renderPlot(dataset)
  2. 检查svg中是否有#clip元素
  3. 如果不是,请致电heatmap(dataset)
    1. 构造所有静态内容并附加到svg。
    2. 使用更新所需的变量将datum对象附加到svg
  4. 从svg获取datum
  5. 使用datum对象更新svg的内容

function heatmap(dataset) {

var svg = d3.select("#chart")
    .select("svg");

var xLabels = [],
    yLabels = [];
for (i = 0; i < dataset.length; i++) {
    if (i==0){
        xLabels.push(dataset[i].xLabel);
        var j = 0;
        while (dataset[j+1].xLabel == dataset[j].xLabel){
            yLabels.push(dataset[j].yLabel);
            j++;
        }
        yLabels.push(dataset[j].yLabel);
    } else {
        if (dataset[i-1].xLabel == dataset[i].xLabel){
            //do nothing
        } else {
            xLabels.push(dataset[i].xLabel);                    
        }
    }
};

var margin = {top: 0, right: 25,
              bottom: 60, left: 75};  

var width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom;

var dotSpacing = 0,
    dotWidth = width/(2*(xLabels.length+1)),
    dotHeight = height/(2*yLabels.length);

var daysRange = d3.extent(dataset, function (d) {return d.xKey}),
    days = daysRange[1] - daysRange[0];

var hoursRange = d3.extent(dataset, function (d) {return d.yKey}),
    hours = hoursRange[1] - hoursRange[0];    

var tRange = d3.extent(dataset, function (d) {return d.val}),
    tMin = tRange[0],
    tMax = tRange[1];

var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];

// the scale
var scale = {
    x: d3.scaleLinear()
       .range([-1, width]),
    y: d3.scaleLinear()
       .range([height, 0]),
};

var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
    yBand = d3.scaleBand().domain(yLabels).range([height, 0]);

var axis = {
    x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
    y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
};

var colorScale = d3.scaleQuantile()
    .domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
    .range(colors);

var zoom = d3.zoom()
    .scaleExtent([1, dotHeight])
    .on("zoom", zoomed);

var tooltip = d3.select("body").append("div")
    .attr("id", "tooltip")
    .style("opacity", 0);

// SVG canvas
svg .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .call(zoom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// Clip path
svg.append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height+dotHeight);


// Heatmap dots
var heatDotsGroup = svg.append("g")
    .attr("clip-path", "url(#clip)")
    .append("g");

//Create X axis
var renderXAxis = svg.append("g")
    .attr("class", "x axis")
    //.attr("transform", "translate(0," + scale.y(-0.5) + ")")
    //.call(axis.x)

//Create Y axis
var renderYAxis = svg.append("g")
    .attr("class", "y axis")
    .call(axis.y);


function zoomed() {
    d3.event.transform.y = 0;
    d3.event.transform.x = Math.min(d3.event.transform.x, 5);
    d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
    // console.log(d3.event.transform)

    // update: rescale x axis
    renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));

    // Make sure that only the x axis is zoomed
    heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}

var chartData = {};
chartData.scale = scale;
chartData.axis = axis;
chartData.xBand = xBand;
chartData.yBand = yBand;
chartData.colorScale = colorScale;
chartData.heatDotsGroup = heatDotsGroup;
chartData.dotWidth = dotWidth;
chartData.dotHeight = dotHeight;

svg.datum(chartData);

//svg.call(renderPlot, dataset)
}
function updateScales(data, scale){
    scale.x.domain([0, d3.max(data, d => d.xKey)]),
    scale.y.domain([0, d3.max(data, d => d.yKey)])
}

function renderPlot(dataset){
    
    var svg = d3.select("#chart")
        .select("svg");
    if (svg.select("#clip").empty()) { heatmap(dataset); }
    chartData = svg.datum();
    //Do the axes
    updateScales(dataset, chartData.scale);
    svg.select('.y.axis').call(chartData.axis.y)
    svg.select('.x.axis')
            .attr("transform", "translate(0," + chartData.scale.y(-0.5) + ")")
            .call(chartData.axis.x)
    
    // Do the chart
    const update = chartData.heatDotsGroup.selectAll("ellipse")
    .data(dataset);
    
    update
    .enter()
    .append("ellipse")
    .attr("rx", chartData.dotWidth)
    .attr("ry", chartData.dotHeight)
    .merge(update)
    .transition().duration(800)
        .attr("cx", function (d) {return chartData.scale.x(d.xKey) - chartData.xBand.bandwidth();})
        .attr("cy", function (d) {return chartData.scale.y(d.yKey) + chartData.yBand.bandwidth();})
        .attr("fill", function (d) { return chartData.colorScale(d.val);} );
    
    update.exit().remove();
}
#clickMe{
height:50px;
width:150px;
background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">

<title>Heatmap Chart</title>

<!-- Reference style.css -->
<!--    <link rel="stylesheet" type="text/css" href="style.css">-->

<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<script src='heatmap_v4.js' type='text/javascript'></script>
</head>

<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />

<div id='chart'>
<svg width="700" height="500">
  <g class="focus">
    <g class="xaxis"></g>
    <g class="yaxis"></g>
  </g>
</svg>
   </div>

   
<script>
    function run() {
        var dataset = [];
        for (let i = 1; i < 360; i++) { //360
            for (j = 1; j < 7; j++) {  //75
                dataset.push({
                    xKey: i,
                    xLabel: "xMark " + i,
                    yKey: j,
                    yLabel: "yMark " + j,
                    val: Math.random() * 25,
                })
            }
        };

        renderPlot(dataset)
    }

    $(document).ready(function() {});
</script>
</body>

</html>