Search code examples
dc.jscrossfilter

How to calculate the total sales and make the displayed totals dynamic?


I need to show the total sales of my data to both sellers and cities. However, the values shown in the totals must be dynamic.

P.S. My table allows the user to select the rows, this is because the values are rendered according to each selection made. (This dynamism between the tables is already working perfectly).

The initial state of the Total is rendered with the general sum of all sellers and cities, below I show the tables with their respective totals, however, as shown in the image, the values are static and not dynamic.

enter image description here

I need it to be diahanic because if I select for example the seller João Luis and Miguel, the value to be shown in the total must be the sum of these two sellers.

enter image description here

I asked a question a while ago about How to add row in datatable - DC.js, a friend (@Gordon) who is helping me a lot in doubts with DC.js and Crossfilter told me to use crossfilter.add(). Unfortunately I couldn't do it the way suggested, so I'm here again with practically the same question (sorry for my stupidity).

var composite = dc.compositeChart('#composite');
var vendedorTable = dc.dataTable("#vendedores");
var citiesTable = dc.dataTable("#cities");

function remove_empty_bins(source_group) {
  return {
    top: function(N) {
      return source_group.all().filter(function(d) {
        return d.value.totalAno > 1e-3 ||
          d.value.totalHomologo > 1e-3;
      }).slice(0, N);
    }
  };
}
var adjustX = 10,
  adjustY = 40;

