Skip to content

SquareX: the measure language

SquareX is DataSquares’ measure language. You define a measure once, at the data-model level, and it computes consistently everywhere the model is used — charts, dashboards, reports, exports, and the API. If you know Power BI’s DAX or Tableau’s LOD expressions, SquareX gives you the same class of power with a deliberately smaller, plainer surface: about 25 functions, no row context, and everything compiles to SQL that runs in your own database.

A measure is a name, :=, and an expression:

Revenue := Sum(Sales.Amount)
Orders := CountDistinct(Sales.OrderId)
AvgOrderValue := Divide([Revenue], [Orders])

Three things to notice:

  • Columns are plain dotted paths: Sales.Amount. No quotes, no brackets.
  • Measures reference each other in square brackets: [Revenue]. Build small measures and compose them.
  • Divide is safe by default — division by zero gives blank (NULL), not an error or infinity. The common intent is the default, not a gotcha.

Once saved, measures appear under ƒx Measures in the field tree — drag one onto a chart like any field, and it aggregates correctly at whatever grain the chart uses.

Filter context, and changing it with Calculate

Section titled “Filter context, and changing it with Calculate”

A measure always evaluates inside the current filter context — whatever dashboard filters, chart filters, and grouping apply where it’s used. That’s what makes one definition reusable everywhere.

Calculate is the one function that modifies that context:

// Add a filter (intersects with whatever filters are already active)
OpenSRCount := Calculate(
Count(ServiceRequest.Id),
ServiceRequest.Status = "Open"
)
// Remove filters — the classic %-of-total denominator
PctOfTotalRevenue := Divide(
[Revenue],
Calculate([Revenue], RemoveFilters(Product, Customer))
)
  • A bare predicate adds a filter. It intersects with the outer context — if the dashboard filters Region to “East” and your measure adds Region = "West", the result is empty, not overridden.
  • RemoveFilters(…) lifts the named dimensions’ user filters (and their grouping), which is how you get totals. RemoveFilters() with no arguments is the grand total.
  • To override a filter rather than intersect with it, remove then add — removals always apply first, regardless of the order you write them:
// West revenue regardless of any dashboard Region filter
WestRevenue := Calculate([Revenue], Sales.Region = "West", RemoveFilters(Sales.Region))

One important guarantee: RemoveFilters can never touch row-level security. RLS filters are structural and sit beneath everything a measure can express.

Fixed evaluates an expression at an explicit grain, regardless of the chart’s grouping — Tableau users know this as a FIXED level-of-detail expression:

CustomerRevenue := Fixed(Customer.Id, [Revenue])
// Date columns take an inline grain — no truncation boilerplate
CustomersPerMonth := Fixed(Date.Date:month, CountDistinct(Sales.CustomerId))

Put [CustomerRevenue] on a by-region chart with aggregation avg and you get average revenue per customer per region. One deliberate difference from Tableau: SquareX Fixed respects the filters active in the view — filters “just work” on it, no context-filter promotion required.

// Same period last year
RevenueLY := Calculate([Revenue], DateShift(Date.Date, -1, "year"))
// Year-to-date accumulation
RevenueYTD := YTD([Revenue], Date.Date)
// 12-month rolling total (missing months count as zero, not skipped)
Revenue12mRolling := RollingSum([Revenue], 12, "month")

Combine them for the standard comparison set: [Revenue], [RevenueLY], and Divide([Revenue] - [RevenueLY], [RevenueLY]) for year-over-year growth.

One correctness guard worth knowing: YTD and RollingSum only accept additive expressions (sums and counts, and combinations of them). Asking for YTD([AvgOrderValue]) is rejected — a running sum of ratios is silently wrong — and the fix that says what you mean is Divide(YTD([Revenue]), YTD([Orders])).

// Searched conditions — first true wins, no SWITCH(TRUE()) idiom needed
RevenueTier := Case(
[Revenue] >= 1000000, "Tier 1",
[Revenue] >= 100000, "Tier 2",
"Tier 3")
// Match a value
OrderVolumeBand := Switch([Orders], 0, "None", 1, "Single", "Multi")
// Name a sub-expression once, compute it once
MarginPct := Let(rev, [Revenue],
Divide(rev - Sum(Sales.Cost), rev))

Iif(cond, then, else) covers the simple two-branch case, and // comments work anywhere — measures can document themselves.

In the model editor, every measure can be authored two ways:

  • Template builder — forms for the common shapes: aggregation, ratio, % of total, filtered measure, time comparison, to-date, rolling window, fixed-grain, branching. No syntax to learn; the builder shows the generated code as you go.
  • Code editor — full SquareX with autocomplete (tables, columns, measures, functions), inline diagnostics as you type, format-on-save, and a compiled-SQL preview so you can see exactly what will run.

Both edit the same definition. A measure built with templates opens in the builder; one that uses code-only shapes (like Let) shows a read-only summary with an Edit as code path.

Things SquareX deliberately doesn’t have

Section titled “Things SquareX deliberately doesn’t have”
  • No row context. Everything in a measure is aggregate-level; a bare column outside an aggregation is an error that points you to calculated fields, which is where row-level expressions live. This removes the single most confusing concept in DAX.
  • No 250-function catalog. The surface is ~25 curated functions; the escape hatch for exotic needs is SQL, not more functions.
  • SQL semantics for NULL and types, not invented ones: aggregates skip NULLs, NULL + 5 is NULL (use Coalesce([Measure], 0) when you want zeros), and there’s no implicit string↔number coercion — format numbers with ToText before concatenating with &.