QueryDataModel

QueryDataModel

This class definition lets you create a Data Model from a specific JSON object which
describes a n-dimensional system. The created model allows you to query
and navigate through the data via an API, UI control, and/or mouse interaction.

That model will handle all the data management and delegate the data processing
or rendering to your application.

This documentation will focus on its API and its usage.

JSON Structure

The core of that Query Data Model is the JSON structure that it loads
and works with. The following JSON structure illustrate what this module is expecting:

{
id : 'unique_identifier_like_a_sha',
type : [ 'tonic-query-data-model',
'extended-model-name-1',
'extended-model-name-2' ],
arguments : {
't' : {
label : 'Time',
default : 3,
ui : 'slider',
values : [ '00', '05', '10', '15', '20' ],
loop : 'reverse',
bind : {
mouse : { zoom : { modifier: 0, coordinate: 1, step: 0 } }
}
},
'field' : {
label : 'Field',
default : 1,
ui : 'list',
values : [ 'Pressure', 'Temperature', 'Velocity' ]
},
'alpha' : {
label : 'Angle',
ui : 'slider',
values : [ '0', '90', '180', '270' ],
loop : 'modulo',
bind : {
mouse : { drag : { modifier: 0, coordinate: 0, step: 10 } }
}
}
},
arguments_order: [ 'field', 'alpha', 't' ],
data: [
{
name : 'image',
type : 'blob',
mimeType: 'image/jpg',
pattern : '{field}/{t}_{alpha}.jpg'
},{
name : 'description',
type : 'text',
pattern : '{field}/{t}.txt'
},{
name : 'statistic',
type : 'json',
pattern : '{field}/{t}_{alpha}.json',
category: ['option-b']
}
],
metadata: { /* Can contain what you want */ }
}

id

A string that can be used to uniquely identify the given model. If none is provided,
the QueryDataModel will automatically generate one.

type

Describe the composition of the given model. As we describe a Query Data Model
it must contain ‘tonic-query-data-model’ but can be augmented with any
additional data which could also then be used to drive the querying/processing/rendering.

What is described here is what we expect as a ‘tonic-query-data-model’ type.
Additional data structures can be added but they should not interfere or redefine
the existing core structure.

arguments

List of arguments that can be used to describe the query.
Each argument must have at least a list of values.

The rest of the options will then be derived from their default.
Here is the list of defaults that will be picked if they are not provided.

{
arguments: {
argId: {
label : 'argId', // The actual name of the argument
default : 0, // The first item in the values list
ui : 'list', // The UI will use a drop down
loop : undefined, // No looping on this parameter
bind : undefined // No binding will occur on mouse/key/?
}
}
}

Here is the possible set of values for each argument and what they are:

  • label : Human readable name for UI.
  • default : Index within the values for initial value. (default: 0)
  • ui : Type of UI component to use. Can either be [ undefined, ‘slider’, ‘list‘].
  • loop : Specify a periodic/looping behavior. Can either be [ undefined, ‘modulo’, ‘reverse’]
  • bind : Specify how to bind interactions to attribute.

arguments_order

Order of the arguments that should be used to build the user interface.
This is an array of strings with the name of all the attributes you want to see
inside the user interface.

data

List of data to be fetched. Each element in the list must have a name, type and a pattern.
Then if the type is ‘blob’, a mimeType must be specified.

pattern must be defined is using the {} to surround the name of the
parameter that can compose the query.

Optionally, data can be grouped by categories to simplify the way they’re fetched. This allows you to exclusively fetch the data you are currently interested in.
If no category is provided, that means the full data list is mandatory.

metadata

User data that can easily be accessed via the API.

constructor(jsonObject, basepath)

The jsonObject content has been previously described but the basepath is used to
fetch the related resource relative to that basepath.

getQuery() : { argA: valueA, argB: valueB }

Returns a simple map where the keys are the arguments attribute names and the
values are the actual value for the given attribute.

fetchData(category)

