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 aStreamer
instance (client.openStreamer
) - The
value
property of anIterator
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