Chart .dimension(...).group(...).columns is not a function (d3.js v4.2.8, dc.js, React) - reactjs

After building a dc.js data table in d3 v3 looking to build the same data table in d3 v4.2.8 (did it for the line chart,please see DC.js to React conversion ) .Keep getting the error bitRateTableChart.dimension(...).group(...).columns is not a function(…). It seems that the columns are not being defined properly. The code of TableChart.js:
import React, { PropTypes } from 'react';
import ReactDOM from "react-dom";
import * as d3 from 'd3';
import dc from "dc";
import * as crossfilter from 'crossfilter';
import {Jumbotron } from 'react-bootstrap';
import functionDCHelper from './functionDCHelper';
import {scaleTime, scaleLinear} from 'd3-scale';
class TableChart extends React.Component {
componentDidMount() {
var bitRateTableChart = dc.compositeChart(this.refs.tableChart);
var dateDimension = functionDCHelper.generateValues(this.props.data);
bitRateTableChart /* dc.dataTable('.dc-data-table', 'chartGroup') */
.dimension(dateDimension) // .dimension(dateDimension)
// Data table does not use crossfilter group but rather a closure as a grouping function
.group(function (d) {
var format = d3.format('02d');
return d.bitdate.getFullYear() + '/' + (d.bitdate.getMonth() ); //+ 1
})
// (_optional_) max number of records to be shown, `default = 25`
.columns([
'CHANNEL_ID',
'BITRATE'
])
.ordering(function (d ) { return d.bitdate; })
// (_optional_) custom renderlet to post-process chart using [D3](http://d3js.org)
.on('renderlet', function (table) {
// d3.select(.tableChart).selectAll('.info');
})
// .size(15)
bitRateTableChart.render();
}
render() {
return( <div ref="tableChart">
</div>)
}
}
export default TableChart;
The code of functionDcHelper.js:
import crossfilter from 'crossfilter';
import * as d3 from 'd3';
import dc from 'dc';
var minDate,min15,bitrateWeekMinIntervalGroupMove,maxDate,minIntervalWeekBitrateGroup,dateDimension,dateFormat,numberFormat,maxbit;
function nonzero_min(chart) {
dc.override(chart, 'yAxisMin', function () {
var min = d3.min(chart.data(), function (layer) {
return d3.min(layer.values, function (p) {
return p.y + p.y0;
});
});
return dc.utils.subtract(min, chart.yAxisPadding());
});
return chart;
}
// 15 Min Interval - copied from https://github.com/mbostock/d3/blob/master/src/time/interval.js
var d3_date = Date;
function d3_time_interval(local, step, number) {
function round(date) {
var d0 = local(date), d1 = offset(d0, 1);
return date - d0 < d1 - date ? d0 : d1;
}
function ceil(date) {
step(date = local(new d3_date(date - 1)), 1);
return date;
}
function offset(date, k) {
step(date = new d3_date(+date), k);
return date;
}
function range(t0, t1, dt) {
var time = ceil(t0), times = [];
if (dt > 1) {
while (time < t1) {
if (!(number(time) % dt)) times.push(new Date(+time));
step(time, 1);
}
} else {
while (time < t1) times.push(new Date(+time)), step(time, 1);
}
return times;
}
function range_utc(t0, t1, dt) {
try {
d3_date = d3_date_utc;
var utc = new d3_date_utc();
utc._ = t0;
return range(utc, t1, dt);
} finally {
d3_date = Date;
}
}
local.floor = local;
local.round = round;
local.ceil = ceil;
local.offset = offset;
local.range = range;
var utc = local.utc = d3_time_interval_utc(local);
utc.floor = utc;
utc.round = d3_time_interval_utc(round);
utc.ceil = d3_time_interval_utc(ceil);
utc.offset = d3_time_interval_utc(offset);
utc.range = range_utc;
return local;
}
function d3_time_interval_utc(method) {
return function (date, k) {
try {
d3_date = d3_date_utc;
var utc = new d3_date_utc();
utc._ = date;
return method(utc, k)._;
} finally {
d3_date = Date;
}
};
}
// generalization of d3.time.minute copied from- https://github.com/mbostock/d3/blob/master/src/time/minute.js
function n_minutes_interval(nmins) {
var denom = 6e4 * nmins;
return d3_time_interval(function (date) {
return new d3_date(Math.floor(date / denom) * denom);
}, function (date, offset) {
date.setTime(date.getTime() + Math.floor(offset) * denom); // DST breaks setMinutes
}, function (date) {
return date.getMinutes();
});
}
min15 = n_minutes_interval(15);
dateFormat = d3.timeFormat('%Y/%m/%d/%H:%M');
//dateFormat = d3.timeParse ('%Y/%m/%d/%H:%M');
// parse the date / time
//var dateFormat = d3.timeParse("%d-%b-%y");
numberFormat = d3.format('d');
//### Crossfilter Dimensions
function generateValues(data) {
data.forEach(function (d) {
d.bitdate = new Date(d.DATETIME); //d.DATETIME = dateFormat.parse(d.DATETIME);
// d.month = d3.time.month(d.bitdate);
// d.week = d3.time.week(d.bitdate);
d.BITRATE = String(d.BITRATE).match(/\d+/); //d.BITRATE = +d.BITRATE;
});
var crossFilteredData = crossfilter(data);
var all = crossFilteredData.groupAll();
// Dimension by full date
dateDimension = crossFilteredData.dimension(function (d) {
return d.bitdate;
});
maxbit = d3.max(data, function (d) { return +d["BITRATE"]; }); //alert(maxbit);
//Group bitrate per week, 15 minInterval - maintain running tallies
bitrateWeekMinIntervalGroupMove = dateDimension.group(min15).reduce(
/* callback for when data is added to the current filter results */
function (p, v) {
++p.count;
p.BITRATE = +v.BITRATE;
p.total += +v.BITRATE;
p.avg = p.count ? Math.round(p.total / p.count) : 0;
return p;
},
/* callback for when data is removed from the current filter results */
function (p, v) {
--p.count;
p.BITRATE = +v.BITRATE;
p.total -= +v.BITRATE;
p.avg = p.count ? Math.round(p.total / p.count) : 0;
return p;
},
/* initialize p */
function () {
return {
count: 0,
bitrate: 0,
total: 0,
avg: 0
};
}
);
try {
minDate = dateDimension.bottom(1)[0].DATETIME;
} catch(err) {
minDate = new Date("2016-06-14 0:00");
}
try {
maxDate = dateDimension.top(1)[0].DATETIME;
} catch(err) {
maxDate = new Date("2016-06-18 23:55");
}
return {
min15, minDate, maxDate, bitrateWeekMinIntervalGroupMove,minIntervalWeekBitrateGroup, dateDimension, maxbit
};
}
export default {
generateValues,
nonzero_min,
dateFormat,
numberFormat
};
Any help would be appreciated.

