docs: protected final DataTable buildAggregationReport(F form) throws TypeMismatchException {
          // Pull data out of form for per-group fetching
          final AggregationInterval interval = form.getInterval();
          final DateMidnight start = form.getStart();
          final DateMidnight end = form.getEnd();
  
          final DateTime startDateTime = start.toDateTime();
          // Use a query end of the end date at 23:59:59
          final DateTime endDateTime = end.plusDays(1).toDateTime().minusSeconds(1);
  
          // Get the list of DateTimes used on the X axis in the report
          final List<DateTime> reportTimes =
                  this.intervalHelper.getIntervalStartDateTimesBetween(
                          interval, startDateTime, endDateTime, maxIntervals);
  
          final Map<D, SortedSet<T>> groupedAggregations = createColumnDiscriminatorMap(form);
  
          // Determine the ValueType of the date/time column. Use the most specific column type
          // possible
          final ValueType dateTimeColumnType;
          if (interval.isHasTimePart()) {
              // If start/end are the same day just display the time
              if (startDateTime.toDateMidnight().equals(endDateTime.toDateMidnight())) {
                  dateTimeColumnType = ValueType.TIMEOFDAY;
              }
              // interval has time data and start/end are on different days, show full date time
              else {
                  dateTimeColumnType = ValueType.DATETIME;
              }
          }
          // interval is date only
          else {
              dateTimeColumnType = ValueType.DATE;
          }
  
          // Setup the date/time column description
          final ColumnDescription dateTimeColumn;
          switch (dateTimeColumnType) {
              case TIMEOFDAY:
                  {
                      dateTimeColumn = new ColumnDescription("time", dateTimeColumnType, "Time");
                      break;
                  }
              default:
                  {
                      dateTimeColumn = new ColumnDescription("date", dateTimeColumnType, "Date");
                  }
          }
  
          final DataTable table = new JsonDataTable();
          table.addColumn(dateTimeColumn);
  
          // Setup columns in the DataTable
          final Set<D> columnGroups = groupedAggregations.keySet();
          for (final D columnMapping : columnGroups) {
              final Collection<ColumnDescription> columnDescriptions =
                      this.getColumnDescriptions(columnMapping, form);
              table.addColumns(columnDescriptions);
          }
  
          // Query for all aggregation data in the time range for all groups.  Only the
          // interval and discriminator data is used from the keys.
          final Set<K> keys = createAggregationsQueryKeyset(columnGroups, form);
          final BaseAggregationDao<T, K> baseAggregationDao = this.getBaseAggregationDao();
          final Collection<T> aggregations =
                  baseAggregationDao.getAggregations(
                          startDateTime, endDateTime, keys, extractGroupsArray(columnGroups));
  
          // Organize the results by group and sort them chronologically by adding them to the sorted
          // set
          for (final T aggregation : aggregations) {
              final D discriminator = aggregation.getAggregationDiscriminator();
              final SortedSet<T> results = groupedAggregations.get(discriminator);
              results.add(aggregation);
          }
  
          // Build Map from discriminator column mapping to result iterator to allow putting results
          // into
          // the correct column AND the correct time slot in the column
          Comparator<? super D> comparator = getDiscriminatorComparator();
          final Map<D, PeekingIterator<T>> groupedAggregationIterators =
                  new TreeMap<D, PeekingIterator<T>>((comparator));
          for (final Entry<D, SortedSet<T>> groupedAggregationEntry :
                  groupedAggregations.entrySet()) {
              groupedAggregationIterators.put(
                      groupedAggregationEntry.getKey(),
                      Iterators.peekingIterator(groupedAggregationEntry.getValue().iterator()));
          }
  
          /*
           * populate the data, filling in blank spots. The full list of interval DateTimes is used to create every row in the
           * query range. Then the iterator
           */
          for (final DateTime rowTime : reportTimes) {
              // create the row
              final TableRow row = new TableRow();
  
              // add the date to the first cell
              final Value dateTimeValue;
              switch (dateTimeColumnType) {
                  case DATE:
                      {
                          dateTimeValue =
                                  new DateValue(
                                          rowTime.getYear(),
                                          rowTime.getMonthOfYear() - 1,
                                          rowTime.getDayOfMonth());
                          break;
                      }
                  case TIMEOFDAY:
                      {
                          dateTimeValue =
                                  new TimeOfDayValue(
                                          rowTime.getHourOfDay(), rowTime.getMinuteOfHour(), 0);
                          break;
                      }
                  default:
                      {
                          dateTimeValue =
                                  new DateTimeValue(
                                          rowTime.getYear(),
                                          rowTime.getMonthOfYear() - 1,
                                          rowTime.getDayOfMonth(),
                                          rowTime.getHourOfDay(),
                                          rowTime.getMinuteOfHour(),
                                          0,
                                          0);
                          break;
                      }
              }
              row.addCell(new TableCell(dateTimeValue));
  
              for (final PeekingIterator<T> groupedAggregationIteratorEntry :
                      groupedAggregationIterators.values()) {
                  List<Value> values = null;
  
                  if (groupedAggregationIteratorEntry.hasNext()) {
                      final T aggr = groupedAggregationIteratorEntry.peek();
                      if (rowTime.equals(aggr.getDateTime())) {
                          // Data is for the correct time slot, advance the iterator
                          groupedAggregationIteratorEntry.next();
  
                          values = createRowValues(aggr, form);
                      }
                  }
  
                  // Gap in the data, fill it in using a null aggregation
                  if (values == null) {
                      values = createRowValues(null, form);
                  }
  
                  // Add the values to the row
                  for (final Value value : values) {
                      row.addCell(value);
                  }
              }
  
              table.addRow(row);
          }
  
          return table;
      }