> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ngnmarket.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Candlestick Chart

> Fetch OHLCV data from the NGN Market API and render a professional candlestick chart.

export const demoHtml = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #fff; overflow: hidden; }
    #legend { position: absolute; top: 12px; left: 14px; z-index: 10; pointer-events: none; }
    .badge { display: inline-flex; align-items: center; gap: 6px; background: #f0fdf4; color: #15803d; font-size: 11px; font-weight: 500; padding: 3px 10px; border-radius: 20px; border: 1px solid #dcfce7; letter-spacing: 0.02em; }
    .dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; }
    #chart { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="legend">
    <div class="badge"><span class="dot"></span>DANGCEM &nbsp;·&nbsp; NGX &nbsp;·&nbsp; Daily</div>
  </div>
  <div id="chart"></div>
  <script>
    function generateCandles() {
      const candles = [];
      let price = 295;
      const d = new Date('2026-01-06');
      for (let i = 0; i < 100; i++) {
        while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1);
        const drift = (Math.random() - 0.47) * price * 0.022;
        const open  = parseFloat((price + (Math.random() - 0.5) * price * 0.006).toFixed(2));
        const close = parseFloat((Math.max(open * 0.93, open + drift)).toFixed(2));
        const high  = parseFloat((Math.max(open, close) + Math.random() * price * 0.009).toFixed(2));
        const low   = parseFloat((Math.min(open, close) - Math.random() * price * 0.009).toFixed(2));
        candles.push({ time: new Date(d).getTime(), open, high, low, close, volume: Math.floor(800000 + Math.random() * 7500000) });
        price = close;
        d.setDate(d.getDate() + 1);
      }
      return candles;
    }
    const data = generateCandles();
    const dates = data.map(d => new Date(d.time).toLocaleDateString('en-NG', { month: 'short', day: 'numeric' }));
    const chart = echarts.init(document.getElementById('chart'));
    chart.setOption({
      backgroundColor: '#ffffff',
      tooltip: {
        trigger: 'axis',
        axisPointer: { type: 'cross' },
        formatter(params) {
          const c = params.find(p => p.seriesType === 'candlestick');
          if (!c) return '';
          const [o, cl, lo, hi] = c.value;
          const col = cl >= o ? '#16a34a' : '#dc2626';
          return '<div style="font-size:11px;font-family:SF Mono,monospace;color:#6b7280">' +
            '<span style="color:#9ca3af">O</span> <span style="color:' + col + '">' + o.toFixed(2) + '</span>' +
            '<span style="color:#9ca3af;margin-left:8px">H</span> <span style="color:' + col + '">' + hi.toFixed(2) + '</span>' +
            '<span style="color:#9ca3af;margin-left:8px">L</span> <span style="color:' + col + '">' + lo.toFixed(2) + '</span>' +
            '<span style="color:#9ca3af;margin-left:8px">C</span> <span style="color:' + col + '">' + cl.toFixed(2) + '</span>' +
            '</div>';
        }
      },
      grid: [
        { left: 60, right: 20, top: 50, bottom: 100 },
        { left: 60, right: 20, top: '72%', bottom: 40 },
      ],
      xAxis: [
        { type: 'category', data: dates, axisLine: { lineStyle: { color: '#e5e7eb' } }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { color: '#6b7280', fontSize: 11 } },
        { type: 'category', gridIndex: 1, data: dates, show: false },
      ],
      yAxis: [
        { scale: true, axisLine: { show: false }, axisTick: { show: false }, splitLine: { lineStyle: { color: '#f3f4f6' } }, axisLabel: { color: '#6b7280', fontSize: 11 } },
        { gridIndex: 1, scale: true, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false } },
      ],
      dataZoom: [
        { type: 'inside', xAxisIndex: [0, 1] },
        { type: 'slider', xAxisIndex: [0, 1], bottom: 10, height: 20, borderColor: '#e5e7eb', textStyle: { color: '#6b7280', fontSize: 10 } },
      ],
      series: [
        {
          type: 'candlestick',
          data: data.map(d => [d.open, d.close, d.low, d.high]),
          itemStyle: { color: '#22c55e', color0: '#ef4444', borderColor: '#22c55e', borderColor0: '#ef4444' },
        },
        {
          type: 'bar',
          xAxisIndex: 1,
          yAxisIndex: 1,
          data: data.map(d => ({ value: d.volume, itemStyle: { color: d.close >= d.open ? '#22c55e28' : '#ef444428' } })),
        },
      ],
    });
    window.addEventListener('resize', () => chart.resize());
  </script>
