converting to ES6 class syntax

This commit is contained in:
ppisljar 2016-09-08 16:28:44 +02:00
parent d2aff6c944
commit 161ba75d46
25 changed files with 4520 additions and 4535 deletions

View file

@ -9,25 +9,29 @@ export default function ErrorHandlerFactory() {
* @class ErrorHandler
* @constructor
*/
function ErrorHandler() {}
class ErrorHandler {
constructor() {
/**
* Validates the height and width are > 0
* min size must be at least 1 px
*
* @method validateWidthandHeight
* @param width {Number} HTMLElement width
* @param height {Number} HTMLElement height
* @returns {HTMLElement} HTML div with an error message
*/
ErrorHandler.prototype.validateWidthandHeight = function (width, height) {
let badWidth = _.isNaN(width) || width <= 0;
let badHeight = _.isNaN(height) || height <= 0;
if (badWidth || badHeight) {
throw new errors.ContainerTooSmall();
}
};
/**
* Validates the height and width are > 0
* min size must be at least 1 px
*
* @method validateWidthandHeight
* @param width {Number} HTMLElement width
* @param height {Number} HTMLElement height
* @returns {HTMLElement} HTML div with an error message
*/
validateWidthandHeight(width, height) {
let badWidth = _.isNaN(width) || width <= 0;
let badHeight = _.isNaN(height) || height <= 0;
if (badWidth || badHeight) {
throw new errors.ContainerTooSmall();
}
};
}
return ErrorHandler;
};

View file

@ -1,4 +1,3 @@
import d3 from 'd3';
import $ from 'jquery';
import _ from 'lodash';
import Binder from 'ui/binder';
@ -11,90 +10,88 @@ export default function AlertsFactory(Private) {
* @constructor
* @param el {HTMLElement} Reference to DOM element
*/
function Alerts(vis, data, alertDefs) {
if (!(this instanceof Alerts)) {
return new Alerts(vis, data, alertDefs);
class Alerts {
constructor(vis, data, alertDefs) {
this.vis = vis;
this.data = data;
this.binder = new Binder();
this.alertDefs = alertDefs || [];
this.binder.jqOn(vis.el, 'mouseenter', '.vis-alerts-tray', function () {
let $tray = $(this);
hide();
$(vis.el).on('mousemove', checkForExit);
function hide() {
$tray.css({
'pointer-events': 'none',
opacity: 0.3
});
}
function show() {
$(vis.el).off('mousemove', checkForExit);
$tray.css({
'pointer-events': 'auto',
opacity: 1
});
}
function checkForExit(event) {
let pos = $tray.offset();
if (pos.top > event.clientY || pos.left > event.clientX) return show();
let bottom = pos.top + $tray.height();
if (event.clientY > bottom) return show();
let right = pos.left + $tray.width();
if (event.clientX > right) return show();
}
});
}
this.vis = vis;
this.data = data;
this.binder = new Binder();
this.alertDefs = alertDefs || [];
/**
* Renders chart titles
*
* @method render
* @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles
*/
render() {
let vis = this.vis;
let data = this.data;
this.binder.jqOn(vis.el, 'mouseenter', '.vis-alerts-tray', function () {
let $tray = $(this);
hide();
$(vis.el).on('mousemove', checkForExit);
let alerts = _(this.alertDefs)
.map(function (alertDef) {
if (!alertDef) return;
if (alertDef.test && !alertDef.test(vis, data)) return;
function hide() {
$tray.css({
'pointer-events': 'none',
opacity: 0.3
});
}
let type = alertDef.type || 'info';
let icon = alertDef.icon || type;
let msg = alertDef.msg;
function show() {
$(vis.el).off('mousemove', checkForExit);
$tray.css({
'pointer-events': 'auto',
opacity: 1
});
}
// alert container
let $icon = $('<i>').addClass('vis-alerts-icon fa fa-' + icon);
let $text = $('<p>').addClass('vis-alerts-text').text(msg);
function checkForExit(event) {
let pos = $tray.offset();
if (pos.top > event.clientY || pos.left > event.clientX) return show();
return $('<div>').addClass('vis-alert vis-alert-' + type).append([$icon, $text]);
})
.compact();
let bottom = pos.top + $tray.height();
if (event.clientY > bottom) return show();
if (!alerts.size()) return;
let right = pos.left + $tray.width();
if (event.clientX > right) return show();
}
});
$(vis.el).find('.vis-alerts').append(
$('<div>').addClass('vis-alerts-tray').append(alerts.value())
);
};
/**
* Tear down the Alerts
* @return {undefined}
*/
destroy() {
this.binder.destroy();
};
}
/**
* Renders chart titles
*
* @method render
* @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles
*/
Alerts.prototype.render = function () {
let vis = this.vis;
let data = this.data;
let alerts = _(this.alertDefs)
.map(function (alertDef) {
if (!alertDef) return;
if (alertDef.test && !alertDef.test(vis, data)) return;
let type = alertDef.type || 'info';
let icon = alertDef.icon || type;
let msg = alertDef.msg;
// alert container
let $icon = $('<i>').addClass('vis-alerts-icon fa fa-' + icon);
let $text = $('<p>').addClass('vis-alerts-text').text(msg);
return $('<div>').addClass('vis-alert vis-alert-' + type).append([$icon, $text]);
})
.compact();
if (!alerts.size()) return;
$(vis.el).find('.vis-alerts').append(
$('<div>').addClass('vis-alerts-tray').append(alerts.value())
);
};
/**
* Tear down the Alerts
* @return {undefined}
*/
Alerts.prototype.destroy = function () {
this.binder.destroy();
};
return Alerts;
};

View file

@ -15,62 +15,60 @@ export default function AxisTitleFactory(Private) {
* @param xTitle {String} X-axis title
* @param yTitle {String} Y-axis title
*/
_.class(AxisTitle).inherits(ErrorHandler);
function AxisTitle(el, xTitle, yTitle) {
if (!(this instanceof AxisTitle)) {
return new AxisTitle(el, xTitle, yTitle);
class AxisTitle extends ErrorHandler {
constructor(el, xTitle, yTitle) {
super();
this.el = el;
this.xTitle = xTitle;
this.yTitle = yTitle;
}
this.el = el;
this.xTitle = xTitle;
this.yTitle = yTitle;
}
/**
* Renders both x and y axis titles
*
* @method render
* @returns {HTMLElement} DOM Element with axis titles
*/
AxisTitle.prototype.render = function () {
d3.select(this.el).select('.x-axis-title').call(this.draw(this.xTitle));
d3.select(this.el).select('.y-axis-title').call(this.draw(this.yTitle));
};
/**
* Appends an SVG with title text
*
* @method draw
* @param title {String} Axis title
* @returns {Function} Appends axis title to a D3 selection
*/
AxisTitle.prototype.draw = function (title) {
let self = this;
return function (selection) {
selection.each(function () {
let el = this;
let div = d3.select(el);
let width = $(el).width();
let height = $(el).height();
self.validateWidthandHeight(width, height);
div.append('svg')
.attr('width', width)
.attr('height', height)
.append('text')
.attr('transform', function () {
if (div.attr('class') === 'x-axis-title') {
return 'translate(' + width / 2 + ',11)';
}
return 'translate(11,' + height / 2 + ')rotate(270)';
})
.attr('text-anchor', 'middle')
.text(title);
});
/**
* Renders both x and y axis titles
*
* @method render
* @returns {HTMLElement} DOM Element with axis titles
*/
render() {
d3.select(this.el).select('.x-axis-title').call(this.draw(this.xTitle));
d3.select(this.el).select('.y-axis-title').call(this.draw(this.yTitle));
};
};
/**
* Appends an SVG with title text
*
* @method draw
* @param title {String} Axis title
* @returns {Function} Appends axis title to a D3 selection
*/
draw(title) {
let self = this;
return function (selection) {
selection.each(function () {
let el = this;
let div = d3.select(el);
let width = $(el).width();
let height = $(el).height();
self.validateWidthandHeight(width, height);
div.append('svg')
.attr('width', width)
.attr('height', height)
.append('text')
.attr('transform', function () {
if (div.attr('class') === 'x-axis-title') {
return 'translate(' + width / 2 + ',11)';
}
return 'translate(11,' + height / 2 + ')rotate(270)';
})
.attr('text-anchor', 'middle')
.text(title);
});
};
};
}
return AxisTitle;
};

View file

@ -15,117 +15,117 @@ export default function ChartTitleFactory(Private) {
* @constructor
* @param el {HTMLElement} Reference to DOM element
*/
_.class(ChartTitle).inherits(ErrorHandler);
function ChartTitle(el) {
if (!(this instanceof ChartTitle)) {
return new ChartTitle(el);
}
this.el = el;
this.tooltip = new Tooltip('chart-title', el, function (d) {
return '<p>' + _.escape(d.label) + '</p>';
});
}
/**
* Renders chart titles
*
* @method render
* @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles
*/
ChartTitle.prototype.render = function () {
let el = d3.select(this.el).select('.chart-title').node();
let width = el ? el.clientWidth : 0;
let height = el ? el.clientHeight : 0;
return d3.select(this.el).selectAll('.chart-title').call(this.draw(width, height));
};
/**
* Truncates chart title text
*
* @method truncate
* @param size {Number} Height or width of the HTML Element
* @returns {Function} Truncates text
*/
ChartTitle.prototype.truncate = function (size) {
let self = this;
return function (selection) {
selection.each(function () {
let text = d3.select(this);
let n = text[0].length;
let maxWidth = size / n * 0.9;
let length = this.getComputedTextLength();
let str;
let avg;
let end;
if (length > maxWidth) {
str = text.text();
avg = length / str.length;
end = Math.floor(maxWidth / avg) - 5;
str = str.substr(0, end) + '...';
self.addMouseEvents(text);
return text.text(str);
}
return text.text();
class ChartTitle extends ErrorHandler {
constructor(el) {
super();
this.el = el;
this.tooltip = new Tooltip('chart-title', el, function (d) {
return '<p>' + _.escape(d.label) + '</p>';
});
};
};
/**
* Adds tooltip events on truncated chart titles
*
* @method addMouseEvents
* @param target {HTMLElement} DOM element to attach event listeners
* @returns {*} DOM element with event listeners attached
*/
ChartTitle.prototype.addMouseEvents = function (target) {
if (this.tooltip) {
return target.call(this.tooltip.render());
}
};
/**
* Appends chart titles to the visualization
*
* @method draw
* @returns {Function} Appends chart titles to a D3 selection
*/
ChartTitle.prototype.draw = function (width, height) {
let self = this;
/**
* Renders chart titles
*
* @method render
* @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles
*/
render() {
let el = d3.select(this.el).select('.chart-title').node();
let width = el ? el.clientWidth : 0;
let height = el ? el.clientHeight : 0;
return function (selection) {
selection.each(function () {
let div = d3.select(this);
let dataType = this.parentNode.__data__.rows ? 'rows' : 'columns';
let size = dataType === 'rows' ? height : width;
let txtHtOffset = 11;
return d3.select(this.el).selectAll('.chart-title').call(this.draw(width, height));
};
self.validateWidthandHeight(width, height);
/**
* Truncates chart title text
*
* @method truncate
* @param size {Number} Height or width of the HTML Element
* @returns {Function} Truncates text
*/
truncate(size) {
let self = this;
div.append('svg')
.attr('width', width)
.attr('height', height)
.append('text')
.attr('transform', function () {
if (dataType === 'rows') {
return 'translate(' + txtHtOffset + ',' + height / 2 + ')rotate(270)';
return function (selection) {
selection.each(function () {
let text = d3.select(this);
let n = text[0].length;
let maxWidth = size / n * 0.9;
let length = this.getComputedTextLength();
let str;
let avg;
let end;
if (length > maxWidth) {
str = text.text();
avg = length / str.length;
end = Math.floor(maxWidth / avg) - 5;
str = str.substr(0, end) + '...';
self.addMouseEvents(text);
return text.text(str);
}
return 'translate(' + width / 2 + ',' + txtHtOffset + ')';
})
.attr('text-anchor', 'middle')
.text(function (d) { return d.label; });
// truncate long chart titles
div.selectAll('text')
.call(self.truncate(size));
});
return text.text();
});
};
};
};
/**
* Adds tooltip events on truncated chart titles
*
* @method addMouseEvents
* @param target {HTMLElement} DOM element to attach event listeners
* @returns {*} DOM element with event listeners attached
*/
addMouseEvents(target) {
if (this.tooltip) {
return target.call(this.tooltip.render());
}
};
/**
* Appends chart titles to the visualization
*
* @method draw
* @returns {Function} Appends chart titles to a D3 selection
*/
draw(width, height) {
let self = this;
return function (selection) {
selection.each(function () {
let div = d3.select(this);
let dataType = this.parentNode.__data__.rows ? 'rows' : 'columns';
let size = dataType === 'rows' ? height : width;
let txtHtOffset = 11;
self.validateWidthandHeight(width, height);
div.append('svg')
.attr('width', width)
.attr('height', height)
.append('text')
.attr('transform', function () {
if (dataType === 'rows') {
return 'translate(' + txtHtOffset + ',' + height / 2 + ')rotate(270)';
}
return 'translate(' + width / 2 + ',' + txtHtOffset + ')';
})
.attr('text-anchor', 'middle')
.text(function (d) {
return d.label;
});
// truncate long chart titles
div.selectAll('text')
.call(self.truncate(size));
});
};
};
}
return ChartTitle;
};

File diff suppressed because it is too large Load diff

View file

@ -14,300 +14,299 @@ export default function DispatchClass(Private) {
* @param handler {Object} Reference to Handler Class Object
*/
_.class(Dispatch).inherits(SimpleEmitter);
function Dispatch(handler) {
if (!(this instanceof Dispatch)) {
return new Dispatch(handler);
class Dispatch extends SimpleEmitter {
constructor(handler) {
super();
this.handler = handler;
this._listeners = {};
}
Dispatch.Super.call(this);
this.handler = handler;
this._listeners = {};
}
/**
* Response to click and hover events
*
* @param d {Object} Data point
* @param i {Number} Index number of data point
* @returns {{value: *, point: *, label: *, color: *, pointIndex: *,
/**
* Response to click and hover events
*
* @param d {Object} Data point
* @param i {Number} Index number of data point
* @returns {{value: *, point: *, label: *, color: *, pointIndex: *,
* series: *, config: *, data: (Object|*),
* e: (d3.event|*), handler: (Object|*)}} Event response object
*/
Dispatch.prototype.eventResponse = function (d, i) {
let datum = d._input || d;
let data = d3.event.target.nearestViewportElement ?
d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__;
let label = d.label ? d.label : d.name;
let isSeries = !!(data && data.series);
let isSlices = !!(data && data.slices);
let series = isSeries ? data.series : undefined;
let slices = isSlices ? data.slices : undefined;
let handler = this.handler;
let color = _.get(handler, 'data.color');
let isPercentage = (handler && handler._attr.mode === 'percentage');
*/
eventResponse(d, i) {
let datum = d._input || d;
let data = d3.event.target.nearestViewportElement ?
d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__;
let label = d.label ? d.label : d.name;
let isSeries = !!(data && data.series);
let isSlices = !!(data && data.slices);
let series = isSeries ? data.series : undefined;
let slices = isSlices ? data.slices : undefined;
let handler = this.handler;
let color = _.get(handler, 'data.color');
let isPercentage = (handler && handler._attr.mode === 'percentage');
let eventData = {
value: d.y,
point: datum,
datum: datum,
label: label,
color: color ? color(label) : undefined,
pointIndex: i,
series: series,
slices: slices,
config: handler && handler._attr,
data: data,
e: d3.event,
handler: handler
};
if (isSeries) {
// Find object with the actual d value and add it to the point object
let object = _.find(series, { 'label': d.label });
eventData.value = +object.values[i].y;
if (isPercentage) {
// Add the formatted percentage to the point object
eventData.percent = (100 * d.y).toFixed(1) + '%';
}
}
return eventData;
};
/**
* Returns a function that adds events and listeners to a D3 selection
*
* @method addEvent
* @param event {String}
* @param callback {Function}
* @returns {Function}
*/
Dispatch.prototype.addEvent = function (event, callback) {
return function (selection) {
selection.each(function () {
let element = d3.select(this);
if (typeof callback === 'function') {
return element.on(event, callback);
}
});
};
};
/**
*
* @method addHoverEvent
* @returns {Function}
*/
Dispatch.prototype.addHoverEvent = function () {
let self = this;
let isClickable = this.listenerCount('click') > 0;
let addEvent = this.addEvent;
let $el = this.handler.el;
if (!this.handler.highlight) {
this.handler.highlight = self.highlight;
}
function hover(d, i) {
// Add pointer if item is clickable
if (isClickable) {
self.addMousePointer.call(this, arguments);
}
self.handler.highlight.call(this, $el);
self.emit('hover', self.eventResponse(d, i));
}
return addEvent('mouseover', hover);
};
/**
*
* @method addMouseoutEvent
* @returns {Function}
*/
Dispatch.prototype.addMouseoutEvent = function () {
let self = this;
let addEvent = this.addEvent;
let $el = this.handler.el;
if (!this.handler.unHighlight) {
this.handler.unHighlight = self.unHighlight;
}
function mouseout() {
self.handler.unHighlight.call(this, $el);
}
return addEvent('mouseout', mouseout);
};
/**
*
* @method addClickEvent
* @returns {Function}
*/
Dispatch.prototype.addClickEvent = function () {
let self = this;
let addEvent = this.addEvent;
function click(d, i) {
self.emit('click', self.eventResponse(d, i));
}
return addEvent('click', click);
};
/**
* Determine if we will allow brushing
*
* @method allowBrushing
* @returns {Boolean}
*/
Dispatch.prototype.allowBrushing = function () {
let xAxis = this.handler.xAxis;
// Don't allow brushing for time based charts from non-time-based indices
let hasTimeField = this.handler.vis._attr.hasTimeField;
return Boolean(hasTimeField && xAxis.ordered && xAxis.xScale && _.isFunction(xAxis.xScale.invert));
};
/**
* Determine if brushing is currently enabled
*
* @method isBrushable
* @returns {Boolean}
*/
Dispatch.prototype.isBrushable = function () {
return this.allowBrushing() && this.listenerCount('brush') > 0;
};
/**
*
* @param svg
* @returns {Function}
*/
Dispatch.prototype.addBrushEvent = function (svg) {
if (!this.isBrushable()) return;
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.xAxis.yScale;
let brush = this.createBrush(xScale, svg);
function brushEnd() {
if (!validBrushClick(d3.event)) return;
let bar = d3.select(this);
let startX = d3.mouse(svg.node());
let startXInv = xScale.invert(startX[0]);
// Reset the brush value
brush.extent([startXInv, startXInv]);
// Magic!
// Need to call brush on svg to see brush when brushing
// while on top of bars.
// Need to call brush on bar to allow the click event to be registered
svg.call(brush);
bar.call(brush);
}
return this.addEvent('mousedown', brushEnd);
};
/**
* Mouseover Behavior
*
* @method addMousePointer
* @returns {D3.Selection}
*/
Dispatch.prototype.addMousePointer = function () {
return d3.select(this).style('cursor', 'pointer');
};
/**
* Mouseover Behavior
*
* @param element {D3.Selection}
* @method highlight
*/
Dispatch.prototype.highlight = function (element) {
let label = this.getAttribute('data-label');
if (!label) return;
//Opacity 1 is needed to avoid the css application
$('[data-label]', element.parentNode).css('opacity', 1).not(
function (els, el) { return `${$(el).data('label')}` === label;}
).css('opacity', 0.5);
};
/**
* Mouseout Behavior
*
* @param element {D3.Selection}
* @method unHighlight
*/
Dispatch.prototype.unHighlight = function (element) {
$('[data-label]', element.parentNode).css('opacity', 1);
};
/**
* Adds D3 brush to SVG and returns the brush function
*
* @param xScale {Function} D3 xScale function
* @param svg {HTMLElement} Reference to SVG
* @returns {*} Returns a D3 brush function and a SVG with a brush group attached
*/
Dispatch.prototype.createBrush = function (xScale, svg) {
let self = this;
let attr = self.handler._attr;
let height = attr.height;
let margin = attr.margin;
// Brush scale
let brush = d3.svg.brush()
.x(xScale)
.on('brushend', function brushEnd() {
// Assumes data is selected at the chart level
// In this case, the number of data objects should always be 1
let data = d3.select(this).data()[0];
let isTimeSeries = (data.ordered && data.ordered.date);
// Allows for brushing on d3.scale.ordinal()
let selected = xScale.domain().filter(function (d) {
return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]);
});
let range = isTimeSeries ? brush.extent() : selected;
return self.emit('brush', {
range: range,
config: attr,
let eventData = {
value: d.y,
point: datum,
datum: datum,
label: label,
color: color ? color(label) : undefined,
pointIndex: i,
series: series,
slices: slices,
config: handler && handler._attr,
data: data,
e: d3.event,
data: data
});
});
handler: handler
};
// if `addBrushing` is true, add brush canvas
if (self.listenerCount('brush')) {
svg.insert('g', 'g')
.attr('class', 'brush')
.call(brush)
.call(function (brushG) {
// hijack the brush start event to filter out right/middle clicks
let brushHandler = brushG.on('mousedown.brush');
if (!brushHandler) return; // touch events in use
brushG.on('mousedown.brush', function () {
if (validBrushClick(d3.event)) brushHandler.apply(this, arguments);
if (isSeries) {
// Find object with the actual d value and add it to the point object
let object = _.find(series, {'label': d.label});
eventData.value = +object.values[i].y;
if (isPercentage) {
// Add the formatted percentage to the point object
eventData.percent = (100 * d.y).toFixed(1) + '%';
}
}
return eventData;
};
/**
* Returns a function that adds events and listeners to a D3 selection
*
* @method addEvent
* @param event {String}
* @param callback {Function}
* @returns {Function}
*/
addEvent(event, callback) {
return function (selection) {
selection.each(function () {
let element = d3.select(this);
if (typeof callback === 'function') {
return element.on(event, callback);
}
});
})
.selectAll('rect')
.attr('height', height - margin.top - margin.bottom);
};
};
return brush;
}
};
/**
*
* @method addHoverEvent
* @returns {Function}
*/
addHoverEvent() {
let self = this;
let isClickable = this.listenerCount('click') > 0;
let addEvent = this.addEvent;
let $el = this.handler.el;
if (!this.handler.highlight) {
this.handler.highlight = self.highlight;
}
function hover(d, i) {
// Add pointer if item is clickable
if (isClickable) {
self.addMousePointer.call(this, arguments);
}
self.handler.highlight.call(this, $el);
self.emit('hover', self.eventResponse(d, i));
}
return addEvent('mouseover', hover);
};
/**
*
* @method addMouseoutEvent
* @returns {Function}
*/
addMouseoutEvent() {
let self = this;
let addEvent = this.addEvent;
let $el = this.handler.el;
if (!this.handler.unHighlight) {
this.handler.unHighlight = self.unHighlight;
}
function mouseout() {
self.handler.unHighlight.call(this, $el);
}
return addEvent('mouseout', mouseout);
};
/**
*
* @method addClickEvent
* @returns {Function}
*/
addClickEvent() {
let self = this;
let addEvent = this.addEvent;
function click(d, i) {
self.emit('click', self.eventResponse(d, i));
}
return addEvent('click', click);
};
/**
* Determine if we will allow brushing
*
* @method allowBrushing
* @returns {Boolean}
*/
allowBrushing() {
let xAxis = this.handler.xAxis;
// Don't allow brushing for time based charts from non-time-based indices
let hasTimeField = this.handler.vis._attr.hasTimeField;
return Boolean(hasTimeField && xAxis.ordered && xAxis.xScale && _.isFunction(xAxis.xScale.invert));
};
/**
* Determine if brushing is currently enabled
*
* @method isBrushable
* @returns {Boolean}
*/
isBrushable() {
return this.allowBrushing() && this.listenerCount('brush') > 0;
};
/**
*
* @param svg
* @returns {Function}
*/
addBrushEvent(svg) {
if (!this.isBrushable()) return;
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.xAxis.yScale;
let brush = this.createBrush(xScale, svg);
function brushEnd() {
if (!validBrushClick(d3.event)) return;
let bar = d3.select(this);
let startX = d3.mouse(svg.node());
let startXInv = xScale.invert(startX[0]);
// Reset the brush value
brush.extent([startXInv, startXInv]);
// Magic!
// Need to call brush on svg to see brush when brushing
// while on top of bars.
// Need to call brush on bar to allow the click event to be registered
svg.call(brush);
bar.call(brush);
}
return this.addEvent('mousedown', brushEnd);
};
/**
* Mouseover Behavior
*
* @method addMousePointer
* @returns {D3.Selection}
*/
addMousePointer() {
return d3.select(this).style('cursor', 'pointer');
};
/**
* Mouseover Behavior
*
* @param element {D3.Selection}
* @method highlight
*/
highlight(element) {
let label = this.getAttribute('data-label');
if (!label) return;
//Opacity 1 is needed to avoid the css application
$('[data-label]', element.parentNode).css('opacity', 1).not(
function (els, el) {
return `${$(el).data('label')}` === label;
}
).css('opacity', 0.5);
};
/**
* Mouseout Behavior
*
* @param element {D3.Selection}
* @method unHighlight
*/
unHighlight(element) {
$('[data-label]', element.parentNode).css('opacity', 1);
};
/**
* Adds D3 brush to SVG and returns the brush function
*
* @param xScale {Function} D3 xScale function
* @param svg {HTMLElement} Reference to SVG
* @returns {*} Returns a D3 brush function and a SVG with a brush group attached
*/
createBrush(xScale, svg) {
let self = this;
let attr = self.handler._attr;
let height = attr.height;
let margin = attr.margin;
// Brush scale
let brush = d3.svg.brush()
.x(xScale)
.on('brushend', function brushEnd() {
// Assumes data is selected at the chart level
// In this case, the number of data objects should always be 1
let data = d3.select(this).data()[0];
let isTimeSeries = (data.ordered && data.ordered.date);
// Allows for brushing on d3.scale.ordinal()
let selected = xScale.domain().filter(function (d) {
return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]);
});
let range = isTimeSeries ? brush.extent() : selected;
return self.emit('brush', {
range: range,
config: attr,
e: d3.event,
data: data
});
});
// if `addBrushing` is true, add brush canvas
if (self.listenerCount('brush')) {
svg.insert('g', 'g')
.attr('class', 'brush')
.call(brush)
.call(function (brushG) {
// hijack the brush start event to filter out right/middle clicks
let brushHandler = brushG.on('mousedown.brush');
if (!brushHandler) return; // touch events in use
brushG.on('mousedown.brush', function () {
if (validBrushClick(d3.event)) brushHandler.apply(this, arguments);
});
})
.selectAll('rect')
.attr('height', height - margin.top - margin.bottom);
return brush;
}
};
}
function validBrushClick(event) {
return event.button === 0;

View file

@ -18,195 +18,192 @@ export default function HandlerBaseClass(Private) {
* @param opts {Object} Reference to Visualization constructors needed to
* create the visualization
*/
function Handler(vis, opts) {
if (!(this instanceof Handler)) {
return new Handler(vis, opts);
}
class Handler {
constructor(vis, opts) {
this.data = opts.data || new Data(vis.data, vis._attr, vis.uiState);
this.vis = vis;
this.el = vis.el;
this.ChartClass = vis.ChartClass;
this.charts = [];
this.data = opts.data || new Data(vis.data, vis._attr, vis.uiState);
this.vis = vis;
this.el = vis.el;
this.ChartClass = vis.ChartClass;
this.charts = [];
this._attr = _.defaults(vis._attr || {}, {
'margin' : { top: 10, right: 3, bottom: 5, left: 3 }
});
this.xAxis = opts.xAxis;
this.yAxis = opts.yAxis;
this.chartTitle = opts.chartTitle;
this.axisTitle = opts.axisTitle;
this.alerts = opts.alerts;
this.layout = new Layout(vis.el, vis.data, vis._attr.type, opts);
this.binder = new Binder();
this.renderArray = _.filter([
this.layout,
this.axisTitle,
this.chartTitle,
this.alerts,
this.xAxis,
this.yAxis,
], Boolean);
// memoize so that the same function is returned every time,
// allowing us to remove/re-add the same function
this.getProxyHandler = _.memoize(function (event) {
let self = this;
return function (e) {
self.vis.emit(event, e);
};
});
}
/**
* Validates whether data is actually present in the data object
* used to render the Vis. Throws a no results error if data is not
* present.
*
* @private
*/
Handler.prototype._validateData = function () {
let dataType = this.data.type;
if (!dataType) {
throw new errors.NoResults();
}
};
/**
* Renders the constructors that create the visualization,
* including the chart constructor
*
* @method render
* @returns {HTMLElement} With the visualization child element
*/
Handler.prototype.render = function () {
let self = this;
let charts = this.charts = [];
let selection = d3.select(this.el);
selection.selectAll('*').remove();
this._validateData();
this.renderArray.forEach(function (property) {
if (typeof property.render === 'function') {
property.render();
}
});
// render the chart(s)
selection.selectAll('.chart')
.each(function (chartData) {
let chart = new self.ChartClass(self, this, chartData);
self.vis.activeEvents().forEach(function (event) {
self.enable(event, chart);
this._attr = _.defaults(vis._attr || {}, {
'margin': {top: 10, right: 3, bottom: 5, left: 3}
});
charts.push(chart);
chart.render();
});
};
this.xAxis = opts.xAxis;
this.yAxis = opts.yAxis;
this.chartTitle = opts.chartTitle;
this.axisTitle = opts.axisTitle;
this.alerts = opts.alerts;
this.layout = new Layout(vis.el, vis.data, vis._attr.type, opts);
this.binder = new Binder();
this.renderArray = _.filter([
this.layout,
this.axisTitle,
this.chartTitle,
this.alerts,
this.xAxis,
this.yAxis,
], Boolean);
/**
* Enables events, i.e. binds specific events to the chart
* object(s) `on` method. For example, `click` or `mousedown` events.
*
* @method enable
* @param event {String} Event type
* @param chart {Object} Chart
* @returns {*}
*/
Handler.prototype.enable = chartEventProxyToggle('on');
// memoize so that the same function is returned every time,
// allowing us to remove/re-add the same function
this.getProxyHandler = _.memoize(function (event) {
let self = this;
return function (e) {
self.vis.emit(event, e);
};
});
/**
* Disables events for all charts
*
* @method disable
* @param event {String} Event type
* @param chart {Object} Chart
* @returns {*}
*/
Handler.prototype.disable = chartEventProxyToggle('off');
/**
* Enables events, i.e. binds specific events to the chart
* object(s) `on` method. For example, `click` or `mousedown` events.
*
* @method enable
* @param event {String} Event type
* @param chart {Object} Chart
* @returns {*}
*/
this.enable = this.chartEventProxyToggle('on');
/**
* Disables events for all charts
*
* @method disable
* @param event {String} Event type
* @param chart {Object} Chart
* @returns {*}
*/
this.disable = this.chartEventProxyToggle('off');
}
function chartEventProxyToggle(method) {
return function (event, chart) {
let proxyHandler = this.getProxyHandler(event);
/**
* Validates whether data is actually present in the data object
* used to render the Vis. Throws a no results error if data is not
* present.
*
* @private
*/
_validateData() {
let dataType = this.data.type;
_.each(chart ? [chart] : this.charts, function (chart) {
chart.events[method](event, proxyHandler);
if (!dataType) {
throw new errors.NoResults();
}
};
/**
* Renders the constructors that create the visualization,
* including the chart constructor
*
* @method render
* @returns {HTMLElement} With the visualization child element
*/
render() {
let self = this;
let charts = this.charts = [];
let selection = d3.select(this.el);
selection.selectAll('*').remove();
this._validateData();
this.renderArray.forEach(function (property) {
if (typeof property.render === 'function') {
property.render();
}
});
// render the chart(s)
selection.selectAll('.chart')
.each(function (chartData) {
let chart = new self.ChartClass(self, this, chartData);
self.vis.activeEvents().forEach(function (event) {
self.enable(event, chart);
});
charts.push(chart);
chart.render();
});
};
chartEventProxyToggle(method) {
return function (event, chart) {
let proxyHandler = this.getProxyHandler(event);
_.each(chart ? [chart] : this.charts, function (chart) {
chart.events[method](event, proxyHandler);
});
};
}
/**
* Removes all DOM elements from the HTML element provided
*
* @method removeAll
* @param el {HTMLElement} Reference to the HTML Element that
* contains the chart
* @returns {D3.Selection|D3.Transition.Transition} With the chart
* child element removed
*/
removeAll(el) {
return d3.select(el).selectAll('*').remove();
};
/**
* Displays an error message in the DOM
*
* @method error
* @param message {String} Error message to display
* @returns {HTMLElement} Displays the input message
*/
error(message) {
this.removeAll(this.el);
let div = d3.select(this.el)
.append('div')
// class name needs `chart` in it for the polling checkSize function
// to continuously call render on resize
.attr('class', 'visualize-error chart error');
if (message === 'No results found') {
div.append('div')
.attr('class', 'text-center visualize-error visualize-chart ng-scope')
.append('div').attr('class', 'item top')
.append('div').attr('class', 'item')
.append('h2').html('<i class="fa fa-meh-o"></i>')
.append('h4').text(message);
div.append('div').attr('class', 'item bottom');
return div;
}
return div.append('h4').text(message);
};
/**
* Destroys all the charts in the visualization
*
* @method destroy
*/
destroy() {
this.binder.destroy();
this.renderArray.forEach(function (renderable) {
if (_.isFunction(renderable.destroy)) {
renderable.destroy();
}
});
this.charts.splice(0).forEach(function (chart) {
if (_.isFunction(chart.destroy)) {
chart.destroy();
}
});
};
}
/**
* Removes all DOM elements from the HTML element provided
*
* @method removeAll
* @param el {HTMLElement} Reference to the HTML Element that
* contains the chart
* @returns {D3.Selection|D3.Transition.Transition} With the chart
* child element removed
*/
Handler.prototype.removeAll = function (el) {
return d3.select(el).selectAll('*').remove();
};
/**
* Displays an error message in the DOM
*
* @method error
* @param message {String} Error message to display
* @returns {HTMLElement} Displays the input message
*/
Handler.prototype.error = function (message) {
this.removeAll(this.el);
let div = d3.select(this.el)
.append('div')
// class name needs `chart` in it for the polling checkSize function
// to continuously call render on resize
.attr('class', 'visualize-error chart error');
if (message === 'No results found') {
div.append('div')
.attr('class', 'text-center visualize-error visualize-chart ng-scope')
.append('div').attr('class', 'item top')
.append('div').attr('class', 'item')
.append('h2').html('<i class="fa fa-meh-o"></i>')
.append('h4').text(message);
div.append('div').attr('class', 'item bottom');
return div;
}
return div.append('h4').text(message);
};
/**
* Destroys all the charts in the visualization
*
* @method destroy
*/
Handler.prototype.destroy = function () {
this.binder.destroy();
this.renderArray.forEach(function (renderable) {
if (_.isFunction(renderable.destroy)) {
renderable.destroy();
}
});
this.charts.splice(0).forEach(function (chart) {
if (_.isFunction(chart.destroy)) {
chart.destroy();
}
});
};
return Handler;
};

View file

@ -21,133 +21,131 @@ export default function LayoutFactory(Private) {
* @param data {Object} Elasticsearch query results for this specific chart
* @param chartType {Object} Reference to chart functions, i.e. Pie
*/
function Layout(el, data, chartType, opts) {
if (!(this instanceof Layout)) {
return new Layout(el, data, chartType, opts);
class Layout {
constructor(el, data, chartType, opts) {
this.el = el;
this.data = data;
this.opts = opts;
this.layoutType = layoutType[chartType](this.el, this.data);
}
this.el = el;
this.data = data;
this.opts = opts;
this.layoutType = layoutType[chartType](this.el, this.data);
}
// Render the layout
/**
* Renders visualization HTML layout
* Remove all elements from the current visualization and creates the layout
*
* @method render
*/
render() {
this.removeAll(this.el);
this.createLayout(this.layoutType);
};
// Render the layout
/**
* Renders visualization HTML layout
* Remove all elements from the current visualization and creates the layout
*
* @method render
*/
Layout.prototype.render = function () {
this.removeAll(this.el);
this.createLayout(this.layoutType);
};
/**
* Create the layout based on the json array provided
* for each object in the layout array, call the layout function
*
* @method createLayout
* @param arr {Array} Json array
* @returns {*} Creates the visualization layout
*/
createLayout(arr) {
let self = this;
/**
* Create the layout based on the json array provided
* for each object in the layout array, call the layout function
*
* @method createLayout
* @param arr {Array} Json array
* @returns {*} Creates the visualization layout
*/
Layout.prototype.createLayout = function (arr) {
let self = this;
return _.each(arr, function (obj) {
self.layout(obj);
});
};
/**
* Appends a DOM element based on the object keys
* check to see if reference to DOM element is string but not class selector
* Create a class selector
*
* @method layout
* @param obj {Object} Instructions for creating the layout of a DOM Element
* @returns {*} DOM Element
*/
Layout.prototype.layout = function (obj) {
if (!obj.parent) {
throw new Error('No parent element provided');
}
if (!obj.type) {
throw new Error('No element type provided');
}
if (typeof obj.type !== 'string') {
throw new Error(obj.type + ' must be a string');
}
if (typeof obj.parent === 'string' && obj.parent.charAt(0) !== '.') {
obj.parent = '.' + obj.parent;
}
let childEl = this.appendElem(obj.parent, obj.type, obj.class);
if (obj.datum) {
childEl.datum(obj.datum);
}
if (obj.splits) {
childEl.call(obj.splits, obj.parent, this.opts);
}
if (obj.children) {
let newParent = childEl[0][0];
_.forEach(obj.children, function (obj) {
if (!obj.parent) {
obj.parent = newParent;
}
return _.each(arr, function (obj) {
self.layout(obj);
});
};
this.createLayout(obj.children);
}
/**
* Appends a DOM element based on the object keys
* check to see if reference to DOM element is string but not class selector
* Create a class selector
*
* @method layout
* @param obj {Object} Instructions for creating the layout of a DOM Element
* @returns {*} DOM Element
*/
layout(obj) {
if (!obj.parent) {
throw new Error('No parent element provided');
}
return childEl;
};
if (!obj.type) {
throw new Error('No element type provided');
}
/**
* Appends a `type` of DOM element to `el` and gives it a class name attribute `className`
*
* @method appendElem
* @param el {HTMLElement} Reference to a DOM Element
* @param type {String} DOM element type
* @param className {String} CSS class name
* @returns {*} Reference to D3 Selection
*/
Layout.prototype.appendElem = function (el, type, className) {
if (!el || !type || !className) {
throw new Error('Function requires that an el, type, and class be provided');
}
if (typeof obj.type !== 'string') {
throw new Error(obj.type + ' must be a string');
}
if (typeof el === 'string') {
// Create a DOM reference with a d3 selection
// Need to make sure that the `el` is bound to this object
// to prevent it from being appended to another Layout
el = d3.select(this.el)
.select(el)[0][0];
}
if (typeof obj.parent === 'string' && obj.parent.charAt(0) !== '.') {
obj.parent = '.' + obj.parent;
}
return d3.select(el)
.append(type)
.attr('class', className);
};
let childEl = this.appendElem(obj.parent, obj.type, obj.class);
/**
* Removes all DOM elements from DOM element
*
* @method removeAll
* @param el {HTMLElement} Reference to DOM element
* @returns {D3.Selection|D3.Transition.Transition} Reference to an empty DOM element
*/
Layout.prototype.removeAll = function (el) {
return d3.select(el).selectAll('*').remove();
};
if (obj.datum) {
childEl.datum(obj.datum);
}
if (obj.splits) {
childEl.call(obj.splits, obj.parent, this.opts);
}
if (obj.children) {
let newParent = childEl[0][0];
_.forEach(obj.children, function (obj) {
if (!obj.parent) {
obj.parent = newParent;
}
});
this.createLayout(obj.children);
}
return childEl;
};
/**
* Appends a `type` of DOM element to `el` and gives it a class name attribute `className`
*
* @method appendElem
* @param el {HTMLElement} Reference to a DOM Element
* @param type {String} DOM element type
* @param className {String} CSS class name
* @returns {*} Reference to D3 Selection
*/
appendElem(el, type, className) {
if (!el || !type || !className) {
throw new Error('Function requires that an el, type, and class be provided');
}
if (typeof el === 'string') {
// Create a DOM reference with a d3 selection
// Need to make sure that the `el` is bound to this object
// to prevent it from being appended to another Layout
el = d3.select(this.el)
.select(el)[0][0];
}
return d3.select(el)
.append(type)
.attr('class', className);
};
/**
* Removes all DOM elements from DOM element
*
* @method removeAll
* @param el {HTMLElement} Reference to DOM element
* @returns {D3.Selection|D3.Transition.Transition} Reference to an empty DOM element
*/
removeAll(el) {
return d3.select(el).selectAll('*').remove();
};
}
return Layout;
};

View file

@ -15,512 +15,510 @@ export default function XAxisFactory(Private) {
* @param args {{el: (HTMLElement), xValues: (Array), ordered: (Object|*),
* xAxisFormatter: (Function), _attr: (Object|*)}}
*/
_.class(XAxis).inherits(ErrorHandler);
function XAxis(args) {
if (!(this instanceof XAxis)) {
return new XAxis(args);
class XAxis extends ErrorHandler {
constructor(args) {
super();
this.el = args.el;
this.xValues = args.xValues;
this.ordered = args.ordered;
this.xAxisFormatter = args.xAxisFormatter;
this.expandLastBucket = args.expandLastBucket == null ? true : args.expandLastBucket;
this._attr = _.defaults(args._attr || {});
}
this.el = args.el;
this.xValues = args.xValues;
this.ordered = args.ordered;
this.xAxisFormatter = args.xAxisFormatter;
this.expandLastBucket = args.expandLastBucket == null ? true : args.expandLastBucket;
this._attr = _.defaults(args._attr || {});
}
/**
* Renders the x axis
*
* @method render
* @returns {D3.UpdateSelection} Appends x axis to visualization
*/
render() {
d3.select(this.el).selectAll('.x-axis-div').call(this.draw());
};
/**
* Renders the x axis
*
* @method render
* @returns {D3.UpdateSelection} Appends x axis to visualization
*/
XAxis.prototype.render = function () {
d3.select(this.el).selectAll('.x-axis-div').call(this.draw());
};
/**
* Returns d3 x axis scale function.
* If time, return time scale, else return d3 ordinal scale for nominal data
*
* @method getScale
* @returns {*} D3 scale function
*/
getScale() {
let ordered = this.ordered;
/**
* Returns d3 x axis scale function.
* If time, return time scale, else return d3 ordinal scale for nominal data
*
* @method getScale
* @returns {*} D3 scale function
*/
XAxis.prototype.getScale = function () {
let ordered = this.ordered;
if (ordered && ordered.date) {
return d3.time.scale.utc();
}
return d3.scale.ordinal();
};
if (ordered && ordered.date) {
return d3.time.scale.utc();
}
return d3.scale.ordinal();
};
/**
* Add domain to the x axis scale.
* if time, return a time domain, and calculate the min date, max date, and time interval
* else, return a nominal (d3.scale.ordinal) domain, i.e. array of x axis values
*
* @method getDomain
* @param scale {Function} D3 scale
* @returns {*} D3 scale function
*/
getDomain(scale) {
let ordered = this.ordered;
/**
* Add domain to the x axis scale.
* if time, return a time domain, and calculate the min date, max date, and time interval
* else, return a nominal (d3.scale.ordinal) domain, i.e. array of x axis values
*
* @method getDomain
* @param scale {Function} D3 scale
* @returns {*} D3 scale function
*/
XAxis.prototype.getDomain = function (scale) {
let ordered = this.ordered;
if (ordered && ordered.date) {
return this.getTimeDomain(scale, this.xValues);
}
return this.getOrdinalDomain(scale, this.xValues);
};
if (ordered && ordered.date) {
return this.getTimeDomain(scale, this.xValues);
}
return this.getOrdinalDomain(scale, this.xValues);
};
/**
* Returns D3 time domain
*
* @method getTimeDomain
* @param scale {Function} D3 scale function
* @param data {Array}
* @returns {*} D3 scale function
*/
getTimeDomain(scale, data) {
return scale.domain([this.minExtent(data), this.maxExtent(data)]);
};
/**
* Returns D3 time domain
*
* @method getTimeDomain
* @param scale {Function} D3 scale function
* @param data {Array}
* @returns {*} D3 scale function
*/
XAxis.prototype.getTimeDomain = function (scale, data) {
return scale.domain([this.minExtent(data), this.maxExtent(data)]);
};
minExtent(data) {
return this._calculateExtent(data || this.xValues, 'min');
};
XAxis.prototype.minExtent = function (data) {
return this._calculateExtent(data || this.xValues, 'min');
};
maxExtent(data) {
return this._calculateExtent(data || this.xValues, 'max');
};
XAxis.prototype.maxExtent = function (data) {
return this._calculateExtent(data || this.xValues, 'max');
};
/**
*
* @param data
* @param extent
*/
_calculateExtent(data, extent) {
let ordered = this.ordered;
let opts = [ordered[extent]];
/**
*
* @param data
* @param extent
*/
XAxis.prototype._calculateExtent = function (data, extent) {
let ordered = this.ordered;
let opts = [ordered[extent]];
let point = d3[extent](data);
if (this.expandLastBucket && extent === 'max') {
point = this.addInterval(point);
}
opts.push(point);
let point = d3[extent](data);
if (this.expandLastBucket && extent === 'max') {
point = this.addInterval(point);
}
opts.push(point);
return d3[extent](opts.reduce(function (opts, v) {
if (!_.isNumber(v)) v = +v;
if (!isNaN(v)) opts.push(v);
return opts;
}, []));
};
return d3[extent](opts.reduce(function (opts, v) {
if (!_.isNumber(v)) v = +v;
if (!isNaN(v)) opts.push(v);
return opts;
}, []));
};
/**
* Add the interval to a point on the x axis,
* this properly adds dates if needed.
*
* @param {number} x - a value on the x-axis
* @returns {number} - x + the ordered interval
*/
addInterval(x) {
return this.modByInterval(x, +1);
};
/**
* Add the interval to a point on the x axis,
* this properly adds dates if needed.
*
* @param {number} x - a value on the x-axis
* @returns {number} - x + the ordered interval
*/
XAxis.prototype.addInterval = function (x) {
return this.modByInterval(x, +1);
};
/**
* Subtract the interval to a point on the x axis,
* this properly subtracts dates if needed.
*
* @param {number} x - a value on the x-axis
* @returns {number} - x - the ordered interval
*/
subtractInterval(x) {
return this.modByInterval(x, -1);
};
/**
* Subtract the interval to a point on the x axis,
* this properly subtracts dates if needed.
*
* @param {number} x - a value on the x-axis
* @returns {number} - x - the ordered interval
*/
XAxis.prototype.subtractInterval = function (x) {
return this.modByInterval(x, -1);
};
/**
* Modify the x value by n intervals, properly
* handling dates if needed.
*
* @param {number} x - a value on the x-axis
* @param {number} n - the number of intervals
* @returns {number} - x + n intervals
*/
modByInterval(x, n) {
let ordered = this.ordered;
if (!ordered) return x;
let interval = ordered.interval;
if (!interval) return x;
/**
* Modify the x value by n intervals, properly
* handling dates if needed.
*
* @param {number} x - a value on the x-axis
* @param {number} n - the number of intervals
* @returns {number} - x + n intervals
*/
XAxis.prototype.modByInterval = function (x, n) {
let ordered = this.ordered;
if (!ordered) return x;
let interval = ordered.interval;
if (!interval) return x;
if (!ordered.date) {
return x += (ordered.interval * n);
}
if (!ordered.date) {
return x += (ordered.interval * n);
}
let y = moment(x);
let method = n > 0 ? 'add' : 'subtract';
let y = moment(x);
let method = n > 0 ? 'add' : 'subtract';
_.times(Math.abs(n), function () {
y[method](interval);
});
return y.valueOf();
};
/**
* Return a nominal(d3 ordinal) domain
*
* @method getOrdinalDomain
* @param scale {Function} D3 scale function
* @param xValues {Array} Array of x axis values
* @returns {*} D3 scale function
*/
XAxis.prototype.getOrdinalDomain = function (scale, xValues) {
return scale.domain(xValues);
};
/**
* Return the range for the x axis scale
* if time, return a normal range, else if nominal, return rangeBands with a default (0.1) spacer specified
*
* @method getRange
* @param scale {Function} D3 scale function
* @param width {Number} HTML Element width
* @returns {*} D3 scale function
*/
XAxis.prototype.getRange = function (domain, width) {
let ordered = this.ordered;
if (ordered && ordered.date) {
return domain.range([0, width]);
}
return domain.rangeBands([0, width], 0.1);
};
/**
* Return the x axis scale
*
* @method getXScale
* @param width {Number} HTML Element width
* @returns {*} D3 x scale function
*/
XAxis.prototype.getXScale = function (width) {
let domain = this.getDomain(this.getScale());
return this.getRange(domain, width);
};
/**
* Creates d3 xAxis function
*
* @method getXAxis
* @param width {Number} HTML Element width
*/
XAxis.prototype.getXAxis = function (width) {
this.xScale = this.getXScale(width);
if (!this.xScale || _.isNaN(this.xScale)) {
throw new Error('xScale is ' + this.xScale);
}
this.xAxis = d3.svg.axis()
.scale(this.xScale)
.ticks(10)
.tickFormat(this.xAxisFormatter)
.orient('bottom');
};
/**
* Renders the x axis
*
* @method draw
* @returns {Function} Renders the x axis to a D3 selection
*/
XAxis.prototype.draw = function () {
let self = this;
let div;
let width;
let height;
let svg;
let parentWidth;
let n;
this._attr.isRotated = false;
return function (selection) {
n = selection[0].length;
parentWidth = $(self.el)
.find('.x-axis-div-wrapper')
.width();
selection.each(function () {
div = d3.select(this);
width = parentWidth / n;
height = $(this.parentElement).height();
self.validateWidthandHeight(width, height);
self.getXAxis(width);
svg = div.append('svg')
.attr('width', width)
.attr('height', height);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,0)')
.call(self.xAxis);
_.times(Math.abs(n), function () {
y[method](interval);
});
selection.call(self.filterOrRotate());
return y.valueOf();
};
};
/**
* Returns a function that evaluates scale type and
* applies filter to tick labels on time scales
* rotates and truncates tick labels on nominal/ordinal scales
*
* @method filterOrRotate
* @returns {Function} Filters or rotates x axis tick labels
*/
XAxis.prototype.filterOrRotate = function () {
let self = this;
let ordered = self.ordered;
let axis;
let labels;
/**
* Return a nominal(d3 ordinal) domain
*
* @method getOrdinalDomain
* @param scale {Function} D3 scale function
* @param xValues {Array} Array of x axis values
* @returns {*} D3 scale function
*/
getOrdinalDomain(scale, xValues) {
return scale.domain(xValues);
};
return function (selection) {
selection.each(function () {
axis = d3.select(this);
labels = axis.selectAll('.tick text');
if (ordered && ordered.date) {
axis.call(self.filterAxisLabels());
} else {
axis.call(self.rotateAxisLabels());
/**
* Return the range for the x axis scale
* if time, return a normal range, else if nominal, return rangeBands with a default (0.1) spacer specified
*
* @method getRange
* @param scale {Function} D3 scale function
* @param width {Number} HTML Element width
* @returns {*} D3 scale function
*/
getRange(domain, width) {
let ordered = this.ordered;
if (ordered && ordered.date) {
return domain.range([0, width]);
}
return domain.rangeBands([0, width], 0.1);
};
/**
* Return the x axis scale
*
* @method getXScale
* @param width {Number} HTML Element width
* @returns {*} D3 x scale function
*/
getXScale(width) {
let domain = this.getDomain(this.getScale());
return this.getRange(domain, width);
};
/**
* Creates d3 xAxis function
*
* @method getXAxis
* @param width {Number} HTML Element width
*/
getXAxis(width) {
this.xScale = this.getXScale(width);
if (!this.xScale || _.isNaN(this.xScale)) {
throw new Error('xScale is ' + this.xScale);
}
this.xAxis = d3.svg.axis()
.scale(this.xScale)
.ticks(10)
.tickFormat(this.xAxisFormatter)
.orient('bottom');
};
/**
* Renders the x axis
*
* @method draw
* @returns {Function} Renders the x axis to a D3 selection
*/
draw() {
let self = this;
let div;
let width;
let height;
let svg;
let parentWidth;
let n;
this._attr.isRotated = false;
return function (selection) {
n = selection[0].length;
parentWidth = $(self.el)
.find('.x-axis-div-wrapper')
.width();
selection.each(function () {
div = d3.select(this);
width = parentWidth / n;
height = $(this.parentElement).height();
self.validateWidthandHeight(width, height);
self.getXAxis(width);
svg = div.append('svg')
.attr('width', width)
.attr('height', height);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,0)')
.call(self.xAxis);
});
selection.call(self.filterOrRotate());
};
};
/**
* Returns a function that evaluates scale type and
* applies filter to tick labels on time scales
* rotates and truncates tick labels on nominal/ordinal scales
*
* @method filterOrRotate
* @returns {Function} Filters or rotates x axis tick labels
*/
filterOrRotate() {
let self = this;
let ordered = self.ordered;
let axis;
let labels;
return function (selection) {
selection.each(function () {
axis = d3.select(this);
labels = axis.selectAll('.tick text');
if (ordered && ordered.date) {
axis.call(self.filterAxisLabels());
} else {
axis.call(self.rotateAxisLabels());
}
});
self.updateXaxisHeight();
selection.call(self.fitTitles());
};
};
/**
* Rotate the axis tick labels within selection
*
* @returns {Function} Rotates x axis tick labels of a D3 selection
*/
rotateAxisLabels() {
let self = this;
let text;
let barWidth = self.xScale.rangeBand();
let maxRotatedLength = 120;
let xAxisPadding = 15;
let svg;
let lengths = [];
let length;
self._attr.isRotated = false;
return function (selection) {
text = selection.selectAll('.tick text');
text.each(function textWidths() {
lengths.push(d3.select(this).node().getBBox().width);
});
length = _.max(lengths);
self._attr.xAxisLabelHt = length + xAxisPadding;
// if longer than bar width, rotate
if (length > barWidth) {
self._attr.isRotated = true;
}
});
self.updateXaxisHeight();
selection.call(self.fitTitles());
};
};
/**
* Rotate the axis tick labels within selection
*
* @returns {Function} Rotates x axis tick labels of a D3 selection
*/
XAxis.prototype.rotateAxisLabels = function () {
let self = this;
let text;
let barWidth = self.xScale.rangeBand();
let maxRotatedLength = 120;
let xAxisPadding = 15;
let svg;
let lengths = [];
let length;
self._attr.isRotated = false;
return function (selection) {
text = selection.selectAll('.tick text');
text.each(function textWidths() {
lengths.push(d3.select(this).node().getBBox().width);
});
length = _.max(lengths);
self._attr.xAxisLabelHt = length + xAxisPadding;
// if longer than bar width, rotate
if (length > barWidth) {
self._attr.isRotated = true;
}
// if longer than maxRotatedLength, truncate
if (length > maxRotatedLength) {
self._attr.xAxisLabelHt = maxRotatedLength;
}
if (self._attr.isRotated) {
text
.text(function truncate() {
return self.truncateLabel(this, self._attr.xAxisLabelHt);
})
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '-.60em')
.attr('transform', function rotate() {
return 'rotate(-90)';
})
.append('title')
.text(text => text);
selection.select('svg')
.attr('height', self._attr.xAxisLabelHt);
}
};
};
/**
* Returns a string that is truncated to fit size
*
* @method truncateLabel
* @param text {HTMLElement}
* @param size {Number}
* @returns {*|jQuery}
*/
XAxis.prototype.truncateLabel = function (text, size) {
let node = d3.select(text).node();
let str = $(node).text();
let width = node.getBBox().width;
let chars = str.length;
let pxPerChar = width / chars;
let endChar = 0;
let ellipsesPad = 4;
if (width > size) {
endChar = Math.floor((size / pxPerChar) - ellipsesPad);
while (str[endChar - 1] === ' ' || str[endChar - 1] === '-' || str[endChar - 1] === ',') {
endChar = endChar - 1;
}
str = str.substr(0, endChar) + '...';
}
return str;
};
/**
* Filter out text labels by width and position on axis
* trims labels that would overlap each other
* or extend past left or right edges
* if prev label pos (or 0) + half of label width is < label pos
* and label pos + half width is not > width of axis
*
* @method filterAxisLabels
* @returns {Function}
*/
XAxis.prototype.filterAxisLabels = function () {
let self = this;
let startX = 0;
let maxW;
let par;
let myX;
let myWidth;
let halfWidth;
let padding = 1.1;
return function (selection) {
selection.selectAll('.tick text')
.text(function (d) {
par = d3.select(this.parentNode).node();
myX = self.xScale(d);
myWidth = par.getBBox().width * padding;
halfWidth = myWidth / 2;
maxW = $(self.el).find('.x-axis-div').width();
if ((startX + halfWidth) < myX && maxW > (myX + halfWidth)) {
startX = myX + halfWidth;
return self.xAxisFormatter(d);
} else {
d3.select(this.parentNode).remove();
// if longer than maxRotatedLength, truncate
if (length > maxRotatedLength) {
self._attr.xAxisLabelHt = maxRotatedLength;
}
});
if (self._attr.isRotated) {
text
.text(function truncate() {
return self.truncateLabel(this, self._attr.xAxisLabelHt);
})
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '-.60em')
.attr('transform', function rotate() {
return 'rotate(-90)';
})
.append('title')
.text(text => text);
selection.select('svg')
.attr('height', self._attr.xAxisLabelHt);
}
};
};
};
/**
* Returns a function that adjusts axis titles and
* chart title transforms to fit axis label divs.
* Sets transform of x-axis-title to fit .x-axis-title div width
* if x-axis-chart-titles, set transform of x-axis-chart-titles
* to fit .chart-title div width
*
* @method fitTitles
* @returns {Function}
*/
XAxis.prototype.fitTitles = function () {
let visEls = $('.vis-wrapper');
let xAxisChartTitle;
let yAxisChartTitle;
let text;
let titles;
/**
* Returns a string that is truncated to fit size
*
* @method truncateLabel
* @param text {HTMLElement}
* @param size {Number}
* @returns {*|jQuery}
*/
truncateLabel(text, size) {
let node = d3.select(text).node();
let str = $(node).text();
let width = node.getBBox().width;
let chars = str.length;
let pxPerChar = width / chars;
let endChar = 0;
let ellipsesPad = 4;
return function () {
if (width > size) {
endChar = Math.floor((size / pxPerChar) - ellipsesPad);
while (str[endChar - 1] === ' ' || str[endChar - 1] === '-' || str[endChar - 1] === ',') {
endChar = endChar - 1;
}
str = str.substr(0, endChar) + '...';
}
return str;
};
visEls.each(function () {
let visEl = d3.select(this);
let $visEl = $(this);
let xAxisTitle = $visEl.find('.x-axis-title');
let yAxisTitle = $visEl.find('.y-axis-title');
let titleWidth = xAxisTitle.width();
let titleHeight = yAxisTitle.height();
/**
* Filter out text labels by width and position on axis
* trims labels that would overlap each other
* or extend past left or right edges
* if prev label pos (or 0) + half of label width is < label pos
* and label pos + half width is not > width of axis
*
* @method filterAxisLabels
* @returns {Function}
*/
filterAxisLabels() {
let self = this;
let startX = 0;
let maxW;
let par;
let myX;
let myWidth;
let halfWidth;
let padding = 1.1;
text = visEl.select('.x-axis-title')
.select('svg')
.attr('width', titleWidth)
.select('text')
.attr('transform', 'translate(' + (titleWidth / 2) + ',11)');
return function (selection) {
selection.selectAll('.tick text')
.text(function (d) {
par = d3.select(this.parentNode).node();
myX = self.xScale(d);
myWidth = par.getBBox().width * padding;
halfWidth = myWidth / 2;
maxW = $(self.el).find('.x-axis-div').width();
text = visEl.select('.y-axis-title')
.select('svg')
.attr('height', titleHeight)
.select('text')
.attr('transform', 'translate(11,' + (titleHeight / 2) + ')rotate(-90)');
if ((startX + halfWidth) < myX && maxW > (myX + halfWidth)) {
startX = myX + halfWidth;
return self.xAxisFormatter(d);
} else {
d3.select(this.parentNode).remove();
}
});
};
};
if ($visEl.find('.x-axis-chart-title').length) {
xAxisChartTitle = $visEl.find('.x-axis-chart-title');
titleWidth = xAxisChartTitle.find('.chart-title').width();
/**
* Returns a function that adjusts axis titles and
* chart title transforms to fit axis label divs.
* Sets transform of x-axis-title to fit .x-axis-title div width
* if x-axis-chart-titles, set transform of x-axis-chart-titles
* to fit .chart-title div width
*
* @method fitTitles
* @returns {Function}
*/
fitTitles() {
let visEls = $('.vis-wrapper');
let xAxisChartTitle;
let yAxisChartTitle;
let text;
let titles;
titles = visEl.select('.x-axis-chart-title').selectAll('.chart-title');
titles.each(function () {
text = d3.select(this)
return function () {
visEls.each(function () {
let visEl = d3.select(this);
let $visEl = $(this);
let xAxisTitle = $visEl.find('.x-axis-title');
let yAxisTitle = $visEl.find('.y-axis-title');
let titleWidth = xAxisTitle.width();
let titleHeight = yAxisTitle.height();
text = visEl.select('.x-axis-title')
.select('svg')
.attr('width', titleWidth)
.select('text')
.attr('transform', 'translate(' + (titleWidth / 2) + ',11)');
});
}
if ($visEl.find('.y-axis-chart-title').length) {
yAxisChartTitle = $visEl.find('.y-axis-chart-title');
titleHeight = yAxisChartTitle.find('.chart-title').height();
titles = visEl.select('.y-axis-chart-title').selectAll('.chart-title');
titles.each(function () {
text = d3.select(this)
text = visEl.select('.y-axis-title')
.select('svg')
.attr('height', titleHeight)
.select('text')
.attr('transform', 'translate(11,' + (titleHeight / 2) + ')rotate(-90)');
});
}
if ($visEl.find('.x-axis-chart-title').length) {
xAxisChartTitle = $visEl.find('.x-axis-chart-title');
titleWidth = xAxisChartTitle.find('.chart-title').width();
titles = visEl.select('.x-axis-chart-title').selectAll('.chart-title');
titles.each(function () {
text = d3.select(this)
.select('svg')
.attr('width', titleWidth)
.select('text')
.attr('transform', 'translate(' + (titleWidth / 2) + ',11)');
});
}
if ($visEl.find('.y-axis-chart-title').length) {
yAxisChartTitle = $visEl.find('.y-axis-chart-title');
titleHeight = yAxisChartTitle.find('.chart-title').height();
titles = visEl.select('.y-axis-chart-title').selectAll('.chart-title');
titles.each(function () {
text = d3.select(this)
.select('svg')
.attr('height', titleHeight)
.select('text')
.attr('transform', 'translate(11,' + (titleHeight / 2) + ')rotate(-90)');
});
}
});
};
};
/**
* Appends div to make .y-axis-spacer-block
* match height of .x-axis-wrapper
*
* @method updateXaxisHeight
*/
updateXaxisHeight() {
let selection = d3.select(this.el).selectAll('.vis-wrapper');
selection.each(function () {
let visEl = d3.select(this);
if (visEl.select('.inner-spacer-block').node() === null) {
visEl.select('.y-axis-spacer-block')
.append('div')
.attr('class', 'inner-spacer-block');
}
let xAxisHt = visEl.select('.x-axis-wrapper').style('height');
visEl.select('.inner-spacer-block').style('height', xAxisHt);
});
};
};
/**
* Appends div to make .y-axis-spacer-block
* match height of .x-axis-wrapper
*
* @method updateXaxisHeight
*/
XAxis.prototype.updateXaxisHeight = function () {
let selection = d3.select(this.el).selectAll('.vis-wrapper');
selection.each(function () {
let visEl = d3.select(this);
if (visEl.select('.inner-spacer-block').node() === null) {
visEl.select('.y-axis-spacer-block')
.append('div')
.attr('class', 'inner-spacer-block');
}
let xAxisHt = visEl.select('.x-axis-wrapper').style('height');
visEl.select('.inner-spacer-block').style('height', xAxisHt);
});
};
}
return XAxis;
};

View file

@ -14,221 +14,223 @@ export default function YAxisFactory(Private) {
* @constructor
* @param args {{el: (HTMLElement), yMax: (Number), _attr: (Object|*)}}
*/
_.class(YAxis).inherits(ErrorHandler);
function YAxis(args) {
this.el = args.el;
this.scale = null;
this.domain = [args.yMin, args.yMax];
this.yAxisFormatter = args.yAxisFormatter;
this._attr = args._attr || {};
}
class YAxis extends ErrorHandler {
constructor(args) {
super();
this.el = args.el;
this.scale = null;
this.domain = [args.yMin, args.yMax];
this.yAxisFormatter = args.yAxisFormatter;
this._attr = args._attr || {};
}
/**
* Renders the y axis
*
* @method render
* @return {D3.UpdateSelection} Renders y axis to visualization
*/
YAxis.prototype.render = function () {
d3.select(this.el).selectAll('.y-axis-div').call(this.draw());
};
/**
* Renders the y axis
*
* @method render
* @return {D3.UpdateSelection} Renders y axis to visualization
*/
render() {
d3.select(this.el).selectAll('.y-axis-div').call(this.draw());
};
YAxis.prototype._isPercentage = function () {
return (this._attr.mode === 'percentage');
};
_isPercentage() {
return (this._attr.mode === 'percentage');
};
YAxis.prototype._isUserDefined = function () {
return (this._attr.setYExtents);
};
_isUserDefined() {
return (this._attr.setYExtents);
};
YAxis.prototype._isYExtents = function () {
return (this._attr.defaultYExtents);
};
_isYExtents() {
return (this._attr.defaultYExtents);
};
YAxis.prototype._validateUserExtents = function (domain) {
let self = this;
_validateUserExtents(domain) {
let self = this;
return domain.map(function (val) {
val = parseInt(val, 10);
return domain.map(function (val) {
val = parseInt(val, 10);
if (isNaN(val)) throw new Error(val + ' is not a valid number');
if (self._isPercentage() && self._attr.setYExtents) return val / 100;
return val;
});
};
YAxis.prototype._getExtents = function (domain) {
let min = domain[0];
let max = domain[1];
if (this._isUserDefined()) return this._validateUserExtents(domain);
if (this._isYExtents()) return domain;
if (this._attr.scale === 'log') return this._logDomain(min, max); // Negative values cannot be displayed with a log scale.
if (!this._isYExtents() && !this._isUserDefined()) return [Math.min(0, min), Math.max(0, max)];
return domain;
};
YAxis.prototype._throwCustomError = function (message) {
throw new Error(message);
};
YAxis.prototype._throwLogScaleValuesError = function () {
throw new errors.InvalidLogScaleValues();
};
/**
* Returns the appropriate D3 scale
*
* @param fnName {String} D3 scale
* @returns {*}
*/
YAxis.prototype._getScaleType = function (fnName) {
if (fnName === 'square root') fnName = 'sqrt'; // Rename 'square root' to 'sqrt'
fnName = fnName || 'linear';
if (typeof d3.scale[fnName] !== 'function') return this._throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function');
return d3.scale[fnName]();
};
/**
* Return the domain for log scale, i.e. the extent of the log scale.
* Log scales must begin at 1 since the log(0) = -Infinity
*
* @param {Number} min
* @param {Number} max
* @returns {Array}
*/
YAxis.prototype._logDomain = function (min, max) {
if (min < 0 || max < 0) return this._throwLogScaleValuesError();
return [1, max];
};
/**
* Creates the d3 y scale function
*
* @method getYScale
* @param height {Number} DOM Element height
* @returns {D3.Scale.QuantitiveScale|*} D3 yScale function
*/
YAxis.prototype.getYScale = function (height) {
let scale = this._getScaleType(this._attr.scale);
let domain = this._getExtents(this.domain);
this.yScale = scale
.domain(domain)
.range([height, 0]);
if (!this._isUserDefined()) this.yScale.nice(); // round extents when not user defined
// Prevents bars from going off the chart when the y extents are within the domain range
if (this._attr.type === 'histogram') this.yScale.clamp(true);
return this.yScale;
};
YAxis.prototype.getScaleType = function () {
return this._attr.scale;
};
YAxis.prototype.tickFormat = function () {
let isPercentage = this._attr.mode === 'percentage';
if (isPercentage) return d3.format('%');
if (this.yAxisFormatter) return this.yAxisFormatter;
return d3.format('n');
};
YAxis.prototype._validateYScale = function (yScale) {
if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale);
};
/**
* Creates the d3 y axis function
*
* @method getYAxis
* @param height {Number} DOM Element height
* @returns {D3.Svg.Axis|*} D3 yAxis function
*/
YAxis.prototype.getYAxis = function (height) {
let yScale = this.getYScale(height);
this._validateYScale(yScale);
// Create the d3 yAxis function
this.yAxis = d3.svg.axis()
.scale(yScale)
.tickFormat(this.tickFormat(this.domain))
.ticks(this.tickScale(height))
.orient('left');
return this.yAxis;
};
/**
* Create a tick scale for the y axis that modifies the number of ticks
* based on the height of the wrapping DOM element
* Avoid using even numbers in the yTickScale.range
* Causes the top most tickValue in the chart to be missing
*
* @method tickScale
* @param height {Number} DOM element height
* @returns {number} Number of y axis ticks
*/
YAxis.prototype.tickScale = function (height) {
let yTickScale = d3.scale.linear()
.clamp(true)
.domain([20, 40, 1000])
.range([0, 3, 11]);
return Math.ceil(yTickScale(height));
};
/**
* Renders the y axis to the visualization
*
* @method draw
* @returns {Function} Renders y axis to visualization
*/
YAxis.prototype.draw = function () {
let self = this;
let margin = this._attr.margin;
let mode = this._attr.mode;
let isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette');
return function (selection) {
selection.each(function () {
let el = this;
let div = d3.select(el);
let width = $(el).parent().width();
let height = $(el).height();
let adjustedHeight = height - margin.top - margin.bottom;
// Validate whether width and height are not 0 or `NaN`
self.validateWidthandHeight(width, adjustedHeight);
let yAxis = self.getYAxis(adjustedHeight);
// The yAxis should not appear if mode is set to 'wiggle' or 'silhouette'
if (!isWiggleOrSilhouette) {
// Append svg and y axis
let svg = div.append('svg')
.attr('width', width)
.attr('height', height);
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')')
.call(yAxis);
let container = svg.select('g.y.axis').node();
if (container) {
let cWidth = Math.max(width, container.getBBox().width);
svg.attr('width', cWidth);
svg.select('g')
.attr('transform', 'translate(' + (cWidth - 2) + ',' + margin.top + ')');
}
}
if (isNaN(val)) throw new Error(val + ' is not a valid number');
if (self._isPercentage() && self._attr.setYExtents) return val / 100;
return val;
});
};
};
_getExtents(domain) {
let min = domain[0];
let max = domain[1];
if (this._isUserDefined()) return this._validateUserExtents(domain);
if (this._isYExtents()) return domain;
if (this._attr.scale === 'log') return this._logDomain(min, max); // Negative values cannot be displayed with a log scale.
if (!this._isYExtents() && !this._isUserDefined()) return [Math.min(0, min), Math.max(0, max)];
return domain;
};
_throwCustomError(message) {
throw new Error(message);
};
_throwLogScaleValuesError() {
throw new errors.InvalidLogScaleValues();
};
/**
* Returns the appropriate D3 scale
*
* @param fnName {String} D3 scale
* @returns {*}
*/
_getScaleType(fnName) {
if (fnName === 'square root') fnName = 'sqrt'; // Rename 'square root' to 'sqrt'
fnName = fnName || 'linear';
if (typeof d3.scale[fnName] !== 'function') return this._throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function');
return d3.scale[fnName]();
};
/**
* Return the domain for log scale, i.e. the extent of the log scale.
* Log scales must begin at 1 since the log(0) = -Infinity
*
* @param {Number} min
* @param {Number} max
* @returns {Array}
*/
_logDomain(min, max) {
if (min < 0 || max < 0) return this._throwLogScaleValuesError();
return [1, max];
};
/**
* Creates the d3 y scale function
*
* @method getYScale
* @param height {Number} DOM Element height
* @returns {D3.Scale.QuantitiveScale|*} D3 yScale function
*/
getYScale(height) {
let scale = this._getScaleType(this._attr.scale);
let domain = this._getExtents(this.domain);
this.yScale = scale
.domain(domain)
.range([height, 0]);
if (!this._isUserDefined()) this.yScale.nice(); // round extents when not user defined
// Prevents bars from going off the chart when the y extents are within the domain range
if (this._attr.type === 'histogram') this.yScale.clamp(true);
return this.yScale;
};
getScaleType() {
return this._attr.scale;
};
tickFormat() {
let isPercentage = this._attr.mode === 'percentage';
if (isPercentage) return d3.format('%');
if (this.yAxisFormatter) return this.yAxisFormatter;
return d3.format('n');
};
_validateYScale(yScale) {
if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale);
};
/**
* Creates the d3 y axis function
*
* @method getYAxis
* @param height {Number} DOM Element height
* @returns {D3.Svg.Axis|*} D3 yAxis function
*/
getYAxis(height) {
let yScale = this.getYScale(height);
this._validateYScale(yScale);
// Create the d3 yAxis function
this.yAxis = d3.svg.axis()
.scale(yScale)
.tickFormat(this.tickFormat(this.domain))
.ticks(this.tickScale(height))
.orient('left');
return this.yAxis;
};
/**
* Create a tick scale for the y axis that modifies the number of ticks
* based on the height of the wrapping DOM element
* Avoid using even numbers in the yTickScale.range
* Causes the top most tickValue in the chart to be missing
*
* @method tickScale
* @param height {Number} DOM element height
* @returns {number} Number of y axis ticks
*/
tickScale(height) {
let yTickScale = d3.scale.linear()
.clamp(true)
.domain([20, 40, 1000])
.range([0, 3, 11]);
return Math.ceil(yTickScale(height));
};
/**
* Renders the y axis to the visualization
*
* @method draw
* @returns {Function} Renders y axis to visualization
*/
draw() {
let self = this;
let margin = this._attr.margin;
let mode = this._attr.mode;
let isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette');
return function (selection) {
selection.each(function () {
let el = this;
let div = d3.select(el);
let width = $(el).parent().width();
let height = $(el).height();
let adjustedHeight = height - margin.top - margin.bottom;
// Validate whether width and height are not 0 or `NaN`
self.validateWidthandHeight(width, adjustedHeight);
let yAxis = self.getYAxis(adjustedHeight);
// The yAxis should not appear if mode is set to 'wiggle' or 'silhouette'
if (!isWiggleOrSilhouette) {
// Append svg and y axis
let svg = div.append('svg')
.attr('width', width)
.attr('height', height);
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')')
.call(yAxis);
let container = svg.select('g.y.axis').node();
if (container) {
let cWidth = Math.max(width, container.getBBox().width);
svg.attr('width', cWidth);
svg.select('g')
.attr('transform', 'translate(' + (cWidth - 2) + ',' + margin.top + ')');
}
}
});
};
};
}
return YAxis;
};

View file

@ -23,164 +23,162 @@ export default function VisFactory(Private) {
* @param $el {HTMLElement} jQuery selected HTML element
* @param config {Object} Parameters that define the chart type and chart options
*/
_.class(Vis).inherits(Events);
function Vis($el, config) {
if (!(this instanceof Vis)) {
return new Vis($el, config);
}
Vis.Super.apply(this, arguments);
this.el = $el.get ? $el.get(0) : $el;
this.binder = new Binder();
this.ChartClass = chartTypes[config.type];
this._attr = _.defaults({}, config || {}, {
legendOpen: true
});
class Vis extends Events {
constructor($el, config) {
super(arguments);
this.el = $el.get ? $el.get(0) : $el;
this.binder = new Binder();
this.ChartClass = chartTypes[config.type];
this._attr = _.defaults({}, config || {}, {
legendOpen: true
});
// bind the resize function so it can be used as an event handler
this.resize = _.bind(this.resize, this);
this.resizeChecker = new ResizeChecker(this.el);
this.binder.on(this.resizeChecker, 'resize', this.resize);
}
/**
* Renders the visualization
*
* @method render
* @param data {Object} Elasticsearch query results
*/
Vis.prototype.render = function (data, uiState) {
let chartType = this._attr.type;
if (!data) {
throw new Error('No valid data!');
// bind the resize function so it can be used as an event handler
this.resize = _.bind(this.resize, this);
this.resizeChecker = new ResizeChecker(this.el);
this.binder.on(this.resizeChecker, 'resize', this.resize);
}
if (this.handler) {
this.data = null;
this._runOnHandler('destroy');
}
/**
* Renders the visualization
*
* @method render
* @param data {Object} Elasticsearch query results
*/
render(data, uiState) {
let chartType = this._attr.type;
this.data = data;
if (!this.uiState) {
this.uiState = uiState;
uiState.on('change', this._uiStateChangeHandler = () => this.render(this.data, this.uiState));
}
this.handler = handlerTypes[chartType](this) || handlerTypes.column(this);
this._runOnHandler('render');
};
/**
* Resizes the visualization
*
* @method resize
*/
Vis.prototype.resize = function () {
if (!this.data) {
// TODO: need to come up with a solution for resizing when no data is available
return;
}
if (this.handler && _.isFunction(this.handler.resize)) {
this._runOnHandler('resize');
} else {
this.render(this.data, this.uiState);
}
};
Vis.prototype._runOnHandler = function (method) {
try {
this.handler[method]();
} catch (error) {
if (error instanceof errors.KbnError) {
error.displayToScreen(this.handler);
} else {
throw error;
if (!data) {
throw new Error('No valid data!');
}
}
};
if (this.handler) {
this.data = null;
this._runOnHandler('destroy');
}
/**
* Destroys the visualization
* Removes chart and all elements associated with it.
* Removes chart and all elements associated with it.
* Remove event listeners and pass destroy call down to owned objects.
*
* @method destroy
*/
Vis.prototype.destroy = function () {
let selection = d3.select(this.el).select('.vis-wrapper');
this.data = data;
this.binder.destroy();
this.resizeChecker.destroy();
if (this.uiState) this.uiState.off('change', this._uiStateChangeHandler);
if (this.handler) this._runOnHandler('destroy');
if (!this.uiState) {
this.uiState = uiState;
uiState.on('change', this._uiStateChangeHandler = () => this.render(this.data, this.uiState));
}
selection.remove();
selection = null;
};
this.handler = handlerTypes[chartType](this) || handlerTypes.column(this);
this._runOnHandler('render');
};
/**
* Sets attributes on the visualization
*
* @method set
* @param name {String} An attribute name
* @param val {*} Value to which the attribute name is set
*/
Vis.prototype.set = function (name, val) {
this._attr[name] = val;
this.render(this.data, this.uiState);
};
/**
* Resizes the visualization
*
* @method resize
*/
resize() {
if (!this.data) {
// TODO: need to come up with a solution for resizing when no data is available
return;
}
/**
* Gets attributes from the visualization
*
* @method get
* @param name {String} An attribute name
* @returns {*} The value of the attribute name
*/
Vis.prototype.get = function (name) {
return this._attr[name];
};
if (this.handler && _.isFunction(this.handler.resize)) {
this._runOnHandler('resize');
} else {
this.render(this.data, this.uiState);
}
};
/**
* Turns on event listeners.
*
* @param event {String}
* @param listener{Function}
* @returns {*}
*/
Vis.prototype.on = function (event, listener) {
let first = this.listenerCount(event) === 0;
let ret = Events.prototype.on.call(this, event, listener);
let added = this.listenerCount(event) > 0;
_runOnHandler(method) {
try {
this.handler[method]();
} catch (error) {
// if this is the first listener added for the event
// enable the event in the handler
if (first && added && this.handler) this.handler.enable(event);
if (error instanceof errors.KbnError) {
error.displayToScreen(this.handler);
} else {
throw error;
}
return ret;
};
}
};
/**
* Turns off event listeners.
*
* @param event {String}
* @param listener{Function}
* @returns {*}
*/
Vis.prototype.off = function (event, listener) {
let last = this.listenerCount(event) === 1;
let ret = Events.prototype.off.call(this, event, listener);
let removed = this.listenerCount(event) === 0;
/**
* Destroys the visualization
* Removes chart and all elements associated with it.
* Removes chart and all elements associated with it.
* Remove event listeners and pass destroy call down to owned objects.
*
* @method destroy
*/
destroy() {
let selection = d3.select(this.el).select('.vis-wrapper');
// Once all listeners are removed, disable the events in the handler
if (last && removed && this.handler) this.handler.disable(event);
return ret;
};
this.binder.destroy();
this.resizeChecker.destroy();
if (this.uiState) this.uiState.off('change', this._uiStateChangeHandler);
if (this.handler) this._runOnHandler('destroy');
selection.remove();
selection = null;
};
/**
* Sets attributes on the visualization
*
* @method set
* @param name {String} An attribute name
* @param val {*} Value to which the attribute name is set
*/
set(name, val) {
this._attr[name] = val;
this.render(this.data, this.uiState);
};
/**
* Gets attributes from the visualization
*
* @method get
* @param name {String} An attribute name
* @returns {*} The value of the attribute name
*/
get(name) {
return this._attr[name];
};
/**
* Turns on event listeners.
*
* @param event {String}
* @param listener{Function}
* @returns {*}
*/
on(event, listener) {
let first = this.listenerCount(event) === 0;
let ret = Events.prototype.on.call(this, event, listener);
let added = this.listenerCount(event) > 0;
// if this is the first listener added for the event
// enable the event in the handler
if (first && added && this.handler) this.handler.enable(event);
return ret;
};
/**
* Turns off event listeners.
*
* @param event {String}
* @param listener{Function}
* @returns {*}
*/
off(event, listener) {
let last = this.listenerCount(event) === 1;
let ret = Events.prototype.off.call(this, event, listener);
let removed = this.listenerCount(event) === 0;
// Once all listeners are removed, disable the events in the handler
if (last && removed && this.handler) this.handler.disable(event);
return ret;
};
}
return Vis;
};

View file

@ -18,80 +18,78 @@ export default function ChartBaseClass(Private) {
* @param el {HTMLElement} HTML element to which the chart will be appended
* @param chartData {Object} Elasticsearch query results for this specific chart
*/
function Chart(handler, el, chartData) {
if (!(this instanceof Chart)) {
return new Chart(handler, el, chartData);
class Chart {
constructor(handler, el, chartData) {
this.handler = handler;
this.chartEl = el;
this.chartData = chartData;
this.tooltips = [];
let events = this.events = new Dispatch(handler);
if (_.get(this.handler, '_attr.addTooltip')) {
let $el = this.handler.el;
let formatter = this.handler.data.get('tooltipFormatter');
// Add tooltip
this.tooltip = new Tooltip('chart', $el, formatter, events);
this.tooltips.push(this.tooltip);
}
this._attr = _.defaults(this.handler._attr || {}, {});
this._addIdentifier = _.bind(this._addIdentifier, this);
}
this.handler = handler;
this.chartEl = el;
this.chartData = chartData;
this.tooltips = [];
/**
* Renders the chart(s)
*
* @method render
* @returns {HTMLElement} Contains the D3 chart
*/
render() {
let selection = d3.select(this.chartEl);
let events = this.events = new Dispatch(handler);
selection.selectAll('*').remove();
selection.call(this.draw());
};
if (_.get(this.handler, '_attr.addTooltip')) {
let $el = this.handler.el;
let formatter = this.handler.data.get('tooltipFormatter');
/**
* Append the data label to the element
*
* @method _addIdentifier
* @param selection {Object} d3 select object
*/
_addIdentifier(selection, labelProp) {
labelProp = labelProp || 'label';
let labels = this.handler.data.labels;
// Add tooltip
this.tooltip = new Tooltip('chart', $el, formatter, events);
this.tooltips.push(this.tooltip);
}
function resolveLabel(datum) {
if (labels.length === 1) return labels[0];
if (datum[0]) return datum[0][labelProp];
return datum[labelProp];
}
this._attr = _.defaults(this.handler._attr || {}, {});
this._addIdentifier = _.bind(this._addIdentifier, this);
selection.each(function (datum) {
let label = resolveLabel(datum);
if (label != null) dataLabel(this, label);
});
};
/**
* Removes all DOM elements from the root element
*
* @method destroy
*/
destroy() {
let selection = d3.select(this.chartEl);
this.events.removeAllListeners();
this.tooltips.forEach(function (tooltip) {
tooltip.destroy();
});
selection.remove();
selection = null;
};
}
/**
* Renders the chart(s)
*
* @method render
* @returns {HTMLElement} Contains the D3 chart
*/
Chart.prototype.render = function () {
let selection = d3.select(this.chartEl);
selection.selectAll('*').remove();
selection.call(this.draw());
};
/**
* Append the data label to the element
*
* @method _addIdentifier
* @param selection {Object} d3 select object
*/
Chart.prototype._addIdentifier = function (selection, labelProp) {
labelProp = labelProp || 'label';
let labels = this.handler.data.labels;
function resolveLabel(datum) {
if (labels.length === 1) return labels[0];
if (datum[0]) return datum[0][labelProp];
return datum[labelProp];
}
selection.each(function (datum) {
let label = resolveLabel(datum);
if (label != null) dataLabel(this, label);
});
};
/**
* Removes all DOM elements from the root element
*
* @method destroy
*/
Chart.prototype.destroy = function () {
let selection = d3.select(this.chartEl);
this.events.removeAllListeners();
this.tooltips.forEach(function (tooltip) {
tooltip.destroy();
});
selection.remove();
selection = null;
};
return Chart;
};

View file

@ -41,285 +41,287 @@ export default function MapFactory(Private, tilemap, $sanitize) {
* @param chartData {Object} Elasticsearch query results for this map
* @param params {Object} Parameters used to build a map
*/
function TileMapMap(container, chartData, params) {
this._container = $(container).get(0);
this._chartData = chartData;
class TileMapMap {
constructor(container, chartData, params) {
this._container = $(container).get(0);
this._chartData = chartData;
// keep a reference to all of the optional params
this._events = _.get(params, 'events');
this._markerType = markerTypes[params.markerType] ? params.markerType : defaultMarkerType;
this._valueFormatter = params.valueFormatter || _.identity;
this._tooltipFormatter = params.tooltipFormatter || _.identity;
this._geoJson = _.get(this._chartData, 'geoJson');
this._mapZoom = Math.max(Math.min(params.zoom || defaultMapZoom, tilemapOptions.maxZoom), tilemapOptions.minZoom);
this._mapCenter = params.center || defaultMapCenter;
this._attr = params.attr || {};
// keep a reference to all of the optional params
this._events = _.get(params, 'events');
this._markerType = markerTypes[params.markerType] ? params.markerType : defaultMarkerType;
this._valueFormatter = params.valueFormatter || _.identity;
this._tooltipFormatter = params.tooltipFormatter || _.identity;
this._geoJson = _.get(this._chartData, 'geoJson');
this._mapZoom = Math.max(Math.min(params.zoom || defaultMapZoom, tilemapOptions.maxZoom), tilemapOptions.minZoom);
this._mapCenter = params.center || defaultMapCenter;
this._attr = params.attr || {};
let mapOptions = {
minZoom: tilemapOptions.minZoom,
maxZoom: tilemapOptions.maxZoom,
noWrap: true,
maxBounds: L.latLngBounds([-90, -220], [90, 220]),
scrollWheelZoom: false,
fadeAnimation: false,
};
let mapOptions = {
minZoom: tilemapOptions.minZoom,
maxZoom: tilemapOptions.maxZoom,
noWrap: true,
maxBounds: L.latLngBounds([-90, -220], [90, 220]),
scrollWheelZoom: false,
fadeAnimation: false,
};
this._createMap(mapOptions);
}
TileMapMap.prototype.addBoundingControl = function () {
if (this._boundingControl) return;
let self = this;
let drawOptions = { draw: {} };
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
if (self._events && !self._events.listenerCount(drawShape)) {
drawOptions.draw[drawShape] = false;
} else {
drawOptions.draw[drawShape] = {
shapeOptions: {
stroke: false,
color: '#000'
}
};
}
});
this._boundingControl = new L.Control.Draw(drawOptions);
this.map.addControl(this._boundingControl);
};
TileMapMap.prototype.addFitControl = function () {
if (this._fitControl) return;
let self = this;
let fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
// Add button to fit container to points
let FitControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function (map) {
$(fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
.on('click', function (e) {
e.preventDefault();
self._fitBounds();
});
return fitContainer;
},
onRemove: function (map) {
$(fitContainer).off('click');
}
});
this._fitControl = new FitControl();
this.map.addControl(this._fitControl);
};
/**
* Adds label div to each map when data is split
*
* @method addTitle
* @param mapLabel {String}
* @return {undefined}
*/
TileMapMap.prototype.addTitle = function (mapLabel) {
if (this._label) return;
let label = this._label = L.control();
label.onAdd = function () {
this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label');
this.update();
return this._div;
};
label.update = function () {
this._div.innerHTML = '<h2>' + _.escape(mapLabel) + '</h2>';
};
// label.addTo(this.map);
this.map.addControl(label);
};
/**
* remove css class for desat filters on map tiles
*
* @method saturateTiles
* @return undefined
*/
TileMapMap.prototype.saturateTiles = function () {
if (!this._attr.isDesaturated) {
$('img.leaflet-tile-loaded').addClass('filters-off');
this._createMap(mapOptions);
}
};
TileMapMap.prototype.updateSize = function () {
this.map.invalidateSize({
debounceMoveend: true
});
};
addBoundingControl() {
if (this._boundingControl) return;
TileMapMap.prototype.destroy = function () {
if (this._label) this._label.removeFrom(this.map);
if (this._fitControl) this._fitControl.removeFrom(this.map);
if (this._boundingControl) this._boundingControl.removeFrom(this.map);
if (this._markers) this._markers.destroy();
this.map.remove();
this.map = undefined;
};
let self = this;
let drawOptions = {draw: {}};
/**
* Switch type of data overlay for map:
* creates featurelayer from mapData (geoJson)
*
* @method _addMarkers
*/
TileMapMap.prototype._addMarkers = function () {
if (!this._geoJson) return;
if (this._markers) this._markers.destroy();
this._markers = this._createMarkers({
tooltipFormatter: this._tooltipFormatter,
valueFormatter: this._valueFormatter,
attr: this._attr
});
if (this._geoJson.features.length > 1) {
this._markers.addLegend();
}
};
/**
* Create the marker instance using the given options
*
* @method _createMarkers
* @param options {Object} options to give to marker class
* @return {Object} marker layer
*/
TileMapMap.prototype._createMarkers = function (options) {
let MarkerType = markerTypes[this._markerType];
return new MarkerType(this.map, this._geoJson, options);
};
TileMapMap.prototype._attachEvents = function () {
let self = this;
let saturateTiles = self.saturateTiles.bind(self);
this._tileLayer.on('tileload', saturateTiles);
this.map.on('unload', function () {
self._tileLayer.off('tileload', saturateTiles);
});
this.map.on('moveend', function setZoomCenter(ev) {
if (!self.map) return;
// update internal center and zoom references
const uglyCenter = self.map.getCenter();
self._mapCenter = [uglyCenter.lat, uglyCenter.lng];
self._mapZoom = self.map.getZoom();
self._addMarkers();
if (!self._events) return;
self._events.emit('mapMoveEnd', {
chart: self._chartData,
map: self.map,
center: self._mapCenter,
zoom: self._mapZoom,
});
});
this.map.on('draw:created', function (e) {
let drawType = e.layerType;
if (!self._events || !self._events.listenerCount(drawType)) return;
// TODO: Different drawTypes need differ info. Need a switch on the object creation
let bounds = e.layer.getBounds();
let SElng = bounds.getSouthEast().lng;
if (SElng > 180) {
SElng -= 360;
}
let NWlng = bounds.getNorthWest().lng;
if (NWlng < -180) {
NWlng += 360;
}
self._events.emit(drawType, {
e: e,
chart: self._chartData,
bounds: {
top_left: {
lat: bounds.getNorthWest().lat,
lon: NWlng
},
bottom_right: {
lat: bounds.getSouthEast().lat,
lon: SElng
}
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
if (self._events && !self._events.listenerCount(drawShape)) {
drawOptions.draw[drawShape] = false;
} else {
drawOptions.draw[drawShape] = {
shapeOptions: {
stroke: false,
color: '#000'
}
};
}
});
});
this.map.on('zoomend', function () {
if (!self.map) return;
self._mapZoom = self.map.getZoom();
if (!self._events) return;
this._boundingControl = new L.Control.Draw(drawOptions);
this.map.addControl(this._boundingControl);
};
self._events.emit('mapZoomEnd', {
chart: self._chartData,
map: self.map,
zoom: self._mapZoom,
addFitControl() {
if (this._fitControl) return;
let self = this;
let fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
// Add button to fit container to points
let FitControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function (map) {
$(fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
.on('click', function (e) {
e.preventDefault();
self._fitBounds();
});
return fitContainer;
},
onRemove: function (map) {
$(fitContainer).off('click');
}
});
});
};
TileMapMap.prototype._createMap = function (mapOptions) {
if (this.map) this.destroy();
this._fitControl = new FitControl();
this.map.addControl(this._fitControl);
};
// add map tiles layer, using the mapTiles object settings
if (this._attr.wms && this._attr.wms.enabled) {
_.assign(mapOptions, {
minZoom: 1,
maxZoom: 18
/**
* Adds label div to each map when data is split
*
* @method addTitle
* @param mapLabel {String}
* @return {undefined}
*/
addTitle(mapLabel) {
if (this._label) return;
let label = this._label = L.control();
label.onAdd = function () {
this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label');
this.update();
return this._div;
};
label.update = function () {
this._div.innerHTML = '<h2>' + _.escape(mapLabel) + '</h2>';
};
// label.addTo(this.map);
this.map.addControl(label);
};
/**
* remove css class for desat filters on map tiles
*
* @method saturateTiles
* @return undefined
*/
saturateTiles() {
if (!this._attr.isDesaturated) {
$('img.leaflet-tile-loaded').addClass('filters-off');
}
};
updateSize() {
this.map.invalidateSize({
debounceMoveend: true
});
this._tileLayer = L.tileLayer.wms(this._attr.wms.url, this._attr.wms.options);
} else {
this._tileLayer = L.tileLayer(mapTiles.url, mapTiles.options);
}
};
// append tile layers, center and zoom to the map options
mapOptions.layers = this._tileLayer;
mapOptions.center = this._mapCenter;
mapOptions.zoom = this._mapZoom;
destroy() {
if (this._label) this._label.removeFrom(this.map);
if (this._fitControl) this._fitControl.removeFrom(this.map);
if (this._boundingControl) this._boundingControl.removeFrom(this.map);
if (this._markers) this._markers.destroy();
this.map.remove();
this.map = undefined;
};
this.map = L.map(this._container, mapOptions);
this._attachEvents();
this._addMarkers();
};
/**
* Switch type of data overlay for map:
* creates featurelayer from mapData (geoJson)
*
* @method _addMarkers
*/
_addMarkers() {
if (!this._geoJson) return;
if (this._markers) this._markers.destroy();
/**
* zoom map to fit all features in featureLayer
*
* @method _fitBounds
* @param map {Leaflet Object}
* @return {boolean}
*/
TileMapMap.prototype._fitBounds = function () {
this.map.fitBounds(this._getDataRectangles());
};
this._markers = this._createMarkers({
tooltipFormatter: this._tooltipFormatter,
valueFormatter: this._valueFormatter,
attr: this._attr
});
/**
* Get the Rectangles representing the geohash grid
*
* @return {LatLngRectangles[]}
*/
TileMapMap.prototype._getDataRectangles = function () {
if (!this._geoJson) return [];
return _.pluck(this._geoJson.features, 'properties.rectangle');
};
if (this._geoJson.features.length > 1) {
this._markers.addLegend();
}
};
/**
* Create the marker instance using the given options
*
* @method _createMarkers
* @param options {Object} options to give to marker class
* @return {Object} marker layer
*/
_createMarkers(options) {
let MarkerType = markerTypes[this._markerType];
return new MarkerType(this.map, this._geoJson, options);
};
_attachEvents() {
let self = this;
let saturateTiles = self.saturateTiles.bind(self);
this._tileLayer.on('tileload', saturateTiles);
this.map.on('unload', function () {
self._tileLayer.off('tileload', saturateTiles);
});
this.map.on('moveend', function setZoomCenter(ev) {
if (!self.map) return;
// update internal center and zoom references
const uglyCenter = self.map.getCenter();
self._mapCenter = [uglyCenter.lat, uglyCenter.lng];
self._mapZoom = self.map.getZoom();
self._addMarkers();
if (!self._events) return;
self._events.emit('mapMoveEnd', {
chart: self._chartData,
map: self.map,
center: self._mapCenter,
zoom: self._mapZoom,
});
});
this.map.on('draw:created', function (e) {
let drawType = e.layerType;
if (!self._events || !self._events.listenerCount(drawType)) return;
// TODO: Different drawTypes need differ info. Need a switch on the object creation
let bounds = e.layer.getBounds();
let SElng = bounds.getSouthEast().lng;
if (SElng > 180) {
SElng -= 360;
}
let NWlng = bounds.getNorthWest().lng;
if (NWlng < -180) {
NWlng += 360;
}
self._events.emit(drawType, {
e: e,
chart: self._chartData,
bounds: {
top_left: {
lat: bounds.getNorthWest().lat,
lon: NWlng
},
bottom_right: {
lat: bounds.getSouthEast().lat,
lon: SElng
}
}
});
});
this.map.on('zoomend', function () {
if (!self.map) return;
self._mapZoom = self.map.getZoom();
if (!self._events) return;
self._events.emit('mapZoomEnd', {
chart: self._chartData,
map: self.map,
zoom: self._mapZoom,
});
});
};
_createMap(mapOptions) {
if (this.map) this.destroy();
// add map tiles layer, using the mapTiles object settings
if (this._attr.wms && this._attr.wms.enabled) {
_.assign(mapOptions, {
minZoom: 1,
maxZoom: 18
});
this._tileLayer = L.tileLayer.wms(this._attr.wms.url, this._attr.wms.options);
} else {
this._tileLayer = L.tileLayer(mapTiles.url, mapTiles.options);
}
// append tile layers, center and zoom to the map options
mapOptions.layers = this._tileLayer;
mapOptions.center = this._mapCenter;
mapOptions.zoom = this._mapZoom;
this.map = L.map(this._container, mapOptions);
this._attachEvents();
this._addMarkers();
};
/**
* zoom map to fit all features in featureLayer
*
* @method _fitBounds
* @param map {Leaflet Object}
* @return {boolean}
*/
_fitBounds() {
this.map.fitBounds(this._getDataRectangles());
};
/**
* Get the Rectangles representing the geohash grid
*
* @return {LatLngRectangles[]}
*/
_getDataRectangles() {
if (!this._geoJson) return [];
return _.pluck(this._geoJson.features, 'properties.rectangle');
};
}
return TileMapMap;
};

View file

@ -10,175 +10,171 @@ export default function PointSeriesChartProvider(Private) {
let Tooltip = Private(VislibComponentsTooltipProvider);
let touchdownTmpl = _.template(require('ui/vislib/partials/touchdown.tmpl.html'));
_.class(PointSeriesChart).inherits(Chart);
function PointSeriesChart(handler, chartEl, chartData) {
if (!(this instanceof PointSeriesChart)) {
return new PointSeriesChart(handler, chartEl, chartData);
class PointSeriesChart extends Chart {
constructor(handler, chartEl, chartData) {
super(handler, chartEl, chartData);
}
PointSeriesChart.Super.apply(this, arguments);
}
_stackMixedValues(stackCount) {
let currentStackOffsets = [0, 0];
let currentStackIndex = 0;
PointSeriesChart.prototype._stackMixedValues = function (stackCount) {
let currentStackOffsets = [0, 0];
let currentStackIndex = 0;
return function (d, y0, y) {
let firstStack = currentStackIndex % stackCount === 0;
let lastStack = ++currentStackIndex === stackCount;
return function (d, y0, y) {
let firstStack = currentStackIndex % stackCount === 0;
let lastStack = ++currentStackIndex === stackCount;
if (firstStack) {
currentStackOffsets = [0, 0];
}
if (firstStack) {
currentStackOffsets = [0, 0];
}
if (lastStack) currentStackIndex = 0;
if (lastStack) currentStackIndex = 0;
if (y >= 0) {
d.y0 = currentStackOffsets[1];
currentStackOffsets[1] += y;
} else {
d.y0 = currentStackOffsets[0];
currentStackOffsets[0] += y;
}
};
};
/**
* Stacks chart data values
*
* @method stackData
* @param data {Object} Elasticsearch query result for this chart
* @returns {Array} Stacked data objects with x, y, and y0 values
*/
PointSeriesChart.prototype.stackData = function (data) {
let self = this;
let isHistogram = (this._attr.type === 'histogram' && this._attr.mode === 'stacked');
let stack = this._attr.stack;
if (isHistogram) stack.out(self._stackMixedValues(data.series.length));
return stack(data.series.map(function (d) {
let label = d.label;
return d.values.map(function (e, i) {
return {
_input: e,
label: label,
x: self._attr.xValue.call(d.values, e, i),
y: self._attr.yValue.call(d.values, e, i)
};
});
}));
};
PointSeriesChart.prototype.validateDataCompliesWithScalingMethod = function (data) {
const invalidLogScale = data.series && data.series.some(valuesSmallerThanOne);
if (this._attr.scale === 'log' && invalidLogScale) {
throw new errors.InvalidLogScaleValues();
}
};
function valuesSmallerThanOne(d) {
return d.values && d.values.some(e => e.y < 1);
}
/**
* Creates rects to show buckets outside of the ordered.min and max, returns rects
*
* @param xScale {Function} D3 xScale function
* @param svg {HTMLElement} Reference to SVG
* @method createEndZones
* @returns {D3.Selection}
*/
PointSeriesChart.prototype.createEndZones = function (svg) {
let self = this;
let xAxis = this.handler.xAxis;
let xScale = xAxis.xScale;
let ordered = xAxis.ordered;
let missingMinMax = !ordered || _.isUndefined(ordered.min) || _.isUndefined(ordered.max);
if (missingMinMax || ordered.endzones === false) return;
let attr = this.handler._attr;
let height = attr.height;
let width = attr.width;
let margin = attr.margin;
let color = '#004c99';
// we don't want to draw endzones over our min and max values, they
// are still a part of the dataset. We want to start the endzones just
// outside of them so we will use these values rather than ordered.min/max
let oneUnit = (ordered.units || _.identity)(1);
let beyondMin = ordered.min - oneUnit;
let beyondMax = ordered.max + oneUnit;
// points on this axis represent the amount of time they cover,
// so draw the endzones at the actual time bounds
let leftEndzone = {
x: 0,
w: Math.max(xScale(ordered.min), 0)
};
let rightLastVal = xAxis.expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.xValues));
let rightStart = rightLastVal + oneUnit;
let rightEndzone = {
x: xScale(rightStart),
w: Math.max(width - xScale(rightStart), 0)
};
this.endzones = svg.selectAll('.layer')
.data([leftEndzone, rightEndzone])
.enter()
.insert('g', '.brush')
.attr('class', 'endzone')
.append('rect')
.attr('class', 'zone')
.attr('x', function (d) {
return d.x;
})
.attr('y', 0)
.attr('height', height - margin.top - margin.bottom)
.attr('width', function (d) {
return d.w;
});
function callPlay(event) {
let boundData = event.target.__data__;
let mouseChartXCoord = event.clientX - self.chartEl.getBoundingClientRect().left;
let wholeBucket = boundData && boundData.x != null;
// the min and max that the endzones start in
let min = leftEndzone.w;
let max = rightEndzone.x;
// bounds of the cursor to consider
let xLeft = mouseChartXCoord;
let xRight = mouseChartXCoord;
if (wholeBucket) {
xLeft = xScale(boundData.x);
xRight = xScale(xAxis.addInterval(boundData.x));
}
return {
wholeBucket: wholeBucket,
touchdown: min > xLeft || max < xRight
if (y >= 0) {
d.y0 = currentStackOffsets[1];
currentStackOffsets[1] += y;
} else {
d.y0 = currentStackOffsets[0];
currentStackOffsets[0] += y;
}
};
}
function textFormatter() {
return touchdownTmpl(callPlay(d3.event));
}
let endzoneTT = new Tooltip('endzones', this.handler.el, textFormatter, null);
this.tooltips.push(endzoneTT);
endzoneTT.order = 0;
endzoneTT.showCondition = function inEndzone() {
return callPlay(d3.event).touchdown;
};
endzoneTT.render()(svg);
};
/**
* Stacks chart data values
*
* @method stackData
* @param data {Object} Elasticsearch query result for this chart
* @returns {Array} Stacked data objects with x, y, and y0 values
*/
stackData(data) {
let self = this;
let isHistogram = (this._attr.type === 'histogram' && this._attr.mode === 'stacked');
let stack = this._attr.stack;
if (isHistogram) stack.out(self._stackMixedValues(data.series.length));
return stack(data.series.map(function (d) {
let label = d.label;
return d.values.map(function (e, i) {
return {
_input: e,
label: label,
x: self._attr.xValue.call(d.values, e, i),
y: self._attr.yValue.call(d.values, e, i)
};
});
}));
};
validateDataCompliesWithScalingMethod(data) {
const valuesSmallerThanOne = function (d) {
return d.values && d.values.some(e => e.y < 1);
};
const invalidLogScale = data.series && data.series.some(valuesSmallerThanOne);
if (this._attr.scale === 'log' && invalidLogScale) {
throw new errors.InvalidLogScaleValues();
}
};
/**
* Creates rects to show buckets outside of the ordered.min and max, returns rects
*
* @param xScale {Function} D3 xScale function
* @param svg {HTMLElement} Reference to SVG
* @method createEndZones
* @returns {D3.Selection}
*/
createEndZones(svg) {
let self = this;
let xAxis = this.handler.xAxis;
let xScale = xAxis.xScale;
let ordered = xAxis.ordered;
let missingMinMax = !ordered || _.isUndefined(ordered.min) || _.isUndefined(ordered.max);
if (missingMinMax || ordered.endzones === false) return;
let attr = this.handler._attr;
let height = attr.height;
let width = attr.width;
let margin = attr.margin;
let color = '#004c99';
// we don't want to draw endzones over our min and max values, they
// are still a part of the dataset. We want to start the endzones just
// outside of them so we will use these values rather than ordered.min/max
let oneUnit = (ordered.units || _.identity)(1);
let beyondMin = ordered.min - oneUnit;
let beyondMax = ordered.max + oneUnit;
// points on this axis represent the amount of time they cover,
// so draw the endzones at the actual time bounds
let leftEndzone = {
x: 0,
w: Math.max(xScale(ordered.min), 0)
};
let rightLastVal = xAxis.expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.xValues));
let rightStart = rightLastVal + oneUnit;
let rightEndzone = {
x: xScale(rightStart),
w: Math.max(width - xScale(rightStart), 0)
};
this.endzones = svg.selectAll('.layer')
.data([leftEndzone, rightEndzone])
.enter()
.insert('g', '.brush')
.attr('class', 'endzone')
.append('rect')
.attr('class', 'zone')
.attr('x', function (d) {
return d.x;
})
.attr('y', 0)
.attr('height', height - margin.top - margin.bottom)
.attr('width', function (d) {
return d.w;
});
function callPlay(event) {
let boundData = event.target.__data__;
let mouseChartXCoord = event.clientX - self.chartEl.getBoundingClientRect().left;
let wholeBucket = boundData && boundData.x != null;
// the min and max that the endzones start in
let min = leftEndzone.w;
let max = rightEndzone.x;
// bounds of the cursor to consider
let xLeft = mouseChartXCoord;
let xRight = mouseChartXCoord;
if (wholeBucket) {
xLeft = xScale(boundData.x);
xRight = xScale(xAxis.addInterval(boundData.x));
}
return {
wholeBucket: wholeBucket,
touchdown: min > xLeft || max < xRight
};
}
function textFormatter() {
return touchdownTmpl(callPlay(d3.event));
}
let endzoneTT = new Tooltip('endzones', this.handler.el, textFormatter, null);
this.tooltips.push(endzoneTT);
endzoneTT.order = 0;
endzoneTT.showCondition = function inEndzone() {
return callPlay(d3.event).touchdown;
};
endzoneTT.render()(svg);
};
}
return PointSeriesChart;
};

View file

@ -20,364 +20,367 @@ export default function AreaChartFactory(Private) {
* @param chartData {Object} Elasticsearch query results for this specific
* chart
*/
_.class(AreaChart).inherits(PointSeriesChart);
function AreaChart(handler, chartEl, chartData) {
if (!(this instanceof AreaChart)) {
return new AreaChart(handler, chartEl, chartData);
}
class AreaChart extends PointSeriesChart {
constructor(handler, chartEl, chartData) {
super(handler, chartEl, chartData);
AreaChart.Super.apply(this, arguments);
this.isOverlapping = (handler._attr.mode === 'overlap');
this.isOverlapping = (handler._attr.mode === 'overlap');
if (this.isOverlapping) {
if (this.isOverlapping) {
// Default opacity should return to 0.6 on mouseout
let defaultOpacity = 0.6;
handler._attr.defaultOpacity = defaultOpacity;
handler.highlight = function (element) {
let label = this.getAttribute('data-label');
if (!label) return;
// Default opacity should return to 0.6 on mouseout
let defaultOpacity = 0.6;
handler._attr.defaultOpacity = defaultOpacity;
handler.highlight = function (element) {
let label = this.getAttribute('data-label');
if (!label) return;
let highlightOpacity = 0.8;
let highlightElements = $('[data-label]', element.parentNode).filter(
function (els, el) {
return `${$(el).data('label')}` === label;
});
$('[data-label]', element.parentNode).not(highlightElements).css('opacity', defaultOpacity / 2); // half of the default opacity
highlightElements.css('opacity', highlightOpacity);
};
handler.unHighlight = function (element) {
$('[data-label]', element).css('opacity', defaultOpacity);
let highlightOpacity = 0.8;
let highlightElements = $('[data-label]', element.parentNode).filter(
function (els, el) {
return `${$(el).data('label')}` === label;
});
$('[data-label]', element.parentNode).not(highlightElements).css('opacity', defaultOpacity / 2); // half of the default opacity
highlightElements.css('opacity', highlightOpacity);
};
handler.unHighlight = function (element) {
$('[data-label]', element).css('opacity', defaultOpacity);
//The legend should keep max opacity
$('[data-label]', $(element).siblings()).css('opacity', 1);
};
}
this.checkIfEnoughData();
this._attr = _.defaults(handler._attr || {}, {
xValue: function (d) { return d.x; },
yValue: function (d) { return d.y; }
});
}
/**
* Adds SVG path to area chart
*
* @method addPath
* @param svg {HTMLElement} SVG to which rect are appended
* @param layers {Array} Chart data array
* @returns {D3.UpdateSelection} SVG with path added
*/
AreaChart.prototype.addPath = function (svg, layers) {
let self = this;
let ordered = this.handler.data.get('ordered');
let isTimeSeries = (ordered && ordered.date);
let isOverlapping = this.isOverlapping;
let color = this.handler.data.getColorFunc();
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate;
let area = d3.svg.area()
.x(function (d) {
if (isTimeSeries) {
return xScale(d.x);
}
return xScale(d.x) + xScale.rangeBand() / 2;
})
.y0(function (d) {
if (isOverlapping) {
return yScale(0);
//The legend should keep max opacity
$('[data-label]', $(element).siblings()).css('opacity', 1);
};
}
return yScale(d.y0);
})
.y1(function (d) {
if (isOverlapping) {
return yScale(d.y);
}
this.checkIfEnoughData();
return yScale(d.y0 + d.y);
})
.defined(function (d) { return !_.isNull(d.y); })
.interpolate(interpolate);
// Data layers
let layer = svg.selectAll('.layer')
.data(layers)
.enter()
.append('g')
.attr('class', function (d, i) {
return 'pathgroup ' + i;
});
// Append path
let path = layer.append('path')
.call(this._addIdentifier)
.style('fill', function (d) {
return color(d[0].label);
})
.classed('overlap_area', function () {
return isOverlapping;
});
// update
path.attr('d', function (d) {
return area(d);
});
return path;
};
/**
* Adds Events to SVG circles
*
* @method addCircleEvents
* @param element {D3.UpdateSelection} SVG circles
* @returns {D3.Selection} circles with event listeners attached
*/
AreaChart.prototype.addCircleEvents = function (element, svg) {
let events = this.events;
let isBrushable = events.isBrushable();
let brush = isBrushable ? events.addBrushEvent(svg) : undefined;
let hover = events.addHoverEvent();
let mouseout = events.addMouseoutEvent();
let click = events.addClickEvent();
let attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
}
return attachedEvents;
};
/**
* Adds SVG circles to area chart
*
* @method addCircles
* @param svg {HTMLElement} SVG to which circles are appended
* @param data {Array} Chart data array
* @returns {D3.UpdateSelection} SVG with circles added
*/
AreaChart.prototype.addCircles = function (svg, data) {
let self = this;
let color = this.handler.data.getColorFunc();
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let ordered = this.handler.data.get('ordered');
let circleRadius = 12;
let circleStrokeWidth = 0;
let tooltip = this.tooltip;
let isTooltip = this._attr.addTooltip;
let isOverlapping = this.isOverlapping;
let layer;
let circles;
layer = svg.selectAll('.points')
.data(data)
.enter()
.append('g')
.attr('class', 'points area');
// append the circles
circles = layer
.selectAll('circles')
.data(function appendData(data) {
return data.filter(function isZeroOrNull(d) {
return d.y !== 0 && !_.isNull(d.y);
this._attr = _.defaults(handler._attr || {}, {
xValue: function (d) {
return d.x;
},
yValue: function (d) {
return d.y;
}
});
});
// exit
circles.exit().remove();
// enter
circles
.enter()
.append('circle')
.call(this._addIdentifier)
.attr('stroke', function strokeColor(d) {
return color(d.label);
})
.attr('fill', 'transparent')
.attr('stroke-width', circleStrokeWidth);
// update
circles
.attr('cx', function cx(d) {
if (ordered && ordered.date) {
return xScale(d.x);
}
return xScale(d.x) + xScale.rangeBand() / 2;
})
.attr('cy', function cy(d) {
if (isOverlapping) {
return yScale(d.y);
}
return yScale(d.y0 + d.y);
})
.attr('r', circleRadius);
// Add tooltip
if (isTooltip) {
circles.call(tooltip.render());
}
return circles;
};
/**
* Adds SVG path to area chart
*
* @method addPath
* @param svg {HTMLElement} SVG to which rect are appended
* @param layers {Array} Chart data array
* @returns {D3.UpdateSelection} SVG with path added
*/
addPath(svg, layers) {
let self = this;
let ordered = this.handler.data.get('ordered');
let isTimeSeries = (ordered && ordered.date);
let isOverlapping = this.isOverlapping;
let color = this.handler.data.getColorFunc();
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate;
let area = d3.svg.area()
.x(function (d) {
if (isTimeSeries) {
return xScale(d.x);
}
return xScale(d.x) + xScale.rangeBand() / 2;
})
.y0(function (d) {
if (isOverlapping) {
return yScale(0);
}
/**
* Adds SVG clipPath
*
* @method addClipPath
* @param svg {HTMLElement} SVG to which clipPath is appended
* @param width {Number} SVG width
* @param height {Number} SVG height
* @returns {D3.UpdateSelection} SVG with clipPath added
*/
AreaChart.prototype.addClipPath = function (svg, width, height) {
// Prevents circles from being clipped at the top of the chart
let startX = 0;
let startY = 0;
let id = 'chart-area' + _.uniqueId();
return yScale(d.y0);
})
.y1(function (d) {
if (isOverlapping) {
return yScale(d.y);
}
// Creating clipPath
return svg
.attr('clip-path', 'url(#' + id + ')')
.append('clipPath')
.attr('id', id)
.append('rect')
.attr('x', startX)
.attr('y', startY)
.attr('width', width)
.attr('height', height);
};
return yScale(d.y0 + d.y);
})
.defined(function (d) {
return !_.isNull(d.y);
})
.interpolate(interpolate);
AreaChart.prototype.checkIfEnoughData = function () {
let series = this.chartData.series;
let message = 'Area charts require more than one data point. Try adding ' +
'an X-Axis Aggregation';
let notEnoughData = series.some(function (obj) {
return obj.values.length < 2;
});
if (notEnoughData) {
throw new errors.NotEnoughData(message);
}
};
AreaChart.prototype.validateWiggleSelection = function () {
let isWiggle = this._attr.mode === 'wiggle';
let ordered = this.handler.data.get('ordered');
if (isWiggle && !ordered) throw new errors.InvalidWiggleSelection();
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the area chart
*/
AreaChart.prototype.draw = function () {
// Attributes
let self = this;
let xScale = this.handler.xAxis.xScale;
let $elem = $(this.chartEl);
let margin = this._attr.margin;
let elWidth = this._attr.width = $elem.width();
let elHeight = this._attr.height = $elem.height();
let yMin = this.handler.yAxis.yMin;
let yScale = this.handler.yAxis.yScale;
let minWidth = 20;
let minHeight = 20;
let addTimeMarker = this._attr.addTimeMarker;
let times = this._attr.times || [];
let timeMarker;
let div;
let svg;
let width;
let height;
let layers;
let circles;
let path;
return function (selection) {
selection.each(function (data) {
// Stack data
layers = self.stackData(data);
// Get the width and height
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
self.validateWiggleSelection();
// Select the current DOM element
div = d3.select(this);
// Create the canvas for the visualization
svg = div.append('svg')
.attr('width', width)
.attr('height', height + margin.top + margin.bottom)
// Data layers
let layer = svg.selectAll('.layer')
.data(layers)
.enter()
.append('g')
.attr('transform', 'translate(0,' + margin.top + ')');
.attr('class', function (d, i) {
return 'pathgroup ' + i;
});
// add clipPath to hide circles when they go out of bounds
self.addClipPath(svg, width, height);
self.createEndZones(svg);
// Append path
let path = layer.append('path')
.call(this._addIdentifier)
.style('fill', function (d) {
return color(d[0].label);
})
.classed('overlap_area', function () {
return isOverlapping;
});
// add path
path = self.addPath(svg, layers);
// update
path.attr('d', function (d) {
return area(d);
});
if (yMin < 0 && self._attr.mode !== 'wiggle' && self._attr.mode !== 'silhouette') {
return path;
};
// Draw line at yScale 0 value
svg.append('line')
.attr('class', 'zero-line')
/**
* Adds Events to SVG circles
*
* @method addCircleEvents
* @param element {D3.UpdateSelection} SVG circles
* @returns {D3.Selection} circles with event listeners attached
*/
addCircleEvents(element, svg) {
let events = this.events;
let isBrushable = events.isBrushable();
let brush = isBrushable ? events.addBrushEvent(svg) : undefined;
let hover = events.addHoverEvent();
let mouseout = events.addMouseoutEvent();
let click = events.addClickEvent();
let attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
}
return attachedEvents;
};
/**
* Adds SVG circles to area chart
*
* @method addCircles
* @param svg {HTMLElement} SVG to which circles are appended
* @param data {Array} Chart data array
* @returns {D3.UpdateSelection} SVG with circles added
*/
addCircles(svg, data) {
let self = this;
let color = this.handler.data.getColorFunc();
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let ordered = this.handler.data.get('ordered');
let circleRadius = 12;
let circleStrokeWidth = 0;
let tooltip = this.tooltip;
let isTooltip = this._attr.addTooltip;
let isOverlapping = this.isOverlapping;
let layer;
let circles;
layer = svg.selectAll('.points')
.data(data)
.enter()
.append('g')
.attr('class', 'points area');
// append the circles
circles = layer
.selectAll('circles')
.data(function appendData(data) {
return data.filter(function isZeroOrNull(d) {
return d.y !== 0 && !_.isNull(d.y);
});
});
// exit
circles.exit().remove();
// enter
circles
.enter()
.append('circle')
.call(this._addIdentifier)
.attr('stroke', function strokeColor(d) {
return color(d.label);
})
.attr('fill', 'transparent')
.attr('stroke-width', circleStrokeWidth);
// update
circles
.attr('cx', function cx(d) {
if (ordered && ordered.date) {
return xScale(d.x);
}
return xScale(d.x) + xScale.rangeBand() / 2;
})
.attr('cy', function cy(d) {
if (isOverlapping) {
return yScale(d.y);
}
return yScale(d.y0 + d.y);
})
.attr('r', circleRadius);
// Add tooltip
if (isTooltip) {
circles.call(tooltip.render());
}
return circles;
};
/**
* Adds SVG clipPath
*
* @method addClipPath
* @param svg {HTMLElement} SVG to which clipPath is appended
* @param width {Number} SVG width
* @param height {Number} SVG height
* @returns {D3.UpdateSelection} SVG with clipPath added
*/
addClipPath(svg, width, height) {
// Prevents circles from being clipped at the top of the chart
let startX = 0;
let startY = 0;
let id = 'chart-area' + _.uniqueId();
// Creating clipPath
return svg
.attr('clip-path', 'url(#' + id + ')')
.append('clipPath')
.attr('id', id)
.append('rect')
.attr('x', startX)
.attr('y', startY)
.attr('width', width)
.attr('height', height);
};
checkIfEnoughData() {
let series = this.chartData.series;
let message = 'Area charts require more than one data point. Try adding ' +
'an X-Axis Aggregation';
let notEnoughData = series.some(function (obj) {
return obj.values.length < 2;
});
if (notEnoughData) {
throw new errors.NotEnoughData(message);
}
};
validateWiggleSelection() {
let isWiggle = this._attr.mode === 'wiggle';
let ordered = this.handler.data.get('ordered');
if (isWiggle && !ordered) throw new errors.InvalidWiggleSelection();
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the area chart
*/
draw() {
// Attributes
let self = this;
let xScale = this.handler.xAxis.xScale;
let $elem = $(this.chartEl);
let margin = this._attr.margin;
let elWidth = this._attr.width = $elem.width();
let elHeight = this._attr.height = $elem.height();
let yMin = this.handler.yAxis.yMin;
let yScale = this.handler.yAxis.yScale;
let minWidth = 20;
let minHeight = 20;
let addTimeMarker = this._attr.addTimeMarker;
let times = this._attr.times || [];
let timeMarker;
let div;
let svg;
let width;
let height;
let layers;
let circles;
let path;
return function (selection) {
selection.each(function (data) {
// Stack data
layers = self.stackData(data);
// Get the width and height
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
self.validateWiggleSelection();
// Select the current DOM element
div = d3.select(this);
// Create the canvas for the visualization
svg = div.append('svg')
.attr('width', width)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(0,' + margin.top + ')');
// add clipPath to hide circles when they go out of bounds
self.addClipPath(svg, width, height);
self.createEndZones(svg);
// add path
path = self.addPath(svg, layers);
if (yMin < 0 && self._attr.mode !== 'wiggle' && self._attr.mode !== 'silhouette') {
// Draw line at yScale 0 value
svg.append('line')
.attr('class', 'zero-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
}
// add circles
circles = self.addCircles(svg, layers);
// add click and hover events to circles
self.addCircleEvents(circles, svg);
// chart base line
let line = svg.append('line')
.attr('class', 'base-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
}
// add circles
circles = self.addCircles(svg, layers);
if (addTimeMarker) {
timeMarker.render(svg);
}
// add click and hover events to circles
self.addCircleEvents(circles, svg);
// chart base line
let line = svg.append('line')
.attr('class', 'base-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
return svg;
});
};
};
};
}
return AreaChart;
};

View file

@ -20,313 +20,314 @@ export default function ColumnChartFactory(Private) {
* @param el {HTMLElement} HTML element to which the chart will be appended
* @param chartData {Object} Elasticsearch query results for this specific chart
*/
_.class(ColumnChart).inherits(PointSeriesChart);
function ColumnChart(handler, chartEl, chartData) {
if (!(this instanceof ColumnChart)) {
return new ColumnChart(handler, chartEl, chartData);
}
class ColumnChart extends PointSeriesChart {
constructor(handler, chartEl, chartData) {
super(handler, chartEl, chartData);
ColumnChart.Super.apply(this, arguments);
// Column chart specific attributes
this._attr = _.defaults(handler._attr || {}, {
xValue: function (d) { return d.x; },
yValue: function (d) { return d.y; }
});
}
/**
* Adds SVG rect to Vertical Bar Chart
*
* @method addBars
* @param svg {HTMLElement} SVG to which rect are appended
* @param layers {Array} Chart data array
* @returns {D3.UpdateSelection} SVG with rect added
*/
ColumnChart.prototype.addBars = function (svg, layers) {
let self = this;
let color = this.handler.data.getColorFunc();
let tooltip = this.tooltip;
let isTooltip = this._attr.addTooltip;
let layer;
let bars;
layer = svg.selectAll('.layer')
.data(layers)
.enter().append('g')
.attr('class', function (d, i) {
return 'series ' + i;
});
bars = layer.selectAll('rect')
.data(function (d) {
return d;
});
bars
.exit()
.remove();
bars
.enter()
.append('rect')
.call(this._addIdentifier)
.attr('fill', function (d) {
return color(d.label);
});
self.updateBars(bars);
// Add tooltip
if (isTooltip) {
bars.call(tooltip.render());
}
return bars;
};
/**
* Determines whether bars are grouped or stacked and updates the D3
* selection
*
* @method updateBars
* @param bars {D3.UpdateSelection} SVG with rect added
* @returns {D3.UpdateSelection}
*/
ColumnChart.prototype.updateBars = function (bars) {
let offset = this._attr.mode;
if (offset === 'grouped') {
return this.addGroupedBars(bars);
}
return this.addStackedBars(bars);
};
/**
* Adds stacked bars to column chart visualization
*
* @method addStackedBars
* @param bars {D3.UpdateSelection} SVG with rect added
* @returns {D3.UpdateSelection}
*/
ColumnChart.prototype.addStackedBars = function (bars) {
let data = this.chartData;
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let height = yScale.range()[0];
let yMin = this.handler.yAxis.yScale.domain()[0];
let barWidth;
if (data.ordered && data.ordered.date) {
let start = data.ordered.min;
let end = moment(data.ordered.min).add(data.ordered.interval).valueOf();
barWidth = xScale(end) - xScale(start);
barWidth = barWidth - Math.min(barWidth * 0.25, 15);
}
// update
bars
.attr('x', function (d) {
return xScale(d.x);
})
.attr('width', function () {
return barWidth || xScale.rangeBand();
})
.attr('y', function (d) {
if (d.y < 0) {
return yScale(d.y0);
}
return yScale(d.y0 + d.y);
})
.attr('height', function (d) {
if (d.y < 0) {
return Math.abs(yScale(d.y0 + d.y) - yScale(d.y0));
}
// Due to an issue with D3 not returning zeros correctly when using
// an offset='expand', need to add conditional statement to handle zeros
// appropriately
if (d._input.y === 0) {
return 0;
}
// for split bars or for one series,
// last series will have d.y0 = 0
if (d.y0 === 0 && yMin > 0) {
return yScale(yMin) - yScale(d.y);
}
return yScale(d.y0) - yScale(d.y0 + d.y);
});
return bars;
};
/**
* Adds grouped bars to column chart visualization
*
* @method addGroupedBars
* @param bars {D3.UpdateSelection} SVG with rect added
* @returns {D3.UpdateSelection}
*/
ColumnChart.prototype.addGroupedBars = function (bars) {
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let data = this.chartData;
let n = data.series.length;
let height = yScale.range()[0];
let groupSpacingPercentage = 0.15;
let isTimeScale = (data.ordered && data.ordered.date);
let minWidth = 1;
let barWidth;
// update
bars
.attr('x', function (d, i, j) {
if (isTimeScale) {
let groupWidth = xScale(data.ordered.min + data.ordered.interval) -
xScale(data.ordered.min);
let groupSpacing = groupWidth * groupSpacingPercentage;
barWidth = (groupWidth - groupSpacing) / n;
return xScale(d.x) + barWidth * j;
}
return xScale(d.x) + xScale.rangeBand() / n * j;
})
.attr('width', function () {
if (barWidth < minWidth) {
throw new errors.ContainerTooSmall();
}
if (isTimeScale) {
return barWidth;
}
return xScale.rangeBand() / n;
})
.attr('y', function (d) {
if (d.y < 0) {
return yScale(0);
}
return yScale(d.y);
})
.attr('height', function (d) {
return Math.abs(yScale(0) - yScale(d.y));
});
return bars;
};
/**
* Adds Events to SVG rect
* Visualization is only brushable when a brush event is added
* If a brush event is added, then a function should be returned.
*
* @method addBarEvents
* @param element {D3.UpdateSelection} target
* @param svg {D3.UpdateSelection} chart SVG
* @returns {D3.Selection} rect with event listeners attached
*/
ColumnChart.prototype.addBarEvents = function (element, svg) {
let events = this.events;
let isBrushable = events.isBrushable();
let brush = isBrushable ? events.addBrushEvent(svg) : undefined;
let hover = events.addHoverEvent();
let mouseout = events.addMouseoutEvent();
let click = events.addClickEvent();
let attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
}
return attachedEvents;
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the vertical bar chart
*/
ColumnChart.prototype.draw = function () {
let self = this;
let $elem = $(this.chartEl);
let margin = this._attr.margin;
let elWidth = this._attr.width = $elem.width();
let elHeight = this._attr.height = $elem.height();
let yScale = this.handler.yAxis.yScale;
let xScale = this.handler.xAxis.xScale;
let minWidth = 20;
let minHeight = 20;
let addTimeMarker = this._attr.addTimeMarker;
let times = this._attr.times || [];
let timeMarker;
let div;
let svg;
let width;
let height;
let layers;
let bars;
return function (selection) {
selection.each(function (data) {
layers = self.stackData(data);
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
// Column chart specific attributes
this._attr = _.defaults(handler._attr || {}, {
xValue: function (d) {
return d.x;
},
yValue: function (d) {
return d.y;
}
self.validateDataCompliesWithScalingMethod(data);
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (
data.series.length > 1 &&
(self._attr.scale === 'log' || self._attr.scale === 'square root') &&
(self._attr.mode === 'stacked' || self._attr.mode === 'percentage')
) {
throw new errors.StackedBarChartConfig(`Cannot display ${self._attr.mode} bar charts for multiple data series \
with a ${self._attr.scale} scaling method. Try 'linear' scaling instead.`);
}
div = d3.select(this);
svg = div.append('svg')
.attr('width', width)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(0,' + margin.top + ')');
bars = self.addBars(svg, layers);
self.createEndZones(svg);
// Adds event listeners
self.addBarEvents(bars, svg);
let line = svg.append('line')
.attr('class', 'base-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
}
/**
* Adds SVG rect to Vertical Bar Chart
*
* @method addBars
* @param svg {HTMLElement} SVG to which rect are appended
* @param layers {Array} Chart data array
* @returns {D3.UpdateSelection} SVG with rect added
*/
addBars(svg, layers) {
let self = this;
let color = this.handler.data.getColorFunc();
let tooltip = this.tooltip;
let isTooltip = this._attr.addTooltip;
let layer;
let bars;
layer = svg.selectAll('.layer')
.data(layers)
.enter().append('g')
.attr('class', function (d, i) {
return 'series ' + i;
});
bars = layer.selectAll('rect')
.data(function (d) {
return d;
});
bars
.exit()
.remove();
bars
.enter()
.append('rect')
.call(this._addIdentifier)
.attr('fill', function (d) {
return color(d.label);
});
self.updateBars(bars);
// Add tooltip
if (isTooltip) {
bars.call(tooltip.render());
}
return bars;
};
};
/**
* Determines whether bars are grouped or stacked and updates the D3
* selection
*
* @method updateBars
* @param bars {D3.UpdateSelection} SVG with rect added
* @returns {D3.UpdateSelection}
*/
updateBars(bars) {
let offset = this._attr.mode;
if (offset === 'grouped') {
return this.addGroupedBars(bars);
}
return this.addStackedBars(bars);
};
/**
* Adds stacked bars to column chart visualization
*
* @method addStackedBars
* @param bars {D3.UpdateSelection} SVG with rect added
* @returns {D3.UpdateSelection}
*/
addStackedBars(bars) {
let data = this.chartData;
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let height = yScale.range()[0];
let yMin = this.handler.yAxis.yScale.domain()[0];
let barWidth;
if (data.ordered && data.ordered.date) {
let start = data.ordered.min;
let end = moment(data.ordered.min).add(data.ordered.interval).valueOf();
barWidth = xScale(end) - xScale(start);
barWidth = barWidth - Math.min(barWidth * 0.25, 15);
}
// update
bars
.attr('x', function (d) {
return xScale(d.x);
})
.attr('width', function () {
return barWidth || xScale.rangeBand();
})
.attr('y', function (d) {
if (d.y < 0) {
return yScale(d.y0);
}
return yScale(d.y0 + d.y);
})
.attr('height', function (d) {
if (d.y < 0) {
return Math.abs(yScale(d.y0 + d.y) - yScale(d.y0));
}
// Due to an issue with D3 not returning zeros correctly when using
// an offset='expand', need to add conditional statement to handle zeros
// appropriately
if (d._input.y === 0) {
return 0;
}
// for split bars or for one series,
// last series will have d.y0 = 0
if (d.y0 === 0 && yMin > 0) {
return yScale(yMin) - yScale(d.y);
}
return yScale(d.y0) - yScale(d.y0 + d.y);
});
return bars;
};
/**
* Adds grouped bars to column chart visualization
*
* @method addGroupedBars
* @param bars {D3.UpdateSelection} SVG with rect added
* @returns {D3.UpdateSelection}
*/
addGroupedBars(bars) {
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let data = this.chartData;
let n = data.series.length;
let height = yScale.range()[0];
let groupSpacingPercentage = 0.15;
let isTimeScale = (data.ordered && data.ordered.date);
let minWidth = 1;
let barWidth;
// update
bars
.attr('x', function (d, i, j) {
if (isTimeScale) {
let groupWidth = xScale(data.ordered.min + data.ordered.interval) -
xScale(data.ordered.min);
let groupSpacing = groupWidth * groupSpacingPercentage;
barWidth = (groupWidth - groupSpacing) / n;
return xScale(d.x) + barWidth * j;
}
return xScale(d.x) + xScale.rangeBand() / n * j;
})
.attr('width', function () {
if (barWidth < minWidth) {
throw new errors.ContainerTooSmall();
}
if (isTimeScale) {
return barWidth;
}
return xScale.rangeBand() / n;
})
.attr('y', function (d) {
if (d.y < 0) {
return yScale(0);
}
return yScale(d.y);
})
.attr('height', function (d) {
return Math.abs(yScale(0) - yScale(d.y));
});
return bars;
};
/**
* Adds Events to SVG rect
* Visualization is only brushable when a brush event is added
* If a brush event is added, then a function should be returned.
*
* @method addBarEvents
* @param element {D3.UpdateSelection} target
* @param svg {D3.UpdateSelection} chart SVG
* @returns {D3.Selection} rect with event listeners attached
*/
addBarEvents(element, svg) {
let events = this.events;
let isBrushable = events.isBrushable();
let brush = isBrushable ? events.addBrushEvent(svg) : undefined;
let hover = events.addHoverEvent();
let mouseout = events.addMouseoutEvent();
let click = events.addClickEvent();
let attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
}
return attachedEvents;
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the vertical bar chart
*/
draw() {
let self = this;
let $elem = $(this.chartEl);
let margin = this._attr.margin;
let elWidth = this._attr.width = $elem.width();
let elHeight = this._attr.height = $elem.height();
let yScale = this.handler.yAxis.yScale;
let xScale = this.handler.xAxis.xScale;
let minWidth = 20;
let minHeight = 20;
let addTimeMarker = this._attr.addTimeMarker;
let times = this._attr.times || [];
let timeMarker;
let div;
let svg;
let width;
let height;
let layers;
let bars;
return function (selection) {
selection.each(function (data) {
layers = self.stackData(data);
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
self.validateDataCompliesWithScalingMethod(data);
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (
data.series.length > 1 &&
(self._attr.scale === 'log' || self._attr.scale === 'square root') &&
(self._attr.mode === 'stacked' || self._attr.mode === 'percentage')
) {
throw new errors.StackedBarChartConfig(`Cannot display ${self._attr.mode} bar charts for multiple data series \
with a ${self._attr.scale} scaling method. Try 'linear' scaling instead.`);
}
div = d3.select(this);
svg = div.append('svg')
.attr('width', width)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(0,' + margin.top + ')');
bars = self.addBars(svg, layers);
self.createEndZones(svg);
// Adds event listeners
self.addBarEvents(bars, svg);
let line = svg.append('line')
.attr('class', 'base-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};
};
}
return ColumnChart;
};

View file

@ -19,337 +19,340 @@ export default function LineChartFactory(Private) {
* @param el {HTMLElement} HTML element to which the chart will be appended
* @param chartData {Object} Elasticsearch query results for this specific chart
*/
_.class(LineChart).inherits(PointSeriesChart);
function LineChart(handler, chartEl, chartData) {
if (!(this instanceof LineChart)) {
return new LineChart(handler, chartEl, chartData);
}
class LineChart extends PointSeriesChart {
constructor(handler, chartEl, chartData) {
super(handler, chartEl, chartData);
LineChart.Super.apply(this, arguments);
// Line chart specific attributes
this._attr = _.defaults(handler._attr || {}, {
interpolate: 'linear',
xValue: function (d) { return d.x; },
yValue: function (d) { return d.y; }
});
}
/**
* Adds Events to SVG circle
*
* @method addCircleEvents
* @param element{D3.UpdateSelection} Reference to SVG circle
* @returns {D3.Selection} SVG circles with event listeners attached
*/
LineChart.prototype.addCircleEvents = function (element, svg) {
let events = this.events;
let isBrushable = events.isBrushable();
let brush = isBrushable ? events.addBrushEvent(svg) : undefined;
let hover = events.addHoverEvent();
let mouseout = events.addMouseoutEvent();
let click = events.addClickEvent();
let attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
}
return attachedEvents;
};
/**
* Adds circles to SVG
*
* @method addCircles
* @param svg {HTMLElement} SVG to which rect are appended
* @param data {Array} Array of object data points
* @returns {D3.UpdateSelection} SVG with circles added
*/
LineChart.prototype.addCircles = function (svg, data) {
let self = this;
let showCircles = this._attr.showCircles;
let color = this.handler.data.getColorFunc();
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let ordered = this.handler.data.get('ordered');
let tooltip = this.tooltip;
let isTooltip = this._attr.addTooltip;
let radii = _(data)
.map(function (series) {
return _.pluck(series, '_input.z');
})
.flattenDeep()
.reduce(function (result, val) {
if (result.min > val) result.min = val;
if (result.max < val) result.max = val;
return result;
}, {
min: Infinity,
max: -Infinity
});
let radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this._attr.radiusRatio, 2);
let layer = svg.selectAll('.points')
.data(data)
.enter()
.append('g')
.attr('class', 'points line');
let circles = layer
.selectAll('circle')
.data(function appendData(data) {
return data.filter(function (d) {
return !_.isNull(d.y);
// Line chart specific attributes
this._attr = _.defaults(handler._attr || {}, {
interpolate: 'linear',
xValue: function (d) {
return d.x;
},
yValue: function (d) {
return d.y;
}
});
});
}
circles
.exit()
.remove();
/**
* Adds Events to SVG circle
*
* @method addCircleEvents
* @param element{D3.UpdateSelection} Reference to SVG circle
* @returns {D3.Selection} SVG circles with event listeners attached
*/
addCircleEvents(element, svg) {
let events = this.events;
let isBrushable = events.isBrushable();
let brush = isBrushable ? events.addBrushEvent(svg) : undefined;
let hover = events.addHoverEvent();
let mouseout = events.addMouseoutEvent();
let click = events.addClickEvent();
let attachedEvents = element.call(hover).call(mouseout).call(click);
function cx(d) {
if (ordered && ordered.date) {
return xScale(d.x);
if (isBrushable) {
attachedEvents.call(brush);
}
return xScale(d.x) + xScale.rangeBand() / 2;
}
function cy(d) {
return yScale(d.y);
}
return attachedEvents;
};
function cColor(d) {
return color(d.label);
}
/**
* Adds circles to SVG
*
* @method addCircles
* @param svg {HTMLElement} SVG to which rect are appended
* @param data {Array} Array of object data points
* @returns {D3.UpdateSelection} SVG with circles added
*/
addCircles(svg, data) {
let self = this;
let showCircles = this._attr.showCircles;
let color = this.handler.data.getColorFunc();
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let ordered = this.handler.data.get('ordered');
let tooltip = this.tooltip;
let isTooltip = this._attr.addTooltip;
function colorCircle(d) {
let parent = d3.select(this).node().parentNode;
let lengthOfParent = d3.select(parent).data()[0].length;
let isVisible = (lengthOfParent === 1);
let radii = _(data)
.map(function (series) {
return _.pluck(series, '_input.z');
})
.flattenDeep()
.reduce(function (result, val) {
if (result.min > val) result.min = val;
if (result.max < val) result.max = val;
return result;
}, {
min: Infinity,
max: -Infinity
});
// If only 1 point exists, show circle
if (!showCircles && !isVisible) return 'none';
return cColor(d);
}
function getCircleRadiusFn(modifier) {
return function getCircleRadius(d) {
let margin = self._attr.margin;
let width = self._attr.width - margin.left - margin.right;
let height = self._attr.height - margin.top - margin.bottom;
let circleRadius = (d._input.z - radii.min) / radiusStep;
let radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this._attr.radiusRatio, 2);
return _.min([Math.sqrt((circleRadius || 2) + 2), width, height]) + (modifier || 0);
};
}
let layer = svg.selectAll('.points')
.data(data)
.enter()
.append('g')
.attr('class', 'points line');
circles
.enter()
.append('circle')
.attr('r', getCircleRadiusFn())
.attr('fill-opacity', (this._attr.drawLinesBetweenPoints ? 1 : 0.7))
.attr('cx', cx)
.attr('cy', cy)
.attr('class', 'circle-decoration')
.call(this._addIdentifier)
.attr('fill', colorCircle);
circles
.enter()
.append('circle')
.attr('r', getCircleRadiusFn(10))
.attr('cx', cx)
.attr('cy', cy)
.attr('fill', 'transparent')
.attr('class', 'circle')
.call(this._addIdentifier)
.attr('stroke', cColor)
.attr('stroke-width', 0);
if (isTooltip) {
circles.call(tooltip.render());
}
return circles;
};
/**
* Adds path to SVG
*
* @method addLines
* @param svg {HTMLElement} SVG to which path are appended
* @param data {Array} Array of object data points
* @returns {D3.UpdateSelection} SVG with paths added
*/
LineChart.prototype.addLines = function (svg, data) {
let self = this;
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let xAxisFormatter = this.handler.data.get('xAxisFormatter');
let color = this.handler.data.getColorFunc();
let ordered = this.handler.data.get('ordered');
let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate;
let line = d3.svg.line()
.defined(function (d) { return !_.isNull(d.y); })
.interpolate(interpolate)
.x(function x(d) {
if (ordered && ordered.date) {
return xScale(d.x);
}
return xScale(d.x) + xScale.rangeBand() / 2;
})
.y(function y(d) {
return yScale(d.y);
});
let lines;
lines = svg
.selectAll('.lines')
.data(data)
.enter()
.append('g')
.attr('class', 'pathgroup lines');
lines.append('path')
.call(this._addIdentifier)
.attr('d', function lineD(d) {
return line(d.values);
})
.attr('fill', 'none')
.attr('stroke', function lineStroke(d) {
return color(d.label);
})
.attr('stroke-width', 2);
return lines;
};
/**
* Adds SVG clipPath
*
* @method addClipPath
* @param svg {HTMLElement} SVG to which clipPath is appended
* @param width {Number} SVG width
* @param height {Number} SVG height
* @returns {D3.UpdateSelection} SVG with clipPath added
*/
LineChart.prototype.addClipPath = function (svg, width, height) {
let clipPathBuffer = 5;
let startX = 0;
let startY = 0 - clipPathBuffer;
let id = 'chart-area' + _.uniqueId();
return svg
.attr('clip-path', 'url(#' + id + ')')
.append('clipPath')
.attr('id', id)
.append('rect')
.attr('x', startX)
.attr('y', startY)
.attr('width', width)
// Adding clipPathBuffer to height so it doesn't
// cutoff the lower part of the chart
.attr('height', height + clipPathBuffer);
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the line chart
*/
LineChart.prototype.draw = function () {
let self = this;
let $elem = $(this.chartEl);
let margin = this._attr.margin;
let elWidth = this._attr.width = $elem.width();
let elHeight = this._attr.height = $elem.height();
let scaleType = this.handler.yAxis.getScaleType();
let yMin = this.handler.yAxis.yMin;
let yScale = this.handler.yAxis.yScale;
let xScale = this.handler.xAxis.xScale;
let minWidth = 20;
let minHeight = 20;
let startLineX = 0;
let lineStrokeWidth = 1;
let addTimeMarker = this._attr.addTimeMarker;
let times = this._attr.times || [];
let timeMarker;
let div;
let svg;
let width;
let height;
let lines;
let circles;
return function (selection) {
selection.each(function (data) {
let el = this;
let layers = data.series.map(function mapSeries(d) {
let label = d.label;
return d.values.map(function mapValues(e, i) {
return {
_input: e,
label: label,
x: self._attr.xValue.call(d.values, e, i),
y: self._attr.yValue.call(d.values, e, i)
};
let circles = layer
.selectAll('circle')
.data(function appendData(data) {
return data.filter(function (d) {
return !_.isNull(d.y);
});
});
width = elWidth - margin.left - margin.right;
height = elHeight - margin.top - margin.bottom;
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
circles
.exit()
.remove();
function cx(d) {
if (ordered && ordered.date) {
return xScale(d.x);
}
self.validateDataCompliesWithScalingMethod(data);
return xScale(d.x) + xScale.rangeBand() / 2;
}
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
function cy(d) {
return yScale(d.y);
}
function cColor(d) {
return color(d.label);
}
function colorCircle(d) {
let parent = d3.select(this).node().parentNode;
let lengthOfParent = d3.select(parent).data()[0].length;
let isVisible = (lengthOfParent === 1);
// If only 1 point exists, show circle
if (!showCircles && !isVisible) return 'none';
return cColor(d);
}
function getCircleRadiusFn(modifier) {
return function getCircleRadius(d) {
let margin = self._attr.margin;
let width = self._attr.width - margin.left - margin.right;
let height = self._attr.height - margin.top - margin.bottom;
let circleRadius = (d._input.z - radii.min) / radiusStep;
return _.min([Math.sqrt((circleRadius || 2) + 2), width, height]) + (modifier || 0);
};
}
circles
.enter()
.append('circle')
.attr('r', getCircleRadiusFn())
.attr('fill-opacity', (this._attr.drawLinesBetweenPoints ? 1 : 0.7))
.attr('cx', cx)
.attr('cy', cy)
.attr('class', 'circle-decoration')
.call(this._addIdentifier)
.attr('fill', colorCircle);
div = d3.select(el);
circles
.enter()
.append('circle')
.attr('r', getCircleRadiusFn(10))
.attr('cx', cx)
.attr('cy', cy)
.attr('fill', 'transparent')
.attr('class', 'circle')
.call(this._addIdentifier)
.attr('stroke', cColor)
.attr('stroke-width', 0);
svg = div.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
if (isTooltip) {
circles.call(tooltip.render());
}
self.addClipPath(svg, width, height);
if (self._attr.drawLinesBetweenPoints) {
lines = self.addLines(svg, data.series);
}
circles = self.addCircles(svg, layers);
self.addCircleEvents(circles, svg);
self.createEndZones(svg);
let scale = (scaleType === 'log') ? yScale(1) : yScale(0);
if (scale) {
svg.append('line')
.attr('class', 'base-line')
.attr('x1', startLineX)
.attr('y1', scale)
.attr('x2', width)
.attr('y2', scale)
.style('stroke', '#ddd')
.style('stroke-width', lineStrokeWidth);
}
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
return circles;
};
};
/**
* Adds path to SVG
*
* @method addLines
* @param svg {HTMLElement} SVG to which path are appended
* @param data {Array} Array of object data points
* @returns {D3.UpdateSelection} SVG with paths added
*/
addLines(svg, data) {
let self = this;
let xScale = this.handler.xAxis.xScale;
let yScale = this.handler.yAxis.yScale;
let xAxisFormatter = this.handler.data.get('xAxisFormatter');
let color = this.handler.data.getColorFunc();
let ordered = this.handler.data.get('ordered');
let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate;
let line = d3.svg.line()
.defined(function (d) {
return !_.isNull(d.y);
})
.interpolate(interpolate)
.x(function x(d) {
if (ordered && ordered.date) {
return xScale(d.x);
}
return xScale(d.x) + xScale.rangeBand() / 2;
})
.y(function y(d) {
return yScale(d.y);
});
let lines;
lines = svg
.selectAll('.lines')
.data(data)
.enter()
.append('g')
.attr('class', 'pathgroup lines');
lines.append('path')
.call(this._addIdentifier)
.attr('d', function lineD(d) {
return line(d.values);
})
.attr('fill', 'none')
.attr('stroke', function lineStroke(d) {
return color(d.label);
})
.attr('stroke-width', 2);
return lines;
};
/**
* Adds SVG clipPath
*
* @method addClipPath
* @param svg {HTMLElement} SVG to which clipPath is appended
* @param width {Number} SVG width
* @param height {Number} SVG height
* @returns {D3.UpdateSelection} SVG with clipPath added
*/
addClipPath(svg, width, height) {
let clipPathBuffer = 5;
let startX = 0;
let startY = 0 - clipPathBuffer;
let id = 'chart-area' + _.uniqueId();
return svg
.attr('clip-path', 'url(#' + id + ')')
.append('clipPath')
.attr('id', id)
.append('rect')
.attr('x', startX)
.attr('y', startY)
.attr('width', width)
// Adding clipPathBuffer to height so it doesn't
// cutoff the lower part of the chart
.attr('height', height + clipPathBuffer);
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the line chart
*/
draw() {
let self = this;
let $elem = $(this.chartEl);
let margin = this._attr.margin;
let elWidth = this._attr.width = $elem.width();
let elHeight = this._attr.height = $elem.height();
let scaleType = this.handler.yAxis.getScaleType();
let yMin = this.handler.yAxis.yMin;
let yScale = this.handler.yAxis.yScale;
let xScale = this.handler.xAxis.xScale;
let minWidth = 20;
let minHeight = 20;
let startLineX = 0;
let lineStrokeWidth = 1;
let addTimeMarker = this._attr.addTimeMarker;
let times = this._attr.times || [];
let timeMarker;
let div;
let svg;
let width;
let height;
let lines;
let circles;
return function (selection) {
selection.each(function (data) {
let el = this;
let layers = data.series.map(function mapSeries(d) {
let label = d.label;
return d.values.map(function mapValues(e, i) {
return {
_input: e,
label: label,
x: self._attr.xValue.call(d.values, e, i),
y: self._attr.yValue.call(d.values, e, i)
};
});
});
width = elWidth - margin.left - margin.right;
height = elHeight - margin.top - margin.bottom;
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
self.validateDataCompliesWithScalingMethod(data);
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
div = d3.select(el);
svg = div.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
self.addClipPath(svg, width, height);
if (self._attr.drawLinesBetweenPoints) {
lines = self.addLines(svg, data.series);
}
circles = self.addCircles(svg, layers);
self.addCircleEvents(circles, svg);
self.createEndZones(svg);
let scale = (scaleType === 'log') ? yScale(1) : yScale(0);
if (scale) {
svg.append('line')
.attr('class', 'base-line')
.attr('x1', startLineX)
.attr('y1', scale)
.attr('x2', width)
.attr('y2', scale)
.style('stroke', '#ddd')
.style('stroke-width', lineStrokeWidth);
}
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};
};
}
return LineChart;
};

View file

@ -11,254 +11,256 @@ export default function MarkerFactory() {
* @param geoJson {geoJson Object}
* @param params {Object}
*/
function BaseMarker(map, geoJson, params) {
this.map = map;
this.geoJson = geoJson;
this.popups = [];
class BaseMarker {
constructor(map, geoJson, params) {
this.map = map;
this.geoJson = geoJson;
this.popups = [];
this._tooltipFormatter = params.tooltipFormatter || _.identity;
this._valueFormatter = params.valueFormatter || _.identity;
this._attr = params.attr || {};
this._tooltipFormatter = params.tooltipFormatter || _.identity;
this._valueFormatter = params.valueFormatter || _.identity;
this._attr = params.attr || {};
// set up the default legend colors
this.quantizeLegendColors();
}
// set up the default legend colors
this.quantizeLegendColors();
}
/**
* Adds legend div to each map when data is split
* uses d3 scale from BaseMarker.prototype.quantizeLegendColors
*
* @method addLegend
* @return {undefined}
*/
BaseMarker.prototype.addLegend = function () {
// ensure we only ever create 1 legend
if (this._legend) return;
/**
* Adds legend div to each map when data is split
* uses d3 scale from BaseMarker.prototype.quantizeLegendColors
*
* @method addLegend
* @return {undefined}
*/
addLegend() {
// ensure we only ever create 1 legend
if (this._legend) return;
let self = this;
let self = this;
// create the legend control, keep a reference
self._legend = L.control({position: 'bottomright'});
// create the legend control, keep a reference
self._legend = L.control({position: 'bottomright'});
self._legend.onAdd = function () {
// creates all the neccessary DOM elements for the control, adds listeners
// on relevant map events, and returns the element containing the control
let $div = $('<div>').addClass('tilemap-legend');
self._legend.onAdd = function () {
// creates all the neccessary DOM elements for the control, adds listeners
// on relevant map events, and returns the element containing the control
let $div = $('<div>').addClass('tilemap-legend');
_.each(self._legendColors, function (color, i) {
let labelText = self._legendQuantizer
.invertExtent(color)
.map(self._valueFormatter)
.join('  ');
_.each(self._legendColors, function (color, i) {
let labelText = self._legendQuantizer
.invertExtent(color)
.map(self._valueFormatter)
.join('  ');
let label = $('<div>').text(labelText);
let label = $('<div>').text(labelText);
let icon = $('<i>').css({
background: color,
'border-color': self.darkerColor(color)
let icon = $('<i>').css({
background: color,
'border-color': self.darkerColor(color)
});
label.append(icon);
$div.append(label);
});
label.append(icon);
$div.append(label);
return $div.get(0);
};
self._legend.addTo(self.map);
};
/**
* Apply style with shading to feature
*
* @method applyShadingStyle
* @param value {Object}
* @return {Object}
*/
applyShadingStyle(value) {
let color = this._legendQuantizer(value);
return {
fillColor: color,
color: this.darkerColor(color),
weight: 1.5,
opacity: 1,
fillOpacity: 0.75
};
};
/**
* Binds popup and events to each feature on map
*
* @method bindPopup
* @param feature {Object}
* @param layer {Object}
* return {undefined}
*/
bindPopup(feature, layer) {
let self = this;
let popup = layer.on({
mouseover: function (e) {
let layer = e.target;
// bring layer to front if not older browser
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
self._showTooltip(feature);
},
mouseout: function (e) {
self._hidePopup();
}
});
return $div.get(0);
self.popups.push(popup);
};
self._legend.addTo(self.map);
};
/**
* Apply style with shading to feature
*
* @method applyShadingStyle
* @param value {Object}
* @return {Object}
*/
BaseMarker.prototype.applyShadingStyle = function (value) {
let color = this._legendQuantizer(value);
return {
fillColor: color,
color: this.darkerColor(color),
weight: 1.5,
opacity: 1,
fillOpacity: 0.75
/**
* d3 method returns a darker hex color,
* used for marker stroke color
*
* @method darkerColor
* @param color {String} hex color
* @param amount? {Number} amount to darken by
* @return {String} hex color
*/
darkerColor(color, amount) {
amount = amount || 1.3;
return d3.hcl(color).darker(amount).toString();
};
};
/**
* Binds popup and events to each feature on map
*
* @method bindPopup
* @param feature {Object}
* @param layer {Object}
* return {undefined}
*/
BaseMarker.prototype.bindPopup = function (feature, layer) {
let self = this;
destroy() {
let self = this;
let popup = layer.on({
mouseover: function (e) {
let layer = e.target;
// bring layer to front if not older browser
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
self._showTooltip(feature);
},
mouseout: function (e) {
self._hidePopup();
// remove popups
self.popups = self.popups.filter(function (popup) {
popup.off('mouseover').off('mouseout');
});
if (self._legend) {
self.map.removeControl(self._legend);
self._legend = undefined;
}
});
self.popups.push(popup);
};
/**
* d3 method returns a darker hex color,
* used for marker stroke color
*
* @method darkerColor
* @param color {String} hex color
* @param amount? {Number} amount to darken by
* @return {String} hex color
*/
BaseMarker.prototype.darkerColor = function (color, amount) {
amount = amount || 1.3;
return d3.hcl(color).darker(amount).toString();
};
BaseMarker.prototype.destroy = function () {
let self = this;
// remove popups
self.popups = self.popups.filter(function (popup) {
popup.off('mouseover').off('mouseout');
});
if (self._legend) {
self.map.removeControl(self._legend);
self._legend = undefined;
}
// remove marker layer from map
if (self._markerGroup) {
self.map.removeLayer(self._markerGroup);
self._markerGroup = undefined;
}
};
BaseMarker.prototype._addToMap = function () {
this.map.addLayer(this._markerGroup);
};
/**
* Creates leaflet marker group, passing options to L.geoJson
*
* @method _createMarkerGroup
* @param options {Object} Options to pass to L.geoJson
*/
BaseMarker.prototype._createMarkerGroup = function (options) {
let self = this;
let defaultOptions = {
onEachFeature: function (feature, layer) {
self.bindPopup(feature, layer);
},
style: function (feature) {
let value = _.get(feature, 'properties.value');
return self.applyShadingStyle(value);
},
filter: self._filterToMapBounds()
// remove marker layer from map
if (self._markerGroup) {
self.map.removeLayer(self._markerGroup);
self._markerGroup = undefined;
}
};
this._markerGroup = L.geoJson(this.geoJson, _.defaults(defaultOptions, options));
this._addToMap();
};
/**
* return whether feature is within map bounds
*
* @method _filterToMapBounds
* @param map {Leaflet Object}
* @return {boolean}
*/
BaseMarker.prototype._filterToMapBounds = function () {
let self = this;
return function (feature) {
let mapBounds = self.map.getBounds();
let bucketRectBounds = _.get(feature, 'properties.rectangle');
return mapBounds.intersects(bucketRectBounds);
_addToMap() {
this.map.addLayer(this._markerGroup);
};
};
/**
* Checks if event latlng is within bounds of mapData
* features and shows tooltip for that feature
*
* @method _showTooltip
* @param feature {LeafletFeature}
* @param latLng? {Leaflet latLng}
* @return undefined
*/
BaseMarker.prototype._showTooltip = function (feature, latLng) {
if (!this.map) return;
let lat = _.get(feature, 'geometry.coordinates.1');
let lng = _.get(feature, 'geometry.coordinates.0');
latLng = latLng || L.latLng(lat, lng);
/**
* Creates leaflet marker group, passing options to L.geoJson
*
* @method _createMarkerGroup
* @param options {Object} Options to pass to L.geoJson
*/
_createMarkerGroup(options) {
let self = this;
let defaultOptions = {
onEachFeature: function (feature, layer) {
self.bindPopup(feature, layer);
},
style: function (feature) {
let value = _.get(feature, 'properties.value');
return self.applyShadingStyle(value);
},
filter: self._filterToMapBounds()
};
let content = this._tooltipFormatter(feature);
this._markerGroup = L.geoJson(this.geoJson, _.defaults(defaultOptions, options));
this._addToMap();
};
if (!content) return;
this._createTooltip(content, latLng);
};
/**
* return whether feature is within map bounds
*
* @method _filterToMapBounds
* @param map {Leaflet Object}
* @return {boolean}
*/
_filterToMapBounds() {
let self = this;
return function (feature) {
let mapBounds = self.map.getBounds();
let bucketRectBounds = _.get(feature, 'properties.rectangle');
return mapBounds.intersects(bucketRectBounds);
};
};
BaseMarker.prototype._createTooltip = function (content, latLng) {
L.popup({autoPan: false})
.setLatLng(latLng)
.setContent(content)
.openOn(this.map);
};
/**
* Checks if event latlng is within bounds of mapData
* features and shows tooltip for that feature
*
* @method _showTooltip
* @param feature {LeafletFeature}
* @param latLng? {Leaflet latLng}
* @return undefined
*/
_showTooltip(feature, latLng) {
if (!this.map) return;
let lat = _.get(feature, 'geometry.coordinates.1');
let lng = _.get(feature, 'geometry.coordinates.0');
latLng = latLng || L.latLng(lat, lng);
/**
* Closes the tooltip on the map
*
* @method _hidePopup
* @return undefined
*/
BaseMarker.prototype._hidePopup = function () {
if (!this.map) return;
let content = this._tooltipFormatter(feature);
this.map.closePopup();
};
if (!content) return;
this._createTooltip(content, latLng);
};
/**
* d3 quantize scale returns a hex color, used for marker fill color
*
* @method quantizeLegendColors
* return {undefined}
*/
BaseMarker.prototype.quantizeLegendColors = function () {
let min = _.get(this.geoJson, 'properties.allmin', 0);
let max = _.get(this.geoJson, 'properties.allmax', 1);
let quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
_createTooltip(content, latLng) {
L.popup({autoPan: false})
.setLatLng(latLng)
.setContent(content)
.openOn(this.map);
};
let reds1 = ['#ff6128'];
let reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
let reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
let bottomCutoff = 2;
let middleCutoff = 24;
/**
* Closes the tooltip on the map
*
* @method _hidePopup
* @return undefined
*/
_hidePopup() {
if (!this.map) return;
if (max - min <= bottomCutoff) {
this._legendColors = reds1;
} else if (max - min <= middleCutoff) {
this._legendColors = reds3;
} else {
this._legendColors = reds5;
}
this.map.closePopup();
};
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
};
/**
* d3 quantize scale returns a hex color, used for marker fill color
*
* @method quantizeLegendColors
* return {undefined}
*/
quantizeLegendColors() {
let min = _.get(this.geoJson, 'properties.allmin', 0);
let max = _.get(this.geoJson, 'properties.allmax', 1);
let quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
let reds1 = ['#ff6128'];
let reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
let reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
let bottomCutoff = 2;
let middleCutoff = 24;
if (max - min <= bottomCutoff) {
this._legendColors = reds1;
} else if (max - min <= middleCutoff) {
this._legendColors = reds3;
} else {
this._legendColors = reds5;
}
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
};
}
return BaseMarker;
};

View file

@ -12,27 +12,27 @@ export default function GeohashGridMarkerFactory(Private) {
* @param geoJson {geoJson Object}
* @param params {Object}
*/
_.class(GeohashGridMarker).inherits(BaseMarker);
function GeohashGridMarker(map, geoJson, params) {
let self = this;
GeohashGridMarker.Super.apply(this, arguments);
class GeohashGridMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
// super min and max from all chart data
let min = this.geoJson.properties.allmin;
let max = this.geoJson.properties.allmax;
// super min and max from all chart data
let min = this.geoJson.properties.allmin;
let max = this.geoJson.properties.allmax;
this._createMarkerGroup({
pointToLayer: function (feature, latlng) {
let geohashRect = feature.properties.rectangle;
// get bounds from northEast[3] and southWest[1]
// corners in geohash rectangle
let corners = [
[geohashRect[3][0], geohashRect[3][1]],
[geohashRect[1][0], geohashRect[1][1]]
];
return L.rectangle(corners);
}
});
this._createMarkerGroup({
pointToLayer: function (feature, latlng) {
let geohashRect = feature.properties.rectangle;
// get bounds from northEast[3] and southWest[1]
// corners in geohash rectangle
let corners = [
[geohashRect[3][0], geohashRect[3][1]],
[geohashRect[1][0], geohashRect[1][1]]
];
return L.rectangle(corners);
}
});
}
}
return GeohashGridMarker;

View file

@ -13,198 +13,186 @@ export default function HeatmapMarkerFactory(Private) {
* @param geoJson {geoJson Object}
* @param params {Object}
*/
_.class(HeatmapMarker).inherits(BaseMarker);
function HeatmapMarker(map, geoJson, params) {
let self = this;
this._disableTooltips = false;
HeatmapMarker.Super.apply(this, arguments);
class HeatmapMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
this._disableTooltips = false;
this._createMarkerGroup({
radius: +this._attr.heatRadius,
blur: +this._attr.heatBlur,
maxZoom: +this._attr.heatMaxZoom,
minOpacity: +this._attr.heatMinOpacity
});
}
/**
* Does nothing, heatmaps don't have a legend
*
* @method addLegend
* @return {undefined}
*/
HeatmapMarker.prototype.addLegend = _.noop;
HeatmapMarker.prototype._createMarkerGroup = function (options) {
let max = _.get(this.geoJson, 'properties.allmax');
let points = this._dataToHeatArray(max);
this._markerGroup = L.heatLayer(points, options);
this._fixTooltips();
this._addToMap();
};
HeatmapMarker.prototype._fixTooltips = function () {
let self = this;
let debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
'leading': true,
'trailing': false
});
if (!this._disableTooltips && this._attr.addTooltip) {
this.map.on('mousemove', debouncedMouseMoveLocation);
this.map.on('mouseout', function () {
self.map.closePopup();
this._createMarkerGroup({
radius: +this._attr.heatRadius,
blur: +this._attr.heatBlur,
maxZoom: +this._attr.heatMaxZoom,
minOpacity: +this._attr.heatMinOpacity
});
this.map.on('mousedown', function () {
self._disableTooltips = true;
self.map.closePopup();
});
this.map.on('mouseup', function () {
self._disableTooltips = false;
this.addLegend = _.noop;
this._getLatLng = _.memoize(function (feature) {
return L.latLng(
feature.geometry.coordinates[1],
feature.geometry.coordinates[0]
);
}, function (feature) {
// turn coords into a string for the memoize cache
return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(',');
});
}
function mouseMoveLocation(e) {
let latlng = e.latlng;
_createMarkerGroup(options) {
let max = _.get(this.geoJson, 'properties.allmax');
let points = this._dataToHeatArray(max);
this.map.closePopup();
this._markerGroup = L.heatLayer(points, options);
this._fixTooltips();
this._addToMap();
};
// unhighlight all svgs
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
_fixTooltips() {
let self = this;
let debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
'leading': true,
'trailing': false
});
if (!this.geoJson.features.length || this._disableTooltips) {
if (!this._disableTooltips && this._attr.addTooltip) {
this.map.on('mousemove', debouncedMouseMoveLocation);
this.map.on('mouseout', function () {
self.map.closePopup();
});
this.map.on('mousedown', function () {
self._disableTooltips = true;
self.map.closePopup();
});
this.map.on('mouseup', function () {
self._disableTooltips = false;
});
}
function mouseMoveLocation(e) {
let latlng = e.latlng;
this.map.closePopup();
// unhighlight all svgs
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
if (!this.geoJson.features.length || this._disableTooltips) {
return;
}
// find nearest feature to event latlng
let feature = this._nearestFeature(latlng);
// show tooltip if close enough to event latlng
if (this._tooltipProximity(latlng, feature)) {
this._showTooltip(feature, latlng);
}
}
};
/**
* Finds nearest feature in mapData to event latlng
*
* @method _nearestFeature
* @param latLng {Leaflet latLng}
* @return nearestPoint {Leaflet latLng}
*/
_nearestFeature(latLng) {
let self = this;
let nearest;
if (latLng.lng < -180 || latLng.lng > 180) {
return;
}
// find nearest feature to event latlng
let feature = this._nearestFeature(latlng);
_.reduce(this.geoJson.features, function (distance, feature) {
let featureLatLng = self._getLatLng(feature);
let dist = latLng.distanceTo(featureLatLng);
// show tooltip if close enough to event latlng
if (this._tooltipProximity(latlng, feature)) {
this._showTooltip(feature, latlng);
}
}
};
if (dist < distance) {
nearest = feature;
return dist;
}
/**
* returns a memoized Leaflet latLng for given geoJson feature
*
* @method addLatLng
* @param feature {geoJson Object}
* @return {Leaflet latLng Object}
*/
HeatmapMarker.prototype._getLatLng = _.memoize(function (feature) {
return L.latLng(
feature.geometry.coordinates[1],
feature.geometry.coordinates[0]
);
}, function (feature) {
// turn coords into a string for the memoize cache
return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(',');
});
return distance;
}, Infinity);
/**
* Finds nearest feature in mapData to event latlng
*
* @method _nearestFeature
* @param latLng {Leaflet latLng}
* @return nearestPoint {Leaflet latLng}
*/
HeatmapMarker.prototype._nearestFeature = function (latLng) {
let self = this;
let nearest;
return nearest;
};
if (latLng.lng < -180 || latLng.lng > 180) {
return;
}
/**
* display tooltip if feature is close enough to event latlng
*
* @method _tooltipProximity
* @param latlng {Leaflet latLng Object}
* @param feature {geoJson Object}
* @return {Boolean}
*/
_tooltipProximity(latlng, feature) {
if (!feature) return;
_.reduce(this.geoJson.features, function (distance, feature) {
let featureLatLng = self._getLatLng(feature);
let dist = latLng.distanceTo(featureLatLng);
let showTip = false;
let featureLatLng = this._getLatLng(feature);
if (dist < distance) {
nearest = feature;
return dist;
// zoomScale takes map zoom and returns proximity value for tooltip display
// domain (input values) is map zoom (min 1 and max 18)
// range (output values) is distance in meters
// used to compare proximity of event latlng to feature latlng
let zoomScale = d3.scale.linear()
.domain([1, 4, 7, 10, 13, 16, 18])
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
let proximity = zoomScale(this.map.getZoom());
let distance = latlng.distanceTo(featureLatLng);
// maxLngDif is max difference in longitudes
// to prevent feature tooltip from appearing 360°
// away from event latlng
let maxLngDif = 40;
let lngDif = Math.abs(latlng.lng - featureLatLng.lng);
if (distance < proximity && lngDif < maxLngDif) {
showTip = true;
}
return distance;
}, Infinity);
return nearest;
};
/**
* display tooltip if feature is close enough to event latlng
*
* @method _tooltipProximity
* @param latlng {Leaflet latLng Object}
* @param feature {geoJson Object}
* @return {Boolean}
*/
HeatmapMarker.prototype._tooltipProximity = function (latlng, feature) {
if (!feature) return;
let showTip = false;
let featureLatLng = this._getLatLng(feature);
// zoomScale takes map zoom and returns proximity value for tooltip display
// domain (input values) is map zoom (min 1 and max 18)
// range (output values) is distance in meters
// used to compare proximity of event latlng to feature latlng
let zoomScale = d3.scale.linear()
.domain([1, 4, 7, 10, 13, 16, 18])
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
let proximity = zoomScale(this.map.getZoom());
let distance = latlng.distanceTo(featureLatLng);
// maxLngDif is max difference in longitudes
// to prevent feature tooltip from appearing 360°
// away from event latlng
let maxLngDif = 40;
let lngDif = Math.abs(latlng.lng - featureLatLng.lng);
if (distance < proximity && lngDif < maxLngDif) {
showTip = true;
}
let testScale = d3.scale.pow().exponent(0.2)
.domain([1, 18])
.range([1500000, 50]);
return showTip;
};
let testScale = d3.scale.pow().exponent(0.2)
.domain([1, 18])
.range([1500000, 50]);
return showTip;
};
/**
* returns data for data for heat map intensity
* if heatNormalizeData attribute is checked/true
normalizes data for heat map intensity
*
* @method _dataToHeatArray
* @param max {Number}
* @return {Array}
*/
HeatmapMarker.prototype._dataToHeatArray = function (max) {
let self = this;
let mapData = this.geoJson;
/**
* returns data for data for heat map intensity
* if heatNormalizeData attribute is checked/true
normalizes data for heat map intensity
*
* @method _dataToHeatArray
* @param max {Number}
* @return {Array}
*/
_dataToHeatArray(max) {
let self = this;
let mapData = this.geoJson;
return this.geoJson.features.map(function (feature) {
let lat = feature.properties.center[0];
let lng = feature.properties.center[1];
let heatIntensity;
return this.geoJson.features.map(function (feature) {
let lat = feature.properties.center[0];
let lng = feature.properties.center[1];
let heatIntensity;
if (!self._attr.heatNormalizeData) {
// show bucket value on heatmap
heatIntensity = feature.properties.value;
} else {
// show bucket value normalized to max value
heatIntensity = feature.properties.value / max;
}
if (!self._attr.heatNormalizeData) {
// show bucket value on heatmap
heatIntensity = feature.properties.value;
} else {
// show bucket value normalized to max value
heatIntensity = feature.properties.value / max;
}
return [lat, lng, heatIntensity];
});
};
}
return [lat, lng, heatIntensity];
});
};
return HeatmapMarker;
};

View file

@ -12,48 +12,48 @@ export default function ScaledCircleMarkerFactory(Private) {
* @param mapData {geoJson Object}
* @param params {Object}
*/
_.class(ScaledCircleMarker).inherits(BaseMarker);
function ScaledCircleMarker(map, geoJson, params) {
let self = this;
ScaledCircleMarker.Super.apply(this, arguments);
class ScaledCircleMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
// multiplier to reduce size of all circles
let scaleFactor = 0.6;
// multiplier to reduce size of all circles
let scaleFactor = 0.6;
this._createMarkerGroup({
pointToLayer: function (feature, latlng) {
let value = feature.properties.value;
let scaledRadius = self._radiusScale(value) * scaleFactor;
return L.circleMarker(latlng).setRadius(scaledRadius);
}
});
this._createMarkerGroup({
pointToLayer: (feature, latlng) => {
let value = feature.properties.value;
let scaledRadius = this._radiusScale(value) * scaleFactor;
return L.circleMarker(latlng).setRadius(scaledRadius);
}
});
}
/**
* radiusScale returns a number for scaled circle markers
* for relative sizing of markers
*
* @method _radiusScale
* @param value {Number}
* @return {Number}
*/
_radiusScale(value) {
let precisionBiasBase = 5;
let precisionBiasNumerator = 200;
let zoom = this.map.getZoom();
let maxValue = this.geoJson.properties.allmax;
let precision = _.max(this.geoJson.features.map(function (feature) {
return String(feature.properties.geohash).length;
}));
let pct = Math.abs(value) / Math.abs(maxValue);
let zoomRadius = 0.5 * Math.pow(2, zoom);
let precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
// square root value percentage
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
};
}
/**
* radiusScale returns a number for scaled circle markers
* for relative sizing of markers
*
* @method _radiusScale
* @param value {Number}
* @return {Number}
*/
ScaledCircleMarker.prototype._radiusScale = function (value) {
let precisionBiasBase = 5;
let precisionBiasNumerator = 200;
let zoom = this.map.getZoom();
let maxValue = this.geoJson.properties.allmax;
let precision = _.max(this.geoJson.features.map(function (feature) {
return String(feature.properties.geohash).length;
}));
let pct = Math.abs(value) / Math.abs(maxValue);
let zoomRadius = 0.5 * Math.pow(2, zoom);
let precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
// square root value percentage
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
};
return ScaledCircleMarker;
};

View file

@ -12,56 +12,58 @@ export default function ShadedCircleMarkerFactory(Private) {
* @param mapData {geoJson Object}
* @return {Leaflet object} featureLayer
*/
_.class(ShadedCircleMarker).inherits(BaseMarker);
function ShadedCircleMarker(map, geoJson, params) {
let self = this;
ShadedCircleMarker.Super.apply(this, arguments);
class ShadedCircleMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
// super min and max from all chart data
let min = this.geoJson.properties.allmin;
let max = this.geoJson.properties.allmax;
// super min and max from all chart data
let min = this.geoJson.properties.allmin;
let max = this.geoJson.properties.allmax;
// multiplier to reduce size of all circles
let scaleFactor = 0.8;
// multiplier to reduce size of all circles
let scaleFactor = 0.8;
this._createMarkerGroup({
pointToLayer: function (feature, latlng) {
let radius = self._geohashMinDistance(feature) * scaleFactor;
return L.circle(latlng, radius);
}
});
this._createMarkerGroup({
pointToLayer: (feature, latlng) => {
let radius = this._geohashMinDistance(feature) * scaleFactor;
return L.circle(latlng, radius);
}
});
}
/**
* _geohashMinDistance returns a min distance in meters for sizing
* circle markers to fit within geohash grid rectangle
*
* @method _geohashMinDistance
* @param feature {Object}
* @return {Number}
*/
_geohashMinDistance(feature) {
let centerPoint = _.get(feature, 'properties.center');
let geohashRect = _.get(feature, 'properties.rectangle');
// centerPoint is an array of [lat, lng]
// geohashRect is the 4 corners of the geoHash rectangle
// an array that starts at the southwest corner and proceeds
// clockwise, each value being an array of [lat, lng]
// center lat and southeast lng
let east = L.latLng([centerPoint[0], geohashRect[2][1]]);
// southwest lat and center lng
let north = L.latLng([geohashRect[3][0], centerPoint[1]]);
// get latLng of geohash center point
let center = L.latLng([centerPoint[0], centerPoint[1]]);
// get smallest radius at center of geohash grid rectangle
let eastRadius = Math.floor(center.distanceTo(east));
let northRadius = Math.floor(center.distanceTo(north));
return _.min([eastRadius, northRadius]);
};
}
/**
* _geohashMinDistance returns a min distance in meters for sizing
* circle markers to fit within geohash grid rectangle
*
* @method _geohashMinDistance
* @param feature {Object}
* @return {Number}
*/
ShadedCircleMarker.prototype._geohashMinDistance = function (feature) {
let centerPoint = _.get(feature, 'properties.center');
let geohashRect = _.get(feature, 'properties.rectangle');
// centerPoint is an array of [lat, lng]
// geohashRect is the 4 corners of the geoHash rectangle
// an array that starts at the southwest corner and proceeds
// clockwise, each value being an array of [lat, lng]
// center lat and southeast lng
let east = L.latLng([centerPoint[0], geohashRect[2][1]]);
// southwest lat and center lng
let north = L.latLng([geohashRect[3][0], centerPoint[1]]);
// get latLng of geohash center point
let center = L.latLng([centerPoint[0], centerPoint[1]]);
// get smallest radius at center of geohash grid rectangle
let eastRadius = Math.floor(center.distanceTo(east));
let northRadius = Math.floor(center.distanceTo(north));
return _.min([eastRadius, northRadius]);
};
return ShadedCircleMarker;
};

View file

@ -17,193 +17,197 @@ export default function PieChartFactory(Private) {
* @param el {HTMLElement} HTML element to which the chart will be appended
* @param chartData {Object} Elasticsearch query results for this specific chart
*/
_.class(PieChart).inherits(Chart);
function PieChart(handler, chartEl, chartData) {
if (!(this instanceof PieChart)) {
return new PieChart(handler, chartEl, chartData);
}
PieChart.Super.apply(this, arguments);
class PieChart extends Chart {
constructor(handler, chartEl, chartData) {
super(handler, chartEl, chartData);
let charts = this.handler.data.getVisData();
this._validatePieData(charts);
let charts = this.handler.data.getVisData();
this._validatePieData(charts);
this._attr = _.defaults(handler._attr || {}, {
isDonut: handler._attr.isDonut || false
});
}
/**
* Checks whether pie slices have all zero values.
* If so, an error is thrown.
*/
PieChart.prototype._validatePieData = function (charts) {
let isAllZeros = charts.every(function (chart) {
return chart.slices.children.length === 0;
});
if (isAllZeros) { throw new errors.PieContainsAllZeros(); }
};
/**
* Adds Events to SVG paths
*
* @method addPathEvents
* @param element {D3.Selection} Reference to SVG path
* @returns {D3.Selection} SVG path with event listeners attached
*/
PieChart.prototype.addPathEvents = function (element) {
let events = this.events;
return element
.call(events.addHoverEvent())
.call(events.addMouseoutEvent())
.call(events.addClickEvent());
};
PieChart.prototype.convertToPercentage = function (slices) {
(function assignPercentages(slices) {
if (slices.sumOfChildren != null) return;
let parent = slices;
let children = parent.children;
let parentPercent = parent.percentOfParent;
let sum = parent.sumOfChildren = Math.abs(children.reduce(function (sum, child) {
return sum + Math.abs(child.size);
}, 0));
children.forEach(function (child) {
child.percentOfGroup = Math.abs(child.size) / sum;
child.percentOfParent = child.percentOfGroup;
if (parentPercent != null) {
child.percentOfParent *= parentPercent;
}
if (child.children) {
assignPercentages(child);
}
this._attr = _.defaults(handler._attr || {}, {
isDonut: handler._attr.isDonut || false
});
}(slices));
};
}
/**
* Adds pie paths to SVG
*
* @method addPath
* @param width {Number} Width of SVG
* @param height {Number} Height of SVG
* @param svg {HTMLElement} Chart SVG
* @param slices {Object} Chart data
* @returns {D3.Selection} SVG with paths attached
*/
PieChart.prototype.addPath = function (width, height, svg, slices) {
let self = this;
let marginFactor = 0.95;
let isDonut = self._attr.isDonut;
let radius = (Math.min(width, height) / 2) * marginFactor;
let color = self.handler.data.getPieColorFunc();
let tooltip = self.tooltip;
let isTooltip = self._attr.addTooltip;
/**
* Checks whether pie slices have all zero values.
* If so, an error is thrown.
*/
_validatePieData(charts) {
let isAllZeros = charts.every(function (chart) {
return chart.slices.children.length === 0;
});
let partition = d3.layout.partition()
.sort(null)
.value(function (d) {
return d.percentOfParent * 100;
});
let x = d3.scale.linear()
.range([0, 2 * Math.PI]);
let y = d3.scale.sqrt()
.range([0, radius]);
let arc = d3.svg.arc()
.startAngle(function (d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x)));
})
.endAngle(function (d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
})
.innerRadius(function (d) {
// option for a single layer, i.e pie chart
if (d.depth === 1 && !isDonut) {
// return no inner radius
return 0;
if (isAllZeros) {
throw new errors.PieContainsAllZeros();
}
};
/**
* Adds Events to SVG paths
*
* @method addPathEvents
* @param element {D3.Selection} Reference to SVG path
* @returns {D3.Selection} SVG path with event listeners attached
*/
addPathEvents(element) {
let events = this.events;
return element
.call(events.addHoverEvent())
.call(events.addMouseoutEvent())
.call(events.addClickEvent());
};
convertToPercentage(slices) {
(function assignPercentages(slices) {
if (slices.sumOfChildren != null) return;
let parent = slices;
let children = parent.children;
let parentPercent = parent.percentOfParent;
let sum = parent.sumOfChildren = Math.abs(children.reduce(function (sum, child) {
return sum + Math.abs(child.size);
}, 0));
children.forEach(function (child) {
child.percentOfGroup = Math.abs(child.size) / sum;
child.percentOfParent = child.percentOfGroup;
if (parentPercent != null) {
child.percentOfParent *= parentPercent;
}
if (child.children) {
assignPercentages(child);
}
});
}(slices));
};
/**
* Adds pie paths to SVG
*
* @method addPath
* @param width {Number} Width of SVG
* @param height {Number} Height of SVG
* @param svg {HTMLElement} Chart SVG
* @param slices {Object} Chart data
* @returns {D3.Selection} SVG with paths attached
*/
addPath(width, height, svg, slices) {
let self = this;
let marginFactor = 0.95;
let isDonut = self._attr.isDonut;
let radius = (Math.min(width, height) / 2) * marginFactor;
let color = self.handler.data.getPieColorFunc();
let tooltip = self.tooltip;
let isTooltip = self._attr.addTooltip;
let partition = d3.layout.partition()
.sort(null)
.value(function (d) {
return d.percentOfParent * 100;
});
let x = d3.scale.linear()
.range([0, 2 * Math.PI]);
let y = d3.scale.sqrt()
.range([0, radius]);
let arc = d3.svg.arc()
.startAngle(function (d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x)));
})
.endAngle(function (d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
})
.innerRadius(function (d) {
// option for a single layer, i.e pie chart
if (d.depth === 1 && !isDonut) {
// return no inner radius
return 0;
}
return Math.max(0, y(d.y));
})
.outerRadius(function (d) {
return Math.max(0, y(d.y + d.dy));
});
let path = svg
.datum(slices)
.selectAll('path')
.data(partition.nodes)
.enter()
.append('path')
.attr('d', arc)
.attr('class', function (d) {
if (d.depth === 0) {
return;
}
return 'slice';
})
.call(self._addIdentifier, 'name')
.style('stroke', '#fff')
.style('fill', function (d) {
if (d.depth === 0) {
return 'none';
}
return color(d.name);
});
if (isTooltip) {
path.call(tooltip.render());
}
return Math.max(0, y(d.y));
})
.outerRadius(function (d) {
return Math.max(0, y(d.y + d.dy));
});
let path = svg
.datum(slices)
.selectAll('path')
.data(partition.nodes)
.enter()
.append('path')
.attr('d', arc)
.attr('class', function (d) {
if (d.depth === 0) { return; }
return 'slice';
})
.call(self._addIdentifier, 'name')
.style('stroke', '#fff')
.style('fill', function (d) {
if (d.depth === 0) { return 'none'; }
return color(d.name);
});
if (isTooltip) {
path.call(tooltip.render());
}
return path;
};
PieChart.prototype._validateContainerSize = function (width, height) {
let minWidth = 20;
let minHeight = 20;
if (width <= minWidth || height <= minHeight) {
throw new errors.ContainerTooSmall();
}
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the pie chart
*/
PieChart.prototype.draw = function () {
let self = this;
return function (selection) {
selection.each(function (data) {
let slices = data.slices;
let div = d3.select(this);
let width = $(this).width();
let height = $(this).height();
let path;
if (!slices.children.length) return;
self.convertToPercentage(slices);
self._validateContainerSize(width, height);
let svg = div.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
path = self.addPath(width, height, svg, slices);
self.addPathEvents(path);
return svg;
});
return path;
};
};
_validateContainerSize(width, height) {
let minWidth = 20;
let minHeight = 20;
if (width <= minWidth || height <= minHeight) {
throw new errors.ContainerTooSmall();
}
};
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the pie chart
*/
draw() {
let self = this;
return function (selection) {
selection.each(function (data) {
let slices = data.slices;
let div = d3.select(this);
let width = $(this).width();
let height = $(this).height();
let path;
if (!slices.children.length) return;
self.convertToPercentage(slices);
self._validateContainerSize(width, height);
let svg = div.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
path = self.addPath(width, height, svg, slices);
self.addPathEvents(path);
return svg;
});
};
};
}
return PieChart;
};

View file

@ -18,117 +18,114 @@ export default function TileMapFactory(Private) {
* @param chartEl {HTMLElement} HTML element to which the map will be appended
* @param chartData {Object} Elasticsearch query results for this map
*/
_.class(TileMap).inherits(Chart);
function TileMap(handler, chartEl, chartData) {
if (!(this instanceof TileMap)) {
return new TileMap(handler, chartEl, chartData);
class TileMap extends Chart {
constructor(handler, chartEl, chartData) {
super(handler, chartEl, chartData);
// track the map objects
this.maps = [];
this._chartData = chartData || {};
_.assign(this, this._chartData);
this._appendGeoExtents();
}
TileMap.Super.apply(this, arguments);
/**
* Draws tile map, called on chart render
*
* @method draw
* @return {Function} - function to add a map to a selection
*/
draw() {
let self = this;
// track the map objects
this.maps = [];
this._chartData = chartData || {};
_.assign(this, this._chartData);
// clean up old maps
self.destroy();
this._appendGeoExtents();
}
return function (selection) {
selection.each(function () {
self._appendMap(this);
});
};
};
/**
* Draws tile map, called on chart render
*
* @method draw
* @return {Function} - function to add a map to a selection
*/
TileMap.prototype.draw = function () {
let self = this;
// clean up old maps
self.destroy();
return function (selection) {
selection.each(function () {
self._appendMap(this);
/**
* Invalidate the size of the map, so that leaflet will resize to fit.
* then moves to center
*
* @method resizeArea
* @return {undefined}
*/
resizeArea() {
this.maps.forEach(function (map) {
map.updateSize();
});
};
};
/**
* Invalidate the size of the map, so that leaflet will resize to fit.
* then moves to center
*
* @method resizeArea
* @return {undefined}
*/
TileMap.prototype.resizeArea = function () {
this.maps.forEach(function (map) {
map.updateSize();
});
};
/**
* clean up the maps
*
* @method destroy
* @return {undefined}
*/
destroy() {
this.maps = this.maps.filter(function (map) {
map.destroy();
});
};
/**
* clean up the maps
*
* @method destroy
* @return {undefined}
*/
TileMap.prototype.destroy = function () {
this.maps = this.maps.filter(function (map) {
map.destroy();
});
};
/**
* Adds allmin and allmax properties to geoJson data
*
* @method _appendMap
* @param selection {Object} d3 selection
*/
_appendGeoExtents() {
// add allmin and allmax to geoJson
let geoMinMax = this.handler.data.getGeoExtents();
this.geoJson.properties.allmin = geoMinMax.min;
this.geoJson.properties.allmax = geoMinMax.max;
};
/**
* Adds allmin and allmax properties to geoJson data
*
* @method _appendMap
* @param selection {Object} d3 selection
*/
TileMap.prototype._appendGeoExtents = function () {
// add allmin and allmax to geoJson
let geoMinMax = this.handler.data.getGeoExtents();
this.geoJson.properties.allmin = geoMinMax.min;
this.geoJson.properties.allmax = geoMinMax.max;
};
/**
* Renders map
*
* @method _appendMap
* @param selection {Object} d3 selection
*/
_appendMap(selection) {
const container = $(selection).addClass('tilemap');
const uiStateParams = this.handler.vis ? {
mapCenter: this.handler.vis.uiState.get('mapCenter'),
mapZoom: this.handler.vis.uiState.get('mapZoom')
} : {};
/**
* Renders map
*
* @method _appendMap
* @param selection {Object} d3 selection
*/
TileMap.prototype._appendMap = function (selection) {
const container = $(selection).addClass('tilemap');
const uiStateParams = this.handler.vis ? {
mapCenter: this.handler.vis.uiState.get('mapCenter'),
mapZoom: this.handler.vis.uiState.get('mapZoom')
} : {};
const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams);
const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams);
const map = new TileMapMap(container, this._chartData, {
center: params.mapCenter,
zoom: params.mapZoom,
events: this.events,
markerType: this._attr.mapType,
tooltipFormatter: this.tooltipFormatter,
valueFormatter: this.valueFormatter,
attr: this._attr
});
const map = new TileMapMap(container, this._chartData, {
center: params.mapCenter,
zoom: params.mapZoom,
events: this.events,
markerType: this._attr.mapType,
tooltipFormatter: this.tooltipFormatter,
valueFormatter: this.valueFormatter,
attr: this._attr
});
// add title for splits
if (this.title) {
map.addTitle(this.title);
}
// add title for splits
if (this.title) {
map.addTitle(this.title);
}
// add fit to bounds control
if (_.get(this.geoJson, 'features.length') > 0) {
map.addFitControl();
map.addBoundingControl();
}
// add fit to bounds control
if (_.get(this.geoJson, 'features.length') > 0) {
map.addFitControl();
map.addBoundingControl();
}
this.maps.push(map);
};
this.maps.push(map);
};
}
return TileMap;
};

View file

@ -2,72 +2,70 @@ import d3 from 'd3';
import dateMath from '@elastic/datemath';
export default function TimeMarkerFactory() {
function TimeMarker(times, xScale, height) {
if (!(this instanceof TimeMarker)) {
return new TimeMarker(times, xScale, height);
class TimeMarker {
constructor(times, xScale, height) {
let currentTimeArr = [{
'time': new Date().getTime(),
'class': 'time-marker',
'color': '#c80000',
'opacity': 0.3,
'width': 2
}];
this.xScale = xScale;
this.height = height;
this.times = (times.length) ? times.map(function (d) {
return {
'time': dateMath.parse(d.time),
'class': d.class || 'time-marker',
'color': d.color || '#c80000',
'opacity': d.opacity || 0.3,
'width': d.width || 2
};
}) : currentTimeArr;
}
let currentTimeArr = [{
'time': new Date().getTime(),
'class': 'time-marker',
'color': '#c80000',
'opacity': 0.3,
'width': 2
}];
_isTimeBasedChart(selection) {
let data = selection.data();
return data.every(function (datum) {
return (datum.ordered && datum.ordered.date);
});
};
this.xScale = xScale;
this.height = height;
this.times = (times.length) ? times.map(function (d) {
return {
'time': dateMath.parse(d.time),
'class': d.class || 'time-marker',
'color': d.color || '#c80000',
'opacity': d.opacity || 0.3,
'width': d.width || 2
};
}) : currentTimeArr;
render(selection) {
let self = this;
// return if not time based chart
if (!self._isTimeBasedChart(selection)) return;
selection.each(function () {
d3.select(this).selectAll('time-marker')
.data(self.times)
.enter().append('line')
.attr('class', function (d) {
return d.class;
})
.attr('pointer-events', 'none')
.attr('stroke', function (d) {
return d.color;
})
.attr('stroke-width', function (d) {
return d.width;
})
.attr('stroke-opacity', function (d) {
return d.opacity;
})
.attr('x1', function (d) {
return self.xScale(d.time);
})
.attr('x2', function (d) {
return self.xScale(d.time);
})
.attr('y1', self.height)
.attr('y2', self.xScale.range()[0]);
});
};
}
TimeMarker.prototype._isTimeBasedChart = function (selection) {
let data = selection.data();
return data.every(function (datum) {
return (datum.ordered && datum.ordered.date);
});
};
TimeMarker.prototype.render = function (selection) {
let self = this;
// return if not time based chart
if (!self._isTimeBasedChart(selection)) return;
selection.each(function () {
d3.select(this).selectAll('time-marker')
.data(self.times)
.enter().append('line')
.attr('class', function (d) {
return d.class;
})
.attr('pointer-events', 'none')
.attr('stroke', function (d) {
return d.color;
})
.attr('stroke-width', function (d) {
return d.width;
})
.attr('stroke-opacity', function (d) {
return d.opacity;
})
.attr('x1', function (d) {
return self.xScale(d.time);
})
.attr('x2', function (d) {
return self.xScale(d.time);
})
.attr('y1', self.height)
.attr('y2', self.xScale.range()[0]);
});
};
return TimeMarker;
};