# How Does a Live Gold Price Chart Work?

A live gold price chart is built from two related data sets: a current spot quote for the headline number and historical OHLC bars for the plotted series. The client loads a window of bars, sorts them by time, renders their values, then refreshes only the newest observation. The chart should also show whether that newest bar is still forming.

That distinction matters. A spot quote can change between two chart bars, while the close of a forming bar is provisional. Treating both as the same value produces duplicate points, misleading timestamps, or a line that appears fresher than its underlying data.

## The four parts of a live chart

Most gold charts need four small pieces:

- a symbol and interval, such as `XAU-USD-SPOT` and `1d`
- an initial range of OHLC bars
- a current spot quote for the price label
- a refresh loop that replaces the latest bar instead of blindly appending it

The chart renderer is only the last step. The important work happens before drawing: validating numbers, ordering observations, retaining timestamps, and deciding how to treat a forming bar.

## Spot quotes and OHLC bars answer different questions

The spot endpoint answers “what is the current derived price?” A daily bar answers “what were the open, high, low, and close values for this UTC day?”

```text
GET https://api.goldprice.dev/v1/spot/XAU-USD-SPOT
GET https://api.goldprice.dev/v1/bars?symbol=XAU-USD-SPOT&interval=1d&from=2026-07-25&to=2026-08-23
```

The spot response includes `price`, `computed_at`, and `is_stale`. Use those fields for the large current-price label and its freshness state.

Each bar includes `bar_start`, `open`, `high`, `low`, `close`, `volume`, and `is_closed`. Gold spot bars can have `volume: null`, so volume should not be required to draw a price chart. A bar with `is_closed: false` is still forming. Its OHLC values may change on the next refresh.

Bars are returned newest first. Most drawing code expects time to move from left to right, so reverse or sort the data before creating points.

## Fetch and normalize the initial chart data

Keep decimal prices as strings while they are in your data model. Convert them to JavaScript numbers only at the drawing boundary, where SVG coordinates require arithmetic.

```ts
type GoldBar = {
  bar_start: string;
  open: string | null;
  high: string | null;
  low: string | null;
  close: string | null;
  volume: string | null;
  is_closed: boolean;
};

type BarsResponse = {
  bars: GoldBar[];
  next_cursor: string | null;
};

type ChartPoint = {
  time: string;
  close: number;
  isClosed: boolean;
};

async function loadPoints(from: string, to: string): Promise<ChartPoint[]> {
  const url = new URL("https://api.goldprice.dev/v1/bars");
  url.search = new URLSearchParams({
    symbol: "XAU-USD-SPOT",
    interval: "1d",
    from,
    to,
  }).toString();

  const response = await fetch(url);
  if (!response.ok) throw new Error(`Gold bars failed: ${response.status}`);

  const page = (await response.json()) as BarsResponse;

  return page.bars
    .filter((bar) => bar.close !== null && Number.isFinite(Number(bar.close)))
    .map((bar) => ({
      time: bar.bar_start,
      close: Number(bar.close),
      isClosed: bar.is_closed,
    }))
    .sort((a, b) => Date.parse(a.time) - Date.parse(b.time));
}
```

This example requests one page because a recent daily chart fits within a short range. A larger history view should continue with `next_cursor` until it has enough observations or the cursor is null.

## Draw the line with plain SVG

SVG is enough to explain the chart math without tying the data layer to a package. The function below maps time to the horizontal axis and closing price to the vertical axis. It returns a path string for a `<path>` element.

