通过删除已经存在的条来更新 D3 交互式条形图

Updating D3 interactive barchart by removing bars that are already there

问题:我想更新图表中的条形图,以便在单击“Dreamworks”按钮时添加新条形图并删除旧条形图。我知道这是一个enter(), exit()问题,但我不知道具体如何实现它。

Context:当我的按钮被点击时,它会激活一个函数来提取我按钮的内部 HTML 并使用它来过滤我的数据,因此只有来自一个公司留下来。该代码有效,但不是删除旧条,而是将新条附加到旧条之上。当您查看控制台时,每次单击按钮时都会出现一个新的“g”元素(其中包含新的“rects”)。我降低了“矩形”的不透明度以显示正在发生的事情。我从我的代码中删除了所有 exit() 和 remove() 尝试,因为没有任何效果。

HTML代码:

            <div class= "button-holder">
                <button class= "button button-dreamworks">DreamWorks</button>
                <button class= "button button-disney">Disney</button>
                <button class= "button button-pixar">Pixar</button>
            </div>
                <div class = "chart chart1"></div>

JS代码:

async function drawBar() {
    // 2. Create Chart Dimensions 
    const width = 600
    let dimensions = {
        width,
        height: width*0.6, 
        margin: {
            top: 30,
            right: 10,
            bottom: 50,
            left: 50
        }
    }
    
    dimensions.boundedWidth = dimensions.width
        -dimensions.margin.right -dimensions.margin.left 
    dimensions.boundedHeight = dimensions.height 
        -dimensions.margin.top -dimensions.margin.left 

// 3. Draw Canvas 
const wrapper = d3.select(".chart1")
    .append("svg")
    .attr("width", dimensions.width)
    .attr("height", dimensions.height)



// 4. Load Data  
    const raw_data = await d3.csv("./data/all_movie_data.csv")


    
    const drawBarChart = function(company_name) {
    const dataset = raw_data.filter(function(d){ return  d["company"] == company_name })
    const xAccessor = d => d["name"]
    const yAccessor = d => parseFloat(d.budget) 


let bounds = wrapper
    .append("g")
    .attr("class", "bounds")
    .style(
      "transform",
      `translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
    );


// 5. Create scales 
const xScale = d3.scaleBand()
    .domain(dataset.map(xAccessor))
    .range([0,dimensions.boundedWidth])
    .padding(0.4);
    
const yScale = d3.scaleLinear()
    .domain(d3.extent(dataset,yAccessor))
    .range([dimensions.boundedHeight, 0])


// 6. Draw Data

bounds.selectAll("rect")
    .data(dataset)
    .join("rect")
    .attr("x", (d) =>  xScale(xAccessor(d))) 
    .attr("y", (d) => yScale(yAccessor(d)))
    .attr("width",  xScale.bandwidth()) 
    .attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d))) 
    .attr("fill", "blue");
    

}

//6. Interactions 
drawBarChart("Pixar");

const button1 = d3.select(".button-dreamworks")
    .node()
    .addEventListener("click", onClick1) 

function onClick1() {
    const company = document.querySelector(".button-dreamworks").innerHTML;
    drawBarChart(company);
    }

}

drawBar(); 

你可以在这个代码笔中找到我的代码版本:https://codepen.io/larylc/pen/XWzbQGy

除了数据之外,一切都一样,我只是为了说明问题而编造的。

回答:我现在明白enter(),exit()结构了。

  1. 通过将“rects”设置为变量(我称之为条),我现在可以操纵它们(这给了我一个与以前不同的选择对象)。

  2. 我现在可以将 exit() 和 remove() 添加到 bars 变量。这在以前是不可能的。

  3. 我将边界变量移动到我的 canvas 所在的部分,这完成了 exit() 和 enter() 模式。每次单击按钮时,DOM(条)中的 SVG 元素将匹配应该添加的数据元素的数量。因此,如果我在 DOM 中有 22 个柱,它将更新为 34(或任何新数据集)。我之前尝试过这个并且它有效但是我的栏没有正确更新,这让我想到了我的最后一点。

  4. 最后一个问题是将添加或删除条形以匹配新数据点的数量而不更改现有数据点。这意味着 DOM 不会更新条形以匹配实际数据。因此,如果我有 13 个现有柱并且新数据是 22,它只会保留 13 并添加最后的 9。这不一定与数据匹配。所以我需要添加 merge(bars) 语句以确保我所有的条形图(包括现有的条形图)都会更新。

我的新 JS 代码(所有按钮都正常工作)

async function drawBar() {
    //  Create Chart Dimensions 
    const width = 600
    let dimensions = {
        width,
        height: width*0.6, 
        margin: {
            top: 30,
            right: 10,
            bottom: 50,
            left: 50
        }
    }
    
    dimensions.boundedWidth = dimensions.width
        -dimensions.margin.right -dimensions.margin.left 
    dimensions.boundedHeight = dimensions.height 
        -dimensions.margin.top -dimensions.margin.left 

//  Draw Canvas 
const wrapper = d3.select(".chart1")
    .append("svg")
    .attr("width", dimensions.width)
    .attr("height", dimensions.height)

let bounds = wrapper
    .append("g")
    .attr("class", "bounds")
    .style(
      "transform",
      `translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
    );
    


// Load Data  
const raw_data = await d3.csv("./data/all_movie_data.csv")



//  Function that draws data 
const drawBarChart = function(company_name) {
    const dataset = raw_data.filter(function(d){ return  d["company"] == company_name })
    const xAccessor = d => d["name"]
    const yAccessor = d => parseFloat(d.budget) 


//  Create scales 
const xScale = d3.scaleBand()
    .domain(dataset.map(xAccessor))
    .range([0,dimensions.boundedWidth])
    .padding(0.4);
    
const yScale = d3.scaleLinear()
    .domain(d3.extent(dataset,yAccessor))
    .range([dimensions.boundedHeight, 0])


//  Draw Data

const bars = bounds.selectAll("rect")
    .data(dataset)

bars.join("rect").merge(bars)
    .attr("x", (d) =>  xScale(xAccessor(d))) 
    .attr("y", (d) => yScale(yAccessor(d)))
    .attr("width",  xScale.bandwidth()) 
    .attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d))) 
    .attr("fill", "blue")
    .attr("opacity", 0.4)

    
bars.exit().remove();



}

// Interactions 
drawBarChart("Pixar");

// All Buttons and functions that triggers data change 
const button1 = d3.select(".button-dreamworks")
    .node()
    .addEventListener("click", onClick1) 

function onClick1() {
    const company = document.querySelector(".button-dreamworks").innerHTML;
    drawBarChart(company);
}

const button2 = d3.select(".button-disney")
    .node()
    .addEventListener("click", onClick2) 

function onClick2() {
    const company2 = document.querySelector(".button-disney").innerHTML;
    drawBarChart(company2);
    }

const button3 = d3.select(".button-pixar")
    .node()
    .addEventListener("click", onClick3) 

function onClick3() {
    const company3 = document.querySelector(".button-pixar").innerHTML;
    drawBarChart(company3);
    }


}

drawBar();