|
| 1 | +--- |
| 2 | +title: Visualization functions |
| 3 | +sidebar_label: Visualization |
| 4 | +description: SQL functions for rendering inline charts in query results using Unicode block characters. |
| 5 | +--- |
| 6 | + |
| 7 | +Visualization functions render numeric data as compact Unicode block charts |
| 8 | +directly in query results. The output is a `varchar` cell that works everywhere: |
| 9 | +psql, the web console, JDBC clients, CSV exports. |
| 10 | + |
| 11 | +| Function | Type | Description | |
| 12 | +| :------- | :--- | :---------- | |
| 13 | +| [bar](#bar) | Scalar | Horizontal bar proportional to a value within a range | |
| 14 | +| [sparkline](#sparkline) | Aggregate | Vertical block chart of values within a group | |
| 15 | + |
| 16 | +## bar |
| 17 | + |
| 18 | +`bar(value, min, max, width)` - Renders a single numeric value as a horizontal |
| 19 | +bar. The bar is made of full block characters with a fractional block at the end |
| 20 | +for sub-character precision. |
| 21 | + |
| 22 | +Characters used (varying width): |
| 23 | + |
| 24 | +``` |
| 25 | +▏▎▍▌▋▊▉█ |
| 26 | +``` |
| 27 | + |
| 28 | +Characters range from `▏` (1/8 fill, U+258F) to `█` (full fill, U+2588). A |
| 29 | +`width` of 20 characters gives 160 discrete levels of resolution (20 x 8). |
| 30 | + |
| 31 | +Since `bar` is a scalar function, it can wrap aggregates like `sum()`, `avg()`, |
| 32 | +or `count()` to visualize their results inline. |
| 33 | + |
| 34 | +#### Parameters |
| 35 | + |
| 36 | +All four arguments are required: |
| 37 | + |
| 38 | +- `value` is any numeric value. Implicitly cast to `double`. `NULL` produces |
| 39 | + `NULL` output. |
| 40 | +- `min` (`double`): the value that maps to an empty bar (zero length). |
| 41 | +- `max` (`double`): the value that maps to a full bar (`width` characters). |
| 42 | +- `width` (`int`): the number of characters at `max` value. |
| 43 | + |
| 44 | +Values below `min` are clamped to an empty bar. Values above `max` are clamped |
| 45 | +to a full bar of `width` characters. If `min`, `max`, or `width` are `NULL`, or |
| 46 | +if `min >= max`, the function returns `NULL`. |
| 47 | + |
| 48 | +#### Return value |
| 49 | + |
| 50 | +Return value type is `varchar`. |
| 51 | + |
| 52 | +#### Examples |
| 53 | + |
| 54 | +```questdb-sql demo title="Visualize aggregated volume per minute" |
| 55 | +SELECT timestamp, symbol, |
| 56 | + round(sum(amount), 2) total, |
| 57 | + bar(sum(amount), 0, 50, 30) |
| 58 | +FROM trades |
| 59 | +WHERE symbol IN ('BTC-USDT', 'ETH-USDT') |
| 60 | +SAMPLE BY 1m |
| 61 | +LIMIT -10; |
| 62 | +``` |
| 63 | + |
| 64 | +```questdb-sql demo title="Per-symbol scaling with window functions" |
| 65 | +SELECT timestamp, symbol, round(total, 2) total, |
| 66 | + bar(total, min(total) OVER (PARTITION BY symbol), |
| 67 | + max(total) OVER (PARTITION BY symbol), 30) |
| 68 | +FROM ( |
| 69 | + SELECT timestamp, symbol, sum(amount) total |
| 70 | + FROM trades |
| 71 | + WHERE symbol IN ('BTC-USDT', 'ETH-USDT') |
| 72 | + SAMPLE BY 1m |
| 73 | +) |
| 74 | +LIMIT -10; |
| 75 | +``` |
| 76 | + |
| 77 | +| timestamp | symbol | total | bar | |
| 78 | +| :-------------------------- | :------- | :----- | :----------------- | |
| 79 | +| 2026-03-06T17:18:00.000000Z | ETH-USDT | 72.94 | ██ | |
| 80 | +| 2026-03-06T17:18:00.000000Z | BTC-USDT | 6.76 | ██████ | |
| 81 | +| 2026-03-06T17:19:00.000000Z | ETH-USDT | 118.19 | ███ | |
| 82 | +| 2026-03-06T17:19:00.000000Z | BTC-USDT | 1.59 | █ | |
| 83 | +| 2026-03-06T17:20:00.000000Z | ETH-USDT | 246.87 | ███████ | |
| 84 | +| 2026-03-06T17:20:00.000000Z | BTC-USDT | 14.36 | █████████████ | |
| 85 | +| 2026-03-06T17:21:00.000000Z | BTC-USDT | 2.9 | ██ | |
| 86 | +| 2026-03-06T17:21:00.000000Z | ETH-USDT | 375.3 | ██████████ | |
| 87 | +| 2026-03-06T17:22:00.000000Z | BTC-USDT | 8.07 | ███████ | |
| 88 | +| 2026-03-06T17:22:00.000000Z | ETH-USDT | 529.74 | ███████████████ | |
| 89 | + |
| 90 | +Each symbol's bars scale independently because `PARTITION BY symbol` gives each |
| 91 | +its own min/max range. |
| 92 | + |
| 93 | +```questdb-sql demo title="Global scaling across all symbols" |
| 94 | +SELECT timestamp, symbol, round(total, 2) total, |
| 95 | + bar(total, min(total) OVER (), |
| 96 | + max(total) OVER (), 30) |
| 97 | +FROM ( |
| 98 | + SELECT timestamp, symbol, sum(amount) total |
| 99 | + FROM trades |
| 100 | + WHERE symbol IN ('BTC-USDT', 'ETH-USDT') |
| 101 | + SAMPLE BY 1m |
| 102 | +) |
| 103 | +LIMIT -10; |
| 104 | +``` |
| 105 | + |
| 106 | +All symbols share the same min/max, making bars comparable across groups. |
| 107 | + |
| 108 | +```questdb-sql demo title="Inline with row-level data" |
| 109 | +SELECT symbol, price, |
| 110 | + bar(price, 0, 100000, 25) |
| 111 | +FROM trades |
| 112 | +LATEST ON timestamp PARTITION BY symbol; |
| 113 | +``` |
| 114 | + |
| 115 | +#### See also |
| 116 | + |
| 117 | +- [sparkline](#sparkline) - Aggregate trend chart |
| 118 | + |
| 119 | +## sparkline |
| 120 | + |
| 121 | +`sparkline(value)` or `sparkline(value, min, max, width)` - Collects numeric |
| 122 | +values within a group and renders them as a compact vertical block chart. Each |
| 123 | +value maps to one character. Best for showing trends, cycles, and spikes. |
| 124 | + |
| 125 | +Characters used (varying height): |
| 126 | + |
| 127 | +``` |
| 128 | +▁▂▃▄▅▆▇█ |
| 129 | +``` |
| 130 | + |
| 131 | +Characters range from `▁` (lowest, U+2581) to `█` (highest, U+2588), giving 8 |
| 132 | +levels of vertical resolution per character. |
| 133 | + |
| 134 | +Since `sparkline` is an aggregate, it pairs naturally with |
| 135 | +[SAMPLE BY](/docs/query/sql/sample-by/) to show intra-bucket trends. |
| 136 | + |
| 137 | +The input can be any numeric type (`double`, `int`, `long`, `short`, `float`) - |
| 138 | +it is implicitly cast to `double`. |
| 139 | + |
| 140 | +#### Parameters |
| 141 | + |
| 142 | +- `value` is any numeric value. Each value produces one character in the output. |
| 143 | +- `min` (optional, `double`): lower bound for scaling. Pass `NULL` to |
| 144 | + auto-compute from data. Values below `min` are clamped to the lowest |
| 145 | + character. |
| 146 | +- `max` (optional, `double`): upper bound for scaling. Pass `NULL` to |
| 147 | + auto-compute from data. Values above `max` are clamped to the highest |
| 148 | + character. |
| 149 | +- `width` (optional, `int`, constant): maximum number of output characters. When |
| 150 | + the group has more values than `width`, the function sub-samples by dividing |
| 151 | + values into equal buckets and averaging each bucket. Must be a positive |
| 152 | + integer. |
| 153 | + |
| 154 | +`min` and `max` can each independently be `NULL`, allowing partial auto-scaling. |
| 155 | +For example, `sparkline(price, 0, NULL, 24)` fixes the floor at 0 but |
| 156 | +auto-computes the ceiling from the data. |
| 157 | + |
| 158 | +#### Return value |
| 159 | + |
| 160 | +Return value type is `varchar`. |
| 161 | + |
| 162 | +#### Null handling |
| 163 | + |
| 164 | +- `NULL` input values are silently skipped. |
| 165 | +- If all values in a group are `NULL`, the function returns `NULL`. |
| 166 | +- An empty group (no rows) also returns `NULL`. |
| 167 | +- When all values are identical (`min` equals `max`), every character renders as |
| 168 | + `█`, signaling "no variation". |
| 169 | + |
| 170 | +#### Examples |
| 171 | + |
| 172 | +```questdb-sql demo title="Hourly price trends with sub-sampling" |
| 173 | +SELECT timestamp, symbol, |
| 174 | + round(avg(price), 0) avg_price, |
| 175 | + sparkline(price, NULL, NULL, 20) |
| 176 | +FROM trades |
| 177 | +WHERE symbol IN ('BTC-USDT', 'ETH-USDT') |
| 178 | + AND timestamp IN '2026-03-07' |
| 179 | +SAMPLE BY 1h |
| 180 | +LIMIT 10; |
| 181 | +``` |
| 182 | + |
| 183 | +| timestamp | symbol | avg_price | sparkline | |
| 184 | +| :-------------------------- | :------- | :-------- | :------------------- | |
| 185 | +| 2026-03-07T00:00:00.000000Z | BTC-USDT | 68229 | ▄▄▄▄▄▄▃▂▁▁▂▃▃▄▆▇▇▇▇▇ | |
| 186 | +| 2026-03-07T00:00:00.000000Z | ETH-USDT | 1981 | ▆▅▄▅▅▄▅▅▆▆▆▄▂▂▂▄▇▆▇▇ | |
| 187 | +| 2026-03-07T01:00:00.000000Z | BTC-USDT | 68239 | ▇▅▃▃▂▃▃▂▂▂▂▁▁▁▂▃▃▃▅▅ | |
| 188 | +| 2026-03-07T01:00:00.000000Z | ETH-USDT | 1979 | ▇▅▃▃▃▃▂▁▁▃▄▃▂▂▃▂▂▁▂▅ | |
| 189 | +| 2026-03-07T02:00:00.000000Z | BTC-USDT | 68182 | ▇▇▇▆▆▆▄▃▂▃▂▂▂▁▂▄▅▆▆▆ | |
| 190 | +| 2026-03-07T02:00:00.000000Z | ETH-USDT | 1978 | ▆▆▅▄▃▃▃▃▃▂▂▂▃▅▅▆▆▇▇▇ | |
| 191 | +| 2026-03-07T03:00:00.000000Z | BTC-USDT | 68286 | ▇▆▆▆▅▅▅▅▅▄▄▃▂▂▃▃▃▁▁▁ | |
| 192 | +| 2026-03-07T03:00:00.000000Z | ETH-USDT | 1986 | ▁▄▇▇▇▆▆▅▅▅▅▅▃▃▃▄▃▂▂▂ | |
| 193 | +| 2026-03-07T04:00:00.000000Z | ETH-USDT | 1973 | ▁▁▂▂▃▃▃▄▄▅▇▇▇▇▇▇▆▆▆▆ | |
| 194 | +| 2026-03-07T04:00:00.000000Z | BTC-USDT | 68026 | ▁▃▃▃▃▄▄▄▄▅▅▅▇▇▇▇▇▇▆▆ | |
| 195 | + |
| 196 | +The `width` of 20 sub-samples each hour's tick data into 20 characters, |
| 197 | +regardless of how many ticks exist within each bucket. |
| 198 | + |
| 199 | +```questdb-sql demo title="Compare intra-day trends across symbols" |
| 200 | +SELECT symbol, sparkline(price) |
| 201 | +FROM trades |
| 202 | +WHERE timestamp IN '2026-03-07' |
| 203 | +SAMPLE BY 1h; |
| 204 | +``` |
| 205 | + |
| 206 | +```questdb-sql demo title="Fixed scale for cross-symbol comparison" |
| 207 | +SELECT symbol, sparkline(amount, 0, 1000000, 24) |
| 208 | +FROM trades |
| 209 | +SAMPLE BY 1d |
| 210 | +LIMIT -5; |
| 211 | +``` |
| 212 | + |
| 213 | +This ensures 0 is always `▁` and 1,000,000 is always `█` across all symbols, |
| 214 | +making the sparklines visually comparable. |
| 215 | + |
| 216 | +```questdb-sql demo title="Partial auto-scaling with fixed floor" |
| 217 | +SELECT symbol, sparkline(price, 0, NULL, 24) |
| 218 | +FROM trades |
| 219 | +SAMPLE BY 1d |
| 220 | +LIMIT -5; |
| 221 | +``` |
| 222 | + |
| 223 | +Fixes the floor at 0 but auto-computes the ceiling from the data. |
| 224 | + |
| 225 | +#### Clamping |
| 226 | + |
| 227 | +When explicit `min`/`max` are provided, out-of-range values are clamped: |
| 228 | + |
| 229 | +- A value below `min` renders as `▁` (clamped to floor) |
| 230 | +- A value above `max` renders as `█` (clamped to ceiling) |
| 231 | +- Values are never silently dropped |
| 232 | + |
| 233 | +#### Limitations |
| 234 | + |
| 235 | +- **Sub-sampling averages buckets.** When `width` is smaller than the number of |
| 236 | + collected values, the function divides values into equal buckets and averages |
| 237 | + each. This smooths spikes. If preserving peaks is important, use a `width` |
| 238 | + equal to or greater than the expected number of values. |
| 239 | + |
| 240 | +- **Limited FILL support.** When used with `SAMPLE BY`, `sparkline` supports |
| 241 | + `FILL(NULL)`, `FILL(NONE)`, and `FILL(PREV)`. `FILL(LINEAR)` and |
| 242 | + `FILL(value)` are not supported. |
| 243 | + |
| 244 | +#### See also |
| 245 | + |
| 246 | +- [bar](#bar) - Scalar horizontal bar |
| 247 | +- [Aggregate functions](/docs/query/functions/aggregation/) - Full aggregate |
| 248 | + reference |
| 249 | +- [SAMPLE BY](/docs/query/sql/sample-by/) - Time-series aggregation |
| 250 | + |
| 251 | +## Configuration |
| 252 | + |
| 253 | +Both functions enforce a maximum output size controlled by an existing server |
| 254 | +property: |
| 255 | + |
| 256 | +```ini |
| 257 | +# server.conf |
| 258 | +cairo.sql.string.function.buffer.max.size=1048576 |
| 259 | +``` |
| 260 | + |
| 261 | +Default is 1,048,576 bytes (1 MB). This is the same property used by |
| 262 | +`string_agg()`, `lpad()`, and `rpad()`. |
| 263 | + |
| 264 | +Each output character is 3 bytes in UTF-8, so the default allows up to 349,525 |
| 265 | +characters of output. For `sparkline`, this limits the number of values |
| 266 | +accumulated per group. For `bar`, this limits the `width` parameter. If the |
| 267 | +limit is exceeded, the function throws a non-critical error. |
| 268 | + |
| 269 | +In practice these limits are generous - a sparkline or bar of 349K characters |
| 270 | +would be unusable. The limit exists to prevent accidental memory exhaustion. |
0 commit comments