UNPKG

mermaid

Version:

Markdownish syntax for generating flowcharts, sequence diagrams and gantt charts.

459 lines (458 loc) 23.3 kB
///** // * Created by knut on 2015-07-21. // */ ///* // // 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); // } // } // // if we're up to the last word in the array, // // get the computed length as is without // // appending anything further to it // else 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); // } // } // } // // // position the overall text node // 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;} // return y_offset; // }); // // shift to the right by the padding value // if(padding) { // text_node_selected // .attr('x', bounds.x) // ; // } // // // 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('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; // // }) // .attr('x', function() { // // return bounds.x; // }); // } // } // } // } // // 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 */