var url = 'https://gist.githubusercontent.com/bernalvinicius/3cece295bc37de1697e7f83418e7fcc9/raw/a5820379ec6eae76ee792495cc5dd1685c977a73/vendedores.json';
d3.json(url).then(function(data) {

  data.forEach(d =>
    Object.assign(d, {
      mes: d.Month,
      atual: d.Vendas_Ano,
      passado: d.Vendas_Ant
    })
  );
  var cf = crossfilter(data);

  vendedorDim = cf.dimension(function(d) {
    return d.vendnm;
  });
  var vendedorGroup = vendedorDim.group().reduce(reduceAdd, reduceRemove, reduceInitial);

  citiesDim = cf.dimension(function(d) {
    return d.zona;
  });
  var citiesGroup = citiesDim.group().reduce(reduceAdd, reduceRemove, reduceInitial);

  var dim = cf.dimension(dc.pluck('mes')),
    grp1 = dim.group().reduceSum(dc.pluck('atual')),
    grp2 = dim.group().reduceSum(dc.pluck('passado'));
  var minMonth = dim.bottom(1)[0].mes;
  var maxMonth = dim.top(1)[0].mes;

  var all = cf.groupAll();

  dc.dataCount(".dc-data-count")
    .dimension(cf)
    .group(all);

  function reduceAdd(p, v) {
    p.totalAno += +v.Vendas_Ano;
    p.totalHomologo += +v.Vendas_Ant;
    return p;
  }

  function reduceRemove(p, v) {
    p.totalAno -= v.Vendas_Ano;
    p.totalHomologo -= v.Vendas_Ant;
    return p;
  }

  function reduceInitial() {
    return {
      totalAno: 0,
      totalHomologo: 0,
    };
  }

  // Fake Dimension
  rank = function(p) {
    return ""
  };

  // Chart by months
  composite
    .width(window.innerWidth - adjustX)
    .height(300)
    .x(d3.scaleLinear().domain([1, 12]))
    .yAxisLabel("")
    .xAxisLabel("Month")
    .legend(dc.legend().x(500).y(0).itemHeight(13).gap(5))
    .renderHorizontalGridLines(true)
    .compose([
      dc.lineChart(composite)
      .dimension(dim)
      .colors('steelblue')
      .group(grp1, "Currently Year"),
      dc.lineChart(composite)
      .dimension(dim)
      .colors('darkorange')
      .group(grp2, "Last Year")
    ])
    .brushOn(true);

  composite.brush().extent([-0.5, data.length + 1.5])
  composite.extendBrush = function(brushSelection) {
    if (brushSelection) {
      vendedorTable.filter(null);
      vendedorDim.filter(null);
      citiesTable.filter(null);
      citiesDim.filter(null);
      const point = Math.round((brushSelection[0] + brushSelection[1]) / 2);
      return [
        point - 0.5,
        point + 0.5
      ];
    }
  };

  // Sales Table
  vendedorTable.width(500)
    .height(480)
    .dimension(remove_empty_bins(vendedorGroup))
    .group(rank)
    .columns([function(d) {
        return d.key;
      },
      function(d) {
        return Number(Math.round(d.value.totalAno * 100) / 100).toLocaleString("es-ES", {
          minimumFractionDigits: 2
        }) + '€';
      },
      function(d) {
        return Number(Math.round(d.value.totalHomologo * 100) / 100).toLocaleString("es-ES", {
          minimumFractionDigits: 2
        }) + '€';
      }
    ])
    .sortBy(function(d) {
      return d.value.totalAno
    })
    .order(d3.descending)

  // Cities Table
  citiesTable
    .width(500)
    .height(480)
    .dimension(remove_empty_bins(citiesGroup))
    .group(rank)
    .columns([function(d) {
        return d.key;
      },
      function(d) {
        return Number(Math.round(d.value.totalAno * 100) / 100).toLocaleString("es-ES", {
          minimumFractionDigits: 2
        }) + '€';
      },
      function(d) {
        return Number(Math.round(d.value.totalHomologo * 100) / 100).toLocaleString("es-ES", {
          minimumFractionDigits: 2
        }) + '€';
      }
    ])
    .controlsUseVisibility(true)
    .sortBy(function(d) {
      return d.value.totalAno
    })
    .order(d3.descending)

  // Sales click events
  vendedorTable.on('pretransition', function(table) {
    table.selectAll('td.dc-table-column')
      .on('click', function(d) {
        let filters = table.filters().slice();
        if (filters.indexOf(d.key) === -1)
          filters.push(d.key);
        else
          filters = filters.filter(k => k != d.key);
        if (filters.length === 0)
          vendedorDim.filter(null);
        else
          vendedorDim.filterFunction(function(d) {
            return filters.indexOf(d) !== -1;
          })
        table.replaceFilter([filters]);

        citiesTable.filter(null);
        citiesDim.filter(null);
        composite.filter(null);

        dc.redrawAll();
      });
    let filters = table.filters();
    table.selectAll('tr.dc-table-row')
      .classed('sel-rows', d => filters.indexOf(d.key) !== -1);
  });

  // Cities click events
  citiesTable.on('pretransition', function(table) {
    table.selectAll('td.dc-table-column')
      .on('click', function(d) {
        let filters = table.filters().slice();
        if (filters.indexOf(d.key) === -1)
          filters.push(d.key);
        else
          filters = filters.filter(k => k != d.key);
        if (filters.length === 0)
          citiesDim.filter(null);
        else
          citiesDim.filterFunction(function(d) {
            return filters.indexOf(d) !== -1;
          })
        table.replaceFilter([filters]);

        vendedorTable.filter(null);
        vendedorDim.filter(null);
        composite.filter(null);

        dc.redrawAll();
      });
    let filters = table.filters();
    table.selectAll('tr.dc-table-row')
      .classed('sel-rows', d => filters.indexOf(d.key) !== -1);
  });


  dc.renderAll();

  // reset functions
  $('#reset').on('click', function() {
    vendedorTable.filter(null);
    vendedorDim.filter(null);
    citiesTable.filter(null);
    citiesDim.filter(null);
    composite.filter(null);

    dc.redrawAll();
  });

  $('#resetTable').on('click', function() {
    vendedorTable.filter(null);
    vendedorDim.filter(null);
    citiesTable.filter(null);
    citiesDim.filter(null);
    composite.filter(null);

    dc.redrawAll();
  });

  $('#resetTable2').on('click', function() {
    vendedorTable.filter(null);
    vendedorDim.filter(null);
    citiesTable.filter(null);
    citiesDim.filter(null);
    composite.filter(null);

    dc.redrawAll();
  });


  /****************************************************************************/
  // Functions to handle responsive
  apply_resizing(composite, adjustX, adjustY, function(composite) {
    composite.legend().x(window.innerWidth - 200);
  });

  var find_query = function() {
    var _map = window.location.search.substr(1).split('&').map(function(a) {
      return a.split('=');
    }).reduce(function(p, v) {
      if (v.length > 1)
        p[v[0]] = decodeURIComponent(v[1].replace(/\+/g, " "));
      else
        p[v[0]] = true;
      return p;
    }, {});
    return function(field) {
      return _map[field] || null;
    };
  }();

  var resizeMode = find_query('resize') || 'widhei';

  function apply_resizing(composite, adjustX, adjustY, onresize) {
    if (resizeMode === 'viewbox') {
      composite
        .width(300)
        .height(200)
        .useViewBoxResizing(true);
      d3.select(composite.anchor()).classed('fullsize', false);
    } else {
      adjustX = adjustX || 0;
      adjustY = adjustY || adjustX || 0;
      composite
        .width(window.innerWidth - adjustX)
        .height(300);
      window.onresize = function() {
        if (onresize) {
          onresize(composite);
        }
        composite
          .width(window.innerWidth - adjustX)
          .height(300);

        if (composite.rescale) {
          composite.rescale();
        }
        composite.redraw();
      };
    }
  }
});
#composite {
  padding: 10px;
}

