escher-vis
Version:
Escher: A Web Application for Building, Sharing, and Embedding Data-Rich Visualizations of Biological Pathways
1,133 lines (1,015 loc) • 31.5 kB
JavaScript
/* global Blob, XMLSerializer, Image, btoa */
var vkbeautify = require('vkbeautify')
var _ = require('underscore')
var d3_json = require('d3-request').json
var d3_text = require('d3-request').text
var d3_csvParseRows = require('d3-dsv').csvParseRows
var d3_selection = require('d3-selection').selection
try {
var saveAs = require('file-saver').saveAs
} catch (e) {
console.warn('Not a browser, so FileSaver.js not available.')
}
module.exports = {
set_options: set_options,
remove_child_nodes: remove_child_nodes,
load_css: load_css,
load_files: load_files,
load_the_file: load_the_file,
make_class: make_class,
setup_defs: setup_defs,
draw_an_object: draw_an_object,
draw_a_nested_object: draw_a_nested_object,
make_array: make_array,
make_array_ref: make_array_ref,
compare_arrays: compare_arrays,
array_to_object: array_to_object,
clone: clone,
extend: extend,
unique_concat: unique_concat,
unique_strings_array: unique_strings_array,
debounce: debounce,
object_slice_for_ids: object_slice_for_ids,
object_slice_for_ids_ref: object_slice_for_ids_ref,
c_plus_c: c_plus_c,
c_minus_c: c_minus_c,
c_times_scalar: c_times_scalar,
download_json: download_json,
load_json: load_json,
load_json_or_csv: load_json_or_csv,
download_svg: download_svg,
download_png: download_png,
rotate_coords_recursive: rotate_coords_recursive,
rotate_coords: rotate_coords,
get_angle: get_angle,
to_degrees: to_degrees,
to_radians_norm: to_radians_norm,
angle_for_event: angle_for_event,
distance: distance,
check_undefined: check_undefined,
compartmentalize: compartmentalize,
decompartmentalize: decompartmentalize,
mean: mean,
median: median,
quartiles: quartiles,
random_characters: random_characters,
generate_map_id: generate_map_id,
check_for_parent_tag: check_for_parent_tag,
name_to_url: name_to_url,
parse_url_components: parse_url_components,
get_document: get_document,
get_window: get_window,
d3_transform_catch: d3_transform_catch,
check_browser: check_browser
}
/**
* Check if Blob is available, and alert if it is not.
*/
function _check_filesaver() {
try {
var isFileSaverSupported = !!new Blob()
} catch (e) {
alert('Blob not supported')
}
}
function set_options(options, defaults, must_be_float) {
if (options === undefined || options === null) {
return defaults
}
var i = -1
var out = {}
for (var key in defaults) {
var has_key = ((key in options) &&
(options[key] !== null) &&
(options[key] !== undefined))
var val = (has_key ? options[key] : defaults[key])
if (must_be_float && key in must_be_float) {
val = parseFloat(val)
if (isNaN(val)) {
if (has_key) {
console.warn('Bad float for option ' + key)
val = parseFloat(defaults[key])
if (isNaN(val)) {
console.warn('Bad float for default ' + key)
val = null
}
} else {
console.warn('Bad float for default ' + key)
val = null
}
}
}
out[key] = val
}
return out
}
function remove_child_nodes(selection) {
/** Removes all child nodes from a d3 selection
*/
var node = selection.node()
while (node.hasChildNodes()) {
node.removeChild(node.lastChild)
}
}
function load_css(css_path, callback) {
var css = ""
if (css_path) {
d3_text(css_path, function(error, text) {
if (error) {
console.warn(error)
}
css = text
callback(css)
})
}
return false
}
function _ends_with (str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1
}
/**
* Load a file.
* @param {} t - this context for callback. Should be an object.
* @param {} files_to_load - A filename to load. Must be JSON or CSS.
* @param {} callback - Function to run after the file is loaded. Takes the
* arguments error and data.
* @param {} value - If the value is specified, just assign it and do not
* execute the ajax query.
*/
function load_the_file (t, file, callback, value) {
if (value) {
if (file) console.warn('File ' + file + ' overridden by value.')
callback.call(t, null, value)
return
}
if (!file) {
callback.call(t, 'No filename', null)
return
}
if (_ends_with(file, 'json')) {
d3_json(file, function(e, d) { callback.call(t, e, d) })
} else if (_ends_with(file, 'css')) {
d3_text(file, function(e, d) { callback.call(t, e, d) })
} else {
callback.call(t, 'Unrecognized file type', null)
}
return
}
function load_files (t, files_to_load, final_callback) {
/** Load multiple files asynchronously by calling utils.load_the_file.
t: this context for callback. Should be an object.
files_to_load: A list of objects with the attributes:
{ file: a_filename.json, callback: a_callback_fn }
File must be JSON or CSS.
final_callback: Function that runs after all files have loaded.
*/
if (files_to_load.length === 0) final_callback.call(t)
var i = -1,
remaining = files_to_load.length
while (++i < files_to_load.length) {
load_the_file(
t,
files_to_load[i].file,
function(e, d) {
this.call(t, e, d)
if (!--remaining) final_callback.call(t)
}.bind(files_to_load[i].callback),
files_to_load[i].value
)
}
}
/**
* Create a constructor that returns a new object with our without the 'new'
* keyword.
*
* Adapted from Hubert Kauker (MIT Licensed), John Resig (MIT Licensed).
* http://stackoverflow.com/questions/7892884/simple-class-instantiation
*/
function make_class () {
var is_internal
var constructor = function (args) {
if (this instanceof constructor) {
if (typeof this.init === 'function') {
this.init.apply(this, is_internal ? args : arguments)
}
} else {
is_internal = true
var instance = new constructor(arguments)
is_internal = false
return instance
}
}
return constructor
}
function setup_defs(svg, style) {
// add stylesheet
svg.select("defs").remove()
var defs = svg.append("defs")
// make sure the defs is the first node
var node = defs.node()
node.parentNode.insertBefore(node, node.parentNode.firstChild)
defs.append("style")
.attr("type", "text/css")
.text(style)
return defs
}
/**
* Run through the d3 data binding steps for an object. Also checks to make sure
* none of the values in the *object* are undefined, and ignores those.
*
* The create_function, update_function, and exit_function CAN modify the input
* data object.
*
* @param {} container_sel - A d3 selection containing all objects.
*
* @param {} parent_node_selector - A selector string for a subselection of
* container_sel.
*
* @param {} children_selector - A selector string for each DOM element to bind.
*
* @param {} object - An object to bind to the selection.
*
* @param {} id_key - The key that will be used to store object IDs in the bound
* data points.
*
* @param {} create_function - A function for enter selection. Create function
* must return a selection of the new nodes.
*
* @param {} update_function - A function for update selection.
*
* @param {} exit_function - A function for exit selection.
*/
function draw_an_object (container_sel, parent_node_selector, children_selector,
object, id_key, create_function, update_function,
exit_function) {
var draw_object = {}
for (var id in object) {
if (object[id] === undefined) {
console.warn('Undefined value for id ' + id + ' in object. Ignoring.')
} else {
draw_object[id] = object[id]
}
}
var sel = container_sel.select(parent_node_selector)
.selectAll(children_selector)
.data(make_array_ref(draw_object, id_key),
function (d) { return d[id_key] })
// enter: generate and place reaction
var update_sel = create_function
? create_function(sel.enter()).merge(sel)
: sel
// update: update when necessary
if (update_function) {
update_sel.call(update_function)
}
// exit
if (exit_function) {
sel.exit().call(exit_function)
}
}
/**
* Run through the d3 data binding steps for an object that is nested within
* another element with D3 data.
*
* The create_function, update_function, and exit_function CAN modify the input
* data object.
*
* @param {} container_sel - A d3 selection containing all objects.
*
* @param {} children_selector - A selector string for each DOM element to bind.
*
* @param {} object_data_key - A key for the parent object containing data for
* the new selection.
*
* @param {} id_key - The key that will be used to store object IDs in the bound
* data points.
*
* @param {} create_function - A function for enter selection. Create function
* must return a selection of the new nodes.
*
* @param {} update_function - A function for update selection.
*
* @param {} exit_function - A function for exit selection.
*/
function draw_a_nested_object (container_sel, children_selector, object_data_key,
id_key, create_function, update_function,
exit_function) {
var sel = container_sel.selectAll(children_selector)
.data(function(d) { return make_array_ref(d[object_data_key], id_key) },
function(d) { return d[id_key] })
// enter: generate and place reaction
var update_sel = (create_function ?
create_function(sel.enter()).merge(sel) :
sel)
// update: update when necessary
if (update_function) {
update_sel.call(update_function)
}
// exit
if (exit_function) {
sel.exit().call(exit_function)
}
}
function make_array(obj, id_key) { // is this super slow?
var array = []
for (var key in obj) {
// copy object
var it = clone(obj[key])
// add key as 'id'
it[id_key] = key
// add object to array
array.push(it)
}
return array
}
function make_array_ref(obj, id_key) {
/** Turn the object into an array, but only by reference. Faster than
make_array.
*/
var array = []
for (var key in obj) {
// copy object
var it = obj[key]
// add key as 'id'
it[id_key] = key
// add object to array
array.push(it)
}
return array
}
function compare_arrays(a1, a2) {
/** Compares two simple (not-nested) arrays.
*/
if (!a1 || !a2) return false
if (a1.length != a2.length) return false
for (var i = 0, l=a1.length; i < l; i++) {
if (a1[i] != a2[i]) {
// Warning - two different object instances will never be equal: {x:20} != {x:20}
return false
}
}
return true
}
function array_to_object(arr) {
/** Convert an array of objects to an object with all keys and values
that are arrays of the same length as arr. Fills in spaces with null.
For example, [ { a: 1 }, { b: 2 }] becomes { a: [1, null], b: [null, 2] }.
*/
// new object
var obj = {}
// for each element of the array
for (var i = 0, l = arr.length; i < l; i++) {
var column = arr[i],
keys = Object.keys(column)
for (var k = 0, nk = keys.length; k < nk; k++) {
var id = keys[k]
if (!(id in obj)) {
var n = []
// fill spaces with null
for (var j = 0; j < l; j++) {
n[j] = null
}
n[i] = column[id]
obj[id] = n
} else {
obj[id][i] = column[id]
}
}
}
return obj
}
/**
* Deep copy for array and object types. All other types are returned by
* reference.
* @param {T<Object|Array|*>} obj - The object to copy.
* @return {T} The copied object.
*/
function clone (obj) {
if (_.isArray(obj))
return _.map(obj, function(t) { return clone(t) })
else if (_.isObject(obj))
return _.mapObject(obj, function (t, k) { return clone(t) })
else
return obj
}
function extend(obj1, obj2, overwrite) {
/** Extends obj1 with keys/values from obj2. Performs the extension
cautiously, and does not override attributes, unless the overwrite
argument is true.
Arguments
---------
obj1: Object to extend
obj2: Object with which to extend.
overwrite: (Optional, Default false) Overwrite attributes in obj1.
*/
if (overwrite === undefined)
overwrite = false
for (var attrname in obj2) {
if (!(attrname in obj1) || overwrite) // UNIT TEST This
obj1[attrname] = obj2[attrname]
else
throw new Error('Attribute ' + attrname + ' already in object.')
}
}
function unique_concat (arrays) {
var new_array = []
arrays.forEach(function (a) {
a.forEach(function (x) {
if (new_array.indexOf(x) < 0) {
new_array.push(x)
}
})
})
return new_array
}
/**
* Return unique values in array of strings.
*
* http://stackoverflow.com/questions/1960473/unique-values-in-an-array
*/
function unique_strings_array (arr) {
var a = []
for (var i = 0, l = arr.length; i < l; i++) {
if (a.indexOf(arr[i]) === -1) {
a.push(arr[i])
}
}
return a
}
/**
* Returns a function, that, as long as it continues to be invoked, will not be
* triggered. The function will be called after it stops being called for N
* milliseconds. If "immediate" is passed, trigger the function on the leading
* edge, instead of the trailing.
*/
function debounce (func, wait, immediate) {
var timeout
return function () {
var context = this
var args = arguments
var later = function () {
timeout = null
if (!immediate) func.apply(context, args)
}
var callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
/**
* Return a copy of the object with just the given ids.
* @param {} obj - An object
* @param {} ids - An array of id strings
*/
function object_slice_for_ids (obj, ids) {
var subset = {}
var i = -1
while (++i < ids.length) {
subset[ids[i]] = clone(obj[ids[i]])
}
if (ids.length !== Object.keys(subset).length) {
console.warn('did not find correct reaction subset')
}
return subset
}
/**
* Return a reference of the object with just the given ids. Faster than
* object_slice_for_ids.
* @param {} obj - An object.
* @param {} ids - An array of id strings.
*/
function object_slice_for_ids_ref (obj, ids) {
var subset = {}
var i = -1
while (++i < ids.length) {
subset[ids[i]] = obj[ids[i]]
}
if (ids.length !== Object.keys(subset).length) {
console.warn('did not find correct reaction subset')
}
return subset
}
function c_plus_c (coords1, coords2) {
if (coords1 === null || coords2 === null ||
coords1 === undefined || coords2 === undefined) {
return null
}
return {
x: coords1.x + coords2.x,
y: coords1.y + coords2.y,
}
}
function c_minus_c (coords1, coords2) {
if (coords1 === null || coords2 === null ||
coords1 === undefined || coords2 === undefined) {
return null
}
return {
x: coords1.x - coords2.x,
y: coords1.y - coords2.y,
}
}
function c_times_scalar (coords, scalar) {
return {
x: coords.x * scalar,
y: coords.y * scalar,
}
}
/**
* Download JSON file in a blob.
*/
function download_json (json, name) {
// Alert if blob isn't going to work
_check_filesaver()
var j = JSON.stringify(json)
var blob = new Blob([j], { type: 'application/json' })
saveAs(blob, name + '.json')
}
/**
* Try to load the file as JSON.
* @param {} f - The file path
* @param {} callback - A callback function that accepts arguments: error, data.
* @param {} pre_fn (optional) - A function to call before loading the data.
* @param {} failure_fn (optional) - A function to call if the load fails or is
* aborted.
*/
function load_json (f, callback, pre_fn, failure_fn) {
// Check for the various File API support
if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
callback('The File APIs are not fully supported in this browser.', null)
}
var reader = new window.FileReader()
// Closure to capture the file information.
reader.onload = function (event) {
var result = event.target.result
var data
// Try JSON
try {
data = JSON.parse(result)
} catch (e) {
// If it failed, return the error
callback(e, null)
return
}
// If successful, return the data
callback(null, data)
}
if (pre_fn !== undefined && pre_fn !== null) {
try { pre_fn() }
catch (e) { console.warn(e) }
}
reader.onabort = function(event) {
try { failure_fn() }
catch (e) { console.warn(e) }
}
reader.onerror = function(event) {
try { failure_fn() }
catch (e) { console.warn(e) }
}
// Read in the image file as a data URL
reader.readAsText(f)
}
/**
* Try to load the file as JSON or CSV (JSON first).
* @param {String} f - The file path
* @param {Function} csv_converter - A function to convert the CSV output to equivalent JSON.
* @param {Function} callback - A callback function that accepts arguments: error, data.
* @param {} pre_fn (optional) - A function to call before loading the data.
* @param {} failure_fn (optional) - A function to call if the load fails or is
* aborted.
* @param {} debug_event (optional) - An event, with a string at
* event.target.result, to load as though it was the contents of a loaded file.
*/
function load_json_or_csv (f, csv_converter, callback, pre_fn, failure_fn,
debug_event) {
// Capture the file information.
var onload_function = function(event) {
var result = event.target.result
var data
var errors
// try JSON
try {
data = JSON.parse(result)
} catch (e) {
errors = 'JSON error: ' + e
// try csv
try {
data = csv_converter(d3_csvParseRows(result))
} catch (e) {
// if both failed, return the errors
callback(errors + '\nCSV error: ' + e, null)
return
}
}
// if successful, return the data
callback(null, data)
}
if (debug_event !== undefined && debug_event !== null) {
console.warn('Debugging load_json_or_csv')
return onload_function(debug_event)
}
// Check for the various File API support.
if (!(window.File && window.FileReader && window.FileList && window.Blob))
callback("The File APIs are not fully supported in this browser.", null)
var reader = new window.FileReader()
if (pre_fn !== undefined && pre_fn !== null) {
try { pre_fn(); }
catch (e) { console.warn(e); }
}
reader.onabort = function(event) {
try { failure_fn(); }
catch (e) { console.warn(e); }
}
reader.onerror = function(event) {
try { failure_fn(); }
catch (e) { console.warn(e); }
}
// Read in the image file as a data URL.
reader.onload = onload_function
reader.readAsText(f)
}
/**
* Download an svg file using FileSaver.js.
* @param {String} name - The filename (without extension)
* @param {D3 Selection} svg_sel - The d3 selection for the SVG element
* @param {Boolean} do_beautify - If true, then beautify the SVG output
*/
function download_svg (name, svg_sel, do_beautify) {
// Alert if blob isn't going to work
_check_filesaver()
// Make the xml string
var xml = (new XMLSerializer()).serializeToString(svg_sel.node())
if (do_beautify) xml = vkbeautify.xml(xml)
xml = ('<?xml version="1.0" encoding="utf-8"?>\n' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n' +
' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
xml)
// Save
var blob = new Blob([ xml ], { type: 'image/svg+xml' })
saveAs(blob, name + '.svg')
}
/**
* Download a png file using FileSaver.js.
* @param {String} name - The filename (without extension).
* @param {D3 Selection} svg_sel - The d3 selection for the SVG element.
* @param {Boolean} do_beautify - If true, then beautify the SVG output.
*/
function download_png (name, svg_sel, do_beautify) {
// Alert if blob isn't going to work
_check_filesaver()
// Make the xml string
var xml = new XMLSerializer().serializeToString(svg_sel.node())
if (do_beautify) xml = vkbeautify.xml(xml)
xml = ('<?xml version="1.0" encoding="utf-8"?>\n' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n' +
' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
xml)
// Canvas to hold the image
var canvas = document.createElement('canvas')
var context = canvas.getContext('2d')
// Get SVG size
var svg_size = svg_sel.node().getBBox()
var svg_width = svg_size.width + svg_size.x
var svg_height = svg_size.height + svg_size.y
// Canvas size = SVG size. Constrained to 10000px for very large SVGs
if (svg_width < 10000 && svg_height < 10000) {
canvas.width = svg_width
canvas.height = svg_height
} else {
if (canvas.width > canvas.height) {
canvas.width = 10000
canvas.height = 10000 * (svg_height / svg_width)
} else {
canvas.width = 10000 * (svg_width / svg_height)
canvas.height = 10000
}
}
// Image element appended with data
var base_image = new Image()
base_image.src = 'data:image/svg+xml;base64,' + btoa(xml)
base_image.onload = function () {
// Draw image to canvas with white background
context.fillStyle = '#FFF'
context.fillRect(0, 0, canvas.width, canvas.height)
context.drawImage(base_image, 0, 0, canvas.width, canvas.height)
// Save image
canvas.toBlob(function (blob) {
saveAs(blob, name + '.png')
})
}
}
function rotate_coords_recursive (coords_array, angle, center) {
return coords_array.map(function (c) {
return rotate_coords(c, angle, center)
})
}
/**
* Calculates displacement { x: dx, y: dy } based on rotating point c around
* center with angle.
*/
function rotate_coords (c, angle, center) {
var dx = Math.cos(-angle) * (c.x - center.x) +
Math.sin(-angle) * (c.y - center.y)
+ center.x - c.x
var dy = - Math.sin(-angle) * (c.x - center.x) +
Math.cos(-angle) * (c.y - center.y)
+ center.y - c.y
return { x: dx, y: dy }
}
/**
* Get the angle between coordinates
* @param {Object} coords - Array of 2 coordinate objects { x: 1, y: 1 }
* @return {Number} angle between 0 and 2PI.
*/
function get_angle (coords) {
var denominator = coords[1].x - coords[0].x
var numerator = coords[1].y - coords[0].y
if (denominator === 0 && numerator >= 0) {
return Math.PI/2
} else if (denominator === 0 && numerator < 0) {
return 3*Math.PI/2
} else if (denominator >= 0 && numerator >= 0) {
return Math.atan(numerator/denominator)
} else if (denominator >= 0) {
return (Math.atan(numerator/denominator) + 2*Math.PI)
} else {
return (Math.atan(numerator/denominator) + Math.PI)
}
}
function to_degrees (radians) {
return radians * 180 / Math.PI
}
/**
* Force to domain - PI to PI
*/
function _angle_norm (radians) {
if (radians < -Math.PI) {
radians = radians + Math.ceil(radians / (-2*Math.PI)) * 2*Math.PI
} else if (radians > Math.PI) {
radians = radians - Math.ceil(radians / (2*Math.PI)) * 2*Math.PI
}
return radians
}
/**
* Convert to radians, and force to domain -PI to PI
*/
function to_radians_norm (degrees) {
var radians = Math.PI / 180 * degrees
return _angle_norm(radians)
}
function angle_for_event (displacement, point, center) {
var gamma = Math.atan2((point.x - center.x), (center.y - point.y))
var beta = Math.atan2((point.x - center.x + displacement.x),
(center.y - point.y - displacement.y))
var angle = beta - gamma
return angle
}
function distance (start, end) {
return Math.sqrt(Math.pow(end.y - start.y, 2) + Math.pow(end.x - start.x, 2))
}
/**
* Report an error if any of the arguments are undefined. Call by passing in
* "arguments" from any function and an array of argument names.
*/
function check_undefined (args, names) {
names.map(function (name, i) {
if (args[i] === undefined) {
console.error('Argument is undefined: ' + String(names[i]))
}
})
}
function compartmentalize (bigg_id, compartment_id) {
return bigg_id + '_' + compartment_id
}
/**
* Returns an array of [bigg_id, compartment id]. Matches compartment ids with
* length 1 or 2. Return [ id, null ] if no match is found.
*/
function decompartmentalize (id) {
var reg = /(.*)_([a-z0-9]{1,2})$/;
var result = reg.exec(id)
return result !== null ? result.slice(1,3) : [ id, null ]
}
function mean (array) {
var sum = array.reduce(function (a, b) { return a + b })
var avg = sum / array.length
return avg
}
function median (array) {
array.sort(function(a, b) { return a - b })
var half = Math.floor(array.length / 2)
if(array.length % 2 == 1) {
return array[half]
} else {
return (array[half-1] + array[half]) / 2.0
}
}
function quartiles (array) {
array.sort(function (a, b) { return a - b })
var half = Math.floor(array.length / 2)
if (array.length === 1) {
return [
array[0],
array[0],
array[0],
]
} else if (array.length % 2 === 1) {
return [
median(array.slice(0, half)),
array[half],
median(array.slice(half + 1)),
]
} else {
return [
median(array.slice(0, half)),
(array[half-1] + array[half]) / 2.0,
median(array.slice(half)),
]
}
}
/**
* Generate random characters
*
* Thanks to @csharptest.net
* http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript
*/
function random_characters (num) {
var text = ''
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for (var i = 0; i < num; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}
function generate_map_id () {
return random_characters(12)
}
/**
* Check that the selection has the given parent tag.
* @param {D3 Selection|DOM Node} el - A D3 Selection or DOM Node to check.
* @param {String} tag - A tag name (case insensitive).
*/
function check_for_parent_tag (el, tag) {
// make sure it is a node
if (el instanceof d3_selection) {
el = el.node()
}
while (el.parentNode !== null) {
el = el.parentNode
if (el.tagName === undefined) {
continue
}
if (el.tagName.toLowerCase() === tag.toLowerCase()) {
return true
}
}
return false
}
/**
* Convert model or map name to url.
* @param {String} name - The short name, e.g. e_coli.iJO1366.central_metabolism.
* @param {String} download_url (optional) - The url to prepend.
*/
function name_to_url (name, download_url) {
if (download_url !== undefined && download_url !== null) {
// strip download_url
download_url = download_url.replace(/^\/|\/$/g, '')
name = [download_url, name].join('/')
}
// strip final path
return name.replace(/^\/|\/$/g, '') + '.json'
}
/**
* Parse the URL and return options based on the URL arguments.
*
* Adapted from http://stackoverflow.com/questions/979975/how-to-get-the-value-from-url-parameter
*
* @param {} the_window - A reference to the global window.
* @param {Object} options (optional) - an existing options object to which new
* options will be added. Overwrites existing arguments in options.
*/
function parse_url_components (the_window, options) {
if (_.isUndefined(options)) options = {}
var query = the_window.location.search.substring(1)
var vars = query.split('&')
for (var i = 0, l = vars.length; i < l; i++) {
var pair = vars[i].split('=')
var val = decodeURIComponent(pair[1])
// deal with array options
if (pair[0].indexOf('[]') === pair[0].length - 2) {
var o = pair[0].replace('[]', '')
if (!(o in options)) {
options[o] = []
}
options[o].push(val)
} else {
options[pair[0]] = val
}
}
return options
}
/**
* Get the document for the node
*/
function get_document (node) {
return node.ownerDocument
}
/**
* Get the window for the node
*/
function get_window (node) {
return get_document(node).defaultView
}
/**
* Get translation and rotation values for a transform string. This used to be
* in d3, but since v4, I just adapted a solution from SO:
*
* http://stackoverflow.com/questions/38224875/replacing-d3-transform-in-d3-v4
*
* To get skew and scale out, go back to that example.
*
* TODO rename function without "catch"
*
* @param {String} transform_attr - A transform string.
*/
function d3_transform_catch (transform_attr) {
if (transform_attr.indexOf('skew') !== -1 ||
transform_attr.indexOf('matrix') !== -1) {
throw new Error('d3_transform_catch does not work with skew or matrix')
}
var translate_res = (/translate\s*\(\s*([0-9.-]+)\s*,\s*([0-9.-]+)\s*\)/
.exec(transform_attr))
var tn = _.isNull(translate_res)
var tx = tn ? 0.0 : Number(translate_res[1])
var ty = tn ? 0.0 : Number(translate_res[2])
var rotate_res = (/rotate\s*\(\s*([0-9.-]+)\s*\)/
.exec(transform_attr))
var rn = _.isNull(rotate_res)
var r = rn ? 0.0 : Number(rotate_res[1])
var scale_res = (/scale\s*\(\s*([0-9.-]+)\s*\)/
.exec(transform_attr))
var sn = _.isNull(scale_res)
var s = sn ? 0.0 : Number(scale_res[1])
return { translate: [ tx, ty ], rotate: r, scale: s, }
// // Create a dummy g for calculation purposes only. This will new be appended
// // to the DOM and will be discarded once this function returns.
// var g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
// // Set the transform attribute to the provided string value.
// g.setAttributeNS(null, 'transform', transform_attr)
// // Consolidate the SVGTransformList containing all Try to a single
// // SVGTransform of type SVG_TRANSFORM_MATRIX and get its SVGMatrix.
// var matrix = g.transform.baseVal.consolidate().matrix
// // Below calculations are taken and adapted from the private func
// // transform/decompose.js of D3's module d3-interpolate.
// var a = matrix.a
// var b = matrix.b
// var c = matrix.c
// var d = matrix.d
// var e = matrix.e
// var f = matrix.f
// var scaleX = Math.sqrt(a * a + b * b)
// if (scaleX) {
// a /= scaleX
// b /= scaleX
// }
// if (a * d < b * c) {
// a = -a
// b = -b
// }
// return {
// translate: [ e, f ],
// rotate: Math.atan2(b, a) * Math.PI / 180,
// }
}
/**
* Look for name in the user agent string.
*/
function check_browser (name) {
var browser = function() {
// Thanks to
// http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
var ua = navigator.userAgent
var M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []
var tem
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || []
return 'IE '+ (tem[1] || '')
}
if (M[1] === 'Chrome') {
tem = ua.match(/\b(OPR|Edge)\/(\d+)/)
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera')
}
M = M[2] ? [ M[1], M[2] ]: [ navigator.appName, navigator.appVersion, '-?' ]
if ((tem = ua.match(/version\/(\d+)/i)) !== null) {
M.splice(1, 1, tem[1])
}
return M.join(' ')
}
try {
// navigator.userAgent is deprecated, so don't count on it
return browser().toLowerCase().indexOf(name) > -1
} catch (e) {
return false
}
}