score:1

Accepted answer

You can do this with d3.nest and a bit of data wrangling if you calculate the summary first and integrate it with the nested data. It will also be easier if you make a function for adding td elements (i.e. just turn the existing code into a function):

function addCells ( selection ) {
// create a cell in each row for each column
  selection.selectAll('td')
  .data(function(row) {
    return columns.map(function(column) {
      return {
        column: getHeaderWithColumn(column),
        value: row[column],
      };
    });
  })
  .enter()
  .append('td')
  .style("color", function(d) {
    if (d.column === 'PM') {
      return pmColorScale(d.value);
    }

    if (d.column === 'Profit') {
      if (d.value < 0) {
        return "red";
      }
    }
  }).html(function(d) {
    percentFormatter = d3.format(".0%");
    dollarFormatter = d3.format("$,");
    if (d.column === 'PM') {
      if (!isNaN(d.value)) {
        if (isNaN(d.value)) {
          d.value === Number.parseInt(0);
        }
        return percentFormatter(d.value / 100);
      }
    }
    if (d.column === 'Spend' || d.column === 'Revenue' || d.column === 'CPC' || d.column === 'RPC' || d.column === 'RPA' || d.column === 'Profit') {
      if (!isNaN(d.value)) {
        return dollarFormatter(d.value);
      }
    }
    return d.value;
  });
}

Tables can have any number of tbody elements, so we can take advantage of that and add a separate tbody for each set of rows representing an affiliate.

First, calculate the summary:

const summary = merged.reduce(function(val, acc) {
  if (!val[acc.affiliateId]) val[acc.affiliateId] = {
    affiliateId: acc.affiliateId,
    Spend: 0,
    revenue: 0,
    profit: 0,
    profitMargin: 0,
    Clicks: 0,
    Conversions: 0
  };
  val[acc.affiliateId].Clicks += Number.parseFloat(acc.Clicks);
  val[acc.affiliateId].Conversions += Number.parseFloat(acc.Conversions);
  val[acc.affiliateId].Spend += Number.parseFloat(acc.Spend);
  val[acc.affiliateId].revenue += Number.parseFloat(acc.revenue);
  val[acc.affiliateId].profit += Number.parseFloat(acc.profit);
  val[acc.affiliateId].Campaign_Name = acc.Campaign_Name;
  val[acc.affiliateId].affiliate = acc.affiliate;
  val[acc.affiliateId].advertiser = acc.advertiser;

  return val;
}, {});

Nest the data using the affiliateId as the key, and integrate the summary data into the nested data:

const nested = d3.nest()
.key( d => d.affiliateId )
.entries(merged)
.map( d => { d.header = summary[d.key]; return d } );

nested is now an array with entries that look like this:

{key: "6480", 
 values: [Array], // rows with affiliateId 6480 
 header: Object   // collated data on 6480 from `summary`
}

Bind that to the table and add a tbody for each entry:

var tbody = table1.selectAll('tbody')
  .data(nested)
  .enter()
  .append('tbody');

Add rows for the summary data by taking the header from the bound data. Note that d3 needs data to be in an array, so we return the header data as a single-element array. Give the row a class to distinguish it from the monthly data that we'll add next.

var summaryRow = tbody
  .selectAll('tr.summary')
  .data(function(d) { return [d.header] })
  .enter()
  .append('tr')
  .classed('summary',true)

Add the td elements for the row:

addCells(summary)

Now you can do the same with the rows for the monthly datasets, which d3.nest has put in d.values. Add the rows and then add the cells to the rows:

var rows = tbody.selectAll('tr.entry')
  .data(d => {
    return d.values
  })
  .enter()
  .append('tr')
  .classed('entry', true)

addCells(rows);

Full demo with some fake data:

