TinyBase logoTinyBase

Advanced Metric Definition

This guide describes how the metrics module let you create more complex types of metrics based on the data in Store objects.

Advanced Aggregations

The four standard aggregation types you can use when defining a Metric are sum, avg, min, and max, but often you will want to create more interesting aggregations of the data in your Table.

Instead of a string as the third parameter of the setMetricDefinition method, you can provide an Aggregate parameter. This is simply a function that takes an array of numbers and returns the aggregation. So for example, you could create a Metric which is the hypotenuse of the numeric distance Cell values in every Row:

import {createMetrics, createStore} from 'tinybase';

const store = createStore().setTable('dimensions', {
  x: {distance: 1},
  y: {distance: 2},
  z: {distance: 2},
});

const metrics = createMetrics(store);
metrics.setMetricDefinition(
  'hypotenuse', //                            metricId
  'dimensions', //                            tableId to aggregate
  (distances) => Math.hypot(...distances), // custom aggregation
  'distance', //                              cellId to aggregate
);

console.log(metrics.getMetric('hypotenuse'));
// -> 3

Such a Metric will have a performance complexity linear with the size of the Table, any time any value changes. But some aggregations have shortcuts: if a contributing value is updated, you can sometimes calculate the new value for the metric without scanning every value again. For example, if your aggregation is a sum, and an additional Row is added, its value can simply be added to the previous total.

There are three types of shortcuts you can add if the aggregation can benefit from them, and they can be provided as the final three parameters of the setMetricDefinition method. These describe how to change the overall Metric when a number is added, removed, or replaced.

Our hypotenuse example can benefit. If a new value is added to the list of numbers to be aggregated, square the current result, add the square of the new number, and square root the total. This is then constant cost, regardless of the number of Row objects being aggregated:

const sqr = (num) => num * num;
const sqrt = Math.sqrt;
metrics.setMetricDefinition(
  'fasterHypotenuse', //                      metricId
  'dimensions', //                            tableId to aggregate
  (distances) => Math.hypot(...distances), // custom aggregation
  'distance', //                              cellId to aggregate
  (metric, add) => sqr(sqr(metric) + sqr(add)), //                 add
  (metric, rem) => sqr(sqr(metric) - sqr(rem)), //                 remove
  (metric, add, rem) => sqr(sqr(metric) + sqr(add) - sqr(rem)), // replace
);

store.setCell('dimensions', 'x', 'distance', 3);
store.setCell('dimensions', 'z', 'distance', 6);
console.log(metrics.getMetric('hypotenuse'));
// -> 7

Getting Custom Values From Rows

By default, our Metric definitions have named a Cell in the Row which contains a numeric value - like the distance Cell in the example above. Sometimes you may wish to derive a number for each Row that is not in a single Cell, and in this case you can replace the fourth parameter with a function which can process the Row in any way you wish.

In this example, we average the density of a set of cuboids, which means that each Row's contributory number needs to be the mass divided by the volume:

store.setTable('cuboids', {
  0: {mass: 10, volume: 5},
  1: {mass: 12, volume: 3},
  2: {mass: 24, volume: 4},
});

metrics.setMetricDefinition(
  'averageDensity', //                                 metricId
  'cuboids', //                                        tableId to aggregate
  'avg', //                                            aggregation
  (getCell) => getCell('mass') / getCell('volume'), // => number to aggregate
);

console.log(metrics.getMetric('averageDensity'));
// -> 4

Of course it is possible to combine both advanced aggregations and getting custom values from each Row:

metrics.setMetricDefinition(
  'countOfDenseCuboids',
  'cuboids',
  (densities) => densities.filter((density) => density > 5).length,
  (getCell) => getCell('mass') / getCell('volume'),
);

console.log(metrics.getMetric('countOfDenseCuboids'));
// -> 1

And with that, we have covered most of the basics of using the metrics module. Let's move on to a very similar module for indexing data, as covered in the Using Indexes guide.