如何使用D3对详细信息行上方的摘要行将HTML表格条目进行分组

时间:2018-10-09 20:09:37

标签: javascript d3.js

我仍在学习编程和D3,所以请在这里忍受。

我有一个表,该表是使用D3从JSON数据生成的。我希望接下来要做的是在详细信息的上方放置一个摘要行,以输入相同但数字不同的日期。我有一个reducer函数,该函数可以显示每个条目的总和,但不确定如何将其附加到详细信息上方的表中,从而产生类似以下内容的结果:

mockup

我的归约工作已准备好摘要,那么如何将其附加到现在正在使用明细数据的表中?我认为我们必须使用nest,但不能完全确定要走的路。我的代码如下:

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-10-09",
"Campaign_Name": "Bar Mutual - 7157_2020",
"affiliateId": "7157",
"Clicks": 583,
"Conversions": 0,
"Spend": 166.0008698087,
"affiliate": "Y_Bar Mutual_7157",
"revenue": 2.22,
"advertiser": "Bar Mutual Insurance",
"spend": 166,
"profit": -163.78,
"profitMargin": "-7378",
"cpc": 0.28,
"rpc": 0,
"rpa": ""
}, {
"date": "2018-10-09",
"Campaign_Name": "test - Baz Deals - CAN - 4086_1743",
"affiliateId": "4086",
"Clicks": 1,
"Conversions": 0,
"Spend": 0.0108815003,
"affiliate": "Y_Mobile_OMBaz_CAN_4086",
"revenue": "",
"advertiser": "Acme, Inc. ",
"spend": 0.01,
"profit": -0.01,
"cpc": 0.01,
"rpc": 0,
"rpa": ""
}, {
"date": "2018-10-09",
"Campaign_Name": "test - GetStuff - 7191_2133",
"affiliateId": "7191",
"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-10-09",
"Campaign_Name": "test - Lawyer - 7275_2165",
"affiliateId": "7275",
"Clicks": 199,
"Conversions": 0,
"Spend": 10.2255493868,
"affiliate": "Y_Lawyer_7275",
"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;
                }
            }
        };

        // // 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');

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

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

        //   // 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;
                }

            });

        //  // create a row for each object in the data
        var rows = tbody.selectAll('tr')
            .data(merged)
            .enter()
            .append('tr');

        //   // create a cell in each row for each column
        var cells = rows.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;
            }
        }

        // 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;
        }, {});
        // console.log(summary); // returns the array with the accumulators and ids as keys, after which I then I set to an array to look like typical JSON
          const summaryArr = [];
        for (var entry in summary) {
            // console.log(sum[entry]);
            summaryArr.push(summary[entry]);
        }

        }

    }

1 个答案:

答案 0 :(得分:1)

如果首先计算摘要并将其与嵌套数据集成,则可以使用d3.nest和一些数据争执来进行此操作。如果您制作了一个用于添加td元素的函数(也就是将现有代码变成一个函数),也会更容易:

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;
  });
}

表可以包含任意数量的tbody元素,因此我们可以利用它,并为代表会员的每行添加一个单独的tbody

首先,计算摘要:

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;
}, {});

使用affiliateId作为键嵌套数据,并将summary数据集成到嵌套数据中:

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

nested现在是一个数组,其中的条目如下所示:

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

将其绑定到表中,并为每个条目添加一个tbody

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

通过从绑定数据中获取标头,为摘要数据添加行。请注意,d3需要数据位于数组中,因此我们将标头数据作为单元素数组返回。给该行一个类,以区别于接下来要添加的每月数据。

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

为该行添加td元素:

addCells(summary)

现在,您可以对d3.nest放入d.values中的每月数据集的行进行相同的操作。添加行,然后将单元格添加到行:

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

addCells(rows);

带有一些虚假数据的完整演示:

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>