# Series and frames

> Create series and frames, get values out of them, and use them with NumPy, pandas, and typed arrays.

A series holds the samples of one [channel](https://docs.synnaxlabs.com/reference/channels) as a typed array. A frame holds the series of one or more channels, keyed by channel name or key. Reads and streamers return series and frames, and writers accept them.

## Series

All samples in a series have one data type, such as `float32` or `timestamp`.

### Create a series

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

```python
import numpy as np
import synnax as sy

# The first value sets the data type: float64.
series = sy.Series([20.0, 20.5, 21.0])

# Set the data type yourself.
series = sy.Series([20.0, 20.5, 21.0], data_type=sy.DataType.FLOAT32)

# A NumPy array keeps its dtype: int16.
series = sy.Series(np.array([1, 2, 3], dtype=np.int16))

# Strings make a string series, and dictionaries make a json series.
series = sy.Series(["open", "closed"])
series = sy.Series([{"valve": "open"}, {"valve": "closed"}])
```

The first value of a list sets the data type. `sy.Series([20, 20.5])` is an `int64` series that holds `[20, 20]`. Write `20.0`, or set `data_type`. A list that starts with a number and holds a string raises a `ValueError`.

**TypeScript**

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

// Numbers make a float64 series.
const pressures = new Series([20.0, 20.5, 21.0]);

// Set the data type yourself.
const float32s = new Series({ data: [20.0, 20.5, 21.0], dataType: "float32" });

// A typed array keeps its type. This is the fastest way to build a large series.
const int16s = new Series(new Int16Array([1, 2, 3]));

// Strings make a string series, and objects make a json series.
const states = new Series(["open", "closed"]);
const events = new Series([{ valve: "open" }, { valve: "closed" }]);
```

All values must have the same type. The constructor does not check: a string in a number series becomes `NaN`.

### Use a series as an array

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

A series works with NumPy, so you can pass it to NumPy, pandas, or Matplotlib with no conversion:

```python
import matplotlib.pyplot as plt
import numpy as np
import synnax as sy

series = sy.Series([20.0, 20.5, 21.0])
print(np.mean(series))  # 20.5
array = series.to_numpy()
plt.plot(series)
```

**TypeScript**

A series is iterable. The `data` property returns the samples of a numeric series as a typed array, such as a `Float64Array`:

```typescript
const series = new Series([20.0, 20.5, 21.0]);
console.log([...series]); // [ 20, 20.5, 21 ]
console.log(series.data); // Float64Array(3) [ 20, 20.5, 21 ]
```

### Get a sample

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

```python
import synnax as sy

series = sy.Series([20.0, 20.5, 21.0])
print(series[0])  # 20.0
print(series[-1])  # 21.0
```

**TypeScript**

```typescript
const series = new Series([20.0, 20.5, 21.0]);
console.log(series.at(0)); // 20
console.log(series.at(-1)); // 21
console.log(series.at(5)); // undefined
series.at(5, true); // throws, because the index is out of range
```

`at` returns `undefined` when the index is outside the series. Pass `true` as the second argument to throw instead.

### Series properties

| Python              | TypeScript         | Gives you                                                       |
| ------------------- | ------------------ | --------------------------------------------------------------- |
| `len(series)`       | `series.length`    | The number of samples.                                          |
| `series.data_type`  | `series.dataType`  | The data type, such as `float32`.                               |
| `series.time_range` | `series.timeRange` | The time range of the samples, on a series that a read returns. |
| `np.max(series)`    | `series.max`       | The largest sample.                                             |
| `np.min(series)`    | `series.min`       | The smallest sample.                                            |
|                     | `series.bounds`    | The smallest and largest samples, as `{ lower, upper }`.        |

The time range of a series starts at its first sample and ends just after its last sample.

## Frames

A frame holds the series of one or more channels. Each channel can have more than one series. Here, a frame holds the series of an index channel and a data channel:

| `tank_time`             | `tank_pressure` |
| ----------------------- | --------------- |
| 2026-10-03 12:00:00.000 | 20.0            |
| 2026-10-03 12:00:00.100 | 20.5            |
| 2026-10-03 12:00:00.200 | 21.0            |

These calls return a frame:

* `client.read` with more than one channel. With one channel name or key, it returns a `MultiSeries`.
* `read` on a streamer.
* `value` on an iterator.

### Create a frame

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

```python
import pandas as pd
import synnax as sy

# Key the series by channel name, or by channel key.
frame = sy.Frame({
    "tank_pressure": sy.Series([20.0, 20.5, 21.0]),
    "tank_temperature": sy.Series([290.0, 290.2, 290.4]),
})

# One sample for each channel.
frame = sy.Frame({"tank_pressure": 20.0, "tank_temperature": 290.0})

# Many series for one channel: a list of channels and a list of series.
frame = sy.Frame(
    ["tank_pressure", "tank_pressure"],
    [sy.Series([20.0, 20.5]), sy.Series([21.0, 21.5])],
)

# Each column of a DataFrame becomes a series.
frame = sy.Frame(pd.DataFrame({"tank_pressure": [20.0, 20.5, 21.0]}))
```

**TypeScript**

```typescript
import { Frame, Series } from "@synnaxlabs/client";

// Key the series by channel name, or by channel key.
const frame = new Frame({
  tank_pressure: new Series([20.0, 20.5, 21.0]),
  tank_temperature: new Series([290.0, 290.2, 290.4]),
});

// Many series for one channel.
const multi = new Frame({
  tank_pressure: [new Series([20.0, 20.5]), new Series([21.0, 21.5])],
});
```

The series in a frame can have different lengths. A write has more rules: see [the rules of writes](https://docs.synnaxlabs.com/reference/client/write-data#the-rules-of-writes).

### Get data from a frame

Get the data of a channel by its name or key. The result is a `MultiSeries`: all the series of the channel, used like one series.

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

```python
frame = sy.Frame(
    ["tank_pressure", "tank_pressure"],
    [sy.Series([20.0, 20.5]), sy.Series([21.0, 21.5])],
)
pressure = frame["tank_pressure"]
print(pressure.to_numpy())  # [20.  20.5 21.  21.5]
print(pressure[0])  # 20.0
print(len(pressure.series))  # 2

# One column for each channel.
df = frame.to_df()
```

`to_df` gives one column for each channel. When the frame holds the index channel of each channel, as a frame that you read from a list of channels does, the `DataFrame` index is the UTC timestamps of the samples, and the index channels are not columns. The index has the name of the index channel, or `time` when the frame holds more than one index channel. Channels with different index channels are joined on time, and a cell is empty when a channel has no sample at that time. Integer columns with empty cells use pandas nullable types, such as `Int64`, to keep large values exact. A `min_max` read is not indexed by time, because its samples do not keep their times. In other frames, the index is the sample number, and all channels must have the same number of samples. `frame[mask]` selects the rows of `to_df` where a boolean mask is true. The mask has one value for each row.

When the frame has no series for the channel, `frame["name"]` returns an empty `MultiSeries`, and `frame.get("name")` returns `None` or the default that you give. A frame that you read by key also finds a channel by its name. When two channels in the frame have the same name, `frame["name"]` raises a `MultipleFoundError`.

**TypeScript**

```typescript
const frame = new Frame({
  tank_pressure: [new Series([20.0, 20.5]), new Series([21.0, 21.5])],
  tank_temperature: new Series([290.0, 290.2, 290.4, 290.6]),
});
const pressure = frame.get("tank_pressure");
console.log([...pressure]); // [ 20, 20.5, 21, 21.5 ]
console.log(pressure.at(0)); // 20
console.log(pressure.series.length); // 2

// One sample for each channel, at an index.
console.log(frame.at(1)); // { tank_pressure: 20.5, tank_temperature: 290.2 }
```

`frame.at` returns `undefined` for a channel that has no sample at the index. Pass `true` as the second argument to throw instead.

## Typed samples in TypeScript

`at` returns a `TelemValue`: a number, bigint, string, boolean, or `TimeStamp`. When you know the type of a series, `as` returns a `Series` of that type:

```typescript
const series = new Series([20.0, 20.5, 21.0]);
const numbers: Series<number> = series.as("number");
const first: number = numbers.at(0, true);
```

`as` takes `"number"`, `"bigint"`, `"string"`, or `"boolean"`. For `"number"`, `"bigint"`, and `"boolean"`, it throws when the data type does not match:

```plaintext
cannot convert series of type string to number
```