Triggers the download of the data associated with the current query.

If no category is provided, then only the data with no category will be downloaded
while if a category is provided all the data that belong to that category will be
downloaded.

In order to be aware when that set of data is available for processing you can
attach a listener by calling the following method.

// For the default category
instance.onDataChange( (data, envelope) => {} );

// For custom category
instance.on( categoryName, (data, envelope) => {});

// Data layout will look like that
var data = {
dataName1: dataContentFromDataManager,
dataName2: dataContentFromDataManager
}

lazyFetchData(category)

Similar to fetchData(category) except that it won’t make a network request
until the previous one is fully done. Moreover any lazyFetchData() call made
during that pending period will be ignored and only the latest one will be triggered.

Very useful when interacting with a slider and/or mouse interaction for changing
the underlying query.

{action}(attributeName) : changeDetected

Several actions can be performed on a given attribute to change the query.
Sometimes an action won’t result in any change as we may have reached the end
of the range of a non-looping attribute. Hence we provide feedback to the user
to let them know if their method call affected the underline query or not.

Here is the list of possible actions:

  • first : Move to the first value of the values list.
  • previous : Move to the previous value in the values list.
  • next : Move to the next value in the values list.
  • last : Move to the last value in the values list.

This will trigger the following events:

  • state.change.first
  • state.change.previous
  • state.change.next
  • state.change.last

Those events could be capture by adding a listener using the onStateChange method.

setValue(attributeName, value) : changeDetected

Assigns a new value to the attribute, but the value must be part of the values list.

This will trigger the following event:

  • state.change.value

That event can be capture by adding a listener using the onStateChange method.

setIndex(attributeName, index) : changeDetected

Assigns a new value to the attribute base on the value that is at provided index
inside the values list.

This will trigger the following event:

  • state.change.idx

That event can be capture by adding a listener using the onStateChange method.

getValue(attributeName) : value

Returns the current value inside the values list.

getValues(attributeName) : values

Returns the full list of possible values.

getIndex(attributeName) : index

Returns the current value index inside the values list.

getSize(attributeName) : index

Returns the number of values inside the values list or null if the attribute
was not found.

getUiType(attributeName) : String

Returns the type of the UI that should be created. It can either be slider or list.

label(attributeName) : String

Returns the label that should be used for the given attribute.

setAnimationFlag(attributeName, flagState)

Tags or untags a given attribute to be part of the animation process.

This will trigger the following event:

  • state.change.animation

That event can be capture by adding a listener using the onStateChange method.

getAnimationFlag(attributeName)

Returns the current state of the animation flag for a given attribute.

toggleAnimationFlag(attributeName) : currentState

Toggles the animation flag state and return its actual value.

This will trigger the following event:

  • state.change.animation

That event can be capture by adding a listener using the onStateChange method.

hasAnimationFlag() : Boolean

Returns true if at least one argument has been toggled for animation.

isAnimating() : Boolean

Returns true if an animation is currently playing.

animate( start[, deltaT=500] )

Start or stop an animation where a deltaT can be provided in milliseconds. For example:

// Will start the animation with a wait time of 750ms
// before going to the next data query.
animate(true, 750);

To stop a running animation.

animate(false);

On an animation stop, the following event will be triggered:

  • state.change.play

That event can be capture by adding a listener using the onStateChange method.

getDataMetaData(dataName) : { data metadata }

Helper function used to retrieved any metadata that could be associated to a data
entry.

onDataChange( listener ) : subscriptionObject

Add a data listener to be aware when the expected set of data will be ready.
The data object that is provided to the listener will contains all the
cachedDataObject from dataManager instance that is part of the category that was
used to register the listener, using their name from the original JSON data model.

function listener(data, envelope) {
// Where data is
// {
// data-name: {data-manager}cacheObject
// }
}

More information on subscriptionObject and listeners can be found on
monologue.js.

For instance, to listen for a specific category, you should use the following method:

instance.on(categoryName, listener);