.dc-table-group {
  visibility: collapse;
}

tr.dc-table-row.sel-rows {
  background-color: lightblue;
}

.brush .custom-brush-handle {
  display: none;
}
<head>
  <!-- favicon -->
  <link rel="shortcut icon" href="https://img.icons8.com/nolan/64/puzzle.png">
  <!-- bootstrap.css -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- bootstrap-theme.css -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- dc.css -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.8/dc.css">
  <!-- jquery.js -->
  <script src="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>
  <!-- bootstrap.js -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
  <!-- d3.v5.js -->
  <script src="https://d3js.org/d3.v5.js"></script>
  <!-- crossfilter.js -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.js"></script>
  <!-- dc.js -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dc/3.1.8/dc.js"></script>

  <title>Chart</title>
</head>

<body>
  <div id="composite"></div>
  <div class="container-fluid">
    <div class="row content">
      <div class="col-md-12">
        <div class="col-md-6">
          <table class="table" id="totaisVendedores">
            <thead>
              <tr>
                <th>Total Sales</th>
                <th>Current Year</th>
                <th>Last Year</th>
              </tr>
            </thead>
          </table>
        </div>
        <div class="col-md-6">
          <table class="table" id="totaisCities">
            <thead>
              <tr>
                <th>Total City</th>
                <th>Current Year</th>
                <th>Last Year</th>
              </tr>
            </thead>
          </table>
        </div>
        <div class="col-md-6">
          <table class="table" id="vendedores">
            <thead>
              <tr>
                <th>Sales</th>
                <th>Current Year</th>
                <th>Last Year</th>
              </tr>
            </thead>
          </table>
        </div>
        <div class="col-md-6">
          <table class="table" id="cities">
            <thead>
              <tr>
                <th>City</th>
                <th>Current Year</th>
                <th>Last Year</th>
              </tr>
            </thead>
          </table>
        </div>
      </div>
    </div>
  </div>
</body>


