vizabi-bubblechart
Version:
Gapminder's bubble chart data visualisation tool. See it in action on gapminder.org/tools
1,281 lines (1,140 loc) • 144 kB
JavaScript
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // identity function for calling harmony imports with the correct context
/******/ __webpack_require__.i = function(value) { return value; };
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 8);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
__webpack_require__(5);
var _component = __webpack_require__(1);
var _component2 = _interopRequireDefault(_component);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var VERSION_INFO = { version: "2.1.2", build: 1527869512823 };
exports.default = Vizabi.Tool.extend("BubbleChart", {
/**
* Initializes the tool (Bubble Chart Tool).
* Executed once before any template is rendered.
* @param {Object} placeholder Placeholder element for the tool
* @param {Object} external_model Model as given by the external page
*/
init: function init(placeholder, external_model) {
this.name = "bubblechart";
//specifying components
this.components = [{
component: _component2.default,
placeholder: ".vzb-tool-viz",
model: ["state.time", "state.marker", "locale", "ui"] //pass models to component
}, {
component: Vizabi.Component.get("timeslider"),
placeholder: ".vzb-tool-timeslider",
model: ["state.time", "state.marker", "ui"]
}, {
component: Vizabi.Component.get("dialogs"),
placeholder: ".vzb-tool-dialogs",
model: ["state", "ui", "locale"]
}, {
component: Vizabi.Component.get("buttonlist"),
placeholder: ".vzb-tool-buttonlist",
model: ["state", "ui", "locale"]
}, {
component: Vizabi.Component.get("treemenu"),
placeholder: ".vzb-tool-treemenu",
model: ["state.marker", "state.time", "locale", "ui"]
}, {
component: Vizabi.Component.get("datawarning"),
placeholder: ".vzb-tool-datawarning",
model: ["locale"]
}, {
component: Vizabi.Component.get("datanotes"),
placeholder: ".vzb-tool-datanotes",
model: ["state.marker", "locale"]
}, {
component: Vizabi.Component.get("steppedspeedslider"),
placeholder: ".vzb-tool-stepped-speed-slider",
model: ["state.time", "locale"]
}];
this._super(placeholder, external_model);
},
validate: function validate(model) {
model = this.model || model;
this._super(model);
if (model.ui.chart.lockNonSelected && (!model.ui.splash || model.state.time.splash === false)) {
var time = model.state.time.parse("" + model.ui.chart.lockNonSelected);
if (time < model.state.time.start) model.ui.chart.lockNonSelected = model.state.time.formatDate(model.state.time.start);
if (time > model.state.time.end) model.ui.chart.lockNonSelected = model.state.time.formatDate(model.state.time.end);
}
},
/**
* Determines the default model of this tool
*/
default_model: {
state: {
time: {
"autoconfig": {
"type": "time"
}
},
entities: {
"autoconfig": {
"type": "entity_domain",
"excludeIDs": ["tag"]
}
},
entities_colorlegend: {
"autoconfig": {
"type": "entity_domain",
"excludeIDs": ["tag"]
}
},
marker: {
limit: 5000,
space: ["entities", "time"],
axis_x: {
use: "indicator",
"autoconfig": {
index: 0,
type: "measure"
}
},
axis_y: {
use: "indicator",
"autoconfig": {
index: 1,
type: "measure"
}
},
label: {
use: "property",
"autoconfig": {
"includeOnlyIDs": ["name"],
"type": "string"
}
},
size: {
"autoconfig": {
index: 2,
type: "measure"
}
},
color: {
syncModels: ["marker_colorlegend"],
"autoconfig": {}
},
size_label: {
use: "constant",
which: "_default",
scaleType: "ordinal",
_important: false,
extent: [0, 0.33],
allow: {
names: ["_default"]
}
}
},
"marker_colorlegend": {
"space": ["entities_colorlegend"],
"label": {
"use": "property",
"which": "name"
},
"hook_rank": {
"use": "property",
"which": "rank"
},
"hook_geoshape": {
"use": "property",
"which": "shape_lores_svg"
}
}
},
locale: {},
ui: {
chart: {
decorations: {
enabled: true,
xAxisGroups: null
},
superhighlightOnMinimapHover: true,
whenHovering: {
showProjectionLineX: true,
showProjectionLineY: true,
higlightValueX: true,
higlightValueY: true
},
labels: {
dragging: true,
removeLabelBox: false
},
margin: {
left: 0,
top: 0
},
trails: true,
lockNonSelected: 0
},
datawarning: {
doubtDomain: [],
doubtRange: []
},
show_ticks: true,
presentation: false,
panWithArrow: false,
adaptMinMaxZoom: false,
cursorMode: "arrow",
zoomOnScrolling: false,
buttons: ["colors", "find", "zoom", "trails", "lock", "moreoptions", "fullscreen", "presentation"],
dialogs: {
popup: ["colors", "find", "size", "zoom", "moreoptions"],
sidebar: ["colors", "find", "size", "zoom"],
moreoptions: ["opacity", "speed", "axes", "size", "colors", "label", "zoom", "presentation", "about"]
}
}
},
versionInfo: VERSION_INFO
});
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _trail = __webpack_require__(3);
var _trail2 = _interopRequireDefault(_trail);
var _panzoom = __webpack_require__(2);
var _panzoom2 = _interopRequireDefault(_panzoom);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _Vizabi = Vizabi,
utils = _Vizabi.utils;
var _Vizabi$helpers = Vizabi.helpers,
Exporter = _Vizabi$helpers.svgexport,
Labels = _Vizabi$helpers.labels,
axisSmart = _Vizabi$helpers['d3.axisWithLabelPicker'],
DynamicBackground = _Vizabi$helpers['d3.dynamicBackground'];
var _Vizabi$iconset = Vizabi.iconset,
iconWarn = _Vizabi$iconset.warn,
iconQuestion = _Vizabi$iconset.question;
// BUBBLE CHART COMPONENT
var BubbleChart = Vizabi.Component.extend("bubblechart", {
/**
* Initializes the component (Bubble Chart).
* Executed once before any template is rendered.
* @param {Object} config The config passed to the component
* @param {Object} context The component's parent
*/
init: function init(config, context) {
var _this2 = this;
var _this = this;
this.name = "bubblechart";
this.template = __webpack_require__(6);
//define expected models for this component
this.model_expects = [{
name: "time",
type: "time"
}, {
name: "marker",
type: "marker"
}, {
name: "locale",
type: "locale"
}, {
name: "ui",
type: "ui"
}];
this.model_binds = {
"change:time.playing": function changeTimePlaying(evt, original) {
if (utils.isTouchDevice() && _this.model.time.playing && _this.someHighlighted) {
_this.model.marker.clearHighlighted();
}
},
"change:time.start": function changeTimeStart(evt, original) {
if (!_this._readyOnce || _this.model.time.splash) return;
if (["color", "axis_x", "axis_y"].filter(function (hook) {
return _this.model.marker[hook].which == _this.model.time.dim;
}).length) {
_this.ready();
return;
};
_this._trails.create().then(function () {
_this._trails.run(["findVisible", "reveal", "opacityHandler"]);
});
},
"change:time.end": function changeTimeEnd(evt, original) {
if (!_this._readyOnce || _this.model.time.splash) return;
if (["color", "axis_x", "axis_y"].filter(function (hook) {
return _this.model.marker[hook].which == _this.model.time.dim;
}).length) {
_this.ready();
return;
};
_this._trails.create().then(function () {
_this._trails.run(["findVisible", "reveal", "opacityHandler"]);
});
},
"change:time.record": function changeTimeRecord() {
//console.log("change time record");
if (_this.model.time.record) {
_this._export.open(this.element, this.name);
} else {
_this._export.reset();
}
},
"change:ui.chart.trails": function changeUiChartTrails(evt) {
//console.log("EVENT change:time:trails");
if (!_this._readyOnce) return;
_this._trails.toggle(_this.model.ui.chart.trails);
_this.redrawDataPoints();
},
"change:ui.chart.lockNonSelected": function changeUiChartLockNonSelected(evt) {
if (!_this._readyOnce) return;
//console.log("EVENT change:time:lockNonSelected");
_this.redrawDataPoints(500);
},
"change:ui.chart.decorations": function changeUiChartDecorations(evt) {
if (!_this._readyOnce) return;
_this._updateDecorations(500);
},
"change:marker": function changeMarker(evt, path) {
// bubble size change is processed separately
if (!_this._readyOnce) return;
if (path.indexOf("scaleType") > -1) {
_this.ready();
return;
}
if (path.indexOf("marker.color") !== -1) return;
if (path.indexOf("marker.size") !== -1) return;
if (path.indexOf("marker.size_label") !== -1) return;
if (path.indexOf("domainMin") > -1 || path.indexOf("domainMax") > -1) {
if (!_this.yScale || !_this.xScale) return; //abort if building of the scale is in progress
_this.updateSize();
_this.updateMarkerSizeLimits();
_this._trails.run("findVisible");
_this.redrawDataPoints();
_this._trails.run("resize", null, 500);
} else if (path.indexOf("zoomedMin") > -1 || path.indexOf("zoomedMax") > -1) {
if (_this.draggingNow) return;
//avoid zooming again if values didn't change.
//also prevents infinite loop on forced URL update from zoom.stop()
if (utils.approxEqual(_this._zoomedXYMinMax.axis_x.zoomedMin, _this.model.marker.axis_x.zoomedMin, 0.01) && utils.approxEqual(_this._zoomedXYMinMax.axis_x.zoomedMax, _this.model.marker.axis_x.zoomedMax, 0.01) && utils.approxEqual(_this._zoomedXYMinMax.axis_y.zoomedMin, _this.model.marker.axis_y.zoomedMin, 0.01) && utils.approxEqual(_this._zoomedXYMinMax.axis_y.zoomedMax, _this.model.marker.axis_y.zoomedMax, 0.01)) return;
var playAfterZoom = false;
if (_this.model.time.playing) {
playAfterZoom = true;
_this.model.time.pause(true);
}
_this._trails.run("abortAnimation");
_this._panZoom.zoomToMaxMin(_this.model.marker.axis_x.getZoomedMin(), _this.model.marker.axis_x.getZoomedMax(), _this.model.marker.axis_y.getZoomedMin(), _this.model.marker.axis_y.getZoomedMax(), 500 /*duration*/, "don't feed these zoom values back to state");
if (playAfterZoom) {
_this.model.time.postponePause = false;
}
}
//console.log("EVENT change:marker", evt);
},
"change:marker.select": function changeMarkerSelect(evt, path) {
if (!_this._readyOnce || !_this.entityBubbles) return;
//console.log("EVENT change:marker:select");
//disable trails if too many items get selected at once
//otherwise it's too much waiting time
if ((evt.source._val || []).length - (evt.source._previousVal || []).length > 50) _this.model.ui.chart.trails = false;
_this.selectDataPoints();
_this.redrawDataPoints();
_this._trails.create().then(function () {
_this._trails.run(["findVisible", "reveal", "opacityHandler"]);
});
_this.updateBubbleOpacity();
_this._updateDoubtOpacity();
},
"change:marker.superHighlight": function changeMarkerSuperHighlight(evt, path) {
if (_this2._readyOnce) {
_this2._blinkSuperHighlighted();
}
},
"change:marker.highlight": function changeMarkerHighlight(evt, path) {
if (!_this._readyOnce) return;
//path have values if trail is highlighted
if (path != "highlight") {
if (path !== null) {
var titles = _this._formatSTitleValues(path.size, path.color);
_this._updateSTitle(titles[0], titles[1]);
} else {
_this._updateSTitle();
}
return;
}
//console.log("EVENT change:marker:highlight");
_this.highlightDataPoints();
},
"change:time.value": function changeTimeValue() {
if (_this.model.time.splash || !_this._readyOnce || !_this.entityBubbles) return;
if (!_this.calculationQueue) {
// collect timestamp that we request
_this.calculationQueue = [_this.model.time.value.toString()];
} else {
_this.calculationQueue.push(_this.model.time.value.toString());
}
(function (time) {
// isolate timestamp
//_this._bubblesInteract().mouseout();
_this.model.marker.getFrame(time, function (frame, time) {
if (!_this._frameIsValid(frame)) return utils.warn("change:time.value: empty data received from marker.getFrame(). doing nothing");
var index = _this.calculationQueue.indexOf(time.toString()); //
if (index == -1) {
// we was receive more recent frame before so we pass this frame
return;
}
_this.calculationQueue.splice(0, index + 1); // remove timestamps that added to queue before current timestamp
_this.frameChanged(frame, time);
});
})(_this.model.time.value);
},
"change:ui.adaptMinMaxZoom": function changeUiAdaptMinMaxZoom() {
//console.log("EVENT change:ui:adaptMinMaxZoom");
if (_this.model.ui.adaptMinMaxZoom) {
_this._panZoom.expandCanvas(500);
} else {
_this._panZoom.reset();
}
},
"change:marker.size.extent": function changeMarkerSizeExtent(evt, path) {
//console.log("EVENT change:marker:size:max");
if (!_this._readyOnce) return;
_this.updateMarkerSizeLimits();
_this.redrawDataPointsOnlySize();
_this._trails.run("resize");
},
"change:marker.color": function changeMarkerColor(evt, path) {
if (!_this._readyOnce) return;
//console.log("EVENT change:marker:color:palette");
_this.redrawDataPointsOnlyColors();
_this._trails.run("recolor");
},
// 'change:marker.color.palette': function(evt, path) {
// if(!_this._readyOnce) return;
// //console.log("EVENT change:marker:color:palette");
// _this.redrawDataPointsOnlyColors();
// _this._trails.run("recolor");
// },
"change:marker.opacitySelectDim": function changeMarkerOpacitySelectDim() {
_this.updateBubbleOpacity();
},
"change:marker.opacityRegular": function changeMarkerOpacityRegular() {
_this.updateBubbleOpacity();
_this._trails.run("opacityHandler");
},
"change:ui.cursorMode": function changeUiCursorMode() {
var svg = _this.chartSvg;
if (_this.model.ui.cursorMode === "plus") {
svg.classed("vzb-zoomin", true);
svg.classed("vzb-zoomout", false);
svg.classed("vzb-panhand", false);
} else if (_this.model.ui.cursorMode === "minus") {
svg.classed("vzb-zoomin", false);
svg.classed("vzb-zoomout", true);
svg.classed("vzb-panhand", false);
} else if (_this.model.ui.cursorMode === "hand") {
svg.classed("vzb-zoomin", false);
svg.classed("vzb-zoomout", false);
svg.classed("vzb-panhand", true);
} else {
svg.classed("vzb-zoomin", false);
svg.classed("vzb-zoomout", false);
svg.classed("vzb-panhand", false);
}
},
"change:marker.space": function changeMarkerSpace() {
if (_this.someHighlighted) {
_this.model.marker.clearHighlighted();
}
if (_this.someSelected) {
_this.model.marker.clearSelected();
}
},
"ready": function ready() {
// if(_this.model.marker.color.scaleType === 'time') {
// _this.model.marker.color.scale = null;
// utils.defer(function() {
// _this.trigger('ready');
// });
// }
}
};
this._super(config, context);
this.xScale = null;
this.yScale = null;
this.sScale = null;
this.cScale = null;
this.xAxis = axisSmart("bottom");
this.yAxis = axisSmart("left");
_this.COLOR_BLACKISH = "#333";
_this.COLOR_WHITEISH = "#fdfdfd";
this.isCanvasPreviouslyExpanded = false;
this.draggingNow = null;
this._trails = new _trail2.default(this);
this._panZoom = new _panzoom2.default(this);
this._export = new Exporter(this);
this._export.prefix("vzb-bc-").deleteClasses(["vzb-bc-bubbles-crop", "vzb-hidden", "vzb-bc-year", "vzb-bc-zoom-rect", "vzb-bc-projection-x", "vzb-bc-projection-y", "vzb-bc-axis-c-title"]);
this._labels = new Labels(this);
this._labels.config({
CSS_PREFIX: "vzb-bc",
LABELS_CONTAINER_CLASS: "vzb-bc-labels",
LINES_CONTAINER_CLASS: "vzb-bc-bubbles",
LINES_CONTAINER_SELECTOR_PREFIX: "bubble-"
});
},
_rangeBump: function _rangeBump(arg, undo) {
var bump = this.activeProfile.maxRadiusPx / 2;
undo = undo ? -1 : 1;
if (utils.isArray(arg) && arg.length > 1) {
var z1 = arg[0];
var z2 = arg[arg.length - 1];
//the sign of bump depends on the direction of the scale
if (z1 < z2) {
z1 += bump * undo;
z2 -= bump * undo;
// if the scale gets inverted because of bump, set it to avg between z1 and z2
if (z1 > z2) z1 = z2 = (z1 + z2) / 2;
} else if (z1 > z2) {
z1 -= bump * undo;
z2 += bump * undo;
// if the scale gets inverted because of bump, set it to avg between z1 and z2
if (z1 < z2) z1 = z2 = (z1 + z2) / 2;
} else {
// rangeBump error: the input scale range has 0 length. that sucks but we keep cool
}
return [z1, z2];
}
utils.warn("rangeBump error: input is not an array or empty");
},
/**
* Executes right after the template is in place, but the model is not yet ready
*/
readyOnce: function readyOnce() {
var _this = this;
this._readyOnce = false;
this.scrollableAncestor = utils.findScrollableAncestor(this.element);
this.element = d3.select(this.element);
// reference elements
this.chartSvg = this.element.select("svg");
this.graph = this.element.select(".vzb-bc-graph");
this.yAxisElContainer = this.graph.select(".vzb-bc-axis-y");
this.yAxisEl = this.yAxisElContainer.select("g");
this.xAxisElContainer = this.graph.select(".vzb-bc-axis-x");
this.xAxisEl = this.xAxisElContainer.select("g");
this.ySubTitleEl = this.graph.select(".vzb-bc-axis-y-subtitle");
this.xSubTitleEl = this.graph.select(".vzb-bc-axis-x-subtitle");
this.yTitleEl = this.graph.select(".vzb-bc-axis-y-title");
this.xTitleEl = this.graph.select(".vzb-bc-axis-x-title");
this.sTitleEl = this.graph.select(".vzb-bc-axis-s-title");
this.cTitleEl = this.graph.select(".vzb-bc-axis-c-title");
this.yearEl = this.graph.select(".vzb-bc-year");
this.year = new DynamicBackground(this.yearEl);
this.yInfoEl = this.graph.select(".vzb-bc-axis-y-info");
this.xInfoEl = this.graph.select(".vzb-bc-axis-x-info");
this.dataWarningEl = this.graph.select(".vzb-data-warning");
this.projectionX = this.graph.select(".vzb-bc-projection-x");
this.projectionY = this.graph.select(".vzb-bc-projection-y");
this.decorationsEl = this.graph.select(".vzb-bc-decorations");
this.lineEqualXY = this.decorationsEl.select(".vzb-bc-line-equal-xy");
this.xAxisGroupsEl = this.decorationsEl.select(".vzb-bc-x-axis-groups");
this.trailsContainer = this.graph.select(".vzb-bc-trails");
this.bubbleContainerCrop = this.graph.select(".vzb-bc-bubbles-crop");
this.zoomSelection = this.graph.select(".vzb-zoom-selection");
this.labelsContainerCrop = this.graph.select(".vzb-bc-labels-crop");
this.bubbleContainer = this.graph.select(".vzb-bc-bubbles");
this.labelsContainer = this.graph.select(".vzb-bc-labels");
this.linesContainer = this.graph.select(".vzb-bc-lines");
this.zoomRect = this.element.select(".vzb-bc-zoom-rect");
this.eventArea = this.element.select(".vzb-bc-eventarea");
this.entityBubbles = null;
this.bubbleCrown = this.element.select(".vzb-bc-bubble-crown");
//set filter
this.bubbleCrown.selectAll(".vzb-crown-glow").attr("filter", "url(" + location.pathname + "#vzb-glow-filter)");
this.tooltipMobile = this.element.select(".vzb-tooltip-mobile");
//component events
this.on("resize", function () {
//console.log("EVENT: resize");
//return if updatesize exists with error
_this._trails.run("abortAnimation");
if (_this.updateSize()) return;
_this.updateMarkerSizeLimits();
_this._labels.updateSize();
(function (xMin, xMax, yMin, yMax) {
_this._panZoom.zoomer.dontFeedToState = true;
_this._panZoom.rerun(); // includes redraw data points and trail resize
_this._panZoom.zoomToMaxMin(xMin, xMax, yMin, yMax, 0, true);
})(_this._zoomedXYMinMax.axis_x.zoomedMin, _this._zoomedXYMinMax.axis_x.zoomedMax, _this._zoomedXYMinMax.axis_y.zoomedMin, _this._zoomedXYMinMax.axis_y.zoomedMax);
});
//keyboard listeners
d3.select("body").on("keydown", function () {
if (_this.model.ui.cursorMode !== "arrow" && _this.model.ui.cursorMode !== "hand") return;
if (d3.event.metaKey || d3.event.ctrlKey) _this.element.select("svg").classed("vzb-zoomin", true);
}).on("keyup", function () {
if (_this.model.ui.cursorMode !== "arrow" && _this.model.ui.cursorMode !== "hand") return;
if (!d3.event.metaKey && !d3.event.ctrlKey) _this.element.select("svg").classed("vzb-zoomin", false);
})
//this is for the case when user would press ctrl and move away from the browser tab or window
//keyup event would happen somewhere else and won't be captured, so zoomin class would get stuck
.on("mouseenter", function () {
if (_this.model.ui.cursorMode !== "arrow" && _this.model.ui.cursorMode !== "hand") return;
if (!d3.event.metaKey && !d3.event.ctrlKey) _this.element.select("svg").classed("vzb-zoomin", false);
});
this.root.on("resetZoom", function () {
_this._panZoom.reset(null, 500);
});
this._panZoom.zoomSelection(this.bubbleContainerCrop);
this.bubbleContainerCrop.call(this._panZoom.dragRectangle).call(this._panZoom.zoomer).on("dblclick.zoom", null).on("mouseup", function () {
_this.draggingNow = false;
}).on("click", function () {
var cursor = _this.model.ui.cursorMode;
if (!d3.event.defaultPrevented && cursor !== "arrow" && cursor !== "hand") {
_this._panZoom.zoomByIncrement(cursor, 500);
}
});
this.TIMEDIM = this.model.time.getDimension();
this.KEYS = utils.unique(this.model.marker._getAllDimensions({ exceptType: "time" }));
this.KEY = this.KEYS.join(",");
this.dataKeys = this.model.marker.getDataKeysPerHook();
this.updateUIStrings();
this.wScale = d3.scaleLinear().domain(this.model.ui.datawarning.doubtDomain).range(this.model.ui.datawarning.doubtRange);
this._labels.readyOnce();
_this._readyOnce = true;
},
_frameIsValid: function _frameIsValid(frame) {
return !(!frame || Object.keys(frame.axis_y).length === 0 || Object.keys(frame.axis_x).length === 0 || Object.keys(frame.size).length === 0);
},
ready: function ready() {
var _this = this;
this.KEYS = utils.unique(this.model.marker._getAllDimensions({ exceptType: "time" }));
this.KEY = this.KEYS.join(",");
this.dataKeys = this.model.marker.getDataKeysPerHook();
this.updateUIStrings();
var endTime = this.model.time.end;
this.updateIndicators();
this.updateTime();
if (!_this.model.time.splash) {
_this._trails.create();
}
this.model.marker.getFrame(this.model.time.value, function (frame, time) {
// TODO: temporary fix for case when after data loading time changed on validation
if (time.toString() != _this.model.time.value.toString()) {
utils.defer(function () {
_this.ready();
});
return;
}
if (!_this._frameIsValid(frame)) return utils.warn("ready: empty data received from marker.getFrame(). doing nothing");
_this.frame = frame;
_this.updateSize();
_this.updateMarkerSizeLimits();
_this.updateEntities();
_this._labels.ready();
_this.redrawDataPoints();
_this.selectDataPoints();
_this.updateBubbleOpacity();
_this._updateDoubtOpacity();
_this.zoomToMarkerMaxMin(); // includes redraw data points and trail resize
if (!_this.model.time.splash) {
_this._trails.run(["findVisible", "reveal", "opacityHandler"]);
}
if (_this.model.ui.adaptMinMaxZoom) _this._panZoom.expandCanvas();
});
},
/*
* Zoom to the min and max values given in the URL axes markers.
*/
zoomToMarkerMaxMin: function zoomToMarkerMaxMin() {
/*
* Reset just the zoom values without triggering a zoom event. This ensures
* a clean zoom state for the subsequent zoom event.
*/
this._panZoom.resetZoomState();
var xAxis = this.model.marker.axis_x;
var yAxis = this.model.marker.axis_y;
var xDomain = xAxis.getScale().domain();
var yDomain = yAxis.getScale().domain();
/*
* The axes may return null when there is no value given for the zoomed
* min and max values. In that case, fall back to the axes' domain values.
*/
var zoomedMinX = xAxis.getZoomedMin();
var zoomedMaxX = xAxis.getZoomedMax();
var zoomedMinY = yAxis.getZoomedMin();
var zoomedMaxY = yAxis.getZoomedMax();
//by default this will apply no transition and feed values back to state
this._panZoom.zoomToMaxMin(zoomedMinX, zoomedMaxX, zoomedMinY, zoomedMaxY, 0, "don't feed these zoom values back to state");
},
/*
* UPDATE INDICATORS
*/
updateIndicators: function updateIndicators() {
var _this = this;
//scales
this.yScale = this.model.marker.axis_y.getScale();
this.xScale = this.model.marker.axis_x.getScale();
this.sScale = this.model.marker.size.getScale();
this.cScale = this.model.marker.color.getScale();
this._labels.setScales(this.xScale, this.yScale);
this.yAxis.tickFormat(_this.model.marker.axis_y.getTickFormatter());
this.xAxis.tickFormat(_this.model.marker.axis_x.getTickFormatter());
},
frameChanged: function frameChanged(frame, time) {
// if (time.toString() != this.model.time.value.toString()) return; // frame is outdated
this.frame = frame;
this.updateTime();
this._updateDoubtOpacity();
this._trails.run("findVisible");
if (this.model.ui.adaptMinMaxZoom) {
this._panZoom.expandCanvas();
} else {
this.redrawDataPoints();
}
this._trails.run("reveal", null, this.duration);
this.tooltipMobile.classed("vzb-hidden", true);
this._reorderEntities();
},
_getSubtitle: function _getSubtitle(title, shortTitle) {
var subtitle = title.replace(shortTitle, "");
if (subtitle[0] === ",") subtitle = subtitle.slice(1);
var regexpResult = /^\((.*)\)$|.*/.exec(subtitle.trim());
return regexpResult[1] || regexpResult[0] || "";
},
updateUIStrings: function updateUIStrings() {
var _this = this;
var layoutProfile = this.getLayoutProfile();
var conceptPropsY = _this.model.marker.axis_y.getConceptprops();
var conceptPropsX = _this.model.marker.axis_x.getConceptprops();
var conceptPropsS = _this.model.marker.size.getConceptprops();
var conceptPropsC = _this.model.marker.color.getConceptprops();
this.translator = this.model.locale.getTFunction();
this.strings = {
title: {
Y: conceptPropsY.name,
X: conceptPropsX.name,
S: conceptPropsS.name,
C: conceptPropsC.name
},
title_short: {
Y: conceptPropsY.name_short,
X: conceptPropsX.name_short,
S: conceptPropsS.name_short,
C: conceptPropsC.name_short
},
subtitle: {
Y: this._getSubtitle(conceptPropsY.name, conceptPropsY.name_short),
X: this._getSubtitle(conceptPropsX.name, conceptPropsX.name_short),
S: conceptPropsS.name_short,
C: conceptPropsC.name_short
},
unit: {
Y: conceptPropsY.unit || "",
X: conceptPropsX.unit || "",
S: conceptPropsS.unit || "",
C: conceptPropsC.unit || ""
}
};
var ySubTitle = this.ySubTitleEl.selectAll("text").data([0]);
ySubTitle.enter().append("text");
var xSubTitle = this.xSubTitleEl.selectAll("text").data([0]);
xSubTitle.enter().append("text");
var yTitle = this.yTitleEl.selectAll("text").data([0]);
yTitle.enter().append("text");
yTitle
//.attr("y", "-6px")
.on("click", function () {
_this.parent.findChildByName("gapminder-treemenu").markerID("axis_y").alignX(_this.model.locale.isRTL() ? "right" : "left").alignY("top").updateView().toggle();
});
var xTitle = this.xTitleEl.selectAll("text").data([0]);
xTitle.enter().append("text");
xTitle.on("click", function () {
_this.parent.findChildByName("gapminder-treemenu").markerID("axis_x").alignX(_this.model.locale.isRTL() ? "right" : "left").alignY("bottom").updateView().toggle();
});
var sTitle = this.sTitleEl.selectAll("text").data([0]);
sTitle.enter().append("text");
sTitle.attr("text-anchor", "end");
utils.setIcon(this.dataWarningEl, iconWarn).select("svg").attr("width", "0px").attr("height", "0px");
this.dataWarningEl.append("text").attr("text-anchor", "end").text(this.translator("hints/dataWarning"));
utils.setIcon(this.yInfoEl, iconQuestion).select("svg").attr("width", "0px").attr("height", "0px").style('opacity', Number(Boolean(conceptPropsY.description || conceptPropsY.sourceLink)));
utils.setIcon(this.xInfoEl, iconQuestion).select("svg").attr("width", "0px").attr("height", "0px").style('opacity', Number(Boolean(conceptPropsX.description || conceptPropsX.sourceLink)));
//TODO: move away from UI strings, maybe to ready or ready once
this.yInfoEl.on("click", function () {
_this.parent.findChildByName("gapminder-datanotes").pin();
});
this.yInfoEl.on("mouseover", function () {
var rect = this.getBBox();
var coord = utils.makeAbsoluteContext(this, this.farthestViewportElement)(rect.x - 10, rect.y + rect.height + 10);
var toolRect = _this.root.element.getBoundingClientRect();
var chartRect = _this.element.node().getBoundingClientRect();
_this.parent.findChildByName("gapminder-datanotes").setHook("axis_y").show().setPos(coord.x + chartRect.left - toolRect.left, coord.y);
});
this.yInfoEl.on("mouseout", function () {
_this.parent.findChildByName("gapminder-datanotes").hide();
});
this.xInfoEl.on("click", function () {
_this.parent.findChildByName("gapminder-datanotes").pin();
});
this.xInfoEl.on("mouseover", function () {
if (_this.model.time.dragging) return;
var rect = this.getBBox();
var coord = utils.makeAbsoluteContext(this, this.farthestViewportElement)(rect.x - 10, rect.y + rect.height + 10);
var toolRect = _this.root.element.getBoundingClientRect();
var chartRect = _this.element.node().getBoundingClientRect();
_this.parent.findChildByName("gapminder-datanotes").setHook("axis_x").show().setPos(coord.x + chartRect.left - toolRect.left, coord.y);
});
this.xInfoEl.on("mouseout", function () {
if (_this.model.time.dragging) return;
_this.parent.findChildByName("gapminder-datanotes").hide();
});
this.dataWarningEl.on("click", function () {
_this.parent.findChildByName("gapminder-datawarning").toggle();
}).on("mouseover", function () {
_this._updateDoubtOpacity(1);
}).on("mouseout", function () {
_this._updateDoubtOpacity();
});
},
_updateDoubtOpacity: function _updateDoubtOpacity(opacity) {
if (opacity == null) opacity = this.wScale(+this.model.time.formatDate(this.time));
if (this.someSelected) opacity = 1;
this.dataWarningEl.style("opacity", opacity);
},
/*
* UPDATE ENTITIES:
* Ideally should only update when show parameters change or data changes
*/
updateEntities: function updateEntities() {
var _this = this;
var dataKeys = this.dataKeys = this.model.marker.getDataKeysPerHook();
var KEYS = this.KEYS;
var KEY = this.KEY;
var TIMEDIM = this.TIMEDIM;
var getKeys = function getKeys(prefix) {
prefix = prefix || "";
return _this.model.marker.getKeys().map(function (d) {
var pointer = Object.assign({}, d);
//pointer[KEY] = d[KEY];
pointer[TIMEDIM] = endTime;
pointer.sortValue = _this.frame.size[utils.getKey(d, dataKeys.size)] || 0;
pointer[KEY] = prefix + utils.getKey(d, KEYS);
return pointer;
}).sort(function (a, b) {
return b.sortValue - a.sortValue;
});
};
// get array of GEOs, sorted by the size hook
// that makes larger bubbles go behind the smaller ones
var endTime = this.model.time.end;
var markers = getKeys.call(this);
this.model.marker.setVisible(markers);
//unselecting bubbles with no data is used for the scenario when
//some bubbles are selected and user would switch indicator.
//bubbles would disappear but selection would stay
if (!this.model.time.splash) {
this.unselectBubblesWithNoData(markers);
}
this.entityBubbles = this.bubbleContainer.selectAll("circle.vzb-bc-entity").data(this.model.marker.getVisible(), function (d) {
return d[KEY];
}); // trails have not keys
//exit selection
this.entityBubbles.exit().remove();
//enter selection -- init circles
this.entityBubbles = this.entityBubbles.enter().append("circle").attr("class", function (d) {
return "vzb-bc-entity " + "bubble-" + d[KEY];
}).on("mouseover", function (d, i) {
if (utils.isTouchDevice() || _this.model.ui.cursorMode !== "arrow" && _this.model.ui.cursorMode !== "hand") return;
_this._bubblesInteract().mouseover(d, i);
}).on("mouseout", function (d, i) {
if (utils.isTouchDevice() || _this.model.ui.cursorMode !== "arrow" && _this.model.ui.cursorMode !== "hand") return;
_this._bubblesInteract().mouseout(d, i);
}).on("click", function (d, i) {
if (utils.isTouchDevice() || _this.model.ui.cursorMode !== "arrow" && _this.model.ui.cursorMode !== "hand") return;
_this._bubblesInteract().click(d, i);
}).onTap(function (d, i) {
d3.event.stopPropagation();
_this._bubblesInteract().click(d, i);
}).onLongTap(function (d, i) {}).merge(this.entityBubbles);
this._reorderEntities();
},
unselectBubblesWithNoData: function unselectBubblesWithNoData(entities) {
var _this = this;
var KEYS = this.KEYS;
var KEY = this.KEY;
if (!this.model.marker.select.length) return;
var _select = [];
var keys = entities.map(function (d) {
return d[KEY];
});
this.model.marker.select.forEach(function (d) {
if (keys.indexOf(utils.getKey(d, KEYS)) !== -1) _select.push(d);
});
if (_select.length !== _this.model.marker.select.length) _this.model.marker.select = _select;
},
_reorderEntities: function _reorderEntities() {
var _this = this;
var dataKeys = this.dataKeys;
var KEY = this.KEY;
this.bubbleContainer.selectAll(".vzb-bc-entity").sort(function (a, b) {
var sizeA = _this.frame.size[utils.getKey(a, dataKeys.size)];
var sizeB = _this.frame.size[utils.getKey(b, dataKeys.size)];
if (typeof sizeA === "undefined" && typeof sizeB !== "undefined") return -1;
if (typeof sizeA !== "undefined" && typeof sizeB === "undefined") return 1;
if (sizeA != sizeB) return d3.descending(sizeA, sizeB);
if (a[KEY] != b[KEY]) return d3.ascending(a[KEY], b[KEY]);
if (typeof a.trailStartTime !== "undefined" || typeof b.trailStartTime !== "undefined") return typeof a.trailStartTime !== "undefined" ? -1 : 1; // only lines has trailStartTime
if (typeof a.status !== "undefined" || typeof b.status !== "undefined") return typeof a.status !== "undefined" ? -1 : 1; // only trails has attribute status
return d3.descending(sizeA, sizeB);
});
},
_bubblesInteract: function _bubblesInteract() {
var _this = this;
var KEY = this.KEY;
var TIMEDIM = this.TIMEDIM;
return {
mouseover: function mouseover(d, i) {
_this.model.marker.highlightMarker(d);
_this._labels.showCloseCross(d, true);
},
mouseout: function mouseout(d, i) {
_this.model.marker.clearHighlighted();
_this._labels.showCloseCross(d, false);
},
click: function click(d, i) {
if (_this.draggingNow) return;
var isSelected = _this.model.marker.isSelected(d);
_this.model.marker.selectMarker(d);
//return to highlighted state
if (!utils.isTouchDevice()) {
if (isSelected) _this.model.marker.highlightMarker(d);
_this.highlightDataPoints();
}
}
};
},
/*
* UPDATE TIME:
* Ideally should only update when time or data changes
*/
updateTime: function updateTime() {
var _this = this;
this.time_1 = this.time == null ? this.model.time.value : this.time;
this.time = this.model.time.value;
this.duration = this.model.time.playing && this.time - this.time_1 > 0 ? this.model.time.delayAnimations : 0;
this.year.setText(this.model.time.formatDate(this.time, "ui"), this.duration);
},
/*
* RESIZE:
* Executed whenever the container is resized
*/
updateSize: function updateSize() {
var chartSvg = this.chartSvg;
var svgWidth = utils.px2num(chartSvg.style("width"));
var svgHeight = utils.px2num(chartSvg.style("height"));
var marginScaleH = function marginScaleH(marginMin) {
var ratio = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
return marginMin + svgHeight * ratio;
};
var marginScaleW = function marginScaleW(marginMin) {
var ratio = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
return marginMin + svgWidth * ratio;
};
var profiles = {
small: {
margin: { top: 30, bottom: 35, left: 30, right: 10 },
leftMarginRatio: 1,
padding: 2,
minRadiusPx: 0.5,
maxRadiusEm: this.model.ui.chart.maxRadiusEm || 0.05,
infoElHeight: 16,
yAxisTitleBottomMargin: 6,
xAxisTitleBottomMargin: 4
},
medium: {
margin: { top: 15, bottom: 40, left: 40, right: 15 },
leftMarginRatio: 1.6,
padding: 2,
minRadiusPx: 1,
maxRadiusEm: this.model.ui.chart.maxRadiusEm || 0.05,
infoElHeight: 20,
yAxisTitleBottomMargin: 3,
xAxisTitleBottomMargin: 4
},
large: {
margin: { top: 15, bottom: marginScaleH(30, 0.03), left: marginScaleW(31, 0.015), right: 20 },
leftMarginRatio: 1.8,
padding: 2,
minRadiusPx: 1,
maxRadiusEm: this.model.ui.chart.maxRadiusEm || 0.05,
infoElHeight: 22,
yAxisTitleBottomMargin: 3, //marginScaleH(4, 0.01),
xAxisTitleBottomMargin: marginScaleH(0, 0.01),
hideSTitle: true
}
};
var presentationProfileChanges = {
medium: {
margin: { top: 20, bottom: 55, left: 50, right: 20 },
yAxisTitleBottomMargin: 3,
xAxisTitleBottomMargin: 4,
infoElHeight: 26
},
large: {
margin: { top: 30, bottom: marginScaleH(45, 0.03), left: marginScaleW(35, 0.025), right: 30 },
yAxisTitleBottomMargin: 3, //marginScaleH(4, 0.01),
xAxisTitleBottomMargin: marginScaleH(-10, 0.01),
infoElHeight: 32,
hideSTitle: true
}
};
var _this = this;
this.activeProfile = this.getActiveProfile(profiles, presentationProfileChanges);
var layoutProfile = this.getLayoutProfile();
var containerWH = this.root.getVizWidthHeight();
this.activeProfile.maxRadiusPx = Math.max(this.activeProfile.minRadiusPx, this.activeProfile.maxRadiusEm * utils.hypotenuse(containerWH.width, containerWH.height));
var margin = this.activeProfile.margin;
var infoElHeight = this.activeProfile.infoElHeight;
//labels
_this._labels.setCloseCrossHeight(_this.activeProfile.infoElHeight * 1.2);
_this._labels.setTooltipFontSize(_this.activeProfile.infoElHeight + "px");
//stage
this.height = parseInt(this.element.style("height"), 10) - margin.top - margin.bottom || 0;
this.width = parseInt(this.element.style("width"), 10) - margin.left * this.activeProfile.leftMarginRatio - margin.right || 0;
if (this.height <= 0 || this.width <= 0) {
this.height = 0;
this.width = 0;
utils.warn("Bubble chart updateSize(): vizabi container is too little or has display:none");
}
//graph group is shifted according to margins (while svg element is at 100 by 100%)
this.graph.attr("transform", "translate(" + margin.left * this.activeProfile.leftMarginRatio + "," + margin.top + ")");
this.year.resize(this.width, this.height);
this.eventArea.attr("width", this.width).attr("height", Math.max(0, this.height));
//update scales to the new range
if (this.model.marker.axis_y.scaleType !== "ordinal") {
this.yScale.range(this._rangeBump([this.height, 0]));
} else {
this.yScale.rangePoints([this.height, 0], _this.activeProfile.padding).range();
}
if (this.model.marker.axis_x.scaleType !== "ordinal") {
this.xScale.range(this._rangeBump([0, this.width]));
} else {
this.xScale.rangePoints([0, this.width], _this.activeProfile.padding).range();
}
//apply scales to axes and redraw
this.yAxis.scale(this.yScale).tickSizeInner(-this.width).tickSizeOuter(0).tickPadding(6).tickSizeMinor(-this.width, 0).labelerOptions({
scaleType: this.model.marker.axis_y.scaleType,
toolMargin: margin,
limitMaxTickNumber: 6,
bump: this.activeProfile.maxRadiusPx / 2,
viewportLength: this.height,
formatter: this.model.marker.axis_y.getTickFormatter()
});
this.xAxis.scale(this.xScale).tickSizeInner(-this.height).tickSizeOuter(0).tickPadding(6).tickSizeMinor(-this.height, 0).labelerOptions({
scaleType: this.model.marker.axis_x.scaleType,
toolMargin: margin,
bump: this.activeProfile.maxRadiusPx / 2,
viewportLength: this.width,
formatter: this.model.marker.axis_x.getTickFormatter()
});
this.bubbleContainerCrop.attr("width", this.width).attr("height", Math.max(0, this.height));
this.labelsContainerCrop.attr("width", this.width).attr("height", Math.max(0, this.height));
this.xAxisElContainer.attr("width", this.width + 1).attr("height", this.activeProfile.margin.bottom + this.height).attr("y", -1).attr("x", -1);
this.xAxisEl.attr("transform", "translate(1," + (1 + this.height) + ")");
this.yAxisElContainer.attr("width", this.activeProfile.margin.left + this.width).attr("height", Math.max(0, this.height)).attr("x", -this.activeProfile.margin.left);
this.yAxisEl.attr("transform", "translate(" + (this.activeProfile.margin.left - 1) + "," + 0 + ")");
this.yAxisEl.call(this.yAxis);
this.xAxisEl.call(this.xAxis);
this.projectionX.attr("y1", _this.yScale.range()[0] + this.activeProfile.maxRadiusPx / 2);
this.projectionY.attr("x2", _this.xScale.range()[0] - this.activeProfile.maxRadiusPx / 2);
// reduce font size if the caption doesn't fit
this._updateSTitle();
this.sTitleEl.attr("transform", "translate(" + this.width + "," + 20 + ") rotate(-90)");
if (layoutProfile !== "small") {
this.ySubTitleEl.select("text").attr("dy", infoElHeight * 0.6).text(this.strings.subtitle.Y);
this.xSubTitleEl.select("text").attr("dy", -infoElHeight * 0.3).text(this.strings.subtitle.X);
this.yTitleEl.select("text").text(this.strings.title_short.Y + " ").append("tspan").style("font-size", infoElHeight * 0.7 + "px").text("▼");
this.xTitleEl.select("text").text(this.strings.title_short.X + " ").append("tspan").style("font-size", infoElHeight * 0.7 + "px").text("▼");
} else {
this.ySubTitleEl.select("text").text("");
this.xSubTitleEl.select("text").text("");
var yTitleText = this.yTitleEl.select("text").text(this.strings.title.Y);
if (yTitleText.node().getBBox().width > this.width) yTitleText.text(this.strings.title_short.Y);
var xTitleText = this.xTitleEl.select("text").text(this.strings.title.X);
if (xTitleText.node().getBBox().width > this.width - 100) xTitleText.text(this.strings.title_short.X);
}
var isRTL = this.model.locale.isRTL();
this.ySubTitleEl.style("font-size", infoElHeight * 0.8 + "px").attr("transform", "translate(" + 0 + "," + 0 + ") rotate(-90)");
this.xSubTitleEl.style("font-size", infoElHeight * 0.8 + "px").attr("transform", "translate(" + this.width + "," + this.height + ")");
this.yTitleEl.style("font-size", infoElHeight + "px").attr("transform", layoutProfile !== "small" ? "translate(" + (-margin.left - this.activeProfile.yAxisTitleBottomMargin) + "," + this.height * 0.5 + ") rotate(-90)" : "translate(" + (isRTL ? this.width : 10 - this.activeProfile.margin.left) + ", -" + this.activeProfile.yAxisTitleBottomMargin + ")");
this.xTitleEl.style("font-size", infoElHeight + "px").attr("transform", layoutProfile !== "small" ? "translate(" + this.width * 0.5 + "," + (this.height + margin.bottom - this.activeProfile.xAxisTitleBottomMargin) + ")" : "translate(" + (isRTL ? this.width : 0) + "," + (this.height + margin.bottom - this.activeProfile.xAxisTitleBottomMargin) + ")");
if (this.yInfoEl.select("svg").node()) {
var titleBBox = this.yTitleEl.node().getBBox();
var t = utils.transform(this.yTitleEl.node());
var hTranslate = isRTL ? titleBBox.x + t.translateX - infoElHeight * 1.4 : titleBBox.x + t.translateX + titleBBox.width + infoElHeight * 0.4;
var vTranslate = isRTL ? t.translateY + infoElHeight * 1.4 + titleBBox.width * 0.5 : t.translateY - infoElHeight * 0.4 - titleBBox.width * 0.5;
this.yInfoEl.select("svg").attr("width", infoElHeight + "px").attr("height", infoElHeight + "px");
this.yInfoEl