Repository: highcharts/highcharts
Pull request #22958: grid/fetch-demo
Author: login: sebastianbochan
Labels: name: Grid
description: Not used anymore
Created: 2025-04-23T09:44:34Z | Merged: 2025-04-25T12:38:52Z

Added fetch-data Grid demo.


Diff:
diff --git a/samples/dashboards/demo/cloud-monitoring/demo.js b/samples/dashboards/demo/cloud-monitoring/demo.js
index 7425fb64daf..81fcd8653a9 100644
--- a/samples/dashboards/demo/cloud-monitoring/demo.js
+++ b/samples/dashboards/demo/cloud-monitoring/demo.js
@@ -681,9 +681,7 @@ const setupDashboard = instanceId => {
                             const val = this.value;
                             return `<img src="https://www.highcharts.com/samples/graphics/dashboards/cloud-monitoring/${
                                 val.toLowerCase()
-                            }-ico.${val === 'Critical' ? 'png' : 'svg'}" alt="${
-                                val
-                            }"/>`;
+                            }-ico.svg" alt="${val}"/>`;
                         }
                     }
                 }],
diff --git a/samples/graphics/dashboards/cloud-monitoring/critical-ico.svg b/samples/graphics/dashboards/cloud-monitoring/critical-ico.svg
new file mode 100644
index 00000000000..f2c7b87fbee
--- /dev/null
+++ b/samples/graphics/dashboards/cloud-monitoring/critical-ico.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Warstwa_1" data-name="Warstwa 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 509.86 501.82">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #e50000;
+      }
+    </style>
+  </defs>
+  <path class="cls-1" d="M250.57,0l259.29,251.26-259.29,250.57L0,250.53,250.57,0ZM268.57,320.54c4.08-4.08,5.35-9.74,6.24-15.26,2.96-54.63,14.11-110.54,17.11-164.89,3.31-60.04-69.69-55.99-73.98-12.98-.4,4.04-.21,9.83,0,13.98,2.82,54.29,14.22,110.33,17.09,164.91.65,17.05,21.28,26.51,33.54,14.25ZM250.68,353.15c-37.76,5.11-33.8,62.22,4.69,61.77,42.71-.5,38.23-67.58-4.69-61.77Z"/>
+</svg>
\ No newline at end of file
diff --git a/samples/grid-lite/demo/parse-data/demo.css b/samples/grid-lite/demo/parse-data/demo.css
new file mode 100644
index 00000000000..987cca1ad7a
--- /dev/null
+++ b/samples/grid-lite/demo/parse-data/demo.css
@@ -0,0 +1,25 @@
+@import url("https://cdn.jsdelivr.net/npm/@highcharts/grid-lite/css/grid.css");
+
+#container {
+    padding: 8px;
+}
+
+.highcharts-description {
+    padding: 0 20px;
+}
+
+.hcg-table td.text-center,
+.hcg-table th.text-center span {
+    text-align: center;
+}
+
+td img {
+    width: 16px;
+    height: 16px;
+}
+
+@media (prefers-color-scheme: dark) {
+    #container {
+        background-color: #292929;
+    }
+}
diff --git a/samples/grid-lite/demo/parse-data/demo.details b/samples/grid-lite/demo/parse-data/demo.details
new file mode 100644
index 00000000000..f8733a08fc0
--- /dev/null
+++ b/samples/grid-lite/demo/parse-data/demo.details
@@ -0,0 +1,12 @@
+---
+name: Grid with data from an external source
+authors:
+  - Sebastian Bochan
+js_wrap: b
+tags:
+  - Highcharts Grid demo
+categories:
+  - General:
+        priority: 80
+use_png_thumbnail: true
+...
\ No newline at end of file
diff --git a/samples/grid-lite/demo/parse-data/demo.html b/samples/grid-lite/demo/parse-data/demo.html
new file mode 100644
index 00000000000..13071c24a28
--- /dev/null
+++ b/samples/grid-lite/demo/parse-data/demo.html
@@ -0,0 +1,7 @@
+<script src="https://cdn.jsdelivr.net/npm/@highcharts/grid-lite/grid-lite.js"></script>
+
+<div id="container"></div>
+<p class="highcharts-description">
+    This demo show how you can fetch JSON from an external source and then shape
+    that dataset into a format that Grid can consume.
+</p>
diff --git a/samples/grid-lite/demo/parse-data/demo.js b/samples/grid-lite/demo/parse-data/demo.js
new file mode 100644
index 00000000000..a2ac873f718
--- /dev/null
+++ b/samples/grid-lite/demo/parse-data/demo.js
@@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            rows: {
+                strictHeights: true
+            }
+        },
+        header: [{
+            columnId: 'InstanceId',
+            format: 'ID'
+        }, {
+            columnId: 'InstanceType',
+            format: 'Type'
+        }, {
+            columnId: 'PublicIpAddress',
+            format: 'Public IP'
+        },
+        'State',
+        {
+            columnId: 'UsedGB',
+            format: 'Disk used / size'
+        }, {
+            columnId: 'HealthIndicator',
+            format: 'Health'
+        }],
+        columns: [{
+            id: 'UsedGB',
+            cells: {
+                formatter: function () {
+                    return `${this.value}GB / ${this.row.data.SizeGB}GB`;
+                }
+            }
+        },
+        {
+            id: 'HealthIndicator',
+            width: '100px',
+            className: 'text-center',
+            cells: {
+                formatter: function () {
+                    const val = this.value;
+                    return `<img
+                        src='https://www.highcharts.com/samples/graphics/dashboards/cloud-monitoring/${val.toLowerCase()}-ico.svg'
+                        alt='${val}'
+                    />`;
+                }
+            }
+        }]
+    });
+};
+
+// Init
+(async () => {
+    const data = {};
+
+    // Fetch JSON data from external source
+    const instances = await fetch(
+        'https://demo-live-data.highcharts.com/instances.json'
+    )
+        .then(response => response.json())
+        .catch(err => console.log(err));
+
+    // The properties we would like to extract from the JSON
+    const fields = [
+        'InstanceId',
+        'InstanceType',
+        'PublicIpAddress',
+        'State',
+        'DiskSpace.RootDisk.UsedGB',
+        'DiskSpace.RootDisk.SizeGB',
+        'HealthIndicator'
+    ];
+
+    // Parse the properties to a data format that Grid can consume (https://www.highcharts.com/docs/grid/understanding-grid#datatable)
+    (instances || []).forEach(instance => {
+        fields.forEach(field => {
+            const keys = field.split('.');
+            const fieldName = keys[keys.length - 1];
+            let current = instance;
+
+            if (!data[fieldName]) {
+                data[fieldName] = [];
+            }
+
+            for (let i = 0; i < keys.length - 1; i++) {
+                if (!(keys[i] in current)) {
+                    current[keys[i]] = {};
+                }
+                current = current[keys[i]];
+            }
+
+            data[fieldName].push(current[keys[keys.length - 1]]);
+        });
+    });
+
+    // Init Grid
+    initGrid(data);
+})();