</body>
</html>`;

The demo above is what you'll have by the end of this guide. It's built with [Apache ECharts](https://echarts.apache.org/) and the NGN Market API's `format=ohlcv` option, which returns data in exactly the shape charting libraries expect.

**Endpoint used:**

* [`GET /companies/{symbol}/chart`](/api-reference/companies/chart) (Starter plan)

## Live demo

<iframe srcDoc={demoHtml} style={{ width: "100%", height: "380px", border: "1px solid #e5e7eb", borderRadius: "8px" }} scrolling="no" />

Hover over any candle to see the OHLCV breakdown. This demo runs on generated sample data so it works without an API key. The code below swaps that out for real NGX prices.

## What the data looks like

When you request `format=ohlcv`, each entry in the `data` array is a compact array in this order:

```
[timestamp, open, high, low, close, volume]
```

```json theme={null}
{
  "data": [
    [1744243200000, 293.00, 297.50, 292.00, 295.00, 3821000],
    [1744329600000, 295.50, 301.00, 294.00, 300.00, 4102000],
    [1744416000000, 300.00, 303.50, 298.00, 302.50, 5143000]
  ]
}
```

The timestamp is in Unix milliseconds. ECharts works with milliseconds natively, so you can pass it straight to a `Date` constructor — no conversion needed.

One thing to watch: ECharts candlestick series expects data in `[open, close, lowest, highest]` order, which is different from the API's `[open, high, low, close]` order. The mapping section below handles this.

## Handling missing OHLC data

`close` is the only value guaranteed on every entry. For some dates, typically when you request a long history (`period=5y`, `period=all`, or an old `from` date), a company's intraday range wasn't tracked that far back. When that happens, `open`, `high`, `low`, and `volume` come back as `null` while `close` is still populated: `[1583020800000, null, null, null, 12.5, null]`.

Filter these out before handing data to a candlestick series. A candle can't be drawn without all four OHLC values:

```javascript JavaScript theme={null}
const candles = raw.filter(([, open, high, low]) => open != null && high != null && low != null);
```

If you'd rather keep the full close-price history visible instead of dropping those points, render two series: candlesticks for the rows with full OHLC, and a thin line plotting every row's close price underneath for continuity. The examples below use the simple filter.

## Vanilla JavaScript

You can load ECharts from a CDN with no build step required.

```html HTML theme={null}
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<div id="chart" style="width: 100%; height: 400px;"></div>
```

```javascript JavaScript theme={null}
const API_KEY = 'ngm_live_YOUR_KEY';

