diff --git a/.idea/PetitTetonMeteor.iml b/.idea/PetitTetonMeteor.iml index 81070da..e85550c 100644 --- a/.idea/PetitTetonMeteor.iml +++ b/.idea/PetitTetonMeteor.iml @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/.idea/dictionaries/Grumpy.xml b/.idea/dictionaries/Grumpy.xml index d3a465a..e75d550 100644 --- a/.idea/dictionaries/Grumpy.xml +++ b/.idea/dictionaries/Grumpy.xml @@ -2,7 +2,9 @@ clickable + dataset noselect + perserve signup styl yorkville diff --git a/.idea/libraries/meteor_packages_auto_import_browser.xml b/.idea/libraries/meteor_packages_auto_import_browser.xml index 0740bce..1f6ad38 100644 --- a/.idea/libraries/meteor_packages_auto_import_browser.xml +++ b/.idea/libraries/meteor_packages_auto_import_browser.xml @@ -234,7 +234,10 @@ + + + @@ -643,7 +646,10 @@ + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 2bd8f31..b87498a 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,48 +2,33 @@ - - - - - - - - - - - - + + - - + + + - - + + + - - - - - - + - - - + @@ -69,8 +54,62 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -79,8 +118,18 @@ - - + + + + + + + + + + + + @@ -89,89 +138,63 @@ - - + + - - - + + + - - + + - - + + - - + + - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - + + + @@ -187,21 +210,6 @@ - prevProducts - Session. - productCount - sAlert - prevSales - dataTable - btn-success - ; - includes - blaze - Session.set(PREFIX + 'displayNewProduct' - Need - toggleUpdateHistory - get - deactivated setPrevious productTags table @@ -217,10 +225,24 @@ # $ InsertSaleProduct + var + x + Template. + find + _id + xdomain + Product + Session.set(PREFIX + "editedMeasure", undefined) + Session + product + .name + measure + Measure + postfix + Postfix firstRow - venue row insertSale subcategory @@ -233,10 +255,17 @@ Products Product product - let saleCount prevButton input[name="product"] + let + x0Scale + measure + Measure + venue + Venue + Type + type @@ -245,57 +274,57 @@ @@ -317,8 +346,8 @@ - @@ -369,6 +398,7 @@ + @@ -391,7 +421,7 @@ @@ -408,32 +438,6 @@ - @@ -534,19 +555,12 @@ - + - - - - - - - @@ -554,6 +568,13 @@ + + + + + + + @@ -707,12 +728,14 @@ - + + + - @@ -724,27 +747,27 @@ - - + + - + - - + - + - + + @@ -776,225 +799,138 @@ - + - - - + + - + - - - + + - + + - - - + + - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - + + @@ -1003,11 +939,159 @@ + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1015,168 +1099,86 @@ - + - - + + - - + + + + + + + + + + - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - + - + - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - - + + diff --git a/.meteor/packages b/.meteor/packages index eeb92c2..1a3146e 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -59,3 +59,4 @@ momentjs:moment mizzao:bootboxjs # ??? aldeed:template-extension juliancwirko:s-alert # Client error/alert handling +jcbernack:reactive-aggregate diff --git a/.meteor/versions b/.meteor/versions index c3970b3..19a4a72 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -43,6 +43,7 @@ html-tools@1.0.11 htmljs@1.0.11 http@1.1.8 id-map@1.0.9 +jcbernack:reactive-aggregate@0.7.0 jquery@1.11.10 juliancwirko:s-alert@3.2.0 kadira:blaze-layout@2.3.0 @@ -55,6 +56,8 @@ matb33:collection-hooks@0.8.4 mdg:validation-error@0.2.0 meteor@1.6.0 meteor-base@1.0.4 +meteorhacks:aggregate@1.3.0 +meteorhacks:collection-utils@1.2.0 meteortoys:allthings@3.0.0 meteortoys:authenticate@3.0.0 meteortoys:autopub@3.0.0 @@ -81,6 +84,7 @@ modules-runtime@0.7.7 momentjs:moment@2.17.1 mongo@1.1.14 mongo-id@1.0.6 +mongo-livedata@1.0.12 msavin:jetsetter@2.0.0 msavin:mongol@2.0.1 npm-bcrypt@0.9.2 diff --git a/client/head.html b/client/head.html index e4d0619..cc91107 100644 --- a/client/head.html +++ b/client/head.html @@ -1,4 +1,6 @@ PT App + + \ No newline at end of file diff --git a/client/main.styl b/client/main.styl index 779e63a..ae15df1 100644 --- a/client/main.styl +++ b/client/main.styl @@ -298,8 +298,10 @@ input, textarea, keygen, select, button, meter, progress @import "../imports/ui/UserManagement.import.styl" @import "../imports/ui/Measures.import.styl" +@import "../imports/ui/Venues.import.styl" @import "../imports/ui/Products.import.styl" @import "../imports/ui/ProductTags.import.styl" @import "../imports/ui/Sales.import.styl" @import "../imports/ui/Pricing.import.styl" -@import "../imports/ui/Production.import.styl" \ No newline at end of file +@import "../imports/ui/Production.import.styl" +@import "../imports/ui/Graphs.import.styl" \ No newline at end of file diff --git a/imports/api/Measure.js b/imports/api/Measure.js index 9af3b65..9ec89a2 100644 --- a/imports/api/Measure.js +++ b/imports/api/Measure.js @@ -45,82 +45,75 @@ Measures.attachSchema(new SimpleSchema({ // denyInsert: true, optional: true }, - deletedAt: { - type: Date, - label: "Deleted On", + deactivated: { + type: Boolean, + label: "Deactivated", optional: true }, - deletedBy: { - type: String, - label: "Deleted By", - optional: true - }, - restoredAt: { - type: Date, - label: "Restored On", - optional: true - }, - restoredBy: { - type: String, - label: "Restored By", + hidden: { + type: Boolean, + label: "Hidden", optional: true } })); -//https://github.com/zimme/meteor-collection-softremovable -Measures.attachBehaviour("softRemovable", { - removed: 'deleted', - removedAt: 'deletedAt', - removedBy: 'removedBy', - restoredAt: 'restoredAt', - restoredBy: 'restoredBy' -}); - if(Meteor.isServer) Meteor.publish('measures', function() { return Measures.find({}); }); -// Requires: meteor add matb33:collection-hooks -Measures.before.insert(function(userId, doc) { - // check(userId, String); - doc.createdAt = new Date(); -}); -Measures.before.update(function(userId, doc, fieldNames, modifier, options) { - modifier.$set = modifier.$set || {}; //Make sure there is an object. - modifier.$set.updatedAt = new Date(); -}); - if(Meteor.isServer) { Meteor.methods({ - insertMeasure: function(measure) { - check(measure, { - name: String, - order: Number, - postfix: String - }); - - measure.createdAt = new Date(); + createMeasure: function(name, postfix, order) { + check(name, String); + check(postfix, String); + check(order, Number); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Measures.insert(measure); + Measures.insert({name, postfix, order, createdAt: new Date()}); } else throw new Meteor.Error(403, "Not authorized."); }, - deleteMeasure: function(id) { + deactivateMeasure: function(id) { if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Measures.remove(id); + //Measures.remove(id); + Measures.update(id, {$set: {deactivated: true}}, {bypassCollection2: true}); } else throw new Meteor.Error(403, "Not authorized."); }, - updateMeasure: function(measure) { - check(measure, { - name: String, - order: Number, - postfix: String - }); + reactivateMeasure: function(id) { + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + Measures.update(id, {$set: {deactivated: false}}, {bypassCollection2: true}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + hideMeasure: function(id) { //One step past deactivated - will only show in the measures list if hidden measures are enabled. + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + //Measures.remove(id); + Measures.update(id, {$set: {hidden: true}}, {bypassCollection2: true}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + showMeasure: function(id) { //Returns the measure to being simply deactivated. Will again show in lists. + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + Measures.update(id, {$set: {hidden: false}}, {bypassCollection2: true}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + //deleteMeasure: function(id) { + // if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + // //TODO: Should troll the database looking for references to remove or replace. This is currently not used. + // Measures.remove(id); + // } + // else throw new Meteor.Error(403, "Not authorized."); + //}, + updateMeasure: function(id, name, postfix, order) { + check(id, String); + check(name, String); + check(postfix, String); + check(order, Number); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Products.update(id, {$set: {name: measure.name, order: measure.order, postfix: measure.postfix, updateAt: new Date()}}); + Products.update(id, {$set: {name, postfix, order, updatedAt: new Date()}}); } else throw new Meteor.Error(403, "Not authorized."); } diff --git a/imports/api/Product.js b/imports/api/Product.js index 1607597..6d86544 100644 --- a/imports/api/Product.js +++ b/imports/api/Product.js @@ -217,7 +217,7 @@ if(Meteor.isServer) { if(measures) check(measures, [String]); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Products.update(id, {$set: {name: name, tags: tags, aliases: aliases, measures: measures, updateAt: new Date()}}, {bypassCollection2: true}); + Products.update(id, {$set: {name: name, tags: tags, aliases: aliases, measures: measures, updatedAt: new Date()}}, {bypassCollection2: true}); } else throw new Meteor.Error(403, "Not authorized."); }, diff --git a/imports/api/ProductTag.js b/imports/api/ProductTag.js index 8bc0610..74d5452 100644 --- a/imports/api/ProductTag.js +++ b/imports/api/ProductTag.js @@ -83,7 +83,7 @@ if(Meteor.isServer) { name: String }); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - ProductTags.update(tag._id, {$set: {name: tag.name, updateAt: new Date()}}); + ProductTags.update(tag._id, {$set: {name: tag.name, updatedAt: new Date()}}); } else throw new Meteor.Error(403, "Not authorized."); } diff --git a/imports/api/Sale.js b/imports/api/Sale.js index 4e6a2f2..64cd3f2 100644 --- a/imports/api/Sale.js +++ b/imports/api/Sale.js @@ -92,6 +92,86 @@ if(Meteor.isServer) { dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {}; return Meteor.collections.Sales.find(dbQuery, {limit: limit, sort: {date: -1, createdAt: -1}, skip: skipCount}); }); + // time: expects either undefined, 'weekly', or 'monthly' + // options: expects either undefined, 'markets', or 'types' + Meteor.publish('salesTotals', function(time, options) { + let pipeline = []; + let group = { + $group: { + _id: { + year: {$dateToString: {format: '%Y', date: '$date'}}//{$year: '$date'} + }, + 'total': { + $sum: { + $multiply: ['$price', '$amount'] + } + } + } + }; + let project = { + $project: { + year: '$_id.year', + date: '$_id.year', + total: true, + _id: {$concat: ['$_id.year']} + } + }; + + pipeline.push(group); + pipeline.push(project); + + //Annual is assumed if not week or month. + if(time === 'weekly') { + group.$group._id.week = {$dateToString: {format: '%U', date: '$date'}}; //{$week: '$date'}; + project.$project.week = '$_id.week'; + project.$project.date = {$concat: ['$_id.week', '-', '$_id.year']}; + project.$project._id.$concat.push('$_id.week'); + } + else if(time === 'monthly') { + group.$group._id.month = {$dateToString: {format: '%m', date: '$date'}}; //{$month: '$date'}; + project.$project.month = '$_id.month'; + project.$project.date = {$concat: ['$_id.month', '-', '$_id.year']}; + project.$project._id.$concat.push('$_id.month'); + } + + if(options === 'markets') { + group.$group._id.venueId = '$venueId'; + project.$project.venueId = '$_id.venueId'; + project.$project._id.$concat.push('$_id.venueId'); + pipeline.push({$lookup: {from: 'Venues', localField: 'venueId', foreignField: '_id', as: 'venue'}}); + pipeline.push({$project: {year: 1, week: 1, month: 1, total: 1, venueId: 1, venue: {$arrayElemAt: ['$venue', 0]}}}); + pipeline.push({$project: {year: 1, week: 1, month: 1, total: 1, venueId: 1, venue: '$venue.name'}}); + } + else if(options === 'types') { + //query[0].$group.month = {$month: '$date'}; + //TODO: Need to divide the sales up by: + // Sweets + // Savories + // Meats + // VAP + // Egg + // Other Produce + // Total Produce + // Jars + } + + + ReactiveAggregate(this, Sales, pipeline, {clientCollection: 'salesTotals'}); + + /* + {$sales: { + _id: Random.id(), + year: $_id.$year, + total: $total + }} + */ + //ReactiveAggregate(this, Sales, [query], {clientCollection: 'salesTotals', transform: function(doc) { + // console.log("Running transform function"); + // Object.assign(doc._id, doc); + // doc._id = Random.id(); + // return doc; + //}}); + }); Meteor.methods({ getSalesCount: function(query) { @@ -105,7 +185,7 @@ if(Meteor.isServer) { if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { Sales.insert(sale, function(err, id) { if(err) console.log(err); - }); + }, {bypassCollection2: true}); } else throw new Meteor.Error(403, "Not authorized."); }, @@ -113,7 +193,7 @@ if(Meteor.isServer) { check(id, String); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Sales.remove(id); + Sales.remove(id, {bypassCollection2: true}); } else throw new Meteor.Error(403, "Not authorized."); } diff --git a/imports/api/Venue.js b/imports/api/Venue.js index 5ecc580..d576f54 100644 --- a/imports/api/Venue.js +++ b/imports/api/Venue.js @@ -29,38 +29,19 @@ let VenuesSchema = new SimpleSchema({ label: "Updated On", optional: true }, - deletedAt: { - type: Date, - label: "Deleted On", + deactivated: { + type: Boolean, + label: "Deactivated", optional: true }, - deletedBy: { - type: String, - label: "Deleted By", - optional: true - }, - restoredAt: { - type: Date, - label: "Restored On", - optional: true - }, - restoredBy: { - type: String, - label: "Restored By", + hidden: { + type: Boolean, + label: "Hidden", optional: true } }); Venues.attachSchema(VenuesSchema); -//https://github.com/zimme/meteor-collection-softremovable -Venues.attachBehaviour("softRemovable", { - removed: 'deleted', - removedAt: 'deletedAt', - removedBy: 'removedBy', - restoredAt: 'restoredAt', - restoredBy: 'restoredBy' -}); - if(Meteor.isServer) Meteor.publish('venues', function() { return Venues.find({}); }); @@ -77,54 +58,58 @@ if(Meteor.isServer) { }); Meteor.methods({ - insertVenue: function(venue) { - check(venue, { - name: String, - type: String - }); - - venue.createdAt = new Date(); + createVenue: function(name, type) { + check(name, String); + check(type, String); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Venues.insert(venue); + Venues.insert({name, type, createdAt: new Date()}); } else throw new Meteor.Error(403, "Not authorized."); }, - deleteVenue: function(id) { + deactivateVenue: function(id) { if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Venues.remove(id); + //Venues.remove(id); + Venues.update(id, {$set: {deactivated: true}}, {bypassCollection2: true}); } else throw new Meteor.Error(403, "Not authorized."); }, - updateVenue: function(venue) { - check(venue, { - name: String, - type: String - }); + reactivateVenue: function(id) { + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + Venues.update(id, {$set: {deactivated: false}}, {bypassCollection2: true}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + hideVenue: function(id) { //One step past deactivated - will only show in the venues list if hidden venues are enabled. + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + //Venues.remove(id); + Venues.update(id, {$set: {hidden: true}}, {bypassCollection2: true}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + showVenue: function(id) { //Returns the venue to being simply deactivated. Will again show in lists. + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + Venues.update(id, {$set: {hidden: false}}, {bypassCollection2: true}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + //deleteVenue: function(id) { + // if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + // Venues.remove(id); //TODO: If this is ever allowed, we should either remove or replace references to the deleted venue in the rest of the database. + // } + // else throw new Meteor.Error(403, "Not authorized."); + //}, + updateVenue: function(id, name, type) { + check(id, String); + check(name, String); + check(type, String); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { - Venues.update(id, {$set: {name: venue.name, type: venue.type, updateAt: new Date()}}); + Venues.update(id, {$set: {name, type, updatedAt: new Date()}}); } else throw new Meteor.Error(403, "Not authorized."); } }); } -//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes. -// Meteor.allow({ -// insert: true, -// update: ()->{return true}, -// remove: checkUser -// }; -// checkUser = function(userId, doc) { -// return doc && doc.userId === userId; -// }; - -//Allows the client to interact with the db through the server via custom methods. -// Meteor.methods({ -// deleteMeasure: function(id) { -// Measures.remove(id); -// } -// }); - export default Venues; \ No newline at end of file diff --git a/imports/startup/client/routes.js b/imports/startup/client/routes.js index c963d71..18e7028 100644 --- a/imports/startup/client/routes.js +++ b/imports/startup/client/routes.js @@ -56,4 +56,25 @@ pri.route('/pricing', { require("/imports/ui/Pricing.js"); BlazeLayout.render('Body', {content: 'Pricing'}); } +}); +pri.route('/venues', { + name: 'Venues', + action: function(params, queryParams) { + require("/imports/ui/Venues.js"); + BlazeLayout.render('Body', {content: 'Venues'}); + } +}); +pri.route('/graphs', { + name: 'Graphs', + action: function(params, queryParams) { + require("/imports/ui/Graphs.js"); + BlazeLayout.render('Body', {content: 'Graphs'}); + } +}); +pri.route('/graphTest', { + name: 'GraphTest', + action: function(params, queryParams) { + require("/imports/ui/GraphTest.js"); + BlazeLayout.render('Body', {content: 'GraphTest'}); + } }); \ No newline at end of file diff --git a/imports/ui/GraphTest.html b/imports/ui/GraphTest.html new file mode 100644 index 0000000..ae7a5b4 --- /dev/null +++ b/imports/ui/GraphTest.html @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/imports/ui/GraphTest.import.styl b/imports/ui/GraphTest.import.styl new file mode 100644 index 0000000..f98fbca --- /dev/null +++ b/imports/ui/GraphTest.import.styl @@ -0,0 +1,38 @@ +#graphTest + margin: 10px 20px + height: 100% + svg + width: 100% + .bar + fill: steelblue + .xAxisLabels + font-size: 16px + font-family: "Arial Black", "Arial Bold", Gadget, sans-serif + font-weight: 800 + .yAxisLabels + font-size: 12px + font-family: "Arial Black", "Arial Bold", Gadget, sans-serif + font-weight: 800 + .barText + font-size: 14px + font-family: "Arial Black", "Arial Bold", Gadget, sans-serif + font-weight: 800 + table + table-layout: fixed + > thead + > tr + > th.total + width: 200px + > th.market + width: 200px + > th.week + width: 200px + > th.month + width: 200px + > th.year + width: 200px + > tbody + > tr.deactivated + background-color: #fac0d1 + > tr.deactivated:hover + background-color: #ffcadb diff --git a/imports/ui/GraphTest.js b/imports/ui/GraphTest.js new file mode 100644 index 0000000..5b880bb --- /dev/null +++ b/imports/ui/GraphTest.js @@ -0,0 +1,232 @@ + +import './GraphTest.html'; +import d3 from 'd3'; + +let SalesTotals = new Meteor.Collection(null); + +function changeData() { + if(SalesTotals.find({}).count() > 0) { + let sales = SalesTotals.find({}).fetch(); + + for(let sale of sales) { + SalesTotals.update(sale._id, {$set: {year: sale.year, total: Math.round(Math.random() * 10000) / 100}}); + } + } + else { + let startYear = Math.round(Math.random() * 10) + 2000; + let yearCount = 4; + + for(let i = 0; i < yearCount; i++) + //for(let m = 0; m < 12; m++) + SalesTotals.insert({year: startYear + i/*, month: m*/, total: Math.round(Math.random() * 10000) / 100}); + } +} + +Template.GraphTest.onCreated(function() { + let template = Template.instance(); + + changeData(); +}); +Template.GraphTest.onRendered(function() { + ////Build the SVG Graphs + let margin = {top: 20, right: 20, bottom: 30, left: 80}; + let width = 960 - margin.left - margin.right; + let height = 500 - margin.top - margin.bottom; + let x0Scale = d3.scaleBand().range([0, width]).padding(0.1); + let yScale = d3.scaleLinear().range([height, 0]); + let svg = d3.select('svg.salesGraph') + .attr("viewBox", "0 0 960 500") + .attr("perserveAspectRatio", "xMidYMid meet") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + let xLabels = svg.append("g").attr("transform", "translate(0, " + height + ")"); + let yLabels = svg.append("g"); + + Tracker.autorun(function() { + let dataset = SalesTotals.find({}).fetch(); + + //Update scale domains + x0Scale.domain([...new Set(dataset.map(item => item.year))]); + yScale.domain([0, d3.max(dataset, function(d) {return d.total})]); + + //Join the data set with the existing data in the svg element. + let bars = svg.selectAll('.bar').data(dataset); + + //Handle existing elements. + // bars.[do something here ie: attr('class', 'oldValues')] + //Handle the new data elements by adding them to the svg element. + bars.enter().append('rect') + .attr('class', 'bar') + .attr('x', function(d) { + return x0Scale(d.year); + }) + .attr('width', x0Scale.bandwidth()) + .attr('y', function(d) { + return yScale(d.total); + }) + .attr('height', function(d) { + return height - yScale(d.total); + }); + + bars.transition() + // .delay(function(d, i) { + // return i / dataset.length * 1000; + // }) // this delay will make transistions sequential instead of parallel + .duration(500) + .attr("x", function(d, i) { + return x0Scale(d.year); + }) + .attr("y", function(d) { + return yScale(d.total); + }) + .attr("width", x0Scale.bandwidth()) + .attr("height", function(d) { + return height - yScale(d.total); + }); + //.attr("fill", function(d) { + // return "rgb(0, 0, " + (d.total * 10) + ")"; + //}); + + bars.exit() + .transition() + .duration(500) + .attr("x", -x0Scale.bandwidth()) + .remove(); + + let barTexts = svg.selectAll('text.barText').data(dataset); + + barTexts.enter().append("text") + .attr("class", "barText") + .attr("text-anchor", "middle") + .attr('x', function(d) {return x0Scale(d.year) + x0Scale.bandwidth() / 2;}) + .attr('y', function(d) {return yScale(d.total) - 2;}) + .text(function(d) {return "$" + Math.round(d.total)}); + + barTexts.transition() + .duration(500) + .attr('x', function(d) {return x0Scale(d.year) + x0Scale.bandwidth() / 2;}) + .attr('y', function(d) {return yScale(d.total) - 2;}) + .text(function(d) {return "$" + Math.round(d.total)}); + + barTexts.exit() + .remove(); + + //Add the x & y axis labels + xLabels.attr("class", "xAxisLabels").call(d3.axisBottom(x0Scale)); + yLabels.attr("class", "yAxisLabels").call(d3.axisLeft(yScale)); + }); +}); +Template.GraphTest.helpers({ + sales: function() { + let sort = []; + + sort.push(['year', 'asc']); + + return SalesTotals.find({}, {sort: sort}); + }, + formatTotal: function(total) { + return "$" + total.toFixed(2); + } +}); +Template.GraphTest.events({ + 'click button[name="changeData"]': function(event, template) { + changeData(); + } +}); + + + + +/* +//Select… +let bars = svg.selectAll("rect").data(dataset, key); + +//Enter… +bars.enter() + .append("rect") + .attr("x", w) + .attr("y", function(d) { + return h - yScale(d.total); + }) + //.attr("width", x0Scale.rangeBand()) + .attr("width", x0Scale.bandwidth()) + .attr("height", function(d) { + return yScale(d.total); + }) + .attr("fill", function(d) { + return "rgb(0, 0, " + (d.total * 10) + ")"; + }) + .attr("data-id", function(d){ + return d._id; + }); + +//Update… +bars.transition() + // .delay(function(d, i) { + // return i / dataset.length * 1000; + // }) // this delay will make transistions sequential instead of paralle + .duration(500) + .attr("x", function(d, i) { + return x0Scale(i); + }) + .attr("y", function(d) { + return h - yScale(d.total); + }) + .attr("width", x0Scale.bandwidth()) + .attr("height", function(d) { + return yScale(d.total); + }).attr("fill", function(d) { + return "rgb(0, 0, " + (d.total * 10) + ")"; + }); + +//Exit… +bars.exit() + .transition() + .duration(500) + .attr("x", -x0Scale.bandwidth()) + .remove(); + + + +//Update all labels + +//Select… +let labels = svg.selectAll("text") + .data(dataset, key); + +//Enter… +labels.enter() + .append("text") + .text(function(d) { + return d.total; + }) + .attr("text-anchor", "middle") + .attr("x", w) + .attr("y", function(d) { + return h - yScale(d.total) + 14; + }) + .attr("font-family", "sans-serif") + .attr("font-size", "11px") + .attr("fill", "white"); + +//Update… +labels.transition() + // .delay(function(d, i) { + // return i / dataset.length * 1000; + // }) // this delay will make transistions sequential instead of paralle + .duration(500) + .attr("x", function(d, i) { + return x0Scale(i) + x0Scale.bandwidth() / 2; + }).attr("y", function(d) { + return h - yScale(d.total) + 14; + }).text(function(d) { + return d.total; + }); + +//Exit… +labels.exit() + .transition() + .duration(500) + .attr("x", -x0Scale.bandwidth()) + .remove(); + */ \ No newline at end of file diff --git a/imports/ui/Graphs.html b/imports/ui/Graphs.html new file mode 100644 index 0000000..ed1f1f9 --- /dev/null +++ b/imports/ui/Graphs.html @@ -0,0 +1,53 @@ + \ No newline at end of file diff --git a/imports/ui/Graphs.import.styl b/imports/ui/Graphs.import.styl new file mode 100644 index 0000000..2d6068c --- /dev/null +++ b/imports/ui/Graphs.import.styl @@ -0,0 +1,38 @@ +#graphs + margin: 10px 20px + height: 100% + svg + width: 100% + .bar + fill: steelblue + .xAxisLabels + font-size: 7px + font-family: "Arial", Gadget, sans-serif + font-weight: 100 + .yAxisLabels + font-size: 12px + font-family: "Arial Black", "Arial Bold", Gadget, sans-serif + font-weight: 800 + .barText + font-size: 14px + font-family: "Arial Black", "Arial Bold", Gadget, sans-serif + font-weight: 800 + table + table-layout: fixed + > thead + > tr + > th.total + width: 200px + > th.market + width: 200px + > th.week + width: 200px + > th.month + width: 200px + > th.year + width: 200px + > tbody + > tr.deactivated + background-color: #fac0d1 + > tr.deactivated:hover + background-color: #ffcadb diff --git a/imports/ui/Graphs.js b/imports/ui/Graphs.js new file mode 100644 index 0000000..1cdafef --- /dev/null +++ b/imports/ui/Graphs.js @@ -0,0 +1,404 @@ + +import './Graphs.html'; +import d3 from 'd3'; + +let PREFIX = "graphs."; + +let SalesTotals = new Meteor.Collection("salesTotals"); + +Meteor.subscribe("venues"); +Meteor.subscribe("productTags"); + +let salesTotalsSubscription; +//Save the time and options values outside the session also (in addition to saving it in the session), so we can access it in the autorun for the graph without triggering a re-run of the graph. +//Triggering a re-run of the graph from the session change will cause the graph to redraw BEFORE the data arrives from the server (due to the options changing), causing all sorts of grief. +let time = "annual"; +let options = "none"; + +Template.Graphs.onCreated(function() { + let template = Template.instance(); + + if(!Session.get(PREFIX + "time")) Session.set(PREFIX + "time", time); + else time = Session.get(PREFIX + "time"); + if(!Session.get(PREFIX + "options")) Session.set(PREFIX + "options", options); + else options = Session.get(PREFIX + "options"); + + Tracker.autorun(function() { + salesTotalsSubscription = template.subscribe("salesTotals", Session.get(PREFIX + "time"), Session.get(PREFIX + "options")); + }); +}); +Template.Graphs.onRendered(function() { + //Reset the pull downs to their former states. + Template.instance().$('select[name="time"]').val(time); + Template.instance().$('select[name="options"]').val(options); + + //Build the SVG Graphs + let margin = {top: 10, right: 0, bottom: 30, left: 40}; + let width = 960 - margin.left - margin.right; + let height = 500 - margin.top - margin.bottom; + let x0Scale = d3.scaleBand().range([0, width]).padding(0.05); + let x1Scale = d3.scaleBand().padding(0.05); + let yScale = d3.scaleLinear().range([height, 0]); + let svg = d3.select('svg.salesGraph') + .attr("viewBox", "0 0 960 500") + .attr("perserveAspectRatio", "xMidYMid meet") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + let xLabels = svg.append("g").attr('class', 'xLabels').attr("transform", "translate(0, " + height + ")"); + let yLabels = svg.append("g").attr('class', 'yLabels'); + + Tracker.autorun(function() { + if(salesTotalsSubscription.ready()) { + //NOTE: Setting these time/options based flags is necessary in the autorun since they change, but don't use the session variables for the values, otherwise the autorun will re-run when the session changes, but before the new data arrives from the server. + let showMonths = time == 'monthly'; + let showWeeks = time == 'weekly'; + let showMarkets = options == 'markets'; + let showTypes = options == 'types'; + + let grouped = time != 'annual' || options != 'none'; + let stacked = time != 'annual' && options != 'none'; + + let x1ScaleKeyFunction = function(d) {return showMonths ? d.month : showWeeks ? Number(d.week) : showMarkets ? d.venueId + "-" + d.year : d.year}; + + console.log("collection count: " + SalesTotals.find({}).count()); + let dataset = SalesTotals.find({}).fetch(); + let x1UniqueValues; + + if(showMonths) x1UniqueValues = [...new Set(dataset.map(item => item.month))]; + else if(showWeeks) x1UniqueValues = ([...new Set(dataset.map(item => Number(item.week)))]).sort(function(a,b) {return a - b}); + else if(showMarkets) x1UniqueValues = [...new Set(dataset.map(item => item.venueId))]; + else x1UniqueValues = [""]; + + //Update scale domains + let years = [...new Set(dataset.map(item => item.year))]; + x0Scale.domain(years); //Gets the unique set of years from the dataset. + x1Scale.domain(x1UniqueValues).rangeRound([0, x0Scale.bandwidth()]); + console.log("x1Scale's unique values: " + x1UniqueValues); + console.log("x1Scale's bandwidth: " + x1Scale.bandwidth()); + yScale.domain([0, d3.max(dataset, function(d) {return d.total;})]); + + //Join the data set with the existing data in the svg element. + let barGroups = svg.selectAll('.barGroup').data(years); + let barsInGroup; + + let handleBarsInGroup = function(barsInGroup) { + barsInGroup.exit() + .transition() + .duration(500) + .style('opacity', 0) + .remove(); + barsInGroup.enter().append('rect') + .attr('class', 'bar') + .attr('y', function(d) {return yScale(d.total)}) + .attr('height', function(d) {return height - yScale(d.total)}) + .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))}) + .attr('width', x1Scale.bandwidth()); + barsInGroup + .transition() + .duration(500) + .attr('y', function(d) {return yScale(d.total)}) + .attr('height', function(d) {return height - yScale(d.total)}) + .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))}) + .attr('width', x1Scale.bandwidth()); + }; + + barGroups.exit() + .transition() + .duration(500) + .style('opacity', 0) + .remove(); + barsInGroup = barGroups.enter().append('g') + .attr('class', 'barGroup') + .attr('transform', function(d) {return 'translate(' + x0Scale(d) + ',0)'}) + .selectAll('.bar') + .data(function(d) {return dataset.filter(function(x) {return x.year == d})}, function(d) {return d._id}); + //barGroups = svg.selectAll('.barGroup').data(dataset); + handleBarsInGroup(barsInGroup); + barsInGroup = barGroups + .attr('transform', function(d) {return 'translate(' + x0Scale(d) + ',0)'}) + .selectAll('.bar') + .data(function(d) {return dataset.filter(function(x) {return x.year == d})}, function(d) {return d._id}); + handleBarsInGroup(barsInGroup); + //barGroups.attr('transform', function(d) {return 'translate(' + x0Scale(d.year) + ',0)'}); + //barsInGroup = barsInGroup.data(function(d) { + // console.log("Getting the data for a bar group for year: " + d); + // let r = dataset.filter(function(x) {return x.year == d}); + // console.log(r); + // return r;}); + + + + ////Handle the new data elements by adding them to the svg element. + //let barsInNewGroup = barGroups.enter().append('g') + // .attr('transform', function(d) {return 'translate(' + x0Scale(d.year) + ',0)'}) + // .attr('class', 'barGroup') + // .selectAll('bar') + // .data(function(d) {return dataset.filter(function(x) {return x.year == d})}); + // + //let barsInModifiedGroup = barGroups.transition() + // .duration(500) + // .attr('transform', function(d) {return 'translate(' + x0Scale(d.year) + ',0)'}) + // .selectAll('bar') + // .data(function(d) {return dataset.filter(function(x) {return x.year == d})}); + // + //let barHandler = function(barInGroup) { + // barInGroup.enter().append('rect') + // .attr('class', 'bar') + // .attr('y', function(d) {return yScale(d.total)}) + // .attr('height', function(d) {return height - yScale(d.total)}) + // .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))}) + // .attr('width', x1Scale.bandwidth()); + // barInGroup.transition() + // .duration(500) + // .attr('y', function(d) {return yScale(d.total)}) + // .attr('height', function(d) {return height - yScale(d.total)}) + // .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))}) + // .attr('width', x1Scale.bandwidth()); + // barInGroup.exit() + // .transition() + // .duration(500) + // .remove() + // .attr("x", -x1Scale.bandwidth()); + //}; + // + //barHandler(barsInNewGroup); + //barHandler(barsInModifiedGroup); + + + + + + + ////Transition existing elements to their new states. + //let barsTransition = barGroups.transition() + // .duration(500) + // .attr('y', function(d) { + // return yScale(d.total); + // }) + // .attr('height', function(d) { + // return height - yScale(d.total); + // }) + // .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))}) + // .attr('width', x1Scale.bandwidth()); + // + ////Transition removed elements off the screen. + //let barsExit = barGroups.exit() + // .transition() + // .duration(500) + // .remove() + // .attr("x", -x1Scale.bandwidth()); + + + + + + //let barTexts = svg.selectAll('text.barText').data(dataset); + + //barTexts.enter().append("text") + // .attr("class", "barText") + // .attr("text-anchor", "middle") + // .attr('x', function(d) {return x0Scale(d._id) + x0Scale.bandwidth() / 2;}) + // .attr('y', function(d) {return yScale(d.total) - 2;}) + // .text(function(d) {return "$" + Math.round(d.total)}); + //barTexts.transition() + // .attr("class", "barText") + // .attr("text-anchor", "middle") + // .attr('x', function(d) {return x0Scale(d._id) + x0Scale.bandwidth() / 2;}) + // .attr('y', function(d) {return yScale(d.total) - 2;}) + // .text(function(d) {return "$" + Math.round(d.total)}); + //barTexts.exit() + // .remove(); + + //Add the x & y axis labels + //xLabels.attr("class", "xAxisLabels").call(d3.axisBottom(x1Scale)); + xLabels.attr('class', 'xAxisLabels'). + yLabels.attr("class", "yAxisLabels").call(d3.axisLeft(yScale)); + } + }); + + //let margin = {top: 20, right: 20, bottom: 30, left: 80}; + //let width = 960 - margin.left - margin.right; + //let height = 500 - margin.top - margin.bottom; + //let x0Scale = d3.scaleBand().range([0, width]).padding(0.1); + //let yScale = d3.scaleLinear().range([height, 0]); + //let svg = d3.select('svg.salesGraph')//d3.select("#graphs").append("svg") + // .attr("viewBox", "0 0 960 500") + // .attr("perserveAspectRatio", "xMidYMid meet") + // .append("g") + // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + //let xLabels = svg.append("g").attr("transform", "translate(0, " + height + ")"); + //let yLabels = svg.append("g"); + // + //Tracker.autorun(function() { + // console.log("Autorun"); + // if(salesTotalsSubscription.ready()) { + // console.log("Ready"); + // let dataset = SalesTotals.find({}).fetch(); + // + // //Update scale domains + // x0Scale.domain(dataset.map(function(d) {return d._id})); + // yScale.domain([0, d3.max(dataset, function(d) {return d.total;})]); + // + // //Join the data set with the existing data in the svg element. + // let bars = svg.selectAll('.bar').data(dataset); + // + // //Handle existing elements. + // // bars.[do something here ie: attr('class', 'oldValues')] + // //Handle the new data elements by adding them to the svg element. + // bars.enter().append('rect') + // .attr('class', 'bar') + // .attr('x', function(d) { + // return x0Scale(d._id); + // }) + // .attr('width', x0Scale.bandwidth()) + // .attr('y', function(d) { + // return yScale(d.total); + // }) + // .attr('height', function(d) { + // return height - yScale(d.total); + // }); + // //Handle removed data elements? + // // bars.exit().remove(); + // + // let barTexts = svg.selectAll('text.barText').data(dataset); + // + // barTexts.enter().append("text") + // .attr("class", "barText") + // .attr("text-anchor", "middle") + // .attr('x', function(d) {return x0Scale(d._id) + x0Scale.bandwidth() / 2;}) + // .attr('y', function(d) {return yScale(d.total) - 2;}) + // .text(function(d) {return "$" + Math.round(d.total)}); + // //Add the x & y axis labels + // xLabels.attr("class", "xAxisLabels").call(d3.axisBottom(x0Scale)); + // yLabels.attr("class", "yAxisLabels").call(d3.axisLeft(yScale)); + // } + //}); +}); +Template.Graphs.helpers({ + sales: function() { + let sort = []; + + sort.push(['year', 'asc']);// year = 1; + if(Session.get(PREFIX + "time") === 'weekly') sort.push(['week', 'asc']); // .week = 1; + if(Session.get(PREFIX + "time") === 'monthly') sort.push(['month', 'asc']); // .month = 1; + if(Session.get(PREFIX + "options") === 'markets') sort.push(['venue', 'asc']); // .month = 1; + + return SalesTotals.find({}, {sort: sort}); + }, + showTime: function(time) { + return Session.get(PREFIX + "time") === time; + }, + showOption: function(option) { + return Session.get(PREFIX + "options") === option; + }, + formatTotal: function(total) { + return "$" + total.toFixed(2); + } +}); +Template.Graphs.events({ + 'change select[name="time"]': function(event, template) { + Session.set(PREFIX + "time", $(event.target).val()); + time = $(event.target).val(); + }, + 'change select[name="options"]': function(event, template) { + Session.set(PREFIX + "options", $(event.target).val()); + options = $(event.target).val(); + } +}); + + + + +/* +//Select… +let bars = svg.selectAll("rect").data(dataset, key); + +//Enter… +bars.enter() + .append("rect") + .attr("x", w) + .attr("y", function(d) { + return h - yScale(d.total); + }) + //.attr("width", x0Scale.rangeBand()) + .attr("width", x0Scale.bandwidth()) + .attr("height", function(d) { + return yScale(d.total); + }) + .attr("fill", function(d) { + return "rgb(0, 0, " + (d.total * 10) + ")"; + }) + .attr("data-id", function(d){ + return d._id; + }); + +//Update… +bars.transition() + // .delay(function(d, i) { + // return i / dataset.length * 1000; + // }) // this delay will make transistions sequential instead of paralle + .duration(500) + .attr("x", function(d, i) { + return x0Scale(i); + }) + .attr("y", function(d) { + return h - yScale(d.total); + }) + .attr("width", x0Scale.bandwidth()) + .attr("height", function(d) { + return yScale(d.total); + }).attr("fill", function(d) { + return "rgb(0, 0, " + (d.total * 10) + ")"; + }); + +//Exit… +bars.exit() + .transition() + .duration(500) + .attr("x", -x0Scale.bandwidth()) + .remove(); + + + +//Update all labels + +//Select… +let labels = svg.selectAll("text") + .data(dataset, key); + +//Enter… +labels.enter() + .append("text") + .text(function(d) { + return d.total; + }) + .attr("text-anchor", "middle") + .attr("x", w) + .attr("y", function(d) { + return h - yScale(d.total) + 14; + }) + .attr("font-family", "sans-serif") + .attr("font-size", "11px") + .attr("fill", "white"); + +//Update… +labels.transition() + // .delay(function(d, i) { + // return i / dataset.length * 1000; + // }) // this delay will make transistions sequential instead of paralle + .duration(500) + .attr("x", function(d, i) { + return x0Scale(i) + x0Scale.bandwidth() / 2; + }).attr("y", function(d) { + return h - yScale(d.total) + 14; + }).text(function(d) { + return d.total; + }); + +//Exit… +labels.exit() + .transition() + .duration(500) + .attr("x", -x0Scale.bandwidth()) + .remove(); + */ \ No newline at end of file diff --git a/imports/ui/Measures.html b/imports/ui/Measures.html index 5a544b1..50dcf35 100644 --- a/imports/ui/Measures.html +++ b/imports/ui/Measures.html @@ -1,18 +1,70 @@ -