In fact, the onDataChange is calling the this.on(‘_’, listener) as the ‘_’ is
the default category name.

onStateChange(listener)

Shortcut for this.on(‘state.change.*’, listener).

And the content of such event will be as follow.

var event = {
value: "0.1",
idx: 0,
name: "Time",
instance: queryDataModelInstance
};

getMouseListener() : { drag: callback, zoom: callback }

This method will create a callback map that can be used to be attach to a MouseHandler instance.

If no mouse binding was defined in the original model, then the method will return null.

clone() : new instance

This method will create a new instance of QueryDataModel using the same core
but totally disconnected event or query wise.

getCacheSize()

Return the maximum size that the cache can contain before the gc() take any action.

setCacheSize( maxSizeInBytes )

Update the maximum cache size to adjust when the garbage collector should empty it.

getMemoryUsage()

Return the current memory usage of the cache data in Bytes.

link(queryDataModel, args=null, fetch=false) : Subscription

Link any attribute change from the given queryDataModel to your local instance.
Optionally, you can provide a list of arguments that you want to synchronize as
well as any change could also trigger a fetchData() call on your instance.
The returned object let you unsubscribe of the change update.

Source

index.js
import hasOwn from 'mout/object/hasOwn';
import max from 'mout/object/max';
import min from 'mout/object/min';
import Monologue from 'monologue.js';
import now from 'mout/src/time/now';
import omit from 'mout/object/omit';
import size from 'mout/object/size';

import DataManager from 'paraviewweb/src/IO/Core/DataManager';

// ============================================================================
const dataManager = new DataManager();
const DEFAULT_KEY_NAME = '_';
// ============================================================================
let queryDataModelCounter = 0;
// ============================================================================

// Helper function used to handle next/previous when the loop function is 'reverse'
function deltaReverse(arg, increment) {
let newIdx = arg.idx + arg.direction * increment;
if (newIdx >= arg.values.length) {
arg.direction *= -1; // Reverse direction
newIdx = arg.values.length - 2;
}

if (newIdx < 0) {
arg.direction *= -1; // Reverse direction
newIdx = 1;
}

if (newIdx >= 0 && newIdx < arg.values.length) {
arg.idx = newIdx;
}

return true;
}

// Helper function used to handle next/previous when the loop function is 'modulo'
function deltaModulo(arg, increment) {
arg.idx = (arg.values.length + arg.idx + increment) % arg.values.length;
return true;
}

// Helper function used to handle next/previous when the loop function is 'none'
function deltaNone(arg, increment) {
let newIdx = arg.idx + increment;

if (newIdx >= arg.values.length) {
newIdx = arg.values.length - 1;
}

if (newIdx < 0) {
newIdx = 0;
}

if (arg.idx !== newIdx) {
arg.idx = newIdx;
return true;
}

return false;
}