Following Gordon's answer here is the current code for the dataTable (dc.js,crossfilter, d3 v4.2.8 ,React,):
import React, { PropTypes } from 'react';
import ReactDOM from "react-dom";
import * as d3 from 'd3';
import dc from "dc";
import * as crossfilter from 'crossfilter';
import {Jumbotron } from 'react-bootstrap';
import functionDCHelper from './functionDCHelper';
import {scaleTime, scaleLinear} from 'd3-scale';
class TableChart extends React.Component {
componentDidMount() {
var bitRateTableChart = dc.dataTable(this.refs.tableChart);
var { dateDimension } = functionDCHelper.generateValues(this.props.data);
bitRateTableChart
.dimension(dateDimension)
// Data table does not use crossfilter group but rather a closure as a grouping function
.group(function (d) {
var format = d3.format('02d');
return d.bitdate.getFullYear() + '/' + format((d.bitdate.getMonth() + 1));
})
.columns(
[
'DATETIME',
'CHANNEL_ID',
'BITRATE'
])
.ordering(function (d ) { return d.bitdate; }) /// sortBy(function (d) { return d.bitdate; })
// (_optional_) custom renderlet to post-process chart using [D3](http://d3js.org)
.on('renderlet', function (table) {
table.selectAll('.dc-table-group').classed('info', true);
})
.size(15)
bitRateTableChart.render();// dc.renderAll();
}
render() {
return(
<div className="row">
<div className="table table-hover dc-data-table dc-table-row" ref="tableChart">
</div>
</div>
)
}
}
export default TableChart;

Related

How can I link data coming from an API to a column in a data table in Angularjs

