Skip to content

Commit

Permalink
Group by string value in StringColumn (V4) (#218)
Browse files Browse the repository at this point in the history
Group by string value in StringColumn (V4)
  • Loading branch information
thinkh committed Jan 10, 2020
2 parents 16df0a0 + 89a10b7 commit 1019f08
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 36 deletions.
39 changes: 39 additions & 0 deletions demo/string_grouping.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>

<head lang="en">
<meta charset="UTF-8">
<title>LineUp String Grouping Test</title>

<link href="./LineUpJS.css" rel="stylesheet">
<link href="./demo.css" rel="stylesheet">
</head>

<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="./LineUpJS.js"></script>

<script>
window.onload = function () {
const arr = [];
for (let i = 0; i < 5000; ++i) {
arr.push({
group: Math.floor(Math.random() * 2000).toString()
});
}
const builder = LineUpJS.builder(arr);
builder
.column(LineUpJS.buildStringColumn('group'));
// and two rankings
const ranking = LineUpJS.buildRanking()
.supportTypes()
.allColumns(); // add all columns
builder
.ranking(ranking);
builder.buildTaggle(document.body);
};
</script>

</body>

</html>
11 changes: 7 additions & 4 deletions src/model/LinkColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {IDataRow, IGroup, IValueColumnDesc, ITypeFactory} from './interfaces';
import {patternFunction} from './internal';
import ValueColumn, {dataLoaded} from './ValueColumn';
import {IEventListener, ISequence} from '../internal';
import {IStringDesc, EAlignment} from './StringColumn';
import {IStringDesc, EAlignment, IStringGroupCriteria, EStringGroupCriteriaType} from './StringColumn';
import StringColumn from './StringColumn';

export interface ILinkDesc extends IStringDesc {
Expand Down Expand Up @@ -64,7 +64,10 @@ export default class LinkColumn extends ValueColumn<string | ILink> {
readonly patternTemplates: string[];

private currentFilter: string | RegExp | null = null;
private currentGroupCriteria: (RegExp | string)[] = [];
private currentGroupCriteria: IStringGroupCriteria = {
type: EStringGroupCriteriaType.startsWith,
values: []
};

readonly alignment: EAlignment;
readonly escape: boolean;
Expand Down Expand Up @@ -193,10 +196,10 @@ export default class LinkColumn extends ValueColumn<string | ILink> {
}

getGroupCriteria() {
return this.currentGroupCriteria.slice();
return this.currentGroupCriteria;
}

setGroupCriteria(value: (string | RegExp)[]) {
setGroupCriteria(value: IStringGroupCriteria) {
return StringColumn.prototype.setGroupCriteria.call(this, value);
}

Expand Down
64 changes: 50 additions & 14 deletions src/model/StringColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export enum EAlignment {
right = 'right'
}

export enum EStringGroupCriteriaType {
value = 'value',
startsWith = 'startsWith',
regex = 'regex'
}

export interface IStringGroupCriteria {
type: EStringGroupCriteriaType;
values: (string | RegExp)[];
}

export interface IStringDesc {
/**
* column alignment: left, center, right
Expand Down Expand Up @@ -59,7 +70,10 @@ export default class StringColumn extends ValueColumn<string> {
readonly alignment: EAlignment;
readonly escape: boolean;

private currentGroupCriteria: (RegExp | string)[] = [];
private currentGroupCriteria: IStringGroupCriteria = {
type: EStringGroupCriteriaType.startsWith,
values: []
};

constructor(id: string, desc: Readonly<IStringColumnDesc>) {
super(id, desc);
Expand Down Expand Up @@ -109,7 +123,11 @@ export default class StringColumn extends ValueColumn<string> {
r.filter = this.currentFilter;
}
if (this.currentGroupCriteria) {
r.groupCriteria = this.currentGroupCriteria.map((d) => typeof d === 'string' ? d : `REGEX:${d.source}`);
const {type, values} = this.currentGroupCriteria;
r.groupCriteria = {
type,
values: values.map((value) => `${type}:${value instanceof RegExp && type === EStringGroupCriteriaType.regex ? value.source : value}`)
};
}
return r;
}
Expand All @@ -121,8 +139,13 @@ export default class StringColumn extends ValueColumn<string> {
} else {
this.currentFilter = dump.filter || null;
}
// tslint:disable-next-line: early-exit
if (dump.groupCriteria) {
this.currentGroupCriteria = dump.groupCriteria.map((d: string) => d.startsWith('REGEX:') ? new RegExp(d.slice(6), 'gm') : d);
const {type, values} = <IStringGroupCriteria>dump.groupCriteria;
this.currentGroupCriteria = {
type,
values: values.map((value) => type === EStringGroupCriteriaType.regex ? new RegExp(<string>value, 'gm') : value)
};
}
}

Expand Down Expand Up @@ -169,16 +192,16 @@ export default class StringColumn extends ValueColumn<string> {
return was;
}

getGroupCriteria() {
return this.currentGroupCriteria.slice();
getGroupCriteria(): IStringGroupCriteria {
return this.currentGroupCriteria;
}

setGroupCriteria(value: (string | RegExp)[]) {
if (equal(this.currentGroupCriteria, value)) {
setGroupCriteria(value: IStringGroupCriteria) {
if (equal(this.currentGroupCriteria, value) || value == null) {
return;
}
const bak = this.getGroupCriteria();
this.currentGroupCriteria = value.slice();
this.currentGroupCriteria = value;
this.fire([StringColumn.EVENT_GROUPING_CHANGED, Column.EVENT_DIRTY_VALUES, Column.EVENT_DIRTY], bak, value);
}

Expand All @@ -187,7 +210,7 @@ export default class StringColumn extends ValueColumn<string> {
return Object.assign({}, missingGroup);
}

if (this.currentGroupCriteria.length === 0) {
if (!this.currentGroupCriteria) {
return Object.assign({}, othersGroup);
}
const value = this.getLabel(row);
Expand All @@ -196,15 +219,28 @@ export default class StringColumn extends ValueColumn<string> {
return Object.assign({}, missingGroup);
}

for (const criteria of this.currentGroupCriteria) {
if (!((criteria instanceof RegExp && criteria.test(value)) || (typeof criteria === 'string' && value.startsWith(criteria)))) {
continue;
}
const {type, values} = this.currentGroupCriteria;
if (type === EStringGroupCriteriaType.value) {
return {
name: typeof criteria === 'string' ? criteria : criteria.source,
name: value,
color: defaultGroup.color
};
}
for (const groupValue of values) {
if (type === EStringGroupCriteriaType.startsWith && typeof groupValue === 'string' && value.startsWith(groupValue)) {
return {
name: groupValue,
color: defaultGroup.color
};
}
// tslint:disable-next-line: early-exit
if (type === EStringGroupCriteriaType.regex && groupValue instanceof RegExp && groupValue.test(value)) {
return {
name: groupValue.source,
color: defaultGroup.color
};
}
}
return Object.assign({}, othersGroup);
}

Expand Down
58 changes: 40 additions & 18 deletions src/ui/dialogs/groupString.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,65 @@
import {IDialogContext} from './ADialog';
import {StringColumn} from '../../model';
import {cssClass} from '../../styles';

import {StringColumn, EStringGroupCriteriaType} from '../../model';
import {cssClass} from '../../styles/index';

/** @internal */
export default function append(col: StringColumn, node: HTMLElement, dialog: IDialogContext) {
const current = col.getGroupCriteria();
const isRegex = current.length > 0 && current[0] instanceof RegExp;
const {type, values} = current;

node.insertAdjacentHTML('beforeend', `
<label class="${cssClass('checkbox')}">
<input type="radio" name="regex" value="startsWith" id="${dialog.idPrefix}RW" ${!isRegex ? 'checked' : ''}>
<input type="radio" name="${dialog.idPrefix}groupString" value="${EStringGroupCriteriaType.value}" id="${dialog.idPrefix}VAL" ${type === EStringGroupCriteriaType.value ? 'checked' : ''}>
<span>Use text value</span>
</label>
<label class="${cssClass('checkbox')}">
<input type="radio" name="${dialog.idPrefix}groupString" value="${EStringGroupCriteriaType.startsWith}" id="${dialog.idPrefix}RW" ${type === EStringGroupCriteriaType.startsWith ? 'checked' : ''}>
<span>Text starts with &hellip;</span>
</label>
<label class="${cssClass('checkbox')}">
<input type="radio" name="regex" value="regex" id="${dialog.idPrefix}RE" ${isRegex ? 'checked' : ''}>
<input type="radio" name="${dialog.idPrefix}groupString" value="${EStringGroupCriteriaType.regex}" id="${dialog.idPrefix}RE" ${type === EStringGroupCriteriaType.regex ? 'checked' : ''}>
<span>Use regular expressions</span>
</label>
<textarea class="${cssClass('textarea')}" required rows="5" placeholder="e.g. Test,a.*" id="${dialog.idPrefix}T">${current.map((d) => typeof d === 'string' ? d : d.source).join('\n')}</textarea>
<button class="${cssClass('dialog-button')}" id="${dialog.idPrefix}A">Apply</button>
<textarea required rows="5" placeholder="e.g. Test,a.*" id="${dialog.idPrefix}T">${values.map((value) => value instanceof RegExp ? value.source : value).join('\n')}</textarea>
<button id="${dialog.idPrefix}A">Apply</button>
`);

const button = node.querySelector<HTMLButtonElement>(`#${dialog.idPrefix}A`)!;
const isRegexCheck = node.querySelector<HTMLInputElement>(`#${dialog.idPrefix}RE`)!;
const groups = node.querySelector<HTMLTextAreaElement>(`#${dialog.idPrefix}T`)!;
const valueRadioButton = node.querySelector<HTMLInputElement>(`#${dialog.idPrefix}VAL`)!;
const startsWithRadioButton = node.querySelector<HTMLInputElement>(`#${dialog.idPrefix}RW`)!;
const regexRadioButton = node.querySelector<HTMLInputElement>(`#${dialog.idPrefix}RE`)!;
const text = node.querySelector<HTMLTextAreaElement>(`#${dialog.idPrefix}T`)!;

const showOrHideTextarea = (show: boolean) => {
text.style.display = show ? null : 'none';
};

showOrHideTextarea(type !== EStringGroupCriteriaType.value);
valueRadioButton.onchange = () => showOrHideTextarea(!valueRadioButton.checked);
startsWithRadioButton.onchange = () => showOrHideTextarea(startsWithRadioButton.checked);
regexRadioButton.onchange = () => showOrHideTextarea(regexRadioButton.checked);

button.onclick = (evt) => {
evt.preventDefault();
evt.stopPropagation();
const checkedNode = node.querySelector<HTMLInputElement>(`input[name="${dialog.idPrefix}groupString"]:checked`)!;
const newType = <EStringGroupCriteriaType>checkedNode.value;
let items: (string | RegExp)[] = text.value.trim().split('\n').map((d) => d.trim()).filter((d) => d.length > 0);

let items: (string | RegExp)[] = groups.value.trim().split('\n').map((d) => d.trim()).filter((d) => d.length > 0);
const invalid = items.length === 0;
groups.setCustomValidity(invalid ? 'At least one group is required' : '');
if (invalid) {
(<any>groups).reportValidity(); // typedoc not uptodate
return;
if (newType !== EStringGroupCriteriaType.value) {
const invalid = items.length === 0;
text.setCustomValidity(invalid ? 'At least one entry is required' : '');
if (invalid) {
(<any>text).reportValidity(); // typedoc not uptodate
return;
}
}
if (isRegexCheck.checked) {
if (newType === EStringGroupCriteriaType.regex) {
items = items.map((d) => new RegExp(d.toString(), 'gm'));
}
col.setGroupCriteria(items);
col.setGroupCriteria({
type: newType,
values: items
});
};
}

0 comments on commit 1019f08

Please sign in to comment.