PBToolboxAI v3 ← Site

crosstab — u_pbt_crosstab #

← Component reference · Guide contents

A complete crosstab table: row / column / value areas fed from a DataStore, aggregates, filters, conditional formatting, date grouping, calculated measures, and CSV / Excel exports.

▶ See it live — Demo application, Crosstab tile: the preview, the code behind it and this page, side by side.


At a glance #

Userobjectu_pbt_crosstab
Item class— (fields are placed through methods)
Used forGiving your users a cross-tab analysis of your data that they rearrange themselves, without writing SQL or exporting to Excel
Demo mode limit500 source rows processed; CSV and Excel exports disabled — see demo mode

How it works #

You hand the component a flat data set — a DataStore, so any query already written in your application. The crosstab takes care of the rest: it derives the field list, and you distribute those fields across four areas.

AreaWhat it holdsEffect
RowsGrouping fieldsOne row level per field, collapsible
ColumnsGrouping fieldsOne column header level per field
ValuesNumeric fields and their aggregateWhat gets calculated in the cells
FiltersSelection fieldsA filter above the table, applied to everything

All the computing happens inside the component: once the data has been handed over, rearranging the table triggers no round trip to the database or to PowerBuilder.


Quick start #

// window open event
datastore lds
lds = create datastore
lds.dataobject = "d_ventes"
lds.SetTransObject(SQLCA)
lds.Retrieve()

// 1. Hand over the data: fields are derived from the columns
uo_croise.of_set_data(lds)

// 2. Distribute the fields across the areas
uo_croise.of_add_row_field(/*field*/ "region")
uo_croise.of_add_col_field(/*field*/ "annee")
uo_croise.of_add_value_field(/*field*/ "montant", /*aggregate*/ uo_croise.AGG_SUM)

// 3. Present: amount format and grand totals
uo_croise.of_set_value_format(/*field*/ "montant", /*decimals*/ 0, /*thousands*/ "locale", /*symbol*/ "$", /*symbol_before*/ true)
uo_croise.ib_row_grand_total = true
uo_croise.ib_col_grand_total = true
// ue_cell_double_clicked event of uo_croise: (string as_row_tuple_json, string as_col_tuple_json, double ad_value)
// The user wants the detail behind a figure: open the matching list.
of_ouvrir_detail(as_row_tuple_json, as_col_tuple_json)

Constants #

ConstantValueFor
TOTALS_BOTTOM · TOTALS_TOP"bottom" "top"is_totals_position
VALUES_COLS · VALUES_ROWS"cols" "rows"is_values_axis
AGG_SUM · AGG_COUNT · AGG_DISTINCT_COUNT"sum" "count" "dcount"of_add_value_field
AGG_AVG · AGG_MIN · AGG_MAX"avg" "min" "max"of_add_value_field

The aggregate constants are read on the component: uo_croise.AGG_SUM. Those of a field (SHOW_*, CF_*) are read on the field handle.


Properties #

PropertyTypeDefaultPurpose
is_totals_positionstring"bottom"Where the grand total row sits: TOTALS_BOTTOM (at the foot, the default) or TOTALS_TOP (at the top, right below the headers)
is_values_axisstring"cols"How measures are laid out when there is more than one: VALUES_COLS (side by side in columns, the default) or VALUES_ROWS (stacked in rows)
is_currency_symbolstring""The currency the Number format menu of a value chip offers, next to No symbol and %. Empty = the display language's ($ in English, € elsewhere)
ib_field_listbooleantrueShows the field panel, where the user rearranges the table with the mouse
ib_row_subtotalsbooleantrueShows a subtotal for each row group
ib_col_subtotalsbooleantrueShows a subtotal for each column group
ib_row_grand_totalbooleantrueShows the grand-total row under the table (the twin of ib_col_grand_total)
ib_col_grand_totalbooleantrueShows the grand-total column after the table (the twin of ib_row_grand_total)
ib_enabledbooleantrueGreyed out: the grid still shows its figures — an empty crosstab is a different thing from one the application has switched off — but it stops answering the pointer
is_theme_stylestringfluentVisual style of the component (THEME_STYLE_* constants)
is_theme_modestringlightLight or dark variant (THEME_MODE_* constants)
il_theme_accentlong-1Accent color of this component (-1 = the theme accent)
is_tooltipstring""Simple tooltip shown when hovering the component
is_super_tooltip_titlestring""Title of the rich tooltip (takes precedence over is_tooltip)
is_super_tooltip_textstring""Text of the rich tooltip (rich markup accepted)
is_super_tooltip_imagestring""Image of the rich tooltip

