AG Studio: Build dashboards using native components in web apps. Join us for a webinar on 28th July at 2pm UTC+1 Register

Angular Embedded AnalyticsCalendars

Studio groups and analyses date fields through calendars - named time dimensions declared in the data definition alongside your sources. A calendar owns a set of fragments (Year, Month, Day of Week, …) that appear in the Data Panel and can be used like normal fields.

Declaring a Calendar Copy Link

Add a calendars array to the data definition, alongside sources and relationships. Each calendar declares its id, display label, the date range it covers, and the fragments to expose. Range endpoints are literal date strings, epoch-ms numbers, or field references (with aggregation: 'min' | 'max' to detect the range from a column's data):

const studioProperties = {
    data: {
        sources: [{ id: 'sales', data: [...], fields: [...] }],
        calendars: [
            {
                id: 'calendar',
                label: 'Calendar',
                range: { from: { literal: '2022-01-01' }, to: { literal: '2024-12-31' } },
                fragments: [
                    'year', 'quarter', 'month', 'week', 'day', 'monthOfYear', 'dayOfWeek'
                ],
            },
        ],
    },
};

Binding Date Columns Copy Link

Bindings are declared as relationships in the data sources definition. To bind a fact date column to a calendar, add a relationship whose target.calendarId matches the calendar's id:

const studioProperties = {
    data: {
        sources: [...],
        relationships: [
            {
                id: 'sales-calendar',
                source: { tableId: 'sales', fieldId: 'order_date' },
                target: { calendarId: 'calendar' },
            },
        ],
        calendars: [
            {
                id: 'calendar',
                label: 'Calendar',
                range: { from: { literal: '2022-01-01' }, to: { literal: '2024-12-31' } },
                fragments: ['year', 'quarter', 'month'],
            },
        ],
    },
};

When a table has more than one date column, add a separate relationship entry for each column - each relationship binds one column independently.

The grain a binding can reach comes from the bound column's data type. A date column reaches day; a datetime column also reaches hour and minute. Fragments finer than the column supports resolve to empty rather than erroring, so they are only offered where the column's type allows them.

Datetime Columns Copy Link

When binding a datetime column you can truncate it to a coarser grain before it joins the calendar - useful when the time component is noise and you only want to group by day:

const relationships = [
    {
        id: 'events-calendar',
        source: { tableId: 'events', fieldId: 'occurred_at' },  // a datetime column
        target: { calendarId: 'calendar' },
        truncate: 'day',   // drop the time component; group at day grain
    },
];

To group by time of day, declare the sub-day fragments (hour, minute, hourOfDay) on the calendar alongside the date fragments:

const calendar = {
    id: 'calendar',
    label: 'Calendar',
    range: { from: { literal: '2024-01-01' }, to: { literal: '2024-01-31' } },
    fragments: ['day', 'hour', 'minute', 'hourOfDay'],
};

Sub-day grains generate far more spine rows than date grains (minute grain over a single day is 1,440 rows; over a year, more than half a million). Keep the calendar range tight when exposing hour or minute.

Fragment Types Copy Link

Fragments fall into two categories:

  • Chronological fragments - each value is a distinct point in time. E.g. month produces one row per calendar month (January 2023 and January 2024 are separate rows).
  • Cyclic fragments - values are bucketed together. E.g. all Januaries from any year map to the same bucket. These appear under "Seasonality" in the Data Panel.
CategoryFragments
Chronologicalyear, halfYear, quarter, month, week, day, hour, minute
CyclicmonthOfYear, dayOfWeek, monthOfQuarter, weekOfMonth, halfOfYear, hourOfDay

The hour and minute chronological fragments and the hourOfDay cyclic fragment are sub-day grains. They only produce values when the bound column carries a time component - see Datetime Columns.

Using Calendar Fragments in Widgets Copy Link

Reference calendar fragments in dataMapping using the format calendarId::fragmentId:

const studioProperties = {
    initialState: {
        pages: [{
            widgets: {
                'revenue-by-month': {
                    type: 'line-chart',
                    dataMapping: {
                        categoryKey: [{ id: 'calendar::month' }],
                        valueKey: [{ id: 'sales.amount', aggregation: 'sum' }],
                        tooltipKey: [],
                    },
                },
            },
        }],
    },
};

The calendar prefix is the calendar's id. Fragment fields are visible in the data panel under the calendar's name alongside the regular data source tables.

The example above shows three pages:

  • By Year - calendar::year (chronological; one bar per calendar year)
  • By Month - calendar::month (chronological; one line point per calendar month)
  • Seasonality - calendar::monthOfYear and calendar::dayOfWeek (cyclic; all years folded together)

Fragment Reference Copy Link

Each fragment has a default display format and sort order. The table below lists every built-in fragment.

FragmentCategoryDefault formatSortNotes
yearchronologicalyyyyasc
halfYearchronologicalH1 yyyyascH1 = Jan - Jun, H2 = Jul - Dec
quarterchronological"Q"qq yyyyasc
monthchronologicalmmm yyyyasc
weekchronological"W"ww yyyyascISO week (Mon start)
daychronologicald mmm yyyyasc
hourchronologicalyyyy-mm-dd hh:00ascDatetime columns only
minutechronologicalyyyy-mm-dd hh:mmascDatetime columns only
monthOfYearcyclicmmmmascRenders as month name (January - December)
dayOfWeekcyclicdddascISO (1 = Mon)
monthOfQuartercyclic0asc1 - 3
weekOfMonthcyclic0asc1 - 5
halfOfYearcyclicH1ascH1 or H2, all years folded together
hourOfDaycyclic0asc0 - 23, datetime columns only

To override the format for a specific fragment, replace the bare string in fragments with an object:

const calendar = {
    id: 'calendar',
    label: 'Calendar',
    range: { from: { literal: '2022-01-01' }, to: { literal: '2024-12-31' } },
    fragments: [
        'year',
        { unit: 'month', format: 'mmmm yyyy' },   // override: spell out month name
        'monthOfYear',
    ],
}

The format string uses the same Excel-like pattern as field format definitions elsewhere in Studio (d, mmm, yyyy, "Q"q, etc.).

Adopted Calendars Copy Link

If you already maintain a date table, it can be used as-is without needing to generate a new one.

Declare an adopted calendar by setting range.sourceId to the source ID of the date table. Add a mappings array that declares which field in that table provides which calendar fragment:

const calendar = {
    id: 'calendar',
    label: 'Calendar',
    range: {
        sourceId: 'dimDate',
        mappings: [
            { fieldId: 'year_month', components: ['year', 'month'] },  // e.g. yyyymm integer
            { fieldId: 'month_of_yr', components: ['monthOfYear'] },
        ],
    },
}

Studio computes each fragment from the bound fact date column using the same formula as the mapping, so the two sides always agree without an explicit join key.

Sub-year fragment values must be unique within a year - a bare month number of 4 is ambiguous across years. Pair the fragment with year in the same components array. The accessor receives the full source row, so it can read any column - pair from the same packed field, or from two separate columns:

const mappings = [
    // packed: year_month stores yyyymm, e.g. 202304
    {
        fieldId: 'year_month',
        components: ['year', 'month'],
        accessor: (row) => [Math.floor(row.year_month / 100), row.year_month % 100],
    },

    // separate columns: year_num and month_num on the same row
    {
        fieldId: 'month_num',
        components: ['year', 'month'],
        accessor: (row) => [row.year_num, row.month_num],
    },
];

The components order and the accessor's return order match positionally. Two cases to watch:

  • day must include year and month - e.g. a yyyymmdd field unpacked as [year, month, day].
  • week must pair with the ISO week-numbering year, not the calendar year - they diverge in the days around 1 January.

Packed Columns and Accessors Copy Link

When a single field encodes several components (such as a packed yyyymmdd integer), add an accessor function to the mapping. It receives the full data row and returns the component values in the order declared by components:

const calendar = {
    id: 'calendar',
    label: 'Calendar',
    range: {
        sourceId: 'dimDate',
        mappings: [
            {
                fieldId: 'yyyymmdd',
                components: ['year', 'month'],
                accessor: (row) => [
                    Math.floor(row.yyyymmdd / 10000),
                    Math.floor((row.yyyymmdd % 10000) / 100),
                ],
            },
            { fieldId: 'month_of_year', components: ['monthOfYear'] },
        ],
    },
}

Binding to an Adopted Calendar Copy Link

Bind fact date columns to an adopted calendar the same way as a generated calendar - add a relationship with target.calendarId set to the adopted calendar's id:

const relationships = [
    {
        id: 'sales-calendar',
        source: { tableId: 'sales', fieldId: 'order_date' },
        target: { calendarId: 'calendar' },
    },
],

The calendar type is inferred from its shape - a calendar whose range has a sourceId property is adopted; one without is generated.

The example above uses a pre-built date table with a packed yyyymmdd integer and a month_of_year column. The calendar unpacks year and month from yyyymmdd via an accessor, and reads month_of_year directly. Widgets reference those fragments with the same calendarId::fragmentId syntax as generated calendars. The fourth page shows the raw date table columns for reference.

The same pattern works for string-encoded dates. If the date table stores months as "YYYY-MM" strings, split and parse in the accessor:

{
    fieldId: 'year_month',
    components: ['year', 'month'],
    accessor: (row) => {
        const [y, m] = (row.year_month as string).split('-');
        return [parseInt(y, 10), parseInt(m, 10)];
    },
}

The example above uses a date table where months are stored as "YYYY-MM" strings. The accessor splits the string and returns [year, month] as integers. The month_of_year column is read directly with no accessor needed.