Skip to content

Commit

Permalink
Enable tree data as an object using the hierarchy of its properties (#98
Browse files Browse the repository at this point in the history
)

* Enable tree data as an object using its hierarchy

* reduces complexity to group function

* removes hardcoded group key using index number as string

* fixes parent group converting as string

* adds test case with tree object with groups with number array

* adds treeLeafKey option

* fixes path calculation

* changes check if the tree is an object

* tests pull_request_target event on CI

* reverts pull_request_target from CI

* adds sample

* adds sample - 2
  • Loading branch information
stockiNail committed Sep 18, 2022
1 parent 3aefb91 commit b91f42a
Show file tree
Hide file tree
Showing 17 changed files with 501 additions and 23 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ module.exports = {
'basic',
'labels',
'groups',
'tree',
'captions',
'dividers',
'rtl'
Expand Down
77 changes: 77 additions & 0 deletions docs/samples/tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Tree

```js chart-editor
// <block:options:1>
const options = {
plugins: {
title: {
display: true
},
legend: {
display: false
},
tooltip: {
callbacks: {
title(items) {
const dataItem = items[0].raw;
const obj = dataItem._data;
return obj.name;
},
}
}
}
};
// </block:options>

// <block:config:0>
const config = {
type: 'treemap',
data: {
datasets: [{
tree: Data.objectsTree,
treeLeafKey: 'name',
key: 'value',
groups: [],
spacing: 1,
borderWidth: 0.5,
borderColor: '#FF8F00',
backgroundColor: 'rgba(255,167,38,0.3)',
hoverBackgroundColor: 'rgba(238,238,238,0.5)',
captions: {
align: 'center'
},
labels: {
display: true,
formatter: (ctx) => {
return ctx.raw.v;
}
}
}]
},
options
};
// </block:config>

function toggle(chart) {
const dataset = chart.data.datasets[0];
if (dataset.groups.length) {
dataset.groups = [];
} else {
dataset.groups = [0, 1];
dataset.groups.push('name');
}
chart.update();
}

const actions = [
{
name: 'Toggle GroupBy',
handler: (chart) => toggle(chart)
}
];

module.exports = {
actions,
config,
};
```
81 changes: 81 additions & 0 deletions docs/scripts/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,84 @@ export const statsByState = [
area: 253335
}
];