Reviews:
user: login: stitot
body: I like this, but have one rather big objection, so let's deal with that before the full review:

Even thought rendering of `this.value.RootDisk.UsedGB` etc. works fine our support for objects as values is limited. Breaks sorting (ref. https://github.com/highcharts/highcharts/issues/22936) and isn't editable. Instead I suggest the approach below (courtesy of ChatGPT, so beware). Then follow my suggestion of hiding column in https://github.com/highcharts/highcharts/issues/22936 instead.

This way the column is at least sortable by `UsedGB` and that value can be edited. Still a shortcoming that its not sortable by `SizeGB` and that `SizeGB` can not be edited, but it's still better IMO.

```
function parseFields(items, paths) {
  return paths.reduce((acc, path) => {
    const keys = path.split('.');
    acc[path] = items.map(item =>
      keys.reduce((obj, key) => obj?.[key], item)
    );
    return acc;
  }, {});
}

async function fetchAndParse(requestedFields) {
  const resp = await fetch(
    'https://demo-live-data.highcharts.com/instances.json'
  );
  if (!resp.ok) {
    throw new Error(`HTTP ${resp.status}`);
  }
  const instances = await resp.json();
  return parseFields(instances, requestedFields);
}

const fields = [
  "InstanceId",
  "InstanceType",
  "DiskSpace.RootDisk.SizeGB",
  "DiskSpace.RootDisk.UsedGB",
  ...
];

fetchAndParse(fields)
  .then(data => {
    initGrid(data);
  })
  .catch(console.error);

```



state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T11:47:37Z
user: login: karolkolodziej
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-24T06:58:13Z
user: login: KamilKubik
body: Shouldn't we rename the folder name to `external-source-data` or something? 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T07:30:12Z
user: login: sebastianbochan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:59:37Z
user: login: ddragula
body: Looks good!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-24T16:21:26Z
user: login: stitot
body: Very nice! Just a few inline comments.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T10:27:35Z
user: login: stitot
body: Perfect! Thank you.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:33:10Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+@import url("https://cdn.jsdelivr.net/npm/@highcharts/grid-lite/css/grid.css");
+
+#container {
+    padding: 8px;
+}
+
+.highcharts-description {
+    padding: 0 20px;
+}
+
+.hcg-table td[data-column-id="HealthIndicator"] {
+    text-align: center;
path: samples/grid-lite/demo/parse-data/demo.css
user: login: karolkolodziej
body: It looks a bit off when it's centred 🤔 
created_at: 2025-04-24T06:56:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,12 @@
+---
+name: Fetch Data Grid
path: samples/grid-lite/demo/parse-data/demo.details
user: login: karolkolodziej
body: ```suggestion
name: Grid with data from an external source
```
created_at: 2025-04-24T06:58:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,24 @@
+@import url("https://cdn.jsdelivr.net/npm/@highcharts/grid-lite/css/grid.css");
+
+#container {
+    padding: 8px;
+}
+
+.highcharts-description {
+    padding: 0 20px;
+}
+
+.hcg-table td[data-column-id="HealthIndicator"] {
+    text-align: center;
path: samples/grid-lite/demo/parse-data/demo.css
user: login: sebastianbochan
body: Currently, we have the width option, so we will adjust it.
created_at: 2025-04-24T08:59:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            columns: {
+                distribution: 'mixed'
+            }
+        },
+        header: [{
+            columnId: 'InstanceId',
+            format: 'ID'
+        }, {
+            columnId: 'InstanceType',
+            format: 'Type'
+        }, {
+            columnId: 'PublicIpAddress',
+            format: 'Public IP'
+        },
+        'State',
+        {
+            columnId: 'UsedGB',
+            format: 'Disk used / size'
+        }, {
+            columnId: 'HealthIndicator',
+            format: 'Health'
+        }],
+        columns: [
+            {
path: samples/grid-lite/demo/parse-data/demo.js
user: login: ddragula
body: please use compact `}, {` & `[{` instead of splitting them into separate lines
created_at: 2025-04-24T13:48:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,25 @@
+@import url("https://cdn.jsdelivr.net/npm/@highcharts/grid-lite/css/grid.css");
+
+#container {
+    padding: 8px;
+}
+
+.highcharts-description {
+    padding: 0 20px;
+}
+
+.hcg-table td[data-column-id="HealthIndicator"],
path: samples/grid-lite/demo/parse-data/demo.css
user: login: stitot
body: Suggestion: instead use `.centered` and add that class name via the `className` option. A more generic and reusable approach, and what we recommend instead of targeting individual columns in CSS.
created_at: 2025-04-25T10:12:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,6 @@
+<script src="https://cdn.jsdelivr.net/npm/@highcharts/grid-lite/grid-lite.js"></script>
+
+<div id="container"></div>
+<p class="highcharts-description">
+    Grid with dynamically loaded data from the external resource.
path: samples/grid-lite/demo/parse-data/demo.html
user: login: stitot
body: This demo show how you can fetch JSON from an external source and then shape that dataset into a format that Grid can consume.
created_at: 2025-04-25T10:16:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            columns: {
+                distribution: 'mixed'
+            }
path: samples/grid-lite/demo/parse-data/demo.js
user: login: stitot
body: ```suggestion
            rows: {
                strictHeights: true
            }
```
columns.distribution is not needed, (turned on automatically if `width` exists) and potentially adds confusion. But `strictHeights` is nice, to prevent some ugly line/word breaks.
created_at: 2025-04-25T10:19:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            columns: {
+                distribution: 'mixed'
+            }
+        },
+        header: [{
+            columnId: 'InstanceId',
+            format: 'ID'
+        }, {
+            columnId: 'InstanceType',
+            format: 'Type'
+        }, {
+            columnId: 'PublicIpAddress',
+            format: 'Public IP'
+        },
+        'State',
+        {
+            columnId: 'UsedGB',
+            format: 'Disk used / size'
+        }, {
+            columnId: 'HealthIndicator',
+            format: 'Health'
+        }],
+        columns: [
+            {
+                id: 'UsedGB',
+                cells: {
+                    formatter: function () {
+                        return `${this.value}GB / ${this.row.data.SizeGB}GB`;
+                    }
+                }
+            },
+            {
+                id: 'HealthIndicator',
+                width: '100px',
+                useHTML: true,
path: samples/grid-lite/demo/parse-data/demo.js
user: login: stitot
body: This is not an option in Grid, and not needed
created_at: 2025-04-25T10:20:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            columns: {
+                distribution: 'mixed'
+            }
+        },
+        header: [{
+            columnId: 'InstanceId',
+            format: 'ID'
+        }, {
+            columnId: 'InstanceType',
+            format: 'Type'
+        }, {
+            columnId: 'PublicIpAddress',
+            format: 'Public IP'
+        },
+        'State',
+        {
+            columnId: 'UsedGB',
+            format: 'Disk used / size'
+        }, {
+            columnId: 'HealthIndicator',
+            format: 'Health'
+        }],
+        columns: [
+            {
+                id: 'UsedGB',
+                cells: {
+                    formatter: function () {
+                        return `${this.value}GB / ${this.row.data.SizeGB}GB`;
+                    }
+                }
+            },
+            {
+                id: 'HealthIndicator',
+                width: '100px',
+                useHTML: true,
+                cells: {
+                    formatter: function () {
+                        const val = this.value;
+                        return `<img
+                            src='https://www.highcharts.com/samples/graphics/dashboards/cloud-monitoring/${val.toLowerCase()}-ico.${val === 'Critical' ? 'png' : 'svg'}'
path: samples/grid-lite/demo/parse-data/demo.js
user: login: stitot
body: This is just confusing, IMO. Not possible to get hold of `critical.svg` somehow?
created_at: 2025-04-25T10:22:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            columns: {
+                distribution: 'mixed'
+            }
+        },
+        header: [{
+            columnId: 'InstanceId',
+            format: 'ID'
+        }, {
+            columnId: 'InstanceType',
+            format: 'Type'
+        }, {
+            columnId: 'PublicIpAddress',
+            format: 'Public IP'
+        },
+        'State',
+        {
+            columnId: 'UsedGB',
+            format: 'Disk used / size'
+        }, {
+            columnId: 'HealthIndicator',
+            format: 'Health'
+        }],
+        columns: [
+            {
+                id: 'UsedGB',
+                cells: {
+                    formatter: function () {
+                        return `${this.value}GB / ${this.row.data.SizeGB}GB`;
+                    }
+                }
+            },
+            {
+                id: 'HealthIndicator',
+                width: '100px',
+                useHTML: true,
+                cells: {
+                    formatter: function () {
+                        const val = this.value;
+                        return `<img
+                            src='https://www.highcharts.com/samples/graphics/dashboards/cloud-monitoring/${val.toLowerCase()}-ico.${val === 'Critical' ? 'png' : 'svg'}'
+                            alt='${val}'
+                        />`;
+                    }
+                }
+            }
+        ]
+    });
+};
+
+// Init
+(async () => {
+    const data = {};
+
+    // Load data
path: samples/grid-lite/demo/parse-data/demo.js
user: login: stitot
body: ```suggestion
    // Fetch JSON data from external source
```
created_at: 2025-04-25T10:22:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            columns: {
+                distribution: 'mixed'
+            }
+        },
+        header: [{
+            columnId: 'InstanceId',
+            format: 'ID'
+        }, {
+            columnId: 'InstanceType',
+            format: 'Type'
+        }, {
+            columnId: 'PublicIpAddress',
+            format: 'Public IP'
+        },
+        'State',
+        {
+            columnId: 'UsedGB',
+            format: 'Disk used / size'
+        }, {
+            columnId: 'HealthIndicator',
+            format: 'Health'
+        }],
+        columns: [
+            {
+                id: 'UsedGB',
+                cells: {
+                    formatter: function () {
+                        return `${this.value}GB / ${this.row.data.SizeGB}GB`;
+                    }
+                }
+            },
+            {
+                id: 'HealthIndicator',
+                width: '100px',
+                useHTML: true,
+                cells: {
+                    formatter: function () {
+                        const val = this.value;
+                        return `<img
+                            src='https://www.highcharts.com/samples/graphics/dashboards/cloud-monitoring/${val.toLowerCase()}-ico.${val === 'Critical' ? 'png' : 'svg'}'
+                            alt='${val}'
+                        />`;
+                    }
+                }
+            }
+        ]
+    });
+};
+
+// Init
+(async () => {
+    const data = {};
+
+    // Load data
+    const instances = await fetch(
+        'https://demo-live-data.highcharts.com/instances.json'
+    ).then(response => response.json());
+
+    // Fields that we would like to extract from data
path: samples/grid-lite/demo/parse-data/demo.js
user: login: stitot
body: ```suggestion
    // The properties we would like to extract from the JSON
```
created_at: 2025-04-25T10:23:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+const initGrid = data => {
+    Grid.grid('container', {
+        dataTable: {
+            columns: data
+        },
+        rendering: {
+            columns: {
+                distribution: 'mixed'
+            }
+        },
+        header: [{
+            columnId: 'InstanceId',
+            format: 'ID'
+        }, {
+            columnId: 'InstanceType',
+            format: 'Type'
+        }, {
+            columnId: 'PublicIpAddress',
+            format: 'Public IP'
+        },
+        'State',
+        {
+            columnId: 'UsedGB',
+            format: 'Disk used / size'
+        }, {
+            columnId: 'HealthIndicator',
+            format: 'Health'
+        }],
+        columns: [
+            {
+                id: 'UsedGB',
+                cells: {
+                    formatter: function () {
+                        return `${this.value}GB / ${this.row.data.SizeGB}GB`;
+                    }
+                }
+            },
+            {
+                id: 'HealthIndicator',
+                width: '100px',
+                useHTML: true,
+                cells: {
+                    formatter: function () {
+                        const val = this.value;
+                        return `<img
+                            src='https://www.highcharts.com/samples/graphics/dashboards/cloud-monitoring/${val.toLowerCase()}-ico.${val === 'Critical' ? 'png' : 'svg'}'
+                            alt='${val}'
+                        />`;
+                    }
+                }
+            }
+        ]
+    });
+};
+
+// Init
+(async () => {
+    const data = {};
+
+    // Load data
+    const instances = await fetch(
+        'https://demo-live-data.highcharts.com/instances.json'
+    ).then(response => response.json());
+
+    // Fields that we would like to extract from data
+    const fields = [
+        'InstanceId',
+        'InstanceType',
+        'PublicIpAddress',
+        'State',
+        'DiskSpace.RootDisk.UsedGB',
+        'DiskSpace.RootDisk.SizeGB',
+        'HealthIndicator'
+    ];
+
+    // Parse data
path: samples/grid-lite/demo/parse-data/demo.js
user: login: stitot
body: ```suggestion
    // Parse the properties to a data format that Grid can consume (https://www.highcharts.com/docs/grid/understanding-grid#datatable)
```
created_at: 2025-04-25T10:26:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: highsoft-bot
created_at: 2025-04-23T09:47:27Z
body: ## Visual test results - No difference found

---
<details>
<summary>Samples changed</summary><p>

| Change type | Sample |
| --- | --- |


</p>
</details>

author_association: COLLABORATOR
user: login: highsoft-bot
created_at: 2025-04-23T09:52:14Z
body: ### File size comparison
No differences found
author_association: COLLABORATOR
user: login: sebastianbochan
created_at: 2025-04-24T13:28:33Z
body: @stitot 
1. I modified my parser a bit; the sorting option works. In between, I would rather use the simplest parser as it can (instead of using the `reduce` function, suggested by our close friend GPT) because of our users' different entry levels. IMO, the demo should be the easiest to understand.
2. Refactored columns / headers a bit to hide column.

author_association: CONTRIBUTOR
user: login: highsoft-bot
created_at: 2025-04-25T11:40:41Z
body: ## Lighthouse report
### dashboards-demo-minimal.json
|   | Reference | Proposed | Diff |
| :---- | ------ | ----- | ----- |
| performance – score | 1 | 1 | 0.00 |
| first-contentful-paint – score | 1 | 1 | 0.00 |
| first-contentful-paint – milliseconds | 188.27 | 187.71 | -0.56 |
| first-meaningful-paint – score | 1 | 1 | 0.00 |
| first-meaningful-paint – milliseconds | 492.34 | 477.75 | -14.59 |
| dom-size – score | 1 | 1 | 0.00 |
| dom-size – elements | 352 | 352 | 0.00 |

author_association: COLLABORATOR
user: login: highsoft-bot
created_at: 2025-04-25T11:40:48Z
body: ## Dashboard visual diffs
No differences found
author_association: COLLABORATOR