Field properties #

of_field (string as_field) returns the handle of a field: you fetch it once, then you drive the field through its properties. The handle is created on the first call and reused afterwards.

PropertyTypeDefaultPurpose
is_labelstring""Readable caption for the field ("montant" → "Revenue")
is_showstring"normal"What the cell displays: "normal", "pctGrand" (% of the grand total), "pctRow" (% of the row), "pctCol" (% of the column), "running" (running total), "diff" (difference from the previous one)
is_conditional_formattingstring"none"Conditional formatting: CF_NONE, CF_SCALE (color scale) or CF_BARS (bars inside the cell)

The values of is_show and is_conditional_formatting are also available as constants on the handle (SHOW_PCT_COL, CF_SCALE…).

n_pbt_crosstab_field lnv_champ

lnv_champ = uo_croise.of_field(/*field*/ "montant")
lnv_champ.is_label = "Revenue"
lnv_champ.is_conditional_formatting = lnv_champ.CF_SCALE

⚠️ Breaking change. This property used to be called is_cf: the abbreviation said nothing at the call site. The old name no longer exists — code that uses it does not compile. The replacement is mechanical: is_cf → is_conditional_formatting, with no change in values or behaviour.


Methods #

Feeding and naming #

MethodPurpose
of_set_data (datastore ads_data)Hands over the data set: fields are derived from the DataStore columns, their captions from the header text. Returns 0 once the data is loaded, -5 if the DataStore is not valid or has no column, -2 if the component is not created
of_field (string as_field)Returns the handle of a field, to caption or format it (see Field properties)

Building the table #

MethodPurpose
of_clear_layout ( )Empties all four areas: the table goes back to blank, the data stays loaded. Returns 0 once applied, -2 if the component is not created
of_add_row_field (string as_field)Adds a field to the Rows area (the call order sets the level order). Returns 0 once applied, -2 if the component is not created
of_add_col_field (string as_field)Adds a field to the Columns area. Returns 0 once applied, -2 if the component is not created
of_add_value_field (string as_field, string as_agg)Adds a measure to the Values area, along with its aggregate. Returns 0 once applied, -2 if the component is not created
of_add_filter_field (string as_field)Adds a field to the Filters area, above the table. Returns 0 once applied, -2 if the component is not created

The aggregates accepted by of_add_value_field are carried by the component as constants: AGG_SUM (default), AGG_COUNT, AGG_DISTINCT_COUNT (distinct value count), AGG_AVG, AGG_MIN, AGG_MAX.

Totals and subtotals #

MethodPurpose

Filtering #

MethodPurpose
of_set_member_filter (string as_field, string as_values_tab)Keeps only the listed values of a field. Values are separated by tabs (~t). Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_clear_member_filter (string as_field)Removes that filter. Returns 0 once applied, -2 if the component is not created
of_set_value_filter (string as_field, string as_type, double ad_a, double ad_b, integer ai_measure_index)Filters on the total: "top" (the top ad_a), "gt", "lt", "between". ai_measure_index designates the measure concerned (the first one = 1). Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_clear_value_filter (string as_field)Removes that filter. Returns 0 once applied, -2 if the component is not created
of_set_label_filter (string as_field, string as_type, double ad_a, double ad_b)Filters on the value of the field itself: "gt", "lt", "between". Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_clear_label_filter (string as_field)Removes that filter. Returns 0 once applied, -2 if the component is not created