There is a data table which is getting data from an API.
This API is responsible for populating every column on the data table. I added a new column named "Project Involved" which should get filled by the data "ProjectNames" coming from API.
The problem that am facing is- I am not able to link this data to the column on data table.
There are other columns that are getting populated.
This is the column name:
{
data: 'ProjectsInvolved',
title: $translate.instant('Projects involved'),
width: 80
}
This is where we link the API to the data table:
"use strict";
angular
.module('project')
.controller(
'ProjectPermissionsGridController',
function (
$scope,
roleConstants,
$timeout,
FormHelperService,
$translate,
$mdDialog,
logger,
projectConstants,
DateHelperService,
ResourceService,
ProjectService,
$stateParams,
$filter
) {
var pm = this;
pm.resources = [];
pm.copyOfAllResources = [];
pm.pmFormBtn = $translate.instant('btn.save');
pm.projectMemberList = [];
pm.IsProjectManager = false;
pm.maxAllowedHoursLimit = projectConstants.PROJECT_COST_PER_HOUR;
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
// ProjectNames: pm.projectMemberList.ProjectNames
ProjectNames: ''
};
pm.showNoErrorsPerm = true;
pm.maxDate = DateHelperService.setTimeFormattedObject(new Date());
var currentIndex = '';
pm.isProjectMemberEdit = false;
var oldProjectMemberData = {};
pm.udpateResourcePersmissions = function () {
if (pm.projectMembersData.IsProjectManager) {
pm.projectMembersData.Read = roleConstants.ALL;
pm.projectMembersData.Write = roleConstants.ALL;
}
};
pm.initDataTableConfig = function (column, config, api) {
$scope.dtColumns = column;
$scope.dtConfig = config;
$scope.dtApiProjectMembers = api;
};
// Catching the event and binding the data
$scope.$on('evt.project-member-details', function (e, projectMembers) {
pm.projectMemberList = projectMembers;
pm.bindDataToGrid(false, projectMembers);
});
$scope.$on('evt.resources-list', function (e, resources) {
pm.resources = resources;
pm.copyOfAllResources = resources;
});
// On change to remove the cost per hour required validation.
pm.removeCostperHourValidation = function () {
if (!pm.projectMembersData.CostPerHour) {
pm.projectMembersData.CostPerHour = null;
}
if (
parseFloat(pm.projectMembersData.CostPerHour) >
pm.maxAllowedHoursLimit
) {
pm.projectMembersData.CostPerHour = pm.maxAllowedHoursLimit;
}
};
/**
* Add / update new data to project members grid.
*/
pm.addNewProjectMember = function (valid, $event) {
if (!valid) {
$scope.showNoErrorsPerm = false;
return false;
}
pm.projectMembersData.ProjectMember =
FormHelperService.getSelectedObject(
pm.resources,
'EmpID',
pm.projectMembersData.EmpID,
false
).EmpFirstName +
' ' +
FormHelperService.getSelectedObject(
pm.resources,
'EmpID',
pm.projectMembersData.EmpID,
false
).EmpLastName;
pm.projectMembersData.ReadText = projectConstants.PERMISSION_TYPE_OBJ[
pm.projectMembersData.Read
]
? projectConstants.PERMISSION_TYPE_OBJ[pm.projectMembersData.Read]
: '--';
pm.projectMembersData.WriteText = projectConstants.PERMISSION_TYPE_OBJ[
pm.projectMembersData.Write
]
? projectConstants.PERMISSION_TYPE_OBJ[pm.projectMembersData.Write]
: '--';
// pm.projectMembersData.ProjectsInvolved = 'OSMOSYS';
if ($scope.isProjectEdit) {
membersEditProjectForm();
} else {
memberNewProjectForm($event);
}
};
/**
* Function to be executed in the new project page.
* #param {*} event
*/
function memberNewProjectForm($event) {
// Setting this value at the time of clicking on edit button.
if (pm.isProjectMemberEdit) {
oldProjectMemberData = {};
angular.extend(
pm.projectMemberList[currentIndex],
pm.projectMembersData
);
pm.pmFormBtn = $translate.instant('btn.add');
} else {
pm.projectMembersData.CreatedOn =
DateHelperService.setTimeFormatString(new Date());
pm.projectMemberList.splice(0, 0, pm.projectMembersData);
// remove that element from employees dropdown to not to select it again
enableOrDisableEmployee(pm.projectMembersData.EmpID, true);
}
angular.element($event.target).data('isInvalid', true);
$scope.showNoErrorsPerm = true;
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
ProjectNames: ''
};
pm.bindDataToGrid(!pm.isProjectMemberEdit, pm.projectMemberList);
$scope.updateProjectMembers({ projectMembers: pm.projectMemberList });
pm.isProjectMemberEdit = false;
}
/**
* Function to add a new project member
* #param {*} data
* #param {*} projectMemberDetails
*/
function addProjectMember(data, projectMemberDetails) {
ProjectService.addProjectMember(data).then(function (response) {
if (response.isValid()) {
pm.projectMemberList.splice(0, 0, pm.projectMembersData);
pm.bindDataToGrid(false, pm.projectMemberList);
enableOrDisableEmployee(pm.projectMembersData.EmpID, true);
resetPMForm();
$scope.$emit('evt.update-project-managers', projectMemberDetails);
logger.success('Resource has been added to the project');
} else {
var errorMsg = response.getMessage(
$translate.instant('prj.resource'),
$translate.instant('event.adding')
);
logger.error(errorMsg);
}
});
}
/**
* Function to update a project member's details
* #param {*} data
* #param {*} projectMemberDetails
*/
function updateProjectMember(data, projectMemberDetails) {
ProjectService.updateProjectMember(data).then(function (response) {
if (response.isValid()) {
angular.extend(
pm.projectMemberList[currentIndex],
pm.projectMembersData
);
pm.pmFormBtn = $translate.instant('btn.add');
pm.bindDataToGrid(pm.isProjectMemberEdit, pm.projectMemberList);
enableOrDisableEmployee(pm.projectMembersData.EmpID, true);
$scope.$emit('evt.update-project-managers', projectMemberDetails);
resetPMForm();
pm.isProjectMemberEdit = false;
logger.success("Project member's details have been updated.");
} else {
var errorMsg = response.getMessage(
$translate.instant('prj.project_members_details'),
$translate.instant('event.updating')
);
logger.error(errorMsg);
}
});
}
/**
* Function to be executed in the project details form
*/
function membersEditProjectForm() {
var projectMemberDetails = {
empID: pm.projectMembersData.EmpID,
name: pm.projectMembersData.ProjectMember,
isProjectManager: pm.projectMembersData.IsProjectManager,
};
var members = [];
// Adding the projectCode to project member's data
pm.projectMembersData.ProjectCode = $stateParams.projectID;
// Creating the UserID property as the add and update project members API methods expects the UserID and not
// the EmpID
pm.projectMembersData.UserID = pm.projectMembersData.EmpID;
// Adding the current project memeber to the members array as the API methods for the
// add and update project member expects an array
members.push(pm.projectMembersData);
if (pm.isProjectMemberEdit) {
updateProjectMember(members, projectMemberDetails);
} else {
addProjectMember(members, projectMemberDetails);
}
}
pm.eleFocus = function () {
FormHelperService.searchElementFocus();
};
// Clearing the searched item when closing the dropdown
pm.clearSearchTerm = function () {
pm.projectMemberSearchText = '';
};
/**
* return the search results for auto complete controls.
*/
pm.querySearch = function (
query,
list,
listObjProp,
listObjPropName,
splitObjProp
) {
if (!list || list === '') {
list = [];
}
if (listObjPropName) {
var queryObj = {};
queryObj[listObjPropName] = query;
if (splitObjProp) {
var listOjProp = listObjProp.split('.');
pm[listOjProp[0]][listOjProp[1]] = $filter('filter')(
list,
queryObj
);
} else {
pm[listObjProp] = $filter('filter')(list, queryObj);
}
} else {
pm[listObjProp] = $filter('filter')(list, query);
}
};
// Fetching the row and row index when clicking on edit button and showing the details in form
angular.element('table').on('click', '.edit-project-member', function () {
pm.projectMembersData = $scope.dtApiProjectMembers.getRowData(
this.parentElement
);
pm.IsProjectManager = pm.projectMembersData.IsProjectManager;
if (pm.projectMembersData.EmpCostPerHour) {
pm.projectMembersData.EmpCostPerHour =
typeof pm.projectMembersData.EmpCostPerHour === 'string'
? pm.projectMembersData.EmpCostPerHour
: pm.projectMembersData.EmpCostPerHour.toFixed(2);
}
if (pm.projectMembersData.CostPerHour) {
pm.projectMembersData.CostPerHour =
typeof pm.projectMembersData.CostPerHour === 'string'
? pm.projectMembersData.CostPerHour
: pm.projectMembersData.CostPerHour.toFixed(2);
}
// Adding the code as IsProjectManager property is mandatory and sometimes may not be defined
if (!pm.projectMembersData.IsProjectManager) {
pm.projectMembersData.IsProjectManager = false;
}
angular.copy(pm.projectMembersData, oldProjectMemberData);
pm.isProjectMemberEdit = true;
pm.pmFormBtn = $translate.instant('btn.update');
currentIndex = $scope.dtApiProjectMembers.getRowIndex(
this.parentElement
);
});
// Showing an alert and Deleting the row data
angular
.element('table')
.on('click', '.delete-project-member', function () {
var deleteRowIndex = $scope.dtApiProjectMembers.getRowIndex(
this.parentElement
);
var deleteRowData = $scope.dtApiProjectMembers.getRowData(
this.parentElement
);
showConfirmDialog(deleteRowIndex, deleteRowData);
});
function showConfirmDialog(deleteRowIndex, deleteRowData) {
var confirm = $mdDialog
.confirm({
onComplete: function afterShowAnimation() {
var $dialog = angular.element(
document.querySelector('md-dialog')
);
var $dialogContent = $dialog.find('md-dialog-content');
var $actionsSection = $dialog.find('md-dialog-actions');
var $cancelButton = $actionsSection.children()[0];
var $confirmButton = $actionsSection.children()[1];
angular.element($dialogContent).addClass('custom-confirm-class');
angular.element($confirmButton).addClass('md-raised md-warn');
angular.element($cancelButton).addClass('md-raised');
},
})
.title($translate.instant('prj.confirm_pm_delete'))
.textContent(
'Are you sure you want to delete the selected project member?' +
deleteRowData.ProjectMember
)
.ariaLabel($translate.instant('prj.confirm_pm_delete'))
.ok($translate.instant('bar.delete'))
.cancel($translate.instant('bar.cancel'));
$mdDialog.show(confirm).then(function () {
pm.isProjectMemberEdit = false;
if ($scope.isProjectEdit) {
var projectMemberData = [
{
ProjectCode: $stateParams.projectID,
UserID: deleteRowData.EmpID,
},
];
ProjectService.deleteProjectMember(projectMemberData).then(
function (response) {
if (response.isValid()) {
logger.success(
$translate.instant('prj.project_member_removed_success_msg')
);
deleteProjectMember(deleteRowIndex, deleteRowData);
if (deleteRowData.IsProjectManager) {
$scope.$emit(
'evt.project-manager-removed',
deleteRowData.EmpID.toString()
);
}
} else {
var errorMsg = response.getMessage(
$translate.instant('prj.project_member'),
$translate.instant('event.deleting')
);
logger.error(errorMsg);
}
}
);
} else {
deleteProjectMember(deleteRowIndex, deleteRowData);
$scope.updateProjectMembers({
projectMembers: pm.projectMemberList,
});
}
pm.pmFormBtn = $translate.instant('btn.save');
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
};
});
}
function deleteProjectMember(deleteRowIndex, deleteRowData) {
pm.projectMemberList.splice(deleteRowIndex, 1);
pm.bindDataToGrid(false, pm.projectMemberList);
var oldEmpId = deleteRowData.EmpID;
// Add that element to employees dropdown to be able to select it again
enableOrDisableEmployee(oldEmpId, false);
}
function enableOrDisableEmployee(empId, isEnable) {
for (var j = 0; j < pm.resources.length; j++) {
if (pm.resources[j].EmpID === empId) {
pm.resources[j].isDisabled = isEnable;
return;
}
}
}
pm.isReadPermissionLevelLess = function (level) {
return pm.projectMembersData.Read < level ? true : false;
};
/**
* To change the write permission default value based on the read permission
* Ex : If read - own, then write should not be all
*/
pm.changeWriteDefaultValue = function () {
if (pm.projectMembersData.Read < pm.projectMembersData.Write) {
pm.projectMembersData.Write = pm.projectMembersData.Read;
}
};
/**
* Get the cost per hour of an employee when the employee value is changed
*/
pm.getCostPerHour = function () {
ResourceService.getEmployeeCostPerHour(
pm.projectMembersData.EmpID
).then(
function (response) {
if (response.isValid()) {
var costPerHour = response.getData();
if (costPerHour && costPerHour[0]) {
pm.projectMembersData.EmpCostPerHour =
costPerHour[0].CostPerHour.toFixed(2);
pm.projectMembersData.CostPerHour =
pm.projectMembersData.EmpCostPerHour;
}
}
},
function (error) {
logger.error(error);
}
);
};
$scope.$on("evt.clear-pm-members-grids-data", function () {
pm.projectMemberList = [];
pm.bindDataToGrid(false, pm.projectMemberList);
});
// Binding the data to grid with updated data.
pm.bindDataToGrid = function (reOrder, projectMembers) {
pm.projectMemberList = projectMembers;
if (pm.projectMemberList && pm.projectMemberList.length) {
var currPage = $scope.dtApiProjectMembers.getPageInfo()['page'];
$scope.dtApiProjectMembers.bindData(pm.projectMemberList);
currPage = currPage >= 0 ? currPage : 0;
if (reOrder) {
$scope.dtApiProjectMembers.orderByColumn([[1, 'desc']]);
} else {
$scope.dtApiProjectMembers.setPage(currPage);
}
} else {
$scope.dtApiProjectMembers.bindData([]);
}
};
function resetPMForm() {
$scope.showNoErrorsPerm = true;
$scope.vm.pmData.$setPristine();
$scope.vm.pmData.$setUntouched(true);
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
};
}
$scope.$on('evt.reset-project-members-form', function () {
resetPMForm();
});
}
);
I tried linking data when adding new resource- I can see project getting linked. I hard coded this to see if I was working on the right container.