async function fetchOHLCV(symbol, period = '1y') {
  const res = await fetch(
    `https://api.ngnmarket.com/v1/companies/${symbol}/chart?period=${period}&format=ohlcv`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  const json = await res.json();
  return json.data.data;
}

async function renderChart(symbol) {
  const raw = await fetchOHLCV(symbol);
  // Drop points without a full OHLC range, see "Handling missing OHLC data" above
  const candles = raw.filter(([, open, high, low]) => open != null && high != null && low != null);

  const dates = candles.map(([ts]) =>
    new Date(ts).toLocaleDateString('en-NG', { month: 'short', day: 'numeric' })
  );

  const chart = echarts.init(document.getElementById('chart'));

  chart.setOption({
    tooltip: {
      trigger: 'axis',
      axisPointer: { type: 'cross' },
    },
    grid: { left: 60, right: 20, top: 30, bottom: 60 },
    xAxis: {
      type: 'category',
      data: dates,
      splitLine: { show: false },
    },
    yAxis: {
      scale: true,
      splitLine: { lineStyle: { color: '#f0f0f0' } },
    },
    dataZoom: [
      { type: 'inside', start: 50, end: 100 },
      { type: 'slider', start: 50, end: 100 },
    ],
    series: [{
      type: 'candlestick',
      // API order: [ts, open, high, low, close] → ECharts order: [open, close, lowest, highest]
      data: candles.map(([_ts, open, high, low, close]) => [open, close, low, high]),
      itemStyle: {
        color: '#22c55e',
        color0: '#ef4444',
        borderColor: '#22c55e',
        borderColor0: '#ef4444',
      },
    }],
  });

  window.addEventListener('resize', () => chart.resize());
}

renderChart('DANGCEM');
```

## React component

If you're working in React, here's a reusable component that handles fetching, rendering, and cleaning up the chart when the component unmounts. It also responds to container resizes automatically.

```bash theme={null}
npm install echarts
```

```tsx React theme={null}
import { useEffect, useRef } from 'react';
import * as echarts from 'echarts';

const API_KEY = 'ngm_live_YOUR_KEY';

async function fetchOHLCV(symbol, period = '1y') {
  const res = await fetch(
    `https://api.ngnmarket.com/v1/companies/${symbol}/chart?period=${period}&format=ohlcv`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  const json = await res.json();
  return json.data.data;
}

export function CandlestickChart({ symbol = 'DANGCEM', period = '1y' }) {
  const containerRef = useRef(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const chart = echarts.init(containerRef.current);

    fetchOHLCV(symbol, period).then(raw => {
      // Drop points without a full OHLC range, see "Handling missing OHLC data" above
      const candles = raw.filter(([, open, high, low]) => open != null && high != null && low != null);
      const dates = candles.map(([ts]) =>
        new Date(ts).toLocaleDateString('en-NG', { month: 'short', day: 'numeric' })
      );

      chart.setOption({
        tooltip: {
          trigger: 'axis',
          axisPointer: { type: 'cross' },
        },
        grid: { left: 60, right: 20, top: 30, bottom: 60 },
        xAxis: {
          type: 'category',
          data: dates,
          splitLine: { show: false },
        },
        yAxis: {
          scale: true,
          splitLine: { lineStyle: { color: '#f0f0f0' } },
        },
        dataZoom: [
          { type: 'inside', start: 50, end: 100 },
          { type: 'slider', start: 50, end: 100 },
        ],
        series: [{
          type: 'candlestick',
          data: candles.map(([_ts, open, high, low, close]) => [open, close, low, high]),
          itemStyle: {
            color: '#22c55e',
            color0: '#ef4444',
            borderColor: '#22c55e',
            borderColor0: '#ef4444',
          },
        }],
      });
    });

    const observer = new ResizeObserver(() => chart.resize());
    observer.observe(containerRef.current);

    return () => {
      observer.disconnect();
      chart.dispose();
    };
  }, [symbol, period]);

  return <div ref={containerRef} style={{ width: '100%', height: 400 }} />;
}
```

Drop it anywhere in your app:

```tsx React theme={null}
<CandlestickChart symbol="GTCO" period="90d" />
```

## Adding a volume panel

Volume is the sixth value in each array (index 5). ECharts supports multiple grids in a single chart instance, so you can stack a volume bar chart directly beneath the candlesticks and link their x-axes so zoom and pan stay in sync.

```javascript JavaScript theme={null}
async function renderChartWithVolume(symbol) {
  const raw = await fetchOHLCV(symbol);
  // Drop points without a full OHLC range, see "Handling missing OHLC data" above
  const candles = raw.filter(([, open, high, low]) => open != null && high != null && low != null);

  const dates = candles.map(([ts]) =>
    new Date(ts).toLocaleDateString('en-NG', { month: 'short', day: 'numeric' })
  );

  const chart = echarts.init(document.getElementById('chart'));

  chart.setOption({
    tooltip: {
      trigger: 'axis',
      axisPointer: { type: 'cross' },
    },
    grid: [
      { left: 60, right: 20, top: 30, bottom: 120 },
      { left: 60, right: 20, top: '72%', bottom: 40 },
    ],
    xAxis: [
      { type: 'category', data: dates, splitLine: { show: false } },
      { type: 'category', gridIndex: 1, data: dates, show: false },
    ],
    yAxis: [
      { scale: true, splitLine: { lineStyle: { color: '#f0f0f0' } } },
      { gridIndex: 1, scale: true, axisLabel: { show: false }, splitLine: { show: false } },
    ],
    dataZoom: [
      { type: 'inside', xAxisIndex: [0, 1], start: 50, end: 100 },
      { type: 'slider', xAxisIndex: [0, 1], start: 50, end: 100 },
    ],
    series: [
      {
        type: 'candlestick',
        data: candles.map(([_ts, open, high, low, close]) => [open, close, low, high]),
        itemStyle: {
          color: '#22c55e',
          color0: '#ef4444',
          borderColor: '#22c55e',
          borderColor0: '#ef4444',
        },
      },
      {
        type: 'bar',
        xAxisIndex: 1,
        yAxisIndex: 1,
        data: candles.map(([_ts, open, _high, _low, close, volume]) => ({
          value: volume,
          itemStyle: { color: close >= open ? '#22c55e55' : '#ef444455' },
        })),
      },
    ],
  });

  window.addEventListener('resize', () => chart.resize());
}
```

## A few things worth knowing

**The data order mismatch.** The API returns `[timestamp, open, high, low, close, volume]`. ECharts candlestick expects `[open, close, lowest, highest]`. Always map `[open, close, low, high]` — swapping high and low will render wicks upside down.

**Pick a period that fits your use case.** `7d` and `30d` are good for a focused recent view. `1y` works well for trend analysis. `5y` is what you want for a long-term research screen. Shorter periods load faster and render more smoothly.

**The symbol lookup is case-insensitive.** `DANGCEM`, `dangcem`, and `DangCem` all resolve to the same company.

**Switching symbols without rebuilding the chart.** If you're building a stock picker where users switch between companies, call `chart.setOption({ series: [{ data: newCandles }] })` on the existing instance rather than disposing and reinitialising. The transition is smoother and you keep the zoom and scroll position.

<CardGroup cols={2}>
  <Card title="Chart endpoint reference" icon="book" href="/api-reference/companies/chart">
    All parameters for `GET /companies/{symbol}/chart`
  </Card>

  <Card title="Company profile reference" icon="book" href="/api-reference/companies/detail">
    Pair this with company data for a full stock detail page
  </Card>
</CardGroup>