Formatting #

MethodPurpose
of_set_value_format (string as_field, integer ai_decimals, string as_thousands, string as_suffix { , boolean ab_symbol_before })Format of a measure: number of decimals (0 to 6), thousands separator ("locale", "space", "none"), symbol — after the number by default (1 234 EUR), BEFORE it when ab_symbol_before is true ($1,234). Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_clear_value_format (string as_field)Back to the default format. Returns 0 once applied, -2 if the component is not created
of_set_member_order (string as_field, string as_values_tab)Display order forced on the values of a field (separated by ~t); an empty string restores the natural order. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created

Dates and calculated fields #

MethodPurpose
of_group_date_field (string as_field, string as_part)Creates a field derived from a date column: "year", "quarter" or "month". It joins the field list and is used like any other. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_add_calc_field (string as_name, string as_label, string as_formula)Calculated field, evaluated row by row ("[montant] * 0.8" = the net of each sale, then summed like any column), usable in any area. Not for a ratio of totals (an average price): that is of_add_calc_measure. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_remove_calc_field (string as_name)Removes a calculated field. Returns 0 once applied, -2 if the component is not created
of_add_calc_measure (string as_name, string as_label, string as_formula)Calculated measure, evaluated cell by cell, on the totals ("[marge] / [ca]" = overall margin rate). Goes in the Values area only. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_remove_calc_measure (string as_name)Removes a calculated measure. Returns 0 once applied, -2 if the component is not created

A formula accepts the + - * / ( ) operators, numbers, and field names in square brackets. An invalid formula raises ue_calc_field_error — nothing crashes.

Expanding, saving, exporting #

MethodPurpose
of_expand_all ( ) · of_collapse_all ( )Expands or collapses every row group. Returns 0 once applied, -2 if the component is not created
of_expand_to_level (integer ai_level)Expands down to a given level (1 = first level only). Returns 0 once applied, -2 if the component is not created
of_get_layout ( ) → stringReturns the full state of the table — store it as-is, then replay it through of_set_layout
of_set_layout (string as_state_json)Restores a state obtained earlier. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_export_csv (string as_path)Writes a CSV file of the current view (UTF-8 with a BOM, semicolon separated); ue_csv_saved confirms. The twin of of_export_xlsx. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_export_xlsx (string as_path)Writes an Excel file of the table, formatting included; ue_xlsx_saved confirms. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created

Shared #

MethodPurpose
of_reset ( )Returns the component to its brand-new state. Returns 0 once applied, -2 if the component is not created
of_set_redraw (boolean)Batches a burst of changes into a single render. Returns 0 once applied, -2 if the component is not created
of_save_as_png (string) · of_save_as_jpg (string)Exports the rendering as an image. Returns 0, -2 if the component is not created, -4 if the capture fails, -5 on an empty path

Events #

EventRaised when
ue_layout_changed (string as_layout_json)The user has rearranged the table (moved a field, changed an aggregate, collapsed a group…)
ue_cell_double_clicked (string as_row_tuple_json, string as_col_tuple_json, double ad_value)Double-click on a cell: the first two arguments describe the intersection, the third the displayed value. This is the entry point for a drill-down
ue_csv_saved (string as_path, boolean ab_ok, string as_error)The CSV file has been written — or not, and as_error says why
ue_xlsx_saved (string as_path, boolean ab_ok, string as_error)The Excel file has been written — or not, and as_error says why
ue_calc_field_error (string as_field, string as_message)A calculated field or measure formula is invalid
ue_copy (string as_tsv)The user has copied a cell selection (Ctrl+C): it is up to you to put it on the clipboard
ue_ready ( )The component has finished loading; everything sent beforehand has been replayed
ue_runtime_missing ( )The WebView2 runtime is missing: the component stays empty
ue_bg_color (long al_color)The component has computed its theme background color; the userobject has already adopted it (backcolor)

What the user can do without a single line of code #

The table is alive: that is the whole point of the component. With the field panel shown (ib_field_list = true), the user can:

