UNPKG

@domoinc/multiline-chart

Version:

MultiLineChart - Domo Widget

1,234 lines (1,090 loc) 66.3 kB
/*! 2016-02-04 * Copyright (c) 2016 Domo Apps Team;*/ var d3 = require('d3'); var SummaryNumber = require('@domoinc/summary-number'); var service = new SummaryNumber(); var da = {}; module.exports = da; //********************************************************************************** // Domo Fallback Images for avatars // This function can be called on an image (d3 selection) and it will check to see // if the user image has loaded. If it has not, it will display a gray box with the // user's initials appended to the center of it. // // Example Usage: // var image = d3.select('svg').append('image').attr({ // x: 0, // y: 0, // width: '50px', // height: '50px' // }) // .call(d3.avatar, '//s3.amazonaws.com/uifaces/faces/twitter/fffabs/128.jpg', 'John Doe'); // // Note: I'm using SVG's 'getBBox()' method to determine the size of the image so this function will // break in firefox if you image is hidden while this function is run. //********************************************************************************** d3.avatar = function (rawImage, url, name, initialsSize) { // Setup Variables var node = rawImage.node(), image = d3.select(node), bb = node.getBBox(), textSize; if (typeof initialsSize === 'number') { textSize = initialsSize + 'px'; } else if (typeof initialsSize === 'string') { textSize = initialsSize; } else { textSize = '14px'; } // Parent Element **EACH IMAGE MUST HAVE ITS OWN PARENT** var parent = d3.select(node.parentNode); // Background Rect var rect = parent.selectAll('rect.imageCover').data([name]); rect.enter().append("rect") .attr({ class: "imageCover", x: bb.x, y: bb.y, width: bb.width, height: bb.height }) .style({ fill: "#e4e5e5" }); rect.attr({ x: bb.x, y: bb.y, width: bb.width, height: bb.height }); // User initials. var init = parent.selectAll('text.nameInitials').data([name]); init.enter().append('text') .text(toInit(name)) .style({ "fill": "#8a8d8f", "font-size": textSize, "font-weight": 400, "font-family": "Open Sans", "text-anchor": "middle", "pointer-events": "none", }) .attr({ class: "nameInitials", x: (bb.x + bb.width / 2), y: (bb.y + bb.height / 2), dy: function(d) { return parseInt(d3.select(this).style('font-size'), 10) * 0.35; } }); init.attr({ x: (bb.x + bb.width / 2), y: (bb.y + bb.height / 2), }) .text(function(d){ return toInit(d); }); if (rawImage.tween) { rawImage.tween('size', function() { var fontSize = init.style('font-size'); var i = d3.interpolate(fontSize, textSize); return function(t) { var bb = node.getBBox(); rect.attr(bb); init.style({ "font-size": i(t) }) .attr({ x: (bb.x + bb.width / 2), y: (bb.y + bb.height / 2), dy: function(d) { return parseInt(d3.select(this) .style('font-size'), 10) * 0.35; } }); } }); } function onLoad() { image.style('opacity', 1); init.style('opacity', 0); rect.style('opacity', 0); } function onError() { image.style('opacity', 0); init.style('opacity', 1); rect.style('opacity', 1); } var newImage = new Image(); newImage.addEventListener('load', onLoad, false); newImage.addEventListener('error', onError, false); newImage.src = url; image.attr("xlink:href", newImage.src); /** * Converts a Full name to initials * @param {string} fullName The full name * @return {string} The initials of the full name. */ function toInit(fullName) { return fullName.split(' ') .map(function (s) { return s.charAt(0); }) .join('') .toUpperCase(); } return this; } d3.browser = { 'getInternetExplorerVersion': function() { var rv = -1; if (navigator.appName == 'Microsoft Internet Explorer') { var ua = navigator.userAgent; var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); if (re.exec(ua) != null) rv = parseFloat( RegExp.$1 ); } else if (navigator.appName == 'Netscape') { var ua = navigator.userAgent; var re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})"); if (re.exec(ua) != null) rv = parseFloat( RegExp.$1 ); } return rv; } }; //********************************************************************************** // Returns the rgb of a color, with a given alpha and background rgb. //********************************************************************************** da.CalcRGBWithAlphaOnBackground = function rgbWithAlpha (rgb, alpha, backgroundRGB) { var r = 0; var g = 0; var b = 0; //opacity*original + (1-opacity)*background r = Math.floor(alpha*rgb.r + (1-alpha)*backgroundRGB.r); g = Math.floor(alpha*rgb.g + (1-alpha)*backgroundRGB.g); b = Math.floor(alpha*rgb.b + (1-alpha)*backgroundRGB.b); return 'rgb(' + r + ',' + g + ',' + b + ')'; }; //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // Domo Careted Rectangle with rounded corners: This function returns a path element // that is a function of the parameters of the input width, height, radius, caret, x, y // if x and y are not sent in then the function will assume 0 and if other elemnt is // missing it will assume the default // 100, 50, 5, 10,0,0)) // // Example: // var tooltip_group = svg.append('g') // var bubble = tooltip_group.append('path') // .attr('d', d3.DomoCaretRect(100, 50, 5, 10, 0, 0)) // var bubble_text = tooltip_group.append('text') // .attr('transform', 'tranlate(0, -25)') // .style("text-anchor", "middle") // .text('Sample') //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- d3.DomoCaretRect = function (width, height, radius, caret, x, y){ //********************************************************************************** // Generation Function: This function returns a rounded Rect with a caret on the // bottom, it is used to create the pop up bubble //********************************************************************************** if(x === undefined){ x = 0; } if(y === undefined){ y = 0; } if(width === undefined){ width = 100; } if(height === undefined){ height = 50; } if(radius === undefined){ radius = 5; } if(caret === undefined){ caret = 10; } return "M" + (x) + "," + (y) + "l" + ""+ -caret/2 + " " + -(caret/2) + "h" + ((radius) - (width/2 - (caret)/2)) + "a" + radius + "," + radius + " 0 0 1 " + -radius +"," + -radius + "v" + (-height + (2*radius)) + "a" + radius + "," + radius + " 0 0 1 " + radius +"," + -radius + "h" + (width - (2*radius)) + "a" + radius + "," + radius + " 0 0 1 " + radius +"," + radius + "v" + (height - (2*radius)) + "a" + radius + "," + radius + " 0 0 1 " + -radius +"," + radius + "h" + ((radius) - (width/2 - (caret)/2)) + "l" + ""+ -caret/2 + " " + (caret/2) + "z"; } //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // CatigoryIndexGenerator Generator: // This extension generates categories for data. You send in the Parameters // number of categories and the range for min and max. The function then generates // the correlating number of categories based of the range and appends a no // data catigory to the beginning. // // Detailed Explaination: This means that if you requested 4 categories you’ll // actually get 5, with the zero catigory being -2 to zero, the 1 catigory containing // your min, and so on with the 5 catigory containing your max. // // After calling this function it returns an object containing a 2d array, you can // then call the following functions on the object: // 1. GetCatigoryIndexForValue(value) : which allows you to get the catigory // for any value that you send to the object // 2. UpdateCatigoryRanges(newNumCategories, newDataValueRange): Essentially // refactors the catigory object with a new number of categories and a new // data value range. // // Example: // var cat_object = d3.DomoCatigoryIndexGenerator(4, [0,100]) // // cat_object is now initialized // cat_object.getCatigoryIndexForValue(25) // // returns 2 // cat_object.getCatigoryIndexForValue(50) // // returns 3 // cat_object.getCatigoryIndexForValue(49.99) // // returns 2 // cat_object.getCatigoryIndexForValue(5000) // // returns -1 // console.log(cat_object.rangeForEachCatigory) // //Logs [ [-2, 0], [0, 25], [25, 50], [50, 75], [75, 101] ] // // on Evaluation it's Greater than or equal to the bottom but always less than the top // // //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- d3.DomoCatigoryIndexGenerator = function(numberOfCatigories, dataRange) { //********************************************************************************** // Returns an array of ranges. One range for each 'numCategories' given the // the min and max values specified in dataValueRange. // dataValueRange == [min, max] // numCategories == 5 //********************************************************************************** function genRangesInEachColorCatigory(dataValueRange, numCategories) { var rangesInEachColorCatigory = []; rangesInEachColorCatigory.push([-2, 0]); var deltaStep = Math.floor((dataValueRange[1] - dataValueRange[0]) / numCategories); var stepVal = dataValueRange[0]; //Append Catigory labels for (var i = 0; i < numCategories - 1; i++) { rangesInEachColorCatigory.push([stepVal, stepVal + deltaStep]); stepVal += deltaStep; } //Append Top Catigory label rangesInEachColorCatigory.push([stepVal, dataValueRange[1] + 1]); return rangesInEachColorCatigory; } //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- var cateGenerator = { numCategories: numberOfCatigories, range: dataRange, rangeForEachCatigory: null, //********************************************************************************** // getCatigoryIndexForValue: This function will convert a value within a given // range into and index. //********************************************************************************** getCatigoryIndexForValue: function(value) { if (cateGenerator.rangeForEachCatigory == null) cateGenerator.rangeForEachCatigory = genRangesInEachColorCatigory(cateGenerator.range, cateGenerator.numCategories); if (value == undefined) return 0; for (var i = 0; i < cateGenerator.rangeForEachCatigory.length; i++) { if (cateGenerator.rangeForEachCatigory[i][0] <= value && value < cateGenerator.rangeForEachCatigory[i][1]) return i; } console.log("Error: returning index of catigory wasn't found for value:" + value); return -1; }, //********************************************************************************** // Updates the catIndexGenerators valueRange, numberOfCatigories, and // the array of ranges in each catigory. // IE: call on data update //********************************************************************************** updateCatigoryRanges: function(newNumCategories, newDataValueRange) { cateGenerator.range = newDataValueRange; cateGenerator.numCategories = newNumCategories; cateGenerator.rangeForEachCatigory = genRangesInEachColorCatigory(newDataValueRange, newNumCategories); }, }; return cateGenerator; }; //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // ClipPath Generator: // Example: // usmChart._clip = d3.DomoClipPath(); // usmChart._clip.appendNewDefsAndDomoClipPath(usmChart._height,usmChart._width,usmChart.base); // usmChart.base.attr("clip-path", "url(#" + usmChart._clip.getTheRecentlyAppendedDomoClipPathsId() + ")"); // //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- d3.DomoClipPath = function() { var CLIP_ID = "DomoClipPath"; //********************************************************************************** // Searches Dom for a clipId. Return the clipId + (number of id's found plus one) // Thus generating a unique clipId. //********************************************************************************** function generateNewUniqueDomoClipPathId() { var numClipIds = d3.selectAll("[id^="+CLIP_ID+"]").size(); return CLIP_ID + "_" + numClipIds; } //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- var clipIt = { clipsId: null, recentRect: null, //********************************************************************************** // Appends a <defs> and <clippath> to the container. //********************************************************************************** appendNewDefsAndDomoClipPath: function (height, width, container) { clipIt.clipsId = generateNewUniqueDomoClipPathId(); clipIt.recentRect = container.append("defs") .append("clipPath") .attr("id", clipIt.clipsId) .append("rect") .attr("x", 0) .attr("y", 0); clipIt.updateHeightAndWidthOfClippingRect(height, width); return clipIt; }, //********************************************************************************** // Return the id that was generated during the last call to // appendNewDefsAndDomoClipPath //********************************************************************************** getTheRecentlyAppendedDomoClipPathsId: function () { return clipIt.clipsId; }, //********************************************************************************** // This function will update the cliping rectangle height and width. //********************************************************************************** updateHeightAndWidthOfClippingRect: function (height, width) { clipIt.height = (height < 0 ? 0 : height); clipIt.width = (width < 0 ? 0 : width); if (clipIt.recentRect == null) {console.log("clipIt recentRect not set."); return;} clipIt.recentRect.attr("height", clipIt.height) .attr("width", clipIt.width); }, }; return clipIt; }; //********************************************************************************** // Returns primary and secondary color ranges in an object so you can use the correct // color range given the input parameter of series count // Example: // var color_range = d3.DomoColorRange(5) // //Now color Range is an object containing two keys 'primary', and 'secondary' using // //the key will give you access to the array of colors. // //secondary colors are often used for text elements on the correcting primary color // //********************************************************************************** d3.DomoColorRange = function(seriesCount){ if(seriesCount <=4){ return {primary : ["#73B0D7","#BBE491","#FB8D34","#E45621"].slice(0,seriesCount), secondary : ["#D9EBFD","#387B26","#FDECAD","#FDECAD"].slice(0,seriesCount)}; }else if(seriesCount <= 17){ var colorRanges = { 5 : {primary : ["#73B0D7","#A0D771","#559E38","#FBAD56","#E45621"], secondary : ["#D9EBFD","#DDF4BA","#DDF4BA","#FDECAD","#FDECAD"]}, 6 : {primary : ["#90c4e4","#4E8CBA","#A0D771","#559E38","#FBAD56","#E45621"], secondary : ["#D9EBFD","#D9EBFD","#DDF4BA","#DDF4BA","#FDECAD","#FDECAD"]}, 7 : {primary : ["#90c4e4","#4E8CBA","#A0D771","#559E38","#FCCF84","#E45621","#FB8D34"], secondary : ["#D9EBFD","#D9EBFD","#DDF4BA","#DDF4BA","#A43724","#FDECAD","#FDECAD"]}, 8 : {primary : ["#90c4e4","#4E8CBA","#BBE491","#559E38","#80C25D","#FCCF84","#E45621","#FB8D34"], secondary : ["#D9EBFD","#D9EBFD","#387B26","#DDF4BA","#DDF4BA","#A43724","#FDECAD","#FDECAD"]}, 9 : {primary : ["#B7DAF5","#4E8CBA","#73B0D7","#BBE491","#559E38","#80C25D","#FCCF84","#E45621","#FB8D34"], secondary : ["#31689B","#D9EBFD","#D9EBFD","#387B26","#DDF4BA","#DDF4BA","#A43724","#FDECAD","#FDECAD"]}, 10 : {primary : ["#B7DAF5","#4E8CBA","#73B0D7","#BBE491","#559E38","#80C25D","#FDECAD","#FB8D34","#FCCF84","#E45621"], secondary : ["#31689B","#D9EBFD","#D9EBFD","#387B26","#DDF4BA","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD"]}, 11 : {primary : ["#B7DAF5","#4E8CBA","#73B0D7","#DDF4BA","#80C25D","#BBE491","#559E38","#FDECAD","#FB8D34","#FCCF84","#E45621"], secondary : ["#31689B","#D9EBFD","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD"]}, 12 : {primary : ["#D9EBFD","#73B0D7","#B7DAF5","#4E8CBA","#DDF4BA","#80C25D","#BBE491","#559E38","#FDECAD","#FB8D34","#FCCF84","#E45621"], secondary : ["#4E8CBA","#D9EBFD","#31689B","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD"]}, 13 : {primary : ["#D9EBFD","#73B0D7","#B7DAF5","#4E8CBA","#DDF4BA","#80C25D","#BBE491","#559E38","#FDECAD","#FB8D34","#FCCF84","#E45621","#FBAD56"], secondary : ["#4E8CBA","#D9EBFD","#31689B","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD","#FDECAD"]}, 14 : {primary : ["#D9EBFD","#73B0D7","#B7DAF5","#4E8CBA","#DDF4BA","#80C25D","#BBE491","#559E38","#A0D771","#FDECAD","#FB8D34","#FCCF84","#E45621","#FBAD56"], secondary : ["#4E8CBA","#D9EBFD","#31689B","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD","#FDECAD"]}, 15 : {primary : ["#D9EBFD","#73B0D7","#B7DAF5","#4E8CBA","#90c4e4","#DDF4BA","#80C25D","#BBE491","#559E38","#A0D771","#FDECAD","#FB8D34","#FCCF84","#E45621","#FBAD56"], secondary : ["#4E8CBA","#D9EBFD","#31689B","#D9EBFD","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD","#FDECAD"]}, 16 : {primary : ["#D9EBFD","#73B0D7","#B7DAF5","#4E8CBA","#90c4e4","#DDF4BA","#80C25D","#BBE491","#559E38","#A0D771","#FDECAD","#FB8D34","#FCCF84","#E45621","#FBAD56","#A43724"], secondary : ["#4E8CBA","#D9EBFD","#31689B","#D9EBFD","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD","#FDECAD","#FDECAD"]}, 17 : {primary : ["#D9EBFD","#73B0D7","#B7DAF5","#4E8CBA","#90c4e4","#DDF4BA","#80C25D","#BBE491","#559E38","#A0D771","#387B26","#FDECAD","#FB8D34","#FCCF84","#E45621","#FBAD56","#A43724"], secondary : ["#4E8CBA","#D9EBFD","#31689B","#D9EBFD","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#DDF4BA","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD","#FDECAD","#FDECAD"]}, }; return colorRanges[seriesCount]; }else{ return {primary : ["#D9EBFD","#73B0D7","#B7DAF5","#4E8CBA","#90c4e4","#31689B","#DDF4BA","#80C25D","#BBE491","#559E38","#A0D771","#387B26","#FDECAD","#FB8D34","#FCCF84","#E45621","#FBAD56","#A43724","#F3E4FE","#B391CA","#DDC8EF","#8F6DC0","#C5ACDE","#7940A1","#FCD7E6","#EE76BF","#FBB6DD","#CF51AC","#F395CD","#A62A92","#D8F4DE","#68BEA8","#ABE4CA","#46998A","#8DD5BE","#227872", "#FDDDDD","#FCBCB7","#FD9A93","#FD7F76","#E45850","#C92E25", "#F1F2F2","#CACBCC","#B0B1B2","#959899","#7B7E80","#54585A"].slice(0,seriesCount), secondary : ["#4E8CBA","#D9EBFD","#31689B","#D9EBFD","#D9EBFD","#D9EBFD","#559E38","#DDF4BA","#387B26","#DDF4BA","#DDF4BA","#DDF4BA","#E45621","#FDECAD","#A43724","#FDECAD","#FDECAD","#FDECAD","#8F6DC0","#F3E4FE","#7940A1","#F3E4FE","#F3E4FE","#F3E4FE","#CF51AC","#FCD7E6","#A62A92","#FCD7E6","#FCD7E6","#FCD7E6","#46998A","#D8F4DE","#227872","#D8F4DE","#D8F4DE","#D8F4DE", "#FD7F76","#E45850","#C92E25","#FDDDDD","#FCBCB7","#FD9A93", "#959899","#7B7E80","#54585A","#F1F2F2","#CACBCC","#B0B1B2"].slice(0,seriesCount)}; } }; //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // DomoFormatted Number Director: // This util can append a Domo formatted number and set its contents. // // Example: (http://jsfiddle.net/chrisclm09/gXjB5/) // var test = d3.select("#test"); // var t = d3.appendDomoFormattedNumberTextElementToContainer(test); // d3.setDomoFormattedTextElement(t, "$", 100000, 18, "DAYS", 10); //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- //********************************************************************************** // This function will append a text element to the given container with two // inner tspans for formatting later. //********************************************************************************** d3.appendDomoFormattedNumberTextElementToContainer = function(container) { var textElement = container.append("text"); textElement.append("tspan").attr("id", "largeTspan").classed("largeTspan", true); textElement.append("tspan").attr("id", "smallTspan").classed("smallTspan", true); return textElement; }; //********************************************************************************** // This function will set a DomoFormattedTextElement's text and size. // Prefix: Something to append to the front of the number, like $ // prefixSize: Size of the prefix text and the number. // number: number to be show, this will be formatted. // postfix: Something to be appended to the end of the number, like 'DAYS' or '%' // If there is no magnitude on the number i.e.:(10.0K) there will be no space // between the number and postfix: (10%) (10DAYS). // If there is a magnitude on the number a space will be added to separate // the magnitude and the postfix, as long as the postfix doesn't start with // a space. // Example: (magnitude, postfix) -> output // '' , 'DAYS' -> 10DAYS // 'K' , 'DAYS' -> 10K DAYS // '' , ' DAYS' -> 10 DAYS // 'K' , ' DAYS' -> 10K DAYS // postfixSize: Size of the magnitude if appended and the postfix string. // // Example: // (dom, '$', 1000, 18, 'USA', 10) -> "$1.00K USA" //********************************************************************************** d3.setDomoFormattedTextElement = function(domoElement, prefix, number, prefixSize, postfix, postfixSize) { var largeTspan = domoElement.select("#largeTspan"); var smallTspan = domoElement.select("#smallTspan"); var numberAndUnit = service.summaryNumberToObject(number); largeTspan .style("font-size", (prefixSize + 'px')) .text(prefix + String(numberAndUnit.prefix)); // smallTspan // .style("font-size", postfixSize) // .text(numberAndUnit.magnitude + // ((numberAndUnit.magnitude == '' || // (postfix && postfix.length && postfix[0] == ' ')) ? '' : ' ') + // postfix); smallTspan .style("font-size", (postfixSize + 'px')) .text(numberAndUnit.magnitude + ' ' + postfix); }; //********************************************************************************** // Return a str formatted number. // Example: // 1000 -> '1.00K' //********************************************************************************** d3.DomoFormatNumber = function (val) { return service.summaryNumber(val); } //../lib/moment.min.js required. //********************************************************************************** // This function will return an array of formatted dates between a given // date range. // Example: // [new Date(2014, 0, 1), new Date(2014, 0, 9)] -> //********************************************************************************** //********************************************************************************** // This function will determine the font size for a string given the amount of // room available to show it. // textElement: the selection of the text. // returns: font-size (i.e. "14px" - "0px") // Example: // bubblesGroup.append("text").attr("class", "bubbleTitle") // .attr("fill", "black") // .text("Hey you!") // .attr("font-size", function (d) // { // return d3.DomoGetFontSizeToFitTextInBounds(d3.select(this), 10, 10); // }) // . // . // //********************************************************************************** // d3.DomoGetFontSizeToFitTextInBounds = function(textElement, width, height) // { // var possibleTextSizes = ["14px", "12px", "10px", "8px", "0px"]; // var pTextIndex = 0; // var padding = 5; // var tempBox = null; // while (pTextIndex != possibleTextSizes.length - 1) // { // //Try a font size // textElement.attr("font-size", possibleTextSizes[pTextIndex]); // tempBox = textElement.node().getBBox(); // //Check if it fits // if (tempBox.width < width - padding && // tempBox.height < height - padding) // break; //Got it // else // pTextIndex++; //Try the next smaller size. // } // return possibleTextSizes[pTextIndex]; // }; // this edit makes it possible to have a 0px size returned, and allows you to pass a starting // size e.g. startingSize = "10px" will return text at 10px or smaller. d3.DomoGetFontSizeToFitTextInBounds = function(textElement, width, height, startingSize) { var possibleTextSizes = ["14px", "10px", "8px", "0.1px"]; var pTextIndex = possibleTextSizes.indexOf( startingSize ) || 0; var padding = 5; var tempBox = null; while (pTextIndex < possibleTextSizes.length) { //Try a font size textElement.style("font-size", possibleTextSizes[pTextIndex]); tempBox = textElement.node().getBBox(); //Check if it fits if (tempBox.width < width - padding && tempBox.height < height - padding) break; //Got it else pTextIndex++; //Try the next smaller size. } return possibleTextSizes[pTextIndex]; }; //********************************************************************************** // Same as above but let you pick the start size. // It will start as the passed in size and decrement that size by 2 until // the text fits in the passed in width and height minus a small amount of padding. //********************************************************************************** d3.DomoGetFontSizeToFitTextInBoundsStartAtSize = function(textElement, width, height, size) { var padding = 5; var tempBox = null; while (size > 0) { //Try a font size textElement.style("font-size", size + "px"); tempBox = textElement.node().getBBox(); //Check if it fits if (tempBox.width < width - padding && tempBox.height < height - padding) break; //Got it else size = size - 2; //Try the next smaller size. } return (size < 0 ? 0 : size); }; d3.domoIcons = { "pencil": function ( element ) { element.append("path") .style("fill", "#CBCBCC") .attr({ "d": "M10.2,2.9l-8.8,8.8h0l0,0h0l0,0L0,16l4.3-1.4l0,0l0,0l0,0l0,0l8.8-8.8L10.2,2.9z M3.4,12.1l-0.5-0.5l7.2-7.2l0.5,0.5L3.4,12.1z", }); element.append("rect") .attr({ "x": 12.2, "y": 0.4, "width": 3.3, "height":4.0, "transform": "matrix(0.707 -0.7072 0.7072 0.707 2.2664 10.3121)", }) .style({ "fill" : "#CBCBCC", "width" : 2.7, "height" : 4.1 }); element.append("rect") .attr({ "width" : 17, "height" : 17, "opacity" : 0.0, }) .style({ "fill" : "#ffddff" }) return element; }, "funnel": function ( element ) { // Funnel by Volodin Anton from The Noun Project element.append("path") .style({ "fill": "#444", }) .attr({ "d": "M87.5,27.292c0-6.944-16.789-12.576-37.501-12.576c-20.71,0-37.499,5.632-37.499,12.576c0,0.148,0.018,0.294,0.033,0.441 c-0.006,0.035-0.033,0.046-0.033,0.09c0,0.665,0,1.77,0,2.454c0,0.686,0.324,1.571,0.72,1.968c0.396,0.396,1.441,1.445,2.325,2.331 l27.111,27.188c0,8.948,0,19.865,0,20.237c0,0.634,1.196,3.283,7.345,3.283c6.147,0,7.343-2.489,7.343-3.283 c0-0.467,0-11.375,0-20.294l27.399-27.424c0.883-0.886,1.865-1.868,2.183-2.184c0.314-0.317,0.574-1.043,0.574-1.615 s0-1.678,0-2.455c0-0.069-0.034-0.095-0.046-0.149C87.481,27.685,87.5,27.489,87.5,27.292z M49.999,20.472 c18.605,0,29.207,4.472,31.473,6.82c-2.266,2.349-12.867,6.82-31.473,6.82c-18.604,0-29.205-4.472-31.47-6.82 C20.794,24.943,31.395,20.472,49.999,20.472z", }); return element; }, "magnifyingGlass": function ( element ) { // <path d="M16.6,15.1c1.2-1.6,2-3.5,2-5.6c0-5-4.1-9.1-9.1-9.1 S0.4,4.4,0.4,9.4s4.1,9.1,9.1,9.1c2.1,0,4.1-0.7,5.6-2l5.1,5.1c0.4,0.4,1,0.4,1.4,0l0.1-0.1c0.4-0.4,0.4-1,0-1.4L16.6,15.1z M9.5,16.6c-4,0-7.2-3.2-7.2-7.2s3.2-7.2,7.2-7.2s7.2,3.2,7.2,7.2S13.4,16.6,9.5,16.6z" style="fill: rgb(138, 141, 143);"></path> element.append("path") .style({ "fill": "rgb(138, 141, 143)", }) .attr({ "d": "M16.6,15.1c1.2-1.6,2-3.5,2-5.6c0-5-4.1-9.1-9.1-9.1 S0.4,4.4,0.4,9.4s4.1,9.1,9.1,9.1c2.1,0,4.1-0.7,5.6-2l5.1,5.1c0.4,0.4,1,0.4,1.4,0l0.1-0.1c0.4-0.4,0.4-1,0-1.4L16.6,15.1z M9.5,16.6c-4,0-7.2-3.2-7.2-7.2s3.2-7.2,7.2-7.2s7.2,3.2,7.2,7.2S13.4,16.6,9.5,16.6z", }); return element; }, "diamond": function(element, attr) { element.append('polygon') .attr('points', '4,0 0,5 4,10 8,5'); if(attr === undefined) { element.attr('fill', '#CBCBCC'); } else { element.attr(attr); } return element }, "help": function () { console.log( "d3.DomoIcons.pencil( element ): Draws a light grey pencil icon and returns the element passed in.\n" + "Example:\n" + "\td3.DomoIcons.pencil( d3.select('g') )\n" + "\t\t.attr('transform', 'translate(10,10)');" ); } } //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // Number Cleanup: // This function will take a number and format it to something with up to 3 digits // and a decimal point, as a result of this process something needs an abbreviation, // so 10000 becomes 10K, 10100 become 10.1K, and 10010 becomes 10.01K. // Thif function works regardless of if the number is a string or // if the param is an actual integer value // // Example: // var formattedNum = d3.DomoNumberCleanup(10000) // // returns “10K” // formattedNum = d3.DomoNumberCleanup(“10000”) // // returns “10K” // formattedNum = d3.DomoNumberCleanup(“1100000”) // // returns “1.1M" // // Test Cases: // Tested on PI // Tested on random numbers using a function like this: function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min;} // Tested on 10000, 1100000, 10010, 10100 //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- d3.DomoNumberCleanup = function (number, numOfDigits){ if (numOfDigits === undefined){ numOfDigits === 3 } var cleanedNum = d3.DomoFormatNum((+number), numOfDigits) return cleanedNum[0] + cleanedNum [1]; }; //********************************************************************************** // Formatting Function: This function (combined with d3.DomoShortenNum function below) // originally came from James. I've heavily modified it to take a number and format it to // something with up to 3 digits and a decimal point, as a result of this process // something needs an abbreviation, so 10000 becomes 10K 10100 become 10.1K // and 10010 becomes 10.0K. // // There is NO rounding. // This function operates on the idea that truncation doesn't falsely represent data // // // formatNum finds the value of the number and as long as it is lower than a Quintillion // it will call the function shortenNum on the value divided by highest possible whole // value either 10 to the 6th 10 to the 9th etc. up to ten to the 15th // returns array with number at index 0 and magnitude at index 1 //********************************************************************************** d3.DomoFormatNum = function(num, numToShow){ if (numToShow === undefined){ numToShow = 3 } if( num >= 1000000000000000){ return shortenNum((+num/1000000000000000), "Q", numToShow); }else if(num >= 1000000000000){ return shortenNum((+num/1000000000000), "T", numToShow); }else if(num >= 1000000000){ return shortenNum((+num/1000000000), "B", numToShow); }else if(num >= 1000000){ return shortenNum((+num/1000000), "M", numToShow); }else if(num >= 1000){ return shortenNum((+num/1000), "K", numToShow); }else{ return shortenNum(+num, '', numToShow); } //********************************************************************************** // ShortenNum function: // 1. Converts number to a string and stores the original decimal value // 2. If there is no decimal value then the number is just returned (for example) 1000 is 1K // 3. Uses the decimal to break the string into two parts // 4. combines the two parts without the decimal // 5. loops through two parts to truncate it down to 3 characters or less (decimal is not included) // 5a. loop is prevented from going to infinity by capping the amount of loops possible at 5000 // 6. Period is added back into it's original position // 7. If the last character is a period it removes the period // 8. Add the abbreviation sent in from the format number function //********************************************************************************** function shortenNum (number, abbrev, numberToShow){ if (numberToShow === undefined || numberToShow < 3){ numberToShow = 3 } var numberString = number + '' var indexOfPeriod = numberString.indexOf('.') if (indexOfPeriod === -1){ return [numberString, abbrev] } var everythingAfterDecimal = numberString.substring(indexOfPeriod + 1, numberString.length) if (everythingAfterDecimal.length > 2){ everythingAfterDecimal = everythingAfterDecimal.substring(0, 2) } var everythingBeforeDecimal = numberString.substring(0, indexOfPeriod); var result = everythingBeforeDecimal + everythingAfterDecimal var infinitePreventer = 0 while (result.length > numberToShow && infinitePreventer < 5000){ var length = result.length -1 result = result.substring(0, length) infinitePreventer ++; } result = result.substring(0, indexOfPeriod) + '.' + result.substring(indexOfPeriod, result.length) if ((result.length - 1) === result.indexOf('.')){ result = result.substring(0, result.length - 1) } return [result, abbrev] } }; //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // Round any corner of a rectangle you want: // Usage: append a path and use this function to build the "d" attribute. // Parameters: // x: x-coordinate // y: y-coordinate // w: width // h: height // r: corner radius // tl: top_left rounded? // tr: top_right rounded? // bl: bottom_left rounded? // br: bottom_right rounded? // example: /*\ |*| |*| d3.select("path") |*| .attr({ |*| "d": function (d, i) { |*| return d3.roundedRect( 0, 0, 100, 100, 5, true, false, false, true ); |*| } |*| }); |*| \*/ //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- d3.roundedRect = function(x, y, w, h, r, tl, tr, bl, br) { var retval; retval = "M" + (x + r) + "," + y; retval += "h" + (w - 2*r); if (tr) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + r; } else { retval += "h" + r; retval += "v" + r; } retval += "v" + (h - 2*r); if (br) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + r; } else { retval += "v" + r; retval += "h" + -r; } retval += "h" + (2*r - w); if (bl) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + -r; } else { retval += "h" + -r; retval += "v" + -r; } retval += "v" + (2*r - h); if (tl) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + -r; } else { retval += "v" + -r; retval += "h" + r; } retval += "z"; return retval; } //To use this utility, pass in any container with paths inside of it and it will convert the paths to a dot //matrix. It works really well on maps, but I haven't implemented a way for it to work with zoomable maps yet. //Anybody? d3.shapeToGrid = function(container){ var allPaths = container.selectAll("path"), i = 0, defs = d3.select('#defs'); allPaths.each(function(){ i++; var bb = this.getBBox(), svgBB = container.node().getBBox(), path = d3.select(this).attr("d"), color = d3.select(this).style("fill"), parent = d3.select(this.parentNode), defsClip = defs.append("clipPath").attr("id","clip"+i).append("path").attr('d',path), group = parent.append('g').attr('clip-path',"url(#clip"+i+")"), xCount = Math.ceil(bb.width / 5)+1, yCount = Math.ceil(bb.height / 5)+1, newX = Math.floor(bb.x/5)*5, newY = Math.floor(bb.y/5)*5; for (var cy = 0; cy < yCount; cy++){ for (var cx = 0; cx < xCount; cx++){ group.append("circle").attr("cx",cx*5+2.5+newX).attr("cy",cy*5+2.5+newY).attr("r",2).style("fill",color); } } d3.select(this).remove(); }); } // d3.domoStrings is an object of functions that will alter strings with font shrinking and truncating. // trunc(string, characters, useWordBoundary): The string to modify and the characters you want to truncate to. // Ex: .text(function(d){ return d3.domoStrings.trunc(d.name, 25) // truncToFit(textElement, width): textElement is the d3 selection of the text element and width is the width that you want the element to fit inside after truncating. // Ex: d3.selectAll("text").each(function(){ return d3.domoStrings.truncToFit(d3.select(this), 45); }); // shrinkToFit(textElement, width, height, size): textElement is the d3 selection of the text element, width is the target width to shrink to, height is the target height, size is the starting size of the font. // Ex: Ex: d3.selectAll("text").each(function(){ return d3.domoStrings.shrinkToFit(d3.select(this), 45, 25, 10); }); // shrinkToTrunc(textElement, width, size): textElement is the d3 selection of the text element, width is the target width to shrink to and size is used if you want to define a starting font size to shrink from. If size is null sizes start at 14 // Ex: d3.selectAll("text").each(function(){ return d3.domoStrings.truncToFit(d3.select(this), 45, 18); }); d3.domoStrings = { 'trunc': function(str, chars, useWordBoundary) { var toLong = str.length > chars, s_ = toLong ? str.substr(0, chars - 1) : str; s_ = useWordBoundary && toLong ? s_.substr(0, s_.lastIndexOf(' ')) : s_; s_ = toLong ? s_ + '...' : s_; return s_; }, 'truncToFitVertical': function(textElement, height) { padding = 5; var wasEdited = false; str = textElement.text(); tempBox = textElement.node() .getBoundingClientRect(); for (var i = str.length; i >= 0; i--) { textElement .text(str) tempbox = textElement.node() .getBoundingClientRect(); if (height - padding < tempbox.height) { str = str.substr(0, str.length - 1) wasEdited = true; } else { break; } } if (wasEdited === true) { str = str.substr(0, str.length - 1) + '...'; textElement.text(str); } else { return; } }, 'truncToFit': function(textElement, width) { padding = 5; var wasEdited = false; str = textElement.text(); tempBox = textElement.node() .getBoundingClientRect(); for (var i = str.length; i >= 0; i--) { textElement .text(str) tempbox = textElement.node() .getBoundingClientRect(); if (width - padding < tempbox.width) { str = str.substr(0, str.length - 1) wasEdited = true; } else { break; } } if (wasEdited === true) { str = str.substr(0, str.length - 1) + '...'; textElement.text(str); } else { return; } }, 'shrinkToFit': function(textElement, width, height, size) { var padding = 5, tempBox; if (size) { while (size > 6) { //Try a font size textElement.style("font-size", size + "px"); tempBox = textElement.node() .getBoundingClientRect(); //Check if it fits if (tempBox.width < width - padding && tempBox.height < height - padding) break; else size = size - 2; } if (size < 8) { textElement.style("font-size", "0px"); } } else { var possibleTextSizes = ["14px", "12px", "10px", "8px", "0px"]; var pTextIndex = 0; while (pTextIndex !== possibleTextSizes.length - 1) { //Try a font size textElement.style("font-size", possibleTextSizes[pTextIndex] + 'px'); tempBox = textElement.node() .getBoundingClientRect(); //Check if it fits if (tempBox.width < width - padding && tempBox.height < height - padding) break; //Got it else pTextIndex++; //Try the next smaller size. } if (pTextIndex === possibleTextSizes.length - 1) { textElement.style("font-size", "0px"); } } }, 'shrinkToTrunc': function(textElement, width, height, size, useWordBoundary) { str = textElement.text(); var tempBox, padding = 5; if (size) { while (size > 6) { //Try a font size textElement.style("font-size", size + "px"); tempBox = textElement.node() .getBoundingClientRect(); //Check if it fits if (tempBox.width < width - padding && tempBox.height < height - padding) break; else size = size - 2; } if (size < 8) //still doesnt fit with just font shrinking, now to truncate { textElement.style("font-size", "8px"); d3.domoStrings.truncToFit(textElement, width) } } else { var possibleTextSizes = ["14px", "12px", "10px", "8px", "0px"]; var pTextIndex = 0; while (pTextIndex !== possibleTextSizes.length - 1) { //Try a font size textElement.style("font-size", possibleTextSizes[pTextIndex] + 'px'); tempBox = textElement.node() .getBoundingClientRect(); //Check if it fits if (tempBox.width < width - padding && tempBox.height < height - padding) break; //Got it else pTextIndex++; //Try the next smaller size. } //Once we hit the end of our array of sizes, start truncating if (pTextIndex === possibleTextSizes.length - 1) { //once again set font size to 8 to begin with textElement.style("font-size", "8px"); d3.domoStrings.truncToFit(textElement, width) } } }, 'wrapText': function(textElement, width) { var padding = 5; var str = textElement.text(); var dy = textElement.node() .getBoundingClientRect() .height; var words = str.split(' '); if (words.length <= 1) { return; } var lines = []; var curLine = words[0]; var testLine = curLine; for (var i = 1; i < words.length; i++) { testLine += ' ' + words[i]; textElement.text(testLine); var testBox = textElement.node() .getBoundingClientRect(); if (testBox.width <= width - padding) { curLine = testLine; } else { lines.push(curLine); curLine = words[i]; testLine = curLine; } if (i === words.length - 1) { lines.push(curLine); } } textElement.text(''); for (i = 0; i < lines.length; i++) { var tspan = textElement.append('tspan') .text(lines[i]); if (i > 0) { tspan.attr({ 'x': 0 + 'px', 'dy': dy + 'px' }) } } }, 'help': function() { console.log('d3.domoStrings is an object of functions that will alter strings with font shrinking and truncating.\n\ntrunc(string, characters, useWordBoundary): The string to modify and the characters you want to truncate to.\nEx: .text(function(d){ return d3.domoStrings.trunc(d.name, 25)\n\ntruncToFit(textElement, width): textElement is the d3 selection of the text element and width is the width that you want the element to fit inside after truncating.\nEx: d3.selectAll("text").each(function(){ return d3.domoStrings.truncToFit(d3.select(this), 45); });\n\nshrinkToFit(textElement, width, height, size): textElement is the d3 selection of the text element, width is the target width to shrink to, height is the target height, size is the starting size of the font.\nEx: Ex: d3.selectAll("text").each(function(){ return d3.domoStrings.shrinkToFit(d3.select(this), 45, 25, 10); });\n\nshrinkToTrunc(textElement, width, size): textElement is the d3 selection of the text element, width is the target width to shrink to and size is used if you want to define a starting font size to shrink from. If size is null sizes start at 14\nEx: d3.selectAll("text").each(function(){ return d3.domoStrings.truncToFit(d3.select(this), 45, 18); });') } } //********************************************************************************** // This function will right-align a text element, based on its current position and content // This is especially useful in aligning text elements created in Illustrator //********************************************************************************** d3.rightAlignText = function(textElem) { var bounds = textElem.node() .getBBox(); textElem.attr('text-anchor', 'end') .attr('transform', function() { return d3.select(this) .attr('transform') + 'translate(' + (bounds.width) + ',0)'; }); return textElem; } //********************************************************************************** // This function will center-align a text element, based on its current position and content // This is especially useful in aligning text elements created in Illustrator //********************************************************************************** d3.centerAlignText = function(textElem) { textElem.attr("text-anchor", "middle") .attr("transform", function() { var bounds = textElem.node() .getBBox(); return d3.select(this) .attr("transform") + "translate(" + ((bounds.width / 2)) + ",0)"; }); return textElem; } //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- // Is a end transition such that it will call your callback function at the end // of all the transitions occuring. // Example: // d3.selectAll("circles").transition() // .delay(750) // .duration(300) // .attr("r", 10) // .call(d3.DomoTransitionEndAll, function () // { // //Do Somthing, all circles now have a radius of 10. // }); //---------------------------------------------------------------------------------- //---------------------------------------------------------------------------------- d3.DomoTransitionEndAll = function (transition, callback) { var n = 0; transition .each(function() { ++n; }) .each("end", function() { if (!--n) callback.apply(this, arguments); }); }; //********************************************************************************** // This function will truncate a given text string to the given number of // characters. // It will also give you the option of truncating words on a word boundary. //********************************************************************************** function domoUtilTruncateStringToNumCharacters(string, numCharacters, useWordBoundary) { console.warn("The domoUtilTruncateStringToNumCharacters method has been deprecated. Use d3.domoStrings.trunc instead.") var toLong = string.length > numCharacters; var s_ = toLong ? string.substr(0, numCharacters - 1) : string;