Series and Frames

Work with series and frames to access and manipulate data.

Series and Frames are the two fundamental structures used for working with channel data in Synnax. This guide will walk you through how to use the Frame and Series classes exposed by the TypeScript client.

Series

A series is a strongly typed array of samples. A series can contain any data you want it to, but in the context of Synnax it almost always represents a set of contiguous samples from a single channel.

Constructing a Series

Here are a few simple ways to construct a series:

import { Series } from "@synnaxlabs/client";

// Construct a series from an array of numbers. In this case, the series will 
// automatically be of type float64.
const series = new Series([1, 2, 3, 4, 5]);

// Construct a series from an array of numbers, but this time we specify the type
// explicitly.
const series = new Series({ data: [1, 2, 3, 4, 5], dataType: "float32" });

// Construct a series from an array of strings. In this case, the series will
// automatically be of type string.
const series = new Series(["apple", "banana", "cherry"]);

// Construct a series from a Float32Array. This is the most efficient way to
// construct a series from a large amount of data.
const series = new Series(new Float32Array([1, 2, 3, 4, 5]));

// Construct a series from a JSON object. This is useful when you have a series
// that has been serialized to JSON.
const series = new Series([
  { red: "cherry" }, 
  { yellow: "banana" }, 
  {orange: "orange" }
]);

The values passed to a series cannot have different data types. The constructor will not throw an error, as validating data types is an expensive operation, but the series will not behave as expected:

const series = new Series([1, "a", 3, "b", 5]);

Accessing Data

The at method

The easiest way to access data from a series is to use the at method. This method behaves the same as the at method on a Javascript array, and allows for negative indexing:

const series = new Series([1, 2, 3, 4, 5]);

console.log(series.at(0)); // 1
console.log(series.at(-1)); // 5

The as method

An important caveat of at is that its return type will be a Synnax TelemValue type, which is a union of all the possible data types that a series can contain:

import { TelemValue } from "@synnaxlabs/client";

const series = new Series([1, 2, 3, 4, 5]);
// Is it a number? Is it a string? Who knows?
const v: TelemValue = series.at(0);

This can make it difficult to write type-safe code when working with a series. If you know that a series should be of a certain javascript type (number, string, object, bigint), you can use the as method to validate the data type and return a Series<T> of that type:

const series = new Series([1, 2, 3, 4, 5]);
const easierSeries: Series<number> = series.as("number");
// Now we have a guarantee that this is a series of numbers.
const v: number = easierSeries.at(0);

Accessing a TypedArray

You use the data attribute to get the a TypedArray representation of the series:

const series = new Series({ data: [1, 2, 3, 4, 5], dataType: "int8" });
const ta = series.data;
console.log(ta); // Int8Array [ 1, 2, 3, 4, 5 ]

Note that while this method will work for string and json series, the returned typed array will be a Uint8Array representing the encoded data.

Converting to a Javascript Array

Series implement the Iterable interface, so you can use the spread operator to convert a series to a Javascript array, or use the Array.from method:

const series = new Series([1, 2, 3, 4, 5]);
const jsArray = [...series];
console.log(jsArray); // [ 1, 2, 3, 4, 5 ]
const jsArray2 = Array.from(series);
console.log(jsArray2); // [ 1, 2, 3, 4, 5 ]

This method will also work for json and string encoded series:

const series = new Series([{ red: "cherry", yellow: "banana", orange: "orange" }]);
const jsArray = [...series];
console.log(jsArray); // [ { red: 'cherry', yellow: 'banana', orange: 'orange' } ]

The Time Range Property

Whenever you read a series from Synnax, it will have a timeRange property that represents the time range occupied by the samples in the Series. This method can be useful for getting a high-level understanding of when the samples were recorded without needing to query an index channel.

The start field represents the timestamp for the first sample, and the end field represents a timestamp just after the last sample (start-inclusive, end-exclusive).

We can also define a time range when constructing the series:

import { TimeRange, TimeStamp, TimeSpan } from "@synnaxlabs/client";

const start = TimeStamp.now();

const series = new Series({
  data: [1, 2, 3, 4, 5],
  dataType: "float64",
  timeRange: new TimeRange({ start, end: start.add(TimeSpan.seconds(6)) })
});

Other Useful Properties

Length