Display more than 100 markers in angularjs google maps with rotation

I have been using ngMap with my angularjs code for displaying markers. However, with more than 100 markers I have noticed that there is a considerable decrease in performance mainly related to ng-repeat and two way binding. I would like to add markers with custom HTML elements similar to CustomMarker but using ordinary Markers and modified from controller when required.
Challenges faced :
I have SVG images which need to be dynamically coloured based on the conditions (These SVGs are not single path ones, hence doesn't seem to work well when I used it with Symbol)
These are vehicle markers and hence should support rotation
I have solved this by creating CustomMarker with Overlay and then adding the markers that are only present in the current map bounds to the map so that map doesn't lag.
Below is the code snippet with which I achieved it.
createCustomMarkerComponent();
/**
* options : [Object] : options to be passed on
* - position : [Object] : Google maps latLng object
* - map : [Object] : Google maps instance
* - markerId : [String] : Marker id
* - innerHTML : [String] : innerHTML string for the marker
**/
function CustomMarker(options) {
var self = this;
self.options = options || {};
self.el = document.createElement('div');
self.el.style.display = 'block';
self.el.style.visibility = 'hidden';
self.visible = true;
self.display = false;
for (var key in options) {
self[key] = options[key];
}
self.setContent();
google.maps.event.addListener(self.options.map, "idle", function (event) {
//This is the current user-viewable region of the map
var bounds = self.options.map.getBounds();
checkElementVisibility(self, bounds);
});
if (this.options.onClick) {
google.maps.event.addDomListener(this.el, "click", this.options.onClick);
}
}
function checkElementVisibility(item, bounds) {
//checks if marker is within viewport and displays the marker accordingly - triggered by google.maps.event "idle" on the map Object
if (bounds.contains(item.position)) {
//If the item isn't already being displayed
if (item.display != true) {
item.display = true;
item.setMap(item.options.map);
}
} else {
item.display = false;
item.setMap(null);
}
}
var supportedTransform = (function getSupportedTransform() {
var prefixes = 'transform WebkitTransform MozTransform OTransform msTransform'.split(' ');
var div = document.createElement('div');
for (var i = 0; i < prefixes.length; i++) {
if (div && div.style[prefixes[i]] !== undefined) {
return prefixes[i];
}
}
return false;
})();
function createCustomMarkerComponent() {
if (window.google) {
CustomMarker.prototype = new google.maps.OverlayView();
CustomMarker.prototype.setContent = function () {
this.el.innerHTML = this.innerHTML;
this.el.style.position = 'absolute';
this.el.style.cursor = 'pointer';
this.el.style.top = 0;
this.el.style.left = 0;
};
CustomMarker.prototype.getPosition = function () {
return this.position;
};
CustomMarker.prototype.getDraggable = function () {
return this.draggable;
};
CustomMarker.prototype.setDraggable = function (draggable) {
this.draggable = draggable;
};
CustomMarker.prototype.setPosition = function (position) {
var self = this;
return new Promise(function () {
position && (self.position = position); /* jshint ignore:line */
if (self.getProjection() && typeof self.position.lng == 'function') {
var setPosition = function () {
if (!self.getProjection()) {
return;
}
var posPixel = self.getProjection().fromLatLngToDivPixel(self.position);
var x = Math.round(posPixel.x - (self.el.offsetWidth / 2));
var y = Math.round(posPixel.y - self.el.offsetHeight + 10); // 10px for anchor; 18px for label if not position-absolute
if (supportedTransform) {
self.el.style[supportedTransform] = "translate(" + x + "px, " + y + "px)";
} else {
self.el.style.left = x + "px";
self.el.style.top = y + "px";
}
self.el.style.visibility = "visible";
};
if (self.el.offsetWidth && self.el.offsetHeight) {
setPosition();
} else {
//delayed left/top calculation when width/height are not set instantly
setTimeout(setPosition, 300);
}
}
});
};
CustomMarker.prototype.setZIndex = function (zIndex) {
if (zIndex === undefined) return;
(this.zIndex !== zIndex) && (this.zIndex = zIndex); /* jshint ignore:line */
(this.el.style.zIndex !== this.zIndex) && (this.el.style.zIndex = this.zIndex);
};
CustomMarker.prototype.getVisible = function () {
return this.visible;
};
CustomMarker.prototype.setVisible = function (visible) {
if (this.el.style.display === 'none' && visible) {
this.el.style.display = 'block';
} else if (this.el.style.display !== 'none' && !visible) {
this.el.style.display = 'none';
}
this.visible = visible;
};
CustomMarker.prototype.addClass = function (className) {
var classNames = this.el.className.trim().split(' ');
(classNames.indexOf(className) == -1) && classNames.push(className); /* jshint ignore:line */
this.el.className = classNames.join(' ');
};
CustomMarker.prototype.removeClass = function (className) {
var classNames = this.el.className.split(' ');
var index = classNames.indexOf(className);
(index > -1) && classNames.splice(index, 1); /* jshint ignore:line */
this.el.className = classNames.join(' ');
};
CustomMarker.prototype.onAdd = function () {
this.getPanes().overlayMouseTarget.appendChild(this.el);
// this.getPanes().markerLayer.appendChild(label-div); // ??
};
CustomMarker.prototype.draw = function () {
this.setPosition();
this.setZIndex(this.zIndex);
this.setVisible(this.visible);
};
CustomMarker.prototype.onRemove = function () {
this.el.parentNode.removeChild(this.el);
// this.el = null;
};
} else {
setTimeout(createCustomMarkerComponent, 200);
}
}
The checkElementVisibility function helps in identifying whether a marker should appear or not.
In case there are better solutions please add it here.Thanks!

Angular not detecting change to first element in array

I'm trying to display a list of dates for the current week in an Angular app. I want to allow users to view previous weeks at the click of a button, so I'm using an Observable to update the array of dates, and attempting to display the updated array.
All items are updated in the view, except for the first item in the array. Plunker example here
I've tried using *ngFor and the async pipe, as well as explicitly creating elements for each item in the array (like below). Both have the same issue. I'm struggling to find a solution.
//our root app component
import {Component, NgModule, VERSION} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
#Component({
selector: 'my-app',
template: `
<button (click)="previousWeek()">Prev Week</button>
<div>{{dates[0]}}</div>
<div>{{dates[1]}}</div>
<div>{{dates[2]}}</div>
`,
})
export class App {
name:string;
dates: Date[];
public $datesSource: Observable<Date[]>;
private datesSource: Subject<Date[]>;
constructor() {
this.datesSource = new Subject<Date[]>();
this.datesSource$ = this.getDatesWithObservable();
this.datesSource$.subscribe((dates) => {
console.log(dates);
this.dates = dates;
})
this.setDates(new Date());
}
setMonday(date: Date): Date {
const day = date.getDay() || 7;
if (day !== 1) {
date.setHours(-24 * (day - 1));
}
return date;
}
setDates(date: Date): void {
const dates = [
new Date(),
new Date(),
new Date(),
new Date(),
new Date(),
new Date(),
new Date()
];
const monday = this.setMonday(date);
dates[0] = monday;
const mondayDate = monday.getTime();
dates.forEach((date, idx) => {
console.log(idx);
date.setTime(monday.getTime() + (idx * 24 * 60 * 60 * 1000));
});
this.addDates(dates);
}
addDates(dates: Date[]): void {
this.datesSource.next(dates);
}
getDatesWithObservable(): Observable<Date[]> {
return this.datesSource.asObservable();
}
previousWeek(): void {
const day = this.dates[0].getDay() || 7;
const lastWeek = this.dates[0];
const days = 7;
lastWeek.setTime(lastWeek.getTime() - (days * 24 * 60 * 60 * 1000));
this.setDates(lastWeek);
}
}
try this , i commented the line in the middle and it's working, can you check:
const monday = this.setMonday(date);
//dates[0] = monday;
const mondayDate = monday.getTime();

