Skip to content

Commit

Permalink
improve group unification
Browse files Browse the repository at this point in the history
  • Loading branch information
sgratzl committed Mar 13, 2019
1 parent 8a38228 commit af4ff2a
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 34 deletions.
8 changes: 4 additions & 4 deletions demo/grouping.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
window.onload = function () {
const arr = [];
const cats = ['c1', 'c2', 'c3'];
const cats2 = ['a1', 'a2'];
for (let i = 0; i < 100; ++i) {
arr.push({
a: Math.random() * 10,
d: 'Row ' + i,
cat: cats[Math.floor(Math.random() * 3)],
cat2: cats[Math.floor(Math.random() * 3)]
cat2: cats2[Math.floor(Math.random() * 2)]
})
}
const builder = LineUpJS.builder(arr);
Expand All @@ -30,9 +31,8 @@
.supportTypes()
.group()
.allColumns() // add all columns
.groupBy('cat')
.sortBy('a', 'desc')
.sortBy('d', 'asc')
.groupBy('cat', 'cat2')
.groupSortBy('a')

builder
.ranking(ranking);
Expand Down
87 changes: 57 additions & 30 deletions src/model/internal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {schemeCategory10, schemeSet3} from 'd3-scale-chromatic';
import Column, {defaultGroup, IGroup, IGroupParent, IndicesArray, IOrderedGroup, ECompareValueType} from '.';
import {OrderedSet} from '../internal';
import {OrderedSet, max} from '../internal';
import {DEFAULT_COLOR} from './interfaces';


Expand Down Expand Up @@ -50,37 +50,68 @@ export function toGroupID(group: IGroup) {
}

/** @internal */
export function isOrderedGroup(g: IOrderedGroup | Readonly<IGroupParent>): g is IOrderedGroup {
return (<IOrderedGroup>g).order != null;
}


/** @internal */
function isGroupParent(g: IOrderedGroup | Readonly<IGroupParent>): g is IGroupParent {
return (<IGroupParent>g).subGroups != null;
}

/**
* unify the parents of the given groups by reusing the same group parent if possible
* @param groups
*/
export function unifyParents<T extends IOrderedGroup>(groups: T[]) {
if (groups.length <= 1) {
return;
return groups;
}
const lookup = new Map<string, IGroupParent>();

const resolve = (g: IGroupParent): {g: IGroupParent, id: string} => {
let id = g.name;
if (g.parent) {
const parent = resolve(g.parent);
g.parent = parent.g;
id = `${parent.id}.${id}`;
}
// ensure there is only one instance per id (i.e. share common parents
if (lookup.has(id)) {
return {g: lookup.get(id)!, id};
}
if (g.parent) {
g.parent.subGroups.push(g);

const toPath = (group: T) => {
const path: (IGroupParent | T)[] = [group];
let p = group.parent;
while (p) {
path.unshift(p);
p = p.parent;
}
g.subGroups = []; // clear old children
lookup.set(id, g);
return {g, id};
return path;
};
// resolve just parents
groups.forEach((g) => {
if (g.parent) {
g.parent = resolve(g.parent).g;
g.parent.subGroups.push(g);

const paths = groups.map(toPath);
// now unify paths
const maxLevel = max(paths.map((d) => d.length));
for (let level = 0; level < maxLevel; ++level) {
let currentParent: IGroupParent | null = null;
for (const path of paths) {
const node = path[level];
// null reset
if (!node || !isGroupParent(node)) {
currentParent = null;
continue;
}
// first time seeing this parent
if (!currentParent || currentParent.parent !== node.parent || currentParent.name !== node.name) {
currentParent = node;
const firstChild = path[level + 1];
// reset parent
if (firstChild) {
node.subGroups = [firstChild];
} else {
node.subGroups = [];
}
continue;
}
// same parent, reuse instance
const nextChild = path[level + 1];
if (nextChild) {
currentParent.subGroups.push(nextChild);
nextChild.parent = currentParent;
}
}
});
}
return groups;
}

/** @internal */
Expand All @@ -96,10 +127,6 @@ export function groupRoots(groups: IOrderedGroup[]) {
return Array.from(roots);
}

/** @internal */
export function isOrderedGroup(g: IOrderedGroup | Readonly<IGroupParent>): g is IOrderedGroup {
return (<IOrderedGroup>g).order != null;
}

// based on https://github.com/d3/d3-scale-chromatic#d3-scale-chromatic
const colors = schemeCategory10.concat(schemeSet3);
Expand Down
86 changes: 86 additions & 0 deletions tests/model/internal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {unifyParents} from '../../src/model/internal';
import {IGroupParent, IOrderedGroup} from '../../src/model';

function groupGen(name: string, parent?: IGroupParent): IOrderedGroup {
return {
color: 'gray',
name,
order: [],
parent
};
}

function parentGen(name: string, parent?: IGroupParent): IGroupParent {
return {
name,
color: 'gray',
parent,
subGroups: []
};
}

describe('unifyParents', () => {
it('[]', () => {
expect(unifyParents([])).toEqual([]);
});
it('single', () => {
const a = groupGen('a');
expect(unifyParents([a])).toEqual([a]);
});
it('different', () => {
const a = groupGen('a');
const b = groupGen('b');
expect(unifyParents([a, b])).toEqual([a, b]);
});
it('common parent', () => {
const p = parentGen('p');
const pClone = parentGen('p');
const a = groupGen('a', p);
const b = groupGen('b', pClone);
unifyParents([a, b]);
expect(a.parent).toBe(p);
expect(a.parent).toBe(b.parent);
});
it('different parent', () => {
const p = parentGen('p');
const p2 = parentGen('p2');
const a = groupGen('a', p);
const b = groupGen('b', p2);
unifyParents([a, b]);
expect(a.parent).toBe(p);
expect(a.parent).not.toBe(b.parent);
});
it('different in the middle', () => {
const p = parentGen('p');
const pClone = parentGen('p');
const p2 = parentGen('p2');
const a = groupGen('a', p);
const b = groupGen('b', p2);
const c = groupGen('c', pClone); // same as p
unifyParents([a, b, c]);
expect(a.parent).toBe(p);
expect(b.parent).toBe(p2);
expect(c.parent).toBe(pClone);
expect(a.parent).not.toBe(c.parent);
});

it('same - different - separate', () => {
const r = parentGen('r');
const rClone = parentGen('r');
const rClone2 = parentGen('r');

const p = parentGen('p', r);
const pClone = parentGen('p', rClone);
const p2 = parentGen('p2', rClone2);
const a = groupGen('a', p);
const b = groupGen('b', p2);
const c = groupGen('c', pClone);
unifyParents([a, b, c]);
expect(a.parent).toBe(p);
expect(b.parent).toBe(p2);
expect(c.parent).toBe(pClone);
expect(p.parent).toBe(r);
expect(pClone.parent).toBe(r);
expect(p2.parent).toBe(r);
});
});

0 comments on commit af4ff2a

Please sign in to comment.