├── .bowerrc ├── .gitignore ├── Gruntfile.js ├── README.md ├── bower.json ├── env ├── dev │ ├── clj │ │ └── asterion │ │ │ └── dev.clj │ └── cljs │ │ └── asterion │ │ └── dev.cljs └── prod │ └── cljs │ └── asterion │ └── prod.cljs ├── externs ├── dagre-d3.ext.js └── misc.js ├── less ├── colors.less ├── font-awesome-4.1.0.less ├── graph.less ├── layout.less ├── loader.less ├── main.less ├── normalize-3.0.1.less ├── overlay.less └── util.less ├── package.json ├── project.clj ├── resources └── public │ ├── app.js │ ├── css │ └── main.css │ ├── example.config.json │ ├── fonts │ ├── DroidSansMono.woff2 │ ├── FontAwesome.otf │ ├── OpenSans-Light.woff │ ├── OpenSans-Semibold.woff │ ├── OpenSans.woff │ ├── RobotoSlab-Regular.woff2 │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff │ ├── img │ ├── clojure.svg │ ├── logo.icns │ ├── logo.ico │ └── logo_96x96.png │ ├── index.html │ ├── js │ └── vendor │ │ ├── dagre-d3.min.js │ │ ├── ga.js │ │ └── palette.js │ ├── package.json │ └── prod.config.json ├── scripts ├── build-windows-exe.nsi ├── dmg │ ├── TestBkg.png │ └── TestBkg@2x.png ├── setup.bat └── setup.sh └── src ├── clj └── asterion │ ├── deps.clj │ ├── deps │ ├── clojure.clj │ └── javascript.clj │ └── server.clj ├── cljc └── graph │ └── util.cljc └── cljs ├── asterion ├── analytics.cljs ├── click.cljs ├── components.cljs ├── core.cljs ├── d3.cljs ├── deps.cljs ├── dir.cljs ├── project.cljs ├── ring.js └── tree.js └── cljs └── node └── io.cljs /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "app/components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | /classes 4 | /resources/public/cached/ 5 | tmp 6 | /checkouts 7 | pom.xml 8 | pom.xml.asc 9 | figwheel_server.log 10 | *.jar 11 | *.class 12 | *.min.css 13 | /.lein-* 14 | /.nrepl- 15 | /app/js/p 16 | /resources/public/js/p 17 | /app/projects 18 | /.repl 19 | /out 20 | /node_modules 21 | /electron 22 | /app/config.json 23 | /app/*.log 24 | /builds 25 | /app/components 26 | .nrepl-port 27 | /spec/static/test.js 28 | /.repl* 29 | *-init.clj 30 | /.idea 31 | *.iml 32 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | 4 | var moment = require('moment'), 5 | path = require('path'), 6 | fs = require('fs-plus'), 7 | asar = require('asar'); 8 | 9 | var os = (function(){ 10 | var platform = process.platform; 11 | if (/^win/.test(platform)) { return "windows"; } 12 | if (/^darwin/.test(platform)) { return "mac"; } 13 | if (/^linux/.test(platform)) { return "linux"; } 14 | return null; 15 | })(); 16 | 17 | var exe = { 18 | windows: "electron.exe", 19 | mac: "Electron.app/Contents/MacOS/Electron", 20 | linux: "electron" 21 | }; 22 | 23 | var electron_path = "electron"; 24 | 25 | //------------------------------------------------------------------------------ 26 | // ShellJS 27 | //------------------------------------------------------------------------------ 28 | 29 | require('shelljs/global'); 30 | // shelljs/global makes the following imports: 31 | // cwd, pwd, ls, find, cp, rm, mv, mkdir, test, cat, 32 | // str.to, str.toEnd, sed, grep, which, echo, 33 | // pushd, popd, dirs, ln, exit, env, exec, chmod, 34 | // tempdir, error 35 | 36 | var shellconfig = require('shelljs').config; 37 | shellconfig.silent = false; // hide shell cmd output? 38 | shellconfig.fatal = false; // stop if cmd failed? 39 | 40 | //------------------------------------------------------------------------------ 41 | // Grunt Config 42 | //------------------------------------------------------------------------------ 43 | 44 | 45 | grunt.initConfig({ 46 | 47 | less: { 48 | options: { 49 | compress: true 50 | }, 51 | 52 | default: { 53 | files: { 54 | 'resources/public/css/main.min.css': 'less/main.less' 55 | } 56 | } 57 | }, 58 | 59 | watch: { 60 | options: { 61 | atBegin: true 62 | }, 63 | 64 | less: { 65 | files: "less/*.less", 66 | tasks: "less:default" 67 | } 68 | }, 69 | 70 | 'download-electron': { 71 | version: '0.31.0', 72 | outputDir: 'electron' 73 | } 74 | 75 | }); 76 | 77 | //------------------------------------------------------------------------------ 78 | // Third-party tasks 79 | //------------------------------------------------------------------------------ 80 | 81 | 82 | grunt.loadNpmTasks('grunt-contrib-less'); 83 | grunt.loadNpmTasks('grunt-contrib-watch'); 84 | grunt.loadNpmTasks('grunt-download-electron'); 85 | if (os === "mac") { 86 | grunt.loadNpmTasks('grunt-appdmg'); 87 | } 88 | grunt.loadNpmTasks('winresourcer'); 89 | 90 | //------------------------------------------------------------------------------ 91 | // Setup Tasks 92 | //------------------------------------------------------------------------------ 93 | 94 | grunt.registerTask('setup', [ 95 | 'download-electron', 96 | 'ensure-config-exists', 97 | 'run-app-bower' 98 | ]); 99 | 100 | grunt.registerTask('ensure-config-exists', function() { 101 | pushd("app"); 102 | if (!test("-f", "config.json")) { 103 | grunt.log.writeln("Creating default config.json..."); 104 | cp("example.config.json", "config.json"); 105 | } 106 | popd(); 107 | }); 108 | 109 | grunt.registerTask('run-app-bower', function() { 110 | exec("bower install"); 111 | }); 112 | 113 | grunt.registerTask('cljsbuild-prod', function() { 114 | grunt.log.writeln("\nCleaning and building ClojureScript production files..."); 115 | exec("lein do clean, with-profile production cljsbuild once"); 116 | }); 117 | 118 | grunt.registerTask('launch', function(async) { 119 | var IsAsync = (async == "true"); 120 | grunt.log.writeln("\nLaunching development version..."); 121 | var local_exe = exe[os]; 122 | exec(path.join(electron_path, local_exe) + " app", {async:IsAsync}); 123 | }); 124 | 125 | grunt.registerTask('check-old', function() { 126 | grunt.log.writeln("\nChecking clojure dependencies"); 127 | exec("lein ancient :all", {silent:false}); 128 | grunt.log.writeln("\nChecking npm dependencies"); 129 | exec("npm outdated", {silent:false}); 130 | grunt.log.writeln("\nChecking bower dependencies"); 131 | exec("bower list", {silent:false}); 132 | }); 133 | 134 | //------------------------------------------------------------------------------ 135 | // Release 136 | //------------------------------------------------------------------------------ 137 | 138 | grunt.registerTask('release', function() { 139 | 140 | var build = getBuildMeta(); 141 | var paths = getReleasePaths(build); 142 | var done = this.async(); 143 | 144 | prepRelease( build, paths); 145 | copyElectronAndBuildToRelease( build, paths); 146 | setReleaseConfig( build, paths); 147 | installNodeDepsToRelease( build, paths); 148 | stampRelease( build, paths); 149 | makeAsarRelease( build, paths, done); 150 | 151 | }); 152 | 153 | grunt.registerTask('fresh-release', ['cljsbuild-prod', 'release']); 154 | 155 | //------------------------------------------------------------------------------ 156 | // Release - config 157 | //------------------------------------------------------------------------------ 158 | 159 | var electronShell = { 160 | windows: { 161 | exeToRename: "electron.exe", 162 | renamedExe: "asterion.exe", 163 | resources: "resources", 164 | installExt: "exe" 165 | }, 166 | mac: { 167 | exeToRename: "Electron.app", 168 | renamedExe: "asterion.app", 169 | plist: "Electron.app/Contents/Info.plist", 170 | resources: "Electron.app/Contents/Resources", 171 | installExt: "dmg" 172 | }, 173 | linux: { 174 | exeToRename: "electron", 175 | renamedExe: "asterion", 176 | resources: "resources" 177 | } 178 | }[os]; 179 | 180 | function getBuildMeta() { 181 | grunt.log.writeln("Getting project metadata..."); 182 | var tokens = cat("project.clj").split(" "); 183 | var build = { 184 | name: tokens[1], 185 | version: tokens[2].replace(/"/g, "").trim(), 186 | date: moment().format("YYYY-MM-DD"), 187 | commit: exec("git rev-list HEAD --count", {silent:true}).output.trim() 188 | }; 189 | build.releaseName = build.name + "-v" + build.version + "-" + os; 190 | grunt.log.writeln("name: "+build.name.cyan); 191 | grunt.log.writeln("version: "+build.version.cyan); 192 | grunt.log.writeln("date: "+build.date.cyan); 193 | grunt.log.writeln("commit: "+build.commit.cyan); 194 | grunt.log.writeln("release: "+build.releaseName.cyan); 195 | return build; 196 | } 197 | 198 | function getReleasePaths(build) { 199 | var paths = { 200 | electron: "electron", 201 | builds: "builds", 202 | devApp: "app", 203 | rootPkg: "package.json" 204 | }; 205 | paths.release = path.join(paths.builds, build.releaseName); 206 | paths.resources = path.join(paths.release, electronShell.resources); 207 | paths.install = paths.release + "." + electronShell.installExt; 208 | paths.releaseApp = paths.resources + path.sep + paths.devApp; 209 | paths.devPkg = paths.devApp + "/package.json"; 210 | paths.prodCfg = paths.devApp + "/prod.config.json"; 211 | paths.releasePkg = paths.releaseApp + "/package.json"; 212 | paths.releaseCfg = paths.releaseApp + "/config.json"; 213 | paths.exeToRename = path.join(paths.release, electronShell.exeToRename); 214 | paths.renamedExe = path.join(paths.release, electronShell.renamedExe); 215 | paths.releaseResources = path.join(paths.resources, paths.devApp, "components"); 216 | return paths; 217 | } 218 | 219 | //------------------------------------------------------------------------------ 220 | // Release - subtasks 221 | //------------------------------------------------------------------------------ 222 | 223 | function prepRelease(build, paths) { 224 | grunt.log.writeln("\nCleaning previous release..."); 225 | mkdir('-p', paths.builds); 226 | rm('-rf', paths.install, paths.release); 227 | } 228 | 229 | function copyElectronAndBuildToRelease(build, paths) { 230 | grunt.log.writeln("\nCopying Electron and asterion to release folder..."); 231 | grunt.log.writeln(paths.electron + " ==> " + paths.release.cyan); 232 | grunt.log.writeln(paths.devApp + " ==> " + paths.resources.cyan); 233 | cp('-r', paths.electron + "/", paths.release); 234 | cp('-r', paths.devApp, paths.resources); 235 | 236 | //delete extra resources 237 | 238 | rm('-rf', path.join(paths.releaseApp, "js", "p", "out")); 239 | 240 | } 241 | 242 | function setReleaseConfig(build, paths) { 243 | grunt.log.writeln("\nRemoving config to force default release settings..."); 244 | rm('-f', paths.releaseCfg); 245 | cp(paths.prodCfg, paths.releaseCfg); 246 | } 247 | 248 | function installNodeDepsToRelease(build, paths) { 249 | grunt.log.writeln("\nCopying node dependencies to release..."); 250 | cp('-f', paths.rootPkg, paths.releaseApp); 251 | pushd(paths.releaseApp); 252 | exec('npm install --no-optional --production'); 253 | popd(); 254 | cp('-f', paths.devPkg, paths.releaseApp); 255 | } 256 | 257 | function makeAsarRelease(build, paths, done) { 258 | var new_path = path.join(paths.releaseApp, "..", "app.asar"); 259 | grunt.log.writeln("Release: " + paths.releaseApp + ", Asar: " + new_path); 260 | asar.createPackage(paths.releaseApp, new_path , function(err) { 261 | grunt.log.writeln("Asar file created."); 262 | cp(path.join(paths.releaseApp, "img", "logo.icns"), paths.resources); 263 | rm('-rf', path.join(paths.releaseApp)); 264 | switch (os) { 265 | case "mac": finalizeMacRelease( build, paths); break; 266 | case "linux": finalizeLinuxRelease( build, paths); break; 267 | case "windows": finalizeWindowsRelease( build, paths); break; 268 | } 269 | done(err); 270 | }); 271 | } 272 | 273 | function stampRelease(build, paths) { 274 | grunt.log.writeln("\nStamping release with build metadata..."); 275 | var pkg = grunt.file.readJSON(paths.releasePkg); 276 | pkg.version = build.version; 277 | pkg["build-commit"] = build.commit; 278 | pkg["build-date"] = build.date; 279 | JSON.stringify(pkg, null, " ").to(paths.releasePkg); 280 | } 281 | 282 | function updateVersionInReadme(build, paths) { 283 | grunt.log.writeln("\nUpdating version and download links in readme..."); 284 | sed('-i', /v\d+\.\d+/g, "v"+build.version, "README.md"); 285 | } 286 | 287 | //------------------------------------------------------------------------------ 288 | // Release - finalization 289 | //------------------------------------------------------------------------------ 290 | 291 | function finalizeMacRelease(build, paths) { 292 | 293 | grunt.log.writeln("\nChanging Electron app icon and bundle name..."); 294 | var plist = path.join(__dirname, paths.release, electronShell.plist); 295 | exec("defaults write " + plist + " CFBundleIconFile logo.icns"); 296 | exec("defaults write " + plist + " CFBundleDisplayName asterion"); 297 | exec("defaults write " + plist + " CFBundleName asterion"); 298 | exec("defaults write " + plist + " CFBundleIdentifier com.example"); 299 | mv(paths.exeToRename, paths.renamedExe); 300 | var app = paths.renamedExe; 301 | 302 | grunt.log.writeln("\nCreating dmg image..."); 303 | grunt.config.set("appdmg", { 304 | options: { 305 | "title": "asterion", 306 | "background": "scripts/dmg/TestBkg.png", 307 | "icon-size": 80, 308 | "contents": [ 309 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" }, 310 | { "x": 192, "y": 344, "type": "file", "path": app } 311 | ] 312 | }, 313 | target: { 314 | dest: paths.install 315 | } 316 | }); 317 | grunt.task.run("appdmg"); 318 | } 319 | 320 | function finalizeLinuxRelease(build, paths) { 321 | mv(paths.exeToRename, paths.renamedExe); 322 | } 323 | 324 | function finalizeWindowsRelease(build, paths) { 325 | grunt.log.writeln("\nChanging electron app icon and bundle name..."); 326 | mv(paths.exeToRename, paths.renamedExe); 327 | var app = paths.renamedExe; 328 | grunt.config.set("winresourcer", { 329 | main: { 330 | operation: "Update", 331 | exeFile: app, 332 | resourceType: "Icongroup", 333 | resourceName: "1", 334 | resourceFile: "app/img/logo.ico" 335 | } 336 | }); 337 | grunt.task.run("winresourcer"); 338 | 339 | grunt.config.set("makensis", { 340 | version: build.version, 341 | releaseDir: paths.release, 342 | outFile: paths.install 343 | }); 344 | grunt.task.run("makensis"); 345 | } 346 | 347 | grunt.registerTask('makensis', function() { 348 | grunt.log.writeln("\nCreating installer..."); 349 | var config = grunt.config.get("makensis"); 350 | exec(["makensis", 351 | "/DPRODUCT_VERSION=" + config.version, 352 | "/DRELEASE_DIR=../" + config.releaseDir, 353 | "/DOUTFILE=../" + config.outFile, 354 | "scripts/build-windows-exe.nsi"].join(" ")); 355 | }); 356 | 357 | //------------------------------------------------------------------------------ 358 | // Test Tasks 359 | //------------------------------------------------------------------------------ 360 | 361 | 362 | //------------------------------------------------------------------------------ 363 | // Default Task 364 | //------------------------------------------------------------------------------ 365 | 366 | grunt.registerTask('default', ['setup']); 367 | 368 | // end module.exports 369 | }; 370 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asterion 2 | 3 | Make and explore dependency graphs for Clojure(Script) projects. 4 | 5 | > La casa es del tamaño del mundo; mejor dicho, es el mundo. 6 | 7 | Asterion will run `tools.namespace` against your project sources, 8 | generate a dependency graph, and then graph it using something similar 9 | to [graphviz](https://github.com/cpettitt/dagre-d3). To get your 10 | project sources, it will try to parse your `project.clj` and if that 11 | works, the graph is displayed, you can navigate it (zoom), 12 | filter out namespaces by their names, and highlight namespaces that 13 | contain a certain word in their file (full text search with grep). 14 | 15 | Here is an initial [demo](https://www.youtube.com/watch?v=NOJLmkIyS2A). 16 | and 17 | 18 | ## Usage 19 | 20 | Asterion is currently structured as a webapp, we'll see how it can be 21 | packaged in the future. You can try it [here](http://asterion-dev.elasticbeanstalk.com/index.html) 22 | 23 | To use it locally: 24 | 25 | ```sh 26 | # Compile ClojureScript 27 | lein cljsbuild once production 28 | # Start the server 29 | lein ring server 30 | ``` 31 | 32 | ## Developing 33 | 34 | ### Special Requirements 35 | 36 | First install an experimental version of `tools.namespace`: 37 | 38 | ```sh 39 | https://github.com/bensu/tools.namespace 40 | cd tools.namespace 41 | git checkout node 42 | mvn install 43 | ``` 44 | 45 | If it worked, you should have a `0.3.19-SNAPSHOT` folder under: 46 | 47 | ```sh 48 | ls ~/.m2/repository/org/clojure/tools.namespace/ 49 | ``` 50 | 51 | With `tools.namespace` installed you can start the Electron app as 52 | usual. 53 | 54 | ## Development mode 55 | 56 | Start the figwheel server: 57 | 58 | ``` 59 | lein figwheel 60 | ``` 61 | 62 | If you are on OSX/Linux and have `rlwrap` installed, you can start the figwheel server with: 63 | 64 | ``` 65 | rlwrap lein figwheel 66 | ``` 67 | 68 | This will give better readline support. 69 | 70 | More about [figwheel](https://github.com/bhauman/lein-figwheel) here. 71 | 72 | 73 | ### Dependencies 74 | 75 | Node dependencies are in `package.json` file. Bower dependencies are in `bower.json` file. Clojure/ClojureScript dependencies are in `project.clj`. 76 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draft", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "bootstrap": "3.3.5" 6 | }, 7 | "license": "MIT", 8 | "private": true, 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests", 15 | "spec" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /env/dev/clj/asterion/dev.clj: -------------------------------------------------------------------------------- 1 | (ns asterion.dev 2 | (:require [clojure.tools.namespace.repl :refer (refresh refresh-all)] 3 | [com.stuartsierra.component :as component] 4 | [asterion.server :as server])) 5 | 6 | ;; ====================================================================== 7 | ;; Reloaded Workflow 8 | 9 | (def system nil) 10 | 11 | (defn init 12 | "Constructs the current development system." 13 | [] 14 | (alter-var-root #'system (constantly server/new-system))) 15 | 16 | (defn start 17 | "Starts the current development system." 18 | [] 19 | (alter-var-root #'system component/start)) 20 | 21 | (defn stop 22 | "Shuts down and destroys the current development system." 23 | [] 24 | (alter-var-root #'system 25 | (fn [s] (when s (component/stop s))))) 26 | 27 | (defn go 28 | "Initializes the current development system and starts it running." 29 | [] 30 | (init) 31 | (start)) 32 | 33 | (defn reset [] 34 | (stop) 35 | (refresh :after 'asterion.dev/go)) 36 | -------------------------------------------------------------------------------- /env/dev/cljs/asterion/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.dev 2 | (:require [asterion.core :as core])) 3 | 4 | (core/init!) 5 | -------------------------------------------------------------------------------- /env/prod/cljs/asterion/prod.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load asterion.prod 2 | (:require [asterion.core :as core])) 3 | 4 | (core/init!) 5 | -------------------------------------------------------------------------------- /externs/dagre-d3.ext.js: -------------------------------------------------------------------------------- 1 | var dagreD3 = { 2 | "render": function() {}, 3 | "graphlib": { 4 | "Graph": function() {} 5 | } 6 | }; 7 | 8 | dagreD3.graphlib.Graph.prototype = function() {}; 9 | dagreD3.graphlib.Graph.prototype = { 10 | "setGraph": function() {} 11 | }; 12 | 13 | dagreD3.graphlib.Graph.prototype.setGraph.prototype = function() {}; 14 | dagreD3.graphlib.Graph.prototype.setGraph.prototype = { 15 | "node": function() {}, 16 | "setNode": function() {}, 17 | "setEdge": function() {} 18 | }; 19 | 20 | dagreD3.graphlib.Graph.prototype.setGraph.prototype.node.prototype = function() {}; 21 | 22 | dagreD3.graphlib.Graph.prototype.setGraph.prototype.node.prototype = { 23 | "name": {}, 24 | "rx": {}, 25 | "ry": {} 26 | }; 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /externs/misc.js: -------------------------------------------------------------------------------- 1 | // Node.JS externs: https://github.com/dcodeIO/node.js-closure-compiler-externs 2 | 3 | var ipc = {}; 4 | ipc.on = function() {}; 5 | ipc.send = function() {}; 6 | 7 | 8 | var fs = {}; 9 | fs.listTreeSync = function() {}; 10 | 11 | 12 | var remote = {}; 13 | remote.require = function() {}; 14 | remote.buildFromTemplate = function() {}; 15 | remote.popup = function() {}; 16 | remote.getCurrentWindow = function() {}; 17 | remote.showErrorBox = function() {}; 18 | remote.setTitle = function() {}; 19 | remote.setRepresentedFilename = function() {}; 20 | remote.showMessageBox = function() {}; 21 | 22 | var process = { 23 | platform: {} 24 | }; 25 | -------------------------------------------------------------------------------- /less/colors.less: -------------------------------------------------------------------------------- 1 | // cljs info colors 2 | 3 | @whiteGhost: #f9fbfb; 4 | @blueHavelock: #607ebe; 5 | @greenAtlantis: #99cb54; 6 | @greenOrinoco: #ccdfac; 7 | @greenJuniper: #6e9393; 8 | @indianRed: #BE6060; -------------------------------------------------------------------------------- /less/font-awesome-4.1.0.less: -------------------------------------------------------------------------------- 1 | // Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome 2 | // License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 3 | 4 | /* FONT PATH 5 | * -------------------------- */ 6 | @font-face { 7 | font-family: 'FontAwesome'; 8 | src: url('../fonts/fontawesome-webfont.eot?v=4.1.0'); 9 | src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | .fa { 14 | display: inline-block; 15 | font-family: FontAwesome; 16 | font-style: normal; 17 | font-weight: normal; 18 | line-height: 1; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | /* makes the font 33% larger relative to the icon container */ 23 | .fa-lg { 24 | font-size: 1.33333333em; 25 | line-height: 0.75em; 26 | vertical-align: -15%; 27 | } 28 | .fa-2x { 29 | font-size: 2em; 30 | } 31 | .fa-3x { 32 | font-size: 3em; 33 | } 34 | .fa-4x { 35 | font-size: 4em; 36 | } 37 | .fa-5x { 38 | font-size: 5em; 39 | } 40 | .fa-fw { 41 | width: 1.28571429em; 42 | text-align: center; 43 | } 44 | .fa-ul { 45 | padding-left: 0; 46 | margin-left: 2.14285714em; 47 | list-style-type: none; 48 | } 49 | .fa-ul > li { 50 | position: relative; 51 | } 52 | .fa-li { 53 | position: absolute; 54 | left: -2.14285714em; 55 | width: 2.14285714em; 56 | top: 0.14285714em; 57 | text-align: center; 58 | } 59 | .fa-li.fa-lg { 60 | left: -1.85714286em; 61 | } 62 | .fa-border { 63 | padding: .2em .25em .15em; 64 | border: solid 0.08em #eeeeee; 65 | border-radius: .1em; 66 | } 67 | .pull-right { 68 | float: right; 69 | } 70 | .pull-left { 71 | float: left; 72 | } 73 | .fa.pull-left { 74 | margin-right: .3em; 75 | } 76 | .fa.pull-right { 77 | margin-left: .3em; 78 | } 79 | .fa-spin { 80 | -webkit-animation: spin 2s infinite linear; 81 | -moz-animation: spin 2s infinite linear; 82 | -o-animation: spin 2s infinite linear; 83 | animation: spin 2s infinite linear; 84 | } 85 | @-moz-keyframes spin { 86 | 0% { 87 | -moz-transform: rotate(0deg); 88 | } 89 | 100% { 90 | -moz-transform: rotate(359deg); 91 | } 92 | } 93 | @-webkit-keyframes spin { 94 | 0% { 95 | -webkit-transform: rotate(0deg); 96 | } 97 | 100% { 98 | -webkit-transform: rotate(359deg); 99 | } 100 | } 101 | @-o-keyframes spin { 102 | 0% { 103 | -o-transform: rotate(0deg); 104 | } 105 | 100% { 106 | -o-transform: rotate(359deg); 107 | } 108 | } 109 | @keyframes spin { 110 | 0% { 111 | -webkit-transform: rotate(0deg); 112 | transform: rotate(0deg); 113 | } 114 | 100% { 115 | -webkit-transform: rotate(359deg); 116 | transform: rotate(359deg); 117 | } 118 | } 119 | .fa-rotate-90 { 120 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); 121 | -webkit-transform: rotate(90deg); 122 | -moz-transform: rotate(90deg); 123 | -ms-transform: rotate(90deg); 124 | -o-transform: rotate(90deg); 125 | transform: rotate(90deg); 126 | } 127 | .fa-rotate-180 { 128 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); 129 | -webkit-transform: rotate(180deg); 130 | -moz-transform: rotate(180deg); 131 | -ms-transform: rotate(180deg); 132 | -o-transform: rotate(180deg); 133 | transform: rotate(180deg); 134 | } 135 | .fa-rotate-270 { 136 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); 137 | -webkit-transform: rotate(270deg); 138 | -moz-transform: rotate(270deg); 139 | -ms-transform: rotate(270deg); 140 | -o-transform: rotate(270deg); 141 | transform: rotate(270deg); 142 | } 143 | .fa-flip-horizontal { 144 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); 145 | -webkit-transform: scale(-1, 1); 146 | -moz-transform: scale(-1, 1); 147 | -ms-transform: scale(-1, 1); 148 | -o-transform: scale(-1, 1); 149 | transform: scale(-1, 1); 150 | } 151 | .fa-flip-vertical { 152 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); 153 | -webkit-transform: scale(1, -1); 154 | -moz-transform: scale(1, -1); 155 | -ms-transform: scale(1, -1); 156 | -o-transform: scale(1, -1); 157 | transform: scale(1, -1); 158 | } 159 | .fa-stack { 160 | position: relative; 161 | display: inline-block; 162 | width: 2em; 163 | height: 2em; 164 | line-height: 2em; 165 | vertical-align: middle; 166 | } 167 | .fa-stack-1x, 168 | .fa-stack-2x { 169 | position: absolute; 170 | left: 0; 171 | width: 100%; 172 | text-align: center; 173 | } 174 | .fa-stack-1x { 175 | line-height: inherit; 176 | } 177 | .fa-stack-2x { 178 | font-size: 2em; 179 | } 180 | .fa-inverse { 181 | color: #ffffff; 182 | } 183 | /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen 184 | readers do not read off random characters that represent icons */ 185 | .fa-glass:before { 186 | content: "\f000"; 187 | } 188 | .fa-music:before { 189 | content: "\f001"; 190 | } 191 | .fa-search:before { 192 | content: "\f002"; 193 | } 194 | .fa-envelope-o:before { 195 | content: "\f003"; 196 | } 197 | .fa-heart:before { 198 | content: "\f004"; 199 | } 200 | .fa-star:before { 201 | content: "\f005"; 202 | } 203 | .fa-star-o:before { 204 | content: "\f006"; 205 | } 206 | .fa-user:before { 207 | content: "\f007"; 208 | } 209 | .fa-film:before { 210 | content: "\f008"; 211 | } 212 | .fa-th-large:before { 213 | content: "\f009"; 214 | } 215 | .fa-th:before { 216 | content: "\f00a"; 217 | } 218 | .fa-th-list:before { 219 | content: "\f00b"; 220 | } 221 | .fa-check:before { 222 | content: "\f00c"; 223 | } 224 | .fa-times:before { 225 | content: "\f00d"; 226 | } 227 | .fa-search-plus:before { 228 | content: "\f00e"; 229 | } 230 | .fa-search-minus:before { 231 | content: "\f010"; 232 | } 233 | .fa-power-off:before { 234 | content: "\f011"; 235 | } 236 | .fa-signal:before { 237 | content: "\f012"; 238 | } 239 | .fa-gear:before, 240 | .fa-cog:before { 241 | content: "\f013"; 242 | } 243 | .fa-trash-o:before { 244 | content: "\f014"; 245 | } 246 | .fa-home:before { 247 | content: "\f015"; 248 | } 249 | .fa-file-o:before { 250 | content: "\f016"; 251 | } 252 | .fa-clock-o:before { 253 | content: "\f017"; 254 | } 255 | .fa-road:before { 256 | content: "\f018"; 257 | } 258 | .fa-download:before { 259 | content: "\f019"; 260 | } 261 | .fa-arrow-circle-o-down:before { 262 | content: "\f01a"; 263 | } 264 | .fa-arrow-circle-o-up:before { 265 | content: "\f01b"; 266 | } 267 | .fa-inbox:before { 268 | content: "\f01c"; 269 | } 270 | .fa-play-circle-o:before { 271 | content: "\f01d"; 272 | } 273 | .fa-rotate-right:before, 274 | .fa-repeat:before { 275 | content: "\f01e"; 276 | } 277 | .fa-refresh:before { 278 | content: "\f021"; 279 | } 280 | .fa-list-alt:before { 281 | content: "\f022"; 282 | } 283 | .fa-lock:before { 284 | content: "\f023"; 285 | } 286 | .fa-flag:before { 287 | content: "\f024"; 288 | } 289 | .fa-headphones:before { 290 | content: "\f025"; 291 | } 292 | .fa-volume-off:before { 293 | content: "\f026"; 294 | } 295 | .fa-volume-down:before { 296 | content: "\f027"; 297 | } 298 | .fa-volume-up:before { 299 | content: "\f028"; 300 | } 301 | .fa-qrcode:before { 302 | content: "\f029"; 303 | } 304 | .fa-barcode:before { 305 | content: "\f02a"; 306 | } 307 | .fa-tag:before { 308 | content: "\f02b"; 309 | } 310 | .fa-tags:before { 311 | content: "\f02c"; 312 | } 313 | .fa-book:before { 314 | content: "\f02d"; 315 | } 316 | .fa-bookmark:before { 317 | content: "\f02e"; 318 | } 319 | .fa-print:before { 320 | content: "\f02f"; 321 | } 322 | .fa-camera:before { 323 | content: "\f030"; 324 | } 325 | .fa-font:before { 326 | content: "\f031"; 327 | } 328 | .fa-bold:before { 329 | content: "\f032"; 330 | } 331 | .fa-italic:before { 332 | content: "\f033"; 333 | } 334 | .fa-text-height:before { 335 | content: "\f034"; 336 | } 337 | .fa-text-width:before { 338 | content: "\f035"; 339 | } 340 | .fa-align-left:before { 341 | content: "\f036"; 342 | } 343 | .fa-align-center:before { 344 | content: "\f037"; 345 | } 346 | .fa-align-right:before { 347 | content: "\f038"; 348 | } 349 | .fa-align-justify:before { 350 | content: "\f039"; 351 | } 352 | .fa-list:before { 353 | content: "\f03a"; 354 | } 355 | .fa-dedent:before, 356 | .fa-outdent:before { 357 | content: "\f03b"; 358 | } 359 | .fa-indent:before { 360 | content: "\f03c"; 361 | } 362 | .fa-video-camera:before { 363 | content: "\f03d"; 364 | } 365 | .fa-photo:before, 366 | .fa-image:before, 367 | .fa-picture-o:before { 368 | content: "\f03e"; 369 | } 370 | .fa-pencil:before { 371 | content: "\f040"; 372 | } 373 | .fa-map-marker:before { 374 | content: "\f041"; 375 | } 376 | .fa-adjust:before { 377 | content: "\f042"; 378 | } 379 | .fa-tint:before { 380 | content: "\f043"; 381 | } 382 | .fa-edit:before, 383 | .fa-pencil-square-o:before { 384 | content: "\f044"; 385 | } 386 | .fa-share-square-o:before { 387 | content: "\f045"; 388 | } 389 | .fa-check-square-o:before { 390 | content: "\f046"; 391 | } 392 | .fa-arrows:before { 393 | content: "\f047"; 394 | } 395 | .fa-step-backward:before { 396 | content: "\f048"; 397 | } 398 | .fa-fast-backward:before { 399 | content: "\f049"; 400 | } 401 | .fa-backward:before { 402 | content: "\f04a"; 403 | } 404 | .fa-play:before { 405 | content: "\f04b"; 406 | } 407 | .fa-pause:before { 408 | content: "\f04c"; 409 | } 410 | .fa-stop:before { 411 | content: "\f04d"; 412 | } 413 | .fa-forward:before { 414 | content: "\f04e"; 415 | } 416 | .fa-fast-forward:before { 417 | content: "\f050"; 418 | } 419 | .fa-step-forward:before { 420 | content: "\f051"; 421 | } 422 | .fa-eject:before { 423 | content: "\f052"; 424 | } 425 | .fa-chevron-left:before { 426 | content: "\f053"; 427 | } 428 | .fa-chevron-right:before { 429 | content: "\f054"; 430 | } 431 | .fa-plus-circle:before { 432 | content: "\f055"; 433 | } 434 | .fa-minus-circle:before { 435 | content: "\f056"; 436 | } 437 | .fa-times-circle:before { 438 | content: "\f057"; 439 | } 440 | .fa-check-circle:before { 441 | content: "\f058"; 442 | } 443 | .fa-question-circle:before { 444 | content: "\f059"; 445 | } 446 | .fa-info-circle:before { 447 | content: "\f05a"; 448 | } 449 | .fa-crosshairs:before { 450 | content: "\f05b"; 451 | } 452 | .fa-times-circle-o:before { 453 | content: "\f05c"; 454 | } 455 | .fa-check-circle-o:before { 456 | content: "\f05d"; 457 | } 458 | .fa-ban:before { 459 | content: "\f05e"; 460 | } 461 | .fa-arrow-left:before { 462 | content: "\f060"; 463 | } 464 | .fa-arrow-right:before { 465 | content: "\f061"; 466 | } 467 | .fa-arrow-up:before { 468 | content: "\f062"; 469 | } 470 | .fa-arrow-down:before { 471 | content: "\f063"; 472 | } 473 | .fa-mail-forward:before, 474 | .fa-share:before { 475 | content: "\f064"; 476 | } 477 | .fa-expand:before { 478 | content: "\f065"; 479 | } 480 | .fa-compress:before { 481 | content: "\f066"; 482 | } 483 | .fa-plus:before { 484 | content: "\f067"; 485 | } 486 | .fa-minus:before { 487 | content: "\f068"; 488 | } 489 | .fa-asterisk:before { 490 | content: "\f069"; 491 | } 492 | .fa-exclamation-circle:before { 493 | content: "\f06a"; 494 | } 495 | .fa-gift:before { 496 | content: "\f06b"; 497 | } 498 | .fa-leaf:before { 499 | content: "\f06c"; 500 | } 501 | .fa-fire:before { 502 | content: "\f06d"; 503 | } 504 | .fa-eye:before { 505 | content: "\f06e"; 506 | } 507 | .fa-eye-slash:before { 508 | content: "\f070"; 509 | } 510 | .fa-warning:before, 511 | .fa-exclamation-triangle:before { 512 | content: "\f071"; 513 | } 514 | .fa-plane:before { 515 | content: "\f072"; 516 | } 517 | .fa-calendar:before { 518 | content: "\f073"; 519 | } 520 | .fa-random:before { 521 | content: "\f074"; 522 | } 523 | .fa-comment:before { 524 | content: "\f075"; 525 | } 526 | .fa-magnet:before { 527 | content: "\f076"; 528 | } 529 | .fa-chevron-up:before { 530 | content: "\f077"; 531 | } 532 | .fa-chevron-down:before { 533 | content: "\f078"; 534 | } 535 | .fa-retweet:before { 536 | content: "\f079"; 537 | } 538 | .fa-shopping-cart:before { 539 | content: "\f07a"; 540 | } 541 | .fa-folder:before { 542 | content: "\f07b"; 543 | } 544 | .fa-folder-open:before { 545 | content: "\f07c"; 546 | } 547 | .fa-arrows-v:before { 548 | content: "\f07d"; 549 | } 550 | .fa-arrows-h:before { 551 | content: "\f07e"; 552 | } 553 | .fa-bar-chart-o:before { 554 | content: "\f080"; 555 | } 556 | .fa-twitter-square:before { 557 | content: "\f081"; 558 | } 559 | .fa-facebook-square:before { 560 | content: "\f082"; 561 | } 562 | .fa-camera-retro:before { 563 | content: "\f083"; 564 | } 565 | .fa-key:before { 566 | content: "\f084"; 567 | } 568 | .fa-gears:before, 569 | .fa-cogs:before { 570 | content: "\f085"; 571 | } 572 | .fa-comments:before { 573 | content: "\f086"; 574 | } 575 | .fa-thumbs-o-up:before { 576 | content: "\f087"; 577 | } 578 | .fa-thumbs-o-down:before { 579 | content: "\f088"; 580 | } 581 | .fa-star-half:before { 582 | content: "\f089"; 583 | } 584 | .fa-heart-o:before { 585 | content: "\f08a"; 586 | } 587 | .fa-sign-out:before { 588 | content: "\f08b"; 589 | } 590 | .fa-linkedin-square:before { 591 | content: "\f08c"; 592 | } 593 | .fa-thumb-tack:before { 594 | content: "\f08d"; 595 | } 596 | .fa-external-link:before { 597 | content: "\f08e"; 598 | } 599 | .fa-sign-in:before { 600 | content: "\f090"; 601 | } 602 | .fa-trophy:before { 603 | content: "\f091"; 604 | } 605 | .fa-github-square:before { 606 | content: "\f092"; 607 | } 608 | .fa-upload:before { 609 | content: "\f093"; 610 | } 611 | .fa-lemon-o:before { 612 | content: "\f094"; 613 | } 614 | .fa-phone:before { 615 | content: "\f095"; 616 | } 617 | .fa-square-o:before { 618 | content: "\f096"; 619 | } 620 | .fa-bookmark-o:before { 621 | content: "\f097"; 622 | } 623 | .fa-phone-square:before { 624 | content: "\f098"; 625 | } 626 | .fa-twitter:before { 627 | content: "\f099"; 628 | } 629 | .fa-facebook:before { 630 | content: "\f09a"; 631 | } 632 | .fa-github:before { 633 | content: "\f09b"; 634 | } 635 | .fa-unlock:before { 636 | content: "\f09c"; 637 | } 638 | .fa-credit-card:before { 639 | content: "\f09d"; 640 | } 641 | .fa-rss:before { 642 | content: "\f09e"; 643 | } 644 | .fa-hdd-o:before { 645 | content: "\f0a0"; 646 | } 647 | .fa-bullhorn:before { 648 | content: "\f0a1"; 649 | } 650 | .fa-bell:before { 651 | content: "\f0f3"; 652 | } 653 | .fa-certificate:before { 654 | content: "\f0a3"; 655 | } 656 | .fa-hand-o-right:before { 657 | content: "\f0a4"; 658 | } 659 | .fa-hand-o-left:before { 660 | content: "\f0a5"; 661 | } 662 | .fa-hand-o-up:before { 663 | content: "\f0a6"; 664 | } 665 | .fa-hand-o-down:before { 666 | content: "\f0a7"; 667 | } 668 | .fa-arrow-circle-left:before { 669 | content: "\f0a8"; 670 | } 671 | .fa-arrow-circle-right:before { 672 | content: "\f0a9"; 673 | } 674 | .fa-arrow-circle-up:before { 675 | content: "\f0aa"; 676 | } 677 | .fa-arrow-circle-down:before { 678 | content: "\f0ab"; 679 | } 680 | .fa-globe:before { 681 | content: "\f0ac"; 682 | } 683 | .fa-wrench:before { 684 | content: "\f0ad"; 685 | } 686 | .fa-tasks:before { 687 | content: "\f0ae"; 688 | } 689 | .fa-filter:before { 690 | content: "\f0b0"; 691 | } 692 | .fa-briefcase:before { 693 | content: "\f0b1"; 694 | } 695 | .fa-arrows-alt:before { 696 | content: "\f0b2"; 697 | } 698 | .fa-group:before, 699 | .fa-users:before { 700 | content: "\f0c0"; 701 | } 702 | .fa-chain:before, 703 | .fa-link:before { 704 | content: "\f0c1"; 705 | } 706 | .fa-cloud:before { 707 | content: "\f0c2"; 708 | } 709 | .fa-flask:before { 710 | content: "\f0c3"; 711 | } 712 | .fa-cut:before, 713 | .fa-scissors:before { 714 | content: "\f0c4"; 715 | } 716 | .fa-copy:before, 717 | .fa-files-o:before { 718 | content: "\f0c5"; 719 | } 720 | .fa-paperclip:before { 721 | content: "\f0c6"; 722 | } 723 | .fa-save:before, 724 | .fa-floppy-o:before { 725 | content: "\f0c7"; 726 | } 727 | .fa-square:before { 728 | content: "\f0c8"; 729 | } 730 | .fa-navicon:before, 731 | .fa-reorder:before, 732 | .fa-bars:before { 733 | content: "\f0c9"; 734 | } 735 | .fa-list-ul:before { 736 | content: "\f0ca"; 737 | } 738 | .fa-list-ol:before { 739 | content: "\f0cb"; 740 | } 741 | .fa-strikethrough:before { 742 | content: "\f0cc"; 743 | } 744 | .fa-underline:before { 745 | content: "\f0cd"; 746 | } 747 | .fa-table:before { 748 | content: "\f0ce"; 749 | } 750 | .fa-magic:before { 751 | content: "\f0d0"; 752 | } 753 | .fa-truck:before { 754 | content: "\f0d1"; 755 | } 756 | .fa-pinterest:before { 757 | content: "\f0d2"; 758 | } 759 | .fa-pinterest-square:before { 760 | content: "\f0d3"; 761 | } 762 | .fa-google-plus-square:before { 763 | content: "\f0d4"; 764 | } 765 | .fa-google-plus:before { 766 | content: "\f0d5"; 767 | } 768 | .fa-money:before { 769 | content: "\f0d6"; 770 | } 771 | .fa-caret-down:before { 772 | content: "\f0d7"; 773 | } 774 | .fa-caret-up:before { 775 | content: "\f0d8"; 776 | } 777 | .fa-caret-left:before { 778 | content: "\f0d9"; 779 | } 780 | .fa-caret-right:before { 781 | content: "\f0da"; 782 | } 783 | .fa-columns:before { 784 | content: "\f0db"; 785 | } 786 | .fa-unsorted:before, 787 | .fa-sort:before { 788 | content: "\f0dc"; 789 | } 790 | .fa-sort-down:before, 791 | .fa-sort-desc:before { 792 | content: "\f0dd"; 793 | } 794 | .fa-sort-up:before, 795 | .fa-sort-asc:before { 796 | content: "\f0de"; 797 | } 798 | .fa-envelope:before { 799 | content: "\f0e0"; 800 | } 801 | .fa-linkedin:before { 802 | content: "\f0e1"; 803 | } 804 | .fa-rotate-left:before, 805 | .fa-undo:before { 806 | content: "\f0e2"; 807 | } 808 | .fa-legal:before, 809 | .fa-gavel:before { 810 | content: "\f0e3"; 811 | } 812 | .fa-dashboard:before, 813 | .fa-tachometer:before { 814 | content: "\f0e4"; 815 | } 816 | .fa-comment-o:before { 817 | content: "\f0e5"; 818 | } 819 | .fa-comments-o:before { 820 | content: "\f0e6"; 821 | } 822 | .fa-flash:before, 823 | .fa-bolt:before { 824 | content: "\f0e7"; 825 | } 826 | .fa-sitemap:before { 827 | content: "\f0e8"; 828 | } 829 | .fa-umbrella:before { 830 | content: "\f0e9"; 831 | } 832 | .fa-paste:before, 833 | .fa-clipboard:before { 834 | content: "\f0ea"; 835 | } 836 | .fa-lightbulb-o:before { 837 | content: "\f0eb"; 838 | } 839 | .fa-exchange:before { 840 | content: "\f0ec"; 841 | } 842 | .fa-cloud-download:before { 843 | content: "\f0ed"; 844 | } 845 | .fa-cloud-upload:before { 846 | content: "\f0ee"; 847 | } 848 | .fa-user-md:before { 849 | content: "\f0f0"; 850 | } 851 | .fa-stethoscope:before { 852 | content: "\f0f1"; 853 | } 854 | .fa-suitcase:before { 855 | content: "\f0f2"; 856 | } 857 | .fa-bell-o:before { 858 | content: "\f0a2"; 859 | } 860 | .fa-coffee:before { 861 | content: "\f0f4"; 862 | } 863 | .fa-cutlery:before { 864 | content: "\f0f5"; 865 | } 866 | .fa-file-text-o:before { 867 | content: "\f0f6"; 868 | } 869 | .fa-building-o:before { 870 | content: "\f0f7"; 871 | } 872 | .fa-hospital-o:before { 873 | content: "\f0f8"; 874 | } 875 | .fa-ambulance:before { 876 | content: "\f0f9"; 877 | } 878 | .fa-medkit:before { 879 | content: "\f0fa"; 880 | } 881 | .fa-fighter-jet:before { 882 | content: "\f0fb"; 883 | } 884 | .fa-beer:before { 885 | content: "\f0fc"; 886 | } 887 | .fa-h-square:before { 888 | content: "\f0fd"; 889 | } 890 | .fa-plus-square:before { 891 | content: "\f0fe"; 892 | } 893 | .fa-angle-double-left:before { 894 | content: "\f100"; 895 | } 896 | .fa-angle-double-right:before { 897 | content: "\f101"; 898 | } 899 | .fa-angle-double-up:before { 900 | content: "\f102"; 901 | } 902 | .fa-angle-double-down:before { 903 | content: "\f103"; 904 | } 905 | .fa-angle-left:before { 906 | content: "\f104"; 907 | } 908 | .fa-angle-right:before { 909 | content: "\f105"; 910 | } 911 | .fa-angle-up:before { 912 | content: "\f106"; 913 | } 914 | .fa-angle-down:before { 915 | content: "\f107"; 916 | } 917 | .fa-desktop:before { 918 | content: "\f108"; 919 | } 920 | .fa-laptop:before { 921 | content: "\f109"; 922 | } 923 | .fa-tablet:before { 924 | content: "\f10a"; 925 | } 926 | .fa-mobile-phone:before, 927 | .fa-mobile:before { 928 | content: "\f10b"; 929 | } 930 | .fa-circle-o:before { 931 | content: "\f10c"; 932 | } 933 | .fa-quote-left:before { 934 | content: "\f10d"; 935 | } 936 | .fa-quote-right:before { 937 | content: "\f10e"; 938 | } 939 | .fa-spinner:before { 940 | content: "\f110"; 941 | } 942 | .fa-circle:before { 943 | content: "\f111"; 944 | } 945 | .fa-mail-reply:before, 946 | .fa-reply:before { 947 | content: "\f112"; 948 | } 949 | .fa-github-alt:before { 950 | content: "\f113"; 951 | } 952 | .fa-folder-o:before { 953 | content: "\f114"; 954 | } 955 | .fa-folder-open-o:before { 956 | content: "\f115"; 957 | } 958 | .fa-smile-o:before { 959 | content: "\f118"; 960 | } 961 | .fa-frown-o:before { 962 | content: "\f119"; 963 | } 964 | .fa-meh-o:before { 965 | content: "\f11a"; 966 | } 967 | .fa-gamepad:before { 968 | content: "\f11b"; 969 | } 970 | .fa-keyboard-o:before { 971 | content: "\f11c"; 972 | } 973 | .fa-flag-o:before { 974 | content: "\f11d"; 975 | } 976 | .fa-flag-checkered:before { 977 | content: "\f11e"; 978 | } 979 | .fa-terminal:before { 980 | content: "\f120"; 981 | } 982 | .fa-code:before { 983 | content: "\f121"; 984 | } 985 | .fa-mail-reply-all:before, 986 | .fa-reply-all:before { 987 | content: "\f122"; 988 | } 989 | .fa-star-half-empty:before, 990 | .fa-star-half-full:before, 991 | .fa-star-half-o:before { 992 | content: "\f123"; 993 | } 994 | .fa-location-arrow:before { 995 | content: "\f124"; 996 | } 997 | .fa-crop:before { 998 | content: "\f125"; 999 | } 1000 | .fa-code-fork:before { 1001 | content: "\f126"; 1002 | } 1003 | .fa-unlink:before, 1004 | .fa-chain-broken:before { 1005 | content: "\f127"; 1006 | } 1007 | .fa-question:before { 1008 | content: "\f128"; 1009 | } 1010 | .fa-info:before { 1011 | content: "\f129"; 1012 | } 1013 | .fa-exclamation:before { 1014 | content: "\f12a"; 1015 | } 1016 | .fa-superscript:before { 1017 | content: "\f12b"; 1018 | } 1019 | .fa-subscript:before { 1020 | content: "\f12c"; 1021 | } 1022 | .fa-eraser:before { 1023 | content: "\f12d"; 1024 | } 1025 | .fa-puzzle-piece:before { 1026 | content: "\f12e"; 1027 | } 1028 | .fa-microphone:before { 1029 | content: "\f130"; 1030 | } 1031 | .fa-microphone-slash:before { 1032 | content: "\f131"; 1033 | } 1034 | .fa-shield:before { 1035 | content: "\f132"; 1036 | } 1037 | .fa-calendar-o:before { 1038 | content: "\f133"; 1039 | } 1040 | .fa-fire-extinguisher:before { 1041 | content: "\f134"; 1042 | } 1043 | .fa-rocket:before { 1044 | content: "\f135"; 1045 | } 1046 | .fa-maxcdn:before { 1047 | content: "\f136"; 1048 | } 1049 | .fa-chevron-circle-left:before { 1050 | content: "\f137"; 1051 | } 1052 | .fa-chevron-circle-right:before { 1053 | content: "\f138"; 1054 | } 1055 | .fa-chevron-circle-up:before { 1056 | content: "\f139"; 1057 | } 1058 | .fa-chevron-circle-down:before { 1059 | content: "\f13a"; 1060 | } 1061 | .fa-html5:before { 1062 | content: "\f13b"; 1063 | } 1064 | .fa-css3:before { 1065 | content: "\f13c"; 1066 | } 1067 | .fa-anchor:before { 1068 | content: "\f13d"; 1069 | } 1070 | .fa-unlock-alt:before { 1071 | content: "\f13e"; 1072 | } 1073 | .fa-bullseye:before { 1074 | content: "\f140"; 1075 | } 1076 | .fa-ellipsis-h:before { 1077 | content: "\f141"; 1078 | } 1079 | .fa-ellipsis-v:before { 1080 | content: "\f142"; 1081 | } 1082 | .fa-rss-square:before { 1083 | content: "\f143"; 1084 | } 1085 | .fa-play-circle:before { 1086 | content: "\f144"; 1087 | } 1088 | .fa-ticket:before { 1089 | content: "\f145"; 1090 | } 1091 | .fa-minus-square:before { 1092 | content: "\f146"; 1093 | } 1094 | .fa-minus-square-o:before { 1095 | content: "\f147"; 1096 | } 1097 | .fa-level-up:before { 1098 | content: "\f148"; 1099 | } 1100 | .fa-level-down:before { 1101 | content: "\f149"; 1102 | } 1103 | .fa-check-square:before { 1104 | content: "\f14a"; 1105 | } 1106 | .fa-pencil-square:before { 1107 | content: "\f14b"; 1108 | } 1109 | .fa-external-link-square:before { 1110 | content: "\f14c"; 1111 | } 1112 | .fa-share-square:before { 1113 | content: "\f14d"; 1114 | } 1115 | .fa-compass:before { 1116 | content: "\f14e"; 1117 | } 1118 | .fa-toggle-down:before, 1119 | .fa-caret-square-o-down:before { 1120 | content: "\f150"; 1121 | } 1122 | .fa-toggle-up:before, 1123 | .fa-caret-square-o-up:before { 1124 | content: "\f151"; 1125 | } 1126 | .fa-toggle-right:before, 1127 | .fa-caret-square-o-right:before { 1128 | content: "\f152"; 1129 | } 1130 | .fa-euro:before, 1131 | .fa-eur:before { 1132 | content: "\f153"; 1133 | } 1134 | .fa-gbp:before { 1135 | content: "\f154"; 1136 | } 1137 | .fa-dollar:before, 1138 | .fa-usd:before { 1139 | content: "\f155"; 1140 | } 1141 | .fa-rupee:before, 1142 | .fa-inr:before { 1143 | content: "\f156"; 1144 | } 1145 | .fa-cny:before, 1146 | .fa-rmb:before, 1147 | .fa-yen:before, 1148 | .fa-jpy:before { 1149 | content: "\f157"; 1150 | } 1151 | .fa-ruble:before, 1152 | .fa-rouble:before, 1153 | .fa-rub:before { 1154 | content: "\f158"; 1155 | } 1156 | .fa-won:before, 1157 | .fa-krw:before { 1158 | content: "\f159"; 1159 | } 1160 | .fa-bitcoin:before, 1161 | .fa-btc:before { 1162 | content: "\f15a"; 1163 | } 1164 | .fa-file:before { 1165 | content: "\f15b"; 1166 | } 1167 | .fa-file-text:before { 1168 | content: "\f15c"; 1169 | } 1170 | .fa-sort-alpha-asc:before { 1171 | content: "\f15d"; 1172 | } 1173 | .fa-sort-alpha-desc:before { 1174 | content: "\f15e"; 1175 | } 1176 | .fa-sort-amount-asc:before { 1177 | content: "\f160"; 1178 | } 1179 | .fa-sort-amount-desc:before { 1180 | content: "\f161"; 1181 | } 1182 | .fa-sort-numeric-asc:before { 1183 | content: "\f162"; 1184 | } 1185 | .fa-sort-numeric-desc:before { 1186 | content: "\f163"; 1187 | } 1188 | .fa-thumbs-up:before { 1189 | content: "\f164"; 1190 | } 1191 | .fa-thumbs-down:before { 1192 | content: "\f165"; 1193 | } 1194 | .fa-youtube-square:before { 1195 | content: "\f166"; 1196 | } 1197 | .fa-youtube:before { 1198 | content: "\f167"; 1199 | } 1200 | .fa-xing:before { 1201 | content: "\f168"; 1202 | } 1203 | .fa-xing-square:before { 1204 | content: "\f169"; 1205 | } 1206 | .fa-youtube-play:before { 1207 | content: "\f16a"; 1208 | } 1209 | .fa-dropbox:before { 1210 | content: "\f16b"; 1211 | } 1212 | .fa-stack-overflow:before { 1213 | content: "\f16c"; 1214 | } 1215 | .fa-instagram:before { 1216 | content: "\f16d"; 1217 | } 1218 | .fa-flickr:before { 1219 | content: "\f16e"; 1220 | } 1221 | .fa-adn:before { 1222 | content: "\f170"; 1223 | } 1224 | .fa-bitbucket:before { 1225 | content: "\f171"; 1226 | } 1227 | .fa-bitbucket-square:before { 1228 | content: "\f172"; 1229 | } 1230 | .fa-tumblr:before { 1231 | content: "\f173"; 1232 | } 1233 | .fa-tumblr-square:before { 1234 | content: "\f174"; 1235 | } 1236 | .fa-long-arrow-down:before { 1237 | content: "\f175"; 1238 | } 1239 | .fa-long-arrow-up:before { 1240 | content: "\f176"; 1241 | } 1242 | .fa-long-arrow-left:before { 1243 | content: "\f177"; 1244 | } 1245 | .fa-long-arrow-right:before { 1246 | content: "\f178"; 1247 | } 1248 | .fa-apple:before { 1249 | content: "\f179"; 1250 | } 1251 | .fa-windows:before { 1252 | content: "\f17a"; 1253 | } 1254 | .fa-android:before { 1255 | content: "\f17b"; 1256 | } 1257 | .fa-linux:before { 1258 | content: "\f17c"; 1259 | } 1260 | .fa-dribbble:before { 1261 | content: "\f17d"; 1262 | } 1263 | .fa-skype:before { 1264 | content: "\f17e"; 1265 | } 1266 | .fa-foursquare:before { 1267 | content: "\f180"; 1268 | } 1269 | .fa-trello:before { 1270 | content: "\f181"; 1271 | } 1272 | .fa-female:before { 1273 | content: "\f182"; 1274 | } 1275 | .fa-male:before { 1276 | content: "\f183"; 1277 | } 1278 | .fa-gittip:before { 1279 | content: "\f184"; 1280 | } 1281 | .fa-sun-o:before { 1282 | content: "\f185"; 1283 | } 1284 | .fa-moon-o:before { 1285 | content: "\f186"; 1286 | } 1287 | .fa-archive:before { 1288 | content: "\f187"; 1289 | } 1290 | .fa-bug:before { 1291 | content: "\f188"; 1292 | } 1293 | .fa-vk:before { 1294 | content: "\f189"; 1295 | } 1296 | .fa-weibo:before { 1297 | content: "\f18a"; 1298 | } 1299 | .fa-renren:before { 1300 | content: "\f18b"; 1301 | } 1302 | .fa-pagelines:before { 1303 | content: "\f18c"; 1304 | } 1305 | .fa-stack-exchange:before { 1306 | content: "\f18d"; 1307 | } 1308 | .fa-arrow-circle-o-right:before { 1309 | content: "\f18e"; 1310 | } 1311 | .fa-arrow-circle-o-left:before { 1312 | content: "\f190"; 1313 | } 1314 | .fa-toggle-left:before, 1315 | .fa-caret-square-o-left:before { 1316 | content: "\f191"; 1317 | } 1318 | .fa-dot-circle-o:before { 1319 | content: "\f192"; 1320 | } 1321 | .fa-wheelchair:before { 1322 | content: "\f193"; 1323 | } 1324 | .fa-vimeo-square:before { 1325 | content: "\f194"; 1326 | } 1327 | .fa-turkish-lira:before, 1328 | .fa-try:before { 1329 | content: "\f195"; 1330 | } 1331 | .fa-plus-square-o:before { 1332 | content: "\f196"; 1333 | } 1334 | .fa-space-shuttle:before { 1335 | content: "\f197"; 1336 | } 1337 | .fa-slack:before { 1338 | content: "\f198"; 1339 | } 1340 | .fa-envelope-square:before { 1341 | content: "\f199"; 1342 | } 1343 | .fa-wordpress:before { 1344 | content: "\f19a"; 1345 | } 1346 | .fa-openid:before { 1347 | content: "\f19b"; 1348 | } 1349 | .fa-institution:before, 1350 | .fa-bank:before, 1351 | .fa-university:before { 1352 | content: "\f19c"; 1353 | } 1354 | .fa-mortar-board:before, 1355 | .fa-graduation-cap:before { 1356 | content: "\f19d"; 1357 | } 1358 | .fa-yahoo:before { 1359 | content: "\f19e"; 1360 | } 1361 | .fa-google:before { 1362 | content: "\f1a0"; 1363 | } 1364 | .fa-reddit:before { 1365 | content: "\f1a1"; 1366 | } 1367 | .fa-reddit-square:before { 1368 | content: "\f1a2"; 1369 | } 1370 | .fa-stumbleupon-circle:before { 1371 | content: "\f1a3"; 1372 | } 1373 | .fa-stumbleupon:before { 1374 | content: "\f1a4"; 1375 | } 1376 | .fa-delicious:before { 1377 | content: "\f1a5"; 1378 | } 1379 | .fa-digg:before { 1380 | content: "\f1a6"; 1381 | } 1382 | .fa-pied-piper-square:before, 1383 | .fa-pied-piper:before { 1384 | content: "\f1a7"; 1385 | } 1386 | .fa-pied-piper-alt:before { 1387 | content: "\f1a8"; 1388 | } 1389 | .fa-drupal:before { 1390 | content: "\f1a9"; 1391 | } 1392 | .fa-joomla:before { 1393 | content: "\f1aa"; 1394 | } 1395 | .fa-language:before { 1396 | content: "\f1ab"; 1397 | } 1398 | .fa-fax:before { 1399 | content: "\f1ac"; 1400 | } 1401 | .fa-building:before { 1402 | content: "\f1ad"; 1403 | } 1404 | .fa-child:before { 1405 | content: "\f1ae"; 1406 | } 1407 | .fa-paw:before { 1408 | content: "\f1b0"; 1409 | } 1410 | .fa-spoon:before { 1411 | content: "\f1b1"; 1412 | } 1413 | .fa-cube:before { 1414 | content: "\f1b2"; 1415 | } 1416 | .fa-cubes:before { 1417 | content: "\f1b3"; 1418 | } 1419 | .fa-behance:before { 1420 | content: "\f1b4"; 1421 | } 1422 | .fa-behance-square:before { 1423 | content: "\f1b5"; 1424 | } 1425 | .fa-steam:before { 1426 | content: "\f1b6"; 1427 | } 1428 | .fa-steam-square:before { 1429 | content: "\f1b7"; 1430 | } 1431 | .fa-recycle:before { 1432 | content: "\f1b8"; 1433 | } 1434 | .fa-automobile:before, 1435 | .fa-car:before { 1436 | content: "\f1b9"; 1437 | } 1438 | .fa-cab:before, 1439 | .fa-taxi:before { 1440 | content: "\f1ba"; 1441 | } 1442 | .fa-tree:before { 1443 | content: "\f1bb"; 1444 | } 1445 | .fa-spotify:before { 1446 | content: "\f1bc"; 1447 | } 1448 | .fa-deviantart:before { 1449 | content: "\f1bd"; 1450 | } 1451 | .fa-soundcloud:before { 1452 | content: "\f1be"; 1453 | } 1454 | .fa-database:before { 1455 | content: "\f1c0"; 1456 | } 1457 | .fa-file-pdf-o:before { 1458 | content: "\f1c1"; 1459 | } 1460 | .fa-file-word-o:before { 1461 | content: "\f1c2"; 1462 | } 1463 | .fa-file-excel-o:before { 1464 | content: "\f1c3"; 1465 | } 1466 | .fa-file-powerpoint-o:before { 1467 | content: "\f1c4"; 1468 | } 1469 | .fa-file-photo-o:before, 1470 | .fa-file-picture-o:before, 1471 | .fa-file-image-o:before { 1472 | content: "\f1c5"; 1473 | } 1474 | .fa-file-zip-o:before, 1475 | .fa-file-archive-o:before { 1476 | content: "\f1c6"; 1477 | } 1478 | .fa-file-sound-o:before, 1479 | .fa-file-audio-o:before { 1480 | content: "\f1c7"; 1481 | } 1482 | .fa-file-movie-o:before, 1483 | .fa-file-video-o:before { 1484 | content: "\f1c8"; 1485 | } 1486 | .fa-file-code-o:before { 1487 | content: "\f1c9"; 1488 | } 1489 | .fa-vine:before { 1490 | content: "\f1ca"; 1491 | } 1492 | .fa-codepen:before { 1493 | content: "\f1cb"; 1494 | } 1495 | .fa-jsfiddle:before { 1496 | content: "\f1cc"; 1497 | } 1498 | .fa-life-bouy:before, 1499 | .fa-life-saver:before, 1500 | .fa-support:before, 1501 | .fa-life-ring:before { 1502 | content: "\f1cd"; 1503 | } 1504 | .fa-circle-o-notch:before { 1505 | content: "\f1ce"; 1506 | } 1507 | .fa-ra:before, 1508 | .fa-rebel:before { 1509 | content: "\f1d0"; 1510 | } 1511 | .fa-ge:before, 1512 | .fa-empire:before { 1513 | content: "\f1d1"; 1514 | } 1515 | .fa-git-square:before { 1516 | content: "\f1d2"; 1517 | } 1518 | .fa-git:before { 1519 | content: "\f1d3"; 1520 | } 1521 | .fa-hacker-news:before { 1522 | content: "\f1d4"; 1523 | } 1524 | .fa-tencent-weibo:before { 1525 | content: "\f1d5"; 1526 | } 1527 | .fa-qq:before { 1528 | content: "\f1d6"; 1529 | } 1530 | .fa-wechat:before, 1531 | .fa-weixin:before { 1532 | content: "\f1d7"; 1533 | } 1534 | .fa-send:before, 1535 | .fa-paper-plane:before { 1536 | content: "\f1d8"; 1537 | } 1538 | .fa-send-o:before, 1539 | .fa-paper-plane-o:before { 1540 | content: "\f1d9"; 1541 | } 1542 | .fa-history:before { 1543 | content: "\f1da"; 1544 | } 1545 | .fa-circle-thin:before { 1546 | content: "\f1db"; 1547 | } 1548 | .fa-header:before { 1549 | content: "\f1dc"; 1550 | } 1551 | .fa-paragraph:before { 1552 | content: "\f1dd"; 1553 | } 1554 | .fa-sliders:before { 1555 | content: "\f1de"; 1556 | } 1557 | .fa-share-alt:before { 1558 | content: "\f1e0"; 1559 | } 1560 | .fa-share-alt-square:before { 1561 | content: "\f1e1"; 1562 | } 1563 | .fa-bomb:before { 1564 | content: "\f1e2"; 1565 | } 1566 | -------------------------------------------------------------------------------- /less/graph.less: -------------------------------------------------------------------------------- 1 | circle { 2 | stroke-width: 1.5px; 3 | } 4 | 5 | line { 6 | stroke: #999; 7 | } 8 | 9 | .edgePath { 10 | stroke: black; 11 | } 12 | -------------------------------------------------------------------------------- /less/layout.less: -------------------------------------------------------------------------------- 1 | @import "util.less"; 2 | 3 | html, body { 4 | background: @whiteGhost; 5 | height: 100%; 6 | font-family: 'Open Sans Light'; 7 | } 8 | 9 | #app { 10 | height: 100%; 11 | } 12 | 13 | .whole-page { 14 | height: 100vh; 15 | } 16 | 17 | .center-container { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | flex-direction: column; 22 | } 23 | 24 | .page { 25 | height: inherit; 26 | } 27 | 28 | @topMargin: 20px; 29 | 30 | .nav { 31 | width: 250px; 32 | margin-left: @topMargin; 33 | margin-top: @topMargin; 34 | z-index: 1; 35 | } 36 | 37 | .box { 38 | padding: 12px 12px 12px 18px; 39 | border-radius: 3px; 40 | } 41 | 42 | @boxWidth: 400px; 43 | 44 | .center { 45 | flex: none; 46 | max-width: 50%; 47 | } 48 | 49 | .float-right-corner { 50 | position: absolute; 51 | top: 10px; 52 | right: 10px; 53 | } 54 | 55 | .float-bottom-corner { 56 | position: absolute; 57 | bottom: 15px; 58 | right: 15px; 59 | } 60 | 61 | .float-box { 62 | .box; 63 | .BoxShadowHelper(2); 64 | width: @boxWidth; 65 | position: relative; 66 | margin-bottom: 8px; 67 | } 68 | 69 | .float-box--side { 70 | .box; 71 | position: absolute; 72 | } 73 | 74 | // Style 75 | 76 | .blue { 77 | font-family: 'Open Sans Light'; 78 | color: @whiteGhost; 79 | background: @blueHavelock; 80 | } 81 | 82 | .blue-box { 83 | // font-size: larger; 84 | .blue; 85 | } 86 | 87 | .top-break { 88 | margin-top: 12px; 89 | } 90 | 91 | .blue-input { 92 | margin-bottom: 10px; 93 | border: 0; 94 | outline: 0; 95 | border-bottom: 1px solid @whiteGhost; 96 | width: 90%; 97 | .blue; 98 | } 99 | 100 | input::-webkit-input-placeholder { 101 | color: fade(@whiteGhost,50%); 102 | } 103 | 104 | input::-moz-input-placeholder { 105 | color: fade(@whiteGhost,50%); 106 | } 107 | 108 | .error-card { 109 | z-index: 2; 110 | // font-size: larger; 111 | color: @whiteGhost; 112 | background: @indianRed; 113 | > p { 114 | margin-bottom: 8px; 115 | } 116 | } 117 | 118 | .notification { 119 | position: absolute; 120 | left: 0; 121 | right: 0; 122 | margin: 20px auto; 123 | } 124 | 125 | .Title { 126 | font-family: 'Open Sans Semibold'; 127 | text-transform: uppercase; 128 | } 129 | 130 | .box__title { 131 | margin-top: 8px; 132 | margin-bottom: 12px; 133 | } 134 | 135 | .blue-box__title { 136 | .box__title; 137 | font-family: 'Open Sans Light'; 138 | font-weight: 300; 139 | } 140 | 141 | .blue-box__subtitle { 142 | .box__title; 143 | .Title; 144 | } 145 | 146 | .project-name { 147 | .blue-box__title; 148 | text-transform: lowercase; 149 | margin-bottom: 0px; 150 | } 151 | 152 | .build-title { 153 | margin-top: 3px; 154 | } 155 | 156 | .build-name { 157 | cursor: pointer; 158 | font-style: italic; 159 | font-size: medium; 160 | display: inline-block; 161 | vertical-align: middle; 162 | } 163 | 164 | @iconOpacity: 0.7; 165 | 166 | .menu-icon { 167 | margin-right: 3px; 168 | opacity: @iconOpacity; 169 | } 170 | 171 | .build-item { 172 | margin: 0; 173 | padding: 0; 174 | } 175 | 176 | .builds { 177 | margin-bottom: 0px; 178 | margin-top: 0px; 179 | padding-left: 10px; 180 | list-style-type: none; 181 | } 182 | 183 | .icon-btn { 184 | cursor: pointer; 185 | margin-left: 8px; 186 | font-size: 16px; 187 | 188 | } 189 | 190 | .clear-btn { 191 | .icon-btn; 192 | opacity: @iconOpacity; 193 | color: @whiteGhost; 194 | } 195 | 196 | .help-btn { 197 | .icon-btn; 198 | color: @blueHavelock; 199 | font-size: 30px; 200 | } 201 | 202 | .clear-btn:hover { 203 | text-decoration: none; 204 | opacity: 1; 205 | } 206 | 207 | .last-btns { 208 | margin-bottom: 6px; 209 | } 210 | 211 | .btn-container--center { 212 | .last-btns; 213 | text-align: right; 214 | } 215 | 216 | .btn-center { 217 | margin: 0 6px 0 auto; 218 | } 219 | 220 | // Button 221 | 222 | .btn { 223 | .BoxShadowHelper(1); 224 | .box; 225 | cursor: pointer; 226 | cursor: hand; 227 | display: inline-block; 228 | text-align: center; 229 | min-width: 50px; 230 | margin-top: 8px; 231 | margin-bottom: 8px; 232 | padding: 5px; 233 | padding-right: 10px; 234 | padding-left: 10px; 235 | .Title; 236 | } 237 | 238 | .btn--green { 239 | .btn; 240 | background: @greenAtlantis; 241 | } 242 | 243 | 244 | .btn--green--active { 245 | .btn; 246 | background: darken(@greenAtlantis,15%); 247 | } 248 | 249 | .divider { 250 | margin-top: 2px; 251 | display: block; 252 | height: 1px; 253 | background-color: fade(@whiteGhost,50%); 254 | } 255 | 256 | // Platform Buttons 257 | 258 | .platform--btns { 259 | display: flex; 260 | flex-direction: row; 261 | justify-content: space-around; 262 | .last-btns; 263 | } 264 | 265 | // Folder List 266 | 267 | .div-explorer { 268 | width: 0.90 * @boxWidth; 269 | } 270 | 271 | .folder-list { 272 | list-style: none; 273 | padding-left: 10px; 274 | } 275 | 276 | .folder-list__item { 277 | margin-right: 10px; 278 | } 279 | 280 | .file--activate { 281 | color: white; 282 | font-weight: bold; 283 | } 284 | 285 | .clickable { 286 | cursor: pointer; 287 | } 288 | 289 | .file-item { 290 | margin-top: 10px; 291 | } 292 | 293 | .file-item__text { 294 | .file-item; 295 | color: white; 296 | text-decoration: none; 297 | .clickable; 298 | // &:hover { 299 | // .file--activate; 300 | // } 301 | margin-right: 7px; 302 | } 303 | 304 | .file-item__text--activated { 305 | .file-item__text; 306 | .file--activate; 307 | &:hover { 308 | font-weight: initial; 309 | } 310 | } 311 | 312 | .expand-activate { 313 | color: @whiteGhost; 314 | } 315 | .expand-icon { 316 | color: fade(@whiteGhost,70%); 317 | font-size: 0.75em; 318 | &:hover { 319 | .expand-activate; 320 | } 321 | } 322 | 323 | .expand-icon--active { 324 | .expand-icon; 325 | .expand-activate; 326 | } -------------------------------------------------------------------------------- /less/loader.less: -------------------------------------------------------------------------------- 1 | @loaderHeight: 1px; 2 | 3 | .loader { 4 | height: @loaderHeight; 5 | width: 90%; 6 | position: relative; 7 | overflow: hidden; 8 | background-color: @blueHavelock; // #ddd; 9 | } 10 | 11 | .loader:before{ 12 | display: block; 13 | position: absolute; 14 | content: ""; 15 | left: -200px; 16 | width: 200px; 17 | height: @loaderHeight; 18 | background-color: @whiteGhost; // @blueHavelock; // #2980b9; 19 | animation: loading 3s linear infinite; 20 | } 21 | 22 | @keyframes loading { 23 | from {left: -200px; width: 30%;} 24 | 50% {width: 30%;} 25 | 70% {width: 50%;} 26 | 80% {left: 50%;} 27 | 90% {left: 105%;} 28 | to {left: 100%;} 29 | } -------------------------------------------------------------------------------- /less/main.less: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // Imports 3 | //------------------------------------------------------------------------------ 4 | 5 | @import "normalize-3.0.1.less"; 6 | @import "font-awesome-4.1.0.less"; 7 | @import "colors.less"; 8 | @import "graph.less"; 9 | @import "layout.less"; 10 | @import "loader.less"; 11 | @import "overlay.less"; 12 | 13 | //------------------------------------------------------------------------------ 14 | // Fonts 15 | //------------------------------------------------------------------------------ 16 | 17 | @font-face { 18 | font-family: 'Open Sans Light'; 19 | font-style: normal; 20 | font-weight: 300; 21 | src: local('Open Sans Light'), local('OpenSans-Light'), 22 | url('../fonts/OpenSans-Light.woff') format('woff'); 23 | } 24 | 25 | @font-face { 26 | font-family: 'Open Sans'; 27 | font-style: normal; 28 | font-weight: 400; 29 | src: local('Open Sans'), local('OpenSans'), 30 | url('../fonts/OpenSans.woff') format('woff'); 31 | } 32 | 33 | @font-face { 34 | font-family: 'Open Sans'; 35 | font-style: normal; 36 | font-weight: 600; 37 | src: local('Open Sans Semibold'), local('OpenSans-Semibold'), 38 | url('../fonts/OpenSans-Semibold.woff') format('woff'); 39 | } 40 | 41 | @font-face { 42 | font-family: 'Roboto Slab'; 43 | font-style: normal; 44 | font-weight: 400; 45 | src: local('Roboto Slab Regular'), local('RobotoSlab-Regular'), 46 | url('../fonts/RobotoSlab-Regular.woff2') format('woff2'); 47 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 48 | } 49 | 50 | @font-face { 51 | font-family: 'Droid Sans Mono'; 52 | font-style: normal; 53 | font-weight: 400; 54 | src: local('Droid Sans Mono'), local('DroidSansMono'), 55 | url('../fonts/DroidSansMono.woff2') format('woff2'); 56 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 57 | } 58 | 59 | .font-light(@size) { 60 | font-family: 'Open Sans Light'; 61 | font-size: @size; 62 | font-weight: 300; 63 | } 64 | 65 | .font-normal(@size) { 66 | font-family: 'Open Sans'; 67 | font-size: @size; 68 | font-weight: 400; 69 | } 70 | 71 | .font-bold(@size) { 72 | font-family: 'Open Sans'; 73 | font-size: @size; 74 | font-weight: 600; 75 | } 76 | 77 | //------------------------------------------------------------------------------ 78 | // Variables and Mixins 79 | //------------------------------------------------------------------------------ 80 | 81 | @activeBlue: #0075ea; 82 | @borderGrey: #e1e1e1; 83 | @buttonBorderColor: #d7d7d7; 84 | @errorRed: #e44545; 85 | @inactiveGrey: #666; 86 | @warningOrange: #c47f00; 87 | @primaryGreen: #309630; 88 | @tooltipBackgroundColor: #333; 89 | @tooltipZ: 10; 90 | 91 | @modalZ: 1000; 92 | 93 | .border-radius(@topright: 0, @bottomright: 0, @bottomleft: 0, @topleft: 0) { 94 | background-clip: padding-box; 95 | border-top-right-radius: @topright; 96 | border-bottom-right-radius: @bottomright; 97 | border-bottom-left-radius: @bottomleft; 98 | border-top-left-radius: @topleft; 99 | } 100 | 101 | //------------------------------------------------------------------------------ 102 | // Tags 103 | //------------------------------------------------------------------------------ 104 | 105 | body { 106 | box-sizing: border-box; 107 | color: #444; 108 | } 109 | 110 | // http://stackoverflow.com/a/3397158/2137320 111 | *:focus { 112 | outline: 0; 113 | } 114 | 115 | //------------------------------------------------------------------------------ 116 | // Snowflake Classes 117 | //------------------------------------------------------------------------------ 118 | 119 | .clr-737fa { 120 | clear: both; 121 | } 122 | 123 | .app-ca3cd { 124 | min-width: 800px; 125 | padding: 20px; 126 | } 127 | 128 | .title-8749a { 129 | font-family: 'Roboto Slab', serif; 130 | font-size: 32px; 131 | float: left; 132 | } 133 | 134 | .version-8838a { 135 | color: #888; 136 | display: inline-block; 137 | .font-normal(14px); 138 | padding-left: 5px; 139 | } 140 | 141 | .logo-0a166 { 142 | display: block; 143 | float: left; 144 | margin: 6px 5px 0 0; 145 | width: 35px; 146 | } 147 | 148 | .settings-link-c1709 { 149 | border: 1px solid @borderGrey; 150 | border-radius: 4px; 151 | cursor: pointer; 152 | float: right; 153 | padding: 10px 20px; 154 | } 155 | 156 | .title-links-42b06 { 157 | float: right; 158 | } 159 | 160 | .link-3d3ad { 161 | color: @inactiveGrey; 162 | cursor: pointer; 163 | display: inline-block; 164 | .font-normal(18px); 165 | margin: 15px 0 0 30px; 166 | 167 | .fa { 168 | margin-right: 5px; 169 | } 170 | } 171 | 172 | .link-3d3ad:hover { 173 | color: @activeBlue; 174 | text-decoration: underline; 175 | } 176 | 177 | .header-a4c14 { 178 | border-bottom: 1px solid @borderGrey; 179 | margin-bottom: 25px; 180 | padding-bottom: 20px; 181 | position: relative; 182 | } 183 | 184 | .btn-da85d { 185 | background: #e6e6e6; 186 | border: 1px solid @buttonBorderColor; 187 | border-radius: 4px; 188 | color: #000; 189 | cursor: pointer; 190 | display: inline-block; 191 | .font-normal(14px); 192 | margin-left: 10px; 193 | padding: 8px 12px; 194 | } 195 | 196 | .count-cfa27 { 197 | color: #555; 198 | display: inline-block; 199 | .font-light(12px); 200 | margin-left: 5px; 201 | } 202 | 203 | .disabled-btn-1884b { 204 | background: #e6e6e6; 205 | border: 0; 206 | border-radius: 4px; 207 | color: #555; 208 | cursor: auto; 209 | display: inline-block; 210 | .font-normal(14px); 211 | margin-right: 10px; 212 | padding: 8px 12px; 213 | } 214 | 215 | .project-1b83a { 216 | margin-bottom: 50px; 217 | } 218 | 219 | .wrapper-714e4 { 220 | height: 40px; 221 | margin-bottom: 20px; 222 | position: relative; 223 | } 224 | 225 | .left-ba9e7 { 226 | bottom: 0; 227 | .font-bold(28px); 228 | left: 0; 229 | position: absolute; 230 | } 231 | 232 | .right-f5656 { 233 | bottom: 0; 234 | position: absolute; 235 | right: 0; 236 | } 237 | 238 | .error-56f38 { 239 | background: #ffffcc; 240 | margin-bottom: 2px; 241 | padding: 10px; 242 | } 243 | 244 | .success-5c065 { 245 | color: green; 246 | } 247 | 248 | .with-warnings-4b105 { 249 | color: @warningOrange; 250 | } 251 | 252 | .errors-2718a { 253 | color: @errorRed; 254 | } 255 | 256 | .status-b8614 { 257 | color: @inactiveGrey; 258 | display: inline-block; 259 | .font-light(18px); 260 | margin-left: 8px; 261 | } 262 | 263 | .edit-c0ba4 { 264 | color: @inactiveGrey; 265 | cursor: pointer; 266 | display: inline-block; 267 | .font-light(18px); 268 | margin-left: 8px; 269 | } 270 | 271 | .edit-c0ba4:hover { 272 | color: @activeBlue; 273 | text-decoration: underline; 274 | } 275 | 276 | .tbl-bdf39 { 277 | border: 1px solid @borderGrey; 278 | width: 100%; 279 | 280 | .fa { 281 | margin-right: 5px; 282 | } 283 | } 284 | 285 | .header-row-50e32 { 286 | background: #f2f6ff; 287 | border-bottom: 1px solid @borderGrey; 288 | } 289 | 290 | .th-92ca4 { 291 | color: #8e8e8e; 292 | .font-bold(11px); 293 | padding: 12px; 294 | text-align: left; 295 | text-transform: uppercase; 296 | 297 | .fa { 298 | color: #444; 299 | font-size: 14px; 300 | width: 14px; 301 | } 302 | } 303 | 304 | .small-check-7b3d7 { 305 | font-size: 12px !important; 306 | } 307 | 308 | .build-row-fdd97 { 309 | 310 | } 311 | 312 | .odd-c27e6 { 313 | background-color: #fafafa; 314 | } 315 | 316 | .even-158a9 { 317 | background-color: #fff; 318 | } 319 | 320 | // NOTE: if you apply opacity to the tr directly you'll get a border bug 321 | .not-active-a8d35 { 322 | td { 323 | opacity: 0.50; 324 | } 325 | } 326 | 327 | .warning-row-097c8 { 328 | background-color: rgba(255, 255, 100, 0.5); 329 | } 330 | 331 | .warning-cell-b9f12 { 332 | color: #000; 333 | .font-normal(14px); 334 | padding: 12px; 335 | } 336 | 337 | .error-row-b3028 { 338 | background-color: @errorRed; 339 | } 340 | 341 | .error-cell-1ccea { 342 | color: #fff; 343 | .font-normal(14px); 344 | padding: 12px; 345 | } 346 | 347 | .cell-9ad24 { 348 | .font-normal(14px); 349 | padding: 12px; 350 | } 351 | 352 | .time-fc085 { 353 | font-family: Verdana; 354 | font-size: 12px; 355 | } 356 | 357 | .menu-b4b27 { 358 | background: #fff; 359 | border: 1px solid #e1e1e1; 360 | .font-normal(14px); 361 | margin-top: 10px; 362 | min-width: 140px; 363 | padding: 10px; 364 | position: absolute; 365 | right: 0; 366 | z-index: 10; 367 | } 368 | 369 | .menu-b4b27:after, .menu-b4b27:before { 370 | bottom: 100%; 371 | border: solid transparent; 372 | content: " "; 373 | height: 0; 374 | width: 0; 375 | position: absolute; 376 | pointer-events: none; 377 | } 378 | 379 | .menu-b4b27:after { 380 | border-color: rgba(255, 255, 255, 0); 381 | border-bottom-color: #fff; 382 | border-width: 8px; 383 | margin-left: -8px; 384 | right: 8px; 385 | } 386 | 387 | .menu-b4b27:before { 388 | border-color: rgba(225, 225, 225, 0); 389 | border-bottom-color: #e1e1e1; 390 | border-width: 9px; 391 | margin-left: -9px; 392 | right: 7px; 393 | } 394 | 395 | .label-ec878 { 396 | cursor: pointer; 397 | display: block; 398 | padding: 5px; 399 | 400 | .fa { 401 | width: 20px; 402 | } 403 | } 404 | 405 | .bld-e7c4d { 406 | cursor: pointer; 407 | padding: 5px; 408 | 409 | .fa { 410 | width: 20px; 411 | } 412 | } 413 | 414 | .label-ec878:hover, .bld-e7c4d:hover { 415 | background: #d9edf7; 416 | } 417 | 418 | .small-cffc5 { 419 | color: #8e8e8e; 420 | display: block; 421 | .font-bold(11px); 422 | padding: 5px; 423 | text-transform: uppercase; 424 | } 425 | 426 | .spacer-685b6 { 427 | height: 10px; 428 | } 429 | 430 | .compile-btn-17a78 { 431 | background: #e6e6e6; 432 | border: 1px solid @buttonBorderColor; 433 | .border-radius(0, 0, 4px, 4px); 434 | color: #000; 435 | cursor: pointer; 436 | display: inline-block; 437 | .font-normal(14px); 438 | padding: 8px 12px; 439 | } 440 | 441 | .menu-btn-550bf { 442 | background: #e6e6e6; 443 | border: 1px solid @buttonBorderColor; 444 | border-left: 0; 445 | color: #000; 446 | cursor: pointer; 447 | display: inline-block; 448 | .font-normal(14px); 449 | padding: 8px 12px; 450 | .border-radius(4px, 4px, 0, 0); 451 | } 452 | 453 | .status-984ee { 454 | .font-light(20px); 455 | display: inline-block; 456 | vertical-align: bottom; 457 | } 458 | 459 | .status-d941c { 460 | .font-light(20px); 461 | display: inline-block; 462 | vertical-align: bottom; 463 | 464 | .fa { 465 | font-size: 17px; 466 | margin-right: 5px; 467 | } 468 | } 469 | 470 | .project-icons-dd1bb { 471 | font-size: 16px; 472 | margin-left: 10px; 473 | vertical-align: middle; 474 | } 475 | 476 | .project-icon-1711d { 477 | cursor: pointer; 478 | margin-left: 8px; 479 | color: #888; 480 | } 481 | 482 | .project-icon-1711d:hover { 483 | color: @activeBlue; 484 | text-decoration: none; 485 | } 486 | 487 | .loading-6792e { 488 | .font-light(42px); 489 | left: 50%; 490 | position: absolute; 491 | top: 25%; 492 | transform: translate(-50%, 0%); 493 | 494 | .fa { 495 | font-size: 36px; 496 | margin-right: 10px; 497 | } 498 | } 499 | 500 | .jre-5d930 { 501 | .font-light(32px); 502 | color: #666; 503 | position: absolute; 504 | top: 25%; 505 | text-align: center; 506 | width: 100%; 507 | 508 | a { 509 | color: @activeBlue; 510 | cursor: pointer; 511 | } 512 | } 513 | 514 | .outer-f80bb { 515 | position: relative; 516 | } 517 | 518 | .inner-aa3fc { 519 | left: 50%; 520 | position: absolute; 521 | top: 80px; 522 | transform: translate(-50%, -50%); 523 | 524 | h4 { 525 | font-size: 24px; 526 | padding: 0; 527 | margin: 0 0 8px 0; 528 | } 529 | 530 | p { 531 | .font-light(18px); 532 | margin: 0; 533 | padding: 0; 534 | } 535 | } 536 | 537 | .link-e7e58 { 538 | color: @activeBlue; 539 | cursor: pointer; 540 | text-decoration: none; 541 | } 542 | 543 | .link-e7e58:hover { 544 | text-decoration: underline; 545 | } 546 | 547 | .modal-overlay-120d3 { 548 | background-color: #3d464d; 549 | height: 100%; 550 | left: 0; 551 | opacity: .6; 552 | position: fixed; 553 | top: 0; 554 | width: 100%; 555 | z-index: (@modalZ - 1); 556 | } 557 | 558 | .Modal() { 559 | box-sizing: border-box; 560 | background: #f8f9fa; 561 | border: 1px solid #e3e3e3; 562 | border-radius: 3px; 563 | left: 50%; 564 | min-width: 500px; 565 | position: absolute; 566 | top: 15%; 567 | transform: translate(-50%, 0%); 568 | z-index: @modalZ; 569 | } 570 | 571 | .modal-body-fe4db { 572 | .Modal(); 573 | padding: 30px 0 0 0; 574 | 575 | code { 576 | font-family: 'Droid Sans Mono'; 577 | } 578 | } 579 | 580 | .modal-body-8c212 { 581 | .Modal(); 582 | padding: 20px; 583 | } 584 | 585 | .big-btn-a5d18 { 586 | background: @primaryGreen; 587 | border: 0; 588 | border-radius: 2px; 589 | color: #fff; 590 | display: inline-block; 591 | .font-normal(16px); 592 | padding: 10px 20px; 593 | } 594 | 595 | .label-b0246 { 596 | display: block; 597 | .font-light(18px); 598 | margin: 0 0 10px 0; 599 | padding: 0; 600 | } 601 | 602 | .modal-chunk-2041a { 603 | margin: 0 0 30px 0; 604 | padding: 0 20px; 605 | } 606 | 607 | .modal-bottom-050c3 { 608 | background: #eee; 609 | padding: 20px; 610 | 611 | button { 612 | display: inline-block; 613 | margin-right: 12px; 614 | padding: 5px 10px; 615 | } 616 | } 617 | 618 | .icon-e70fb { 619 | margin-right: 8px; 620 | } 621 | 622 | .small-info-b72e9 { 623 | .font-light(16px); 624 | margin: 10px 0 0 0; 625 | padding: 0; 626 | } 627 | 628 | .success-hdr-0f1c6 { 629 | .font-bold(20px); 630 | margin: 0; 631 | padding: 0; 632 | } 633 | 634 | .text-input-4800e { 635 | box-sizing: border-box; 636 | font-size: 24px; 637 | padding: 6px; 638 | width: 100%; 639 | } 640 | 641 | .creating-6d31a { 642 | .font-normal(24px); 643 | } 644 | 645 | .error-cdef1 { 646 | background: @errorRed; 647 | color: #fff; 648 | font-size: 16px; 649 | padding: 15px 10px; 650 | margin: 0 10px; 651 | 652 | .fa { 653 | margin-right: 6px; 654 | } 655 | } 656 | 657 | .primary-d0cd0 { 658 | background: @primaryGreen; 659 | border: 0; 660 | border-radius: 2px; 661 | color: #fff; 662 | display: inline-block; 663 | font-size: 14px; 664 | padding: 6px 12px; 665 | } 666 | 667 | .bottom-link-7d8d7 { 668 | color: @activeBlue; 669 | cursor: pointer; 670 | .font-normal(14px); 671 | text-decoration: none; 672 | } 673 | 674 | .bottom-link-7d8d7:hover { 675 | text-decoration: underline; 676 | } 677 | 678 | .modal-title-8e35b { 679 | border-bottom: 1px solid #e1e1e1; 680 | .font-light(28px); 681 | margin: 0 0 20px 0; 682 | padding: 0 0 10px 0; 683 | } 684 | 685 | .settings-label-7daea { 686 | cursor: pointer; 687 | display: block; 688 | margin-bottom: 10px; 689 | 690 | .fa { 691 | width: 20px; 692 | } 693 | } 694 | 695 | .spacer-586a6 { 696 | height: 15px; 697 | } 698 | 699 | .no-builds-2295f { 700 | .font-normal(14px); 701 | background-color: #fafafa; 702 | padding: 12px; 703 | opacity: 0.50; 704 | } 705 | 706 | .load-builds-1b35d { 707 | .font-normal(14px); 708 | background-color: #fafafa; 709 | padding: 12px; 710 | opacity: 0.50; 711 | } 712 | 713 | .info-bar-b38b4 { 714 | background: #ff6; 715 | color: #000; 716 | .font-normal(16px); 717 | padding: 15px 20px; 718 | 719 | .fa { 720 | margin-right: 8px; 721 | } 722 | } 723 | 724 | .show-me-btn-16a12 { 725 | background: @primaryGreen; 726 | border: 0; 727 | border-radius: 2px; 728 | color: #fff; 729 | display: inline-block; 730 | margin-left: 10px; 731 | padding: 6px 12px; 732 | } 733 | 734 | .ignore-link-ff917 { 735 | color: @activeBlue; 736 | color: #20c; 737 | cursor: pointer; 738 | display: inline-block; 739 | margin-left: 10px; 740 | } 741 | 742 | .tooltip-aca75 { 743 | background: @tooltipBackgroundColor; 744 | border-radius: 4px; 745 | color: #fff; 746 | left: 135px; 747 | padding: 10px; 748 | position: absolute; 749 | top: 44px; 750 | z-index: @tooltipZ; 751 | 752 | td { 753 | height: 20px; 754 | } 755 | } 756 | 757 | .tooltip-aca75:after { 758 | bottom: 100%; 759 | left: 17px; 760 | border: solid transparent; 761 | content: " "; 762 | height: 0; 763 | width: 0; 764 | position: absolute; 765 | pointer-events: none; 766 | border-color: rgba(51, 51, 51, 0); 767 | border-bottom-color: #333; 768 | border-width: 5px; 769 | margin-left: -5px; 770 | } 771 | 772 | .label-be32b { 773 | color: #999; 774 | .font-bold(11px); 775 | text-transform: uppercase; 776 | } 777 | 778 | .value-aee48 { 779 | padding-left: 6px; 780 | font-size: 14px; 781 | } 782 | 783 | .mono-cd368 { 784 | font-family: "Droid Sans Mono"; 785 | font-size: 14px; 786 | } 787 | -------------------------------------------------------------------------------- /less/normalize-3.0.1.less: -------------------------------------------------------------------------------- 1 | // normalize.css v3.0.1 | MIT License | git.io/normalize 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox. 29 | * Correct `block` display not defined for `main` in IE 11. 30 | */ 31 | 32 | article, 33 | aside, 34 | details, 35 | figcaption, 36 | figure, 37 | footer, 38 | header, 39 | hgroup, 40 | main, 41 | nav, 42 | section, 43 | summary { 44 | display: block; 45 | } 46 | 47 | /** 48 | * 1. Correct `inline-block` display not defined in IE 8/9. 49 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 50 | */ 51 | 52 | audio, 53 | canvas, 54 | progress, 55 | video { 56 | display: inline-block; /* 1 */ 57 | vertical-align: baseline; /* 2 */ 58 | } 59 | 60 | /** 61 | * Prevent modern browsers from displaying `audio` without controls. 62 | * Remove excess height in iOS 5 devices. 63 | */ 64 | 65 | audio:not([controls]) { 66 | display: none; 67 | height: 0; 68 | } 69 | 70 | /** 71 | * Address `[hidden]` styling not present in IE 8/9/10. 72 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 73 | */ 74 | 75 | [hidden], 76 | template { 77 | display: none; 78 | } 79 | 80 | /* Links 81 | ========================================================================== */ 82 | 83 | /** 84 | * Remove the gray background color from active links in IE 10. 85 | */ 86 | 87 | a { 88 | background: transparent; 89 | } 90 | 91 | /** 92 | * Improve readability when focused and also mouse hovered in all browsers. 93 | */ 94 | 95 | a:active, 96 | a:hover { 97 | outline: 0; 98 | } 99 | 100 | /* Text-level semantics 101 | ========================================================================== */ 102 | 103 | /** 104 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 105 | */ 106 | 107 | abbr[title] { 108 | border-bottom: 1px dotted; 109 | } 110 | 111 | /** 112 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: bold; 118 | } 119 | 120 | /** 121 | * Address styling not present in Safari and Chrome. 122 | */ 123 | 124 | dfn { 125 | font-style: italic; 126 | } 127 | 128 | /** 129 | * Address variable `h1` font-size and margin within `section` and `article` 130 | * contexts in Firefox 4+, Safari, and Chrome. 131 | */ 132 | 133 | h1 { 134 | font-size: 2em; 135 | margin: 0.67em 0; 136 | } 137 | 138 | /** 139 | * Address styling not present in IE 8/9. 140 | */ 141 | 142 | mark { 143 | background: #ff0; 144 | color: #000; 145 | } 146 | 147 | /** 148 | * Address inconsistent and variable font size in all browsers. 149 | */ 150 | 151 | small { 152 | font-size: 80%; 153 | } 154 | 155 | /** 156 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 157 | */ 158 | 159 | sub, 160 | sup { 161 | font-size: 75%; 162 | line-height: 0; 163 | position: relative; 164 | vertical-align: baseline; 165 | } 166 | 167 | sup { 168 | top: -0.5em; 169 | } 170 | 171 | sub { 172 | bottom: -0.25em; 173 | } 174 | 175 | /* Embedded content 176 | ========================================================================== */ 177 | 178 | /** 179 | * Remove border when inside `a` element in IE 8/9/10. 180 | */ 181 | 182 | img { 183 | border: 0; 184 | } 185 | 186 | /** 187 | * Correct overflow not hidden in IE 9/10/11. 188 | */ 189 | 190 | svg:not(:root) { 191 | overflow: hidden; 192 | } 193 | 194 | /* Grouping content 195 | ========================================================================== */ 196 | 197 | /** 198 | * Address margin not present in IE 8/9 and Safari. 199 | */ 200 | 201 | figure { 202 | margin: 1em 40px; 203 | } 204 | 205 | /** 206 | * Address differences between Firefox and other browsers. 207 | */ 208 | 209 | hr { 210 | -moz-box-sizing: content-box; 211 | box-sizing: content-box; 212 | height: 0; 213 | } 214 | 215 | /** 216 | * Contain overflow in all browsers. 217 | */ 218 | 219 | pre { 220 | overflow: auto; 221 | } 222 | 223 | /** 224 | * Address odd `em`-unit font size rendering in all browsers. 225 | */ 226 | 227 | code, 228 | kbd, 229 | pre, 230 | samp { 231 | font-family: monospace, monospace; 232 | font-size: 1em; 233 | } 234 | 235 | /* Forms 236 | ========================================================================== */ 237 | 238 | /** 239 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 240 | * styling of `select`, unless a `border` property is set. 241 | */ 242 | 243 | /** 244 | * 1. Correct color not being inherited. 245 | * Known issue: affects color of disabled elements. 246 | * 2. Correct font properties not being inherited. 247 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 248 | */ 249 | 250 | button, 251 | input, 252 | optgroup, 253 | select, 254 | textarea { 255 | color: inherit; /* 1 */ 256 | font: inherit; /* 2 */ 257 | margin: 0; /* 3 */ 258 | } 259 | 260 | /** 261 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 262 | */ 263 | 264 | button { 265 | overflow: visible; 266 | } 267 | 268 | /** 269 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 270 | * All other form control elements do not inherit `text-transform` values. 271 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 272 | * Correct `select` style inheritance in Firefox. 273 | */ 274 | 275 | button, 276 | select { 277 | text-transform: none; 278 | } 279 | 280 | /** 281 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 282 | * and `video` controls. 283 | * 2. Correct inability to style clickable `input` types in iOS. 284 | * 3. Improve usability and consistency of cursor style between image-type 285 | * `input` and others. 286 | */ 287 | 288 | button, 289 | html input[type="button"], /* 1 */ 290 | input[type="reset"], 291 | input[type="submit"] { 292 | -webkit-appearance: button; /* 2 */ 293 | cursor: pointer; /* 3 */ 294 | } 295 | 296 | /** 297 | * Re-set default cursor for disabled elements. 298 | */ 299 | 300 | button[disabled], 301 | html input[disabled] { 302 | cursor: default; 303 | } 304 | 305 | /** 306 | * Remove inner padding and border in Firefox 4+. 307 | */ 308 | 309 | button::-moz-focus-inner, 310 | input::-moz-focus-inner { 311 | border: 0; 312 | padding: 0; 313 | } 314 | 315 | /** 316 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 317 | * the UA stylesheet. 318 | */ 319 | 320 | input { 321 | line-height: normal; 322 | } 323 | 324 | /** 325 | * It's recommended that you don't attempt to style these elements. 326 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 327 | * 328 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 329 | * 2. Remove excess padding in IE 8/9/10. 330 | */ 331 | 332 | input[type="checkbox"], 333 | input[type="radio"] { 334 | box-sizing: border-box; /* 1 */ 335 | padding: 0; /* 2 */ 336 | } 337 | 338 | /** 339 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 340 | * `font-size` values of the `input`, it causes the cursor style of the 341 | * decrement button to change from `default` to `text`. 342 | */ 343 | 344 | input[type="number"]::-webkit-inner-spin-button, 345 | input[type="number"]::-webkit-outer-spin-button { 346 | height: auto; 347 | } 348 | 349 | /** 350 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 351 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 352 | * (include `-moz` to future-proof). 353 | */ 354 | 355 | input[type="search"] { 356 | -webkit-appearance: textfield; /* 1 */ 357 | -moz-box-sizing: content-box; 358 | -webkit-box-sizing: content-box; /* 2 */ 359 | box-sizing: content-box; 360 | } 361 | 362 | /** 363 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 364 | * Safari (but not Chrome) clips the cancel button when the search input has 365 | * padding (and `textfield` appearance). 366 | */ 367 | 368 | input[type="search"]::-webkit-search-cancel-button, 369 | input[type="search"]::-webkit-search-decoration { 370 | -webkit-appearance: none; 371 | } 372 | 373 | /** 374 | * Define consistent border, margin, and padding. 375 | */ 376 | 377 | fieldset { 378 | border: 1px solid #c0c0c0; 379 | margin: 0 2px; 380 | padding: 0.35em 0.625em 0.75em; 381 | } 382 | 383 | /** 384 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 385 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 386 | */ 387 | 388 | legend { 389 | border: 0; /* 1 */ 390 | padding: 0; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove default vertical scrollbar in IE 8/9/10/11. 395 | */ 396 | 397 | textarea { 398 | overflow: auto; 399 | } 400 | 401 | /** 402 | * Don't inherit the `font-weight` (applied by a rule above). 403 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 404 | */ 405 | 406 | optgroup { 407 | font-weight: bold; 408 | } 409 | 410 | /* Tables 411 | ========================================================================== */ 412 | 413 | /** 414 | * Remove most spacing between table cells. 415 | */ 416 | 417 | table { 418 | border-collapse: collapse; 419 | border-spacing: 0; 420 | } 421 | 422 | td, 423 | th { 424 | padding: 0; 425 | } -------------------------------------------------------------------------------- /less/overlay.less: -------------------------------------------------------------------------------- 1 | @zModal: 10; 2 | 3 | .overlay { 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | z-index: @zModal; 10 | background-color: rgba(0,0,0,0.5); /*dim the background*/ 11 | } 12 | 13 | .modal { 14 | width: 100%; 15 | height: 100%; 16 | top: 0; 17 | left: 0; 18 | position: absolute; 19 | z-index: @zModal + 1; 20 | } 21 | -------------------------------------------------------------------------------- /less/util.less: -------------------------------------------------------------------------------- 1 | .BoxShadowHelper(@level: 1){ 2 | & when (@level = 1) { 3 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 4 | } 5 | & when (@level = 2) { 6 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 7 | } 8 | & when (@level = 3) { 9 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); 10 | } 11 | & when (@level = 4) { 12 | box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); 13 | } 14 | & when (@level = 5) { 15 | box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22); 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asterion", 3 | "license": "Private", 4 | "private": true, 5 | "dependencies": { 6 | "fs-plus": "2.8.1", 7 | "madge": "0.5.2" 8 | }, 9 | "devDependencies": { 10 | "asar": "0.8.0", 11 | "closurecompiler-externs": "1.0.4", 12 | "grunt": "0.4.5", 13 | "grunt-download-electron": "2.1.1", 14 | "moment": "2.10.6", 15 | "shelljs": "0.5.3", 16 | "winresourcer": "0.9.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject asterion "0.1.0-alpha1" 2 | :description "Make and explore dependency graphs for Clojure(Script) projects." 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :source-paths ["src/clj" "src/cljs" "src/cljc"] 8 | 9 | :dependencies [[org.clojure/clojure "1.7.0"] 10 | [org.clojure/clojurescript "1.7.145"] 11 | [org.clojure/tools.namespace "0.3.19-SNAPSHOT"] 12 | [org.clojure/tools.reader "0.10.0-alpha1"] 13 | [org.clojure/data.json "0.2.6"] 14 | [commons-io/commons-io "2.4"] 15 | [environ "1.0.1"] 16 | [com.stuartsierra/component "0.2.3"] 17 | [leiningen-core "2.5.3"] 18 | [clj-jgit "0.8.8"] 19 | [ring "1.3.2"] 20 | [compojure "1.4.0"] 21 | [cljsjs/d3 "3.5.5-3"] 22 | [org.omcljs/om "0.9.0"] 23 | [cljs-ajax "0.5.0"]] 24 | 25 | :min-lein-version "2.5.1" 26 | 27 | :ring {:handler asterion.server/app-handler} 28 | 29 | :cljsbuild 30 | {:builds {:dev {:source-paths ["src/cljs" "env/dev/cljs"] 31 | :figwheel {:on-jsload "asterion.core/init!"} 32 | :compiler {:output-to "resources/public/js/p/app.js" 33 | :output-dir "resources/public/js/p/out" 34 | :asset-path "js/p/out" 35 | :source-map true 36 | :main "asterion.dev" 37 | :verbose true 38 | :optimizations :none 39 | :pretty-print true 40 | :cache-analysis true}} 41 | :production 42 | {:source-paths ["src/cljs" "env/prod/cljs"] 43 | :compiler {:output-to "resources/public/js/p/app.js" 44 | :optimizations :simple 45 | :main "asterion.prod" 46 | :closure-defines {:goog.DEBUG false}}}}} 47 | 48 | :clean-targets ^{:protect false} [:target-path "out" "resources/public/js/p"] 49 | 50 | :figwheel {:css-dirs ["resources/public/css"]} 51 | 52 | :profiles {:dev {:plugins [[lein-ancient "0.6.7"] 53 | [lein-cljsbuild "1.1.0"] 54 | [lein-environ "1.0.1"] 55 | [lein-kibit "0.1.2"] 56 | [lein-ring "0.9.7"] 57 | [lein-cljfmt "0.3.0"] 58 | [lein-beanstalk "0.2.7"] 59 | [lein-figwheel "0.4.1"]]}}) 60 | -------------------------------------------------------------------------------- /resources/public/app.js: -------------------------------------------------------------------------------- 1 | var app = require('app'), 2 | BrowserWindow = require('browser-window'), 3 | fs = require('fs-plus'), 4 | ipc = require('ipc'), 5 | Menu = require('menu'), 6 | path = require('path'), 7 | dialog = require('dialog'), 8 | shell = require('shell'), 9 | child_process = require('child_process'), 10 | packageJson = require(__dirname + '/package.json'); 11 | 12 | // Report crashes to atom-shell. 13 | require('crash-reporter').start(); 14 | 15 | const devConfigFile = __dirname + '/config.json'; 16 | var devConfig = {}; 17 | if (fs.existsSync(devConfigFile)) { 18 | devConfig = require(devConfigFile); 19 | } 20 | 21 | 22 | const isDev = (packageJson.version.indexOf("DEV") !== -1); 23 | const onMac = (process.platform === 'darwin'); 24 | const acceleratorKey = onMac ? "Command" : "Control"; 25 | const isInternal = (devConfig.hasOwnProperty('internal') && devConfig['internal'] === true); 26 | 27 | 28 | 29 | // Keep a global reference of the window object, if you don't, the window will 30 | // be closed automatically when the javascript object is GCed. 31 | var mainWindow = null; 32 | 33 | // make sure app.getDataPath() exists 34 | // https://github.com/oakmac/cuttle/issues/92 35 | if (!fs.isDirectorySync(app.getDataPath())) { 36 | fs.mkdirSync(app.getDataPath()); 37 | } 38 | 39 | 40 | //------------------------------------------------------------------------------ 41 | // Main 42 | //------------------------------------------------------------------------------ 43 | 44 | const versionString = "Version " + packageJson.version + "\nDate " + packageJson["build-date"] + "\nCommit " + packageJson["build-commit"]; 45 | 46 | 47 | function showVersion() { 48 | dialog.showMessageBox({type: "info", title: "Version", buttons: ["OK"], message: versionString}); 49 | } 50 | 51 | var fileMenu = { 52 | label: 'File', 53 | submenu: [ 54 | { 55 | label: 'Quit', 56 | accelerator: acceleratorKey + '+Q', 57 | click: function () 58 | { 59 | app.quit(); 60 | } 61 | }] 62 | }; 63 | 64 | var helpMenu = { 65 | label: 'Help', 66 | submenu: [ 67 | { 68 | label: 'Version', 69 | click: showVersion 70 | }] 71 | }; 72 | 73 | var debugMenu = { 74 | label: 'Debug', 75 | submenu: [ 76 | { 77 | label: 'Toggle DevTools', 78 | click: function () 79 | { 80 | mainWindow.toggleDevTools(); 81 | } 82 | } 83 | ] 84 | }; 85 | 86 | var menuTemplate = [fileMenu, debugMenu, helpMenu]; 87 | 88 | 89 | // NOTE: not all of the browserWindow options listed on the docs page work 90 | // on all operating systems 91 | const browserWindowOptions = { 92 | height: 850, 93 | title: 'asterion', 94 | width: 1400, 95 | icon: __dirname + '/img/logo_96x96.png' 96 | }; 97 | 98 | 99 | //------------------------------------------------------------------------------ 100 | // Register IPC Calls from the Renderers 101 | //------------------------------------------------------------------------------ 102 | 103 | // Open files 104 | const projectDialogOpts = { 105 | title: 'Please select an existing project.clj file', 106 | properties: ['openFile'], 107 | filters: [ 108 | { 109 | name: 'Leiningen project.clj', 110 | extensions: ['boot', 'xml', 'clj'] 111 | } 112 | ] 113 | }; 114 | 115 | function addProjectDialog(event, platform) { 116 | dialog.showOpenDialog(projectDialogOpts, function(filenames) { 117 | if (filenames) { 118 | var filename = filenames[0]; 119 | mainWindow.webContents.send('add-project-success', filename, platform); 120 | } 121 | }); 122 | } 123 | 124 | ipc.on('request-project-dialog', addProjectDialog); 125 | 126 | // grep files 127 | 128 | function grepWithFork(event, filenames, stringPattern) { 129 | var cmd = "egrep -Rl -m 100 '" 130 | + stringPattern 131 | + "' " 132 | + filenames.join(" "); 133 | child_process.exec(cmd, {maxBuffer: 200000000}, function(err, stdout) { 134 | if (err && (err.code == 2)) { 135 | console.log("There was an error:\n"); 136 | process.stdout.write(stdout); 137 | mainWindow.webContents.send('search-error',err); 138 | } else if (err && (err.code == 1)) { 139 | mainWindow.webContents.send('search-not-found'); 140 | } else { 141 | mainWindow.webContents.send('search-success',stdout.split(/\n/)); 142 | } 143 | }); 144 | } 145 | 146 | ipc.on('request-search', grepWithFork); 147 | 148 | //------------------------------------------------------------------------------ 149 | // Ready 150 | //------------------------------------------------------------------------------ 151 | 152 | 153 | // This method will be called when atom-shell has done everything 154 | // initialization and ready for creating browser windows. 155 | app.on('ready', function() { 156 | // Create the browser window. 157 | mainWindow = new BrowserWindow(browserWindowOptions); 158 | 159 | // and load the index.html of the app. 160 | mainWindow.loadUrl('file://' + __dirname + '/index.html'); 161 | 162 | var menu = Menu.buildFromTemplate(menuTemplate); 163 | 164 | Menu.setApplicationMenu(menu); 165 | 166 | // Emitted when the window is closed. 167 | mainWindow.on('closed', function() { 168 | // Dereference the window object, usually you would store windows 169 | // in an array if your app supports multi windows, this is the time 170 | // when you should delete the corresponding element. 171 | mainWindow = null; 172 | app.quit(); 173 | }); 174 | 175 | if (devConfig.hasOwnProperty('dev-tools') && devConfig['dev-tools'] === true) { 176 | mainWindow.openDevTools(); 177 | } 178 | 179 | }); 180 | -------------------------------------------------------------------------------- /resources/public/css/main.css: -------------------------------------------------------------------------------- 1 | circle { 2 | stroke-width: 1.5px; 3 | } 4 | 5 | line { 6 | stroke: #999; 7 | } 8 | 9 | .edgePath { 10 | stroke: black; 11 | } 12 | -------------------------------------------------------------------------------- /resources/public/example.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev-tools": true 3 | } 4 | -------------------------------------------------------------------------------- /resources/public/fonts/DroidSansMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/DroidSansMono.woff2 -------------------------------------------------------------------------------- /resources/public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /resources/public/fonts/OpenSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/OpenSans-Light.woff -------------------------------------------------------------------------------- /resources/public/fonts/OpenSans-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/OpenSans-Semibold.woff -------------------------------------------------------------------------------- /resources/public/fonts/OpenSans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/OpenSans.woff -------------------------------------------------------------------------------- /resources/public/fonts/RobotoSlab-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/RobotoSlab-Regular.woff2 -------------------------------------------------------------------------------- /resources/public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /resources/public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /resources/public/img/clojure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/public/img/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/img/logo.icns -------------------------------------------------------------------------------- /resources/public/img/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/img/logo.ico -------------------------------------------------------------------------------- /resources/public/img/logo_96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/resources/public/img/logo_96x96.png -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Asterion 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/public/js/vendor/ga.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 5 | 6 | ga('create', 'UA-69327501-1', 'auto'); 7 | ga('send', 'pageview'); 8 | 9 | -------------------------------------------------------------------------------- /resources/public/js/vendor/palette.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | * 3 | * Colour Palette Generator script. 4 | * Copyright (c) 2014 Google Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. You may 8 | * obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 15 | * implied. See the License for the specific language governing 16 | * permissions and limitations under the License. 17 | * 18 | * Furthermore, ColorBrewer colour schemes are covered by the following: 19 | * 20 | * Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and 21 | * The Pennsylvania State University. 22 | * 23 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 24 | * not use this file except in compliance with the License. You may obtain 25 | * a copy of the License at 26 | * 27 | * http://www.apache.org/licenses/LICENSE-2.0 28 | * 29 | * Unless required by applicable law or agreed to in writing, software 30 | * distributed under the License is distributed on an "AS IS" BASIS, 31 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 32 | * implied. See the License for the specific language governing 33 | * permissions and limitations under the License. 34 | * 35 | * Redistribution and use in source and binary forms, with or without 36 | * modification, are permitted provided that the following conditions are 37 | * met: 38 | * 39 | * 1. Redistributions as source code must retain the above copyright 40 | * notice, this list of conditions and the following disclaimer. 41 | * 42 | * 2. The end-user documentation included with the redistribution, if any, 43 | * must include the following acknowledgment: "This product includes color 44 | * specifications and designs developed by Cynthia Brewer 45 | * (http://colorbrewer.org/)." Alternately, this acknowledgment may appear 46 | * in the software itself, if and wherever such third-party 47 | * acknowledgments normally appear. 48 | * 49 | * 4. The name "ColorBrewer" must not be used to endorse or promote 50 | * products derived from this software without prior written 51 | * permission. For written permission, please contact Cynthia Brewer at 52 | * cbrewer@psu.edu. 53 | * 54 | * 5. Products derived from this software may not be called "ColorBrewer", 55 | * nor may "ColorBrewer" appear in their name, without prior written 56 | * permission of Cynthia Brewer. 57 | * 58 | * Furthermore, Solarized colour schemes are covered by the following: 59 | * 60 | * Copyright (c) 2011 Ethan Schoonover 61 | * 62 | * Permission is hereby granted, free of charge, to any person obtaining 63 | * a copy of this software and associated documentation files (the 64 | * "Software"), to deal in the Software without restriction, including 65 | * without limitation the rights to use, copy, modify, merge, publish, 66 | * distribute, sublicense, and/or sell copies of the Software, and to 67 | * permit persons to whom the Software is furnished to do so, subject to 68 | * the following conditions: 69 | * 70 | * The above copyright notice and this permission notice shall be included 71 | * in all copies or substantial portions of the Software. 72 | * 73 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 74 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 75 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 76 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 77 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 78 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 79 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 80 | */ 81 | 82 | 'use strict'; 83 | 84 | var palette = (function() { 85 | 86 | var proto = Array.prototype; 87 | var slice = function(arr, opt_begin, opt_end) { 88 | return proto.slice.apply(arr, proto.slice.call(arguments, 1)); 89 | }; 90 | 91 | var extend = function(arr, arr2) { 92 | return proto.push.apply(arr, arr2); 93 | }; 94 | 95 | var function_type = typeof function() {}; 96 | 97 | var INF = 1000000000; // As far as we're concerned, that's infinity. ;) 98 | 99 | 100 | /** 101 | * Generate a colour palette from given scheme. 102 | * 103 | * If scheme argument is not a function it is passed to palettes.listSchemes 104 | * function (along with the number argument). This may result in an array 105 | * of more than one available scheme. If that is the case, scheme at 106 | * opt_index position is taken. 107 | * 108 | * This allows using different palettes for different data without having to 109 | * name the schemes specifically, for example: 110 | * 111 | * palette_for_foo = palette('sequential', 10, 0); 112 | * palette_for_bar = palette('sequential', 10, 1); 113 | * palette_for_baz = palette('sequential', 10, 2); 114 | * 115 | * @param {!palette.SchemeType|string|palette.Palette} scheme Scheme to 116 | * generate palette for. Either a function constructed with 117 | * palette.Scheme object, or anything that palette.listSchemes accepts 118 | * as name argument. 119 | * @param {number} number Number of colours to return. If negative, 120 | * absolute value is taken and colours will be returned in reverse 121 | * order. 122 | * @param {number=} opt_index If scheme is a name of a group or an array and 123 | * results in more than one scheme, index of the scheme to use. The 124 | * index wraps around. 125 | * @param {...*} varargs Additional arguments to pass to palette or colour 126 | * generator (if the chosen scheme uses those). 127 | * @return {Array.} Array of abs(number) 'RRGGBB' strings or null if 128 | * no matching scheme was found. 129 | */ 130 | var palette = function(scheme, number, opt_index, varargs) { 131 | number |= 0; 132 | if (number == 0) { 133 | return []; 134 | } 135 | 136 | if (typeof scheme !== function_type) { 137 | var arr = palette.listSchemes( 138 | /** @type {string|palette.Palette} */ (scheme), number); 139 | if (!arr.length) { 140 | return null; 141 | } 142 | scheme = arr[(opt_index || 0) % arr.length]; 143 | } 144 | 145 | var args = slice(arguments, 2); 146 | args[0] = number; 147 | return scheme.apply(scheme, args); 148 | }; 149 | 150 | 151 | /** 152 | * Returns a callable colour scheme object. 153 | * 154 | * Just after being created, the scheme has no colour palettes and no way of 155 | * generating any, thus generate method will return null. To turn scheme 156 | * into a useful object, addPalette, addPalettes or setColorFunction methods 157 | * need to be used. 158 | * 159 | * To generate a colour palette with given number colours using function 160 | * returned by this method, just call it with desired number of colours. 161 | * 162 | * Since this function *returns* a callable object, it must *not* be used 163 | * with the new operator. 164 | * 165 | * @param {string} name Name of the scheme. 166 | * @param {string|!Array.=} opt_groups A group name or list of 167 | * groups the scheme should be categorised under. Three typical groups 168 | * to use are 'qualitative', 'sequential' and 'diverging', but any 169 | * groups may be created. 170 | * @return {!palette.SchemeType} A colour palette generator function, which 171 | * in addition has methods and properties like a regular object. Think 172 | * of it as a callable object. 173 | */ 174 | palette.Scheme = function(name, opt_groups) { 175 | /** 176 | * A map from a number to a colour palettes with given number of colours. 177 | * @type {!Object.} 178 | */ 179 | var palettes = {}; 180 | 181 | /** 182 | * The biggest palette in palettes map. 183 | * @type {number} 184 | */ 185 | var palettes_max = 0; 186 | 187 | /** 188 | * The smallest palette in palettes map. 189 | * @type {number} 190 | */ 191 | var palettes_min = INF; 192 | 193 | var makeGenerator = function() { 194 | if (arguments.length <= 1) { 195 | return self.color_func.bind(self); 196 | } else { 197 | var args = slice(arguments); 198 | return function(x) { 199 | args[0] = x; 200 | return self.color_func.apply(self, args); 201 | }; 202 | } 203 | }; 204 | 205 | /** 206 | * Generate a colour palette from the scheme. 207 | * 208 | * If there was a palette added with addPalette (or addPalettes) with 209 | * enough colours, that palette will be used. Otherwise, if colour 210 | * function has been set using setColorFunction method, that function will 211 | * be used to generate the palette. Otherwise null is returned. 212 | * 213 | * @param {number} number Number of colours to return. If negative, 214 | * absolute value is taken and colours will be returned in reverse 215 | * order. 216 | * @param {...*} varargs Additional arguments to pass to palette or colour 217 | * generator (if the chosen scheme uses those). 218 | */ 219 | var self = function(number, varargs) { 220 | number |= 0; 221 | if (!number) { 222 | return []; 223 | } 224 | 225 | var _number = number; 226 | number = Math.abs(number); 227 | 228 | if (number <= palettes_max) { 229 | for (var i = Math.max(number, palettes_min); !(i in palettes); ++i) { 230 | /* nop */ 231 | } 232 | var colors = palettes[i]; 233 | if (i > number) { 234 | var take_head = 235 | 'shrinking_takes_head' in colors ? 236 | colors.shrinking_takes_head : self.shrinking_takes_head; 237 | if (take_head) { 238 | colors = colors.slice(0, number); 239 | i = number; 240 | } else { 241 | return palette.generate( 242 | function(x) { return colors[Math.round(x)]; }, 243 | _number, 0, colors.length - 1); 244 | } 245 | } 246 | colors = colors.slice(); 247 | if (_number < 0) { 248 | colors.reverse(); 249 | } 250 | return colors; 251 | 252 | } else if (self.color_func) { 253 | return palette.generate(makeGenerator.apply(self, arguments), 254 | _number, 0, 1, self.color_func_cyclic); 255 | 256 | } else { 257 | return null; 258 | } 259 | }; 260 | 261 | /** 262 | * The name of the palette. 263 | * @type {string} 264 | */ 265 | self.scheme_name = name; 266 | 267 | /** 268 | * A list of groups the palette belongs to. 269 | * @type {!Array.} 270 | */ 271 | self.groups = opt_groups ? 272 | typeof opt_groups === 'string' ? [opt_groups] : opt_groups : []; 273 | 274 | /** 275 | * The biggest palette this scheme can generate. 276 | * @type {number} 277 | */ 278 | self.max = 0; 279 | 280 | /** 281 | * The biggest palette this scheme can generate that is colour-blind 282 | * friendly. 283 | * @type {number} 284 | */ 285 | self.cbf_max = INF; 286 | 287 | 288 | /** 289 | * Adds a colour palette to the colour scheme. 290 | * 291 | * @param {palette.Palette} palette An array of 'RRGGBB' strings 292 | * representing the palette to add. 293 | * @param {boolean=} opt_is_cbf Whether the palette is colourblind friendly. 294 | */ 295 | self.addPalette = function(palette, opt_is_cbf) { 296 | var len = palette.length; 297 | if (len) { 298 | palettes[len] = palette; 299 | palettes_min = Math.min(palettes_min, len); 300 | palettes_max = Math.max(palettes_max, len); 301 | self.max = Math.max(self.max, len); 302 | if (!opt_is_cbf && len != 1) { 303 | self.cbf_max = Math.min(self.cbf_max, len - 1); 304 | } 305 | } 306 | }; 307 | 308 | /** 309 | * Adds number of colour palettes to the colour scheme. 310 | * 311 | * @param {palette.PalettesList} palettes A map or an array of colour 312 | * palettes to add. If map, i.e. object, is used, properties should 313 | * use integer property names. 314 | * @param {number=} opt_max Size of the biggest palette in palettes set. 315 | * If not set, palettes must have a length property which will be used. 316 | * @param {number=} opt_cbf_max Size of the biggest palette which is still 317 | * colourblind friendly. 1 by default. 318 | */ 319 | self.addPalettes = function(palettes, opt_max, opt_cbf_max) { 320 | opt_max = opt_max || palettes.length; 321 | for (var i = 0; i < opt_max; ++i) { 322 | if (i in palettes) { 323 | self.addPalette(palettes[i], true); 324 | } 325 | } 326 | self.cbf_max = Math.min(self.cbf_max, opt_cbf_max || 1); 327 | }; 328 | 329 | /** 330 | * Enable shrinking palettes taking head of the list of colours. 331 | * 332 | * When user requests n-colour palette but the smallest palette added with 333 | * addPalette (or addPalettes) is m-colour one (where n < m), n colours 334 | * across the palette will be returned. For example: 335 | * var ex = palette.Scheme('ex'); 336 | * ex.addPalette(['000000', 'bcbcbc', 'ffffff']); 337 | * var pal = ex(2); 338 | * // pal == ['000000', 'ffffff'] 339 | * 340 | * This works for palettes where the distance between colours is 341 | * correlated to distance in the palette array, which is true in gradients 342 | * such as the one above. 343 | * 344 | * To turn this feature off shrinkByTakingHead can be set to true either 345 | * for all palettes in the scheme (if opt_idx is not given) or for palette 346 | * with given number of colours only. In general, setting the option for 347 | * given palette overwrites whatever has been set for the scheme. The 348 | * default, as described above, is false. 349 | * 350 | * Alternatively, the feature can be enabled by setting 351 | * shrinking_takes_head property for the palette Array or the scheme 352 | * object. 353 | * 354 | * For example, all of the below give equivalent results: 355 | * var pal = ['ff0000', '00ff00', '0000ff']; 356 | * 357 | * var ex = palette.Scheme('ex'); 358 | * ex.addPalette(pal); // ex(2) == ['ff0000', '0000ff'] 359 | * ex.shrinkByTakingHead(true); // ex(2) == ['ff0000', '00ff00'] 360 | * 361 | * ex = palette.Scheme('ex'); 362 | * ex.addPalette(pal); // ex(2) == ['ff0000', '0000ff'] 363 | * ex.shrinkByTakingHead(true, 3); // ex(2) == ['ff0000', '00ff00'] 364 | * 365 | * ex = palette.Scheme('ex'); 366 | * ex.addPalette(pal); 367 | * ex.addPalette(pal); // ex(2) == ['ff0000', '0000ff'] 368 | * pal.shrinking_takes_head = true; // ex(2) == ['ff0000', '00ff00'] 369 | * 370 | * @param {boolean} enabled Whether to enable or disable the “shrinking 371 | * takes head” feature. It is disabled by default. 372 | * @param {number=} opt_idx If given, the “shrinking takes head” option 373 | * for palette with given number of colours is set. If such palette 374 | * does not exist, nothing happens. 375 | */ 376 | self.shrinkByTakingHead = function(enabled, opt_idx) { 377 | if (opt_idx !== void(0)) { 378 | if (opt_idx in palettes) { 379 | palettes[opt_idx].shrinking_takes_head = !!enabled; 380 | } 381 | } else { 382 | self.shrinking_takes_head = !!enabled; 383 | } 384 | }; 385 | 386 | /** 387 | * Sets a colour generation function of the colour scheme. 388 | * 389 | * The function must accept a singe number argument whose value can be 390 | * from 0.0 to 1.0, and return a colour as an 'RRGGBB' string. This 391 | * function will be used when generating palettes, i.e. if 11-colour 392 | * palette is requested, this function will be called with arguments 0.0, 393 | * 0.1, …, 1.0. 394 | * 395 | * If the palette generated by the function is colourblind friendly, 396 | * opt_is_cbf should be set to true. 397 | * 398 | * In some cases, it is not desirable to reach 1.0 when generating 399 | * a palette. This happens for hue-rainbows where the 0–1 range corresponds 400 | * to a 0°–360° range in hues, and since hue at 0° is the same as at 360°, 401 | * it's desired to stop short the end of the range when generating 402 | * a palette. To accomplish this, opt_cyclic should be set to true. 403 | * 404 | * @param {palette.ColorFunction} func A colour generator function. 405 | * @param {boolean=} opt_is_cbf Whether palette generate with the function 406 | * is colour-blind friendly. 407 | * @param {boolean=} opt_cyclic Whether colour at 0.0 is the same as the 408 | * one at 1.0. 409 | */ 410 | self.setColorFunction = function(func, opt_is_cbf, opt_cyclic) { 411 | self.color_func = func; 412 | self.color_func_cyclic = !!opt_cyclic; 413 | self.max = INF; 414 | if (!opt_is_cbf && self.cbf_max === INF) { 415 | self.cbf_max = 1; 416 | } 417 | }; 418 | 419 | self.color = function(x, varargs) { 420 | if (self.color_func) { 421 | return self.color_func.apply(this, arguments); 422 | } else { 423 | return null; 424 | } 425 | }; 426 | 427 | return self; 428 | }; 429 | 430 | 431 | /** 432 | * Creates a new palette.Scheme and initialises it by calling addPalettes 433 | * method with the rest of the arguments. 434 | * 435 | * @param {string} name Name of the scheme. 436 | * @param {string|!Array.} groups A group name or list of group 437 | * names the scheme belongs to. 438 | * @param {!Object.|!Array.} 439 | * palettes A map or an array of colour palettes to add. If map, i.e. 440 | * object, is used, properties should use integer property names. 441 | * @param {number=} opt_max Size of the biggest palette in palettes set. 442 | * If not set, palettes must have a length property which will be used. 443 | * @param {number=} opt_cbf_max Size of the biggest palette which is still 444 | * colourblind friendly. 1 by default. 445 | * @return {!palette.SchemeType} A colour palette generator function, which 446 | * in addition has methods and properties like a regular object. Think 447 | * of it as a callable object. 448 | */ 449 | palette.Scheme.fromPalettes = function(name, groups, 450 | palettes, opt_max, opt_cbf_max) { 451 | var scheme = palette.Scheme(name, groups); 452 | scheme.addPalettes.apply(scheme, slice(arguments, 2)); 453 | return scheme; 454 | }; 455 | 456 | 457 | /** 458 | * Creates a new palette.Scheme and initialises it by calling 459 | * setColorFunction method with the rest of the arguments. 460 | * 461 | * @param {string} name Name of the scheme. 462 | * @param {string|!Array.} groups A group name or list of group 463 | * names the scheme belongs to. 464 | * @param {palette.ColorFunction} func A colour generator function. 465 | * @param {boolean=} opt_is_cbf Whether palette generate with the function 466 | * is colour-blind friendly. 467 | * @param {boolean=} opt_cyclic Whether colour at 0.0 is the same as the 468 | * one at 1.0. 469 | * @return {!palette.SchemeType} A colour palette generator function, which 470 | * in addition has methods and properties like a regular object. Think 471 | * of it as a callable object. 472 | */ 473 | palette.Scheme.withColorFunction = function(name, groups, 474 | func, opt_is_cbf, opt_cyclic) { 475 | var scheme = palette.Scheme(name, groups); 476 | scheme.setColorFunction.apply(scheme, slice(arguments, 2)); 477 | return scheme; 478 | }; 479 | 480 | 481 | /** 482 | * A map of registered schemes. Maps a scheme or group name to a list of 483 | * scheme objects. Property name is either 'n-' for single scheme 484 | * names or 'g-' for scheme group names. 485 | * 486 | * @type {!Object.>} 487 | */ 488 | var registered_schemes = {}; 489 | 490 | 491 | /** 492 | * Registers a new colour scheme. 493 | * 494 | * @param {!palette.SchemeType} scheme The scheme to add. 495 | */ 496 | palette.register = function(scheme) { 497 | registered_schemes['n-' + scheme.scheme_name] = [scheme]; 498 | scheme.groups.forEach(function(g) { 499 | (registered_schemes['g-' + g] = 500 | registered_schemes['g-' + g] || []).push(scheme); 501 | }); 502 | (registered_schemes['g-all'] = 503 | registered_schemes['g-all'] || []).push(scheme); 504 | }; 505 | 506 | 507 | /** 508 | * List all schemes that match given name and number of colours. 509 | * 510 | * name argument can be either a string or an array of strings. In the 511 | * former case, the function acts as if the argument was an array with name 512 | * as a single argument (i.e. “palette.listSchemes('foo')” is exactly the same 513 | * as “palette.listSchemes(['foo'])”). 514 | * 515 | * Each name can be either name of a palette (e.g. 'tol-sq' for Paul Tol's 516 | * sequential palette), or a name of a group (e.g. 'sequential' for all 517 | * sequential palettes). Name can therefore map to a single scheme or 518 | * several schemes. 519 | * 520 | * Furthermore, name can be suffixed with '-cbf' to indicate that only 521 | * schemes that are colourblind friendly should be returned. For example, 522 | * 'rainbow' returns a HSV rainbow scheme, but because it is not colourblind 523 | * friendly, 'rainbow-cbf' returns no schemes. 524 | * 525 | * Some schemes may produce colourblind friendly palettes for some number of 526 | * colours. For example ColorBrewer's Dark2 scheme is colourblind friendly 527 | * if no more than 3 colours are generated. If opt_number is not specified, 528 | * 'qualitative-cbf' will include 'cb-Dark2' but if opt_number is given as, 529 | * say, 5 it won't. 530 | * 531 | * Name can also be 'all' which will return all registered schemes. 532 | * Naturally, 'all-cbf' will return all colourblind friendly schemes. 533 | * 534 | * Schemes are added to the library using palette.register. Schemes are 535 | * created using palette.Scheme function. By default, the following schemes 536 | * are available: 537 | * 538 | * Name Description 539 | * -------------- ----------------------------------------------------- 540 | * tol Paul Tol's qualitative scheme, cbf, max 12 colours. 541 | * tol-dv Paul Tol's diverging scheme, cbf. 542 | * tol-seq Paul Tol's sequential scheme, cbf. 543 | * tol-rainbow Paul Tol's qualitative scheme, cbf. 544 | * 545 | * rainbow A rainbow palette. 546 | * 547 | * cb-YlGn ColorBrewer sequential schemes. 548 | * cb-YlGnBu 549 | * cb-GnBu 550 | * cb-BuGn 551 | * cb-PuBuGn 552 | * cb-PuBu 553 | * cb-BuPu 554 | * cb-RdPu 555 | * cb-PuRd 556 | * cb-OrRd 557 | * cb-YlOrRd 558 | * cb-YlOrBr 559 | * cb-Purples 560 | * cb-Blues 561 | * cb-Greens 562 | * cb-Oranges 563 | * cb-Reds 564 | * cb-Greys 565 | * 566 | * cb-PuOr ColorBrewer diverging schemes. 567 | * cb-BrBG 568 | * cb-PRGn 569 | * cb-PiYG 570 | * cb-RdBu 571 | * cb-RdGy 572 | * cb-RdYlBu 573 | * cb-Spectral 574 | * cb-RdYlGn 575 | * 576 | * cb-Accent ColorBrewer qualitative schemes. 577 | * cb-Dark2 578 | * cb-Paired 579 | * cb-Pastel1 580 | * cb-Pastel2 581 | * cb-Set1 582 | * cb-Set2 583 | * cb-Set3 584 | * 585 | * sol-base Solarized base colours. 586 | * sol-accent Solarized accent colours. 587 | * 588 | * The following groups are also available by default: 589 | * 590 | * Name Description 591 | * -------------- ----------------------------------------------------- 592 | * all All registered schemes. 593 | * sequential All sequential schemes. 594 | * diverging All diverging schemes. 595 | * qualitative All qualitative schemes. 596 | * cb-sequential All ColorBrewer sequential schemes. 597 | * cb-diverging All ColorBrewer diverging schemes. 598 | * cb-qualitative All ColorBrewer qualitative schemes. 599 | * 600 | * You can read more about Paul Tol's palettes at http://www.sron.nl/~pault/. 601 | * You can read more about ColorBrewer at http://colorbrewer2.org. 602 | * 603 | * @param {string|!Array.} name A name of a colour scheme, of 604 | * a group of colour schemes, or an array of any of those. 605 | * @param {number=} opt_number When requesting only colourblind friendly 606 | * schemes, number of colours the scheme must provide generating such 607 | * that the palette is still colourblind friendly. 2 by default. 608 | * @return {!Array.} An array of colour scheme objects 609 | * matching the criteria. Sorted by scheme name. 610 | */ 611 | palette.listSchemes = function(name, opt_number) { 612 | if (!opt_number) { 613 | opt_number = 2; 614 | } else if (opt_number < 0) { 615 | opt_number = -opt_number; 616 | } 617 | 618 | var ret = []; 619 | (typeof name === 'string' ? [name] : name).forEach(function(n) { 620 | var cbf = n.substring(n.length - 4) === '-cbf'; 621 | if (cbf) { 622 | n = n.substring(0, n.length - 4); 623 | } 624 | var schemes = 625 | registered_schemes['g-' + n] || 626 | registered_schemes['n-' + n] || 627 | []; 628 | for (var i = 0, scheme; (scheme = schemes[i]); ++i) { 629 | if ((cbf ? scheme.cbf : scheme.max) >= opt_number) { 630 | ret.push(scheme); 631 | } 632 | } 633 | }); 634 | 635 | ret.sort(function(a, b) { 636 | return a.scheme_name >= b.scheme_name ? 637 | a.scheme_name > b.scheme_name ? 1 : 0 : -1; 638 | }); 639 | return ret; 640 | }; 641 | 642 | 643 | /** 644 | * Generates a palette using given colour generating function. 645 | * 646 | * The color_func callback must accept a singe number argument whose value 647 | * can vary from 0.0 to 1.0 (or in general from opt_start to opt_end), and 648 | * return a colour as an 'RRGGBB' string. This function will be used when 649 | * generating palettes, i.e. if 11-colour palette is requested, this 650 | * function will be called with arguments 0.0, 0.1, …, 1.0. 651 | * 652 | * In some cases, it is not desirable to reach 1.0 when generating 653 | * a palette. This happens for hue-rainbows where the 0–1 range corresponds 654 | * to a 0°–360° range in hues, and since hue at 0° is the same as at 360°, 655 | * it's desired to stop short the end of the range when generating 656 | * a palette. To accomplish this, opt_cyclic should be set to true. 657 | * 658 | * opt_start and opt_end may be used to change the range the colour 659 | * generation function is called with. opt_end may be less than opt_start 660 | * which will case to traverse the range in reverse. Another way to reverse 661 | * the palette is requesting negative number of colours. The two methods do 662 | * not always lead to the same results (especially if opt_cyclic is set). 663 | * 664 | * @param {palette.ColorFunction} color_func A colours generating callback 665 | * function. 666 | * @param {number} number Number of colours to generate in the palette. If 667 | * number is negative, colours in the palette will be reversed. If only 668 | * one colour is requested, colour at opt_start will be returned. 669 | * @param {number=} opt_start Optional starting point for the palette 670 | * generation function. Zero by default. 671 | * @param {number=} opt_end Optional ending point for the palette generation 672 | * function. One by default. 673 | * @param {boolean=} opt_cyclic If true, function will assume colour at 674 | * point opt_start is the same as one at opt_end. 675 | * @return {palette.Palette} An array of 'RRGGBB' colours. 676 | */ 677 | palette.generate = function(color_func, number, opt_start, opt_end, 678 | opt_cyclic) { 679 | if (Math.abs(number) < 1) { 680 | return []; 681 | } 682 | 683 | opt_start = opt_start === void(0) ? 0 : opt_start; 684 | opt_end = opt_end === void(0) ? 1 : opt_end; 685 | 686 | if (Math.abs(number) < 2) { 687 | return [color_func(opt_start)]; 688 | } 689 | 690 | var i = Math.abs(number); 691 | var x = opt_start; 692 | var ret = []; 693 | var step = (opt_end - opt_start) / (opt_cyclic ? i : (i - 1)); 694 | 695 | for (; --i >= 0; x += step) { 696 | ret.push(color_func(x)); 697 | } 698 | if (number < 0) { 699 | ret.reverse(); 700 | } 701 | return ret; 702 | }; 703 | 704 | 705 | /** 706 | * Clamps value to [0, 1] range. 707 | * @param {number} v Number to limit value of. 708 | * @return {number} If v is inside of [0, 1] range returns v, otherwise 709 | * returns 0 or 1 depending which side of the range v is closer to. 710 | */ 711 | var clamp = function(v) { 712 | return v > 0 ? (v < 1 ? v : 1) : 0; 713 | }; 714 | 715 | /** 716 | * Converts r, g, b triple into RRGGBB hex representation. 717 | * @param {number} r Red value of the colour in the range [0, 1]. 718 | * @param {number} g Green value of the colour in the range [0, 1]. 719 | * @param {number} b Blue value of the colour in the range [0, 1]. 720 | * @return {string} A lower-case RRGGBB representation of the colour. 721 | */ 722 | palette.rgbColor = function(r, g, b) { 723 | return [r, g, b].map(function(v) { 724 | v = Number(Math.round(clamp(v) * 255)).toString(16); 725 | return v.length == 1 ? '0' + v : v; 726 | }).join(''); 727 | }; 728 | 729 | /** 730 | * Converts a linear r, g, b triple into RRGGBB hex representation. 731 | * @param {number} r Linear red value of the colour in the range [0, 1]. 732 | * @param {number} g Linear green value of the colour in the range [0, 1]. 733 | * @param {number} b Linear blue value of the colour in the range [0, 1]. 734 | * @return {string} A lower-case RRGGBB representation of the colour. 735 | */ 736 | palette.linearRgbColor = function(r, g, b) { 737 | // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html 738 | return [r, g, b].map(function(v) { 739 | v = clamp(v); 740 | if (v <= 0.0031308) { 741 | v = 12.92 * v; 742 | } else { 743 | v = 1.055 * Math.pow(v, 1 / 2.4) - 0.055; 744 | } 745 | v = Number(Math.round(v * 255)).toString(16); 746 | return v.length == 1 ? '0' + v : v; 747 | }).join(''); 748 | }; 749 | 750 | /** 751 | * Converts an HSV colours to RRGGBB hex representation. 752 | * @param {number} h Hue in the range [0, 1]. 753 | * @param {number=} opt_s Saturation in the range [0, 1]. One by default. 754 | * @param {number=} opt_v Value in the range [0, 1]. One by default. 755 | * @return {string} An RRGGBB representation of the colour. 756 | */ 757 | palette.hsvColor = function(h, opt_s, opt_v) { 758 | h *= 6; 759 | var s = opt_s === void(0) ? 1 : clamp(opt_s); 760 | var v = opt_v === void(0) ? 1 : clamp(opt_v); 761 | var x = v * (1 - s * Math.abs(h % 2 - 1)); 762 | var m = v * (1 - s); 763 | switch (Math.floor(h) % 6) { 764 | case 0: return palette.rgbColor(v, x, m); 765 | case 1: return palette.rgbColor(x, v, m); 766 | case 2: return palette.rgbColor(m, v, x); 767 | case 3: return palette.rgbColor(m, x, v); 768 | case 4: return palette.rgbColor(x, m, v); 769 | default: return palette.rgbColor(v, m, x); 770 | } 771 | }; 772 | 773 | palette.register(palette.Scheme.withColorFunction( 774 | 'rainbow', 'qualitative', palette.hsvColor, false, true)); 775 | 776 | return palette; 777 | })(); 778 | 779 | 780 | /** @typedef {function(number): string} */ 781 | palette.ColorFunction; 782 | 783 | /** @typedef {!Array.} */ 784 | palette.Palette; 785 | 786 | /** @typedef {!Object.|!Array.} */ 787 | palette.PalettesList; 788 | 789 | /** 790 | * @typedef { 791 | * function(number, ...[?]): Array.| 792 | * { 793 | * scheme_name: string, 794 | * groups: !Array., 795 | * max: number, 796 | * cbf_max: number, 797 | * addPalette: function(!Array., boolean=), 798 | * addPalettes: function(palette.PalettesList, number=, number=), 799 | * shrinkByTakingHead: function(boolean, number=), 800 | * setColorFunction: function(palette.ColorFunction, boolean=, boolean=), 801 | * color: function(number, ...[?]): ?string}} 802 | */ 803 | palette.SchemeType; 804 | 805 | 806 | /* Paul Tol's schemes start here. *******************************************/ 807 | /* See http://www.sron.nl/~pault/ */ 808 | 809 | (function() { 810 | var rgb = palette.rgbColor; 811 | 812 | /** 813 | * Calculates value of a polynomial at given point. 814 | * For example, poly(x, 1, 2, 3) calculates value of “1 + 2*x + 2*X^2”. 815 | * @param {number} x Value to calculate polynomial for. 816 | * @param {...number} varargs Coefficients of the polynomial specified in 817 | * the order of rising powers of x including constant as the first 818 | * variable argument. 819 | */ 820 | var poly = function(x, varargs) { 821 | var i = arguments.length - 1, n = arguments[i]; 822 | while (i > 1) { 823 | n = n * x + arguments[--i]; 824 | } 825 | return n; 826 | }; 827 | 828 | /** 829 | * Calculate approximate value of error function with maximum error of 0.0005. 830 | * See . 831 | * @param {number} x Argument of the error function. 832 | * @return {number} Value of error function for x. 833 | */ 834 | var erf = function(x) { 835 | // https://en.wikipedia.org/wiki/Error_function#Approximation_with_elementary_functions 836 | // This produces a maximum error of 0.0005 which is more then we need. In 837 | // the worst case, that error is multiplied by four and then divided by 838 | // two before being multiplied by 255, so in the end, the error is 839 | // multiplied by 510 which produces 0.255 which is less than a single 840 | // colour step. 841 | var y = poly(Math.abs(x), 1, 0.278393, 0.230389, 0.000972, 0.078108); 842 | y *= y; // y^2 843 | y *= y; // y^4 844 | y = 1 - 1 / y; 845 | return x < 0 ? -y : y; 846 | }; 847 | 848 | palette.register(palette.Scheme.fromPalettes('tol', 'qualitative', [ 849 | ['4477aa'], 850 | ['4477aa', 'cc6677'], 851 | ['4477aa', 'ddcc77', 'cc6677'], 852 | ['4477aa', '117733', 'ddcc77', 'cc6677'], 853 | ['332288', '88ccee', '117733', 'ddcc77', 'cc6677'], 854 | ['332288', '88ccee', '117733', 'ddcc77', 'cc6677', 'aa4499'], 855 | ['332288', '88ccee', '44aa99', '117733', 'ddcc77', 'cc6677', 'aa4499'], 856 | ['332288', '88ccee', '44aa99', '117733', '999933', 'ddcc77', 'cc6677', 857 | 'aa4499'], 858 | ['332288', '88ccee', '44aa99', '117733', '999933', 'ddcc77', 'cc6677', 859 | '882255', 'aa4499'], 860 | ['332288', '88ccee', '44aa99', '117733', '999933', 'ddcc77', '661100', 861 | 'cc6677', '882255', 'aa4499'], 862 | ['332288', '6699cc', '88ccee', '44aa99', '117733', '999933', 'ddcc77', 863 | '661100', 'cc6677', '882255', 'aa4499'], 864 | ['332288', '6699cc', '88ccee', '44aa99', '117733', '999933', 'ddcc77', 865 | '661100', 'cc6677', 'aa4466', '882255', 'aa4499'] 866 | ], 12, 12)); 867 | 868 | /** 869 | * Calculates a colour along Paul Tol's sequential colours axis. 870 | * See figure 7 and equation 1. 871 | * @param {number} x Position of the colour on the axis in the [0, 1] range. 872 | * @return {string} An RRGGBB representation of the colour. 873 | */ 874 | palette.tolSequentialColor = function(x) { 875 | return rgb(1 - 0.392 * (1 + erf((x - 0.869) / 0.255)), 876 | 1.021 - 0.456 * (1 + erf((x - 0.527) / 0.376)), 877 | 1 - 0.493 * (1 + erf((x - 0.272) / 0.309))); 878 | }; 879 | 880 | palette.register(palette.Scheme.withColorFunction( 881 | 'tol-sq', 'sequential', palette.tolSequentialColor, true)); 882 | 883 | /** 884 | * Calculates a colour along Paul Tol's diverging colours axis. 885 | * See figure 8 and equation 2. 886 | * @param {number} x Position of the colour on the axis in the [0, 1] range. 887 | * @return {string} An RRGGBB representation of the colour. 888 | */ 889 | palette.tolDivergingColor = function(x) { 890 | var g = poly(x, 0.572, 1.524, -1.811) / poly(x, 1, -0.291, 0.1574); 891 | return rgb(poly(x, 0.235, -2.13, 26.92, -65.5, 63.5, -22.36), 892 | g * g, 893 | 1 / poly(x, 1.579, -4.03, 12.92, -31.4, 48.6, -23.36)); 894 | }; 895 | 896 | palette.register(palette.Scheme.withColorFunction( 897 | 'tol-dv', 'diverging', palette.tolDivergingColor, true)); 898 | 899 | /** 900 | * Calculates a colour along Paul Tol's rainbow colours axis. 901 | * See figure 13 and equation 3. 902 | * @param {number} x Position of the colour on the axis in the [0, 1] range. 903 | * @return {string} An RRGGBB representation of the colour. 904 | */ 905 | palette.tolRainbowColor = function(x) { 906 | return rgb(poly(x, 0.472, -0.567, 4.05) / poly(x, 1, 8.72, -19.17, 14.1), 907 | poly(x, 0.108932, -1.22635, 27.284, -98.577, 163.3, -131.395, 908 | 40.634), 909 | 1 / poly(x, 1.97, 3.54, -68.5, 243, -297, 125)); 910 | }; 911 | 912 | palette.register(palette.Scheme.withColorFunction( 913 | 'tol-rainbow', 'qualitative', palette.tolRainbowColor, true)); 914 | })(); 915 | 916 | 917 | /* Solarized colour schemes start here. *************************************/ 918 | /* See http://ethanschoonover.com/solarized */ 919 | 920 | (function() { 921 | /* 922 | * Those are not really designed to be used in graphs, but we're keeping 923 | * them here in case someone cares. 924 | */ 925 | palette.register(palette.Scheme.fromPalettes('sol-base', 'sequential', [ 926 | ['002b36', '073642', '586e75', '657b83', '839496', '93a1a1', 'eee8d5', 927 | 'fdf6e3'] 928 | ], 1, 8)); 929 | palette.register(palette.Scheme.fromPalettes('sol-accent', 'qualitative', [ 930 | ['b58900', 'cb4b16', 'dc322f', 'd33682', '6c71c4', '268bd2', '2aa198', 931 | '859900'] 932 | ])); 933 | })(); 934 | 935 | 936 | /* ColorBrewer colour schemes start here. ***********************************/ 937 | /* See http://colorbrewer2.org/ */ 938 | 939 | (function() { 940 | var schemes = { 941 | YlGn: { 942 | type: 'sequential', 943 | cbf: 42, 944 | 3: ['f7fcb9', 'addd8e', '31a354'], 945 | 4: ['ffffcc', 'c2e699', '78c679', '238443'], 946 | 5: ['ffffcc', 'c2e699', '78c679', '31a354', '006837'], 947 | 6: ['ffffcc', 'd9f0a3', 'addd8e', '78c679', '31a354', '006837'], 948 | 7: ['ffffcc', 'd9f0a3', 'addd8e', '78c679', '41ab5d', '238443', 949 | '005a32'], 950 | 8: ['ffffe5', 'f7fcb9', 'd9f0a3', 'addd8e', '78c679', '41ab5d', 951 | '238443', '005a32'], 952 | 9: ['ffffe5', 'f7fcb9', 'd9f0a3', 'addd8e', '78c679', '41ab5d', 953 | '238443', '006837', '004529'] 954 | }, 955 | YlGnBu: { 956 | type: 'sequential', 957 | cbf: 42, 958 | 3: ['edf8b1', '7fcdbb', '2c7fb8'], 959 | 4: ['ffffcc', 'a1dab4', '41b6c4', '225ea8'], 960 | 5: ['ffffcc', 'a1dab4', '41b6c4', '2c7fb8', '253494'], 961 | 6: ['ffffcc', 'c7e9b4', '7fcdbb', '41b6c4', '2c7fb8', '253494'], 962 | 7: ['ffffcc', 'c7e9b4', '7fcdbb', '41b6c4', '1d91c0', '225ea8', 963 | '0c2c84'], 964 | 8: ['ffffd9', 'edf8b1', 'c7e9b4', '7fcdbb', '41b6c4', '1d91c0', 965 | '225ea8', '0c2c84'], 966 | 9: ['ffffd9', 'edf8b1', 'c7e9b4', '7fcdbb', '41b6c4', '1d91c0', 967 | '225ea8', '253494', '081d58'] 968 | }, 969 | GnBu: { 970 | type: 'sequential', 971 | cbf: 42, 972 | 3: ['e0f3db', 'a8ddb5', '43a2ca'], 973 | 4: ['f0f9e8', 'bae4bc', '7bccc4', '2b8cbe'], 974 | 5: ['f0f9e8', 'bae4bc', '7bccc4', '43a2ca', '0868ac'], 975 | 6: ['f0f9e8', 'ccebc5', 'a8ddb5', '7bccc4', '43a2ca', '0868ac'], 976 | 7: ['f0f9e8', 'ccebc5', 'a8ddb5', '7bccc4', '4eb3d3', '2b8cbe', 977 | '08589e'], 978 | 8: ['f7fcf0', 'e0f3db', 'ccebc5', 'a8ddb5', '7bccc4', '4eb3d3', 979 | '2b8cbe', '08589e'], 980 | 9: ['f7fcf0', 'e0f3db', 'ccebc5', 'a8ddb5', '7bccc4', '4eb3d3', 981 | '2b8cbe', '0868ac', '084081'] 982 | }, 983 | BuGn: { 984 | type: 'sequential', 985 | cbf: 42, 986 | 3: ['e5f5f9', '99d8c9', '2ca25f'], 987 | 4: ['edf8fb', 'b2e2e2', '66c2a4', '238b45'], 988 | 5: ['edf8fb', 'b2e2e2', '66c2a4', '2ca25f', '006d2c'], 989 | 6: ['edf8fb', 'ccece6', '99d8c9', '66c2a4', '2ca25f', '006d2c'], 990 | 7: ['edf8fb', 'ccece6', '99d8c9', '66c2a4', '41ae76', '238b45', 991 | '005824'], 992 | 8: ['f7fcfd', 'e5f5f9', 'ccece6', '99d8c9', '66c2a4', '41ae76', 993 | '238b45', '005824'], 994 | 9: ['f7fcfd', 'e5f5f9', 'ccece6', '99d8c9', '66c2a4', '41ae76', 995 | '238b45', '006d2c', '00441b'] 996 | }, 997 | PuBuGn: { 998 | type: 'sequential', 999 | cbf: 42, 1000 | 3: ['ece2f0', 'a6bddb', '1c9099'], 1001 | 4: ['f6eff7', 'bdc9e1', '67a9cf', '02818a'], 1002 | 5: ['f6eff7', 'bdc9e1', '67a9cf', '1c9099', '016c59'], 1003 | 6: ['f6eff7', 'd0d1e6', 'a6bddb', '67a9cf', '1c9099', '016c59'], 1004 | 7: ['f6eff7', 'd0d1e6', 'a6bddb', '67a9cf', '3690c0', '02818a', 1005 | '016450'], 1006 | 8: ['fff7fb', 'ece2f0', 'd0d1e6', 'a6bddb', '67a9cf', '3690c0', 1007 | '02818a', '016450'], 1008 | 9: ['fff7fb', 'ece2f0', 'd0d1e6', 'a6bddb', '67a9cf', '3690c0', 1009 | '02818a', '016c59', '014636'] 1010 | }, 1011 | PuBu: { 1012 | type: 'sequential', 1013 | cbf: 42, 1014 | 3: ['ece7f2', 'a6bddb', '2b8cbe'], 1015 | 4: ['f1eef6', 'bdc9e1', '74a9cf', '0570b0'], 1016 | 5: ['f1eef6', 'bdc9e1', '74a9cf', '2b8cbe', '045a8d'], 1017 | 6: ['f1eef6', 'd0d1e6', 'a6bddb', '74a9cf', '2b8cbe', '045a8d'], 1018 | 7: ['f1eef6', 'd0d1e6', 'a6bddb', '74a9cf', '3690c0', '0570b0', 1019 | '034e7b'], 1020 | 8: ['fff7fb', 'ece7f2', 'd0d1e6', 'a6bddb', '74a9cf', '3690c0', 1021 | '0570b0', '034e7b'], 1022 | 9: ['fff7fb', 'ece7f2', 'd0d1e6', 'a6bddb', '74a9cf', '3690c0', 1023 | '0570b0', '045a8d', '023858'] 1024 | }, 1025 | BuPu: { 1026 | type: 'sequential', 1027 | cbf: 42, 1028 | 3: ['e0ecf4', '9ebcda', '8856a7'], 1029 | 4: ['edf8fb', 'b3cde3', '8c96c6', '88419d'], 1030 | 5: ['edf8fb', 'b3cde3', '8c96c6', '8856a7', '810f7c'], 1031 | 6: ['edf8fb', 'bfd3e6', '9ebcda', '8c96c6', '8856a7', '810f7c'], 1032 | 7: ['edf8fb', 'bfd3e6', '9ebcda', '8c96c6', '8c6bb1', '88419d', 1033 | '6e016b'], 1034 | 8: ['f7fcfd', 'e0ecf4', 'bfd3e6', '9ebcda', '8c96c6', '8c6bb1', 1035 | '88419d', '6e016b'], 1036 | 9: ['f7fcfd', 'e0ecf4', 'bfd3e6', '9ebcda', '8c96c6', '8c6bb1', 1037 | '88419d', '810f7c', '4d004b'] 1038 | }, 1039 | RdPu: { 1040 | type: 'sequential', 1041 | cbf: 42, 1042 | 3: ['fde0dd', 'fa9fb5', 'c51b8a'], 1043 | 4: ['feebe2', 'fbb4b9', 'f768a1', 'ae017e'], 1044 | 5: ['feebe2', 'fbb4b9', 'f768a1', 'c51b8a', '7a0177'], 1045 | 6: ['feebe2', 'fcc5c0', 'fa9fb5', 'f768a1', 'c51b8a', '7a0177'], 1046 | 7: ['feebe2', 'fcc5c0', 'fa9fb5', 'f768a1', 'dd3497', 'ae017e', 1047 | '7a0177'], 1048 | 8: ['fff7f3', 'fde0dd', 'fcc5c0', 'fa9fb5', 'f768a1', 'dd3497', 1049 | 'ae017e', '7a0177'], 1050 | 9: ['fff7f3', 'fde0dd', 'fcc5c0', 'fa9fb5', 'f768a1', 'dd3497', 1051 | 'ae017e', '7a0177', '49006a'] 1052 | }, 1053 | PuRd: { 1054 | type: 'sequential', 1055 | cbf: 42, 1056 | 3: ['e7e1ef', 'c994c7', 'dd1c77'], 1057 | 4: ['f1eef6', 'd7b5d8', 'df65b0', 'ce1256'], 1058 | 5: ['f1eef6', 'd7b5d8', 'df65b0', 'dd1c77', '980043'], 1059 | 6: ['f1eef6', 'd4b9da', 'c994c7', 'df65b0', 'dd1c77', '980043'], 1060 | 7: ['f1eef6', 'd4b9da', 'c994c7', 'df65b0', 'e7298a', 'ce1256', 1061 | '91003f'], 1062 | 8: ['f7f4f9', 'e7e1ef', 'd4b9da', 'c994c7', 'df65b0', 'e7298a', 1063 | 'ce1256', '91003f'], 1064 | 9: ['f7f4f9', 'e7e1ef', 'd4b9da', 'c994c7', 'df65b0', 'e7298a', 1065 | 'ce1256', '980043', '67001f'] 1066 | }, 1067 | OrRd: { 1068 | type: 'sequential', 1069 | cbf: 42, 1070 | 3: ['fee8c8', 'fdbb84', 'e34a33'], 1071 | 4: ['fef0d9', 'fdcc8a', 'fc8d59', 'd7301f'], 1072 | 5: ['fef0d9', 'fdcc8a', 'fc8d59', 'e34a33', 'b30000'], 1073 | 6: ['fef0d9', 'fdd49e', 'fdbb84', 'fc8d59', 'e34a33', 'b30000'], 1074 | 7: ['fef0d9', 'fdd49e', 'fdbb84', 'fc8d59', 'ef6548', 'd7301f', 1075 | '990000'], 1076 | 8: ['fff7ec', 'fee8c8', 'fdd49e', 'fdbb84', 'fc8d59', 'ef6548', 1077 | 'd7301f', '990000'], 1078 | 9: ['fff7ec', 'fee8c8', 'fdd49e', 'fdbb84', 'fc8d59', 'ef6548', 1079 | 'd7301f', 'b30000', '7f0000'] 1080 | }, 1081 | YlOrRd: { 1082 | type: 'sequential', 1083 | cbf: 42, 1084 | 3: ['ffeda0', 'feb24c', 'f03b20'], 1085 | 4: ['ffffb2', 'fecc5c', 'fd8d3c', 'e31a1c'], 1086 | 5: ['ffffb2', 'fecc5c', 'fd8d3c', 'f03b20', 'bd0026'], 1087 | 6: ['ffffb2', 'fed976', 'feb24c', 'fd8d3c', 'f03b20', 'bd0026'], 1088 | 7: ['ffffb2', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', 'e31a1c', 1089 | 'b10026'], 1090 | 8: ['ffffcc', 'ffeda0', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', 1091 | 'e31a1c', 'b10026'], 1092 | 9: ['ffffcc', 'ffeda0', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', 1093 | 'e31a1c', 'bd0026', '800026'] 1094 | }, 1095 | YlOrBr: { 1096 | type: 'sequential', 1097 | cbf: 42, 1098 | 3: ['fff7bc', 'fec44f', 'd95f0e'], 1099 | 4: ['ffffd4', 'fed98e', 'fe9929', 'cc4c02'], 1100 | 5: ['ffffd4', 'fed98e', 'fe9929', 'd95f0e', '993404'], 1101 | 6: ['ffffd4', 'fee391', 'fec44f', 'fe9929', 'd95f0e', '993404'], 1102 | 7: ['ffffd4', 'fee391', 'fec44f', 'fe9929', 'ec7014', 'cc4c02', 1103 | '8c2d04'], 1104 | 8: ['ffffe5', 'fff7bc', 'fee391', 'fec44f', 'fe9929', 'ec7014', 1105 | 'cc4c02', '8c2d04'], 1106 | 9: ['ffffe5', 'fff7bc', 'fee391', 'fec44f', 'fe9929', 'ec7014', 1107 | 'cc4c02', '993404', '662506'] 1108 | }, 1109 | Purples: { 1110 | type: 'sequential', 1111 | cbf: 42, 1112 | 3: ['efedf5', 'bcbddc', '756bb1'], 1113 | 4: ['f2f0f7', 'cbc9e2', '9e9ac8', '6a51a3'], 1114 | 5: ['f2f0f7', 'cbc9e2', '9e9ac8', '756bb1', '54278f'], 1115 | 6: ['f2f0f7', 'dadaeb', 'bcbddc', '9e9ac8', '756bb1', '54278f'], 1116 | 7: ['f2f0f7', 'dadaeb', 'bcbddc', '9e9ac8', '807dba', '6a51a3', 1117 | '4a1486'], 1118 | 8: ['fcfbfd', 'efedf5', 'dadaeb', 'bcbddc', '9e9ac8', '807dba', 1119 | '6a51a3', '4a1486'], 1120 | 9: ['fcfbfd', 'efedf5', 'dadaeb', 'bcbddc', '9e9ac8', '807dba', 1121 | '6a51a3', '54278f', '3f007d'] 1122 | }, 1123 | Blues: { 1124 | type: 'sequential', 1125 | cbf: 42, 1126 | 3: ['deebf7', '9ecae1', '3182bd'], 1127 | 4: ['eff3ff', 'bdd7e7', '6baed6', '2171b5'], 1128 | 5: ['eff3ff', 'bdd7e7', '6baed6', '3182bd', '08519c'], 1129 | 6: ['eff3ff', 'c6dbef', '9ecae1', '6baed6', '3182bd', '08519c'], 1130 | 7: ['eff3ff', 'c6dbef', '9ecae1', '6baed6', '4292c6', '2171b5', 1131 | '084594'], 1132 | 8: ['f7fbff', 'deebf7', 'c6dbef', '9ecae1', '6baed6', '4292c6', 1133 | '2171b5', '084594'], 1134 | 9: ['f7fbff', 'deebf7', 'c6dbef', '9ecae1', '6baed6', '4292c6', 1135 | '2171b5', '08519c', '08306b'] 1136 | }, 1137 | Greens: { 1138 | type: 'sequential', 1139 | cbf: 42, 1140 | 3: ['e5f5e0', 'a1d99b', '31a354'], 1141 | 4: ['edf8e9', 'bae4b3', '74c476', '238b45'], 1142 | 5: ['edf8e9', 'bae4b3', '74c476', '31a354', '006d2c'], 1143 | 6: ['edf8e9', 'c7e9c0', 'a1d99b', '74c476', '31a354', '006d2c'], 1144 | 7: ['edf8e9', 'c7e9c0', 'a1d99b', '74c476', '41ab5d', '238b45', 1145 | '005a32'], 1146 | 8: ['f7fcf5', 'e5f5e0', 'c7e9c0', 'a1d99b', '74c476', '41ab5d', 1147 | '238b45', '005a32'], 1148 | 9: ['f7fcf5', 'e5f5e0', 'c7e9c0', 'a1d99b', '74c476', '41ab5d', 1149 | '238b45', '006d2c', '00441b'] 1150 | }, 1151 | Oranges: { 1152 | type: 'sequential', 1153 | cbf: 42, 1154 | 3: ['fee6ce', 'fdae6b', 'e6550d'], 1155 | 4: ['feedde', 'fdbe85', 'fd8d3c', 'd94701'], 1156 | 5: ['feedde', 'fdbe85', 'fd8d3c', 'e6550d', 'a63603'], 1157 | 6: ['feedde', 'fdd0a2', 'fdae6b', 'fd8d3c', 'e6550d', 'a63603'], 1158 | 7: ['feedde', 'fdd0a2', 'fdae6b', 'fd8d3c', 'f16913', 'd94801', 1159 | '8c2d04'], 1160 | 8: ['fff5eb', 'fee6ce', 'fdd0a2', 'fdae6b', 'fd8d3c', 'f16913', 1161 | 'd94801', '8c2d04'], 1162 | 9: ['fff5eb', 'fee6ce', 'fdd0a2', 'fdae6b', 'fd8d3c', 'f16913', 1163 | 'd94801', 'a63603', '7f2704'] 1164 | }, 1165 | Reds: { 1166 | type: 'sequential', 1167 | cbf: 42, 1168 | 3: ['fee0d2', 'fc9272', 'de2d26'], 1169 | 4: ['fee5d9', 'fcae91', 'fb6a4a', 'cb181d'], 1170 | 5: ['fee5d9', 'fcae91', 'fb6a4a', 'de2d26', 'a50f15'], 1171 | 6: ['fee5d9', 'fcbba1', 'fc9272', 'fb6a4a', 'de2d26', 'a50f15'], 1172 | 7: ['fee5d9', 'fcbba1', 'fc9272', 'fb6a4a', 'ef3b2c', 'cb181d', 1173 | '99000d'], 1174 | 8: ['fff5f0', 'fee0d2', 'fcbba1', 'fc9272', 'fb6a4a', 'ef3b2c', 1175 | 'cb181d', '99000d'], 1176 | 9: ['fff5f0', 'fee0d2', 'fcbba1', 'fc9272', 'fb6a4a', 'ef3b2c', 1177 | 'cb181d', 'a50f15', '67000d'] 1178 | }, 1179 | Greys: { 1180 | type: 'sequential', 1181 | cbf: 42, 1182 | 3: ['f0f0f0', 'bdbdbd', '636363'], 1183 | 4: ['f7f7f7', 'cccccc', '969696', '525252'], 1184 | 5: ['f7f7f7', 'cccccc', '969696', '636363', '252525'], 1185 | 6: ['f7f7f7', 'd9d9d9', 'bdbdbd', '969696', '636363', '252525'], 1186 | 7: ['f7f7f7', 'd9d9d9', 'bdbdbd', '969696', '737373', '525252', 1187 | '252525'], 1188 | 8: ['ffffff', 'f0f0f0', 'd9d9d9', 'bdbdbd', '969696', '737373', 1189 | '525252', '252525'], 1190 | 9: ['ffffff', 'f0f0f0', 'd9d9d9', 'bdbdbd', '969696', '737373', 1191 | '525252', '252525', '000000'] 1192 | }, 1193 | PuOr: { 1194 | type: 'diverging', 1195 | cbf: 42, 1196 | 3: ['f1a340', 'f7f7f7', '998ec3'], 1197 | 4: ['e66101', 'fdb863', 'b2abd2', '5e3c99'], 1198 | 5: ['e66101', 'fdb863', 'f7f7f7', 'b2abd2', '5e3c99'], 1199 | 6: ['b35806', 'f1a340', 'fee0b6', 'd8daeb', '998ec3', '542788'], 1200 | 7: ['b35806', 'f1a340', 'fee0b6', 'f7f7f7', 'd8daeb', '998ec3', 1201 | '542788'], 1202 | 8: ['b35806', 'e08214', 'fdb863', 'fee0b6', 'd8daeb', 'b2abd2', 1203 | '8073ac', '542788'], 1204 | 9: ['b35806', 'e08214', 'fdb863', 'fee0b6', 'f7f7f7', 'd8daeb', 1205 | 'b2abd2', '8073ac', '542788'], 1206 | 10: ['7f3b08', 'b35806', 'e08214', 'fdb863', 'fee0b6', 'd8daeb', 1207 | 'b2abd2', '8073ac', '542788', '2d004b'], 1208 | 11: ['7f3b08', 'b35806', 'e08214', 'fdb863', 'fee0b6', 'f7f7f7', 1209 | 'd8daeb', 'b2abd2', '8073ac', '542788', '2d004b'] 1210 | }, 1211 | BrBG: { 1212 | type: 'diverging', 1213 | cbf: 42, 1214 | 3: ['d8b365', 'f5f5f5', '5ab4ac'], 1215 | 4: ['a6611a', 'dfc27d', '80cdc1', '018571'], 1216 | 5: ['a6611a', 'dfc27d', 'f5f5f5', '80cdc1', '018571'], 1217 | 6: ['8c510a', 'd8b365', 'f6e8c3', 'c7eae5', '5ab4ac', '01665e'], 1218 | 7: ['8c510a', 'd8b365', 'f6e8c3', 'f5f5f5', 'c7eae5', '5ab4ac', 1219 | '01665e'], 1220 | 8: ['8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'c7eae5', '80cdc1', 1221 | '35978f', '01665e'], 1222 | 9: ['8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'f5f5f5', 'c7eae5', 1223 | '80cdc1', '35978f', '01665e'], 1224 | 10: ['543005', '8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'c7eae5', 1225 | '80cdc1', '35978f', '01665e', '003c30'], 1226 | 11: ['543005', '8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'f5f5f5', 1227 | 'c7eae5', '80cdc1', '35978f', '01665e', '003c30'] 1228 | }, 1229 | PRGn: { 1230 | type: 'diverging', 1231 | cbf: 42, 1232 | 3: ['af8dc3', 'f7f7f7', '7fbf7b'], 1233 | 4: ['7b3294', 'c2a5cf', 'a6dba0', '008837'], 1234 | 5: ['7b3294', 'c2a5cf', 'f7f7f7', 'a6dba0', '008837'], 1235 | 6: ['762a83', 'af8dc3', 'e7d4e8', 'd9f0d3', '7fbf7b', '1b7837'], 1236 | 7: ['762a83', 'af8dc3', 'e7d4e8', 'f7f7f7', 'd9f0d3', '7fbf7b', 1237 | '1b7837'], 1238 | 8: ['762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'd9f0d3', 'a6dba0', 1239 | '5aae61', '1b7837'], 1240 | 9: ['762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'f7f7f7', 'd9f0d3', 1241 | 'a6dba0', '5aae61', '1b7837'], 1242 | 10: ['40004b', '762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'd9f0d3', 1243 | 'a6dba0', '5aae61', '1b7837', '00441b'], 1244 | 11: ['40004b', '762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'f7f7f7', 1245 | 'd9f0d3', 'a6dba0', '5aae61', '1b7837', '00441b'] 1246 | }, 1247 | PiYG: { 1248 | type: 'diverging', 1249 | cbf: 42, 1250 | 3: ['e9a3c9', 'f7f7f7', 'a1d76a'], 1251 | 4: ['d01c8b', 'f1b6da', 'b8e186', '4dac26'], 1252 | 5: ['d01c8b', 'f1b6da', 'f7f7f7', 'b8e186', '4dac26'], 1253 | 6: ['c51b7d', 'e9a3c9', 'fde0ef', 'e6f5d0', 'a1d76a', '4d9221'], 1254 | 7: ['c51b7d', 'e9a3c9', 'fde0ef', 'f7f7f7', 'e6f5d0', 'a1d76a', 1255 | '4d9221'], 1256 | 8: ['c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'e6f5d0', 'b8e186', 1257 | '7fbc41', '4d9221'], 1258 | 9: ['c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'f7f7f7', 'e6f5d0', 1259 | 'b8e186', '7fbc41', '4d9221'], 1260 | 10: ['8e0152', 'c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'e6f5d0', 1261 | 'b8e186', '7fbc41', '4d9221', '276419'], 1262 | 11: ['8e0152', 'c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'f7f7f7', 1263 | 'e6f5d0', 'b8e186', '7fbc41', '4d9221', '276419'] 1264 | }, 1265 | RdBu: { 1266 | type: 'diverging', 1267 | cbf: 42, 1268 | 3: ['ef8a62', 'f7f7f7', '67a9cf'], 1269 | 4: ['ca0020', 'f4a582', '92c5de', '0571b0'], 1270 | 5: ['ca0020', 'f4a582', 'f7f7f7', '92c5de', '0571b0'], 1271 | 6: ['b2182b', 'ef8a62', 'fddbc7', 'd1e5f0', '67a9cf', '2166ac'], 1272 | 7: ['b2182b', 'ef8a62', 'fddbc7', 'f7f7f7', 'd1e5f0', '67a9cf', 1273 | '2166ac'], 1274 | 8: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'd1e5f0', '92c5de', 1275 | '4393c3', '2166ac'], 1276 | 9: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'f7f7f7', 'd1e5f0', 1277 | '92c5de', '4393c3', '2166ac'], 1278 | 10: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'd1e5f0', 1279 | '92c5de', '4393c3', '2166ac', '053061'], 1280 | 11: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'f7f7f7', 1281 | 'd1e5f0', '92c5de', '4393c3', '2166ac', '053061'] 1282 | }, 1283 | RdGy: { 1284 | type: 'diverging', 1285 | cbf: 42, 1286 | 3: ['ef8a62', 'ffffff', '999999'], 1287 | 4: ['ca0020', 'f4a582', 'bababa', '404040'], 1288 | 5: ['ca0020', 'f4a582', 'ffffff', 'bababa', '404040'], 1289 | 6: ['b2182b', 'ef8a62', 'fddbc7', 'e0e0e0', '999999', '4d4d4d'], 1290 | 7: ['b2182b', 'ef8a62', 'fddbc7', 'ffffff', 'e0e0e0', '999999', 1291 | '4d4d4d'], 1292 | 8: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'e0e0e0', 'bababa', 1293 | '878787', '4d4d4d'], 1294 | 9: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'ffffff', 'e0e0e0', 1295 | 'bababa', '878787', '4d4d4d'], 1296 | 10: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'e0e0e0', 1297 | 'bababa', '878787', '4d4d4d', '1a1a1a'], 1298 | 11: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'ffffff', 1299 | 'e0e0e0', 'bababa', '878787', '4d4d4d', '1a1a1a'] 1300 | }, 1301 | RdYlBu: { 1302 | type: 'diverging', 1303 | cbf: 42, 1304 | 3: ['fc8d59', 'ffffbf', '91bfdb'], 1305 | 4: ['d7191c', 'fdae61', 'abd9e9', '2c7bb6'], 1306 | 5: ['d7191c', 'fdae61', 'ffffbf', 'abd9e9', '2c7bb6'], 1307 | 6: ['d73027', 'fc8d59', 'fee090', 'e0f3f8', '91bfdb', '4575b4'], 1308 | 7: ['d73027', 'fc8d59', 'fee090', 'ffffbf', 'e0f3f8', '91bfdb', 1309 | '4575b4'], 1310 | 8: ['d73027', 'f46d43', 'fdae61', 'fee090', 'e0f3f8', 'abd9e9', 1311 | '74add1', '4575b4'], 1312 | 9: ['d73027', 'f46d43', 'fdae61', 'fee090', 'ffffbf', 'e0f3f8', 1313 | 'abd9e9', '74add1', '4575b4'], 1314 | 10: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee090', 'e0f3f8', 1315 | 'abd9e9', '74add1', '4575b4', '313695'], 1316 | 11: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee090', 'ffffbf', 1317 | 'e0f3f8', 'abd9e9', '74add1', '4575b4', '313695'] 1318 | }, 1319 | Spectral: { 1320 | type: 'diverging', 1321 | cbf: 0, 1322 | 3: ['fc8d59', 'ffffbf', '99d594'], 1323 | 4: ['d7191c', 'fdae61', 'abdda4', '2b83ba'], 1324 | 5: ['d7191c', 'fdae61', 'ffffbf', 'abdda4', '2b83ba'], 1325 | 6: ['d53e4f', 'fc8d59', 'fee08b', 'e6f598', '99d594', '3288bd'], 1326 | 7: ['d53e4f', 'fc8d59', 'fee08b', 'ffffbf', 'e6f598', '99d594', 1327 | '3288bd'], 1328 | 8: ['d53e4f', 'f46d43', 'fdae61', 'fee08b', 'e6f598', 'abdda4', 1329 | '66c2a5', '3288bd'], 1330 | 9: ['d53e4f', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', 'e6f598', 1331 | 'abdda4', '66c2a5', '3288bd'], 1332 | 10: ['9e0142', 'd53e4f', 'f46d43', 'fdae61', 'fee08b', 'e6f598', 1333 | 'abdda4', '66c2a5', '3288bd', '5e4fa2'], 1334 | 11: ['9e0142', 'd53e4f', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', 1335 | 'e6f598', 'abdda4', '66c2a5', '3288bd', '5e4fa2'] 1336 | }, 1337 | RdYlGn: { 1338 | type: 'diverging', 1339 | cbf: 0, 1340 | 3: ['fc8d59', 'ffffbf', '91cf60'], 1341 | 4: ['d7191c', 'fdae61', 'a6d96a', '1a9641'], 1342 | 5: ['d7191c', 'fdae61', 'ffffbf', 'a6d96a', '1a9641'], 1343 | 6: ['d73027', 'fc8d59', 'fee08b', 'd9ef8b', '91cf60', '1a9850'], 1344 | 7: ['d73027', 'fc8d59', 'fee08b', 'ffffbf', 'd9ef8b', '91cf60', 1345 | '1a9850'], 1346 | 8: ['d73027', 'f46d43', 'fdae61', 'fee08b', 'd9ef8b', 'a6d96a', 1347 | '66bd63', '1a9850'], 1348 | 9: ['d73027', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', 'd9ef8b', 1349 | 'a6d96a', '66bd63', '1a9850'], 1350 | 10: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee08b', 'd9ef8b', 1351 | 'a6d96a', '66bd63', '1a9850', '006837'], 1352 | 11: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', 1353 | 'd9ef8b', 'a6d96a', '66bd63', '1a9850', '006837'] 1354 | }, 1355 | Accent: { 1356 | type: 'qualitative', 1357 | cbf: 0, 1358 | 3: ['7fc97f', 'beaed4', 'fdc086'], 1359 | 4: ['7fc97f', 'beaed4', 'fdc086', 'ffff99'], 1360 | 5: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0'], 1361 | 6: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0', 'f0027f'], 1362 | 7: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0', 'f0027f', 1363 | 'bf5b17'], 1364 | 8: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0', 'f0027f', 1365 | 'bf5b17', '666666'] 1366 | }, 1367 | Dark2: { 1368 | type: 'qualitative', 1369 | cbf: 3, 1370 | 3: ['1b9e77', 'd95f02', '7570b3'], 1371 | 4: ['1b9e77', 'd95f02', '7570b3', 'e7298a'], 1372 | 5: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e'], 1373 | 6: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e', 'e6ab02'], 1374 | 7: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e', 'e6ab02', 1375 | 'a6761d'], 1376 | 8: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e', 'e6ab02', 1377 | 'a6761d', '666666'] 1378 | }, 1379 | Paired: { 1380 | type: 'qualitative', 1381 | cbf: 4, 1382 | 3: ['a6cee3', '1f78b4', 'b2df8a'], 1383 | 4: ['a6cee3', '1f78b4', 'b2df8a', '33a02c'], 1384 | 5: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99'], 1385 | 6: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c'], 1386 | 7: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', 1387 | 'fdbf6f'], 1388 | 8: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', 1389 | 'fdbf6f', 'ff7f00'], 1390 | 9: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', 1391 | 'fdbf6f', 'ff7f00', 'cab2d6'], 1392 | 10: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', 1393 | 'fdbf6f', 'ff7f00', 'cab2d6', '6a3d9a'], 1394 | 11: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', 1395 | 'fdbf6f', 'ff7f00', 'cab2d6', '6a3d9a', 'ffff99'], 1396 | 12: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', 1397 | 'fdbf6f', 'ff7f00', 'cab2d6', '6a3d9a', 'ffff99', 'b15928'] 1398 | }, 1399 | Pastel1: { 1400 | type: 'qualitative', 1401 | cbf: 0, 1402 | 3: ['fbb4ae', 'b3cde3', 'ccebc5'], 1403 | 4: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4'], 1404 | 5: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6'], 1405 | 6: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc'], 1406 | 7: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc', 1407 | 'e5d8bd'], 1408 | 8: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc', 1409 | 'e5d8bd', 'fddaec'], 1410 | 9: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc', 1411 | 'e5d8bd', 'fddaec', 'f2f2f2'] 1412 | }, 1413 | Pastel2: { 1414 | type: 'qualitative', 1415 | cbf: 0, 1416 | 3: ['b3e2cd', 'fdcdac', 'cbd5e8'], 1417 | 4: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4'], 1418 | 5: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9'], 1419 | 6: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9', 'fff2ae'], 1420 | 7: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9', 'fff2ae', 1421 | 'f1e2cc'], 1422 | 8: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9', 'fff2ae', 1423 | 'f1e2cc', 'cccccc'] 1424 | }, 1425 | Set1: { 1426 | type: 'qualitative', 1427 | cbf: 0, 1428 | 3: ['e41a1c', '377eb8', '4daf4a'], 1429 | 4: ['e41a1c', '377eb8', '4daf4a', '984ea3'], 1430 | 5: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00'], 1431 | 6: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33'], 1432 | 7: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33', 1433 | 'a65628'], 1434 | 8: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33', 1435 | 'a65628', 'f781bf'], 1436 | 9: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33', 1437 | 'a65628', 'f781bf', '999999'] 1438 | }, 1439 | Set2: { 1440 | type: 'qualitative', 1441 | cbf: 3, 1442 | 3: ['66c2a5', 'fc8d62', '8da0cb'], 1443 | 4: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3'], 1444 | 5: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854'], 1445 | 6: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854', 'ffd92f'], 1446 | 7: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854', 'ffd92f', 1447 | 'e5c494'], 1448 | 8: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854', 'ffd92f', 1449 | 'e5c494', 'b3b3b3'] 1450 | }, 1451 | Set3: { 1452 | type: 'qualitative', 1453 | cbf: 0, 1454 | 3: ['8dd3c7', 'ffffb3', 'bebada'], 1455 | 4: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072'], 1456 | 5: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3'], 1457 | 6: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462'], 1458 | 7: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', 1459 | 'b3de69'], 1460 | 8: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', 1461 | 'b3de69', 'fccde5'], 1462 | 9: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', 1463 | 'b3de69', 'fccde5', 'd9d9d9'], 1464 | 10: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', 1465 | 'b3de69', 'fccde5', 'd9d9d9', 'bc80bd'], 1466 | 11: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', 1467 | 'b3de69', 'fccde5', 'd9d9d9', 'bc80bd', 'ccebc5'], 1468 | 12: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', 1469 | 'b3de69', 'fccde5', 'd9d9d9', 'bc80bd', 'ccebc5', 'ffed6f'] 1470 | } 1471 | }; 1472 | 1473 | for (var name in schemes) { 1474 | var scheme = schemes[name]; 1475 | scheme = palette.Scheme.fromPalettes( 1476 | 'cb-' + name, [scheme.type, 'cb-' + scheme.type], scheme, 12, scheme.cbf); 1477 | palette.register(scheme); 1478 | } 1479 | })(); 1480 | -------------------------------------------------------------------------------- /resources/public/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "asterion", 3 | "version" : "0.0-DEV", 4 | "build-commit" : "0000", 5 | "build-date" : "0000-00-00", 6 | "main" : "app.js", 7 | "private": true 8 | } 9 | -------------------------------------------------------------------------------- /resources/public/prod.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev-tools": false 3 | } 4 | -------------------------------------------------------------------------------- /scripts/build-windows-exe.nsi: -------------------------------------------------------------------------------- 1 | ;; This script generates the windows .exe file for distribution. 2 | ;; NSIS - http://nsis.sourceforge.net/Main_Page 3 | 4 | ;; HM NIS Edit Wizard helper defines 5 | !define PRODUCT_NAME "draft" 6 | ;; !define PRODUCT_VERSION "0.0" ;; This is auto-defined by the caller in the release script 7 | !define PRODUCT_WEB_SITE "https://github.com/" 8 | !define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\draft.exe" 9 | !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" 10 | !define PRODUCT_UNINST_ROOT_KEY "HKLM" 11 | !define PRODUCT_STARTMENU_REGVAL "NSIS:StartMenuDir" 12 | 13 | ;; MUI 1.67 compatible ------ 14 | !include "MUI.nsh" 15 | 16 | ;; MUI Settings 17 | !define MUI_ABORTWARNING 18 | !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" 19 | !define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" 20 | 21 | ;; Welcome page 22 | !insertmacro MUI_PAGE_WELCOME 23 | 24 | ;; Directory page 25 | !insertmacro MUI_PAGE_DIRECTORY 26 | 27 | ;; Start menu page 28 | var ICONS_GROUP 29 | !define MUI_STARTMENUPAGE_NODISABLE 30 | !define MUI_STARTMENUPAGE_DEFAULTFOLDER "Example" 31 | !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}" 32 | !define MUI_STARTMENUPAGE_REGISTRY_KEY "${PRODUCT_UNINST_KEY}" 33 | !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${PRODUCT_STARTMENU_REGVAL}" 34 | !insertmacro MUI_PAGE_STARTMENU Application $ICONS_GROUP 35 | 36 | ;; Instfiles page 37 | !insertmacro MUI_PAGE_INSTFILES 38 | 39 | ;; Finish page 40 | !define MUI_FINISHPAGE_RUN "$INSTDIR\draft.exe" 41 | !insertmacro MUI_PAGE_FINISH 42 | 43 | ; Uninstaller pages 44 | !insertmacro MUI_UNPAGE_INSTFILES 45 | 46 | ; Language files 47 | !insertmacro MUI_LANGUAGE "English" 48 | 49 | ; Source directory 50 | ; (auto-defined by the caller in the release script) 51 | ;; !define OUTFILE 52 | ;; !define RELEASE_DIR 53 | 54 | ; MUI end ------ 55 | 56 | Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" 57 | OutFile "${OUTFILE}" 58 | InstallDir "$PROGRAMFILES\Example" 59 | InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" "" 60 | ShowInstDetails show 61 | ShowUnInstDetails show 62 | 63 | Section "MainSection" SEC01 64 | SetOutPath "$INSTDIR" 65 | SetOverwrite ifnewer 66 | File /r "${RELEASE_DIR}\*" 67 | 68 | ;; Shortcuts 69 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 70 | CreateDirectory "$SMPROGRAMS\$ICONS_GROUP" 71 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\draft.lnk" "$INSTDIR\draft.exe" 72 | CreateShortCut "$DESKTOP\draft.lnk" "$INSTDIR\draft.exe" 73 | !insertmacro MUI_STARTMENU_WRITE_END 74 | SectionEnd 75 | 76 | Section -AdditionalIcons 77 | SetOutPath $INSTDIR 78 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 79 | WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}" 80 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Website.lnk" "$INSTDIR\${PRODUCT_NAME}.url" 81 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Uninstall.lnk" "$INSTDIR\uninst.exe" 82 | !insertmacro MUI_STARTMENU_WRITE_END 83 | SectionEnd 84 | 85 | Section -Post 86 | WriteUninstaller "$INSTDIR\uninst.exe" 87 | WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\draft.exe" 88 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" 89 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe" 90 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\draft.exe" 91 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" 92 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" 93 | SectionEnd 94 | 95 | 96 | Function un.onUninstSuccess 97 | HideWindow 98 | MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer." 99 | FunctionEnd 100 | 101 | Function un.onInit 102 | MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2 103 | Abort 104 | FunctionEnd 105 | 106 | Section Uninstall 107 | !insertmacro MUI_STARTMENU_GETFOLDER "Application" $ICONS_GROUP 108 | 109 | Delete "$SMPROGRAMS\$ICONS_GROUP\Uninstall.lnk" 110 | Delete "$SMPROGRAMS\$ICONS_GROUP\Website.lnk" 111 | Delete "$DESKTOP\draft.lnk" 112 | Delete "$SMPROGRAMS\$ICONS_GROUP\draft.lnk" 113 | 114 | RMDir "$SMPROGRAMS\$ICONS_GROUP" 115 | 116 | RMDir /r "$INSTDIR" 117 | 118 | DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" 119 | DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}" 120 | SetAutoClose true 121 | SectionEnd 122 | -------------------------------------------------------------------------------- /scripts/dmg/TestBkg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/scripts/dmg/TestBkg.png -------------------------------------------------------------------------------- /scripts/dmg/TestBkg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bensu/asterion/847b1b152bdf368921268fe7aaf3da047f944a97/scripts/dmg/TestBkg@2x.png -------------------------------------------------------------------------------- /scripts/setup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | pushd "%~dp0\.." 4 | 5 | echo. & echo Installing node dependencies... 6 | call npm install --no-optional 7 | 8 | echo. & echo Installing grunt & bower... 9 | call npm install -g grunt-cli 10 | call npm install -g bower 11 | 12 | call grunt setup 13 | 14 | echo. & echo setup complete. 15 | 16 | popd 17 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # exit on errors 4 | set -e 5 | 6 | cd "`dirname $0`/.." 7 | 8 | echo; echo "Installing node dependencies..." 9 | if [[ "$OSTYPE" == "darwin"* ]]; then 10 | npm install 11 | else 12 | # "grunt-appdmg" is a mac-only dependency that will fail to build on linux. 13 | # So we are including it as an optionalDependency in package.json 14 | # and preventing its installation with npm's --no-optional flag. 15 | npm install --no-optional 16 | fi 17 | 18 | 19 | echo; echo "Installing grunt and bower..." 20 | 21 | npm install -g grunt-cli bower 22 | 23 | grunt setup 24 | 25 | echo; echo "Setup complete." 26 | -------------------------------------------------------------------------------- /src/clj/asterion/deps.clj: -------------------------------------------------------------------------------- 1 | (ns asterion.deps 2 | (:import [java.util UUID] 3 | [org.apache.commons.io FileUtils] 4 | [org.eclipse.jgit.api.errors InvalidRemoteException] 5 | [org.eclipse.jgit.errors TransportException]) 6 | (:require [clojure.java.io :as io] 7 | [clj-jgit.porcelain :as git] 8 | [clj-jgit.util :as git-util] 9 | [asterion.deps.clojure :as clj] 10 | [asterion.deps.javascript :as js])) 11 | 12 | ;; ====================================================================== 13 | ;; Utils 14 | 15 | (defn uuid [] 16 | (str (UUID/randomUUID))) 17 | 18 | (defn project-type [dir subpath] 19 | (letfn [(file-exists? [f] 20 | (.exists (io/file dir subpath f)))] 21 | (cond 22 | (file-exists? "package.json") :js 23 | (file-exists? "project.clj") :clj 24 | :else nil))) 25 | 26 | (defmulti parse-repo project-type) 27 | 28 | (defmethod parse-repo :default [_ _] {:error :project/cant-find-sources}) 29 | 30 | (defmethod parse-repo :clj 31 | [dir subpath] 32 | (clj/parse-repo dir subpath)) 33 | 34 | (defmethod parse-repo :js 35 | [dir subpath] 36 | (js/parse-repo dir subpath)) 37 | 38 | (defn parse-url 39 | "Given a git url (string) it will fetch the repository and try to return 40 | a dependency graph for it. It may also return the following errors: 41 | 42 | - {:error :no-project-file} 43 | - {:error :project-not-found} 44 | - {:error :project-protected} 45 | - {:error #} ;; unknown exception" 46 | ([url] (parse-url url "")) 47 | ([url subpath] 48 | {:pre [(string? url)]} 49 | (let [repo-name (git-util/name-from-uri url) 50 | dir (io/file "tmp" (str (uuid) repo-name))] 51 | (try 52 | (git/git-clone-full url (.getPath dir)) 53 | (parse-repo dir subpath) 54 | (catch InvalidRemoteException _ 55 | {:error :project/not-found}) 56 | (catch TransportException _ 57 | {:error :project/protected}) 58 | (catch Exception e 59 | {:error e}) 60 | (finally (future (FileUtils/deleteDirectory dir))))))) 61 | -------------------------------------------------------------------------------- /src/clj/asterion/deps/clojure.clj: -------------------------------------------------------------------------------- 1 | (ns asterion.deps.clojure 2 | (:import [java.io File]) 3 | (:require [clojure.java.io :as io] 4 | [clojure.set :as set] 5 | [leiningen.core.project :as project] 6 | [clojure.tools.namespace.file :as file] 7 | [clojure.tools.namespace.find :as find] 8 | [clojure.tools.namespace.track :as track] 9 | [clojure.tools.namespace.dependency :as dep])) 10 | 11 | ;; ====================================================================== 12 | ;; Helpers 13 | 14 | (defn file->ns-name [f] 15 | (-> f file/read-file-ns-decl rest first)) 16 | 17 | ;; ====================================================================== 18 | ;; Dependency graph 19 | 20 | (defn project->builds 21 | "Takes a project map and returns the parsed :cljsbuild config, 22 | returns nil if there are none" 23 | [project] 24 | {:post [(every? string? (keys %)) (every? coll? (vals %))]} 25 | (when-let [builds (:builds (:cljsbuild project))] 26 | (->> (cond 27 | (map? builds) builds 28 | (vector? builds) (map (fn [build] [(:id build) build]) builds) 29 | :else (throw (Exception. "Bad cljsbuild options"))) 30 | (map (fn [[k v]] 31 | [(name k) (:source-paths v)])) 32 | (into {})))) 33 | 34 | (defn parse-project 35 | "Takes a directory and returns the Clojure Sources for it" 36 | [^File project-dir] 37 | (let [f (io/file project-dir "project.clj") 38 | _ (assert (.exists f) "project.clj not found") 39 | project (project/read-raw (.getPath f)) ] 40 | (->> {"clj" (:source-paths project)} 41 | (merge (project->builds project)) 42 | (map (fn [[k v]] 43 | [k (mapv (partial io/file project-dir) v)])) 44 | (into {})))) 45 | 46 | (defn platform->ext [platform] 47 | (get {:clj find/clj 48 | :cljs find/cljs 49 | :cljc (update find/clj :extensions (partial cons ".cljs"))} 50 | platform 51 | find/clj)) 52 | 53 | (defn depgraph 54 | "Constructs a graph - {:nodes [{:name foo.bar}] 55 | :edges [{:source foo.bar :target foo.baz}]} 56 | from a set of source-paths." 57 | ([srcs] (depgraph nil srcs)) 58 | ([platform srcs] 59 | (let [exts (platform->ext platform) 60 | source-fs (apply set/union 61 | (map #(find/find-sources-in-dir % exts) srcs)) 62 | dep-graph ((file/add-files {} source-fs) ::track/deps) 63 | ns-names (set (map (comp second file/read-file-ns-decl) source-fs)) 64 | part-of-project? (partial contains? ns-names) 65 | nodes (filter part-of-project? (reverse (dep/topo-sort dep-graph))) 66 | edges (->> nodes 67 | (mapcat #(->> (filter part-of-project? 68 | (dep/immediate-dependencies dep-graph %)) 69 | (map (partial vector %)))))] 70 | {:edges (mapv (fn [[from to]] {:source from :target to}) edges) 71 | :nodes (->> nodes 72 | (map-indexed (fn [i n] [n i])) 73 | (into {}) 74 | (sort-by second) 75 | (mapv (fn [[n i]] {:name (str n)})))}))) 76 | 77 | (defn parse-repo [dir subpath] 78 | (try 79 | (->> 80 | (parse-project (io/file dir subpath)) 81 | (map (fn [[id srcs]] 82 | [id (depgraph (if (= "clj" id) :clj :cljs) srcs)])) 83 | (into {})) 84 | (catch java.lang.AssertionError _ 85 | {:error :project/no-project-file}) 86 | (catch clojure.lang.ExceptionInfo e 87 | {:error :project/circular-dependency}))) 88 | -------------------------------------------------------------------------------- /src/clj/asterion/deps/javascript.clj: -------------------------------------------------------------------------------- 1 | (ns asterion.deps.javascript 2 | (:require [clojure.set :as set] 3 | [clojure.string :as str] 4 | [clojure.java.io :as io] 5 | [clojure.java.shell :refer [sh]] 6 | [clojure.data.json :as json] 7 | [graph.util :as graph])) 8 | 9 | (defn maybe-conj [coll v] 10 | (cond-> coll 11 | (some? v) (conj v))) 12 | 13 | (defn rename-file [f] 14 | (str/replace f #"/" ".")) 15 | 16 | (defn module-name [n] 17 | (last (str/split n #"/"))) 18 | 19 | (defn normalize-graph 20 | "Transforms all the module names into full paths 21 | and removes the outside dependencies" 22 | [g] 23 | (let [node-names (set (keys g)) 24 | module-names (zipmap (map module-name node-names) 25 | node-names) 26 | in-project #(or (node-names %) (get module-names %))] 27 | (zipmap node-names 28 | (map #(remove nil? (mapv in-project %)) (vals g))))) 29 | 30 | 31 | (defn parse-repo 32 | [dir subpath] 33 | (with-open [rdr (io/reader (io/file dir subpath "package.json"))] 34 | (let [package-json (json/read rdr) 35 | srcs (-> #{} 36 | (set/union (set (get package-json "files")) #{"src" "lib"}) 37 | (maybe-conj (get-in package-json ["directories" "lib"]))) 38 | srcs-with-root (->> srcs 39 | (map (partial io/file dir subpath)) 40 | (filter #(.exists %)) 41 | (map #(.getPath %)))] 42 | (assert (every? some? srcs-with-root) "Paths can't be nil") 43 | (let [g (-> (apply sh "madge" "-j" srcs-with-root) 44 | :out 45 | json/read-str 46 | normalize-graph 47 | graph/clear-disj-nodes) 48 | node-names (keys g)] 49 | {"js" {:nodes (mapv (comp (partial hash-map :name) rename-file) 50 | node-names) 51 | :edges (->> node-names 52 | (mapcat (fn [n] 53 | (->> (get g n) 54 | (map (fn [e] 55 | {:source (rename-file n) 56 | :target (rename-file e)})) 57 | (remove nil?)))) 58 | vec)}})))) 59 | -------------------------------------------------------------------------------- /src/clj/asterion/server.clj: -------------------------------------------------------------------------------- 1 | (ns asterion.server 2 | (:require [clojure.string :as str] 3 | [clojure.java.io :as io] 4 | [com.stuartsierra.component :as component] 5 | [ring.middleware.params :as params] 6 | [ring.util.response :as response] 7 | [ring.adapter.jetty :as jetty] 8 | [compojure.core :refer :all] 9 | [compojure.route :as route] 10 | [compojure.handler :as handler] 11 | [asterion.deps :as deps])) 12 | 13 | ;; ====================================================================== 14 | ;; Utils 15 | 16 | (defn ->url [user repo] 17 | (str "https://github.com/" user "/" repo ".git")) 18 | 19 | (defn error-response [error] 20 | {:status 500 21 | :headers {"Content-Type" "application/edn"} 22 | :body (pr-str error)}) 23 | 24 | (defn ok-response [body] 25 | {:status 200 26 | :headers {"Content-Type" "application/edn"} 27 | :body (pr-str body)}) 28 | 29 | ;; ====================================================================== 30 | ;; Cache 31 | 32 | (def cache-root "cached") 33 | 34 | (defn cache-path [user repo] 35 | (str (str/join "/" [cache-root user repo]) ".edn") ) 36 | 37 | (defn spit-graph 38 | "Writes to disk the results" 39 | [{:keys [user repo graph] :as data}] 40 | {:pre [(every? some? (vals data))]} 41 | (let [p (str "resources/public/" (cache-path user repo)) 42 | parent (.getParentFile (io/file p))] 43 | ;; ensure directory 44 | (when-not (.exists parent) 45 | (.mkdirs parent)) 46 | (spit p data) 47 | p)) 48 | 49 | (defn cache! 50 | ([user repo] 51 | (cache! user repo "")) 52 | ([user repo subpath] 53 | (spit-graph {:user user 54 | :repo repo 55 | :graphs (deps/parse-url (->url user repo) subpath)}))) 56 | 57 | (defn cached 58 | "Returns a path if the graph for the repo is in cache, otwherwise nil" 59 | [user repo] 60 | (when-let [c (some-> (cache-path user repo) io/resource io/file)] 61 | (when (.exists c) 62 | (.getPath c)))) 63 | 64 | (defn repo-handler [user repo] 65 | (try 66 | (if-let [cache-path (cached user repo)] 67 | (response/file-response cache-path) 68 | (let [graphs (deps/parse-url (->url user repo))] 69 | (if (contains? graphs :error) 70 | (error-response graphs) 71 | (do 72 | (future (spit-graph {:user user :repo repo :graphs graphs})) 73 | (ok-response {:graphs graphs}))))) 74 | (catch Exception e 75 | (error-response {:error e})))) 76 | 77 | (defroutes app-routes 78 | (HEAD "/" [] "") 79 | (GET "/" [] (response/redirect "index.html")) 80 | (GET "/repo/:user/:repo" [user repo] 81 | (if (and (string? user) (string? repo)) 82 | (repo-handler user repo) 83 | (error-response (str "Bad params: " user repo)))) 84 | (route/resources "/") 85 | (route/files "/") 86 | (route/not-found "

404 - Page not found

")) 87 | 88 | (def app-handler 89 | (-> app-routes 90 | handler/site)) 91 | 92 | (defn start-jetty [handler port] 93 | (jetty/run-jetty handler {:port (Integer. port) :join? false})) 94 | 95 | (defrecord Server [port jetty] 96 | component/Lifecycle 97 | (start [component] 98 | (println "Start server at port " port) 99 | (assoc component :jetty (start-jetty app-handler port))) 100 | (stop [component] 101 | (println "Stop server") 102 | (when jetty 103 | (.stop jetty)) 104 | component)) 105 | 106 | (def new-system (Server. 3000 nil)) 107 | -------------------------------------------------------------------------------- /src/cljc/graph/util.cljc: -------------------------------------------------------------------------------- 1 | (ns graph.util 2 | (:require [clojure.set :as set])) 3 | 4 | (defn useful-nodes [g] 5 | (loop [useful-nodes #{} 6 | nodes (keys g)] 7 | (if-let [node (first nodes)] 8 | (let [edges (set (get g node))] 9 | (recur (cond-> (set/union useful-nodes edges) 10 | (not (empty? edges)) (conj node)) 11 | (rest nodes))) 12 | useful-nodes))) 13 | 14 | (defn clear-disj-nodes [g] 15 | (let [disj-nodes (set/difference (set (keys g)) 16 | (useful-nodes g))] 17 | (apply dissoc g disj-nodes))) 18 | -------------------------------------------------------------------------------- /src/cljs/asterion/analytics.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.analytics) 2 | 3 | (defn repo-event! [event data] 4 | (js/ga "send" "event" "Repositories" event data)) 5 | 6 | (defn nav-event! [event data] 7 | (js/ga "send" "event" "Nav" event data)) 8 | -------------------------------------------------------------------------------- /src/cljs/asterion/click.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.click 2 | "Provides a listener for click elements, click-outside" 3 | (:import [goog.events EventType] 4 | [goog.events Event]) 5 | (:require [goog.dom :as gdom] 6 | [goog.events :as events])) 7 | 8 | (defn- click-handler [in? sel f e] 9 | {:pre [(coll? sel) (fn? f)]} 10 | (let [t (.-target e) 11 | ff (if in? identity not)] 12 | (when (ff (some (partial gdom/getAncestorByClass t) sel)) 13 | ;; We want to prevent the "in" element from handling it 14 | (when (ff true) 15 | (Event.stopPropagation e) 16 | (Event.preventDefault e)) 17 | (f e) 18 | nil))) 19 | 20 | (defn install! [in? sel f] 21 | (events/listen js/window EventType.CLICK 22 | (partial click-handler in? sel f) true)) 23 | 24 | (defn install-out! 25 | "Calls f with e whenever there is a click event e outside of the 26 | elements matched by the selectors in sel. Currently only classes are 27 | supported as selectors. 28 | Ex: sel = [\"some-class\" \"some-other-class\"] 29 | 30 | would match all clicks outside elements with those classes." 31 | [sel f] 32 | (install! false sel f)) 33 | 34 | (defn install-in! 35 | "Calls f with e whenever there is a click event e inside of the 36 | elements matched by the selectors in sel. Currently only classes are 37 | supported as selectors. 38 | Ex: sel = [\"some-class\" \"some-other-class\"] 39 | 40 | would match all clicks outside elements with those classes." 41 | [sel f] 42 | (install! true sel f)) 43 | 44 | (defn uninstall! [key] 45 | (events/unlistenByKey key)) 46 | -------------------------------------------------------------------------------- /src/cljs/asterion/components.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.components 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true] 4 | [asterion.analytics :refer [nav-event!]])) 5 | 6 | (defn icon-button [{:keys [icon-class title]} owner {:keys [click-fn]}] 7 | (om/component 8 | (dom/i #js {:className (str icon-class " fa clickable") 9 | :title title 10 | :onClick (if (fn? click-fn) 11 | click-fn 12 | identity)}))) 13 | 14 | (defn link 15 | ([href] (link href href)) 16 | ([href label] (link label href "")) 17 | ([href label class] 18 | (dom/a #js {:className (str "file-item__text " class) 19 | :onClick (fn [_] 20 | (nav-event! "link-out" href)) 21 | :target "_blank" 22 | :href href} 23 | label))) 24 | 25 | (def form-url 26 | "https://docs.google.com/forms/d/19FW2TpxN3FmtJ6drU5N6Ia8g3v_yhr1VuVq5sa74dZ4/viewform") 27 | 28 | (defn forms-link [label] 29 | (dom/a #js {:className (str "file-item__text file--activate") 30 | :onClick (fn [_] 31 | (nav-event! "link-out" "to-form")) 32 | :target "_blank" 33 | :href form-url} 34 | label)) 35 | -------------------------------------------------------------------------------- /src/cljs/asterion/core.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.core 2 | (:import [goog.dom ViewportSizeMonitor]) 3 | (:require [goog.string.linkify :as links] 4 | [goog.events :as events] 5 | [clojure.string :as str] 6 | [cljs.reader :as reader] 7 | [ajax.core :refer [GET POST]] 8 | [om.core :as om :include-macros true] 9 | [om.dom :as dom :include-macros true] 10 | [asterion.d3] 11 | [asterion.deps :as deps] 12 | [asterion.tree :as tree] 13 | [asterion.project :as project] 14 | [asterion.components :as components] 15 | [asterion.analytics :refer [repo-event! nav-event!]] 16 | [asterion.click :as click])) 17 | 18 | ;; ====================================================================== 19 | ;; Model 20 | 21 | (def init-state {:nav {:ns "" 22 | :highlight "" ;; can be local state 23 | :open? false 24 | :build nil 25 | :highlighted #{}} 26 | :buffer {} 27 | :graphs [] 28 | :errors #{} 29 | :project {:url "" 30 | :user nil ;; string 31 | :repo nil ;; string 32 | :srcs #{} 33 | :platform nil}}) 34 | 35 | (defonce app-state (atom init-state)) 36 | 37 | ;; ====================================================================== 38 | ;; Update 39 | 40 | (def ->form 41 | [(dom/p nil "We couldn't find the repository. ") 42 | (dom/p nil "Do you want to graph a " 43 | (components/forms-link "private repository?"))]) 44 | 45 | (def error->msg* 46 | {:graph/empty-nodes "We found nothing to graph after filtering" 47 | :project/parse-error "We couldn't read your project.clj" 48 | :project/not-found "We couldn't find the repository" 49 | :project/protected ->form 50 | :project/cant-find-sources "We couldn't find your package.json/project.clj" 51 | :project/timeout "It took too long to talk to the server. We don't know what happened!" 52 | :project/no-project-file "We couldn't find a project.clj in the repository's top level directory." 53 | :project/invalid-url "The given url is invalid. Try copy-pasting from the url bar, ex: https://github.com/juxt/yada" 54 | :project/circular-dependency "The project contains a circular dependency, probably a cljs file requiring a clj macro file with the same name. This is a bug, we'll fix it soon!" 55 | :nav/search-error "There was a problem while searching" 56 | :nav/search-not-found "No matches found" 57 | :unknown-error "We are truly sorry but we don't know what happened."}) 58 | 59 | (defn error->msg [error] 60 | (if (string? error) 61 | error 62 | (error->msg* error))) 63 | 64 | (defn update-state [data [tag msg]] 65 | (case tag 66 | :graph/add (let [build-id (or (first (keys msg)) "clj")] 67 | (-> data 68 | (assoc :graphs msg) 69 | (assoc-in [:nav :build] build-id) 70 | (update-state [:nav/graph->buffer (get msg build-id)]) 71 | (update-state [:nav/draw! nil]))) 72 | 73 | :project/data (update data :project #(merge % msg)) 74 | 75 | :project/url (assoc-in data [:project :url] msg) 76 | 77 | :project/wait (assoc data :waiting? true) 78 | 79 | :project/done (assoc data :waiting? false) 80 | 81 | :project/invalid-url (update data :errors #(conj % :project/invalid-url)) 82 | 83 | :project/clear init-state 84 | 85 | :nav/new-build (-> data 86 | (assoc-in [:nav :build] msg) 87 | (update-state [:nav/graph->buffer (get (:graphs data) msg)]) 88 | (update-state [:nav/draw! nil])) 89 | 90 | :nav/toggle-builds (update-in data [:nav :open?] not) 91 | 92 | :nav/open-help (do (nav-event! "open-help" nil) 93 | (assoc data :overlay? true)) 94 | 95 | :nav/close-help (do (nav-event! "close-help" nil) 96 | (assoc data :overlay? false)) 97 | 98 | :nav/graph->buffer (assoc data :buffer msg) 99 | 100 | :nav/ns (assoc-in data [:nav :ns] msg) 101 | 102 | :nav/highlight (assoc-in data [:nav :highlight] msg) 103 | 104 | :nav/clear-highlighted (-> data 105 | (assoc-in [:nav :highlighted] #{}) 106 | (update-state [:nav/draw! nil])) 107 | 108 | :nav/add-error (update data :errors #(conj % msg)) 109 | 110 | :nav/clear-errors (assoc data :errors #{}) 111 | 112 | :nav/draw! (let [g (-> (get (:graphs data) (:build (:nav data))) 113 | (deps/filter-graph (str/split (:ns (:nav data)) " ")) 114 | (deps/highlight-graph (:highlighted (:nav data))))] 115 | (if (deps/valid-graph? g) 116 | (update-state data [:nav/graph->buffer g]) 117 | (update-state data [:nav/add-error :graph/empty-nodes]))) 118 | 119 | ;; If action is unknonw, return data unchanged 120 | data)) 121 | 122 | ;; TODO: support raising several events sequentially 123 | (defn raise! [data tag msg] 124 | {:pre [(keyword? tag) (om/cursor? data)]} 125 | (om/transact! data #(update-state % [tag msg]))) 126 | 127 | ;; ====================================================================== 128 | ;; Sources Screen 129 | 130 | (defn help-button [data owner] 131 | (om/component 132 | (om/build components/icon-button 133 | {:title "Help & About" 134 | :icon-class "fa-question float-bottom-corner help-btn"} 135 | {:opts {:click-fn (fn [_] 136 | (raise! data :nav/open-help nil))}}))) 137 | 138 | (defn close-button [data owner] 139 | (om/component 140 | (om/build components/icon-button 141 | {:title "Close Help" 142 | :icon-class "fa-times float-right-corner clear-btn"} 143 | {:opts {:click-fn (fn [_] 144 | (raise! data :nav/close-help nil))}}))) 145 | 146 | (defn clear-button [data owner] 147 | (om/component 148 | (om/build components/icon-button 149 | {:title "Clear Project" 150 | :icon-class "fa-reply float-right-corner clear-btn"} 151 | {:opts {:click-fn (fn [_] 152 | (nav-event! "clear-repo" (:url (:project data))) 153 | (raise! data :project/clear nil))}}))) 154 | 155 | (defn error-card [{:keys [error] :as data} owner {:keys [close-fn class]}] 156 | (om/component 157 | (dom/div #js {:className class} 158 | (om/build components/icon-button 159 | {:icon-class "fa-times float-right-corner clear-btn" 160 | :title "Dismiss"} 161 | {:opts {:click-fn close-fn}}) 162 | (dom/h3 #js {:className "blue-box__subtitle" 163 | :title "A veces me equivoco y nos reimos buenamente los dos"} 164 | (:title error)) 165 | (cond 166 | (string? (:msg error)) (dom/p nil (:msg error)) 167 | :else (apply dom/div nil (:msg error)))))) 168 | 169 | 170 | ;; ====================================================================== 171 | ;; Project Screen 172 | 173 | (def examples-with-path 174 | [["https://github.com/bhauman/lein-figwheel" "sidecar"] 175 | ["https://github.com/aphyr/jepsen" "jepsen"] 176 | ["https://github.com/emezeske/lein-cljsbuild" "support"]]) 177 | 178 | (def examples 179 | ["https://github.com/clojure/clojurescript" 180 | "https://github.com/circleci/frontend" 181 | "https://github.com/tonsky/datascript" 182 | "https://github.com/ztellman/manifold" 183 | "https://github.com/aphyr/riemann" 184 | "https://github.com/onyx-platform/onyx" 185 | "https://github.com/metabase/metabase" 186 | "https://github.com/Engelberg/instaparse" 187 | "https://github.com/overtone/overtone" 188 | "https://github.com/juxt/yada"]) 189 | 190 | (defn error-handler [data err] 191 | (raise! data :project/done nil) 192 | (let [e (try 193 | (if (= :timeout (:failure err)) 194 | :project/timeout 195 | (let [res (reader/read-string (:response err))] 196 | (if (keyword? (:error res)) 197 | (:error res) 198 | :unknown-error))) 199 | (catch :default e 200 | :unknown-error))] 201 | (repo-event! "receive-error" (clj->js e)) 202 | (raise! data :nav/add-error e))) 203 | 204 | (defn handler [data res] 205 | (repo-event! "receive-cached" (:url (:project data))) 206 | (raise! data :project/done nil) 207 | (raise! data :graph/add (:graphs (reader/read-string res)))) 208 | 209 | ;; TODO: this function does the unexpected when the repo *does* 210 | ;; contain ".git" in the name! 211 | (defn remove-git-ext [^string url] 212 | (str/replace url #"\.git$" "")) 213 | 214 | (defn parse-url [url] 215 | (let [url' (links/findFirstUrl url)] 216 | (when-not (empty? url') 217 | (let [tokens (take-last 2 (str/split (remove-git-ext url') "/"))] 218 | (when (and (every? (comp not empty?) tokens) 219 | (not (re-find #"github" (first tokens)))) 220 | tokens))))) 221 | 222 | (defn start! [data e] 223 | (raise! data :nav/clear-errors nil) 224 | (let [url (:url (:project data))] 225 | (repo-event! "start" url) 226 | (if-let [[user repo] (parse-url url)] 227 | (do 228 | (raise! data :project/wait nil) 229 | (raise! data :project/data {:user user :repo repo}) 230 | (GET (str "/cached/" user "/" repo ".edn") 231 | {:handler (partial handler data) 232 | :error-handler 233 | (fn [err] 234 | (if (= 404 (:status err)) 235 | (GET (str "/repo/" user "/" repo) 236 | {:handler (partial handler data) 237 | :error-handler (partial error-handler data)}) 238 | (error-handler data err)))})) 239 | (do 240 | (repo-event! "invalid" url) 241 | (raise! data :project/invalid-url url))))) 242 | 243 | (defn button [label f] 244 | (dom/div #js {:className "btn--green" 245 | :onClick f} 246 | label)) 247 | 248 | (defn project-name [data] 249 | (let [project (:project data)] 250 | (when-let [repo (:repo project)] 251 | (when-let [user (:user project)] 252 | (str user "/" repo))))) 253 | 254 | (defn example-link [link owner] 255 | (om/component 256 | (dom/li #js {:className "file-item"} 257 | (components/link link)))) 258 | 259 | (defn overlay [data owner] 260 | (om/component 261 | (dom/div #js {:className "overlay"}))) 262 | 263 | (defn modal [data owner] 264 | (reify 265 | om/IDidMount 266 | (did-mount [_] 267 | (om/set-state! owner :listen-click-in 268 | (click/install-out! ["about-modal"] 269 | #(raise! data :nav/close-help nil)))) 270 | om/IWillUnmount 271 | (will-unmount [_] 272 | (click/uninstall! (om/get-state owner :listen-click-in))) 273 | om/IRender 274 | (render [_] 275 | (dom/div #js {:className "modal"} 276 | (dom/div #js {:className "page center-container"} 277 | (dom/div #js {:className "about-modal float-box blue-box"} 278 | (om/build close-button data) 279 | (dom/h1 #js {:className "blue-box__title"} "About") 280 | (dom/p nil "Dependency graphs can help you comunicate 281 | with your teammates, introduce people to the codebase, 282 | explore changes to the architecture, and help you 283 | enforce it during reviews.") 284 | (dom/p nil "Code analysis can be " 285 | (components/link "https://www.youtube.com/watch?v=hWhBmJJZoNM" 286 | "very useful" "file--activate") 287 | "but it is inconvienient to setup and generally ignored. 288 | Asterion aims to make code analysis more approachable. 289 | Are you interested in graphing " 290 | (components/forms-link "private repositories?")) 291 | (dom/p nil "To use the tool, make sure you are pasting a link to a 292 | Github repository that contains a project.clj file in 293 | it's top directory. For example:") 294 | (components/link "https://github.com/juxt/yada" 295 | "https://github.com/juxt/yada" "file--activate") 296 | (dom/p nil "will work, but " 297 | (components/link "https://github.com/clojure/clojure" 298 | "https://github.com/clojure/clojure") 299 | "won't because it doesn't have a project.clj. Boot projects are 300 | not yet supported.") 301 | (dom/p nil "If you have any feedback, you can find me at " 302 | (components/link "mailto:sbensu@gmail.com" "sbensu@gmail.com" "file--activate")))))))) 303 | 304 | (defn select-project [data owner] 305 | (om/component 306 | (dom/div #js {:className "page center-container whole-page"} 307 | (when (:overlay? data) 308 | (om/build overlay data)) 309 | (when (:overlay? data) 310 | (om/build modal data)) 311 | (om/build help-button data) 312 | (when-not (empty? (:errors data)) 313 | (om/build error-card 314 | (assoc data 315 | :error {:title "Error" 316 | :msg (error->msg (first (:errors data)))}) 317 | {:opts {:class "float-box error-card center" 318 | :close-fn (fn [_] 319 | (raise! data :nav/clear-errors nil))}})) 320 | (dom/div #js {:className "float-box blue-box center"} 321 | (dom/h1 #js {:className "blue-box__title"} "Asterion") 322 | (if (:waiting? data) 323 | (dom/div nil 324 | (dom/p nil (str "Processing " (or (project-name data) "..."))) 325 | (dom/div #js {:className "loader"}) 326 | (dom/p nil "Try other cool projects:") 327 | (apply dom/ul #js {:className "folder-list"} 328 | (->> examples 329 | (remove (partial = (:url (:project data)))) 330 | shuffle 331 | (take 3) 332 | (map (partial om/build example-link)))) 333 | (dom/p nil 334 | (dom/span nil "but whatever you do, don't try " 335 | (components/link "https://github.com/clojure/core.typed" 336 | "core.typed")))) 337 | (dom/div nil 338 | (dom/p nil "Make dependency graphs for Clojure projects.") 339 | (dom/p nil "Paste a link for a github repo:") 340 | (dom/input #js {:type "url" 341 | :className "blue-input" 342 | :placeholder (str "Ex: " (rand-nth examples)) 343 | :value (:url (:project data)) 344 | :onKeyDown (fn [e] 345 | (when (= "Enter" (.-key e)) 346 | (start! data e))) 347 | :onChange (fn [e] 348 | (let [url (.. e -target -value)] 349 | (raise! data :project/url url)))}) 350 | (dom/div #js {:className "center-container"} 351 | (button "Graph" (partial start! data))))))))) 352 | 353 | ;; ====================================================================== 354 | ;; Graph Screen 355 | 356 | (defn nav-input [data owner {:keys [value-key placeholder title 357 | on-change on-enter]}] 358 | (om/component 359 | (dom/input #js {:className "top-break blue-input" 360 | :type "text" 361 | :title title 362 | :placeholder placeholder 363 | :value (get data value-key) 364 | :onKeyDown (fn [e] 365 | (when (= "Enter" (.-key e)) 366 | (on-enter e))) 367 | :onChange (fn [e] 368 | (on-change (.. e -target -value)))}))) 369 | 370 | (defn build-item [{:keys [data label]} owner] 371 | (om/component 372 | (dom/li #js {:onClick (fn [_] 373 | (raise! data :nav/new-build label) 374 | (raise! data :nav/toggle-builds nil)) 375 | :className "build-item"} 376 | (dom/span #js {:className "build-name"} label)))) 377 | 378 | (defn nav [data owner] 379 | (reify 380 | om/IRender 381 | (render [_] 382 | (dom/div #js {:className "float-box--side blue-box nav"} 383 | (if (empty? (:repo (:project data))) 384 | (dom/h3 #js {:className "blue-box__title"} "Asterion") 385 | 386 | (dom/div nil 387 | (dom/h3 #js {:className "project-name"} (:repo (:project data))) 388 | (when (< 1 (count (:graphs data))) 389 | (when-let [build (:build (:nav data))] 390 | (dom/div nil 391 | (dom/span #js {:className "build-title build-name" 392 | :onClick (fn [e] 393 | (raise! data :nav/toggle-builds nil))} 394 | (if (:open? (:nav data)) 395 | (dom/i #js {:className "menu-icon fa fa-caret-down"}) 396 | (dom/i #js {:className "menu-icon fa fa-caret-right"})) 397 | build) 398 | (when (:open? (:nav data)) 399 | (apply dom/ul #js {:className "builds"} 400 | (->> (keys (:graphs data)) 401 | (remove (partial = build)) 402 | (map #(om/build build-item {:data data 403 | :label %})))))))))) 404 | (om/build clear-button data) 405 | (om/build nav-input (:nav data) 406 | {:opts {:on-change (partial raise! data :nav/ns) 407 | :on-enter (fn [e] 408 | (nav-event! "filter" (.. e -target -value)) 409 | (raise! data :nav/draw! nil)) 410 | :title "When you press Enter, I'll remove the ns with names that match these words." 411 | :value-key :ns 412 | :placeholder "filter out namespaces"}}))))) 413 | 414 | (defn graph [buffer owner] 415 | (letfn [(draw! [] 416 | (when-not (empty? @buffer) 417 | (tree/drawTree "#graph" (clj->js @buffer))))] 418 | (reify 419 | om/IRender 420 | (render [_] 421 | (dom/svg #js {:id "graph"} 422 | (dom/g #js {}))) 423 | om/IDidUpdate 424 | (did-update [_ _ _] 425 | (draw!)) 426 | om/IDidMount 427 | (did-mount [_] 428 | (let [vsm (ViewportSizeMonitor.)] 429 | (om/set-state! owner :vsm vsm) 430 | (events/listen vsm events/EventType.RESIZE (fn [e] (draw!)))) 431 | (draw!)) 432 | om/IWillUnmount 433 | (will-unmount [_] 434 | (.unlisten (om/get-state owner :vsm) events/EventType.RESIZE))))) 435 | 436 | ;; TODO: should show a loader 437 | (defn graph-explorer [data owner] 438 | (reify 439 | om/IRender 440 | (render [_] 441 | (dom/div #js {:className "page"} 442 | (om/build nav data) 443 | (when-not (empty? (:errors data)) 444 | (om/build error-card 445 | (assoc data 446 | :error {:title "Error" 447 | :msg (error->msg (first (:errors data)))}) 448 | {:opts {:class "notification float-box error-card" 449 | :close-fn (fn [_] 450 | (raise! data :nav/clear-errors nil))}})) 451 | (om/build graph (:buffer data)))))) 452 | 453 | ;; ====================================================================== 454 | ;; Component Dispatcher 455 | 456 | ;; TODO: should be a multimethod 457 | (defn main [data owner] 458 | (om/component 459 | (if (empty? (:graphs data)) 460 | (om/build select-project data) 461 | (om/build graph-explorer data)))) 462 | 463 | (defn init! [] 464 | (om/root main app-state {:target (.getElementById js/document "app")})) 465 | -------------------------------------------------------------------------------- /src/cljs/asterion/d3.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.d3 2 | (:require [cljsjs.d3])) 3 | 4 | (def d3 js/d3) 5 | -------------------------------------------------------------------------------- /src/cljs/asterion/deps.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.deps 2 | (:require [clojure.set :as set])) 3 | 4 | ;; ====================================================================== 5 | ;; Helpers 6 | 7 | (defn valid-graph? [graph] 8 | (not (empty? (:nodes graph)))) 9 | 10 | (defn filter-graph 11 | "Removes from the graph the nodes and edges that match the namespaces in ns" 12 | [graph ns] 13 | (letfn [(starts-with-any? [s] 14 | (->> ns 15 | (map #(re-find (js/RegExp. %) s)) 16 | (some some?))) 17 | (node-matches? [node] 18 | (starts-with-any? (name (:name node)))) 19 | (edge-matches? [edge] 20 | (->> (vals (select-keys edge [:target :source])) 21 | (map (comp starts-with-any? name)) 22 | (some true?)))] 23 | (-> graph 24 | (update :nodes (comp vec (partial remove node-matches?))) 25 | (update :edges (comp vec (partial remove edge-matches?)))))) 26 | 27 | (defn highlight-graph 28 | "Adds :highlight true to the nodes in the graph that are also in highlighted" 29 | [graph highlighted] 30 | {:pre [(map? graph) (set? highlighted)]} 31 | (let [highlighted (set (map name highlighted))] 32 | (update graph :nodes 33 | (partial mapv #(assoc % :highlight (contains? highlighted (:name %))))))) 34 | -------------------------------------------------------------------------------- /src/cljs/asterion/dir.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.dir 2 | (:require [cljs.node.io :as io] 3 | [om.core :as om :include-macros true] 4 | [om.dom :as dom :include-macros true] 5 | [asterion.components :as components])) 6 | 7 | (defn src? 8 | "Tries to guess if the dir-name is a source directory" 9 | [dir-name] 10 | (some? (re-find #"src" dir-name))) 11 | 12 | (defn ls->srcs [ls] 13 | (->> ls 14 | (filter :selected?) 15 | (map :name) 16 | set)) 17 | 18 | (defn root->list-dir [root] 19 | (->> (io/list-dirs root) 20 | (mapv (fn [f] {:name f})))) 21 | 22 | (declare list-dir) 23 | 24 | (defn dir-item [{:keys [srcs dir]} owner {:keys [click-fn] :as opts}] 25 | (reify 26 | om/IInitState 27 | (init-state [_] 28 | {:expand? false 29 | :ls (root->list-dir (:name dir))}) 30 | om/IRenderState 31 | (render-state [_ {:keys [ls expand?]}] 32 | (let [selected? (contains? srcs (:name dir))] 33 | (dom/li #js {:className "file-item"} 34 | (dom/span #js {:className (str "clickable " 35 | (if selected? 36 | "file-item__text--activated" 37 | "file-item__text")) 38 | :onClick (partial click-fn (:name dir)) 39 | :title (if selected? 40 | "Unselect directory" 41 | "Select directory")} 42 | (io/file-name (:name dir))) 43 | (when-not (empty? ls) 44 | (om/build components/icon-button 45 | {:title "Expand directory" 46 | :icon-class (str "fa-chevron-down " 47 | (if expand? 48 | "expand-icon--active" 49 | "expand-icon"))} 50 | {:opts {:click-fn (fn [_] 51 | (om/update-state! owner :expand? not))}})) 52 | (dom/div #js {:className "divider"} nil) 53 | (when expand? 54 | (om/build list-dir {:srcs srcs :ls ls} {:opts opts}))))))) 55 | 56 | (defn list-dir [{:keys [srcs ls]} owner opts] 57 | (om/component 58 | (apply dom/ul #js {:className "folder-list"} 59 | (map-indexed 60 | (fn [i dir] 61 | (om/build dir-item {:dir dir :srcs srcs} {:opts opts})) 62 | ls)))) 63 | -------------------------------------------------------------------------------- /src/cljs/asterion/project.cljs: -------------------------------------------------------------------------------- 1 | (ns asterion.project 2 | (:require [clojure.string :as str] 3 | [clojure.set :as set] 4 | [cljs.tools.reader.edn :as edn])) 5 | 6 | ;; if (typeof window.DOMParser !== 'undefined') { 7 | ;; return function(str) { 8 | ;; var parser = new window.DOMParser() 9 | ;; return parser.parseFromString(str, 'application/xml') 10 | ;; } 11 | ;; } 12 | 13 | (defn parse-xml [s] 14 | (let [constructor (.-DOMParser js/window) 15 | parser (constructor.)] 16 | (.parseFromString parser s "application/xml"))) 17 | 18 | (enable-console-print!) 19 | 20 | (defn re-pos [re s] 21 | {:pre [(not (string? re)) (string? s)]} 22 | (let [re (js/RegExp. (.-source re) "g")] 23 | (loop [res {}] 24 | (if-let [m (.exec re s)] 25 | (recur (assoc res (.-index m) (first m))) 26 | res)))) 27 | 28 | (defn extract-positions [project-string re] 29 | (->> project-string 30 | (re-pos re) 31 | keys)) 32 | 33 | (defn extract-exp-at [project-string re pos] 34 | (-> project-string 35 | (subs pos) 36 | (str/replace re "") 37 | edn/read-string)) 38 | 39 | (defn normalize-builds [builds] 40 | (cond 41 | (map? builds) (mapv (fn [[k v]] (assoc v :id (name k))) builds) 42 | (vector? builds) builds 43 | :else (throw (js/Error. "Builds should be maps or vectors")))) 44 | 45 | (defn parse-pom-name [pom-string] 46 | (-> (parse-xml pom-string) 47 | (.getElementsByTagName "artifactId") 48 | (aget 0) 49 | .-textContent)) 50 | 51 | (defn parse-project-name [project-string] 52 | (let [re #"defproject"] 53 | (->> re 54 | (extract-positions project-string) 55 | first 56 | (extract-exp-at project-string re) 57 | name))) 58 | 59 | (comment 60 | (parse-pom-name "tools.namespace")) 61 | 62 | ;; TODO: move to test 63 | (comment 64 | (.log js/console (parse-name "(defproject komunike \"0.9.0-SNAPSHOT)")) 65 | (.log js/console (parse-name "(defproject org.omcljs/om \"0.9.0-SNAPSHOT)"))) 66 | 67 | (defn parse-main-srcs [project-string] 68 | {:pre [(string? project-string)] 69 | :post [(set? %) (every? string? %)]} 70 | (let [re #":source-paths"] 71 | (->> (extract-positions project-string re) 72 | first 73 | (extract-exp-at project-string re) 74 | set))) 75 | 76 | (defn parse-builds-srcs [project-string] 77 | (let [re #":cljsbuild"] 78 | (->> (extract-positions project-string re) 79 | (map (partial extract-exp-at project-string re)) 80 | ;; First :cljsbuild occurrence (mulitple 81 | ;; profiles,multiple builds) 82 | first 83 | :builds 84 | normalize-builds 85 | ;; ;; First build out all the possible builds 86 | first 87 | :source-paths 88 | set))) 89 | 90 | (defn parse [project-string] 91 | (set/union (parse-main-srcs project-string) 92 | (parse-builds-srcs project-string))) 93 | -------------------------------------------------------------------------------- /src/cljs/asterion/ring.js: -------------------------------------------------------------------------------- 1 | goog.provide('asterion.ring'); 2 | 3 | /** 4 | * Class for Ring 5 | * @param initArray 6 | * @constructor 7 | * @template T 8 | */ 9 | asterion.ring.Ring = function(initArray) { 10 | /** 11 | * Internal index 12 | * @private {number} 13 | */ 14 | this.index = 0; 15 | /** 16 | * Underlying array 17 | * @private {!Array.} 18 | */ 19 | this.array = initArray.slice(); 20 | }; 21 | 22 | /** 23 | * Gets the next value in the ring 24 | **/ 25 | asterion.ring.Ring.prototype.next = function() { 26 | this.index = (this.index < this.array.length) ? this.index : 0; 27 | var out = this.array[this.index]; 28 | this.index = this.index + 1; 29 | return out; 30 | }; 31 | 32 | /** 33 | * @param {Ring} initRing 34 | * @return {Function} 35 | */ 36 | asterion.ring.memoize = function(initRing) { 37 | var dict = {}; 38 | return function(val) { 39 | if (typeof dict[val] === "undefined") { 40 | dict[val] = initRing.next(); 41 | }; 42 | return dict[val]; 43 | }; 44 | }; 45 | 46 | -------------------------------------------------------------------------------- /src/cljs/asterion/tree.js: -------------------------------------------------------------------------------- 1 | goog.provide('asterion.tree'); 2 | 3 | goog.require('goog.array'); 4 | goog.require('asterion.d3'); 5 | goog.require('asterion.ring'); 6 | 7 | asterion.tree.config = function() { 8 | return { 9 | w: window.innerWidth, 10 | h: window.outerHeight, 11 | rx: 60, 12 | ry: 30, 13 | fill: d3.scale.category20() 14 | }; 15 | }; 16 | 17 | asterion.tree.svg = function(nodeId) { 18 | var config = asterion.tree.config(); 19 | return d3.select(nodeId).attr("width", config.w).attr("height", config.h); 20 | }; 21 | 22 | asterion.tree.formatNs = function(s) { 23 | return s.split(".").join("\n"); 24 | }; 25 | 26 | asterion.tree.colors = new asterion.ring.Ring(palette('tol-rainbow',12).reverse()); 27 | 28 | asterion.tree.groupToColor = asterion.ring.memoize(asterion.tree.colors); 29 | 30 | asterion.tree.nodeToGroup = function(name) { 31 | // TODO: looking at the second token works for libraries, not for apps 32 | var parts = name.split("\."); 33 | if (parts.length > 1) { 34 | return parts[1].replace("-",""); 35 | } else { 36 | return name; 37 | } 38 | }; 39 | 40 | asterion.tree.nodeToSubGroup = function(name) { 41 | var tokens = name.split("\."); 42 | return (tokens.length > 2) ? tokens[1] : null; 43 | }; 44 | 45 | asterion.tree.Graph = function(json) { 46 | var config = asterion.tree.config(); 47 | var g = new dagreD3.graphlib.Graph().setGraph({}); 48 | 49 | json.nodes.forEach(function(node) { 50 | var group = asterion.tree.nodeToGroup(node.name); 51 | var color = asterion.tree.groupToColor(group); 52 | var hgStyle = ""; 53 | var labelStyle = ""; 54 | if (node.highlight) { 55 | labelStyle = "fill:#f0f1eb"; 56 | hgStyle = "fill:black;"; 57 | } 58 | g.setNode(node.name, { 59 | label: node.name, 60 | labelStyle: labelStyle, 61 | style: "fill:#" + color + ";stroke:black;" + hgStyle}); 62 | }); 63 | 64 | json.edges.forEach(function(edge) { 65 | g.setEdge(edge.source, edge.target,{}); 66 | }); 67 | g.nodes().forEach(function(v) { 68 | var node = g.node(v); 69 | if (typeof node !== 'undefined') { 70 | node.rx = config.rx; 71 | node.ry = config.ry; 72 | } 73 | }); 74 | return g; 75 | }; 76 | 77 | asterion.tree.drawTree = function(nodeId, nsOpts, json) { 78 | var g = asterion.tree.Graph(nsOpts, json); 79 | var root = asterion.tree.svg(nodeId); 80 | var inner = root.select("g"); 81 | var zoom = d3.behavior.zoom().on("zoom", function() { 82 | inner.attr("transform", "translate(" + d3.event.translate + ")" + 83 | "scale(" + d3.event.scale + ")"); 84 | }); 85 | root.call(zoom); 86 | var render = new dagreD3.render(); 87 | render(inner, g); 88 | 89 | var initialScale = 0.75; 90 | zoom.translate([(root.attr("width") - g.graph().width * initialScale) / 2, 20]) 91 | .scale(initialScale) 92 | .event(root); 93 | // root.attr("height", g.graph().height * initialScale + 40); 94 | }; 95 | -------------------------------------------------------------------------------- /src/cljs/cljs/node/io.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.node.io 2 | "Provides file utilities" 3 | (:require [cljs.nodejs])) 4 | 5 | (def path (js/require "path")) 6 | 7 | (def fs (js/require "fs")) 8 | 9 | (defn file->folder [p] 10 | (path.dirname p)) 11 | 12 | (defn extension [p] 13 | (path.extname p)) 14 | 15 | (defn join-paths [& args] 16 | (apply path.join args)) 17 | 18 | (defn list-files [dir] 19 | (mapv (partial join-paths dir) (into-array (fs.readdirSync dir)))) 20 | 21 | (defn dir? [d] 22 | (try 23 | (.isDirectory (fs.lstatSync d)) 24 | (catch js/Object _ false))) 25 | 26 | (defn file-name [p] 27 | (.-name (path.parse p))) 28 | 29 | (defn list-dirs [dir] 30 | (vec (filter dir? (list-files dir)))) 31 | 32 | (defn read-file [file] 33 | (fs.readFileSync file "utf8")) 34 | --------------------------------------------------------------------------------