├── Commands ├── byCorrespondents.mmCommand ├── bySender.mmCommand ├── byTag.mmCommand └── byTime.mmCommand ├── README.md ├── Support ├── HTML │ ├── byCorrespondents │ │ ├── byCorrespondents.js │ │ └── index.html │ ├── bySender │ │ ├── bySender.js │ │ └── index.html │ ├── byTag │ │ ├── byTag.js │ │ └── index.html │ ├── byTime │ │ ├── byTime.js │ │ └── index.html │ ├── style.css │ └── utilities │ │ ├── d3.min.js │ │ ├── jquery-1.12.0.min.js │ │ ├── lodash-license.txt │ │ ├── lodash.min.js │ │ ├── saveSvgAsPng-license.txt │ │ ├── saveSvgAsPng.js │ │ ├── spectrum-license │ │ ├── spectrum.css │ │ ├── spectrum.js │ │ └── url_object.js └── bin │ └── display └── info.plist /Commands/byCorrespondents.mmCommand: -------------------------------------------------------------------------------- 1 | { 2 | name = 'Emails by correspondents'; 3 | input = 'formatted'; 4 | output = 'actions'; 5 | keyEquivalent = "^c"; 6 | uuid = '5D36DE69-0233-41B6-A148-896D020500BD'; 7 | formatString = '"${#any-address.#correspondent.name:${#any-address.#correspondent.address}}"'; 8 | separatorString = '\n'; 9 | executionMode = 'multipleMessages'; 10 | command = '#!/bin/bash\n"${MM_BUNDLE_SUPPORT}/bin/display" byCorrespondents "correspondent"\n'; 11 | } 12 | -------------------------------------------------------------------------------- /Commands/bySender.mmCommand: -------------------------------------------------------------------------------- 1 | { 2 | name = 'Emails by sender'; 3 | input = 'formatted'; 4 | output = 'actions'; 5 | keyEquivalent = "^b"; 6 | uuid = '87660C0C-72A1-4CD0-B102-0632890942E4'; 7 | 8 | formatString = '"${from.name:?${from.name/"/""/g}:${from.address/"/""/g}}","${from.address/"/""/g}","${subject/"/""/g}","${#mailer.name/"/""/g}",1'; 9 | executionMode = 'multipleMessages'; 10 | command = '#!/bin/bash\n"${MM_BUNDLE_SUPPORT}/bin/display" bySender "name,email,subjectLine,mailer,count"\n'; 11 | } 12 | -------------------------------------------------------------------------------- /Commands/byTag.mmCommand: -------------------------------------------------------------------------------- 1 | { 2 | name = 'Emails by tag'; 3 | input = 'formatted'; 4 | output = 'actions'; 5 | keyEquivalent = "^g"; 6 | uuid = '82B369F4-7281-4C55-93C3-71779ACBB439'; 7 | 8 | formatString = '"${from.name:?${from.name/"/""/g}:${from.address/"/""/g}}","${from.address/"/""/g}","${subject/"/""/g}","${#mailer.name/"/""/g}",${##tags.tag.#name},1'; 9 | executionMode = 'multipleMessages'; 10 | command = '#!/bin/bash\n"${MM_BUNDLE_SUPPORT}/bin/display" byTag "name,email,subjectLine,mailer,tag,count"\n'; 11 | } 12 | -------------------------------------------------------------------------------- /Commands/byTime.mmCommand: -------------------------------------------------------------------------------- 1 | { 2 | name = 'Emails by time'; 3 | input = 'formatted'; 4 | output = 'actions'; 5 | keyEquivalent = "^t"; 6 | uuid = 'C6CA01E4-9817-488B-8C23-28DA34D68643'; 7 | 8 | formatString = '"${from.name:?${from.name/"/""/g}:${from.address/"/""/g}}","${from.address/"/""/g}","${subject/"/""/g}","${#mailer.name/"/""/g}",${#date.#local},1'; 9 | executionMode = 'multipleMessages'; 10 | command = '#!/bin/bash\n"${MM_BUNDLE_SUPPORT}/bin/display" byTime "name,email,subjectLine,mailer,localDateTime,count"\n'; 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visualize bundle for [MailMate](http://freron.com/ "MailMate") 2 | 3 | Visualize email history and trends. 4 | 5 | ![Pie chart of selected emails by day of week](https://github.com/sheriferson/Visualize.mmBundle/blob/master/screenshots/by_time_pie_chart.png) 6 | 7 | ![Bar chart of selected emails by sender](https://github.com/sheriferson/Visualize.mmBundle/blob/master/screenshots/by_sender_bar_chart.png) 8 | 9 | ![Scatterplot of selected emails over time](https://github.com/sheriferson/Visualize.mmBundle/blob/master/screenshots/by_time_scatterplot.png) 10 | 11 | ![Bar chart of selected emails by tag](https://github.com/sheriferson/Visualize.mmBundle/blob/master/screenshots/screenshots/by_tag_bar_chart.png) 12 | 13 | You can change colors. 14 | 15 | ![Different color](https://github.com/sheriferson/Visualize.mmBundle/blob/master/screenshots/by_time_scatterplot_pink.png) 16 | 17 | And even plot a moving average instead of raw numbers. 18 | 19 | ![Moving average](https://github.com/sheriferson/Visualize.mmBundle/blob/master/screenshots/by_time_scatterplot_pink_movmean.png) 20 | 21 | # Installation 22 | 23 | You can install this bundle in MailMate by opening the preferences and going to the bundles tab. After installation it will be automatically updated for you. 24 | 25 | # Usage 26 | 27 | Select a few - or many! - messages in any folder or in your Inbox. 28 | 29 | With those messages selected, you have access to four visualization commands: 30 | 31 | - Emails by correspondents (shortcut: `^c`) 32 | - Emails by sender (shortcut: `^b`) 33 | - Emails by time (shortcut: `^t`) 34 | - Emails by tag (shortcut: `^g`) 35 | 36 | The plots will then be displayed in Safari. 37 | 38 | Hover over parts of the pie chart, bars in the bar chart, or dots in the scatterplot to display more information. 39 | 40 | Use the "Save as Image" buttons to produce `.png`s from the plots for your records. 41 | 42 | Have fun :) 43 | 44 | # License 45 | 46 | If not otherwise specified (see below), files in this repository fall under the following license: 47 | 48 | > Permission to copy, use, modify, sell and distribute this 49 | software is granted. This software is provided "as is" without 50 | express or implied warranty, and with no claim as to its 51 | suitability for any purpose. 52 | 53 | An exception is made for files in readable text which contain their own license information, or files where an accompanying file exists (in the same directory) with a “-license” suffix added to the base-name name of the original file, and an extension of txt, html, or similar. 54 | 55 | # Thanks 56 | 57 | To Benny, excellent developer of MailMate, for providing tons of help with the bundle. 58 | -------------------------------------------------------------------------------- /Support/HTML/byCorrespondents/byCorrespondents.js: -------------------------------------------------------------------------------- 1 | // color palette 2 | var color = d3.scale.ordinal() 3 | .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]); 4 | 5 | datafile = urlObject().parameters.csv_file 6 | 7 | d3.csv(datafile, function(data) { 8 | var emails = data 9 | pieBySender(emails) 10 | barBySender(emails) 11 | }) 12 | 13 | // .o .o o8o 14 | // .8' .8' `"' 15 | // .888888888888' oo.ooooo. oooo .ooooo. 16 | // .8' .8' 888' `88b `888 d88' `88b 17 | // .888888888888' 888 888 888 888ooo888 18 | // .8' .8' 888 888 888 888 .o 19 | // .8' .8' 888bod8P' o888o `Y8bod8P' 20 | // 888 21 | // o888o 22 | 23 | function pieBySender(emails) { 24 | 25 | // aggregate the count of emails per sender 26 | var data = d3.nest() 27 | .key(function(d) { return d.correspondent; }) 28 | .rollup(function(d) { 29 | return d3.sum(d, function(g) { return 1; }) 30 | }) 31 | .entries(emails); 32 | 33 | // sort the data in descending order of frequency 34 | data.sort(function compareNumbers(a, b) { 35 | return b.values - a.values; 36 | }) 37 | 38 | // plot the data into an svg 39 | var width = 600, 40 | height = 500, 41 | radius = Math.min(width, height) / 2; 42 | 43 | var arc = d3.svg.arc() 44 | .outerRadius(radius - 10) 45 | .innerRadius(radius - 160) 46 | 47 | var pie = d3.layout.pie() 48 | .sort(null) 49 | .value(function(d) { return d.values; }); 50 | 51 | var svg = d3.select("#pieBySender").append("svg") 52 | .attr('id', 'pie') // we'll use this to convert and download the image 53 | .attr("width", width) 54 | .attr("height", height) 55 | .append("g") 56 | .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); 57 | 58 | var g = svg.selectAll("arc") 59 | .data(pie(data)) 60 | .enter().append("g") 61 | .attr("class", "arc"); 62 | 63 | g.append("path") 64 | .attr("d", arc) 65 | .style("fill", function(d) { return color(d.data.key); }) 66 | .style('stroke', function(d) { return color(d.data.key); }) 67 | 68 | // we will use this value to only automatically show names of people 69 | // who have frequencies above a certain threshold 70 | var totalEmails = d3.sum(data, function(d) { return d.values; }) 71 | 72 | g.append("text") 73 | .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; }) 74 | .attr("dy", ".35em") 75 | .style("text-anchor", "middle") 76 | .style('fill', 'white') 77 | .style('font-family', 'Fira Sans') 78 | .style("font-size", 12) 79 | .filter(function(d) { return (d.data.values/totalEmails) > .06; }) 80 | .text(function(d) { return d.data.key; }) 81 | .attr('opacity', 0) 82 | .transition() 83 | .duration(600) 84 | .attr('opacity', 1) 85 | 86 | var tooltip = d3.select('#pieBySender').append('div') 87 | .style('position', 'absolute') 88 | .style('background', 'lightblue') 89 | .style('color', 'black') 90 | .style('padding', '0 10px') 91 | .style('opacity', 0) 92 | 93 | svg.selectAll('path') 94 | .on('mouseover', function(d) { 95 | d3.select(this).transition() 96 | .style('opacity', .8) 97 | tooltip.transition() 98 | .style('opacity', .9) 99 | 100 | tooltip.html(d.data.key + ", " + d.data.values) 101 | .style('left', (d3.event.pageX - 35) + 'px') 102 | .style('top', (d3.event.pageY - 30) + 'px') 103 | }) 104 | 105 | .on('mouseout', function(d) { 106 | d3.select(this).transition() 107 | .style('opacity', 1) 108 | tooltip.transition() 109 | .style('opacity', 0) 110 | }) 111 | } 112 | 113 | // .o .o .o8 114 | // .8' .8' "888 115 | // .888888888888' 888oooo. .oooo. oooo d8b 116 | // .8' .8' d88' `88b `P )88b `888""8P 117 | // .888888888888' 888 888 .oP"888 888 118 | // .8' .8' 888 888 d8( 888 888 119 | // .8' .8' `Y8bod8P' `Y888""8o d888b 120 | 121 | function barBySender(emails) { 122 | // aggregate the count of emails per sender 123 | var data = d3.nest() 124 | .key(function(d) { return d.correspondent; }) 125 | .rollup(function(d) { 126 | return d3.sum(d, function(g) { return 1; }) 127 | }) 128 | .entries(emails); 129 | 130 | // sort the data in descending order of frequency 131 | data.sort(function compareNumbers(a, b) { 132 | return b.values - a.values; 133 | }) 134 | 135 | // limit number of senders represented in bar graph to 50 136 | var originalSenderNumber = data.length 137 | if (originalSenderNumber > 50) 138 | { 139 | data = data.slice(0, 50) 140 | var excludedSenderNumber = originalSenderNumber - data.length 141 | 142 | if (excludedSenderNumber < 2) { 143 | var exSenderMessage = " sender couldn't fit in the bar graph and was excluded." 144 | } else { 145 | exSenderMessage = " correspondents couldn't fit in the bar graph and were excluded." 146 | } 147 | 148 | d3.select('#excludedSenders') 149 | .style('fill', '#000') 150 | .style('color', '#c43c35') 151 | .style('border-radius', '3px') 152 | .style('padding-left', '5px') 153 | .style('padding-right', '5px') 154 | .style('display', 'inline-block') 155 | .append('p') 156 | .html("" + 157 | excludedSenderNumber + 158 | "" + 159 | exSenderMessage) 160 | } else { 161 | d3.select('#excludedSenders').remove() 162 | } 163 | 164 | // o8o 165 | // `"' 166 | // ooo. .oo. .oo. .oooo. oooo d8b .oooooooo oooo ooo. .oo. 167 | // `888P"Y88bP"Y88b `P )88b `888""8P 888' `88b `888 `888P"Y88b 168 | // 888 888 888 .oP"888 888 888 888 888 888 888 169 | // 888 888 888 d8( 888 888 `88bod8P' 888 888 888 170 | // o888o o888o o888o `Y888""8o d888b `8oooooo. o888o o888o o888o 171 | // d" YD 172 | // "Y88888P' 173 | 174 | var margin = { top: 30, right: 40, bottom: 250, left: 70 } 175 | 176 | 177 | var width = 900 - margin.left - margin.right, 178 | height = 680 - margin.top - margin.bottom 179 | 180 | var xScale = d3.scale.ordinal().domain(d3.range(0,data.length)).rangeBands([0, width], .2); 181 | 182 | var maxFrequency = d3.max(data, function(d) { return d.values; }) 183 | 184 | var yScale = d3.scale.linear().domain([0, maxFrequency]).range([0, height]) 185 | 186 | 187 | // plot the data into an svg 188 | d3.selectAll('#barBySender').append('svg') 189 | .attr('id', 'barchart') 190 | .attr('width', width + margin.left + margin.right) // handling margins 191 | .attr('height', height + margin.top + margin.bottom) // handling margins 192 | .append('g') 193 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')' ) 194 | .selectAll('rect') 195 | .data(data) 196 | .enter() 197 | .append('rect') 198 | .style('fill', function(d, i) { return color(i); }) 199 | .attr('width', xScale.rangeBand()) 200 | .attr('height', function(d) { return yScale(d.values); }) 201 | .attr('y', function(d) { return height - yScale(d.values); }) 202 | .attr('x', function(d,i) { return xScale(i); }) 203 | 204 | var tooltip = d3.select('#barBySender').append('div') 205 | .style('position', 'absolute') 206 | .style('background', 'lightblue') 207 | .style('color', 'black') 208 | .style('padding', '0 10px') 209 | .style('opacity', 0) 210 | 211 | d3.selectAll('rect') 212 | .on('mouseover', function(d) { 213 | d3.select(this).transition() 214 | .style('opacity', .8) 215 | tooltip.transition() 216 | .style('opacity', .9) 217 | 218 | tooltip.html(d.key + ", " + d.values) 219 | .style('left', (d3.event.pageX - 15) + 'px') 220 | .style('top', (d3.event.pageY - 20) + 'px') 221 | }) 222 | .on('mouseout', function(d) { 223 | d3.select(this).transition() 224 | .style('opacity', 1) 225 | tooltip.transition() 226 | .style('opacity', 0) 227 | }) 228 | 229 | // create the vertical axis scale 230 | var yAxisScale = d3.scale.linear() 231 | .domain([0, maxFrequency]) 232 | .range([height, 0]) 233 | // create the vertical axis object 234 | var yAxis = d3.svg.axis() 235 | .scale(yAxisScale) 236 | .orient('left') 237 | .ticks(10) 238 | 239 | // create the vertical axis guide 240 | var yGuide = d3.select('#barchart').append('g') 241 | yAxis(yGuide) 242 | // handle positioning based on margins 243 | yGuide.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') 244 | 245 | yGuide.selectAll('path') 246 | .style('fill', 'none') 247 | .style('stroke', '#000') 248 | 249 | yGuide.selectAll('line') 250 | .style('stroke', '#000') 251 | 252 | yGuide.selectAll('text') 253 | .style('font-size', '12px') // need to set these for savetopng to work 254 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 255 | 256 | // create the horizontal axis object 257 | var xAxis = d3.svg.axis() 258 | .scale(xScale) 259 | .orient('bottom') 260 | .tickFormat(function(d,i) { return data[i].key; }) 261 | 262 | // create the horizontal axis guide 263 | var xGuide = d3.select('#barchart').append('g') 264 | xAxis(xGuide) 265 | // handle positioning based on margins 266 | xGuide.attr('transform', 'translate(' + margin.left + ',' + (height + margin.top) + ')' ) 267 | 268 | xGuide.selectAll('path') 269 | .style('fill', 'none') 270 | .style('stroke', '#000') 271 | 272 | xGuide.selectAll('line') 273 | .style('stroke', '#000') 274 | 275 | xGuide.selectAll('text') 276 | .style('font-size', '12px') // need to set these for savetopng to work 277 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 278 | .style('text-anchor', 'start') 279 | .attr("dx", ".8em") 280 | .attr("dy", ".15em") 281 | .attr('transform', 'rotate(65)' ) 282 | 283 | } 284 | 285 | // .oooo.o .oooo. oooo ooo .ooooo. 286 | // d88( "8 `P )88b `88. .8' d88' `88b 287 | // `"Y88b. .oP"888 `88..8' 888ooo888 288 | // o. )88b d8( 888 `888' 888 .o 289 | // 8""888P' `Y888""8o `8' `Y8bod8P' 290 | 291 | d3.select("#savePie").on("click", function(){ 292 | saveSvgAsPng(document.getElementById("pie"), "pie_by_sender.png"); 293 | }); 294 | 295 | d3.select("#saveBar").on("click", function(){ 296 | saveSvgAsPng(document.getElementById("barchart"), "bar_by_sender.png"); 297 | }); 298 | -------------------------------------------------------------------------------- /Support/HTML/byCorrespondents/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | Your email visualized by correspondents 15 | 16 | 17 | 18 |

Glances at your email

19 | 20 | Correspondents are all non-user addresses. i.e., they are addresses that do not match your email as MailMate knows it. 21 | 22 |

Hover over the piechart and bar plot for more information.

23 | 24 |

Pie chart of messages by sender

25 | 26 |
27 | 28 |

Bar chart of messages by correspondent

29 | 30 |

31 | 32 | 33 |

34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Support/HTML/bySender/bySender.js: -------------------------------------------------------------------------------- 1 | // color palette 2 | var color = d3.scale.ordinal() 3 | .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]); 4 | 5 | datafile = urlObject().parameters.csv_file 6 | 7 | d3.csv(datafile, function(data) { 8 | var emails = data 9 | pieBySender(emails) 10 | barBySender(emails) 11 | }) 12 | 13 | // .o .o o8o 14 | // .8' .8' `"' 15 | // .888888888888' oo.ooooo. oooo .ooooo. 16 | // .8' .8' 888' `88b `888 d88' `88b 17 | // .888888888888' 888 888 888 888ooo888 18 | // .8' .8' 888 888 888 888 .o 19 | // .8' .8' 888bod8P' o888o `Y8bod8P' 20 | // 888 21 | // o888o 22 | 23 | function pieBySender(emails) { 24 | 25 | // aggregate the count of emails per sender 26 | var data = d3.nest() 27 | .key(function(d) { return d.name; }) 28 | .rollup(function(d) { 29 | return d3.sum(d, function(g) { return g.count; }) 30 | }) 31 | .entries(emails); 32 | 33 | // sort the data in descending order of frequency 34 | data.sort(function compareNumbers(a, b) { 35 | return b.values - a.values; 36 | }) 37 | 38 | // plot the data into an svg 39 | var width = 600, 40 | height = 500, 41 | radius = Math.min(width, height) / 2; 42 | 43 | var arc = d3.svg.arc() 44 | .outerRadius(radius - 10) 45 | .innerRadius(radius - 160) 46 | 47 | var pie = d3.layout.pie() 48 | .sort(null) 49 | .value(function(d) { return d.values; }); 50 | 51 | var svg = d3.select("#pieBySender").append("svg") 52 | .attr('id', 'pie') // we'll use this to convert and download the image 53 | .attr("width", width) 54 | .attr("height", height) 55 | .append("g") 56 | .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); 57 | 58 | var g = svg.selectAll("arc") 59 | .data(pie(data)) 60 | .enter().append("g") 61 | .attr("class", "arc"); 62 | 63 | g.append("path") 64 | .attr("d", arc) 65 | .style("fill", function(d) { return color(d.data.key); }) 66 | .style('stroke', function(d) { return color(d.data.key); }) 67 | 68 | // we will use this value to only automatically show names of people 69 | // who have frequencies above a certain threshold 70 | var totalEmails = d3.sum(data, function(d) { return d.values; }) 71 | 72 | g.append("text") 73 | .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; }) 74 | .attr("dy", ".35em") 75 | .style("text-anchor", "middle") 76 | .style('fill', 'white') 77 | .style('font-family', 'Fira Sans') 78 | .style("font-size", 12) 79 | .filter(function(d) { return (d.data.values/totalEmails) > .06; }) 80 | .text(function(d) { return d.data.key; }) 81 | .attr('opacity', 0) 82 | .transition() 83 | .duration(600) 84 | .attr('opacity', 1) 85 | 86 | var tooltip = d3.select('#pieBySender').append('div') 87 | .style('position', 'absolute') 88 | .style('background', 'lightblue') 89 | .style('color', 'black') 90 | .style('padding', '0 10px') 91 | .style('opacity', 0) 92 | 93 | svg.selectAll('path') 94 | .on('mouseover', function(d) { 95 | d3.select(this).transition() 96 | .style('opacity', .8) 97 | tooltip.transition() 98 | .style('opacity', .9) 99 | 100 | tooltip.html(d.data.key + ", " + d.data.values) 101 | .style('left', (d3.event.pageX - 35) + 'px') 102 | .style('top', (d3.event.pageY - 30) + 'px') 103 | }) 104 | 105 | .on('mouseout', function(d) { 106 | d3.select(this).transition() 107 | .style('opacity', 1) 108 | tooltip.transition() 109 | .style('opacity', 0) 110 | }) 111 | } 112 | 113 | // .o .o .o8 114 | // .8' .8' "888 115 | // .888888888888' 888oooo. .oooo. oooo d8b 116 | // .8' .8' d88' `88b `P )88b `888""8P 117 | // .888888888888' 888 888 .oP"888 888 118 | // .8' .8' 888 888 d8( 888 888 119 | // .8' .8' `Y8bod8P' `Y888""8o d888b 120 | 121 | function barBySender(emails) { 122 | // aggregate the count of emails per sender 123 | var data = d3.nest() 124 | .key(function(d) { return d.name; }) 125 | .rollup(function(d) { 126 | return d3.sum(d, function(g) { return g.count; }) 127 | }) 128 | .entries(emails); 129 | 130 | // rename variables in the aggregated set 131 | // data.forEach(function(d) { 132 | // d.name = d.key 133 | // d.count = d.values 134 | // }) 135 | 136 | // sort the data in descending order of frequency 137 | data.sort(function compareNumbers(a, b) { 138 | return b.values - a.values; 139 | }) 140 | 141 | // limit number of senders represented in bar graph to 50 142 | var originalSenderNumber = data.length 143 | if (originalSenderNumber > 50) 144 | { 145 | data = data.slice(0, 50) 146 | var excludedSenderNumber = originalSenderNumber - data.length 147 | 148 | if (excludedSenderNumber < 2) { 149 | var exSenderMessage = " sender couldn't fit in the bar graph and was excluded." 150 | } else { 151 | exSenderMessage = " senders couldn't fit in the bar graph and were excluded." 152 | } 153 | 154 | d3.select('#excludedSenders') 155 | .style('fill', '#000') 156 | .style('color', '#c43c35') 157 | .style('border-radius', '3px') 158 | .style('padding-left', '5px') 159 | .style('padding-right', '5px') 160 | .style('display', 'inline-block') 161 | .append('p') 162 | .html("" + 163 | excludedSenderNumber + 164 | "" + 165 | exSenderMessage) 166 | } else { 167 | d3.select('#excludedSenders').remove() 168 | } 169 | 170 | // o8o 171 | // `"' 172 | // ooo. .oo. .oo. .oooo. oooo d8b .oooooooo oooo ooo. .oo. 173 | // `888P"Y88bP"Y88b `P )88b `888""8P 888' `88b `888 `888P"Y88b 174 | // 888 888 888 .oP"888 888 888 888 888 888 888 175 | // 888 888 888 d8( 888 888 `88bod8P' 888 888 888 176 | // o888o o888o o888o `Y888""8o d888b `8oooooo. o888o o888o o888o 177 | // d" YD 178 | // "Y88888P' 179 | 180 | var margin = { top: 30, right: 40, bottom: 250, left: 70 } 181 | 182 | 183 | var width = 900 - margin.left - margin.right, 184 | height = 680 - margin.top - margin.bottom 185 | 186 | var xScale = d3.scale.ordinal().domain(d3.range(0,data.length)).rangeBands([0, width], .2); 187 | 188 | var maxFrequency = d3.max(data, function(d) { return d.values; }) 189 | 190 | var yScale = d3.scale.linear().domain([0, maxFrequency]).range([0, height]) 191 | 192 | 193 | // plot the data into an svg 194 | d3.selectAll('#barBySender').append('svg') 195 | .attr('id', 'barchart') 196 | .attr('width', width + margin.left + margin.right) // handling margins 197 | .attr('height', height + margin.top + margin.bottom) // handling margins 198 | .append('g') 199 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')' ) 200 | .selectAll('rect') 201 | .data(data) 202 | .enter() 203 | .append('rect') 204 | .style('fill', function(d, i) { return color(i); }) 205 | .attr('width', xScale.rangeBand()) 206 | .attr('height', function(d) { return yScale(d.values); }) 207 | .attr('y', function(d) { return height - yScale(d.values); }) 208 | .attr('x', function(d,i) { return xScale(i); }) 209 | 210 | var tooltip = d3.select('#barBySender').append('div') 211 | .style('position', 'absolute') 212 | .style('background', 'lightblue') 213 | .style('color', 'black') 214 | .style('padding', '0 10px') 215 | .style('opacity', 0) 216 | 217 | d3.selectAll('rect') 218 | .on('mouseover', function(d) { 219 | d3.select(this).transition() 220 | .style('opacity', .8) 221 | tooltip.transition() 222 | .style('opacity', .9) 223 | 224 | tooltip.html(d.key + ", " + d.values) 225 | .style('left', (d3.event.pageX - 15) + 'px') 226 | .style('top', (d3.event.pageY - 20) + 'px') 227 | }) 228 | .on('mouseout', function(d) { 229 | d3.select(this).transition() 230 | .style('opacity', 1) 231 | tooltip.transition() 232 | .style('opacity', 0) 233 | }) 234 | 235 | // create the vertical axis scale 236 | var yAxisScale = d3.scale.linear() 237 | .domain([0, maxFrequency]) 238 | .range([height, 0]) 239 | // create the vertical axis object 240 | var yAxis = d3.svg.axis() 241 | .scale(yAxisScale) 242 | .orient('left') 243 | .ticks(10) 244 | 245 | // create the vertical axis guide 246 | var yGuide = d3.select('#barchart').append('g') 247 | yAxis(yGuide) 248 | // handle positioning based on margins 249 | yGuide.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') 250 | 251 | yGuide.selectAll('path') 252 | .style('fill', 'none') 253 | .style('stroke', '#000') 254 | 255 | yGuide.selectAll('line') 256 | .style('stroke', '#000') 257 | 258 | yGuide.selectAll('text') 259 | .style('font-size', '12px') // need to set these for savetopng to work 260 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 261 | 262 | // create the horizontal axis object 263 | var xAxis = d3.svg.axis() 264 | .scale(xScale) 265 | .orient('bottom') 266 | .tickFormat(function(d,i) { return data[i].key; }) 267 | 268 | // create the horizontal axis guide 269 | var xGuide = d3.select('#barchart').append('g') 270 | xAxis(xGuide) 271 | // handle positioning based on margins 272 | xGuide.attr('transform', 'translate(' + margin.left + ',' + (height + margin.top) + ')' ) 273 | 274 | xGuide.selectAll('path') 275 | .style('fill', 'none') 276 | .style('stroke', '#000') 277 | 278 | xGuide.selectAll('line') 279 | .style('stroke', '#000') 280 | 281 | xGuide.selectAll('text') 282 | .style('font-size', '12px') // need to set these for savetopng to work 283 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 284 | .style('text-anchor', 'start') 285 | .attr("dx", ".8em") 286 | .attr("dy", ".15em") 287 | .attr('transform', 'rotate(65)' ) 288 | 289 | } 290 | 291 | // .oooo.o .oooo. oooo ooo .ooooo. 292 | // d88( "8 `P )88b `88. .8' d88' `88b 293 | // `"Y88b. .oP"888 `88..8' 888ooo888 294 | // o. )88b d8( 888 `888' 888 .o 295 | // 8""888P' `Y888""8o `8' `Y8bod8P' 296 | 297 | d3.select("#savePie").on("click", function(){ 298 | saveSvgAsPng(document.getElementById("pie"), "pie_by_sender.png"); 299 | }); 300 | 301 | d3.select("#saveBar").on("click", function(){ 302 | saveSvgAsPng(document.getElementById("barchart"), "bar_by_sender.png"); 303 | }); -------------------------------------------------------------------------------- /Support/HTML/bySender/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | Your email visualized by sender 15 | 16 | 17 | 18 |

Glances at your email

19 | 20 |

Hover over the piechart and bar plot for more information.

21 | 22 |

Pie chart of messages by sender

23 | 24 |
25 | 26 |

Bar chart of messages by sender

27 | 28 |

29 | 30 | 31 |

32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Support/HTML/byTag/byTag.js: -------------------------------------------------------------------------------- 1 | // color palette 2 | var color = d3.scale.ordinal() 3 | .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]); 4 | 5 | datafile = urlObject().parameters.csv_file 6 | 7 | d3.csv(datafile, function(data) { 8 | var emails = data 9 | pieByTag(emails) 10 | barByTag(emails) 11 | }) 12 | 13 | // .o .o o8o 14 | // .8' .8' `"' 15 | // .888888888888' oo.ooooo. oooo .ooooo. 16 | // .8' .8' 888' `88b `888 d88' `88b 17 | // .888888888888' 888 888 888 888ooo888 18 | // .8' .8' 888 888 888 888 .o 19 | // .8' .8' 888bod8P' o888o `Y8bod8P' 20 | // 888 21 | // o888o 22 | 23 | function pieByTag(emails) { 24 | 25 | // aggregate the count of emails per tag 26 | var data = d3.nest() 27 | .key(function(d) { return d.tag; }) 28 | .rollup(function(d) { 29 | return d3.sum(d, function(g) { return g.count; }) 30 | }) 31 | .entries(emails); 32 | 33 | // sort the data in descending order of frequency 34 | data.sort(function compareNumbers(a, b) { 35 | return b.values - a.values; 36 | }) 37 | 38 | // plot the data into an svg 39 | var width = 600, 40 | height = 500, 41 | radius = Math.min(width, height) / 2; 42 | 43 | var arc = d3.svg.arc() 44 | .outerRadius(radius - 10) 45 | .innerRadius(radius - 160) 46 | 47 | var pie = d3.layout.pie() 48 | .sort(null) 49 | .value(function(d) { return d.values; }); 50 | 51 | var svg = d3.select("#pieByTag").append("svg") 52 | .attr('id', 'pie') // we'll use this to convert and download the image 53 | .attr("width", width) 54 | .attr("height", height) 55 | .append("g") 56 | .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); 57 | 58 | var g = svg.selectAll("arc") 59 | .data(pie(data)) 60 | .enter().append("g") 61 | .attr("class", "arc"); 62 | 63 | g.append("path") 64 | .attr("d", arc) 65 | .style("fill", function(d) { return color(d.data.key); }) 66 | .style('stroke', function(d) { return color(d.data.key); }) 67 | 68 | // we will use this value to only automatically show names of tags 69 | // that have frequencies above a certain threshold 70 | var totalEmails = d3.sum(data, function(d) { return d.values; }) 71 | 72 | g.append("text") 73 | .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; }) 74 | .attr("dy", ".35em") 75 | .style("text-anchor", "middle") 76 | .style('fill', 'white') 77 | .style('font-family', 'Fira Sans') 78 | .style("font-size", 12) 79 | .filter(function(d) { return (d.data.values/totalEmails) > .06; }) 80 | .text(function(d) { return d.data.key; }) 81 | .attr('opacity', 0) 82 | .transition() 83 | .duration(600) 84 | .attr('opacity', 1) 85 | 86 | var tooltip = d3.select('#pieByTag').append('div') 87 | .style('position', 'absolute') 88 | .style('background', 'lightblue') 89 | .style('color', 'black') 90 | .style('padding', '0 10px') 91 | .style('opacity', 0) 92 | 93 | svg.selectAll('path') 94 | .on('mouseover', function(d) { 95 | d3.select(this).transition() 96 | .style('opacity', .8) 97 | tooltip.transition() 98 | .style('opacity', .9) 99 | 100 | tooltip.html(d.data.key + ", " + d.data.values) 101 | .style('left', (d3.event.pageX - 35) + 'px') 102 | .style('top', (d3.event.pageY - 30) + 'px') 103 | }) 104 | 105 | .on('mouseout', function(d) { 106 | d3.select(this).transition() 107 | .style('opacity', 1) 108 | tooltip.transition() 109 | .style('opacity', 0) 110 | }) 111 | } 112 | 113 | // .o .o .o8 114 | // .8' .8' "888 115 | // .888888888888' 888oooo. .oooo. oooo d8b 116 | // .8' .8' d88' `88b `P )88b `888""8P 117 | // .888888888888' 888 888 .oP"888 888 118 | // .8' .8' 888 888 d8( 888 888 119 | // .8' .8' `Y8bod8P' `Y888""8o d888b 120 | 121 | function barByTag(emails) { 122 | // aggregate the count of emails per tag 123 | var data = d3.nest() 124 | .key(function(d) { return d.tag; }) 125 | .rollup(function(d) { 126 | return d3.sum(d, function(g) { return g.count; }) 127 | }) 128 | .entries(emails); 129 | 130 | // rename variables in the aggregated set 131 | // data.forEach(function(d) { 132 | // d.tag = d.key 133 | // d.count = d.values 134 | // }) 135 | 136 | // sort the data in descending order of frequency 137 | data.sort(function compareNumbers(a, b) { 138 | return b.values - a.values; 139 | }) 140 | 141 | // limit number of tags represented in bar graph to 50 142 | var originalTagNumber = data.length 143 | if (originalTagNumber > 50) 144 | { 145 | data = data.slice(0, 50) 146 | var excludedTagNumber = originalTagNumber - data.length 147 | 148 | if (excludedTagNumber < 2) { 149 | var exTagMessage = " tags couldn't fit in the bar graph and was excluded." 150 | } else { 151 | exTagMessage = " tags couldn't fit in the bar graph and were excluded." 152 | } 153 | 154 | d3.select('#excludedTag') 155 | .style('fill', '#000') 156 | .style('color', '#c43c35') 157 | .style('border-radius', '3px') 158 | .style('padding-left', '5px') 159 | .style('padding-right', '5px') 160 | .style('display', 'inline-block') 161 | .append('p') 162 | .html("" + 163 | excludedTagNumber + 164 | "" + 165 | exTagMessage) 166 | } else { 167 | d3.select('#excludedTags').remove() 168 | } 169 | 170 | // o8o 171 | // `"' 172 | // ooo. .oo. .oo. .oooo. oooo d8b .oooooooo oooo ooo. .oo. 173 | // `888P"Y88bP"Y88b `P )88b `888""8P 888' `88b `888 `888P"Y88b 174 | // 888 888 888 .oP"888 888 888 888 888 888 888 175 | // 888 888 888 d8( 888 888 `88bod8P' 888 888 888 176 | // o888o o888o o888o `Y888""8o d888b `8oooooo. o888o o888o o888o 177 | // d" YD 178 | // "Y88888P' 179 | 180 | var margin = { top: 30, right: 40, bottom: 250, left: 70 } 181 | 182 | 183 | var width = 900 - margin.left - margin.right, 184 | height = 680 - margin.top - margin.bottom 185 | 186 | var xScale = d3.scale.ordinal().domain(d3.range(0,data.length)).rangeBands([0, width], .2); 187 | 188 | var maxFrequency = d3.max(data, function(d) { return d.values; }) 189 | 190 | var yScale = d3.scale.linear().domain([0, maxFrequency]).range([0, height]) 191 | 192 | 193 | // plot the data into an svg 194 | d3.selectAll('#barByTag').append('svg') 195 | .attr('id', 'barchart') 196 | .attr('width', width + margin.left + margin.right) // handling margins 197 | .attr('height', height + margin.top + margin.bottom) // handling margins 198 | .append('g') 199 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')' ) 200 | .selectAll('rect') 201 | .data(data) 202 | .enter() 203 | .append('rect') 204 | .style('fill', function(d, i) { return color(i); }) 205 | .attr('width', xScale.rangeBand()) 206 | .attr('height', function(d) { return yScale(d.values); }) 207 | .attr('y', function(d) { return height - yScale(d.values); }) 208 | .attr('x', function(d,i) { return xScale(i); }) 209 | 210 | var tooltip = d3.select('#barByTags').append('div') 211 | .style('position', 'absolute') 212 | .style('background', 'lightblue') 213 | .style('color', 'black') 214 | .style('padding', '0 10px') 215 | .style('opacity', 0) 216 | 217 | d3.selectAll('rect') 218 | .on('mouseover', function(d) { 219 | d3.select(this).transition() 220 | .style('opacity', .8) 221 | tooltip.transition() 222 | .style('opacity', .9) 223 | 224 | tooltip.html(d.key + ", " + d.values) 225 | .style('left', (d3.event.pageX - 15) + 'px') 226 | .style('top', (d3.event.pageY - 20) + 'px') 227 | }) 228 | .on('mouseout', function(d) { 229 | d3.select(this).transition() 230 | .style('opacity', 1) 231 | tooltip.transition() 232 | .style('opacity', 0) 233 | }) 234 | 235 | // create the vertical axis scale 236 | var yAxisScale = d3.scale.linear() 237 | .domain([0, maxFrequency]) 238 | .range([height, 0]) 239 | // create the vertical axis object 240 | var yAxis = d3.svg.axis() 241 | .scale(yAxisScale) 242 | .orient('left') 243 | .ticks(10) 244 | 245 | // create the vertical axis guide 246 | var yGuide = d3.select('#barchart').append('g') 247 | yAxis(yGuide) 248 | // handle positioning based on margins 249 | yGuide.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') 250 | 251 | yGuide.selectAll('path') 252 | .style('fill', 'none') 253 | .style('stroke', '#000') 254 | 255 | yGuide.selectAll('line') 256 | .style('stroke', '#000') 257 | 258 | yGuide.selectAll('text') 259 | .style('font-size', '12px') // need to set these for savetopng to work 260 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 261 | 262 | // create the horizontal axis object 263 | var xAxis = d3.svg.axis() 264 | .scale(xScale) 265 | .orient('bottom') 266 | .tickFormat(function(d,i) { return data[i].key; }) 267 | 268 | // create the horizontal axis guide 269 | var xGuide = d3.select('#barchart').append('g') 270 | xAxis(xGuide) 271 | // handle positioning based on margins 272 | xGuide.attr('transform', 'translate(' + margin.left + ',' + (height + margin.top) + ')' ) 273 | 274 | xGuide.selectAll('path') 275 | .style('fill', 'none') 276 | .style('stroke', '#000') 277 | 278 | xGuide.selectAll('line') 279 | .style('stroke', '#000') 280 | 281 | xGuide.selectAll('text') 282 | .style('font-size', '12px') // need to set these for savetopng to work 283 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 284 | .style('text-anchor', 'start') 285 | .attr("dx", ".8em") 286 | .attr("dy", ".15em") 287 | .attr('transform', 'rotate(65)' ) 288 | 289 | } 290 | 291 | // .oooo.o .oooo. oooo ooo .ooooo. 292 | // d88( "8 `P )88b `88. .8' d88' `88b 293 | // `"Y88b. .oP"888 `88..8' 888ooo888 294 | // o. )88b d8( 888 `888' 888 .o 295 | // 8""888P' `Y888""8o `8' `Y8bod8P' 296 | 297 | d3.select("#savePie").on("click", function(){ 298 | saveSvgAsPng(document.getElementById("pie"), "pie_by_tag.png"); 299 | }); 300 | 301 | d3.select("#saveBar").on("click", function(){ 302 | saveSvgAsPng(document.getElementById("barchart"), "bar_by_tag.png"); 303 | }); 304 | -------------------------------------------------------------------------------- /Support/HTML/byTag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | Your email visualized by tag 15 | 16 | 17 | 18 |

Glances at your email

19 | 20 |

Hover over the piechart and bar plot for more information.

21 | 22 |

Pie chart of messages by tag

23 | 24 |
25 | 26 |

Bar chart of messages by tag

27 | 28 |

29 | 30 | 31 |

32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Support/HTML/byTime/byTime.js: -------------------------------------------------------------------------------- 1 | // color palette 2 | var color = d3.scale.ordinal() 3 | .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]); 4 | 5 | // days of the week corresponding to the integer returned by [date].getDay() 6 | var days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] 7 | 8 | // margin and dimensions needed for both scatterplot and linegraph 9 | var margin = { top: 70, right: 100, bottom: 60, left: 30 } 10 | 11 | var width = 900 - margin.left - margin.right, 12 | timeGraphWidth = width, 13 | timeGraphHeight = 200, 14 | height = 800 - timeGraphHeight - margin.top - margin.bottom 15 | 16 | datafile = urlObject().parameters.csv_file 17 | 18 | d3.csv(datafile, function(data) { 19 | // need emails to be global for function that redraws line 20 | emails = data 21 | 22 | // filter out emails with missing date headers 23 | // (which is a thing that happens) 24 | emails = sanitizeEmails(emails) 25 | pieByDays(emails) 26 | seriesTime(emails) 27 | lineTime(emails, 4, false) 28 | }) 29 | 30 | // o8o 31 | // `"' 32 | // .oooo.o .oooo. ooo. .oo. oooo 33 | // d88( "8 `P )88b `888P"Y88b `888 34 | // `"Y88b. .oP"888 888 888 888 35 | // o. )88b d8( 888 888 888 888 36 | // 8""888P' `Y888""8o o888o o888o o888o 37 | 38 | function sanitizeEmails(emails) { 39 | var originalCount = emails.length 40 | emails = emails.filter(function(ems) { 41 | return ems.localDateTime != ""; 42 | }) 43 | var newCount = emails.length 44 | var excludedCount = originalCount - newCount 45 | 46 | if (excludedCount > 0) { 47 | if (excludedCount < 2) { 48 | var datemessage = " message was missing a date header and was excluded." 49 | } else { 50 | var datemessage = " messages were missing a date header and were excluded." 51 | } 52 | 53 | d3.select('#excludedEmails') 54 | .style('color', 'white') 55 | .style('background-color', '#c43c35') 56 | .style('text-transform', 'uppercase') 57 | .style('border-radius', '3px') 58 | .style('padding-left', '5px') 59 | .style('padding-right', '5px') 60 | .style('display', 'inline-block') 61 | .append('p') 62 | .html("" + 63 | excludedCount + 64 | "" + 65 | datemessage) 66 | } else { // no excluded emails, delete excludedEmails div 67 | d3.select('#excludedEmails').remove() 68 | } 69 | 70 | return emails 71 | } 72 | 73 | // .o .o o8o .o8 74 | // .8' .8' `"' "888 75 | // .888888888888' oo.ooooo. oooo .oooo888 .oooo. oooo ooo .oooo.o 76 | // .8' .8' 888' `88b `888 d88' `888 `P )88b `88. .8' d88( "8 77 | // .888888888888' 888 888 888 888 888 .oP"888 `88..8' `"Y88b. 78 | // .8' .8' 888 888 888 888 888 d8( 888 `888' o. )88b 79 | // .8' .8' 888bod8P' o888o `Y8bod88P" `Y888""8o .8' 8""888P' 80 | // 888 .o..P' 81 | // o888o `Y8P' 82 | 83 | function pieByDays(emails) { 84 | // some functions to parse the date 85 | parseDate = d3.time.format("%Y-%m-%d").parse 86 | parseTime = d3.time.format("%H:%M:%S").parse 87 | 88 | // create new value of day of week from date 89 | emails.forEach(function(d) { 90 | dateSplit = d.localDateTime.split(" ") 91 | d.date = parseDate(dateSplit[0]) 92 | d.time = parseTime(dateSplit[1]) 93 | d.dayOfWeekInt = d.date.getDay() 94 | }) 95 | 96 | // aggregate frequencies by day of the week 97 | var data = d3.nest() 98 | .key(function(d) { return d.dayOfWeekInt; }) 99 | .rollup(function(d) { 100 | return d3.sum(d, function(g) { return g.count; }) 101 | }) 102 | .entries(emails); 103 | 104 | // sort days of the week 105 | data.sort(function compareNumbers(a, b) { 106 | return a.key - b.key 107 | }) 108 | 109 | // oooo . 110 | // `888 .o8 111 | // oo.ooooo. 888 .ooooo. .o888oo 112 | // 888' `88b 888 d88' `88b 888 113 | // 888 888 888 888 888 888 114 | // 888 888 888 888 888 888 . 115 | // 888bod8P' o888o `Y8bod8P' "888" 116 | // 888 117 | // o888o 118 | 119 | // plot the data into an svg 120 | 121 | var width = 600, 122 | height = 500, 123 | radius = Math.min(width, height) / 2 124 | 125 | var arc = d3.svg.arc() 126 | .outerRadius(radius - 10) 127 | .innerRadius(radius - 160) 128 | 129 | var pie = d3.layout.pie() 130 | .sort(null) 131 | .value(function(d) { return d.values; }) 132 | 133 | var svg = d3.select('#pieByTime').append('svg') 134 | .attr('id', 'pie') 135 | .attr('width', width) 136 | .attr('height', height) 137 | .append('g') 138 | .attr('transform', 'translate(' + width/2 + ',' + height/2 + ')') 139 | 140 | var g = svg.selectAll('arc') 141 | .data(pie(data)) 142 | .enter().append('g') 143 | .attr('class', 'arc') 144 | 145 | g.append('path') 146 | .attr('d', arc) 147 | .style('fill', function(d, i) { return color(i); }) 148 | .style('stroke', function(d, i) { return color(i); }) 149 | 150 | g.append('text') 151 | .attr('transform', function(d) { return 'translate(' + arc.centroid(d) + ')'; }) 152 | .attr('dy', '.35em') 153 | .attr('text-anchor', 'middle') 154 | .attr('fill', 'white') 155 | .style('font-family', 'Fira Sans') 156 | .style('font-size', 12) 157 | .text(function(d) { return days[d.data.key]; }) 158 | .attr('opacity', 0) 159 | .transition().duration(600) 160 | .attr('opacity', 1) 161 | 162 | var tooltip = d3.select('#pieByTime').append('div') 163 | .style('position', 'absolute') 164 | .style('background', 'lightblue') 165 | .style('color', 'black') 166 | .style('padding', '0 10px') 167 | .style('opacity', 0) 168 | 169 | svg.selectAll('path') 170 | .on('mouseover', function(d) { 171 | d3.select(this).transition() 172 | .style('opacity', .8) 173 | tooltip.transition() 174 | .style('opacity', .9) 175 | tooltip.html(d.data.values) 176 | .style('left', (d3.event.pageX - 45) + 'px') 177 | .style('top', (d3.event.pageY - 30) + 'px') 178 | }) 179 | 180 | .on('mouseout', function(d) { 181 | d3.select(this).transition() 182 | .style('opacity', 1) 183 | tooltip.transition() 184 | .style('opacity', 0) 185 | }) 186 | 187 | } 188 | 189 | // .o .o o8o 190 | // .8' .8' `"' 191 | // .888888888888' .oooo.o .ooooo. oooo d8b oooo .ooooo. .oooo.o 192 | // .8' .8' d88( "8 d88' `88b `888""8P `888 d88' `88b d88( "8 193 | // .888888888888' `"Y88b. 888ooo888 888 888 888ooo888 `"Y88b. 194 | // .8' .8' o. )88b 888 .o 888 888 888 .o o. )88b 195 | // .8' .8' 8""888P' `Y8bod8P' d888b o888o `Y8bod8P' 8""888P' 196 | 197 | 198 | function seriesTime(emails) { 199 | 200 | // o8o 201 | // `"' 202 | // ooo. .oo. .oo. .oooo. oooo d8b .oooooooo oooo ooo. .oo. 203 | // `888P"Y88bP"Y88b `P )88b `888""8P 888' `88b `888 `888P"Y88b 204 | // 888 888 888 .oP"888 888 888 888 888 888 888 205 | // 888 888 888 d8( 888 888 `88bod8P' 888 888 888 206 | // o888o o888o o888o `Y888""8o d888b `8oooooo. o888o o888o o888o 207 | // d" YD 208 | // "Y88888P' 209 | 210 | var formatDay_Time = d3.time.format("%H:%M") // tooltip time 211 | var formatWeek_Year = d3.time.format("%B %d, %Y") // tooltip date 212 | 213 | var x = d3.time.scale().range([0, width]) 214 | var y = d3.time.scale().range([0, height]) 215 | 216 | // Set the domains 217 | y.domain([new Date(1899, 12, 02, 0, 0, 0), 218 | new Date(1899, 12, 01, 0, 0, 1)]) 219 | x.domain(d3.extent(emails, function(d) { return d.date; })) 220 | 221 | var xAxis = d3.svg.axis() 222 | .scale(x) 223 | .orient('bottom') 224 | .ticks(10) 225 | .outerTickSize(0) 226 | 227 | var yAxis = d3.svg.axis() 228 | .scale(y) 229 | .orient('right') 230 | .ticks(24) 231 | .outerTickSize(0) 232 | .tickFormat(formatDay_Time) 233 | 234 | var svg = d3.select('#timeseries').append('svg') 235 | .attr('id', 'timeseriesSVG') 236 | .style('background-color', 'white') 237 | .attr('width', width + margin.left + margin.right) 238 | .attr('height', height + timeGraphHeight + margin.top + margin.bottom) 239 | .append('g') 240 | .attr('class', "timeSeriesG") 241 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') 242 | 243 | // Draw the Axes and the tick labels 244 | var hAxis = svg.append("g") 245 | .attr("transform", "translate(0," + (height+5) + ")") 246 | .call(xAxis) 247 | .selectAll('path') 248 | .style('fill', 'none') 249 | .style('stroke', '#000') 250 | 251 | svg.selectAll("text") 252 | .style('font-size', '12px') // need to set these for savetopng to work 253 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 254 | .style('text-anchor', 'start') 255 | .attr("dx", ".8em") 256 | .attr('transform', 'rotate(35)' ) 257 | 258 | svg.append("g") 259 | .attr("class", "y axis") 260 | .attr("transform", "translate(" + (width + 15) + ",0)") 261 | .call(yAxis) 262 | .selectAll('path') 263 | .style('fill', 'none') 264 | .style('stroke', '#000') 265 | 266 | svg.selectAll("text") 267 | .style('font-size', '12px') // need to set these for savetopng to work 268 | .style('font-family', 'Fira Sans') // need to set these for savetopng to work 269 | .style('text-anchor', 'start') 270 | 271 | // draw the plotted circles 272 | svg.selectAll("dot") 273 | .data(emails) 274 | .enter().append("circle") 275 | .attr("class", "dot") 276 | .attr("r", 2 ) 277 | .style("opacity", 0.9) 278 | .style("fill", "#33a3cc" ) 279 | .attr("cx", function(d) { return x(d.date); }) 280 | .attr("cy", function(d) { return y(d.time); }) 281 | 282 | var seriesTooltip = d3.select('#timeseries').append('div') 283 | .style('position', 'absolute') 284 | .style('background', 'lightblue') 285 | .style('color', 'black') 286 | .style('padding', '5px 10px') 287 | .style('opacity', 0) 288 | .style('text-align', 'left') 289 | 290 | d3.selectAll('circle') 291 | .on('mouseover', function(d) { 292 | d3.select(this).transition().duration(100) 293 | .attr('r', 5) 294 | .style('fill', color(2)) 295 | 296 | seriesTooltip.transition() 297 | .style('opacity', .9) 298 | 299 | seriesTooltip.html(d.name + "
" + d.subjectLine + "
" + formatWeek_Year(d.date) + "
" + formatDay_Time(d.time)) 300 | .style('left', (d3.event.pageX - 55) + 'px') 301 | .style('top', (d3.event.pageY - 90) + 'px') 302 | }) 303 | 304 | .on('mouseout', function(d) { 305 | d3.select(this).transition().duration(100) 306 | .attr('r', 2) 307 | .style('fill', "#33a3cc") 308 | 309 | seriesTooltip.html() 310 | seriesTooltip.transition() 311 | .style('opacity', 0) 312 | }) 313 | 314 | // extend axes with lines for separation 315 | d3.select('#timeseriesSVG').append('line') 316 | .style('stroke', 'black') 317 | .attr('x1', width + margin.left) 318 | .attr('y1', height + margin.top + 5) 319 | .attr('x2', width + margin.left + 80) 320 | .attr('y2', height + margin.top + 5) 321 | 322 | d3.select('#timeseriesSVG').append('line') 323 | .style('stroke', 'black') 324 | .attr('x1', width + margin.left + 15) 325 | .attr('y1', height + margin.top) 326 | .attr('x2', width + margin.left + 15) 327 | .attr('y2', height + margin.top + 55) 328 | 329 | } 330 | 331 | function lineTime(emails, movingMeanWindow, overrideWindow) { 332 | 333 | // oooo o8o 334 | // `888 `"' 335 | // 888 oooo ooo. .oo. .ooooo. 336 | // 888 `888 `888P"Y88b d88' `88b 337 | // 888 888 888 888 888ooo888 338 | // 888 888 888 888 888 .o 339 | // o888o o888o o888o o888o `Y8bod8P' 340 | 341 | 342 | d3.selectAll('#timeseriesSVG').append('g') 343 | .attr('id', 'lineGraph') 344 | .attr('transform', 'translate(' + margin.left + ',' + (height + margin.top + 55) + ')') 345 | .append('rect') 346 | .attr('height', timeGraphHeight) 347 | .attr('width', timeGraphWidth) 348 | .style('fill', 'none') 349 | 350 | // aggregate totals by day 351 | var lineData = d3.nest() 352 | .key(function(d) { return d.date; }) 353 | .rollup(function(d) { 354 | return d3.sum(d, function(g) { return g.count; }) 355 | }) 356 | .entries(emails); 357 | 358 | lineData.forEach(function(d) { 359 | d.key = new Date(d.key) 360 | }) 361 | 362 | // sort lineData by date 363 | lineData.sort(function compareNumbers(a, b) { 364 | return a.key - b.key 365 | }) 366 | 367 | var earliest = d3.min(lineData, function(d) { return d.key; }) 368 | var latest = d3.max(lineData, function(d) { return d.key; }) 369 | 370 | var dirtyLineXRange = d3.time.scale().domain(d3.extent(lineData, function(d) { return d.key; })).range([0, timeGraphWidth]) 371 | 372 | // fill in missing days with value of 0 emails 373 | var cleanedData = dirtyLineXRange.ticks(d3.time.day, 1).map(function(iteratedDay) { 374 | return _.find(lineData, {key: iteratedDay}) || {key: iteratedDay, values: 0} 375 | }) 376 | 377 | // implement moving mean 378 | var movingMean = [] 379 | var smooth = false 380 | if (overrideWindow == true) { 381 | smooth = true 382 | } else { 383 | if (cleanedData.length > 400 && cleanedData.length < 1000) { 384 | smooth = true 385 | } else if (cleanedData.length > 1000 && cleanedData.length < 3000) { 386 | smooth = true 387 | movingMeanWindow = 6 388 | } else if (cleanedData.length > 3000) { 389 | smooth = true 390 | movingMeanWindow = 6 391 | } 392 | } 393 | 394 | if (smooth == true) { 395 | // display value of moving mean to user 396 | document.getElementById('smoothingwindow').value = movingMeanWindow 397 | 398 | // create a moving mean array from cleanedData 399 | for (var ii = 0; ii <= cleanedData.length; ii++) 400 | { 401 | if (ii < (movingMeanWindow/2)) { 402 | var meanSlice = cleanedData.slice(0, 403 | movingMeanWindow - ii); 404 | } else if (ii > (cleanedData.length - movingMeanWindow)) { 405 | var meanSlice = cleanedData.slice(cleanedData.length - movingMeanWindow - ii, 406 | cleanedData.length); 407 | } else { 408 | var meanSlice = cleanedData.slice(ii - (movingMeanWindow/2), 409 | ii + (movingMeanWindow/2)); 410 | } 411 | 412 | var mean = d3.sum(meanSlice, function(meanSlice) { return meanSlice.values }) / meanSlice.length 413 | 414 | movingMean.push(Math.round(mean * 100) / 100) 415 | } 416 | 417 | // transfer values of array to cleanedData array 418 | for (var ii = 0; ii < cleanedData.length; ii++) 419 | { 420 | cleanedData[ii].values = movingMean[ii] 421 | } 422 | } 423 | 424 | // continue with the scales and plotting the line 425 | var lineXRange = d3.time.scale().domain(d3.extent(cleanedData, function(d) { return d.key; })).range([0, timeGraphWidth]) 426 | var lineYRange = d3.scale.linear().domain([0, d3.max(cleanedData, function(d) { return d.values; })]).range([timeGraphHeight, 0]) 427 | 428 | lineYAxis = d3.svg.axis() 429 | .scale(lineYRange) 430 | .ticks(10) 431 | .tickPadding(10) 432 | .outerTickSize(0) 433 | .orient('right') 434 | 435 | var linePlotFunc = d3.svg.line() 436 | .x(function(d) { return lineXRange(d.key); }) 437 | .y(function(d) { return lineYRange(d.values); }) 438 | .interpolate('monotone') 439 | 440 | timeGraph = d3.select("#lineGraph") 441 | 442 | timeGraph.append('g') 443 | .attr('transform', 'translate(' + (timeGraphWidth + 15) + ',0)' ) 444 | .call(lineYAxis) 445 | .selectAll('path') 446 | .style('fill', 'none') 447 | .style('stroke', '#000') 448 | 449 | timeGraph.append('path') 450 | .attr('id', 'lineplot') 451 | .attr('d', linePlotFunc(cleanedData) ) 452 | .attr('stroke', "#33a3cc") 453 | .attr('fill', 'none') 454 | .style('stroke-width', 1.5) 455 | 456 | timeGraph.selectAll('text') 457 | .style('font-size', '12px') // need to set these for savetopng to work 458 | .style('font-family', '-apple-system') // need to set these for savetopng to work 459 | } 460 | 461 | // .oooo.o .oooo. oooo ooo .ooooo. 462 | // d88( "8 `P )88b `88. .8' d88' `88b 463 | // `"Y88b. .oP"888 `88..8' 888ooo888 464 | // o. )88b d8( 888 `888' 888 .o 465 | // 8""888P' `Y888""8o `8' `Y8bod8P' 466 | 467 | d3.select('#savePieTime').on('click', function() { 468 | saveSvgAsPng(document.getElementById('pie'), 'pie_by_days_of_week.png') 469 | }) 470 | 471 | d3.select('#saveTimeSeries').on('click', function() { 472 | saveSvgAsPng(document.getElementById('timeseriesSVG'), 'email_time_series.png') 473 | }) 474 | 475 | 476 | d3.select('#redrawLine').on('click', function() { 477 | var newWindow = document.getElementById('smoothingwindow').value 478 | 479 | if (newWindow < 0 || newWindow == "") { 480 | d3.select('#windowwarning').text("value must be a positive integer") 481 | } else { 482 | d3.select('#windowwarning').text("") 483 | d3.select('#lineGraph').remove() 484 | lineTime(emails, newWindow, true) 485 | } 486 | }) 487 | 488 | // Initialize color picker and act on color changes 489 | $("#colorpicker").spectrum({ 490 | showInput: true, 491 | showInitial: true, 492 | preferredFormat: "hex", 493 | color: "#33a3cc" 494 | }); 495 | 496 | $("#colorpicker").on('change.spectrum', function(e, tinycolor) { 497 | var pickedColor = tinycolor.toHexString() 498 | console.log(pickedColor); 499 | 500 | d3.select("#timeseries").selectAll('circle') 501 | .style('fill', pickedColor) 502 | 503 | d3.select("#lineGraph").selectAll('#lineplot') 504 | .style('stroke', pickedColor) 505 | }); 506 | -------------------------------------------------------------------------------- /Support/HTML/byTime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Your email visualized 20 | 21 | 22 | 23 |

Glances at your email

24 | 25 |
26 | 27 |

Hover over the piechart and scatterplot for more information.

28 | 29 |

Pie chart of messages by day of the week

30 | 31 |
32 | 33 |

Time series/scatterplot of messages by date and time of day

34 | 35 | Choose your color: 36 |

37 | 38 | Size of moving mean window (set to 1 to disable): 39 | 40 | 41 | 42 |

43 | 44 | 45 | 46 |

47 | 48 | 49 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Support/HTML/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Fira+Sans:400,700,400italic,700italic); 2 | 3 | body { 4 | font-family: -apple-system, 'Fira Sans', 'Helvetica Neue', Helvetica, sans-serif; 5 | font-size: 12px; 6 | } 7 | 8 | p { 9 | font-size: 14px; 10 | } 11 | 12 | div#pieBySender, #barBySender, #pieByTime, #timeseries { 13 | text-align: center; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /Support/HTML/utilities/lodash-license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2012-2016 The Dojo Foundation 2 | Based on Underscore.js, copyright 2009-2016 Jeremy Ashkenas, 3 | DocumentCloud and Investigative Reporters & Editors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Support/HTML/utilities/lodash.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * lodash 3.10.1 (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE 4 | * Build: `lodash modern -o ./lodash.js` 5 | */ 6 | ;(function(){function n(n,t){if(n!==t){var r=null===n,e=n===w,u=n===n,o=null===t,i=t===w,f=t===t;if(n>t&&!o||!u||r&&!i&&f||e&&f)return 1;if(n=n&&9<=n&&13>=n||32==n||160==n||5760==n||6158==n||8192<=n&&(8202>=n||8232==n||8233==n||8239==n||8287==n||12288==n||65279==n); 8 | }function v(n,t){for(var r=-1,e=n.length,u=-1,o=[];++r=F&&gu&&lu?new Dn(t):null,c=t.length;a&&(i=Mn,f=false,t=a);n:for(;++oi(t,a,0)&&u.push(a);return u}function at(n,t){var r=true;return Su(n,function(n,e,u){return r=!!t(n,e,u)}),r}function ct(n,t,r,e){var u=e,o=u;return Su(n,function(n,i,f){i=+t(n,i,f),(r(i,u)||i===e&&i===o)&&(u=i, 14 | o=n)}),o}function lt(n,t){var r=[];return Su(n,function(n,e,u){t(n,e,u)&&r.push(n)}),r}function st(n,t,r,e){var u;return r(n,function(n,r,o){return t(n,r,o)?(u=e?r:n,false):void 0}),u}function pt(n,t,r,e){e||(e=[]);for(var u=-1,o=n.length;++ut&&(t=-t>u?0:u+t),r=r===w||r>u?u:+r||0,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=Be(u);++e=c)break n;o=e[o],u*="asc"===o||true===o?1:-1;break n}u=t.b-r.b}return u})}function $t(n,t){ 21 | var r=0;return Su(n,function(n,e,u){r+=+t(n,e,u)||0}),r}function St(n,t){var e=-1,u=xr(),o=n.length,i=u===r,f=i&&o>=F,a=f&&gu&&lu?new Dn(void 0):null,c=[];a?(u=Mn,i=false):(f=false,a=t?[]:c);n:for(;++eu(a,s,0)&&((t||f)&&a.push(s),c.push(l))}return c}function Ft(n,t){for(var r=-1,e=t.length,u=Be(e);++r>>1,i=n[o];(r?i<=t:iu?w:o,u=1);++e=F)return t.plant(e).value();for(var u=0,n=r?o[u].apply(this,n):e;++uarguments.length;return typeof e=="function"&&o===w&&Oo(r)?n(r,e,u,i):Ot(r,wr(e,o,4),u,i,t)}}function sr(n,t,r,e,u,o,i,f,a,c){function l(){for(var m=arguments.length,b=m,j=Be(m);b--;)j[b]=arguments[b];if(e&&(j=Mt(j,e,u)),o&&(j=qt(j,o,i)),_||y){var b=l.placeholder,k=v(j,b),m=m-k.length;if(mt?0:t)):[]}function Pr(n,t,r){var e=n?n.length:0;return e?((r?Ur(n,t,r):null==t)&&(t=1),t=e-(+t||0),Et(n,0,0>t?0:t)):[]}function Kr(n){return n?n[0]:w}function Vr(n,t,e){var u=n?n.length:0;if(!u)return-1;if(typeof e=="number")e=0>e?bu(u+e,0):e;else if(e)return e=Lt(n,t), 42 | er?bu(u+r,0):r||0,typeof n=="string"||!Oo(n)&&be(n)?r<=u&&-1t?0:+t||0,e);++r=n&&(t=w),r}}function ae(n,t,r){function e(t,r){r&&iu(r),a=p=h=w,t&&(_=ho(),c=n.apply(s,f),p||a||(f=s=w))}function u(){var n=t-(ho()-l);0>=n||n>t?e(h,a):p=su(u,n)}function o(){e(g,p); 46 | }function i(){if(f=arguments,l=ho(),s=this,h=g&&(p||!y),false===v)var r=y&&!p;else{a||y||(_=l);var e=v-(l-_),i=0>=e||e>v;i?(a&&(a=iu(a)),_=l,c=n.apply(s,f)):a||(a=su(o,e))}return i&&p?p=iu(p):p||t===v||(p=su(u,t)),r&&(i=true,c=n.apply(s,f)),!i||p||a||(f=s=w),c}var f,a,c,l,s,p,h,_=0,v=false,g=true;if(typeof n!="function")throw new Ge(L);if(t=0>t?0:+t||0,true===r)var y=true,g=false;else ge(r)&&(y=!!r.leading,v="maxWait"in r&&bu(+r.maxWait||0,t),g="trailing"in r?!!r.trailing:g);return i.cancel=function(){p&&iu(p),a&&iu(a), 47 | _=0,a=p=h=w},i}function ce(n,t){function r(){var e=arguments,u=t?t.apply(this,e):e[0],o=r.cache;return o.has(u)?o.get(u):(e=n.apply(this,e),r.cache=o.set(u,e),e)}if(typeof n!="function"||t&&typeof t!="function")throw new Ge(L);return r.cache=new ce.Cache,r}function le(n,t){if(typeof n!="function")throw new Ge(L);return t=bu(t===w?n.length-1:+t||0,0),function(){for(var r=arguments,e=-1,u=bu(r.length-t,0),o=Be(u);++et}function pe(n){return h(n)&&Er(n)&&nu.call(n,"callee")&&!cu.call(n,"callee")}function he(n,t,r,e){return e=(r=typeof r=="function"?Bt(r,e,3):w)?r(n,t):w,e===w?dt(n,t,r):!!e}function _e(n){return h(n)&&typeof n.message=="string"&&ru.call(n)==P}function ve(n){return ge(n)&&ru.call(n)==K}function ge(n){var t=typeof n;return!!n&&("object"==t||"function"==t)}function ye(n){ 49 | return null==n?false:ve(n)?uu.test(Qe.call(n)):h(n)&&Rn.test(n)}function de(n){return typeof n=="number"||h(n)&&ru.call(n)==V}function me(n){var t;if(!h(n)||ru.call(n)!=Z||pe(n)||!(nu.call(n,"constructor")||(t=n.constructor,typeof t!="function"||t instanceof t)))return false;var r;return ht(n,function(n,t){r=t}),r===w||nu.call(n,r)}function we(n){return ge(n)&&ru.call(n)==Y}function be(n){return typeof n=="string"||h(n)&&ru.call(n)==G}function xe(n){return h(n)&&Sr(n.length)&&!!Sn[ru.call(n)]}function Ae(n,t){ 50 | return nt||!n||!mu(t))return r;do t%2&&(r+=n),t=yu(t/2),n+=n;while(t);return r}function We(n,t,r){var e=n;return(n=u(n))?(r?Ur(e,t,r):null==t)?n.slice(g(n),y(n)+1):(t+="",n.slice(o(n,t),i(n,t)+1)):n}function $e(n,t,r){return r&&Ur(n,t,r)&&(t=w),n=u(n),n.match(t||Wn)||[]}function Se(n,t,r){return r&&Ur(n,t,r)&&(t=w),h(n)?Ne(n):ut(n,t)}function Fe(n){ 52 | return n}function Ne(n){return bt(ot(n,true))}function Te(n,t,r){if(null==r){var e=ge(t),u=e?zo(t):w;((u=u&&u.length?gt(t,u):w)?u.length:e)||(u=false,r=t,t=n,n=this)}u||(u=gt(t,zo(t)));var o=true,e=-1,i=ve(n),f=u.length;false===r?o=false:ge(r)&&"chain"in r&&(o=r.chain);for(;++e=$)return r}else n=0;return Lu(r,e)}}(),Mu=le(function(n,t){ 55 | return h(n)&&Er(n)?ft(n,pt(t,false,true)):[]}),qu=tr(),Pu=tr(true),Ku=le(function(n){for(var t=n.length,e=t,u=Be(l),o=xr(),i=o===r,f=[];e--;){var a=n[e]=Er(a=n[e])?a:[];u[e]=i&&120<=a.length&&gu&&lu?new Dn(e&&a):null}var i=n[0],c=-1,l=i?i.length:0,s=u[0];n:for(;++c(s?Mn(s,a):o(f,a,0))){for(e=t;--e;){var p=u[e];if(0>(p?Mn(p,a):o(n[e],a,0)))continue n}s&&s.push(a),f.push(a)}return f}),Vu=le(function(t,r){r=pt(r);var e=rt(t,r);return It(t,r.sort(n)),e}),Zu=vr(),Yu=vr(true),Gu=le(function(n){return St(pt(n,false,true)); 56 | }),Ju=le(function(n,t){return Er(n)?ft(n,t):[]}),Xu=le(Jr),Hu=le(function(n){var t=n.length,r=2--n?t.apply(this,arguments):void 0}},Nn.ary=function(n,t,r){return r&&Ur(n,t,r)&&(t=w),t=n&&null==t?n.length:bu(+t||0,0),gr(n,E,w,w,w,w,t)},Nn.assign=Co,Nn.at=no,Nn.before=fe,Nn.bind=_o,Nn.bindAll=vo,Nn.bindKey=go,Nn.callback=Se,Nn.chain=Qr,Nn.chunk=function(n,t,r){t=(r?Ur(n,t,r):null==t)?1:bu(yu(t)||1,1),r=0;for(var e=n?n.length:0,u=-1,o=Be(vu(e/t));rr&&(r=-r>u?0:u+r),e=e===w||e>u?u:+e||0,0>e&&(e+=u),u=r>e?0:e>>>0,r>>>=0;rt?0:t)):[]},Nn.takeRight=function(n,t,r){var e=n?n.length:0;return e?((r?Ur(n,t,r):null==t)&&(t=1),t=e-(+t||0),Et(n,0>t?0:t)):[]},Nn.takeRightWhile=function(n,t,r){ 71 | return n&&n.length?Nt(n,wr(t,r,3),false,true):[]},Nn.takeWhile=function(n,t,r){return n&&n.length?Nt(n,wr(t,r,3)):[]},Nn.tap=function(n,t,r){return t.call(r,n),n},Nn.throttle=function(n,t,r){var e=true,u=true;if(typeof n!="function")throw new Ge(L);return false===r?e=false:ge(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),ae(n,t,{leading:e,maxWait:+t,trailing:u})},Nn.thru=ne,Nn.times=function(n,t,r){if(n=yu(n),1>n||!mu(n))return[];var e=-1,u=Be(xu(n,4294967295));for(t=Bt(t,r,1);++ee?u[e]=t(e):t(e); 72 | return u},Nn.toArray=je,Nn.toPlainObject=ke,Nn.transform=function(n,t,r,e){var u=Oo(n)||xe(n);return t=wr(t,e,4),null==r&&(u||ge(n)?(e=n.constructor,r=u?Oo(n)?new e:[]:$u(ve(e)?e.prototype:w)):r={}),(u?Pn:_t)(n,function(n,e,u){return t(r,n,e,u)}),r},Nn.union=Gu,Nn.uniq=Gr,Nn.unzip=Jr,Nn.unzipWith=Xr,Nn.values=Ee,Nn.valuesIn=function(n){return Ft(n,Re(n))},Nn.where=function(n,t){return re(n,bt(t))},Nn.without=Ju,Nn.wrap=function(n,t){return t=null==t?Fe:t,gr(t,R,w,[n],[])},Nn.xor=function(){for(var n=-1,t=arguments.length;++nr?0:+r||0,e),r-=t.length,0<=r&&n.indexOf(t,r)==r},Nn.escape=function(n){return(n=u(n))&&hn.test(n)?n.replace(sn,c):n},Nn.escapeRegExp=function(n){return(n=u(n))&&bn.test(n)?n.replace(wn,l):n||"(?:)"},Nn.every=te,Nn.find=ro,Nn.findIndex=qu,Nn.findKey=$o,Nn.findLast=eo, 75 | Nn.findLastIndex=Pu,Nn.findLastKey=So,Nn.findWhere=function(n,t){return ro(n,bt(t))},Nn.first=Kr,Nn.floor=ni,Nn.get=function(n,t,r){return n=null==n?w:yt(n,Dr(t),t+""),n===w?r:n},Nn.gt=se,Nn.gte=function(n,t){return n>=t},Nn.has=function(n,t){if(null==n)return false;var r=nu.call(n,t);if(!r&&!Wr(t)){if(t=Dr(t),n=1==t.length?n:yt(n,Et(t,0,-1)),null==n)return false;t=Zr(t),r=nu.call(n,t)}return r||Sr(n.length)&&Cr(t,n.length)&&(Oo(n)||pe(n))},Nn.identity=Fe,Nn.includes=ee,Nn.indexOf=Vr,Nn.inRange=function(n,t,r){ 76 | return t=+t||0,r===w?(r=t,t=0):r=+r||0,n>=xu(t,r)&&nr?bu(e+r,0):xu(r||0,e-1))+1;else if(r)return u=Lt(n,t,true)-1,n=n[u],(t===t?t===n:n!==n)?u:-1; 78 | if(t!==t)return p(n,u,true);for(;u--;)if(n[u]===t)return u;return-1},Nn.lt=Ae,Nn.lte=function(n,t){return n<=t},Nn.max=ti,Nn.min=ri,Nn.noConflict=function(){return Zn._=eu,this},Nn.noop=Le,Nn.now=ho,Nn.pad=function(n,t,r){n=u(n),t=+t;var e=n.length;return er?0:+r||0,n.length),n.lastIndexOf(t,r)==r},Nn.sum=function(n,t,r){if(r&&Ur(n,t,r)&&(t=w),t=wr(t,r,3),1==t.length){n=Oo(n)?n:zr(n),r=n.length;for(var e=0;r--;)e+=+t(n[r])||0;n=e}else n=$t(n,t);return n},Nn.template=function(n,t,r){var e=Nn.templateSettings;r&&Ur(n,t,r)&&(t=r=w),n=u(n),t=nt(tt({},r||t),e,Qn),r=nt(tt({},t.imports),e.imports,Qn); 81 | var o,i,f=zo(r),a=Ft(r,f),c=0;r=t.interpolate||Cn;var l="__p+='";r=Ze((t.escape||Cn).source+"|"+r.source+"|"+(r===gn?jn:Cn).source+"|"+(t.evaluate||Cn).source+"|$","g");var p="sourceURL"in t?"//# sourceURL="+t.sourceURL+"\n":"";if(n.replace(r,function(t,r,e,u,f,a){return e||(e=u),l+=n.slice(c,a).replace(Un,s),r&&(o=true,l+="'+__e("+r+")+'"),f&&(i=true,l+="';"+f+";\n__p+='"),e&&(l+="'+((__t=("+e+"))==null?'':__t)+'"),c=a+t.length,t}),l+="';",(t=t.variable)||(l="with(obj){"+l+"}"),l=(i?l.replace(fn,""):l).replace(an,"$1").replace(cn,"$1;"), 82 | l="function("+(t||"obj")+"){"+(t?"":"obj||(obj={});")+"var __t,__p=''"+(o?",__e=_.escape":"")+(i?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}",t=Jo(function(){return qe(f,p+"return "+l).apply(w,a)}),t.source=l,_e(t))throw t;return t},Nn.trim=We,Nn.trimLeft=function(n,t,r){var e=n;return(n=u(n))?n.slice((r?Ur(e,t,r):null==t)?g(n):o(n,t+"")):n},Nn.trimRight=function(n,t,r){var e=n;return(n=u(n))?(r?Ur(e,t,r):null==t)?n.slice(0,y(n)+1):n.slice(0,i(n,t+"")+1):n; 83 | },Nn.trunc=function(n,t,r){r&&Ur(n,t,r)&&(t=w);var e=U;if(r=W,null!=t)if(ge(t)){var o="separator"in t?t.separator:o,e="length"in t?+t.length||0:e;r="omission"in t?u(t.omission):r}else e=+t||0;if(n=u(n),e>=n.length)return n;if(e-=r.length,1>e)return r;if(t=n.slice(0,e),null==o)return t+r;if(we(o)){if(n.slice(e).search(o)){var i,f=n.slice(0,e);for(o.global||(o=Ze(o.source,(kn.exec(o)||"")+"g")),o.lastIndex=0;n=o.exec(f);)i=n.index;t=t.slice(0,null==i?e:i)}}else n.indexOf(o,e)!=e&&(o=t.lastIndexOf(o), 84 | -1u.__dir__?"Right":"")}),u},zn.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),Pn(["filter","map","takeWhile"],function(n,t){ 86 | var r=t+1,e=r!=T;zn.prototype[n]=function(n,t){var u=this.clone();return u.__iteratees__.push({iteratee:wr(n,t,1),type:r}),u.__filtered__=u.__filtered__||e,u}}),Pn(["first","last"],function(n,t){var r="take"+(t?"Right":"");zn.prototype[n]=function(){return this[r](1).value()[0]}}),Pn(["initial","rest"],function(n,t){var r="drop"+(t?"":"Right");zn.prototype[n]=function(){return this.__filtered__?new zn(this):this[r](1)}}),Pn(["pluck","where"],function(n,t){var r=t?"filter":"map",e=t?bt:ze;zn.prototype[n]=function(n){ 87 | return this[r](e(n))}}),zn.prototype.compact=function(){return this.filter(Fe)},zn.prototype.reject=function(n,t){return n=wr(n,t,1),this.filter(function(t){return!n(t)})},zn.prototype.slice=function(n,t){n=null==n?0:+n||0;var r=this;return r.__filtered__&&(0t)?new zn(r):(0>n?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==w&&(t=+t||0,r=0>t?r.dropRight(-t):r.take(t-n)),r)},zn.prototype.takeRightWhile=function(n,t){return this.reverse().takeWhile(n,t).reverse()},zn.prototype.toArray=function(){return this.take(Ru); 88 | },_t(zn.prototype,function(n,t){var r=/^(?:filter|map|reject)|While$/.test(t),e=/^(?:first|last)$/.test(t),u=Nn[e?"take"+("last"==t?"Right":""):t];u&&(Nn.prototype[t]=function(){function t(n){return e&&i?u(n,1)[0]:u.apply(w,Jn([n],o))}var o=e?[1]:arguments,i=this.__chain__,f=this.__wrapped__,a=!!this.__actions__.length,c=f instanceof zn,l=o[0],s=c||Oo(f);return s&&r&&typeof l=="function"&&1!=l.length&&(c=s=false),l={func:ne,args:[t],thisArg:w},a=c&&!a,e&&!i?a?(f=f.clone(),f.__actions__.push(l),n.call(f)):u.call(w,this.value())[0]:!e&&s?(f=a?f:new zn(this), 89 | f=n.apply(f,o),f.__actions__.push(l),new Ln(f,i)):this.thru(t)})}),Pn("join pop push replace shift sort splice split unshift".split(" "),function(n){var t=(/^(?:replace|split)$/.test(n)?He:Je)[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:join|pop|replace|shift)$/.test(n);Nn.prototype[n]=function(){var n=arguments;return e&&!this.__chain__?t.apply(this.value(),n):this[r](function(r){return t.apply(r,n)})}}),_t(zn.prototype,function(n,t){var r=Nn[t];if(r){var e=r.name+"";(Wu[e]||(Wu[e]=[])).push({ 90 | name:t,func:r})}}),Wu[sr(w,A).name]=[{name:"wrapper",func:w}],zn.prototype.clone=function(){var n=new zn(this.__wrapped__);return n.__actions__=qn(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=qn(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=qn(this.__views__),n},zn.prototype.reverse=function(){if(this.__filtered__){var n=new zn(this);n.__dir__=-1,n.__filtered__=true}else n=this.clone(),n.__dir__*=-1;return n},zn.prototype.value=function(){ 91 | var n,t=this.__wrapped__.value(),r=this.__dir__,e=Oo(t),u=0>r,o=e?t.length:0;n=o;for(var i=this.__views__,f=0,a=-1,c=i.length;++a"'`]/g,pn=RegExp(ln.source),hn=RegExp(sn.source),_n=/<%-([\s\S]+?)%>/g,vn=/<%([\s\S]+?)%>/g,gn=/<%=([\s\S]+?)%>/g,yn=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,dn=/^\w*$/,mn=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,wn=/^[:!,]|[\\^$.*+?()[\]{}|\/]|(^[0-9a-fA-Fnrtuvx])|([\n\r\u2028\u2029])/g,bn=RegExp(wn.source),xn=/[\u0300-\u036f\ufe20-\ufe23]/g,An=/\\(\\)?/g,jn=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,kn=/\w*$/,In=/^0[xX]/,Rn=/^\[object .+?Constructor\]$/,On=/^\d+$/,En=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,Cn=/($^)/,Un=/['\n\r\u2028\u2029\\]/g,Wn=RegExp("[A-Z\\xc0-\\xd6\\xd8-\\xde]+(?=[A-Z\\xc0-\\xd6\\xd8-\\xde][a-z\\xdf-\\xf6\\xf8-\\xff]+)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+|[A-Z\\xc0-\\xd6\\xd8-\\xde]+|[0-9]+","g"),$n="Array ArrayBuffer Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Math Number Object RegExp Set String _ clearTimeout isFinite parseFloat parseInt setTimeout TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap".split(" "),Sn={}; 94 | Sn[X]=Sn[H]=Sn[Q]=Sn[nn]=Sn[tn]=Sn[rn]=Sn[en]=Sn[un]=Sn[on]=true,Sn[B]=Sn[D]=Sn[J]=Sn[M]=Sn[q]=Sn[P]=Sn[K]=Sn["[object Map]"]=Sn[V]=Sn[Z]=Sn[Y]=Sn["[object Set]"]=Sn[G]=Sn["[object WeakMap]"]=false;var Fn={};Fn[B]=Fn[D]=Fn[J]=Fn[M]=Fn[q]=Fn[X]=Fn[H]=Fn[Q]=Fn[nn]=Fn[tn]=Fn[V]=Fn[Z]=Fn[Y]=Fn[G]=Fn[rn]=Fn[en]=Fn[un]=Fn[on]=true,Fn[P]=Fn[K]=Fn["[object Map]"]=Fn["[object Set]"]=Fn["[object WeakMap]"]=false;var Nn={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a", 95 | "\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y", 96 | "\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss"},Tn={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},Ln={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},zn={"function":true,object:true},Bn={0:"x30",1:"x31",2:"x32",3:"x33",4:"x34",5:"x35",6:"x36",7:"x37",8:"x38",9:"x39",A:"x41",B:"x42",C:"x43",D:"x44",E:"x45",F:"x46",a:"x61",b:"x62",c:"x63",d:"x64",e:"x65",f:"x66",n:"x6e",r:"x72",t:"x74",u:"x75",v:"x76",x:"x78"},Dn={"\\":"\\", 97 | "'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Mn=zn[typeof exports]&&exports&&!exports.nodeType&&exports,qn=zn[typeof module]&&module&&!module.nodeType&&module,Pn=zn[typeof self]&&self&&self.Object&&self,Kn=zn[typeof window]&&window&&window.Object&&window,Vn=qn&&qn.exports===Mn&&Mn,Zn=Mn&&qn&&typeof global=="object"&&global&&global.Object&&global||Kn!==(this&&this.window)&&Kn||Pn||this,Yn=m();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Zn._=Yn, define(function(){ 98 | return Yn})):Mn&&qn?Vn?(qn.exports=Yn)._=Yn:Mn._=Yn:Zn._=Yn}).call(this); -------------------------------------------------------------------------------- /Support/HTML/utilities/saveSvgAsPng-license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Eric Shull 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Support/HTML/utilities/saveSvgAsPng.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this; 3 | 4 | var doctype = ']>'; 5 | 6 | function isElement(obj) { 7 | return obj instanceof HTMLElement || obj instanceof SVGElement; 8 | } 9 | 10 | function requireDomNode(el) { 11 | if (!isElement(el)) { 12 | throw new Error('an HTMLElement or SVGElement is required; got ' + el); 13 | } 14 | } 15 | 16 | function isExternal(url) { 17 | return url && url.lastIndexOf('http',0) == 0 && url.lastIndexOf(window.location.host) == -1; 18 | } 19 | 20 | function inlineImages(el, callback) { 21 | requireDomNode(el); 22 | 23 | var images = el.querySelectorAll('image'), 24 | left = images.length, 25 | checkDone = function() { 26 | if (left === 0) { 27 | callback(); 28 | } 29 | }; 30 | 31 | checkDone(); 32 | for (var i = 0; i < images.length; i++) { 33 | (function(image) { 34 | var href = image.getAttributeNS("http://www.w3.org/1999/xlink", "href"); 35 | if (href) { 36 | if (isExternal(href.value)) { 37 | console.warn("Cannot render embedded images linking to external hosts: "+href.value); 38 | return; 39 | } 40 | } 41 | var canvas = document.createElement('canvas'); 42 | var ctx = canvas.getContext('2d'); 43 | var img = new Image(); 44 | href = href || image.getAttribute('href'); 45 | if (href) { 46 | img.src = href; 47 | img.onload = function() { 48 | canvas.width = img.width; 49 | canvas.height = img.height; 50 | ctx.drawImage(img, 0, 0); 51 | image.setAttributeNS("http://www.w3.org/1999/xlink", "href", canvas.toDataURL('image/png')); 52 | left--; 53 | checkDone(); 54 | } 55 | img.onerror = function() { 56 | console.log("Could not load "+href); 57 | left--; 58 | checkDone(); 59 | } 60 | } else { 61 | left--; 62 | checkDone(); 63 | } 64 | })(images[i]); 65 | } 66 | } 67 | 68 | function styles(el, selectorRemap) { 69 | var css = ""; 70 | var sheets = document.styleSheets; 71 | for (var i = 0; i < sheets.length; i++) { 72 | try { 73 | var rules = sheets[i].cssRules; 74 | } catch (e) { 75 | console.warn("Stylesheet could not be loaded: "+sheets[i].href); 76 | continue; 77 | } 78 | 79 | if (rules != null) { 80 | for (var j = 0; j < rules.length; j++) { 81 | var rule = rules[j]; 82 | if (typeof(rule.style) != "undefined") { 83 | var match, selectorText; 84 | 85 | try { 86 | selectorText = rule.selectorText; 87 | } catch(err) { 88 | console.warn('The following CSS rule has an invalid selector: "' + rule + '"', err); 89 | } 90 | 91 | try { 92 | if (selectorText) { 93 | match = el.querySelector(selectorText); 94 | } 95 | } catch(err) { 96 | console.warn('Invalid CSS selector "' + selectorText + '"', err); 97 | } 98 | 99 | if (match) { 100 | var selector = selectorRemap ? selectorRemap(rule.selectorText) : rule.selectorText; 101 | css += selector + " { " + rule.style.cssText + " }\n"; 102 | } else if(rule.cssText.match(/^@font-face/)) { 103 | css += rule.cssText + '\n'; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | return css; 110 | } 111 | 112 | function getDimension(el, clone, dim) { 113 | var v = (el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) || 114 | (clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) || 115 | el.getBoundingClientRect()[dim] || 116 | parseInt(clone.style[dim]) || 117 | parseInt(window.getComputedStyle(el).getPropertyValue(dim)); 118 | return (typeof v === 'undefined' || v === null || isNaN(parseFloat(v))) ? 0 : v; 119 | } 120 | 121 | function reEncode(data) { 122 | data = encodeURIComponent(data); 123 | data = data.replace(/%([0-9A-F]{2})/g, function(match, p1) { 124 | var c = String.fromCharCode('0x'+p1); 125 | return c === '%' ? '%25' : c; 126 | }); 127 | return decodeURIComponent(data); 128 | } 129 | 130 | out$.svgAsDataUri = function(el, options, cb) { 131 | requireDomNode(el); 132 | 133 | options = options || {}; 134 | options.scale = options.scale || 1; 135 | options.responsive = options.responsive || false; 136 | var xmlns = "http://www.w3.org/2000/xmlns/"; 137 | 138 | inlineImages(el, function() { 139 | var outer = document.createElement("div"); 140 | var clone = el.cloneNode(true); 141 | var width, height; 142 | if(el.tagName == 'svg') { 143 | width = options.width || getDimension(el, clone, 'width'); 144 | height = options.height || getDimension(el, clone, 'height'); 145 | } else if(el.getBBox) { 146 | var box = el.getBBox(); 147 | width = box.x + box.width; 148 | height = box.y + box.height; 149 | clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '')); 150 | 151 | var svg = document.createElementNS('http://www.w3.org/2000/svg','svg') 152 | svg.appendChild(clone) 153 | clone = svg; 154 | } else { 155 | console.error('Attempted to render non-SVG element', el); 156 | return; 157 | } 158 | 159 | clone.setAttribute("version", "1.1"); 160 | if (!clone.getAttribute('xmlns')) { 161 | clone.setAttributeNS(xmlns, "xmlns", "http://www.w3.org/2000/svg"); 162 | } 163 | if (!clone.getAttribute('xmlns:xlink')) { 164 | clone.setAttributeNS(xmlns, "xmlns:xlink", "http://www.w3.org/1999/xlink"); 165 | } 166 | 167 | if (options.responsive) { 168 | clone.removeAttribute('width'); 169 | clone.removeAttribute('height'); 170 | clone.setAttribute('preserveAspectRatio', 'xMinYMin meet'); 171 | } else { 172 | clone.setAttribute("width", width * options.scale); 173 | clone.setAttribute("height", height * options.scale); 174 | } 175 | 176 | clone.setAttribute("viewBox", [ 177 | options.left || 0, 178 | options.top || 0, 179 | width, 180 | height 181 | ].join(" ")); 182 | 183 | var fos = clone.querySelectorAll('foreignObject > *'); 184 | for (var i = 0; i < fos.length; i++) { 185 | if (!fos[i].getAttributeNS('xml', 'xmlns')) { 186 | fos[i].setAttributeNS(xmlns, "xmlns", "http://www.w3.org/1999/xhtml"); 187 | } 188 | } 189 | 190 | outer.appendChild(clone); 191 | 192 | var css = styles(el, options.selectorRemap); 193 | var s = document.createElement('style'); 194 | s.setAttribute('type', 'text/css'); 195 | s.innerHTML = ""; 196 | var defs = document.createElement('defs'); 197 | defs.appendChild(s); 198 | clone.insertBefore(defs, clone.firstChild); 199 | 200 | var svg = doctype + outer.innerHTML; 201 | var uri = 'data:image/svg+xml;base64,' + window.btoa(reEncode(svg)); 202 | if (cb) { 203 | cb(uri); 204 | } 205 | }); 206 | } 207 | 208 | out$.svgAsPngUri = function(el, options, cb) { 209 | requireDomNode(el); 210 | 211 | options.encoderType = options.encoderType || 'image/png'; 212 | options.encoderOptions = options.encoderOptions || 0.8; 213 | 214 | out$.svgAsDataUri(el, options, function(uri) { 215 | var image = new Image(); 216 | image.onload = function() { 217 | var canvas = document.createElement('canvas'); 218 | canvas.width = image.width; 219 | canvas.height = image.height; 220 | var context = canvas.getContext('2d'); 221 | if(options && options.backgroundColor){ 222 | context.fillStyle = options.backgroundColor; 223 | context.fillRect(0, 0, canvas.width, canvas.height); 224 | } 225 | context.drawImage(image, 0, 0); 226 | var a = document.createElement('a'), png; 227 | try { 228 | png = canvas.toDataURL(options.encoderType, options.encoderOptions); 229 | } catch (e) { 230 | if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name == "SecurityError") { 231 | console.error("Rendered SVG images cannot be downloaded in this browser."); 232 | return; 233 | } else { 234 | throw e; 235 | } 236 | } 237 | cb(png); 238 | } 239 | image.onerror = function() { 240 | console.error( 241 | 'There was an error loading the data URI as an image on the following SVG\n', 242 | window.atob(uri.slice(26)), '\n', 243 | "Open the following link to see browser's diagnosis\n", 244 | uri); 245 | } 246 | image.src = uri; 247 | }); 248 | } 249 | 250 | function download(name, uri) { 251 | var a = document.createElement('a'); 252 | a.download = name; 253 | a.href = uri; 254 | document.body.appendChild(a); 255 | a.click(); 256 | a.parentNode.removeChild(a); 257 | } 258 | 259 | out$.saveSvg = function(el, name, options) { 260 | requireDomNode(el); 261 | 262 | options = options || {}; 263 | out$.svgAsDataUri(el, options, function(uri) { 264 | download(name, uri); 265 | }); 266 | } 267 | 268 | out$.saveSvgAsPng = function(el, name, options) { 269 | requireDomNode(el); 270 | 271 | options = options || {}; 272 | out$.svgAsPngUri(el, options, function(uri) { 273 | download(name, uri); 274 | }); 275 | } 276 | 277 | // if define is defined create as an AMD module 278 | if (typeof define !== 'undefined') { 279 | define(function() { 280 | return out$; 281 | }); 282 | } 283 | })(); 284 | -------------------------------------------------------------------------------- /Support/HTML/utilities/spectrum-license: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining 2 | a copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be 10 | included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 13 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Support/HTML/utilities/spectrum.css: -------------------------------------------------------------------------------- 1 | /*** 2 | Spectrum Colorpicker v1.8.0 3 | https://github.com/bgrins/spectrum 4 | Author: Brian Grinstead 5 | License: MIT 6 | ***/ 7 | 8 | .sp-container { 9 | position:absolute; 10 | top:0; 11 | left:0; 12 | display:inline-block; 13 | *display: inline; 14 | *zoom: 1; 15 | /* https://github.com/bgrins/spectrum/issues/40 */ 16 | z-index: 9999994; 17 | overflow: hidden; 18 | } 19 | .sp-container.sp-flat { 20 | position: relative; 21 | } 22 | 23 | /* Fix for * { box-sizing: border-box; } */ 24 | .sp-container, 25 | .sp-container * { 26 | -webkit-box-sizing: content-box; 27 | -moz-box-sizing: content-box; 28 | box-sizing: content-box; 29 | } 30 | 31 | /* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */ 32 | .sp-top { 33 | position:relative; 34 | width: 100%; 35 | display:inline-block; 36 | } 37 | .sp-top-inner { 38 | position:absolute; 39 | top:0; 40 | left:0; 41 | bottom:0; 42 | right:0; 43 | } 44 | .sp-color { 45 | position: absolute; 46 | top:0; 47 | left:0; 48 | bottom:0; 49 | right:20%; 50 | } 51 | .sp-hue { 52 | position: absolute; 53 | top:0; 54 | right:0; 55 | bottom:0; 56 | left:84%; 57 | height: 100%; 58 | } 59 | 60 | .sp-clear-enabled .sp-hue { 61 | top:33px; 62 | height: 77.5%; 63 | } 64 | 65 | .sp-fill { 66 | padding-top: 80%; 67 | } 68 | .sp-sat, .sp-val { 69 | position: absolute; 70 | top:0; 71 | left:0; 72 | right:0; 73 | bottom:0; 74 | } 75 | 76 | .sp-alpha-enabled .sp-top { 77 | margin-bottom: 18px; 78 | } 79 | .sp-alpha-enabled .sp-alpha { 80 | display: block; 81 | } 82 | .sp-alpha-handle { 83 | position:absolute; 84 | top:-4px; 85 | bottom: -4px; 86 | width: 6px; 87 | left: 50%; 88 | cursor: pointer; 89 | border: 1px solid black; 90 | background: white; 91 | opacity: .8; 92 | } 93 | .sp-alpha { 94 | display: none; 95 | position: absolute; 96 | bottom: -14px; 97 | right: 0; 98 | left: 0; 99 | height: 8px; 100 | } 101 | .sp-alpha-inner { 102 | border: solid 1px #333; 103 | } 104 | 105 | .sp-clear { 106 | display: none; 107 | } 108 | 109 | .sp-clear.sp-clear-display { 110 | background-position: center; 111 | } 112 | 113 | .sp-clear-enabled .sp-clear { 114 | display: block; 115 | position:absolute; 116 | top:0px; 117 | right:0; 118 | bottom:0; 119 | left:84%; 120 | height: 28px; 121 | } 122 | 123 | /* Don't allow text selection */ 124 | .sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button { 125 | -webkit-user-select:none; 126 | -moz-user-select: -moz-none; 127 | -o-user-select:none; 128 | user-select: none; 129 | } 130 | 131 | .sp-container.sp-input-disabled .sp-input-container { 132 | display: none; 133 | } 134 | .sp-container.sp-buttons-disabled .sp-button-container { 135 | display: none; 136 | } 137 | .sp-container.sp-palette-buttons-disabled .sp-palette-button-container { 138 | display: none; 139 | } 140 | .sp-palette-only .sp-picker-container { 141 | display: none; 142 | } 143 | .sp-palette-disabled .sp-palette-container { 144 | display: none; 145 | } 146 | 147 | .sp-initial-disabled .sp-initial { 148 | display: none; 149 | } 150 | 151 | 152 | /* Gradients for hue, saturation and value instead of images. Not pretty... but it works */ 153 | .sp-sat { 154 | background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0))); 155 | background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0)); 156 | background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); 157 | background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); 158 | background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); 159 | background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0)); 160 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)"; 161 | filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81'); 162 | } 163 | .sp-val { 164 | background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0))); 165 | background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); 166 | background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); 167 | background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); 168 | background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); 169 | background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0)); 170 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)"; 171 | filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000'); 172 | } 173 | 174 | .sp-hue { 175 | background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); 176 | background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); 177 | background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); 178 | background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000)); 179 | background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); 180 | background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); 181 | } 182 | 183 | /* IE filters do not support multiple color stops. 184 | Generate 6 divs, line them up, and do two color gradients for each. 185 | Yes, really. 186 | */ 187 | .sp-1 { 188 | height:17%; 189 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00'); 190 | } 191 | .sp-2 { 192 | height:16%; 193 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00'); 194 | } 195 | .sp-3 { 196 | height:17%; 197 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff'); 198 | } 199 | .sp-4 { 200 | height:17%; 201 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff'); 202 | } 203 | .sp-5 { 204 | height:16%; 205 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff'); 206 | } 207 | .sp-6 { 208 | height:17%; 209 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000'); 210 | } 211 | 212 | .sp-hidden { 213 | display: none !important; 214 | } 215 | 216 | /* Clearfix hack */ 217 | .sp-cf:before, .sp-cf:after { content: ""; display: table; } 218 | .sp-cf:after { clear: both; } 219 | .sp-cf { *zoom: 1; } 220 | 221 | /* Mobile devices, make hue slider bigger so it is easier to slide */ 222 | @media (max-device-width: 480px) { 223 | .sp-color { right: 40%; } 224 | .sp-hue { left: 63%; } 225 | .sp-fill { padding-top: 60%; } 226 | } 227 | .sp-dragger { 228 | border-radius: 5px; 229 | height: 5px; 230 | width: 5px; 231 | border: 1px solid #fff; 232 | background: #000; 233 | cursor: pointer; 234 | position:absolute; 235 | top:0; 236 | left: 0; 237 | } 238 | .sp-slider { 239 | position: absolute; 240 | top:0; 241 | cursor:pointer; 242 | height: 3px; 243 | left: -1px; 244 | right: -1px; 245 | border: 1px solid #000; 246 | background: white; 247 | opacity: .8; 248 | } 249 | 250 | /* 251 | Theme authors: 252 | Here are the basic themeable display options (colors, fonts, global widths). 253 | See http://bgrins.github.io/spectrum/themes/ for instructions. 254 | */ 255 | 256 | .sp-container { 257 | border-radius: 0; 258 | background-color: #ECECEC; 259 | border: solid 1px #f0c49B; 260 | padding: 0; 261 | } 262 | .sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear { 263 | font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; 264 | -webkit-box-sizing: border-box; 265 | -moz-box-sizing: border-box; 266 | -ms-box-sizing: border-box; 267 | box-sizing: border-box; 268 | } 269 | .sp-top { 270 | margin-bottom: 3px; 271 | } 272 | .sp-color, .sp-hue, .sp-clear { 273 | border: solid 1px #666; 274 | } 275 | 276 | /* Input */ 277 | .sp-input-container { 278 | float:right; 279 | width: 100px; 280 | margin-bottom: 4px; 281 | } 282 | .sp-initial-disabled .sp-input-container { 283 | width: 100%; 284 | } 285 | .sp-input { 286 | font-size: 12px !important; 287 | border: 1px inset; 288 | padding: 4px 5px; 289 | margin: 0; 290 | width: 100%; 291 | background:transparent; 292 | border-radius: 3px; 293 | color: #222; 294 | } 295 | .sp-input:focus { 296 | border: 1px solid orange; 297 | } 298 | .sp-input.sp-validation-error { 299 | border: 1px solid red; 300 | background: #fdd; 301 | } 302 | .sp-picker-container , .sp-palette-container { 303 | float:left; 304 | position: relative; 305 | padding: 10px; 306 | padding-bottom: 300px; 307 | margin-bottom: -290px; 308 | } 309 | .sp-picker-container { 310 | width: 172px; 311 | border-left: solid 1px #fff; 312 | } 313 | 314 | /* Palettes */ 315 | .sp-palette-container { 316 | border-right: solid 1px #ccc; 317 | } 318 | 319 | .sp-palette-only .sp-palette-container { 320 | border: 0; 321 | } 322 | 323 | .sp-palette .sp-thumb-el { 324 | display: block; 325 | position:relative; 326 | float:left; 327 | width: 24px; 328 | height: 15px; 329 | margin: 3px; 330 | cursor: pointer; 331 | border:solid 2px transparent; 332 | } 333 | .sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active { 334 | border-color: orange; 335 | } 336 | .sp-thumb-el { 337 | position:relative; 338 | } 339 | 340 | /* Initial */ 341 | .sp-initial { 342 | float: left; 343 | border: solid 1px #333; 344 | } 345 | .sp-initial span { 346 | width: 30px; 347 | height: 25px; 348 | border:none; 349 | display:block; 350 | float:left; 351 | margin:0; 352 | } 353 | 354 | .sp-initial .sp-clear-display { 355 | background-position: center; 356 | } 357 | 358 | /* Buttons */ 359 | .sp-palette-button-container, 360 | .sp-button-container { 361 | float: right; 362 | } 363 | 364 | /* Replacer (the little preview div that shows up instead of the ) */ 365 | .sp-replacer { 366 | margin:0; 367 | overflow:hidden; 368 | cursor:pointer; 369 | padding: 4px; 370 | display:inline-block; 371 | *zoom: 1; 372 | *display: inline; 373 | border: solid 1px #91765d; 374 | background: #eee; 375 | color: #333; 376 | vertical-align: middle; 377 | } 378 | .sp-replacer:hover, .sp-replacer.sp-active { 379 | border-color: #F0C49B; 380 | color: #111; 381 | } 382 | .sp-replacer.sp-disabled { 383 | cursor:default; 384 | border-color: silver; 385 | color: silver; 386 | } 387 | .sp-dd { 388 | padding: 2px 0; 389 | height: 16px; 390 | line-height: 16px; 391 | float:left; 392 | font-size:10px; 393 | } 394 | .sp-preview { 395 | position:relative; 396 | width:25px; 397 | height: 20px; 398 | border: solid 1px #222; 399 | margin-right: 5px; 400 | float:left; 401 | z-index: 0; 402 | } 403 | 404 | .sp-palette { 405 | *width: 220px; 406 | max-width: 220px; 407 | } 408 | .sp-palette .sp-thumb-el { 409 | width:16px; 410 | height: 16px; 411 | margin:2px 1px; 412 | border: solid 1px #d0d0d0; 413 | } 414 | 415 | .sp-container { 416 | padding-bottom:0; 417 | } 418 | 419 | 420 | /* Buttons: http://hellohappy.org/css3-buttons/ */ 421 | .sp-container button { 422 | background-color: #eeeeee; 423 | background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc); 424 | background-image: -moz-linear-gradient(top, #eeeeee, #cccccc); 425 | background-image: -ms-linear-gradient(top, #eeeeee, #cccccc); 426 | background-image: -o-linear-gradient(top, #eeeeee, #cccccc); 427 | background-image: linear-gradient(to bottom, #eeeeee, #cccccc); 428 | border: 1px solid #ccc; 429 | border-bottom: 1px solid #bbb; 430 | border-radius: 3px; 431 | color: #333; 432 | font-size: 14px; 433 | line-height: 1; 434 | padding: 5px 4px; 435 | text-align: center; 436 | text-shadow: 0 1px 0 #eee; 437 | vertical-align: middle; 438 | } 439 | .sp-container button:hover { 440 | background-color: #dddddd; 441 | background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb); 442 | background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb); 443 | background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb); 444 | background-image: -o-linear-gradient(top, #dddddd, #bbbbbb); 445 | background-image: linear-gradient(to bottom, #dddddd, #bbbbbb); 446 | border: 1px solid #bbb; 447 | border-bottom: 1px solid #999; 448 | cursor: pointer; 449 | text-shadow: 0 1px 0 #ddd; 450 | } 451 | .sp-container button:active { 452 | border: 1px solid #aaa; 453 | border-bottom: 1px solid #888; 454 | -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; 455 | -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; 456 | -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; 457 | -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; 458 | box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; 459 | } 460 | .sp-cancel { 461 | font-size: 11px; 462 | color: #d93f3f !important; 463 | margin:0; 464 | padding:2px; 465 | margin-right: 5px; 466 | vertical-align: middle; 467 | text-decoration:none; 468 | 469 | } 470 | .sp-cancel:hover { 471 | color: #d93f3f !important; 472 | text-decoration: underline; 473 | } 474 | 475 | 476 | .sp-palette span:hover, .sp-palette span.sp-thumb-active { 477 | border-color: #000; 478 | } 479 | 480 | .sp-preview, .sp-alpha, .sp-thumb-el { 481 | position:relative; 482 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); 483 | } 484 | .sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner { 485 | display:block; 486 | position:absolute; 487 | top:0;left:0;bottom:0;right:0; 488 | } 489 | 490 | .sp-palette .sp-thumb-inner { 491 | background-position: 50% 50%; 492 | background-repeat: no-repeat; 493 | } 494 | 495 | .sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner { 496 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=); 497 | } 498 | 499 | .sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner { 500 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=); 501 | } 502 | 503 | .sp-clear-display { 504 | background-repeat:no-repeat; 505 | background-position: center; 506 | background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==); 507 | } 508 | -------------------------------------------------------------------------------- /Support/HTML/utilities/url_object.js: -------------------------------------------------------------------------------- 1 | /* This is a gist from https://gist.github.com/aymanfarhat/5608517 */ 2 | function urlObject(options) { 3 | "use strict"; 4 | /*global window, document*/ 5 | 6 | var url_search_arr, 7 | option_key, 8 | i, 9 | urlObj, 10 | get_param, 11 | key, 12 | val, 13 | url_query, 14 | url_get_params = {}, 15 | a = document.createElement('a'), 16 | default_options = { 17 | 'url': window.location.href, 18 | 'unescape': true, 19 | 'convert_num': true 20 | }; 21 | 22 | if (typeof options !== "object") { 23 | options = default_options; 24 | } else { 25 | for (option_key in default_options) { 26 | if (default_options.hasOwnProperty(option_key)) { 27 | if (options[option_key] === undefined) { 28 | options[option_key] = default_options[option_key]; 29 | } 30 | } 31 | } 32 | } 33 | 34 | a.href = options.url; 35 | url_query = a.search.substring(1); 36 | url_search_arr = url_query.split('&'); 37 | 38 | if (url_search_arr[0].length > 1) { 39 | for (i = 0; i < url_search_arr.length; i += 1) { 40 | get_param = url_search_arr[i].split("="); 41 | 42 | if (options.unescape) { 43 | key = decodeURI(get_param[0]); 44 | val = decodeURI(get_param[1]); 45 | } else { 46 | key = get_param[0]; 47 | val = get_param[1]; 48 | } 49 | 50 | if (options.convert_num) { 51 | if (val.match(/^\d+$/)) { 52 | val = parseInt(val, 10); 53 | } else if (val.match(/^\d+\.\d+$/)) { 54 | val = parseFloat(val); 55 | } 56 | } 57 | 58 | if (url_get_params[key] === undefined) { 59 | url_get_params[key] = val; 60 | } else if (typeof url_get_params[key] === "string") { 61 | url_get_params[key] = [url_get_params[key], val]; 62 | } else { 63 | url_get_params[key].push(val); 64 | } 65 | 66 | get_param = []; 67 | } 68 | } 69 | 70 | urlObj = { 71 | protocol: a.protocol, 72 | hostname: a.hostname, 73 | host: a.host, 74 | port: a.port, 75 | hash: a.hash.substr(1), 76 | pathname: a.pathname, 77 | search: a.search, 78 | parameters: url_get_params 79 | }; 80 | 81 | return urlObj; 82 | } 83 | -------------------------------------------------------------------------------- /Support/bin/display: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DISPLAY_TYPE=$1 4 | KEYS=$2 5 | 6 | SAFE_FOLDER="${TMPDIR}/${MM_BUNDLE_ITEM_UUID}" 7 | mkdir -p ${SAFE_FOLDER} 8 | FILEPATH="${SAFE_FOLDER}/data.csv" 9 | 10 | echo "${KEYS}" > ${FILEPATH} 11 | cat >> "${FILEPATH}" 12 | 13 | HTMLPATH="${MM_BUNDLE_SUPPORT}/HTML/${DISPLAY_TYPE}/index.html" 14 | 15 | cat << END 16 | { actions = ( 17 | { 18 | type = showAsHTML; 19 | filePath = '${HTMLPATH}'; 20 | parameters = { 21 | "csv_file" = "${FILEPATH}"; 22 | }; 23 | }, 24 | ); 25 | } 26 | END 27 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | contactEmailRot13 6 | qri@ffbyvzna.pbz 7 | contactName 8 | Sherif Soliman 9 | description 10 | Visualize email history and trends 11 | name 12 | Visualize 13 | uuid 14 | CDE5EA87-EAC3-42D6-8E00-B8049F55682C 15 | 16 | 17 | --------------------------------------------------------------------------------