Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions docs-mintlify/docs/explore-analyze/charts/chart-types/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,37 @@ Click the dropdown arrow on any field in the **Fields** section to configure it:
| **Word wrap** | Allow cell content to wrap to multiple lines |
| **Hide** | Show or hide the column |

## Inline bars
## Display tab — showing columns as links, images, bars, or sparklines

By default, columns display their raw value. The **Display** tab for each field lets you change this:

### Links

Display a field's value as a clickable hyperlink. To create dynamic per-row links:

1. Add a [calculated field](/docs/explore-analyze/workbooks/calculated-fields) that produces a URL — for example:
```
CONCAT("https://example.com/orders/", order_items.order_id)
```
2. In the table visualization, hide the calculated field column.
3. In the **Display** tab for the field you want to link, set **Display as** to **Link** and select the hidden URL field as the source.

{/* TODO screenshot: column display dropdown showing Link option selected (hidden — replace this comment with <Frame><img src="..." /></Frame> when image is ready) */}

### Images

Display a field as an image by setting **Display as** to **Image**. Configure height and width. To make the image a link, check **Link image** and set the **Link URL**.

The URL must be publicly accessible without authentication.

### Inline bars

Display a numeric column as a proportional in-cell bar. In the column's per-column section on the **Style** tab, use the **Display as** control to add a bar. Each bar's length reflects the value's magnitude within the column's range.

| Option | Description |
|---|---|
| **Display as** | Choose **Value**, **Bar**, or both. Checking **Bar** reveals the bar options below; keeping **Value** checked shows the number alongside the bar (uncheck it for a bar-only cell). |
| **Display as** | Choose **Value**, **Inline bars**, or **Sparkline** — a single choice; the modes are mutually exclusive. Selecting **Inline bars** reveals the bar options below. |
| **Show value** | Show the formatted number alongside the bar. Turn it off for a bar-only cell. |
| **Positive bar color** | Fill color for non-negative bars |
| **Negative bar color** | Fill color for negative bars (used when the column contains both positive and negative values) |
| **Bar scale** | **Auto** anchors each bar at zero and scales to the column's largest value, so even the smallest value still shows a bar; **Manual** scales against the bounds you set |
Expand All @@ -65,6 +89,38 @@ When a column contains both positive and negative values, bars are drawn in both

{/* TODO screenshot: table with inline bar column (hidden — replace this comment with <Frame><img src="..." /></Frame> when image is ready) */}

### Sparklines

Display a numeric column as a **sparkline** — a mini trend chart in each cell that plots the measure across a time dimension. In the column's per-column section on the **Style** tab, set **Display as** to **Sparkline**.

A sparkline needs a time dimension to use as its horizontal axis. When you switch a column to **Sparkline** and pick its horizontal axis time dimension, that dimension is **removed from the table query** (if it was there): the table shows one row per remaining dimension, correctly aggregated, while the sparkline plots the measure's value across the time dimension. Values are always correct for any measure type, including counts of distinct values, averages, and custom measures.

<Note>

Internally, sparklines are powered by additional queries grouped by time dimension and granularity: measures sharing the same dimension and granularity are fetched together, so a chart with several sparklines runs at most one extra query per distinct dimension and granularity combination.

</Note>

| Option | Description |
|---|---|
| **Display as** | Set to **Sparkline**. Available only for numeric columns, and only when the query has a time dimension. |
| **Horizontal axis** | The time dimension plotted along the sparkline. Auto-selected (the first time dimension in the query); change it here when the query has several. |
| **Granularity** | The time bucket for the horizontal axis. Defaults to the dimension's granularity in the query if present, otherwise month. |
| **Type** | **Area** (filled line, the default), **Line**, or **Bar** (mini columns). |
| **Line color** / **Area color** | Stroke color for line and bar; fill color for area. |
| **Line width** | Stroke width in pixels (Line and Area types). |
| **Show value** | Show the most recent value as a headline number next to the sparkline. Turn it off for a chart-only cell. |

A row needs at least two data points to draw a sparkline; cells with fewer fall back to the formatted value.

<Note>

A granularity that is too fine for the data's time span (e.g. by the second over several years) makes each background query return many rows, which can make a table with sparklines slow to load for end users. Pick the coarsest granularity that still shows the trend you need.

</Note>

{/* TODO screenshot: table with a sparkline column (hidden — replace this comment with <Frame><img src="..." /></Frame> when image is ready) */}

## Cell menu