Each of these actions comes back in ue_layout_changed: combined with of_get_layout / of_set_layout, this lets you offer "saved views" to your users.

From the keyboard. Every one of those gestures is reachable without a mouse: Tab lands on a field, a collapse triangle or a sortable header, Enter or Space triggers it. On a field it opens its menu — the one carrying Add to rows / columns / values / filters and Remove: the whole build of the table goes through it.


Examples #

A complete sales report #

// Rows: region, then city within each region
uo_croise.of_clear_layout()
uo_croise.of_add_row_field(/*field*/ "region")
uo_croise.of_add_row_field(/*field*/ "ville")

// Columns: one per year
uo_croise.of_add_col_field(/*field*/ "annee")

// Cells: the total amount
uo_croise.of_add_value_field(/*field*/ "montant", /*aggregate*/ uo_croise.AGG_SUM)

// Presentation: readable amounts, subtotals and grand totals
uo_croise.of_set_value_format(/*field*/ "montant", /*decimals*/ 0, /*thousands*/ "locale", /*symbol*/ "$", /*symbol_before*/ true)
uo_croise.ib_row_subtotals = true
uo_croise.ib_row_grand_total = true
uo_croise.ib_col_grand_total = true

// Color the cells to spot the big amounts at a glance
uo_croise.of_field(/*field*/ "montant").is_conditional_formatting = n_pbt_crosstab_field.CF_SCALE

Readable captions #

Your columns are often named mt_ht or cd_reg. Rename them once and for all, right after of_set_data.

uo_croise.of_set_data(lds)

uo_croise.of_field("region").is_label    = "Region"
uo_croise.of_field("ville").is_label     = "City"
uo_croise.of_field("categorie").is_label = "Category"
uo_croise.of_field("annee").is_label     = "Year"
uo_croise.of_field("montant").is_label   = "Revenue"
uo_croise.of_field("quantite").is_label  = "Quantity"

Analyzing shares rather than amounts #

n_pbt_crosstab_field lnv_montant

uo_croise.of_clear_layout()
uo_croise.of_add_row_field(/*field*/ "categorie")
uo_croise.of_add_col_field(/*field*/ "annee")
uo_croise.of_add_value_field(/*field*/ "montant", /*aggregate*/ uo_croise.AGG_SUM)

// Keep only two categories on screen (values separated by a tab)
uo_croise.of_set_member_filter(/*field*/ "categorie", /*values*/ "Informatique~tMobilier")

lnv_montant = uo_croise.of_field(/*field*/ "montant")

// Show the share of each cell in the total of its column
lnv_montant.is_show = lnv_montant.SHOW_PCT_COL

// A small bar in each cell to compare the shares at a glance
lnv_montant.is_conditional_formatting = lnv_montant.CF_BARS

A measure of your own: the average price #

A calculated measure is evaluated on the totals of each cell, not row by row: that is what makes a ratio correct.

uo_croise.of_clear_layout()
uo_croise.of_add_row_field(/*field*/ "region")

// The two totals the calculation will be based on
uo_croise.of_add_value_field(/*field*/ "montant",  /*aggregate*/ uo_croise.AGG_SUM)
uo_croise.of_add_value_field(/*field*/ "quantite", /*aggregate*/ uo_croise.AGG_SUM)

// Average price = total amount divided by total quantity
uo_croise.of_add_calc_measure(/*name*/ "prix_moyen", /*caption*/ "Average price", &
                              /*formula*/ "[montant] / [quantite]")
uo_croise.of_set_value_format(/*field*/ "prix_moyen", /*decimals*/ 2, /*thousands*/ "locale", /*symbol*/ "$", /*symbol_before*/ true)

// Then place it among the values like any field (the aggregate does not matter: a measure is computed)
uo_croise.of_add_value_field(/*field*/ "prix_moyen", /*aggregate*/ uo_croise.AGG_SUM)
// ue_calc_field_error event of uo_croise: (string as_field, string as_message)
// Invalid formula: warn without breaking anything, the table stays on screen.
uo_statut.of_item("main").is_text = "Formula " + as_field + ": " + as_message

