diff --git a/scout-style/10strap.less b/scout-style/10strap.less index 2ff784eccb0..4c6d3cc9948 100644 --- a/scout-style/10strap.less +++ b/scout-style/10strap.less @@ -54,7 +54,8 @@ hr.inverse { } code { - color: @purple2; + font-family: Menlo, monospace; + color: @gray1; background-color: @gray8; } diff --git a/scout-ui/src/minicharts/d3fns/boolean.js b/scout-ui/src/minicharts/d3fns/boolean.js index 75188e2cd50..fb4865ae9c0 100644 --- a/scout-ui/src/minicharts/d3fns/boolean.js +++ b/scout-ui/src/minicharts/d3fns/boolean.js @@ -1,6 +1,7 @@ var d3 = require('d3'); var _ = require('lodash'); var few = require('./few'); +var shared = require('./shared'); var debug = require('debug')('scout-ui:minicharts:boolean'); module.exports = function(opts) { @@ -8,27 +9,24 @@ module.exports = function(opts) { // group by true/false var data = _(values) - .groupBy(function(d) { - // extract string representations of values - return d; - }) - .defaults({false: [], true: []}) - .map(function(v, k) { - return { - x: k, - y: v.length, - tooltip: k - }; - }) - .sortByOrder('x', [false]) // descending on y - .value(); - - var margin = { - top: 10, - right: 0, - bottom: 10, - left: 0 - }; + .groupBy(function(d) { + // extract string representations of values + return d; + }) + .defaults({ + false: [], + true: [] + }) + .map(function(v, k) { + return { + label: k, + value: v.length + }; + }) + .sortByOrder('label', [false]) // order: false, true + .value(); + + var margin = shared.margin; var width = opts.width - margin.left - margin.right; var height = opts.height - margin.top - margin.bottom; diff --git a/scout-ui/src/minicharts/d3fns/date.js b/scout-ui/src/minicharts/d3fns/date.js index 921f2746380..3e6235bc0c9 100644 --- a/scout-ui/src/minicharts/d3fns/date.js +++ b/scout-ui/src/minicharts/d3fns/date.js @@ -1,12 +1,17 @@ var d3 = require('d3'); var _ = require('lodash'); var moment = require('moment'); +var shared = require('./shared'); var debug = require('debug')('scout-ui:minicharts:date'); var many = require('./many'); +require('d3-tip')(d3); + function generateDefaults(n) { var doc = {}; - _.each(_.range(n), function (d) { doc[d] = 0; }); + _.each(_.range(n), function(d) { + doc[d] = 0; + }); return doc; } @@ -17,7 +22,7 @@ module.exports = function(opts) { // distinguish ObjectIDs from real dates if (values.length && values[0]._bsontype !== undefined) { if (values[0]._bsontype === 'ObjectID') { - values = _.map(values, function (v) { + values = _.map(values, function(v) { return v.getTimestamp(); }); } @@ -26,13 +31,7 @@ module.exports = function(opts) { // A formatter for dates var format = d3.time.format('%Y-%m-%d %H:%M:%S'); - var margin = { - top: 10, - right: 0, - bottom: 10, - left: 0 - }; - + var margin = shared.margin; var width = opts.width - margin.left - margin.right; var height = opts.height - margin.top - margin.bottom; var el = opts.el; @@ -45,20 +44,20 @@ module.exports = function(opts) { .range([0, width]); var upperBarBottom = height / 2 - 20; - var upperRatio = 2; + var upperRatio = 2.5; var upperMargin = 15; // group by weekdays - var weekdayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + var weekdayLabels = moment.weekdays(); var weekdays = _(values) - .groupBy(function (d) { + .groupBy(function(d) { return moment(d).weekday(); }) .defaults(generateDefaults(7)) - .map(function (d, i) { + .map(function(d, i) { return { - x: weekdayLabels[i], - y: d.length, + label: weekdayLabels[i], + value: d.length, tooltip: weekdayLabels[i] }; }) @@ -67,15 +66,15 @@ module.exports = function(opts) { // group by hours var hourLabels = d3.range(24); var hours = _(values) - .groupBy(function (d) { + .groupBy(function(d) { return d.getHours(); }) - .defaults(generateDefaults(23)) - .map(function (d, i) { + .defaults(generateDefaults(24)) + .map(function(d, i) { return { - x: hourLabels[i], - y: d.length, - tooltip: hourLabels[i] + 'h' + label: hourLabels[i] + ':00', + value: d.length, + tooltip: hourLabels[i] + ':00' }; }) .value(); @@ -83,9 +82,19 @@ module.exports = function(opts) { // clear element first d3.select(el).selectAll('*').remove(); + // set up tooltips + var tip = d3.tip() + .attr('class', 'd3-tip') + .html(function(d, i) { + return format(d); + }) + .direction('n') + .offset([-9, 0]); + var svg = d3.select(el) .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + .call(tip); var line = svg.selectAll('.line') .data(values) @@ -98,7 +107,9 @@ module.exports = function(opts) { .attr('x2', function(d) { return barcodeX(d); }) - .attr('y2', barcodeBottom); + .attr('y2', barcodeBottom) + .on('mouseover', tip.show) + .on('mouseout', tip.hide); var text = svg.selectAll('.text') .data(barcodeX.domain()) @@ -112,24 +123,36 @@ module.exports = function(opts) { .attr('text-anchor', function(d, i) { return i ? 'end' : 'start'; }) - .text(function(d) { - return format(d); + .text(function(d, i) { + if (format(barcodeX.domain()[0]) === format(barcodeX.domain()[1])) { + if (i === 0) { + return 'inserted: ' + format(d); + } + } else { + return (i ? 'last: ' : 'first: ') + format(d); + } }); var weekdayContainer = svg.append('g'); - many(weekdays, weekdayContainer, width / (upperRatio+1) - upperMargin, upperBarBottom, { - 'text-anchor': 'middle', - 'text': function (d) { - return d.x[0]; + many(weekdays, weekdayContainer, width / (upperRatio + 1) - upperMargin, upperBarBottom, { + bgbars: true, + labels: { + 'text-anchor': 'middle', + 'text': function(d) { + return d.label[0]; + } } }); var hourContainer = svg.append('g') - .attr('transform', 'translate(' + (width/(upperRatio+1) + upperMargin) + ', 0)'); - - many(hours, hourContainer, width/(upperRatio+1)*upperRatio - upperMargin, upperBarBottom, { - 'text': function (d, i) { - return (i % 6 === 0 || i === 23) ? d.x : ''; + .attr('transform', 'translate(' + (width / (upperRatio + 1) + upperMargin) + ', 0)'); + + many(hours, hourContainer, width / (upperRatio + 1) * upperRatio - upperMargin, upperBarBottom, { + bgbars: true, + labels: { + 'text': function(d, i) { + return (i % 6 === 0 || i === 23) ? d.label : ''; + } } }); diff --git a/scout-ui/src/minicharts/d3fns/few.js b/scout-ui/src/minicharts/d3fns/few.js index 464e40d956d..78f8a6fec60 100644 --- a/scout-ui/src/minicharts/d3fns/few.js +++ b/scout-ui/src/minicharts/d3fns/few.js @@ -1,27 +1,32 @@ var d3 = require('d3'); var _ = require('lodash'); +var tooltipHtml = require('./tooltip.jade'); var debug = require('debug')('scout-ui:minicharts:few'); require('d3-tip')(d3); -module.exports = function (data, g, width, height) { - // @todo make barOffset equal to longest label - var barOffset = width / 4; +module.exports = function(data, g, width, height, options) { + + var barHeight = 25; + var values = _.pluck(data, 'value'); + var sumValues = d3.sum(values); // data.x is still the label, and data.y the length of the bar var x = d3.scale.linear() - .domain([0, d3.max(_.pluck(data, 'y'))]) - .range([0, width - barOffset]); - - var y = d3.scale.ordinal() - .domain(_.pluck(data, 'x')) - .rangeBands([0, height], 0.3, 0.0); + .domain([0, sumValues]) + .range([0, width]); // set up tooltips var tip = d3.tip() .attr('class', 'd3-tip') - .html(function(d) { - return d.tooltip || d.x; + .html(function(d, i) { + if (typeof d.tooltip === 'function') { + return d.tooltip(d, i); + } + return d.tooltip || tooltipHtml({ + label: d.label, + value: Math.round(d.value / sumValues * 100) + }); }) .direction('n') .offset([-9, 0]); @@ -33,42 +38,36 @@ module.exports = function (data, g, width, height) { var bar = g.selectAll('.bar') .data(data) .enter().append('g') - .attr('class', 'bar') - .attr('transform', function(d) { - return 'translate(0, ' + y(d.x) + ')'; - }); + .attr('class', 'bar few') + .attr('transform', function(d, i) { - bar.append('rect') - .attr('class', 'bg') - .attr('x', barOffset) - .attr('width', width - barOffset) - .attr('height', y.rangeBand()); + var xpos = _.sum(_(data) + .slice(0, i) + .pluck('value') + .value() + ); + return 'translate(' + x(xpos) + ', ' + (height - barHeight) / 2 + ')'; + }); bar.append('rect') .attr('class', 'fg') .attr('y', 0) - .attr('x', barOffset) + .attr('x', 0) .attr('width', function(d) { - return x(d.y); + return x(d.value); }) - .attr('height', y.rangeBand()); - - bar.append('rect') - .attr('class', 'glass') - .attr('x', barOffset) - .attr('width', width - barOffset) - .attr('height', y.rangeBand()) + .attr('height', barHeight) .on('mouseover', tip.show) .on('mouseout', tip.hide); bar.append('text') - .attr('dx', '-10') - .attr('dy', '0.4em') - .attr('y', y.rangeBand() / 2) - .attr('x', barOffset) - .attr('text-anchor', 'end') + .attr('y', barHeight / 2) + .attr('dy', '0.3em') + .attr('dx', 10) + .attr('text-anchor', 'start') .text(function(d) { - return d.x; - }); + return d.label; + }) + .attr('fill', 'white'); }; diff --git a/scout-ui/src/minicharts/d3fns/many.js b/scout-ui/src/minicharts/d3fns/many.js index 095356b3ce6..d382772bc5d 100644 --- a/scout-ui/src/minicharts/d3fns/many.js +++ b/scout-ui/src/minicharts/d3fns/many.js @@ -1,19 +1,37 @@ var d3 = require('d3'); var _ = require('lodash'); +var tooltipHtml = require('./tooltip.jade'); var debug = require('debug')('scout-ui:minicharts:many'); require('d3-tip')(d3); -module.exports = function (data, g, width, height, labels) { +module.exports = function(data, g, width, height, options) { + + // if legend present, save some space + var scaleWidth = 40; + + options = _.defaults(options || {}, { + bgbars: false, + scale: false, + labels: false // label defaults will be set further below + }); + +// if (options.scale) { +// width = width - scaleWidth; +// } var x = d3.scale.ordinal() - .domain(_.pluck(data, 'x')) + .domain(_.pluck(data, 'label')) .rangeBands([0, width], 0.3, 0.0); + var values = _.pluck(data, 'value'); + var y = d3.scale.linear() - .domain([0, d3.max(_.pluck(data, 'y'))]) + .domain([0, d3.max(values)]) .range([height, 0]); + var sumY = d3.sum(values); + // set up tooltips var tip = d3.tip() .attr('class', 'd3-tip') @@ -21,7 +39,10 @@ module.exports = function (data, g, width, height, labels) { if (typeof d.tooltip === 'function') { return d.tooltip(d, i); } - return d.tooltip || d.x; + return d.tooltip || tooltipHtml({ + label: d.label, + value: d.value + }); }) .direction('n') .offset([-9, 0]); @@ -30,63 +51,131 @@ module.exports = function (data, g, width, height, labels) { g.selectAll('*').remove(); g.call(tip); + if (options.scale) { + var maxVal = d3.max(y.domain()); + var format = d3.format('%.1f'); + var legendValues = [format(maxVal), format(maxVal / 2)]; + + // @todo use a scale and wrap both text and line in g element + var legend = g.append('g') + .attr('class', 'legend'); + + legend.append('text') + .attr('class', 'legend') + .attr('x', width) + .attr('dx', '1em') + .attr('y', 0) + .attr('dy', '0.3em') + .attr('text-anchor', 'start') + .text(d3.max(y.domain()) + '%'); + + legend.append('text') + .attr('class', 'legend') + .attr('x', width) + .attr('dx', '1em') + .attr('y', height / 2) + .attr('dy', '0.3em') + .attr('text-anchor', 'start') + .text(d3.max(y.domain()) / 2 + '%'); + + legend.append('text') + .attr('class', 'legend') + .attr('x', width) + .attr('dx', '1em') + .attr('y', height) + .attr('dy', '0.3em') + .attr('text-anchor', 'start') + .text('0%'); + + legend.append('line') + .attr('class', 'bg legend') + .attr('x1', 0) + .attr('x2', width + 5) + .attr('y1', 0) + .attr('y2', 0); + + legend.append('line') + .attr('class', 'bg legend') + .attr('x1', 0) + .attr('x2', width + 5) + .attr('y1', height / 2) + .attr('y2', height / 2); + + legend.append('line') + .attr('class', 'bg legend') + .attr('x1', 0) + .attr('x2', width + 5) + .attr('y1', height) + .attr('y2', height); + } + + var bar = g.selectAll('.bar') .data(data) .enter().append('g') .attr('class', 'bar') .attr('transform', function(d) { - return 'translate(' + x(d.x) + ', 0)'; + return 'translate(' + x(d.label) + ', 0)'; }); - bar.append('rect') - .attr('class', 'bg') - .attr('width', x.rangeBand()) - .attr('height', height); + if (options.bgbars) { + bar.append('rect') + .attr('class', 'bg') + .attr('width', x.rangeBand()) + .attr('height', height); + } - bar.append('rect') + var fgbars = bar.append('rect') .attr('class', 'fg') .attr('x', 0) .attr('y', function(d) { - return y(d.y); + return y(d.value); }) .attr('width', x.rangeBand()) .attr('height', function(d) { - return height - y(d.y); + return height - y(d.value); }); + if (options.bgbars) { bar.append('rect') .attr('class', 'glass') .attr('width', x.rangeBand()) .attr('height', height) .on('mouseover', tip.show) .on('mouseout', tip.hide); + } else { + // atach tooltips directly to foreground bars + fgbars + .on('mouseover', tip.show) + .on('mouseout', tip.hide); + } + + if (options.labels) { + var labels = options.labels; + _.defaults(labels, { + 'x': labels['text-anchor'] === 'middle' ? x.rangeBand() / 2 : function(d, i) { + if (i === 0) return 0; + if (i === data.length - 1) return x.rangeBand(); + return x.rangeBand() / 2; + }, + 'y': height + 5, + 'dy': '0.75em', + 'text-anchor': function(d, i) { + if (i === 0) return 'start'; + if (i === data.length - 1) return 'end'; + return 'middle'; + }, + 'text': function(d) { + return d.value; + } + }); - if (labels) { - - _.defaults(labels, { - 'x': labels['text-anchor'] === 'middle' ? x.rangeBand() / 2 : function (d, i) { - if (i === 0) return 0; - if (i === data.length - 1) return x.rangeBand(); - return x.rangeBand() / 2; - }, - 'y': height+5, - 'dy': '0.75em', - 'text-anchor': function (d, i) { - if (i === 0) return 'start'; - if (i === data.length - 1) return 'end'; - return 'middle'; - }, - 'text': function (d) { - return d.x; - } - }); - - bar.append('text') + bar.append('text') .attr('x', labels.x) .attr('dx', labels.dx) .attr('y', labels.y) .attr('dy', labels.dy) .attr('text-anchor', labels['text-anchor']) .text(labels.text); - } + } }; diff --git a/scout-ui/src/minicharts/d3fns/number.js b/scout-ui/src/minicharts/d3fns/number.js index 7241a084dde..3acad57a3c8 100644 --- a/scout-ui/src/minicharts/d3fns/number.js +++ b/scout-ui/src/minicharts/d3fns/number.js @@ -1,18 +1,14 @@ var d3 = require('d3'); var _ = require('lodash'); var many = require('./many'); +var shared = require('./shared'); +var tooltipHtml = require('./tooltip.jade'); var debug = require('debug')('scout-ui:minicharts:number'); module.exports = function(opts) { var values = opts.data.values.toJSON(); - var margin = { - top: 10, - right: 0, - bottom: 10, - left: 0 - }; - + var margin = shared.margin; var width = opts.width - margin.left - margin.right; var height = opts.height - margin.top - margin.bottom; var el = opts.el; @@ -28,15 +24,24 @@ module.exports = function(opts) { .bins(ticks); var data = hist(values); - _.each(data, function (d, i) { + var sumY = d3.sum(_.pluck(data, 'y')); + + _.each(data, function(d, i) { + var label; if (i === 0) { - d.tooltip = '< ' + (d.x+d.dx); - } - else if (i === data.length - 1) { - d.tooltip = '≥ ' + d.x; + label = '< ' + (d.x + d.dx); + } else if (i === data.length - 1) { + label = '≥ ' + d.x; } else { - d.tooltip = d.x + '-' + (d.x+d.dx); + label = d.x + '-' + (d.x + d.dx); } + // remapping keys to conform with all other types + d.value = d.y; + d.label = label; + d.tooltip = tooltipHtml({ + label: label, + value: Math.round(d.y / sumY * 100) + }); }); // clear element first @@ -46,11 +51,15 @@ module.exports = function(opts) { .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - many(data, g, width, height-10, { - text: function (d, i) { - if (i === 0) return d3.min(values); - if (i === data.length - 1) return d3.max(values); - return ''; + many(data, g, width, height - 10, { + scale: true, + bgbars: false, + labels: { + text: function(d, i) { + if (i === 0) return 'min: ' + d3.min(values); + if (i === data.length - 1) return 'max: ' + d3.max(values); + return ''; + } } }); }; diff --git a/scout-ui/src/minicharts/d3fns/shared.js b/scout-ui/src/minicharts/d3fns/shared.js new file mode 100644 index 00000000000..d335d3a8e6c --- /dev/null +++ b/scout-ui/src/minicharts/d3fns/shared.js @@ -0,0 +1,10 @@ +module.exports = { + + margin: { + top: 10, + right: 40, + bottom: 10, + left: 0 + }, + +}; diff --git a/scout-ui/src/minicharts/d3fns/string.js b/scout-ui/src/minicharts/d3fns/string.js index 60e19d46b20..56b349abf97 100644 --- a/scout-ui/src/minicharts/d3fns/string.js +++ b/scout-ui/src/minicharts/d3fns/string.js @@ -3,34 +3,28 @@ var _ = require('lodash'); var debug = require('debug')('scout-ui:minicharts:string'); var few = require('./few'); var many = require('./many'); +var shared = require('./shared'); module.exports = function(opts) { var values = opts.data.values.toJSON(); - var margin = { - top: 10, - right: 0, - bottom: 10, - left: 0 - }; - + var margin = shared.margin; var width = opts.width - margin.left - margin.right; var height = opts.height - margin.top - margin.bottom; var el = opts.el; - // group into categories (x) and count (y) the values per bucket, sort descending + // group into labels and values per bucket, sort descending var data = _(values) .groupBy(function(d) { return d; }) .map(function(v, k) { return { - x: k, - y: v.length, - tooltip: k + label: k, + value: v.length }; }) - .sortByOrder('y', [false]) // descending on y + .sortByOrder('value', [false]) // descending on value .value(); // clear element first @@ -43,6 +37,9 @@ module.exports = function(opts) { .attr('height', height); var chart = data.length <= 5 ? few : many; - chart(data, g, width, height); + chart(data, g, width, height, { + scale: true, + bglines: true + }); }; diff --git a/scout-ui/src/minicharts/d3fns/tooltip.jade b/scout-ui/src/minicharts/d3fns/tooltip.jade new file mode 100644 index 00000000000..093970460f9 --- /dev/null +++ b/scout-ui/src/minicharts/d3fns/tooltip.jade @@ -0,0 +1,3 @@ +.tooltip-wrapper + .tooltip-label!= label + .tooltip-value #{value}% diff --git a/scout-ui/src/minicharts/index.js b/scout-ui/src/minicharts/index.js index e1790676a45..72f5fdb4300 100644 --- a/scout-ui/src/minicharts/index.js +++ b/scout-ui/src/minicharts/index.js @@ -20,17 +20,18 @@ module.exports = AmpersandView.extend({ renderMode: 'svg', className: 'minichart', debounceRender: false, - vizFn: vizFns[opts.model._id.toLowerCase()] || null + vizFn: vizFns[opts.model.getId().toLowerCase()] || null }); }, render: function() { - this.renderWithTemplate(); + this.renderWithTemplate(this); // unique values get a div-based minichart if ((this.model._id === 'String') && (_.uniq(this.model.values.toJSON()).length === this.model.count)) { this.viewOptions.renderMode = 'html'; + this.viewOptions.className = 'minichart unique'; this.subview = new UniqueMinichartView(this.viewOptions); } else { this.subview = new VizView(this.viewOptions); diff --git a/scout-ui/src/minicharts/index.less b/scout-ui/src/minicharts/index.less index 923efe9670e..5cc01696ce3 100644 --- a/scout-ui/src/minicharts/index.less +++ b/scout-ui/src/minicharts/index.less @@ -1,7 +1,7 @@ // minicharts -@mc-bar-bg: #F2F4F5; -@mc-bar-fg: #38A1DF; -@mc-bar-hl: #BEDFF0; +@mc-bg: #F2F4F5; +@mc-fg: #38A1DF; +@mc-hl: #BEDFF0; div.minichart.unique { font-size: 12px; @@ -25,19 +25,37 @@ div.minichart.unique { color: @gray1; } } + i.mms-icon-continuous { // remove after wrapping this in an again + color: @gray5; + cursor: pointer; + + &:hover { + color: @gray1; + } + } } dd { margin-left: 30px; overflow: hidden; + + ul li { + margin-bottom: 6px; + + code { + font-size: 12px; + line-height: 20px; + } + } } } } svg.minichart { - font: 10px sans-serif; + font-size: 10px; text { - fill: #000; + fill: @gray4; + font-weight: bold; } .glass { @@ -48,20 +66,42 @@ svg.minichart { shape-rendering: crispEdges; rect.bg { - fill: @mc-bar-bg; + fill: @mc-bg; } rect.fg { - fill: @mc-bar-fg; + fill: @mc-fg; } rect.hl { - fill: @mc-bar-hl; + fill: @mc-hl; + } + + &.few { + rect.fg { + stroke: white; + stroke-width: 2px; + } + + text { + fill: white; + font-size: 12px; + } } } .line { - stroke: @mc-bar-fg; + stroke: @mc-fg; + } + + .legend { + text { + fill: @gray5; + } + + line { + stroke: @gray7; + } shape-rendering: crispEdges; } @@ -72,6 +112,10 @@ svg.minichart { } } +.tooltip-wrapper { + line-height: 120%; +} + // -- d3-tip styling .d3-tip { line-height: 1; diff --git a/scout-ui/src/minicharts/minichart.jade b/scout-ui/src/minicharts/minichart.jade index 87400273c16..059be82372b 100644 --- a/scout-ui/src/minicharts/minichart.jade +++ b/scout-ui/src/minicharts/minichart.jade @@ -1 +1 @@ -div(data-hook='minichart', class='minichart unique', style='width:400px; height:100px;') +div(data-hook='minichart', class='minichart-wrapper', style='width:400px; height:100px;') diff --git a/scout-ui/src/minicharts/unique.jade b/scout-ui/src/minicharts/unique.jade index cc424eb6721..22339f32833 100644 --- a/scout-ui/src/minicharts/unique.jade +++ b/scout-ui/src/minicharts/unique.jade @@ -5,12 +5,13 @@ div(data-hook='viz-container') //- = minValue dt - a(href="#") - i.mms-icon-continuous(data-hook='refresh') + //-a(href="#") + i.mms-icon-continuous(data-hook='refresh') dd ul.list-inline each val in randomValues - li= val + li + code= val //- dt max //- dd