Blog/Developer

How Does a Live Gold Price Chart Work?

Learn how a live gold price chart combines spot quotes, OHLC bars, UTC timestamps, and controlled polling, with a working TypeScript SVG example.

Developer.md

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?”

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.

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.

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:

<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.

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.

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. For live tick subscriptions and the distinction between ticks and bars, see the WebSocket guide.

related guides

goldprice.dev

Live gold prices, historical OHLC, and multi-source aggregation — available via REST and SSE.