Skip to content

Commit

Permalink
[CDF-564] - CCC - Active Scene Event
Browse files Browse the repository at this point in the history
  • Loading branch information
dcleao committed Jun 15, 2015
1 parent aa1e3cd commit a762df1
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 28 deletions.
1 change: 1 addition & 0 deletions build-res/r.js-configs/pvc.build.js
Expand Up @@ -100,6 +100,7 @@
'ccc/core/base/chart/chart.panels',
'ccc/core/base/chart/chart.selection',
'ccc/core/base/chart/chart.extension',
'ccc/core/base/chart/chart.activeScene',
'ccc/core/base/multi/multiChart-options',
'ccc/core/base/multi/multiChart-panel',
'ccc/core/base/multi/smallChart-options',
Expand Down
1 change: 1 addition & 0 deletions ccc-bundle-files.txt
Expand Up @@ -66,6 +66,7 @@ ccc/core/base/chart/chart.axes
ccc/core/base/chart/chart.panels
ccc/core/base/chart/chart.selection
ccc/core/base/chart/chart.extension
ccc/core/base/chart/chart.activeScene

ccc/core/base/multi/multiChart-options
ccc/core/base/multi/multiChart-panel
Expand Down
170 changes: 170 additions & 0 deletions package-res/ccc/core/base/chart/chart.activeScene.js
@@ -0,0 +1,170 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

pvc.BaseChart
.add({
//_activeScene: null,

/**
* Gets the chart's active scene.
*
* @return {pvc.visual.Scene} The active scene or `null`, when none is active.
*/
activeScene: function() {
return this._activeScene || null;
},

/**
* Sets the chart's active scene.
*
* If the scene changes, triggers the chart's "active:change" event.
*
* @return {boolean} <tt>true</tt> if the active scene changed, <tt>false</tt> otherwise.
*/
_setActiveScene: function(to) {
// Send to root chart.
if(this.parent) return this.root._setActiveScene(to);

// Normalize to null
if(!to) to = null;
// Only owner scenes can become active!
else if(to.ownerScene) to = to.ownerScene;

var from = this.activeScene();
if(to === from) return false;

// A context with no mark and no scene.
var ctx = new pvc.visual.Context(this.basePanel),
ev = this._acting('active:change', function() {
return this.chart._activeSceneChange(this);
});

ev.from = from;
ev.to = to;
ctx.event = ev;
ev.trigger(ctx, []);

return true;
},

/**
* Called to <b>actually</b> change the chart active scene.
*
* @param {pvc.visual.Context} ctx The `"active:change"` context.
* @virtual
* @protected
* @private
*/
_activeSceneChange: function(ctx) {
var from = ctx.event.from, to = ctx.event.to;

// Change
if(from) from._clearActive();
if((this._activeScene = to)) to._setActive(true);

// Render
// Unless from same panel (<=> same root scene).
if(from && (!to || to.root !== from.root))
from.panel().renderInteractive();

if(to) to.panel().renderInteractive();
},

/**
* Processes a new event handler.
* If the event is `"active:change"` and the handler has a `role` or a `dims` property,
* it is given a filter function that only actually calls the handler if the
* implied dimension tuple changed its value.
*
* @param {string} name The name of the event.
* @param {object} hi The handler info object.
* @param {boolean} before Indicates the phase of the handler.
* @private
*/
_on: function(name, hi, before) {
if(name === "active:change" && (hi.role || hi.dims)) {
// Add a filter function to the event handler
chart_activeSceneEvent_addFilter(name, hi);
}
}
});

function chart_activeSceneEvent_addFilter(name, hi) {
var inited = false,
normDimsKey, normDimNames;

hi._filter = eventFilter;
hi._handler = eventHandler;

// Applies the filter to check whether the event handler should run.
/** @this pvc.visual.Context */
function eventFilter() {
// On the first run, determines dimsKey and dimNames.
if(!inited) {
inited = true;
this.chart._processViewSpec(/*viewSpec*/hi);
normDimNames = hi.dimNames;
normDimsKey = hi.dimsKey;
}

if(!normDimNames) return false;

var activeFilters = def.lazy(this, '_activeFilters'),
value = def.getOwn(activeFilters, normDimsKey);

// Not yet determined
if(value === undefined)
activeFilters[normDimsKey] = value = evalEventFilter(this.event);

return value;
}

// Calls the real handler with an enhanced event object.
/** @this pvc.visual.Context */
function eventHandler() {
// this - possible strict violation.
/*jshint -W040*/
var ev1 = this.event,
ev2 = Object.create(ev1);

ev2.viewKey = normDimsKey;
ev2.viewFrom = function() { return getSceneView(ev1.from); };
ev2.viewTo = function() { return getSceneView(ev1.to ); };

this.event = ev2;
try {
hi.handler.call(this);
} finally {
this.event = ev1;
}
}

function evalEventFilter(ev) {
// assert from !== to

var vFrom = getSceneView(ev.from, null),
vTo = getSceneView(ev.to, null);

// Compare from and to.

// Both null. No change.
if(vFrom === vTo) return false;

// Only one is null. Changed.
if(vFrom == null || vTo == null) return true;

var i = normDimNames.length,
atomsFrom = vFrom.atoms,
atomsTo = vTo.atoms;
while(i--)
if(atomsFrom[normDimNames[i]].value !== atomsTo[normDimNames[i]].value)
return true;

return false;
}

function getSceneView(scene, dv) {
return scene ? scene._asView(normDimsKey, normDimNames) : dv;
}
}
41 changes: 41 additions & 0 deletions package-res/ccc/core/base/chart/chart.visualRoles.js
Expand Up @@ -155,6 +155,47 @@ pvc.BaseChart
(preGrouping = role.preBoundGrouping()) ? preGrouping.lastDimensionName() :
useDefault ? role.defaultDimensionGroup :
null;
},

