├── src ├── assets │ └── .placeholder ├── data │ ├── .placeholder │ └── graphic.csv ├── js │ ├── config.js │ ├── detectFeatures.js │ ├── utils.js │ ├── share.js │ ├── fm.js │ └── thing.js ├── favicon.ico ├── jade │ ├── includes │ │ ├── test-pattern.jade │ │ └── share.jade │ ├── thing.jade │ └── index.jade └── styl │ ├── qz │ ├── buttons.styl │ ├── share.styl │ ├── test-pattern.styl │ ├── colors.styl │ ├── dropdowns.styl │ ├── icons.styl │ ├── chart-elements.styl │ └── type.styl │ ├── main.styl │ ├── thing.styl │ ├── layout.styl │ ├── responsive.styl │ └── normalize.styl ├── .gitignore ├── README.md ├── gulp ├── cli.js ├── utils.js └── config.js ├── content.json ├── __build.sh ├── package.json └── gulpfile.js /src/assets/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/config.js: -------------------------------------------------------------------------------- 1 | var ENV = '/* @echo ENV */'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | build 3 | !node_modules/stylus-normalize 4 | .DS_Store 5 | .tmp 6 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quartz/inequality-and-unions/master/src/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inequality-and-unions 2 | 3 | The relationship between inequality and unions in the OECD. 4 | -------------------------------------------------------------------------------- /src/jade/includes/test-pattern.jade: -------------------------------------------------------------------------------- 1 | #test-pattern 2 | .circ.corner.t.l 3 | .circ.corner.t.r 4 | .circ.corner.b.l 5 | .circ.corner.b.r 6 | .circ.cent -------------------------------------------------------------------------------- /src/jade/thing.jade: -------------------------------------------------------------------------------- 1 | h2 The correlation between union membership and inequality 2 | 3 | div#graphic 4 | 5 | div.footnotes Data: OECD. (TKTK link) 6 | -------------------------------------------------------------------------------- /gulp/cli.js: -------------------------------------------------------------------------------- 1 | var opts = require('nomnom') 2 | .option('build', { 3 | abbr: 'b', 4 | help: 'Build project. [local | move | commit | push]', 5 | choices: ['local', 'move', 'commit', 'push'] 6 | }) 7 | .option('dont-minify', { 8 | abbr: 'd', 9 | flag: true, 10 | help: 'Prevent build from minifying your js' 11 | }); 12 | 13 | module.exports = opts; 14 | -------------------------------------------------------------------------------- /src/jade/includes/share.jade: -------------------------------------------------------------------------------- 1 | ul.share-buttons 2 | li.share-icon.twitter 3 | a(title='Tweet' class='share-action icon icon-twitter' target='_blank') 4 | li.share-icon.facebook 5 | a(title='Share on Facebook' class='share-action icon icon-facebook' target='_blank') 6 | li.share-icon.linkedin 7 | a(title='Share on LinkedIn' class='share-action icon icon-linkedin' target='_blank') 8 | li.share-icon.email 9 | a(title='Email' class='share-action icon icon-email' target='_blank') 10 | -------------------------------------------------------------------------------- /src/styl/qz/buttons.styl: -------------------------------------------------------------------------------- 1 | qz-button-group() 2 | text-align center 3 | button 4 | color white 5 | border none 6 | background-color $qz-gray-1 7 | font-family $qz-sans 8 | border-radius 3px 9 | margin 2px 4px 2px 0 10 | vertical-align middle 11 | padding 0px 8px 12 | line-height 2em 13 | display inline-block 14 | &.active 15 | //font-family $qz-sans-bold 16 | background-color $qz-purp-2 17 | transition background-color 0.2s linear 18 | &:focus 19 | outline 0 20 | -------------------------------------------------------------------------------- /gulp/utils.js: -------------------------------------------------------------------------------- 1 | function generateShellCmd (buildArg, qzdataPath, thingName) { 2 | var baseCmd = "./__build.sh " + qzdataPath + " " + thingName + " "; // then commit? push? 3 | 4 | switch (buildArg) { 5 | case "move": 6 | return baseCmd + "false false"; 7 | case "commit": 8 | return baseCmd + "true false"; 9 | case "push": 10 | return baseCmd + "true true"; 11 | default: 12 | return 'echo ""'; 13 | } 14 | } 15 | 16 | module.exports = { 17 | generateShellCmd: generateShellCmd 18 | }; 19 | -------------------------------------------------------------------------------- /src/js/detectFeatures.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var features = {}; 3 | 4 | features.hasDeviceMotion = 'ondevicemotion' in window; 5 | features.isAndroid = (/android/gi).test(navigator.appVersion); 6 | features.isIDevice = (/iphone|ipad/gi).test(navigator.appVersion); 7 | features.isTouchPad = (/hp-tablet/gi).test(navigator.appVersion); 8 | features.isKindle = (/silk/gi).test(navigator.appVersion); 9 | features.hasTouchEvents = ( 10 | features.isAndroid || 11 | features.isIDevice || 12 | features.isTouchPad || 13 | features.isKindle 14 | ); 15 | 16 | return features; 17 | }; 18 | -------------------------------------------------------------------------------- /src/styl/main.styl: -------------------------------------------------------------------------------- 1 | // Normalize -- https://github.com/bymathias/normalize.styl 2 | @import 'normalize' 3 | 4 | // nib -- http://visionmedia.github.io/nib/ 5 | @import 'nib' 6 | 7 | // Qz modules. Comment out the things you don't need. 8 | @import 'qz/type' 9 | @import 'qz/colors' 10 | 11 | // Optional components. Uncomment if you need them. See wiki for usage info 12 | // note: `share` depends on `icons` 13 | //@import 'qz/icons' 14 | //@import 'qz/share' 15 | //@import 'qz/dropdowns' 16 | //@import 'qz/buttons' 17 | //@import 'qz/chart-elements' 18 | 19 | // Thing-specific css 20 | @import 'responsive' 21 | @import 'layout' 22 | @import 'thing' 23 | -------------------------------------------------------------------------------- /content.json: -------------------------------------------------------------------------------- 1 | { 2 | "hed": "This Quartz Thing has not connected to a Google Doc", 3 | "dek": "Perhaps you forgot to start the server? Make viewable to anyone with the link?", 4 | "items": [{ 5 | "copy": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." 6 | }, { 7 | "copy": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." 8 | }, { 9 | "copy": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum" 10 | }] 11 | } 12 | -------------------------------------------------------------------------------- /src/styl/qz/share.styl: -------------------------------------------------------------------------------- 1 | // Icon-related colors 2 | $color-twitter = #00aced 3 | $color-facebook = #3b5998 4 | $color-linkedin = #007bb6 5 | $color-email = #0096b6 6 | $color-icon-hover = #c3c5c0 7 | 8 | ul.share-buttons 9 | list-style-type none 10 | margin 0 11 | padding 0 12 | 13 | li.share-icon 14 | display inline-block 15 | font-size 1.2em 16 | a 17 | text-decoration none 18 | cursor pointer 19 | 20 | .icon-email 21 | color $color-email 22 | &:hover 23 | color $color-icon-hover 24 | 25 | .icon-facebook 26 | color $color-facebook 27 | &:hover 28 | color $color-icon-hover 29 | 30 | .icon-linkedin 31 | color $color-linkedin 32 | &:hover 33 | color $color-icon-hover 34 | 35 | .icon-twitter 36 | color $color-twitter 37 | &:hover 38 | color $color-icon-hover 39 | -------------------------------------------------------------------------------- /src/jade/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 6 | title inequality-and-unions 7 | meta(name='description', content='The relationship between inequality and unions in the OECD.') 8 | meta(name='viewport', content='width=device-width, initial-scale=1') 9 | link(rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Serif:400,700,400italic") 10 | link(rel='stylesheet', href='css/main.css?t=' + build_info.timestamp) 11 | link(rel="icon" href="/favicon.ico") 12 | script(src='https://app.qz.com/js/frameMessager/min/frameMessager.min.js') 13 | 14 | body 15 | base(target="_blank") 16 | div.item-body 17 | div#interactive-content 18 | include thing.jade 19 | script(src='js/config.js?t=' + build_info.timestamp) 20 | script(src='js/thing.js?t=' + build_info.timestamp) 21 | -------------------------------------------------------------------------------- /src/styl/thing.styl: -------------------------------------------------------------------------------- 1 | @import 'qz/chart-elements' 2 | 3 | #annotation-background 4 | fill: $qz-background-gray 5 | 6 | #annotation 7 | font-family: $qz-sans-light 8 | fill: $qz-gray-2 9 | 10 | .dot 11 | fill $qz-purp-3 12 | 13 | .fit-line 14 | fill none 15 | stroke-width 2px 16 | stroke $qz-gray-1 17 | stroke-linecap round 18 | stroke-linejoin round 19 | 20 | .arrows path 21 | stroke-width: 1.5px 22 | stroke: $qz-body-black 23 | fill: none 24 | 25 | .labels text 26 | fill: $qz-body-black 27 | font-family: $qz-sans-bold 28 | -webkit-font-smoothing: antialiased 29 | font-size: 14px 30 | 31 | #x-label 32 | fill: $qz-gray-2 33 | font-family $qz-sans-light 34 | 35 | 36 | .footnotes 37 | font-size: 0.8em 38 | font-family: $qz-sans 39 | color: $qz-gray-2 40 | padding-left: 20px 41 | padding-bottom: 20px 42 | 43 | .clear 44 | clear:both 45 | 46 | @media only screen and (max-width $width-mobile) 47 | .arrows path 48 | stroke-width 1px 49 | 50 | .labels text 51 | font-family $qz-sans 52 | -------------------------------------------------------------------------------- /src/data/graphic.csv: -------------------------------------------------------------------------------- 1 | "country","gini","pct_unionized" 2 | "ICE","24.37306701","85.5 " 3 | "NO","25.2","52.1 " 4 | "DEN","25.39","66.8 " 5 | "SLV","25.5470453","21.2 " 6 | "FN","25.735","69.0 " 7 | "CZ","26.16899727","12.7 " 8 | "BEL","26.75199686","55.1 " 9 | "SVK","26.91359694","13.3 " 10 | "AUT","27.96277636","27.8 " 11 | "SWE","28.0804","67.7 " 12 | "LUX","28.14576261","32.8 " 13 | "NED","28.3","17.8 " 14 | "HU","28.767","10.5 " 15 | "GE","29.215","18.1 " 16 | "FR","29.4","7.7 " 17 | "SWZ","29.5402653","16.2 " 18 | "POL","29.97268518","12.7 " 19 | "SK","30.23756836","10.1 " 20 | "IR","30.90000212","29.6 " 21 | "CA","32.18378515","27.1 " 22 | "IT","32.54833407","37.3 " 23 | "JP","33","17.8 " 24 | "NZ","33.3","19.8 " 25 | "AUS","33.7","17.0 " 26 | "POR","34.15638896","18.9 " 27 | "GR","34.32115027","21.5 " 28 | "SP","34.59415976","16.9 " 29 | "UK","35.8","25.8 " 30 | "EST","36.10388952","5.7 " 31 | "IS","36.45","22.8 " 32 | "TU","39.3","6.3 " 33 | "US","39.381632","10.8 " 34 | "MX","45.93388","13.6 " 35 | "CHL","46.5","15.0 " 36 | -------------------------------------------------------------------------------- /src/styl/layout.styl: -------------------------------------------------------------------------------- 1 | // Page layout setup 2 | 3 | html, body 4 | min-width 270px 5 | font-family $qz-font-body 6 | color $qz-body-black 7 | background-color $qz-background-gray 8 | 9 | h1, h2, h4, h5 10 | font-family $qz-sans-bold 11 | color $qz-header-black 12 | 13 | h1 14 | line-height 50px 15 | margin-bottom 10px 16 | 17 | h2 18 | font-size 24px 19 | line-height 24px 20 | font-style bold 21 | margin 40px 0px 22 | 23 | h3 24 | font-size 20px 25 | font-family $qz-serif 26 | color: $qz-header-black 27 | font-weight: bold; 28 | 29 | p 30 | position relative 31 | margin 0 0 0 0 32 | padding 16px 0 16px 0 33 | line-height 1.6 34 | color #4c4c4c 35 | 36 | .item-body 37 | margin 0 auto 38 | //max-width 940px 39 | 40 | .clearfix 41 | clear both 42 | 43 | #interactive-content 44 | width 100% 45 | margin-left auto 46 | margin-right auto 47 | 48 | .copy-wrap 49 | width 86.25% 50 | max-width 940px 51 | margin 0 auto 52 | 53 | .copy 54 | max-width 640px 55 | width 100% 56 | float right 57 | -------------------------------------------------------------------------------- /src/styl/qz/test-pattern.styl: -------------------------------------------------------------------------------- 1 | #test-pattern 2 | .circ 3 | // border 2px solid $qz-body-black 4 | border-radius 1000vw 5 | box-sizing border-box 6 | 7 | &.cent 8 | width 100% 9 | height 100% 10 | margin 0 auto 11 | position absolute 12 | border 4px solid $qz-body-black 13 | 14 | &.corner 15 | width 25vw 16 | height 25vw 17 | position absolute 18 | background-color alpha($qz-purp-2, 0.8) 19 | 20 | &.t 21 | top 0 22 | &.b 23 | bottom 0 24 | &.r 25 | right 0 26 | &.l 27 | left 0 28 | 29 | #interactive-content 30 | &[data-frame="0"] 31 | #test-pattern .circ.corner 32 | background-color alpha($qz-gray-1, 0.8) 33 | 34 | &[data-frame="1"] 35 | #test-pattern .circ.corner 36 | background-color alpha($qz-purp-2, 0.8) 37 | 38 | &[data-frame="2"] 39 | #test-pattern .circ.corner 40 | background-color alpha($qz-blue-2, 0.8) 41 | 42 | &[data-frame="3"] 43 | #test-pattern .circ.corner 44 | background-color alpha($qz-green-3, 0.8) 45 | 46 | 47 | -------------------------------------------------------------------------------- /__build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CURR_COMMIT=$(git log --oneline -n 1) 3 | 4 | printf '%s\n' "Copying built files to master repo" 5 | printf '%s\n' ------------------------- 6 | rsync -rtvu --delete ./build/* $1 7 | 8 | cd $1 9 | QZDATA_BRANCH=$(git rev-parse --abbrev-ref HEAD) 10 | 11 | if [[ "$3" = true ]]; then 12 | printf '%s\n' "Committing ${CURR_COMMIT} to origin:${QZDATA_BRANCH}" 13 | printf '%s\n' ------------------------- 14 | git add . 15 | git commit -m "built: ${2} | ${CURR_COMMIT}" 16 | fi 17 | 18 | if [[ "$4" = true ]]; then 19 | printf '%s\n' "Executing dry run of git push to origin:${QZDATA_BRANCH}" 20 | printf '%s\n' ------------------------- 21 | echo `git push --dry-run origin ${QZDATA_BRANCH}` 22 | printf '%s\n' "That was a dry run to origin:${QZDATA_BRANCH}. Actually push? (y/n)" 23 | read cont -1 23 | if (ENV == 'prod' || local_prod_test) { 24 | if(local_prod_test) { 25 | PARENT_DOMAIN = "localhost:3000" 26 | } 27 | FM = frameMessager( 28 | _.assign({}, 29 | {parentDomain : PARENT_DOMAIN}, 30 | options 31 | ) 32 | ); 33 | 34 | document.body.style.overflow = "hidden"; 35 | // Test environment: no frame messenging 36 | } else { 37 | document.body.style.border = "#ff8080"; 38 | } 39 | } 40 | 41 | /** 42 | * Compute the height of the interactive. 43 | */ 44 | /** 45 | * Get height of window including margin 46 | */ 47 | function _getDocumentHeight() { 48 | var height = interactiveEl.offsetHeight; 49 | var style = getComputedStyle(interactiveEl); 50 | 51 | return height + parseInt(style.marginTop) + parseInt(style.marginBottom); 52 | } 53 | 54 | /** 55 | * Update parent height. 56 | */ 57 | function updateHeight (height) { 58 | if (!FM) { 59 | return; 60 | } 61 | 62 | height = height || _getDocumentHeight(); 63 | 64 | FM.triggerMessage("QZParent", "child:updateHeight", { 65 | height : height 66 | }); 67 | 68 | return; 69 | } 70 | 71 | /** 72 | * Update parent hash. 73 | */ 74 | function updateHash (hash) { 75 | if (!FM) { 76 | window.location.hash = hash; 77 | } else { 78 | FM.triggerMessage("QZParent", "child:updateHash", { 79 | hash : hash 80 | }); 81 | } 82 | return; 83 | } 84 | 85 | /** 86 | * Read parent hash. 87 | */ 88 | function getWindowProps () { 89 | if (!FM) { 90 | var frame = document.getElementById("interactive-content") 91 | var frame_bb = frame.getBoundingClientRect() 92 | 93 | var clientWidth = window.innerWidth; 94 | var clientHeight = window.innerHeight; 95 | 96 | return propCallback({ 97 | action: "parent:readWindowProps", 98 | fromId: "QZParent", 99 | toId: "interactive-local", 100 | data: { 101 | windowProps: { 102 | clientDimensions: { 103 | width: clientWidth, 104 | height: clientHeight, 105 | }, 106 | pageOffset: { 107 | x: window.scrollX, 108 | y: window.scrollY 109 | }, 110 | uri: { 111 | hash: window.location.hash, 112 | href: window.location.href, 113 | origin: window.location.origin, 114 | pathname: window.location.pathname 115 | } 116 | } 117 | } 118 | }); 119 | } 120 | 121 | FM.triggerMessage("QZParent", "child:getWindowProps"); 122 | 123 | return; 124 | } 125 | 126 | function setupItemScroll(callback) { 127 | if(!FM) { 128 | var frame = document.getElementById("interactive-content") 129 | 130 | window.onscroll = function(){ 131 | var rect = frame.getBoundingClientRect(); 132 | var windowHeight = window.innerHeight; 133 | 134 | callback({ 135 | "action":"itemWell:scroll", 136 | "data":{ 137 | "frameTop":{ 138 | "nav": rect.top, 139 | "window": windowHeight - rect.top 140 | }, 141 | "frameBottom":{ 142 | "nav": rect.bottom, 143 | "window": windowHeight - rect.bottom 144 | }, 145 | "scrollDepth": document.body.scrollTop, 146 | "viewable":true, 147 | "visible":true 148 | }, 149 | "fromId":"QZParent", 150 | "toId":"interactive-local" 151 | }) 152 | } 153 | } else { 154 | FM.onMessage("itemWell:scroll", callback) 155 | } 156 | } 157 | 158 | /** 159 | * Set up a callback that will handle incoming hash data 160 | */ 161 | function setupReadWindow(callback) { 162 | if (!FM) { 163 | propCallback = callback; 164 | } else { 165 | FM.onMessage("parent:readWindowProps", callback); 166 | } 167 | } 168 | 169 | /** 170 | * Resize the parent to match the new child height. 171 | */ 172 | function resize () { 173 | updateHeight(_getDocumentHeight()); 174 | } 175 | 176 | /** 177 | * Get height of window including margin 178 | */ 179 | function _getWindowHeight() { 180 | var height = interactiveEl.offsetHeight; 181 | var style = getComputedStyle(interactiveEl); 182 | 183 | return height + parseInt(style.marginTop) + parseInt(style.marginBottom); 184 | } 185 | 186 | /** 187 | * Scroll the parent window to a given location. 188 | * 189 | * Call like this: 190 | * fm.scrollToPosition($("#scrollToThisDiv").offset().top,500) 191 | * 192 | * Where 500 is the duration of the scroll animation 193 | */ 194 | function scrollToPosition (position,duration) { 195 | if (!FM) { 196 | d3.transition() 197 | .delay(0) 198 | .duration(duration) 199 | .tween("scroll", scrollTween(position)); 200 | } else { 201 | FM.triggerMessage("QZParent", "child:scrollToPosition", { 202 | position : position, 203 | duration : 500 204 | }); 205 | } 206 | } 207 | 208 | 209 | function scrollTween(offset) { 210 | return function() { 211 | var i = d3.interpolateNumber(window.pageYOffset || document.documentElement.scrollTop, offset); 212 | return function(t) { scrollTo(0, i(t)); }; 213 | }; 214 | } 215 | 216 | /** 217 | * Get a reference to the parent window. 218 | */ 219 | function getParentWindow () { 220 | return FM.triggerMessage("QZParent", "child:getWindow"); 221 | } 222 | 223 | // setupFrameMessenger(); 224 | 225 | module.exports = { 226 | setup: setupFrameMessenger, 227 | updateHeight: updateHeight, 228 | resize: resize, 229 | scrollToPosition: scrollToPosition, 230 | getParentWindow: getParentWindow, 231 | updateHash: updateHash, 232 | getWindowProps: getWindowProps, 233 | setupReadWindow: setupReadWindow, 234 | setupItemScroll: setupItemScroll 235 | }; 236 | -------------------------------------------------------------------------------- /src/js/thing.js: -------------------------------------------------------------------------------- 1 | // NPM modules 2 | var _ = {}; 3 | _.assign = require('lodash.assign'); 4 | 5 | var d3 = _.assign({}, 6 | require("d3-selection"), 7 | require("d3-request"), 8 | require("d3-scale"), 9 | require("d3-axis"), 10 | require("d3-shape"), 11 | require("d3-array"), 12 | require("d3-format"), 13 | require("d3-time-format") 14 | ); 15 | 16 | d3.getEvent = function(){ return require("d3-selection").event}.bind(this); 17 | 18 | 19 | // Local modules 20 | var features = require('./detectFeatures')(); 21 | var fm = require('./fm'); 22 | var utils = require('./utils'); 23 | 24 | // Globals 25 | var DEFAULT_WIDTH = 940; 26 | var MOBILE_BREAKPOINT = 600; 27 | 28 | var LABEL_DEFAULTS = { 29 | 'text-anchor': 'middle', 30 | 'font-size': 0.8, 31 | 'rotate': 0 32 | }; 33 | 34 | var LABELS = [ 35 | { 36 | 'country': 'Iceland', 37 | 'gini': 32, 38 | 'pct_unionized': 77.5, 39 | 'text-anchor': 'end' 40 | }, 41 | { 42 | 'country': 'Chile', 43 | 'gini': 46.5, 44 | 'pct_unionized': 16, 45 | 'text-anchor': 'start' 46 | }, 47 | { 48 | 'country': 'USA', 49 | 'gini': 39.2, 50 | 'pct_unionized': 11.75, 51 | 'text-anchor': 'start' 52 | } 53 | ]; 54 | 55 | var ARROWS = [ 56 | // Iceland 57 | { 58 | 'path': [ 59 | [78, 32], 60 | [84, 30], 61 | [85.5, 26] 62 | ] 63 | } 64 | ]; 65 | 66 | var FIT_SLOPE = -0.136362972; 67 | var FIT_INTERCEPT = 35.20618078; 68 | 69 | var graphicData = null; 70 | var isMobile = false; 71 | 72 | /** 73 | * Initialize the graphic. 74 | * 75 | * Fetch data, format data, cache HTML references, etc. 76 | */ 77 | function init() { 78 | d3.csv('data/graphic.csv', function(error, data) { 79 | graphicData = formatData(data); 80 | 81 | fm.setup() 82 | 83 | render(); 84 | window.addEventListener("resize", utils.throttle(onResize, 250), true); 85 | }); 86 | } 87 | 88 | /** 89 | * Format data or generate any derived variables. 90 | */ 91 | function formatData(data) { 92 | data.forEach(function(d) { 93 | d['gini'] = +d['gini']; 94 | d['pct_unionized'] = +d['pct_unionized']; 95 | }); 96 | 97 | return data; 98 | } 99 | 100 | /** 101 | * Invoke on resize. By default simply rerenders the graphic. 102 | */ 103 | function onResize() { 104 | render(); 105 | } 106 | 107 | /** 108 | * Figure out the current frame size and render the graphic. 109 | */ 110 | function render() { 111 | var width = d3.select("#interactive-content").node().getBoundingClientRect().width; 112 | 113 | if (width <= MOBILE_BREAKPOINT) { 114 | isMobile = true; 115 | } else { 116 | isMobile = false; 117 | } 118 | 119 | renderGraphic({ 120 | container: '#graphic', 121 | width: width, 122 | data: graphicData 123 | }); 124 | 125 | // Inform parent frame of new height 126 | fm.resize() 127 | } 128 | 129 | /* 130 | * Render the graphic. 131 | */ 132 | function renderGraphic(config) { 133 | // Configuration 134 | var aspectRatio = 16 / 9; 135 | 136 | var margins = { 137 | top: 10, 138 | right: 30, 139 | bottom: 50, 140 | left: 30 141 | }; 142 | 143 | // Calculate actual chart dimensions 144 | var width = config['width']; 145 | var height = width / aspectRatio; 146 | 147 | var chartWidth = width - (margins['left'] + margins['right']); 148 | var chartHeight = height - (margins['top'] + margins['bottom']); 149 | 150 | // Clear existing graphic (for redraw) 151 | var containerElement = d3.select(config['container']); 152 | containerElement.html(''); 153 | 154 | // Create the root SVG element 155 | var chartWrapper = containerElement.append('div') 156 | .attr('class', 'graphic-wrapper'); 157 | 158 | var chartElement = chartWrapper.append('svg') 159 | .attr('width', chartWidth + margins['left'] + margins['right']) 160 | .attr('height', chartHeight + margins['top'] + margins['bottom']) 161 | .append('g') 162 | .attr('transform', 'translate(' + margins['left'] + ',' + margins['top'] + ')'); 163 | 164 | // Create scales 165 | var xScale = d3.scaleLinear() 166 | .range([0, chartWidth]) 167 | .domain([0, 100]); 168 | 169 | var yScale = d3.scaleLinear() 170 | .range([chartHeight, 0]) 171 | .domain([0, 60]); 172 | 173 | // Create axes 174 | var xAxis = d3.axisBottom() 175 | .scale(xScale) 176 | .ticks(5) 177 | .tickFormat(function(d) { 178 | return d.toFixed(0) + '%'; 179 | }) 180 | 181 | var yAxis = d3.axisLeft() 182 | .scale(yScale) 183 | .ticks(5) 184 | .tickFormat(function(d) { 185 | return d.toFixed(0); 186 | }) 187 | 188 | // Render axes 189 | var xAxisElement = chartElement.append('g') 190 | .attr('class', 'x axis') 191 | .attr('transform', "translate(" + [0, chartHeight] + ")") 192 | .call(xAxis); 193 | 194 | var yAxisElement = chartElement.append('g') 195 | .attr('class', 'y axis') 196 | .call(yAxis); 197 | 198 | // Render axes grids 199 | var xAxisGrid = function() { 200 | return xAxis; 201 | }; 202 | 203 | xAxisElement.append('g') 204 | .attr('class', 'x grid') 205 | .call(xAxisGrid() 206 | .tickSize(-chartHeight, 0) 207 | .tickFormat('') 208 | ); 209 | 210 | var yAxisGrid = function() { 211 | return yAxis; 212 | }; 213 | 214 | yAxisElement.append('g') 215 | .attr('class', 'y grid') 216 | .call(yAxisGrid() 217 | .tickSize(-chartWidth, 0) 218 | .tickFormat('') 219 | ); 220 | 221 | var fitLine = d3.line() 222 | .x(function(d) { 223 | return xScale(d['pct_unionized']); 224 | }) 225 | .y(function(d) { 226 | return yScale(FIT_SLOPE * d['pct_unionized'] + FIT_INTERCEPT); 227 | }) 228 | 229 | chartElement.append("path") 230 | .datum(config['data']) 231 | .attr("class", "fit-line") 232 | .attr("d", fitLine); 233 | 234 | chartElement.append('g') 235 | .attr('class', 'dots') 236 | .selectAll('circle') 237 | .data(config['data']) 238 | .enter() 239 | .append('circle') 240 | .attr('r', isMobile ? 3 : 5) 241 | .attr('cx', function(d) { 242 | return xScale(d['pct_unionized']); 243 | }) 244 | .attr('cy', function(d) { 245 | return yScale(d['gini']); 246 | }) 247 | .attr('class', 'dot') 248 | .attr('id', function(d) { 249 | return utils.classify(d['country']); 250 | }) 251 | 252 | /* 253 | * Render labels and arrows. 254 | */ 255 | chartElement.append('defs') 256 | .append('marker') 257 | .attr('id','arrowhead') 258 | .attr('orient','auto') 259 | .attr('viewBox','0 0 5.108 8.18') 260 | .attr('markerHeight','8.18') 261 | .attr('markerWidth','5.108') 262 | .attr('orient','auto') 263 | .attr('refY','4.09') 264 | .attr('refX','5') 265 | .append('polygon') 266 | .attr('points','0.745,8.05 0.07,7.312 3.71,3.986 0.127,0.599 0.815,-0.129 5.179,3.999') 267 | .attr('fill','#4C4C4C') 268 | 269 | var arrowLine = d3.line() 270 | .curve(d3.curveNatural) 271 | .x(function(d) { 272 | return xScale(d[0]); 273 | }) 274 | .y(function(d) { 275 | return yScale(d[1]); 276 | }); 277 | 278 | var arrows = chartElement.append('g') 279 | .attr('class', 'arrows'); 280 | 281 | arrows.selectAll('path') 282 | .data(ARROWS) 283 | .enter().append('path') 284 | .attr('d', function(d) { return arrowLine(d['path']); }) 285 | .style('marker-end', 'url(#arrowhead)'); 286 | 287 | var labels = chartElement.append('g') 288 | .attr('class', 'labels'); 289 | 290 | labels.selectAll('text') 291 | .data(LABELS) 292 | .enter().append('text') 293 | .attr('x', function(d) { 294 | return xScale(d['pct_unionized']); 295 | }) 296 | .attr('y', function(d) { 297 | return yScale(d['gini']) 298 | }) 299 | .attr('text-anchor', function(d) { 300 | return d['text-anchor'] || LABEL_DEFAULTS['text-anchor']; 301 | }) 302 | .style('alignment-baseline', function(d) { 303 | return 'middle'; 304 | }) 305 | .html(function(d) { 306 | return d['country']; 307 | }); 308 | 309 | var annotation = chartElement.append('text') 310 | .attr('id', 'annotation') 311 | .attr('x', function(d) { 312 | return xScale(0) + 5; 313 | }) 314 | .attr('y', function(d) { 315 | return yScale(60) + 2 316 | }) 317 | .attr('text-anchor', 'start') 318 | .style('alignment-baseline', 'middle') 319 | .style('font-size', '16px') 320 | .html('gini coefficient of inequality'); 321 | 322 | var bbox = annotation.node().getBBox(); 323 | 324 | chartElement.append('rect') 325 | .attr('id', 'annotation-background') 326 | .attr('x', bbox.x) 327 | .attr('y', bbox.y) 328 | .attr('width', bbox.width + 5) 329 | .attr('height', bbox.height + 5) 330 | 331 | annotation.moveToFront(); 332 | 333 | chartElement.append('text') 334 | .attr('id', 'x-label') 335 | .attr('x', chartWidth / 2) 336 | .attr('y', chartHeight + 45) 337 | .attr('text-anchor', 'middle') 338 | .html('Share of workers who are unionized') 339 | } 340 | 341 | d3.selection.prototype.moveToFront = function() { 342 | return this.each(function(){ 343 | this.parentNode.appendChild(this); 344 | }); 345 | }; 346 | 347 | // Bind on-load handler 348 | document.addEventListener("DOMContentLoaded", function() { 349 | init(); 350 | }); 351 | -------------------------------------------------------------------------------- /src/styl/normalize.styl: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.0 | 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 | * Remove default margin. 16 | */ 17 | 18 | body 19 | margin: 0 20 | 21 | /* HTML5 display definitions 22 | ========================================================================== */ 23 | 24 | /** 25 | * Correct `block` display not defined in IE 8/9. 26 | */ 27 | 28 | article, 29 | aside, 30 | details, 31 | figcaption, 32 | figure, 33 | footer, 34 | header, 35 | hgroup, 36 | main, 37 | nav, 38 | section, 39 | summary 40 | display: block 41 | 42 | /** 43 | * 1. Correct `inline-block` display not defined in IE 8/9. 44 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 45 | */ 46 | 47 | audio, 48 | canvas, 49 | progress, 50 | video 51 | display: inline-block // 1 52 | vertical-align: baseline // 2 53 | 54 | /** 55 | * Prevent modern browsers from displaying `audio` without controls. 56 | * Remove excess height in iOS 5 devices. 57 | */ 58 | 59 | audio:not([controls]) 60 | display: none 61 | height: 0 62 | 63 | /** 64 | * Address `[hidden]` styling not present in IE 8/9. 65 | * Hide the `template` element in IE, Safari, and Firefox < 22. 66 | */ 67 | 68 | [hidden], 69 | template 70 | display: none 71 | 72 | /* Links 73 | ========================================================================== */ 74 | 75 | /** 76 | * 1. Remove the gray background color from active links in IE 10. 77 | * 2. Improve readability when focused and also mouse hovered in all browsers. 78 | */ 79 | 80 | a 81 | background: transparent // 1 82 | &:active, 83 | &:hover 84 | outline: 0 // 2 85 | 86 | /* Text-level semantics 87 | ========================================================================== */ 88 | 89 | /** 90 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 91 | */ 92 | 93 | abbr[title] 94 | border-bottom: 1px dotted 95 | 96 | /** 97 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 98 | */ 99 | 100 | b, 101 | strong 102 | font-weight: bold 103 | 104 | /** 105 | * Address styling not present in Safari 5 and Chrome. 106 | */ 107 | 108 | dfn 109 | font-style: italic 110 | 111 | /** 112 | * Address variable `h1` font-size and margin within `section` and `article` 113 | * contexts in Firefox 4+, Safari 5, and Chrome. 114 | */ 115 | 116 | h1 117 | font-size: 2em 118 | margin: 0.67em 0 119 | 120 | /** 121 | * Address styling not present in IE 8/9. 122 | */ 123 | 124 | mark 125 | background: #ff0 126 | color: #000 127 | 128 | /** 129 | * Address inconsistent and variable font size in all browsers. 130 | */ 131 | 132 | small 133 | font-size: 80% 134 | 135 | /** 136 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 137 | */ 138 | 139 | sub, 140 | sup 141 | font-size: 75% 142 | line-height: 0 143 | position: relative 144 | vertical-align: baseline 145 | 146 | sup 147 | top: -0.5em 148 | 149 | sub 150 | bottom: -0.25em 151 | 152 | /* Embedded content 153 | ========================================================================== */ 154 | 155 | /** 156 | * Remove border when inside `a` element in IE 8/9. 157 | */ 158 | 159 | img 160 | border: 0 161 | 162 | /** 163 | * Correct overflow displayed oddly in IE 9. 164 | */ 165 | 166 | svg:not(:root) 167 | overflow: hidden 168 | 169 | /* Grouping content 170 | ========================================================================== */ 171 | 172 | /** 173 | * Address margin not present in IE 8/9 and Safari 5. 174 | */ 175 | 176 | figure 177 | margin: 1em 40px 178 | 179 | /** 180 | * Address differences between Firefox and other browsers. 181 | */ 182 | 183 | hr 184 | -moz-box-sizing: content-box 185 | box-sizing: content-box 186 | height: 0 187 | 188 | /** 189 | * Contain overflow in all browsers. 190 | */ 191 | 192 | pre 193 | overflow: auto 194 | 195 | /** 196 | * Address odd `em`-unit font size rendering in all browsers. 197 | */ 198 | 199 | code, 200 | kbd, 201 | pre, 202 | samp 203 | font-family: monospace, monospace 204 | font-size: 1em 205 | 206 | /* Forms 207 | ========================================================================== */ 208 | 209 | /** 210 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 211 | * styling of `select`, unless a `border` property is set. 212 | */ 213 | 214 | /** 215 | * 1. Correct color not being inherited. 216 | * Known issue: affects color of disabled elements. 217 | * 2. Correct font properties not being inherited. 218 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 219 | */ 220 | 221 | button, 222 | input, 223 | optgroup, 224 | select, 225 | textarea 226 | color: inherit // 1 227 | font: inherit // 2 228 | margin: 0 // 3 229 | 230 | /** 231 | * Address `overflow` set to `hidden` in IE 8/9/10. 232 | */ 233 | 234 | button 235 | overflow: visible 236 | 237 | /** 238 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 239 | * All other form control elements do not inherit `text-transform` values. 240 | * Correct `button` style inheritance in Firefox, IE 8+, and Opera 241 | * Correct `select` style inheritance in Firefox. 242 | */ 243 | 244 | button, 245 | select 246 | text-transform: none 247 | 248 | /** 249 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 250 | * and `video` controls. 251 | * 2. Correct inability to style clickable `input` types in iOS. 252 | * 3. Improve usability and consistency of cursor style between image-type 253 | * `input` and others. 254 | */ 255 | 256 | button, 257 | html input[type="button"], // 1 258 | input[type="reset"], 259 | input[type="submit"] 260 | -webkit-appearance: button // 2 261 | cursor: pointer // 3 262 | 263 | /** 264 | * Re-set default cursor for disabled elements. 265 | */ 266 | 267 | button[disabled], 268 | html input[disabled] 269 | cursor: default 270 | 271 | /** 272 | * Remove inner padding and border in Firefox 4+. 273 | */ 274 | 275 | button::-moz-focus-inner, 276 | input::-moz-focus-inner 277 | border: 0 278 | padding: 0 279 | 280 | /** 281 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 282 | * the UA stylesheet. 283 | */ 284 | 285 | input 286 | line-height: normal 287 | 288 | /** 289 | * It's recommended that you don't attempt to style these elements. 290 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 291 | * 292 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 293 | * 2. Remove excess padding in IE 8/9/10. 294 | */ 295 | 296 | input[type="checkbox"], 297 | input[type="radio"] 298 | box-sizing: border-box // 1 299 | padding: 0 // 2 300 | 301 | /** 302 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 303 | * `font-size` values of the `input`, it causes the cursor style of the 304 | * decrement button to change from `default` to `text`. 305 | */ 306 | 307 | input[type="number"]::-webkit-inner-spin-button, 308 | input[type="number"]::-webkit-outer-spin-button 309 | height: auto 310 | 311 | /** 312 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 313 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 314 | * (include `-moz` to future-proof). 315 | */ 316 | 317 | input[type="search"] 318 | -webkit-appearance: textfield // 1 319 | -moz-box-sizing: content-box 320 | -webkit-box-sizing: content-box // 2 321 | box-sizing: content-box 322 | 323 | /** 324 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 325 | * Safari (but not Chrome) clips the cancel button when the search input has 326 | * padding (and `textfield` appearance). 327 | */ 328 | 329 | input[type="search"]::-webkit-search-cancel-button, 330 | input[type="search"]::-webkit-search-decoration 331 | -webkit-appearance: none 332 | 333 | /** 334 | * Define consistent border, margin, and padding. 335 | */ 336 | 337 | fieldset 338 | border: 1px solid #c0c0c0 339 | margin: 0 2px 340 | padding: 0.35em 0.625em 0.75em 341 | 342 | /** 343 | * 1. Correct `color` not being inherited in IE 8/9. 344 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 345 | */ 346 | 347 | legend 348 | border: 0 // 1 349 | padding: 0 // 2 350 | 351 | /** 352 | * Remove default vertical scrollbar in IE 8/9. 353 | */ 354 | 355 | textarea 356 | overflow: auto 357 | 358 | /** 359 | * Don't inherit the `font-weight` (applied by a rule above). 360 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 361 | */ 362 | 363 | optgroup 364 | font-weight: bold 365 | 366 | /* Tables ================================================================== */ 367 | 368 | /** 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table 373 | border-collapse: collapse 374 | border-spacing: 0 375 | 376 | td, 377 | th 378 | padding: 0 379 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Node modules 2 | var browserify = require("browserify"); 3 | var source = require("vinyl-source-stream"); 4 | var browserSync = require("browser-sync"); 5 | var reload = browserSync.reload; 6 | var nib = require("nib"); 7 | var del = require("del"); 8 | 9 | // Gulp-related 10 | var gulp = require("gulp"); 11 | var changed = require("gulp-changed"); 12 | var jade = require("gulp-jade"); 13 | var preprocess = require("gulp-preprocess"); 14 | var shell = require("gulp-shell"); 15 | var stylus = require("gulp-stylus"); 16 | 17 | // Non-gulp NPM modules 18 | var fs = require("fs"); 19 | var archieml = require("archieml"); 20 | var request = require('request'); 21 | var http = require('http'); 22 | 23 | // Local modules 24 | var args = require("./gulp/cli").parse(); 25 | var config = require("./gulp/config"); 26 | var utils = require("./gulp/utils"); 27 | 28 | // Configuration 29 | var gdoc_id = ""; 30 | var gdoc_host = "127.0.0.1:6006"; 31 | var gdoc_url = "http://"+gdoc_host+"/"+ gdoc_id; 32 | var content = {}; 33 | 34 | var qzdataPath = process.env.QZDATA_PATH || "~/qzdata"; 35 | var thingName = "inequality-and-unions"; 36 | var thingPath = qzdataPath + "/2017/inequality-and-unions"; 37 | 38 | var isProd = args.build ? true : false; 39 | var preprocessOpts = { 40 | context: { 41 | ENV: isProd ? "prod" : "dev" 42 | } 43 | }; 44 | 45 | var allTasks = [ 46 | "get-content", 47 | "jade", 48 | "stylus", 49 | "browserify", 50 | "copy-libs", 51 | "copy-assets", 52 | "copy-fonts", 53 | "copy-data", 54 | "copy-favicon" 55 | ]; 56 | 57 | var shellCmd = utils.generateShellCmd(args.build, thingPath, thingName); 58 | 59 | /** 60 | * Reads compiled ArchieML content from the local JSON file. 61 | */ 62 | function readContentFromFile(doneCallback) { 63 | fs.readFile("content.json", function(err, data){ 64 | if (!err) { 65 | content = JSON.parse(data); 66 | doneCallback(); 67 | } 68 | else { 69 | console.log("Cannot load content from file"); 70 | doneCallback(err); 71 | } 72 | }); 73 | } 74 | 75 | /** 76 | * Fetch ArchieML data from Google Docs. 77 | */ 78 | function getContentTask(doneCallback) { 79 | if(gdoc_id !== "") { 80 | request.get({ 81 | "url": gdoc_url 82 | }, 83 | function(error, resp, body) { 84 | if(!error && resp.statusCode < 400) { 85 | content = JSON.parse(body); 86 | fs.writeFileSync("content.json", body, "utf-8"); 87 | doneCallback(); 88 | } 89 | else { 90 | // if the server isn't up load from file 91 | if(resp && resp.statusCode >= 400) { 92 | console.log(body); 93 | } 94 | 95 | console.log("Cannot load content from server, loading from file"); 96 | readContentFromFile(doneCallback); 97 | } 98 | }); 99 | } 100 | else { 101 | console.log("No google doc specified, loading from file"); 102 | readContentFromFile(doneCallback); 103 | } 104 | } 105 | 106 | getContentTask.description = "Fetch ArchieML data from Google Docs"; 107 | gulp.task("get-content", getContentTask); 108 | 109 | /** 110 | * Compile Jade HTML templates. (Also triggers a refetch of the ArchieML doc.) 111 | */ 112 | function compileJadeTask() { 113 | var context = { 114 | "content": content, 115 | "build_info": { 116 | "timestamp": (new Date()).getTime().toString() 117 | } 118 | } 119 | 120 | return gulp.src(config.paths.src.jade + "/index.jade") 121 | .pipe(jade({ pretty: true, locals: context })) 122 | .pipe(gulp.dest(config.dirs.build)) 123 | .pipe(reload({ stream: true })); 124 | } 125 | 126 | compileJadeTask.description = "Compile Jade HTML templates (also triggers 'get-content')"; 127 | gulp.task("jade", ["get-content"], compileJadeTask); 128 | 129 | /** 130 | * Compile Stylus CSS meta-language. 131 | */ 132 | function compileStylusTask() { 133 | return gulp.src(config.paths.src.styl + "/main.styl") 134 | .pipe(stylus({ 135 | use: [nib()], 136 | "include css": true, 137 | errors: true 138 | })) 139 | .pipe(gulp.dest(config.paths.build.css)) 140 | .pipe(reload({ stream: true })); 141 | } 142 | 143 | compileStylusTask.description = "Compile Stylus CSS meta-language"; 144 | gulp.task("stylus", compileStylusTask); 145 | 146 | /** 147 | * Bundle Javascript with browserify. 148 | */ 149 | function compileJavascriptTask(doneCallback) { 150 | var bundler = browserify({ 151 | entries: [config.paths.src.js + "/thing.js"], 152 | debug: !isProd 153 | }); 154 | 155 | if (isProd && !args["dont-minify"]) { 156 | bundler.transform({ global: true }, "uglifyify"); 157 | } 158 | 159 | return bundler 160 | .bundle() 161 | .on('error', function(err) { 162 | console.error('ERROR IN JS'); 163 | console.error(err.message); 164 | doneCallback(); 165 | }) 166 | .pipe(source("thing.js")) 167 | .pipe(gulp.dest(config.paths.build.js)) 168 | .pipe(reload({ stream: true })); 169 | } 170 | 171 | compileJavascriptTask.description = "Bundle Javascript with browserify"; 172 | gulp.task("browserify", ["preprocess"], compileJavascriptTask); 173 | 174 | /** 175 | * Clear files from the build directory. 176 | */ 177 | function cleanTask() { 178 | return del([ 179 | config.dirs.build + "/**" 180 | ]); 181 | } 182 | 183 | cleanTask.description = "Clear files from the build directory"; 184 | gulp.task("clean", cleanTask); 185 | 186 | /** 187 | * Copy Javascript libraries to build path. 188 | */ 189 | function copyLibrariesTask() { 190 | return gulp.src(config.paths.src.js + "/libs/*") 191 | .pipe(gulp.dest(config.paths.build.js + "/libs")) 192 | .pipe(reload({ stream: true })); 193 | } 194 | 195 | copyLibrariesTask.description = "Copy Javascript libraries to build path"; 196 | gulp.task("copy-libs", copyLibrariesTask); 197 | 198 | function copyLibrariesTask() { 199 | return gulp.src(config.paths.src.js + "/libs/*") 200 | .pipe(gulp.dest(config.paths.build.js + "/libs")) 201 | .pipe(reload({ stream: true })); 202 | } 203 | 204 | copyLibrariesTask.description = "Copy Javascript libraries to build path"; 205 | gulp.task("copy-libs", copyLibrariesTask); 206 | 207 | /** 208 | * Copy static assets to build path. 209 | */ 210 | function copyAssetsTask() { 211 | return gulp.src(config.paths.src.assets + "/**") 212 | .pipe(gulp.dest(config.paths.build.assets)) 213 | .pipe(reload({ stream: true })); 214 | } 215 | 216 | copyAssetsTask.description = "Copy static assets to build path"; 217 | gulp.task("copy-assets", copyAssetsTask); 218 | 219 | function copyFaviconTask() { 220 | return gulp.src("./src/favicon.ico") 221 | .pipe(gulp.dest("./build")) 222 | .pipe(reload({ stream: true })); 223 | } 224 | 225 | copyAssetsTask.description = "Copy favicon to build path"; 226 | gulp.task("copy-favicon", copyFaviconTask); 227 | 228 | /** 229 | * Copy font files to build path. 230 | */ 231 | function copyFontsTask() { 232 | return gulp.src(config.paths.src.fonts + "/**") 233 | .pipe(gulp.dest(config.paths.build.fonts)) 234 | .pipe(reload({ stream: true })); 235 | } 236 | 237 | copyFontsTask.description = "Copy font files to build path"; 238 | gulp.task("copy-fonts", copyFontsTask); 239 | 240 | /** 241 | * Copy data files to build path. 242 | */ 243 | function copyDataTask() { 244 | return gulp.src(config.paths.src.data + "/**") 245 | .pipe(gulp.dest(config.paths.build.data)) 246 | .pipe(reload({ stream: true })); 247 | } 248 | 249 | copyDataTask.description = "Copy data files to build path" 250 | gulp.task("copy-data", copyDataTask); 251 | 252 | /** 253 | * Writes environment configuration variables to config.js and puts it in 254 | * the build directory. 255 | */ 256 | function preprocessTask() { 257 | return gulp.src([config.paths.src.js + "/config.js"]) 258 | .pipe(preprocess(preprocessOpts)) 259 | .pipe(gulp.dest(config.paths.build.js)); 260 | } 261 | 262 | preprocessTask.description = "Preprocess dev/prod conditional code"; 263 | gulp.task("preprocess", preprocessTask); 264 | 265 | /** 266 | * Watches project files for changes and runs the appropriate copy/compile 267 | * tasks. 268 | */ 269 | function watchTask(done) { 270 | gulp.watch(config.paths.src.libs + "/**", ["copy-libs"]); 271 | gulp.watch(config.paths.src.fonts + "/**", ["copy-fonts"]); 272 | gulp.watch(config.paths.src.data + "/**", ["copy-data"]); 273 | gulp.watch(config.paths.src.assets + "/**", ["copy-assets"]); 274 | gulp.watch(config.paths.src.js + "/**", ["browserify"]); 275 | gulp.watch(config.paths.src.styl + "/**", ["stylus"]); 276 | gulp.watch(config.paths.src.jade + "/**", ["jade"]); 277 | done(); 278 | } 279 | 280 | watchTask.description = "Watch local files for changes and re-build as necessary." 281 | gulp.task("watch", allTasks, watchTask); 282 | 283 | /** 284 | * Starts the browsersync server. 285 | */ 286 | function browserSyncTask() { 287 | browserSync({ 288 | server: { 289 | baseDir: "build" 290 | }, 291 | open: false 292 | }); 293 | } 294 | 295 | browserSyncTask.description = "Serve the built project using BrowserSync"; 296 | gulp.task("browser-sync", ["watch"], browserSyncTask); 297 | 298 | /** 299 | * Other tasks 300 | */ 301 | gulp.task("shell", allTasks, shell.task(shellCmd)); 302 | gulp.task("build", ["shell"]); 303 | 304 | if (args.build) { 305 | gulp.task("default", ["clean"], function () { 306 | gulp.start("build"); 307 | }); 308 | } else { 309 | gulp.task("default", ["clean"], function () { 310 | gulp.start("browser-sync"); 311 | }); 312 | } 313 | --------------------------------------------------------------------------------