12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148 |
- 'use strict';
- /**
- * Licensed Materials - Property of IBM
- * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2013, 2020
- * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
- *
- * DataPlayerView
- */
- define(['jquery', 'underscore', '../../../util/DashboardFormatter', '../VisView', '../../../widgets/livewidget/nls/StringResources', 'text!./DataPlayerView.html', '../../../util/EventUtils'], function ($, _, Formatter, VisView, StringResources, template, EventUtils) {
- 'use strict';
- var DataPlayerView = null;
- DataPlayerView = VisView.extend({
- // constants
- templateString: template,
- playerSpeed: 1600,
- tickMinDistance: 44,
- maxLabelLayerCount: 2,
- verticalPositionScaleFactor: 2 / 3,
- barOverflow: 60,
- // references to JQuery selected elements
- $_sliderNode: null,
- $_sliderBar: null,
- $_sliderContainer: null,
- $_playPauseButton: null,
- _dataItemAPI: null,
- _playTimeout: null,
- _playIndex: -1,
- _currentSelectionIndex: -1,
- _mouseStartX: 0,
- _mouseOrgStartX: 0,
- _nodeVirtualPosition: 0,
- _snapThreshold: 15,
- _dragging: false,
- $tick: $('<div class="sliderTickForHorizontal"><div class="tick"></div></div>'),
- $label: $('<div class="sliderLabel" title=""></div>'),
- events: {
- 'clicktap .sliderLabel': 'onLabelClick',
- 'keydown .sliderLabel': 'onLabelKeypress',
- 'click .sliderTickForHorizontal': 'onTickClick',
- 'tap .sliderTickForHorizontal': 'onTickClick',
- 'touchstart .sliderNode': 'onSliderNodeMouseDown',
- 'touchend .slider': 'onSliderMouseUp',
- 'mousedown .sliderNode': 'onSliderNodeMouseDown',
- 'mouseup .slider': 'onSliderMouseUp',
- 'clicktap .playOrPause': 'onPlayOrPauseClick',
- 'mousedown .playOrPause': 'preventSelection',
- 'keydown .playOrPause': 'onPlayOrPauseKeypress'
- },
- getRenderer: function getRenderer() {
- return 'dashboard-analytics/visualizations/renderer/dataplayer/DataPlayerRenderer';
- },
- init: function init() {
- DataPlayerView.inherited('init', this, arguments);
- if (!this.visModel) {
- throw 'Invalid VisModel reference.';
- }
- var iconsFeature = this.dashboardApi.getFeature('Icons');
- var playIcon = iconsFeature.getIcon('playIcon');
- var pauseIcon = iconsFeature.getIcon('pauseIcon');
- $(this.el).find('.playOrPause').append('<svg class="play"><use xlink:href="#' + playIcon.id + '"/></svg><svg class="pause"><use xlink:href="#' + pauseIcon.id + '"/></svg>');
- this.el.style.position = 'relative';
- // main list of player entries
- this.playerEntries = [];
- },
- remove: function remove() {
- // clear timer
- this._clearPlayTimer();
- this.ownerWidget.dashboardApi.off('open:sharePanel', this._onSharePanelOpen, this);
- this.ownerWidget.dashboardApi.off('close:sharePanel', this._onSharePanelClose, this);
- // call overridden super class
- DataPlayerView.inherited('remove', this, arguments);
- },
- /**
- * RENDERING SEQUENCE:
- * For dataPlayer, whenDataReady will resolve when a column query for the column of interest
- * is completed.
- * @returns a promise which resolves when the column query for the data completes.
- */
- whenDataReady: function whenDataReady() {
- var _this = this;
- if (!this.isMappingComplete() || this.hasUnavailableMetadataColumns() || this.hasMissingFilters()) {
- return Promise.resolve({
- data: {}
- });
- }
- this._dataItemAPI = this.visualization.getSlots().getSlotList()[0].getDataItemList()[0];
- var queryExecution = this.content.getFeature('DataQueryExecution');
- return queryExecution.executeQueries().then(function (retData) {
- if (_this.filterIndicator) {
- _this.filterIndicator.update();
- }
- return {
- data: retData
- };
- });
- },
- getDescription: function getDescription() {
- // Append the F12 key instruction to the description
- var description = DataPlayerView.inherited('getDescription', this, arguments);
- return StringResources.get('WidgetLabelWithDescripion', {
- label: description,
- description: StringResources.get('f12KeyDescription')
- });
- },
- /**
- * @param {object} renderInfo - the renderInfo as passed from the render sequence.
- * @returns a promise which is resolved when the render is complete
- */
- render: function render(renderInfo) {
- if (!this.visModel || this.visModel.getRenderer() !== this.getRenderer()) {
- return Promise.resolve(this);
- }
- this.$_playPauseButton = $(this.visEl).find('.playOrPause');
- if (!this.isMappingComplete() || this.hasMissingFilters() || this.hasUnavailableMetadataColumns()) {
- this._getSliderContainer().empty();
- this.renderIconView();
- this.resizeToWidget(renderInfo);
- // render complete so fade in
- $(this.visEl).animate({
- opacity: 1
- });
- this.$_playPauseButton.hide();
- return Promise.resolve(this);
- }
- this.removeIconView();
- this.resizeToWidget(renderInfo);
- this.onResultsReady(renderInfo.data.getResult());
- this.$_playPauseButton.attr('aria-label', StringResources.get('playButtonLabel'));
- this.$_playPauseButton.show();
- this.ownerWidget.dashboardApi.on('open:sharePanel', this._onSharePanelOpen, this);
- this.ownerWidget.dashboardApi.on('close:sharePanel', this._onSharePanelClose, this);
- //NOTE: renderComplete is called from the base class visView.
- return DataPlayerView.inherited('render', this, arguments);
- },
- onExitContainer: function onExitContainer() {
- this.$_playPauseButton.attr('tabindex', '-1');
- this.$el.find('.sliderLabel.selected').attr('tabindex', '-1');
- },
- /**
- * Handle the on container entered event
- */
- onEnterContainer: function onEnterContainer() {
- this.$_playPauseButton.attr('tabindex', '0');
- this.$el.find('.sliderLabel.selected').attr('tabindex', '0');
- this.$_playPauseButton.focus();
- },
- onLabelKeypress: function onLabelKeypress(evt) {
- var $target = $(evt.currentTarget);
- var key = evt.keyCode;
- if (key === 37 || key === 38) {
- //left
- this._moveToItem($target, $target.prevAll('.sliderLabel').first());
- evt.stopPropagation();
- } else if (key === 39 || key === 40) {
- //right
- this._moveToItem($target, $target.nextAll('.sliderLabel').first());
- evt.stopPropagation();
- }
- },
- preventSelection: function preventSelection(evt) {
- evt.stopPropagation();
- },
- _moveToItem: function _moveToItem($current, $next) {
- $current.attr('tabindex', '-1');
- $next.click().attr('tabindex', '0').focus();
- },
- getFilterIndicatorSpec: function getFilterIndicatorSpec() {
- return {
- localFilters: true
- };
- },
- onResultsReady: function onResultsReady(resultData) {
- // clear up the content
- var dataItems = [];
- // data player has only one data item
- var rowSize = resultData.getResultItemList()[0].getRowCount();
- for (var i = 0; i < rowSize; i++) {
- dataItems.push(resultData.getValue(i, 0)[0]);
- }
- this._renderSlider(dataItems);
- },
- /**
- * Event handler for clicking or tapping on a slider tick.
- * @param event: Event object
- **/
- onTickClick: function onTickClick(event) {
- // get clicked item and set filter
- this._setFilterByEventTarget(event, 'sliderTickForHorizontal', false);
- // prevent click after a tap
- if (event.gesture) {
- event.gesture.preventDefault();
- }
- },
- /**
- * Event handler for clicking or tapping on a slider label.
- * @param event: Event object
- **/
- onLabelClick: function onLabelClick(event) {
- // get clicked item and either select or deselect
- this._setFilterByEventTarget(event, 'sliderLabel', true);
- },
- onPlayOrPauseKeypress: function onPlayOrPauseKeypress(event) {
- if (event.keyCode === 13 || event.keyCode === 32) {
- this.onPlayOrPauseClick(event);
- }
- },
- /**
- * Event handler for clicking or tapping on the play or pause button. It will
- * toggle the button between play and pause.
- * @param event: Event object
- **/
- onPlayOrPauseClick: function onPlayOrPauseClick(event) {
- // toggle playing mode
- if (this._isPlaying()) {
- this._pause();
- } else {
- this._play();
- }
- event.stopPropagation();
- },
- _onSharePanelOpen: function _onSharePanelOpen() {
- if (this._isPlaying()) {
- this._pause();
- this.shareState = { wasPlaying: true };
- }
- },
- _onSharePanelClose: function _onSharePanelClose() {
- if (this.shareState && this.shareState.wasPlaying) {
- this.shareState = null;
- this._play();
- }
- },
- /**
- * Event handler for the mouse or touch down on the slider node. This function
- * tracks the start position and starts the dragging logic.
- * @param event: Event object
- **/
- onSliderNodeMouseDown: function onSliderNodeMouseDown(event) {
- // stop playing (set flag for mouse up event to know playing was just terminated or not)
- this._wasJustPlaying = false;
- if (this._isPlaying()) {
- this._pause();
- this._wasJustPlaying = true;
- }
- // record mouse start position and virtual node position (need virtual position because of snap)
- this._mouseStartX = this._getPageX(event);
- this._mouseOrgStartX = this._mouseStartX;
- this._nodeVirtualPosition = this.$_sliderNode.position().left + this.$_sliderContainer.scrollLeft();
- // setup for dragging
- this.$_sliderContainer.on('mousemove touchmove', this.onSliderMouseMove.bind(this));
- this._dragging = true;
- // stop the event from being passed down to underlying elements
- event.stopPropagation();
- // prevent other browser events (like real mouse down if this is a touch event)
- event.preventDefault();
- },
- /**
- * Event handler for the mouse or touch up on the slider node and slider area.
- * This function processes the end of drag as well as determines if a deselect should happen.
- * @param event: event object
- **/
- onSliderMouseUp: function onSliderMouseUp(event) {
- // stop dragging as necessary
- if (this._dragging) {
- // turn off dragging
- this._dragging = false;
- // cancel the mouse move
- this.$_sliderContainer.off('mousemove touchmove');
- // check if has moved
- if (this._mouseOrgStartX !== this._mouseStartX) {
- // get nearest tick datavalue
- var snap = this._snapToTick(this._nodeVirtualPosition, true);
- // set filter
- this._setFilter(snap.index);
- } else {
- // clear the selection if not playing
- if (!this._wasJustPlaying) {
- this._clearSelection();
- }
- }
- // prevent other browser events (like real mouse up if this is a touch event)
- event.preventDefault();
- }
- },
- /**
- * Event handler for the mouse or touch move. This occurs on the slider area.
- * It moves the slider in the case of dragging and will snap to the nearest tick.
- * @param event: event object
- **/
- onSliderMouseMove: function onSliderMouseMove(event) {
- // calculate mouse delta X
- var pageX = this._getPageX(event);
- var deltaX = pageX - this._mouseStartX;
- // update mouse start position
- this._mouseStartX = pageX;
- // calculate new left positon
- var newLeft = this._nodeVirtualPosition + deltaX;
- // record position
- this._nodeVirtualPosition = newLeft;
- // snap to tick as necessary
- var snap = this._snapToTick(newLeft, false);
- // move the slider with the mouse but locked to track
- this.$_sliderNode.css('left', snap.left + 'px');
- // set the filter if a snap occurred
- if (snap.left !== newLeft) {
- this._selectLabel(snap.index);
- this._setFilter(snap.index);
- }
- // prevent other browser events (like real mouse down if this is a touch event)
- event.preventDefault();
- },
- /**
- * Listener function for the filter change event. This overrides the base one.
- * This function gets the current filter selection and moves the slider to the
- * corresponding location.
- **/
- onChangeFilter: function onChangeFilter() {
- //Call base class to process page context changes, requery etc.
- return DataPlayerView.inherited('onChangeFilter', this, arguments).then(function () {
- // watch for null column or dragging
- if (!this._dataItemAPI || this._dragging) {
- return;
- }
- // get the single filter selection
- var filter = this.getController().getSelectedValues(this._dataItemAPI.getColumnId());
- // get entry index bases on filter
- var dataItemIndex = -1;
- if (filter && filter.length > 0) {
- dataItemIndex = this._getEntryIndex(filter[0]);
- }
- // no need to reselect if already selected (in the case of multiple change events fired back to back)
- if (dataItemIndex !== this._currentSelectionIndex) {
- // store the new selection
- this._currentSelectionIndex = dataItemIndex;
- // position slider by value
- this._positionSlider(this._currentSelectionIndex, true, true);
- }
- }.bind(this));
- },
- /**
- * This function handles the rendering of the slider elements. It calculates
- * placement of ticks and labels while hidden and then animates a fade in to
- * show the control. It tracks data entries in a global array.
- * @param dataItems: Array of data results to populate the slider
- **/
- _renderSlider: function _renderSlider(dataItems) {
- // init and clear slider container
- this.$_sliderContainer = this._getSliderContainer();
- this.$_sliderContainer.attr('aria-label', StringResources.get('dataPlayerValueListLabel'));
- this.$_sliderContainer.empty();
- // sanity check for data
- if (!(dataItems && dataItems.length > 1)) {
- return;
- }
- // get container size
- var w = this.$_sliderContainer[0].clientWidth;
- var h = this.$_sliderContainer[0].clientHeight;
- // init the play button
- this._initPlayPauseButton(h);
- // calculate min length
- var barMinLength = this.tickMinDistance * (dataItems.length - 1);
- // bar length includes overflow front and end
- var barLength = Math.max(barMinLength, w - this.barOverflow * 2);
- // calculate the actual tick spacing
- var tickSpacing = barLength / (dataItems.length - 1);
- // position from top
- var barTop = Math.round(h * this.verticalPositionScaleFactor);
- // create the slider bar and position it
- this._createSliderBar(barTop, this.barOverflow / 2, barLength);
- // create the current position indicator
- this._createSliderNode();
- // calculate the baseline for the labels
- var labelBaseline = barTop - this.$_sliderNode[0].clientHeight / 2;
- // init vars for label positioning
- var aoLabels = [];
- // loop process results into player entries
- this.playerEntries.length = 0;
- var tickSizeCache = {};
- for (var i = 0; i < dataItems.length; i++) {
- // create new player entry
- var playerEntry = {
- index: i,
- value: dataItems[i],
- tick: this._createTick(i, barTop, tickSpacing, tickSizeCache)
- };
- // create new label
- aoLabels.push(this._createLabel(dataItems[i].label, i, playerEntry.tick.left + playerEntry.tick.width / 2, labelBaseline, i === 0 || i === dataItems.length - 1));
- // append entry object to collection
- this.playerEntries.push(playerEntry);
- }
- // reset scrollleft to the start. Some browsers seem to remember this setting and will scroll
- // to the place last left which is not necessarily correct
- this.$_sliderContainer.animate({
- scrollLeft: 0
- });
- // position labels (collision avoidance)
- this._positionLabels(aoLabels);
- // position slider silently to default on first tick or first filtered value
- var filter = this.getController().getSelectedValues(this._dataItemAPI.getColumnId());
- var dataIndex = -1;
- var showNode = false;
- if (filter && filter.length > 0) {
- dataIndex = this._getEntryIndex(filter[0]);
- dataIndex = dataIndex !== -1 ? dataIndex : 0;
- showNode = true;
- }
- this._currentSelectionIndex = dataIndex;
- this._positionSlider(dataIndex, false, showNode);
- // render complete so fade in
- $(this.visEl).animate({
- opacity: 1
- });
- },
- /**
- * This is a helper function that looks up and returns the data entry for
- * the passed in value.
- * @param dataItemValue: String - value of the data entry to look up
- * *@return object: Object - Data entry object
- **/
- _getPlayerEntry: function _getPlayerEntry(dataItemValue) {
- // init
- var entry = null;
- // search array for entry
- var searchResults = $.grep(this.playerEntries, function (entry) {
- return entry.value.value === dataItemValue;
- });
- // if found then continue
- if (searchResults.length > 0) {
- // get entry
- entry = searchResults[0];
- }
- // return
- return entry;
- },
- /**
- * This is a helper function that looks up and returns the index for a passed in value
- * @param dataItemValue: String - value of the data entry to look up
- * *@return Integer: The index of the entry or -1 by if not found.
- **/
- _getEntryIndex: function _getEntryIndex(dataItemValue) {
- // init
- var index = -1;
- var entry = this._getPlayerEntry(dataItemValue);
- // return index
- if (entry) {
- index = entry.index;
- }
- // return
- return index;
- },
- /**
- * This is a helper function to calculate the slider position based on the
- * given data index. It looks up the corresponding data entry and returns
- * the position object for the slider node.
- * @param dataItemValue: String - value of the data entry to look up
- * @return object: Object - position object for the slider node
- **/
- _calcSliderPosition: function _calcSliderPosition(dataItemIndex) {
- // init
- var oSliderPosition = {};
- // get entry
- var entry = this.playerEntries[dataItemIndex];
- if (entry) {
- oSliderPosition = this._getTickTopLeftPosition(entry);
- }
- // return
- return oSliderPosition;
- },
- _getTickTopLeftPosition: function _getTickTopLeftPosition(entry) {
- var position = {};
- // set slider top (need slight adjustment for visual alignment of circle)
- position.top = Math.round(entry.tick.top + entry.tick.height / 2 - this.$_sliderNode[0].clientHeight / 2);
- // set slider left
- position.left = Math.round(entry.tick.left + entry.tick.width / 2 - this.$_sliderNode[0].clientWidth / 2);
- return position;
- },
- /**
- * This function moves the slider to the tick for the corresponding passed in
- * data index. It can optionally be animated and made visible. This function
- * also sets the play index counter to the next tick down the line.
- * @param dataItemIndex: Integer - index of the data entry to look up
- * @param animate: Boolean - flag to indicate if the movement is
- * to be animated or not
- * @param showNode: Boolean - flag to indicate if the slider node
- * should be made visible
- **/
- _positionSlider: function _positionSlider(dataItemIndex, animate, showNode) {
- // if no dataItemIndex then no selection
- if (dataItemIndex < 0) {
- this.$_sliderNode.css('opacity', 0);
- this.$_sliderNode.css('display', 'none');
- this._selectLabel(-1);
- return;
- }
- // get scrollLeftPosition and container width
- var scrollLeftPos = this.$_sliderContainer.scrollLeft();
- var sliderViewPortWidth = this.$_sliderContainer[0].clientWidth;
- // get slider position
- var oSliderPosition = this._calcSliderPosition(dataItemIndex);
- // set top
- this.$_sliderNode.css('top', oSliderPosition.top + 'px');
- // scroll the div back if the node goes off the front or back of scroll
- if (oSliderPosition.left >= scrollLeftPos + sliderViewPortWidth - 10 || oSliderPosition.left < scrollLeftPos) {
- this.$_sliderContainer.animate({
- scrollLeft: oSliderPosition.left - this.barOverflow
- });
- }
- var styles = {
- left: oSliderPosition.left
- };
- // ensure the node is visible
- if (showNode) {
- styles.opacity = 1;
- styles.display = 'block';
- }
- // now move to new position
- if (animate) {
- this.$_sliderNode.animate(styles, 200);
- } else {
- this.$_sliderNode.css(styles);
- }
- // select the label
- if (showNode) {
- this._selectLabel(dataItemIndex);
- }
- // update the play index to the next one down the line (so that play starts there)
- this._playIndex = (dataItemIndex + 1) % this.playerEntries.length;
- },
- /**
- * This is a helper function to highlight the label that corresponds to the
- * passed in data index. This function will de-select allowToggle other labels.
- * @param dataItemIndex: Integer - value of the data entry to look up
- **/
- _selectLabel: function _selectLabel(dataItemIndex) {
- // remove all selections
- var $selected = $(this.visEl).find('.sliderLabel.selected');
- var tabindex = $selected.attr('tabindex');
- $selected.removeClass('selected');
- $selected.attr('tabindex', -1);
- $selected.attr('aria-selected', 'false');
- // select if index given
- if (dataItemIndex >= 0) {
- $(this.visEl).find('.sliderLabel[data-item-index="' + dataItemIndex + '"]').addClass('selected').attr('aria-selected', 'true').attr('tabindex', tabindex);
- }
- },
- /**
- * This is a helper function to position the labels passed in as an array.
- * The array of labels contains initial position information. Labels are
- * recursively processed and layered so that they do not collide with each other.
- * If a label does not fit, then it is moved to a new layer above the other labels.
- * @param aoLabels: Array - list of label objects inclusing positon info
- **/
- _positionLabels: function _positionLabels(aoLabels) {
- // check for empty
- if (aoLabels.length === 0) {
- return;
- }
- // create the label layers array (labels can be layered for collision avoidance)
- var aaLabelLayers = [aoLabels];
- // loop and process the layers array until all layers have been processed
- // layers will get added as collisions are detected
- for (var i = 0; i < aaLabelLayers.length; i++) {
- // process the last layer in the array
- this._processLabelLayers(aaLabelLayers);
- // now that the layer has been processed, positon the labels in it
- for (var j = 0; j < aaLabelLayers[i].length; j++) {
- var oLabel = aaLabelLayers[i][j];
- oLabel.$el.css('top', Math.max(-8, oLabel.labelBaseline - oLabel.height * (i + 1)) + 'px');
- oLabel.$el.css('left', oLabel.left + 'px');
- }
- // if the layer count exceeds the max then rotate the labels
- if (aaLabelLayers.length > this.maxLabelLayerCount) {
- // collapse all labels into a single array
- var aoRotateLabels = [];
- for (var k = 0; k < aaLabelLayers.length; k++) {
- $.merge(aoRotateLabels, aaLabelLayers[k]);
- }
- // rotate labels
- this._rotateLabels(aoRotateLabels);
- // no need to continue processing layers
- break;
- }
- }
- },
- /**
- * This is a helper function that processes a single layer of labels which is
- * taken from the first elemenrt of the array passed in. If collisions are
- * detected between elements of this array then those elements are pushed into
- * a new array list that is then appended to the passed in array for further
- * processing.
- * @param aaLabelLayers: Array - array of arrays to be processed. Only the
- * first element of the array is processed.
- **/
- _processLabelLayers: function _processLabelLayers(aaLabelLayers) {
- // init next layer if needed
- var aNewLabelLayer = [];
- var iPrevLabelEnd = -99999999;
- // process the last layer in the array
- var aoLabelLayer = aaLabelLayers[aaLabelLayers.length - 1];
- for (var i = 0; i < aoLabelLayer.length; i++) {
- // does this label's left collide with the previous end
- if (aoLabelLayer[i].left < iPrevLabelEnd) {
- // move this label to the next layer
- aNewLabelLayer = aNewLabelLayer.concat(aoLabelLayer.splice(i, 1));
- // array was altered so next element has replaced this one.
- i--;
- } else {
- // just record the ending point and move along
- iPrevLabelEnd = aoLabelLayer[i].right;
- }
- }
- // check if any labels were added to the new array and append as necessary
- if (aNewLabelLayer.length > 0) {
- aaLabelLayers.push(aNewLabelLayer);
- }
- },
- /**
- * This is a helper function that takes an array of labels and positions and rotates them.
- * @param aoRotateLabels: Array - list of labels to rotate and position
- **/
- _rotateLabels: function _rotateLabels(aoRotateLabels) {
- if (aoRotateLabels.length < 1) {
- return;
- }
- var widgetWidget = this.$_sliderContainer[0].scrollWidth || this.$_sliderContainer[0].clientWidth;
- // The top is the same for all the labels, so calculate it once
- var top = Math.max(-8, aoRotateLabels[0].labelBaseline - aoRotateLabels[0].height);
- // Since the labels are shown at a 45 degree angle, use Pythagorean theorem to calculate the maximum length
- var maxWidth = Math.max(25, Math.floor(Math.sqrt(top * top + top * top)));
- // loop and process all labels
- for (var i = 0; i < aoRotateLabels.length; i++) {
- // position the label and rotate (restore width for possoibly truncated labels)
- var $oLabel = aoRotateLabels[i].$el;
- $oLabel.css('top', top + 'px');
- $oLabel.css('left', aoRotateLabels[i].tickLeft + 'px');
- // If ever we don't have enough room left over to do a 45 degree triangle (top), then recalculate the max width. This hanppens
- // at the end of the data player, the last few labels would extend past the right edge and cause scrollbars
- if (widgetWidget - aoRotateLabels[i].tickLeft < top) {
- var roomLeft = widgetWidget - aoRotateLabels[i].tickLeft;
- $oLabel.css('width', Math.floor(Math.sqrt(roomLeft * roomLeft + roomLeft * roomLeft)) + 'px');
- } else {
- $oLabel.css('width', maxWidth + 'px');
- }
- $oLabel.addClass('rotated');
- }
- },
- /**
- * This function takes the given index and sets a filter accordingly.
- * It will clear all existing filters before setting the new one.
- * @param index: Integer - index of value to look up
- **/
- _setFilter: function _setFilter(index) {
- // Clear any tooltips or other toolbars when we change the data to avoid stale info in our view
- this.ownerWidget.dashboardApi.triggerDashboardEvent('widget:hideToolbar');
- var value = this.playerEntries[index];
- this.getController().select({
- itemIds: [this._dataItemAPI.getColumnId()],
- tuple: [EventUtils.toDeprecatedPayload(value.value)],
- command: 'update',
- slotsToClear: this.visualization.getSlots().getMappedSlotList()
- });
- },
- /**
- * This is a helper function that sets a filter by event and target.
- * @param event: Event - event object
- * @param targetName: String - target to filter on
- * @param allowToggle: Boolean - flag to indicate if the filter should
- * toggle if already set
- **/
- _setFilterByEventTarget: function _setFilterByEventTarget(event, targetName, allowToggle) {
- // get clicked item and either select or deselect
- var el = this.getTarget(event.target, targetName);
- var index = el.getAttribute('data-item-index');
- if (index) {
- // make sure the index is an integer
- index = parseInt(index, 10);
- // select if not selected
- if (index !== this._currentSelectionIndex) {
- // immediately show label as selected
- this._selectLabel(index);
- // set filer
- this._setFilter(index);
- } else {
- // deselect
- if (allowToggle && !this._isPlaying()) {
- this._clearSelection();
- }
- }
- }
- },
- /**
- * This function initiates play mode.
- **/
- _play: function _play() {
- // unpause
- this.$_playPauseButton.removeClass('paused');
- // start playing
- this._playIteration();
- },
- /**
- * This function stops playback.
- **/
- _pause: function _pause() {
- // clear timer
- this._clearPlayTimer();
- // pause
- this.$_playPauseButton.addClass('paused');
- },
- /**
- * Helper function to determine if control is currently playing.
- * @return Boolean - True if playing, false if not.
- **/
- _isPlaying: function _isPlaying() {
- return !this.$_playPauseButton.hasClass('paused');
- },
- /**
- * This function conducts the playing operation and gets executed on a timer.
- **/
- _playIteration: function _playIteration() {
- // clear timer
- this._clearPlayTimer();
- // sanity check for data
- if (!(this._dataItemAPI && this.playerEntries && this.playerEntries.length)) {
- return;
- }
- // when slider position is not set, start playing with index 0 instead of -1;
- if (this._playIndex === -1) {
- this._playIndex = 0;
- }
- // set global filter
- this._setFilter(this._playIndex, true);
- // trigger next iteration
- this._playTimeout = window.setTimeout(this._playIteration.bind(this), this.playerSpeed);
- },
- /**
- * Helper function to access the slider container.
- **/
- _getSliderContainer: function _getSliderContainer() {
- return $(this.visEl).find('.slider');
- },
- /**
- * Helper function to reset the timer.
- **/
- _clearPlayTimer: function _clearPlayTimer() {
- if (this._playTimeout) {
- window.clearTimeout(this._playTimeout);
- this._playTimout = null;
- }
- },
- /**
- * This function clears all filters and selections.
- **/
- _clearSelection: function _clearSelection() {
- if (this._currentSelectionIndex !== -1) {
- this.getController().select({
- itemIds: this._dataItemAPI.getColumnId(),
- tuple: EventUtils.toDeprecatedPayload(this.playerEntries[this._currentSelectionIndex].value),
- command: 'remove',
- slotsToClear: this.visualization.getSlots().getMappedSlotList()
- });
- }
- // clear the slider
- this._positionSlider(-1);
- // clear current selection
- this._selectLabel(-1);
- this._currentSelectionIndex = -1;
- },
- /**
- * This function takes in the left position of the slider and determines if
- * it is in range to snap to a tick. It will return a new left position.
- * @param leftPosition: Integer - the left position of the slider node
- * @param splitThreshold: Boolean - flag to indicate if the threshold is
- * pre-defined or split between ticks.
- * @return Object: Integer position and tick index value
- **/
- _snapToTick: function _snapToTick(leftPosition, splitThreshold) {
- // init
- var centeredLeftPos = leftPosition + this.$_sliderNode[0].clientWidth / 2;
- var newLeft = leftPosition;
- var index = -1;
- // clear current selection so that it will snap correctly
- this._currentSelectionIndex = -1;
- // threshold is either pre-defined or is split halfway between ticks
- var threshold = this._snapThreshold;
- if (splitThreshold) {
- threshold = null;
- }
- // loop through collect and determine either snap to tick, limit to start, or limit to end
- for (var i = 0; i < this.playerEntries.length; i++) {
- // set threshold as necessary
- if (!threshold) {
- threshold = this.playerEntries[i].tick.spacing / 2;
- }
- // get tick center
- var tickCenterLeft = this.playerEntries[i].tick.left + this.playerEntries[i].tick.width / 2;
- // get snap left and snap right
- var snapLeft = centeredLeftPos <= tickCenterLeft + threshold;
- var snapRight = centeredLeftPos >= tickCenterLeft - threshold;
- // check if first or last entry and if snap is needed
- var isFirstAndSnap = i === 0 && snapLeft;
- var isLastAndSnap = i === this.playerEntries.length - 1 && snapRight;
- // check limits and snap to tick
- if (isFirstAndSnap || isLastAndSnap || snapLeft && snapRight) {
- // calculate new left & record value
- newLeft = this._calcSliderPosition(i).left;
- index = i;
- break;
- }
- }
- // return
- return {
- left: Math.round(newLeft),
- index: index
- };
- },
- /**
- * Helper function to create the slider bar and append it to the DOM.
- * @param barTop: Integer - top position
- * @param barLeft: Integer - left position
- * @param barLength: Integer - length
- **/
- _createSliderBar: function _createSliderBar(barTop, barLeft, barLength) {
- this.$_sliderBar = $('<div class="sliderBarHorizontal"></div>');
- this.$_sliderContainer.append(this.$_sliderBar);
- this.$_sliderBar.css('top', barTop + 'px');
- this.$_sliderBar.css('left', barLeft + 'px');
- this.$_sliderBar.css('width', barLength + 'px');
- },
- /**
- * Helper function to create the slider node and append it to the DOM. It is
- * drawn using SVG instead of the icon font because of precise pixel level
- * positioning differences across browsers.
- **/
- _createSliderNode: function _createSliderNode() {
- this.$_sliderNode = $('<div class="sliderNode"><svg class="sliderSymbol" height="18" width="18"><circle cx="10" cy="10" r="8" fill-opacity="1"/></svg></div>');
- this.$_sliderContainer.append(this.$_sliderNode);
- },
- /**
- * Helper function to position the play/pause button.
- * @param containerHeight: Integer - container height
- **/
- _initPlayPauseButton: function _initPlayPauseButton(containerHeight) {
- // get button
- this.$_playPauseButton = $(this.visEl).find('.playOrPause');
- // position
- this.$_playPauseButton.css('top', Math.round(containerHeight * this.verticalPositionScaleFactor - 28) + 'px');
- },
- /**
- * This function creates an individual tick and appends it to the DOM. It
- * returns a tick object to be used in the global entries list.
- * @param index: Integer - position index (zero based)
- * @param barTop: Integer - top position of bar
- * @param tickSpacing: Integer - distance between ticks
- * @param sizeCache Object - simple cache for width and hight of ticks
- * based on constant width and constant height
- * @return object: Tick object
- **/
- _createTick: function _createTick(index, barTop, tickSpacing, sizeCache) {
- // create new tick
- var $tick = this.$tick.clone();
- this.$_sliderNode.before($tick);
- var tickClientWidth;
- if (sizeCache.w) {
- tickClientWidth = sizeCache.w;
- } else {
- tickClientWidth = $tick[0].clientWidth;
- sizeCache.w = tickClientWidth;
- }
- var tickClientHeight;
- if (sizeCache.h) {
- tickClientHeight = sizeCache.h;
- } else {
- tickClientHeight = $tick[0].clientHeight;
- sizeCache.h = tickClientHeight;
- }
- // set further attributes
- $tick.attr('data-item-index', index);
- $tick.css('height', tickClientHeight + 'px');
- var tickTop = barTop - Math.round(tickClientHeight / 2);
- var tickLeft = Math.round(this.barOverflow / 2 + tickSpacing * index - tickClientWidth / 2);
- $tick.css('top', tickTop + 'px');
- $tick.css('left', tickLeft + 'px');
- // return tick object
- return {
- top: tickTop,
- left: tickLeft,
- width: tickClientWidth,
- height: tickClientHeight,
- spacing: tickSpacing
- };
- },
- /**
- * This function creates a label and appends it to the DOM. It returns a
- * tick object to be used in positioning.
- * @param value: String - data value
- * @param index: Integer - position index (zero based)
- * @param tickLeft: Integer - left position
- * @param labelBaseline: Integer - vertical bottom of labels
- * @param isFirstOrLastLabel: Boolean - flag to indicate if first of last
- * label to be processed.
- * @return object: Label object
- **/
- _createLabel: function _createLabel(value, index, tickLeft, labelBaseline, isFirstOrLastLabel) {
- // format the text for the label
- var label = Formatter.format(value, this._dataItemAPI.getFormat());
- // create the label object
- var $label = this.$label.clone();
- $label.attr('title', label);
- $label.attr('role', 'option');
- $label.attr('tabindex', '-1');
- $label.text(label);
- this.$_sliderNode.before($label);
- $label.attr('data-item-index', index);
- // calculate label width and truncate the first and last labels as necessary
- var labelWidth = $label[0].clientWidth;
- var actualWidth = labelWidth;
- if (isFirstOrLastLabel) {
- var truncLength = this.barOverflow * 2 - 10; // the 10 is a fudge factor
- if (labelWidth > truncLength) {
- labelWidth = Math.round(truncLength);
- $label.css('width', labelWidth + 'px');
- }
- }
- // calculate the rest of the label info and store in array to be used for positioning
- var labelHeight = $label[0].clientHeight;
- var labelLeft = Math.round(tickLeft - labelWidth / 2);
- var labelRight = labelLeft + labelWidth;
- if (labelLeft < 0) {
- //if the left is off the screen, set it to 0, but add the difference to the right.
- labelRight += 0 - labelLeft;
- labelLeft = 0;
- }
- return {
- $el: $label,
- left: labelLeft,
- right: labelRight,
- width: labelWidth,
- actualWidth: actualWidth,
- height: labelHeight,
- labelBaseline: labelBaseline,
- tickLeft: tickLeft
- };
- },
- /**
- * Helper function to get the pageX position between a touch event and regular mouse event.
- * @param event: event object
- **/
- _getPageX: function _getPageX(event) {
- return event.type.substr(0, 5) !== 'touch' ? event.pageX : event.originalEvent.touches[0].pageX;
- }
- });
- return DataPlayerView;
- });
- //# sourceMappingURL=DataPlayerView.js.map
|