setState not changing UI data

I have a react component which has componentWillMount and updates the state using setState method. I am updating the state variable rideTiles with an array of objects, though I am not getting ant data for rendering. I tried shouldComponentUpdate() by returning default true, still component doesn't render with updated values. I am not able to determine what is going wrong?
Here is my code
import React from 'react';
import {TouchableOpacity, Image, View, Text, StyleSheet, Dimensions, ScrollView} from 'react-native';
import Gradient from '../components/gradient';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import PRides from '../components/PRides'
import {GetRequest, GetUrl} from '../lib/net.js';
import Realm from '../lib/schemas.js';
import sqlite from '../lib/sqlite';
import config from '../../config.js';
import {DistanceAndTime} from '../lib/geoDistCal.js'
const AvroCodec = require('avroschema');
export default class Rides extends React.Component {
static navigationOptions = {
label: 'Rides',
tabBarIcon: () => (
<Image
source={require('../res/img/icn-ftrlnk2.png')}
style={[styles.img]}
/>
),
}
constructor(props){
super(props);
this.onRideData = this.onRideData.bind(this);
EE.addEventListener(EE.EVENT_TYPES.RIDE_DETAILS, this.onRideData);
this.eventCounts = 0;
this.insertedCount = 0;
this.rideTiles = [];
}
componentWillMount(){
var self = this;
GetRequest(GetUrl(config.glHost + "/gl/users/rides/findrides/?off=0&cnt=20&ist=false"), function(res){
if(res.status == 200){
var data = JSON.parse(res._response).data;
console.log("API data", data);
for(i = 0; i < data.length; i++){
Realm.write(() => {
Realm.create('Rides',{
idx: data[i].idx,
nm: data[i].nm,
sdt: data[i].sdt,
edt: data[i].edt,
flt: data[i].flt,
fln: data[i].fln,
tlt: data[i].tlt,
tln: data[i].tln,
uid: data[i].uid,
ist: data[i].ist,
isg: data[i].isg,
ec: data[i].ec,
// kms: data[i].kms,
// avs: data[i].avs,
// tps: data[i].tps
}, true);
self.insertLocationData(data[i]);
});
}
}
});
this.rideTiles = Array.from(Realm.objects('Rides'));
}
onRideData(event){
var self = this;
if(event){
var insertQuery = `insert into events (devId,rideId,ts,lat,lng,alt,spd,brg,hepe,vepe,ang,temp,motion,ignition,mainPower,relayState) values (
${event.devid}
,${event.rideid}
,${event.ts}
,${event.lat}
,${event.lng}
,${event.alt}
,${event.spd}
,${event.brg}
,${event.hepe}
,${event.vepe}
,${event.ang}
,${event.temp}
,${(event.motion ? 1 : 0)}
,${(event.ignition ? 1 : 0)}
,${(event.mainPower ? 1 : 0)}
,${(event.relayState ? 1 : 0)})`;
sqlite.runQuery(insertQuery, function(result){
console.log(result.rows);
if(result.rowsAffected === 1){
self.insertedCount += 1;
console.log("self.insertedCount, ", self.insertedCount, "self.eventCounts, ", self.eventCounts);
if(self.insertedCount === self.eventCounts){
//Completed Ride inserts, now compute tps avs and update in Realm
console.log("computing stats");
var tps, avs, kms, pingData = [];
sqlite.runQuery("select max(spd) as tps from events where rideId="+ event.rideid, function(result){
tps = result.rows.item(0).tps;
sqlite.runQuery(`select lat,lng,ts from events where rideId=${event.rideid} order by ts asc`, function(results){
var len = results.rows.length;
for (let i = 0; i < len; i++) {
pingData.push(results.rows.item(i));
}
console.log("pingData", pingData);
var stats = DistanceAndTime(pingData);
console.log("stats", stats);
kms = stats.D/1000;
avs = (5/18)*(stats.D/stats.T);
debugger
Realm.write(() => {
Realm.create('Rides', {idx:event.rideid, tps: Number(tps), kms: Number(kms), avs: Number(avs)}, true);
});
console.log("Before", self.rideTiles);
self.setState({rideTiles : Array.from(Realm.objects('Rides'))})
});
});
sqlite.runQuery('select distinct(ts) as ts from events', function(results){
var uniqueDate = [];
var len = results.rows.length;
for (let i = 0; i < len; i++) {
console.log(uniqueDate.indexOf(new Date(results.rows.item(i).ts*1000).toDateString()));
if(uniqueDate.indexOf(new Date(results.rows.item(i).ts*1000).toDateString()) === -1){
console.log("uniqueDate in if", uniqueDate);
uniqueDate.push(new Date(results.rows.item(i).ts*1000).toDateString());
}
}
uniqueDate.forEach(function(value){
var ts = ((new Date(value)).getTime()/1000).toFixed(0);
var tps, avs, kms, ang, dt = new Date(value);
sqlite.runQuery(`select lat,lng,ts from events where ts between ${ts} and ${ts+24*60*60} order by ts asc`, function(results){
var len = results.rows.length;
var pingData = [];
for (let i = 0; i < len; i++) {
pingData.push(results.rows.item(i));
}
var stats = DistanceAndTime(pingData);
console.log("stats", stats);
kms = stats.D/1000;
avs = (5/18)*(stats.D/stats.T);
console.log(`kms ${kms} avs : ${avs}`);
sqlite.runQuery(`select max(spd) as spd, max(ang) as ang from events where ts between ${ts} and ${ts+24*60*60}`, function(result){
tps = result.rows.item(0).spd;
ang = result.rows.item(0).ang;
console.log(`tps ${tps} ang : ${ang}`);
Realm.write(() => {
Realm.create('Stats', {top:tps, avg: avs, dis: kms, ang: ang, dt: dt }, true);
});
console.log('Stats', Array.from(Realm.objects('Stats')));
});
});
});
});
}
}
});
}
}
insertLocationData(ride){
var self = this;
sqlite.runQuery("select count(*) as count from events where rideId="+ ride.idx, function(result){
var sqlCount = result.rows.item(0).count;
console.log("sqlCount : ", sqlCount, "ride ec : ", ride.ec);
if(sqlCount !== ride.ec){
console.log("in ec eventCounts : ", self.eventCounts, "ride.ec, ", ride.ec);
self.eventCounts = ride.ec;
EE.emitEvent(EE.EVENT_TYPES.WS_REQ, AvroCodec.Encode({
devid: null,
rideid: ride.idx,
count: null,
offset: null,
tsFr: ride.sdt,
tsTo: Math.ceil(new Date()/1000)
}, AvroCodec.SCHEMA.A_REQ_FILTER)
);
}
});
}
render(){
return(
<View style={{flex:1}}>
<View style={[styles.posAbsolute, styles.gradient]}>
<Gradient.MainGradient height={430}/>
</View>
<View style={{zIndex:1,flex:1}}>
<PRides rides={this.rideTiles} navigation={this.props.navigation} />
</View>
</View>
)
}
}
const styles = {
img: {
height: 24,
width: 24,
},
gradient:{
left:0,
right:0
},
header:{
paddingTop:10,
paddingLeft:20,
paddingRight:20
},
posAbsolute: {
position: 'absolute',
},
}
In your render code, rideTiles is not set as state.
<View style={{zIndex:1,flex:1}}>
<PRides rides={this.rideTiles} navigation={this.props.navigation} />
</View>
To be updated by setState function, this.rideTiles needs to be this.state.rideTiles and this should be initialized in your constructor as this:
this.state = {
rideTiles: []
};