Analyzing by month, quarter or year #

A date column cannot be cross-tabbed as is — every single day would get its own row. Derive the level you want first.

// Create three fields derived from the date_vente column
uo_croise.of_group_date_field(/*field*/ "date_vente", /*level*/ "year")
uo_croise.of_group_date_field(/*field*/ "date_vente", /*level*/ "quarter")
uo_croise.of_group_date_field(/*field*/ "date_vente", /*level*/ "month")

// Then cross-tab them like any other field: year in columns, quarter underneath
uo_croise.of_add_col_field("date_vente__year")
uo_croise.of_add_col_field("date_vente__quarter")

The top ten regions #

// Keep only the 10 regions with the highest total on the first measure
uo_croise.of_set_value_filter(/*field*/ "region", /*type*/ "top", &
                              /*a*/ 10, /*b*/ 0, /*measure*/ 1)

Exporting #

The export reproduces the current view exactly: same filters, same totals, same formatting.

// To Excel: the file is written straight to the given path
uo_croise.of_export_xlsx("C:\temp\ventes.xlsx")
// ue_xlsx_saved event of uo_croise: (string as_path, boolean ab_ok, string as_error)
// inv_notif = an n_pbt_toaster declared as an instance variable of the window
if ab_ok then
    inv_notif.is_title = "Export complete"
    inv_notif.is_text  = as_path
    inv_notif.is_kind  = inv_notif.KIND_SUCCESS
else
    inv_notif.is_title = "Export failed"
    inv_notif.is_text  = as_error
    inv_notif.is_kind  = inv_notif.KIND_ERROR
end if
inv_notif.of_show()
// To CSV: a file, like for Excel; ue_csv_saved confirms
uo_croise.of_export_csv(/*path*/ "C:\exports\ventes.csv")

Offering saved views #

string ls_vue

// Save the current view : of_get_layout answers straight away
ls_vue = uo_croise.of_get_layout()

// Keep the content AS IS: it replays without any transformation.
of_enregistrer_vue(is_vue_courante, ls_vue)
// Later on: replay a saved view
uo_croise.of_set_layout(of_lire_vue("Ventes par region"))

Drilling down behind a figure #

// ue_cell_double_clicked event of uo_croise: (string as_row_tuple_json, string as_col_tuple_json, double ad_value)
// The first two arguments describe the intersection (which row values,
// which column values): enough to rebuild a detail query.
w_detail_ventes lw_detail

OpenWithParm(lw_detail, as_row_tuple_json + "|" + as_col_tuple_json)

Best practices #

Inherited from the common base #

These members exist on every visual component — they are not specific to this one. They are detailed once, in the transverse chapters; this table only says where to read them.

MembersRoleDetailed in
of_count · of_keys_at · of_hasWalk what the component holds3.2 Items
of_resetPut the component back to zero3.6 Resetting a component: of_reset()
of_set_property · of_get_property · of_component_nameDriving a property by its name3.1 The property engine
of_register_shortcut · of_clear_shortcutsThe component's keyboard chords3.5 Keyboard shortcuts
of_is_created · of_is_ready · of_get_last_errorWhether it was born, whether it is ready, what failed3.7 Diagnostics
of_save_as_png · of_save_as_jpgExport the rendering as an image3.8 Exporting the rendering as an image
of_set_redrawGroup changes into a single repaint3.10 Best practices
of_preload_iconsIcons shown with no delayInstant display: of_icon
of_set_translationTranslate one of the component's labels5.2 Adapting a label: of_set_translation
of_focus_webviewGive the component the focus6.4 Keyboard and focus
of_print · of_print_to_pdfPrint, or write a PDF6.9 Printing

Two helpers are not inherited: of_icon and of_escape_markup live on n_pbt_utils. Declare one — n_pbt_utils lnv_utils, nothing to create — and call them on it.


← Component reference · Guide contents