Home > Software design >  How can I use d3.js to sort a table by different variables?
How can I use d3.js to sort a table by different variables?

Time:10-28

    data = [
      {
        "name": "Dave",
        "dept": "Marketing",
        "region": "South",
        "items": 28
      },
      {
        "name": "Amy",
        "dept": "IT",
        "region": "West",
        "items": 46
      },
      {
        "name": "John",
        "dept": "Sales",
        "region": "North",
        "items": 35
      },
      {
        "name": "Sarah",
        "dept": "Communications",
        "region": "North",
        "items": 13
      }
    ]

    drawTable(data)
    sortTable(data)

    function drawTable(data) {

      // not working
      // when redrawing table, trying to re-initialize sort function
      sortTable(data)

      d3.select('#data-table').remove()

      let table = d3.select('#table').append('div').attr('id', 'data-table')

      let row = table.selectAll('.row')
        .data(data)
        .enter()
        .append('div')
        .attr('class', 'row')

      let grid = row
        .append('div')
        .attr('class', 'grid')

      let name = grid.append('div')
        .attr('class', 'box')
        .html(d => d.name)

      let dept = grid.append('div')
        .attr('class', 'box')
        .html(d => d.dept)

      let region = grid.append('div')
        .attr('class', 'box')
        .html(d => d.region)

      let items = grid.append('div')
        .attr('class', 'box')
        .html(d => d.items)

    }

    function sortTable(data) {
      d3.selectAll(".name,.dept,.region,.items").on('click', function () {
        let sortWhich = d3.select(this).attr("class")
        console.log('sortWhich', sortWhich)

        let tableSort
        if (sortWhich == "name") {
          tableSort = data.sort((a, b) => d3.ascending(a.name, b.name))
        } else if (sortWhich == "dept") {
          tableSort = data.sort((a, b) => d3.ascending(a.dept, b.dept))
        } else if (sortWhich == 'region') {
          tableSort = data.sort((a, b) => d3.ascending(a.region, b.region))
        } else {
          tableSort = data.sort((a, b) => d3.ascending(a.items, b.items))
        }

        // Can I combine the above and pass a variable to a sort function? This is not working:
        // let tableSort = data.sort((a, b) => a.sortWhich - b.sortWhich)

        drawTable(tableSort)
      })
    }
html,
    body {
      height: 100%;
      margin: 0;
    }

    .grid {
      display: flex;
      flex-wrap: wrap;
      flex-direction: row;
    }

    .grid>div {
      display: flex;
      flex-basis: calc(100%/4 - 28px);
      justify-content: center;
      flex-direction: column;
      padding: 10px;
    }

    .box {
      margin: 0;
    }

    .box {
      color: #000;
      border: .5px solid #ccc;
    }

    .hrbox {
      background-color: #333;
      color: #fff;
    }
<!DOCTYPE html>
<html lang="en">

<head>
  <script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
  <div id="table">
    <div class="grid">
      <div class="box hrbox name">
        <div>name</div>
      </div>
      <div class="box hrbox dept">
        <div>dept</div>
      </div>
      <div class="box hrbox region">
        <div>region</div>
      </div>
      <div class="box hrbox items">
        <div>items</div>
      </div>
    </div>
    <div id="data-table"></div>
  </div>
</body>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

I created a flexbox table using d3.js that pulls json data to populate it. I want to be able to sort each column by its header row, and I wrote a sort function for that. However, I'm only able to sort one column at a time right now. Then if I refresh the page, I can sort a different column. I want to be able to switch without needing to refresh the page. So- sort by name, then decide to sort by items, then region, to see different views of the data. What am I missing?

I'd appreciate any feedback.

[I'd also like to just have one sort function that I pass a variable to instead of having all those conditionals. I'm not sure if it's possible. (Definitely not my main concern.) But putting it out there]

Thanks!

CodePudding user response:

If you look at the console you'll see that sortWhich is always something like "box hrbox whatever", and because of that all conditions will fail and the code will go to the else at the bottom.

A simplest solution is using includes():

if (sortWhich.includes("name")) etc...

Here is your code with that change:

Show code snippet

data = [{
    "name": "Dave",
    "dept": "Marketing",
    "region": "South",
    "items": 28
  },
  {
    "name": "Amy",
    "dept": "IT",
    "region": "West",
    "items": 46
  },
  {
    "name": "John",
    "dept": "Sales",
    "region": "North",
    "items": 35
  },
  {
    "name": "Sarah",
    "dept": "Communications",
    "region": "North",
    "items": 13
  }
]

drawTable(data)
sortTable(data)

function drawTable(data) {

  // not working
  // when redrawing table, trying to re-initialize sort function
  sortTable(data)

  d3.select('#data-table').remove()

  let table = d3.select('#table').append('div').attr('id', 'data-table')

  let row = table.selectAll('.row')
    .data(data)
    .enter()
    .append('div')
    .attr('class', 'row')

  let grid = row
    .append('div')
    .attr('class', 'grid')

  let name = grid.append('div')
    .attr('class', 'box')
    .html(d => d.name)

  let dept = grid.append('div')
    .attr('class', 'box')
    .html(d => d.dept)

  let region = grid.append('div')
    .attr('class', 'box')
    .html(d => d.region)

  let items = grid.append('div')
    .attr('class', 'box')
    .html(d => d.items)

}

function sortTable(data) {
  d3.selectAll(".name,.dept,.region,.items").on('click', function() {
    let sortWhich = d3.select(this).attr("class")
    console.log('sortWhich', sortWhich)

    let tableSort
    if (sortWhich.includes("name")) {
      tableSort = data.sort((a, b) => d3.ascending(a.name, b.name))
    } else if (sortWhich.includes("dept")) {
      tableSort = data.sort((a, b) => d3.ascending(a.dept, b.dept))
    } else if (sortWhich.includes('region')) {
      tableSort = data.sort((a, b) => d3.ascending(a.region, b.region))
    } else {
      tableSort = data.sort((a, b) => d3.ascending(a.items, b.items))
    }

    // Can I combine the above and pass a variable to a sort function? This is not working:
    // let tableSort = data.sort((a, b) => a.sortWhich - b.sortWhich)

    drawTable(tableSort)
  })
}
html,
body {
  height: 100%;
  margin: 0;
}

.grid {
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
}

.grid>div {
  display: flex;
  flex-basis: calc(100%/4 - 28px);
  justify-content: center;
  flex-direction: column;
  padding: 10px;
}

.box {
  margin: 0;
}

.box {
  color: #000;
  border: .5px solid #ccc;
}

.hrbox {
  background-color: #333;
  color: #fff;
}
<!DOCTYPE html>
<html lang="en">

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

<body>
  <div id="table">
    <div class="grid">
      <div class="box hrbox name">
        <div>name</div>
      </div>
      <div class="box hrbox dept">
        <div>dept</div>
      </div>
      <div class="box hrbox region">
        <div>region</div>
      </div>
      <div class="box hrbox items">
        <div>items</div>
      </div>
    </div>
    <div id="data-table"></div>
  </div>
</body>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

There are some problems here:

  • sort() sorts the array in place, no need for that tableSort variable
  • Removing the elements for painting the table again is not idiomatic D3, just use enter/update/exit selections.

Regarding your secondary question, if you had the proper variable names you could just use bracket notation, removing all that if...else section:

data.sort((a, b) => d3.ascending(a[sortWhich], b[sortWhich]))
  • Related