// QueryDataModel class definition
export default class QueryDataModel {
constructor(jsonData, basepath) {
this.originalData = jsonData;
this.basepath = basepath; // Needed for cloning
queryDataModelCounter += 1;
this.id = `QueryDataModel_${queryDataModelCounter} :`;
this.args = {};
this.externalArgs = {};
this.dataCount = {};
this.categories = {};
this.requests = [];
this.keepAnimating = false;
this.animationTimerId = 0;
this.mouseListener = null;
this.dataMetadata = {};
this.lazyFetchRequest = null;
this.registeredURLs = [];
this.disableImageSelection = false;

this.playNext = () => {
if (this.keepAnimating) {
let changeDetected = false;
this.lastPlay = +new Date();

// Move all flagged arg to next()
Object.keys(this.args).forEach((argName) => {
if (this.args[argName].anime) {
changeDetected = this.next(argName) || changeDetected;
}
});

// Keep moving if change detected
if (changeDetected) {
// Get new data
this.lazyFetchData(); // FIXME may need a category
} else {
// Auto stop as nothing change
this.keepAnimating = false;
this.emit('state.change.play', {
instance: this,
});
}
} else {
this.emit('state.change.play', {
instance: this,
});
}
};

const processRequest = (request) => {
const dataToBroadcast = {};
let count = request.urls.length;
let hasPending = false;
let hasError = false;

if (this.animationTimerId !== 0) {
clearTimeout(this.animationTimerId);
this.animationTimerId = 0;
}

while (count) {
count -= 1;
const item = request.urls[count];
dataToBroadcast[item.key] = dataManager.get(item.url);
if (dataToBroadcast[item.key]) {
hasPending = hasPending || dataToBroadcast[item.key].pending;
} else {
hasError = true;
}
}

if (hasPending) {
// put the request back in the queue
setTimeout(() => {
this.requests.push(request);
}, 0);
} else if (!hasError) {
// We are good to go
// Broadcast data to the category
this.emit(request.category, dataToBroadcast);

// Trigger new fetch data if any lazyFetchData is pending
if (this.requests.length === 0 && this.lazyFetchRequest) {
this.fetchData(this.lazyFetchRequest);
this.lazyFetchRequest = null;
}
}

// Handle animation if any
if (this.keepAnimating) {
const ts = +new Date();
this.animationTimerId = setTimeout(
this.playNext,
ts - this.lastPlay > this.deltaT ? 0 : this.deltaT
);
}
};

const dataHandler = (data, envelope) => {
this.dataCount[envelope.topic] += 1;

// Pre-decode image urls
if (
data.url &&
data.type === 'blob' &&
data.data.type.indexOf('image') !== -1 &&
data.image === undefined
) {
data.image = new Image();
data.image.src = data.url;
if (this.disableImageSelection) {
data.image.style.userSelect = 'none';
data.image.style.webkitUserSelect = 'none';
}
}

if (data.error) {
this.emit('error', envelope);
return;
// console.error('Error when fetching ' + envelope.topic);
}

// All fetched request are complete
const minValue = min(this.dataCount);
const maxValue = max(this.dataCount);
const dataSize = size(this.dataCount);

if (minValue === maxValue && (dataSize === 1 ? minValue === 0 : true)) {
// Handling requests after any re-queue
setTimeout(() => {
while (this.requests.length) {
processRequest(this.requests.pop());
}
}, 0);
}
};

// Flatten args
Object.keys(jsonData.arguments).forEach((key) => {
const arg = jsonData.arguments[key];
let delta = deltaNone;
if (arg.loop === 'reverse') {
delta = deltaReverse;
}
if (arg.loop === 'modulo') {
delta = deltaModulo;
}
this.args[key] = {
label: arg.label ? arg.label : key,
idx: arg.default ? arg.default : 0,
direction: 1,
anime: false,
values: arg.values,
ui: arg.ui ? arg.ui : 'list',
delta,
};
});

// Register all data urls
jsonData.data.forEach((dataEntry) => {
const dataId = this.id + dataEntry.name;

// Register data metadata if any
this.dataMetadata[dataEntry.name] = dataEntry.metadata || {};

// Fill categories with dataIds
(dataEntry.categories || [DEFAULT_KEY_NAME]).forEach((category) => {
if (hasOwn(this.categories, category)) {
this.categories[category].push(dataId);
} else {
this.categories[category] = [dataId];
}
});

// Register data handler + listener
dataManager.registerURL(
dataId,
(dataEntry.absolute ? '' : basepath) + dataEntry.pattern,
dataEntry.type,
dataEntry.mimeType
);
this.registeredURLs.push(dataId);
dataManager.on(dataId, dataHandler);
this.dataCount[dataId] = 0;
});

// Data Exploration handling
this.exploreState = {
order: jsonData.arguments_order.map((f) => f).reverse(), // Clone
idxs: jsonData.arguments_order.map((i) => 0), // Reset index
sizes: jsonData.arguments_order.map((f) => this.getSize(f)).reverse(), // Get Size
onDataReady: true,
animate: false,
};

this.explorationSubscription = this.onDataChange(() => {
if (this.exploreState.animate && this.exploreState.onDataReady) {
setTimeout((_) => this.nextExploration(), 0);
}
});
}

getDataMetaData(dataName) {
return this.dataMetadata[dataName];
}

// Return the current set of arguments values
getQuery() {
const query = {};

Object.keys(this.args).forEach((key) => {
const arg = this.args[key];
query[key] = arg.values[arg.idx];
});

// Add external args to the query too
Object.keys(this.externalArgs).forEach((eKey) => {
query[eKey] = this.externalArgs[eKey];
});

return query;
}

// Fetch data for a given category or _ if none provided
fetchData(category = DEFAULT_KEY_NAME) {
let dataToFetch = [];
const query = this.getQuery();
const request = { urls: [] };

// fill the data to fetch
if (category.name) {
request.category = category.name;
category.categories.forEach((cat) => {
if (this.categories[cat]) {
dataToFetch = dataToFetch.concat(this.categories[cat]);
}
});
} else if (this.categories[category]) {
request.category = category;
dataToFetch = dataToFetch.concat(this.categories[category]);
}

// Decrease the count and record the category request + trigger fetch
if (dataToFetch.length) {
this.requests.push(request);
}

dataToFetch.forEach((dataId) => {
this.dataCount[dataId] -= 1;
request.urls.push({
key: dataId.slice(this.id.length),
url: dataManager.fetch(dataId, query),
});
});
}

lazyFetchData(category = DEFAULT_KEY_NAME) {
if (this.lazyFetchRequest || this.requests.length > 0) {
this.lazyFetchRequest = category;
} else {
this.fetchData(category);
}
}

// Got to the first value of a given attribute and return true if data has changed
first(attributeName) {
const arg = this.args[attributeName];

if (arg && arg.idx !== 0) {
arg.idx = 0;
this.emit('state.change.first', {
value: arg.values[arg.idx],
idx: arg.idx,
name: attributeName,
instance: this,
});
return true;
}

return false;
}

// Got to the last value of a given attribute and return true if data has changed
last(attributeName) {
const arg = this.args[attributeName];
const last = arg.values.length - 1;

if (arg && arg.idx !== last) {
arg.idx = last;
this.emit('state.change.last', {
value: arg.values[arg.idx],
idx: arg.idx,
name: attributeName,
instance: this,
});
return true;
}

return false;
}

// Got to the next value of a given attribute and return true if data has changed
next(attributeName) {
const arg = this.args[attributeName];
if (arg && arg.delta(arg, +1)) {
this.emit('state.change.next', {
delta: 1,
value: arg.values[arg.idx],
idx: arg.idx,
name: attributeName,
instance: this,
});
return true;
}
return false;
}

// Got to the previous value of a given attribute and return true if data has changed
previous(attributeName) {
const arg = this.args[attributeName];
if (arg && arg.delta(arg, -1)) {
this.emit('state.change.previous', {
delta: -1,
value: arg.values[arg.idx],
idx: arg.idx,
name: attributeName,
instance: this,
});
return true;
}
return false;
}

// Set a value to an argument (must be in values) and return true if data has changed
// If argument is not in the argument list. This will be added inside the external argument list.
setValue(attributeName, value) {
const arg = this.args[attributeName];
const newIdx = arg ? arg.values.indexOf(value) : 0;

if (arg && newIdx !== -1 && newIdx !== arg.idx) {
arg.idx = newIdx;
this.emit('state.change.value', {
value: arg.values[arg.idx],
idx: arg.idx,
name: attributeName,
instance: this,
});
return true;
}

if (arg === undefined && this.externalArgs[attributeName] !== value) {
this.externalArgs[attributeName] = value;
this.emit('state.change.value', {
value,
name: attributeName,
external: true,
instance: this,
});
return true;
}

return false;
}

// Set a new index to an argument (must be in values range) and return true if data has changed
setIndex(attributeName, idx) {
const arg = this.args[attributeName];

if (arg && idx > -1 && idx < arg.values.length && arg.idx !== idx) {
arg.idx = idx;
this.emit('state.change.idx', {
value: arg.values[arg.idx],
idx: arg.idx,
name: attributeName,
instance: this,
});
return true;
}

return false;
}

// Return the argument value or null if the argument was not found
// If argument is not in the argument list.
// We will also search inside the external argument list.
getValue(attributeName) {
const arg = this.args[attributeName];
return arg ? arg.values[arg.idx] : this.externalArgs[attributeName];
}

// Return the argument values list or null if the argument was not found
getValues(attributeName) {
const arg = this.args[attributeName];
return arg ? arg.values : null;
}

// Return the argument index or null if the argument was not found
getIndex(attributeName) {
const arg = this.args[attributeName];
return arg ? arg.idx : null;
}

// Return the argument index or null if the argument was not found
getUiType(attributeName) {
const arg = this.args[attributeName];
return arg ? arg.ui : null;
}

// Return the argument size or null if the argument was not found
getSize(attributeName) {
const arg = this.args[attributeName];
return arg ? arg.values.length : null;
}

// Return the argument label or null if the argument was not found
label(attributeName) {
const arg = this.args[attributeName];
return arg ? arg.label : null;
}

// Return the argument animation flag or false if the argument was not found
getAnimationFlag(attributeName) {
const arg = this.args[attributeName];
return arg ? arg.anime : false;
}

// Set the argument animation flag and return true if the value changed
setAnimationFlag(attributeName, state) {
const arg = this.args[attributeName];

if (arg && arg.anime !== state) {
arg.anime = state;
this.emit('state.change.animation', {
animation: arg.anim,
name: arg.name,
instance: this,
});
return true;
}

return false;
}

// Toggle the argument animation flag state and return the current state or
// null if not found.
toggleAnimationFlag(attributeName) {
const arg = this.args[attributeName];

if (arg) {
arg.anime = !arg.anime;
this.emit('state.change.animation', {
animation: arg.anim,
name: arg.name,
instance: this,
});
return arg.anime;
}

return null;
}

// Check if one of the argument is currently active for the animation
hasAnimationFlag() {
let flag = false;
Object.keys(this.args).forEach((key) => {
if (this.args[key].anime) {
flag = true;
}
});
return flag;
}

// Return true if an animation is currently running
isAnimating() {
return this.keepAnimating;
}

// Start/Stop an animation
animate(start, deltaT = 500) {
// Update deltaT
this.deltaT = deltaT;

if (start !== this.keepAnimating) {
this.keepAnimating = start;
this.playNext();
}
}

// Mouse handler if any base on the binding
getMouseListener() {
if (this.mouseListener) {
return this.mouseListener;
}

// Record last action time
this.lastTime = {};
this.newMouseTimeout = 250;

// We need to create a mouse listener
const self = this;
const actions = {};

// Create an action map
Object.keys(this.originalData.arguments).forEach((key) => {
const value = this.originalData.arguments[key];
if (value.bind && value.bind.mouse) {
Object.keys(value.bind.mouse).forEach((action) => {
const obj = omit(value.bind.mouse[action]);
obj.name = key;
obj.lastCoord = 0;
if (obj.orientation === undefined) {
obj.orientation = 1;
}
if (actions[action]) {
actions[action].push(obj);
} else {
actions[action] = [obj];
}
});
}
});

/* eslint-disable complexity */
function processEvent(event, envelope) {
const array = actions[event.topic];
const time = now();
const newEvent = self.lastTime[event.topic] + self.newMouseTimeout < time;
let count = array.length;
let changeDetected = false;
let eventHandled = false;

// Check all associated actions
while (count) {
count -= 1;
const item = array[count];
const deltaName = item.coordinate === 0 ? 'deltaX' : 'deltaY';

if (newEvent) {
item.lastCoord = 0;
}

/* eslint-disable no-bitwise */
if (
item.modifier & event.modifier ||
item.modifier === event.modifier
) {
eventHandled = true;
event.preventDefault(); // Consume event
const delta = event[deltaName] - item.lastCoord;
self.lastTime[event.topic] = time;

if (Math.abs(delta) > item.step) {
item.lastCoord = Number(event[deltaName]);

if (item.orientation * delta > 0) {
changeDetected = self.next(item.name) || changeDetected;
} else {
changeDetected = self.previous(item.name) || changeDetected;
}
}
}
}

if (changeDetected) {
self.lazyFetchData(); // FIXME category
}

return eventHandled;
}
/* eslint-enable complexity */

this.mouseListener = {};
Object.keys(actions).forEach((actionName) => {
this.mouseListener[actionName] = processEvent;
this.lastTime[actionName] = now();
});

return this.mouseListener;
}

// Event helpers
onStateChange(callback) {
return this.on('state.change.*', callback);
}

onDataChange(callback) {
return this.on(DEFAULT_KEY_NAME, callback);
}

// Return a new instance based on the same metadata and basepath
clone() {
return new QueryDataModel(this.originalData, this.basepath);
}

destroy() {
this.explorationSubscription.unsubscribe();
this.explorationSubscription = null;

// Remove links to dataManager
while (this.registeredURLs.length) {
dataManager.unregisterURL(this.registeredURLs.pop());
}

this.off();
}

// Data exploration -----------------------------------------------------------

exploreQuery(start = true, fromBeguining = true, onDataReady = true) {
if (fromBeguining) {
this.exploreState.idxs = this.exploreState.order.map((i) => 0);
} else {
this.exploreState.idxs = this.exploreState.order.map((field) =>
this.getIndex(field)
);
}
this.exploreState.onDataReady = onDataReady;
this.exploreState.animate = start;

// Start animation
if (this.exploreState.animate) {
this.nextExploration();
}

this.emit('state.change.exploration', {
exploration: this.exploreState,
instance: this,
});
}

nextExploration() {
if (this.exploreState.animate) {
// Update internal query
this.exploreState.order.forEach((f, i) => {
this.setIndex(f, this.exploreState.idxs[i]);
});

// Move to next step
const idxs = this.exploreState.idxs;
const sizes = this.exploreState.sizes;
let count = idxs.length;

// May overshoot
idxs[count - 1] += 1;

// Handle overshoot
while (count) {
count -= 1;
if (idxs[count] < sizes[count]) {
// We are good
/* eslint-disable no-continue */
continue;
/* eslint-enable no-continue */
} else if (count > 0) {
// We need to move the index back up
idxs[count] = 0;
idxs[count - 1] += 1;
} else {
this.exploreState.animate = false;
this.emit('state.change.exploration', {
exploration: this.exploreState,
instance: this,
});
return this.exploreState.animate; // We are done
}
}

// Trigger the fetchData
this.lazyFetchData();
}
return this.exploreState.animate;
}

/* eslint-disable class-methods-use-this */
setCacheSize(sizeBeforeGC) {
dataManager.cacheSize = sizeBeforeGC;
}

getCacheSize() {
return dataManager.cacheSize;
}

getMemoryUsage() {
return dataManager.cacheData.size;
}

useZipContent(zipContent, options) {
return dataManager.useZipContent(zipContent, options);
}

useHttpRequest() {
dataManager.useHttpRequest();
}

useHtmlContent() {
dataManager.useHtmlContent();
}

getDataManager() {
return dataManager;
}

link(queryDataModel, args = null, fetch = false) {
return queryDataModel.onStateChange((data, envelope) => {
if (data.name !== undefined && data.value !== undefined) {
if (args === null || args.indexOf(data.name) !== -1) {
if (this.setValue(data.name, data.value) && fetch) {
this.lazyFetchData();
}
}
}
});
}
}

Monologue.mixInto(QueryDataModel);