Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions examples-src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,17 @@
<h3>&lt;SortOptions/&gt;</h3>
<SortOptions
variable="sample_id"
:by="sampleSortBy"
:by="sampleSortByExposures"
:getScale="getScale"
:getData="getData"
:getStack="getStack"
/>
<SortOptions
variable="sample_id"
:by="sampleSortByAge"
:getScale="getScale"
:getData="getData"
:getStack="getStack"
/>


Expand Down Expand Up @@ -639,7 +647,7 @@ const getScale = function(scaleKey) {


// Initialize the stack
const stack = new HistoryStack(getScale);
const stack = new HistoryStack(getScale, getData);
stack.push(new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "reset"), true);
stack.push(new HistoryEvent(HistoryEvent.types.SCALE, "exposure", "reset"), true);
stack.push(new HistoryEvent(HistoryEvent.types.SCALE, "signature", "reset"), true);
Expand All @@ -656,11 +664,15 @@ const getStack = function() {
return stack;
}

const sampleSortBy = new SortBy(
const sampleSortByExposures = new SortBy(
"exposures_data",
signatureScale.domain
);

const sampleSortByAge = new SortBy(
'clinical_data',
['age']
);


export default {
Expand Down Expand Up @@ -688,9 +700,10 @@ export default {
return {
getData: getData,
getScale: getScale,
sampleSortBy: sampleSortBy,
getStack: getStack,
showStack: false
showStack: false,
sampleSortByExposures: sampleSortByExposures,
sampleSortByAge: sampleSortByAge
}
},
created() {
Expand Down
13 changes: 12 additions & 1 deletion src/components/SortOptions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@

<script>
import SortBy from './../sort/SortBy.js';
import HistoryEvent from '../history/HistoryEvent.js';
import { computedParam } from '../history/HistoryStack.js';

let uuid = 0;
export default {
name: 'SortOptions',
props: {
Expand All @@ -39,6 +40,9 @@ export default {
},
'getData': {
type: Function
},
'getStack': {
type: Function
}
},
data() {
Expand All @@ -58,6 +62,8 @@ export default {
created() {
console.assert(this.by instanceof SortBy);
// TODO: Make assertions about scale types?

this._stack = this.getStack();
},
methods: {
validSelection(varValue) {
Expand All @@ -66,6 +72,11 @@ export default {
go() {
if(this.validSelection(this.selectedKey)) {
this.getScale(this.variable).sort(this.getData(this.by.data), this.selectedKey, this.sortAscending);
this._stack.push(new HistoryEvent(HistoryEvent.types.SCALE, this.variable, "sort", [
computedParam("getData", [this.by.data]),
this.selectedKey,
this.sortAscending
]));
}
}
}
Expand Down
52 changes: 45 additions & 7 deletions src/history/HistoryStack.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import HistoryEvent from './HistoryEvent.js';

const VDP_COMPUTED_PARAM = "$vdp_val_from_getter";

/**
* Returns an object that represents a "computed" history event parameter.
* @param {string} getterFunction Name of function to be called
* @param {array} getterParams Params passed to the function to be called.
* @returns {object} Returns an object with an identifier signifying that this parameter should be computed.
*/
export const computedParam = (getterFunction, getterParams) => {
return {
[VDP_COMPUTED_PARAM]: true,
"getterFunction": getterFunction,
"getterParams": getterParams
};
};

/**
* Represents a history of all application interaction events,
* which can be used for forward(redo)/backward(undo) navigation.
Expand All @@ -9,9 +25,11 @@ export default class HistoryStack {
/**
* Create a new history stack.
* @param {function} getScale Function that returns a scale object for a provided string key.
* @param {function} getData Function that returns a data container object for a provided string key.
*/
constructor(getScale) {
constructor(getScale, getData) {
this._getScale = getScale;
this._getData = getData;
this._initial = []; // initial stack
this._stack = []; // user-event stack
this._pointer = undefined; // user-event stack pointer
Expand Down Expand Up @@ -144,6 +162,29 @@ export default class HistoryStack {
this._pointer = (this._pointer === undefined ? 0 : this._pointer + 1);
}

/**
* Parse parameters to check for the need to call a getter function.
* @param {array} params The serialized parameter array.
* @returns {array} Parsed params, replacing with calls to getter functions if necessary.
*/
parseParams(params) {
return params.map((p) => {
if(typeof p === "object") {
if(p.hasOwnProperty(VDP_COMPUTED_PARAM)) {
// can assume that this object represents a call to a "getter": getScale, getStack, etc...
console.assert(typeof p.getterFunction === "string");
console.assert(p.getterFunction.substring(0, 3) === "get");
let getterFunction = this[("_" + p.getterFunction)];
console.assert(Array.isArray(p.getterParams));
let getterParams = p.getterParams;

return getterFunction( ...getterParams );
}
}
return p;
});
}

/**
* Execute a provided event.
* @param {HistoryEvent} event
Expand All @@ -167,12 +208,9 @@ export default class HistoryStack {

if(getTargetFunc !== undefined) {
let target = getTargetFunc(event.id);
/*
TODO: parse event.params to search for symbols?
For example, if one wanted to use a specific dataset as a param,
could encode as the string "{data:myDatasetKey}" or something...
*/
target[event.action]( ...event.params );

let parsedParams = this.parseParams(event.params)
target[event.action]( ...parsedParams );
} else {
console.error("Error: the target function specified by the HistoryEvent type is undefined");
}
Expand Down
4 changes: 3 additions & 1 deletion src/scales/AbstractScale.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class AbstractScale {
this._name = name;
this._domain = domain;
this._domainFiltered = domain.slice();
this._domainOriginal = domain.slice();
this._dispatch = d3_dispatch(
DISPATCH_EVENT_UPDATE,
DISPATCH_EVENT_HIGHLIGHT,
Expand Down Expand Up @@ -190,7 +191,8 @@ export default class AbstractScale {
* Resets the filtered domain, using the full original domain.
*/
reset() {
this.setDomainFiltered(this._domain.slice());
this.setDomain(this._domainOriginal.slice());
this.setDomainFiltered(this._domainOriginal.slice());
this.emitUpdate();
}
}
49 changes: 33 additions & 16 deletions src/scales/CategoricalScale.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,30 +100,47 @@ export default class CategoricalScale extends AbstractScale {
* @param {boolean} ascending Whether to sort ascending or descending.
*/
sort(dataContainer, var1D, ascending=true) {
// TODO: use d3_descending/ascending
let comparator;
let compareFunc = d3_ascending;
if(!ascending) {
compareFunc = d3_descending;
}

comparator = (a, b) => compareFunc(
(a[var1D] == "nan" ? -1 : +a[var1D]),
(b[var1D] == "nan" ? -1 : +b[var1D])
);

// TODO: Sort the data using the comparator, doing something like this

let data = dataContainer.dataCopy;
console.assert(data instanceof Array);
data = data.sort(comparator);
// Use map to get array of this.id, filter using those indices
// Set overall domain
let elementsSorted = data.map((el) => el[this.id]);
this.setDomain(elementsSorted);
console.assert(Array.isArray(data));

comparator = (domainA, domainB) => {
let dataA = data.find((el) => el[this.id] === domainA);
let dataB = data.find((el) => el[this.id] === domainB);

let a, b;
if(dataA === undefined || dataA[var1D] === "nan") {
a = -1;
} else {
a = +dataA[var1D];
}
if(dataB === undefined || dataB[var1D] === "nan") {
b = -1;
} else {
b = +dataB[var1D];
}

return compareFunc(a, b)
};



let domainCopy = this.domain.slice();
let domainFilteredCopy = this.domainFiltered.slice();



let newDomain = domainCopy.sort(comparator);
this.setDomain(newDomain);

// Set filtered domain
let elementsSortedFiltered = elementsSorted.filter((el) => this._domainFiltered.includes(el));
this.setDomainFiltered(elementsSortedFiltered);
let newDomainFiltered = domainFilteredCopy.sort(comparator);
this.setDomainFiltered(newDomainFiltered);

this.emitUpdate();
}
Expand Down
89 changes: 88 additions & 1 deletion test/history/HistoryStack.spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
import HistoryEvent from '../../src/history/HistoryEvent';
import HistoryStack from '../../src/history/HistoryStack';
import HistoryStack, { computedParam } from '../../src/history/HistoryStack';
import CategoricalScale from '../../src/scales/CategoricalScale';
import DataContainer from '../../src/data/DataContainer';

let getScale;
let getData;
let sampleScale;
let sampleData;
beforeEach(() => {
sampleScale = new CategoricalScale("sample_id", "Samples", ["S1", "S2", "S3", "S4", "S5", "S6"]);
getScale = () => sampleScale;

sampleData = [
{
"sample_id": "S1",
"age": 3
},
{
"sample_id": "S2",
"age": 2
},
{
"sample_id": "S3",
"age": 10
},
{
"sample_id": "S4",
"age": 1
},
{
"sample_id": "S5",
"age": 5
}
];

let sampleDataContainer = new DataContainer("sample_data", "Sample Data", sampleData);

getData = () => sampleDataContainer;
});

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

});

test('able to get computedParam JSON object', () => {
let cp = computedParam("getData", ["my_data"])

expect(cp).toHaveProperty("$vdp_val_from_getter");
expect(cp).toHaveProperty("getterFunction");
expect(cp).toHaveProperty("getterParams");
});

test('able to execute event with computed parameter', () => {
let stack = new HistoryStack(getScale, getData);

let e0 = new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "reset");
stack.push(e0, true);

expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
expect(sampleScale.domainFiltered).toEqual(["S1", "S2", "S3", "S4", "S5", "S6"]);

// Sort by age ascending, store in stack
sampleScale.sort(getData(), "age", true);
let e1 = new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "sort", [computedParam("getData", []), "age", true]);
stack.push(e1);

expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
expect(sampleScale.domainFiltered).toEqual(["S6", "S4", "S2", "S1", "S5", "S3"]);

// Sort by age descending, store in stack
sampleScale.sort(getData(), "age", false);
let e2 = new HistoryEvent(HistoryEvent.types.SCALE, "sample_id", "sort", [computedParam("getData", []), "age", false]);
stack.push(e2);

expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
expect(sampleScale.domainFiltered).toEqual(["S3", "S5", "S1", "S2", "S4", "S6"]);

// Go back and forward, check resulting domain orderings

// Back to ascending
expect(stack.canGoBack()).toBe(true);
stack.goBack();
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
expect(sampleScale.domainFiltered).toEqual(["S6", "S4", "S2", "S1", "S5", "S3"]);

// Forward to descending
expect(stack.canGoForward()).toBe(true);
stack.goForward();
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
expect(sampleScale.domainFiltered).toEqual(["S3", "S5", "S1", "S2", "S4", "S6"]);

// Back twice to original "reset" ordering
expect(stack.canGoBack()).toBe(true);
stack.goBack();
expect(stack.canGoBack()).toBe(true);
stack.goBack();
expect(sampleScale.domain).toEqual(sampleScale.domainFiltered);
expect(sampleScale.domainFiltered).toEqual(["S1", "S2", "S3", "S4", "S5", "S6"]);

});
3 changes: 2 additions & 1 deletion test/scales/CategoricalScale.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ test('able to filter a CategoricalScale', () => {
scale.filter([1, 2])
expect(scale.domain.length).toBe(6);
expect(scale.domainFiltered.length).toBe(2);
});
});