export const objectsTree = {
analytics: {
cluster: {
agglomerative: {
value: 3938
},
communityStructure: {
value: 3812
},
hierarchical: {
value: 6714
},
mergeEdge: {
value: 743
},
},
graph: {
betweennessCentrality: {
value: 3534
},
linkDistance: {
value: 5731
},
maxFlowMinCut: {
value: 7840
},
shortestPaths: {
value: 5914
},
spanningTree: {
value: 3416
},
},
optimization: {
aspectRatioBanker: {
value: 7074
}
}
},
animate: {
easing: {
value: 17010
},
functionSequence: {
vaue: 5842
},
interpolate: {
arrayInterpolator: {
value: 1983
},
colorInterpolator: {
value: 2047
},
dateInterpolator: {
value: 1375
},
interpolator: {
value: 8746
},
matrixInterpolator: {
value: 2202
},
numberInterpolator: {
value: 1382
},
objectInterpolator: {
value: 1629
},
pointInterpolator: {
value: 1675
},
rectangleInterpolator: {
value: 2042
},
},
schedulable: {
value: 1041
}
}
};
6 changes: 4 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ These are used to set display properties for a specific dataset.
| [`labels`](#labels) | `object` | - |
| [`rtl`](#general) | `boolean` | - | `false`
| [`spacing`](#styling) | `number` | - | `0.5`
| [`tree`](#general) | `number[]` \| `object[]` | - | **required**
| [`tree`](#general) | `number[]` \| `object[]` \| `object` | - | **required**
| [`treeLeafKey`](#general) | `string` | - | `_leaf` |

All these values, if `undefined`, fallback to the scopes described in [option resolution](https://www.chartjs.org/docs/latest/general/options.html).

Expand All @@ -114,8 +115,9 @@ All these values, if `undefined`, fallback to the scopes described in [option re
| `label` | The label for the dataset which appears in the legend and tooltips.
| `rtl` | If `true`, the treemap elements are rendering from right to left.
| `tree` | Tree data should be provided in `tree` property of dataset. `data` is then automatically build.
| `treeLeafKey` | The name of the key where the object key of leaf node of tree object is stored. Used only when `tree` is an `object`, as hierarchical data.

Only the `tree`, `key` and `groups` options need to be specified in the dataset namespace.
Only the `tree`, `treeLeafKey`, `key` and `groups` options need to be specified in the dataset namespace.

```js
function colorFromRaw(ctx) {
Expand Down
16 changes: 10 additions & 6 deletions src/controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Chart, DatasetController, registry} from 'chart.js';
import {toFont, valueOrDefault} from 'chart.js/helpers';
import {group, requireVersion} from './utils';
import {toFont, valueOrDefault, isObject} from 'chart.js/helpers';
import {group, requireVersion, normalizeTreeToArray, getGroupKey} from './utils';
import {shouldDrawCaption} from './element';
import squarify from './squarify';
import {version} from '../package.json';
Expand Down Expand Up @@ -30,7 +30,11 @@ function arrayNotEqual(a1, a2) {

function buildData(dataset, mainRect, captions) {
const key = dataset.key || '';
const treeLeafKey = dataset.treeLeafKey || '_leaf';
let tree = dataset.tree || [];
if (isObject(tree)) {
tree = normalizeTreeToArray(key, treeLeafKey, tree);
}
const groups = dataset.groups || [];
const glen = groups.length;
const sp = valueOrDefault(dataset.spacing, 0) + valueOrDefault(dataset.borderWidth, 0);
Expand All @@ -39,9 +43,9 @@ function buildData(dataset, mainRect, captions) {
const padding = valueOrDefault(captions.padding, 3);

function recur(gidx, rect, parent, gs) {
const g = groups[gidx];
const pg = (gidx > 0) && groups[gidx - 1];
const gdata = group(tree, g, key, pg, parent);
const g = getGroupKey(groups[gidx]);
const pg = (gidx > 0) && getGroupKey(groups[gidx - 1]);
const gdata = group(tree, g, key, treeLeafKey, pg, parent, groups.filter((item, index) => index <= gidx));
const gsq = squarify(gdata, rect, key, g, gidx, gs);
const ret = gsq.slice();
let subRect;
Expand Down Expand Up @@ -199,7 +203,7 @@ TreemapController.overrides = {
label(item) {
const dataset = item.dataset;
const dataItem = dataset.data[item.dataIndex];
const label = dataItem.g || dataset.label;
const label = dataItem.g || dataItem._data.label || dataset.label;
return (label ? label + ': ' : '') + dataItem.v;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,15 +276,15 @@ TreemapElement.defaults = {
align: undefined,
color: 'black',
display: true,
formatter: (ctx) => ctx.raw.g || '',
formatter: (ctx) => ctx.raw.g || ctx.raw._data.label || '',
font: {},
padding: 3
},
labels: {
align: 'center',
color: 'black',
display: false,
formatter: (ctx) => ctx.raw.g ? [ctx.raw.g, ctx.raw.v] : ctx.raw.v,
formatter: (ctx) => ctx.raw.g ? [ctx.raw.g, ctx.raw.v] : (ctx.raw._data.label ? [ctx.raw._data.label, ctx.raw.v] : ctx.raw.v),
font: {},
position: 'middle',
padding: 3
Expand Down
90 changes: 81 additions & 9 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
import {isObject} from 'chart.js/helpers';

export const getGroupKey = (lvl) => '' + lvl;

function scanTreeObject(key, treeLeafKey, obj, tree = [], lvl = 0, result = []) {
const objIndex = lvl - 1;
if (key in obj && lvl > 0) {
const record = tree.reduce(function(reduced, item, i) {
if (i !== objIndex) {
reduced[getGroupKey(i)] = item;
}
return reduced;
}, {});
record[treeLeafKey] = tree[objIndex];
record[key] = obj[key];
result.push(record);
} else {
for (const childKey of Object.keys(obj)) {
const child = obj[childKey];
if (isObject(child)) {
tree.push(childKey);
scanTreeObject(key, treeLeafKey, child, tree, lvl + 1, result);
}
}
}
tree.splice(objIndex, 1);
return result;
}

export function normalizeTreeToArray(key, treeLeafKey, obj) {
const data = scanTreeObject(key, treeLeafKey, obj);
if (!data.length) {
return data;
}
const max = data.reduce(function(maxValue, element) {
// minus 2 because _leaf and value properties are added
// on top to groups ones
const keys = Object.keys(element).length - 2;
return maxValue > keys ? maxValue : keys;
});
data.forEach(function(element) {
for (let i = 0; i < max; i++) {
const groupKey = getGroupKey(i);
if (!element[groupKey]) {
element[groupKey] = '';
}
}
});
return data;
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat
export function flatten(input) {
const stack = [...input];
Expand All @@ -18,36 +67,59 @@ export function flatten(input) {
return res.reverse();
}

function getPath(groups, value, defaultValue) {
if (!groups.length) {
return;
}
const path = [];
for (const grp of groups) {
const item = value[grp];
if (item === '') {
path.push(defaultValue);
break;
}
path.push(item);
}
return path.length ? path.join('.') : defaultValue;
}

/**
* @param {[]} values
* @param {string} grp
* @param {string} key
* @param {string} treeeLeafKey
* @param {string} [mainGrp]
* @param {*} [mainValue]
* @param {[]} groups
*/
export function group(values, grp, key, mainGrp, mainValue) {
export function group(values, grp, key, treeLeafKey, mainGrp, mainValue, groups = []) {
const tmp = Object.create(null);
const data = Object.create(null);
const ret = [];
let g, i, n, v;
let g, i, n;
for (i = 0, n = values.length; i < n; ++i) {
v = values[i];
const v = values[i];
if (mainGrp && v[mainGrp] !== mainValue) {
continue;
}
g = v[grp] || '';
g = v[grp] || v[treeLeafKey] || '';
if (!(g in tmp)) {
tmp[g] = 0;
tmp[g] = {value: 0};
data[g] = [];
}
tmp[g] += +v[key];
tmp[g].value += +v[key];
tmp[g].label = v[grp] || '';
tmp[g].path = getPath(groups, v, g);
data[g].push(v);
}

Object.keys(tmp).forEach((k) => {
v = {children: data[k]};
v[key] = +tmp[k];
v[grp] = k;
const v = {children: data[k]};
v[key] = +tmp[k].value;
v[grp] = tmp[k].label;
v.label = k;
v.path = tmp[k].path;

if (mainGrp) {
v[mainGrp] = mainValue;
}
Expand Down
Loading

0 comments on commit b91f42a

Please sign in to comment.