> For the complete documentation index, see [llms.txt](https://docs.tryterra.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tryterra.co/graphs/embedding.md).

# Embedding graphs

Put a graph you built in the dashboard into your web or mobile app.

Every embed needs the same two things: the **graph id** from [**Dashboard → Graphs**](https://dashboard.tryterra.co/dashboard/graphs) (select **Embed** on a card to copy it) and the **Terra user id** of the person looking at it.

Use `example` in place of a user id to render generated data while you build.

***

## Web

On the web, graphs render directly into your page. There's no iframe, so the chart sizes to its container, inherits your layout, and behaves like any other element.

{% tabs %}
{% tab title="React" %}

```bash
npm install @tryterra/graphs-react
```

```jsx
import { TerraGraph } from "@tryterra/graphs-react";

export function StepsCard({ terraUserId }) {
  return (
    <TerraGraph
      sessionId="YOUR_GRAPH_ID"
      userId={terraUserId}
      timeframe={30}
      style={{ width: "100%", height: 360 }}
      onError={(error) => console.error(error.message, error.traceId)}
    />
  );
}
```

Works in Next.js, Remix and Astro with no `dynamic()` wrapper — the component renders nothing on the server and mounts in the browser.
{% endtab %}

{% tab title="HTML" %}

```bash
npm install @tryterra/graphs
```

```html
<script type="module">
  import "@tryterra/graphs";
</script>

<terra-graph
  session-id="YOUR_GRAPH_ID"
  user-id="TERRA_USER_ID"
  timeframe="30"
></terra-graph>

<style>
  terra-graph { display: block; width: 100%; height: 360px; }
</style>
```

{% endtab %}

{% tab title="Vue" %}

```bash
npm install @tryterra/graphs
```

Tell the Vue compiler that `terra-graph` is a custom element, in `vite.config.js`:

```js
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === "terra-graph" } } });
```

```vue
<script setup>
import "@tryterra/graphs";
defineProps(["graphId", "terraUserId"]);
</script>

<template>
  <terra-graph :session-id="graphId" :user-id="terraUserId" timeframe="30" />
</template>
```

{% endtab %}

{% tab title="Svelte" %}

```bash
npm install @tryterra/graphs
```

```svelte
<script>
  import "@tryterra/graphs";
  export let graphId;
  export let terraUserId;
</script>

<terra-graph session-id={graphId} user-id={terraUserId} timeframe="30" />

<style>
  terra-graph { display: block; width: 100%; height: 360px; }
</style>
```

{% endtab %}

{% tab title="Angular" %}

```bash
npm install @tryterra/graphs
```

```ts
import "@tryterra/graphs";
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";

@Component({
  selector: "app-steps",
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <terra-graph
      [attr.session-id]="graphId"
      [attr.user-id]="terraUserId"
      timeframe="30"
    ></terra-graph>
  `,
})
export class StepsComponent {
  graphId = "YOUR_GRAPH_ID";
  terraUserId = "TERRA_USER_ID";
}
```

{% endtab %}
{% endtabs %}

The element has no height of its own — give it one, or it collapses.

{% hint style="info" %}
The chart engine is loaded from Terra when a graph mounts, which is what keeps every embed in step with the dashboard. If you run a Content Security Policy, allow `script-src https://api.tryterra.co`, `connect-src https://api.tryterra.co`, and — to keep Terra's typeface — `font-src https://fonts.gstatic.com` and `style-src https://fonts.googleapis.com`.
{% endhint %}

***

## React Native

React Native has a package of its own, which draws the graph as native views — no web view:

```bash
npm install @tryterra/graphs-react-native
```

```jsx
import { TerraGraph } from "@tryterra/graphs-react-native";

<TerraGraph
  sessionId="YOUR_GRAPH_ID"
  userId={terraUserId}
  timeframe={30}
  height={240}
/>
```

There is no chart engine to install or configure — the package ships its own. The one thing it can't bundle is a drawing surface, since React Native has none built in: `react-native-svg` is a native module, declared as a peer so npm installs it for you.

On Expo, run `npx expo install react-native-svg` once afterwards so the version matches your SDK.

For faster painting on very dense charts, import `@tryterra/graphs-react-native/skia` instead and add `@shopify/react-native-skia` and `react-native-reanimated`. Same component, same props.

{% hint style="info" %}
`react-native-svg` ships inside Expo Go, so this runs there as-is — no development build needed. If you would rather add no native modules at all, use the web view below.
{% endhint %}

**One difference worth knowing.** On the web, the chart engine is loaded from Terra at render time, so improvements reach your users as soon as we ship them. A native app bundles it instead, so it picks them up when you upgrade the package and release a new build. If you would rather never think about that, the web view below is the option that keeps it.

***

## Other mobile platforms

iOS, Android and Flutter load the hosted graph in a web view. React Native can too, with `react-native-webview`, if you would rather not add a charting library:

```
https://api.tryterra.co/v2/graphs/YOUR_GRAPH_ID/TERRA_USER_ID?timeframe=30
```

{% tabs %}
{% tab title="Swift" %}

