If You Work with Financial Data, This Plot Function Will Save You a Lot of Time

DolphinDB
2026-05-28

FX options, volatility smiles, term structures — how do you turn complex financial data into something you can actually read? In this article, we walk through DolphinDB's built-in plot() function using an FX option volatility surface as a running example, showing you how to build professional 2D and 3D charts with mouse-driven rotation, zoom, and hover tooltips.

1. Core Parameters of theplot()Function

DolphinDB's plot() is a lightweight yet powerful charting function. No third-party libraries required — it takes you straight from data to visualization entirely within the database.

plot(data, [labels], title, chartType, [stacking], [extras])
ParameterData TypeDescription
datamatrix or tableEach column represents one series (LINE) or one scale column (SURFACE); rows represent data points along the X-axis.
labelsSTRING vectorX-axis tick labels. If provided, overrides the matrix's existing row labels. Length must equal the number of rows.
titleSTRING vectorIf a scalar, sets the chart title only. If a vector, elements map in order to ["Title", "X-axis", "Y-axis", "Z-axis"].
chartTypeconstantChart type: LINE / BAR / PIE / AREA / SCATTER / HISTOGRAM / SURFACE
stackingBOOLWhether to display series as stacked. Defaults to false. Currently only effective for BAR and AREA.
extrasDICTAdditional options, passed as a {} literal. Only supported for LINE charts. Accepts { multiYAxes: true } to enable multiple independent Y-axes.

2. Setting Up the Data: USDCNY FX Option Volatility Surface

Before we can plot anything, we need a volatility surface to work with. We'll build one using DolphinDB's built-in fxVolatilitySurfaceBuilder. The inputs are option quotes across 13 tenors for five quote types — ATM, D25_RR, D25_BF, D10_RR, D10_BF — which gives us enough resolution for smooth charts.

refDate = 2025.08.18
ccyPair = "USDCNY"
spot    = 7.1627
quoteTerms = ["1d","1w","2w","3w","1M","2M","3M","6M","9M","1y","18M","2y","3y"]
quoteNames = ["ATM","D25_RR","D25_BF","D10_RR","D10_BF"]
rawQuotes = [
    0.030000,-0.007500, 0.003500,-0.010000, 0.005500,
    ...(13×5 = 65 values in total)
    0.044750, 0.006250, 0.003400, 0.009000, 0.008550]
quotes = reshape(rawQuotes, 5:13).transpose()   // → 13-row × 5-column matrix
// Build interest rate curves and volatility surface
domesticCurve = parseMktData(domesticCurveInfo)   // CNY IrYieldCurve
foreignCurve  = parseMktData(foreignCurveInfo)     // USD IrYieldCurve
surf = fxVolatilitySurfaceBuilder(
    refDate, ccyPair, quoteNames, quoteTerms, quotes, spot,
    domesticCurve, foreignCurve, "SVI")

With the surface ready, we can now start plotting!

Example 1: 2D Line Chart (LINE) — CNY vs USD Interest Rate Curves

Many quant strategies require monitoring both CNY and USD interest rate levels simultaneously. The catch is that CNY and USD rates often sit at very different absolute levels — plot them on a shared Y-axis and the smaller series gets squashed into a flat line, making it unreadable.

plot() solves this with dual Y-axes, letting both curves display clearly on the same chart.

In this example, we use curvePredict to sample rate values at 100 evenly-spaced dates from the vol surface object we built earlier, then plot both curves as smooth lines.

// Prepare date array: 100 evenly-spaced points from 1M to 3Y
startDate = curveDates[4]    // start at 1M, skipping short-end noise
endDate   = curveDates[12]   // 3Y
nPts = 100
span = endDate - startDate
dates100 = array(DATE, nPts)
for (i in 0:nPts) { dates100[i] = startDate + int(i * span / (nPts - 1.0)) }

cnyDense = curvePredict(domesticCurve, dates100) * 100.0
usdDense = curvePredict(foreignCurve, dates100) * 100.0

// Build matrix & assign labels
labels100 = array(STRING, nPts)
for (i in 0:nPts) { labels100[i] = string(dates100[i]) }\

mCurves = matrix(cnyDense, usdDense)    // 100-row × 2-col
mCurves.rename!(labels100, ["CNY (%)", "USD (%)"])
//                             ↑ X-axis  ↑ legend labels

// Plot
plot(mCurves, ,
     ["CNY vs USD Interest Rate Curves", "Date", "Rate (%)"],
     LINE, false, { multiYAxes: true, autoScaleYAxes: true })

Example 2: 3D Surface Chart (SURFACE) — Volatility Surface by Strike

Implied volatility in FX options isn't flat — it varies across both expiry and strike, forming a complex surface. A 3D surface chart makes this easy to read at a glance: you can immediately see where vol is elevated and where it's suppressed.

In this example, we use optionVolPredict to query 630 IV values across a 30-date × 21-strike grid, then render them as an interactive 3D surface.

