# How to Create Custom Charts in Retool with Plotly

- Tool: Retool
- Difficulty: Intermediate
- Time required: 25-35 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

Retool's Chart component is powered by Plotly.js and supports 15+ chart types. For basic charts, use the Series configuration in the Inspector. For advanced types like candlestick, gauge, or heatmap, switch to Plotly JSON mode and provide a full Plotly data array and layout object. Use {{ chart1.selectedPoints }} to build interactive dashboards where clicking a chart filters other components.

## From Basic Charts to Full Plotly JSON Customization

Retool's Chart component wraps Plotly.js, giving you access to its full visualization library. For most use cases — line charts, bar charts, scatter plots, pie charts — the built-in Series configuration in the Inspector is sufficient. You simply point the Data source at a query, choose X and Y column names, and Retool handles the rest.

But Plotly supports dozens of specialized chart types (candlestick, OHLC, waterfall, funnel, gauge, heatmap, treemap) that aren't exposed in the Series UI. For those, Retool exposes a 'Plotly JSON' toggle that lets you provide a raw Plotly data array and layout object — giving you complete control over every pixel of the chart.

This tutorial covers both approaches, plus the interactive chart.selectedPoints property that lets clicking a chart data point filter tables or trigger queries.

## Before you start

- A Retool app with at least one query returning time-series or categorical data
- Basic familiarity with the Retool Inspector panel
- Optionally: familiarity with Plotly.js chart types (plotly.com/javascript)

## Step-by-step guide

### 1. Add a Chart component and connect it to query data

Drag a Chart component onto your canvas from the component panel. In the Inspector, set the Data source to {{ query1.data }}. By default, the chart shows in Series mode — Retool tries to auto-detect X and Y columns. Switch the X axis to a Date or category column and add one or more Y axis series pointing to numeric columns. For example, X: 'date', Y Series 1: 'revenue', Y Series 2: 'expenses'. Choose 'Line' as the chart type from the Type dropdown.

**Expected result:** A line chart appears displaying your query data with correct X and Y axes.

### 2. Customize chart appearance using the Series Inspector options

In the Chart Inspector, expand the Axes section to set axis labels, tick formats, and ranges. Set X axis title to 'Month' and Y axis title to 'USD ($)'. Under each series, set a custom Color (hex code) and Marker size. Enable Show legend to add a chart legend. For numeric Y axes, set Tick prefix to '$' or Tick format to ',.2f' for formatted numbers. These Inspector options cover ~80% of common chart customization needs without writing any JSON.

**Expected result:** Chart has labeled axes, formatted tick values, and colored series.

### 3. Transform query data for multi-series charts

When your query returns data in a flat format (one row per measurement), you may need to pivot it for multi-series charts. Add a Transformer in the Code panel. The transformer receives your query results as the 'data' input and must return a reshaped array. Use JavaScript's reduce() to group by a category column, then produce separate arrays for each series. Reference the transformer output in the Chart component as {{ transformer1.value }}.

```
// Transformer: pivotChartData
// Input: data = [{ date: '2026-01', category: 'A', value: 100 }, ...]
// Output: Plotly-compatible series arrays

const grouped = data.reduce((acc, row) => {
  if (!acc[row.category]) acc[row.category] = { x: [], y: [] };
  acc[row.category].x.push(row.date);
  acc[row.category].y.push(row.value);
  return acc;
}, {});

// Return as Plotly traces array for Plotly JSON mode
return Object.entries(grouped).map(([name, series]) => ({
  name,
  x: series.x,
  y: series.y,
  type: 'scatter',
  mode: 'lines+markers',
}));
```

**Expected result:** Transformer outputs a Plotly-compatible array with one object per chart series.

### 4. Switch to Plotly JSON mode for advanced chart types

In the Chart Inspector, toggle 'Plotly JSON' to ON. The Series UI is replaced by two fields: 'Data (JSON)' and 'Layout (JSON)'. Set Data (JSON) to {{ transformer1.value }} — this should be the Plotly traces array from the previous step. For the Layout (JSON), provide a JSON object controlling the chart's overall appearance. In Plotly JSON mode you have access to every Plotly.js chart type: 'candlestick', 'ohlc', 'waterfall', 'funnel', 'indicator' (gauge), 'heatmap', 'treemap', 'sunburst', and more.

```
// Layout JSON object — paste into the Layout (JSON) field
{
  "title": { "text": "Monthly Revenue by Category" },
  "font": { "family": "Inter, sans-serif", "size": 13 },
  "plot_bgcolor": "rgba(0,0,0,0)",
  "paper_bgcolor": "rgba(0,0,0,0)",
  "xaxis": { "title": "Month", "showgrid": false },
  "yaxis": { "title": "Revenue (USD)", "tickprefix": "$", "gridcolor": "#e5e7eb" },
  "legend": { "orientation": "h", "y": -0.2 },
  "margin": { "l": 60, "r": 20, "t": 50, "b": 60 }
}
```

**Expected result:** Chart switches to Plotly JSON mode and renders using the custom layout configuration.

### 5. Build a gauge chart using Plotly indicator type

For KPI dashboards, a gauge chart is often more impactful than a line chart. In Plotly JSON mode, set Data (JSON) to a single-element array with type 'indicator'. Set the value from a query using {{ query1.data[0].score }}. Configure the gauge range and color thresholds in the gauge object. This creates a speedometer-style gauge that updates dynamically with your query data.