Jump(seek) to markers in videojs on button click

I have added markers to my videojs player. I want to jump to start time of every marker every time I click a particular button (say a next button). How should I do this. I know I have to change the currentTime but I am not getting how to solve the complete problem. I have start time of each marker. Any kind of input would be helpful.
Player.js
import assign from 'object-assign'
import cx from 'classnames'
import blacklist from 'blacklist'
import React from 'react'
module.exports = React.createClass({
displayName: 'VideoJS',
componentDidMount() {
var self = this;
var player = videojs(this.refs.video, this.props.options).ready(function() {
self.player = this;
self.player.on('play', self.handlePlay);
});
if(this.props.onPlayerInit) this.props.onPlayerInit(player);
player.markers({
markerStyle: {},
markers: [
{ startTime:10, endTime:15, time: 9.5, text: "compliance"},
{ startTime:20, endTime:25, time: 16, text: "compliance"},
{ startTime:30, endTime:38, time: 23.6,text: "compliance"},
{ startTime:51, endTime:55, time: 28, text: "compliance"}
]
});
},
handlePlay: function() {
if(this.props.onPlay) {
this.props.onPlay(this.player);
}
},
render() {
var props = blacklist(this.props, 'children', 'className', 'src', 'type', 'onPlay');
props.className = cx(this.props.className, 'videojs', 'video-js vjs-default-skin', 'vjs-big-play-centered');
assign(props, {
ref: 'video',
controls: true
});
return (
<div>
<video {... props}>
<source src={this.props.src} type={this.props.type}/>
</video>
</div>
)
}
});
Marker.js
(function($, video, undefined) {
//default setting
var defaultSetting = {
markerStyle: {
'border-radius': '0%',
},
markerTip: {
display: true,
text: function(marker) {
return "Break: "+ marker.text;
},
time: function(marker) {
return marker.time;
}
},
breakOverlay:{
display: false,
displayTime: 3,
text: function(marker) {
return "Break overlay: " + marker.overlayText;
},
style: {
'width':'100%',
'height': '20%',
'background-color': 'rgba(0,0,0,0.7)',
'color': 'white',
'font-size': '17px'
}
},
onMarkerClick: function(marker) {},
onMarkerReached: function(marker) {},
markers: []
};
// create a non-colliding random number
function generateUUID() {
var d = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=='x' ? r : (r&0x3|0x8)).toString(16);
});
return uuid;
};
function registerVideoJsMarkersPlugin(options) {
/**
* register the markers plugin (dependent on jquery)
*/
var setting = $.extend(true, {}, defaultSetting, options),
markersMap = {},
markersList = [], // list of markers sorted by time
videoWrapper = $(this.el()),
currentMarkerIndex = -1,
player = this,
markerTip = null,
breakOverlay = null,
overlayIndex = -1;
function sortMarkersList() {
// sort the list by time in asc order
markersList.sort(function(a, b){
return setting.markerTip.time(a) - setting.markerTip.time(b);
});
}
function addMarkers(newMarkers) {
// create the markers
$.each(newMarkers, function(index, marker) {
//console.log(index);
// console.log(marker);
marker.key = generateUUID();
//console.log(marker.key);
videoWrapper.find('.vjs-progress-control').append(
createMarkerDiv(marker));
// store marker in an internal hash map
markersMap[marker.key] = marker;
console.log(markersMap);
markersList.push(marker);
});
sortMarkersList();
}
function getPosition(marker){
return (setting.markerTip.time(marker) / player.duration()) * 100
}
function createMarkerDiv(marker, duration) {
var markerDiv = $("<div class='vjs-marker'></div>");
console.log(marker.length);
markerDiv.css(setting.markerStyle)
.css({"margin-left" : -parseFloat(markerDiv.css("width"))/2 + 'px',
"left" : getPosition(marker) + '%', })
.attr("data-marker-key", marker.key)
.attr("data-marker-time", setting.markerTip.time(marker));
console.log(setting.markerTip.time(marker));
// add user-defined class to marker
if (marker.class) {
markerDiv.addClass(marker.class);
}
// bind click event to seek to marker time
markerDiv.on('click', function(e) {
var preventDefault = false;
if (typeof setting.onMarkerClick === "function") {
// if return false, prevent default behavior
preventDefault = setting.onMarkerClick(marker) == false;
}
if (!preventDefault) {
var key = $(this).data('marker-key');
player.currentTime(setting.markerTip.time(markersMap[key]));
}
});
if (setting.markerTip.display) {
registerMarkerTipHandler(markerDiv);
}
return markerDiv;
}
function updateMarkers() {
// update UI for markers whose time changed
for (var i = 0; i< markersList.length; i++) {
var marker = markersList[i];
var markerDiv = videoWrapper.find(".vjs-marker[data-marker-key='" + marker.key +"']");
var markerTime = setting.markerTip.time(marker);
if (markerDiv.data('marker-time') != markerTime) {
markerDiv.css({"left": getPosition(marker) + '%'})
.attr("data-marker-time", markerTime);
}
}
sortMarkersList();
}
function removeMarkers(indexArray) {
// reset overlay
if (breakOverlay){
overlayIndex = -1;
breakOverlay.css("visibility", "hidden");
}
currentMarkerIndex = -1;
for (var i = 0; i < indexArray.length; i++) {
var index = indexArray[i];
var marker = markersList[index];
if (marker) {
// delete from memory
delete markersMap[marker.key];
markersList[index] = null;
// delete from dom
videoWrapper.find(".vjs-marker[data-marker-key='" + marker.key +"']").remove();
}
}
// clean up array
for (var i = markersList.length - 1; i >=0; i--) {
if (markersList[i] === null) {
markersList.splice(i, 1);
}
}
// sort again
sortMarkersList();
}
// attach hover event handler
function registerMarkerTipHandler(markerDiv) {
markerDiv.on('mouseover', function(){
var marker = markersMap[$(this).data('marker-key')];
markerTip.find('.vjs-tip-inner').text(setting.markerTip.text(marker));
// margin-left needs to minus the padding length to align correctly with the marker
markerTip.css({"left" : getPosition(marker) + '%',
"margin-left" : -parseFloat(markerTip.css("width"))/2 - 5 + 'px',
"visibility" : "visible"});
}).on('mouseout',function(){
markerTip.css("visibility", "hidden");
});
}
function initializeMarkerTip() {
markerTip = $("<div class='vjs-tip'><div class='vjs-tip-arrow'></div><div class='vjs-tip-inner'></div></div>");
videoWrapper.find('.vjs-progress-control').append(markerTip);
}
// show or hide break overlays
function updateBreakOverlay() {
if(!setting.breakOverlay.display || currentMarkerIndex < 0){
return;
}
var currentTime = player.currentTime();
var marker = markersList[currentMarkerIndex];
var markerTime = setting.markerTip.time(marker);
if (currentTime >= markerTime &&
currentTime <= (markerTime + setting.breakOverlay.displayTime)) {
if (overlayIndex != currentMarkerIndex){
overlayIndex = currentMarkerIndex;
breakOverlay.find('.vjs-break-overlay-text').html(setting.breakOverlay.text(marker));
}
breakOverlay.css('visibility', "visible");
} else {
overlayIndex = -1;
breakOverlay.css("visibility", "hidden");
}
}
// problem when the next marker is within the overlay display time from the previous marker
function initializeOverlay() {
breakOverlay = $("<div class='vjs-break-overlay'><div class='vjs-break-overlay-text'></div></div>")
.css(setting.breakOverlay.style);
videoWrapper.append(breakOverlay);
overlayIndex = -1;
}
function onTimeUpdate() {
onUpdateMarker();
updateBreakOverlay();
}
function onUpdateMarker() {
/*
check marker reached in between markers
the logic here is that it triggers a new marker reached event only if the player
enters a new marker range (e.g. from marker 1 to marker 2). Thus, if player is on marker 1 and user clicked on marker 1 again, no new reached event is triggered)
*/
var getNextMarkerTime = function(index) {
if (index < markersList.length - 1) {
return setting.markerTip.time(markersList[index + 1]);
}
// next marker time of last marker would be end of video time
return player.duration();
}
var currentTime = player.currentTime();
var newMarkerIndex;
if (currentMarkerIndex != -1) {
// check if staying at same marker
var nextMarkerTime = getNextMarkerTime(currentMarkerIndex);
if(currentTime >= setting.markerTip.time(markersList[currentMarkerIndex]) &&
currentTime < nextMarkerTime) {
return;
}
// check for ending (at the end current time equals player duration)
if (currentMarkerIndex === markersList.length -1 &&
currentTime === player.duration()) {
return;
}
}
// check first marker, no marker is selected
if (markersList.length > 0 &&
currentTime < setting.markerTip.time(markersList[0])) {
newMarkerIndex = -1;
} else {
// look for new index
for (var i = 0; i < markersList.length; i++) {
nextMarkerTime = getNextMarkerTime(i);
if(currentTime >= setting.markerTip.time(markersList[i]) &&
currentTime < nextMarkerTime) {
newMarkerIndex = i;
break;
}
}
}
// set new marker index
if (newMarkerIndex != currentMarkerIndex) {
// trigger event
if (newMarkerIndex != -1 && options.onMarkerReached) {
options.onMarkerReached(markersList[newMarkerIndex]);
}
currentMarkerIndex = newMarkerIndex;
}
}
// setup the whole thing
function initialize() {
if (setting.markerTip.display) {
initializeMarkerTip();
}
// remove existing markers if already initialized
player.markers.removeAll();
addMarkers(options.markers);
if (setting.breakOverlay.display) {
initializeOverlay();
}
onTimeUpdate();
player.on("timeupdate", onTimeUpdate);
}
// setup the plugin after we loaded video's meta data
player.on("loadedmetadata", function() {
initialize();
});
// exposed plugin API
player.markers = {
getMarkers: function() {
return markersList;
},
next : function() {
// go to the next marker from current timestamp
var currentTime = player.currentTime();
for (var i = 0; i < markersList.length; i++) {
var markerTime = setting.markerTip.time(markersList[i]);
if (markerTime > currentTime) {
player.currentTime(markerTime);
break;
}
}
},
prev : function() {
// go to previous marker
var currentTime = player.currentTime();
for (var i = markersList.length - 1; i >=0 ; i--) {
var markerTime = setting.markerTip.time(markersList[i]);
// add a threshold
if (markerTime + 0.5 < currentTime) {
player.currentTime(markerTime);
break;
}
}
},
add : function(newMarkers) {
// add new markers given an array of index
addMarkers(newMarkers);
},
remove: function(indexArray) {
// remove markers given an array of index
removeMarkers(indexArray);
},
removeAll: function(){
var indexArray = [];
for (var i = 0; i < markersList.length; i++) {
indexArray.push(i);
}
removeMarkers(indexArray);
},
updateTime: function(){
// notify the plugin to update the UI for changes in marker times
updateMarkers();
},
reset: function(newMarkers){
// remove all the existing markers and add new ones
player.markers.removeAll();
addMarkers(newMarkers);
},
destroy: function(){
// unregister the plugins and clean up even handlers
player.markers.removeAll();
breakOverlay.remove();
markerTip.remove();
player.off("timeupdate", updateBreakOverlay);
delete player.markers;
},
};
}
videojs.plugin('markers', registerVideoJsMarkersPlugin);
})(jQuery, window.videojs);

Resources