```ts
function linePath(points: ChartPoint[], width: number, height: number): string {
  if (points.length === 0) return "";

  const values = points.map((point) => point.close);
  const min = Math.min(...values);
  const max = Math.max(...values);
  const span = max - min || 1;

  return points
    .map((point, index) => {
      const x = points.length === 1 ? width / 2 : (index / (points.length - 1)) * width;
      const y = height - ((point.close - min) / span) * height;
      return `${index === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`;
    })
    .join(" ");
}
```

Use the result in a component or browser view:

```tsx
<svg viewBox="0 0 720 280" role="img" aria-label="Gold price in US dollars">
  <path
    d={linePath(points, 720, 280)}
    fill="none"
    stroke="currentColor"
    strokeWidth="2"
  />
</svg>
```

The data array remains independent of SVG. You can later pass the same normalized points to Canvas, a mobile chart view, or a charting package without changing the fetch and refresh rules.

## Update the newest bar by timestamp

OHLC bars are available over REST. Poll `/v1/bars/latest` for the freshest settled or forming bar. Do not append every response: repeated responses often refer to the same `bar_start`.

```ts
type LatestBarResponse = { bar: GoldBar | null };

async function refreshLatest(current: ChartPoint[]): Promise<ChartPoint[]> {
  const response = await fetch(
    "https://api.goldprice.dev/v1/bars/latest?symbol=XAU-USD-SPOT&interval=1d",
    { cache: "no-store" },
  );
  if (!response.ok) throw new Error(`Latest bar failed: ${response.status}`);

  const { bar } = (await response.json()) as LatestBarResponse;
  if (!bar || bar.close === null || !Number.isFinite(Number(bar.close))) return current;

  const next: ChartPoint = {
    time: bar.bar_start,
    close: Number(bar.close),
    isClosed: bar.is_closed,
  };

  const index = current.findIndex((point) => point.time === next.time);
  if (index === -1) return [...current, next].sort((a, b) => Date.parse(a.time) - Date.parse(b.time));

  const copy = current.slice();
  copy[index] = next;
  return copy;
}
```

Run that function on a controlled timer, pause when the page is hidden, and ensure only one request is active at a time. The appropriate polling interval depends on the chart interval and the experience you are building. Faster polling cannot make an upstream observation newer.

## Keep the headline price separate

The line chart and the headline price can update on different schedules. Fetch the current quote separately and show the timestamp that belongs to it.

```ts
type SpotQuote = {
  symbol: string;
  price: string;
  computed_at: string;
  is_stale: boolean;
};

async function loadSpot(): Promise<SpotQuote> {
  const response = await fetch(
    "https://api.goldprice.dev/v1/spot/XAU-USD-SPOT",
    { cache: "no-store" },
  );
  if (!response.ok) throw new Error(`Gold spot failed: ${response.status}`);
  return response.json() as Promise<SpotQuote>;
}
```

Label a stale quote instead of silently presenting it as current. Keep `computed_at` visible in tooltips, metadata, or a nearby “updated” label. Never replace it with the browser fetch time; the two timestamps describe different events.

## What happens when the market is closed?

A correct chart can stay unchanged. No new market observation is not a rendering error, and the client should not fabricate points to make the line reach the present moment.

Keep the last valid spot quote with its original `computed_at`. Keep the last bar with its real `bar_start` and `is_closed` value. If your visual design needs empty calendar periods, render them as axis space or gaps, not copied prices presented as new observations.

The same rule applies after a failed request. Continue showing the last known good data with its timestamp, or show an error if no valid data has loaded. Never cache an HTTP error as a zero price.

## Production checks

Before shipping a live gold chart, verify these behaviors:

- use `XAU-USD-SPOT` as the canonical symbol
- sort bars from oldest to newest before drawing
- replace a bar when its `bar_start` matches, append only when it is new
- distinguish `is_closed: false` visually or in a tooltip
- allow `volume` to be null
- keep the spot quote's `computed_at` and `is_stale` fields
- stop overlapping refresh requests and back off after HTTP errors
- keep API keys in server-side code if the application uses authenticated requests
- preserve the last valid timestamp when the market or network is quiet

A chart is trustworthy when every point can be traced back to its observation time and state. Once those rules are part of the data layer, the choice of SVG, Canvas, or chart package becomes a presentation detail.

For endpoint parameters and pagination, see the [historical bars guide](/docs/historical). For live tick subscriptions and the distinction between ticks and bars, see the [WebSocket guide](/docs/stream).
