UNPKG

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
/******/ (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