Skip to main content

DHTMLX Pivot. Grid integration example

Demo of bidirectional integration between DHTMLX Grid and Pivot widgets. Switch between flat table and pivot analysis views while preserving data and configuration state across transitions.

Live example

const defaultGridConfig = {
    columns: [
        { width: 150, id: "country", header: [{ text: "Country" }] },
        { width: 150, id: "region", header: [{ text: "Region" }] },
        { width: 150, id: "rank", header: [{ text: "Rank" }] },
        { width: 150, id: "score", header: [{ text: "Ladder score" }] },
        { width: 150, id: "gdp", header: [{ text: "GDP per capita" }] },
        { width: 150, id: "family", header: [{ text: "Family" }] },
        { width: 150, id: "health", header: [{ text: "Healthy life expectancy" }] },
        { width: 150, id: "freedom", header: [{ text: "Freedom to make life choices" }] },
        { width: 150, id: "corruption", header: [{ text: "Perception of corruption" }] },
        { width: 150, id: "generosity", header: [{ text: "Generosity" }] }
    ],
    data: dataset,
};

let storedGridConfig = defaultGridConfig;
let storedPivotConfig;
let storedPivotFields;
let pivotWidget;
let grid = new dhx.Grid("grid-pivot", storedGridConfig);
let innerGrid = null;

function replaceInnerGrid(pivot) {
    const grid = pivot.container.getElementsByClassName("wx-grid");
    if (grid.length) {
        grid[0].innerText = "";
        innerGrid = new dhx.Grid(grid[0], defaultGridConfig); 
    }
}

function displayButtons(state) {
    document.getElementById("edit-button").style.display = state === 1 ? "none" : "block";
    document.getElementById("aux-buttons").style.display = state === 1 ? "flex" : "none";
}

function convertAndInit(state, save) {
    if (state === 1) {
        if (!storedPivotConfig) {
            const dataTypes = {};
            const firstItem = grid.data.getItem(grid.data.getId(0));

            // auto-detect data types based on the first record in grid
            for (const c in firstItem) {
                dataTypes[c] = typeof(firstItem[c]) === "string" ? "text" : typeof(firstItem[c]) === "number" ? "number" : "date";
            }

            // assume single level headers
            // generate fields once
            if (!storedPivotFields) storedPivotFields = grid.config.columns.map(col => ({ id: col.id, label: col.header[0].text, type: dataTypes[col.id] }));
        } else {
            // prepare pivot config for re-init
            delete storedPivotConfig._ready;
        }

        grid.destructor();

        pivotWidget = new pivot.Pivot("#grid-pivot", { 
            fields: storedPivotFields || [],
            config: storedPivotConfig || {},
            data: dataset,
        });

        if (!storedPivotConfig) setTimeout(() => replaceInnerGrid(pivotWidget), 1);

        pivotWidget.api.on("update-config", config => {
            // show the initial grid on empty pivot config
            if (!config.rows.length && !config.columns.length && !config.values.length) {
                setTimeout(() => replaceInnerGrid(pivotWidget), 1);
            } else {
                if (innerGrid) innerGrid.destructor();
            }
        });

        displayButtons(1);
    } else {
        const tableApi = pivotWidget.getTable();
        const pivotConfig = pivotWidget.api.getState().config;
        if (save) {
            // show the initial grid on empty pivot config
            if (!pivotConfig.rows.length && !pivotConfig.columns.length && !pivotConfig.values.length) {
                storedGridConfig = defaultGridConfig;
                storedPivotConfig = null;
            } else {
                const { columns, data, sizes } = tableApi.getState();
                storedGridConfig = { 
                  columns: columns.map(c => { c.id = c.id.toString(); c.width = c.width ? c.width : sizes.columnWidth; return c; }), 
                  data: data.map(d => {d.values.forEach((v, i) => { d[i+1] = v});return d; }) 
                };
                storedPivotConfig = pivotConfig;
            }
        }

        pivotWidget.destructor();
        grid = new dhx.Grid("grid-pivot", storedGridConfig);
        displayButtons(0);
    }
}
<!-- auxiliary controls for interacting with the sample -->
<link rel="stylesheet" href="https://snippet.dhtmlx.com/codebase/assets/css/auxiliary_controls.css">