```swift
import SwiftUI
import WebKit

struct TerraGraph: UIViewRepresentable {
    let graphId: String
    let userId: String

    func makeUIView(context: Context) -> WKWebView { WKWebView() }

    func updateUIView(_ webView: WKWebView, context: Context) {
        let url = URL(string: "https://api.tryterra.co/v2/graphs/\(graphId)/\(userId)?timeframe=30")!
        webView.load(URLRequest(url: url))
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
@Composable
fun TerraGraph(graphId: String, userId: String) {
    AndroidView(factory = { context ->
        WebView(context).apply {
            settings.javaScriptEnabled = true
            loadUrl("https://api.tryterra.co/v2/graphs/$graphId/$userId?timeframe=30")
        }
    })
}
```

{% endtab %}

{% tab title="Flutter" %}

```dart
import 'package:webview_flutter/webview_flutter.dart';

final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..loadRequest(Uri.parse(
    'https://api.tryterra.co/v2/graphs/$graphId/$terraUserId?timeframe=30',
  ));

// in build():
WebViewWidget(controller: controller)
```

{% endtab %}
{% endtabs %}

The same URL works in an `<iframe>` on the web, if you'd rather not add a package.

***

## Choosing the date range

Every embed takes the same three settings — as props in React, attributes in HTML, and query parameters in a URL.

| Setting                                | Renders                                   |
| -------------------------------------- | ----------------------------------------- |
| `timeframe` = 30                       | The most recent 30 days, including today. |
| `from` = 2026-08-01, `to` = 2026-08-31 | 1–31 August, with `to` included.          |
| `timeframe` = 7, `from` = 2026-08-01   | Seven days starting 1 August.             |
| `timeframe` = 7, `to` = 2026-08-31     | The seven days ending 31 August.          |
| `from` = 2026-08-01                    | 1 August through today.                   |

Dates are `YYYY-MM-DD` in UTC. A range can cover at most **92 days**. With nothing set, you get the last 7 days.

{% hint style="warning" %}
**Pass a date string, not a JavaScript `Date`.** A graph window is a run of calendar days, and a `Date` is a moment in time — converting one to the other picks a timezone, and the usual way of doing it picks the wrong one. A date picker gives you `new Date(2026, 7, 1)` for "1 August", and `.toISOString().slice(0, 10)` turns that into `2026-07-31` for anyone east of Greenwich.

The packages type these as `IsoDate`, so a `Date` won't compile. If you have one, convert it with the helper they export:

```jsx
import { toIsoDate } from "@tryterra/graphs-react";

<TerraGraph sessionId={graphId} userId={terraUserId} from={toIsoDate(picked)} />
```

{% endhint %}

Changing the range redraws the chart in place rather than reloading it, so you can wire a date picker straight to it:

```jsx
const [days, setDays] = useState(30);

<>
  <RangePicker value={days} onChange={setDays} />
  <TerraGraph sessionId={graphId} userId={terraUserId} timeframe={days} />
</>
```

A graph set to **Single session** scope ignores the range and always shows the user's latest workout, night or day.

***

## Matching your app's theme

A graph's colours are part of its design in the dashboard, so the usual answer is to set them there — every embed picks the change up without a release on your side.

For colours that have to follow the app at runtime, such as a dark-mode toggle, pass a theme:

```jsx
<TerraGraph
  sessionId={graphId}
  userId={terraUserId}
  theme={isDark ? { bg: "#0F172A", text: "#E2E8F0", line: "#38BDF8" } : undefined}
/>
```

It's a `theme` property on the element too, if you aren't using React:

```js
document.querySelector("terra-graph").theme = { bg: "#0F172A", text: "#E2E8F0" };
```

Give colours as hex or `rgb()`. If you need two colour schemes of the same chart, building two graphs in the dashboard is usually simpler than theming at runtime.

***

## When a graph doesn't render

A graph that can't be drawn says so in place, and reports a **trace id**. Quote that id to Terra support and we can see exactly what happened.

```jsx
<TerraGraph sessionId={graphId} userId={terraUserId} onError={(e) => report(e.message, e.traceId)} />
```

On the element it's a `terra-graph:error` event carrying the same thing, plus a `data-state` attribute (`loading`, `ready` or `error`) and `data-trace-id` on failure.

The common causes:

| What you see                           | Usually means                                                                                                                                                     |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **No data found for this time period** | Terra holds no data for that user over that range. Check the user in [**Dashboard → Users**](https://dashboard.tryterra.co/dashboard/users), and widen the range. |
| **Invalid graph session ID**           | The graph id is wrong, or the graph was deleted. Copy it again from **Embed**.                                                                                    |
| **Invalid graph type requested**       | The graph was created against an old graph type. Open it in the editor and re-pick the metric.                                                                    |
| Nothing renders, no error              | The element has no height. Give it one in CSS.                                                                                                                    |
| A blocked request in the console       | A Content Security Policy is blocking `api.tryterra.co` — see the note above.                                                                                     |

***

## Changing a graph after you've shipped

Graph ids are stable. Edit a graph in the dashboard — different metric, different colours, an extra header stat — and every embed of it changes on the next load. You don't need to touch your app.

Deleting a graph stops it rendering everywhere it's embedded, so check **Viewers** first if you're not sure who's using it.