The length property returns the number of samples in the series:

const series = new Series([1, 2, 3, 4, 5]);
console.log(series.length); // 5

const stringSeries = new Series(["apple", "banana", "cherry"]);
console.log(stringSeries.length); // 3

Data type

The dataType property returns the data type of the series:

import { DataType } from "@synnaxlabs/client";

const series = new Series([1, 2, 3, 4, 5]);
console.log(series.dataType.toString()); // "float64"
console.log(series.dataType.equals(DataType.STRING)); // true

Max, min, and bounds

The max, min, and bounds properties are intelligently cached properties that return the maximum, minimum, or both values of the series. These properties only work with numeric series:

const series = new Series([1, 2, 3, 4, 5]);
console.log(series.max); // 5
console.log(series.min); // 1
console.log(series.bounds); // { lower: 1, upper: 5 }

Frames

A frame is a collection of series from multiple channels. Frames are returned by

  • The read method of the Synnax data client (client.read)
  • The read method of a Streamer instance (client.openStreamer)
  • The value property of an Iterator instance (client.openIterator)

Constructing a Frame

A frame maps the key or name of a channel to one or more series. Here are a few examples of how to construct a frame:

import { Frame } from "@synnaxlabs/client";

// Construct a frame for the given channel names.
const frame = new Frame({
  "channel1": new Series([1, 2, 3, 4, 5]),
  "channel2": new Series([5, 4, 3, 2, 1]),
  "channel3": new Series([1, 1, 1, 1, 1]),
});

// Construct a frame for the given channel keys
const frame = new Frame({
  1: new Series([1, 2, 3, 4, 5]),
  2: new Series([5, 4, 3, 2, 1]),
  // Notice that series do not need to be the same length.
  3: new Series([1, 1, 1]),
});

// Construct a frame from a map
const frame = new Frame(new Map([
  ["channel1", new Series([1, 2, 3, 4, 5])],
  ["channel2", new Series([5, 4, 3, 2, 1])],
  ["channel3", new Series([1, 1, 1, 1, 1])],
]));

// Or from an array of keys and series
const frame = new Frame(["channel1", "channel2", "channel3"], [
  new Series([1, 2, 3, 4, 5]),
  new Series([5, 4, 3, 2, 1]),
  new Series([1, 1, 1, 1, 1]),
]);

// Or construct a frame with multiple series for a single channel
const frame = new Frame({
  "channel1": [
    new Series([1, 2, 3, 4, 5]),
    new Series([5, 4, 3, 2, 1]),
    new Series([1, 1, 1, 1, 1]),
  ],
  "channel2": [
    new Series([1, 2, 3, 4, 5]),
  ],
});

Accessing Frame Data

The get method

The easiest way to access data from a frame is to use the get method. This method return an instance of a MultiSeries class, which is a special type of series that wraps multiple Series instances, but behaves much like a Series instance:

import { MultiSeries } from "@synnaxlabs/client";

const frame = new Frame({
  "channel1": [new Series([1, 2]), new Series([3, 4, 5])],
  "channel2": new Series([5, 4, 3, 2, 1]),
  "channel3": new Series([1, 1, 1, 1, 1]),
});

const multiSeries: MultiSeries = frame.get("channel1");
// Access a value
console.log(multiSeries.at(0)); // 1

// Access a value from a specific series
console.log(multiSeries.series[0].at(0)); // 1

// Construct a Javascript array from the MultiSeries
const jsArray = [...multiSeries];
console.log(jsArray); // [ 1, 2, 3, 4, 5 ]

The at method

The at method can be used to access a JavaScript object containing a single value for each channel in the frame:

const frame = new Frame({
  "channel1": new Series([1, 2, 3, 4, 5]),
  "channel2": new Series([5, 4, 3, 2, 1]),
  "channel3": new Series([1, 1]),
});

const obj = frame.at(3);
console.log(obj); // { channel1: 1, channel2: 5, channel3: undefined }

If you set the required parameter to true, the method will throw an error if any of the channels are missing:

const frame = new Frame({
  "channel1": new Series([1, 2, 3, 4, 5]),
  "channel2": new Series([5, 4, 3, 2, 1]),
  "channel3": new Series([1, 1]),
});

const obj = frame.at(3, true); // Throws an error