<!-- component container -->
<div id="grid-pivot" style="height: calc(100% - 60px); width: 100%;"></div>
    <div class="dhx_sample-controls" style="justify-content:space-between; background: #fff; height:40px; padding:20px;">
        <div class="label" style="font-weight:500;">World Happiness Report</div>
        <div class="buttons">
	        <button name="form-button" class="dhx_select dhx_sample-btn dhx_sample-btn--flat" id="edit-button" style="width: 120px; text-align: center; display: inline-block; text-transform: none;" onclick="convertAndInit(1)">Edit in Pivot</button>
            <div style="display:none;" id="aux-buttons">
                <button name="form-button" class="dhx_select dhx_sample-btn dhx_sample-btn--flat" id="cancel-button" style="width: 120px; text-align: center; display: inline-block; text-transform: none;" onclick="convertAndInit(0)">Cancel</button>
                <button name="form-button" class="dhx_select dhx_sample-btn dhx_sample-btn--flat" id="save-button" style="width: 120px; text-align: center; display: inline-block; text-transform: none;" onclick="convertAndInit(0, true)">Save</button>
            </div>
        </div>
    </div>
</div>

<!-- custom styles -->

<style>
body {
        /* reduce z-index to avoid overlap with headers */
        --dhx-z-index-force-up: 8;
    }
</style>

<script>
const dataset = [
      {
			"country": "Switzerland",
			"region": "Western Europe",
			"rank": 1,
			"score": 7.587,
			"gdp": 1.39651,
			"family": 1.34951,
			"health": 0.94143,
			"freedom": 0.66557,
			"corruption": 0.41978,
			"generosity": 0.29678,
			"year": 2015
		},
      {
			"country": "Iceland",
			"region": "Western Europe",
			"rank": 2,
			"score": 7.561,
			"gdp": 1.30232,
			"family": 1.40223,
			"health": 0.94784,
			"freedom": 0.62877,
			"corruption": 0.14145,
			"generosity": 0.4363,
			"year": 2015
		},
      // ... 458 more items (see Live Editor for full data)
    ];
</script>

Displaying raw tabular data in a flat grid is straightforward, but users often need to pivot that same data for aggregation and cross-tabulation. Switching between these two views typically requires building separate pages or complex state management to maintain data consistency. Without a unified approach, users lose context when transitioning between flat and analytical views.

This example demonstrates a pattern where DHTMLX Grid and DHTMLX Pivot share the same container and dataset. An "Edit in Pivot" button destroys the grid and initializes a Pivot widget in its place, auto-detecting field types from the grid's data. When the user clicks "Save" or "Cancel", the Pivot is destroyed and the Grid is recreated - either with the pivoted results or the original data.

Use this approach when your application needs to let users explore raw data in a flat grid and then dynamically switch to a pivot view for aggregation, without leaving the page or losing their place in the workflow.

Solution overview

To achieve Grid-to-Pivot integration, the code follows a destroy-and-recreate pattern. The convertAndInit() function handles both directions of the transition. When switching to Pivot mode (state=1), it auto-detects data types from the first grid record, generates field definitions from column headers, destroys the grid, and initializes a new pivot.Pivot in the same container. When switching back (state=0), it extracts the pivoted columns and data from the Pivot's table API, destroys the Pivot, and recreates the Grid with either saved results or the original configuration.

The replaceInnerGrid() function handles an edge case: when the Pivot has no configuration yet (no rows, columns, or values assigned), it replaces the Pivot's internal empty grid with a standard DHTMLX Grid showing the original flat data. The update-config event listener monitors configuration changes to toggle between this initial grid and the actual pivoted output.

Key points

  • Auto-detected field types: The code inspects the first data record to determine whether each field is "text", "number", or "date", eliminating the need to manually define field configurations for the Pivot
  • Bidirectional state preservation: Pivot configuration (rows, columns, values) is stored when switching back to Grid mode, allowing users to return to the same Pivot layout later without reconfiguring
  • Save vs Cancel workflow: The "Save" button extracts pivoted columns and aggregated data from the Pivot's table API and feeds them into a new Grid; "Cancel" restores the original flat dataset
  • Empty config handling: When the Pivot has no rows, columns, or values configured, the internal grid area is replaced with a standard DHTMLX Grid showing the raw data, providing a seamless user experience

API reference

Additional resources