Skip to content

Commit edc53cb

Browse files
authored
Merge pull request #9 from keller-mark/feature/sorting-in-stack
Feature/sorting in stack
2 parents c176064 + 07945c1 commit edc53cb

File tree

7 files changed

+201
-32
lines changed

7 files changed

+201
-32
lines changed

examples-src/App.vue

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,17 @@
388388
<h3>&lt;SortOptions/&gt;</h3>
389389
<SortOptions
390390
variable="sample_id"
391-
:by="sampleSortBy"
391+
:by="sampleSortByExposures"
392392
:getScale="getScale"
393393
:getData="getData"
394+
:getStack="getStack"
395+
/>
396+
<SortOptions
397+
variable="sample_id"
398+
:by="sampleSortByAge"
399+
:getScale="getScale"
400+
:getData="getData"
401+
:getStack="getStack"
394402
/>
395403

396404

@@ -639,7 +647,7 @@ const getScale = function(scaleKey) {
639647
640648
641649
// Initialize the stack
642-
const stack = new HistoryStack(getScale);
650+
const stack = new HistoryStack(getScale, getData);
643651
stack.push(new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "reset"), true);
644652
stack.push(new HistoryEvent(HistoryEvent.types.SCALE, "exposure", "reset"), true);
645653
stack.push(new HistoryEvent(HistoryEvent.types.SCALE, "signature", "reset"), true);
@@ -656,11 +664,15 @@ const getStack = function() {
656664
return stack;
657665
}
658666
659-
const sampleSortBy = new SortBy(
667+
const sampleSortByExposures = new SortBy(
660668
"exposures_data",
661669
signatureScale.domain
662670
);
663671
672+
const sampleSortByAge = new SortBy(
673+
'clinical_data',
674+
['age']
675+
);
664676
665677
666678
export default {
@@ -688,9 +700,10 @@ export default {
688700
return {
689701
getData: getData,
690702
getScale: getScale,
691-
sampleSortBy: sampleSortBy,
692703
getStack: getStack,
693-
showStack: false
704+
showStack: false,
705+
sampleSortByExposures: sampleSortByExposures,
706+
sampleSortByAge: sampleSortByAge
694707
}
695708
},
696709
created() {

src/components/SortOptions.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323

2424
<script>
2525
import SortBy from './../sort/SortBy.js';
26+
import HistoryEvent from '../history/HistoryEvent.js';
27+
import { computedParam } from '../history/HistoryStack.js';
2628
27-
let uuid = 0;
2829
export default {
2930
name: 'SortOptions',
3031
props: {
@@ -39,6 +40,9 @@ export default {
3940
},
4041
'getData': {
4142
type: Function
43+
},
44+
'getStack': {
45+
type: Function
4246
}
4347
},
4448
data() {
@@ -58,6 +62,8 @@ export default {
5862
created() {
5963
console.assert(this.by instanceof SortBy);
6064
// TODO: Make assertions about scale types?
65+
66+
this._stack = this.getStack();
6167
},
6268
methods: {
6369
validSelection(varValue) {
@@ -66,6 +72,11 @@ export default {
6672
go() {
6773
if(this.validSelection(this.selectedKey)) {
6874
this.getScale(this.variable).sort(this.getData(this.by.data), this.selectedKey, this.sortAscending);
75+
this._stack.push(new HistoryEvent(HistoryEvent.types.SCALE, this.variable, "sort", [
76+
computedParam("getData", [this.by.data]),
77+
this.selectedKey,
78+
this.sortAscending
79+
]));
6980
}
7081
}
7182
}

src/history/HistoryStack.js

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import HistoryEvent from './HistoryEvent.js';
22

3+
const VDP_COMPUTED_PARAM = "$vdp_val_from_getter";
4+
5+
/**
6+
* Returns an object that represents a "computed" history event parameter.
7+
* @param {string} getterFunction Name of function to be called
8+
* @param {array} getterParams Params passed to the function to be called.
9+
* @returns {object} Returns an object with an identifier signifying that this parameter should be computed.
10+
*/
11+
export const computedParam = (getterFunction, getterParams) => {
12+
return {
13+
[VDP_COMPUTED_PARAM]: true,
14+
"getterFunction": getterFunction,
15+
"getterParams": getterParams
16+
};
17+
};
18+
319
/**
420
* Represents a history of all application interaction events,
521
* which can be used for forward(redo)/backward(undo) navigation.
@@ -9,9 +25,11 @@ export default class HistoryStack {
925
/**
1026
* Create a new history stack.
1127
* @param {function} getScale Function that returns a scale object for a provided string key.
28+
* @param {function} getData Function that returns a data container object for a provided string key.
1229
*/
13-
constructor(getScale) {
30+
constructor(getScale, getData) {
1431
this._getScale = getScale;
32+
this._getData = getData;
1533
this._initial = []; // initial stack
1634
this._stack = []; // user-event stack
1735
this._pointer = undefined; // user-event stack pointer
@@ -144,6 +162,29 @@ export default class HistoryStack {
144162
this._pointer = (this._pointer === undefined ? 0 : this._pointer + 1);
145163
}
146164

165+
/**
166+
* Parse parameters to check for the need to call a getter function.
167+
* @param {array} params The serialized parameter array.
168+
* @returns {array} Parsed params, replacing with calls to getter functions if necessary.
169+
*/
170+
parseParams(params) {
171+
return params.map((p) => {
172+
if(typeof p === "object") {
173+
if(p.hasOwnProperty(VDP_COMPUTED_PARAM)) {
174+
// can assume that this object represents a call to a "getter": getScale, getStack, etc...
175+
console.assert(typeof p.getterFunction === "string");
176+
console.assert(p.getterFunction.substring(0, 3) === "get");
177+
let getterFunction = this[("_" + p.getterFunction)];
178+
console.assert(Array.isArray(p.getterParams));
179+
let getterParams = p.getterParams;
180+
181+
return getterFunction( ...getterParams );
182+
}
183+
}
184+
return p;
185+
});
186+
}
187+
147188
/**
148189
* Execute a provided event.
149190
* @param {HistoryEvent} event
@@ -167,12 +208,9 @@ export default class HistoryStack {
167208

168209
if(getTargetFunc !== undefined) {
169210
let target = getTargetFunc(event.id);
170-
/*
171-
TODO: parse event.params to search for symbols?
172-
For example, if one wanted to use a specific dataset as a param,
173-
could encode as the string "{data:myDatasetKey}" or something...
174-
*/
175-
target[event.action]( ...event.params );
211+
212+
let parsedParams = this.parseParams(event.params)
213+
target[event.action]( ...parsedParams );
176214
} else {
177215
console.error("Error: the target function specified by the HistoryEvent type is undefined");
178216
}

src/scales/AbstractScale.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default class AbstractScale {
3131
this._name = name;
3232
this._domain = domain;
3333
this._domainFiltered = domain.slice();
34+
this._domainOriginal = domain.slice();
3435
this._dispatch = d3_dispatch(
3536
DISPATCH_EVENT_UPDATE,
3637
DISPATCH_EVENT_HIGHLIGHT,
@@ -190,7 +191,8 @@ export default class AbstractScale {
190191
* Resets the filtered domain, using the full original domain.
191192
*/
192193
reset() {
193-
this.setDomainFiltered(this._domain.slice());
194+
this.setDomain(this._domainOriginal.slice());
195+
this.setDomainFiltered(this._domainOriginal.slice());
194196
this.emitUpdate();
195197
}
196198
}

src/scales/CategoricalScale.js

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,30 +100,47 @@ export default class CategoricalScale extends AbstractScale {
100100
* @param {boolean} ascending Whether to sort ascending or descending.
101101
*/
102102
sort(dataContainer, var1D, ascending=true) {
103-
// TODO: use d3_descending/ascending
104103
let comparator;
105104
let compareFunc = d3_ascending;
106105
if(!ascending) {
107106
compareFunc = d3_descending;
108107
}
109-
110-
comparator = (a, b) => compareFunc(
111-
(a[var1D] == "nan" ? -1 : +a[var1D]),
112-
(b[var1D] == "nan" ? -1 : +b[var1D])
113-
);
114-
115-
// TODO: Sort the data using the comparator, doing something like this
108+
116109
let data = dataContainer.dataCopy;
117-
console.assert(data instanceof Array);
118-
data = data.sort(comparator);
119-
// Use map to get array of this.id, filter using those indices
120-
// Set overall domain
121-
let elementsSorted = data.map((el) => el[this.id]);
122-
this.setDomain(elementsSorted);
110+
console.assert(Array.isArray(data));
111+
112+
comparator = (domainA, domainB) => {
113+
let dataA = data.find((el) => el[this.id] === domainA);
114+
let dataB = data.find((el) => el[this.id] === domainB);
115+
116+
let a, b;
117+
if(dataA === undefined || dataA[var1D] === "nan") {
118+
a = -1;
119+
} else {
120+
a = +dataA[var1D];
121+
}
122+
if(dataB === undefined || dataB[var1D] === "nan") {
123+
b = -1;
124+
} else {
125+
b = +dataB[var1D];
126+
}
127+
128+
return compareFunc(a, b)
129+
};
130+
131+
132+
133+
let domainCopy = this.domain.slice();
134+
let domainFilteredCopy = this.domainFiltered.slice();
135+
136+
137+
138+
let newDomain = domainCopy.sort(comparator);
139+
this.setDomain(newDomain);
123140

124141
// Set filtered domain
125-
let elementsSortedFiltered = elementsSorted.filter((el) => this._domainFiltered.includes(el));
126-
this.setDomainFiltered(elementsSortedFiltered);
142+
let newDomainFiltered = domainFilteredCopy.sort(comparator);
143+
this.setDomainFiltered(newDomainFiltered);
127144

128145
this.emitUpdate();
129146
}

test/history/HistoryStack.spec.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
import HistoryEvent from '../../src/history/HistoryEvent';
2-
import HistoryStack from '../../src/history/HistoryStack';
2+
import HistoryStack, { computedParam } from '../../src/history/HistoryStack';
33
import CategoricalScale from '../../src/scales/CategoricalScale';
4+
import DataContainer from '../../src/data/DataContainer';
45

56
let getScale;
7+
let getData;
68
let sampleScale;
9+
let sampleData;
710
beforeEach(() => {
811
sampleScale = new CategoricalScale("sample_id", "Samples", ["S1", "S2", "S3", "S4", "S5", "S6"]);
912
getScale = () => sampleScale;
13+
14+
sampleData = [
15+
{
16+
"sample_id": "S1",
17+
"age": 3
18+
},
19+
{
20+
"sample_id": "S2",
21+
"age": 2
22+
},
23+
{
24+
"sample_id": "S3",
25+
"age": 10
26+
},
27+
{
28+
"sample_id": "S4",
29+
"age": 1
30+
},
31+
{
32+
"sample_id": "S5",
33+
"age": 5
34+
}
35+
];
36+
37+
let sampleDataContainer = new DataContainer("sample_data", "Sample Data", sampleData);
38+
39+
getData = () => sampleDataContainer;
1040
});
1141

1242
test('able to create a HistoryStack', () => {
@@ -81,4 +111,61 @@ test('able to execute event, go back and go forward', () => {
81111
stack.goForward();
82112
expect(sampleScale.domainFiltered.length).toBe(4);
83113

114+
});
115+
116+
test('able to get computedParam JSON object', () => {
117+
let cp = computedParam("getData", ["my_data"])
118+
119+
expect(cp).toHaveProperty("$vdp_val_from_getter");
120+
expect(cp).toHaveProperty("getterFunction");
121+
expect(cp).toHaveProperty("getterParams");
122+
});
123+
124+
test('able to execute event with computed parameter', () => {
125+
let stack = new HistoryStack(getScale, getData);
126+
127+
let e0 = new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "reset");
128+
stack.push(e0, true);
129+
130+
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
131+
expect(sampleScale.domainFiltered).toEqual(["S1", "S2", "S3", "S4", "S5", "S6"]);
132+
133+
// Sort by age ascending, store in stack
134+
sampleScale.sort(getData(), "age", true);
135+
let e1 = new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "sort", [computedParam("getData", []), "age", true]);
136+
stack.push(e1);
137+
138+
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
139+
expect(sampleScale.domainFiltered).toEqual(["S6", "S4", "S2", "S1", "S5", "S3"]);
140+
141+
// Sort by age descending, store in stack
142+
sampleScale.sort(getData(), "age", false);
143+
let e2 = new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "sort", [computedParam("getData", []), "age", false]);
144+
stack.push(e2);
145+
146+
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
147+
expect(sampleScale.domainFiltered).toEqual(["S3", "S5", "S1", "S2", "S4", "S6"]);
148+
149+
// Go back and forward, check resulting domain orderings
150+
151+
// Back to ascending
152+
expect(stack.canGoBack()).toBe(true);
153+
stack.goBack();
154+
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
155+
expect(sampleScale.domainFiltered).toEqual(["S6", "S4", "S2", "S1", "S5", "S3"]);
156+
157+
// Forward to descending
158+
expect(stack.canGoForward()).toBe(true);
159+
stack.goForward();
160+
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
161+
expect(sampleScale.domainFiltered).toEqual(["S3", "S5", "S1", "S2", "S4", "S6"]);
162+
163+
// Back twice to original "reset" ordering
164+
expect(stack.canGoBack()).toBe(true);
165+
stack.goBack();
166+
expect(stack.canGoBack()).toBe(true);
167+
stack.goBack();
168+
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
169+
expect(sampleScale.domainFiltered).toEqual(["S1", "S2", "S3", "S4", "S5", "S6"]);
170+
84171
});

test/scales/CategoricalScale.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ test('able to filter a CategoricalScale', () => {
3535
scale.filter([1, 2])
3636
expect(scale.domain.length).toBe(6);
3737
expect(scale.domainFiltered.length).toBe(2);
38-
});
38+
});
39+

0 commit comments

Comments
 (0)