```
// Gauge chart — Data (JSON) field
[
  {
    "type": "indicator",
    "mode": "gauge+number+delta",
    "value": {{ query1.data[0].nps_score }},
    "delta": { "reference": {{ query1.data[0].prev_nps_score }} },
    "gauge": {
      "axis": { "range": [0, 100] },
      "steps": [
        { "range": [0, 40], "color": "#fee2e2" },
        { "range": [40, 70], "color": "#fef3c7" },
        { "range": [70, 100], "color": "#d1fae5" }
      ],
      "threshold": {
        "line": { "color": "#6366f1", "width": 4 },
        "thickness": 0.75,
        "value": 80
      }
    },
    "title": { "text": "NPS Score" }
  }
]
```

**Expected result:** A gauge chart appears showing the NPS score with color-coded zones and a delta indicator.

### 6. Use chart.selectedPoints for interactive filtering

The Chart component exposes a {{ chart1.selectedPoints }} property containing an array of the data points the user clicked on. Use this to build cross-filtering: when a user clicks a bar in a bar chart, filter a table to show only rows matching that category. Set the Table component's Data source to a transformer that filters based on {{ chart1.selectedPoints[0].x }} — the X value of the selected point. Add an event handler on the Chart component's 'Point click' event to trigger a filtered query.

```
// Transformer: filterTableByChartSelection
// Filters the full dataset to the selected chart category

const selectedCategory = chart1.selectedPoints?.[0]?.x;

if (!selectedCategory) {
  return query1.data; // No selection — show all
}

return query1.data.filter(row => row.category === selectedCategory);
```

**Expected result:** Clicking a bar in the chart filters the table below to show only rows matching the clicked category.

## Complete code example

File: `Transformer: buildCandlestickData`

```javascript
// Transformer: buildCandlestickData
// Converts OHLCV query data to Plotly candlestick format
// query1.data expected shape: [{ date, open, high, low, close, volume }]

const rows = data; // 'data' is the attached query's result

return [
  {
    type: 'candlestick',
    x: rows.map(r => r.date),
    open: rows.map(r => r.open),
    high: rows.map(r => r.high),
    low: rows.map(r => r.low),
    close: rows.map(r => r.close),
    increasing: { line: { color: '#10b981' } },
    decreasing: { line: { color: '#ef4444' } },
    name: 'OHLC',
  },
  {
    type: 'bar',
    x: rows.map(r => r.date),
    y: rows.map(r => r.volume),
    yaxis: 'y2',
    name: 'Volume',
    marker: { color: '#6366f1', opacity: 0.4 },
  },
];

// Layout JSON for the chart:
// {
//   "yaxis": { "title": "Price" },
//   "yaxis2": { "title": "Volume", "overlaying": "y", "side": "right" },
//   "xaxis": { "rangeslider": { "visible": false } },
//   "plot_bgcolor": "rgba(0,0,0,0)",
//   "paper_bgcolor": "rgba(0,0,0,0)"
// }
```

## Common mistakes

- **Setting Data (JSON) to {{ query1.data }} directly in Plotly JSON mode and expecting it to work as Plotly traces** — undefined Fix: query1.data is an array of row objects, not Plotly trace objects. You must transform the data into the Plotly traces format first using a Transformer, then bind {{ transformer1.value }} to Data (JSON).
- **Accessing chart1.selectedPoints[0] without checking if it exists** — undefined Fix: selectedPoints is empty array [] when nothing is selected. Use optional chaining: {{ chart1.selectedPoints?.[0]?.x ?? 'All' }} to handle the no-selection case.
- **Putting {{ }} expressions inside the Layout JSON string field** — undefined Fix: Layout JSON is a static JSON object — it cannot contain {{ }} Retool expressions. Dynamic layout values (like max Y axis range from a query) must be handled in the Data traces or in a Transformer that builds the full Plotly configuration.
- **Transformers triggering queries or calling setValue() inside their code** — undefined Fix: Transformers are read-only compute functions. They cannot trigger queries, set state variables, or produce side effects. Move any side-effect logic to a JS Query.

## Best practices

- Use Series mode for simple charts and switch to Plotly JSON mode only when you need advanced chart types or fine-grained control
- Always set plot_bgcolor and paper_bgcolor to rgba(0,0,0,0) in Layout JSON to let the Retool app background show through
- Transform data in a Transformer or query-attached transformer before passing to the chart — keep chart Data (JSON) bindings simple
- Handle empty data gracefully: check if your data array has rows before rendering, otherwise Plotly shows a blank chart without error
- Use chart1.selectedPoints carefully — it persists until the user clicks elsewhere. Add a 'Clear selection' button that calls chart1.resetSelection()
- For dashboards with multiple charts, use one primary data query and transformers for each chart to avoid redundant database calls

## Frequently asked questions

### What chart types does Retool support beyond line and bar charts?

In Plotly JSON mode, Retool supports the full Plotly.js chart library: scatter, bar, line, area, pie, donut, box, violin, histogram, heatmap, contour, candlestick, OHLC, waterfall, funnel, gauge (indicator), treemap, sunburst, sankey, and more. Check plotly.com/javascript for the complete list and trace configuration options.

### Can I make a Retool chart respond to changes in other components?

Yes — bind the Chart's Data source to a transformer that references other components (e.g., {{ dateRangePicker1.value }}, {{ select1.value }}). When those components change, the transformer re-runs and the chart updates automatically. For query-driven filtering, set the query to 'Run when inputs change' and reference the component values in the SQL WHERE clause.

### How do I add click event handlers to a Retool chart?

In the Chart Inspector, scroll to the Interaction section and add a handler for the 'Point click' event. The handler runs when a user clicks any data point. Use {{ chart1.selectedPoints }} to access the clicked point's data within the event handler or in downstream queries and transformers.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-create-custom-charts-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-create-custom-charts-in-retool