Solution

  • Sorry, I completely misread your last question. It was nice of you to accept the answer, but you really didn't have to do that. :)

    In order to add totals, you can add a tfoot section to your table

                <tfoot>
                  <tr>
                    <th>TOTAL</th>
                    <th id="total-current-year"></th>
                    <th id="total-last-year"></th>
                  </tr>
                </tfoot>
    

    and drive it with a NumberDisplay widget.

      const totalCYDisplay = dc.numberDisplay('#total-current-year')
        .group(totalAll)
        .valueAccessor(d => d.totalAno)
    

    total screenshot

    The default formatting is .2s but numberDisplay accepts any d3-format instance.

    If you don't like the animation, you can set .transitionDuration(0)

    var composite = dc.compositeChart('#composite');
    var vendedorTable = dc.dataTable("#vendedores");
    var citiesTable = dc.dataTable("#cities");
    
    function remove_empty_bins(source_group) {
      return {
        top: function(N) {
          return source_group.all().filter(function(d) {
            return d.value.totalAno > 1e-3 ||
              d.value.totalHomologo > 1e-3;
          }).slice(0, N);
        }
      };
    }
    var adjustX = 10,
      adjustY = 40;
    
    var url = 'https://gist.githubusercontent.com/bernalvinicius/3cece295bc37de1697e7f83418e7fcc9/raw/a5820379ec6eae76ee792495cc5dd1685c977a73/vendedores.json';
    d3.json(url).then(function(data) {
    
      data.forEach(d =>
        Object.assign(d, {
          mes: d.Month,
          atual: d.Vendas_Ano,
          passado: d.Vendas_Ant
        })
      );
      var cf = crossfilter(data);
    
      vendedorDim = cf.dimension(function(d) {
        return d.vendnm;
      });
      var vendedorGroup = vendedorDim.group().reduce(reduceAdd, reduceRemove, reduceInitial);
    
      citiesDim = cf.dimension(function(d) {
        return d.zona;
      });
      var citiesGroup = citiesDim.group().reduce(reduceAdd, reduceRemove, reduceInitial);
      const totalAll = cf.groupAll()
        .reduce(reduceAdd, reduceRemove, reduceInitial);
    debugger;
    
      var dim = cf.dimension(dc.pluck('mes')),
        grp1 = dim.group().reduceSum(dc.pluck('atual')),
        grp2 = dim.group().reduceSum(dc.pluck('passado'));
      var minMonth = dim.bottom(1)[0].mes;
      var maxMonth = dim.top(1)[0].mes;
    
      var all = cf.groupAll();
    
      dc.dataCount(".dc-data-count")
        .dimension(cf)
        .group(all);
    
      function reduceAdd(p, v) {
        p.totalAno += +v.Vendas_Ano;
        p.totalHomologo += +v.Vendas_Ant;
        return p;
      }
    
      function reduceRemove(p, v) {
        p.totalAno -= v.Vendas_Ano;
        p.totalHomologo -= v.Vendas_Ant;
        return p;
      }
    
      function reduceInitial() {
        return {
          totalAno: 0,
          totalHomologo: 0,
        };
      }
    
      // Fake Dimension
      rank = function(p) {
        return ""
      };
    
      // Chart by months
      composite
        .width(window.innerWidth - adjustX)
        .height(300)
        .x(d3.scaleLinear().domain([1, 12]))
        .yAxisLabel("")
        .xAxisLabel("Month")
        .legend(dc.legend().x(500).y(0).itemHeight(13).gap(5))
        .renderHorizontalGridLines(true)
        .compose([
          dc.lineChart(composite)
          .dimension(dim)
          .colors('steelblue')
          .group(grp1, "Currently Year"),
          dc.lineChart(composite)
          .dimension(dim)
          .colors('darkorange')
          .group(grp2, "Last Year")
        ])
        .brushOn(true);
    
      composite.brush().extent([-0.5, data.length + 1.5])
      composite.extendBrush = function(brushSelection) {
        if (brushSelection) {
          vendedorTable.filter(null);
          vendedorDim.filter(null);
          citiesTable.filter(null);
          citiesDim.filter(null);
          const point = Math.round((brushSelection[0] + brushSelection[1]) / 2);
          return [
            point - 0.5,
            point + 0.5
          ];
        }
      };
    
      // Sales Table
      vendedorTable.width(500)
        .height(480)
        .dimension(remove_empty_bins(vendedorGroup))
        .group(rank)
        .columns([function(d) {
            return d.key;
          },
          function(d) {
            return Number(Math.round(d.value.totalAno * 100) / 100).toLocaleString("es-ES", {
              minimumFractionDigits: 2
            }) + '€';
          },
          function(d) {
            return Number(Math.round(d.value.totalHomologo * 100) / 100).toLocaleString("es-ES", {
              minimumFractionDigits: 2
            }) + '€';
          }
        ])
        .sortBy(function(d) {
          return d.value.totalAno
        })
        .order(d3.descending)
    
      // vendedors totals
      const totalCYDisplay = dc.numberDisplay('#total-current-year')
        .group(totalAll)
        .valueAccessor(d => d.totalAno)
      const totalLYDisplay = dc.numberDisplay('#total-last-year')
        .group(totalAll)
        .valueAccessor(d => d.totalHomologo)
    
      // Cities Table
      citiesTable
        .width(500)
        .height(480)
        .dimension(remove_empty_bins(citiesGroup))
        .group(rank)
        .columns([function(d) {
            return d.key;
          },
          function(d) {
            return Number(Math.round(d.value.totalAno * 100) / 100).toLocaleString("es-ES", {
              minimumFractionDigits: 2
            }) + '€';
          },
          function(d) {
            return Number(Math.round(d.value.totalHomologo * 100) / 100).toLocaleString("es-ES", {
              minimumFractionDigits: 2
            }) + '€';
          }
        ])
        .controlsUseVisibility(true)
        .sortBy(function(d) {
          return d.value.totalAno
        })
        .order(d3.descending)
    
      // Sales click events
      vendedorTable.on('pretransition', function(table) {
        table.selectAll('td.dc-table-column')
          .on('click', function(d) {
            let filters = table.filters().slice();
            if (filters.indexOf(d.key) === -1)
              filters.push(d.key);
            else
              filters = filters.filter(k => k != d.key);
            if (filters.length === 0)
              vendedorDim.filter(null);
            else
              vendedorDim.filterFunction(function(d) {
                return filters.indexOf(d) !== -1;
              })
            table.replaceFilter([filters]);
    
            citiesTable.filter(null);
            citiesDim.filter(null);
            composite.filter(null);
    
            dc.redrawAll();
          });
        let filters = table.filters();
        table.selectAll('tr.dc-table-row')
          .classed('sel-rows', d => filters.indexOf(d.key) !== -1);
      });
    
      // Cities click events
      citiesTable.on('pretransition', function(table) {
        table.selectAll('td.dc-table-column')
          .on('click', function(d) {
            let filters = table.filters().slice();
            if (filters.indexOf(d.key) === -1)
              filters.push(d.key);
            else
              filters = filters.filter(k => k != d.key);
            if (filters.length === 0)
              citiesDim.filter(null);
            else
              citiesDim.filterFunction(function(d) {
                return filters.indexOf(d) !== -1;
              })
            table.replaceFilter([filters]);
    
            vendedorTable.filter(null);
            vendedorDim.filter(null);
            composite.filter(null);
    
            dc.redrawAll();
          });
        let filters = table.filters();
        table.selectAll('tr.dc-table-row')
          .classed('sel-rows', d => filters.indexOf(d.key) !== -1);
      });
    
    
      dc.renderAll();
    
      // reset functions
      $('#reset').on('click', function() {
        vendedorTable.filter(null);
        vendedorDim.filter(null);
        citiesTable.filter(null);
        citiesDim.filter(null);
        composite.filter(null);
    
        dc.redrawAll();
      });
    
      $('#resetTable').on('click', function() {
        vendedorTable.filter(null);
        vendedorDim.filter(null);
        citiesTable.filter(null);
        citiesDim.filter(null);
        composite.filter(null);
    
        dc.redrawAll();
      });
    
      $('#resetTable2').on('click', function() {
        vendedorTable.filter(null);
        vendedorDim.filter(null);
        citiesTable.filter(null);
        citiesDim.filter(null);
        composite.filter(null);
    
        dc.redrawAll();
      });
    
    
      /****************************************************************************/
      // Functions to handle responsive
      apply_resizing(composite, adjustX, adjustY, function(composite) {
        composite.legend().x(window.innerWidth - 200);
      });
    
      var find_query = function() {
        var _map = window.location.search.substr(1).split('&').map(function(a) {
          return a.split('=');
        }).reduce(function(p, v) {
          if (v.length > 1)
            p[v[0]] = decodeURIComponent(v[1].replace(/\+/g, " "));
          else
            p[v[0]] = true;
          return p;
        }, {});
        return function(field) {
          return _map[field] || null;
        };
      }();
    
      var resizeMode = find_query('resize') || 'widhei';
    
      function apply_resizing(composite, adjustX, adjustY, onresize) {
        if (resizeMode === 'viewbox') {
          composite
            .width(300)
            .height(200)
            .useViewBoxResizing(true);
          d3.select(composite.anchor()).classed('fullsize', false);
        } else {
          adjustX = adjustX || 0;
          adjustY = adjustY || adjustX || 0;
          composite
            .width(window.innerWidth - adjustX)
            .height(300);
          window.onresize = function() {
            if (onresize) {
              onresize(composite);
            }
            composite
              .width(window.innerWidth - adjustX)
              .height(300);
    
            if (composite.rescale) {
              composite.rescale();
            }
            composite.redraw();
          };
        }
      }
    });
    #composite {
      padding: 10px;
    }
    
    .dc-table-group {
      visibility: collapse;
    }
    
    tr.dc-table-row.sel-rows {
      background-color: lightblue;
    }
    
    .brush .custom-brush-handle {
      display: none;
    }
    <head>
      <!-- favicon -->
      <link rel="shortcut icon" href="https://img.icons8.com/nolan/64/puzzle.png">
      <!-- bootstrap.css -->
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
      <!-- bootstrap-theme.css -->
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
      <!-- dc.css -->
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.8/dc.css">
      <!-- jquery.js -->
      <script src="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>
      <!-- bootstrap.js -->
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
      <!-- d3.v5.js -->
      <script src="https://d3js.org/d3.v5.js"></script>
      <!-- crossfilter.js -->
      <script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.js"></script>
      <!-- dc.js -->
      <script src="https://cdnjs.cloudflare.com/ajax/libs/dc/3.1.8/dc.js"></script>
    
      <title>Chart</title>
    </head>
    
    <body>
      <div id="composite"></div>
      <div class="container-fluid">
        <div class="row content">
          <div class="col-md-12">
            <div class="col-md-6">
              <table class="table" id="totaisVendedores">
                <thead>
                  <tr>
                    <th>Total Sales</th>
                    <th>Current Year</th>
                    <th>Last Year</th>
                  </tr>
                </thead>
              </table>
            </div>
            <div class="col-md-6">
              <table class="table" id="totaisCities">
                <thead>
                  <tr>
                    <th>Total City</th>
                    <th>Current Year</th>
                    <th>Last Year</th>
                  </tr>
                </thead>
              </table>
            </div>
            <div class="col-md-6">
              <table class="table" id="vendedores">
                <thead>
                  <tr>
                    <th>Sales</th>
                    <th>Current Year</th>
                    <th>Last Year</th>
                  </tr>
                </thead>
                <tfoot>
                  <tr>
                    <th>TOTAL</th>
                    <th id="total-current-year"></th>
                    <th id="total-last-year"></th>
                  </tr>
                </tfoot>
              </table>
            </div>
            <div class="col-md-6">
              <table class="table" id="cities">
                <thead>
                  <tr>
                    <th>City</th>
                    <th>Current Year</th>
                    <th>Last Year</th>
                  </tr>
                </thead>
              </table>
            </div>
          </div>
        </div>
      </div>
    </body>