static getSPIGraph (req, res) {
    req.query.facets = 1;

    // req.query.exp -> req.query.field by viewer.js:expToField

    if (req.query.field !== undefined && !ArkimeUtil.isString(req.query.field, 0)) {
      return res.serverError(403, `Bad 'field' parameter`);
    }

    SessionAPIs.buildSessionQuery(req, (bsqErr, query, indices) => {
      const results = { items: [], graph: {}, map: {} };
      if (bsqErr) {
        return res.serverError(403, bsqErr.toString());
      }

      let options = {};
      if (req.query.cancelId) {
        options.cancelId = `${req.user.userId}::${req.query.cancelId}`;
      }
      options = ViewerUtils.addCluster(req.query.cluster, options);

      delete query.sort;
      query.size = 0;
      const size = +req.query.size || 20;

      let field = req.query.field || 'node';

      if (req.query.exp === 'ip.dst:port') { field = 'ip.dst:port'; }

      if (field === 'ip.dst:port') {
        query.aggregations.field = { terms: { field: 'destination.ip', size }, aggregations: { sub: { terms: { field: 'destination.port', size } } } };
      } else if (field === 'fileand') {
        query.aggregations.field = { terms: { field: 'node', size: 1000 }, aggregations: { sub: { terms: { field: 'fileId', size } } } };
      } else {
        query.aggregations.field = { terms: { field, size: size * 2 } };
      }

      Promise.all([
        Db.numberOfDocuments(Db.getSessionIndices(), options.cluster ? { cluster: options.cluster } : {}),
        Db.searchSessions(indices, query, options)
      ]).then(([total, result]) => {
        if (result.error) { throw result.error; }

        results.recordsTotal = total.count;
        results.recordsFiltered = result.hits.total;

        results.graph = ViewerUtils.graphMerge(req, query, result.aggregations);
        results.map = ViewerUtils.mapMerge(result.aggregations);

        if (!result.aggregations) {
          result.aggregations = { field: { buckets: [] } };
        }

        const aggs = result.aggregations.field.buckets;
        const filter = { term: {} };
        const sfilter = { term: {} };
        query.query.bool.filter.push(filter);

        if (field === 'ip.dst:port') {
          query.query.bool.filter.push(sfilter);
        }

        delete query.aggregations.field;

        let queriesInfo = [];
        async function endCb () {
          queriesInfo = queriesInfo.sort((a, b) => { return b.doc_count - a.doc_count; }).slice(0, size * 2);
          const queries = queriesInfo.map((item) => { return item.query; });

          try {
            const { body: searchResult } = await Db.msearchSessions(indices, queries, options);

            searchResult.responses.forEach((item, i) => {
              const response = {
                name: queriesInfo[i].key,
                count: queriesInfo[i].doc_count
              };

              response.graph = ViewerUtils.graphMerge(req, query, searchResult.responses[i].aggregations);

              const histoKeys = Object.keys(results.graph).filter(j => j.toLowerCase().includes('histo'));
              const xMinName = histoKeys.reduce((prev, curr) => results.graph[prev][0][0] < results.graph[curr][0][0] ? prev : curr);
              const histoXMin = results.graph[xMinName][0][0];
              const xMaxName = histoKeys.reduce((prev, curr) => {
                return results.graph[prev][results.graph[prev].length - 1][0] > results.graph[curr][results.graph[curr].length - 1][0] ? prev : curr;
              });
              const histoXMax = results.graph[xMaxName][results.graph[xMaxName].length - 1][0];

              if (response.graph.xmin === null) {
                response.graph.xmin = results.graph.xmin || histoXMin;
              }

              if (response.graph.xmax === null) {
                response.graph.xmax = results.graph.xmax || histoXMax;
              }

              response.map = ViewerUtils.mapMerge(searchResult.responses[i].aggregations);

              results.items.push(response);
              histoKeys.forEach(key => {
                response[key] = 0.0;
              });

              const graph = response.graph;
              for (let j = 0; j < histoKeys.length; j++) {
                item = histoKeys[j];
                for (let k = 0; k < graph[item].length; k++) {
                  response[item] += graph[item][k][1];
                }
              }

              if (graph['network.packetsTotal'] !== undefined) {
                response['network.packetsHisto'] = graph['network.packetsTotal'];
              }
              if (graph.totDataBytesTotal !== undefined) {
                response.totDataBytesHisto = graph.totDataBytesTotal;
              }
              if (graph['network.bytesTotal'] !== undefined) {
                response['network.bytesHisto'] = graph['network.bytesTotal'];
              }

              if (results.items.length === searchResult.responses.length) {
                const s = req.query.sort || 'sessionsHisto';
                results.items = results.items.sort((a, b) => {
                  let sortResult;
                  if (s === 'name') {
                    sortResult = a.name.localeCompare(b.name);
                  } else {
                    sortResult = b[s] - a[s];
                  }
                  return sortResult;
                }).slice(0, size);
                return res.send(results);
              }
            });
          } catch (err) {
            console.log(`ERROR - ${req.method} /api/spigraph`, util.inspect(err, false, 50));
            return res.send(results);
          }
        }

        const intermediateResults = [];
        function findFileNames () {
          async.each(intermediateResults, async (fsitem) => {
            const split = fsitem.key.split(':');
            const node = split[0];
            const fileId = split[1];
            try {
              const file = await Db.fileIdToFile(node, fileId);
              if (file && file.name) {
                queriesInfo.push({ key: file.name, doc_count: fsitem.doc_count, query: fsitem.query });
              }
            } catch (err) {
              // Ignore error
            }
            return;
          }, () => {
            endCb();
          });
        }

        aggs.forEach((item) => {
          if (field === 'ip.dst:port') {
            filter.term['destination.ip'] = item.key;
            const sep = (item.key.indexOf(':') === -1) ? ':' : '.';
            item.sub.buckets.forEach((sitem) => {
              sfilter.term['destination.port'] = sitem.key;
              queriesInfo.push({ key: item.key + sep + sitem.key, doc_count: sitem.doc_count, query: JSON.stringify(query) });
            });
          } else if (field === 'fileand') {
            filter.term.node = item.key;
            item.sub.buckets.forEach((sitem) => {
              sfilter.term.fileand = sitem.key;
              intermediateResults.push({ key: filter.term.node + ':' + sitem.key, doc_count: sitem.doc_count, query: JSON.stringify(query) });
            });
          } else {
            filter.term[field] = item.key;
            queriesInfo.push({ key: item.key, doc_count: item.doc_count, query: JSON.stringify(query) });
          }
        });

        if (field === 'fileand') { return findFileNames(); }

        return endCb();
      }).catch((err) => {
        console.log(`ERROR - ${req.method} /api/spigraph`, util.inspect(err, false, 50));
        return res.serverError(403, ViewerUtils.errorString(err));
      });
    });
  }
POST/GET - /api/spigraph

Builds an elasticsearch session query. Gets a list of values for a field with counts and graph data and returns them to the client.
@name /spigraph
@param {SessionsQuery} See_List - This API supports a common set of parameters documented in the SessionsQuery section
@param {string} exp - The expression field to return data for. Either exp or field is required, field is given priority if both are present.
@param {string} field=node - The database field to return data for. Either exp or field is required, field is given priority if both are present.
@returns {object} map - The data to populate the main/aggregate spigraph sessions map
@returns {object} graph - The data to populate the main/aggregate spigraph sessions timeline graph
@returns {array} items - The list of field values with their corresponding timeline graph and map data
@returns {number} recordsTotal - The total number of sessions Arkime knows about
@returns {number} recordsFiltered - The number of sessions matching query