From a762df1f3ff1881d149af7be01bb5638c7b9316b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duarte=20Cunha=20Lea=CC=83o?= Date: Thu, 4 Jun 2015 14:13:29 +0100 Subject: [PATCH] [CDF-564] - CCC - Active Scene Event --- build-res/r.js-configs/pvc.build.js | 1 + ccc-bundle-files.txt | 1 + .../ccc/core/base/chart/chart.activeScene.js | 170 ++++++++++++++++++ .../ccc/core/base/chart/chart.visualRoles.js | 41 +++++ package-res/ccc/core/base/scene/scene.js | 106 ++++++++++- package-res/ccc/core/base/sign/sign.js | 24 ++- package-res/lib/protovis.js | 35 ++-- 7 files changed, 350 insertions(+), 28 deletions(-) create mode 100644 package-res/ccc/core/base/chart/chart.activeScene.js diff --git a/build-res/r.js-configs/pvc.build.js b/build-res/r.js-configs/pvc.build.js index 6ee61d42a..90eac236d 100644 --- a/build-res/r.js-configs/pvc.build.js +++ b/build-res/r.js-configs/pvc.build.js @@ -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', diff --git a/ccc-bundle-files.txt b/ccc-bundle-files.txt index bf801daa2..6eeb0efae 100644 --- a/ccc-bundle-files.txt +++ b/ccc-bundle-files.txt @@ -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 diff --git a/package-res/ccc/core/base/chart/chart.activeScene.js b/package-res/ccc/core/base/chart/chart.activeScene.js new file mode 100644 index 000000000..469f4d489 --- /dev/null +++ b/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} true if the active scene changed, false 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 actually 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; + } +} diff --git a/package-res/ccc/core/base/chart/chart.visualRoles.js b/package-res/ccc/core/base/chart/chart.visualRoles.js index 1622f410c..d652e2e46 100644 --- a/package-res/ccc/core/base/chart/chart.visualRoles.js +++ b/package-res/ccc/core/base/chart/chart.visualRoles.js @@ -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 role or dims. + * + * @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(); +} diff --git a/package-res/ccc/core/base/scene/scene.js b/package-res/ccc/core/base/scene/scene.js index 00c03bffb..ad4a903bd 100644 --- a/package-res/ccc/core/base/scene/scene.js +++ b/package-res/ccc/core/base/scene/scene.js @@ -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 of this scene tree, 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; }, @@ -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. diff --git a/package-res/ccc/core/base/sign/sign.js b/package-res/ccc/core/base/sign/sign.js index 432b8f5e6..a51119340 100644 --- a/package-res/ccc/core/base/sign/sign.js +++ b/package-res/ccc/core/base/sign/sign.js @@ -392,8 +392,8 @@ def('pvc.visual.Sign', pvc.visual.BasicSign.extend([{ this._getTooltipFormatter(tipOptions); if(!tooltipFormatter) return; - var tipsyEvent = def.get(ka, 'tipsyEvent') || - (pointingOptions.mode === 'near' ? 'point' : 'mouseover'); + var isNear = pointingOptions.mode === 'near', + tipsyEvent = def.get(ka, 'tipsyEvent') || (isNear ? 'point' : 'mouseover'); this.pvMark .localProperty('tooltip'/*, Function | String*/) @@ -458,17 +458,25 @@ def('pvc.visual.Sign', pvc.visual.BasicSign.extend([{ this.pvMark .ensureEvents() .event(onEvent, function(scene) { - if(scene.hoverable() && !panel.selectingByRubberband() && !panel.animating()) { + if(scene.hoverable() && !panel.selectingByRubberband() && !panel.animating()) scene.setActive(true); - panel.renderInteractive(); - } }) .event(offEvent, function(scene) { - if(scene.hoverable() && !panel.selectingByRubberband() && !panel.animating()) { - // Clears THE active scene, if ANY (not necessarily = scene) - if(scene.clearActive()) panel.renderInteractive(); + // When it is a "point" switch, + // let the scene becoming active to notify the chart. + // Otherwise, we'll be triggering a "null to" event + // immediately followed by a "non-null to" event. + if(scene.hoverable() && + !panel.selectingByRubberband() && + !panel.animating() && + (!pv.event || !pv.event.isPointSwitch)) { + // Clears THE active scene of the scene tree, if ANY (not necessarily = scene) + scene.clearActive(); } }); + + // See pvc.visual.Scene#setActive + this.pvMark._hasHoverable = true; }, /* CLICK & DOUBLE-CLICK */ diff --git a/package-res/lib/protovis.js b/package-res/lib/protovis.js index 92b437848..0fc4d0987 100644 --- a/package-res/lib/protovis.js +++ b/package-res/lib/protovis.js @@ -22413,7 +22413,7 @@ pv.Behavior.drag = function() { * * @extends pv.Behavior * - * @param {object|number} [keyArgs] the fuzzy radius threshold in pixels, or an + * @param {object|number} [keyArgs] the fuzzy radius threshold in pixels, or an * optional keyword arguments object. * @param {number} [keyArgs.radius=30] the fuzzy radius threshold in pixels. * @param {number} [keyArgs.radiusHyst=0] the minimum distance in pixels that @@ -22453,10 +22453,10 @@ pv.Behavior.point = function(keyArgs) { return r * r; } ()); - /** @private - * Search for the mark, - * that has a point handler and - * that is "closest" to the mouse. + /** @private + * Search for the mark, + * that has a point handler and + * that is "closest" to the mouse. */ function searchSceneChildren(scene, curr) { if(scene.visible) @@ -22464,7 +22464,7 @@ pv.Behavior.point = function(keyArgs) { if(searchScenes(scene.children[i], curr)) return true; // stop } - + function searchScenes(scenes, curr) { var mark = scenes.mark, isPanel = mark.type === 'panel', @@ -22483,7 +22483,7 @@ pv.Behavior.point = function(keyArgs) { result = true; break; // stop (among siblings) } - } + } } if(isPanel) { @@ -22506,7 +22506,7 @@ pv.Behavior.point = function(keyArgs) { return result; } - + function sceneVisibility(scenes, index) { var s = scenes[index]; if(!s.visible) return 0; @@ -22524,7 +22524,7 @@ pv.Behavior.point = function(keyArgs) { o > 0.98 ? 1 : 0.5; } - + function evalScene(scenes, index, mouse, curr, visibility, markCostMax) { var shape = scenes.mark.getShape(scenes, index), @@ -22649,7 +22649,7 @@ pv.Behavior.point = function(keyArgs) { curr.scenes = scenes; curr.index = index; curr.shape = shape; - + // Be satisfied with the first insideStrict and opaque (visibility === 1) curr. // Cannot see through. // Hides anything below/after. @@ -22686,6 +22686,9 @@ pv.Behavior.point = function(keyArgs) { // Note: !isFinite(point.cost) => no point after all. if(!point.inside && !isFinite(point.cost)) point = null; + e.pointFrom = unpoint; + e.pointTo = point; + // Unpoint the old target, if it's not the new target. if(unpoint) { if(point && @@ -22733,13 +22736,13 @@ pv.Behavior.point = function(keyArgs) { } /** - * Intercepts click events and redirects them + * Intercepts click events and redirects them * to the pointed by element, if any. - * - * @returns {boolean|array} + * + * @returns {boolean|array} * false to indicate that the event is handled, * otherwise, an event handler info array: [handler, type, scenes, index, ev]. - * + * * @private */ function eventInterceptor(type, ev) { @@ -22750,7 +22753,7 @@ pv.Behavior.point = function(keyArgs) { } // Let event be handled normally } - + /** * Sets or gets the collapse parameter. By default, the standard Cartesian * distance is computed. However, with some visualizations it is desirable to @@ -22777,7 +22780,7 @@ pv.Behavior.point = function(keyArgs) { } return collapse; }; - + if(keyArgs && keyArgs.collapse != null) mousemove.collapse(keyArgs.collapse); keyArgs = null;