Left-clicking a table cell opens a context menu with any [`links` defined on the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { PostgresQuery } from '../../../src/adapter/PostgresQuery';
import { prepareJsCompiler } from '../../unit/PrepareCompiler';
import { dbRunner } from './PostgresDBRunner';

// Rolling window measures queried WITHOUT a time dimension granularity (only a
// dateRange). The window is anchored by `offset`: 'start' anchors at the period
// start, 'end' anchors at the period end. With no granularity the result is a
// single aggregate row.
//
// The seed visitors table has one row dated 2016-09-07 (before the queried
// ranges) which distinguishes offset:'start' (accumulate everything before the
// period start) from offset:'end' (accumulate everything up to the period end).
describe('Rolling window offset without granularity', () => {
jest.setTimeout(200000);

const { compiler, joinGraph, cubeEvaluator } = prepareJsCompiler(`
cube(\`balances\`, {
sql: \`select * from visitors\`,

measures: {
// trailing: 'unbounded'
begBalance: {
type: 'sum',
sql: 'amount',
rollingWindow: { trailing: 'unbounded', offset: 'start' }
},
endBalance: {
type: 'sum',
sql: 'amount',
rollingWindow: { trailing: 'unbounded', offset: 'end' }
},

// leading: 'unbounded'
leadingStart: {
type: 'sum',
sql: 'amount',
rollingWindow: { leading: 'unbounded', offset: 'start' }
},
leadingEnd: {
type: 'sum',
sql: 'amount',
rollingWindow: { leading: 'unbounded', offset: 'end' }
},

// finite trailing interval
trailing5Start: {
type: 'sum',
sql: 'amount',
rollingWindow: { trailing: '5 day', offset: 'start' }
},
trailing5End: {
type: 'sum',
sql: 'amount',
rollingWindow: { trailing: '5 day', offset: 'end' }
},
},

dimensions: {
id: {
type: 'number',
sql: 'id',
primaryKey: true
},
createdAt: {
type: 'time',
sql: 'created_at'
},
},
})

cube(\`balances_fp\`, {
sql: \`select * from visitors WHERE \${FILTER_PARAMS.balances_fp.createdAt.filter('created_at')}\`,

measures: {
begBalance: {
type: 'sum',
sql: 'amount',
rollingWindow: { trailing: 'unbounded', offset: 'start' }
},
},

dimensions: {
id: {
type: 'number',
sql: 'id',
primaryKey: true
},
createdAt: {
type: 'time',
sql: 'created_at'
},
},
})
`);

const runQuery = async (measures: string[], dateRange: [string, string]) => {
const query = new PostgresQuery(
{ joinGraph, cubeEvaluator, compiler },
{
measures,
timeDimensions: [
{
dimension: 'balances.createdAt',
dateRange,
},
],
timezone: 'UTC',
}
);

const queryAndParams = query.buildSqlAndParams();
return dbRunner.testQuery(queryAndParams);
};

it('trailing: unbounded — offset start vs end', () => compiler.compile().then(async () => {
// beg: amount where created_at < 2017-01-01 -> only the 2016-09-07 row (500)
// end: amount where created_at <= 2017-01-30 -> all rows (2000)
expect(await runQuery(
['balances.begBalance', 'balances.endBalance'],
['2017-01-01', '2017-01-30']
)).toEqual([
{ balances__beg_balance: '500', balances__end_balance: '2000' },
]);
}));

it('leading: unbounded — offset start vs end', () => compiler.compile().then(async () => {
// start: amount where created_at >= 2017-01-01 -> all 2017 rows in/after range (1500)
// end: amount where created_at > 2017-01-30 -> none (null)
expect(await runQuery(
['balances.leadingStart', 'balances.leadingEnd'],
['2017-01-01', '2017-01-30']
)).toEqual([
{ balances__leading_start: '1500', balances__leading_end: null },
]);
}));

it('finite trailing interval — offset start vs end', () => compiler.compile().then(async () => {
// range 2017-01-06 .. 2017-01-10
// start: created_at in [from - 5d, from) = [2017-01-01, 2017-01-06) -> 100 + 200 = 300
// end: created_at in (to - 5d, to] = (2017-01-05, 2017-01-10] -> 300 + 400 + 500 = 1200
expect(await runQuery(
['balances.trailing5Start', 'balances.trailing5End'],
['2017-01-06', '2017-01-10']
)).toEqual([
{ balances__trailing5_start: '300', balances__trailing5_end: '1200' },
]);
}));

// FILTER_PARAMS on the time dimension must receive the date-range bounds, not
// the rolling window config — the window's trailing/leading/offset must never
// leak into the filter as query parameters.
it('FILTER_PARAMS does not leak rolling window config into params', () => compiler.compile().then(async () => {
const query = new PostgresQuery(
{ joinGraph, cubeEvaluator, compiler },
{
measures: ['balances_fp.begBalance'],
timeDimensions: [
{
dimension: 'balances_fp.createdAt',
dateRange: ['2017-01-01', '2017-01-30'],
},
],
timezone: 'UTC',
}
);

const [, params] = query.buildSqlAndParams();
expect(params).not.toContain('unbounded');
expect(params).not.toContain('start');
expect(params).not.toContain('end');

// Sanity check: the query still executes.
await dbRunner.testQuery(query.buildSqlAndParams());
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,57 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL
}
]));

it('member to alias for granularized time dimension', async () => runQueryTest({
measures: [
'visitors.visitor_revenue',
'visitors.visitor_count',
'visitors.per_visitor_revenue'
],
dimensions: [
'visitors.source'
],
timeDimensions: [{
dimension: 'visitors.created_at',
dateRange: ['2017-01-01', '2017-01-30'],
granularity: 'month'
}],
timezone: 'America/Los_Angeles',
// SQL API keys a granularized override `{member}.{granularity}` (dotted) and
// references the CubeScan column by it. The native planner must honor it
// instead of defaulting to `{base alias}_{granularity}`.
memberToAlias: {
'visitors.visitor_revenue': 'custom_revenue',
'visitors.visitor_count': 'custom_count',
'visitors.source': 'custom_source',
'visitors.created_at.month': 'custom_created_at_month',
},
order: []
},

[
{
custom_source: 'google',
custom_created_at_month: '2017-01-01T00:00:00.000Z',
custom_revenue: null,
custom_count: '1',
visitors__per_visitor_revenue: null
},
{
custom_source: 'some',
custom_created_at_month: '2017-01-01T00:00:00.000Z',
custom_revenue: '300',
custom_count: '2',
visitors__per_visitor_revenue: '150'
},
{
custom_source: null,
custom_created_at_month: '2017-01-01T00:00:00.000Z',
custom_revenue: null,
custom_count: '2',
visitors__per_visitor_revenue: null
}
]));

it('running total', async () => {
await compiler.compile();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::{FilterOperationSql, FilterSqlContext};
use crate::planner::filter::operators::rolling_window::RegularRollingWindowOp;
use crate::planner::filter::operators::rolling_window::{
RegularRollingWindowOp, RollingWindowOffsetOp,
};
use cubenativeutils::CubeError;

impl FilterOperationSql for RegularRollingWindowOp {
Expand All @@ -22,3 +24,51 @@ impl FilterOperationSql for RegularRollingWindowOp {
}
}
}

impl FilterOperationSql for RollingWindowOffsetOp {
fn to_sql(&self, ctx: &FilterSqlContext) -> Result<String, CubeError> {
let from_start = self.offset == "start";
let member = ctx.member_sql.to_string();

// Anchor: range start (formatted to start-of-day) for offset 'start',
// range end (formatted to end-of-day) for 'end'. Both bounds share it.
let anchor = if from_start {
let from = self.from.as_deref().ok_or_else(|| {
CubeError::internal("Rolling window date range is missing its start".to_string())
})?;
ctx.format_and_allocate_from_date(from)?
} else {
let to = self.to.as_deref().ok_or_else(|| {
CubeError::internal("Rolling window date range is missing its end".to_string())
})?;
ctx.format_and_allocate_to_date(to)?
};

let mut conditions = Vec::new();

// trailing side -> lower bound (shifted back by the trailing interval;
// `unbounded` drops the bound, `None` keeps the anchor unshifted).
if let Some(bound) = ctx.extend_date_range_bound(anchor.clone(), &self.trailing, true)? {
conditions.push(if from_start {
ctx.plan_templates.gte(member.clone(), bound)?
} else {
ctx.plan_templates.gt(member.clone(), bound)?
});
}

// leading side -> upper bound (shifted forward by the leading interval).
if let Some(bound) = ctx.extend_date_range_bound(anchor.clone(), &self.leading, false)? {
conditions.push(if from_start {
ctx.plan_templates.lt(member.clone(), bound)?
} else {
ctx.plan_templates.lte(member.clone(), bound)?
});
}

if conditions.is_empty() {
ctx.plan_templates.always_true()
} else {
Ok(conditions.join(" AND "))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ impl TypedFilter {
}
FilterParamsColumn::Callback(callback) => {
let args = match self.operation() {
FilterOp::DateRange(_) | FilterOp::DateSingle(_) => {
// RollingWindowOffset carries [from, to, trailing, leading, offset];
// only the from/to dates are filter-param args for the callback.
FilterOp::DateRange(_)
| FilterOp::DateSingle(_)
| FilterOp::RollingWindowOffset(_) => {
let ctx = FilterSqlContext {
member_sql: "",
query_tools,
Expand Down Expand Up @@ -115,6 +119,7 @@ fn dispatch_to_sql(op: &FilterOp, ctx: &FilterSqlContext) -> Result<String, Cube
}
FilterOp::Nullability(op) => op.to_sql(ctx),
FilterOp::RegularRollingWindow(op) => op.to_sql(ctx),
FilterOp::RollingWindowOffset(op) => op.to_sql(ctx),
FilterOp::ToDateRollingWindow(op) => op.to_sql(ctx),
}
}
Loading
Loading