mermaid/src/d3.js

494 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* global window */
//log.debug('Setting up d3');
var d3;
if (require) {
try {
d3 = require('d3');
} catch (e) {
//log.debug('Exception ... but ok');
//log.debug(e);
}
}
//log.debug(d3);
if (!d3) {
//if(typeof window !== 'undefined')
d3 = window.d3;
}
//if(typeof window === 'undefined'){
// window = {};
// window.d3 = d3;
//}
//log.debug('window');
//log.debug(window);
module.exports = d3;
/* jshint ignore:start */
/*
D3 Text Wrap
By Vijith Assar
http://www.vijithassar.com
http://www.github.com/vijithassar
@vijithassar
Detailed instructions at http://www.github.com/vijithassar/d3textwrap
*/
(function() {
// set this variable to a string value to always force a particular
// wrap method for development purposes, for example to check tspan
// rendering using a foreignobject-enabled browser. set to 'tspan' to
// use tspans and 'foreignobject' to use foreignobject
var force_wrap_method = false; // by default no wrap method is forced
force_wrap_method = 'tspans'; // uncomment this statement to force tspans
// force_wrap_method = 'foreignobjects'; // uncomment this statement to force foreignobjects
// exit immediately if something in this location
// has already been defined; the plugin will defer to whatever
// else you're doing in your code
if(d3.selection.prototype.textwrap) {
return false;
}
// double check the force_wrap_method flag
// and reset if someone screwed up the above
// settings
if(typeof force_wrap_method == 'undefined') {
var force_wrap_method = false;
}
// create the plugin method twice, both for regular use
// and again for use inside the enter() selection
d3.selection.prototype.textwrap = d3.selection.enter.prototype.textwrap = function(bounds, padding) {
// default value of padding is zero if it's undefined
var padding = parseInt(padding) || 0;
// save callee into a variable so we can continue to refer to it
// as the function scope changes
var selection = this;
// create a variable to store desired return values in
var return_value;
// extract wrap boundaries from any d3-selected rect and return them
// in a format that matches the simpler object argument option
var extract_bounds = function(bounds) {
// discard the nested array wrappers added by d3
var bounding_rect = bounds[0][0];
// sanitize the svg element name so we can test against it
var element_type = bounding_rect.tagName.toString();
// if it's not a rect, exit
if(element_type !== 'rect') {
return false;
// if it's a rect, proceed to extracting the position attributes
} else {
var bounds_extracted = {};
bounds_extracted.x = d3.select(bounding_rect).attr('x') || 0;
bounds_extracted.y = d3.select(bounding_rect).attr('y') || 0;
bounds_extracted.width = d3.select(bounding_rect).attr('width') || 0;
bounds_extracted.height = d3.select(bounding_rect).attr('height') || 0;
// also pass along the getter function
bounds_extracted.attr = bounds.attr;
}
return bounds_extracted;
}
// double check the input argument for the wrapping
// boundaries to make sure it actually contains all
// the information we'll need in order to wrap successfully
var verify_bounds = function(bounds) {
// quickly add a simple getter method so you can use either
// bounds.x or bounds.attr('x') as your notation,
// the latter being a common convention among D3
// developers
if(!bounds.attr) {
bounds.attr = function(property) {
if(this[property]) {
return this[property];
}
}
}
// if it's an associative array, make sure it has all the
// necessary properties represented directly
if(
(typeof bounds == 'object') &&
(typeof bounds.x !== 'undefined') &&
(typeof bounds.y !== 'undefined') &&
(typeof bounds.width !== 'undefined') &&
(typeof bounds.height !== 'undefined')
// if that's the case, then the bounds are fine
) {
// return the lightly modified bounds
return bounds;
// if it's a numerically indexed array, assume it's a
// d3-selected rect and try to extract the positions
} else if (
// first try to make sure it's an array using Array.isArray
(
(typeof Array.isArray == 'function') &&
(Array.isArray(bounds))
) ||
// but since Array.isArray isn't always supported, fall
// back to casting to the object to string when it's not
(Object.prototype.toString.call(bounds) === '[object Array]')
) {
// once you're sure it's an array, extract the boundaries
// from the rect
var extracted_bounds = extract_bounds(bounds);
return extracted_bounds;
} else {
// but if the bounds are neither an object nor a numerical
// array, then the bounds argument is invalid and you'll
// need to fix it
return false;
}
}
var apply_padding = function(bounds, padding) {
var padded_bounds = bounds;
if(padding !== 0) {
padded_bounds.x = parseInt(padded_bounds.x) + padding;
padded_bounds.y = parseInt(padded_bounds.y) + padding;
padded_bounds.width -= padding * 2;
padded_bounds.height -= padding * 2;
}
return padded_bounds;
}
// verify bounds
var verified_bounds = verify_bounds(bounds);
// modify bounds if a padding value is provided
if(padding) {
verified_bounds = apply_padding(verified_bounds, padding);
}
// check that we have the necessary conditions for this function to operate properly
if(
// selection it's operating on cannot be not empty
(selection.length == 0) ||
// d3 must be available
(!d3) ||
// desired wrapping bounds must be provided as an input argument
(!bounds) ||
// input bounds must validate
(!verified_bounds)
) {
// try to return the calling selection if possible
// so as not to interfere with methods downstream in the
// chain
if(selection) {
return selection;
// if all else fails, just return false. if you hit this point then you're
// almost certainly trying to call the textwrap() method on something that
// doesn't make sense!
} else {
return false;
}
// if we've validated everything then we can finally proceed
// to the meat of this operation
} else {
// reassign the verified bounds as the set we want
// to work with from here on; this ensures that we're
// using the same data structure for our bounds regardless
// of whether the input argument was a simple object or
// a d3 selection
bounds = verified_bounds;
// wrap using html and foreignObjects if they are supported
var wrap_with_foreignobjects = function(item) {
// establish variables to quickly reference target nodes later
var parent = d3.select(item[0].parentNode);
var text_node = parent.select('text');
var styled_line_height = text_node.style('line-height');
// extract our desired content from the single text element
var text_to_wrap = text_node.text();
// remove the text node and replace with a foreign object
text_node.remove();
var foreign_object = parent.append('foreignObject');
// add foreign object and set dimensions, position, etc
foreign_object
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
.attr('x', bounds.x)
.attr('y', bounds.y)
.attr('width', bounds.width)
.attr('height', bounds.height);
// insert an HTML div
var wrap_div = foreign_object
.append('xhtml:div')
// this class is currently hardcoded
// probably not necessary but easy to
// override using .classed() and for now
// it's nice to avoid a litany of input
// arguments
.attr('class', 'wrapped');
// set div to same dimensions as foreign object
wrap_div
.style('height', bounds.height)
.style('width', bounds.width)
// insert text content
.html(text_to_wrap);
if(styled_line_height) {
wrap_div.style('line-height', styled_line_height);
}
return_value = parent.select('foreignObject');
}
// wrap with tspans if foreignObject is undefined
var wrap_with_tspans = function(item) {
// operate on the first text item in the selection
var text_node = item[0];
var parent = text_node.parentNode;
var text_node_selected = d3.select(text_node);
// measure initial size of the text node as rendered
var text_node_height = text_node.getBBox().height;
var text_node_width = text_node.getBBox().width;
// figure out the line height, either from rendered height
// of the font or attached styling
var line_height;
var rendered_line_height = text_node_height;
var styled_line_height = text_node_selected.style('line-height');
if(
(styled_line_height) &&
(parseInt(styled_line_height))
) {
line_height = parseInt(styled_line_height.replace('px', ''));
} else {
line_height = rendered_line_height;
}
// only fire the rest of this if the text content
// overflows the desired dimensions
if(text_node_width > bounds.width) {
// store whatever is inside the text node
// in a variable and then zero out the
// initial content; we'll reinsert in a moment
// using tspan elements.
var text_to_wrap = text_node_selected.text();
text_node_selected.text('');
if(text_to_wrap) {
// keep track of whether we are splitting by spaces
// so we know whether to reinsert those spaces later
var break_delimiter;
// split at spaces to create an array of individual words
var text_to_wrap_array;
if(text_to_wrap.indexOf(' ') !== -1) {
var break_delimiter = ' ';
text_to_wrap_array = text_to_wrap.split(' ');
} else {
// if there are no spaces, figure out the split
// points by comparing rendered text width against
// bounds and translating that into character position
// cuts
break_delimiter = '';
var string_length = text_to_wrap.length;
var number_of_substrings = Math.ceil(text_node_width / bounds.width);
var splice_interval = Math.floor(string_length / number_of_substrings);
if(
!(splice_interval * number_of_substrings >= string_length)
) {
number_of_substrings++;
}
var text_to_wrap_array = [];
var substring;
var start_position;
for(var i = 0; i < number_of_substrings; i++) {
start_position = i * splice_interval;
substring = text_to_wrap.substr(start_position, splice_interval);
text_to_wrap_array.push(substring);
}
}
// new array where we'll store the words re-assembled into
// substrings that have been tested against the desired
// maximum wrapping width
var substrings = [];
// computed text length is arguably incorrectly reported for
// all tspans after the first one, in that they will include
// the width of previous separate tspans. to compensate we need
// to manually track the computed text length of all those
// previous tspans and substrings, and then use that to offset
// the miscalculation. this then gives us the actual correct
// position we want to use in rendering the text in the SVG.
var total_offset = 0;
// object for storing the results of text length computations later
var temp = {};
// loop through the words and test the computed text length
// of the string against the maximum desired wrapping width
for(var i = 0; i < text_to_wrap_array.length; i++) {
var word = text_to_wrap_array[i];
var previous_string = text_node_selected.text();
var previous_width = text_node.getComputedTextLength();
// initialize the current word as the first word
// or append to the previous string if one exists
var new_string;
if(previous_string) {
new_string = previous_string + break_delimiter + word;
} else {
new_string = word;
}
// add the newest substring back to the text node and
// measure the length
text_node_selected.text(new_string);
var new_width = text_node.getComputedTextLength();
// adjust the length by the offset we've tracked
// due to the misreported length discussed above
var test_width = new_width - total_offset;
// if our latest version of the string is too
// big for the bounds, use the previous
// version of the string (without the newest word
// added) and use the latest word to restart the
// process with a new tspan
if(new_width > bounds.width) {
if(
(previous_string) &&
(previous_string !== '')
) {
total_offset = total_offset + previous_width;
temp = {string: previous_string, width: previous_width, offset: total_offset};
substrings.push(temp);
text_node_selected.text('');
text_node_selected.text(word);
// Handle case where there is just one more word to be wrapped
if(i == text_to_wrap_array.length - 1) {
new_string = word;
text_node_selected.text(new_string);
new_width = text_node.getComputedTextLength();
}
}
}
// if we're up to the last word in the array,
// get the computed length as is without
// appending anything further to it
if(i == text_to_wrap_array.length - 1) {
text_node_selected.text('');
var final_string = new_string;
if(
(final_string) &&
(final_string !== '')
) {
if((new_width - total_offset) > 0) {new_width = new_width - total_offset}
temp = {string: final_string, width: new_width, offset: total_offset};
substrings.push(temp);
}
}
}
// append each substring as a tspan
var current_tspan;
var tspan_count;
// double check that the text content has been removed
// before we start appending tspans
text_node_selected.text('');
for(var i = 0; i < substrings.length; i++) {
var substring = substrings[i].string;
if(i > 0) {
var previous_substring = substrings[i - 1];
}
// only append if we're sure it won't make the tspans
// overflow the bounds.
if((i) * line_height < bounds.height - (line_height * 1.5)) {
current_tspan = text_node_selected.append('tspan')
.text(substring);
// vertical shift to all tspans after the first one
current_tspan
.attr('dy', function(d) {
if(i > 0) {
return line_height;
}
});
// shift left from default position, which
// is probably based on the full length of the
// text string until we make this adjustment
current_tspan
.attr('x', function() {
var x_offset = bounds.x;
if(padding) {x_offset += padding;}
return x_offset;
});
// .attr('dx', function() {
// if(i == 0) {
// var render_offset = 0;
// } else if(i > 0) {
// render_offset = substrings[i - 1].width;
// render_offset = render_offset * -1;
// }
// return render_offset;
// });
}
}
}
}
// position the overall text node, whether wrapped or not
text_node_selected.attr('y', function() {
var y_offset = bounds.y;
// shift by line-height to move the baseline into
// the bounds otherwise the text baseline would be
// at the top of the bounds
if(line_height) {y_offset += line_height;}
// shift by padding, if it's there
if(padding) {y_offset += padding;}
return y_offset;
});
// shift to the right by the padding value
text_node_selected.attr('x', function() {
var x_offset = bounds.x;
if(padding) {x_offset += padding;}
return x_offset;
});
// assign our modified text node with tspans
// to the return value
return_value = d3.select(parent).selectAll('text');
}
// variable used to hold the functions that let us
// switch between the wrap methods
var wrap_method;
// if a wrap method if being forced, assign that
// function
if(force_wrap_method) {
if(force_wrap_method == 'foreignobjects') {
wrap_method = wrap_with_foreignobjects;
} else if (force_wrap_method == 'tspans') {
wrap_method = wrap_with_tspans;
}
}
// if no wrap method is being forced, then instead
// test for browser support of foreignobject and
// use whichever wrap method makes sense accordingly
if(!force_wrap_method) {
if(typeof SVGForeignObjectElement !== 'undefined') {
wrap_method = wrap_with_foreignobjects;
} else {
wrap_method = wrap_with_tspans;
}
}
// run the desired wrap function for each item
// in the d3 selection that called .textwrap()
for(var i = 0; i < selection.length; i++) {
var item = selection[i];
wrap_method(item);
}
// return the modified nodes so we can chain other
// methods to them.
return return_value;
}
}
})();
/* jshint ignore:end */