diff --git a/scout-ui/gulpfile.js b/scout-ui/gulpfile.js index 77bf7b16f37..e912fbf1052 100644 --- a/scout-ui/gulpfile.js +++ b/scout-ui/gulpfile.js @@ -71,7 +71,7 @@ gulp.task('testserver', function() { }); gulp.task('develop', ['pages', 'assets', 'less'], function() { - gulp.watch(['src/{*,**/*}.less', '../scout-style/*.less'], ['less']); + gulp.watch(['src/{*,**}.less', '../scout-style/*.less'], ['less']); gulp.watch(['src/*.jade'], ['pages']); gulp.watch(['src/img/*', '../scout-style/images/*'], ['assets']); diff --git a/scout-ui/package.json b/scout-ui/package.json index e5a6213797e..64124f303bb 100644 --- a/scout-ui/package.json +++ b/scout-ui/package.json @@ -49,7 +49,6 @@ "ampersand-view-switcher": "^2.0.0", "bootstrap": "https://github.com/twbs/bootstrap/archive/v3.3.2.tar.gz", "d3": "^3.5.5", - "d3-tip": "^0.6.7", "debug": "^2.0.0", "domready": "^1.0.7", "event-stream": "^3.3.0", @@ -61,7 +60,7 @@ "lodash": "^3.8.0", "moment": "^2.8.2", "mongodb-extended-json": "^1.3.1", - "mongodb-schema": "^2.2.1", + "mongodb-schema": "git://github.com/mongodb-js/mongodb-schema.git#INT-203-arrays", "numeral": "^1.5.3", "octicons": "https://github.com/github/octicons/archive/v2.2.0.tar.gz", "phantomjs-polyfill": "0.0.1", diff --git a/scout-ui/src/collection-stats/index.js b/scout-ui/src/collection-stats/index.js index 54cab48d1c2..8e84409df22 100644 --- a/scout-ui/src/collection-stats/index.js +++ b/scout-ui/src/collection-stats/index.js @@ -3,7 +3,7 @@ var numeral = require('numeral'); var CollectionStatsView = AmpersandView.extend({ bindings: { - 'model._id': { + 'model.name': { hook: 'name' }, document_count: { diff --git a/scout-ui/src/field-list/array-field.jade b/scout-ui/src/field-list/array-field.jade deleted file mode 100644 index 731824f5362..00000000000 --- a/scout-ui/src/field-list/array-field.jade +++ /dev/null @@ -1,7 +0,0 @@ -.schema-field.schema-field-array - hr.field-divider - .schema-field-name - span.caret - span(data-hook='name') - |  [ ] - div(data-hook='fields-container') diff --git a/scout-ui/src/field-list/basic-field.jade b/scout-ui/src/field-list/basic-field.jade deleted file mode 100644 index 558f0542380..00000000000 --- a/scout-ui/src/field-list/basic-field.jade +++ /dev/null @@ -1,8 +0,0 @@ -.schema-field.schema-field-basic - hr.field-divider - .row - .col-sm-4 - .schema-field-name(data-hook='name') - div(data-hook='types-container') - .col-sm-7.col-sm-offset-1 - div(data-hook='minichart-container') diff --git a/scout-ui/src/field-list/field.jade b/scout-ui/src/field-list/field.jade new file mode 100644 index 00000000000..eff820e8c5d --- /dev/null +++ b/scout-ui/src/field-list/field.jade @@ -0,0 +1,12 @@ +.schema-field.schema-field-basic + hr.field-divider + .row + .col-sm-4 + .schema-field-name + span(data-hook='caret') + span(data-hook='name') + div(data-hook='types-subview') + .col-sm-7.col-sm-offset-1 + div(data-hook='minichart-container') + div(data-hook='fields-subview') + div(data-hook='arrayfields-subview') diff --git a/scout-ui/src/field-list/index.js b/scout-ui/src/field-list/index.js index c821a439a77..f2b2e82742f 100644 --- a/scout-ui/src/field-list/index.js +++ b/scout-ui/src/field-list/index.js @@ -3,31 +3,54 @@ var TypeListView = require('./type-list'); var MinichartView = require('../minicharts'); var FieldCollection = require('mongodb-schema').FieldCollection; var ViewSwitcher = require('ampersand-view-switcher'); +var debug = require('debug')('scout-ui:field-list:index'); +var $ = require('jquery'); var _ = require('lodash'); -var BasicFieldView = View.extend({ +function handleCaret(el, value, previousValue) { + var $el = $(el); + // only apply to own caret, not children carets + if ($el.next().text() !== this.model.name) return; + if (this.model.fields || this.model.arrayFields) { + $el.addClass('caret'); + } else { + $el.removeClass('caret'); + } +} + +var FieldView = View.extend({ props: { - minichartModel: 'state' + minichartModel: 'state', + expanded: { + type: 'boolean', + default: false + } }, bindings: { - 'model.name': [ - { - hook: 'name' - }, - { - hook: 'name', - type: function(el) { - if (this.model.getId() === '__basic__') { - el.classList.add('hidden'); - } - } - } - ] + 'model.name': { + hook: 'name' + }, + 'model.fields': { + type: handleCaret, + hook: 'caret' + }, + 'model.arrayFields': { + type: handleCaret, + hook: 'caret' + }, + 'expanded': { + type: 'booleanClass', + yes: 'expanded', + no: 'collapsed' + } }, - template: require('./basic-field.jade'), + events: { + 'click .schema-field-name': 'click', + }, + template: require('./field.jade'), subviews: { types: { - hook: 'types-container', + hook: 'types-subview', prepareView: function(el) { return new TypeListView({ el: el, @@ -35,13 +58,35 @@ var BasicFieldView = View.extend({ collection: this.model.types }); } + }, + fields: { + hook: 'fields-subview', + waitFor: 'model.fields', + prepareView: function(el) { + return new FieldListView({ + el: el, + parent: this, + collection: this.model.fields + }); + } + }, + arrayFields: { + hook: 'arrayfields-subview', + waitFor: 'model.arrayFields', + prepareView: function(el) { + return new FieldListView({ + el: el, + parent: this, + collection: this.model.arrayFields + }); + } } }, initialize: function() { var that = this; // debounce prevents excessive rendering - this.model.values.on('add', _.debounce(function(evt) { - // for now pick first type, @todo: make the type bars clickable and toggle chart + this.model.on('change:count', _.debounce(function() { + // pick first type initially that.switchView(that.model.types.at(0)); }, 300)); }, @@ -63,28 +108,6 @@ var BasicFieldView = View.extend({ model: typeModel, }); this.viewSwitcher.set(miniview); - } -}); - -var ExpandableFieldMixin = { - bindings: { - 'model.name': { - hook: 'name' - }, - 'expanded': { - type: 'booleanClass', - yes: 'expanded', - no: 'collapsed' - } - }, - events: { - 'click .schema-field-name': 'click', - }, - props: { - expanded: { - type: 'boolean', - default: false - } }, click: function(evt) { // @todo: persist state of open nodes @@ -93,27 +116,7 @@ var ExpandableFieldMixin = { evt.preventDefault(); evt.stopPropagation(); return false; - }, - subviews: { - fields: { - hook: 'fields-container', - prepareView: function(el) { - return new FieldListView({ - el: el, - parent: this, - collection: this.model.fields - }); - } - } } -}; - -var EmbeddedArrayFieldView = View.extend(ExpandableFieldMixin, { - template: require('./array-field.jade') -}); - -var EmbeddedDocumentFieldView = View.extend(ExpandableFieldMixin, { - template: require('./object-field.jade') }); var FieldListView = View.extend({ @@ -123,16 +126,7 @@ var FieldListView = View.extend({ template: require('./index.jade'), render: function() { this.renderWithTemplate(); - this.renderCollection(this.collection, function(options) { - var type = options.model.type; - if (type === 'Array') { - return new EmbeddedArrayFieldView(options); - } - if (type === 'Object') { - return new EmbeddedDocumentFieldView(options); - } - return new BasicFieldView(options); - }, this.queryByHook('fields')); + this.renderCollection(this.collection, FieldView, this.queryByHook('fields')); } }); diff --git a/scout-ui/src/field-list/object-field.jade b/scout-ui/src/field-list/object-field.jade deleted file mode 100644 index b7c2058b99a..00000000000 --- a/scout-ui/src/field-list/object-field.jade +++ /dev/null @@ -1,7 +0,0 @@ -.schema-field.schema-field-object - hr.field-divider - .schema-field-name - span.caret - span(data-hook='name') - - div(data-hook='fields-container') diff --git a/scout-ui/src/field-list/type-list-item.js b/scout-ui/src/field-list/type-list-item.js index 3641d00fcae..67ed92fe500 100644 --- a/scout-ui/src/field-list/type-list-item.js +++ b/scout-ui/src/field-list/type-list-item.js @@ -42,7 +42,7 @@ module.exports = AmpersandView.extend({ } }, initialize: function() { - this.listenTo(this.model, 'change:probability', _.debounce(function() { + this.listenTo(this.model, 'change:count', _.debounce(function() { $(this.el).tooltip({ title: format('%s (%s)', this.model.getId(), numeral(this.model.probability).format('%')) }); @@ -50,8 +50,9 @@ module.exports = AmpersandView.extend({ }, template: require('./type-list-item.jade'), typeClicked: function() { - if (this.parent.parent.minichartModel.cid !== this.model.cid) { - this.parent.parent.switchView(this.model); + var fieldList = this.parent.parent; + if (!fieldList.minichartModel || (fieldList.minichartModel.modelType !== this.model.modelType)) { + fieldList.switchView(this.model); } } diff --git a/scout-ui/src/field-list/type-list.js b/scout-ui/src/field-list/type-list.js index e990d753274..4ab78505d6e 100644 --- a/scout-ui/src/field-list/type-list.js +++ b/scout-ui/src/field-list/type-list.js @@ -5,6 +5,6 @@ module.exports = AmpersandView.extend({ template: require('./type-list.jade'), render: function() { this.renderWithTemplate({}); - this.renderCollection(this.collection, TypeListItem, this.queryByHook('types')); + this.renderCollection(this.collection.sort(), TypeListItem, this.queryByHook('types')); } }); diff --git a/scout-ui/src/field-list/value-list-item.jade b/scout-ui/src/field-list/value-list-item.jade deleted file mode 100644 index 674d6c5badb..00000000000 --- a/scout-ui/src/field-list/value-list-item.jade +++ /dev/null @@ -1 +0,0 @@ -li.schema-value-list-item(data-hook='value') diff --git a/scout-ui/src/field-list/value-list-item.js b/scout-ui/src/field-list/value-list-item.js deleted file mode 100644 index f64c2de9362..00000000000 --- a/scout-ui/src/field-list/value-list-item.js +++ /dev/null @@ -1,10 +0,0 @@ -var AmpersandView = require('ampersand-view'); - -module.exports = AmpersandView.extend({ - bindings: { - 'model.value': { - hook: 'value' - } - }, - template: require('./value-list-item.jade') -}); diff --git a/scout-ui/src/field-list/value-list.jade b/scout-ui/src/field-list/value-list.jade deleted file mode 100644 index 32f9be4ea4f..00000000000 --- a/scout-ui/src/field-list/value-list.jade +++ /dev/null @@ -1 +0,0 @@ -ul.schema-value-list(data-hook='values') diff --git a/scout-ui/src/field-list/value-list.js b/scout-ui/src/field-list/value-list.js deleted file mode 100644 index f3da889b4ac..00000000000 --- a/scout-ui/src/field-list/value-list.js +++ /dev/null @@ -1,10 +0,0 @@ -var AmpersandView = require('ampersand-view'); -var ValueListItem = require('./value-list-item'); - -module.exports = AmpersandView.extend({ - template: require('./value-list.jade'), - render: function() { - this.renderWithTemplate({}); - this.renderCollection(this.collection, ValueListItem, this.queryByHook('values')); - } -}); diff --git a/scout-ui/src/home/collection.js b/scout-ui/src/home/collection.js index 995a827a0d3..accedf18448 100644 --- a/scout-ui/src/home/collection.js +++ b/scout-ui/src/home/collection.js @@ -5,7 +5,6 @@ var CollectionStatsView = require('../collection-stats'); var FieldListView = require('../field-list'); var DocumentListView = require('../document-view'); var RefineBarView = require('../refine-view'); - var debug = require('debug')('scout-ui:home:collection'); var $ = require('jquery'); @@ -32,7 +31,7 @@ module.exports = AmpersandView.extend({ 'click .splitter': 'onSplitterClick', }, bindings: { - 'model._id': { + 'model.name': { hook: 'name' }, 'sidebarWidth': { diff --git a/scout-ui/src/home/index.js b/scout-ui/src/home/index.js index eb61e990dba..ba429b7a5d8 100644 --- a/scout-ui/src/home/index.js +++ b/scout-ui/src/home/index.js @@ -5,7 +5,6 @@ var debug = require('debug')('scout-ui:home'); var app = require('ampersand-app'); var format = require('util').format; var SidebarView = require('../sidebar'); -var FieldListView = require('../field-list'); var CollectionView = require('./collection'); require('bootstrap/js/dropdown'); diff --git a/scout-ui/src/minicharts/d3-tip.js b/scout-ui/src/minicharts/d3-tip.js new file mode 100644 index 00000000000..bd48c4c7105 --- /dev/null +++ b/scout-ui/src/minicharts/d3-tip.js @@ -0,0 +1,326 @@ +// d3.tip +// Copyright (c) 2013 Justin Palmer +// +// Tooltips for d3.js SVG visualizations + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module with d3 as a dependency. + define(['d3'], factory) + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = function(d3) { + d3.tip = factory(d3) + return d3.tip + } + } else { + // Browser global. + root.d3.tip = factory(root.d3) + } +}(this, function (d3) { + + // Public - contructs a new tooltip + // + // Returns a tip + return function() { + var direction = d3_tip_direction, + offset = d3_tip_offset, + html = d3_tip_html, + node = initNode(), + svg = null, + point = null, + target = null + + function tip(vis) { + svg = getSVGNode(vis) + if (!svg) return; + point = svg.createSVGPoint() + document.body.appendChild(node) + } + + // Public - show the tooltip on the screen + // + // Returns a tip + tip.show = function() { + var args = Array.prototype.slice.call(arguments) + if(args[args.length - 1] instanceof SVGElement) target = args.pop() + + var content = html.apply(this, args), + poffset = offset.apply(this, args), + dir = direction.apply(this, args), + nodel = getNodeEl(), + i = directions.length, + coords, + scrollTop = document.documentElement.scrollTop || document.body.scrollTop, + scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft + + nodel.html(content) + .style({ opacity: 1, 'pointer-events': 'all' }) + + while(i--) nodel.classed(directions[i], false) + coords = direction_callbacks.get(dir).apply(this) + nodel.classed(dir, true).style({ + top: (coords.top + poffset[0]) + scrollTop + 'px', + left: (coords.left + poffset[1]) + scrollLeft + 'px' + }) + + return tip + } + + // Public - hide the tooltip + // + // Returns a tip + tip.hide = function() { + var nodel = getNodeEl() + nodel.style({ opacity: 0, 'pointer-events': 'none' }) + return tip + } + + // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. + // + // n - name of the attribute + // v - value of the attribute + // + // Returns tip or attribute value + tip.attr = function(n, v) { + if (arguments.length < 2 && typeof n === 'string') { + return getNodeEl().attr(n) + } else { + var args = Array.prototype.slice.call(arguments) + d3.selection.prototype.attr.apply(getNodeEl(), args) + } + + return tip + } + + // Public: Proxy style calls to the d3 tip container. Sets or gets a style value. + // + // n - name of the property + // v - value of the property + // + // Returns tip or style property value + tip.style = function(n, v) { + if (arguments.length < 2 && typeof n === 'string') { + return getNodeEl().style(n) + } else { + var args = Array.prototype.slice.call(arguments) + d3.selection.prototype.style.apply(getNodeEl(), args) + } + + return tip + } + + // Public: Set or get the direction of the tooltip + // + // v - One of n(north), s(south), e(east), or w(west), nw(northwest), + // sw(southwest), ne(northeast) or se(southeast) + // + // Returns tip or direction + tip.direction = function(v) { + if (!arguments.length) return direction + direction = v == null ? v : d3.functor(v) + + return tip + } + + // Public: Sets or gets the offset of the tip + // + // v - Array of [x, y] offset + // + // Returns offset or + tip.offset = function(v) { + if (!arguments.length) return offset + offset = v == null ? v : d3.functor(v) + + return tip + } + + // Public: sets or gets the html value of the tooltip + // + // v - String value of the tip + // + // Returns html value or tip + tip.html = function(v) { + if (!arguments.length) return html + html = v == null ? v : d3.functor(v) + + return tip + } + + // Public: destroys the tooltip and removes it from the DOM + // + // Returns a tip + tip.destroy = function() { + if(node) { + getNodeEl().remove(); + node = null; + } + return tip; + } + + function d3_tip_direction() { return 'n' } + function d3_tip_offset() { return [0, 0] } + function d3_tip_html() { return ' ' } + + var direction_callbacks = d3.map({ + n: direction_n, + s: direction_s, + e: direction_e, + w: direction_w, + nw: direction_nw, + ne: direction_ne, + sw: direction_sw, + se: direction_se + }), + + directions = direction_callbacks.keys() + + function direction_n() { + var bbox = getScreenBBox() + return { + top: bbox.n.y - node.offsetHeight, + left: bbox.n.x - node.offsetWidth / 2 + } + } + + function direction_s() { + var bbox = getScreenBBox() + return { + top: bbox.s.y, + left: bbox.s.x - node.offsetWidth / 2 + } + } + + function direction_e() { + var bbox = getScreenBBox() + return { + top: bbox.e.y - node.offsetHeight / 2, + left: bbox.e.x + } + } + + function direction_w() { + var bbox = getScreenBBox() + return { + top: bbox.w.y - node.offsetHeight / 2, + left: bbox.w.x - node.offsetWidth + } + } + + function direction_nw() { + var bbox = getScreenBBox() + return { + top: bbox.nw.y - node.offsetHeight, + left: bbox.nw.x - node.offsetWidth + } + } + + function direction_ne() { + var bbox = getScreenBBox() + return { + top: bbox.ne.y - node.offsetHeight, + left: bbox.ne.x + } + } + + function direction_sw() { + var bbox = getScreenBBox() + return { + top: bbox.sw.y, + left: bbox.sw.x - node.offsetWidth + } + } + + function direction_se() { + var bbox = getScreenBBox() + return { + top: bbox.se.y, + left: bbox.e.x + } + } + + function initNode() { + var node = d3.select(document.createElement('div')) + node.style({ + position: 'absolute', + top: 0, + opacity: 0, + 'pointer-events': 'none', + 'box-sizing': 'border-box' + }) + + return node.node() + } + + function getSVGNode(el) { + el = el.node() + if (!el) return; + if(el.tagName.toLowerCase() === 'svg') + return el + + return el.ownerSVGElement + } + + function getNodeEl() { + if(node === null) { + node = initNode(); + // re-add node to DOM + document.body.appendChild(node); + }; + return d3.select(node); + } + + // Private - gets the screen coordinates of a shape + // + // Given a shape on the screen, will return an SVGPoint for the directions + // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), + // sw(southwest). + // + // +-+-+ + // | | + // + + + // | | + // +-+-+ + // + // Returns an Object {n, s, e, w, nw, sw, ne, se} + function getScreenBBox() { + var targetel = target || d3.event.target; + + while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) { + targetel = targetel.parentNode; + } + + var bbox = {}, + matrix = targetel.getScreenCTM(), + tbbox = targetel.getBBox(), + width = tbbox.width, + height = tbbox.height, + x = tbbox.x, + y = tbbox.y + + point.x = x + point.y = y + bbox.nw = point.matrixTransform(matrix) + point.x += width + bbox.ne = point.matrixTransform(matrix) + point.y += height + bbox.se = point.matrixTransform(matrix) + point.x -= width + bbox.sw = point.matrixTransform(matrix) + point.y -= height / 2 + bbox.w = point.matrixTransform(matrix) + point.x += width + bbox.e = point.matrixTransform(matrix) + point.x -= width / 2 + point.y -= height / 2 + bbox.n = point.matrixTransform(matrix) + point.y += height + bbox.s = point.matrixTransform(matrix) + + return bbox + } + + return tip + }; + +})); diff --git a/scout-ui/src/minicharts/d3fns/date.js b/scout-ui/src/minicharts/d3fns/date.js index e0c3007a12c..ccf437f299a 100644 --- a/scout-ui/src/minicharts/d3fns/date.js +++ b/scout-ui/src/minicharts/d3fns/date.js @@ -5,7 +5,7 @@ var shared = require('./shared'); var many = require('./many'); var debug = require('debug')('scout-ui:minicharts:date'); -require('d3-tip')(d3); +require('../d3-tip')(d3); function generateDefaults(n) { var doc = {}; @@ -180,4 +180,3 @@ module.exports = function(opts) { .text('\uf017'); }; - diff --git a/scout-ui/src/minicharts/d3fns/few.js b/scout-ui/src/minicharts/d3fns/few.js index 0a3e8b8aedb..ee371ad200c 100644 --- a/scout-ui/src/minicharts/d3fns/few.js +++ b/scout-ui/src/minicharts/d3fns/few.js @@ -4,7 +4,7 @@ var tooltipHtml = require('./tooltip.jade'); var shared = require('./shared'); var debug = require('debug')('scout-ui:minicharts:few'); -require('d3-tip')(d3); +require('../d3-tip')(d3); module.exports = function(data, g, width, height, options) { diff --git a/scout-ui/src/minicharts/d3fns/many.js b/scout-ui/src/minicharts/d3fns/many.js index 01604ef5653..ef5300ef736 100644 --- a/scout-ui/src/minicharts/d3fns/many.js +++ b/scout-ui/src/minicharts/d3fns/many.js @@ -4,7 +4,7 @@ var tooltipHtml = require('./tooltip.jade'); var shared = require('./shared'); var debug = require('debug')('scout-ui:minicharts:many'); -require('d3-tip')(d3); +require('../d3-tip')(d3); module.exports = function(data, g, width, height, options) { diff --git a/scout-ui/src/minicharts/index.js b/scout-ui/src/minicharts/index.js index bc9bd8e054e..6ebfc47f9ba 100644 --- a/scout-ui/src/minicharts/index.js +++ b/scout-ui/src/minicharts/index.js @@ -25,11 +25,9 @@ module.exports = AmpersandView.extend({ }, render: function() { this.renderWithTemplate(this); - // unique values get a div-based minichart - if ((this.model._id === 'String') && - (this.model.unique === this.model.count)) { - + if ((['String', 'Number'].indexOf(this.model.name) !== -1) && + (this.model.unique === this.model.count)) { this.viewOptions.renderMode = 'html'; this.viewOptions.className = 'minichart unique'; this.subview = new UniqueMinichartView(this.viewOptions); diff --git a/scout-ui/src/minicharts/unique.js b/scout-ui/src/minicharts/unique.js index b79c6d925c1..74380a0c979 100644 --- a/scout-ui/src/minicharts/unique.js +++ b/scout-ui/src/minicharts/unique.js @@ -12,11 +12,9 @@ module.exports = VizView.extend({ template: require('./unique.jade'), derived: { randomValues: { - deps: ['orderedValues'], cache: false, fn: function() { - // @hack for demo: show values across all types - return _(this.model.collection.parent.values.sample(15)) + return _(this.model.values.sample(15)) .map(function(x) { return x.value; })