/**
* Processes a view specification.
*
* An error is thrown if the specified view specification does
* not have at least one of the properties <i>role</i> or <i>dims</i>.
*
* @param {Object} viewSpec The view specification.
* @param {string} [viewSpec.role] The name of a visual role.
* @param {string|string[]} [viewSpec.dims] The name or names of the view's dimensions.
*/
_processViewSpec: function(viewSpec) {
// If not yet processed
if(!viewSpec.dimsKeys) {
if(viewSpec.role) {
var role = this.visualRoles[viewSpec.role],
grouping = role && role.grouping;
if(grouping) {
viewSpec.dimNames = grouping.dimensionNames().slice().sort();
viewSpec.dimsKey = viewSpec.dimNames.join(",");
}
} else if(viewSpec.dims) {
viewSpec.dimNames = viewSpec_normalizeDims(viewSpec.dims);
viewSpec.dimsKey = String(viewSpec.dimNames);
} else {
throw def.error.argumentInvalid(
"viewSpec",
"Invalid view spec. No 'role' or 'dims' property.");
}
}
}
});

// TODO: expand dim*
function viewSpec_normalizeDims(dims) {
// Assume already normalized
if(def.string.is(dims))
dims = dims.split(",");
else if(!def.array.is(dims))
throw def.error.argumentInvalid('dims', "Must be a string or an array of strings.");

return def.query(dims).distinct().sort().array();
}
106 changes: 102 additions & 4 deletions package-res/ccc/core/base/scene/scene.js
Expand Up @@ -380,14 +380,57 @@ def('pvc.visual.Scene', pvc_Scene.configure({
/* ACTIVITY */
isActive: false,

/**
* Indicates if a scene is active or not.
*
* The use of this method is preferred to
* direct access to property {@link #isActive},
* as it also takes {@link #ownerScene} into account.
*
* @return {boolean} `true` if this scene is considered active, `false`, otherwise.
*/
getIsActive: function() {
return (this.ownerScene || this).isActive;
},

/**
* Activates or deactivates this scene and its owner scene, if any.
* @protected
*/
setActive: function(isActive) {
isActive = !!isActive; // normalize
if(this.isActive !== isActive) rootScene_setActive.call(this.root, this.isActive ? null : this);

// When !isActive, do not warn the chart if
// the scene becoming pointed to, if any,
// is "hoverable" enabled.
// Otherwise, we'll be triggering a "null to" event
// immediately followed by a "non-null to" event.
if((this.getIsActive() !== isActive) &&
(isActive || !scene_isPointSwitchingToHoverableSign(pv.event))) {

this.chart()._setActiveScene(isActive ? this : null);
}
},

// This is misleading as it clears whatever the active scene is,
// not necessarily the scene on which it is called.
clearActive: function() { return rootScene_setActive.call(this.root, null); },
/**
* Clears the active scene <b>of this scene tree</b>, if any.
* The active scene may not be this scene.
*
* @return {boolean} `true` if the scene tree's active scene changed, `false`, otherwise.
* @protected
*/
clearActive: function() {
return !!this.active() && this.chart()._setActiveScene(null);
},

_setActive: function(isActive) {
if(this.isActive !== isActive)
rootScene_setActive.call(this.root, this.isActive ? null : this);
},

_clearActive: function() {
return rootScene_setActive.call(this.root, null);
},

anyActive: function() { return !!this.root._active; },

Expand Down Expand Up @@ -510,11 +553,66 @@ def('pvc.visual.Scene', pvc_Scene.configure({

toggleVisible: function() {
if(cdo.Data.toggleVisible(this.datums())) this.chart().render(true, true, false);
},

/* VIEWS */
/**
* Gets a complex view of the given view specification.
*
* @param {Object} viewSpec The view specification.
* @param {string} [viewSpec.role] The name of a visual role.
* @param {string|string[]} [viewSpec.dims] The name or names of the view's dimensions.
*
* @return {cdo.Complex} The complex view.
*/
asView: function(viewSpec) {
this.chart()._processViewSpec(viewSpec);

return this._asView(viewSpec.dimsKey, viewSpec.dimNames);
},

_asView: function(dimsKey, dimNames) {
if(this.ownerScene) return this.ownerScene._asView(dimsKey, dimNames);

var views = def.lazy(this, '_viewCache'),
view = def.getOwn(views, dimsKey);

// NOTE: `null` is a value view.
if(view === undefined)
views[dimsKey] = view = this._calcView(dimNames);

return view;
},

_calcView: function(normDimNames) {
// Collect atoms of each dimension name.
// Fail on first null one.
var atoms = null, atom, dimName;
for(var i = 0, L = normDimNames.length; i < L; i++) {
dimName = normDimNames[i];
atom = this.atoms[dimName];
if(!atom || atom.value == null) return null;

(atoms || (atoms = {}))[dimName] = atom;
}

return new cdo.Complex(
/*source*/this.data().owner,
atoms,
normDimNames,
/*atomsBase*/null, // defaulted from source.atoms
/*wantLabel*/true,
/*calculate*/false);
}
}
]
}));

function scene_isPointSwitchingToHoverableSign(ev) {
var pointTo;
return !!(ev && (pointTo = ev.pointTo) && pointTo.scenes.mark._hasHoverable);
}

/**
* Called on each sign's pvc.visual.Sign#preBuildInstance
* to ensure cached data per-render is cleared.
Expand Down

0 comments on commit a762df1

Please sign in to comment.