From c8d6926db7e8ef07ffefff1cc8ee98e3fc93449b Mon Sep 17 00:00:00 2001 From: James Dai Date: Wed, 16 May 2018 17:46:15 +0100 Subject: [PATCH 01/46] =?UTF-8?q?preliminary=20copied=20d3=20example?= =?UTF-8?q?=E2=80=93=20no=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javascripts/d3_data_model_view/d3.js | 4149 +++++++++++++++++ .../d3_data_model_view/d3.layout.js | 1890 ++++++++ .../javascripts/d3_data_model_view/style.css | 89 + .../d3_data_model_view/texture-noise.png | Bin 0 -> 25768 bytes .../conf/DataModelUrlMappings.groovy | 1 + .../security/MetadataSecurityService.groovy | 1 + .../views/dataModel/d3_data_model_view.gsp | 191 + 7 files changed, 6321 insertions(+) create mode 100644 ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.js create mode 100644 ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.layout.js create mode 100644 ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css create mode 100644 ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/texture-noise.png create mode 100644 ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.js new file mode 100644 index 0000000000..afa852bc80 --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.js @@ -0,0 +1,4149 @@ +(function(){if (!Date.now) Date.now = function() { + return +new Date; +}; + try { + document.createElement("div").style.setProperty("opacity", 0, ""); + } catch (error) { + var d3_style_prototype = CSSStyleDeclaration.prototype, + d3_style_setProperty = d3_style_prototype.setProperty; + d3_style_prototype.setProperty = function(name, value, priority) { + d3_style_setProperty.call(this, name, value + "", priority); + }; + } + d3 = {version: "2.4.4"}; // semver + var d3_array = d3_arraySlice; // conversion for NodeLists + + function d3_arrayCopy(pseudoarray) { + var i = -1, n = pseudoarray.length, array = []; + while (++i < n) array.push(pseudoarray[i]); + return array; + } + + function d3_arraySlice(pseudoarray) { + return Array.prototype.slice.call(pseudoarray); + } + + try { + d3_array(document.documentElement.childNodes)[0].nodeType; + } catch(e) { + d3_array = d3_arrayCopy; + } + + var d3_arraySubclass = [].__proto__? + + // Until ECMAScript supports array subclassing, prototype injection works well. + function(array, prototype) { + array.__proto__ = prototype; + }: + + // And if your browser doesn't support __proto__, we'll use direct extension. + function(array, prototype) { + for (var property in prototype) array[property] = prototype[property]; + }; + function d3_this() { + return this; + } + d3.functor = function(v) { + return typeof v === "function" ? v : function() { return v; }; + }; +// A getter-setter method that preserves the appropriate `this` context. + d3.rebind = function(object, method) { + return function() { + var x = method.apply(object, arguments); + return arguments.length ? object : x; + }; + }; + d3.ascending = function(a, b) { + return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; + }; + d3.descending = function(a, b) { + return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN; + }; + d3.mean = function(array, f) { + var n = array.length, + a, + m = 0, + i = -1, + j = 0; + if (arguments.length === 1) { + while (++i < n) if (d3_number(a = array[i])) m += (a - m) / ++j; + } else { + while (++i < n) if (d3_number(a = f.call(array, array[i], i))) m += (a - m) / ++j; + } + return j ? m : undefined; + }; + d3.median = function(array, f) { + if (arguments.length > 1) array = array.map(f); + array = array.filter(d3_number); + return array.length ? d3.quantile(array.sort(d3.ascending), .5) : undefined; + }; + d3.min = function(array, f) { + var i = -1, + n = array.length, + a, + b; + if (arguments.length === 1) { + while (++i < n && ((a = array[i]) == null || a != a)) a = undefined; + while (++i < n) if ((b = array[i]) != null && a > b) a = b; + } else { + while (++i < n && ((a = f.call(array, array[i], i)) == null || a != a)) a = undefined; + while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b; + } + return a; + }; + d3.max = function(array, f) { + var i = -1, + n = array.length, + a, + b; + if (arguments.length === 1) { + while (++i < n && ((a = array[i]) == null || a != a)) a = undefined; + while (++i < n) if ((b = array[i]) != null && b > a) a = b; + } else { + while (++i < n && ((a = f.call(array, array[i], i)) == null || a != a)) a = undefined; + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b; + } + return a; + }; + function d3_number(x) { + return x != null && !isNaN(x); + } + d3.sum = function(array, f) { + var s = 0, + n = array.length, + a, + i = -1; + + if (arguments.length === 1) { + while (++i < n) if (!isNaN(a = +array[i])) s += a; + } else { + while (++i < n) if (!isNaN(a = +f.call(array, array[i], i))) s += a; + } + + return s; + }; +// R-7 per + d3.quantile = function(values, p) { + var H = (values.length - 1) * p + 1, + h = Math.floor(H), + v = values[h - 1], + e = H - h; + return e ? v + e * (values[h] - v) : v; + }; + d3.zip = function() { + if (!(n = arguments.length)) return []; + for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m;) { + for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n;) { + zip[j] = arguments[j][i]; + } + } + return zips; + }; + + function d3_zipLength(d) { + return d.length; + } +// Locate the insertion point for x in a to maintain sorted order. The +// arguments lo and hi may be used to specify a subset of the array which should +// be considered; by default the entire array is used. If x is already present +// in a, the insertion point will be before (to the left of) any existing +// entries. The return value is suitable for use as the first argument to +// `array.splice` assuming that a is already sorted. +// +// The returned insertion point i partitions the array a into two halves so that +// all v < x for v in a[lo:i] for the left side and all v >= x for v in a[i:hi] +// for the right side. + d3.bisectLeft = function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = (lo + hi) >> 1; + if (a[mid] < x) lo = mid + 1; + else hi = mid; + } + return lo; + }; + +// Similar to bisectLeft, but returns an insertion point which comes after (to +// the right of) any existing entries of x in a. +// +// The returned insertion point i partitions the array into two halves so that +// all v <= x for v in a[lo:i] for the left side and all v > x for v in a[i:hi] +// for the right side. + d3.bisect = + d3.bisectRight = function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = (lo + hi) >> 1; + if (x < a[mid]) hi = mid; + else lo = mid + 1; + } + return lo; + }; + d3.first = function(array, f) { + var i = 0, + n = array.length, + a = array[0], + b; + if (arguments.length === 1) f = d3.ascending; + while (++i < n) { + if (f.call(array, a, b = array[i]) > 0) { + a = b; + } + } + return a; + }; + d3.last = function(array, f) { + var i = 0, + n = array.length, + a = array[0], + b; + if (arguments.length === 1) f = d3.ascending; + while (++i < n) { + if (f.call(array, a, b = array[i]) <= 0) { + a = b; + } + } + return a; + }; + d3.nest = function() { + var nest = {}, + keys = [], + sortKeys = [], + sortValues, + rollup; + + function map(array, depth) { + if (depth >= keys.length) return rollup + ? rollup.call(nest, array) : (sortValues + ? array.sort(sortValues) + : array); + + var i = -1, + n = array.length, + key = keys[depth++], + keyValue, + object, + o = {}; + + while (++i < n) { + if ((keyValue = key(object = array[i])) in o) { + o[keyValue].push(object); + } else { + o[keyValue] = [object]; + } + } + + for (keyValue in o) { + o[keyValue] = map(o[keyValue], depth); + } + + return o; + } + + function entries(map, depth) { + if (depth >= keys.length) return map; + + var a = [], + sortKey = sortKeys[depth++], + key; + + for (key in map) { + a.push({key: key, values: entries(map[key], depth)}); + } + + if (sortKey) a.sort(function(a, b) { + return sortKey(a.key, b.key); + }); + + return a; + } + + nest.map = function(array) { + return map(array, 0); + }; + + nest.entries = function(array) { + return entries(map(array, 0), 0); + }; + + nest.key = function(d) { + keys.push(d); + return nest; + }; + + // Specifies the order for the most-recently specified key. + // Note: only applies to entries. Map keys are unordered! + nest.sortKeys = function(order) { + sortKeys[keys.length - 1] = order; + return nest; + }; + + // Specifies the order for leaf values. + // Applies to both maps and entries array. + nest.sortValues = function(order) { + sortValues = order; + return nest; + }; + + nest.rollup = function(f) { + rollup = f; + return nest; + }; + + return nest; + }; + d3.keys = function(map) { + var keys = []; + for (var key in map) keys.push(key); + return keys; + }; + d3.values = function(map) { + var values = []; + for (var key in map) values.push(map[key]); + return values; + }; + d3.entries = function(map) { + var entries = []; + for (var key in map) entries.push({key: key, value: map[key]}); + return entries; + }; + d3.permute = function(array, indexes) { + var permutes = [], + i = -1, + n = indexes.length; + while (++i < n) permutes[i] = array[indexes[i]]; + return permutes; + }; + d3.merge = function(arrays) { + return Array.prototype.concat.apply([], arrays); + }; + d3.split = function(array, f) { + var arrays = [], + values = [], + value, + i = -1, + n = array.length; + if (arguments.length < 2) f = d3_splitter; + while (++i < n) { + if (f.call(values, value = array[i], i)) { + values = []; + } else { + if (!values.length) arrays.push(values); + values.push(value); + } + } + return arrays; + }; + + function d3_splitter(d) { + return d == null; + } + function d3_collapse(s) { + return s.replace(/(^\s+)|(\s+$)/g, "").replace(/\s+/g, " "); + } + /** + * @param {number} start + * @param {number=} stop + * @param {number=} step + */ + d3.range = function(start, stop, step) { + if (arguments.length < 3) { + step = 1; + if (arguments.length < 2) { + stop = start; + start = 0; + } + } + if ((stop - start) / step == Infinity) throw new Error("infinite range"); + var range = [], + i = -1, + j; + if (step < 0) while ((j = start + step * ++i) > stop) range.push(j); + else while ((j = start + step * ++i) < stop) range.push(j); + return range; + }; + d3.requote = function(s) { + return s.replace(d3_requote_re, "\\$&"); + }; + + var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; + d3.round = function(x, n) { + return n + ? Math.round(x * Math.pow(10, n)) * Math.pow(10, -n) + : Math.round(x); + }; + d3.xhr = function(url, mime, callback) { + var req = new XMLHttpRequest; + if (arguments.length < 3) callback = mime; + else if (mime && req.overrideMimeType) req.overrideMimeType(mime); + req.open("GET", url, true); + req.onreadystatechange = function() { + if (req.readyState === 4) callback(req.status < 300 ? req : null); + }; + req.send(null); + }; + d3.text = function(url, mime, callback) { + function ready(req) { + callback(req && req.responseText); + } + if (arguments.length < 3) { + callback = mime; + mime = null; + } + d3.xhr(url, mime, ready); + }; + d3.json = function(url, callback) { + d3.text(url, "application/json", function(text) { + callback(text ? JSON.parse(text) : null); + }); + }; + d3.html = function(url, callback) { + d3.text(url, "text/html", function(text) { + if (text != null) { // Treat empty string as valid HTML. + var range = document.createRange(); + range.selectNode(document.body); + text = range.createContextualFragment(text); + } + callback(text); + }); + }; + d3.xml = function(url, mime, callback) { + function ready(req) { + callback(req && req.responseXML); + } + if (arguments.length < 3) { + callback = mime; + mime = null; + } + d3.xhr(url, mime, ready); + }; + d3.ns = { + + prefix: { + svg: "http://www.w3.org/2000/svg", + xhtml: "http://www.w3.org/1999/xhtml", + xlink: "http://www.w3.org/1999/xlink", + xml: "http://www.w3.org/XML/1998/namespace", + xmlns: "http://www.w3.org/2000/xmlns/" + }, + + qualify: function(name) { + var i = name.indexOf(":"); + return i < 0 ? name : { + space: d3.ns.prefix[name.substring(0, i)], + local: name.substring(i + 1) + }; + } + + }; + /** @param {...string} types */ + d3.dispatch = function(types) { + var dispatch = {}, + type; + for (var i = 0, n = arguments.length; i < n; i++) { + type = arguments[i]; + dispatch[type] = d3_dispatch(type); + } + return dispatch; + }; + + function d3_dispatch(type) { + var dispatch = {}, + listeners = []; + + dispatch.add = function(listener) { + for (var i = 0; i < listeners.length; i++) { + if (listeners[i].listener == listener) return dispatch; // already registered + } + listeners.push({listener: listener, on: true}); + return dispatch; + }; + + dispatch.remove = function(listener) { + for (var i = 0; i < listeners.length; i++) { + var l = listeners[i]; + if (l.listener == listener) { + l.on = false; + listeners = listeners.slice(0, i).concat(listeners.slice(i + 1)); + break; + } + } + return dispatch; + }; + + dispatch.dispatch = function() { + var ls = listeners; // defensive reference + for (var i = 0, n = ls.length; i < n; i++) { + var l = ls[i]; + if (l.on) l.listener.apply(this, arguments); + } + }; + + return dispatch; + }; +// TODO align + d3.format = function(specifier) { + var match = d3_format_re.exec(specifier), + fill = match[1] || " ", + sign = match[3] || "", + zfill = match[5], + width = +match[6], + comma = match[7], + precision = match[8], + type = match[9], + scale = 1, + suffix = "", + integer = false; + + if (precision) precision = +precision.substring(1); + + if (zfill) { + fill = "0"; // TODO align = "="; + if (comma) width -= Math.floor((width - 1) / 4); + } + + switch (type) { + case "n": comma = true; type = "g"; break; + case "%": scale = 100; suffix = "%"; type = "f"; break; + case "p": scale = 100; suffix = "%"; type = "r"; break; + case "d": integer = true; precision = 0; break; + case "s": scale = -1; type = "r"; break; + } + + // If no precision is specified for r, fallback to general notation. + if (type == "r" && !precision) type = "g"; + + type = d3_format_types[type] || d3_format_typeDefault; + + return function(value) { + + // Return the empty string for floats formatted as ints. + if (integer && (value % 1)) return ""; + + // Convert negative to positive, and record the sign prefix. + var negative = (value < 0) && (value = -value) ? "\u2212" : sign; + + // Apply the scale, computing it from the value's exponent for si format. + if (scale < 0) { + var prefix = d3.formatPrefix(value, precision); + value *= prefix.scale; + suffix = prefix.symbol; + } else { + value *= scale; + } + + // Convert to the desired precision. + value = type(value, precision); + + // If the fill character is 0, the sign and group is applied after the fill. + if (zfill) { + var length = value.length + negative.length; + if (length < width) value = new Array(width - length + 1).join(fill) + value; + if (comma) value = d3_format_group(value); + value = negative + value; + } + + // Otherwise (e.g., space-filling), the sign and group is applied before. + else { + if (comma) value = d3_format_group(value); + value = negative + value; + var length = value.length; + if (length < width) value = new Array(width - length + 1).join(fill) + value; + } + + return value + suffix; + }; + }; + +// [[fill]align][sign][#][0][width][,][.precision][type] + var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?(#)?(0)?([0-9]+)?(,)?(\.[0-9]+)?([a-zA-Z%])?/; + + var d3_format_types = { + g: function(x, p) { return x.toPrecision(p); }, + e: function(x, p) { return x.toExponential(p); }, + f: function(x, p) { return x.toFixed(p); }, + r: function(x, p) { return d3.round(x, p = d3_format_precision(x, p)).toFixed(Math.max(0, Math.min(20, p))); } + }; + + function d3_format_precision(x, p) { + return p - (x ? 1 + Math.floor(Math.log(x + Math.pow(10, 1 + Math.floor(Math.log(x) / Math.LN10) - p)) / Math.LN10) : 1); + } + + function d3_format_typeDefault(x) { + return x + ""; + } + +// Apply comma grouping for thousands. + function d3_format_group(value) { + var i = value.lastIndexOf("."), + f = i >= 0 ? value.substring(i) : (i = value.length, ""), + t = []; + while (i > 0) t.push(value.substring(i -= 3, i + 3)); + return t.reverse().join(",") + f; + } + var d3_formatPrefixes = ["y","z","a","f","p","n","μ","m","","k","M","G","T","P","E","Z","Y"].map(d3_formatPrefix); + + d3.formatPrefix = function(value, precision) { + var i = 0; + if (value) { + if (value < 0) value *= -1; + if (precision) value = d3.round(value, d3_format_precision(value, precision)); + i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); + i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); + } + return d3_formatPrefixes[8 + i / 3]; + }; + + function d3_formatPrefix(d, i) { + return { + scale: Math.pow(10, (8 - i) * 3), + symbol: d + }; + } + + /* + * TERMS OF USE - EASING EQUATIONS + * + * Open source under the BSD License. + * + * Copyright 2001 Robert Penner + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the author nor the names of contributors may be used to + * endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + var d3_ease_quad = d3_ease_poly(2), + d3_ease_cubic = d3_ease_poly(3); + + var d3_ease = { + linear: function() { return d3_ease_linear; }, + poly: d3_ease_poly, + quad: function() { return d3_ease_quad; }, + cubic: function() { return d3_ease_cubic; }, + sin: function() { return d3_ease_sin; }, + exp: function() { return d3_ease_exp; }, + circle: function() { return d3_ease_circle; }, + elastic: d3_ease_elastic, + back: d3_ease_back, + bounce: function() { return d3_ease_bounce; } + }; + + var d3_ease_mode = { + "in": function(f) { return f; }, + "out": d3_ease_reverse, + "in-out": d3_ease_reflect, + "out-in": function(f) { return d3_ease_reflect(d3_ease_reverse(f)); } + }; + + d3.ease = function(name) { + var i = name.indexOf("-"), + t = i >= 0 ? name.substring(0, i) : name, + m = i >= 0 ? name.substring(i + 1) : "in"; + return d3_ease_clamp(d3_ease_mode[m](d3_ease[t].apply(null, Array.prototype.slice.call(arguments, 1)))); + }; + + function d3_ease_clamp(f) { + return function(t) { + return t <= 0 ? 0 : t >= 1 ? 1 : f(t); + }; + } + + function d3_ease_reverse(f) { + return function(t) { + return 1 - f(1 - t); + }; + } + + function d3_ease_reflect(f) { + return function(t) { + return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); + }; + } + + function d3_ease_linear(t) { + return t; + } + + function d3_ease_poly(e) { + return function(t) { + return Math.pow(t, e); + } + } + + function d3_ease_sin(t) { + return 1 - Math.cos(t * Math.PI / 2); + } + + function d3_ease_exp(t) { + return Math.pow(2, 10 * (t - 1)); + } + + function d3_ease_circle(t) { + return 1 - Math.sqrt(1 - t * t); + } + + function d3_ease_elastic(a, p) { + var s; + if (arguments.length < 2) p = 0.45; + if (arguments.length < 1) { a = 1; s = p / 4; } + else s = p / (2 * Math.PI) * Math.asin(1 / a); + return function(t) { + return 1 + a * Math.pow(2, 10 * -t) * Math.sin((t - s) * 2 * Math.PI / p); + }; + } + + function d3_ease_back(s) { + if (!s) s = 1.70158; + return function(t) { + return t * t * ((s + 1) * t - s); + }; + } + + function d3_ease_bounce(t) { + return t < 1 / 2.75 ? 7.5625 * t * t + : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 + : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 + : 7.5625 * (t -= 2.625 / 2.75) * t + .984375; + } + d3.event = null; + d3.interpolate = function(a, b) { + var i = d3.interpolators.length, f; + while (--i >= 0 && !(f = d3.interpolators[i](a, b))); + return f; + }; + + d3.interpolateNumber = function(a, b) { + b -= a; + return function(t) { return a + b * t; }; + }; + + d3.interpolateRound = function(a, b) { + b -= a; + return function(t) { return Math.round(a + b * t); }; + }; + + d3.interpolateString = function(a, b) { + var m, // current match + i, // current index + j, // current index (for coallescing) + s0 = 0, // start index of current string prefix + s1 = 0, // end index of current string prefix + s = [], // string constants and placeholders + q = [], // number interpolators + n, // q.length + o; + + // Reset our regular expression! + d3_interpolate_number.lastIndex = 0; + + // Find all numbers in b. + for (i = 0; m = d3_interpolate_number.exec(b); ++i) { + if (m.index) s.push(b.substring(s0, s1 = m.index)); + q.push({i: s.length, x: m[0]}); + s.push(null); + s0 = d3_interpolate_number.lastIndex; + } + if (s0 < b.length) s.push(b.substring(s0)); + + // Find all numbers in a. + for (i = 0, n = q.length; (m = d3_interpolate_number.exec(a)) && i < n; ++i) { + o = q[i]; + if (o.x == m[0]) { // The numbers match, so coallesce. + if (o.i) { + if (s[o.i + 1] == null) { // This match is followed by another number. + s[o.i - 1] += o.x; + s.splice(o.i, 1); + for (j = i + 1; j < n; ++j) q[j].i--; + } else { // This match is followed by a string, so coallesce twice. + s[o.i - 1] += o.x + s[o.i + 1]; + s.splice(o.i, 2); + for (j = i + 1; j < n; ++j) q[j].i -= 2; + } + } else { + if (s[o.i + 1] == null) { // This match is followed by another number. + s[o.i] = o.x; + } else { // This match is followed by a string, so coallesce twice. + s[o.i] = o.x + s[o.i + 1]; + s.splice(o.i + 1, 1); + for (j = i + 1; j < n; ++j) q[j].i--; + } + } + q.splice(i, 1); + n--; + i--; + } else { + o.x = d3.interpolateNumber(parseFloat(m[0]), parseFloat(o.x)); + } + } + + // Remove any numbers in b not found in a. + while (i < n) { + o = q.pop(); + if (s[o.i + 1] == null) { // This match is followed by another number. + s[o.i] = o.x; + } else { // This match is followed by a string, so coallesce twice. + s[o.i] = o.x + s[o.i + 1]; + s.splice(o.i + 1, 1); + } + n--; + } + + // Special optimization for only a single match. + if (s.length === 1) { + return s[0] == null ? q[0].x : function() { return b; }; + } + + // Otherwise, interpolate each of the numbers and rejoin the string. + return function(t) { + for (i = 0; i < n; ++i) s[(o = q[i]).i] = o.x(t); + return s.join(""); + }; + }; + + d3.interpolateRgb = function(a, b) { + a = d3.rgb(a); + b = d3.rgb(b); + var ar = a.r, + ag = a.g, + ab = a.b, + br = b.r - ar, + bg = b.g - ag, + bb = b.b - ab; + return function(t) { + return "#" + + d3_rgb_hex(Math.round(ar + br * t)) + + d3_rgb_hex(Math.round(ag + bg * t)) + + d3_rgb_hex(Math.round(ab + bb * t)); + }; + }; + +// interpolates HSL space, but outputs RGB string (for compatibility) + d3.interpolateHsl = function(a, b) { + a = d3.hsl(a); + b = d3.hsl(b); + var h0 = a.h, + s0 = a.s, + l0 = a.l, + h1 = b.h - h0, + s1 = b.s - s0, + l1 = b.l - l0; + return function(t) { + return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t).toString(); + }; + }; + + d3.interpolateArray = function(a, b) { + var x = [], + c = [], + na = a.length, + nb = b.length, + n0 = Math.min(a.length, b.length), + i; + for (i = 0; i < n0; ++i) x.push(d3.interpolate(a[i], b[i])); + for (; i < na; ++i) c[i] = a[i]; + for (; i < nb; ++i) c[i] = b[i]; + return function(t) { + for (i = 0; i < n0; ++i) c[i] = x[i](t); + return c; + }; + }; + + d3.interpolateObject = function(a, b) { + var i = {}, + c = {}, + k; + for (k in a) { + if (k in b) { + i[k] = d3_interpolateByName(k)(a[k], b[k]); + } else { + c[k] = a[k]; + } + } + for (k in b) { + if (!(k in a)) { + c[k] = b[k]; + } + } + return function(t) { + for (k in i) c[k] = i[k](t); + return c; + }; + } + + var d3_interpolate_number = /[-+]?(?:\d+\.\d+|\d+\.|\.\d+|\d+)(?:[eE][-]?\d+)?/g, + d3_interpolate_rgb = {background: 1, fill: 1, stroke: 1}; + + function d3_interpolateByName(n) { + return n in d3_interpolate_rgb || /\bcolor\b/.test(n) + ? d3.interpolateRgb + : d3.interpolate; + } + + d3.interpolators = [ + d3.interpolateObject, + function(a, b) { return (b instanceof Array) && d3.interpolateArray(a, b); }, + function(a, b) { return (typeof b === "string") && d3.interpolateString(String(a), b); }, + function(a, b) { return (typeof b === "string" ? b in d3_rgb_names || /^(#|rgb\(|hsl\()/.test(b) : b instanceof d3_Rgb || b instanceof d3_Hsl) && d3.interpolateRgb(String(a), b); }, + function(a, b) { return (typeof b === "number") && d3.interpolateNumber(+a, b); } + ]; + function d3_uninterpolateNumber(a, b) { + b = b - (a = +a) ? 1 / (b - a) : 0; + return function(x) { return (x - a) * b; }; + } + + function d3_uninterpolateClamp(a, b) { + b = b - (a = +a) ? 1 / (b - a) : 0; + return function(x) { return Math.max(0, Math.min(1, (x - a) * b)); }; + } + d3.rgb = function(r, g, b) { + return arguments.length === 1 + ? (r instanceof d3_Rgb ? d3_rgb(r.r, r.g, r.b) + : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb)) + : d3_rgb(~~r, ~~g, ~~b); + }; + + function d3_rgb(r, g, b) { + return new d3_Rgb(r, g, b); + } + + function d3_Rgb(r, g, b) { + this.r = r; + this.g = g; + this.b = b; + } + + d3_Rgb.prototype.brighter = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + var r = this.r, + g = this.g, + b = this.b, + i = 30; + if (!r && !g && !b) return d3_rgb(i, i, i); + if (r && r < i) r = i; + if (g && g < i) g = i; + if (b && b < i) b = i; + return d3_rgb( + Math.min(255, Math.floor(r / k)), + Math.min(255, Math.floor(g / k)), + Math.min(255, Math.floor(b / k))); + }; + + d3_Rgb.prototype.darker = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + return d3_rgb( + Math.floor(k * this.r), + Math.floor(k * this.g), + Math.floor(k * this.b)); + }; + + d3_Rgb.prototype.hsl = function() { + return d3_rgb_hsl(this.r, this.g, this.b); + }; + + d3_Rgb.prototype.toString = function() { + return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b); + }; + + function d3_rgb_hex(v) { + return v < 0x10 + ? "0" + Math.max(0, v).toString(16) + : Math.min(255, v).toString(16); + } + + function d3_rgb_parse(format, rgb, hsl) { + var r = 0, // red channel; int in [0, 255] + g = 0, // green channel; int in [0, 255] + b = 0, // blue channel; int in [0, 255] + m1, // CSS color specification match + m2, // CSS color specification type (e.g., rgb) + name; + + /* Handle hsl, rgb. */ + m1 = /([a-z]+)\((.*)\)/i.exec(format); + if (m1) { + m2 = m1[2].split(","); + switch (m1[1]) { + case "hsl": { + return hsl( + parseFloat(m2[0]), // degrees + parseFloat(m2[1]) / 100, // percentage + parseFloat(m2[2]) / 100 // percentage + ); + } + case "rgb": { + return rgb( + d3_rgb_parseNumber(m2[0]), + d3_rgb_parseNumber(m2[1]), + d3_rgb_parseNumber(m2[2]) + ); + } + } + } + + /* Named colors. */ + if (name = d3_rgb_names[format]) return rgb(name.r, name.g, name.b); + + /* Hexadecimal colors: #rgb and #rrggbb. */ + if (format != null && format.charAt(0) === "#") { + if (format.length === 4) { + r = format.charAt(1); r += r; + g = format.charAt(2); g += g; + b = format.charAt(3); b += b; + } else if (format.length === 7) { + r = format.substring(1, 3); + g = format.substring(3, 5); + b = format.substring(5, 7); + } + r = parseInt(r, 16); + g = parseInt(g, 16); + b = parseInt(b, 16); + } + + return rgb(r, g, b); + } + + function d3_rgb_hsl(r, g, b) { + var min = Math.min(r /= 255, g /= 255, b /= 255), + max = Math.max(r, g, b), + d = max - min, + h, + s, + l = (max + min) / 2; + if (d) { + s = l < .5 ? d / (max + min) : d / (2 - max - min); + if (r == max) h = (g - b) / d + (g < b ? 6 : 0); + else if (g == max) h = (b - r) / d + 2; + else h = (r - g) / d + 4; + h *= 60; + } else { + s = h = 0; + } + return d3_hsl(h, s, l); + } + + function d3_rgb_parseNumber(c) { // either integer or percentage + var f = parseFloat(c); + return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f; + } + + var d3_rgb_names = { + aliceblue: "#f0f8ff", + antiquewhite: "#faebd7", + aqua: "#00ffff", + aquamarine: "#7fffd4", + azure: "#f0ffff", + beige: "#f5f5dc", + bisque: "#ffe4c4", + black: "#000000", + blanchedalmond: "#ffebcd", + blue: "#0000ff", + blueviolet: "#8a2be2", + brown: "#a52a2a", + burlywood: "#deb887", + cadetblue: "#5f9ea0", + chartreuse: "#7fff00", + chocolate: "#d2691e", + coral: "#ff7f50", + cornflowerblue: "#6495ed", + cornsilk: "#fff8dc", + crimson: "#dc143c", + cyan: "#00ffff", + darkblue: "#00008b", + darkcyan: "#008b8b", + darkgoldenrod: "#b8860b", + darkgray: "#a9a9a9", + darkgreen: "#006400", + darkgrey: "#a9a9a9", + darkkhaki: "#bdb76b", + darkmagenta: "#8b008b", + darkolivegreen: "#556b2f", + darkorange: "#ff8c00", + darkorchid: "#9932cc", + darkred: "#8b0000", + darksalmon: "#e9967a", + darkseagreen: "#8fbc8f", + darkslateblue: "#483d8b", + darkslategray: "#2f4f4f", + darkslategrey: "#2f4f4f", + darkturquoise: "#00ced1", + darkviolet: "#9400d3", + deeppink: "#ff1493", + deepskyblue: "#00bfff", + dimgray: "#696969", + dimgrey: "#696969", + dodgerblue: "#1e90ff", + firebrick: "#b22222", + floralwhite: "#fffaf0", + forestgreen: "#228b22", + fuchsia: "#ff00ff", + gainsboro: "#dcdcdc", + ghostwhite: "#f8f8ff", + gold: "#ffd700", + goldenrod: "#daa520", + gray: "#808080", + green: "#008000", + greenyellow: "#adff2f", + grey: "#808080", + honeydew: "#f0fff0", + hotpink: "#ff69b4", + indianred: "#cd5c5c", + indigo: "#4b0082", + ivory: "#fffff0", + khaki: "#f0e68c", + lavender: "#e6e6fa", + lavenderblush: "#fff0f5", + lawngreen: "#7cfc00", + lemonchiffon: "#fffacd", + lightblue: "#add8e6", + lightcoral: "#f08080", + lightcyan: "#e0ffff", + lightgoldenrodyellow: "#fafad2", + lightgray: "#d3d3d3", + lightgreen: "#90ee90", + lightgrey: "#d3d3d3", + lightpink: "#ffb6c1", + lightsalmon: "#ffa07a", + lightseagreen: "#20b2aa", + lightskyblue: "#87cefa", + lightslategray: "#778899", + lightslategrey: "#778899", + lightsteelblue: "#b0c4de", + lightyellow: "#ffffe0", + lime: "#00ff00", + limegreen: "#32cd32", + linen: "#faf0e6", + magenta: "#ff00ff", + maroon: "#800000", + mediumaquamarine: "#66cdaa", + mediumblue: "#0000cd", + mediumorchid: "#ba55d3", + mediumpurple: "#9370db", + mediumseagreen: "#3cb371", + mediumslateblue: "#7b68ee", + mediumspringgreen: "#00fa9a", + mediumturquoise: "#48d1cc", + mediumvioletred: "#c71585", + midnightblue: "#191970", + mintcream: "#f5fffa", + mistyrose: "#ffe4e1", + moccasin: "#ffe4b5", + navajowhite: "#ffdead", + navy: "#000080", + oldlace: "#fdf5e6", + olive: "#808000", + olivedrab: "#6b8e23", + orange: "#ffa500", + orangered: "#ff4500", + orchid: "#da70d6", + palegoldenrod: "#eee8aa", + palegreen: "#98fb98", + paleturquoise: "#afeeee", + palevioletred: "#db7093", + papayawhip: "#ffefd5", + peachpuff: "#ffdab9", + peru: "#cd853f", + pink: "#ffc0cb", + plum: "#dda0dd", + powderblue: "#b0e0e6", + purple: "#800080", + red: "#ff0000", + rosybrown: "#bc8f8f", + royalblue: "#4169e1", + saddlebrown: "#8b4513", + salmon: "#fa8072", + sandybrown: "#f4a460", + seagreen: "#2e8b57", + seashell: "#fff5ee", + sienna: "#a0522d", + silver: "#c0c0c0", + skyblue: "#87ceeb", + slateblue: "#6a5acd", + slategray: "#708090", + slategrey: "#708090", + snow: "#fffafa", + springgreen: "#00ff7f", + steelblue: "#4682b4", + tan: "#d2b48c", + teal: "#008080", + thistle: "#d8bfd8", + tomato: "#ff6347", + turquoise: "#40e0d0", + violet: "#ee82ee", + wheat: "#f5deb3", + white: "#ffffff", + whitesmoke: "#f5f5f5", + yellow: "#ffff00", + yellowgreen: "#9acd32" + }; + + for (var d3_rgb_name in d3_rgb_names) { + d3_rgb_names[d3_rgb_name] = d3_rgb_parse( + d3_rgb_names[d3_rgb_name], + d3_rgb, + d3_hsl_rgb); + } + d3.hsl = function(h, s, l) { + return arguments.length === 1 + ? (h instanceof d3_Hsl ? d3_hsl(h.h, h.s, h.l) + : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl)) + : d3_hsl(+h, +s, +l); + }; + + function d3_hsl(h, s, l) { + return new d3_Hsl(h, s, l); + } + + function d3_Hsl(h, s, l) { + this.h = h; + this.s = s; + this.l = l; + } + + d3_Hsl.prototype.brighter = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + return d3_hsl(this.h, this.s, this.l / k); + }; + + d3_Hsl.prototype.darker = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + return d3_hsl(this.h, this.s, k * this.l); + }; + + d3_Hsl.prototype.rgb = function() { + return d3_hsl_rgb(this.h, this.s, this.l); + }; + + d3_Hsl.prototype.toString = function() { + return this.rgb().toString(); + }; + + function d3_hsl_rgb(h, s, l) { + var m1, + m2; + + /* Some simple corrections for h, s and l. */ + h = h % 360; if (h < 0) h += 360; + s = s < 0 ? 0 : s > 1 ? 1 : s; + l = l < 0 ? 0 : l > 1 ? 1 : l; + + /* From FvD 13.37, CSS Color Module Level 3 */ + m2 = l <= .5 ? l * (1 + s) : l + s - l * s; + m1 = 2 * l - m2; + + function v(h) { + if (h > 360) h -= 360; + else if (h < 0) h += 360; + if (h < 60) return m1 + (m2 - m1) * h / 60; + if (h < 180) return m2; + if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; + return m1; + } + + function vv(h) { + return Math.round(v(h) * 255); + } + + return d3_rgb(vv(h + 120), vv(h), vv(h - 120)); + } + function d3_selection(groups) { + d3_arraySubclass(groups, d3_selectionPrototype); + return groups; + } + + var d3_select = function(s, n) { return n.querySelector(s); }, + d3_selectAll = function(s, n) { return n.querySelectorAll(s); }; + +// Prefer Sizzle, if available. + if (typeof Sizzle === "function") { + d3_select = function(s, n) { return Sizzle(s, n)[0]; }; + d3_selectAll = function(s, n) { return Sizzle.uniqueSort(Sizzle(s, n)); }; + } + + var d3_selectionPrototype = []; + + d3.selection = function() { + return d3_selectionRoot; + }; + + d3.selection.prototype = d3_selectionPrototype; + d3_selectionPrototype.select = function(selector) { + var subgroups = [], + subgroup, + subnode, + group, + node; + + if (typeof selector !== "function") selector = d3_selection_selector(selector); + + for (var j = -1, m = this.length; ++j < m;) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = -1, n = group.length; ++i < n;) { + if (node = group[i]) { + subgroup.push(subnode = selector.call(node, node.__data__, i)); + if (subnode && "__data__" in node) subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + + return d3_selection(subgroups); + }; + + function d3_selection_selector(selector) { + return function() { + return d3_select(selector, this); + }; + } + d3_selectionPrototype.selectAll = function(selector) { + var subgroups = [], + subgroup, + node; + + if (typeof selector !== "function") selector = d3_selection_selectorAll(selector); + + for (var j = -1, m = this.length; ++j < m;) { + for (var group = this[j], i = -1, n = group.length; ++i < n;) { + if (node = group[i]) { + subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i))); + subgroup.parentNode = node; + } + } + } + + return d3_selection(subgroups); + }; + + function d3_selection_selectorAll(selector) { + return function() { + return d3_selectAll(selector, this); + }; + } + d3_selectionPrototype.attr = function(name, value) { + name = d3.ns.qualify(name); + + // If no value is specified, return the first value. + if (arguments.length < 2) { + var node = this.node(); + return name.local + ? node.getAttributeNS(name.space, name.local) + : node.getAttribute(name); + } + + function attrNull() { + this.removeAttribute(name); + } + + function attrNullNS() { + this.removeAttributeNS(name.space, name.local); + } + + function attrConstant() { + this.setAttribute(name, value); + } + + function attrConstantNS() { + this.setAttributeNS(name.space, name.local, value); + } + + function attrFunction() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttribute(name); + else this.setAttribute(name, x); + } + + function attrFunctionNS() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttributeNS(name.space, name.local); + else this.setAttributeNS(name.space, name.local, x); + } + + return this.each(value == null + ? (name.local ? attrNullNS : attrNull) : (typeof value === "function" + ? (name.local ? attrFunctionNS : attrFunction) + : (name.local ? attrConstantNS : attrConstant))); + }; + d3_selectionPrototype.classed = function(name, value) { + var names = name.split(d3_selection_classedWhitespace), + n = names.length, + i = -1; + if (arguments.length > 1) { + while (++i < n) d3_selection_classed.call(this, names[i], value); + return this; + } else { + while (++i < n) if (!d3_selection_classed.call(this, names[i])) return false; + return true; + } + }; + + var d3_selection_classedWhitespace = /\s+/g; + + function d3_selection_classed(name, value) { + var re = new RegExp("(^|\\s+)" + d3.requote(name) + "(\\s+|$)", "g"); + + // If no value is specified, return the first value. + if (arguments.length < 2) { + var node = this.node(); + if (c = node.classList) return c.contains(name); + var c = node.className; + re.lastIndex = 0; + return re.test(c.baseVal != null ? c.baseVal : c); + } + + function classedAdd() { + if (c = this.classList) return c.add(name); + var c = this.className, + cb = c.baseVal != null, + cv = cb ? c.baseVal : c; + re.lastIndex = 0; + if (!re.test(cv)) { + cv = d3_collapse(cv + " " + name); + if (cb) c.baseVal = cv; + else this.className = cv; + } + } + + function classedRemove() { + if (c = this.classList) return c.remove(name); + var c = this.className, + cb = c.baseVal != null, + cv = cb ? c.baseVal : c; + cv = d3_collapse(cv.replace(re, " ")); + if (cb) c.baseVal = cv; + else this.className = cv; + } + + function classedFunction() { + (value.apply(this, arguments) + ? classedAdd + : classedRemove).call(this); + } + + return this.each(typeof value === "function" + ? classedFunction : value + ? classedAdd + : classedRemove); + } + d3_selectionPrototype.style = function(name, value, priority) { + if (arguments.length < 3) priority = ""; + + // If no value is specified, return the first value. + if (arguments.length < 2) return window + .getComputedStyle(this.node(), null) + .getPropertyValue(name); + + function styleNull() { + this.style.removeProperty(name); + } + + function styleConstant() { + this.style.setProperty(name, value, priority); + } + + function styleFunction() { + var x = value.apply(this, arguments); + if (x == null) this.style.removeProperty(name); + else this.style.setProperty(name, x, priority); + } + + return this.each(value == null + ? styleNull : (typeof value === "function" + ? styleFunction : styleConstant)); + }; + d3_selectionPrototype.property = function(name, value) { + + // If no value is specified, return the first value. + if (arguments.length < 2) return this.node()[name]; + + function propertyNull() { + delete this[name]; + } + + function propertyConstant() { + this[name] = value; + } + + function propertyFunction() { + var x = value.apply(this, arguments); + if (x == null) delete this[name]; + else this[name] = x; + } + + return this.each(value == null + ? propertyNull : (typeof value === "function" + ? propertyFunction : propertyConstant)); + }; + d3_selectionPrototype.text = function(value) { + return arguments.length < 1 ? this.node().textContent + : (this.each(typeof value === "function" + ? function() { this.textContent = value.apply(this, arguments); } + : function() { this.textContent = value; })); + }; + d3_selectionPrototype.html = function(value) { + return arguments.length < 1 ? this.node().innerHTML + : (this.each(typeof value === "function" + ? function() { this.innerHTML = value.apply(this, arguments); } + : function() { this.innerHTML = value; })); + }; +// TODO append(node)? +// TODO append(function)? + d3_selectionPrototype.append = function(name) { + name = d3.ns.qualify(name); + + function append() { + return this.appendChild(document.createElement(name)); + } + + function appendNS() { + return this.appendChild(document.createElementNS(name.space, name.local)); + } + + return this.select(name.local ? appendNS : append); + }; +// TODO insert(node, function)? +// TODO insert(function, string)? +// TODO insert(function, function)? + d3_selectionPrototype.insert = function(name, before) { + name = d3.ns.qualify(name); + + function insert() { + return this.insertBefore( + document.createElement(name), + d3_select(before, this)); + } + + function insertNS() { + return this.insertBefore( + document.createElementNS(name.space, name.local), + d3_select(before, this)); + } + + return this.select(name.local ? insertNS : insert); + }; +// TODO remove(selector)? +// TODO remove(node)? +// TODO remove(function)? + d3_selectionPrototype.remove = function() { + return this.each(function() { + var parent = this.parentNode; + if (parent) parent.removeChild(this); + }); + }; +// TODO data(null) for clearing data? + d3_selectionPrototype.data = function(data, join) { + var enter = [], + update = [], + exit = []; + + function bind(group, groupData) { + var i, + n = group.length, + m = groupData.length, + n0 = Math.min(n, m), + n1 = Math.max(n, m), + updateNodes = [], + enterNodes = [], + exitNodes = [], + node, + nodeData; + + if (join) { + var nodeByKey = {}, + keys = [], + key, + j = groupData.length; + + for (i = -1; ++i < n;) { + key = join.call(node = group[i], node.__data__, i); + if (key in nodeByKey) { + exitNodes[j++] = node; // duplicate key + } else { + nodeByKey[key] = node; + } + keys.push(key); + } + + for (i = -1; ++i < m;) { + node = nodeByKey[key = join.call(groupData, nodeData = groupData[i], i)]; + if (node) { + node.__data__ = nodeData; + updateNodes[i] = node; + enterNodes[i] = exitNodes[i] = null; + } else { + enterNodes[i] = d3_selection_dataNode(nodeData); + updateNodes[i] = exitNodes[i] = null; + } + delete nodeByKey[key]; + } + + for (i = -1; ++i < n;) { + if (keys[i] in nodeByKey) { + exitNodes[i] = group[i]; + } + } + } else { + for (i = -1; ++i < n0;) { + node = group[i]; + nodeData = groupData[i]; + if (node) { + node.__data__ = nodeData; + updateNodes[i] = node; + enterNodes[i] = exitNodes[i] = null; + } else { + enterNodes[i] = d3_selection_dataNode(nodeData); + updateNodes[i] = exitNodes[i] = null; + } + } + for (; i < m; ++i) { + enterNodes[i] = d3_selection_dataNode(groupData[i]); + updateNodes[i] = exitNodes[i] = null; + } + for (; i < n1; ++i) { + exitNodes[i] = group[i]; + enterNodes[i] = updateNodes[i] = null; + } + } + + enterNodes.update + = updateNodes; + + enterNodes.parentNode + = updateNodes.parentNode + = exitNodes.parentNode + = group.parentNode; + + enter.push(enterNodes); + update.push(updateNodes); + exit.push(exitNodes); + } + + var i = -1, + n = this.length, + group; + if (typeof data === "function") { + while (++i < n) { + bind(group = this[i], data.call(group, group.parentNode.__data__, i)); + } + } else { + while (++i < n) { + bind(group = this[i], data); + } + } + + var selection = d3_selection(update); + selection.enter = function() { return d3_selection_enter(enter); }; + selection.exit = function() { return d3_selection(exit); }; + return selection; + }; + + function d3_selection_dataNode(data) { + return {__data__: data}; + } + function d3_selection_enter(selection) { + d3_arraySubclass(selection, d3_selection_enterPrototype); + return selection; + } + + var d3_selection_enterPrototype = []; + + d3_selection_enterPrototype.append = d3_selectionPrototype.append; + d3_selection_enterPrototype.insert = d3_selectionPrototype.insert; + d3_selection_enterPrototype.empty = d3_selectionPrototype.empty; + d3_selection_enterPrototype.select = function(selector) { + var subgroups = [], + subgroup, + subnode, + upgroup, + group, + node; + + for (var j = -1, m = this.length; ++j < m;) { + upgroup = (group = this[j]).update; + subgroups.push(subgroup = []); + subgroup.parentNode = group.parentNode; + for (var i = -1, n = group.length; ++i < n;) { + if (node = group[i]) { + subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i)); + subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + + return d3_selection(subgroups); + }; +// TODO preserve null elements to maintain index? + d3_selectionPrototype.filter = function(filter) { + var subgroups = [], + subgroup, + group, + node; + + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = 0, n = group.length; i < n; i++) { + if ((node = group[i]) && filter.call(node, node.__data__, i)) { + subgroup.push(node); + } + } + } + + return d3_selection(subgroups); + }; + d3_selectionPrototype.map = function(map) { + return this.each(function() { + this.__data__ = map.apply(this, arguments); + }); + }; + d3_selectionPrototype.sort = function(comparator) { + comparator = d3_selection_sortComparator.apply(this, arguments); + for (var j = 0, m = this.length; j < m; j++) { + for (var group = this[j].sort(comparator), i = 1, n = group.length, prev = group[0]; i < n; i++) { + var node = group[i]; + if (node) { + if (prev) prev.parentNode.insertBefore(node, prev.nextSibling); + prev = node; + } + } + } + return this; + }; + + function d3_selection_sortComparator(comparator) { + if (!arguments.length) comparator = d3.ascending; + return function(a, b) { + return comparator(a && a.__data__, b && b.__data__); + }; + } +// type can be namespaced, e.g., "click.foo" +// listener can be null for removal + d3_selectionPrototype.on = function(type, listener, capture) { + if (arguments.length < 3) capture = false; + + // parse the type specifier + var name = "__on" + type, i = type.indexOf("."); + if (i > 0) type = type.substring(0, i); + + // if called with only one argument, return the current listener + if (arguments.length < 2) return (i = this.node()[name]) && i._; + + // remove the old event listener, and add the new event listener + return this.each(function(d, i) { + var node = this; + + if (node[name]) node.removeEventListener(type, node[name], capture); + if (listener) node.addEventListener(type, node[name] = l, capture); + + // wrapped event listener that preserves i + function l(e) { + var o = d3.event; // Events can be reentrant (e.g., focus). + d3.event = e; + try { + listener.call(node, node.__data__, i); + } finally { + d3.event = o; + } + } + + // stash the unwrapped listener for retrieval + l._ = listener; + }); + }; + d3_selectionPrototype.each = function(callback) { + for (var j = -1, m = this.length; ++j < m;) { + for (var group = this[j], i = -1, n = group.length; ++i < n;) { + var node = group[i]; + if (node) callback.call(node, node.__data__, i, j); + } + } + return this; + }; +// +// Note: assigning to the arguments array simultaneously changes the value of +// the corresponding argument! +// +// TODO The `this` argument probably shouldn't be the first argument to the +// callback, anyway, since it's redundant. However, that will require a major +// version bump due to backwards compatibility, so I'm not changing it right +// away. +// + d3_selectionPrototype.call = function(callback) { + callback.apply(this, (arguments[0] = this, arguments)); + return this; + }; + d3_selectionPrototype.empty = function() { + return !this.node(); + }; + d3_selectionPrototype.node = function(callback) { + for (var j = 0, m = this.length; j < m; j++) { + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + var node = group[i]; + if (node) return node; + } + } + return null; + }; + d3_selectionPrototype.transition = function() { + var subgroups = [], + subgroup, + node; + + for (var j = -1, m = this.length; ++j < m;) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n;) { + subgroup.push((node = group[i]) ? {node: node, delay: 0, duration: 250} : null); + } + } + + return d3_transition(subgroups, d3_transitionInheritId || ++d3_transitionId, Date.now()); + }; + var d3_selectionRoot = d3_selection([[document]]); + + d3_selectionRoot[0].parentNode = document.documentElement; + +// TODO fast singleton implementation! +// TODO select(function) + d3.select = function(selector) { + return typeof selector === "string" + ? d3_selectionRoot.select(selector) + : d3_selection([[selector]]); // assume node + }; + +// TODO selectAll(function) + d3.selectAll = function(selector) { + return typeof selector === "string" + ? d3_selectionRoot.selectAll(selector) + : d3_selection([d3_array(selector)]); // assume node[] + }; + function d3_transition(groups, id, time) { + d3_arraySubclass(groups, d3_transitionPrototype); + + var tweens = {}, + event = d3.dispatch("start", "end"), + ease = d3_transitionEase; + + groups.id = id; + + groups.time = time; + + groups.tween = function(name, tween) { + if (arguments.length < 2) return tweens[name]; + if (tween == null) delete tweens[name]; + else tweens[name] = tween; + return groups; + }; + + groups.ease = function(value) { + if (!arguments.length) return ease; + ease = typeof value === "function" ? value : d3.ease.apply(d3, arguments); + return groups; + }; + + groups.each = function(type, listener) { + if (arguments.length < 2) return d3_transition_each.call(groups, type); + event[type].add(listener); + return groups; + }; + + d3.timer(function(elapsed) { + groups.each(function(d, i, j) { + var tweened = [], + node = this, + delay = groups[j][i].delay, + duration = groups[j][i].duration, + lock = node.__transition__ || (node.__transition__ = {active: 0, count: 0}); + + ++lock.count; + + delay <= elapsed ? start(elapsed) : d3.timer(start, delay, time); + + function start(elapsed) { + if (lock.active > id) return stop(); + lock.active = id; + + for (var tween in tweens) { + if (tween = tweens[tween].call(node, d, i)) { + tweened.push(tween); + } + } + + event.start.dispatch.call(node, d, i); + if (!tick(elapsed)) d3.timer(tick, 0, time); + return 1; + } + + function tick(elapsed) { + if (lock.active !== id) return stop(); + + var t = (elapsed - delay) / duration, + e = ease(t), + n = tweened.length; + + while (n > 0) { + tweened[--n].call(node, e); + } + + if (t >= 1) { + stop(); + d3_transitionInheritId = id; + event.end.dispatch.call(node, d, i); + d3_transitionInheritId = 0; + return 1; + } + } + + function stop() { + if (!--lock.count) delete node.__transition__; + return 1; + } + }); + return 1; + }, 0, time); + + return groups; + } + + var d3_transitionRemove = {}; + + function d3_transitionNull(d, i, a) { + return a != "" && d3_transitionRemove; + } + + function d3_transitionTween(b) { + + function transitionFunction(d, i, a) { + var v = b.call(this, d, i); + return v == null + ? a != "" && d3_transitionRemove + : a != v && d3.interpolate(a, v); + } + + function transitionString(d, i, a) { + return a != b && d3.interpolate(a, b); + } + + return typeof b === "function" ? transitionFunction + : b == null ? d3_transitionNull + : (b += "", transitionString); + } + + var d3_transitionPrototype = [], + d3_transitionId = 0, + d3_transitionInheritId = 0, + d3_transitionEase = d3.ease("cubic-in-out"); + + d3_transitionPrototype.call = d3_selectionPrototype.call; + + d3.transition = function() { + return d3_selectionRoot.transition(); + }; + + d3.transition.prototype = d3_transitionPrototype; + d3_transitionPrototype.select = function(selector) { + var subgroups = [], + subgroup, + subnode, + node; + + if (typeof selector !== "function") selector = d3_selection_selector(selector); + + for (var j = -1, m = this.length; ++j < m;) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n;) { + if ((node = group[i]) && (subnode = selector.call(node.node, node.node.__data__, i))) { + if ("__data__" in node.node) subnode.__data__ = node.node.__data__; + subgroup.push({node: subnode, delay: node.delay, duration: node.duration}); + } else { + subgroup.push(null); + } + } + } + + return d3_transition(subgroups, this.id, this.time).ease(this.ease()); + }; + d3_transitionPrototype.selectAll = function(selector) { + var subgroups = [], + subgroup, + subnodes, + node; + + if (typeof selector !== "function") selector = d3_selection_selectorAll(selector); + + for (var j = -1, m = this.length; ++j < m;) { + for (var group = this[j], i = -1, n = group.length; ++i < n;) { + if (node = group[i]) { + subnodes = selector.call(node.node, node.node.__data__, i); + subgroups.push(subgroup = []); + for (var k = -1, o = subnodes.length; ++k < o;) { + subgroup.push({node: subnodes[k], delay: node.delay, duration: node.duration}); + } + } + } + } + + return d3_transition(subgroups, this.id, this.time).ease(this.ease()); + }; + d3_transitionPrototype.attr = function(name, value) { + return this.attrTween(name, d3_transitionTween(value)); + }; + + d3_transitionPrototype.attrTween = function(nameNS, tween) { + var name = d3.ns.qualify(nameNS); + + function attrTween(d, i) { + var f = tween.call(this, d, i, this.getAttribute(name)); + return f === d3_transitionRemove + ? (this.removeAttribute(name), null) + : f && function(t) { this.setAttribute(name, f(t)); }; + } + + function attrTweenNS(d, i) { + var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local)); + return f === d3_transitionRemove + ? (this.removeAttributeNS(name.space, name.local), null) + : f && function(t) { this.setAttributeNS(name.space, name.local, f(t)); }; + } + + return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween); + }; + d3_transitionPrototype.style = function(name, value, priority) { + if (arguments.length < 3) priority = ""; + return this.styleTween(name, d3_transitionTween(value), priority); + }; + + d3_transitionPrototype.styleTween = function(name, tween, priority) { + if (arguments.length < 3) priority = ""; + return this.tween("style." + name, function(d, i) { + var f = tween.call(this, d, i, window.getComputedStyle(this, null).getPropertyValue(name)); + return f === d3_transitionRemove + ? (this.style.removeProperty(name), null) + : f && function(t) { this.style.setProperty(name, f(t), priority); }; + }); + }; + d3_transitionPrototype.text = function(value) { + return this.tween("text", function(d, i) { + this.textContent = typeof value === "function" + ? value.call(this, d, i) + : value; + }); + }; + d3_transitionPrototype.remove = function() { + return this.each("end", function() { + var p; + if (!this.__transition__ && (p = this.parentNode)) p.removeChild(this); + }); + }; + d3_transitionPrototype.delay = function(value) { + var groups = this; + return groups.each(typeof value === "function" + ? function(d, i, j) { groups[j][i].delay = +value.apply(this, arguments); } + : (value = +value, function(d, i, j) { groups[j][i].delay = value; })); + }; + d3_transitionPrototype.duration = function(value) { + var groups = this; + return groups.each(typeof value === "function" + ? function(d, i, j) { groups[j][i].duration = +value.apply(this, arguments); } + : (value = +value, function(d, i, j) { groups[j][i].duration = value; })); + }; + function d3_transition_each(callback) { + for (var j = 0, m = this.length; j < m; j++) { + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + var node = group[i]; + if (node) callback.call(node = node.node, node.__data__, i, j); + } + } + return this; + } + d3_transitionPrototype.transition = function() { + return this.select(d3_this); + }; + var d3_timer_queue = null, + d3_timer_interval, // is an interval (or frame) active? + d3_timer_timeout; // is a timeout active? + +// The timer will continue to fire until callback returns true. + d3.timer = function(callback, delay, then) { + var found = false, + t0, + t1 = d3_timer_queue; + + if (arguments.length < 3) { + if (arguments.length < 2) delay = 0; + else if (!isFinite(delay)) return; + then = Date.now(); + } + + // See if the callback's already in the queue. + while (t1) { + if (t1.callback === callback) { + t1.then = then; + t1.delay = delay; + found = true; + break; + } + t0 = t1; + t1 = t1.next; + } + + // Otherwise, add the callback to the queue. + if (!found) d3_timer_queue = { + callback: callback, + then: then, + delay: delay, + next: d3_timer_queue + }; + + // Start animatin'! + if (!d3_timer_interval) { + d3_timer_timeout = clearTimeout(d3_timer_timeout); + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + } + + function d3_timer_step() { + var elapsed, + now = Date.now(), + t1 = d3_timer_queue; + + while (t1) { + elapsed = now - t1.then; + if (elapsed >= t1.delay) t1.flush = t1.callback(elapsed); + t1 = t1.next; + } + + var delay = d3_timer_flush() - now; + if (delay > 24) { + if (isFinite(delay)) { + clearTimeout(d3_timer_timeout); + d3_timer_timeout = setTimeout(d3_timer_step, delay); + } + d3_timer_interval = 0; + } else { + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + } + + d3.timer.flush = function() { + var elapsed, + now = Date.now(), + t1 = d3_timer_queue; + + while (t1) { + elapsed = now - t1.then; + if (!t1.delay) t1.flush = t1.callback(elapsed); + t1 = t1.next; + } + + d3_timer_flush(); + }; + +// Flush after callbacks, to avoid concurrent queue modification. + function d3_timer_flush() { + var t0 = null, + t1 = d3_timer_queue, + then = Infinity; + while (t1) { + if (t1.flush) { + t1 = t0 ? t0.next = t1.next : d3_timer_queue = t1.next; + } else { + then = Math.min(then, t1.then + t1.delay); + t1 = (t0 = t1).next; + } + } + return then; + } + + var d3_timer_frame = window.requestAnimationFrame + || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame + || window.oRequestAnimationFrame + || window.msRequestAnimationFrame + || function(callback) { setTimeout(callback, 17); }; + function d3_noop() {} + d3.scale = {}; + + function d3_scaleExtent(domain) { + var start = domain[0], stop = domain[domain.length - 1]; + return start < stop ? [start, stop] : [stop, start]; + } + function d3_scale_nice(domain, nice) { + var i0 = 0, + i1 = domain.length - 1, + x0 = domain[i0], + x1 = domain[i1], + dx; + + if (x1 < x0) { + dx = i0; i0 = i1; i1 = dx; + dx = x0; x0 = x1; x1 = dx; + } + + if (dx = x1 - x0) { + nice = nice(dx); + domain[i0] = nice.floor(x0); + domain[i1] = nice.ceil(x1); + } + + return domain; + } + + function d3_scale_niceDefault() { + return Math; + } + d3.scale.linear = function() { + return d3_scale_linear([0, 1], [0, 1], d3.interpolate, false); + }; + + function d3_scale_linear(domain, range, interpolate, clamp) { + var output, + input; + + function rescale() { + var linear = domain.length == 2 ? d3_scale_bilinear : d3_scale_polylinear, + uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber; + output = linear(domain, range, uninterpolate, interpolate); + input = linear(range, domain, uninterpolate, d3.interpolate); + return scale; + } + + function scale(x) { + return output(x); + } + + // Note: requires range is coercible to number! + scale.invert = function(y) { + return input(y); + }; + + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.map(Number); + return rescale(); + }; + + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + + scale.rangeRound = function(x) { + return scale.range(x).interpolate(d3.interpolateRound); + }; + + scale.clamp = function(x) { + if (!arguments.length) return clamp; + clamp = x; + return rescale(); + }; + + scale.interpolate = function(x) { + if (!arguments.length) return interpolate; + interpolate = x; + return rescale(); + }; + + scale.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + + scale.tickFormat = function(m) { + return d3_scale_linearTickFormat(domain, m); + }; + + scale.nice = function() { + d3_scale_nice(domain, d3_scale_linearNice); + return rescale(); + }; + + scale.copy = function() { + return d3_scale_linear(domain, range, interpolate, clamp); + }; + + return rescale(); + }; + + function d3_scale_linearRebind(scale, linear) { + scale.range = d3.rebind(scale, linear.range); + scale.rangeRound = d3.rebind(scale, linear.rangeRound); + scale.interpolate = d3.rebind(scale, linear.interpolate); + scale.clamp = d3.rebind(scale, linear.clamp); + return scale; + } + + function d3_scale_linearNice(dx) { + dx = Math.pow(10, Math.round(Math.log(dx) / Math.LN10) - 1); + return { + floor: function(x) { return Math.floor(x / dx) * dx; }, + ceil: function(x) { return Math.ceil(x / dx) * dx; } + }; + } + +// TODO Dates? Ugh. + function d3_scale_linearTickRange(domain, m) { + var extent = d3_scaleExtent(domain), + span = extent[1] - extent[0], + step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), + err = m / span * step; + + // Filter ticks to get closer to the desired count. + if (err <= .15) step *= 10; + else if (err <= .35) step *= 5; + else if (err <= .75) step *= 2; + + // Round start and stop values to step interval. + extent[0] = Math.ceil(extent[0] / step) * step; + extent[1] = Math.floor(extent[1] / step) * step + step * .5; // inclusive + extent[2] = step; + return extent; + } + + function d3_scale_linearTicks(domain, m) { + return d3.range.apply(d3, d3_scale_linearTickRange(domain, m)); + } + + function d3_scale_linearTickFormat(domain, m) { + return d3.format(",." + Math.max(0, -Math.floor(Math.log(d3_scale_linearTickRange(domain, m)[2]) / Math.LN10 + .01)) + "f"); + } + function d3_scale_bilinear(domain, range, uninterpolate, interpolate) { + var u = uninterpolate(domain[0], domain[1]), + i = interpolate(range[0], range[1]); + return function(x) { + return i(u(x)); + }; + } + function d3_scale_polylinear(domain, range, uninterpolate, interpolate) { + var u = [], + i = [], + j = 0, + n = domain.length; + + while (++j < n) { + u.push(uninterpolate(domain[j - 1], domain[j])); + i.push(interpolate(range[j - 1], range[j])); + } + + return function(x) { + var j = d3.bisect(domain, x, 1, domain.length - 1) - 1; + return i[j](u[j](x)); + }; + } + d3.scale.log = function() { + return d3_scale_log(d3.scale.linear(), d3_scale_logp); + }; + + function d3_scale_log(linear, log) { + var pow = log.pow; + + function scale(x) { + return linear(log(x)); + } + + scale.invert = function(x) { + return pow(linear.invert(x)); + }; + + scale.domain = function(x) { + if (!arguments.length) return linear.domain().map(pow); + log = x[0] < 0 ? d3_scale_logn : d3_scale_logp; + pow = log.pow; + linear.domain(x.map(log)); + return scale; + }; + + scale.nice = function() { + linear.domain(d3_scale_nice(linear.domain(), d3_scale_niceDefault)); + return scale; + }; + + scale.ticks = function() { + var extent = d3_scaleExtent(linear.domain()), + ticks = []; + if (extent.every(isFinite)) { + var i = Math.floor(extent[0]), + j = Math.ceil(extent[1]), + u = Math.round(pow(extent[0])), + v = Math.round(pow(extent[1])); + if (log === d3_scale_logn) { + ticks.push(pow(i)); + for (; i++ < j;) for (var k = 9; k > 0; k--) ticks.push(pow(i) * k); + } else { + for (; i < j; i++) for (var k = 1; k < 10; k++) ticks.push(pow(i) * k); + ticks.push(pow(i)); + } + for (i = 0; ticks[i] < u; i++) {} // strip small values + for (j = ticks.length; ticks[j - 1] > v; j--) {} // strip big values + ticks = ticks.slice(i, j); + } + return ticks; + }; + + scale.tickFormat = function(n, format) { + if (arguments.length < 2) format = d3_scale_logFormat; + if (arguments.length < 1) return format; + var k = n / scale.ticks().length, + f = log === d3_scale_logn ? (e = -1e-15, Math.floor) : (e = 1e-15, Math.ceil), + e; + return function(d) { + return d / pow(f(log(d) + e)) < k ? format(d) : ""; + }; + }; + + scale.copy = function() { + return d3_scale_log(linear.copy(), log); + }; + + return d3_scale_linearRebind(scale, linear); + }; + + var d3_scale_logFormat = d3.format("e"); + + function d3_scale_logp(x) { + return Math.log(x) / Math.LN10; + } + + function d3_scale_logn(x) { + return -Math.log(-x) / Math.LN10; + } + + d3_scale_logp.pow = function(x) { + return Math.pow(10, x); + }; + + d3_scale_logn.pow = function(x) { + return -Math.pow(10, -x); + }; + d3.scale.pow = function() { + return d3_scale_pow(d3.scale.linear(), 1); + }; + + function d3_scale_pow(linear, exponent) { + var powp = d3_scale_powPow(exponent), + powb = d3_scale_powPow(1 / exponent); + + function scale(x) { + return linear(powp(x)); + } + + scale.invert = function(x) { + return powb(linear.invert(x)); + }; + + scale.domain = function(x) { + if (!arguments.length) return linear.domain().map(powb); + linear.domain(x.map(powp)); + return scale; + }; + + scale.ticks = function(m) { + return d3_scale_linearTicks(scale.domain(), m); + }; + + scale.tickFormat = function(m) { + return d3_scale_linearTickFormat(scale.domain(), m); + }; + + scale.nice = function() { + return scale.domain(d3_scale_nice(scale.domain(), d3_scale_linearNice)); + }; + + scale.exponent = function(x) { + if (!arguments.length) return exponent; + var domain = scale.domain(); + powp = d3_scale_powPow(exponent = x); + powb = d3_scale_powPow(1 / exponent); + return scale.domain(domain); + }; + + scale.copy = function() { + return d3_scale_pow(linear.copy(), exponent); + }; + + return d3_scale_linearRebind(scale, linear); + }; + + function d3_scale_powPow(e) { + return function(x) { + return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e); + }; + } + d3.scale.sqrt = function() { + return d3.scale.pow().exponent(.5); + }; + d3.scale.ordinal = function() { + return d3_scale_ordinal([], {t: "range", x: []}); + }; + + function d3_scale_ordinal(domain, ranger) { + var index, + range, + rangeBand; + + function scale(x) { + return range[((index[x] || (index[x] = domain.push(x))) - 1) % range.length]; + } + + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = []; + index = {}; + var i = -1, n = x.length, xi; + while (++i < n) if (!index[xi = x[i]]) index[xi] = domain.push(xi); + return scale[ranger.t](ranger.x, ranger.p); + }; + + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + rangeBand = 0; + ranger = {t: "range", x: x}; + return scale; + }; + + scale.rangePoints = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], + stop = x[1], + step = (stop - start) / (domain.length - 1 + padding); + range = domain.length < 2 ? [(start + stop) / 2] : d3.range(start + step * padding / 2, stop + step / 2, step); + rangeBand = 0; + ranger = {t: "rangePoints", x: x, p: padding}; + return scale; + }; + + scale.rangeBands = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], + stop = x[1], + step = (stop - start) / (domain.length + padding); + range = d3.range(start + step * padding, stop, step); + rangeBand = step * (1 - padding); + ranger = {t: "rangeBands", x: x, p: padding}; + return scale; + }; + + scale.rangeRoundBands = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], + stop = x[1], + step = Math.floor((stop - start) / (domain.length + padding)), + err = stop - start - (domain.length - padding) * step; + range = d3.range(start + Math.round(err / 2), stop, step); + rangeBand = Math.round(step * (1 - padding)); + ranger = {t: "rangeRoundBands", x: x, p: padding}; + return scale; + }; + + scale.rangeBand = function() { + return rangeBand; + }; + + scale.copy = function() { + return d3_scale_ordinal(domain, ranger); + }; + + return scale.domain(domain); + }; + /* + * This product includes color specifications and designs developed by Cynthia + * Brewer (http://colorbrewer.org/). See lib/colorbrewer for more information. + */ + + d3.scale.category10 = function() { + return d3.scale.ordinal().range(d3_category10); + }; + + d3.scale.category20 = function() { + return d3.scale.ordinal().range(d3_category20); + }; + + d3.scale.category20b = function() { + return d3.scale.ordinal().range(d3_category20b); + }; + + d3.scale.category20c = function() { + return d3.scale.ordinal().range(d3_category20c); + }; + + var d3_category10 = [ + "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", + "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf" + ]; + + var d3_category20 = [ + "#1f77b4", "#aec7e8", + "#ff7f0e", "#ffbb78", + "#2ca02c", "#98df8a", + "#d62728", "#ff9896", + "#9467bd", "#c5b0d5", + "#8c564b", "#c49c94", + "#e377c2", "#f7b6d2", + "#7f7f7f", "#c7c7c7", + "#bcbd22", "#dbdb8d", + "#17becf", "#9edae5" + ]; + + var d3_category20b = [ + "#393b79", "#5254a3", "#6b6ecf", "#9c9ede", + "#637939", "#8ca252", "#b5cf6b", "#cedb9c", + "#8c6d31", "#bd9e39", "#e7ba52", "#e7cb94", + "#843c39", "#ad494a", "#d6616b", "#e7969c", + "#7b4173", "#a55194", "#ce6dbd", "#de9ed6" + ]; + + var d3_category20c = [ + "#3182bd", "#6baed6", "#9ecae1", "#c6dbef", + "#e6550d", "#fd8d3c", "#fdae6b", "#fdd0a2", + "#31a354", "#74c476", "#a1d99b", "#c7e9c0", + "#756bb1", "#9e9ac8", "#bcbddc", "#dadaeb", + "#636363", "#969696", "#bdbdbd", "#d9d9d9" + ]; + d3.scale.quantile = function() { + return d3_scale_quantile([], []); + }; + + function d3_scale_quantile(domain, range) { + var thresholds; + + function rescale() { + var k = 0, + n = domain.length, + q = range.length; + thresholds = []; + while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q); + return scale; + } + + function scale(x) { + if (isNaN(x = +x)) return NaN; + return range[d3.bisect(thresholds, x)]; + } + + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.filter(function(d) { return !isNaN(d); }).sort(d3.ascending); + return rescale(); + }; + + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + + scale.quantiles = function() { + return thresholds; + }; + + scale.copy = function() { + return d3_scale_quantile(domain, range); // copy on write! + }; + + return rescale(); + }; + d3.scale.quantize = function() { + return d3_scale_quantize(0, 1, [0, 1]); + }; + + function d3_scale_quantize(x0, x1, range) { + var kx, i; + + function scale(x) { + return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))]; + } + + function rescale() { + kx = range.length / (x1 - x0); + i = range.length - 1; + return scale; + } + + scale.domain = function(x) { + if (!arguments.length) return [x0, x1]; + x0 = +x[0]; + x1 = +x[x.length - 1]; + return rescale(); + }; + + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + + scale.copy = function() { + return d3_scale_quantize(x0, x1, range); // copy on write + }; + + return rescale(); + }; + d3.svg = {}; + d3.svg.arc = function() { + var innerRadius = d3_svg_arcInnerRadius, + outerRadius = d3_svg_arcOuterRadius, + startAngle = d3_svg_arcStartAngle, + endAngle = d3_svg_arcEndAngle; + + function arc() { + var r0 = innerRadius.apply(this, arguments), + r1 = outerRadius.apply(this, arguments), + a0 = startAngle.apply(this, arguments) + d3_svg_arcOffset, + a1 = endAngle.apply(this, arguments) + d3_svg_arcOffset, + da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0), + df = da < Math.PI ? "0" : "1", + c0 = Math.cos(a0), + s0 = Math.sin(a0), + c1 = Math.cos(a1), + s1 = Math.sin(a1); + return da >= d3_svg_arcMax + ? (r0 + ? "M0," + r1 + + "A" + r1 + "," + r1 + " 0 1,1 0," + (-r1) + + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + + "M0," + r0 + + "A" + r0 + "," + r0 + " 0 1,0 0," + (-r0) + + "A" + r0 + "," + r0 + " 0 1,0 0," + r0 + + "Z" + : "M0," + r1 + + "A" + r1 + "," + r1 + " 0 1,1 0," + (-r1) + + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + + "Z") + : (r0 + ? "M" + r1 * c0 + "," + r1 * s0 + + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + + "L" + r0 * c1 + "," + r0 * s1 + + "A" + r0 + "," + r0 + " 0 " + df + ",0 " + r0 * c0 + "," + r0 * s0 + + "Z" + : "M" + r1 * c0 + "," + r1 * s0 + + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + + "L0,0" + + "Z"); + } + + arc.innerRadius = function(v) { + if (!arguments.length) return innerRadius; + innerRadius = d3.functor(v); + return arc; + }; + + arc.outerRadius = function(v) { + if (!arguments.length) return outerRadius; + outerRadius = d3.functor(v); + return arc; + }; + + arc.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3.functor(v); + return arc; + }; + + arc.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3.functor(v); + return arc; + }; + + arc.centroid = function() { + var r = (innerRadius.apply(this, arguments) + + outerRadius.apply(this, arguments)) / 2, + a = (startAngle.apply(this, arguments) + + endAngle.apply(this, arguments)) / 2 + d3_svg_arcOffset; + return [Math.cos(a) * r, Math.sin(a) * r]; + }; + + return arc; + }; + + var d3_svg_arcOffset = -Math.PI / 2, + d3_svg_arcMax = 2 * Math.PI - 1e-6; + + function d3_svg_arcInnerRadius(d) { + return d.innerRadius; + } + + function d3_svg_arcOuterRadius(d) { + return d.outerRadius; + } + + function d3_svg_arcStartAngle(d) { + return d.startAngle; + } + + function d3_svg_arcEndAngle(d) { + return d.endAngle; + } + function d3_svg_line(projection) { + var x = d3_svg_lineX, + y = d3_svg_lineY, + interpolate = "linear", + interpolator = d3_svg_lineInterpolators[interpolate], + tension = .7; + + function line(d) { + return d.length < 1 ? null : "M" + interpolator(projection(d3_svg_linePoints(this, d, x, y)), tension); + } + + line.x = function(v) { + if (!arguments.length) return x; + x = v; + return line; + }; + + line.y = function(v) { + if (!arguments.length) return y; + y = v; + return line; + }; + + line.interpolate = function(v) { + if (!arguments.length) return interpolate; + interpolator = d3_svg_lineInterpolators[interpolate = v]; + return line; + }; + + line.tension = function(v) { + if (!arguments.length) return tension; + tension = v; + return line; + }; + + return line; + } + + d3.svg.line = function() { + return d3_svg_line(Object); + }; + +// Converts the specified array of data into an array of points +// (x-y tuples), by evaluating the specified `x` and `y` functions on each +// data point. The `this` context of the evaluated functions is the specified +// "self" object; each function is passed the current datum and index. + function d3_svg_linePoints(self, d, x, y) { + var points = [], + i = -1, + n = d.length, + fx = typeof x === "function", + fy = typeof y === "function", + value; + if (fx && fy) { + while (++i < n) points.push([ + x.call(self, value = d[i], i), + y.call(self, value, i) + ]); + } else if (fx) { + while (++i < n) points.push([x.call(self, d[i], i), y]); + } else if (fy) { + while (++i < n) points.push([x, y.call(self, d[i], i)]); + } else { + while (++i < n) points.push([x, y]); + } + return points; + } + +// The default `x` property, which references d[0]. + function d3_svg_lineX(d) { + return d[0]; + } + +// The default `y` property, which references d[1]. + function d3_svg_lineY(d) { + return d[1]; + } + +// The various interpolators supported by the `line` class. + var d3_svg_lineInterpolators = { + "linear": d3_svg_lineLinear, + "step-before": d3_svg_lineStepBefore, + "step-after": d3_svg_lineStepAfter, + "basis": d3_svg_lineBasis, + "basis-open": d3_svg_lineBasisOpen, + "basis-closed": d3_svg_lineBasisClosed, + "bundle": d3_svg_lineBundle, + "cardinal": d3_svg_lineCardinal, + "cardinal-open": d3_svg_lineCardinalOpen, + "cardinal-closed": d3_svg_lineCardinalClosed, + "monotone": d3_svg_lineMonotone + }; + +// Linear interpolation; generates "L" commands. + function d3_svg_lineLinear(points) { + var i = 0, + n = points.length, + p = points[0], + path = [p[0], ",", p[1]]; + while (++i < n) path.push("L", (p = points[i])[0], ",", p[1]); + return path.join(""); + } + +// Step interpolation; generates "H" and "V" commands. + function d3_svg_lineStepBefore(points) { + var i = 0, + n = points.length, + p = points[0], + path = [p[0], ",", p[1]]; + while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]); + return path.join(""); + } + +// Step interpolation; generates "H" and "V" commands. + function d3_svg_lineStepAfter(points) { + var i = 0, + n = points.length, + p = points[0], + path = [p[0], ",", p[1]]; + while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]); + return path.join(""); + } + +// Open cardinal spline interpolation; generates "C" commands. + function d3_svg_lineCardinalOpen(points, tension) { + return points.length < 4 + ? d3_svg_lineLinear(points) + : points[1] + d3_svg_lineHermite(points.slice(1, points.length - 1), + d3_svg_lineCardinalTangents(points, tension)); + } + +// Closed cardinal spline interpolation; generates "C" commands. + function d3_svg_lineCardinalClosed(points, tension) { + return points.length < 3 + ? d3_svg_lineLinear(points) + : points[0] + d3_svg_lineHermite((points.push(points[0]), points), + d3_svg_lineCardinalTangents([points[points.length - 2]] + .concat(points, [points[1]]), tension)); + } + +// Cardinal spline interpolation; generates "C" commands. + function d3_svg_lineCardinal(points, tension, closed) { + return points.length < 3 + ? d3_svg_lineLinear(points) + : points[0] + d3_svg_lineHermite(points, + d3_svg_lineCardinalTangents(points, tension)); + } + +// Hermite spline construction; generates "C" commands. + function d3_svg_lineHermite(points, tangents) { + if (tangents.length < 1 + || (points.length != tangents.length + && points.length != tangents.length + 2)) { + return d3_svg_lineLinear(points); + } + + var quad = points.length != tangents.length, + path = "", + p0 = points[0], + p = points[1], + t0 = tangents[0], + t = t0, + pi = 1; + + if (quad) { + path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + + "," + p[0] + "," + p[1]; + p0 = points[1]; + pi = 2; + } + + if (tangents.length > 1) { + t = tangents[1]; + p = points[pi]; + pi++; + path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + + "," + p[0] + "," + p[1]; + for (var i = 2; i < tangents.length; i++, pi++) { + p = points[pi]; + t = tangents[i]; + path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + + "," + p[0] + "," + p[1]; + } + } + + if (quad) { + var lp = points[pi]; + path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + + "," + lp[0] + "," + lp[1]; + } + + return path; + } + +// Generates tangents for a cardinal spline. + function d3_svg_lineCardinalTangents(points, tension) { + var tangents = [], + a = (1 - tension) / 2, + p0, + p1 = points[0], + p2 = points[1], + i = 1, + n = points.length; + while (++i < n) { + p0 = p1; + p1 = p2; + p2 = points[i]; + tangents.push([a * (p2[0] - p0[0]), a * (p2[1] - p0[1])]); + } + return tangents; + } + +// B-spline interpolation; generates "C" commands. + function d3_svg_lineBasis(points) { + if (points.length < 3) return d3_svg_lineLinear(points); + var i = 1, + n = points.length, + pi = points[0], + x0 = pi[0], + y0 = pi[1], + px = [x0, x0, x0, (pi = points[1])[0]], + py = [y0, y0, y0, pi[1]], + path = [x0, ",", y0]; + d3_svg_lineBasisBezier(path, px, py); + while (++i < n) { + pi = points[i]; + px.shift(); px.push(pi[0]); + py.shift(); py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + i = -1; + while (++i < 2) { + px.shift(); px.push(pi[0]); + py.shift(); py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + +// Open B-spline interpolation; generates "C" commands. + function d3_svg_lineBasisOpen(points) { + if (points.length < 4) return d3_svg_lineLinear(points); + var path = [], + i = -1, + n = points.length, + pi, + px = [0], + py = [0]; + while (++i < 3) { + pi = points[i]; + px.push(pi[0]); + py.push(pi[1]); + } + path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py)); + --i; while (++i < n) { + pi = points[i]; + px.shift(); px.push(pi[0]); + py.shift(); py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + +// Closed B-spline interpolation; generates "C" commands. + function d3_svg_lineBasisClosed(points) { + var path, + i = -1, + n = points.length, + m = n + 4, + pi, + px = [], + py = []; + while (++i < 4) { + pi = points[i % n]; + px.push(pi[0]); + py.push(pi[1]); + } + path = [ + d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) + ]; + --i; while (++i < m) { + pi = points[i % n]; + px.shift(); px.push(pi[0]); + py.shift(); py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + + function d3_svg_lineBundle(points, tension) { + var n = points.length - 1, + x0 = points[0][0], + y0 = points[0][1], + dx = points[n][0] - x0, + dy = points[n][1] - y0, + i = -1, + p, + t; + while (++i <= n) { + p = points[i]; + t = i / n; + p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx); + p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy); + } + return d3_svg_lineBasis(points); + } + +// Returns the dot product of the given four-element vectors. + function d3_svg_lineDot4(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; + } + +// Matrix to transform basis (b-spline) control points to bezier +// control points. Derived from FvD 11.2.8. + var d3_svg_lineBasisBezier1 = [0, 2/3, 1/3, 0], + d3_svg_lineBasisBezier2 = [0, 1/3, 2/3, 0], + d3_svg_lineBasisBezier3 = [0, 1/6, 2/3, 1/6]; + +// Pushes a "C" Bézier curve onto the specified path array, given the +// two specified four-element arrays which define the control points. + function d3_svg_lineBasisBezier(path, x, y) { + path.push( + "C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), + ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), + ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), + ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), + ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), + ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y)); + } + +// Computes the slope from points p0 to p1. + function d3_svg_lineSlope(p0, p1) { + return (p1[1] - p0[1]) / (p1[0] - p0[0]); + } + +// Compute three-point differences for the given points. +// http://en.wikipedia.org/wiki/Cubic_Hermite_spline#Finite_difference + function d3_svg_lineFiniteDifferences(points) { + var i = 0, + j = points.length - 1, + m = [], + p0 = points[0], + p1 = points[1], + d = m[0] = d3_svg_lineSlope(p0, p1); + while (++i < j) { + m[i] = d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1])); + } + m[i] = d; + return m; + } + +// Interpolates the given points using Fritsch-Carlson Monotone cubic Hermite +// interpolation. Returns an array of tangent vectors. For details, see +// http://en.wikipedia.org/wiki/Monotone_cubic_interpolation + function d3_svg_lineMonotoneTangents(points) { + var tangents = [], + d, + a, + b, + s, + m = d3_svg_lineFiniteDifferences(points), + i = -1, + j = points.length - 1; + + // The first two steps are done by computing finite-differences: + // 1. Compute the slopes of the secant lines between successive points. + // 2. Initialize the tangents at every point as the average of the secants. + + // Then, for each segment… + while (++i < j) { + d = d3_svg_lineSlope(points[i], points[i + 1]); + + // 3. If two successive yk = y{k + 1} are equal (i.e., d is zero), then set + // mk = m{k + 1} = 0 as the spline connecting these points must be flat to + // preserve monotonicity. Ignore step 4 and 5 for those k. + + if (Math.abs(d) < 1e-6) { + m[i] = m[i + 1] = 0; + } else { + // 4. Let ak = mk / dk and bk = m{k + 1} / dk. + a = m[i] / d; + b = m[i + 1] / d; + + // 5. Prevent overshoot and ensure monotonicity by restricting the + // magnitude of vector to a circle of radius 3. + s = a * a + b * b; + if (s > 9) { + s = d * 3 / Math.sqrt(s); + m[i] = s * a; + m[i + 1] = s * b; + } + } + } + + // Compute the normalized tangent vector from the slopes. Note that if x is + // not monotonic, it's possible that the slope will be infinite, so we protect + // against NaN by setting the coordinate to zero. + i = -1; while (++i <= j) { + s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) + / (6 * (1 + m[i] * m[i])); + tangents.push([s || 0, m[i] * s || 0]); + } + + return tangents; + } + + function d3_svg_lineMonotone(points) { + return points.length < 3 + ? d3_svg_lineLinear(points) + : points[0] + + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points)); + } + d3.svg.line.radial = function() { + var line = d3_svg_line(d3_svg_lineRadial); + line.radius = line.x, delete line.x; + line.angle = line.y, delete line.y; + return line; + }; + + function d3_svg_lineRadial(points) { + var point, + i = -1, + n = points.length, + r, + a; + while (++i < n) { + point = points[i]; + r = point[0]; + a = point[1] + d3_svg_arcOffset; + point[0] = r * Math.cos(a); + point[1] = r * Math.sin(a); + } + return points; + } + function d3_svg_area(projection) { + var x0 = d3_svg_lineX, + x1 = d3_svg_lineX, + y0 = 0, + y1 = d3_svg_lineY, + interpolate, + i0, + i1, + tension = .7; + + function area(d) { + if (d.length < 1) return null; + var points0 = d3_svg_linePoints(this, d, x0, y0), + points1 = d3_svg_linePoints(this, d, x0 === x1 ? d3_svg_areaX(points0) : x1, y0 === y1 ? d3_svg_areaY(points0) : y1); + return "M" + i0(projection(points1), tension) + + "L" + i1(projection(points0.reverse()), tension) + + "Z"; + } + + area.x = function(x) { + if (!arguments.length) return x1; + x0 = x1 = x; + return area; + }; + + area.x0 = function(x) { + if (!arguments.length) return x0; + x0 = x; + return area; + }; + + area.x1 = function(x) { + if (!arguments.length) return x1; + x1 = x; + return area; + }; + + area.y = function(y) { + if (!arguments.length) return y1; + y0 = y1 = y; + return area; + }; + + area.y0 = function(y) { + if (!arguments.length) return y0; + y0 = y; + return area; + }; + + area.y1 = function(y) { + if (!arguments.length) return y1; + y1 = y; + return area; + }; + + area.interpolate = function(x) { + if (!arguments.length) return interpolate; + i0 = d3_svg_lineInterpolators[interpolate = x]; + i1 = i0.reverse || i0; + return area; + }; + + area.tension = function(x) { + if (!arguments.length) return tension; + tension = x; + return area; + }; + + return area.interpolate("linear"); + } + + d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter; + d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore; + + d3.svg.area = function() { + return d3_svg_area(Object); + }; + + function d3_svg_areaX(points) { + return function(d, i) { + return points[i][0]; + }; + } + + function d3_svg_areaY(points) { + return function(d, i) { + return points[i][1]; + }; + } + d3.svg.area.radial = function() { + var area = d3_svg_area(d3_svg_lineRadial); + area.radius = area.x, delete area.x; + area.innerRadius = area.x0, delete area.x0; + area.outerRadius = area.x1, delete area.x1; + area.angle = area.y, delete area.y; + area.startAngle = area.y0, delete area.y0; + area.endAngle = area.y1, delete area.y1; + return area; + }; + d3.svg.chord = function() { + var source = d3_svg_chordSource, + target = d3_svg_chordTarget, + radius = d3_svg_chordRadius, + startAngle = d3_svg_arcStartAngle, + endAngle = d3_svg_arcEndAngle; + + // TODO Allow control point to be customized. + + function chord(d, i) { + var s = subgroup(this, source, d, i), + t = subgroup(this, target, d, i); + return "M" + s.p0 + + arc(s.r, s.p1) + (equals(s, t) + ? curve(s.r, s.p1, s.r, s.p0) + : curve(s.r, s.p1, t.r, t.p0) + + arc(t.r, t.p1) + + curve(t.r, t.p1, s.r, s.p0)) + + "Z"; + } + + function subgroup(self, f, d, i) { + var subgroup = f.call(self, d, i), + r = radius.call(self, subgroup, i), + a0 = startAngle.call(self, subgroup, i) + d3_svg_arcOffset, + a1 = endAngle.call(self, subgroup, i) + d3_svg_arcOffset; + return { + r: r, + a0: a0, + a1: a1, + p0: [r * Math.cos(a0), r * Math.sin(a0)], + p1: [r * Math.cos(a1), r * Math.sin(a1)] + }; + } + + function equals(a, b) { + return a.a0 == b.a0 && a.a1 == b.a1; + } + + function arc(r, p) { + return "A" + r + "," + r + " 0 0,1 " + p; + } + + function curve(r0, p0, r1, p1) { + return "Q 0,0 " + p1; + } + + chord.radius = function(v) { + if (!arguments.length) return radius; + radius = d3.functor(v); + return chord; + }; + + chord.source = function(v) { + if (!arguments.length) return source; + source = d3.functor(v); + return chord; + }; + + chord.target = function(v) { + if (!arguments.length) return target; + target = d3.functor(v); + return chord; + }; + + chord.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3.functor(v); + return chord; + }; + + chord.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3.functor(v); + return chord; + }; + + return chord; + }; + + function d3_svg_chordSource(d) { + return d.source; + } + + function d3_svg_chordTarget(d) { + return d.target; + } + + function d3_svg_chordRadius(d) { + return d.radius; + } + + function d3_svg_chordStartAngle(d) { + return d.startAngle; + } + + function d3_svg_chordEndAngle(d) { + return d.endAngle; + } + d3.svg.diagonal = function() { + var source = d3_svg_chordSource, + target = d3_svg_chordTarget, + projection = d3_svg_diagonalProjection; + + function diagonal(d, i) { + var p0 = source.call(this, d, i), + p3 = target.call(this, d, i), + m = (p0.y + p3.y) / 2, + p = [p0, {x: p0.x, y: m}, {x: p3.x, y: m}, p3]; + p = p.map(projection); + return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3]; + } + + diagonal.source = function(x) { + if (!arguments.length) return source; + source = d3.functor(x); + return diagonal; + }; + + diagonal.target = function(x) { + if (!arguments.length) return target; + target = d3.functor(x); + return diagonal; + }; + + diagonal.projection = function(x) { + if (!arguments.length) return projection; + projection = x; + return diagonal; + }; + + return diagonal; + }; + + function d3_svg_diagonalProjection(d) { + return [d.x, d.y]; + } + d3.svg.diagonal.radial = function() { + var diagonal = d3.svg.diagonal(), + projection = d3_svg_diagonalProjection, + projection_ = diagonal.projection; + + diagonal.projection = function(x) { + return arguments.length + ? projection_(d3_svg_diagonalRadialProjection(projection = x)) + : projection; + }; + + return diagonal; + }; + + function d3_svg_diagonalRadialProjection(projection) { + return function() { + var d = projection.apply(this, arguments), + r = d[0], + a = d[1] + d3_svg_arcOffset; + return [r * Math.cos(a), r * Math.sin(a)]; + }; + } + d3.svg.mouse = function(container) { + return d3_svg_mousePoint(container, d3.event); + }; + +// https://bugs.webkit.org/show_bug.cgi?id=44083 + var d3_mouse_bug44083 = /WebKit/.test(navigator.userAgent) ? -1 : 0; + + function d3_svg_mousePoint(container, e) { + var point = (container.ownerSVGElement || container).createSVGPoint(); + if ((d3_mouse_bug44083 < 0) && (window.scrollX || window.scrollY)) { + var svg = d3.select(document.body) + .append("svg:svg") + .style("position", "absolute") + .style("top", 0) + .style("left", 0); + var ctm = svg[0][0].getScreenCTM(); + d3_mouse_bug44083 = !(ctm.f || ctm.e); + svg.remove(); + } + if (d3_mouse_bug44083) { + point.x = e.pageX; + point.y = e.pageY; + } else { + point.x = e.clientX; + point.y = e.clientY; + } + point = point.matrixTransform(container.getScreenCTM().inverse()); + return [point.x, point.y]; + }; + d3.svg.touches = function(container) { + var touches = d3.event.touches; + return touches ? d3_array(touches).map(function(touch) { + var point = d3_svg_mousePoint(container, touch); + point.identifier = touch.identifier; + return point; + }) : []; + }; + d3.svg.symbol = function() { + var type = d3_svg_symbolType, + size = d3_svg_symbolSize; + + function symbol(d, i) { + return (d3_svg_symbols[type.call(this, d, i)] + || d3_svg_symbols.circle) + (size.call(this, d, i)); + } + + symbol.type = function(x) { + if (!arguments.length) return type; + type = d3.functor(x); + return symbol; + }; + + // size of symbol in square pixels + symbol.size = function(x) { + if (!arguments.length) return size; + size = d3.functor(x); + return symbol; + }; + + return symbol; + }; + + function d3_svg_symbolSize() { + return 64; + } + + function d3_svg_symbolType() { + return "circle"; + } + +// TODO cross-diagonal? + var d3_svg_symbols = { + "circle": function(size) { + var r = Math.sqrt(size / Math.PI); + return "M0," + r + + "A" + r + "," + r + " 0 1,1 0," + (-r) + + "A" + r + "," + r + " 0 1,1 0," + r + + "Z"; + }, + "cross": function(size) { + var r = Math.sqrt(size / 5) / 2; + return "M" + -3 * r + "," + -r + + "H" + -r + + "V" + -3 * r + + "H" + r + + "V" + -r + + "H" + 3 * r + + "V" + r + + "H" + r + + "V" + 3 * r + + "H" + -r + + "V" + r + + "H" + -3 * r + + "Z"; + }, + "diamond": function(size) { + var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), + rx = ry * d3_svg_symbolTan30; + return "M0," + -ry + + "L" + rx + ",0" + + " 0," + ry + + " " + -rx + ",0" + + "Z"; + }, + "square": function(size) { + var r = Math.sqrt(size) / 2; + return "M" + -r + "," + -r + + "L" + r + "," + -r + + " " + r + "," + r + + " " + -r + "," + r + + "Z"; + }, + "triangle-down": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), + ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + ry + + "L" + rx +"," + -ry + + " " + -rx + "," + -ry + + "Z"; + }, + "triangle-up": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), + ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + -ry + + "L" + rx +"," + ry + + " " + -rx + "," + ry + + "Z"; + } + }; + + d3.svg.symbolTypes = d3.keys(d3_svg_symbols); + + var d3_svg_symbolSqrt3 = Math.sqrt(3), + d3_svg_symbolTan30 = Math.tan(30 * Math.PI / 180); + d3.svg.axis = function() { + var scale = d3.scale.linear(), + orient = "bottom", + tickMajorSize = 6, + tickMinorSize = 6, + tickEndSize = 6, + tickPadding = 3, + tickArguments_ = [10], + tickFormat_, + tickSubdivide = 0; + + function axis(selection) { + selection.each(function(d, i, j) { + var g = d3.select(this); + + // If selection is a transition, create subtransitions. + var transition = selection.delay ? function(o) { + var id = d3_transitionInheritId; + try { + d3_transitionInheritId = selection.id; + return o.transition() + .delay(selection[j][i].delay) + .duration(selection[j][i].duration) + .ease(selection.ease()); + } finally { + d3_transitionInheritId = id; + } + } : Object; + + // Ticks. + var ticks = scale.ticks.apply(scale, tickArguments_), + tickFormat = tickFormat_ == null ? scale.tickFormat.apply(scale, tickArguments_) : tickFormat_; + + // Minor ticks. + var subticks = d3_svg_axisSubdivide(scale, ticks, tickSubdivide), + subtick = g.selectAll(".minor").data(subticks, String), + subtickEnter = subtick.enter().insert("svg:line", "g").attr("class", "tick minor").style("opacity", 1e-6), + subtickExit = transition(subtick.exit()).style("opacity", 1e-6).remove(), + subtickUpdate = transition(subtick).style("opacity", 1); + + // Major ticks. + var tick = g.selectAll("g").data(ticks, String), + tickEnter = tick.enter().insert("svg:g", "path").style("opacity", 1e-6), + tickExit = transition(tick.exit()).style("opacity", 1e-6).remove(), + tickUpdate = transition(tick).style("opacity", 1), + tickTransform; + + // Domain. + var range = d3_scaleExtent(scale.range()), + path = g.selectAll(".domain").data([0]), + pathEnter = path.enter().append("svg:path").attr("class", "domain"), + pathUpdate = transition(path); + + // Stash the new scale and grab the old scale. + var scale0 = this.__chart__ || scale; + this.__chart__ = scale.copy(); + + tickEnter.append("svg:line").attr("class", "tick"); + tickEnter.append("svg:text"); + tickUpdate.select("text").text(tickFormat); + + switch (orient) { + case "bottom": { + tickTransform = d3_svg_axisX; + subtickUpdate.attr("x2", 0).attr("y2", tickMinorSize); + tickUpdate.select("line").attr("x2", 0).attr("y2", tickMajorSize); + tickUpdate.select("text").attr("x", 0).attr("y", Math.max(tickMajorSize, 0) + tickPadding).attr("dy", ".71em").attr("text-anchor", "middle"); + pathUpdate.attr("d", "M" + range[0] + "," + tickEndSize + "V0H" + range[1] + "V" + tickEndSize); + break; + } + case "top": { + tickTransform = d3_svg_axisX; + subtickUpdate.attr("x2", 0).attr("y2", -tickMinorSize); + tickUpdate.select("line").attr("x2", 0).attr("y2", -tickMajorSize); + tickUpdate.select("text").attr("x", 0).attr("y", -(Math.max(tickMajorSize, 0) + tickPadding)).attr("dy", "0em").attr("text-anchor", "middle"); + pathUpdate.attr("d", "M" + range[0] + "," + -tickEndSize + "V0H" + range[1] + "V" + -tickEndSize); + break; + } + case "left": { + tickTransform = d3_svg_axisY; + subtickUpdate.attr("x2", -tickMinorSize).attr("y2", 0); + tickUpdate.select("line").attr("x2", -tickMajorSize).attr("y2", 0); + tickUpdate.select("text").attr("x", -(Math.max(tickMajorSize, 0) + tickPadding)).attr("y", 0).attr("dy", ".32em").attr("text-anchor", "end"); + pathUpdate.attr("d", "M" + -tickEndSize + "," + range[0] + "H0V" + range[1] + "H" + -tickEndSize); + break; + } + case "right": { + tickTransform = d3_svg_axisY; + subtickUpdate.attr("x2", tickMinorSize).attr("y2", 0); + tickUpdate.select("line").attr("x2", tickMajorSize).attr("y2", 0); + tickUpdate.select("text").attr("x", Math.max(tickMajorSize, 0) + tickPadding).attr("y", 0).attr("dy", ".32em").attr("text-anchor", "start"); + pathUpdate.attr("d", "M" + tickEndSize + "," + range[0] + "H0V" + range[1] + "H" + tickEndSize); + break; + } + } + + tickEnter.call(tickTransform, scale0); + tickUpdate.call(tickTransform, scale); + tickExit.call(tickTransform, scale); + + subtickEnter.call(tickTransform, scale0); + subtickUpdate.call(tickTransform, scale); + subtickExit.call(tickTransform, scale); + }); + } + + axis.scale = function(x) { + if (!arguments.length) return scale; + scale = x; + return axis; + }; + + axis.orient = function(x) { + if (!arguments.length) return orient; + orient = x; + return axis; + }; + + axis.ticks = function() { + if (!arguments.length) return tickArguments_; + tickArguments_ = arguments; + return axis; + }; + + axis.tickFormat = function(x) { + if (!arguments.length) return tickFormat_; + tickFormat_ = x; + return axis; + }; + + axis.tickSize = function(x, y, z) { + if (!arguments.length) return tickMajorSize; + var n = arguments.length - 1; + tickMajorSize = +x; + tickMinorSize = n > 1 ? +y : tickMajorSize; + tickEndSize = n > 0 ? +arguments[n] : tickMajorSize; + return axis; + }; + + axis.tickPadding = function(x) { + if (!arguments.length) return tickPadding; + tickPadding = +x; + return axis; + }; + + axis.tickSubdivide = function(x) { + if (!arguments.length) return tickSubdivide; + tickSubdivide = +x; + return axis; + }; + + return axis; + }; + + function d3_svg_axisX(selection, x) { + selection.attr("transform", function(d) { return "translate(" + x(d) + ",0)"; }); + } + + function d3_svg_axisY(selection, y) { + selection.attr("transform", function(d) { return "translate(0," + y(d) + ")"; }); + } + + function d3_svg_axisSubdivide(scale, ticks, m) { + subticks = []; + if (m && ticks.length > 1) { + var extent = d3_scaleExtent(scale.domain()), + subticks, + i = -1, + n = ticks.length, + d = (ticks[1] - ticks[0]) / ++m, + j, + v; + while (++i < n) { + for (j = m; --j > 0;) { + if ((v = +ticks[i] - j * d) >= extent[0]) { + subticks.push(v); + } + } + } + for (--i, j = 0; ++j < m && (v = +ticks[i] + j * d) < extent[1];) { + subticks.push(v); + } + } + return subticks; + } + d3.behavior = {}; + d3.behavior.drag = function() { + var event = d3.dispatch("drag", "dragstart", "dragend"); + + function drag() { + this + .on("mousedown.drag", mousedown) + .on("touchstart.drag", mousedown); + + d3.select(window) + .on("mousemove.drag", d3_behavior_dragMove) + .on("touchmove.drag", d3_behavior_dragMove) + .on("mouseup.drag", d3_behavior_dragUp, true) + .on("touchend.drag", d3_behavior_dragUp, true) + .on("click.drag", d3_behavior_dragClick, true); + } + + // snapshot the local context for subsequent dispatch + function start() { + d3_behavior_dragEvent = event; + d3_behavior_dragEventTarget = d3.event.target; + d3_behavior_dragOffset = d3_behavior_dragPoint((d3_behavior_dragTarget = this).parentNode); + d3_behavior_dragMoved = 0; + d3_behavior_dragArguments = arguments; + } + + function mousedown() { + start.apply(this, arguments); + d3_behavior_dragDispatch("dragstart"); + } + + drag.on = function(type, listener) { + event[type].add(listener); + return drag; + }; + + return drag; + }; + + var d3_behavior_dragEvent, + d3_behavior_dragEventTarget, + d3_behavior_dragTarget, + d3_behavior_dragArguments, + d3_behavior_dragOffset, + d3_behavior_dragMoved, + d3_behavior_dragStopClick; + + function d3_behavior_dragDispatch(type) { + var o = d3.event, p = d3_behavior_dragTarget.parentNode, dx = 0, dy = 0; + + if (p) { + p = d3_behavior_dragPoint(p); + dx = p[0] - d3_behavior_dragOffset[0]; + dy = p[1] - d3_behavior_dragOffset[1]; + d3_behavior_dragOffset = p; + d3_behavior_dragMoved |= dx | dy; + } + + try { + d3.event = {dx: dx, dy: dy}; + d3_behavior_dragEvent[type].dispatch.apply(d3_behavior_dragTarget, d3_behavior_dragArguments); + } finally { + d3.event = o; + } + + o.preventDefault(); + } + + function d3_behavior_dragPoint(container) { + return d3.event.touches + ? d3.svg.touches(container)[0] + : d3.svg.mouse(container); + } + + function d3_behavior_dragMove() { + if (!d3_behavior_dragTarget) return; + var parent = d3_behavior_dragTarget.parentNode; + + // O NOES! The drag element was removed from the DOM. + if (!parent) return d3_behavior_dragUp(); + + d3_behavior_dragDispatch("drag"); + d3_behavior_dragCancel(); + } + + function d3_behavior_dragUp() { + if (!d3_behavior_dragTarget) return; + d3_behavior_dragDispatch("dragend"); + d3_behavior_dragTarget = null; + + // If the node was moved, prevent the mouseup from propagating. + // Also prevent the subsequent click from propagating (e.g., for anchors). + if (d3_behavior_dragMoved && d3_behavior_dragEventTarget === d3.event.target) { + d3_behavior_dragStopClick = true; + d3_behavior_dragCancel(); + } + } + + function d3_behavior_dragClick() { + if (d3_behavior_dragStopClick && d3_behavior_dragEventTarget === d3.event.target) { + d3_behavior_dragCancel(); + d3_behavior_dragStopClick = false; + d3_behavior_dragEventTarget = null; + } + } + + function d3_behavior_dragCancel() { + d3.event.stopPropagation(); + d3.event.preventDefault(); + } +// TODO unbind zoom behavior? +// TODO unbind listener? + d3.behavior.zoom = function() { + var xyz = [0, 0, 0], + event = d3.dispatch("zoom"); + + function zoom() { + this + .on("mousedown.zoom", mousedown) + .on("mousewheel.zoom", mousewheel) + .on("DOMMouseScroll.zoom", mousewheel) + .on("dblclick.zoom", dblclick) + .on("touchstart.zoom", touchstart); + + d3.select(window) + .on("mousemove.zoom", d3_behavior_zoomMousemove) + .on("mouseup.zoom", d3_behavior_zoomMouseup) + .on("touchmove.zoom", d3_behavior_zoomTouchmove) + .on("touchend.zoom", d3_behavior_zoomTouchup) + .on("click.zoom", d3_behavior_zoomClick, true); + } + + // snapshot the local context for subsequent dispatch + function start() { + d3_behavior_zoomXyz = xyz; + d3_behavior_zoomDispatch = event.zoom.dispatch; + d3_behavior_zoomEventTarget = d3.event.target; + d3_behavior_zoomTarget = this; + d3_behavior_zoomArguments = arguments; + } + + function mousedown() { + start.apply(this, arguments); + d3_behavior_zoomPanning = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget)); + d3_behavior_zoomMoved = false; + d3.event.preventDefault(); + window.focus(); + } + + // store starting mouse location + function mousewheel() { + start.apply(this, arguments); + if (!d3_behavior_zoomZooming) d3_behavior_zoomZooming = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget)); + d3_behavior_zoomTo(d3_behavior_zoomDelta() + xyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomZooming); + } + + function dblclick() { + start.apply(this, arguments); + var mouse = d3.svg.mouse(d3_behavior_zoomTarget); + d3_behavior_zoomTo(d3.event.shiftKey ? Math.ceil(xyz[2] - 1) : Math.floor(xyz[2] + 1), mouse, d3_behavior_zoomLocation(mouse)); + } + + // doubletap detection + function touchstart() { + start.apply(this, arguments); + var touches = d3_behavior_zoomTouchup(), + touch, + now = Date.now(); + if ((touches.length === 1) && (now - d3_behavior_zoomLast < 300)) { + d3_behavior_zoomTo(1 + Math.floor(xyz[2]), touch = touches[0], d3_behavior_zoomLocations[touch.identifier]); + } + d3_behavior_zoomLast = now; + } + + zoom.on = function(type, listener) { + event[type].add(listener); + return zoom; + }; + + return zoom; + }; + + var d3_behavior_zoomDiv, + d3_behavior_zoomPanning, + d3_behavior_zoomZooming, + d3_behavior_zoomLocations = {}, // identifier -> location + d3_behavior_zoomLast = 0, + d3_behavior_zoomXyz, + d3_behavior_zoomDispatch, + d3_behavior_zoomEventTarget, + d3_behavior_zoomTarget, + d3_behavior_zoomArguments, + d3_behavior_zoomMoved, + d3_behavior_zoomStopClick; + + function d3_behavior_zoomLocation(point) { + return [ + point[0] - d3_behavior_zoomXyz[0], + point[1] - d3_behavior_zoomXyz[1], + d3_behavior_zoomXyz[2] + ]; + } + +// detect the pixels that would be scrolled by this wheel event + function d3_behavior_zoomDelta() { + + // mousewheel events are totally broken! + // https://bugs.webkit.org/show_bug.cgi?id=40441 + // not only that, but Chrome and Safari differ in re. to acceleration! + if (!d3_behavior_zoomDiv) { + d3_behavior_zoomDiv = d3.select("body").append("div") + .style("visibility", "hidden") + .style("top", 0) + .style("height", 0) + .style("width", 0) + .style("overflow-y", "scroll") + .append("div") + .style("height", "2000px") + .node().parentNode; + } + + var e = d3.event, delta; + try { + d3_behavior_zoomDiv.scrollTop = 1000; + d3_behavior_zoomDiv.dispatchEvent(e); + delta = 1000 - d3_behavior_zoomDiv.scrollTop; + } catch (error) { + delta = e.wheelDelta || (-e.detail * 5); + } + + return delta * .005; + } + +// Note: Since we don't rotate, it's possible for the touches to become +// slightly detached from their original positions. Thus, we recompute the +// touch points on touchend as well as touchstart! + function d3_behavior_zoomTouchup() { + var touches = d3.svg.touches(d3_behavior_zoomTarget), + i = -1, + n = touches.length, + touch; + while (++i < n) d3_behavior_zoomLocations[(touch = touches[i]).identifier] = d3_behavior_zoomLocation(touch); + return touches; + } + + function d3_behavior_zoomTouchmove() { + var touches = d3.svg.touches(d3_behavior_zoomTarget); + switch (touches.length) { + + // single-touch pan + case 1: { + var touch = touches[0]; + d3_behavior_zoomTo(d3_behavior_zoomXyz[2], touch, d3_behavior_zoomLocations[touch.identifier]); + break; + } + + // double-touch pan + zoom + case 2: { + var p0 = touches[0], + p1 = touches[1], + p2 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2], + l0 = d3_behavior_zoomLocations[p0.identifier], + l1 = d3_behavior_zoomLocations[p1.identifier], + l2 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2, l0[2]]; + d3_behavior_zoomTo(Math.log(d3.event.scale) / Math.LN2 + l0[2], p2, l2); + break; + } + } + } + + function d3_behavior_zoomMousemove() { + d3_behavior_zoomZooming = null; + if (d3_behavior_zoomPanning) { + d3_behavior_zoomMoved = true; + d3_behavior_zoomTo(d3_behavior_zoomXyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomPanning); + } + } + + function d3_behavior_zoomMouseup() { + if (d3_behavior_zoomPanning) { + if (d3_behavior_zoomMoved && d3_behavior_zoomEventTarget === d3.event.target) { + d3_behavior_zoomStopClick = true; + } + d3_behavior_zoomMousemove(); + d3_behavior_zoomPanning = null; + } + } + + function d3_behavior_zoomClick() { + if (d3_behavior_zoomStopClick && d3_behavior_zoomEventTarget === d3.event.target) { + d3.event.stopPropagation(); + d3.event.preventDefault(); + d3_behavior_zoomStopClick = false; + d3_behavior_zoomEventTarget = null; + } + } + + function d3_behavior_zoomTo(z, x0, x1) { + var K = Math.pow(2, (d3_behavior_zoomXyz[2] = z) - x1[2]), + x = d3_behavior_zoomXyz[0] = x0[0] - K * x1[0], + y = d3_behavior_zoomXyz[1] = x0[1] - K * x1[1], + o = d3.event, // Events can be reentrant (e.g., focus). + k = Math.pow(2, z); + + d3.event = { + scale: k, + translate: [x, y], + transform: function(sx, sy) { + if (sx) transform(sx, x); + if (sy) transform(sy, y); + } + }; + + function transform(scale, o) { + var domain = scale.__domain || (scale.__domain = scale.domain()), + range = scale.range().map(function(v) { return (v - o) / k; }); + scale.domain(domain).domain(range.map(scale.invert)); + } + + try { + d3_behavior_zoomDispatch.apply(d3_behavior_zoomTarget, d3_behavior_zoomArguments); + } finally { + d3.event = o; + } + + o.preventDefault(); + } +})(); diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.layout.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.layout.js new file mode 100644 index 0000000000..0d050fbd94 --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/d3.layout.js @@ -0,0 +1,1890 @@ +(function(){d3.layout = {}; +// Implements hierarchical edge bundling using Holten's algorithm. For each +// input link, a path is computed that travels through the tree, up the parent +// hierarchy to the least common ancestor, and then back down to the destination +// node. Each path is simply an array of nodes. + d3.layout.bundle = function() { + return function(links) { + var paths = [], + i = -1, + n = links.length; + while (++i < n) paths.push(d3_layout_bundlePath(links[i])); + return paths; + }; + }; + + function d3_layout_bundlePath(link) { + var start = link.source, + end = link.target, + lca = d3_layout_bundleLeastCommonAncestor(start, end), + points = [start]; + while (start !== lca) { + start = start.parent; + points.push(start); + } + var k = points.length; + while (end !== lca) { + points.splice(k, 0, end); + end = end.parent; + } + return points; + } + + function d3_layout_bundleAncestors(node) { + var ancestors = [], + parent = node.parent; + while (parent != null) { + ancestors.push(node); + node = parent; + parent = parent.parent; + } + ancestors.push(node); + return ancestors; + } + + function d3_layout_bundleLeastCommonAncestor(a, b) { + if (a === b) return a; + var aNodes = d3_layout_bundleAncestors(a), + bNodes = d3_layout_bundleAncestors(b), + aNode = aNodes.pop(), + bNode = bNodes.pop(), + sharedNode = null; + while (aNode === bNode) { + sharedNode = aNode; + aNode = aNodes.pop(); + bNode = bNodes.pop(); + } + return sharedNode; + } + d3.layout.chord = function() { + var chord = {}, + chords, + groups, + matrix, + n, + padding = 0, + sortGroups, + sortSubgroups, + sortChords; + + function relayout() { + var subgroups = {}, + groupSums = [], + groupIndex = d3.range(n), + subgroupIndex = [], + k, + x, + x0, + i, + j; + + chords = []; + groups = []; + + // Compute the sum. + k = 0, i = -1; while (++i < n) { + x = 0, j = -1; while (++j < n) { + x += matrix[i][j]; + } + groupSums.push(x); + subgroupIndex.push(d3.range(n)); + k += x; + } + + // Sort groups… + if (sortGroups) { + groupIndex.sort(function(a, b) { + return sortGroups(groupSums[a], groupSums[b]); + }); + } + + // Sort subgroups… + if (sortSubgroups) { + subgroupIndex.forEach(function(d, i) { + d.sort(function(a, b) { + return sortSubgroups(matrix[i][a], matrix[i][b]); + }); + }); + } + + // Convert the sum to scaling factor for [0, 2pi]. + // TODO Allow start and end angle to be specified. + // TODO Allow padding to be specified as percentage? + k = (2 * Math.PI - padding * n) / k; + + // Compute the start and end angle for each group and subgroup. + x = 0, i = -1; while (++i < n) { + x0 = x, j = -1; while (++j < n) { + var di = groupIndex[i], + dj = subgroupIndex[i][j], + v = matrix[di][dj]; + subgroups[di + "-" + dj] = { + index: di, + subindex: dj, + startAngle: x, + endAngle: x += v * k, + value: v + }; + } + groups.push({ + index: di, + startAngle: x0, + endAngle: x, + value: (x - x0) / k + }); + x += padding; + } + + // Generate chords for each (non-empty) subgroup-subgroup link. + i = -1; while (++i < n) { + j = i - 1; while (++j < n) { + var source = subgroups[i + "-" + j], + target = subgroups[j + "-" + i]; + if (source.value || target.value) { + chords.push(source.value < target.value + ? {source: target, target: source} + : {source: source, target: target}); + } + } + } + + if (sortChords) resort(); + } + + function resort() { + chords.sort(function(a, b) { + return sortChords(a.target.value, b.target.value); + }); + } + + chord.matrix = function(x) { + if (!arguments.length) return matrix; + n = (matrix = x) && matrix.length; + chords = groups = null; + return chord; + }; + + chord.padding = function(x) { + if (!arguments.length) return padding; + padding = x; + chords = groups = null; + return chord; + }; + + chord.sortGroups = function(x) { + if (!arguments.length) return sortGroups; + sortGroups = x; + chords = groups = null; + return chord; + }; + + chord.sortSubgroups = function(x) { + if (!arguments.length) return sortSubgroups; + sortSubgroups = x; + chords = null; + return chord; + }; + + chord.sortChords = function(x) { + if (!arguments.length) return sortChords; + sortChords = x; + if (chords) resort(); + return chord; + }; + + chord.chords = function() { + if (!chords) relayout(); + return chords; + }; + + chord.groups = function() { + if (!groups) relayout(); + return groups; + }; + + return chord; + }; +// A rudimentary force layout using Gauss-Seidel. + d3.layout.force = function() { + var force = {}, + event = d3.dispatch("tick"), + size = [1, 1], + drag, + alpha, + friction = .9, + linkDistance = d3_layout_forceLinkDistance, + linkStrength = d3_layout_forceLinkStrength, + charge = -30, + gravity = .1, + theta = .8, + interval, + nodes = [], + links = [], + distances, + strengths, + charges; + + function repulse(node) { + return function(quad, x1, y1, x2, y2) { + if (quad.point !== node) { + var dx = quad.cx - node.x, + dy = quad.cy - node.y, + dn = 1 / Math.sqrt(dx * dx + dy * dy); + + /* Barnes-Hut criterion. */ + if ((x2 - x1) * dn < theta) { + var k = quad.charge * dn * dn; + node.px -= dx * k; + node.py -= dy * k; + return true; + } + + if (quad.point && isFinite(dn)) { + var k = quad.pointCharge * dn * dn; + node.px -= dx * k; + node.py -= dy * k; + } + } + return !quad.charge; + }; + } + + function tick() { + var n = nodes.length, + m = links.length, + q, + i, // current index + o, // current object + s, // current source + t, // current target + l, // current distance + k, // current force + x, // x-distance + y; // y-distance + + // gauss-seidel relaxation for links + for (i = 0; i < m; ++i) { + o = links[i]; + s = o.source; + t = o.target; + x = t.x - s.x; + y = t.y - s.y; + if (l = (x * x + y * y)) { + l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l; + x *= l; + y *= l; + t.x -= x * (k = s.weight / (t.weight + s.weight)); + t.y -= y * k; + s.x += x * (k = 1 - k); + s.y += y * k; + } + } + + // apply gravity forces + if (k = alpha * gravity) { + x = size[0] / 2; + y = size[1] / 2; + i = -1; if (k) while (++i < n) { + o = nodes[i]; + o.x += (x - o.x) * k; + o.y += (y - o.y) * k; + } + } + + // compute quadtree center of mass and apply charge forces + if (charge) { + d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges); + i = -1; while (++i < n) { + if (!(o = nodes[i]).fixed) { + q.visit(repulse(o)); + } + } + } + + // position verlet integration + i = -1; while (++i < n) { + o = nodes[i]; + if (o.fixed) { + o.x = o.px; + o.y = o.py; + } else { + o.x -= (o.px - (o.px = o.x)) * friction; + o.y -= (o.py - (o.py = o.y)) * friction; + } + } + + event.tick.dispatch({type: "tick", alpha: alpha}); + + // simulated annealing, basically + return (alpha *= .99) < .005; + } + + force.on = function(type, listener) { + event[type].add(listener); + return force; + }; + + force.nodes = function(x) { + if (!arguments.length) return nodes; + nodes = x; + return force; + }; + + force.links = function(x) { + if (!arguments.length) return links; + links = x; + return force; + }; + + force.size = function(x) { + if (!arguments.length) return size; + size = x; + return force; + }; + + force.linkDistance = function(x) { + if (!arguments.length) return linkDistance; + linkDistance = d3.functor(x); + return force; + }; + + // For backwards-compatibility. + force.distance = force.linkDistance; + + force.linkStrength = function(x) { + if (!arguments.length) return linkStrength; + linkStrength = d3.functor(x); + return force; + }; + + force.friction = function(x) { + if (!arguments.length) return friction; + friction = x; + return force; + }; + + force.charge = function(x) { + if (!arguments.length) return charge; + charge = typeof x === "function" ? x : +x; + return force; + }; + + force.gravity = function(x) { + if (!arguments.length) return gravity; + gravity = x; + return force; + }; + + force.theta = function(x) { + if (!arguments.length) return theta; + theta = x; + return force; + }; + + force.start = function() { + var i, + j, + n = nodes.length, + m = links.length, + w = size[0], + h = size[1], + neighbors, + o; + + for (i = 0; i < n; ++i) { + (o = nodes[i]).index = i; + o.weight = 0; + } + + distances = []; + strengths = []; + for (i = 0; i < m; ++i) { + o = links[i]; + if (typeof o.source == "number") o.source = nodes[o.source]; + if (typeof o.target == "number") o.target = nodes[o.target]; + distances[i] = linkDistance.call(this, o, i); + strengths[i] = linkStrength.call(this, o, i); + ++o.source.weight; + ++o.target.weight; + } + + for (i = 0; i < n; ++i) { + o = nodes[i]; + if (isNaN(o.x)) o.x = position("x", w); + if (isNaN(o.y)) o.y = position("y", h); + if (isNaN(o.px)) o.px = o.x; + if (isNaN(o.py)) o.py = o.y; + } + + charges = []; + if (typeof charge === "function") { + for (i = 0; i < n; ++i) { + charges[i] = +charge.call(this, nodes[i], i); + } + } else { + for (i = 0; i < n; ++i) { + charges[i] = charge; + } + } + + // initialize node position based on first neighbor + function position(dimension, size) { + var neighbors = neighbor(i), + j = -1, + m = neighbors.length, + x; + while (++j < m) if (!isNaN(x = neighbors[j][dimension])) return x; + return Math.random() * size; + } + + // initialize neighbors lazily + function neighbor() { + if (!neighbors) { + neighbors = []; + for (j = 0; j < n; ++j) { + neighbors[j] = []; + } + for (j = 0; j < m; ++j) { + var o = links[j]; + neighbors[o.source.index].push(o.target); + neighbors[o.target.index].push(o.source); + } + } + return neighbors[i]; + } + + return force.resume(); + }; + + force.resume = function() { + alpha = .1; + d3.timer(tick); + return force; + }; + + force.stop = function() { + alpha = 0; + return force; + }; + + // use `node.call(force.drag)` to make nodes draggable + force.drag = function() { + if (!drag) drag = d3.behavior.drag() + .on("dragstart", dragstart) + .on("drag", d3_layout_forceDrag) + .on("dragend", d3_layout_forceDragEnd); + + this.on("mouseover.force", d3_layout_forceDragOver) + .on("mouseout.force", d3_layout_forceDragOut) + .call(drag); + }; + + function dragstart(d) { + d3_layout_forceDragOver(d3_layout_forceDragNode = d); + d3_layout_forceDragForce = force; + } + + return force; + }; + + var d3_layout_forceDragForce, + d3_layout_forceDragNode; + + function d3_layout_forceDragOver(d) { + d.fixed |= 2; + } + + function d3_layout_forceDragOut(d) { + if (d !== d3_layout_forceDragNode) d.fixed &= 1; + } + + function d3_layout_forceDragEnd() { + d3_layout_forceDrag(); + d3_layout_forceDragNode.fixed &= 1; + d3_layout_forceDragForce = d3_layout_forceDragNode = null; + } + + function d3_layout_forceDrag() { + d3_layout_forceDragNode.px += d3.event.dx; + d3_layout_forceDragNode.py += d3.event.dy; + d3_layout_forceDragForce.resume(); // restart annealing + } + + function d3_layout_forceAccumulate(quad, alpha, charges) { + var cx = 0, + cy = 0; + quad.charge = 0; + if (!quad.leaf) { + var nodes = quad.nodes, + n = nodes.length, + i = -1, + c; + while (++i < n) { + c = nodes[i]; + if (c == null) continue; + d3_layout_forceAccumulate(c, alpha, charges); + quad.charge += c.charge; + cx += c.charge * c.cx; + cy += c.charge * c.cy; + } + } + if (quad.point) { + // jitter internal nodes that are coincident + if (!quad.leaf) { + quad.point.x += Math.random() - .5; + quad.point.y += Math.random() - .5; + } + var k = alpha * charges[quad.point.index]; + quad.charge += quad.pointCharge = k; + cx += k * quad.point.x; + cy += k * quad.point.y; + } + quad.cx = cx / quad.charge; + quad.cy = cy / quad.charge; + } + + function d3_layout_forceLinkDistance(link) { + return 20; + } + + function d3_layout_forceLinkStrength(link) { + return 1; + } + d3.layout.partition = function() { + var hierarchy = d3.layout.hierarchy(), + size = [1, 1]; // width, height + + function position(node, x, dx, dy) { + var children = node.children; + node.x = x; + node.y = node.depth * dy; + node.dx = dx; + node.dy = dy; + if (children && (n = children.length)) { + var i = -1, + n, + c, + d; + dx = node.value ? dx / node.value : 0; + while (++i < n) { + position(c = children[i], x, d = c.value * dx, dy); + x += d; + } + } + } + + function depth(node) { + var children = node.children, + d = 0; + if (children && (n = children.length)) { + var i = -1, + n; + while (++i < n) d = Math.max(d, depth(children[i])); + } + return 1 + d; + } + + function partition(d, i) { + var nodes = hierarchy.call(this, d, i); + position(nodes[0], 0, size[0], size[1] / depth(nodes[0])); + return nodes; + } + + partition.size = function(x) { + if (!arguments.length) return size; + size = x; + return partition; + }; + + return d3_layout_hierarchyRebind(partition, hierarchy); + }; + d3.layout.pie = function() { + var value = Number, + sort = null, + startAngle = 0, + endAngle = 2 * Math.PI; + + function pie(data, i) { + + // Compute the start angle. + var a = +(typeof startAngle === "function" + ? startAngle.apply(this, arguments) + : startAngle); + + // Compute the angular range (end - start). + var k = (typeof endAngle === "function" + ? endAngle.apply(this, arguments) + : endAngle) - startAngle; + + // Optionally sort the data. + var index = d3.range(data.length); + if (sort != null) index.sort(function(i, j) { + return sort(data[i], data[j]); + }); + + // Compute the numeric values for each data element. + var values = data.map(value); + + // Convert k into a scale factor from value to angle, using the sum. + k /= values.reduce(function(p, d) { return p + d; }, 0); + + // Compute the arcs! + var arcs = index.map(function(i) { + return { + data: data[i], + value: d = values[i], + startAngle: a, + endAngle: a += d * k + }; + }); + + // Return the arcs in the original data's order. + return data.map(function(d, i) { + return arcs[index[i]]; + }); + } + + /** + * Specifies the value function *x*, which returns a nonnegative numeric value + * for each datum. The default value function is `Number`. The value function + * is passed two arguments: the current datum and the current index. + */ + pie.value = function(x) { + if (!arguments.length) return value; + value = x; + return pie; + }; + + /** + * Specifies a sort comparison operator *x*. The comparator is passed two data + * elements from the data array, a and b; it returns a negative value if a is + * less than b, a positive value if a is greater than b, and zero if a equals + * b. + */ + pie.sort = function(x) { + if (!arguments.length) return sort; + sort = x; + return pie; + }; + + /** + * Specifies the overall start angle of the pie chart. Defaults to 0. The + * start angle can be specified either as a constant or as a function; in the + * case of a function, it is evaluated once per array (as opposed to per + * element). + */ + pie.startAngle = function(x) { + if (!arguments.length) return startAngle; + startAngle = x; + return pie; + }; + + /** + * Specifies the overall end angle of the pie chart. Defaults to 2π. The + * end angle can be specified either as a constant or as a function; in the + * case of a function, it is evaluated once per array (as opposed to per + * element). + */ + pie.endAngle = function(x) { + if (!arguments.length) return endAngle; + endAngle = x; + return pie; + }; + + return pie; + }; +// data is two-dimensional array of x,y; we populate y0 + d3.layout.stack = function() { + var values = Object, + order = d3_layout_stackOrders["default"], + offset = d3_layout_stackOffsets["zero"], + out = d3_layout_stackOut, + x = d3_layout_stackX, + y = d3_layout_stackY; + + function stack(data, index) { + + // Convert series to canonical two-dimensional representation. + var series = data.map(function(d, i) { + return values.call(stack, d, i); + }); + + // Convert each series to canonical [[x,y]] representation. + var points = series.map(function(d, i) { + return d.map(function(v, i) { + return [x.call(stack, v, i), y.call(stack, v, i)]; + }); + }); + + // Compute the order of series, and permute them. + var orders = order.call(stack, points, index); + series = d3.permute(series, orders); + points = d3.permute(points, orders); + + // Compute the baseline… + var offsets = offset.call(stack, points, index); + + // And propagate it to other series. + var n = series.length, + m = series[0].length, + i, + j, + o; + for (j = 0; j < m; ++j) { + out.call(stack, series[0][j], o = offsets[j], points[0][j][1]); + for (i = 1; i < n; ++i) { + out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]); + } + } + + return data; + } + + stack.values = function(x) { + if (!arguments.length) return values; + values = x; + return stack; + }; + + stack.order = function(x) { + if (!arguments.length) return order; + order = typeof x === "function" ? x : d3_layout_stackOrders[x]; + return stack; + }; + + stack.offset = function(x) { + if (!arguments.length) return offset; + offset = typeof x === "function" ? x : d3_layout_stackOffsets[x]; + return stack; + }; + + stack.x = function(z) { + if (!arguments.length) return x; + x = z; + return stack; + }; + + stack.y = function(z) { + if (!arguments.length) return y; + y = z; + return stack; + }; + + stack.out = function(z) { + if (!arguments.length) return out; + out = z; + return stack; + }; + + return stack; + } + + function d3_layout_stackX(d) { + return d.x; + } + + function d3_layout_stackY(d) { + return d.y; + } + + function d3_layout_stackOut(d, y0, y) { + d.y0 = y0; + d.y = y; + } + + var d3_layout_stackOrders = { + + "inside-out": function(data) { + var n = data.length, + i, + j, + max = data.map(d3_layout_stackMaxIndex), + sums = data.map(d3_layout_stackReduceSum), + index = d3.range(n).sort(function(a, b) { return max[a] - max[b]; }), + top = 0, + bottom = 0, + tops = [], + bottoms = []; + for (i = 0; i < n; ++i) { + j = index[i]; + if (top < bottom) { + top += sums[j]; + tops.push(j); + } else { + bottom += sums[j]; + bottoms.push(j); + } + } + return bottoms.reverse().concat(tops); + }, + + "reverse": function(data) { + return d3.range(data.length).reverse(); + }, + + "default": function(data) { + return d3.range(data.length); + } + + }; + + var d3_layout_stackOffsets = { + + "silhouette": function(data) { + var n = data.length, + m = data[0].length, + sums = [], + max = 0, + i, + j, + o, + y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o > max) max = o; + sums.push(o); + } + for (j = 0; j < m; ++j) { + y0[j] = (max - sums[j]) / 2; + } + return y0; + }, + + "wiggle": function(data) { + var n = data.length, + x = data[0], + m = x.length, + max = 0, + i, + j, + k, + s1, + s2, + s3, + dx, + o, + o0, + y0 = []; + y0[0] = o = o0 = 0; + for (j = 1; j < m; ++j) { + for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1]; + for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) { + for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) { + s3 += (data[k][j][1] - data[k][j - 1][1]) / dx; + } + s2 += s3 * data[i][j][1]; + } + y0[j] = o -= s1 ? s2 / s1 * dx : 0; + if (o < o0) o0 = o; + } + for (j = 0; j < m; ++j) y0[j] -= o0; + return y0; + }, + + "expand": function(data) { + var n = data.length, + m = data[0].length, + k = 1 / n, + i, + j, + o, + y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; + else for (i = 0; i < n; i++) data[i][j][1] = k; + } + for (j = 0; j < m; ++j) y0[j] = 0; + return y0; + }, + + "zero": function(data) { + var j = -1, + m = data[0].length, + y0 = []; + while (++j < m) y0[j] = 0; + return y0; + } + + }; + + function d3_layout_stackMaxIndex(array) { + var i = 1, + j = 0, + v = array[0][1], + k, + n = array.length; + for (; i < n; ++i) { + if ((k = array[i][1]) > v) { + j = i; + v = k; + } + } + return j; + } + + function d3_layout_stackReduceSum(d) { + return d.reduce(d3_layout_stackSum, 0); + } + + function d3_layout_stackSum(p, d) { + return p + d[1]; + } + d3.layout.histogram = function() { + var frequency = true, + valuer = Number, + ranger = d3_layout_histogramRange, + binner = d3_layout_histogramBinSturges; + + function histogram(data, i) { + var bins = [], + values = data.map(valuer, this), + range = ranger.call(this, values, i), + thresholds = binner.call(this, range, values, i), + bin, + i = -1, + n = values.length, + m = thresholds.length - 1, + k = frequency ? 1 : 1 / n, + x; + + // Initialize the bins. + while (++i < m) { + bin = bins[i] = []; + bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]); + bin.y = 0; + } + + // Fill the bins, ignoring values outside the range. + i = -1; while(++i < n) { + x = values[i]; + if ((x >= range[0]) && (x <= range[1])) { + bin = bins[d3.bisect(thresholds, x, 1, m) - 1]; + bin.y += k; + bin.push(data[i]); + } + } + + return bins; + } + + // Specifies how to extract a value from the associated data. The default + // value function is `Number`, which is equivalent to the identity function. + histogram.value = function(x) { + if (!arguments.length) return valuer; + valuer = x; + return histogram; + }; + + // Specifies the range of the histogram. Values outside the specified range + // will be ignored. The argument `x` may be specified either as a two-element + // array representing the minimum and maximum value of the range, or as a + // function that returns the range given the array of values and the current + // index `i`. The default range is the extent (minimum and maximum) of the + // values. + histogram.range = function(x) { + if (!arguments.length) return ranger; + ranger = d3.functor(x); + return histogram; + }; + + // Specifies how to bin values in the histogram. The argument `x` may be + // specified as a number, in which case the range of values will be split + // uniformly into the given number of bins. Or, `x` may be an array of + // threshold values, defining the bins; the specified array must contain the + // rightmost (upper) value, thus specifying n + 1 values for n bins. Or, `x` + // may be a function which is evaluated, being passed the range, the array of + // values, and the current index `i`, returning an array of thresholds. The + // default bin function will divide the values into uniform bins using + // Sturges' formula. + histogram.bins = function(x) { + if (!arguments.length) return binner; + binner = typeof x === "number" + ? function(range) { return d3_layout_histogramBinFixed(range, x); } + : d3.functor(x); + return histogram; + }; + + // Specifies whether the histogram's `y` value is a count (frequency) or a + // probability (density). The default value is true. + histogram.frequency = function(x) { + if (!arguments.length) return frequency; + frequency = !!x; + return histogram; + }; + + return histogram; + }; + + function d3_layout_histogramBinSturges(range, values) { + return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1)); + } + + function d3_layout_histogramBinFixed(range, n) { + var x = -1, + b = +range[0], + m = (range[1] - b) / n, + f = []; + while (++x <= n) f[x] = m * x + b; + return f; + } + + function d3_layout_histogramRange(values) { + return [d3.min(values), d3.max(values)]; + } + d3.layout.hierarchy = function() { + var sort = d3_layout_hierarchySort, + children = d3_layout_hierarchyChildren, + value = d3_layout_hierarchyValue; + + // Recursively compute the node depth and value. + // Also converts the data representation into a standard hierarchy structure. + function recurse(data, depth, nodes) { + var childs = children.call(hierarchy, data, depth), + node = d3_layout_hierarchyInline ? data : {data: data}; + node.depth = depth; + nodes.push(node); + if (childs && (n = childs.length)) { + var i = -1, + n, + c = node.children = [], + v = 0, + j = depth + 1; + while (++i < n) { + d = recurse(childs[i], j, nodes); + d.parent = node; + c.push(d); + v += d.value; + } + if (sort) c.sort(sort); + if (value) node.value = v; + } else if (value) { + node.value = +value.call(hierarchy, data, depth) || 0; + } + return node; + } + + // Recursively re-evaluates the node value. + function revalue(node, depth) { + var children = node.children, + v = 0; + if (children && (n = children.length)) { + var i = -1, + n, + j = depth + 1; + while (++i < n) v += revalue(children[i], j); + } else if (value) { + v = +value.call(hierarchy, d3_layout_hierarchyInline ? node : node.data, depth) || 0; + } + if (value) node.value = v; + return v; + } + + function hierarchy(d) { + var nodes = []; + recurse(d, 0, nodes); + return nodes; + } + + hierarchy.sort = function(x) { + if (!arguments.length) return sort; + sort = x; + return hierarchy; + }; + + hierarchy.children = function(x) { + if (!arguments.length) return children; + children = x; + return hierarchy; + }; + + hierarchy.value = function(x) { + if (!arguments.length) return value; + value = x; + return hierarchy; + }; + + // Re-evaluates the `value` property for the specified hierarchy. + hierarchy.revalue = function(root) { + revalue(root, 0); + return root; + }; + + return hierarchy; + }; + +// A method assignment helper for hierarchy subclasses. + function d3_layout_hierarchyRebind(object, hierarchy) { + object.sort = d3.rebind(object, hierarchy.sort); + object.children = d3.rebind(object, hierarchy.children); + object.links = d3_layout_hierarchyLinks; + object.value = d3.rebind(object, hierarchy.value); + + // If the new API is used, enabling inlining. + object.nodes = function(d) { + d3_layout_hierarchyInline = true; + return (object.nodes = object)(d); + }; + + return object; + } + + function d3_layout_hierarchyChildren(d) { + return d.children; + } + + function d3_layout_hierarchyValue(d) { + return d.value; + } + + function d3_layout_hierarchySort(a, b) { + return b.value - a.value; + } + +// Returns an array source+target objects for the specified nodes. + function d3_layout_hierarchyLinks(nodes) { + return d3.merge(nodes.map(function(parent) { + return (parent.children || []).map(function(child) { + return {source: parent, target: child}; + }); + })); + } + +// For backwards-compatibility, don't enable inlining by default. + var d3_layout_hierarchyInline = false; + d3.layout.pack = function() { + var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), + size = [1, 1]; + + function pack(d, i) { + var nodes = hierarchy.call(this, d, i), + root = nodes[0]; + + // Recursively compute the layout. + root.x = 0; + root.y = 0; + d3_layout_packTree(root); + + // Scale the layout to fit the requested size. + var w = size[0], + h = size[1], + k = 1 / Math.max(2 * root.r / w, 2 * root.r / h); + d3_layout_packTransform(root, w / 2, h / 2, k); + + return nodes; + } + + pack.size = function(x) { + if (!arguments.length) return size; + size = x; + return pack; + }; + + return d3_layout_hierarchyRebind(pack, hierarchy); + }; + + function d3_layout_packSort(a, b) { + return a.value - b.value; + } + + function d3_layout_packInsert(a, b) { + var c = a._pack_next; + a._pack_next = b; + b._pack_prev = a; + b._pack_next = c; + c._pack_prev = b; + } + + function d3_layout_packSplice(a, b) { + a._pack_next = b; + b._pack_prev = a; + } + + function d3_layout_packIntersects(a, b) { + var dx = b.x - a.x, + dy = b.y - a.y, + dr = a.r + b.r; + return (dr * dr - dx * dx - dy * dy) > .001; // within epsilon + } + + function d3_layout_packCircle(nodes) { + var xMin = Infinity, + xMax = -Infinity, + yMin = Infinity, + yMax = -Infinity, + n = nodes.length, + a, b, c, j, k; + + function bound(node) { + xMin = Math.min(node.x - node.r, xMin); + xMax = Math.max(node.x + node.r, xMax); + yMin = Math.min(node.y - node.r, yMin); + yMax = Math.max(node.y + node.r, yMax); + } + + // Create node links. + nodes.forEach(d3_layout_packLink); + + // Create first node. + a = nodes[0]; + a.x = -a.r; + a.y = 0; + bound(a); + + // Create second node. + if (n > 1) { + b = nodes[1]; + b.x = b.r; + b.y = 0; + bound(b); + + // Create third node and build chain. + if (n > 2) { + c = nodes[2]; + d3_layout_packPlace(a, b, c); + bound(c); + d3_layout_packInsert(a, c); + a._pack_prev = c; + d3_layout_packInsert(c, b); + b = a._pack_next; + + // Now iterate through the rest. + for (var i = 3; i < n; i++) { + d3_layout_packPlace(a, b, c = nodes[i]); + + // Search for the closest intersection. + var isect = 0, s1 = 1, s2 = 1; + for (j = b._pack_next; j !== b; j = j._pack_next, s1++) { + if (d3_layout_packIntersects(j, c)) { + isect = 1; + break; + } + } + if (isect == 1) { + for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) { + if (d3_layout_packIntersects(k, c)) { + if (s2 < s1) { + isect = -1; + j = k; + } + break; + } + } + } + + // Update node chain. + if (isect == 0) { + d3_layout_packInsert(a, c); + b = c; + bound(c); + } else if (isect > 0) { + d3_layout_packSplice(a, j); + b = j; + i--; + } else { // isect < 0 + d3_layout_packSplice(j, b); + a = j; + i--; + } + } + } + } + + // Re-center the circles and return the encompassing radius. + var cx = (xMin + xMax) / 2, + cy = (yMin + yMax) / 2, + cr = 0; + for (var i = 0; i < n; i++) { + var node = nodes[i]; + node.x -= cx; + node.y -= cy; + cr = Math.max(cr, node.r + Math.sqrt(node.x * node.x + node.y * node.y)); + } + + // Remove node links. + nodes.forEach(d3_layout_packUnlink); + + return cr; + } + + function d3_layout_packLink(node) { + node._pack_next = node._pack_prev = node; + } + + function d3_layout_packUnlink(node) { + delete node._pack_next; + delete node._pack_prev; + } + + function d3_layout_packTree(node) { + var children = node.children; + if (children && children.length) { + children.forEach(d3_layout_packTree); + node.r = d3_layout_packCircle(children); + } else { + node.r = Math.sqrt(node.value); + } + } + + function d3_layout_packTransform(node, x, y, k) { + var children = node.children; + node.x = (x += k * node.x); + node.y = (y += k * node.y); + node.r *= k; + if (children) { + var i = -1, n = children.length; + while (++i < n) d3_layout_packTransform(children[i], x, y, k); + } + } + + function d3_layout_packPlace(a, b, c) { + var db = a.r + c.r, + dx = b.x - a.x, + dy = b.y - a.y; + if (db && (dx || dy)) { + var da = b.r + c.r, + dc = Math.sqrt(dx * dx + dy * dy), + cos = Math.max(-1, Math.min(1, (db * db + dc * dc - da * da) / (2 * db * dc))), + theta = Math.acos(cos), + x = cos * (db /= dc), + y = Math.sin(theta) * db; + c.x = a.x + x * dx + y * dy; + c.y = a.y + x * dy - y * dx; + } else { + c.x = a.x + db; + c.y = a.y; + } + } +// Implements a hierarchical layout using the cluster (or dendogram) algorithm. + d3.layout.cluster = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), + separation = d3_layout_treeSeparation, + size = [1, 1]; // width, height + + function cluster(d, i) { + var nodes = hierarchy.call(this, d, i), + root = nodes[0], + previousNode, + x = 0, + kx, + ky; + + // First walk, computing the initial x & y values. + d3_layout_treeVisitAfter(root, function(node) { + var children = node.children; + if (children && children.length) { + node.x = d3_layout_clusterX(children); + node.y = d3_layout_clusterY(children); + } else { + node.x = previousNode ? x += separation(node, previousNode) : 0; + node.y = 0; + previousNode = node; + } + }); + + // Compute the left-most, right-most, and depth-most nodes for extents. + var left = d3_layout_clusterLeft(root), + right = d3_layout_clusterRight(root), + x0 = left.x - separation(left, right) / 2, + x1 = right.x + separation(right, left) / 2; + + // Second walk, normalizing x & y to the desired size. + d3_layout_treeVisitAfter(root, function(node) { + node.x = (node.x - x0) / (x1 - x0) * size[0]; + node.y = (1 - node.y / root.y) * size[1]; + }); + + return nodes; + } + + cluster.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return cluster; + }; + + cluster.size = function(x) { + if (!arguments.length) return size; + size = x; + return cluster; + }; + + return d3_layout_hierarchyRebind(cluster, hierarchy); + }; + + function d3_layout_clusterY(children) { + return 1 + d3.max(children, function(child) { + return child.y; + }); + } + + function d3_layout_clusterX(children) { + return children.reduce(function(x, child) { + return x + child.x; + }, 0) / children.length; + } + + function d3_layout_clusterLeft(node) { + var children = node.children; + return children && children.length ? d3_layout_clusterLeft(children[0]) : node; + } + + function d3_layout_clusterRight(node) { + var children = node.children, n; + return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node; + } +// Node-link tree diagram using the Reingold-Tilford "tidy" algorithm + d3.layout.tree = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), + separation = d3_layout_treeSeparation, + size = [1, 1]; // width, height + + function tree(d, i) { + var nodes = hierarchy.call(this, d, i), + root = nodes[0]; + + function firstWalk(node, previousSibling) { + var children = node.children, + layout = node._tree; + if (children && (n = children.length)) { + var n, + firstChild = children[0], + previousChild, + ancestor = firstChild, + child, + i = -1; + while (++i < n) { + child = children[i]; + firstWalk(child, previousChild); + ancestor = apportion(child, previousChild, ancestor); + previousChild = child; + } + d3_layout_treeShift(node); + var midpoint = .5 * (firstChild._tree.prelim + child._tree.prelim); + if (previousSibling) { + layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling); + layout.mod = layout.prelim - midpoint; + } else { + layout.prelim = midpoint; + } + } else { + if (previousSibling) { + layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling); + } + } + } + + function secondWalk(node, x) { + node.x = node._tree.prelim + x; + var children = node.children; + if (children && (n = children.length)) { + var i = -1, + n; + x += node._tree.mod; + while (++i < n) { + secondWalk(children[i], x); + } + } + } + + function apportion(node, previousSibling, ancestor) { + if (previousSibling) { + var vip = node, + vop = node, + vim = previousSibling, + vom = node.parent.children[0], + sip = vip._tree.mod, + sop = vop._tree.mod, + sim = vim._tree.mod, + som = vom._tree.mod, + shift; + while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) { + vom = d3_layout_treeLeft(vom); + vop = d3_layout_treeRight(vop); + vop._tree.ancestor = node; + shift = vim._tree.prelim + sim - vip._tree.prelim - sip + separation(vim, vip); + if (shift > 0) { + d3_layout_treeMove(d3_layout_treeAncestor(vim, node, ancestor), node, shift); + sip += shift; + sop += shift; + } + sim += vim._tree.mod; + sip += vip._tree.mod; + som += vom._tree.mod; + sop += vop._tree.mod; + } + if (vim && !d3_layout_treeRight(vop)) { + vop._tree.thread = vim; + vop._tree.mod += sim - sop; + } + if (vip && !d3_layout_treeLeft(vom)) { + vom._tree.thread = vip; + vom._tree.mod += sip - som; + ancestor = node; + } + } + return ancestor; + } + + // Initialize temporary layout variables. + d3_layout_treeVisitAfter(root, function(node, previousSibling) { + node._tree = { + ancestor: node, + prelim: 0, + mod: 0, + change: 0, + shift: 0, + number: previousSibling ? previousSibling._tree.number + 1 : 0 + }; + }); + + // Compute the layout using Buchheim et al.'s algorithm. + firstWalk(root); + secondWalk(root, -root._tree.prelim); + + // Compute the left-most, right-most, and depth-most nodes for extents. + var left = d3_layout_treeSearch(root, d3_layout_treeLeftmost), + right = d3_layout_treeSearch(root, d3_layout_treeRightmost), + deep = d3_layout_treeSearch(root, d3_layout_treeDeepest), + x0 = left.x - separation(left, right) / 2, + x1 = right.x + separation(right, left) / 2, + y1 = deep.depth || 1; + + // Clear temporary layout variables; transform x and y. + d3_layout_treeVisitAfter(root, function(node) { + node.x = (node.x - x0) / (x1 - x0) * size[0]; + node.y = node.depth / y1 * size[1]; + delete node._tree; + }); + + return nodes; + } + + tree.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return tree; + }; + + tree.size = function(x) { + if (!arguments.length) return size; + size = x; + return tree; + }; + + return d3_layout_hierarchyRebind(tree, hierarchy); + }; + + function d3_layout_treeSeparation(a, b) { + return a.parent == b.parent ? 1 : 2; + } + +// function d3_layout_treeSeparationRadial(a, b) { +// return (a.parent == b.parent ? 1 : 2) / a.depth; +// } + + function d3_layout_treeLeft(node) { + var children = node.children; + return children && children.length ? children[0] : node._tree.thread; + } + + function d3_layout_treeRight(node) { + var children = node.children, + n; + return children && (n = children.length) ? children[n - 1] : node._tree.thread; + } + + function d3_layout_treeSearch(node, compare) { + var children = node.children; + if (children && (n = children.length)) { + var child, + n, + i = -1; + while (++i < n) { + if (compare(child = d3_layout_treeSearch(children[i], compare), node) > 0) { + node = child; + } + } + } + return node; + } + + function d3_layout_treeRightmost(a, b) { + return a.x - b.x; + } + + function d3_layout_treeLeftmost(a, b) { + return b.x - a.x; + } + + function d3_layout_treeDeepest(a, b) { + return a.depth - b.depth; + } + + function d3_layout_treeVisitAfter(node, callback) { + function visit(node, previousSibling) { + var children = node.children; + if (children && (n = children.length)) { + var child, + previousChild = null, + i = -1, + n; + while (++i < n) { + child = children[i]; + visit(child, previousChild); + previousChild = child; + } + } + callback(node, previousSibling); + } + visit(node, null); + } + + function d3_layout_treeShift(node) { + var shift = 0, + change = 0, + children = node.children, + i = children.length, + child; + while (--i >= 0) { + child = children[i]._tree; + child.prelim += shift; + child.mod += shift; + shift += child.shift + (change += child.change); + } + } + + function d3_layout_treeMove(ancestor, node, shift) { + ancestor = ancestor._tree; + node = node._tree; + var change = shift / (node.number - ancestor.number); + ancestor.change += change; + node.change -= change; + node.shift += shift; + node.prelim += shift; + node.mod += shift; + } + + function d3_layout_treeAncestor(vim, node, ancestor) { + return vim._tree.ancestor.parent == node.parent + ? vim._tree.ancestor + : ancestor; + } +// Squarified Treemaps by Mark Bruls, Kees Huizing, and Jarke J. van Wijk +// Modified to support a target aspect ratio by Jeff Heer + d3.layout.treemap = function() { + var hierarchy = d3.layout.hierarchy(), + round = Math.round, + size = [1, 1], // width, height + padding = null, + pad = d3_layout_treemapPadNull, + sticky = false, + stickies, + ratio = 0.5 * (1 + Math.sqrt(5)); // golden ratio + + // Compute the area for each child based on value & scale. + function scale(children, k) { + var i = -1, + n = children.length, + child, + area; + while (++i < n) { + area = (child = children[i]).value * (k < 0 ? 0 : k); + child.area = isNaN(area) || area <= 0 ? 0 : area; + } + } + + // Recursively arranges the specified node's children into squarified rows. + function squarify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), + row = [], + remaining = children.slice(), // copy-on-write + child, + best = Infinity, // the best row score so far + score, // the current row score + u = Math.min(rect.dx, rect.dy), // initial orientation + n; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while ((n = remaining.length) > 0) { + row.push(child = remaining[n - 1]); + row.area += child.area; + if ((score = worst(row, u)) <= best) { // continue with this orientation + remaining.pop(); + best = score; + } else { // abort, and try a different orientation + row.area -= row.pop().area; + position(row, u, rect, false); + u = Math.min(rect.dx, rect.dy); + row.length = row.area = 0; + best = Infinity; + } + } + if (row.length) { + position(row, u, rect, true); + row.length = row.area = 0; + } + children.forEach(squarify); + } + } + + // Recursively resizes the specified node's children into existing rows. + // Preserves the existing layout! + function stickify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), + remaining = children.slice(), // copy-on-write + child, + row = []; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while (child = remaining.pop()) { + row.push(child); + row.area += child.area; + if (child.z != null) { + position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length); + row.length = row.area = 0; + } + } + children.forEach(stickify); + } + } + + // Computes the score for the specified row, as the worst aspect ratio. + function worst(row, u) { + var s = row.area, + r, + rmax = 0, + rmin = Infinity, + i = -1, + n = row.length; + while (++i < n) { + if (!(r = row[i].area)) continue; + if (r < rmin) rmin = r; + if (r > rmax) rmax = r; + } + s *= s; + u *= u; + return s + ? Math.max((u * rmax * ratio) / s, s / (u * rmin * ratio)) + : Infinity; + } + + // Positions the specified row of nodes. Modifies `rect`. + function position(row, u, rect, flush) { + var i = -1, + n = row.length, + x = rect.x, + y = rect.y, + v = u ? round(row.area / u) : 0, + o; + if (u == rect.dx) { // horizontal subdivision + if (flush || v > rect.dy) v = v ? rect.dy : 0; // over+underflow + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dy = v; + x += o.dx = v ? round(o.area / v) : 0; + } + o.z = true; + o.dx += rect.x + rect.dx - x; // rounding error + rect.y += v; + rect.dy -= v; + } else { // vertical subdivision + if (flush || v > rect.dx) v = v ? rect.dx : 0; // over+underflow + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dx = v; + y += o.dy = v ? round(o.area / v) : 0; + } + o.z = false; + o.dy += rect.y + rect.dy - y; // rounding error + rect.x += v; + rect.dx -= v; + } + } + + function treemap(d) { + var nodes = stickies || hierarchy(d), + root = nodes[0]; + root.x = 0; + root.y = 0; + root.dx = size[0]; + root.dy = size[1]; + if (stickies) hierarchy.revalue(root); + scale([root], root.dx * root.dy / root.value); + (stickies ? stickify : squarify)(root); + if (sticky) stickies = nodes; + return nodes; + } + + treemap.size = function(x) { + if (!arguments.length) return size; + size = x; + return treemap; + }; + + treemap.padding = function(x) { + if (!arguments.length) return padding; + + function padFunction(node) { + var p = x.call(treemap, node, node.depth); + return p == null + ? d3_layout_treemapPadNull(node) + : d3_layout_treemapPad(node, typeof p === "number" ? [p, p, p, p] : p); + } + + function padConstant(node) { + return d3_layout_treemapPad(node, x); + } + + var type; + pad = (padding = x) == null ? d3_layout_treemapPadNull + : (type = typeof x) === "function" ? padFunction + : type === "number" ? (x = [x, x, x, x], padConstant) + : padConstant; + return treemap; + }; + + treemap.round = function(x) { + if (!arguments.length) return round != Number; + round = x ? Math.round : Number; + return treemap; + }; + + treemap.sticky = function(x) { + if (!arguments.length) return sticky; + sticky = x; + stickies = null; + return treemap; + }; + + treemap.ratio = function(x) { + if (!arguments.length) return ratio; + ratio = x; + return treemap; + }; + + return d3_layout_hierarchyRebind(treemap, hierarchy); + }; + + function d3_layout_treemapPadNull(node) { + return {x: node.x, y: node.y, dx: node.dx, dy: node.dy}; + } + + function d3_layout_treemapPad(node, padding) { + var x = node.x + padding[3], + y = node.y + padding[0], + dx = node.dx - padding[1] - padding[3], + dy = node.dy - padding[0] - padding[2]; + if (dx < 0) { x += dx / 2; dx = 0; } + if (dy < 0) { y += dy / 2; dy = 0; } + return {x: x, y: y, dx: dx, dy: dy}; + } +})(); diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css new file mode 100644 index 0000000000..e08a8f6ae8 --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css @@ -0,0 +1,89 @@ +body { + background: url(texture-noise.png); + overflow: hidden; + margin: 0; + font-size: 14px; + font-family: "Helvetica Neue", Helvetica; +} + +#chart, #header, #footer { + position: absolute; + top: 0; +} + +#header, #footer { + z-index: 1; + display: block; + font-size: 36px; + font-weight: 300; + text-shadow: 0 1px 0 #fff; +} + +#header.inverted, #footer.inverted { + color: #fff; + text-shadow: 0 1px 4px #000; +} + +#header { + top: 80px; + left: 140px; + width: 1000px; +} + +#footer { + top: 680px; + right: 140px; + text-align: right; +} + +rect { + fill: none; + pointer-events: all; +} + +pre { + font-size: 18px; +} + +line { + stroke: #000; + stroke-width: 1.5px; +} + +.string, .regexp { + color: #f39; +} + +.keyword { + color: #00c; +} + +.comment { + color: #777; + font-style: oblique; +} + +.number { + color: #369; +} + +.class, .special { + color: #1181B8; +} + +a:link, a:visited { + color: #000; + text-decoration: none; +} + +a:hover { + color: #666; +} + +.hint { + position: absolute; + right: 0; + width: 1280px; + font-size: 12px; + color: #999; +} diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/texture-noise.png b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/texture-noise.png new file mode 100644 index 0000000000000000000000000000000000000000..684f4469ae119b53a4a396d12a4f86fb7b6211d6 GIT binary patch literal 25768 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 B8&3cL literal 0 HcmV?d00001 diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy index 62337c6595..43a86f10c8 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod class DataModelUrlMappings { static mappings = { + "/dataModel/d3View"(view:"dataModel/d3_data_model_view") "/dataModel/showAssetInAngular/$id"(controller: 'dataModel', action: 'showAssetInAngular', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'index', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'save', method: HttpMethod.POST) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy index b763b13d02..3b51771715 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy @@ -663,6 +663,7 @@ class MetadataSecurityService { ] public static final List DATA_MODEL_MAPPINGS = [ + ["/dataModel/d3View", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], ["/dataModel/create", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], ["/dataModel/save", MetadataRoles.ROLE_CURATOR, HttpMethod.POST], ["/dataModel/showAssetInAngular/*", 'isAuthenticated()', HttpMethod.GET], diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp new file mode 100644 index 0000000000..bb84d35d5b --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -0,0 +1,191 @@ +<%-- + Created by IntelliJ IDEA. + User: james + Date: 16/05/2018 + Time: 17:11 +--%> + +<%@ page contentType="text/html;charset=UTF-8" %> + + + + + + + + + + + +
+ +
+ + + From f1a1e28790788be9e5bcf68ae3b74229e82a1356 Mon Sep 17 00:00:00 2001 From: James Dai Date: Wed, 16 May 2018 18:07:57 +0100 Subject: [PATCH 02/46] D3 view works with sample data --- .../javascripts/d3_data_model_view/init.js | 151 +++++++ .../conf/DataModelUrlMappings.groovy | 2 +- .../core/DataModelController.groovy | 384 ++++++++++++++++++ .../views/dataModel/d3_data_model_view.gsp | 146 +------ 4 files changed, 539 insertions(+), 144 deletions(-) create mode 100644 ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js new file mode 100644 index 0000000000..37a46f62ba --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -0,0 +1,151 @@ + +var m = [20, 120, 20, 120], + w = 1280 - m[1] - m[3], + h = 800 - m[0] - m[2], + i = 0, + root; + +var tree = d3.layout.tree() + .size([h, w]); + +var diagonal = d3.svg.diagonal() + .projection(function(d) { return [d.y, d.x]; }); + +var vis = d3.select("#body").append("svg:svg") + .attr("width", w + m[1] + m[3]) + .attr("height", h + m[0] + m[2]) + .append("svg:g") + .attr("transform", "translate(" + m[3] + "," + m[0] + ")"); + +function parseModelToJS(jsonString) { + jsonString=jsonString.replace(/\"/g,'"'); + jsonString=jsonString.replace(/"/g, '"'); + var jsonObject=$.parseJSON(jsonString); + return jsonObject +} + +// d3.json("flare.json", +function initD3(json) { + root = json; + root.x0 = h / 2; + root.y0 = 0; + + function toggleAll(d) { + if (d.children) { + d.children.forEach(toggleAll); + toggle(d); + } + } + + // Initialize the display to show a few nodes. + root.children.forEach(toggleAll); + toggle(root.children[1]); + toggle(root.children[1].children[2]); + toggle(root.children[9]); + toggle(root.children[9].children[0]); + + update(root); +}; + + + +function update(source) { + var duration = d3.event && d3.event.altKey ? 5000 : 500; + + // Compute the new tree layout. + var nodes = tree.nodes(root).reverse(); + + // Normalize for fixed-depth. + nodes.forEach(function(d) { d.y = d.depth * 180; }); + + // Update the nodes… + var node = vis.selectAll("g.node") + .data(nodes, function(d) { return d.id || (d.id = ++i); }); + + // Enter any new nodes at the parent's previous position. + var nodeEnter = node.enter().append("svg:g") + .attr("class", "node") + .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) + .on("click", function(d) { toggle(d); update(d); }); + + nodeEnter.append("svg:circle") + .attr("r", 1e-6) + .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); + + nodeEnter.append("svg:text") + .attr("x", function(d) { return d.children || d._children ? -10 : 10; }) + .attr("dy", ".35em") + .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) + .text(function(d) { return d.name; }) + .style("fill-opacity", 1e-6); + + // Transition nodes to their new position. + var nodeUpdate = node.transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); + + nodeUpdate.select("circle") + .attr("r", 4.5) + .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); + + nodeUpdate.select("text") + .style("fill-opacity", 1); + + // Transition exiting nodes to the parent's new position. + var nodeExit = node.exit().transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) + .remove(); + + nodeExit.select("circle") + .attr("r", 1e-6); + + nodeExit.select("text") + .style("fill-opacity", 1e-6); + + // Update the links… + var link = vis.selectAll("path.link") + .data(tree.links(nodes), function(d) { return d.target.id; }); + + // Enter any new links at the parent's previous position. + link.enter().insert("svg:path", "g") + .attr("class", "link") + .attr("d", function(d) { + var o = {x: source.x0, y: source.y0}; + return diagonal({source: o, target: o}); + }) + .transition() + .duration(duration) + .attr("d", diagonal); + + // Transition links to their new position. + link.transition() + .duration(duration) + .attr("d", diagonal); + + // Transition exiting nodes to the parent's new position. + link.exit().transition() + .duration(duration) + .attr("d", function(d) { + var o = {x: source.x, y: source.y}; + return diagonal({source: o, target: o}); + }) + .remove(); + + // Stash the old positions for transition. + nodes.forEach(function(d) { + d.x0 = d.x; + d.y0 = d.y; + }); +} + +// Toggle children. +function toggle(d) { + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } +} diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy index 43a86f10c8..0ef2edca55 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy @@ -3,7 +3,7 @@ import org.springframework.http.HttpMethod class DataModelUrlMappings { static mappings = { - "/dataModel/d3View"(view:"dataModel/d3_data_model_view") + "/dataModel/d3View"(controller: 'dataModel', action: 'd3View') "/dataModel/showAssetInAngular/$id"(controller: 'dataModel', action: 'showAssetInAngular', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'index', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'save', method: HttpMethod.POST) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index b8f422c370..88c7c66eac 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -243,6 +243,390 @@ class DataModelController extends AbstractCatalogueE respond instance, [status: OK] } + def d3View() { + def dataModelJson = [ + "name": "flare", + "children": [ + [ + "name": "analytics", + "children": [ + [ + "name": "cluster", + "children": [ + ["name": "AgglomerativeCluster", "size": 3938], + ["name": "CommunityStructure", "size": 3812], + ["name": "HierarchicalCluster", "size": 6714], + ["name": "MergeEdge", "size": 743] + ] + ], + [ + "name": "graph", + "children": [ + ["name": "BetweennessCentrality", "size": 3534], + ["name": "LinkDistance", "size": 5731], + ["name": "MaxFlowMinCut", "size": 7840], + ["name": "ShortestPaths", "size": 5914], + ["name": "SpanningTree", "size": 3416] + ] + ], + [ + "name": "optimization", + "children": [ + ["name": "AspectRatioBanker", "size": 7074] + ] + ] + ] + ], + [ + "name": "animate", + "children": [ + ["name": "Easing", "size": 17010], + ["name": "FunctionSequence", "size": 5842], + [ + "name": "interpolate", + "children": [ + ["name": "ArrayInterpolator", "size": 1983], + ["name": "ColorInterpolator", "size": 2047], + ["name": "DateInterpolator", "size": 1375], + ["name": "Interpolator", "size": 8746], + ["name": "MatrixInterpolator", "size": 2202], + ["name": "NumberInterpolator", "size": 1382], + ["name": "ObjectInterpolator", "size": 1629], + ["name": "PointInterpolator", "size": 1675], + ["name": "RectangleInterpolator", "size": 2042] + ] + ], + ["name": "ISchedulable", "size": 1041], + ["name": "Parallel", "size": 5176], + ["name": "Pause", "size": 449], + ["name": "Scheduler", "size": 5593], + ["name": "Sequence", "size": 5534], + ["name": "Transition", "size": 9201], + ["name": "Transitioner", "size": 19975], + ["name": "TransitionEvent", "size": 1116], + ["name": "Tween", "size": 6006] + ] + ], + [ + "name": "data", + "children": [ + [ + "name": "converters", + "children": [ + ["name": "Converters", "size": 721], + ["name": "DelimitedTextConverter", "size": 4294], + ["name": "GraphMLConverter", "size": 9800], + ["name": "IDataConverter", "size": 1314], + ["name": "JSONConverter", "size": 2220] + ] + ], + ["name": "DataField", "size": 1759], + ["name": "DataSchema", "size": 2165], + ["name": "DataSet", "size": 586], + ["name": "DataSource", "size": 3331], + ["name": "DataTable", "size": 772], + ["name": "DataUtil", "size": 3322] + ] + ], + [ + "name": "display", + "children": [ + ["name": "DirtySprite", "size": 8833], + ["name": "LineSprite", "size": 1732], + ["name": "RectSprite", "size": 3623], + ["name": "TextSprite", "size": 10066] + ] + ], + [ + "name": "flex", + "children": [ + ["name": "FlareVis", "size": 4116] + ] + ], + [ + "name": "physics", + "children": [ + ["name": "DragForce", "size": 1082], + ["name": "GravityForce", "size": 1336], + ["name": "IForce", "size": 319], + ["name": "NBodyForce", "size": 10498], + ["name": "Particle", "size": 2822], + ["name": "Simulation", "size": 9983], + ["name": "Spring", "size": 2213], + ["name": "SpringForce", "size": 1681] + ] + ], + [ + "name": "query", + "children": [ + ["name": "AggregateExpression", "size": 1616], + ["name": "And", "size": 1027], + ["name": "Arithmetic", "size": 3891], + ["name": "Average", "size": 891], + ["name": "BinaryExpression", "size": 2893], + ["name": "Comparison", "size": 5103], + ["name": "CompositeExpression", "size": 3677], + ["name": "Count", "size": 781], + ["name": "DateUtil", "size": 4141], + ["name": "Distinct", "size": 933], + ["name": "Expression", "size": 5130], + ["name": "ExpressionIterator", "size": 3617], + ["name": "Fn", "size": 3240], + ["name": "If", "size": 2732], + ["name": "IsA", "size": 2039], + ["name": "Literal", "size": 1214], + ["name": "Match", "size": 3748], + ["name": "Maximum", "size": 843], + [ + "name": "methods", + "children": [ + ["name": "add", "size": 593], + ["name": "and", "size": 330], + ["name": "average", "size": 287], + ["name": "count", "size": 277], + ["name": "distinct", "size": 292], + ["name": "div", "size": 595], + ["name": "eq", "size": 594], + ["name": "fn", "size": 460], + ["name": "gt", "size": 603], + ["name": "gte", "size": 625], + ["name": "iff", "size": 748], + ["name": "isa", "size": 461], + ["name": "lt", "size": 597], + ["name": "lte", "size": 619], + ["name": "max", "size": 283], + ["name": "min", "size": 283], + ["name": "mod", "size": 591], + ["name": "mul", "size": 603], + ["name": "neq", "size": 599], + ["name": "not", "size": 386], + ["name": "or", "size": 323], + ["name": "orderby", "size": 307], + ["name": "range", "size": 772], + ["name": "select", "size": 296], + ["name": "stddev", "size": 363], + ["name": "sub", "size": 600], + ["name": "sum", "size": 280], + ["name": "update", "size": 307], + ["name": "variance", "size": 335], + ["name": "where", "size": 299], + ["name": "xor", "size": 354], + ["name": "_", "size": 264] + ] + ], + ["name": "Minimum", "size": 843], + ["name": "Not", "size": 1554], + ["name": "Or", "size": 970], + ["name": "Query", "size": 13896], + ["name": "Range", "size": 1594], + ["name": "StringUtil", "size": 4130], + ["name": "Sum", "size": 791], + ["name": "Variable", "size": 1124], + ["name": "Variance", "size": 1876], + ["name": "Xor", "size": 1101] + ] + ], + [ + "name": "scale", + "children": [ + ["name": "IScaleMap", "size": 2105], + ["name": "LinearScale", "size": 1316], + ["name": "LogScale", "size": 3151], + ["name": "OrdinalScale", "size": 3770], + ["name": "QuantileScale", "size": 2435], + ["name": "QuantitativeScale", "size": 4839], + ["name": "RootScale", "size": 1756], + ["name": "Scale", "size": 4268], + ["name": "ScaleType", "size": 1821], + ["name": "TimeScale", "size": 5833] + ] + ], + [ + "name": "util", + "children": [ + ["name": "Arrays", "size": 8258], + ["name": "Colors", "size": 10001], + ["name": "Dates", "size": 8217], + ["name": "Displays", "size": 12555], + ["name": "Filter", "size": 2324], + ["name": "Geometry", "size": 10993], + [ + "name": "heap", + "children": [ + ["name": "FibonacciHeap", "size": 9354], + ["name": "HeapNode", "size": 1233] + ] + ], + ["name": "IEvaluable", "size": 335], + ["name": "IPredicate", "size": 383], + ["name": "IValueProxy", "size": 874], + [ + "name": "math", + "children": [ + ["name": "DenseMatrix", "size": 3165], + ["name": "IMatrix", "size": 2815], + ["name": "SparseMatrix", "size": 3366] + ] + ], + ["name": "Maths", "size": 17705], + ["name": "Orientation", "size": 1486], + [ + "name": "palette", + "children": [ + ["name": "ColorPalette", "size": 6367], + ["name": "Palette", "size": 1229], + ["name": "ShapePalette", "size": 2059], + ["name": "SizePalette", "size": 2291] + ] + ], + ["name": "Property", "size": 5559], + ["name": "Shapes", "size": 19118], + ["name": "Sort", "size": 6887], + ["name": "Stats", "size": 6557], + ["name": "Strings", "size": 22026] + ] + ], + [ + "name": "vis", + "children": [ + [ + "name": "axis", + "children": [ + ["name": "Axes", "size": 1302], + ["name": "Axis", "size": 24593], + ["name": "AxisGridLine", "size": 652], + ["name": "AxisLabel", "size": 636], + ["name": "CartesianAxes", "size": 6703] + ] + ], + [ + "name": "controls", + "children": [ + ["name": "AnchorControl", "size": 2138], + ["name": "ClickControl", "size": 3824], + ["name": "Control", "size": 1353], + ["name": "ControlList", "size": 4665], + ["name": "DragControl", "size": 2649], + ["name": "ExpandControl", "size": 2832], + ["name": "HoverControl", "size": 4896], + ["name": "IControl", "size": 763], + ["name": "PanZoomControl", "size": 5222], + ["name": "SelectionControl", "size": 7862], + ["name": "TooltipControl", "size": 8435] + ] + ], + [ + "name": "data", + "children": [ + ["name": "Data", "size": 20544], + ["name": "DataList", "size": 19788], + ["name": "DataSprite", "size": 10349], + ["name": "EdgeSprite", "size": 3301], + ["name": "NodeSprite", "size": 19382], + [ + "name": "render", + "children": [ + ["name": "ArrowType", "size": 698], + ["name": "EdgeRenderer", "size": 5569], + ["name": "IRenderer", "size": 353], + ["name": "ShapeRenderer", "size": 2247] + ] + ], + ["name": "ScaleBinding", "size": 11275], + ["name": "Tree", "size": 7147], + ["name": "TreeBuilder", "size": 9930] + ] + ], + [ + "name": "events", + "children": [ + ["name": "DataEvent", "size": 2313], + ["name": "SelectionEvent", "size": 1880], + ["name": "TooltipEvent", "size": 1701], + ["name": "VisualizationEvent", "size": 1117] + ] + ], + [ + "name": "legend", + "children": [ + ["name": "Legend", "size": 20859], + ["name": "LegendItem", "size": 4614], + ["name": "LegendRange", "size": 10530] + ] + ], + [ + "name": "operator", + "children": [ + [ + "name": "distortion", + "children": [ + ["name": "BifocalDistortion", "size": 4461], + ["name": "Distortion", "size": 6314], + ["name": "FisheyeDistortion", "size": 3444] + ] + ], + [ + "name": "encoder", + "children": [ + ["name": "ColorEncoder", "size": 3179], + ["name": "Encoder", "size": 4060], + ["name": "PropertyEncoder", "size": 4138], + ["name": "ShapeEncoder", "size": 1690], + ["name": "SizeEncoder", "size": 1830] + ] + ], + [ + "name": "filter", + "children": [ + ["name": "FisheyeTreeFilter", "size": 5219], + ["name": "GraphDistanceFilter", "size": 3165], + ["name": "VisibilityFilter", "size": 3509] + ] + ], + ["name": "IOperator", "size": 1286], + [ + "name": "label", + "children": [ + ["name": "Labeler", "size": 9956], + ["name": "RadialLabeler", "size": 3899], + ["name": "StackedAreaLabeler", "size": 3202] + ] + ], + [ + "name": "layout", + "children": [ + ["name": "AxisLayout", "size": 6725], + ["name": "BundledEdgeRouter", "size": 3727], + ["name": "CircleLayout", "size": 9317], + ["name": "CirclePackingLayout", "size": 12003], + ["name": "DendrogramLayout", "size": 4853], + ["name": "ForceDirectedLayout", "size": 8411], + ["name": "IcicleTreeLayout", "size": 4864], + ["name": "IndentedTreeLayout", "size": 3174], + ["name": "Layout", "size": 7881], + ["name": "NodeLinkTreeLayout", "size": 12870], + ["name": "PieLayout", "size": 2728], + ["name": "RadialTreeLayout", "size": 12348], + ["name": "RandomLayout", "size": 870], + ["name": "StackedAreaLayout", "size": 9121], + ["name": "TreeMapLayout", "size": 9191] + ] + ], + ["name": "Operator", "size": 2490], + ["name": "OperatorList", "size": 5248], + ["name": "OperatorSequence", "size": 4190], + ["name": "OperatorSwitch", "size": 2581], + ["name": "SortOperator", "size": 2023] + ] + ], + ["name": "Visualization", "size": 16540] + ] + ] + ] + ] + render(view: 'd3_data_model_view', model: [dataModelJson: dataModelJson]) + } + /** * Check if a data model contains or imports another catalogue element * @param id of data model, other id of item to be checked diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index bb84d35d5b..e99a49dd71 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -4,7 +4,7 @@ Date: 16/05/2018 Time: 17:11 --%> - +<%@ page import="grails.converters.JSON" %> <%@ page contentType="text/html;charset=UTF-8" %> @@ -43,149 +43,9 @@
click or option-click to expand or collapse
+ From 55b717293f49fc1ce699da5384c2bd5f7604a31e Mon Sep 17 00:00:00 2001 From: James Dai Date: Wed, 16 May 2018 19:09:37 +0100 Subject: [PATCH 03/46] produce JSON tree from dataModel at e.g. http://localhost:8080/dataModel/d3View/80104, with dataClasses and dataElements --- .../javascripts/d3_data_model_view/init.js | 8 +- .../conf/DataModelUrlMappings.groovy | 2 +- .../core/DataModelController.groovy | 417 ++---------------- .../core/DataClassService.groovy | 11 + .../d3viewUtils/D3ViewUtilsService.groovy | 33 ++ .../security/MetadataSecurityService.groovy | 2 +- 6 files changed, 87 insertions(+), 386 deletions(-) create mode 100644 ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index 37a46f62ba..4da1f1941c 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -39,10 +39,10 @@ function initD3(json) { // Initialize the display to show a few nodes. root.children.forEach(toggleAll); - toggle(root.children[1]); - toggle(root.children[1].children[2]); - toggle(root.children[9]); - toggle(root.children[9].children[0]); + // toggle(root.children[1]); + // toggle(root.children[1].children[2]); + // toggle(root.children[9]); + // toggle(root.children[9].children[0]); update(root); }; diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy index 0ef2edca55..353a04c1d7 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy @@ -3,7 +3,7 @@ import org.springframework.http.HttpMethod class DataModelUrlMappings { static mappings = { - "/dataModel/d3View"(controller: 'dataModel', action: 'd3View') + "/dataModel/d3View/$id"(controller: 'dataModel', action: 'd3View') "/dataModel/showAssetInAngular/$id"(controller: 'dataModel', action: 'showAssetInAngular', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'index', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'save', method: HttpMethod.POST) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index 88c7c66eac..9ba6a2bf3d 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -3,7 +3,9 @@ package org.modelcatalogue.core import grails.gorm.DetachedCriteria import grails.plugin.springsecurity.SpringSecurityUtils import org.modelcatalogue.core.asset.MicrosoftOfficeDocument +import org.modelcatalogue.core.d3viewUtils.D3ViewUtilsService import org.modelcatalogue.core.persistence.AssetGormService +import org.modelcatalogue.core.util.MetadataDomain import org.modelcatalogue.core.util.ParamArgs import org.modelcatalogue.core.util.SearchParams import org.modelcatalogue.core.util.lists.ListWithTotalAndTypeImpl @@ -60,6 +62,8 @@ class DataModelController extends AbstractCatalogueE AssetMetadataService assetMetadataService + D3ViewUtilsService d3ViewUtilsService + DataModelController() { super(DataModel, false) } @@ -244,389 +248,42 @@ class DataModelController extends AbstractCatalogueE } def d3View() { - def dataModelJson = [ - "name": "flare", - "children": [ - [ - "name": "analytics", - "children": [ - [ - "name": "cluster", - "children": [ - ["name": "AgglomerativeCluster", "size": 3938], - ["name": "CommunityStructure", "size": 3812], - ["name": "HierarchicalCluster", "size": 6714], - ["name": "MergeEdge", "size": 743] - ] - ], - [ - "name": "graph", - "children": [ - ["name": "BetweennessCentrality", "size": 3534], - ["name": "LinkDistance", "size": 5731], - ["name": "MaxFlowMinCut", "size": 7840], - ["name": "ShortestPaths", "size": 5914], - ["name": "SpanningTree", "size": 3416] - ] - ], - [ - "name": "optimization", - "children": [ - ["name": "AspectRatioBanker", "size": 7074] - ] - ] - ] - ], - [ - "name": "animate", - "children": [ - ["name": "Easing", "size": 17010], - ["name": "FunctionSequence", "size": 5842], - [ - "name": "interpolate", - "children": [ - ["name": "ArrayInterpolator", "size": 1983], - ["name": "ColorInterpolator", "size": 2047], - ["name": "DateInterpolator", "size": 1375], - ["name": "Interpolator", "size": 8746], - ["name": "MatrixInterpolator", "size": 2202], - ["name": "NumberInterpolator", "size": 1382], - ["name": "ObjectInterpolator", "size": 1629], - ["name": "PointInterpolator", "size": 1675], - ["name": "RectangleInterpolator", "size": 2042] - ] - ], - ["name": "ISchedulable", "size": 1041], - ["name": "Parallel", "size": 5176], - ["name": "Pause", "size": 449], - ["name": "Scheduler", "size": 5593], - ["name": "Sequence", "size": 5534], - ["name": "Transition", "size": 9201], - ["name": "Transitioner", "size": 19975], - ["name": "TransitionEvent", "size": 1116], - ["name": "Tween", "size": 6006] - ] - ], - [ - "name": "data", - "children": [ - [ - "name": "converters", - "children": [ - ["name": "Converters", "size": 721], - ["name": "DelimitedTextConverter", "size": 4294], - ["name": "GraphMLConverter", "size": 9800], - ["name": "IDataConverter", "size": 1314], - ["name": "JSONConverter", "size": 2220] - ] - ], - ["name": "DataField", "size": 1759], - ["name": "DataSchema", "size": 2165], - ["name": "DataSet", "size": 586], - ["name": "DataSource", "size": 3331], - ["name": "DataTable", "size": 772], - ["name": "DataUtil", "size": 3322] - ] - ], - [ - "name": "display", - "children": [ - ["name": "DirtySprite", "size": 8833], - ["name": "LineSprite", "size": 1732], - ["name": "RectSprite", "size": 3623], - ["name": "TextSprite", "size": 10066] - ] - ], - [ - "name": "flex", - "children": [ - ["name": "FlareVis", "size": 4116] - ] - ], - [ - "name": "physics", - "children": [ - ["name": "DragForce", "size": 1082], - ["name": "GravityForce", "size": 1336], - ["name": "IForce", "size": 319], - ["name": "NBodyForce", "size": 10498], - ["name": "Particle", "size": 2822], - ["name": "Simulation", "size": 9983], - ["name": "Spring", "size": 2213], - ["name": "SpringForce", "size": 1681] - ] - ], - [ - "name": "query", - "children": [ - ["name": "AggregateExpression", "size": 1616], - ["name": "And", "size": 1027], - ["name": "Arithmetic", "size": 3891], - ["name": "Average", "size": 891], - ["name": "BinaryExpression", "size": 2893], - ["name": "Comparison", "size": 5103], - ["name": "CompositeExpression", "size": 3677], - ["name": "Count", "size": 781], - ["name": "DateUtil", "size": 4141], - ["name": "Distinct", "size": 933], - ["name": "Expression", "size": 5130], - ["name": "ExpressionIterator", "size": 3617], - ["name": "Fn", "size": 3240], - ["name": "If", "size": 2732], - ["name": "IsA", "size": 2039], - ["name": "Literal", "size": 1214], - ["name": "Match", "size": 3748], - ["name": "Maximum", "size": 843], - [ - "name": "methods", - "children": [ - ["name": "add", "size": 593], - ["name": "and", "size": 330], - ["name": "average", "size": 287], - ["name": "count", "size": 277], - ["name": "distinct", "size": 292], - ["name": "div", "size": 595], - ["name": "eq", "size": 594], - ["name": "fn", "size": 460], - ["name": "gt", "size": 603], - ["name": "gte", "size": 625], - ["name": "iff", "size": 748], - ["name": "isa", "size": 461], - ["name": "lt", "size": 597], - ["name": "lte", "size": 619], - ["name": "max", "size": 283], - ["name": "min", "size": 283], - ["name": "mod", "size": 591], - ["name": "mul", "size": 603], - ["name": "neq", "size": 599], - ["name": "not", "size": 386], - ["name": "or", "size": 323], - ["name": "orderby", "size": 307], - ["name": "range", "size": 772], - ["name": "select", "size": 296], - ["name": "stddev", "size": 363], - ["name": "sub", "size": 600], - ["name": "sum", "size": 280], - ["name": "update", "size": 307], - ["name": "variance", "size": 335], - ["name": "where", "size": 299], - ["name": "xor", "size": 354], - ["name": "_", "size": 264] - ] - ], - ["name": "Minimum", "size": 843], - ["name": "Not", "size": 1554], - ["name": "Or", "size": 970], - ["name": "Query", "size": 13896], - ["name": "Range", "size": 1594], - ["name": "StringUtil", "size": 4130], - ["name": "Sum", "size": 791], - ["name": "Variable", "size": 1124], - ["name": "Variance", "size": 1876], - ["name": "Xor", "size": 1101] - ] - ], - [ - "name": "scale", - "children": [ - ["name": "IScaleMap", "size": 2105], - ["name": "LinearScale", "size": 1316], - ["name": "LogScale", "size": 3151], - ["name": "OrdinalScale", "size": 3770], - ["name": "QuantileScale", "size": 2435], - ["name": "QuantitativeScale", "size": 4839], - ["name": "RootScale", "size": 1756], - ["name": "Scale", "size": 4268], - ["name": "ScaleType", "size": 1821], - ["name": "TimeScale", "size": 5833] - ] - ], - [ - "name": "util", - "children": [ - ["name": "Arrays", "size": 8258], - ["name": "Colors", "size": 10001], - ["name": "Dates", "size": 8217], - ["name": "Displays", "size": 12555], - ["name": "Filter", "size": 2324], - ["name": "Geometry", "size": 10993], - [ - "name": "heap", - "children": [ - ["name": "FibonacciHeap", "size": 9354], - ["name": "HeapNode", "size": 1233] - ] - ], - ["name": "IEvaluable", "size": 335], - ["name": "IPredicate", "size": 383], - ["name": "IValueProxy", "size": 874], - [ - "name": "math", - "children": [ - ["name": "DenseMatrix", "size": 3165], - ["name": "IMatrix", "size": 2815], - ["name": "SparseMatrix", "size": 3366] - ] - ], - ["name": "Maths", "size": 17705], - ["name": "Orientation", "size": 1486], - [ - "name": "palette", - "children": [ - ["name": "ColorPalette", "size": 6367], - ["name": "Palette", "size": 1229], - ["name": "ShapePalette", "size": 2059], - ["name": "SizePalette", "size": 2291] - ] - ], - ["name": "Property", "size": 5559], - ["name": "Shapes", "size": 19118], - ["name": "Sort", "size": 6887], - ["name": "Stats", "size": 6557], - ["name": "Strings", "size": 22026] - ] - ], - [ - "name": "vis", - "children": [ - [ - "name": "axis", - "children": [ - ["name": "Axes", "size": 1302], - ["name": "Axis", "size": 24593], - ["name": "AxisGridLine", "size": 652], - ["name": "AxisLabel", "size": 636], - ["name": "CartesianAxes", "size": 6703] - ] - ], - [ - "name": "controls", - "children": [ - ["name": "AnchorControl", "size": 2138], - ["name": "ClickControl", "size": 3824], - ["name": "Control", "size": 1353], - ["name": "ControlList", "size": 4665], - ["name": "DragControl", "size": 2649], - ["name": "ExpandControl", "size": 2832], - ["name": "HoverControl", "size": 4896], - ["name": "IControl", "size": 763], - ["name": "PanZoomControl", "size": 5222], - ["name": "SelectionControl", "size": 7862], - ["name": "TooltipControl", "size": 8435] - ] - ], - [ - "name": "data", - "children": [ - ["name": "Data", "size": 20544], - ["name": "DataList", "size": 19788], - ["name": "DataSprite", "size": 10349], - ["name": "EdgeSprite", "size": 3301], - ["name": "NodeSprite", "size": 19382], - [ - "name": "render", - "children": [ - ["name": "ArrowType", "size": 698], - ["name": "EdgeRenderer", "size": 5569], - ["name": "IRenderer", "size": 353], - ["name": "ShapeRenderer", "size": 2247] - ] - ], - ["name": "ScaleBinding", "size": 11275], - ["name": "Tree", "size": 7147], - ["name": "TreeBuilder", "size": 9930] - ] - ], - [ - "name": "events", - "children": [ - ["name": "DataEvent", "size": 2313], - ["name": "SelectionEvent", "size": 1880], - ["name": "TooltipEvent", "size": 1701], - ["name": "VisualizationEvent", "size": 1117] - ] - ], - [ - "name": "legend", - "children": [ - ["name": "Legend", "size": 20859], - ["name": "LegendItem", "size": 4614], - ["name": "LegendRange", "size": 10530] - ] - ], - [ - "name": "operator", - "children": [ - [ - "name": "distortion", - "children": [ - ["name": "BifocalDistortion", "size": 4461], - ["name": "Distortion", "size": 6314], - ["name": "FisheyeDistortion", "size": 3444] - ] - ], - [ - "name": "encoder", - "children": [ - ["name": "ColorEncoder", "size": 3179], - ["name": "Encoder", "size": 4060], - ["name": "PropertyEncoder", "size": 4138], - ["name": "ShapeEncoder", "size": 1690], - ["name": "SizeEncoder", "size": 1830] - ] - ], - [ - "name": "filter", - "children": [ - ["name": "FisheyeTreeFilter", "size": 5219], - ["name": "GraphDistanceFilter", "size": 3165], - ["name": "VisibilityFilter", "size": 3509] - ] - ], - ["name": "IOperator", "size": 1286], - [ - "name": "label", - "children": [ - ["name": "Labeler", "size": 9956], - ["name": "RadialLabeler", "size": 3899], - ["name": "StackedAreaLabeler", "size": 3202] - ] - ], - [ - "name": "layout", - "children": [ - ["name": "AxisLayout", "size": 6725], - ["name": "BundledEdgeRouter", "size": 3727], - ["name": "CircleLayout", "size": 9317], - ["name": "CirclePackingLayout", "size": 12003], - ["name": "DendrogramLayout", "size": 4853], - ["name": "ForceDirectedLayout", "size": 8411], - ["name": "IcicleTreeLayout", "size": 4864], - ["name": "IndentedTreeLayout", "size": 3174], - ["name": "Layout", "size": 7881], - ["name": "NodeLinkTreeLayout", "size": 12870], - ["name": "PieLayout", "size": 2728], - ["name": "RadialTreeLayout", "size": 12348], - ["name": "RandomLayout", "size": 870], - ["name": "StackedAreaLayout", "size": 9121], - ["name": "TreeMapLayout", "size": 9191] - ] - ], - ["name": "Operator", "size": 2490], - ["name": "OperatorList", "size": 5248], - ["name": "OperatorSequence", "size": 4190], - ["name": "OperatorSwitch", "size": 2581], - ["name": "SortOperator", "size": 2023] - ] - ], - ["name": "Visualization", "size": 16540] - ] + + long dataModelId = params.long('id') + DataModel dataModel = dataModelGormService.findById(dataModelId) + def dataModelJson = [:] + boolean modelTooLarge = false + boolean modelFound = dataModel + + if (dataModel) { + + DataModelFilter filter = DataModelFilter.create(ImmutableSet. of(dataModel), ImmutableSet. of()) + Map stats = dataModelService.getStatistics(filter) + + ListWithTotalAndType dataClasses = dataClassService.getTopLevelDataClasses(filter, [toplevel: true, status: dataModel.status != ElementStatus.DEPRECATED ? 'active' : '']) + try { + dataModelJson = [ + "name": dataModel.name, + "type": MetadataDomain.lowerCamelCaseDomainName(MetadataDomain.ofClass(DataModel)), + "children": dataClasses.items.collect {dataClass -> + d3ViewUtilsService.dataClassD3Json(dataClass) + + } ] - ] - ] - render(view: 'd3_data_model_view', model: [dataModelJson: dataModelJson]) + } + catch (StackOverflowError e) { + // TODO: handle stack overflow (data model too large) + modelTooLarge = true + } + + } + + + render(view: 'd3_data_model_view', model: [dataModelJson: dataModelJson, modelTooLarge: modelTooLarge, modelFound: modelFound]) } + + /** * Check if a data model contains or imports another catalogue element * @param id of data model, other id of item to be checked diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy index fd7bdc2f62..e8531c2cd4 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy @@ -6,6 +6,7 @@ import org.modelcatalogue.core.api.ElementStatus import org.modelcatalogue.core.persistence.DataModelGormService import org.modelcatalogue.core.security.DataModelAclService import org.modelcatalogue.core.util.DataModelFilter +import org.modelcatalogue.core.util.MetadataDomain import org.modelcatalogue.core.util.lists.ListWithTotalAndType import org.modelcatalogue.core.util.lists.Lists @@ -156,6 +157,16 @@ class DataClassService { } } + static List getChildDataClasses(DataClass dataClass) { + List children = dataClass.getOutgoingRelationshipsByType(RelationshipType.hierarchyType)*.destination + children.collect{it as DataClass} + } + + static List getDataElementsIn(DataClass dataClass) { + List dcDEs = dataClass.getOutgoingRelationshipsByType(RelationshipType.containmentType)*.destination + dcDEs.collect{it as DataElement} + } + static int counter = 0; diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy new file mode 100644 index 0000000000..840a87480b --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -0,0 +1,33 @@ +package org.modelcatalogue.core.d3viewUtils + +import grails.transaction.Transactional +import org.modelcatalogue.core.DataClass +import org.modelcatalogue.core.DataClassService +import org.modelcatalogue.core.DataElement +import org.modelcatalogue.core.util.MetadataDomain + +@Transactional +/** + * Produces D3 view json. + * D3 view json is of the format D3JSON = ["name": NAME, "type": TYPE, "children": List] + * TYPE is from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' + */ +class D3ViewUtilsService { + + DataClassService dataClassService + + def dataClassD3Json(DataClass dataClass) { + def dataElementsJson = dataClassService.getDataElementsIn(dataClass).collect{[ + "name": it.name, + "type": MetadataDomain.lowerCamelCaseDomainName(MetadataDomain.ofClass(DataElement)), + ]} + def childDataClassesJson = dataClassService.getChildDataClasses(dataClass).collect{ + dataClassD3Json(it) // recursive + } + + def ret = ["name": dataClass.name, + "type": MetadataDomain.lowerCamelCaseDomainName(MetadataDomain.ofClass(DataClass)), + "children": dataElementsJson + childDataClassesJson] + return ret + } +} diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy index 3b51771715..521f85cb3c 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy @@ -663,7 +663,7 @@ class MetadataSecurityService { ] public static final List DATA_MODEL_MAPPINGS = [ - ["/dataModel/d3View", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], + ["/dataModel/d3View/*", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], ["/dataModel/create", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], ["/dataModel/save", MetadataRoles.ROLE_CURATOR, HttpMethod.POST], ["/dataModel/showAssetInAngular/*", 'isAuthenticated()', HttpMethod.GET], From 919f6594d0f00a66b05433e51e4949c4aa4235c9 Mon Sep 17 00:00:00 2001 From: James Dai Date: Thu, 17 May 2018 12:49:36 +0100 Subject: [PATCH 04/46] added colours for different types of catalogue element --- .../javascripts/d3_data_model_view/init.js | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index 4da1f1941c..58231884bf 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -47,52 +47,65 @@ function initD3(json) { update(root); }; +var coloursMap = { + "dataModel": "blueviolet", + "dataClass": "blue", + "dataElement": "gold" +} + function update(source) { + var duration = d3.event && d3.event.altKey ? 5000 : 500; + var radius = 7 + var unopenedNodeBorderColour = "orangered" + + // Compute the new tree layout. - var nodes = tree.nodes(root).reverse(); + var nodeLayoutData = tree.nodes(root).reverse(); // Normalize for fixed-depth. - nodes.forEach(function(d) { d.y = d.depth * 180; }); + nodeLayoutData.forEach(function(d) { d.y = d.depth * 180; }); // Update the nodes… - var node = vis.selectAll("g.node") - .data(nodes, function(d) { return d.id || (d.id = ++i); }); + var svgNodes = vis.selectAll("g.node") + .data(nodeLayoutData, function(d) { return d.id || (d.id = ++i); }); // Enter any new nodes at the parent's previous position. - var nodeEnter = node.enter().append("svg:g") + var nodeEnter = svgNodes.enter().append("svg:g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .on("click", function(d) { toggle(d); update(d); }); nodeEnter.append("svg:circle") .attr("r", 1e-6) - .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); + .style("fill", function(d) { return !d.children ? coloursMap[d.type] /*"lightsteelblue"*/ : "#fff"; }); nodeEnter.append("svg:text") - .attr("x", function(d) { return d.children || d._children ? -10 : 10; }) + .attr("x", function(d) { return d.children || d._children ? -(radius + 5): radius + 5; }) .attr("dy", ".35em") .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) .text(function(d) { return d.name; }) .style("fill-opacity", 1e-6); // Transition nodes to their new position. - var nodeUpdate = node.transition() + var nodeUpdate = svgNodes.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); nodeUpdate.select("circle") - .attr("r", 4.5) - .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); + .attr("r", radius) + .style("stroke", function(d) { return d._children ? unopenedNodeBorderColour: coloursMap[d.type]; }) + .style("stroke-width", 3) + .style("fill", function(d) { return coloursMap[d.type]}); nodeUpdate.select("text") .style("fill-opacity", 1); // Transition exiting nodes to the parent's new position. - var nodeExit = node.exit().transition() + var nodeExit = svgNodes.exit().transition() .duration(duration) .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) .remove(); @@ -105,7 +118,7 @@ function update(source) { // Update the links… var link = vis.selectAll("path.link") - .data(tree.links(nodes), function(d) { return d.target.id; }); + .data(tree.links(nodeLayoutData), function(d) { return d.target.id; }); // Enter any new links at the parent's previous position. link.enter().insert("svg:path", "g") @@ -133,7 +146,7 @@ function update(source) { .remove(); // Stash the old positions for transition. - nodes.forEach(function(d) { + nodeLayoutData.forEach(function(d) { d.x0 = d.x; d.y0 = d.y; }); From b65be3a3321ab92667c5c0c0bfab598c78d71292 Mon Sep 17 00:00:00 2001 From: James Dai Date: Thu, 17 May 2018 12:51:00 +0100 Subject: [PATCH 05/46] remove footer --- .../grails-app/views/dataModel/d3_data_model_view.gsp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index e99a49dd71..ca2bddc17a 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -38,10 +38,10 @@
- From 0dadf395af3269b43601165f65e078fd12e59c3c Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 12:25:21 +0100 Subject: [PATCH 10/46] Info-general and info-element --- .../javascripts/d3_data_model_view/init.js | 34 +++++++++++++++---- .../core/DataModelController.groovy | 2 +- .../views/dataModel/d3_data_model_view.gsp | 6 ++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index b1d43a2679..eb403ff61b 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -28,11 +28,20 @@ function parseModelToJS(jsonString) { return jsonObject } +// For info panel on the right +function info(d) { // d is data with fields name, type, angularLink, etc. + return "Name: " + "" + d.name + "" +"
" + + "Type: " + d.type + + +} + // d3.json("flare.json", function initD3(json) { root = json; root.x0 = h / 2; root.y0 = 0; + $('#d3-info-element').html(info(root)); function toggleAll(d) { if (d.children) { @@ -77,13 +86,15 @@ function update(source) { var svgNodes = vis.selectAll("g.node") .data(nodeLayoutData, function(d) { return d.id || (d.id = ++i); }); - // Enter any new nodes at the parent's previous position. + + + // ENTER any new nodes at the parent's previous position. var nodeEnter = svgNodes.enter().append("svg:g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .on("click", function(d) { toggle(d); - $('#d3-info').html("Name: " + d.name); + $('#d3-info-element').html(info(d)); update(d); }); nodeEnter.append("svg:circle") @@ -112,7 +123,10 @@ function update(source) { // nodeEnter - // Transition nodes to their new position. + + + + // TRANSITION nodes to their new position. var nodeUpdate = svgNodes.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); @@ -126,7 +140,10 @@ function update(source) { nodeUpdate.select("text") .style("fill-opacity", 1); - // Transition exiting nodes to the parent's new position. + + + + // TRANSITION EXITING nodes to the parent's new position. var nodeExit = svgNodes.exit().transition() .duration(duration) .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) @@ -138,11 +155,14 @@ function update(source) { nodeExit.select("text") .style("fill-opacity", 1e-6); - // Update the links… + + + + // Update the LINKS… var link = vis.selectAll("path.link") .data(tree.links(nodeLayoutData), function(d) { return d.target.id; }); - // Enter any new links at the parent's previous position. + // ENTER any new links at the parent's previous position. link.enter().insert("svg:path", "g") .attr("class", "link") .attr("d", function(d) { @@ -158,7 +178,7 @@ function update(source) { .duration(duration) .attr("d", diagonal); - // Transition exiting nodes to the parent's new position. + // Transition exiting links to the parent's new position. link.exit().transition() .duration(duration) .attr("d", function(d) { diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index 252d96b4a7..4679e5fbed 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -259,7 +259,7 @@ class DataModelController extends AbstractCatalogueE try { -// throw new StackOverflowError("Hi") +// throw new StackOverflowError("Test too large") dataModelJson = d3ViewUtilsService.dataModelD3Json(dataModel, -1) // -1: infinite depth } catch (StackOverflowError e) { diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index 4328dca446..5e91871462 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -43,6 +43,8 @@
click or option-click to expand or collapse
-->
+
+
@@ -51,14 +53,14 @@ if (${modelFound}) { if (${modelTooLarge}) { - $('#d3-info').html("Model ${dataModelId} too large to load fully. Click on the link to see the full view.") + $('#d3-info-general').html("Model ${dataModelId} too large to load fully. Click on the link to see the full view.") } initD3(parseModelToJS("${dataModelJson as JSON}")); // do this anyways even if model is too large; it will just load the one node. } else { - $('#d3-info').html("Model ${dataModelId} not found. Perhaps you are not authorized to view it.") + $('#d3-info-general').html("Model ${dataModelId} not found. Perhaps you are not authorized to view it.") } From edf79b273eb52bdbce2c68b6ae2567a4955bad44 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 14:16:16 +0100 Subject: [PATCH 11/46] Split URL requests into basicView and basicViewData, so we can have initial load page -> load more data --- .../javascripts/d3_data_model_view/init.js | 34 ++++++++++++++----- .../conf/DataModelUrlMappings.groovy | 3 +- .../core/DataModelController.groovy | 29 ++++++++++++++-- .../security/MetadataSecurityService.groovy | 3 +- .../views/dataModel/d3_data_model_view.gsp | 17 ++++++---- 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index eb403ff61b..a1a037f0f1 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -1,18 +1,21 @@ //= require validator-js/validator.min.js //= require_self +// dimensions of page var m = [20, 120, 20, 120], w = 1280 + 6000 - m[1] - m[3], h = 800 - m[0] - m[2], - i = 0, + i = 0, // node ids root; +// layout generator var tree = d3.layout.tree() .size([h, w]); var diagonal = d3.svg.diagonal() .projection(function(d) { return [d.y, d.x]; }); +// visualization pane var vis = d3.select("#body").append("svg:svg") .attr("width", w + m[1] + m[3]) .attr("height", h + m[0] + m[2]) @@ -21,27 +24,42 @@ var vis = d3.select("#body").append("svg:svg") function parseModelToJS(jsonString) { jsonString=jsonString.replace(/\"/g,'"'); - jsonString=jsonString.replace(/"/g, '"'); - jsonString=jsonString.replace(/\n/g, ' '); - jsonString=validator.unescape(jsonString); + //jsonString=jsonString.replace(/"/g, '"'); + jsonString=jsonString.replace(/\n/g, ' '); // i.e. \n + jsonString=validator.unescape(jsonString);h // unescape e.g. & var jsonObject=$.parseJSON(jsonString); return jsonObject } -// For info panel on the right +/** return Upper Case first letter + * @param str + * @returns {string} + */ +function ucFirst(str) { + return str.charAt(0) .toUpperCase() + str.substr(1) +} + +/** + * Return HTML for info panel on the right from node data + * @param d node data + * @returns {string} + */ function info(d) { // d is data with fields name, type, angularLink, etc. return "Name: " + "" + d.name + "" +"
" + - "Type: " + d.type + "Type: " + ucFirst(d.type) } -// d3.json("flare.json", +/** + * Initialize + * @param json + */ function initD3(json) { root = json; root.x0 = h / 2; root.y0 = 0; - $('#d3-info-element').html(info(root)); + $('#d3-info-data-model').html(info(root)); function toggleAll(d) { if (d.children) { diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy index 353a04c1d7..300623e5e5 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy @@ -3,7 +3,8 @@ import org.springframework.http.HttpMethod class DataModelUrlMappings { static mappings = { - "/dataModel/d3View/$id"(controller: 'dataModel', action: 'd3View') + "/dataModel/basicView/$id"(controller: 'dataModel', action: 'basicView') + "/dataModel/basicViewData/$id"(controller: 'dataModel', action: 'basicViewData') "/dataModel/showAssetInAngular/$id"(controller: 'dataModel', action: 'showAssetInAngular', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'index', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'save', method: HttpMethod.POST) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index 4679e5fbed..22357643c0 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -247,10 +247,35 @@ class DataModelController extends AbstractCatalogueE respond instance, [status: OK] } - def d3View() { + /** + * Initialize Basic View (D3 view) with just one node (the Data Model itself, no children) + * @return + */ + def basicView() { + + long dataModelId = params.long('id') + DataModel dataModel = dataModelGormService.findById(dataModelId) + + def dataModelJson = [:] + boolean modelFound = dataModel + + if (modelFound) { + dataModelJson = d3ViewUtilsService.dataModelD3Json(dataModel, 0) // 0: no children + } + + + render(view: 'd3_data_model_view', model: [dataModelJson: dataModelJson, modelFound: modelFound, dataModelId: dataModelId]) + } + + /** + * Return JSON data of full data model + * @return + */ + def basicViewData() { long dataModelId = params.long('id') DataModel dataModel = dataModelGormService.findById(dataModelId) + def dataModelJson = [:] boolean modelTooLarge = false boolean modelFound = dataModel @@ -270,7 +295,7 @@ class DataModelController extends AbstractCatalogueE } - + // TODO: request and render JSON render(view: 'd3_data_model_view', model: [dataModelJson: dataModelJson, modelTooLarge: modelTooLarge, modelFound: modelFound, dataModelId: dataModelId]) } diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy index 521f85cb3c..56e27e69a7 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy @@ -663,7 +663,8 @@ class MetadataSecurityService { ] public static final List DATA_MODEL_MAPPINGS = [ - ["/dataModel/d3View/*", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], + ["/dataModel/basicView/*", 'isAuthenticated()', HttpMethod.GET], + ["/dataModel/basicViewData/*", 'isAuthenticated()', HttpMethod.GET], ["/dataModel/create", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], ["/dataModel/save", MetadataRoles.ROLE_CURATOR, HttpMethod.POST], ["/dataModel/showAssetInAngular/*", 'isAuthenticated()', HttpMethod.GET], diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index 5e91871462..c23778b331 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -43,8 +43,13 @@
click or option-click to expand or collapse
-->
-
-
+

Data Model:


+
+
+

Element:


+
+
+

Messages:


@@ -52,15 +57,15 @@ From dadf42416f066d1430def6bf3b0f37630ee41c63 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 14:26:07 +0100 Subject: [PATCH 12/46] Handle model not found or too large cases with a message; added depth parameter to data model traversal --- .../javascripts/d3_data_model_view/style.css | 6 + .../views/dataModel/d3_data_model_view.gsp | 140 ++++++++++-------- 2 files changed, 85 insertions(+), 61 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css index 96880e7146..ea99f8ed01 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css @@ -50,6 +50,12 @@ padding: 3em; } +.border-bottom { + padding-bottom: 1.5em; + border-bottom-style: solid; + border-bottom-width: medium; +} + rect { fill: none; pointer-events: all; diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index c23778b331..2d55fe152c 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -6,67 +6,85 @@ --%> <%@ page import="grails.converters.JSON" %> <%@ page contentType="text/html;charset=UTF-8" %> + - - - - - - - - - -
- -
-

Data Model:


-
-
-

Element:


-
-
-

Messages:


- -
- - - - + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+

Data Model:


+
+
+ +
+

Element:


+
+
+ +
+

Messages:


+
+ +
+
+ + + + From 2b28b2e63681aae14ccb0de1248962f998368a94 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 15:00:55 +0100 Subject: [PATCH 13/46] Javascript flow added --- ModelCatalogueCorePluginTestApp/.flowconfig | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 ModelCatalogueCorePluginTestApp/.flowconfig diff --git a/ModelCatalogueCorePluginTestApp/.flowconfig b/ModelCatalogueCorePluginTestApp/.flowconfig new file mode 100644 index 0000000000..1fed445333 --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/.flowconfig @@ -0,0 +1,11 @@ +[ignore] + +[include] + +[libs] + +[lints] + +[options] + +[strict] From a6e122e79bf7502b91053bb0342339ee12dddab7 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 15:25:38 +0100 Subject: [PATCH 14/46] Data Model Two-part load: initial and load more data --- .../javascripts/d3_data_model_view/init.js | 2 +- .../core/DataModelController.groovy | 4 +- .../views/dataModel/d3_data_model_view.gsp | 81 +++++++++++++++++-- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index a1a037f0f1..9c9bd90654 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -35,7 +35,7 @@ function parseModelToJS(jsonString) { * @param str * @returns {string} */ -function ucFirst(str) { +function ucFirst(str /*: string */) { return str.charAt(0) .toUpperCase() + str.substr(1) } diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index 22357643c0..7a84bab707 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -296,7 +296,9 @@ class DataModelController extends AbstractCatalogueE } // TODO: request and render JSON - render(view: 'd3_data_model_view', model: [dataModelJson: dataModelJson, modelTooLarge: modelTooLarge, modelFound: modelFound, dataModelId: dataModelId]) + render(contentType: 'text/json') { + [dataModelJson: dataModelJson, modelTooLarge: modelTooLarge, modelFound: modelFound, dataModelId: dataModelId] + } } diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index 2d55fe152c..e33c27e567 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -1,3 +1,4 @@ +// @flow <%-- Created by IntelliJ IDEA. User: james @@ -64,8 +65,11 @@
-
+

Messages:


+
    + +
@@ -74,17 +78,78 @@ From fc63cb6a022a1864cc7ae746bc88389e67dbd417 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 15:28:45 +0100 Subject: [PATCH 15/46] Link to Basic View from Dashboard --- .../grails-app/views/dashboard/_dataModelLink.gsp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp index c78be069bf..3a3c411324 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp @@ -1 +1,5 @@ -${dataModel?.name} \ No newline at end of file +%{--Angular Link:--}% +%{--${dataModel?.name}--}% + +%{--Basic View Link:--}% +${dataModel?.name} From 0049aeabeea479eb92971a8f8048437a6593f9d8 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 15:42:29 +0100 Subject: [PATCH 16/46] Added tooltips with name on node mouseover --- .../javascripts/d3_data_model_view/init.js | 29 +++++++++++++++++-- .../javascripts/d3_data_model_view/style.css | 19 ++++++++++-- .../views/dataModel/d3_data_model_view.gsp | 10 +++---- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index 9c9bd90654..e159c723dd 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -22,6 +22,12 @@ var vis = d3.select("#body").append("svg:svg") .append("svg:g") .attr("transform", "translate(" + (m[3] + 20) + "," + m[0] + ")"); +// Define the div for the tooltip +var div = d3.select("#body").append("div") + .attr("class", "tooltip") + .style("opacity", 0); + + function parseModelToJS(jsonString) { jsonString=jsonString.replace(/\"/g,'"'); //jsonString=jsonString.replace(/"/g, '"'); @@ -115,10 +121,28 @@ function update(source) { $('#d3-info-element').html(info(d)); update(d); }); + // add circles for each node nodeEnter.append("svg:circle") .attr("r", 1e-6) - .style("fill", function(d) { return !d.children ? coloursMap[d.type] /*"lightsteelblue"*/ : "#fff"; }); + .style("fill", function(d) { return !d.children ? coloursMap[d.type] /*"lightsteelblue"*/ : "#fff"; }) + // Tooltip: + .on("mouseover", function(d) { + div.transition() + .duration(0) + .style("opacity", .9); + div .html(d.name) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }) + .on("mouseout", function(d) { + div.transition() + .duration(500) + .style("opacity", 0); + }); + var maxStringLength = 10; + + // Text/link for each node nodeEnter.append("svg:a") .attr("xlink:href", function(d) {return d.angularLink}) .attr("target", "_blank") @@ -153,7 +177,8 @@ function update(source) { .attr("r", radius) .style("stroke", function(d) { return d._children ? unopenedNodeBorderColour: coloursMap[d.type]; }) .style("stroke-width", 3) - .style("fill", function(d) { return coloursMap[d.type]}); + .style("fill", function(d) { return coloursMap[d.type]}) + ; nodeUpdate.select("text") .style("fill-opacity", 1); diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css index ea99f8ed01..6095a97bb1 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css @@ -47,15 +47,30 @@ .column-right { width: 25%; - padding: 3em; } -.border-bottom { +.info-box { padding-bottom: 1.5em; border-bottom-style: solid; border-bottom-width: medium; + padding: 3em; + } +div.tooltip { + position: absolute; + text-align: center; + /*width: 60px;*/ + height: 28px; + padding: 10px; + font: 12px sans-serif; + background: lightsteelblue; + border: 0px; + border-radius: 8px; + pointer-events: none; +} + + rect { fill: none; pointer-events: all; diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index e33c27e567..b3f325315d 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -55,18 +55,18 @@
-
+

Data Model:


-
-

Element:


+
+

Element:


-
-

Messages:


+
+

Messages:


From e600e575588b6ae5b0dec8283d5e090acc39ca9f Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 16:05:09 +0100 Subject: [PATCH 17/46] Forward and back links between basic and advanced view --- .../grails-app/assets/javascripts/d3_data_model_view/init.js | 3 ++- .../javascripts/templates/mc/core/ui/states/mc.tpl.html | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index e159c723dd..fec7ae0edb 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -51,7 +51,8 @@ function ucFirst(str /*: string */) { * @returns {string} */ function info(d) { // d is data with fields name, type, angularLink, etc. - return "Name: " + "" + d.name + "" +"
" + + return "Name: " + "" + d.name + "" + "
" + + " (Click to see Advanced View)" + "
" + "
" + "Type: " + ucFirst(d.type) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html index 6bf132d79d..4686566d50 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html @@ -3,6 +3,11 @@ resizable="{'handles': 'e', 'mirror': '.split-view-right', 'maxWidthPct': 60, 'minWidthPct': 20, 'windowWidthCorrection': 91, 'parentWidthCorrection': 31, 'breakWidth': 768}"> From 24882f22e41c66a495353fbad85aded2f816d5cf Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 16:15:27 +0100 Subject: [PATCH 18/46] scroll right info column --- .../grails-app/assets/javascripts/d3_data_model_view/style.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css index 6095a97bb1..a056c32b9e 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/style.css @@ -47,6 +47,8 @@ .column-right { width: 25%; + overflow: scroll; + height: 800px; } .info-box { From 8099c52785ba52e732ed730a6c509024d992f48b Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 16:31:42 +0100 Subject: [PATCH 19/46] date for messages --- .../grails-app/views/dataModel/d3_data_model_view.gsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index b3f325315d..118c15498f 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -87,7 +87,7 @@ */ function writeMessage(text) { - $('#d3-info-messages').append("
  • " + text + "
  • ") + $('#d3-info-messages').append("
  • " + (new Date().toLocaleString()) + ": " + text + "
  • ") } /** From d26d05a14f68921285328e1a1001d9dd6e799926 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 16:49:44 +0100 Subject: [PATCH 20/46] Handle data elements with no data classes in data model as its children --- .../core/DataElementService.groovy | 7 ++++ .../d3viewUtils/D3ViewUtilsService.groovy | 40 +++++++++++++++---- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy index f954677aa5..e9612b48b6 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy @@ -153,7 +153,14 @@ class DataElementService { return sqlQuery.list()[0] } } + static List getDataClassesOf(DataElement dataElement) { + List deDCs = dataElement.getIncomingRelationshipsByType(RelationshipType.containmentType)*.source + deDCs.collect{it as DataClass} + } + static int countDataClassesOf(DataElement dataElement) { + dataElement.getIncomingRelationshipsByType(RelationshipType.containmentType).size() + } private ListWithTotalAndType buildDataElementsList(Map params, String selectElements, String selectCount, String fromQuery, @DelegatesTo(SQLQuery) Closure closure) { QuerySetupListMethods querySetupListMethods = new QuerySetupListMethods(selectElements, selectCount, fromQuery, closure) return Lists.methodNotClosureLazy(params, DataElement, querySetupListMethods) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy index 51779d555b..a35c1adb20 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -6,6 +6,7 @@ import org.codehaus.groovy.grails.commons.GrailsApplication import org.modelcatalogue.core.DataClass import org.modelcatalogue.core.DataClassService import org.modelcatalogue.core.DataElement +import org.modelcatalogue.core.DataElementService import org.modelcatalogue.core.DataModel import org.modelcatalogue.core.DataModelService import org.modelcatalogue.core.api.ElementStatus @@ -24,6 +25,7 @@ class D3ViewUtilsService { DataModelService dataModelService DataClassService dataClassService + DataElementService dataElementService GrailsApplication grailsApplication static String lowerCamelCaseDomainName(Class clazz) { @@ -45,15 +47,39 @@ class D3ViewUtilsService { DataModelFilter filter = DataModelFilter.create(ImmutableSet. of(dataModel), ImmutableSet. of()) Map stats = dataModelService.getStatistics(filter) + ListWithTotalAndType dataClasses = dataClassService.getTopLevelDataClasses(filter, [toplevel: true, status: dataModel.status != ElementStatus.DEPRECATED ? 'active' : '']) + List unDataClassedDataElements = DataElement.findAllByDataModel(dataModel).findAll{ + dataElementService.countDataClassesOf(it) == 0 + } + + def dataClassChildrenJson = [] + def dataElementChildrenJson = [] + + if (depth != 0) { + dataClassChildrenJson = dataClasses.items.collect {dataClass -> + dataClassD3Json(dataClass, depth-1) + + } + dataElementChildrenJson = unDataClassedDataElements.collect { + dataElementD3Json(it) + } + } + def dataModelJson = [ "name": dataModel.name, "angularLink": angularLink(dataModel.id, dataModel.id, DataModel), "type": lowerCamelCaseDomainName(DataModel), - "children": (depth == 0) ? [] : dataClasses.items.collect {dataClass -> - dataClassD3Json(dataClass, depth-1) + "children": dataClassChildrenJson + dataElementChildrenJson + // TODO: Handle case where there are just DataTypes listed not connected to any DataElements + ] + } - } // TODO: If Data Model has Data Elements not in any Data Class add them + def dataElementD3Json(DataElement dataElement) { + [ + "name": dataElement.name, + "angularLink": angularLink(dataElement.dataModel.id, dataElement.id, DataElement), + "type": lowerCamelCaseDomainName(DataElement), ] } @@ -72,11 +98,9 @@ class D3ViewUtilsService { if (depth != 0) { - dataElementsJson = dataClassService.getDataElementsIn(dataClass).collect{[ - "name": it.name, - "angularLink": angularLink(it.dataModel.id, it.id, DataElement), - "type": lowerCamelCaseDomainName(DataElement), - ]} + dataElementsJson = dataClassService.getDataElementsIn(dataClass).collect{ + dataElementD3Json(it) + } childDataClassesJson = dataClassService.getChildDataClasses(dataClass).collect{ dataClassD3Json(it, depth - 1) // recursive From d57466dc07c206e492d91749e452881c427d98f6 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 17:14:46 +0100 Subject: [PATCH 21/46] Add datatypes and enumerated types to basic view --- .../javascripts/d3_data_model_view/init.js | 15 +++++- .../d3viewUtils/D3ViewUtilsService.groovy | 49 +++++++++++++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index fec7ae0edb..10ef1fc25b 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -53,11 +53,21 @@ function ucFirst(str /*: string */) { function info(d) { // d is data with fields name, type, angularLink, etc. return "Name: " + "" + d.name + "" + "
    " + " (Click to see Advanced View)" + "
    " + "
    " + - "Type: " + ucFirst(d.type) + "Type: " + ucFirst(d.type) + "
    " + + (d.enumerations ? enumerate(d.enumerations): "") } +function enumerate(map) { + var ret = "Enumerations:
      " + Object.keys(map).forEach(function(key) { + ret = ret + "
    • " + key + ": " + map[key] + "
    • " + console.log(key, map[key]); + }); + return ret = ret + "
    " +} + /** * Initialize * @param json @@ -88,7 +98,8 @@ function initD3(json) { var coloursMap = { "dataModel": "blueviolet", "dataClass": "blue", - "dataElement": "gold" + "dataElement": "gold", + "dataType": "green" } diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy index a35c1adb20..b9115967d3 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -9,6 +9,8 @@ import org.modelcatalogue.core.DataElement import org.modelcatalogue.core.DataElementService import org.modelcatalogue.core.DataModel import org.modelcatalogue.core.DataModelService +import org.modelcatalogue.core.DataType +import org.modelcatalogue.core.EnumeratedType import org.modelcatalogue.core.api.ElementStatus import org.modelcatalogue.core.util.DataModelFilter import org.modelcatalogue.core.util.MetadataDomain @@ -17,9 +19,20 @@ import org.modelcatalogue.core.util.lists.ListWithTotalAndType @Transactional /** * Produces D3 view json. - * D3 view json is of the format D3JSON = ["name": NAME, "angularLink": ANGULARLINK, "type": TYPE, "children": List] + * D3 view json is of the recursive format + * + * D3JSON ::= + * ["name": string, + * "angularLink": ANGULARLINK, + * "type": TYPE, + * "enumerations": ENUMERATIONS + * "children": List] + * * TYPE is from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' - * angularLink is e.g. http://localhost:8080/#/82467/dataClass/82470/, which is data model 82467, data class 82470 + * ANGULARLINK is a string of format e.g. "http://localhost:8080/#/82467/dataClass/82470/", which is a link to the angular application view for data model 82467, data class 82470 + * ENUMERATIONS is a map of strings to strings. + * + * */ class D3ViewUtilsService { @@ -76,11 +89,31 @@ class D3ViewUtilsService { } def dataElementD3Json(DataElement dataElement) { - [ + def ret = [ "name": dataElement.name, "angularLink": angularLink(dataElement.dataModel.id, dataElement.id, DataElement), "type": lowerCamelCaseDomainName(DataElement), + + ] + + if (dataElement.dataType) { + ret['children'] = [dataTypeD3Json(dataElement.dataType)] + } + + return ret + } + + def dataTypeD3Json(DataType dataType) { + def ret = [ + "name": dataType.name, + "angularLink": angularLink(dataType.dataModel.id, dataType.id, DataType), + "type": lowerCamelCaseDomainName(DataType), ] + + if (dataType instanceof EnumeratedType) { + ret['enumerations'] = ((EnumeratedType) dataType).enumerations + } + return ret } /** @@ -108,10 +141,18 @@ class D3ViewUtilsService { } // otherwise depth == 0, no children + def ret = ["name": dataClass.name, "angularLink": angularLink(dataClass.dataModel.id, dataClass.id, DataClass), "type": lowerCamelCaseDomainName(DataClass), - "children": dataElementsJson + childDataClassesJson] + ] + + + def children = dataElementsJson + childDataClassesJson + if (children) { + ret['children'] = children + } + return ret } } From a50616f6a82e24e70a27d2bd99674c3b6adadc26 Mon Sep 17 00:00:00 2001 From: James Dai Date: Fri, 18 May 2018 17:30:52 +0100 Subject: [PATCH 22/46] documentation of D3JSON --- .../modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy index b9115967d3..6b2ca8a4c9 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -18,7 +18,7 @@ import org.modelcatalogue.core.util.lists.ListWithTotalAndType @Transactional /** - * Produces D3 view json. + * Produces D3 hierarchical layout json augmented with model catalogue specific fields. * D3 view json is of the recursive format * * D3JSON ::= @@ -30,7 +30,7 @@ import org.modelcatalogue.core.util.lists.ListWithTotalAndType * * TYPE is from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' * ANGULARLINK is a string of format e.g. "http://localhost:8080/#/82467/dataClass/82470/", which is a link to the angular application view for data model 82467, data class 82470 - * ENUMERATIONS is a map of strings to strings. + * ENUMERATIONS is a map of strings to strings for enumerated types. * * */ From 6576e4ff353d3d545e0627b0474ad166236b69af Mon Sep 17 00:00:00 2001 From: James Dai Date: Sat, 19 May 2018 11:55:00 +0100 Subject: [PATCH 23/46] Lazy loading for data model children --- .../javascripts/d3_data_model_view/init.js | 53 +++++++++++++- .../conf/DataModelUrlMappings.groovy | 2 +- .../core/DataModelController.groovy | 58 ++++++++++----- .../d3viewUtils/D3ViewUtilsService.groovy | 63 +++++++++------- .../security/MetadataSecurityService.groovy | 2 +- .../views/dataModel/d3_data_model_view.gsp | 73 +++---------------- 6 files changed, 135 insertions(+), 116 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index 10ef1fc25b..6a5bdbbb55 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -68,6 +68,9 @@ function enumerate(map) { return ret = ret + "" } + +var serverUrl = "" + /** * Initialize * @param json @@ -86,7 +89,6 @@ function initD3(json) { } // Initialize the display to show a few nodes. - root.children.forEach(toggleAll); // toggle(root.children[1]); // toggle(root.children[1].children[2]); // toggle(root.children[9]); @@ -103,6 +105,9 @@ var coloursMap = { } +function writeMessage(text) { + $('#d3-info-messages').append("
  • " + (new Date().toLocaleString()) + ": " + text + "
  • ") +} function update(source) { @@ -122,16 +127,56 @@ function update(source) { var svgNodes = vis.selectAll("g.node") .data(nodeLayoutData, function(d) { return d.id || (d.id = ++i); }); - + /*:: + type ChildrenData = { + children: Array, + canAccessDataModel: boolean + } + */ // ENTER any new nodes at the parent's previous position. var nodeEnter = svgNodes.enter().append("svg:g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .on("click", function(d) { - toggle(d); + $('#d3-info-element').html(info(d)); - update(d); }); + + if (d.loadedChildren) { + if (!d.loading) { + toggle(d); + update(d); + } + } + + else { + if (!d.loading) { + d.loading = true // try to prevent double-loading, although race conditions may still result if you click fast enough. + $.ajax({ + url: serverUrl + "/dataModel/basicViewChildrenData/" + d.type + "/" + d.id + }).then(function(data /*: ChildrenData */) { + + if (data.canAccessDataModel) { + d._children = data.children; + toggle(d); + update(d); + } + + else { + writeMessage("You do not have access to the data model of " + ucFirst(d.type) + " " + d.name) + } + + d.loadedChildren = true; + d.loading = false; + + }, function(jqXHR, textStatus, errorThrown) { // request failure + d.loading = false + }) + } + + + } + }); // add circles for each node nodeEnter.append("svg:circle") diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy index 300623e5e5..266f39ae23 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy @@ -4,7 +4,7 @@ class DataModelUrlMappings { static mappings = { "/dataModel/basicView/$id"(controller: 'dataModel', action: 'basicView') - "/dataModel/basicViewData/$id"(controller: 'dataModel', action: 'basicViewData') + "/dataModel/basicViewChildrenData/$type/$id"(controller: 'dataModel', action: 'basicViewChildrenData') "/dataModel/showAssetInAngular/$id"(controller: 'dataModel', action: 'showAssetInAngular', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'index', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'save', method: HttpMethod.POST) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index 7a84bab707..e8efd4db0e 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -46,6 +46,11 @@ import grails.plugin.springsecurity.SpringSecurityService class DataModelController extends AbstractCatalogueElementController { + static allowedMethods = [ + basicView: "GET", + basicViewChildrenData: "GET", + ] + SessionFactory sessionFactory SpringSecurityService springSecurityService @@ -249,6 +254,13 @@ class DataModelController extends AbstractCatalogueE /** * Initialize Basic View (D3 view) with just one node (the Data Model itself, no children) + * Returns + * + type DataModelDisplayData = { + dataModelJson: D3JSON, + modelFound: boolean, + dataModelId: long + }; * @return */ def basicView() { @@ -260,44 +272,50 @@ class DataModelController extends AbstractCatalogueE boolean modelFound = dataModel if (modelFound) { - dataModelJson = d3ViewUtilsService.dataModelD3Json(dataModel, 0) // 0: no children + dataModelJson = d3ViewUtilsService.dataModelD3Json(dataModel) // no children } - render(view: 'd3_data_model_view', model: [dataModelJson: dataModelJson, modelFound: modelFound, dataModelId: dataModelId]) + render(view: 'd3_data_model_view', model: [ + dataModelJson: dataModelJson, + modelFound: modelFound, + dataModelId: dataModelId]) } /** - * Return JSON data of full data model + * Returns + * type ChildrenData = { + * children: List, + * canAccessDataModel: boolean + * } * @return */ - def basicViewData() { + def basicViewChildrenData() { - long dataModelId = params.long('id') - DataModel dataModel = dataModelGormService.findById(dataModelId) + String type = params.get('type').toString() + long id = params.long('id') - def dataModelJson = [:] - boolean modelTooLarge = false - boolean modelFound = dataModel + boolean canAccessDataModel = false + def children = [] - if (modelFound) { + if (type == 'dataModel') { + DataModel dataModel = dataModelGormService.findById(id) - try { -// throw new StackOverflowError("Test too large") - dataModelJson = d3ViewUtilsService.dataModelD3Json(dataModel, -1) // -1: infinite depth - } - catch (StackOverflowError e) { - // TODO: handle stack overflow (data model too large) - modelTooLarge = true - dataModelJson = d3ViewUtilsService.dataModelD3Json(dataModel, 0) // 0: no children - } + canAccessDataModel = dataModel + + if (canAccessDataModel) { + + children = d3ViewUtilsService.dataModelD3JsonChildren(dataModel) + } } + + // TODO: request and render JSON render(contentType: 'text/json') { - [dataModelJson: dataModelJson, modelTooLarge: modelTooLarge, modelFound: modelFound, dataModelId: dataModelId] + [children: children, canAccessDataModel: canAccessDataModel] } } diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy index 6b2ca8a4c9..ade1ab1647 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -22,16 +22,19 @@ import org.modelcatalogue.core.util.lists.ListWithTotalAndType * D3 view json is of the recursive format * * D3JSON ::= - * ["name": string, + * ["name": String, + * "id": Long, + * "loadedChildren": Boolean, * "angularLink": ANGULARLINK, * "type": TYPE, * "enumerations": ENUMERATIONS * "children": List] * - * TYPE is from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' - * ANGULARLINK is a string of format e.g. "http://localhost:8080/#/82467/dataClass/82470/", which is a link to the angular application view for data model 82467, data class 82470 - * ENUMERATIONS is a map of strings to strings for enumerated types. + * TYPE is a String from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' + * ANGULARLINK is a String of format e.g. "http://localhost:8080/#/82467/dataClass/82470/", which is a link to the angular application view for data model 82467, data class 82470 + * ENUMERATIONS is a Map of Strings to Strings for enumerated types. * + * TODO: Make this an actual class with a marshaller * */ class D3ViewUtilsService { @@ -50,13 +53,28 @@ class D3ViewUtilsService { } /** - * depth -1 means go as far as possible - * depth 0 means don't have any children - * depth 1 means have one layer of children + * No children; load them later * @param dataModel - * @param depth + * @return D3JSON + */ + def dataModelD3Json(DataModel dataModel) { + + def dataModelJson = [ + "name": dataModel.name, + "id": dataModel.id, + "loadedChildren": false, + "loading": false, + "angularLink": angularLink(dataModel.id, dataModel.id, DataModel), + "type": lowerCamelCaseDomainName(DataModel) + ] + } + + /** + * Get "children" of data model for display (top level data classes and data elements + * @param dataModel + * @return List */ - def dataModelD3Json(DataModel dataModel, int depth) { + def dataModelD3JsonChildren(DataModel dataModel) { DataModelFilter filter = DataModelFilter.create(ImmutableSet. of(dataModel), ImmutableSet. of()) Map stats = dataModelService.getStatistics(filter) @@ -66,26 +84,19 @@ class D3ViewUtilsService { dataElementService.countDataClassesOf(it) == 0 } - def dataClassChildrenJson = [] - def dataElementChildrenJson = [] + def dataClassChildrenJson = dataClasses.items.collect {dataClass -> + dataClassD3Json(dataClass, 0) - if (depth != 0) { - dataClassChildrenJson = dataClasses.items.collect {dataClass -> - dataClassD3Json(dataClass, depth-1) - - } - dataElementChildrenJson = unDataClassedDataElements.collect { - dataElementD3Json(it) - } + } + def dataElementChildrenJson = unDataClassedDataElements.collect { + dataElementD3Json(it) } - def dataModelJson = [ - "name": dataModel.name, - "angularLink": angularLink(dataModel.id, dataModel.id, DataModel), - "type": lowerCamelCaseDomainName(DataModel), - "children": dataClassChildrenJson + dataElementChildrenJson - // TODO: Handle case where there are just DataTypes listed not connected to any DataElements - ] + + + return dataClassChildrenJson + dataElementChildrenJson + // TODO: Handle case where there are just DataTypes listed not connected to any DataElements + } def dataElementD3Json(DataElement dataElement) { diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy index 56e27e69a7..d3b6d1f8e8 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy @@ -664,7 +664,7 @@ class MetadataSecurityService { public static final List DATA_MODEL_MAPPINGS = [ ["/dataModel/basicView/*", 'isAuthenticated()', HttpMethod.GET], - ["/dataModel/basicViewData/*", 'isAuthenticated()', HttpMethod.GET], + ["/dataModel/basicViewChildrenData/*/*", 'isAuthenticated()', HttpMethod.GET], ["/dataModel/create", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], ["/dataModel/save", MetadataRoles.ROLE_CURATOR, HttpMethod.POST], ["/dataModel/showAssetInAngular/*", 'isAuthenticated()', HttpMethod.GET], diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index 118c15498f..1d01d05383 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -78,77 +78,22 @@ From 26aac23dcd6de72c67de42a4f4b36d684393a0a0 Mon Sep 17 00:00:00 2001 From: James Dai Date: Sat, 19 May 2018 12:15:32 +0100 Subject: [PATCH 24/46] return types D3JSON --- .../core/DataModelController.groovy | 13 +- .../d3viewUtils/D3ViewUtilsService.groovy | 111 ++++++++++-------- 2 files changed, 65 insertions(+), 59 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index e8efd4db0e..48bdb73c3f 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -3,6 +3,7 @@ package org.modelcatalogue.core import grails.gorm.DetachedCriteria import grails.plugin.springsecurity.SpringSecurityUtils import org.modelcatalogue.core.asset.MicrosoftOfficeDocument +import org.modelcatalogue.core.d3viewUtils.ChildrenData import org.modelcatalogue.core.d3viewUtils.D3ViewUtilsService import org.modelcatalogue.core.persistence.AssetGormService import org.modelcatalogue.core.util.MetadataDomain @@ -283,12 +284,8 @@ class DataModelController extends AbstractCatalogueE } /** - * Returns - * type ChildrenData = { - * children: List, - * canAccessDataModel: boolean - * } - * @return + * + * @return ChildrenData */ def basicViewChildrenData() { @@ -312,10 +309,8 @@ class DataModelController extends AbstractCatalogueE } - - // TODO: request and render JSON render(contentType: 'text/json') { - [children: children, canAccessDataModel: canAccessDataModel] + new ChildrenData(children: children, canAccessDataModel: canAccessDataModel) } } diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy index ade1ab1647..433a60f037 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -19,22 +19,6 @@ import org.modelcatalogue.core.util.lists.ListWithTotalAndType @Transactional /** * Produces D3 hierarchical layout json augmented with model catalogue specific fields. - * D3 view json is of the recursive format - * - * D3JSON ::= - * ["name": String, - * "id": Long, - * "loadedChildren": Boolean, - * "angularLink": ANGULARLINK, - * "type": TYPE, - * "enumerations": ENUMERATIONS - * "children": List] - * - * TYPE is a String from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' - * ANGULARLINK is a String of format e.g. "http://localhost:8080/#/82467/dataClass/82470/", which is a link to the angular application view for data model 82467, data class 82470 - * ENUMERATIONS is a Map of Strings to Strings for enumerated types. - * - * TODO: Make this an actual class with a marshaller * */ class D3ViewUtilsService { @@ -57,16 +41,14 @@ class D3ViewUtilsService { * @param dataModel * @return D3JSON */ - def dataModelD3Json(DataModel dataModel) { - - def dataModelJson = [ - "name": dataModel.name, - "id": dataModel.id, - "loadedChildren": false, - "loading": false, - "angularLink": angularLink(dataModel.id, dataModel.id, DataModel), - "type": lowerCamelCaseDomainName(DataModel) - ] + D3JSON dataModelD3Json(DataModel dataModel) { + + D3JSON dataModelJson = new D3JSON( + name: dataModel.name, + id: dataModel.id, + angularLink: angularLink(dataModel.id, dataModel.id, DataModel), + type: lowerCamelCaseDomainName(DataModel) + ) } /** @@ -74,7 +56,7 @@ class D3ViewUtilsService { * @param dataModel * @return List */ - def dataModelD3JsonChildren(DataModel dataModel) { + List dataModelD3JsonChildren(DataModel dataModel) { DataModelFilter filter = DataModelFilter.create(ImmutableSet. of(dataModel), ImmutableSet. of()) Map stats = dataModelService.getStatistics(filter) @@ -99,30 +81,31 @@ class D3ViewUtilsService { } - def dataElementD3Json(DataElement dataElement) { - def ret = [ - "name": dataElement.name, - "angularLink": angularLink(dataElement.dataModel.id, dataElement.id, DataElement), - "type": lowerCamelCaseDomainName(DataElement), + D3JSON dataElementD3Json(DataElement dataElement) { + D3JSON ret = new D3JSON( - ] + name: dataElement.name, + angularLink: angularLink(dataElement.dataModel.id, dataElement.id, DataElement), + type: lowerCamelCaseDomainName(DataElement) + ) if (dataElement.dataType) { - ret['children'] = [dataTypeD3Json(dataElement.dataType)] + ret.children = [dataTypeD3Json(dataElement.dataType)] } return ret } - def dataTypeD3Json(DataType dataType) { - def ret = [ - "name": dataType.name, - "angularLink": angularLink(dataType.dataModel.id, dataType.id, DataType), - "type": lowerCamelCaseDomainName(DataType), - ] + D3JSON dataTypeD3Json(DataType dataType) { + D3JSON ret = new D3JSON( + name: dataType.name, + angularLink: angularLink(dataType.dataModel.id, dataType.id, DataType), + loadedChildren: true, // No children, so "already loaded children." + type: lowerCamelCaseDomainName(DataType), + ) if (dataType instanceof EnumeratedType) { - ret['enumerations'] = ((EnumeratedType) dataType).enumerations + ret.enumerations = ((EnumeratedType) dataType).enumerations } return ret } @@ -135,10 +118,10 @@ class D3ViewUtilsService { * @param depth * @return */ - def dataClassD3Json(DataClass dataClass, int depth) { + D3JSON dataClassD3Json(DataClass dataClass, int depth) { - def dataElementsJson = [] - def childDataClassesJson = [] + List dataElementsJson = [] + List childDataClassesJson = [] if (depth != 0) { @@ -153,17 +136,45 @@ class D3ViewUtilsService { } // otherwise depth == 0, no children - def ret = ["name": dataClass.name, - "angularLink": angularLink(dataClass.dataModel.id, dataClass.id, DataClass), - "type": lowerCamelCaseDomainName(DataClass), - ] + D3JSON ret = new D3JSON( + name: dataClass.name, + angularLink: angularLink(dataClass.dataModel.id, dataClass.id, DataClass), + type: lowerCamelCaseDomainName(DataClass), + ) + + List children = dataElementsJson + childDataClassesJson - def children = dataElementsJson + childDataClassesJson if (children) { - ret['children'] = children + ret.children = children } return ret } } +/** + * D3 view json recursive format + * + * type is a String from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' + * angularLinkis a String of format e.g. "http://localhost:8080/#/82467/dataClass/82470/", which is a link to the angular application view for data model 82467, data class 82470 + * enumerations is a Map of Strings to Strings for enumerated types. + * + */ +class D3JSON { + String name + long id + + boolean loadedChildren = false + boolean loading = false + + String angularLink + String type + Map enumerations + List children + +} + +class ChildrenData { + List children + boolean canAccessDataModel +} From 5cee2a1baf77d378152f1d4cb33f83967ae5ff65 Mon Sep 17 00:00:00 2001 From: James Dai Date: Sat, 19 May 2018 13:20:04 +0100 Subject: [PATCH 25/46] Lazy loading working for dataClasses. Appropriate changes made to front-end to make stuff work with the laziness --- .../javascripts/d3_data_model_view/init.js | 177 ++++++++++++------ .../core/DataModelController.groovy | 21 ++- .../d3viewUtils/D3ViewUtilsService.groovy | 69 ++++--- 3 files changed, 169 insertions(+), 98 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index 6a5bdbbb55..efb2b3ccd6 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -1,6 +1,8 @@ //= require validator-js/validator.min.js //= require_self +// @flow +//// D3 Setup stuff // dimensions of page var m = [20, 120, 20, 120], w = 1280 + 6000 - m[1] - m[3], @@ -12,6 +14,7 @@ var m = [20, 120, 20, 120], var tree = d3.layout.tree() .size([h, w]); +// transition stuff var diagonal = d3.svg.diagonal() .projection(function(d) { return [d.y, d.x]; }); @@ -28,7 +31,11 @@ var div = d3.select("#body").append("div") .style("opacity", 0); -function parseModelToJS(jsonString) { +//// Link with Grails GSP +var serverUrl = "" + +// Parse jsonString to jsonObject +function parseModelToJS(jsonString /*: string */) /*: Object */ { jsonString=jsonString.replace(/\"/g,'"'); //jsonString=jsonString.replace(/"/g, '"'); jsonString=jsonString.replace(/\n/g, ' '); // i.e. \n @@ -37,6 +44,30 @@ function parseModelToJS(jsonString) { return jsonObject } +/*:: + type D3JSON = { + name: string, + id: number, + angularLink: string, + type: string, + + loadedChildren: boolean, + loading: boolean, + + enumerations: ?Object, + + children: ?Array, + _children: ?Array, + + x: number, + y: number, + x0: number, + y0: number + } + */ + + +//// naming functions /** return Upper Case first letter * @param str * @returns {string} @@ -45,20 +76,30 @@ function ucFirst(str /*: string */) { return str.charAt(0) .toUpperCase() + str.substr(1) } + +function typeAndName(d /*: D3JSON */) { + return ucFirst(d.type) + " " + d.name +} + +//// Info functions + /** * Return HTML for info panel on the right from node data * @param d node data * @returns {string} */ -function info(d) { // d is data with fields name, type, angularLink, etc. +function info(d /*: D3JSON */) { // d is data with fields name, type, angularLink, etc. return "Name: " + "" + d.name + "" + "
    " + " (Click to see Advanced View)" + "
    " + "
    " + "Type: " + ucFirst(d.type) + "
    " + (d.enumerations ? enumerate(d.enumerations): "") - - } +/** + * Return HTML for enumeration from Map + * @param map + * @returns {string} + */ function enumerate(map) { var ret = "Enumerations:
      " Object.keys(map).forEach(function(key) { @@ -68,14 +109,16 @@ function enumerate(map) { return ret = ret + "
    " } +function writeMessage(text) { + $('#d3-info-messages').append("
  • " + (new Date().toLocaleString()) + ": " + text + "
  • ") +} -var serverUrl = "" /** * Initialize * @param json */ -function initD3(json) { +function initD3(json /*: D3JSON */) { root = json; root.x0 = h / 2; root.y0 = 0; @@ -97,6 +140,7 @@ function initD3(json) { update(root); }; +// node colours var coloursMap = { "dataModel": "blueviolet", "dataClass": "blue", @@ -104,11 +148,10 @@ var coloursMap = { "dataType": "green" } - -function writeMessage(text) { - $('#d3-info-messages').append("
  • " + (new Date().toLocaleString()) + ": " + text + "
  • ") -} - +/** + * Update a node (source) + * @param source + */ function update(source) { var duration = d3.event && d3.event.altKey ? 5000 : 500; @@ -125,58 +168,71 @@ function update(source) { // Update the nodes… var svgNodes = vis.selectAll("g.node") - .data(nodeLayoutData, function(d) { return d.id || (d.id = ++i); }); + .data(nodeLayoutData, function(d) { return d.nodeId || (d.nodeId = ++i); }); /*:: type ChildrenData = { children: Array, - canAccessDataModel: boolean + canAccessDataModel: boolean, + caseHandled: boolean } */ + function onNodeClick(d) { - // ENTER any new nodes at the parent's previous position. - var nodeEnter = svgNodes.enter().append("svg:g") - .attr("class", "node") - .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) - .on("click", function(d) { + $('#d3-info-element').html(info(d)); // display info + + if (d.loadedChildren && !d.loading) { + toggle(d); + update(d); + } - $('#d3-info-element').html(info(d)); + else { - if (d.loadedChildren) { - if (!d.loading) { - toggle(d); - update(d); - } - } + if (!d.loading) { // i.e. !d.loadedChildren - else { - if (!d.loading) { - d.loading = true // try to prevent double-loading, although race conditions may still result if you click fast enough. - $.ajax({ - url: serverUrl + "/dataModel/basicViewChildrenData/" + d.type + "/" + d.id - }).then(function(data /*: ChildrenData */) { - - if (data.canAccessDataModel) { - d._children = data.children; - toggle(d); - update(d); - } + writeMessage("Loading children for " + typeAndName(d)) + d.loading = true // try to prevent double-loading, although race conditions may still result if you click fast enough. + $.ajax({ + url: serverUrl + "/dataModel/basicViewChildrenData/" + d.type + "/" + d.id + }).then(function(data /*: ChildrenData */) { - else { - writeMessage("You do not have access to the data model of " + ucFirst(d.type) + " " + d.name) - } + if (data.canAccessDataModel && data.caseHandled) { + d.children = null + d._children = data.children; + writeMessage("Loading children of " + typeAndName(d) + " succeeded!") + toggle(d); + update(d); + } - d.loadedChildren = true; - d.loading = false; + else { + if (!data.canAccessDataModel) { + writeMessage("You do not have access to the data model of " + typeAndName(d)) + } + else { // !data.caseHandled + writeMessage("Loading children is not handled for this case.") + } - }, function(jqXHR, textStatus, errorThrown) { // request failure - d.loading = false - }) - } + } + d.loadedChildren = true; + d.loading = false; + // end loading + }, function(jqXHR, textStatus, errorThrown) { // request failure + writeMessage("Loading children for " + typeAndName(d) + " failed with error message: " + errorThrown) + d.loading = false + // end loading + }) } - }); + + } + } + + // ENTER any new nodes at the parent's previous position. + var nodeEnter = svgNodes.enter().append("svg:g") + .attr("class", "node") + .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) + .on("click", onNodeClick); // add circles for each node nodeEnter.append("svg:circle") @@ -199,28 +255,27 @@ function update(source) { var maxStringLength = 10; + + function textShortLeftOfNode(d /*: D3JSON */) /*: boolean */ { + // return d.children || d._children + return (d.type == 'dataClass' || d.type == 'dataModel' || d.type == 'dataElement') + } + // Text/link for each node nodeEnter.append("svg:a") .attr("xlink:href", function(d) {return d.angularLink}) .attr("target", "_blank") .append("svg:text") - .attr("x", function(d) { return d.children || d._children ? -(radius + 5): radius + 5; }) .attr("dy", ".35em") - .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) + + .attr("x", function(d) { return textShortLeftOfNode(d) ? -(radius + 5): radius + 5; }) + .attr("text-anchor", function(d) { + return textShortLeftOfNode(d) ? "end" : "start"; + }) .text(function(d) { - return (d.name.length >= maxStringLength && (d.children || d._children)) ? d.name.substring(0,maxStringLength) + "..." : d.name; }) + return (d.name.length >= maxStringLength && textShortLeftOfNode(d)) ? d.name.substring(0,maxStringLength) + "..." : d.name; }) .style("fill-opacity", 1e-6) .style("font", "15px sans-serif"); - // .append("rect") - // .attr("x", 0) - // .attr("y", 0) - // .attr("height", 100) - // .attr("width", 200) - // .style("fill", "lightgreen") - // .attr("rx", 10) - // .attr("ry", 10); - - // nodeEnter @@ -232,7 +287,7 @@ function update(source) { nodeUpdate.select("circle") .attr("r", radius) - .style("stroke", function(d) { return d._children ? unopenedNodeBorderColour: coloursMap[d.type]; }) + .style("stroke", function(d) { return (d._children || !d.loadedChildren) ? unopenedNodeBorderColour: coloursMap[d.type]; }) .style("stroke-width", 3) .style("fill", function(d) { return coloursMap[d.type]}) ; diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index 48bdb73c3f..72caa33125 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -6,6 +6,7 @@ import org.modelcatalogue.core.asset.MicrosoftOfficeDocument import org.modelcatalogue.core.d3viewUtils.ChildrenData import org.modelcatalogue.core.d3viewUtils.D3ViewUtilsService import org.modelcatalogue.core.persistence.AssetGormService +import org.modelcatalogue.core.persistence.DataClassGormService import org.modelcatalogue.core.util.MetadataDomain import org.modelcatalogue.core.util.ParamArgs import org.modelcatalogue.core.util.SearchParams @@ -70,6 +71,8 @@ class DataModelController extends AbstractCatalogueE D3ViewUtilsService d3ViewUtilsService + DataClassGormService dataClassGormService + DataModelController() { super(DataModel, false) } @@ -293,12 +296,12 @@ class DataModelController extends AbstractCatalogueE long id = params.long('id') boolean canAccessDataModel = false + boolean caseHandled = true def children = [] if (type == 'dataModel') { DataModel dataModel = dataModelGormService.findById(id) - canAccessDataModel = dataModel if (canAccessDataModel) { @@ -308,9 +311,23 @@ class DataModelController extends AbstractCatalogueE } } + else if (type == 'dataClass') { + DataClass dataClass = dataClassGormService.findById(id) + canAccessDataModel = dataModelAclService.hasReadPermission(dataClass) + + if (canAccessDataModel) { + children = d3ViewUtilsService.dataClassD3JsonChildren(dataClass) + } + + } + + else { + caseHandled = false + } + render(contentType: 'text/json') { - new ChildrenData(children: children, canAccessDataModel: canAccessDataModel) + new ChildrenData(children: children, canAccessDataModel: canAccessDataModel, caseHandled: caseHandled) } } diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy index 433a60f037..9dc1d79340 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -62,21 +62,24 @@ class D3ViewUtilsService { ListWithTotalAndType dataClasses = dataClassService.getTopLevelDataClasses(filter, [toplevel: true, status: dataModel.status != ElementStatus.DEPRECATED ? 'active' : '']) - List unDataClassedDataElements = DataElement.findAllByDataModel(dataModel).findAll{ - dataElementService.countDataClassesOf(it) == 0 - } + def dataClassChildrenJson = dataClasses.items.collect {dataClass -> - dataClassD3Json(dataClass, 0) + dataClassD3Json(dataClass) } - def dataElementChildrenJson = unDataClassedDataElements.collect { - dataElementD3Json(it) - } +// List unDataClassedDataElements = DataElement.findAllByDataModel(dataModel).findAll{ +// dataElementService.countDataClassesOf(it) == 0 +// } +// def dataElementChildrenJson = unDataClassedDataElements.collect { +// dataElementD3Json(it) +// } - return dataClassChildrenJson + dataElementChildrenJson + return dataClassChildrenJson //+ dataElementChildrenJson + + // the dataElementChildrenJson seems to take a long time // TODO: Handle case where there are just DataTypes listed not connected to any DataElements } @@ -85,7 +88,9 @@ class D3ViewUtilsService { D3JSON ret = new D3JSON( name: dataElement.name, + id: dataElement.id, angularLink: angularLink(dataElement.dataModel.id, dataElement.id, DataElement), + loadedChildren: true, // Just load data type, no laziness type: lowerCamelCaseDomainName(DataElement) ) @@ -99,6 +104,7 @@ class D3ViewUtilsService { D3JSON dataTypeD3Json(DataType dataType) { D3JSON ret = new D3JSON( name: dataType.name, + id: dataType.id, angularLink: angularLink(dataType.dataModel.id, dataType.id, DataType), loadedChildren: true, // No children, so "already loaded children." type: lowerCamelCaseDomainName(DataType), @@ -111,45 +117,37 @@ class D3ViewUtilsService { } /** - * depth -1 means go as far as possible - * depth 0 means don't have any children - * depth 1 means have one layer of children - * @param dataClass - * @param depth - * @return + * @return DataClass D3JSON (children will be loaded later) */ - D3JSON dataClassD3Json(DataClass dataClass, int depth) { - - List dataElementsJson = [] - List childDataClassesJson = [] - - if (depth != 0) { - - dataElementsJson = dataClassService.getDataElementsIn(dataClass).collect{ - dataElementD3Json(it) - } - - childDataClassesJson = dataClassService.getChildDataClasses(dataClass).collect{ - dataClassD3Json(it, depth - 1) // recursive - } - - } // otherwise depth == 0, no children + D3JSON dataClassD3Json(DataClass dataClass) { D3JSON ret = new D3JSON( name: dataClass.name, + id: dataClass.id, angularLink: angularLink(dataClass.dataModel.id, dataClass.id, DataClass), type: lowerCamelCaseDomainName(DataClass), ) - List children = dataElementsJson + childDataClassesJson - if (children) { - ret.children = children + return ret + } + + List dataClassD3JsonChildren(DataClass dataClass) { + + List dataElementsJson = dataClassService.getDataElementsIn(dataClass).collect{ + dataElementD3Json(it) } - return ret + List childDataClassesJson = dataClassService.getChildDataClasses(dataClass).collect{ + dataClassD3Json(it) // recursive + } + + + List children = dataElementsJson + childDataClassesJson + + return children } } /** @@ -164,7 +162,7 @@ class D3JSON { String name long id - boolean loadedChildren = false + boolean loadedChildren = false // false by default; children will be loaded later. boolean loading = false String angularLink @@ -177,4 +175,5 @@ class D3JSON { class ChildrenData { List children boolean canAccessDataModel + boolean caseHandled } From 996f420bfe76ce9579c91aa0937c8ec24ad39f73 Mon Sep 17 00:00:00 2001 From: James Dai Date: Sat, 19 May 2018 13:27:16 +0100 Subject: [PATCH 26/46] Expose only certain functions from init.js --- .../javascripts/d3_data_model_view/init.js | 599 +++++++++--------- .../views/dataModel/d3_data_model_view.gsp | 8 +- 2 files changed, 314 insertions(+), 293 deletions(-) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js index efb2b3ccd6..554e37fce3 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/d3_data_model_view/init.js @@ -2,360 +2,375 @@ //= require_self // @flow -//// D3 Setup stuff -// dimensions of page -var m = [20, 120, 20, 120], - w = 1280 + 6000 - m[1] - m[3], - h = 800 - m[0] - m[2], - i = 0, // node ids - root; - -// layout generator -var tree = d3.layout.tree() - .size([h, w]); - -// transition stuff -var diagonal = d3.svg.diagonal() - .projection(function(d) { return [d.y, d.x]; }); - -// visualization pane -var vis = d3.select("#body").append("svg:svg") - .attr("width", w + m[1] + m[3]) - .attr("height", h + m[0] + m[2]) - .append("svg:g") - .attr("transform", "translate(" + (m[3] + 20) + "," + m[0] + ")"); - -// Define the div for the tooltip -var div = d3.select("#body").append("div") - .attr("class", "tooltip") - .style("opacity", 0); - - -//// Link with Grails GSP +// exposed variable var serverUrl = "" -// Parse jsonString to jsonObject -function parseModelToJS(jsonString /*: string */) /*: Object */ { - jsonString=jsonString.replace(/\"/g,'"'); - //jsonString=jsonString.replace(/"/g, '"'); - jsonString=jsonString.replace(/\n/g, ' '); // i.e. \n - jsonString=validator.unescape(jsonString);h // unescape e.g. & - var jsonObject=$.parseJSON(jsonString); - return jsonObject -} - -/*:: - type D3JSON = { - name: string, - id: number, - angularLink: string, - type: string, - - loadedChildren: boolean, - loading: boolean, - - enumerations: ?Object, - - children: ?Array, - _children: ?Array, - - x: number, - y: number, - x0: number, - y0: number +var initFunctions = (function() { + + //// D3 Setup stuff + // dimensions of page + var m = [20, 120, 20, 120], + w = 1280 + 6000 - m[1] - m[3], + h = 800 - m[0] - m[2], + i = 0, // node ids + root; + + // layout generator + var tree = d3.layout.tree() + .size([h, w]); + + // transition stuff + var diagonal = d3.svg.diagonal() + .projection(function(d) { return [d.y, d.x]; }); + + // visualization pane + var vis = d3.select("#body").append("svg:svg") + .attr("width", w + m[1] + m[3]) + .attr("height", h + m[0] + m[2]) + .append("svg:g") + .attr("transform", "translate(" + (m[3] + 20) + "," + m[0] + ")"); + + // Define the div for the tooltip + var div = d3.select("#body").append("div") + .attr("class", "tooltip") + .style("opacity", 0); + + + //// Link with Grails GSP + + // Parse jsonString to jsonObject + function parseModelToJS(jsonString /*: string */) /*: Object */ { + jsonString=jsonString.replace(/\"/g,'"'); + //jsonString=jsonString.replace(/"/g, '"'); + jsonString=jsonString.replace(/\n/g, ' '); // i.e. \n + jsonString=validator.unescape(jsonString);h // unescape e.g. & + var jsonObject=$.parseJSON(jsonString); + return jsonObject } - */ - - -//// naming functions -/** return Upper Case first letter - * @param str - * @returns {string} - */ -function ucFirst(str /*: string */) { - return str.charAt(0) .toUpperCase() + str.substr(1) -} - - -function typeAndName(d /*: D3JSON */) { - return ucFirst(d.type) + " " + d.name -} - -//// Info functions - -/** - * Return HTML for info panel on the right from node data - * @param d node data - * @returns {string} - */ -function info(d /*: D3JSON */) { // d is data with fields name, type, angularLink, etc. - return "Name: " + "" + d.name + "" + "
    " + - " (Click to see Advanced View)" + "
    " + "
    " + - "Type: " + ucFirst(d.type) + "
    " + - (d.enumerations ? enumerate(d.enumerations): "") -} - -/** - * Return HTML for enumeration from Map - * @param map - * @returns {string} - */ -function enumerate(map) { - var ret = "Enumerations:
      " - Object.keys(map).forEach(function(key) { - ret = ret + "
    • " + key + ": " + map[key] + "
    • " - console.log(key, map[key]); - }); - return ret = ret + "
    " -} - -function writeMessage(text) { - $('#d3-info-messages').append("
  • " + (new Date().toLocaleString()) + ": " + text + "
  • ") -} - - -/** - * Initialize - * @param json - */ -function initD3(json /*: D3JSON */) { - root = json; - root.x0 = h / 2; - root.y0 = 0; - $('#d3-info-data-model').html(info(root)); - - function toggleAll(d) { - if (d.children) { - d.children.forEach(toggleAll); - toggle(d); + + /*:: + type D3JSON = { + name: string, + id: number, + angularLink: string, + type: string, + + loadedChildren: boolean, + loading: boolean, + + enumerations: ?Object, + + children: ?Array, + _children: ?Array, + + x: number, + y: number, + x0: number, + y0: number } + */ + + + //// naming functions + /** return Upper Case first letter + * @param str + * @returns {string} + */ + function ucFirst(str /*: string */) { + return str.charAt(0) .toUpperCase() + str.substr(1) } - // Initialize the display to show a few nodes. - // toggle(root.children[1]); - // toggle(root.children[1].children[2]); - // toggle(root.children[9]); - // toggle(root.children[9].children[0]); - update(root); -}; + function typeAndName(d /*: D3JSON */) { + return ucFirst(d.type) + " " + d.name + } -// node colours -var coloursMap = { - "dataModel": "blueviolet", - "dataClass": "blue", - "dataElement": "gold", - "dataType": "green" -} + //// Info functions + + /** + * Return HTML for info panel on the right from node data + * @param d node data + * @returns {string} + */ + function info(d /*: D3JSON */) { // d is data with fields name, type, angularLink, etc. + return "Name: " + "" + d.name + "" + "
    " + + " (Click to see Advanced View)" + "
    " + "
    " + + "Type: " + ucFirst(d.type) + "
    " + + (d.enumerations ? enumerate(d.enumerations): "") + } -/** - * Update a node (source) - * @param source - */ -function update(source) { + /** + * Return HTML for enumeration from Map + * @param map + * @returns {string} + */ + function enumerate(map) { + var ret = "Enumerations:
      " + Object.keys(map).forEach(function(key) { + ret = ret + "
    • " + key + ": " + map[key] + "
    • " + console.log(key, map[key]); + }); + return ret = ret + "
    " + } - var duration = d3.event && d3.event.altKey ? 5000 : 500; + function writeMessage(text) { + $('#d3-info-messages').append("
  • " + (new Date().toLocaleString()) + ": " + text + "
  • ") + } - var radius = 7 - var unopenedNodeBorderColour = "orangered" + /** + * Initialize + * @param json + */ + function initD3(json /*: D3JSON */) { + root = json; + root.x0 = h / 2; + root.y0 = 0; + $('#d3-info-data-model').html(info(root)); + + function toggleAll(d) { + if (d.children) { + d.children.forEach(toggleAll); + toggle(d); + } + } - // Compute the new tree layout. - var nodeLayoutData = tree.nodes(root).reverse(); + // Initialize the display to show a few nodes. + // toggle(root.children[1]); + // toggle(root.children[1].children[2]); + // toggle(root.children[9]); + // toggle(root.children[9].children[0]); + + update(root); + }; + + // node colours + var coloursMap = { + "dataModel": "blueviolet", + "dataClass": "blue", + "dataElement": "gold", + "dataType": "green" + } - // Normalize for fixed-depth. - nodeLayoutData.forEach(function(d) { d.y = d.depth * 180; }); + /** + * Update a node (source) + * @param source + */ + function update(source) { - // Update the nodes… - var svgNodes = vis.selectAll("g.node") - .data(nodeLayoutData, function(d) { return d.nodeId || (d.nodeId = ++i); }); + var duration = d3.event && d3.event.altKey ? 5000 : 500; - /*:: - type ChildrenData = { - children: Array, - canAccessDataModel: boolean, - caseHandled: boolean - } - */ - function onNodeClick(d) { + var radius = 7 + var unopenedNodeBorderColour = "orangered" - $('#d3-info-element').html(info(d)); // display info - if (d.loadedChildren && !d.loading) { - toggle(d); - update(d); - } + // Compute the new tree layout. + var nodeLayoutData = tree.nodes(root).reverse(); - else { + // Normalize for fixed-depth. + nodeLayoutData.forEach(function(d) { d.y = d.depth * 180; }); - if (!d.loading) { // i.e. !d.loadedChildren + // Update the nodes… + var svgNodes = vis.selectAll("g.node") + .data(nodeLayoutData, function(d) { return d.nodeId || (d.nodeId = ++i); }); - writeMessage("Loading children for " + typeAndName(d)) - d.loading = true // try to prevent double-loading, although race conditions may still result if you click fast enough. - $.ajax({ - url: serverUrl + "/dataModel/basicViewChildrenData/" + d.type + "/" + d.id - }).then(function(data /*: ChildrenData */) { + /*:: + type ChildrenData = { + children: Array, + canAccessDataModel: boolean, + caseHandled: boolean + } + */ + function onNodeClick(d) { + + $('#d3-info-element').html(info(d)); // display info + + if (d.loadedChildren && !d.loading) { + toggle(d); + update(d); + } + + else { - if (data.canAccessDataModel && data.caseHandled) { - d.children = null - d._children = data.children; - writeMessage("Loading children of " + typeAndName(d) + " succeeded!") - toggle(d); - update(d); - } + if (!d.loading) { // i.e. !d.loadedChildren - else { - if (!data.canAccessDataModel) { - writeMessage("You do not have access to the data model of " + typeAndName(d)) + writeMessage("Loading children for " + typeAndName(d)) + d.loading = true // try to prevent double-loading, although race conditions may still result if you click fast enough. + $.ajax({ + url: serverUrl + "/dataModel/basicViewChildrenData/" + d.type + "/" + d.id + }).then(function(data /*: ChildrenData */) { + + if (data.canAccessDataModel && data.caseHandled) { + d.children = null + d._children = data.children; + writeMessage("Loading children of " + typeAndName(d) + " succeeded!") + toggle(d); + update(d); } - else { // !data.caseHandled - writeMessage("Loading children is not handled for this case.") + + else { + if (!data.canAccessDataModel) { + writeMessage("You do not have access to the data model of " + typeAndName(d)) + } + else { // !data.caseHandled + writeMessage("Loading children is not handled for this case.") + } + } - } + d.loadedChildren = true; + d.loading = false; + // end loading - d.loadedChildren = true; - d.loading = false; - // end loading + }, function(jqXHR, textStatus, errorThrown) { // request failure + writeMessage("Loading children for " + typeAndName(d) + " failed with error message: " + errorThrown) + d.loading = false + // end loading + }) + } - }, function(jqXHR, textStatus, errorThrown) { // request failure - writeMessage("Loading children for " + typeAndName(d) + " failed with error message: " + errorThrown) - d.loading = false - // end loading - }) } - } - } - // ENTER any new nodes at the parent's previous position. - var nodeEnter = svgNodes.enter().append("svg:g") - .attr("class", "node") - .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) - .on("click", onNodeClick); - - // add circles for each node - nodeEnter.append("svg:circle") - .attr("r", 1e-6) - .style("fill", function(d) { return !d.children ? coloursMap[d.type] /*"lightsteelblue"*/ : "#fff"; }) - // Tooltip: - .on("mouseover", function(d) { + // ENTER any new nodes at the parent's previous position. + var nodeEnter = svgNodes.enter().append("svg:g") + .attr("class", "node") + .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) + .on("click", onNodeClick); + + // add circles for each node + nodeEnter.append("svg:circle") + .attr("r", 1e-6) + .style("fill", function(d) { return !d.children ? coloursMap[d.type] /*"lightsteelblue"*/ : "#fff"; }) + // Tooltip: + .on("mouseover", function(d) { div.transition() .duration(0) .style("opacity", .9); div .html(d.name) .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY - 28) + "px"); - }) - .on("mouseout", function(d) { - div.transition() - .duration(500) - .style("opacity", 0); - }); + }) + .on("mouseout", function(d) { + div.transition() + .duration(500) + .style("opacity", 0); + }); - var maxStringLength = 10; + var maxStringLength = 10; - function textShortLeftOfNode(d /*: D3JSON */) /*: boolean */ { - // return d.children || d._children - return (d.type == 'dataClass' || d.type == 'dataModel' || d.type == 'dataElement') - } + function textShortLeftOfNode(d /*: D3JSON */) /*: boolean */ { + // return d.children || d._children + return (d.type == 'dataClass' || d.type == 'dataModel' || d.type == 'dataElement') + } - // Text/link for each node - nodeEnter.append("svg:a") - .attr("xlink:href", function(d) {return d.angularLink}) - .attr("target", "_blank") - .append("svg:text") - .attr("dy", ".35em") + // Text/link for each node + nodeEnter.append("svg:a") + .attr("xlink:href", function(d) {return d.angularLink}) + .attr("target", "_blank") + .append("svg:text") + .attr("dy", ".35em") - .attr("x", function(d) { return textShortLeftOfNode(d) ? -(radius + 5): radius + 5; }) - .attr("text-anchor", function(d) { - return textShortLeftOfNode(d) ? "end" : "start"; - }) - .text(function(d) { - return (d.name.length >= maxStringLength && textShortLeftOfNode(d)) ? d.name.substring(0,maxStringLength) + "..." : d.name; }) - .style("fill-opacity", 1e-6) - .style("font", "15px sans-serif"); + .attr("x", function(d) { return textShortLeftOfNode(d) ? -(radius + 5): radius + 5; }) + .attr("text-anchor", function(d) { + return textShortLeftOfNode(d) ? "end" : "start"; + }) + .text(function(d) { + return (d.name.length >= maxStringLength && textShortLeftOfNode(d)) ? d.name.substring(0,maxStringLength) + "..." : d.name; }) + .style("fill-opacity", 1e-6) + .style("font", "15px sans-serif"); - // TRANSITION nodes to their new position. - var nodeUpdate = svgNodes.transition() - .duration(duration) - .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); + // TRANSITION nodes to their new position. + var nodeUpdate = svgNodes.transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); - nodeUpdate.select("circle") - .attr("r", radius) - .style("stroke", function(d) { return (d._children || !d.loadedChildren) ? unopenedNodeBorderColour: coloursMap[d.type]; }) - .style("stroke-width", 3) - .style("fill", function(d) { return coloursMap[d.type]}) + nodeUpdate.select("circle") + .attr("r", radius) + .style("stroke", function(d) { return (d._children || !d.loadedChildren) ? unopenedNodeBorderColour: coloursMap[d.type]; }) + .style("stroke-width", 3) + .style("fill", function(d) { return coloursMap[d.type]}) ; - nodeUpdate.select("text") - .style("fill-opacity", 1); + nodeUpdate.select("text") + .style("fill-opacity", 1); - // TRANSITION EXITING nodes to the parent's new position. - var nodeExit = svgNodes.exit().transition() - .duration(duration) - .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) - .remove(); + // TRANSITION EXITING nodes to the parent's new position. + var nodeExit = svgNodes.exit().transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) + .remove(); - nodeExit.select("circle") - .attr("r", 1e-6); + nodeExit.select("circle") + .attr("r", 1e-6); - nodeExit.select("text") - .style("fill-opacity", 1e-6); + nodeExit.select("text") + .style("fill-opacity", 1e-6); - // Update the LINKS… - var link = vis.selectAll("path.link") - .data(tree.links(nodeLayoutData), function(d) { return d.target.id; }); + // Update the LINKS… + var link = vis.selectAll("path.link") + .data(tree.links(nodeLayoutData), function(d) { return d.target.id; }); - // ENTER any new links at the parent's previous position. - link.enter().insert("svg:path", "g") - .attr("class", "link") - .attr("d", function(d) { - var o = {x: source.x0, y: source.y0}; - return diagonal({source: o, target: o}); - }) - .transition() - .duration(duration) - .attr("d", diagonal); + // ENTER any new links at the parent's previous position. + link.enter().insert("svg:path", "g") + .attr("class", "link") + .attr("d", function(d) { + var o = {x: source.x0, y: source.y0}; + return diagonal({source: o, target: o}); + }) + .transition() + .duration(duration) + .attr("d", diagonal); - // Transition links to their new position. - link.transition() - .duration(duration) - .attr("d", diagonal); + // Transition links to their new position. + link.transition() + .duration(duration) + .attr("d", diagonal); - // Transition exiting links to the parent's new position. - link.exit().transition() - .duration(duration) - .attr("d", function(d) { - var o = {x: source.x, y: source.y}; - return diagonal({source: o, target: o}); - }) - .remove(); + // Transition exiting links to the parent's new position. + link.exit().transition() + .duration(duration) + .attr("d", function(d) { + var o = {x: source.x, y: source.y}; + return diagonal({source: o, target: o}); + }) + .remove(); - // Stash the old positions for transition. - nodeLayoutData.forEach(function(d) { - d.x0 = d.x; - d.y0 = d.y; - }); -} + // Stash the old positions for transition. + nodeLayoutData.forEach(function(d) { + d.x0 = d.x; + d.y0 = d.y; + }); + } -// Toggle children. -function toggle(d) { - if (d.children) { - d._children = d.children; - d.children = null; - } else { - d.children = d._children; - d._children = null; + // Toggle children. + function toggle(d) { + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } } -} + + return { + "parseModelToJS": parseModelToJS, + "initD3": initD3, + "writeMessage": writeMessage} +})() + +// exposed functions: +var parseModelToJS = initFunctions.parseModelToJS +var initD3 = initFunctions.initD3 +var writeMessage = initFunctions.writeMessage diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp index 1d01d05383..f2379b0760 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -14,10 +14,12 @@ - + %{--D3--}% + %{--Style--}% +