// Data preparation
nK = 21; nT = 30
denseStrikes = globalKmin + ... * double(0..(nK - 1))    // 21 evenly-spaced strikes
denseDates = ...                                          // 30 evenly-spaced dates
volMat = optionVolPredict(surf, denseDates, denseStrikes) * 100.0  // 30×21 matrix
volMat.rename!(dateLabels, strikeLabels)
//              ↑ Y-axis (row labels)  ↑ X-axis (col labels)
// Plot
plot(volMat, ,
    ["USDCNY Volatility Surface (Strike, 30×21)", "Strike", "Maturity Date", "Implied Vol (%)"],
    SURFACE)

Example 3: 3D Surface Chart (SURFACE) — Volatility Surface by Delta

FX options traders often prefer to look at volatility through the lens of Delta — the option's sensitivity to the spot rate — rather than strike. The reason is practical: ATM strikes differ across tenors, so using Delta provides a standardized basis for comparison.

Before plotting, we need to build a 9-point Delta grid: 10P, 20P, 30P, 40P, ATM, 40C, 30C, 20C, 10C.

10P20P30P40PATM40C30C20C10C
1M1.851.791.751.721.691.671.641.601.55
3M3.833.403.233.163.143.153.183.253.38
1Y4.454.224.063.973.954.034.224.534.95
3Y4.884.614.424.334.484.684.965.325.78

The X-axis maps to Delta — running from 10P through ATM to 10C across 9 points. The Y-axis covers 9 tenors, and the Z-axis shows implied volatility.

//Plot
plot(deltaIvMat, ,
    ["USDCNY Volatility Surface (Delta, 9×9)", "Delta", "Tenor", "Implied Vol (%)"],
    SURFACE)

Pro tip

Since deltaIvMat already has labels set via rename!, there's no need to pass a labels argument explicitly:

  • Row labels = ["1M","2M",...,"3y"] → Y-axis
  • Column labels = ["10P","20P",...,"10C"] → X-axis
  • Matrix values = IV (%) → Z-axis

To switch between the strike view and the delta view, all you need to change is data and title — the SURFACE call structure stays identical.

Example 4: Line Chart (LINE) — Vol Smile by Strike, Multiple Tenors

Sometimes a full 3D surface is more than you need. If the goal is to compare vol smile shapes across a few representative tenors — how implied volatility curves as you move away from ATM — a simple multi-line chart gets the point across more directly.

In this example, we select 5 tenors and plot each one as a line showing IV against strike.

// Data preparation
pickNames = ["1M","3M","6M","1Y","3Y"]
pickDates = [usedDates[0], usedDates[2], usedDates[3], usedDates[5], usedDates[8]]
strikeMat = optionVolPredict(surf, pickDates, denseStrikes) * 100.0   // 5 rows × 21 cols
mVolStrike = strikeMat.transpose()   // 21 rows × 5 cols: one line per column
mVolStrike.rename!(strikeLabels, pickNames)
//                   ↑ X-axis ticks      ↑ legend labels
// Plot
plot(mVolStrike, ,
    ["Vol vs Strike Smile", "Strike", "Implied Vol (%)"],
    LINE, false, { autoScaleYAxes: true })

Example 5: Line Chart (LINE) — Vol Smile by Delta

In the previous chart, using strike as the X-axis means each tenor's ATM sits at a different horizontal position — the curves shift left and right, making it harder to directly compare the shape of the smile.

Switching to Delta as the X-axis eliminates that problem. ATM always corresponds to Delta = 50 regardless of tenor, so all curves are anchored at the same center point. This makes it much easier to compare smile asymmetry (skew) and curvature across tenors.

// Data preparation
pickIdx = [0, 2, 3, 5, 8]    // indices into usedTerms
mVolDelta = matrix(
    flatten(deltaIvMat[pickIdx[0], ]),    // 9 delta points for row 0 (1M)
    flatten(deltaIvMat[pickIdx[1], ]),    // row 2 (3M)
    flatten(deltaIvMat[pickIdx[2], ]),
    flatten(deltaIvMat[pickIdx[3], ]),
    flatten(deltaIvMat[pickIdx[4], ]))
mVolDelta.rename!(deltaLabels, pickNames)
//                 ↑ X-axis: 10P...10C  ↑ legend: 1M/3M/6M/1Y/3Y
// Plot
plot(mVolDelta, ,
    ["Vol vs Delta Smile", "Delta", "Implied Vol (%)"],
    LINE, false, { autoScaleYAxes: true })

Summary: What Makes DolphinDB's plot() Stand Out

FeatureWhat it gives you
Zero dependenciesNo Python, no matplotlib — plot() ships with DolphinDB out of the box
Finance-nativeMatrices flow directly from factor tables and vol surface objects — no conversion needed
Interactive 3DSURFACE charts support mouse-driven rotation and zoom, great for exploratory analysis
Dual Y-axesCleanly compare series with very different magnitudes on the same chart
End-to-end workflowData querying, model computation, and visualization — all in a single script

Requires DolphinDB 3.00.5 or later.

plot() is a small function with a big idea behind it: keeping computation and visualization in the same place. Whether you're running quant backtests, monitoring risk, or putting together an investment research report, you no longer need to export a CSV and switch to Python just to draw a chart. The entire journey from raw data to insight happens inside one DolphinDB script.

If you'd like to try it yourself, download the community edition or visit our website to explore more financial use cases.

To get the full demo script used in this article, reach out to us on LinkedIn or drop us an email at info@dolphindb.com.