function go() {

const merged = [{
  "date": "2018-10-09",
  "Campaign_Name": "Foo - 6480_1925",
  "affiliateId": "6480",
  "Clicks": 6,
  "Conversions": 0,
  "Spend": 0.5019512028,
  "affiliate": "Y_Foo_6480",
  "revenue": 58.22,
  "advertiser": "sky",
  "spend": 0.5,
  "profit": 57.72,
  "profitMargin": "99",
  "cpc": 0.08,
  "rpc": 9.7,
  "rpa": ""
}, {
  "date": "2018-09-09",
  "Campaign_Name": "Foo - 6480_1925",
  "affiliateId": "6480",
  "Clicks": 6,
  "Conversions": 0,
  "Spend": 0.5019512028,
  "affiliate": "Y_Foo_6480",
  "revenue": 58.22,
  "advertiser": "sky",
  "spend": 0.5,
  "profit": 57.72,
  "profitMargin": "99",
  "cpc": 0.08,
  "rpc": 9.7,
  "rpa": ""
}, {
  "date": "2018-08-09",
  "Campaign_Name": "Foo - 6480_1925",
  "affiliateId": "6480",
  "Clicks": 6,
  "Conversions": 0,
  "Spend": 0.5019512028,
  "affiliate": "Y_Foo_6480",
  "revenue": 58.22,
  "advertiser": "sky",
  "spend": 0.5,
  "profit": 57.72,
  "profitMargin": "99",
  "cpc": 0.08,
  "rpc": 9.7,
  "rpa": ""
}, {
  "date": "2018-07-09",
  "Campaign_Name": "Foo - 6480_1925",
  "affiliateId": "6480",
  "Clicks": 6,
  "Conversions": 0,
  "Spend": 0.5019512028,
  "affiliate": "Y_Foo_6480",
  "revenue": 58.22,
  "advertiser": "sky",
  "spend": 0.5,
  "profit": 57.72,
  "profitMargin": "99",
  "cpc": 0.08,
  "rpc": 9.7,
  "rpa": ""
}, {
  "date": "2018-10-09",
  "Campaign_Name": "Bar Mutual - 7157_2020",
  "affiliateId": "7157",
  "Clicks": 583,
  "Conversions": 0,
  "Spend": 166.0008698087,
  "affiliate": "Y_GetStuff_7191",
  "revenue": 2.22,
  "advertiser": "Bar Mutual Insurance",
  "spend": 166,
  "profit": -163.78,
  "profitMargin": "-7378",
  "cpc": 0.28,
  "rpc": 0,
  "rpa": ""
}, {
  "date": "2018-09-09",
  "Campaign_Name": "Bar Mutual - 7157_2020",
  "affiliateId": "7157",
  "Clicks": 1,
  "Conversions": 0,
  "Spend": 0.0108815003,
  "affiliate": "Y_GetStuff_7191",
  "revenue": "",
  "advertiser": "Acme, Inc. ",
  "spend": 0.01,
  "profit": -0.01,
  "cpc": 0.01,
  "rpc": 0,
  "rpa": ""
}, {
  "date": "2018-08-09",
  "Campaign_Name": "Bar Mutual - 7157_2020",
  "affiliateId": "7157",
  "Clicks": 6,
  "Conversions": 0,
  "Spend": 1.3499999642,
  "affiliate": "Y_GetStuff_7191",
  "revenue": 0.36,
  "advertiser": "Art",
  "spend": 1.35,
  "profit": -0.99,
  "profitMargin": "-275",
  "cpc": 0.22,
  "rpc": 0.06,
  "rpa": ""
}, {
  "date": "2018-07-09",
  "Campaign_Name": "Bar Mutual - 7157_2020",
  "affiliateId": "7157",
  "Clicks": 199,
  "Conversions": 0,
  "Spend": 10.2255493868,
  "affiliate": "Y_GetStuff_7191",
  "revenue": "",
  "advertiser": "Acme, Inc. ",
  "spend": 10.23,
  "profit": -10.23,
  "cpc": 0.06,
  "rpc": 0,
  "rpa": ""
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - NS - New Cars - 4735_2092",
  "affiliateId": "4735",
  "Clicks": 200,
  "Conversions": 34,
  "Spend": 59.1212777495,
  "affiliate": "Y_Mobile-3B_OMNewCar_4735",
  "revenue": 20.1,
  "advertiser": "Acme, Inc. ",
  "spend": 59.12,
  "profit": -39.02,
  "profitMargin": "-194",
  "cpc": 0.3,
  "rpc": 0.1,
  "rpa": 0.59
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - NS - New Cars - 6586_2092",
  "affiliateId": "6586",
  "Clicks": 472,
  "Conversions": 79,
  "Spend": 61.0002093334,
  "affiliate": "Y_New Cars_6586",
  "revenue": 0.75,
  "advertiser": "Acme, Inc. ",
  "spend": 61,
  "profit": -60.25,
  "profitMargin": "-8033",
  "cpc": 0.13,
  "rpc": 0,
  "rpa": 0.01
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - NS - New Cars - 6618_2092",
  "affiliateId": "6618",
  "Clicks": 2,
  "Conversions": 1,
  "Spend": 0.2018772066,
  "affiliate": "Y_New Cars_6618",
  "revenue": "",
  "advertiser": "Acme, Inc. ",
  "spend": 0.2,
  "profit": -0.2,
  "cpc": 0.1,
  "rpc": 0,
  "rpa": 0
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - NS - New Cars - 7247_1773",
  "affiliateId": "7247",
  "Clicks": 76,
  "Conversions": 7,
  "Spend": 13.9912065665,
  "affiliate": "Y_New Cars_7247",
  "revenue": "",
  "advertiser": "Acme, Inc. ",
  "spend": 13.99,
  "profit": -13.99,
  "cpc": 0.18,
  "rpc": 0,
  "rpa": 0
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - NS - New Cars - NSConvLAL - 6594_2092",
  "affiliateId": "6594",
  "Clicks": 905,
  "Conversions": 264,
  "Spend": 293.5172631741,
  "affiliate": "Y_New Cars_6594",
  "revenue": 1.72,
  "advertiser": "Acme, Inc. ",
  "spend": 293.64,
  "profit": -291.8,
  "profitMargin": "-16965",
  "cpc": 0.32,
  "rpc": 0,
  "rpa": 0.01
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - NS - New Cars - NSConvLAL - 7251_2092",
  "affiliateId": "7251",
  "Clicks": 202,
  "Conversions": 1,
  "Spend": 64.9944748056,
  "affiliate": "Y_New Cars_7251",
  "revenue": "",
  "advertiser": "Acme, Inc. ",
  "spend": 64.99,
  "profit": -64.99,
  "cpc": 0.26,
  "rpc": 0,
  "rpa": 0
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - NS - New Cars - Span/Eng - 7165_1773",
  "affiliateId": "7165",
  "Clicks": 891,
  "Conversions": 49,
  "Spend": 74.5347691271,
  "affiliate": "Y_New Cars_7165",
  "revenue": "",
  "advertiser": "Acme, Inc. ",
  "spend": 74.53,
  "profit": -74.53,
  "cpc": 0.08,
  "rpc": 0,
  "rpa": 0
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - New Cars - 4713_1875",
  "affiliateId": "4713",
  "Clicks": 1084,
  "Conversions": 326,
  "Spend": 64.7100853845,
  "affiliate": "Y_New Cars_4713",
  "revenue": "",
  "advertiser": "Umbrella",
  "spend": 64.71,
  "profit": -64.71,
  "cpc": 0.05,
  "rpc": 0,
  "rpa": 0
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - New Cars - 7259_1875",
  "affiliateId": "7259",
  "Clicks": 1568,
  "Conversions": 173,
  "Spend": 51.5844874121,
  "affiliate": "Y_New Cars_7259",
  "revenue": "",
  "advertiser": "Umbrella",
  "spend": 51.58,
  "profit": -51.58,
  "cpc": 0.03,
  "rpc": 0,
  "rpa": 0
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - Destination - 7221_2068",
  "affiliateId": "7221",
  "Clicks": 75,
  "Conversions": 0,
  "Spend": 4.9945735649,
  "affiliate": "Y_Destination_7221",
  "revenue": 1.5,
  "advertiser": "L-health",
  "spend": 4.99,
  "profit": -3.17,
  "profitMargin": "-212",
  "cpc": 0.06,
  "rpc": 0.02,
  "rpa": ""
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - Product - 7243_1791",
  "affiliateId": "7243",
  "Clicks": 36,
  "Conversions": 0,
  "Spend": 1.201965495,
  "affiliate": "Y_Product_7243",
  "revenue": 0.07,
  "advertiser": "Product Tubs",
  "spend": 1.2,
  "profit": -1.13,
  "profitMargin": "-1617",
  "cpc": 0.03,
  "rpc": 0,
  "rpa": ""
}, {
  "date": "2018-10-09",
  "Campaign_Name": "test - Homewares - 7269_2163",
  "affiliateId": "7269",
  "Clicks": 11,
  "Conversions": 0,
  "Spend": 0.5186665021,
  "affiliate": "Y_Homewares_7269",
  "revenue": "",
  "advertiser": "Acme, Inc. ",
  "spend": 0.64,
  "profit": -0.64,
  "cpc": 0.05,
  "rpc": 0,
  "rpa": ""
}]
const columnHeaderMap = {
  Date: "date",
  AffiliateId: "affiliateId",
  Spend: "spend",
  Revenue: "revenue",
  CPC: "cpc",
  RPC: "rpc",
  RPA: "rpa",
  Profit: "profit",
  PM: "profitMargin",
  Campaign: "Campaign_Name",
  Affiliate: "affiliate"
};

const headers = Object.keys(columnHeaderMap);
const columns = headers.map(header => columnHeaderMap[header]);

const getHeaderWithColumn = column => {
  for (let header in columnHeaderMap) {
    if (columnHeaderMap[header] === column) {
      return header;
    }
  }
};

var pmColorScale = d3.scaleThreshold()
  .domain([0, 20])
  .range(['red', '#FDE541', 'green']);



// // setup the area for the table
// d3.selectAll('table').data([0]).enter().append('table');
var table1 = d3.select('#table');

table1.selectAll('thead').data([0]).enter().append('thead');
var thead = table1.select('thead');

//   // append the header row
thead.append('tr')
  .selectAll('th')
  .data(headers)
  .enter()
  .append('th')
  .text(function(column) {
    return column;
  })
  .on('click', function(d) {
    thead.attr('class', 'header');
    const columnName = columnHeaderMap[d];
    if (sortAscending) {
      rows.sort((a, b) => {
        if (d === 'PM') {
          if (isNaN(a.profitMargin)) {
            return a.profitMargin == 0;
          }
          if (isNaN(b.profitMargin)) {
            return b.profitMargin == 0;
          }
          a.profitMargin = Number.parseFloat(a.profitMargin);
          b.profitMargin = Number.parseFloat(b.profitMargin);
          // parse the string into a float
          // then do the sort calc
        }
        return b[columnHeaderMap[d]] < a[columnHeaderMap[d]] ? 1 : -1;
      });
      sortAscending = false;
    } else {
      rows.sort((a, b) => {
        if (d === 'PM') {
          if (isNaN(a.profitMargin)) {
            return a.profitMargin == 0;
          }
          if (isNaN(b.profitMargin)) {
            return b.profitMargin == 0;
          }
          a.profitMargin = Number.parseFloat(a.profitMargin);
          b.profitMargin = Number.parseFloat(b.profitMargin);

          // parse the string into a float
          // then do the sort calc
        }
        return b[columnHeaderMap[d]] > a[columnHeaderMap[d]] ? 1 : -1;
      });
      sortAscending = true;
    }

  });

// Time to make the summary

// // This is a subtotal reducer so each id has its total
const summary = merged.reduce(function(val, acc) {
  if (!val[acc.affiliateId]) val[acc.affiliateId] = {
    affiliateId: acc.affiliateId,
    Spend: 0,
    revenue: 0,
    profit: 0,
    profitMargin: 0,
    Clicks: 0,
    Conversions: 0
  };
  val[acc.affiliateId].Clicks += Number.parseFloat(acc.Clicks);
  val[acc.affiliateId].Conversions += Number.parseFloat(acc.Conversions);
  val[acc.affiliateId].Spend += Number.parseFloat(acc.Spend);
  val[acc.affiliateId].revenue += Number.parseFloat(acc.revenue);
  val[acc.affiliateId].profit += Number.parseFloat(acc.profit);
  val[acc.affiliateId].Campaign_Name = acc.Campaign_Name;
  val[acc.affiliateId].affiliate = acc.affiliate;
  val[acc.affiliateId].advertiser = acc.advertiser;

  return val;
}, {});

const nested = d3.nest()
.key( d => d.affiliateId )
.entries(merged)
.map( d => { d.header = summary[d.key]; return d } );


var tbody = table1.selectAll('tbody')
  .data(nested)
  .enter()
    .append('tbody');

var summaryRow = tbody
  .selectAll('tr.summary')
  .data(d => [d.header])
  .enter()
  .append('tr')
  .classed('summary',true)

addCells(summaryRow)


// create a row for each object in the data
var rows = tbody.selectAll('tr.entry')
  .data(d => {
    return d.values
  })
  .enter()
  .append('tr')
  .classed('entry', true)

addCells(rows);

function addCells ( selection ) {
// create a cell in each row for each column
  selection.selectAll('td')
  .data(function(row) {
    return columns.map(function(column) {
      return {
        column: getHeaderWithColumn(column),
        value: row[column],
      };
    });
  })
  .enter()
  .append('td')
  .style("color", function(d) {
    if (d.column === 'PM') {
      return pmColorScale(d.value);
    }

    if (d.column === 'Profit') {
      if (d.value < 0) {
        return "red";
      }
    }
  }).html(function(d) {
    percentFormatter = d3.format(".0%");
    dollarFormatter = d3.format("$,");
    if (d.column === 'PM') {
      if (!isNaN(d.value)) {
        if (isNaN(d.value)) {
          d.value === Number.parseInt(0);
        }
        return percentFormatter(d.value / 100);
      }
    }
    if (d.column === 'Spend' || d.column === 'Revenue' || d.column === 'CPC' || d.column === 'RPC' || d.column === 'RPA' || d.column === 'Profit') {
      if (!isNaN(d.value)) {
        return dollarFormatter(d.value);
      }
    }
    return d.value;
  });
}

function sort(a, b) {
  if (typeof a == "string") {
    var parseA = format.parse(a);
    if (parseA) {
      var dateA = parseA.getDate();
      var dateB = format.parse(b).getDate();
      return dateA > dateB ? 1 : dateA == dateB ? 0 : -1;
    } else
      return a.localeCompare(b);
  } else if (typeof a == "number") {
    return a > b ? 1 : a == b ? 0 : -1;
  } else if (typeof a == "boolean") {
    return b ? 1 : a ? -1 : 0;
  }
}


}
window.onload = go;
.summary td {
  font-weight: bold;
  background-color: aliceblue; 
}
<script src="http://d3js.org/d3.v5.js"></script>  

<table id="table"></table>


Related Query