├── .gitignore ├── .python-version ├── CHANGELOG ├── GRAPHIC_CHECKLIST.md ├── LICENSE ├── README.md ├── app.py ├── app_config.py ├── etc ├── __init__.py ├── ai2html.jsx ├── assets │ └── assetsignore └── gdocs.py ├── fabfile ├── __init__.py ├── assets.py ├── flat.py ├── render.py ├── test.py └── utils.py ├── graphic.py ├── graphic_templates.py ├── graphic_templates ├── _base │ ├── assets │ │ ├── assetsignore │ │ └── private │ │ │ └── .placeholder │ ├── base_filters.py │ ├── base_template.html │ ├── css │ │ └── base.less │ └── js │ │ ├── analytics.js │ │ ├── base.js │ │ ├── helpers.js │ │ └── lib │ │ ├── d3.min.js │ │ └── underscore.js ├── _thumbs │ ├── animated-photo.gif │ ├── bar-chart.png │ ├── block-histogram.png │ ├── column-chart.png │ ├── diverging-bar-chart.png │ ├── dot-chart.png │ ├── graphic.png │ ├── grouped-bar-chart.png │ ├── issue-matrix.png │ ├── line-chart.png │ ├── locator-map.png │ ├── newsletter.png │ ├── slopegraph.png │ ├── stacked-bar-chart.png │ ├── stacked-column-chart.png │ ├── stacked-grouped-column-chart.png │ ├── state-grid-map.png │ └── table.png ├── ai2html_graphic │ ├── ai2html-graphic.html │ ├── assets │ │ └── ai2html-graphic.ai │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ ├── img │ │ ├── ai2html-graphic-medium.jpg │ │ ├── ai2html-graphic-small.jpg │ │ └── ai2html-graphic-wide.jpg │ └── js │ │ └── graphic.js ├── animated_photo │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ ├── img │ │ ├── filmstrip-1000.jpg │ │ ├── filmstrip-375.jpg │ │ ├── filmstrip-600.jpg │ │ ├── filmstrip.gif │ │ └── frames │ │ │ ├── moon01.jpg │ │ │ ├── moon02.jpg │ │ │ ├── moon03.jpg │ │ │ ├── moon04.jpg │ │ │ ├── moon05.jpg │ │ │ ├── moon06.jpg │ │ │ ├── moon07.jpg │ │ │ └── moon08.jpg │ ├── js │ │ ├── graphic.js │ │ └── lib │ │ │ └── canvid.js │ └── process.sh ├── archive_graphic │ ├── child_template.html │ ├── css │ │ └── graphic.less │ └── js │ │ ├── graphic.js │ │ └── lib │ │ └── jquery-1.11.3.min.js ├── bar_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── data.csv │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── block_histogram │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── data.csv │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── column_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── data.csv │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── diverging_bar_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── dot_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── graphic │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── grouped_bar_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── issue_matrix │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ ├── graphic.js │ │ └── lib │ │ ├── tablesort.js │ │ └── tablesort.numeric.js ├── line_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── data.csv │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── locator_map │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── data │ │ └── geodata.json │ ├── geodata.yaml │ ├── graphic_config.py │ └── js │ │ ├── geomath.js │ │ ├── graphic.js │ │ └── lib │ │ ├── d3.geo.projection.v0.min.js │ │ └── topojson.v1.min.js ├── quiz │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ ├── graphic.js │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── modernizr.svg.min.js │ │ └── underscore.js ├── slopegraph │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ ├── graphic.js │ │ └── lib │ │ ├── d3.min.js │ │ ├── modernizr.svg.min.js │ │ ├── pym.js │ │ └── underscore.js ├── stacked_bar_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── data.csv │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── stacked_column_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── data.csv │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── stacked_grouped_column_chart │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ └── graphic.js ├── state_grid_map │ ├── child_template.html │ ├── css │ │ └── graphic.less │ ├── graphic_config.py │ └── js │ │ ├── graphic.js │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── jquery.min.js │ │ ├── modernizr.svg.min.js │ │ ├── pym.js │ │ └── underscore.min.js └── table │ ├── child_template.html │ ├── css │ └── graphic.less │ ├── graphic_config.py │ └── js │ ├── graphic.js │ └── lib │ ├── tablesort.js │ └── tablesort.number.js ├── oauth.py ├── package-lock.json ├── package.json ├── render_utils.py ├── requirements.txt └── templates ├── copyedit ├── graphic.txt └── note.txt ├── index.html ├── oauth ├── _oauth_base.html ├── authenticate.html ├── oauth.html └── warning.html └── parent.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sw[op] 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | 17 | # Installer logs 18 | pip-log.txt 19 | 20 | # Unit test / coverage reports 21 | .coverage 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | 30 | .DS_store 31 | .gzip 32 | 33 | # Test Logs 34 | *.log 35 | 36 | node_modules 37 | www/*.html 38 | www/css/*.min.*.css 39 | www/css/*.min.css 40 | www/css/*.less.css 41 | www/js/*.min.*.js 42 | www/js/*.min.js 43 | www/js/templates.js 44 | www/js/app_config.js 45 | www/js/copy.js 46 | confs/rendered/* 47 | data/copy.xls 48 | tumblr-theme.html 49 | www/test/test.html 50 | www/assets 51 | data/gdoc_*.csv 52 | www/graphics/*/index.html 53 | data/*.xls 54 | data/*.xlsx 55 | www/graphics/*/child.html 56 | dailygraphics.sublime-* 57 | graphic_templates/**/*.xlsx 58 | graphic_templates/_basec 59 | 60 | #Test 61 | test 62 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 2.7.15 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.1.6 2 | ----- 3 | 4 | * Disable gzip compression at deploy, in favor of compression set at the CDN level (#293, #310) 5 | * Table template: Update tablesort version (#223, #309) 6 | * Automatically strip whitespace from COPY sheet inputs (#303) 7 | 8 | 0.1.5 9 | ----- 10 | 11 | This version includes updates to Python and NPM dependencies. Re-run `pip install -r requirements.txt` and `npm install` 12 | 13 | This version moves templates to a separate location from the duplicated spreadsheets. After updating, you will need to update `DRIVE_SPREADSHEETS_FOLDER` in `app_config.py` to either specify a folder or set it to `None`. (#301) 14 | 15 | * Switch to new embed loader (#231) 16 | * Upgrade cssmin version to solve installation issue (#259) 17 | * Auto-generate dates for new graphics (#237) 18 | * Add date to slug of cloned graphic if missing or invalid (#239) 19 | * Solve issue with draft cloned graphic path (#260) 20 | * Animated photo: Remove D3 dependency (#229) 21 | * Animated photo: Store params in the copy spreadsheet (#228) 22 | * Locator map: Add pixelOffset config option (#220) 23 | * State grid map: Add optional U.S. territories (#264) 24 | * State grid map: Adjust map SVG height when toggling territories on and off (#265) 25 | * Auto-generates copyedit note using fabric command (#236) 26 | * Copyedit email: Tweak to formatting (#257) 27 | * Add URL param to embed code, isHomepage boolean and css when homepage checkbox is clicked (#253) 28 | * Make sure viewport test buttons are visible on mobile (#249) 29 | * Bump LESS version to 3.5.3 (#270, #271, #279, #284) 30 | * Update Python libraries in requirements.txt (#272, #274, #283) 31 | * Make sure list of graphics is sorted by alpha (#291) 32 | * Expose direct link to child page to allow for fallback links in story text if the embed doesn't load. 33 | * Add optional drive root folder to store the COPY spreadsheets (#301) 34 | * Accessibility: Added an optional `screenreader` field to describe graphic contents (#299) 35 | 36 | 0.1.4 37 | ----- 38 | 39 | * Change http and // references to https. (#205) 40 | * Block histogram: Assign block colors via JS rather than CSS (#175) 41 | * State grid map: Fix sizing in IE11 (#170) 42 | * State grid map: Add sequential legend option (#191) 43 | * State grid map: Define data column as a variable (#176) 44 | * Animated photo: Check for credit OR caption when displaying caption area (#174) 45 | * Standardize use of _.contains to filter data columns in stacked bar/column chart templates (#183) 46 | * Add AP-style month formatter (#190) 47 | * Line chart template: Update filter for null values (#189) 48 | * Remove references to local copy of pym in a few graphics templates (#188) 49 | * Ensure support for https (#193) 50 | * Add AP-style full date formatter (#198) 51 | * Change flat to accommodate to new staging policy 52 | * Update awscli and Authomatic library requirements 53 | * Remove unused www deployment (#216) 54 | * Remove unused files for s3 deployment (#218) 55 | * Allow render/deploy from an arbitrary folder 56 | * Remove empty env.settings (#221) 57 | * Force utf-8 character set on html deployment to S3 (#215) 58 | * Add testing capabilities 59 | * Add private assets functionality 60 | * New diverging bar chart template (#151) 61 | 62 | 0.1.3 63 | ----- 64 | 65 | * Merge carebot branch 66 | * Remove Newsletter template 67 | 68 | 0.1.2 69 | ----- 70 | 71 | * Integrate new centralized pym v1 72 | * New template: Newsletter template 73 | * New template: Stacked grouped column chart (#181) 74 | * New open_spreadsheet task 75 | * New clone_graphic task 76 | * deploy task accepts multiple project slugs (#155) 77 | * animated_photo process script also outputs a gif (#152) 78 | * New issue matrix graphic template. 79 | * Add a new iTerm v3 configuration applescript 80 | 81 | 0.1.1 82 | ----- 83 | 84 | * New animated_photo graphic template. 85 | * CSS to address SVG overflow quirk in IE11. 86 | * Add tablesort to table template. (#57) 87 | * Add smarty filter. (#139) 88 | * Remove commented-out JS data loader in existing graphics templates. (#147) 89 | * Reorganize base JS and add some frequently-used D3 date/number formatters. (#137) 90 | * Clean up CSS and JS in existing graphics templates. (#126, #140, #141, #145, #146, #148) 91 | * Update boilerplate embed code to match what's needed for NPR's CMS, Seamus. 92 | * Fix issue with creating a new graphic. 93 | * Fix S3 SSL certificate issue. 94 | * Created _base/base_filters.py, a library of default Jinja filters for common formatting tasks. 95 | * Made it possible for graphic_config.py to import other local modules (#133) 96 | 97 | 0.1.0 98 | ----- 99 | 100 | * stacked_bar_chart and stacked_column_chart will now auto-hide labels that don't fit. 101 | * Height-based label fitting for column_chart template. 102 | * bar_chart, column_chart, grouped_bar_chart, stacked_bar_chart and stacked_column_chart templates now supports negative numbers. (#124) 103 | * Add support for custom Jinja filters specified in graphic_config.py. (#131) 104 | * Update Jinja to 2.7.3. (#127) 105 | * Add .DS_Store to default assetsignore. (#129) 106 | * Fix issue where preview border messed with width calculation. 107 | * Update pym to 0.4.5. 108 | * Add block_histogram template. 109 | * Start CHANGELOG. (#132) 110 | -------------------------------------------------------------------------------- /GRAPHIC_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Daily Graphics Checklist 2 | 3 | This is a checklist to help make sure you're covering all the bases when 4 | making graphics. 5 | 6 | For NPR users looking for specifics on the publishing process at NPR, 7 | view the graphic checklist in our private team repo. 8 | 9 | ## Initial Concept 10 | 11 | * Is the data organized by the component most important to the narrative? 12 | * geographic -> map 13 | * temporal -> timeline 14 | * ordinal -> table 15 | * Do we have sufficient narrative context to understand the meaning of the data? 16 | * If the data is vulnerable to being misinterpreted, have we adequately warned against it? 17 | 18 | ## Data 19 | 20 | * Does the data mean we what think it means? (Are we very, very sure?) 21 | * Is the data internally complete? (e.g., no missing years) If not, do we make it clear what data is missing? 22 | * For example, in a chart: dotted or missing lines and a corresponding footnote 23 | * In a table: “n/a” in the table cell, or an asterisk + footnote 24 | * Have we been careful to ensure we don't treat "0" and "null" interchangeably? 25 | * Does any bucketing of the data effectively represent the distribution? (quartiles, quintiles, equal-interval, jenks breaks, box plot, etc.) 26 | 27 | See also: [The Quartz Guide to Bad Data](https://github.com/Quartz/bad-data-guide) (by team alum Chris Groskopf) 28 | 29 | ## Text 30 | 31 | * Is the headline human-friendly? (conversational, non-technical) 32 | * Is Every Word In The Headline Capitalized? (NPR headline style) 33 | * Are quotes in the headline singular? 34 | * Are there any widows in the headline (lone words on the last line) 35 | when the graphic is resized? To avoid this, add a non-breaking space 36 | (` `) between the last two words of the headline. 37 | * Does the source line link back, if possible? 38 | * Does the credit line include everyone who worked on the graphic? 39 | * Do the footnotes explain all caveats a normal reader would need to know to understand the chart fully? 40 | 41 | ## Technical / Code 42 | 43 | * Does the graphic respond correctly? Does it read well on mobile? (e.g., nothing is cut off, text is readable, hover states disabled) 44 | * Are we only including data we’re actually displaying (to keep page weight down)? 45 | * Have you created a static fallback image for your final graphic? (fallback.png) 46 | * If there have been late edits to your graphic, have you updated the fallback image? 47 | 48 | ## Charts 49 | 50 | * Does the chart have a zero-baseline? (Or a very compelling reason for not having a zero-baseline?) 51 | * Is there a grid line at the maximum value of the chart? 52 | * Do the grid lines break at reasonable intervals? 53 | * Are axes, labels, margins and colors consistent between charts that display related information? (small multiples, etc.) 54 | * Are colors readable by someone who is red/green colorblind? 55 | * Are the number of axis labels reasonable at various screen sizes? 56 | * Does the entire chart fit within a normal user’s viewport? 57 | 58 | ## Tables 59 | 60 | * Is the the sort order sensible? 61 | * Is there a useful secondary sort order? 62 | * Have any repetitive unit labels been moved to the header? (%, $) 63 | * Are all numeric values within a column rounded to the same number of decimal places? 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 NPR 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from flask import Flask, make_response, render_template 6 | from glob import glob 7 | from werkzeug.debug import DebuggedApplication 8 | 9 | import app_config 10 | import copytext 11 | import graphic 12 | import graphic_templates 13 | import oauth 14 | from render_utils import make_context 15 | 16 | app = Flask(app_config.PROJECT_SLUG) 17 | app.debug = app_config.DEBUG 18 | 19 | @app.route('/') 20 | def _graphics_list(): 21 | """ 22 | Renders a list of all graphics for local testing. 23 | """ 24 | context = make_context() 25 | context['graphics'] = [] 26 | context['templates'] = [] 27 | 28 | graphics = glob('%s/*' % app_config.GRAPHICS_PATH) 29 | 30 | for graphic in graphics: 31 | name = graphic.split('%s/' % app_config.GRAPHICS_PATH)[1].split('/child.html')[0] 32 | context['graphics'].append(name) 33 | 34 | context['graphics'].sort(); 35 | 36 | context['graphics_count'] = len(context['graphics']) 37 | 38 | templates = glob('%s/*' % app_config.TEMPLATES_PATH) 39 | 40 | for template in templates: 41 | name = template.split('%s/' % app_config.TEMPLATES_PATH)[1] 42 | 43 | if name.startswith('_'): 44 | continue 45 | 46 | context['templates'].append(name) 47 | 48 | context['templates'].sort() 49 | 50 | context['templates_count'] = len(context['templates']) 51 | 52 | return make_response(render_template('index.html', **context)) 53 | 54 | app.register_blueprint(graphic.graphic, url_prefix='/graphics') 55 | app.register_blueprint(graphic_templates.graphic_templates, url_prefix='/templates') 56 | app.register_blueprint(oauth.oauth) 57 | 58 | if app_config.DEBUG: 59 | wsgi_app = DebuggedApplication(app, evalex=False) 60 | else: 61 | wsgi_app = app 62 | 63 | # Boilerplate 64 | if __name__ == '__main__': 65 | print 'This command has been removed! Please run "fab app" instead!' 66 | -------------------------------------------------------------------------------- /app_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # _*_ coding:utf-8 _*_ 3 | """ 4 | Project-wide application configuration. 5 | """ 6 | 7 | import os 8 | 9 | from authomatic.providers import oauth2 10 | from authomatic import Authomatic 11 | 12 | """ 13 | NAMES 14 | """ 15 | # Project name in urls 16 | # Use dashes, not underscores! 17 | PROJECT_SLUG = 'dailygraphics' 18 | 19 | # Slug for assets dir on S3 20 | ASSETS_SLUG = PROJECT_SLUG 21 | 22 | # The name of the repository containing the source 23 | REPOSITORY_NAME = 'dailygraphics' 24 | REPOSITORY_URL = 'git@github.com:nprapps/%s.git' % REPOSITORY_NAME 25 | REPOSITORY_ALT_URL = None # 'git@bitbucket.org:nprapps/%s.git' % REPOSITORY_NAME' 26 | 27 | # Path to the folder containing the graphics 28 | GRAPHICS_PATH = os.path.abspath('../graphics') 29 | 30 | # Path to the folder containing the graphics 31 | ARCHIVE_GRAPHICS_PATH = os.path.abspath('../graphics-archive') 32 | 33 | # Path to the graphic templates 34 | TEMPLATES_PATH = os.path.abspath('graphic_templates') 35 | 36 | # Add specific drive folder where the copied spreadsheet will be stored 37 | # Or set to None to use your root drive folder. 38 | DRIVE_SPREADSHEETS_FOLDER = '0B2rSjDbnpA5XTThSNDZkWlJTX1E' 39 | 40 | """ 41 | PYM 42 | """ 43 | 44 | PYM = { 45 | 'pym_url': 'https://pym.nprapps.org/pym.v1.min.js', 46 | 'pym_loader_url': 'https://pym.nprapps.org/npr-pym-loader.v2.min.js', 47 | } 48 | 49 | """ 50 | CAREBOT 51 | """ 52 | 53 | CAREBOT_ENABLED = True 54 | CAREBOT_URL = 'https://carebot.nprapps.org/carebot-tracker.v0.min.js' 55 | 56 | """ 57 | OAUTH 58 | """ 59 | 60 | GOOGLE_OAUTH_CREDENTIALS_PATH = '~/.google_oauth_credentials' 61 | 62 | authomatic_config = { 63 | 'google': { 64 | 'id': 1, 65 | 'class_': oauth2.Google, 66 | 'consumer_key': os.environ.get('GOOGLE_OAUTH_CLIENT_ID'), 67 | 'consumer_secret': os.environ.get('GOOGLE_OAUTH_CONSUMER_SECRET'), 68 | 'scope': ['https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/userinfo.email'], 69 | 'offline': True, 70 | }, 71 | } 72 | 73 | authomatic = Authomatic(authomatic_config, os.environ.get('AUTHOMATIC_SALT')) 74 | 75 | """ 76 | DEPLOYMENT 77 | """ 78 | PRODUCTION_S3_BUCKET = { 79 | 'bucket_name': 'apps.npr.org', 80 | 'region': 'us-east-1' 81 | } 82 | 83 | STAGING_S3_BUCKET = { 84 | 'bucket_name': 'stage-apps.npr.org', 85 | 'region': 'us-east-1' 86 | } 87 | 88 | ASSETS_S3_BUCKET = { 89 | 'bucket_name': 'assets.apps.npr.org', 90 | 'region': 'us-east-1' 91 | } 92 | 93 | DEFAULT_MAX_AGE = 20 94 | ASSETS_MAX_AGE = 300 95 | 96 | """ 97 | ANALYTICS 98 | """ 99 | 100 | GOOGLE_ANALYTICS = { 101 | 'ACCOUNT_ID': 'UA-5828686-75' 102 | } 103 | 104 | """ 105 | TESTS 106 | """ 107 | AUTOEXECUTE_TESTS = False 108 | TESTS_LOAD_WAIT_TIME = 2 109 | TEST_SCRIPTS_TIMEOUT = 5 110 | 111 | 112 | # These variables will be set at runtime. See configure_targets() below 113 | S3_BUCKET = None 114 | S3_BASE_URL = '' 115 | S3_DEPLOY_URL = None 116 | DEBUG = True 117 | 118 | def configure_targets(deployment_target): 119 | """ 120 | Configure deployment targets. Abstracted so this can be 121 | overriden for rendering before deployment. 122 | """ 123 | global S3_BUCKET 124 | global S3_BASE_URL 125 | global S3_DEPLOY_URL 126 | global DEBUG 127 | global DEPLOYMENT_TARGET 128 | 129 | if deployment_target == 'production': 130 | S3_BUCKET = PRODUCTION_S3_BUCKET 131 | S3_BASE_URL = 'https://%s/%s' % (S3_BUCKET['bucket_name'], PROJECT_SLUG) 132 | S3_DEPLOY_URL = 's3://%s/%s' % (S3_BUCKET['bucket_name'], PROJECT_SLUG) 133 | DEBUG = False 134 | elif deployment_target == 'staging': 135 | S3_BUCKET = STAGING_S3_BUCKET 136 | S3_BASE_URL = 'https://s3.amazonaws.com/%s/%s' % (S3_BUCKET['bucket_name'], PROJECT_SLUG) 137 | S3_DEPLOY_URL = 's3://%s/%s' % (S3_BUCKET['bucket_name'], PROJECT_SLUG) 138 | DEBUG = True 139 | else: 140 | S3_BUCKET = None 141 | S3_BASE_URL = '//127.0.0.1:8000' 142 | S3_DEPLOY_URL = None 143 | DEBUG = True 144 | 145 | DEPLOYMENT_TARGET = deployment_target 146 | 147 | """ 148 | Run automated configuration 149 | """ 150 | DEPLOYMENT_TARGET = os.environ.get('DEPLOYMENT_TARGET', None) 151 | 152 | configure_targets(DEPLOYMENT_TARGET) 153 | -------------------------------------------------------------------------------- /etc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/etc/__init__.py -------------------------------------------------------------------------------- /etc/assets/assetsignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /etc/gdocs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from exceptions import KeyError 4 | import os 5 | 6 | import requests 7 | 8 | import app_config 9 | 10 | class GoogleDoc(object): 11 | """ 12 | A class for accessing a Google document as an object. 13 | Includes the bits necessary for accessing the document and auth and such. 14 | For example: 15 | 16 | doc = { 17 | "key": "123456abcdef", 18 | "file_name": "my_google_doc" 19 | } 20 | g = GoogleDoc(**doc) 21 | g.get_auth() 22 | g.get_document() 23 | 24 | Will download your google doc to data/my_google_doc.xls in the CSV format. 25 | """ 26 | 27 | # You can update these values with kwargs. 28 | # In fact, you better pass a key or else it won't work! 29 | key = None 30 | file_format = 'xlsx' 31 | file_name = 'copy' 32 | 33 | # You can change these with kwargs but it's not recommended. 34 | spreadsheet_url = 'https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=%(key)s&exportFormat=%(format)s' 35 | new_spreadsheet_url = 'https://docs.google.com/spreadsheets/d/%(key)s/export?format=%(format)s&id=%(key)s' 36 | auth = None 37 | email = os.environ.get('APPS_GOOGLE_EMAIL', None) 38 | password = os.environ.get('APPS_GOOGLE_PASS', None) 39 | scope = "https://spreadsheets.google.com/feeds/" 40 | service = "wise" 41 | session = "1" 42 | 43 | def __init__(self, **kwargs): 44 | """ 45 | Because sometimes, just sometimes, you need to update the class when you instantiate it. 46 | In this case, we need, minimally, a document key. 47 | """ 48 | if kwargs: 49 | if kwargs.items(): 50 | for key, value in kwargs.items(): 51 | setattr(self, key, value) 52 | 53 | def get_auth(self): 54 | """ 55 | Gets an authorization token and adds it to the class. 56 | """ 57 | data = {} 58 | if not self.email or not self.password: 59 | raise KeyError("Error! You're missing some variables. You need to export APPS_GOOGLE_EMAIL and APPS_GOOGLE_PASS.") 60 | 61 | else: 62 | data['Email'] = self.email 63 | data['Passwd'] = self.password 64 | data['scope'] = self.scope 65 | data['service'] = self.service 66 | data['session'] = self.session 67 | 68 | r = requests.post("https://www.google.com/accounts/ClientLogin", data=data) 69 | 70 | self.auth = r.content.split('\n')[2].split('Auth=')[1] 71 | 72 | def get_document(self): 73 | """ 74 | Uses the authentication token to fetch a google doc. 75 | """ 76 | 77 | # Handle basically all the things that can go wrong. 78 | if not self.auth: 79 | raise KeyError("Error! You didn't get an auth token. Something very bad happened. File a bug?") 80 | elif not self.key: 81 | raise KeyError("Error! You forgot to pass a key to the class.") 82 | else: 83 | headers = {} 84 | headers['Authorization'] = "GoogleLogin auth=%s" % self.auth 85 | 86 | url_params = { 'key': self.key, 'format': self.file_format } 87 | url = self.spreadsheet_url % url_params 88 | 89 | r = requests.get(url, headers=headers) 90 | 91 | if r.status_code != 200: 92 | url = self.new_spreadsheet_url % url_params 93 | r = requests.get(url, headers=headers) 94 | 95 | if r.status_code != 200: 96 | raise KeyError("Error! Your Google Doc does not exist.") 97 | 98 | with open('%s/%s/%s.%s' % (app_config.GRAPHICS_PATH, self.file_name, self.file_name, self.file_format), 'wb') as writefile: 99 | writefile.write(r.content) 100 | 101 | -------------------------------------------------------------------------------- /fabfile/flat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # _*_ coding:utf-8 _*_ 3 | import copy 4 | from fnmatch import fnmatch 5 | import hashlib 6 | import mimetypes 7 | import os 8 | from boto.s3.key import Key 9 | 10 | import app_config 11 | import utils 12 | 13 | 14 | def deploy_file(src, dst, headers={}): 15 | """ 16 | Deploy a single file to S3, if the local version is different. 17 | """ 18 | bucket = utils.get_bucket(app_config.S3_BUCKET['bucket_name']) 19 | 20 | k = bucket.get_key(dst) 21 | s3_md5 = None 22 | 23 | if k: 24 | s3_md5 = k.etag.strip('"') 25 | else: 26 | k = Key(bucket) 27 | k.key = dst 28 | 29 | file_headers = copy.copy(headers) 30 | 31 | if app_config.S3_BUCKET == app_config.STAGING_S3_BUCKET: 32 | policy = 'private' 33 | else: 34 | policy = 'public-read' 35 | 36 | if 'Content-Type' not in headers: 37 | file_headers['Content-Type'] = mimetypes.guess_type(src)[0] 38 | if file_headers['Content-Type'] == 'text/html': 39 | # Force character encoding header 40 | file_headers['Content-Type'] = '; '.join([ 41 | file_headers['Content-Type'], 42 | 'charset=utf-8']) 43 | 44 | with open(src, 'rb') as f: 45 | local_md5 = hashlib.md5() 46 | local_md5.update(f.read()) 47 | local_md5 = local_md5.hexdigest() 48 | 49 | if local_md5 == s3_md5: 50 | print 'Skipping %s (has not changed)' % src 51 | else: 52 | print 'Uploading %s --> %s' % (src, dst) 53 | k.set_contents_from_filename(src, file_headers, policy=policy) 54 | 55 | 56 | def deploy_folder(src, dst, headers={}, ignore=[]): 57 | """ 58 | Deploy a folder to S3, checking each file to see if it has changed. 59 | """ 60 | to_deploy = [] 61 | 62 | for local_path, subdirs, filenames in os.walk(src, topdown=True): 63 | rel_path = os.path.relpath(local_path, src) 64 | 65 | for name in filenames: 66 | if name.startswith('.'): 67 | continue 68 | 69 | src_path = os.path.join(local_path, name) 70 | 71 | skip = False 72 | 73 | for pattern in ignore: 74 | if fnmatch(src_path, pattern): 75 | skip = True 76 | break 77 | 78 | if skip: 79 | continue 80 | 81 | if rel_path == '.': 82 | dst_path = os.path.join(dst, name) 83 | else: 84 | dst_path = os.path.join(dst, rel_path, name) 85 | 86 | to_deploy.append((src_path, dst_path)) 87 | 88 | for src, dst in to_deploy: 89 | deploy_file(src, dst, headers) 90 | 91 | 92 | def delete_folder(dst): 93 | """ 94 | Delete a folder from S3. 95 | """ 96 | bucket = utils.get_bucket(app_config.S3_BUCKET['bucket_name']) 97 | 98 | for key in bucket.list(prefix='%s/' % dst): 99 | print 'Deleting %s' % (key.key) 100 | 101 | key.delete() 102 | -------------------------------------------------------------------------------- /fabfile/render.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # _*_ coding:utf-8 _*_ 3 | import os 4 | 5 | from fabric.api import task 6 | from fabric.state import env 7 | from glob import glob 8 | 9 | import app 10 | import app_config 11 | import utils 12 | 13 | 14 | @task(default=True) 15 | def render(path=''): 16 | """ 17 | Render HTML templates and compile assets. 18 | """ 19 | custom_location = False 20 | if path: 21 | slug, abspath = utils.parse_path(path) 22 | if abspath != app_config.GRAPHICS_PATH: 23 | custom_location = True 24 | _render_graphics(['%s/%s' % (abspath, slug)], custom_location) 25 | else: 26 | _render_graphics(glob('%s/*' % app_config.GRAPHICS_PATH)) 27 | 28 | 29 | def _render_graphics(paths, custom_location=False): 30 | """ 31 | Render a set of graphics 32 | """ 33 | from flask import g 34 | 35 | # Fake out deployment target 36 | app_config.configure_targets(env.get('settings', None)) 37 | 38 | for path in paths: 39 | slug = path.split('/')[-1] 40 | with app.app.test_request_context(path='graphics/%s/' % slug): 41 | g.compile_includes = True 42 | g.compiled_includes = {} 43 | if custom_location: 44 | # warning message 45 | g.custom_location = True 46 | g.alt_path = path 47 | # Test if there's a local pym copy 48 | if os.path.exists('%s/js/lib/pym.js' % path): 49 | g.local_pym = True 50 | view = app.graphic.__dict__['_graphics_detail'] 51 | content = view(slug).data 52 | 53 | with open('%s/index.html' % path, 'w') as writefile: 54 | writefile.write(content) 55 | 56 | # Fallback for legacy projects w/o child templates 57 | if not os.path.exists('%s/child_template.html' % path): 58 | continue 59 | 60 | with app.app.test_request_context(path='graphics/%s/child.html' % ( 61 | slug)): 62 | g.compile_includes = True 63 | g.compiled_includes = {} 64 | if custom_location: 65 | g.alt_path = path 66 | view = app.graphic.__dict__['_graphics_child'] 67 | content = view(slug).data 68 | 69 | with open('%s/child.html' % path, 'w') as writefile: 70 | writefile.write(content) 71 | 72 | # Un-fake-out deployment target 73 | app_config.configure_targets(app_config.DEPLOYMENT_TARGET) 74 | -------------------------------------------------------------------------------- /fabfile/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # _*_ coding:utf-8 _*_ 3 | import os 4 | import boto 5 | from boto.s3.connection import OrdinaryCallingFormat 6 | from fabric.api import prompt 7 | from distutils.util import strtobool 8 | import app_config 9 | 10 | 11 | def confirm(message): 12 | """ 13 | Verify a users intentions. 14 | """ 15 | answer = prompt(message, default="Not at all") 16 | 17 | if answer.lower() not in ('y', 'yes', 'buzz off', 'screw you'): 18 | exit() 19 | 20 | 21 | def replace_in_file(filename, find, replace): 22 | with open(filename, 'r') as f: 23 | contents = f.read() 24 | 25 | contents = contents.replace(find, replace) 26 | 27 | with open(filename, 'w') as f: 28 | f.write(contents) 29 | 30 | 31 | def get_bucket(bucket_name): 32 | """ 33 | Established a connection and gets s3 bucket 34 | """ 35 | if '.' in bucket_name: 36 | s3 = boto.connect_s3(calling_format=OrdinaryCallingFormat()) 37 | else: 38 | s3 = boto.connect_s3() 39 | 40 | return s3.get_bucket(bucket_name) 41 | 42 | 43 | def parse_path(path): 44 | """ 45 | Parse the path into abspath and slug 46 | """ 47 | bits = path.split('/') 48 | if len(bits) > 1: 49 | slug = bits[-1] 50 | path = '/'.join(bits[:-1]) 51 | abspath = os.path.abspath(path) 52 | else: 53 | slug = path 54 | abspath = app_config.GRAPHICS_PATH 55 | return slug, abspath 56 | 57 | 58 | def prep_bool_arg(arg): 59 | """ 60 | Util to parse fabric boolean args 61 | """ 62 | return bool(strtobool(str(arg))) 63 | -------------------------------------------------------------------------------- /graphic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # _*_ coding:utf-8 _*_ 3 | from mimetypes import guess_type 4 | import os 5 | import subprocess 6 | 7 | from flask import Blueprint, abort, make_response, render_template 8 | from jinja2 import Environment, FileSystemLoader 9 | from jinja2.exceptions import TemplateNotFound 10 | 11 | import app_config 12 | import copytext 13 | import oauth 14 | from render_utils import load_graphic_config, make_context, render_with_context, smarty_filter 15 | 16 | graphic = Blueprint('graphic', __name__) 17 | 18 | @graphic.route('//') 19 | @oauth.oauth_required 20 | def _graphics_detail(slug): 21 | """ 22 | Renders a parent.html index with child.html embedded as iframe. 23 | """ 24 | from flask import request, g 25 | 26 | alt_path = getattr(g, 'alt_path', None) 27 | if alt_path: 28 | graphic_path = alt_path 29 | else: 30 | graphic_path = '%s/%s' % (app_config.GRAPHICS_PATH, slug) 31 | 32 | # NOTE: Parent must load pym.js from same source as child to prevent version conflicts! 33 | context = make_context(asset_depth=2, root_path=graphic_path) 34 | context['slug'] = slug 35 | context['var_name'] = slug.replace('-', '_') 36 | 37 | # Use local_pym for legacy graphics 38 | local_pym = getattr(g, 'local_pym', None) 39 | context['LOCAL_PYM'] = local_pym 40 | # warning message 41 | custom_location = getattr(g, 'custom_location', None) 42 | context['CUSTOM_LOCATION'] = custom_location 43 | 44 | template = 'parent.html' 45 | 46 | try: 47 | graphic_config = load_graphic_config(graphic_path) 48 | context.update(graphic_config.__dict__) 49 | 50 | if hasattr(graphic_config, 'COPY_GOOGLE_DOC_KEY') and graphic_config.COPY_GOOGLE_DOC_KEY: 51 | copy_path = '%s/%s.xlsx' % (graphic_path, slug) 52 | 53 | if request.args.get('refresh'): 54 | oauth.get_document(graphic_config.COPY_GOOGLE_DOC_KEY, copy_path) 55 | 56 | context['COPY'] = copytext.Copy(filename=copy_path) 57 | except IOError: 58 | pass 59 | 60 | try: 61 | env = Environment(loader=FileSystemLoader(graphic_path)) 62 | template = env.get_template('parent.html') 63 | return make_response(template.render(**context)) 64 | except TemplateNotFound: 65 | return make_response(render_template(template, **context)) 66 | 67 | @graphic.route('//child.html') 68 | @oauth.oauth_required 69 | def _graphics_child(slug): 70 | """ 71 | Renders a child.html for embedding. 72 | """ 73 | from flask import g 74 | alt_path = getattr(g, 'alt_path', None) 75 | if alt_path: 76 | graphic_path = alt_path 77 | else: 78 | graphic_path = '%s/%s' % (app_config.GRAPHICS_PATH, slug) 79 | 80 | # Fallback for legacy projects w/o child templates 81 | if not os.path.exists('%s/child_template.html' % graphic_path): 82 | with open('%s/child.html' % graphic_path) as f: 83 | contents = f.read() 84 | 85 | return contents 86 | 87 | context = make_context(asset_depth=2, root_path=graphic_path) 88 | context['slug'] = slug 89 | context['var_name'] = slug.replace('-', '_') 90 | 91 | env = Environment(loader=FileSystemLoader(graphic_path)) 92 | 93 | try: 94 | graphic_config = load_graphic_config(graphic_path) 95 | context.update(graphic_config.__dict__) 96 | 97 | if hasattr(graphic_config, 'JINJA_FILTER_FUNCTIONS'): 98 | for func in graphic_config.JINJA_FILTER_FUNCTIONS: 99 | env.filters[func.__name__] = func 100 | 101 | if hasattr(graphic_config, 'COPY_GOOGLE_DOC_KEY') and graphic_config.COPY_GOOGLE_DOC_KEY: 102 | copy_path = '%s/%s.xlsx' % (graphic_path, slug) 103 | 104 | # Trim strings to avoid whitespace issues 105 | copy = copytext.Copy(filename=copy_path) 106 | for sheet in copy._copy: 107 | worksheet = copy[sheet] 108 | for row in worksheet: 109 | stripped = [] 110 | for item in row._row: 111 | if isinstance(item, str) or isinstance(item, unicode): 112 | stripped.append(item.strip()) 113 | else: 114 | stripped.append(item) 115 | row._row = stripped 116 | 117 | context['COPY'] = copy 118 | except IOError: 119 | pass 120 | 121 | env.globals.update(render=render_with_context) 122 | env.filters['smarty'] = smarty_filter 123 | template = env.get_template('child_template.html') 124 | 125 | return make_response(template.render(**context)) 126 | 127 | # Render graphic LESS files on-demand 128 | @graphic.route('//css/.less') 129 | def _graphic_less(slug, filename): 130 | """ 131 | Compiles LESS for a graphic. 132 | """ 133 | from flask import g 134 | alt_path = getattr(g, 'alt_path', None) 135 | if alt_path: 136 | graphic_path = alt_path 137 | else: 138 | graphic_path = '%s/%s' % (app_config.GRAPHICS_PATH, slug) 139 | less_path = '%s/css/%s.less' % (graphic_path, filename) 140 | 141 | if not os.path.exists(less_path): 142 | abort(404) 143 | 144 | r = subprocess.check_output(['node_modules/less/bin/lessc', less_path]) 145 | 146 | return make_response(r, 200, { 'Content-Type': 'text/css' }) 147 | 148 | # Serve arbitrary static files on-demand 149 | @graphic.route('//') 150 | def _static(slug, path): 151 | from flask import g 152 | alt_path = getattr(g, 'alt_path', None) 153 | if alt_path: 154 | graphic_path = alt_path 155 | else: 156 | graphic_path = '%s/%s' % (app_config.GRAPHICS_PATH, slug) 157 | real_path = '%s/%s' % (graphic_path, path) 158 | 159 | try: 160 | with open(real_path) as f: 161 | return f.read(), 200, { 'Content-Type': guess_type(real_path)[0] } 162 | except IOError: 163 | abort(404) 164 | -------------------------------------------------------------------------------- /graphic_templates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import imp 4 | from mimetypes import guess_type 5 | import os 6 | import subprocess 7 | import sys 8 | 9 | from flask import Blueprint, abort, make_response, render_template, render_template_string 10 | from jinja2 import Environment, FileSystemLoader 11 | 12 | import app_config 13 | import copytext 14 | import oauth 15 | from render_utils import load_graphic_config, make_context, render_with_context, smarty_filter 16 | 17 | graphic_templates = Blueprint('graphic_templates', __name__) 18 | 19 | @graphic_templates.route('//') 20 | @oauth.oauth_required 21 | def _templates_detail(slug): 22 | """ 23 | Renders a parent.html index with child.html embedded as iframe. 24 | """ 25 | from flask import request 26 | 27 | template_path = '%s/%s' % (app_config.TEMPLATES_PATH, slug) 28 | base_template_path = '%s/%s' % (app_config.TEMPLATES_PATH, '_base') 29 | 30 | # NOTE: Parent must load pym.js from same source as child to prevent version conflicts! 31 | context = make_context(asset_depth=2, root_path=template_path) 32 | context['slug'] = slug 33 | 34 | try: 35 | graphic_config = load_graphic_config(template_path, [base_template_path]) 36 | context.update(graphic_config.__dict__) 37 | 38 | if hasattr(graphic_config, 'COPY_GOOGLE_DOC_KEY') and graphic_config.COPY_GOOGLE_DOC_KEY: 39 | copy_path = '%s/%s.xlsx' % (template_path, slug) 40 | 41 | if request.args.get('refresh'): 42 | oauth.get_document(graphic_config.COPY_GOOGLE_DOC_KEY, copy_path) 43 | 44 | context['COPY'] = copytext.Copy(filename=copy_path) 45 | except IOError: 46 | pass 47 | 48 | return make_response(render_template('parent.html', **context)) 49 | 50 | @graphic_templates.route('//child.html') 51 | @oauth.oauth_required 52 | def _templates_child(slug): 53 | """ 54 | Renders a child.html for embedding. 55 | """ 56 | template_path = '%s/%s' % (app_config.TEMPLATES_PATH, slug) 57 | base_template_path = '%s/%s' % (app_config.TEMPLATES_PATH, '_base') 58 | 59 | # Fallback for legacy projects w/o child templates 60 | if not os.path.exists('%s/child_template.html' % template_path): 61 | with open('%s/child.html' % template_path) as f: 62 | contents = f.read() 63 | 64 | return contents 65 | 66 | context = make_context(asset_depth=2, root_path=template_path) 67 | context['slug'] = slug 68 | 69 | env = Environment(loader=FileSystemLoader([template_path, '%s/_base' % app_config.TEMPLATES_PATH])) 70 | 71 | try: 72 | graphic_config = load_graphic_config(template_path, [base_template_path]) 73 | context.update(graphic_config.__dict__) 74 | 75 | if hasattr(graphic_config, 'JINJA_FILTER_FUNCTIONS'): 76 | for func in graphic_config.JINJA_FILTER_FUNCTIONS: 77 | env.filters[func.__name__] = func 78 | 79 | if hasattr(graphic_config, 'COPY_GOOGLE_DOC_KEY') and graphic_config.COPY_GOOGLE_DOC_KEY: 80 | copy_path = '%s/%s.xlsx' % (template_path, slug) 81 | 82 | context['COPY'] = copytext.Copy(filename=copy_path) 83 | except IOError: 84 | pass 85 | 86 | env.globals.update(render=render_with_context) 87 | env.filters['smarty'] = smarty_filter 88 | template = env.get_template('child_template.html') 89 | 90 | return make_response(template.render(**context)) 91 | 92 | # Render graphic LESS files on-demand 93 | @graphic_templates.route('//css/.less') 94 | def _templates_less(slug, filename): 95 | """ 96 | Compiles LESS for a graphic. 97 | """ 98 | template_path = '%s/%s' % (app_config.TEMPLATES_PATH, slug) 99 | less_path = '%s/css/%s.less' % (template_path, filename) 100 | base_less_path = '%s/_base/css/base.less' % app_config.TEMPLATES_PATH 101 | temp_base_less_path = '%s/css/base.less' % template_path 102 | 103 | if not os.path.exists(less_path): 104 | less_path = '%s/_base/css/%s.less' % (app_config.TEMPLATES_PATH, filename) 105 | 106 | if not os.path.exists(less_path): 107 | abort(404) 108 | 109 | if os.path.exists(temp_base_less_path): 110 | os.remove(temp_base_less_path) 111 | 112 | # Temp symlink base.less so it can be included by less compiler 113 | os.symlink(base_less_path, temp_base_less_path) 114 | 115 | r = subprocess.check_output(['node_modules/less/bin/lessc', less_path]) 116 | 117 | # Remove temporary symlink 118 | os.remove(temp_base_less_path) 119 | 120 | return make_response(r, 200, { 'Content-Type': 'text/css' }) 121 | 122 | # Serve arbitrary static files from either graphic or base graphic paths 123 | @graphic_templates.route('//') 124 | def _static(slug, path): 125 | template_path = '%s/%s' % (app_config.TEMPLATES_PATH, slug) 126 | real_path = '%s/%s' % (template_path, path) 127 | 128 | if not os.path.exists(real_path): 129 | real_path = '%s/_base/%s' % (app_config.TEMPLATES_PATH, path) 130 | 131 | if not os.path.exists(real_path): 132 | abort(404) 133 | 134 | with open('%s' % real_path) as f: 135 | return f.read(), 200, { 'Content-Type': guess_type(real_path)[0] } 136 | -------------------------------------------------------------------------------- /graphic_templates/_base/assets/assetsignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /graphic_templates/_base/assets/private/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_base/assets/private/.placeholder -------------------------------------------------------------------------------- /graphic_templates/_base/base_filters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import locale 4 | import re 5 | 6 | locale.setlocale(locale.LC_ALL, 'en_US') 7 | 8 | MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] 9 | AP_MONTHS = ['Jan.', 'Feb.', 'March', 'April', 'May', 'June', 'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.'] 10 | ORDINAL_SUFFIXES = { 1: 'st', 2: 'nd', 3: 'rd' } 11 | 12 | USPS_TO_AP_STATE = { 13 | 'AL': 'Ala.', 14 | 'AK': 'Alaska', 15 | 'AR': 'Ark.', 16 | 'AZ': 'Ariz.', 17 | 'CA': 'Calif.', 18 | 'CO': 'Colo.', 19 | 'CT': 'Conn.', 20 | 'DC': 'D.C.', 21 | 'DE': 'Del.', 22 | 'FL': 'Fla.', 23 | 'GA': 'Ga.', 24 | 'HI': 'Hawaii', 25 | 'IA': 'Iowa', 26 | 'ID': 'Idaho', 27 | 'IL': 'Ill.', 28 | 'IN': 'Ind.', 29 | 'KS': 'Kan.', 30 | 'KY': 'Ky.', 31 | 'LA': 'La.', 32 | 'MA': 'Mass.', 33 | 'MD': 'Md.', 34 | 'ME': 'Maine', 35 | 'MI': 'Mich.', 36 | 'MN': 'Minn.', 37 | 'MO': 'Mo.', 38 | 'MS': 'Miss.', 39 | 'MT': 'Mont.', 40 | 'NC': 'N.C.', 41 | 'ND': 'N.D.', 42 | 'NE': 'Neb.', 43 | 'NH': 'N.H.', 44 | 'NJ': 'N.J.', 45 | 'NM': 'N.M.', 46 | 'NV': 'Nev.', 47 | 'NY': 'N.Y.', 48 | 'OH': 'Ohio', 49 | 'OK': 'Okla.', 50 | 'OR': 'Ore.', 51 | 'PA': 'Pa.', 52 | 'PR': 'P.R.', 53 | 'RI': 'R.I.', 54 | 'SC': 'S.C.', 55 | 'SD': 'S.D.', 56 | 'TN': 'Tenn.', 57 | 'TX': 'Texas', 58 | 'UT': 'Utah', 59 | 'VA': 'Va.', 60 | 'VT': 'Vt.', 61 | 'WA': 'Wash.', 62 | 'WI': 'Wis.', 63 | 'WV': 'W.Va.', 64 | 'WY': 'Wyo.' 65 | } 66 | 67 | def classify(text): 68 | """ 69 | Convert arbitrary strings to valid css classes. 70 | 71 | NOTE: This implementation must be consistent with the Javascript classify 72 | function defined in base.js. 73 | """ 74 | text = unicode(text) # Always start with unicode 75 | text = text.encode('ascii', 'ignore') # Convert to ascii 76 | text = text.lower() # Lowercase 77 | text = re.sub('\s+', '-', text) # Replace spaces with - 78 | text = re.sub('[^\w\-]+', '', text) # Remove all non-word chars 79 | text = re.sub('\-\-+', '-', text) # Replace multiple - with single - 80 | text = re.sub('^-+', '', text) # Trim - from start of text 81 | text = re.sub('-+$', '', text) # Trim - from end of text 82 | 83 | return text 84 | 85 | def comma(value): 86 | """ 87 | Format a number with commas. 88 | """ 89 | return locale.format('%d', float(value), grouping=True) 90 | 91 | def ordinal(num): 92 | """ 93 | Format a number as an ordinal. 94 | """ 95 | num = int(num) 96 | 97 | if 10 <= num % 100 <= 20: 98 | suffix = 'th' 99 | else: 100 | suffix = ORDINAL_SUFFIXES.get(num % 10, 'th') 101 | 102 | return unicode(num) + suffix 103 | 104 | def ap_month(month): 105 | """ 106 | Convert a month name into AP abbreviated style. 107 | """ 108 | i = months.index(month) 109 | 110 | return AP_MONTHS[int(month) - 1] 111 | 112 | def ap_date(value): 113 | """ 114 | Converts a date string in m/d/yyyy format into AP style. 115 | """ 116 | if not value: 117 | return '' 118 | 119 | bits = unicode(value).split('/') 120 | 121 | month, day, year = bits 122 | 123 | output = AP_MONTHS[int(month) - 1] 124 | output += ' ' + unicode(int(day)) 125 | output += ', ' + year 126 | 127 | return output 128 | 129 | def ap_state(usps): 130 | """ 131 | Convert a USPS state abbreviation into AP style. 132 | """ 133 | return USPS_TO_AP_STATE[unicode(usps)] 134 | 135 | FILTERS = [ 136 | classify, 137 | comma, 138 | ordinal, 139 | ap_month, 140 | ap_date, 141 | ap_state 142 | ] 143 | -------------------------------------------------------------------------------- /graphic_templates/_base/base_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Graphic : NPR 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block fonts %} 17 | 18 | 19 | 20 | 35 | 36 | {% endblock %} 37 | 38 | 39 | 43 | 44 | 45 | {{ CSS.push('css/graphic.less') }} 46 | {{ CSS.render('css/graphic-header.css') }} 47 | 48 | 49 | 50 | {{ JS.push('js/helpers.js') }} 51 | {{ JS.push('js/analytics.js') }} 52 | {{ JS.render('js/graphic-header.js') }} 53 | 54 | {% block content %} 55 | 56 | The child template content goes here! 57 | 58 | {% endblock content %} 59 | 60 | 61 | {% block js %} 62 | {{ JS.push('js/lib/underscore.js') }} 63 | {{ JS.push('js/lib/d3.min.js') }} 64 | {{ JS.push('js/base.js') }} 65 | {{ JS.push('js/graphic.js') }} 66 | {{ JS.render('js/graphic-footer.js') }} 67 | {% endblock js %} 68 | 69 | 70 | -------------------------------------------------------------------------------- /graphic_templates/_base/css/base.less: -------------------------------------------------------------------------------- 1 | /* 2 | * Media queries 3 | */ 4 | @screen-medium-above: ~"screen and (min-width: 651px)"; 5 | @screen-mobile-above: ~"screen and (min-width: 501px)"; 6 | @screen-mobile: ~"screen and (max-width: 500px)"; 7 | 8 | /* 9 | * Colors 10 | */ 11 | @red1 : #6C2315; 12 | @red2 : #A23520; 13 | @red3 : #D8472B; 14 | @red4 : #E27560; 15 | @red5 : #ECA395; 16 | @red6 : #F5D1CA; 17 | 18 | @orange1 : #714616; 19 | @orange2 : #AA6A21; 20 | @orange3 : #E38D2C; 21 | @orange4 : #EAAA61; 22 | @orange5 : #F1C696; 23 | @orange6 : #F8E2CA; 24 | 25 | @yellow1 : #77631B; 26 | @yellow2 : #B39429; 27 | @yellow3 : #EFC637; 28 | @yellow4 : #F3D469; 29 | @yellow5 : #F7E39B; 30 | @yellow6 : #FBF1CD; 31 | 32 | @teal1 : #0B403F; 33 | @teal2 : #11605E; 34 | @teal3 : #17807E; 35 | @teal4 : #51A09E; 36 | @teal5 : #8BC0BF; 37 | @teal6 : #C5DFDF; 38 | 39 | @blue1 : #28556F; 40 | @blue2 : #3D7FA6; 41 | @blue3 : #51AADE; 42 | @blue4 : #7DBFE6; 43 | @blue5 : #A8D5EF; 44 | @blue6 : #D3EAF7; 45 | 46 | /* 47 | * Fonts 48 | */ 49 | .gotham() { 50 | font-family: 'Gotham SSm',Helvetica,Arial,sans-serif; 51 | font-weight: normal; 52 | font-weight: 400; 53 | } 54 | 55 | // Normal Knockout 56 | .knockout() { 57 | font-family: 'Knockout 31 4r','Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 58 | font-weight: normal; 59 | } 60 | 61 | // Knockout, uppercased 62 | .knockout-upper() { 63 | .knockout(); 64 | text-transform: uppercase; 65 | } 66 | 67 | // Knockout for headings 68 | .knockout-header() { 69 | .knockout-upper(); 70 | letter-spacing: 0.05em; 71 | -webkit-font-smoothing: antialiased; 72 | } 73 | 74 | .sans-serif() { 75 | font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 76 | } 77 | 78 | /* 79 | * Mixins 80 | */ 81 | .clearfix() { 82 | &:before, 83 | &:after { 84 | content: " "; 85 | display: table; 86 | } 87 | &:after { 88 | clear: both; 89 | } 90 | } 91 | 92 | // NPR multimedia-template 93 | .mmedia-constrained() { 94 | margin-left: auto; 95 | margin-right: auto; 96 | max-width: 650px; 97 | } 98 | 99 | .mmedia-constrained-centered() { 100 | .mmedia-constrained(); 101 | text-align: center; 102 | } 103 | 104 | /* 105 | * Base styles 106 | */ 107 | html { -webkit-text-size-adjust: none; } 108 | 109 | body { 110 | margin: 0; 111 | padding: 33px 0; 112 | font: 14px/1.4 Helvetica, Arial, sans-serif; 113 | color: #555; 114 | } 115 | 116 | h1 { 117 | margin: 0 0 33px 0; 118 | font-size: 20px; 119 | color: #666; 120 | font-family: 'Gotham SSm',Helvetica,Arial,sans-serif; 121 | font-weight: normal; 122 | line-height: 1.3; 123 | font-weight: 400; 124 | -webkit-font-smoothing: antialiased; 125 | } 126 | 127 | h2 { 128 | font-weight: normal; 129 | color: #777; 130 | font-size: 12px; 131 | margin: -22px 0 22px 0; 132 | line-height: 1.6; 133 | } 134 | 135 | h3 { 136 | .knockout-header(); 137 | color: #333; 138 | font-size: 12px; 139 | line-height: 1.2; 140 | margin: 0 0 15px 0; 141 | padding-top: 0; 142 | } 143 | 144 | .nowrap { white-space: nowrap; } 145 | 146 | .footnotes { 147 | margin-bottom: 20px; 148 | 149 | h4 { 150 | margin: 2px 0 7px 0; 151 | color: #666; 152 | font-size: 11px; 153 | } 154 | } 155 | 156 | .footnotes p, 157 | .footer p { 158 | margin: 2px 0 0 0; 159 | font-size: 11px; 160 | line-height: 1.7; 161 | color: #999; 162 | } 163 | 164 | .footer p { font-style: italic; } 165 | .footer p em { font-style: normal; } 166 | .footnotes p strong { color: #666; } 167 | 168 | a, a:link, a:visited { 169 | color: #4774CC; 170 | text-decoration: none; 171 | } 172 | 173 | a:hover, a:active { color: #bccae5; } 174 | 175 | /* 176 | * Standard graphic styles 177 | */ 178 | .graphic-wrapper { 179 | position: relative; 180 | } 181 | 182 | .graphic { 183 | .clearfix(); 184 | margin-bottom: 22px; 185 | position: relative; 186 | 187 | img { 188 | max-width: 100%; 189 | height: auto; 190 | } 191 | } 192 | 193 | .key { 194 | margin: -11px 0 33px 0; 195 | padding: 0; 196 | list-style-type: none; 197 | 198 | .key-item { 199 | display: inline-block; 200 | margin: 0 18px 0 0; 201 | padding: 0; 202 | line-height: 15px; 203 | 204 | b { 205 | display: inline-block; 206 | width: 15px; 207 | height: 15px; 208 | margin-right: 6px; 209 | float: left; 210 | } 211 | 212 | label { 213 | white-space: nowrap; 214 | font-size: 12px; 215 | color: #666; 216 | font-weight: normal; 217 | -webkit-font-smoothing: antialiased; 218 | } 219 | } 220 | } 221 | 222 | svg { overflow: hidden; } 223 | 224 | .axis { 225 | font-size: 11px; 226 | -webkit-font-smoothing: antialiased; 227 | fill: #999; 228 | 229 | path, 230 | line { 231 | fill: none; 232 | stroke: #ccc; 233 | shape-rendering: crispEdges; 234 | } 235 | 236 | &.y { 237 | path { display: none; } 238 | .tick line { display: none; } 239 | } 240 | } 241 | 242 | .grid { 243 | path { display: none; } 244 | 245 | .tick { 246 | stroke: #eee; 247 | stroke-width: 1px; 248 | shape-rendering: crispEdges; 249 | } 250 | 251 | &.y { 252 | g:first-child line { display: none; } 253 | } 254 | } 255 | 256 | .zero-line { 257 | stroke: #666; 258 | stroke-width: 1px; 259 | shape-rendering: crispEdges; 260 | } 261 | 262 | line, 263 | rect { 264 | shape-rendering: crispEdges; 265 | } 266 | 267 | .bars rect { fill: @teal3; } 268 | 269 | .labels { 270 | position: absolute; 271 | margin: 0; 272 | padding: 0; 273 | list-style-type: none; 274 | border: none; 275 | 276 | li { 277 | position: absolute; 278 | text-align: right; 279 | font-size: 12px; 280 | line-height: 1.3; 281 | color: #666; 282 | display: table; 283 | -webkit-font-smoothing: antialiased; 284 | 285 | span { 286 | display: table-cell; 287 | vertical-align: middle; 288 | } 289 | } 290 | } 291 | 292 | .value text { 293 | font-size: 10px; 294 | -webkit-font-smoothing: antialiased; 295 | 296 | &.in { fill: #fff; } 297 | &.out { fill: #999; } 298 | } 299 | 300 | /* 301 | * HOMEPAGE 302 | * styles for when a project is embedded on the homepage 303 | * (if someone clicked the "This code will be embedded on the NPR homepage." 304 | * checkbox when pulling the embed code.) 305 | */ 306 | body.hp { 307 | padding-top: 0; 308 | padding-bottom: 10px; 309 | } 310 | 311 | /* 312 | * CHILDLINK 313 | * Direct links to the child page (iOS app workaround link) 314 | */ 315 | body.childlink { 316 | margin-left: auto; 317 | margin-right: auto; 318 | max-width: 800px; 319 | } 320 | 321 | /* 322 | * Accessibility styles 323 | */ 324 | 325 | img:not([alt]) { 326 | outline: 3px solid red; 327 | } 328 | -------------------------------------------------------------------------------- /graphic_templates/_base/js/analytics.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Module for tracking standardized analytics. 3 | */ 4 | 5 | var ANALYTICS = (function () { 6 | /* 7 | * Google Analytics 8 | */ 9 | var DIMENSION_PARENT_URL = 'dimension1'; 10 | var DIMENSION_PARENT_HOSTNAME = 'dimension2'; 11 | var DIMENSION_PARENT_INITIAL_WIDTH = 'dimension3'; 12 | 13 | var setupGoogle = function() { 14 | (function(i,s,o,g,r,a,m) { 15 | i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 16 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 17 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 18 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 19 | 20 | ga('create', GOOGLE_ANALYTICS_ACCOUNT_ID, 'auto'); 21 | 22 | // By default Google tracks the query string, but we want to ignore it. 23 | var location = window.location.protocol + 24 | '//' + window.location.hostname + 25 | window.location.pathname; 26 | 27 | ga('set', 'location', location); 28 | ga('set', 'page', window.location.pathname); 29 | 30 | // Custom dimensions & metrics 31 | var parentUrl = getParameterByName('parentUrl') || ''; 32 | var parentHostname = ''; 33 | 34 | if (parentUrl) { 35 | parentHostname = urlToLocation(parentUrl).hostname; 36 | } 37 | 38 | var initialWidth = getParameterByName('initialWidth') || ''; 39 | 40 | var customData = {}; 41 | customData[DIMENSION_PARENT_URL] = parentUrl; 42 | customData[DIMENSION_PARENT_HOSTNAME] = parentHostname; 43 | customData[DIMENSION_PARENT_INITIAL_WIDTH] = initialWidth; 44 | 45 | // Track pageview 46 | ga('send', 'pageview', customData); 47 | } 48 | 49 | /* 50 | * Event tracking. 51 | */ 52 | var trackEvent = function(eventName, label, value) { 53 | var eventData = { 54 | 'hitType': 'event', 55 | 'eventCategory': GOOGLE_ANALYTICS_PROJECT_SLUG, 56 | 'eventAction': eventName 57 | } 58 | 59 | if (label) { 60 | eventData['eventLabel'] = label; 61 | } 62 | 63 | if (value) { 64 | eventData['eventValue'] = value 65 | } 66 | 67 | // Track details about the parent with each event 68 | var parentUrl = getParameterByName('parentUrl') || ''; 69 | var parentHostname = ''; 70 | if (parentUrl) { 71 | parentHostname = urlToLocation(parentUrl).hostname; 72 | } 73 | eventData[DIMENSION_PARENT_URL] = parentUrl; 74 | eventData[DIMENSION_PARENT_HOSTNAME] = parentHostname; 75 | 76 | ga('send', eventData); 77 | } 78 | 79 | setupGoogle(); 80 | 81 | return { 82 | 'trackEvent': trackEvent 83 | }; 84 | }()); 85 | -------------------------------------------------------------------------------- /graphic_templates/_base/js/base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Base Javascript code for graphics, including D3 helpers. 3 | */ 4 | 5 | // Global config 6 | var DEFAULT_WIDTH = 300; 7 | var MOBILE_THRESHOLD = 500; 8 | 9 | // D3 formatters 10 | var fmtComma = d3.format(','); 11 | var fmtYearAbbrev = d3.time.format('%y'); 12 | var fmtYearFull = d3.time.format('%Y'); 13 | var fmtMonthNum = d3.time.format('%m'); 14 | 15 | var formatFullDate = function(d) { 16 | // Output example: Dec. 23, 2014 17 | var fmtDayYear = d3.time.format('%e, %Y'); 18 | return getAPMonth(d) + ' ' + fmtDayYear(d).trim(); 19 | }; 20 | -------------------------------------------------------------------------------- /graphic_templates/_base/js/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Basic Javascript helpers used in analytics.js and graphics code. 3 | */ 4 | 5 | var COLORS = { 6 | 'red1': '#6C2315', 'red2': '#A23520', 'red3': '#D8472B', 'red4': '#E27560', 'red5': '#ECA395', 'red6': '#F5D1CA', 7 | 'orange1': '#714616', 'orange2': '#AA6A21', 'orange3': '#E38D2C', 'orange4': '#EAAA61', 'orange5': '#F1C696', 'orange6': '#F8E2CA', 8 | 'yellow1': '#77631B', 'yellow2': '#B39429', 'yellow3': '#EFC637', 'yellow4': '#F3D469', 'yellow5': '#F7E39B', 'yellow6': '#FBF1CD', 9 | 'teal1': '#0B403F', 'teal2': '#11605E', 'teal3': '#17807E', 'teal4': '#51A09E', 'teal5': '#8BC0BF', 'teal6': '#C5DFDF', 10 | 'blue1': '#28556F', 'blue2': '#3D7FA6', 'blue3': '#51AADE', 'blue4': '#7DBFE6', 'blue5': '#A8D5EF', 'blue6': '#D3EAF7' 11 | }; 12 | var isHomepage = false; 13 | 14 | 15 | /* 16 | * Convert arbitrary strings to valid css classes. 17 | * via: https://gist.github.com/mathewbyrne/1280286 18 | * 19 | * NOTE: This implementation must be consistent with the Python classify 20 | * function defined in base_filters.py. 21 | */ 22 | var classify = function(str) { 23 | return str.toLowerCase() 24 | .replace(/\s+/g, '-') // Replace spaces with - 25 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 26 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 27 | .replace(/^-+/, '') // Trim - from start of text 28 | .replace(/-+$/, ''); // Trim - from end of text 29 | } 30 | 31 | /* 32 | * Convert key/value pairs to a style string. 33 | */ 34 | var formatStyle = function(props) { 35 | var s = ''; 36 | 37 | for (var key in props) { 38 | s += key + ': ' + props[key].toString() + '; '; 39 | } 40 | 41 | return s; 42 | } 43 | 44 | /* 45 | * Create a SVG tansform for a given translation. 46 | */ 47 | var makeTranslate = function(x, y) { 48 | var transform = d3.transform(); 49 | 50 | transform.translate[0] = x; 51 | transform.translate[1] = y; 52 | 53 | return transform.toString(); 54 | } 55 | 56 | /* 57 | * Parse a url parameter by name. 58 | * via: http://stackoverflow.com/a/901144 59 | */ 60 | var getParameterByName = function(name) { 61 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 62 | var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), 63 | results = regex.exec(location.search); 64 | return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); 65 | } 66 | 67 | /* 68 | * Convert a url to a location object. 69 | */ 70 | var urlToLocation = function(url) { 71 | var a = document.createElement('a'); 72 | a.href = url; 73 | return a; 74 | } 75 | 76 | /* 77 | * format month abbrs in AP style 78 | */ 79 | var getAPMonth = function(dateObj) { 80 | var apMonths = [ 'Jan.', 'Feb.', 'March', 'April', 'May', 'June', 'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.' ]; 81 | var thisMonth = +fmtMonthNum(dateObj) - 1; 82 | return apMonths[thisMonth]; 83 | } 84 | 85 | /* 86 | * Wrap a block of SVG text to a given width 87 | * adapted from http://bl.ocks.org/mbostock/7555321 88 | */ 89 | var wrapText = function(texts, width, lineHeight) { 90 | texts.each(function() { 91 | var text = d3.select(this); 92 | var words = text.text().split(/\s+/).reverse(); 93 | 94 | var word = null; 95 | var line = []; 96 | var lineNumber = 0; 97 | 98 | var x = text.attr('x'); 99 | var y = text.attr('y'); 100 | 101 | var dx = text.attr('dx') ? parseFloat(text.attr('dx')) : 0; 102 | var dy = text.attr('dy') ? parseFloat(text.attr('dy')) : 0; 103 | 104 | var tspan = text.text(null) 105 | .append('tspan') 106 | .attr('x', x) 107 | .attr('y', y) 108 | .attr('dx', dx + 'px') 109 | .attr('dy', dy + 'px'); 110 | 111 | while (word = words.pop()) { 112 | line.push(word); 113 | tspan.text(line.join(' ')); 114 | 115 | if (tspan.node().getComputedTextLength() > width) { 116 | line.pop(); 117 | tspan.text(line.join(' ')); 118 | line = [word]; 119 | 120 | lineNumber += 1; 121 | 122 | tspan = text.append('tspan') 123 | .attr('x', x) 124 | .attr('y', y) 125 | .attr('dx', dx + 'px') 126 | .attr('dy', (lineNumber * lineHeight) + dy + 'px') 127 | .attr('text-anchor', 'begin') 128 | .text(word); 129 | } 130 | } 131 | }); 132 | } 133 | 134 | /* 135 | * Constructs a location object from a url 136 | */ 137 | var getLocation = function(href) { 138 | var l = document.createElement("a"); 139 | l.href = href; 140 | return l; 141 | }; 142 | 143 | /* 144 | * Checks if we are in production based on the url hostname 145 | * When embedded with pym it checks the parentUrl param 146 | * - If a url is given checks that 147 | * - If no url is given checks window.location.href 148 | */ 149 | var isProduction = function(u) { 150 | var result = true; 151 | var u = u || window.location.href; 152 | var re_embedded = /^.*parentUrl=(.*)$/; 153 | // Check if we are inside the dailygraphics local rig 154 | var m = u.match(re_embedded) 155 | if (m) { 156 | u = decodeURIComponent(m[1]) 157 | } 158 | l = getLocation(u); 159 | if (l.hostname.startsWith("localhost") || 160 | l.hostname.startsWith("stage-") || 161 | l.hostname.startsWith("www-s1")) { 162 | result = false 163 | } 164 | return result; 165 | } 166 | 167 | /* 168 | * Polyfill for String.trim() 169 | */ 170 | if (!String.prototype.trim) { 171 | String.prototype.trim = function () { 172 | return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); 173 | }; 174 | } 175 | 176 | 177 | /* 178 | * Check for special URL parameters 179 | */ 180 | switch (getParameterByName('mode')) { 181 | // Homepage (if someone clicked the "This code will be embedded 182 | // on the NPR homepage." checkbox when pulling the embed code.) 183 | case 'hp': 184 | document.body.classList.add('hp'); 185 | isHomepage = true; 186 | break; 187 | // Direct links to the child page (iOS app workaround link) 188 | case 'childlink': 189 | document.body.classList.add('childlink'); 190 | break; 191 | } 192 | -------------------------------------------------------------------------------- /graphic_templates/_thumbs/animated-photo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/animated-photo.gif -------------------------------------------------------------------------------- /graphic_templates/_thumbs/bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/bar-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/block-histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/block-histogram.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/column-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/column-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/diverging-bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/diverging-bar-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/dot-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/dot-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/graphic.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/grouped-bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/grouped-bar-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/issue-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/issue-matrix.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/line-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/line-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/locator-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/locator-map.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/newsletter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/newsletter.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/slopegraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/slopegraph.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/stacked-bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/stacked-bar-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/stacked-column-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/stacked-column-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/stacked-grouped-column-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/stacked-grouped-column-chart.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/state-grid-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/state-grid-map.png -------------------------------------------------------------------------------- /graphic_templates/_thumbs/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/_thumbs/table.png -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/assets/ai2html-graphic.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/ai2html_graphic/assets/ai2html-graphic.ai -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | {% endblock content %} 29 | 30 | {% block js %} 31 | {{ JS.push('js/graphic.js') }} 32 | {{ JS.render('js/graphic-footer.js') }} 33 | {% endblock js %} 34 | -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | // ai2html artboard sizes 4 | @media screen and (min-width: 730px) { 5 | #g-ai2html-graphic-small, #g-ai2html-graphic-medium { display: none; } 6 | } 7 | 8 | @media screen and (max-width: 729px) and (min-width: 400px) { 9 | #g-ai2html-graphic-small, #g-ai2html-graphic-wide { display: none; } 10 | } 11 | 12 | @media screen and (max-width: 399px) { 13 | #g-ai2html-graphic-medium, #g-ai2html-graphic-wide { display: none; } 14 | } 15 | 16 | // Add custom styles here 17 | -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1G5p4UIsKwUK303k4gLXhAmFnret0lg3w7qLm5S62Tsg' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/img/ai2html-graphic-medium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/ai2html_graphic/img/ai2html-graphic-medium.jpg -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/img/ai2html-graphic-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/ai2html_graphic/img/ai2html-graphic-small.jpg -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/img/ai2html-graphic-wide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/ai2html_graphic/img/ai2html-graphic-wide.jpg -------------------------------------------------------------------------------- /graphic_templates/ai2html_graphic/js/graphic.js: -------------------------------------------------------------------------------- 1 | // Global vars 2 | var pymChild = null; 3 | var isMobile = false; 4 | 5 | /* 6 | * Initialize the graphic. 7 | */ 8 | var onWindowLoaded = function() { 9 | pymChild = new pym.Child({}); 10 | 11 | pymChild.onMessage('on-screen', function(bucket) { 12 | ANALYTICS.trackEvent('on-screen', bucket); 13 | }); 14 | pymChild.onMessage('scroll-depth', function(data) { 15 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 16 | }); 17 | } 18 | 19 | /* 20 | * Initially load the graphic 21 | * (NB: Use window.load to ensure all images have loaded) 22 | */ 23 | window.onload = onWindowLoaded; 24 | -------------------------------------------------------------------------------- /graphic_templates/animated_photo/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 | {% if COPY.labels.caption or COPY.labels.credit %} 8 |
9 | {% if COPY.labels.caption %}

{{ render(COPY.labels.caption)|smarty }}

{% endif %} 10 | {% if COPY.labels.credit %}

{{ COPY.labels.credit|smarty }}

{% endif %} 11 |
12 | {% endif %} 13 | 14 | 17 | {% endblock content %} 18 | 19 | {% block js %} 20 | 21 | {{ JS.push('js/lib/canvid.js') }} 22 | {{ JS.push('js/graphic.js') }} 23 | {{ JS.render('js/graphic-footer.js') }} 24 | 25 | {% endblock js %} 26 | -------------------------------------------------------------------------------- /graphic_templates/animated_photo/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | body { padding: 0; } 4 | 5 | .caption { 6 | margin: 10px 0 2px 0; 7 | padding: 0; 8 | font-size: 12px; 9 | color: #777; 10 | line-height: 20px; 11 | 12 | p { 13 | margin: 0 0 2px 0; 14 | 15 | &.credit { 16 | color: #aaa; 17 | line-height: 1.7; 18 | font-size: 10px; 19 | font-style: italic; 20 | margin-bottom: 0; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphic_templates/animated_photo/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1LIuYuOkquiTVmF9m_YUsSeKRFeuI_wPAqQ7OF9NI7qI' 6 | 7 | USE_ASSETS = True 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | DEFAULT_MAX_AGE = 20 11 | ASSETS_MAX_AGE = 20 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/filmstrip-1000.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/filmstrip-1000.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/filmstrip-375.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/filmstrip-375.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/filmstrip-600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/filmstrip-600.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/filmstrip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/filmstrip.gif -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon01.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon02.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon03.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon04.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon05.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon06.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon07.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/img/frames/moon08.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nprapps/dailygraphics/096d05af42391f3180e4675bc1bf6d5948a1cbd3/graphic_templates/animated_photo/img/frames/moon08.jpg -------------------------------------------------------------------------------- /graphic_templates/animated_photo/js/graphic.js: -------------------------------------------------------------------------------- 1 | // Global vars 2 | var pymChild = null; 3 | var isMobile = false; 4 | var BREAKPOINTS = [ 375, 600, 1000 ]; 5 | 6 | // Canvid params are defined in the spreadsheet 7 | var IMAGE_FOLDER = PARAMS['image_folder']; 8 | var IMAGE_WIDTH = +PARAMS['image_width']; 9 | var IMAGE_HEIGHT = +PARAMS['image_height']; 10 | var FRAMES = +PARAMS['frames']; 11 | var COLS = +PARAMS['cols']; 12 | var FRAMES_PER_SECOND = +PARAMS['frames_per_second']; 13 | 14 | /* 15 | * Initialize the graphic. 16 | */ 17 | var onWindowLoaded = function() { 18 | pymChild = new pym.Child({ 19 | renderCallback: render 20 | }); 21 | 22 | pymChild.onMessage('on-screen', function(bucket) { 23 | ANALYTICS.trackEvent('on-screen', bucket); 24 | }); 25 | pymChild.onMessage('scroll-depth', function(data) { 26 | data = JSON.parse(data); 27 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 28 | }); 29 | } 30 | 31 | /* 32 | * Render the graphic. 33 | */ 34 | var render = function(containerWidth) { 35 | if (!containerWidth) { 36 | containerWidth = DEFAULT_WIDTH; 37 | } 38 | 39 | var sprite = IMAGE_FOLDER + '/filmstrip-' + BREAKPOINTS[BREAKPOINTS.length - 1] + '.jpg'; 40 | for (var i = 0; i < BREAKPOINTS.length; i++) { 41 | if (i == 0 && containerWidth <= BREAKPOINTS[i]) { 42 | sprite = IMAGE_FOLDER + '/filmstrip-' + BREAKPOINTS[i] + '.jpg'; 43 | } else if (containerWidth > BREAKPOINTS[(i - 1)] && containerWidth <= BREAKPOINTS[i]) { 44 | sprite = IMAGE_FOLDER + '/filmstrip-' + BREAKPOINTS[i] + '.jpg'; 45 | } 46 | } 47 | 48 | // clear out previous canvid if it exists 49 | var photoContainers = document.getElementById('graphic'); 50 | while (photoContainers.hasChildNodes()) { 51 | photoContainers.removeChild(photoContainers.firstChild); 52 | } 53 | 54 | var canvidControl = canvid({ 55 | selector : '.photo', 56 | videos: { 57 | // frames = # of stills 58 | // cols = # of stills in a row in the filmstrip. in this case, 59 | // same as frames. 60 | // fps = frames per second (animation speed). integers only 61 | photo: { src: sprite, frames: FRAMES, cols: COLS, fps: FRAMES_PER_SECOND } 62 | }, 63 | width: containerWidth, 64 | // multiply by height, width of original image 65 | height: Math.floor(containerWidth * IMAGE_HEIGHT / IMAGE_WIDTH), 66 | loaded: function() { 67 | canvidControl.play('photo'); 68 | 69 | // Update iframe 70 | if (pymChild) { 71 | pymChild.sendHeight(); 72 | } 73 | 74 | } 75 | }); 76 | } 77 | 78 | 79 | /* 80 | * Initially load the graphic 81 | * (NB: Use window.load to ensure all images have loaded) 82 | */ 83 | window.onload = onWindowLoaded; 84 | -------------------------------------------------------------------------------- /graphic_templates/animated_photo/js/lib/canvid.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | if (typeof module !== 'undefined' && module.exports) { 3 | module.exports = factory(); 4 | } else if (typeof define === 'function' && define.amd) { 5 | define([], factory); 6 | } else { 7 | root.canvid = factory(); 8 | } 9 | }(this, function() { 10 | 11 | function canvid(params) { 12 | var defaultOptions = { 13 | width : 800, 14 | height : 450, 15 | selector: '.canvid-wrapper' 16 | }, 17 | firstPlay = true, 18 | control = { 19 | play: function() { 20 | console.log('Cannot play before images are loaded'); 21 | } 22 | }, 23 | _opts = merge(defaultOptions, params), 24 | el = typeof _opts.selector === 'string' ? document.querySelector(_opts.selector) : _opts.selector; 25 | 26 | if (!el) { 27 | return console.warn('Error. No element found for selector', _opts.selector); 28 | } 29 | 30 | if (!_opts.videos) { 31 | return console.warn('Error. You need to define at least one video object'); 32 | } 33 | 34 | if (hasCanvas()) { 35 | 36 | loadImages(_opts.videos, function(err, images) { 37 | if (err) return console.warn('Error while loading video sources.', err); 38 | 39 | var ctx = initCanvas(), 40 | requestAnimationFrame = reqAnimFrame(); 41 | 42 | control.play = function(key, reverse, fps) { 43 | if (control.pause) control.pause(); // pause current vid 44 | 45 | var img = images[key], 46 | opts = _opts.videos[key], 47 | frameWidth = img.width / opts.cols, 48 | frameHeight = img.height / Math.ceil(opts.frames / opts.cols); 49 | 50 | var curFps = fps || opts.fps || 15, 51 | curFrame = reverse ? opts.frames - 1 : 0, 52 | wait = 0, 53 | playing = true, 54 | loops = 0, 55 | delay = 60 / curFps; 56 | 57 | requestAnimationFrame(frame); 58 | 59 | control.resume = function() { 60 | playing = true; 61 | requestAnimationFrame(frame); 62 | }; 63 | 64 | control.pause = function() { 65 | playing = false; 66 | requestAnimationFrame(frame); 67 | }; 68 | 69 | control.isPlaying = function() { 70 | return playing; 71 | }; 72 | 73 | control.destroy = function(){ 74 | control.pause(); 75 | removeCanvid(); 76 | }; 77 | 78 | if (firstPlay) { 79 | firstPlay = false; 80 | hideChildren(); 81 | } 82 | 83 | function frame() { 84 | if (!wait) { 85 | drawFrame(curFrame); 86 | curFrame = (+curFrame + (reverse ? -1 : 1)); 87 | if (curFrame < 0) curFrame += +opts.frames; 88 | if (curFrame >= opts.frames) curFrame = 0; 89 | if (reverse ? curFrame == opts.frames - 1 : !curFrame) loops++; 90 | if (opts.loops && loops >= opts.loops) playing = false; 91 | } 92 | wait = (wait + 1) % delay; 93 | if (playing && opts.frames > 1) requestAnimationFrame(frame); 94 | } 95 | 96 | function drawFrame(f) { 97 | var fx = Math.floor(f % opts.cols) * frameWidth, 98 | fy = Math.floor(f / opts.cols) * frameHeight; 99 | 100 | ctx.clearRect(0, 0, _opts.width, _opts.height); // clear frame 101 | ctx.drawImage(img, fx, fy, frameWidth, frameHeight, 0, 0, _opts.width, _opts.height); 102 | } 103 | 104 | }; // end control.play 105 | 106 | if (isFunction(_opts.loaded)) { 107 | _opts.loaded(control); 108 | } 109 | 110 | }); // end loadImages 111 | 112 | } else if (opts.srcGif) { 113 | var fallbackImage = new Image(); 114 | fallbackImage.src = opts.srcGif; 115 | 116 | el.appendChild(fallbackImage); 117 | } 118 | 119 | function loadImages(imageList, callback) { 120 | var images = {}, 121 | imagesToLoad = Object.keys(imageList).length; 122 | 123 | if(imagesToLoad === 0) { 124 | return callback('You need to define at least one video object.'); 125 | } 126 | 127 | for (var key in imageList) { 128 | images[key] = new Image(); 129 | images[key].onload = checkCallback; 130 | images[key].onerror = callback; 131 | images[key].src = imageList[key].src; 132 | } 133 | 134 | function checkCallback() { 135 | imagesToLoad--; 136 | if (imagesToLoad === 0) { 137 | callback(null, images); 138 | } 139 | } 140 | } 141 | 142 | function initCanvas() { 143 | var canvas = document.createElement('canvas'); 144 | canvas.width = _opts.width; 145 | canvas.height = _opts.height; 146 | canvas.classList.add('canvid'); 147 | 148 | el.appendChild(canvas); 149 | 150 | return canvas.getContext('2d'); 151 | } 152 | 153 | function hideChildren() { 154 | [].forEach.call(el.children, function(child){ 155 | if(!child.classList.contains('canvid') ){ 156 | child.style.display = 'none'; 157 | } 158 | }); 159 | } 160 | 161 | function removeCanvid(){ 162 | [].forEach.call(el.children, function(child){ 163 | if(child.classList.contains('canvid') ){ 164 | el.removeChild(child); 165 | } 166 | }); 167 | } 168 | 169 | function reqAnimFrame() { 170 | return window.requestAnimationFrame 171 | || window.webkitRequestAnimationFrame 172 | || window.mozRequestAnimationFrame 173 | || window.msRequestAnimationFrame 174 | || function(callback) { 175 | return setTimeout(callback, 1000 / 60); 176 | }; 177 | } 178 | 179 | function hasCanvas() { 180 | // taken from Modernizr 181 | var elem = document.createElement('canvas'); 182 | return !!(elem.getContext && elem.getContext('2d')); 183 | } 184 | 185 | function isFunction(obj) { 186 | // taken from jQuery 187 | return typeof obj === 'function' || !!(obj && obj.constructor && obj.call && obj.apply); 188 | } 189 | 190 | function merge() { 191 | var obj = {}, 192 | key; 193 | 194 | for (var i = 0; i < arguments.length; i++) { 195 | for (key in arguments[i]) { 196 | if (arguments[i].hasOwnProperty(key)) { 197 | obj[key] = arguments[i][key]; 198 | } 199 | } 200 | } 201 | return obj; 202 | } 203 | 204 | return control; 205 | }; // end canvid function 206 | 207 | return canvid; 208 | })); // end factory function 209 | -------------------------------------------------------------------------------- /graphic_templates/animated_photo/process.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SET VARIABLES 4 | FOLDER='img' 5 | COUNT=`ls -l $FOLDER/frames/*.jpg | wc -l` 6 | 7 | # CONVERT FRAMES TO A FILMSTRIP 8 | montage -border 0 -geometry 1000x -tile $COUNT'x' -quality 80% $FOLDER'/frames/*.jpg' $FOLDER'/filmstrip-1000.jpg' 9 | montage -border 0 -geometry 600x -tile $COUNT'x' -quality 70% $FOLDER'/frames/*.jpg' $FOLDER'/filmstrip-600.jpg' 10 | montage -border 0 -geometry 375x -tile $COUNT'x' -quality 60% $FOLDER'/frames/*.jpg' $FOLDER'/filmstrip-375.jpg' 11 | 12 | # CONVERT FRAMES TO GIF 13 | # Note: To change the animation speed, tweak the first number in the "delay" value. Lower = faster. 14 | # (Default of 30x60 means each frame displays for 1/2 second. 60x60 == one second.) 15 | convert -background white -alpha remove -layers optimize-plus -delay 30x60 -resize 600 $FOLDER'/frames/*.jpg' -loop 0 $FOLDER'/filmstrip.gif' 16 | -------------------------------------------------------------------------------- /graphic_templates/archive_graphic/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 |

Headline

6 |

Subhed

7 | 8 |
9 | 10 | 14 | 15 | 19 | 20 | {% endblock content %} 21 | 22 | {% block js %} 23 | 24 | {{ JS.push('js/lib/jquery-1.11.3.min.js') }} 25 | {{ JS.push('js/graphic.js') }} 26 | {{ JS.render('js/graphic-footer.js') }} 27 | 28 | {% endblock js %} 29 | -------------------------------------------------------------------------------- /graphic_templates/archive_graphic/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | // Add less here 4 | -------------------------------------------------------------------------------- /graphic_templates/archive_graphic/js/graphic.js: -------------------------------------------------------------------------------- 1 | // Global vars 2 | var pymChild = null; 3 | var isMobile = false; 4 | 5 | /* 6 | * Initialize the graphic. 7 | */ 8 | var onWindowLoaded = function() { 9 | pymChild = new pym.Child({}); 10 | 11 | pymChild.onMessage('on-screen', function(bucket) { 12 | ANALYTICS.trackEvent('on-screen', bucket); 13 | }); 14 | pymChild.onMessage('scroll-depth', function(data) { 15 | data = JSON.parse(data); 16 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 17 | }); 18 | 19 | // Update iframe 20 | /* 21 | if (pymChild) { 22 | pymChild.sendHeight(); 23 | } 24 | */ 25 | } 26 | 27 | /* 28 | * Initially load the graphic 29 | * (NB: Use window.load to ensure all images have loaded) 30 | */ 31 | window.onload = onWindowLoaded; 32 | -------------------------------------------------------------------------------- /graphic_templates/bar_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/bar_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | // Add custom styles here 4 | -------------------------------------------------------------------------------- /graphic_templates/bar_chart/data.csv: -------------------------------------------------------------------------------- 1 | label,amt 2 | First thing,24 3 | Another thing,14 4 | Thing the Third,1 -------------------------------------------------------------------------------- /graphic_templates/bar_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1nbpweBccoaBqxuyKSL6o0q_qkdponL0OWgJpbozQp58' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/block_histogram/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 32 | 33 | {% endblock content %} 34 | -------------------------------------------------------------------------------- /graphic_templates/block_histogram/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .label-top { 4 | font-style: italic; 5 | fill: #999; 6 | font-size: 11px; 7 | -webkit-font-smoothing: antialiased; 8 | } 9 | 10 | .value text { 11 | fill: #fff; 12 | font-size: 12px; 13 | font-family: 'Knockout 31 4r',Helvetica,Arial,sans-serif; 14 | text-anchor: middle; 15 | -webkit-font-smoothing: antialiased; 16 | } 17 | 18 | line.axis-0 { 19 | stroke: #999; 20 | } 21 | 22 | /* 23 | * small-screen styles 24 | */ 25 | @media @screen-mobile { 26 | .value text { font-size: 10px; } 27 | } 28 | -------------------------------------------------------------------------------- /graphic_templates/block_histogram/data.csv: -------------------------------------------------------------------------------- 1 | name,usps,ap,amt 2 | Alabama,AL,Ala.,11.1 3 | Alaska,AK,Alaska,5.9 4 | Arizona,AZ,Ariz.,-3.8 5 | Arkansas,AR,Ark.,4.9 6 | California,CA,Calif.,5.3 7 | Colorado,CO,Colo.,4.1 8 | Connecticut,CT,Conn.,3.6 9 | Delaware,DE,Del.,2.6 10 | District of Columbia,DC,D.C.,5.1 11 | Florida,FL,Fla.,7.0 12 | Georgia,GA,Ga.,7.5 13 | Hawaii,HI,Hawaii,2.5 14 | Idaho,ID,Idaho,0.0 15 | Illinois,IL,Ill.,-1.2 16 | Indiana,IN,Ind.,1.2 17 | Iowa,IA,Iowa,2.3 18 | Kansas,KS,Kan.,3.6 19 | Kentucky,KY,Ky.,0.0 20 | Louisiana,LA,La.,4.2 21 | Maine,ME,Maine,2.4 22 | Maryland,MD,Md.,2.4 23 | Massachusetts,MA,Mass.,2.4 24 | Michigan,MI,Mich.,4.1 25 | Minnesota,MN,Minn.,3.9 26 | Mississippi,MS,Miss.,1.3 27 | Missouri,MO,Mo.,6.2 28 | Montana,MT,Mont.,2.4 29 | Nebraska,NE,Neb.,2.3 30 | Nevada,NV,Nev.,14.5 31 | New Hampshire,NH,N.H.,1.2 32 | New Jersey,NJ,N.J.,6.0 33 | New Mexico,NM,N.M.,11.1 34 | New York,NY,N.Y.,0.0 35 | North Carolina,NC,N.C.,6.4 36 | North Dakota,ND,N.D.,2.3 37 | Ohio,OH,Ohio,2.5 38 | Oklahoma,OK,Okla.,0.0 39 | Oregon,OR,Ore.,1.5 40 | Pennsylvania,PA,Pa.,3.6 41 | Rhode Island,RI,R.I.,3.9 42 | South Carolina,SC,S.C.,5.4 43 | South Dakota,SD,S.D.,0.0 44 | Tennessee,TN,Tenn.,0.0 45 | Texas,TX,Texas,2.3 46 | Utah,UT,Utah,9.2 47 | Vermont,VT,Vt.,0.0 48 | Virginia,VA,Va.,2.4 49 | Washington,WA,Wash.,0.0 50 | West Virginia,WV,W.Va.,3.8 51 | Wisconsin,WI,Wis.,1.1 52 | Wyoming,WY,Wyo.,-3.8 -------------------------------------------------------------------------------- /graphic_templates/block_histogram/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1QTLmFGjd2BCU3QQvvXb-8RN9YkztBFOaOeaZ40SEKjw' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/column_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/column_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | // Add custom styles here 4 | -------------------------------------------------------------------------------- /graphic_templates/column_chart/data.csv: -------------------------------------------------------------------------------- 1 | label,amt 2009,18 2010,197 2011,192 2012,192 2013,249 2014,278 2015,16 -------------------------------------------------------------------------------- /graphic_templates/column_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1-wN8QJAaAE5zzIMcbfPPchPWAGFj6BnpZrU72Fp6cm4' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/diverging_bar_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/diverging_bar_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .key { 4 | display: webkit-flex; 5 | display: flex; 6 | -webkit-flex-wrap: wrap; 7 | flex-wrap: wrap; 8 | margin-top: 0; 9 | margin-bottom: 15px; 10 | 11 | .key-item { 12 | display: block; 13 | margin: 0 0 3px 0; 14 | width: 50%; 15 | } 16 | } 17 | 18 | .value text { 19 | fill: #fff; 20 | 21 | &.left { text-anchor: start; } 22 | &.right { text-anchor: end; } 23 | &.center { text-anchor: middle; } 24 | &.hidden { display: none; } 25 | 26 | &.out { 27 | font-size: 9px; 28 | 29 | &.left { text-anchor: end; } 30 | &.right { text-anchor: start; } 31 | } 32 | } 33 | 34 | .annotations { 35 | .dhdr { 36 | font-size: 12px; 37 | font-weight: bold; 38 | -webkit-font-smoothing: antialiased; 39 | } 40 | .dhdr-0 { text-anchor: end; } 41 | .dhdr-1 { text-anchor: start; } 42 | } 43 | 44 | /* 45 | * larger-screen styles 46 | */ 47 | @media @screen-medium-above { 48 | .key { 49 | justify-content: center; 50 | flex-wrap: nowrap; 51 | margin-bottom: 22px; 52 | 53 | .key-item { 54 | margin: 0 11px; 55 | width: auto; 56 | } 57 | } 58 | } 59 | 60 | /* 61 | * small-screen styles 62 | */ 63 | @media @screen-mobile { 64 | .value text { font-size: 9px; } 65 | } 66 | -------------------------------------------------------------------------------- /graphic_templates/diverging_bar_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '18Zus5ARv6KLPX49AsIzoOU2_nlfl0NMmhdTEIu58T68' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/dot_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/dot_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .bars line { 4 | fill: none; 5 | stroke-width: 3px; 6 | stroke: #F5D1CA; 7 | } 8 | 9 | .dots circle { 10 | stroke-width: 1px; 11 | stroke: #fff; 12 | fill: #D8472B; 13 | } 14 | 15 | .value text { 16 | font-size: 11px; 17 | fill: #999; 18 | } 19 | 20 | .shadow text { 21 | font-size: 11px; 22 | fill: #fff; 23 | stroke: #fff; 24 | stroke-width: 5px; 25 | } 26 | -------------------------------------------------------------------------------- /graphic_templates/dot_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1pcLyLFhEpKMlNpp3UqWZ1XgW8ZaloVe_d04CGdNnEWc' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/dot_chart/js/graphic.js: -------------------------------------------------------------------------------- 1 | // Global vars 2 | var pymChild = null; 3 | var isMobile = false; 4 | 5 | /* 6 | * Initialize the graphic. 7 | */ 8 | var onWindowLoaded = function() { 9 | formatData(); 10 | 11 | pymChild = new pym.Child({ 12 | renderCallback: render 13 | }); 14 | 15 | pymChild.onMessage('on-screen', function(bucket) { 16 | ANALYTICS.trackEvent('on-screen', bucket); 17 | }); 18 | pymChild.onMessage('scroll-depth', function(data) { 19 | data = JSON.parse(data); 20 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 21 | }); 22 | } 23 | 24 | /* 25 | * Format graphic data for processing by D3. 26 | */ 27 | var formatData = function() { 28 | DATA.forEach(function(d) { 29 | d['amt'] = +d['amt']; 30 | d['min'] = +d['min']; 31 | d['max'] = +d['max']; 32 | }); 33 | } 34 | 35 | /* 36 | * Render the graphic(s). Called by pym with the container width. 37 | */ 38 | var render = function(containerWidth) { 39 | if (!containerWidth) { 40 | containerWidth = DEFAULT_WIDTH; 41 | } 42 | 43 | if (containerWidth <= MOBILE_THRESHOLD) { 44 | isMobile = true; 45 | } else { 46 | isMobile = false; 47 | } 48 | 49 | // Render the chart! 50 | renderDotChart({ 51 | container: '#dot-chart', 52 | width: containerWidth, 53 | data: DATA 54 | }); 55 | 56 | // Update iframe 57 | if (pymChild) { 58 | pymChild.sendHeight(); 59 | } 60 | } 61 | 62 | /* 63 | * Render a bar chart. 64 | */ 65 | var renderDotChart = function(config) { 66 | /* 67 | * Setup 68 | */ 69 | var labelColumn = 'label'; 70 | var valueColumn = 'amt'; 71 | var minColumn = 'min'; 72 | var maxColumn = 'max'; 73 | 74 | var barHeight = 20; 75 | var barGap = 5; 76 | var labelWidth = 60; 77 | var labelMargin = 10; 78 | var valueMinWidth = 30; 79 | var dotRadius = 5; 80 | 81 | var margins = { 82 | top: 0, 83 | right: 20, 84 | bottom: 20, 85 | left: (labelWidth + labelMargin) 86 | }; 87 | 88 | var ticksX = 4; 89 | var roundTicksFactor = 5; 90 | 91 | if (isMobile) { 92 | ticksX = 6; 93 | margins['right'] = 30; 94 | } 95 | 96 | // Calculate actual chart dimensions 97 | var chartWidth = config['width'] - margins['left'] - margins['right']; 98 | var chartHeight = ((barHeight + barGap) * config['data'].length); 99 | 100 | // Clear existing graphic (for redraw) 101 | var containerElement = d3.select(config['container']); 102 | containerElement.html(''); 103 | 104 | /* 105 | * Create the root SVG element. 106 | */ 107 | var chartWrapper = containerElement.append('div') 108 | .attr('class', 'graphic-wrapper'); 109 | 110 | var chartElement = chartWrapper.append('svg') 111 | .attr('width', chartWidth + margins['left'] + margins['right']) 112 | .attr('height', chartHeight + margins['top'] + margins['bottom']) 113 | .append('g') 114 | .attr('transform', 'translate(' + margins['left'] + ',' + margins['top'] + ')'); 115 | 116 | /* 117 | * Create D3 scale objects. 118 | */ 119 | var min = 0; 120 | var max = d3.max(config['data'], function(d) { 121 | return Math.ceil(d[maxColumn] / roundTicksFactor) * roundTicksFactor; 122 | }); 123 | 124 | var xScale = d3.scale.linear() 125 | .domain([min, max]) 126 | .range([0, chartWidth]); 127 | 128 | /* 129 | * Create D3 axes. 130 | */ 131 | var xAxis = d3.svg.axis() 132 | .scale(xScale) 133 | .orient('bottom') 134 | .ticks(ticksX) 135 | .tickFormat(function(d) { 136 | return d + '%'; 137 | }); 138 | 139 | /* 140 | * Render axes to chart. 141 | */ 142 | chartElement.append('g') 143 | .attr('class', 'x axis') 144 | .attr('transform', makeTranslate(0, chartHeight)) 145 | .call(xAxis); 146 | 147 | /* 148 | * Render grid to chart. 149 | */ 150 | var xAxisGrid = function() { 151 | return xAxis; 152 | }; 153 | 154 | chartElement.append('g') 155 | .attr('class', 'x grid') 156 | .attr('transform', makeTranslate(0, chartHeight)) 157 | .call(xAxisGrid() 158 | .tickSize(-chartHeight, 0, 0) 159 | .tickFormat('') 160 | ); 161 | 162 | /* 163 | * Render range bars to chart. 164 | */ 165 | chartElement.append('g') 166 | .attr('class', 'bars') 167 | .selectAll('line') 168 | .data(config['data']) 169 | .enter() 170 | .append('line') 171 | .attr('x1', function(d, i) { 172 | return xScale(d[minColumn]); 173 | }) 174 | .attr('x2', function(d, i) { 175 | return xScale(d[maxColumn]); 176 | }) 177 | .attr('y1', function(d, i) { 178 | return i * (barHeight + barGap) + (barHeight / 2); 179 | }) 180 | .attr('y2', function(d, i) { 181 | return i * (barHeight + barGap) + (barHeight / 2); 182 | }); 183 | 184 | /* 185 | * Render dots to chart. 186 | */ 187 | chartElement.append('g') 188 | .attr('class', 'dots') 189 | .selectAll('circle') 190 | .data(config['data']) 191 | .enter().append('circle') 192 | .attr('cx', function(d, i) { 193 | return xScale(d[valueColumn]); 194 | }) 195 | .attr('cy', function(d, i) { 196 | return i * (barHeight + barGap) + (barHeight / 2); 197 | }) 198 | .attr('r', dotRadius) 199 | 200 | /* 201 | * Render bar labels. 202 | */ 203 | containerElement 204 | .append('ul') 205 | .attr('class', 'labels') 206 | .attr('style', formatStyle({ 207 | 'width': labelWidth + 'px', 208 | 'top': margins['top'] + 'px', 209 | 'left': '0' 210 | })) 211 | .selectAll('li') 212 | .data(config['data']) 213 | .enter() 214 | .append('li') 215 | .attr('style', function(d, i) { 216 | return formatStyle({ 217 | 'width': labelWidth + 'px', 218 | 'height': barHeight + 'px', 219 | 'left': '0px', 220 | 'top': (i * (barHeight + barGap)) + 'px;' 221 | }); 222 | }) 223 | .attr('class', function(d) { 224 | return classify(d[labelColumn]); 225 | }) 226 | .append('span') 227 | .text(function(d) { 228 | return d[labelColumn]; 229 | }); 230 | 231 | /* 232 | * Render bar values. 233 | */ 234 | _.each(['shadow', 'value'], function(cls) { 235 | chartElement.append('g') 236 | .attr('class', cls) 237 | .selectAll('text') 238 | .data(config['data']) 239 | .enter().append('text') 240 | .attr('x', function(d, i) { 241 | return xScale(d[maxColumn]) + 6; 242 | }) 243 | .attr('y', function(d,i) { 244 | return i * (barHeight + barGap) + (barHeight / 2) + 3; 245 | }) 246 | .text(function(d) { 247 | return d[valueColumn].toFixed(1) + '%'; 248 | }); 249 | }); 250 | } 251 | 252 | /* 253 | * Initially load the graphic 254 | * (NB: Use window.load to ensure all images have loaded) 255 | */ 256 | window.onload = onWindowLoaded; 257 | -------------------------------------------------------------------------------- /graphic_templates/graphic/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | {% endblock content %} 29 | -------------------------------------------------------------------------------- /graphic_templates/graphic/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | // Add custom styles here 4 | -------------------------------------------------------------------------------- /graphic_templates/graphic/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1kekyEB3w293-8Ex2R3VdsQZTQmrfJwZ6sfWxS7OBDyA' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/graphic/js/graphic.js: -------------------------------------------------------------------------------- 1 | // Global vars 2 | var pymChild = null; 3 | var isMobile = false; 4 | 5 | /* 6 | * Initialize the graphic. 7 | */ 8 | var onWindowLoaded = function() { 9 | pymChild = new pym.Child({ 10 | renderCallback: render 11 | }); 12 | 13 | pymChild.onMessage('on-screen', function(bucket) { 14 | ANALYTICS.trackEvent('on-screen', bucket); 15 | }); 16 | pymChild.onMessage('scroll-depth', function(data) { 17 | data = JSON.parse(data); 18 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 19 | }); 20 | } 21 | 22 | /* 23 | * Render the graphic. 24 | */ 25 | var render = function(containerWidth) { 26 | if (!containerWidth) { 27 | containerWidth = DEFAULT_WIDTH; 28 | } 29 | 30 | if (containerWidth <= MOBILE_THRESHOLD) { 31 | isMobile = true; 32 | } else { 33 | isMobile = false; 34 | } 35 | 36 | // Render the chart! 37 | // renderGraphic({ 38 | // container: '#graphic', 39 | // width: containerWidth, 40 | // data: [] 41 | // }); 42 | 43 | // Update iframe 44 | if (pymChild) { 45 | pymChild.sendHeight(); 46 | } 47 | } 48 | 49 | /* 50 | * Render a graphic. 51 | */ 52 | var renderGraphic = function(config) { 53 | var aspectWidth = 4; 54 | var aspectHeight = 3; 55 | 56 | var margins = { 57 | top: 0, 58 | right: 15, 59 | bottom: 20, 60 | left: 15 61 | }; 62 | 63 | // Calculate actual chart dimensions 64 | var chartWidth = config['width'] - margins['left'] - margins['right']; 65 | var chartHeight = Math.ceil((config['width'] * aspectHeight) / aspectWidth) - margins['top'] - margins['bottom']; 66 | 67 | // Clear existing graphic (for redraw) 68 | var containerElement = d3.select(config['container']); 69 | containerElement.html(''); 70 | 71 | // Create container 72 | var chartElement = containerElement.append('svg') 73 | .attr('width', chartWidth + margins['left'] + margins['right']) 74 | .attr('height', chartHeight + margins['top'] + margins['bottom']) 75 | .append('g') 76 | .attr('transform', 'translate(' + margins['left'] + ',' + margins['top'] + ')'); 77 | 78 | // Draw here! 79 | } 80 | 81 | /* 82 | * Initially load the graphic 83 | * (NB: Use window.load to ensure all images have loaded) 84 | */ 85 | window.onload = onWindowLoaded; 86 | -------------------------------------------------------------------------------- /graphic_templates/grouped_bar_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/grouped_bar_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | // Add custom styles here 4 | -------------------------------------------------------------------------------- /graphic_templates/grouped_bar_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '19d-SxZs0z5fl7pETB427wp4DYzNwB5znkNZg6kF69j4' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/issue_matrix/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | {% for row in COPY.issues %} 15 | 16 | {% endfor %} 17 | 18 | 19 | 20 | {% for row in COPY.data %} 21 | 22 | 23 | 24 | 25 | {% for issue in COPY.issues %} 26 | {% set i = issue.name %} 27 | {% set position = row[i] %} 28 | {% set link = row[i + '_link'] %} 29 | {% set footnote = row[i + '_footnote'] %} 30 | 31 | 32 | {% endfor %} 33 | 34 | 35 | {% endfor %} 36 | 37 |
{{ COPY.labels.hdr_name }}{{ row.header|smarty }}
{{ row.name|smarty }} ({{ row.party }}){% if row[i + '_link'] %}{% endif %}{{ position|smarty }}{% if link %}{% endif %}{% if footnote %}{{ footnote }}{% endif %}
38 |
39 | 40 | {% if COPY.footnotes[0] %} 41 |
42 |

Notes

43 |

44 | {% for row in COPY.footnotes %} 45 | {{ row.id }}. {{ row.description|smarty }}
46 | {% endfor %} 47 |

48 |
49 | {% endif %} 50 | 51 | 55 | 56 | {% endblock content %} 57 | 58 | {% block js %} 59 | 60 | {{ JS.push('js/graphic.js') }} 61 | {{ JS.render('js/graphic-footer.js') }} 62 | 63 | {% endblock js %} 64 | -------------------------------------------------------------------------------- /graphic_templates/issue_matrix/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | /* 4 | * redefine mobile media queries 5 | */ 6 | @screen-mobile-above: ~"screen and (min-width: 551px)"; 7 | @screen-mobile: ~"screen and (max-width: 550px)"; 8 | 9 | /* 10 | * Base table styles 11 | */ 12 | table { 13 | border-collapse: collapse; 14 | padding: 0; 15 | width: 100%; 16 | font-size: 12px; 17 | color: #666; 18 | 19 | tr.shadedrow { 20 | background-color: #f1f1f1; 21 | color: #333; 22 | font-weight: bold; 23 | } 24 | 25 | th { 26 | .knockout-header(); 27 | line-height: 1.2; 28 | text-align: left; 29 | vertical-align: bottom; 30 | } 31 | 32 | td { 33 | vertical-align: top; 34 | 35 | &.issue { 36 | span { 37 | -webkit-font-smoothing: antialiased; 38 | color: #999; 39 | 40 | &.yes { 41 | color: @teal3; 42 | font-weight: bold; 43 | } 44 | &.no { 45 | color: @orange3; 46 | font-weight: bold; 47 | } 48 | } 49 | 50 | a span { 51 | padding-bottom: 1px; 52 | border-bottom: 1px solid #ddd; 53 | 54 | &:hover { opacity: 0.7; } 55 | } 56 | 57 | sup { 58 | line-height: 0; 59 | color: #aaa; 60 | } 61 | } 62 | } 63 | } 64 | 65 | /* 66 | * larger-screen styles 67 | */ 68 | @media @screen-medium-above { 69 | .name { white-space: nowrap; } 70 | } 71 | 72 | /* 73 | * larger-than-small-screen styles 74 | */ 75 | @media @screen-mobile-above { 76 | table { 77 | th, td { 78 | padding: 10px; 79 | width: 14%; 80 | 81 | // comment these two out if you want to highlight particular rows 82 | &:first-child { padding-left: 0; } 83 | &:last-child { padding-right: 0; } 84 | 85 | &.amt { text-align: right; } 86 | &.issue { text-align: center; } 87 | } 88 | 89 | th { 90 | border-bottom: 2px solid #eee; 91 | padding-top: 0; 92 | } 93 | 94 | td { border-bottom: 1px solid #eee; } 95 | } 96 | } 97 | 98 | /* 99 | * small-screen styles 100 | */ 101 | @media @screen-mobile { 102 | tbody { 103 | display: block; 104 | width: 100%; 105 | } 106 | 107 | table { 108 | thead { display: none; } 109 | 110 | tr, th, td { 111 | display: block; 112 | padding: 0; 113 | white-space: normal; 114 | } 115 | 116 | tr { 117 | border-bottom: 1px solid #eee; 118 | padding: 10px 0; 119 | 120 | &:first-child { border-top: 1px solid #eee; } 121 | } 122 | 123 | td { 124 | margin-bottom: 6px; 125 | 126 | &:empty { display: none; } 127 | } 128 | 129 | tr td:first-child { 130 | .knockout-header(); 131 | color: #333; 132 | font-size: 14px; 133 | margin-bottom: 6px; 134 | 135 | &:before { 136 | content: ''; 137 | display: none; 138 | } 139 | } 140 | 141 | th[data-title]:before, 142 | td[data-title]:before { 143 | content: attr(data-title) ":\00A0"; 144 | display: inline-block; 145 | margin-right: 10px; 146 | width: 70%; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /graphic_templates/issue_matrix/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1VdwisfvhK6MJsycPP6m_VLg-g-u3VUDs0Tc0ruGDEIc' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/issue_matrix/js/graphic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Initialize the graphic. 3 | */ 4 | var onWindowLoaded = function() { 5 | // Uncomment to enable column sorting 6 | // var tablesort = new Tablesort(document.getElementById('state-table')); 7 | 8 | pymChild = new pym.Child({}); 9 | 10 | pymChild.onMessage('on-screen', function(bucket) { 11 | ANALYTICS.trackEvent('on-screen', bucket); 12 | }); 13 | pymChild.onMessage('scroll-depth', function(data) { 14 | data = JSON.parse(data); 15 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 16 | }); 17 | } 18 | 19 | 20 | /* 21 | * Initially load the graphic 22 | * (NB: Use window.load instead of document.ready 23 | * to ensure all images have loaded) 24 | */ 25 | window.onload = onWindowLoaded; 26 | -------------------------------------------------------------------------------- /graphic_templates/issue_matrix/js/lib/tablesort.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | function Tablesort(el, options) { 3 | if (!(this instanceof Tablesort)) return new Tablesort(el, options); 4 | 5 | if (!el || el.tagName !== 'TABLE') { 6 | throw new Error('Element must be a table'); 7 | } 8 | this.init(el, options || {}); 9 | } 10 | 11 | var sortOptions = []; 12 | 13 | var createEvent = function(name) { 14 | var evt; 15 | 16 | if (!window.CustomEvent || typeof window.CustomEvent !== 'function') { 17 | evt = document.createEvent('CustomEvent'); 18 | evt.initCustomEvent(name, false, false, undefined); 19 | } else { 20 | evt = new CustomEvent(name); 21 | } 22 | 23 | return evt; 24 | }; 25 | 26 | var getInnerText = function(el) { 27 | return el.getAttribute('data-sort') || el.textContent || el.innerText || ''; 28 | }; 29 | 30 | // Default sort method if no better sort method is found 31 | var caseInsensitiveSort = function(a, b) { 32 | a = a.toLowerCase(); 33 | b = b.toLowerCase(); 34 | 35 | if (a === b) return 0; 36 | if (a < b) return 1; 37 | 38 | return -1; 39 | }; 40 | 41 | // Stable sort function 42 | // If two elements are equal under the original sort function, 43 | // then there relative order is reversed 44 | var stabilize = function(sort, antiStabilize) { 45 | return function(a, b) { 46 | var unstableResult = sort(a.td, b.td); 47 | 48 | if (unstableResult === 0) { 49 | if (antiStabilize) return b.index - a.index; 50 | return a.index - b.index; 51 | } 52 | 53 | return unstableResult; 54 | }; 55 | }; 56 | 57 | Tablesort.extend = function(name, pattern, sort) { 58 | if (typeof pattern !== 'function' || typeof sort !== 'function') { 59 | throw new Error('Pattern and sort must be a function'); 60 | } 61 | 62 | sortOptions.push({ 63 | name: name, 64 | pattern: pattern, 65 | sort: sort 66 | }); 67 | }; 68 | 69 | Tablesort.prototype = { 70 | 71 | init: function(el, options) { 72 | var that = this, 73 | firstRow, 74 | defaultSort, 75 | i, 76 | cell; 77 | 78 | that.table = el; 79 | that.thead = false; 80 | that.options = options; 81 | 82 | if (el.rows && el.rows.length > 0) { 83 | if (el.tHead && el.tHead.rows.length > 0) { 84 | firstRow = el.tHead.rows[el.tHead.rows.length - 1]; 85 | that.thead = true; 86 | } else { 87 | firstRow = el.rows[0]; 88 | } 89 | } 90 | 91 | if (!firstRow) return; 92 | 93 | var onClick = function() { 94 | if (that.current && that.current !== this) { 95 | that.current.classList.remove('sort-up'); 96 | that.current.classList.remove('sort-down'); 97 | } 98 | 99 | that.current = this; 100 | that.sortTable(this); 101 | }; 102 | 103 | // Assume first row is the header and attach a click handler to each. 104 | for (i = 0; i < firstRow.cells.length; i++) { 105 | cell = firstRow.cells[i]; 106 | if (!cell.classList.contains('no-sort')) { 107 | cell.classList.add('sort-header'); 108 | cell.tabindex = 0; 109 | cell.addEventListener('click', onClick, false); 110 | 111 | if (cell.classList.contains('sort-default')) { 112 | defaultSort = cell; 113 | } 114 | } 115 | } 116 | 117 | if (defaultSort) { 118 | that.current = defaultSort; 119 | that.sortTable(defaultSort); 120 | } 121 | }, 122 | 123 | sortTable: function(header, update) { 124 | var that = this, 125 | column = header.cellIndex, 126 | sortFunction = caseInsensitiveSort, 127 | item = '', 128 | items = [], 129 | i = that.thead ? 0 : 1, 130 | sortDir, 131 | sortMethod = header.getAttribute('data-sort-method'); 132 | 133 | that.table.dispatchEvent(createEvent('beforeSort')); 134 | 135 | // If updating an existing sort `sortDir` should remain unchanged. 136 | if (update) { 137 | sortDir = header.classList.contains('sort-up') ? 'sort-up' : 'sort-down'; 138 | } else { 139 | if (header.classList.contains('sort-up')) { 140 | sortDir = 'sort-down'; 141 | } else if (header.classList.contains('sort-down')) { 142 | sortDir = 'sort-up'; 143 | } else { 144 | sortDir = that.options.descending ? 'sort-up' : 'sort-down'; 145 | } 146 | 147 | header.classList.remove(sortDir === 'sort-down' ? 'sort-up' : 'sort-down'); 148 | header.classList.add(sortDir); 149 | } 150 | 151 | if (that.table.rows.length < 2) return; 152 | 153 | // If we force a sort method, it is not necessary to check rows 154 | if (!sortMethod) { 155 | while (items.length < 3 && i < that.table.tBodies[0].rows.length) { 156 | item = getInnerText(that.table.tBodies[0].rows[i].cells[column]); 157 | item = item.trim(); 158 | 159 | if (item.length > 0) { 160 | items.push(item); 161 | } 162 | 163 | i++; 164 | } 165 | 166 | if (!items) return; 167 | } 168 | 169 | for (i = 0; i < sortOptions.length; i++) { 170 | item = sortOptions[i]; 171 | 172 | if (sortMethod) { 173 | if (item.name === sortMethod) { 174 | sortFunction = item.sort; 175 | break; 176 | } 177 | } else if (items.every(item.pattern)) { 178 | sortFunction = item.sort; 179 | break; 180 | } 181 | } 182 | 183 | that.col = column; 184 | var newRows = [], 185 | noSorts = {}, 186 | j, 187 | totalRows = 0, 188 | noSortsSoFar = 0; 189 | 190 | for (i = 0; i < that.table.tBodies.length; i++) { 191 | for (j = 0; j < that.table.tBodies[i].rows.length; j++) { 192 | item = that.table.tBodies[i].rows[j]; 193 | if (item.classList.contains('no-sort')) { 194 | // keep no-sorts in separate list to be able to insert 195 | // them back at their original position later 196 | noSorts[totalRows] = item; 197 | } else { 198 | // Save the index for stable sorting 199 | newRows.push({ 200 | tr: item, 201 | td: getInnerText(item.cells[that.col]), 202 | index: totalRows 203 | }); 204 | } 205 | totalRows++; 206 | } 207 | } 208 | 209 | // Before we append should we reverse the new array or not? 210 | // If we reverse, the sort needs to be `anti-stable` so that 211 | // the double negatives cancel out 212 | if (sortDir === 'sort-down') { 213 | newRows.sort(stabilize(sortFunction, true)); 214 | newRows.reverse(); 215 | } else { 216 | newRows.sort(stabilize(sortFunction, false)); 217 | } 218 | 219 | // append rows that already exist rather than creating new ones 220 | for (i = 0; i < totalRows; i++) { 221 | if (noSorts[i]) { 222 | // We have a no-sort row for this position, insert it here. 223 | item = noSorts[i]; 224 | noSortsSoFar++; 225 | } else { 226 | item = newRows[i - noSortsSoFar].tr; 227 | } 228 | 229 | // appendChild(x) moves x if already present somewhere else in the DOM 230 | that.table.tBodies[0].appendChild(item); 231 | } 232 | 233 | that.table.dispatchEvent(createEvent('afterSort')); 234 | }, 235 | 236 | refresh: function() { 237 | if (this.current !== undefined) { 238 | this.sortTable(this.current, true); 239 | } 240 | } 241 | }; 242 | 243 | if (typeof module !== 'undefined' && module.exports) { 244 | module.exports = Tablesort; 245 | } else { 246 | window.Tablesort = Tablesort; 247 | } 248 | })(); 249 | -------------------------------------------------------------------------------- /graphic_templates/issue_matrix/js/lib/tablesort.numeric.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var cleanNumber = function(i) { 3 | return i.replace(/[^\-?0-9.]/g, ''); 4 | }, 5 | 6 | compareNumber = function(a, b) { 7 | a = parseFloat(a); 8 | b = parseFloat(b); 9 | 10 | a = isNaN(a) ? 0 : a; 11 | b = isNaN(b) ? 0 : b; 12 | 13 | return a - b; 14 | }; 15 | 16 | Tablesort.extend('number', function(item) { 17 | return item.match(/^-?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/) || // Prefixed currency 18 | item.match(/^-?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/) || // Suffixed currency 19 | item.match(/^-?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/); // Number 20 | }, function(a, b) { 21 | a = cleanNumber(a); 22 | b = cleanNumber(b); 23 | 24 | return compareNumber(b, a); 25 | }); 26 | }()); 27 | -------------------------------------------------------------------------------- /graphic_templates/line_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/line_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .lines { 4 | fill: none; 5 | stroke-width: 3px; 6 | stroke: #ccc; 7 | } 8 | 9 | .value text { 10 | font-size: 12px; 11 | font-weight: bold; 12 | fill: #999; 13 | } 14 | 15 | /* 16 | * larger-than-mobile-screen styles 17 | */ 18 | @media @screen-mobile-above { 19 | .key { display: none; } 20 | } 21 | 22 | /* 23 | * small-screen styles 24 | */ 25 | @media @screen-mobile { 26 | .value text { font-size: 10px; } 27 | } 28 | -------------------------------------------------------------------------------- /graphic_templates/line_chart/data.csv: -------------------------------------------------------------------------------- 1 | date,One,Two,Three,Four 1/1/89,1.84,3.864,5.796,2.76 4/1/89,1.85,3.885,5.8275,2.775 7/1/89,1.87,3.927,5.8905,2.805 10/1/89,1.88,3.948,5.922,2.82 1/1/90,1.85,3.885,5.8275,2.775 4/1/90,1.84,3.864,5.796,2.76 7/1/90,1.82,3.822,5.733,2.73 10/1/90,1.79,3.759,5.6385,2.685 1/1/91,1.85,3.885,5.8275,2.775 4/1/91,1.9,3.99,5.985,2.85 7/1/91,1.9,3.99,5.985,2.85 10/1/91,1.89,3.969,5.9535,2.835 1/1/92,1.88,3.948,5.922,2.82 4/1/92,1.84,3.864,5.796,2.76 7/1/92,1.82,3.822,5.733,2.73 10/1/92,1.85,3.885,5.8275,2.775 1/1/93,1.82,3.822,5.733,2.73 4/1/93,1.85,3.885,5.8275,2.775 7/1/93,1.86,3.906,5.859,2.79 10/1/93,1.88,3.948,5.922,2.82 1/1/94,1.88,3.948,5.922,2.82 4/1/94,1.87,3.927,5.8905,2.805 7/1/94,1.86,3.906,5.859,2.79 10/1/94,1.84,3.864,5.796,2.76 1/1/95,1.8,3.78,5.67,2.7 4/1/95,1.81,3.801,5.7015,2.715 7/1/95,1.86,3.906,5.859,2.79 10/1/95,1.85,3.885,5.8275,2.775 1/1/96,1.86,3.906,5.859,2.79 4/1/96,1.84,3.864,5.796,2.76 7/1/96,1.83,3.843,5.7645,2.745 10/1/96,1.83,3.843,5.7645,2.745 1/1/97,1.84,3.864,5.796,2.76 4/1/97,1.85,3.885,5.8275,2.775 7/1/97,1.85,3.885,5.8275,2.775 10/1/97,1.85,3.885,5.8275,2.775 1/1/98,1.84,3.864,5.796,2.76 4/1/98,1.84,3.864,5.796,2.76 7/1/98,1.83,3.843,5.7645,2.745 10/1/98,1.84,3.864,5.796,2.76 1/1/99,1.85,3.885,5.8275,2.775 4/1/99,1.85,3.885,5.8275,2.775 7/1/99,1.84,3.864,5.796,2.76 10/1/99,1.82,3.822,5.733,2.73 1/1/00,1.8,3.78,5.67,2.7 4/1/00,1.78,3.738,5.607,2.67 7/1/00,1.8,3.78,5.67,2.7 10/1/00,1.79,3.759,5.6385,2.685 1/1/01,1.79,3.759,5.6385,2.685 4/1/01,1.82,3.822,5.733,2.73 7/1/01,1.86,3.906,5.859,2.79 10/1/01,1.88,3.948,5.922,2.82 1/1/02,1.94,4.074,6.111,2.91 4/1/02,1.94,4.074,6.111,2.91 7/1/02,1.96,4.116,6.174,2.94 10/1/02,2.01,4.221,6.3315,3.015 1/1/03,2.03,4.263,6.3945,3.045 4/1/03,2.04,4.284,6.426,3.06 7/1/03,2.07,4.347,6.5205,3.105 10/1/03,2.07,4.347,6.5205,3.105 1/1/04,2.1,4.41,6.615,3.15 4/1/04,2.11,4.431,6.6465,3.165 7/1/04,2.12,4.452,6.678,3.18 10/1/04,2.14,4.494,6.741,3.21 1/1/05,2.2,4.62,6.93,3.3 4/1/05,2.29,4.809,7.2135,3.435 7/1/05,2.32,4.872,7.308,3.48 10/1/05,2.33,4.893,7.3395,3.495 1/1/06,2.25,4.725,7.0875,3.375 4/1/06,2.22,4.662,6.993,3.33 7/1/06,2.16,4.536,6.804,3.24 10/1/06,2.15,4.515,6.7725,3.225 1/1/07,2.13,4.473,6.7095,3.195 4/1/07,2.09,4.389,6.5835,3.135 7/1/07,2.03,4.263,6.3945,3.045 10/1/07,1.95,4.095,6.1425,2.925 1/1/08,1.91,4.011,6.0165,2.865 4/1/08,1.85,3.885,5.8275,2.775 7/1/08,1.79,3.759,5.6385,2.685 10/1/08,1.7,3.57,5.355,2.55 1/1/09,1.69,3.549,5.3235,2.535 4/1/09,1.63,3.423,5.1345,2.445 7/1/09,1.66,3.486,5.229,2.49 10/1/09,1.65,3.465,5.1975,2.475 1/1/10,1.67,3.507,5.2605,2.505 4/1/10,1.63,3.423,5.1345,2.445 7/1/10,1.61,3.381,5.0715,2.415 10/1/10,1.6,3.36,5.04,2.4 1/1/11,1.52,3.192,4.788,2.28 4/1/11,1.47,3.087,4.6305,2.205 7/1/11,1.45,3.045,4.5675,2.175 10/1/11,1.47,3.087,4.6305,2.205 1/1/12,1.48,3.108,4.662,2.22 4/1/12,1.53,3.213,4.8195,2.295 7/1/12,1.55,3.255,4.8825,2.325 10/1/12,1.54,3.234,4.851,2.31 1/1/13,1.61,3.381,5.0715,2.415 4/1/13,1.68,3.528,5.292,2.52 7/1/13,1.69,3.549,5.3235,2.535 10/1/13,1.68,3.528,5.292,2.52 1/1/14,1.7,3.57,5.355,2.55 -------------------------------------------------------------------------------- /graphic_templates/line_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1DLxMcQRpyp1rqGJTjC28jJH5Df1GYrJrJnBl2PW9-MU' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/locator_map/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 32 | 33 | {% endblock content %} 34 | 35 | {% block js %} 36 | 37 | {{ JS.push('js/lib/underscore.js') }} 38 | {{ JS.push('js/lib/d3.min.js') }} 39 | {{ JS.push('js/lib/d3.geo.projection.v0.min.js') }} 40 | {{ JS.push('js/lib/topojson.v1.min.js') }} 41 | {{ JS.push('js/lib/modernizr.svg.min.js') }} 42 | {{ JS.push('js/base.js') }} 43 | {{ JS.push('js/geomath.js') }} 44 | {{ JS.push('js/graphic.js') }} 45 | {{ JS.render('js/graphic-footer.js') }} 46 | 47 | {% endblock js %} 48 | -------------------------------------------------------------------------------- /graphic_templates/locator_map/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | svg { 4 | font-size: 20px; 5 | background-color: #d3eaf7; 6 | } 7 | 8 | .countries path { 9 | fill: #ddd; 10 | stroke: #ebebeb; 11 | stroke-width: 2.5px; 12 | 13 | &.primary { 14 | fill: #fff; 15 | stroke: #bababa; 16 | } 17 | } 18 | 19 | path.landmass { fill: #a8d5ef; } 20 | 21 | .lakes path { 22 | fill: #d3eaf7; 23 | stroke: #a8d5ef; 24 | stroke-width: 1px; 25 | } 26 | 27 | .rivers path { 28 | fill: none; 29 | stroke: #a8d5ef; 30 | stroke-width: 1px; 31 | } 32 | 33 | .cities path { 34 | fill: #999; 35 | 36 | &.admin-0-capital { fill: #333; } 37 | } 38 | 39 | .city-labels text { 40 | .gotham(); 41 | fill: #787878; 42 | font-size: 65%; 43 | text-anchor: start; 44 | -webkit-font-smoothing: antialiased; 45 | 46 | &.admin-0-capital { 47 | fill: #555; 48 | font-weight: bold; 49 | } 50 | } 51 | 52 | .city-labels.shadow text { 53 | fill: #fff; 54 | stroke: #fff; 55 | stroke-width: 2px; 56 | opacity: .7; 57 | } 58 | 59 | .country-labels text { 60 | .gotham(); 61 | fill: #787878; 62 | font-size: 80%; 63 | font-weight: normal; 64 | letter-spacing: 7px; 65 | text-anchor: middle; 66 | text-transform: uppercase; 67 | -webkit-font-smoothing: antialiased; 68 | 69 | &.primary { 70 | fill: #555; 71 | font-size: 90%; 72 | font-weight: 700; 73 | } 74 | &.bhutan { display: none; } 75 | } 76 | 77 | .scale-bar { 78 | line { 79 | stroke: #666; 80 | stroke-width: 5px; 81 | } 82 | text { 83 | font-family: Helvetica, Arial,sans-serif; 84 | fill: #666; 85 | font-size: 60%; 86 | -webkit-font-smoothing: antialiased; 87 | } 88 | } 89 | 90 | /* 91 | * small-screen styles 92 | */ 93 | @media @screen-mobile { 94 | svg { font-size: 12px; } 95 | 96 | .countries path { stroke-width: 2px; } 97 | 98 | .country-labels text { 99 | &.bangladesh, 100 | &.bhutan { 101 | display: none; 102 | } 103 | } 104 | 105 | .city-labels text { letter-spacing: .5px; } 106 | 107 | .city-labels.shadow, 108 | .city-labels text, 109 | .cities path { 110 | &.scalerank-4, 111 | &.scalerank-5, 112 | &.scalerank-6, 113 | &.scalerank-7, 114 | &.scalerank-8 { 115 | display: none; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /graphic_templates/locator_map/geodata.yaml: -------------------------------------------------------------------------------- 1 | bbox: '77.25 24.28 91.45 31.5' 2 | layers: 3 | countries: 4 | type: 'shp' 5 | path: 'http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip' 6 | id-property: 'NAME' 7 | properties: 8 | - 'country=NAME' 9 | cities: 10 | type: 'shp' 11 | path: 'http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places_simple.zip' 12 | id-property: 'name' 13 | properties: 14 | - 'featurecla' 15 | - 'city=name' 16 | - 'scalerank' 17 | where: adm0name = 'Nepal' AND scalerank < 8 18 | 19 | neighbors: 20 | type: 'shp' 21 | path: 'http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places_simple.zip' 22 | id-property: 'name' 23 | properties: 24 | - 'featurecla' 25 | - 'city=name' 26 | - 'scalerank' 27 | where: adm0name != 'Nepal' AND scalerank <= 2 28 | 29 | lakes: 30 | type: 'shp' 31 | path: 'http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/physical/ne_10m_lakes.zip' 32 | 33 | rivers: 34 | type: 'shp' 35 | path: 'http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/physical/ne_10m_rivers_lake_centerlines.zip' 36 | where: featurecla = 'River' AND scalerank < 8 -------------------------------------------------------------------------------- /graphic_templates/locator_map/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1KLrZ6pYx1dBWKORhJ0kFRDAQDpP9NyNnGRfFgtFIWVM' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/locator_map/js/geomath.js: -------------------------------------------------------------------------------- 1 | var EARTH_RADIUS = 6371000; 2 | 3 | /* 4 | * Convert degreest to radians. 5 | */ 6 | var degToRad = function(degrees) { 7 | return degrees * Math.PI / 180; 8 | } 9 | 10 | /* 11 | * Convert radians to degrees. 12 | */ 13 | var radToDeg = function(radians) { 14 | return radians * 180 / Math.PI; 15 | } 16 | 17 | /* 18 | * Convert kilometers to miles. 19 | */ 20 | var kmToMiles = function(km) { 21 | return km * 0.621371; 22 | } 23 | 24 | /* 25 | * Convert miles to kilometers. 26 | */ 27 | var milesToKm = function(miles) { 28 | return miles * 1.60934; 29 | } 30 | 31 | /* 32 | * Calculate the distance between two points. 33 | */ 34 | var distance = function(a, b) { 35 | var lat1Rad = degToRad(a[1]), lng1Rad = degToRad(a[0]); 36 | var lat2Rad = degToRad(b[1]), lng2Rad = degToRad(b[0]); 37 | var latDelta = lat2Rad - lat1Rad; 38 | var lngDelta = lng2Rad - lng1Rad; 39 | 40 | var a = Math.sin(latDelta / 2) * Math.sin(latDelta / 2) + 41 | Math.cos(lat1Rad) * Math.cos(lat2Rad) * 42 | Math.sin(lngDelta / 2) * Math.sin(lngDelta / 2); 43 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 44 | var d = EARTH_RADIUS * c; 45 | 46 | return kmToMiles(d / 1000); 47 | }; 48 | 49 | /* 50 | * Calculate an end point given a starting point, bearing and distance. 51 | * Adapted from http://www.movable-type.co.uk/scripts/latlong.html 52 | */ 53 | var calculateDestinationPoint = function(lat, lon, distance, bearing) { 54 | var distanceFraction = distance / EARTH_RADIUS; 55 | var bearingRad = degToRad(bearing); 56 | 57 | var lat1Rad = degToRad(lat); 58 | var lng1Rad = degToRad(lon); 59 | 60 | var lat2Rad = Math.asin( 61 | Math.sin(lat1Rad) * Math.cos(distanceFraction) + 62 | Math.cos(lat1Rad) * Math.sin(distanceFraction) * Math.cos(bearingRad) 63 | ); 64 | 65 | var lng2Rad = lng1Rad + Math.atan2( 66 | Math.sin(bearingRad) * Math.sin(distanceFraction) * Math.cos(lat1Rad), 67 | Math.cos(distanceFraction) - Math.sin(lat1Rad) * Math.sin(lat2Rad) 68 | ); 69 | 70 | lng2Rad = (lng2Rad + 3 * Math.PI) % (2 * Math.PI) - Math.PI; // normalise to -180..+180° 71 | 72 | return [radToDeg(lng2Rad), radToDeg(lat2Rad)]; 73 | }; 74 | 75 | /* 76 | * Calculate a scale bar, as follows: 77 | * - Select a starting pixel coordinate 78 | * - Convert coordinate to map space 79 | * - Calculate a fixed distance end coordinate *east* of the start coordinate 80 | * - Convert end coordinate back to pixel space 81 | * - Calculate geometric distance between start and end pixel coordinates. 82 | * - Set end coordinate's x value to start coordinate + distance. Y coords hold constant. 83 | */ 84 | var calculateScaleBarEndPoint = function(projection, start, miles) { 85 | var startGeo = projection.invert(start); 86 | 87 | var meters = milesToKm(miles) * 1000; 88 | 89 | var endGeo = calculateDestinationPoint(startGeo[1], startGeo[0], meters, 90); 90 | var end = projection([endGeo[0], endGeo[1]]) 91 | 92 | var distance = Math.sqrt(Math.pow(end[0] - start[0], 2) + Math.pow(end[1] - start[1], 2)); 93 | 94 | return [start[0] + distance, start[1]]; 95 | } 96 | 97 | /* 98 | * Calculate an optimal scale bar length by taking a fraction of the distance 99 | * covered by the map. 100 | */ 101 | var calculateOptimalScaleBarDistance = function(bbox, divisor) { 102 | var mapDistance = distance([bbox[0], bbox[1]], [bbox[2], bbox[3]]); 103 | var fraction = mapDistance / divisor; 104 | var factor = Math.pow(10, Math.floor(Math.log10(fraction))); 105 | 106 | return scaleLength = Math.round(fraction / factor) * factor; 107 | } 108 | -------------------------------------------------------------------------------- /graphic_templates/locator_map/js/lib/topojson.v1.min.js: -------------------------------------------------------------------------------- 1 | !function(){function t(n,t){function r(t){var r,e=n.arcs[0>t?~t:t],o=e[0];return n.transform?(r=[0,0],e.forEach(function(n){r[0]+=n[0],r[1]+=n[1]})):r=e[e.length-1],0>t?[r,o]:[o,r]}function e(n,t){for(var r in n){var e=n[r];delete t[e.start],delete e.start,delete e.end,e.forEach(function(n){o[0>n?~n:n]=1}),f.push(e)}}var o={},i={},u={},f=[],c=-1;return t.forEach(function(r,e){var o,i=n.arcs[0>r?~r:r];i.length<3&&!i[1][0]&&!i[1][1]&&(o=t[++c],t[c]=r,t[e]=o)}),t.forEach(function(n){var t,e,o=r(n),f=o[0],c=o[1];if(t=u[f])if(delete u[t.end],t.push(n),t.end=c,e=i[c]){delete i[e.start];var a=e===t?t:t.concat(e);i[a.start=t.start]=u[a.end=e.end]=a}else i[t.start]=u[t.end]=t;else if(t=i[c])if(delete i[t.start],t.unshift(n),t.start=f,e=u[f]){delete u[e.end];var s=e===t?t:e.concat(t);i[s.start=e.start]=u[s.end=t.end]=s}else i[t.start]=u[t.end]=t;else t=[n],i[t.start=f]=u[t.end=c]=t}),e(u,i),e(i,u),t.forEach(function(n){o[0>n?~n:n]||f.push([n])}),f}function r(n,r,e){function o(n){var t=0>n?~n:n;(s[t]||(s[t]=[])).push({i:n,g:a})}function i(n){n.forEach(o)}function u(n){n.forEach(i)}function f(n){"GeometryCollection"===n.type?n.geometries.forEach(f):n.type in l&&(a=n,l[n.type](n.arcs))}var c=[];if(arguments.length>1){var a,s=[],l={LineString:i,MultiLineString:u,Polygon:u,MultiPolygon:function(n){n.forEach(u)}};f(r),s.forEach(arguments.length<3?function(n){c.push(n[0].i)}:function(n){e(n[0].g,n[n.length-1].g)&&c.push(n[0].i)})}else for(var h=0,p=n.arcs.length;p>h;++h)c.push(h);return{type:"MultiLineString",arcs:t(n,c)}}function e(r,e){function o(n){n.forEach(function(t){t.forEach(function(t){(f[t=0>t?~t:t]||(f[t]=[])).push(n)})}),c.push(n)}function i(n){return l(u(r,{type:"Polygon",arcs:[n]}).coordinates[0])>0}var f={},c=[],a=[];return e.forEach(function(n){"Polygon"===n.type?o(n.arcs):"MultiPolygon"===n.type&&n.arcs.forEach(o)}),c.forEach(function(n){if(!n._){var t=[],r=[n];for(n._=1,a.push(t);n=r.pop();)t.push(n),n.forEach(function(n){n.forEach(function(n){f[0>n?~n:n].forEach(function(n){n._||(n._=1,r.push(n))})})})}}),c.forEach(function(n){delete n._}),{type:"MultiPolygon",arcs:a.map(function(e){var o=[];if(e.forEach(function(n){n.forEach(function(n){n.forEach(function(n){f[0>n?~n:n].length<2&&o.push(n)})})}),o=t(r,o),(n=o.length)>1)for(var u,c=i(e[0][0]),a=0;n>a;++a)if(c===i(o[a])){u=o[0],o[0]=o[a],o[a]=u;break}return o})}}function o(n,t){return"GeometryCollection"===t.type?{type:"FeatureCollection",features:t.geometries.map(function(t){return i(n,t)})}:i(n,t)}function i(n,t){var r={type:"Feature",id:t.id,properties:t.properties||{},geometry:u(n,t)};return null==t.id&&delete r.id,r}function u(n,t){function r(n,t){t.length&&t.pop();for(var r,e=s[0>n?~n:n],o=0,i=e.length;i>o;++o)t.push(r=e[o].slice()),a(r,o);0>n&&f(t,i)}function e(n){return n=n.slice(),a(n,0),n}function o(n){for(var t=[],e=0,o=n.length;o>e;++e)r(n[e],t);return t.length<2&&t.push(t[0].slice()),t}function i(n){for(var t=o(n);t.length<4;)t.push(t[0].slice());return t}function u(n){return n.map(i)}function c(n){var t=n.type;return"GeometryCollection"===t?{type:t,geometries:n.geometries.map(c)}:t in l?{type:t,coordinates:l[t](n)}:null}var a=g(n.transform),s=n.arcs,l={Point:function(n){return e(n.coordinates)},MultiPoint:function(n){return n.coordinates.map(e)},LineString:function(n){return o(n.arcs)},MultiLineString:function(n){return n.arcs.map(o)},Polygon:function(n){return u(n.arcs)},MultiPolygon:function(n){return n.arcs.map(u)}};return c(t)}function f(n,t){for(var r,e=n.length,o=e-t;o<--e;)r=n[o],n[o++]=n[e],n[e]=r}function c(n,t){for(var r=0,e=n.length;e>r;){var o=r+e>>>1;n[o]n&&(n=~n);var r=o[n];r?r.push(t):o[n]=[t]})}function r(n,r){n.forEach(function(n){t(n,r)})}function e(n,t){"GeometryCollection"===n.type?n.geometries.forEach(function(n){e(n,t)}):n.type in u&&u[n.type](n.arcs,t)}var o={},i=n.map(function(){return[]}),u={LineString:t,MultiLineString:r,Polygon:r,MultiPolygon:function(n,t){n.forEach(function(n){r(n,t)})}};n.forEach(e);for(var f in o)for(var a=o[f],s=a.length,l=0;s>l;++l)for(var h=l+1;s>h;++h){var p,v=a[l],g=a[h];(p=i[v])[f=c(p,g)]!==g&&p.splice(f,0,g),(p=i[g])[f=c(p,v)]!==v&&p.splice(f,0,v)}return i}function s(n,t){function r(n){u.remove(n),n[1][2]=t(n),u.push(n)}var e,o=g(n.transform),i=m(n.transform),u=v(),f=0;for(t||(t=h),n.arcs.forEach(function(n){var r=[];n.forEach(o);for(var i=1,f=n.length-1;f>i;++i)e=n.slice(i-1,i+2),e[1][2]=t(e),r.push(e),u.push(e);n[0][2]=n[f][2]=1/0;for(var i=0,f=r.length;f>i;++i)e=r[i],e.previous=r[i-1],e.next=r[i+1]});e=u.pop();){var c=e.previous,a=e.next;e[1][2]0;){var r=(t+1>>1)-1,o=e[r];if(p(n,o)>=0)break;e[o._=t]=o,e[n._=t=r]=n}}function t(n,t){for(;;){var r=t+1<<1,i=r-1,u=t,f=e[u];if(o>i&&p(e[i],f)<0&&(f=e[u=i]),o>r&&p(e[r],f)<0&&(f=e[u=r]),u===t)break;e[f._=t]=f,e[n._=t=u]=n}}var r={},e=[],o=0;return r.push=function(t){return n(e[t._=o]=t,o++),o},r.pop=function(){if(!(0>=o)){var n,r=e[0];return--o>0&&(n=e[o],t(e[n._=0]=n,0)),r}},r.remove=function(r){var i,u=r._;if(e[u]===r)return u!==--o&&(i=e[o],(p(i,r)<0?n:t)(e[i._=u]=i,u)),u},r}function g(n){if(!n)return y;var t,r,e=n.scale[0],o=n.scale[1],i=n.translate[0],u=n.translate[1];return function(n,f){f||(t=r=0),n[0]=(t+=n[0])*e+i,n[1]=(r+=n[1])*o+u}}function m(n){if(!n)return y;var t,r,e=n.scale[0],o=n.scale[1],i=n.translate[0],u=n.translate[1];return function(n,f){f||(t=r=0);var c=0|(n[0]-i)/e,a=0|(n[1]-u)/o;n[0]=c-t,n[1]=a-r,t=c,r=a}}function y(){}var d={version:"1.6.14",mesh:function(n){return u(n,r.apply(this,arguments))},meshArcs:r,merge:function(n){return u(n,e.apply(this,arguments))},mergeArcs:e,feature:o,neighbors:a,presimplify:s};"function"==typeof define&&define.amd?define(d):"object"==typeof module&&module.exports?module.exports=d:this.topojson=d}(); -------------------------------------------------------------------------------- /graphic_templates/quiz/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 |
9 | {% for row in COPY.quiz %} 10 |
11 |
12 |

{{ row.question|smarty }}

13 |
14 |
15 |
    16 | {% if row.status_1 != 'None' %} 17 |
  • 18 | {{ row.option_1 }} 19 | {% if row.status_1 == 'correct' %} 20 |

    {{ row.answer|smarty }}

    21 | {% endif %} 22 |
  • 23 | {% endif %} 24 | {% if row.status_2 != 'None' %} 25 |
  • 26 | {{ row.option_2 }} 27 | {% if row.status_2 == 'correct' %} 28 |

    {{ row.answer|smarty }}

    29 | {% endif %} 30 |
  • 31 | {% endif %} 32 | {% if row.status_3 != 'None' %} 33 |
  • 34 | {{ row.option_3 }} 35 | {% if row.status_3 == 'correct' %} 36 |

    {{ row.answer|smarty }}

    37 | {% endif %} 38 |
  • 39 | {% endif %} 40 | {% if row.status_4 != 'None' %} 41 |
  • 42 | {{ row.option_4 }} 43 | {% if row.status_4 == 'correct' %} 44 |

    {{ row.answer|smarty }}

    45 | {% endif %} 46 |
  • 47 | {% endif %} 48 |
49 |
50 |
51 | {% endfor %} 52 | 53 |

54 |
55 | 56 | {% if COPY.labels.footnote %} 57 |
58 |

Notes

59 |

{{ COPY.labels.footnote|smarty }}

60 |
61 | {% endif %} 62 | 63 | 67 | 68 | 75 | {% endblock content %} 76 | 77 | {% block js %} 78 | {{ JS.push('js/lib/jquery.js') }} 79 | {{ JS.push('js/lib/underscore.js') }} 80 | {{ JS.push('js/graphic.js') }} 81 | {{ JS.render('js/graphic-footer.js') }} 82 | {% endblock js %} -------------------------------------------------------------------------------- /graphic_templates/quiz/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | #quiz { 4 | img { 5 | max-width: 100%; 6 | height: auto; 7 | display: block; 8 | } 9 | 10 | ul { 11 | margin: 0; 12 | padding: 0; 13 | list-style: none; 14 | 15 | li { 16 | display: block; 17 | font-size: 14px; 18 | 19 | strong { 20 | display: block; 21 | background: #ebf4fa; 22 | .gotham(); 23 | list-style:none; 24 | margin: 0 0 7px 0; 25 | padding: 15px; 26 | border-radius: 4px; 27 | -webkit-font-smoothing: antialiased; 28 | transition: background 0.3s ease, color 0.3s ease; 29 | 30 | &:hover { 31 | color: #fff; 32 | background: #7598c9; 33 | cursor: pointer; 34 | } 35 | } 36 | } 37 | } 38 | 39 | .question { 40 | border-top: 1px solid #ddd; 41 | padding-top: 22px; 42 | margin-top: 22px; 43 | 44 | &:first-child { 45 | border-top: none; 46 | padding-top: 0; 47 | margin-top: 0; 48 | } 49 | } 50 | 51 | .q { 52 | p { 53 | margin: 0 0 18px 0; 54 | .gotham(); 55 | font-weight: bold; 56 | } 57 | } 58 | 59 | .answer { 60 | display: none; 61 | color: #787878; 62 | margin-top: 4px; 63 | margin: 0 0 5px 0; 64 | border-left: 1px solid #eee; 65 | border-bottom: 1px solid #eee; 66 | padding: 11px; 67 | .clearfix(); 68 | 69 | b { color: #454545; } 70 | } 71 | 72 | .answered { 73 | pointer-events: none; 74 | 75 | ul { 76 | display: table; 77 | width: 100%; 78 | 79 | li strong:hover { 80 | cursor: default; 81 | opacity: 1; 82 | } 83 | } 84 | 85 | .answer { display: block; } 86 | 87 | .correct strong b, 88 | .incorrect strong b { 89 | display: inline-block; 90 | color: #FFFFFF; 91 | font-size: 10px; 92 | font-weight: bold; 93 | text-transform: uppercase; 94 | padding: 0 6px; 95 | margin-right: 3px; 96 | border-radius: 2px; 97 | } 98 | 99 | .correct strong { 100 | color: #fff; 101 | font-weight: bold; 102 | background-color: #EAAA61; 103 | } 104 | .incorrect strong { background-color: #efefef; } 105 | .correct strong b { background-color: #AA6A21; } 106 | .incorrect.selected strong { 107 | font-weight: bold; 108 | b { background-color: #777; } 109 | } 110 | } 111 | } 112 | 113 | #results { 114 | margin: 55px 0; 115 | font-weight: bold; 116 | font-size: 18px; 117 | line-height: 24px; 118 | text-align: center; 119 | 120 | strong.totals { 121 | padding: 6px 11px; 122 | font-weight: bold; 123 | background-color: #EAAA61; 124 | color: #fff; 125 | font-size: 24px; 126 | display: inline-block; 127 | } 128 | 129 | em { 130 | display: block; 131 | margin-top: 11px; 132 | font-size: 16px; 133 | font-weight: normal; 134 | } 135 | } 136 | 137 | @media screen and (min-width: 501px) { 138 | } 139 | @media screen and (max-width: 501px) { 140 | #quiz { 141 | .q, .a { text-align: center; } 142 | .q { font-size: 13px; } 143 | } 144 | } -------------------------------------------------------------------------------- /graphic_templates/quiz/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1JntHiozQQycbl_Gwru74-IuDVaqIsFJLJBx_DdiQCUE' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/quiz/js/graphic.js: -------------------------------------------------------------------------------- 1 | // Global vars 2 | var pymChild = null; 3 | var isMobile = false; 4 | 5 | var $quiz = null; 6 | var $results = null; 7 | 8 | var numQuestions = 0; 9 | var numTaken = 0; 10 | var numCorrect = 0; 11 | var numRemaining = 0; 12 | 13 | 14 | /* 15 | * Initialize the graphic. 16 | */ 17 | var onWindowLoaded = function() { 18 | // set vars 19 | $quiz = $('#quiz'); 20 | $results = $('#results'); 21 | numQuestions = $quiz.find('div.question').length; 22 | 23 | $quiz.find('li strong').on('click', onAnswerClicked); 24 | 25 | // pym! 26 | pymChild = new pym.Child({ }); 27 | 28 | pymChild.onMessage('on-screen', function(bucket) { 29 | ANALYTICS.trackEvent('on-screen', bucket); 30 | }); 31 | pymChild.onMessage('scroll-depth', function(data) { 32 | data = JSON.parse(data); 33 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 34 | }); 35 | } 36 | 37 | /* 38 | * Quiz 39 | */ 40 | var onAnswerClicked = function() { 41 | console.log($(this)); 42 | var $thisAnswer = $(this); 43 | var $thisQuestion = $thisAnswer.parents('.question'); 44 | var $allAnswers = $thisQuestion.find('strong'); 45 | var resultsMsg = ''; 46 | 47 | // register that this question has been answered 48 | $thisQuestion.addClass('answered'); 49 | numTaken++; 50 | numRemaining = numQuestions - numTaken; 51 | $allAnswers.unbind('click'); 52 | 53 | // Send guess to analytics 54 | // Log the number of the question 55 | var questionMetric = 'guess-question-' + ($thisQuestion.index() + 1); 56 | // Log whether guess was human or machine 57 | var answerValue = $thisAnswer.text(); 58 | ANALYTICS.trackEvent(questionMetric, answerValue); 59 | 60 | // check if the user selected the correct answer 61 | var gotItRight = $thisAnswer.parent('li').hasClass('correct'); 62 | 63 | // tell the user if they got it right 64 | if (gotItRight) { 65 | $thisAnswer.prepend('' + LBL_RIGHT + ' '); 66 | numCorrect++; 67 | } else { 68 | $thisAnswer.parent('li').addClass("selected"); 69 | $thisAnswer.prepend('' + LBL_WRONG + ' '); 70 | } 71 | 72 | // if all questions have been answered, show a rewarding message 73 | if (numTaken == numQuestions) { 74 | resultsMsg = 'You got ' + numCorrect + ' (of ' + numQuestions + ') right. '; 75 | 76 | if (numCorrect <= 2) { 77 | resultsMsg += '' + FINAL_LOW + ''; 78 | } else if (numCorrect <= 6) { 79 | resultsMsg += '' + FINAL_MID + ''; 80 | } else if (numCorrect > 6) { 81 | resultsMsg += '' + FINAL_HIGH + ''; 82 | } 83 | // otherwise, show their status 84 | } else { 85 | resultsMsg = 'You\'ve answered ' + numTaken + ' of ' + numQuestions + ' questions. Keep going!'; 86 | } 87 | $results.html(resultsMsg); 88 | 89 | // update the iframe height 90 | if (pymChild) { 91 | pymChild.sendHeight(); 92 | } 93 | } 94 | 95 | 96 | /* 97 | * Initially load the graphic 98 | * (NB: Use window.load to ensure all images have loaded) 99 | */ 100 | window.onload = onWindowLoaded; -------------------------------------------------------------------------------- /graphic_templates/quiz/js/lib/modernizr.svg.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.7.1 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-svg 3 | */ 4 | ;window.Modernizr=function(a,b,c){function u(a){i.cssText=a}function v(a,b){return u(prefixes.join(a+";")+(b||""))}function w(a,b){return typeof a===b}function x(a,b){return!!~(""+a).indexOf(b)}function y(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:w(f,"function")?f.bind(d||b):f}return!1}var d="2.7.1",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={svg:"http://www.w3.org/2000/svg"},m={},n={},o={},p=[],q=p.slice,r,s={}.hasOwnProperty,t;!w(s,"undefined")&&!w(s.call,"undefined")?t=function(a,b){return s.call(a,b)}:t=function(a,b){return b in a&&w(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.svg=function(){return!!b.createElementNS&&!!b.createElementNS(l.svg,"svg").createSVGRect};for(var z in m)t(m,z)&&(r=z.toLowerCase(),e[r]=m[z](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)t(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},u(""),h=j=null,e._version=d,e}(this,this.document); -------------------------------------------------------------------------------- /graphic_templates/slopegraph/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/slopegraph/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .axis { 4 | fill: #666; 5 | font-weight: bold; 6 | font-size: 12px; 7 | 8 | path { display: none; } 9 | } 10 | 11 | .lines line { 12 | stroke-width: 3px; 13 | stroke: #ddd; 14 | shape-rendering: auto; 15 | } 16 | 17 | .dots circle { 18 | fill: #ddd; 19 | stroke: #fff; 20 | stroke-width: 1px; 21 | } 22 | 23 | .value text { 24 | font-size: 11px; 25 | fill: #333; 26 | } 27 | 28 | .label text { 29 | font-size: 11px; 30 | fill: #333; 31 | } 32 | -------------------------------------------------------------------------------- /graphic_templates/slopegraph/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1MmhQfU4XTYEHSY_9v5PGJXZBsiogr3c8c7h5GprWyQo' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/slopegraph/js/lib/modernizr.svg.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.7.1 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-svg 3 | */ 4 | ;window.Modernizr=function(a,b,c){function u(a){i.cssText=a}function v(a,b){return u(prefixes.join(a+";")+(b||""))}function w(a,b){return typeof a===b}function x(a,b){return!!~(""+a).indexOf(b)}function y(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:w(f,"function")?f.bind(d||b):f}return!1}var d="2.7.1",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={svg:"http://www.w3.org/2000/svg"},m={},n={},o={},p=[],q=p.slice,r,s={}.hasOwnProperty,t;!w(s,"undefined")&&!w(s.call,"undefined")?t=function(a,b){return s.call(a,b)}:t=function(a,b){return b in a&&w(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.svg=function(){return!!b.createElementNS&&!!b.createElementNS(l.svg,"svg").createSVGRect};for(var z in m)t(m,z)&&(r=z.toLowerCase(),e[r]=m[z](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)t(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},u(""),h=j=null,e._version=d,e}(this,this.document); -------------------------------------------------------------------------------- /graphic_templates/stacked_bar_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/stacked_bar_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .value text { 4 | fill: #fff; 5 | 6 | &.hidden { display: none; } 7 | } 8 | -------------------------------------------------------------------------------- /graphic_templates/stacked_bar_chart/data.csv: -------------------------------------------------------------------------------- 1 | label,Confidence,Convenience,Complacency,Other United Kingdom,79,6,13,2 Georgia,69,6,8,17 India,49,18,3,30 Nigeria,36,20,18,26 Pakistan,33,20,6,41 -------------------------------------------------------------------------------- /graphic_templates/stacked_bar_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1DLHWPcJcGoKHRGBtATdBbZVIT0EuiAXG_SiQiDucazg' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/stacked_column_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/stacked_column_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .bar text { 4 | fill: #fff; 5 | font-size: 11px; 6 | 7 | &.hidden { display: none; } 8 | } 9 | -------------------------------------------------------------------------------- /graphic_templates/stacked_column_chart/data.csv: -------------------------------------------------------------------------------- 1 | year,First category,Second category 2005,258,115 2006,248,108 2007,242,106 2008,235,101 2009,234,103 2010,232,104 2011,224,98 2012,223,97 2013,204,93 2014,206,95 -------------------------------------------------------------------------------- /graphic_templates/stacked_column_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1tCkiSX2QV2_LjXWW6sNe7s9MqREeDEYrIGQF8mb0OHw' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/stacked_grouped_column_chart/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 | 15 | 16 | {% if COPY.labels.footnote %} 17 |
18 |

Notes

19 |

{{ COPY.labels.footnote|smarty }}

20 |
21 | {% endif %} 22 | 23 | 27 | 28 | 31 | 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /graphic_templates/stacked_grouped_column_chart/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .bar text { 4 | fill: #fff; 5 | font-size: 11px; 6 | text-anchor: middle; 7 | 8 | &.hidden { display: none; } 9 | } 10 | 11 | .x.axis { 12 | &.category { 13 | text { 14 | font-weight: bold; 15 | fill: #454545; 16 | text-anchor: middle; 17 | } 18 | } 19 | 20 | &.bars { 21 | line, path { 22 | display: none; 23 | } 24 | } 25 | } 26 | 27 | /* 28 | * small-screen styles 29 | */ 30 | @media @screen-mobile { 31 | .bar text { font-size: 10px; } 32 | } 33 | -------------------------------------------------------------------------------- /graphic_templates/stacked_grouped_column_chart/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '12PsbQ7uTHr_iFeJLgt7LSkU66BQUNX8_5wQrJuvjYFU' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/state_grid_map/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | /* 4 | * Key 5 | */ 6 | .key-wrap { 7 | .key { 8 | margin: 0 0 18px 0; 9 | padding: 0; 10 | list-style-type: none; 11 | 12 | .key-item { 13 | display: inline-block; 14 | margin: 0 18px 0 0; 15 | padding: 0; 16 | line-height: 15px; 17 | 18 | b { 19 | display: inline-block; 20 | width: 15px; 21 | height: 15px; 22 | margin-right: 6px; 23 | float: left; 24 | } 25 | 26 | label { 27 | white-space: nowrap; 28 | font-size: 12px; 29 | color: #666; 30 | font-weight: normal; 31 | -webkit-font-smoothing: antialiased; 32 | } 33 | } 34 | } 35 | 36 | // Numeric scale style 37 | &.numeric-scale { 38 | width: 100%; 39 | text-align: center; 40 | 41 | h3 { margin-bottom: 5px; } 42 | 43 | .key { 44 | .key-item { 45 | margin: 0 1px 0 0; 46 | position: relative; 47 | 48 | b { 49 | width: 45px; 50 | height: 10px; 51 | margin-right: 0; 52 | } 53 | 54 | label { 55 | position: absolute; 56 | bottom: -20px; 57 | left: -15%; 58 | 59 | &.end-label { 60 | left: auto; 61 | right: -25%; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | /* 70 | * Grid map 71 | */ 72 | .tile-grid-map { margin-bottom: 33px; } 73 | 74 | .state { fill: #eee; } 75 | 76 | .label { 77 | .knockout(); 78 | fill: #979797; 79 | font-size: 12px; 80 | 81 | &.label-active { fill: #fff; } 82 | } 83 | 84 | /* 85 | * larger-than-mobile-screen styles 86 | */ 87 | @media @screen-mobile-above { 88 | .label { font-size: 8px; } 89 | } 90 | 91 | /* 92 | * small-screen styles 93 | */ 94 | @media @screen-mobile { 95 | .label { font-size: 15px; } 96 | } 97 | -------------------------------------------------------------------------------- /graphic_templates/state_grid_map/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '12e3cNKWd1E2IHcDGN72URkbp7Yjb_3TbJrAxIgpYCTI' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/state_grid_map/js/graphic.js: -------------------------------------------------------------------------------- 1 | // Global config 2 | var MAP_TEMPLATE_ID = '#map-template'; 3 | 4 | // Global vars 5 | var pymChild = null; 6 | var isMobile = false; 7 | 8 | /* 9 | * Initialize the graphic. 10 | */ 11 | var onWindowLoaded = function() { 12 | formatData(); 13 | 14 | pymChild = new pym.Child({ 15 | renderCallback: render 16 | }); 17 | 18 | pymChild.onMessage('on-screen', function(bucket) { 19 | ANALYTICS.trackEvent('on-screen', bucket); 20 | }); 21 | pymChild.onMessage('scroll-depth', function(data) { 22 | data = JSON.parse(data); 23 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 24 | }); 25 | } 26 | 27 | /* 28 | * Format graphic data. 29 | */ 30 | var formatData = function() { 31 | if (LABELS["show_territories"].toLowerCase() === "false") { 32 | var territories = ["Puerto Rico", "U.S. Virgin Islands", "Guam", "Northern Mariana Islands", "American Samoa"]; 33 | 34 | DATA = DATA.filter(function(d) { 35 | return territories.indexOf(d["state_name"]) == -1; 36 | }); 37 | } 38 | } 39 | 40 | 41 | /* 42 | * Render the graphic(s). Called by pym with the container width. 43 | */ 44 | var render = function(containerWidth) { 45 | if (!containerWidth) { 46 | containerWidth = DEFAULT_WIDTH; 47 | } 48 | 49 | if (containerWidth <= MOBILE_THRESHOLD) { 50 | isMobile = true; 51 | } else { 52 | isMobile = false; 53 | } 54 | 55 | if (LABELS['is_numeric'] && LABELS['is_numeric'].toLowerCase() == 'true') { 56 | var isNumeric = true; 57 | } else { 58 | var isNumeric = false; 59 | } 60 | 61 | // Render the map! 62 | renderStateGridMap({ 63 | container: '#state-grid-map', 64 | width: containerWidth, 65 | data: DATA, 66 | // isNumeric will style the legend as a numeric scale 67 | isNumeric: isNumeric 68 | }); 69 | 70 | // Update iframe 71 | if (pymChild) { 72 | pymChild.sendHeight(); 73 | } 74 | } 75 | 76 | 77 | /* 78 | * Render a state grid map. 79 | */ 80 | var renderStateGridMap = function(config) { 81 | var valueColumn = 'category'; 82 | 83 | // Clear existing graphic (for redraw) 84 | var containerElement = d3.select(config['container']); 85 | containerElement.html(''); 86 | 87 | // Copy map template 88 | var template = d3.select(MAP_TEMPLATE_ID); 89 | containerElement.html(template.html()); 90 | 91 | // Extract categories from data 92 | var categories = []; 93 | 94 | if (LABELS['legend_labels'] && LABELS['legend_labels'] !== '') { 95 | // If custom legend labels are specified 96 | var legendLabels = LABELS['legend_labels'].split(','); 97 | legendLabels.forEach(function(label) { 98 | categories.push(label.trim()); 99 | }); 100 | } else { 101 | // Default: Return sorted array of categories 102 | config['data'].forEach(function(state) { 103 | if (state[valueColumn] != null) { 104 | categories.push(state[valueColumn]); 105 | } 106 | }); 107 | 108 | categories = d3.set(categories).values().sort(); 109 | } 110 | 111 | // Create legend 112 | var legendWrapper = containerElement.select('.key-wrap'); 113 | var legendElement = containerElement.select('.key'); 114 | 115 | if (config['isNumeric']) { 116 | legendWrapper.classed('numeric-scale', true); 117 | 118 | var colorScale = d3.scale.ordinal() 119 | .domain(categories) 120 | .range([COLORS['teal6'], COLORS['teal5'], COLORS['teal4'], COLORS['teal3'], COLORS['teal2'], COLORS['teal1']]); 121 | } else { 122 | // Define color scale 123 | var colorScale = d3.scale.ordinal() 124 | .domain(categories) 125 | .range([COLORS['red3'], COLORS['yellow3'], COLORS['blue3'], COLORS['orange3'], COLORS['teal3']]); 126 | } 127 | 128 | colorScale.domain().forEach(function(key, i) { 129 | var keyItem = legendElement.append('li') 130 | .classed('key-item', true) 131 | 132 | keyItem.append('b') 133 | .style('background', colorScale(key)); 134 | 135 | keyItem.append('label') 136 | .text(key); 137 | 138 | // Add the optional upper bound label on numeric scale 139 | if (config['isNumeric'] && i == categories.length - 1) { 140 | if (LABELS['max_label'] && LABELS['max_label'] !== '') { 141 | keyItem.append('label') 142 | .attr('class', 'end-label') 143 | .text(LABELS['max_label']); 144 | } 145 | } 146 | }); 147 | 148 | // Select SVG element 149 | var chartElement = containerElement.select('svg'); 150 | 151 | // resize map (needs to be explicitly set for IE11) 152 | chartElement.attr('width', config['width']) 153 | .attr('height', function() { 154 | var s = d3.select(this); 155 | var viewBox = s.attr('viewBox').split(' '); 156 | return Math.floor(config['width'] * parseInt(viewBox[3]) / parseInt(viewBox[2])); 157 | }); 158 | 159 | // Set state colors 160 | config['data'].forEach(function(state) { 161 | if (state[valueColumn] !== null) { 162 | var stateClass = 'state-' + classify(state['state_name']); 163 | var categoryClass = 'category-' + classify(state[valueColumn]); 164 | 165 | chartElement.select('.' + stateClass) 166 | .attr('class', stateClass + ' state-active ' + categoryClass) 167 | .attr('fill', colorScale(state[valueColumn])); 168 | } 169 | }); 170 | 171 | // Draw state labels 172 | chartElement.append('g') 173 | .selectAll('text') 174 | .data(config['data']) 175 | .enter().append('text') 176 | .attr('text-anchor', 'middle') 177 | .text(function(d) { 178 | var state = STATES.filter(function(v) { return v['name'] == d['state_name'] }).pop(); 179 | 180 | return isMobile ? state['usps'] : state['ap']; 181 | }) 182 | .attr('class', function(d) { 183 | return d[valueColumn] !== null ? 'category-' + classify(d[valueColumn]) + ' label label-active' : 'label'; 184 | }) 185 | .attr('x', function(d) { 186 | var className = '.state-' + classify(d['state_name']); 187 | var tileBox = chartElement.select(className)[0][0].getBBox(); 188 | 189 | return tileBox['x'] + tileBox['width'] * 0.52; 190 | }) 191 | .attr('y', function(d) { 192 | var className = '.state-' + classify(d['state_name']); 193 | var tileBox = chartElement.select(className)[0][0].getBBox(); 194 | var textBox = d3.select(this)[0][0].getBBox(); 195 | var textOffset = textBox['height'] / 2; 196 | 197 | if (isMobile) { 198 | textOffset -= 1; 199 | } 200 | 201 | return (tileBox['y'] + tileBox['height'] * 0.5) + textOffset; 202 | }); 203 | } 204 | 205 | /* 206 | * Initially load the graphic 207 | * (NB: Use window.load to ensure all images have loaded) 208 | */ 209 | window.onload = onWindowLoaded; 210 | -------------------------------------------------------------------------------- /graphic_templates/state_grid_map/js/lib/modernizr.svg.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.7.1 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-svg 3 | */ 4 | ;window.Modernizr=function(a,b,c){function u(a){i.cssText=a}function v(a,b){return u(prefixes.join(a+";")+(b||""))}function w(a,b){return typeof a===b}function x(a,b){return!!~(""+a).indexOf(b)}function y(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:w(f,"function")?f.bind(d||b):f}return!1}var d="2.7.1",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={svg:"http://www.w3.org/2000/svg"},m={},n={},o={},p=[],q=p.slice,r,s={}.hasOwnProperty,t;!w(s,"undefined")&&!w(s.call,"undefined")?t=function(a,b){return s.call(a,b)}:t=function(a,b){return b in a&&w(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.svg=function(){return!!b.createElementNS&&!!b.createElementNS(l.svg,"svg").createSVGRect};for(var z in m)t(m,z)&&(r=z.toLowerCase(),e[r]=m[z](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)t(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},u(""),h=j=null,e._version=d,e}(this,this.document); -------------------------------------------------------------------------------- /graphic_templates/table/child_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_template.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if COPY.labels.headline %}

{{ COPY.labels.headline|smarty }}

{% endif %} 6 | {% if COPY.labels.subhed %}

{{ render(COPY.labels.subhed)|smarty }}

{% endif %} 7 | 8 |
9 | 10 | 11 | 12 | 16 | 20 | 24 | 28 | 29 | 30 | 31 | {% for row in COPY.data %} 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% endfor %} 39 | 40 |
13 |
14 |
{{ COPY.labels.hdr_state }}
15 |
17 |
18 |
{{ COPY.labels.hdr_usps }}
19 |
21 |
22 |
{{ COPY.labels.hdr_ap }}
23 |
25 |
26 |
{{ COPY.labels.hdr_value }}
27 |
{{ row.name }}{{ row.usps }}{{ row.usps|ap_state }}{{ row.value|comma }}
41 |
42 | 43 | {% if COPY.labels.footnote %} 44 |
45 |

Notes

46 |

{{ COPY.labels.footnote|smarty }}

47 |
48 | {% endif %} 49 | 50 | 54 | 55 | {% endblock content %} 56 | 57 | {% block js %} 58 | 59 | {{ JS.push('js/lib/tablesort.js') }} 60 | {{ JS.push('js/lib/tablesort.number.js') }} 61 | {{ JS.push('js/graphic.js') }} 62 | {{ JS.render('js/graphic-footer.js') }} 63 | 64 | {% endblock js %} 65 | -------------------------------------------------------------------------------- /graphic_templates/table/css/graphic.less: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | /* 4 | * Base table styles 5 | */ 6 | table { 7 | border-collapse: collapse; 8 | padding: 0; 9 | width: 100%; 10 | font-size: 12px; 11 | color: #666; 12 | 13 | tr.shadedrow { 14 | background-color: #f1f1f1; 15 | color: #333; 16 | font-weight: bold; 17 | } 18 | 19 | th { 20 | .knockout-header(); 21 | line-height: 1.2; 22 | text-align: left; 23 | vertical-align: bottom; 24 | } 25 | 26 | td { vertical-align: top; } 27 | } 28 | 29 | /* 30 | * Tablesorter styles 31 | */ 32 | table { 33 | th { 34 | &.sort-header { 35 | cursor: pointer; 36 | 37 | &::-moz-selection, 38 | &::selection { 39 | background: transparent; 40 | } 41 | 42 | .sorter { 43 | border-top: 2px solid #404040; 44 | visibility: hidden; 45 | 46 | .icon { 47 | content: ''; 48 | width: 1px; 49 | margin: 3px auto 7px auto; 50 | border-width: 0 4px 4px; 51 | border-style: solid; 52 | border-color: #404040 transparent; 53 | } 54 | } 55 | 56 | &:hover .sorter { 57 | visibility: visible; 58 | } 59 | } 60 | 61 | &.sort-up .sorter, 62 | &.sort-down .sorter, 63 | &.sort-down:hover .sorter { 64 | visibility: visible; 65 | opacity: 0.4; 66 | } 67 | 68 | &.sort-up .sorter .icon { 69 | border-bottom: none; 70 | border-width: 4px 4px 0; 71 | } 72 | } 73 | } 74 | 75 | /* 76 | * larger-than-small-screen styles 77 | * ~ defining some desktop-only styles here to avoid 78 | * ~ writing extra mobile styles to undo them. 79 | */ 80 | @media @screen-mobile-above { 81 | table { 82 | th, td { 83 | padding: 10px; 84 | 85 | // comment these two out if you want to highlight particular rows 86 | &:first-child { padding-left: 0; } 87 | &:last-child { padding-right: 0; } 88 | 89 | &.amt { text-align: right; } 90 | } 91 | 92 | th { 93 | border-bottom: 2px solid #eee; 94 | padding-top: 0; 95 | } 96 | 97 | td { border-bottom: 1px solid #eee; } 98 | } 99 | } 100 | 101 | /* 102 | * small-screen styles 103 | */ 104 | @media @screen-mobile { 105 | tbody { 106 | display: block; 107 | width: 100%; 108 | } 109 | 110 | table { 111 | thead { display: none; } 112 | 113 | tr, th, td { 114 | display: block; 115 | padding: 0; 116 | white-space: normal; 117 | } 118 | 119 | tr { 120 | border-bottom: 1px solid #eee; 121 | padding: 10px 0; 122 | 123 | &:first-child { border-top: 1px solid #eee; } 124 | } 125 | 126 | td { 127 | margin-bottom: 6px; 128 | 129 | &:empty { display: none; } 130 | } 131 | 132 | tr td:first-child { 133 | .knockout-header(); 134 | color: #333; 135 | font-size: 14px; 136 | margin-bottom: 6px; 137 | 138 | &:before { 139 | content: ''; 140 | display: none; 141 | } 142 | } 143 | 144 | th[data-title]:before, 145 | td[data-title]:before { 146 | content: attr(data-title) ":\00A0"; 147 | display: inline-block; 148 | margin-right: 10px; 149 | width: 70%; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /graphic_templates/table/graphic_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base_filters 4 | 5 | COPY_GOOGLE_DOC_KEY = '1a8F0oYWVC0BdEpG8Mbz2HKRkGIn0XQiGWTcVZMXeVdQ' 6 | 7 | USE_ASSETS = False 8 | 9 | # Use these variables to override the default cache timeouts for this graphic 10 | # DEFAULT_MAX_AGE = 20 11 | # ASSETS_MAX_AGE = 300 12 | 13 | JINJA_FILTER_FUNCTIONS = base_filters.FILTERS 14 | -------------------------------------------------------------------------------- /graphic_templates/table/js/graphic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Initialize the graphic. 3 | */ 4 | var onWindowLoaded = function() { 5 | // Uncomment to enable column sorting 6 | // var tablesort = new Tablesort(document.getElementById('state-table')); 7 | 8 | pymChild = new pym.Child({}); 9 | 10 | pymChild.onMessage('on-screen', function(bucket) { 11 | ANALYTICS.trackEvent('on-screen', bucket); 12 | }); 13 | pymChild.onMessage('scroll-depth', function(data) { 14 | data = JSON.parse(data); 15 | ANALYTICS.trackEvent('scroll-depth', data.percent, data.seconds); 16 | }); 17 | } 18 | 19 | 20 | /* 21 | * Initially load the graphic 22 | * (NB: Use window.load instead of document.ready 23 | * to ensure all images have loaded) 24 | */ 25 | window.onload = onWindowLoaded; 26 | -------------------------------------------------------------------------------- /graphic_templates/table/js/lib/tablesort.number.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var cleanNumber = function(i) { 3 | return i.replace(/[^\-?0-9.]/g, ''); 4 | }, 5 | 6 | compareNumber = function(a, b) { 7 | a = parseFloat(a); 8 | b = parseFloat(b); 9 | 10 | a = isNaN(a) ? 0 : a; 11 | b = isNaN(b) ? 0 : b; 12 | 13 | return a - b; 14 | }; 15 | 16 | Tablesort.extend('number', function(item) { 17 | return item.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/) || // Prefixed currency 18 | item.match(/^[-+]?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/) || // Suffixed currency 19 | item.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/); // Number 20 | }, function(a, b) { 21 | a = cleanNumber(a); 22 | b = cleanNumber(b); 23 | 24 | return compareNumber(b, a); 25 | }); 26 | }()); 27 | -------------------------------------------------------------------------------- /oauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import app_config 4 | import os 5 | 6 | from app_config import authomatic 7 | from authomatic.adapters import WerkzeugAdapter 8 | from exceptions import KeyError 9 | from flask import Blueprint, make_response, redirect, render_template, url_for 10 | from functools import wraps 11 | from render_utils import load_graphic_config, make_context 12 | 13 | # Via: https://developers.google.com/drive/v3/reference/files/export 14 | # and: https://developers.google.com/drive/v3/web/manage-downloads 15 | DRIVE_API_EXPORT_TEMPLATE = 'https://www.googleapis.com/drive/v3/files/%s/export?mimeType=%s' 16 | oauth = Blueprint('_oauth', __name__) 17 | 18 | @oauth.route('/oauth/') 19 | def oauth_alert(): 20 | """ 21 | Show an OAuth alert to start authentication process. 22 | """ 23 | context = make_context() 24 | 25 | if not _has_api_credentials(): 26 | return render_template('oauth/warning.html', **context) 27 | 28 | credentials = get_credentials() 29 | if credentials: 30 | resp = authomatic.access(credentials, 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json') 31 | if resp.status == 200: 32 | context['email'] = resp.data['email'] 33 | 34 | return render_template('oauth/oauth.html', **context) 35 | 36 | @oauth.route('/authenticate/', methods=['GET', 'POST']) 37 | def authenticate(): 38 | """ 39 | Run OAuth workflow. 40 | """ 41 | from flask import request 42 | 43 | response = make_response() 44 | context = make_context() 45 | 46 | if not _has_api_credentials(): 47 | return render_template('oauth/warning.html', **context) 48 | 49 | result = authomatic.login(WerkzeugAdapter(request, response), 'google') 50 | 51 | if result: 52 | context['result'] = result 53 | 54 | if not result.error: 55 | save_credentials(result.user.credentials) 56 | 57 | return render_template('oauth/authenticate.html', **context) 58 | 59 | return response 60 | 61 | def oauth_required(f): 62 | """ 63 | Decorator to ensure oauth workflow has happened. 64 | """ 65 | @wraps(f) 66 | def decorated_function(*args, **kwargs): 67 | from flask import request, g 68 | alt_path = getattr(g, 'alt_path', None) 69 | if request.path.startswith('/graphics/'): 70 | slug = request.path.split('/')[-2] 71 | if alt_path: 72 | graphic_path = alt_path 73 | else: 74 | graphic_path = '%s/%s' % (app_config.GRAPHICS_PATH, slug) 75 | 76 | try: 77 | graphic_config = load_graphic_config(graphic_path) 78 | except IOError: 79 | return f(*args, **kwargs) 80 | 81 | credentials = get_credentials() 82 | 83 | if hasattr(graphic_config, 'COPY_GOOGLE_DOC_KEY') and graphic_config.COPY_GOOGLE_DOC_KEY and (not credentials or not credentials.valid): 84 | return redirect(url_for('_oauth.oauth_alert')) 85 | 86 | return f(*args, **kwargs) 87 | return decorated_function 88 | 89 | def get_credentials(): 90 | """ 91 | Read Authomatic credentials object from disk and refresh if necessary. 92 | """ 93 | file_path = os.path.expanduser(app_config.GOOGLE_OAUTH_CREDENTIALS_PATH) 94 | 95 | try: 96 | with open(file_path) as f: 97 | serialized_credentials = f.read() 98 | except IOError: 99 | return None 100 | 101 | credentials = authomatic.credentials(serialized_credentials) 102 | 103 | if not credentials.valid: 104 | credentials.refresh() 105 | save_credentials(credentials) 106 | 107 | return credentials 108 | 109 | def save_credentials(credentials): 110 | """ 111 | Take Authomatic credentials object and save to disk. 112 | """ 113 | file_path = os.path.expanduser(app_config.GOOGLE_OAUTH_CREDENTIALS_PATH) 114 | with open(file_path, 'w') as f: 115 | f.write(credentials.serialize()) 116 | 117 | def get_document(key, file_path, mimeType=None): 118 | """ 119 | Uses Authomatic to get the google doc 120 | """ 121 | # Default to spreadsheet if no mimeType is passed 122 | mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 123 | if not mimeType: 124 | mimeType = mime 125 | credentials = get_credentials() 126 | url = DRIVE_API_EXPORT_TEMPLATE % ( 127 | key, 128 | mimeType) 129 | response = app_config.authomatic.access(credentials, url) 130 | 131 | if response.status != 200: 132 | if response.status == 404: 133 | raise KeyError("Error! Your Google Doc does not exist or you do not have permission to access it.") 134 | else: 135 | raise KeyError("Error! Google returned a %s error" % response.status) 136 | 137 | with open(file_path, 'wb') as writefile: 138 | writefile.write(response.content) 139 | 140 | def _has_api_credentials(): 141 | """ 142 | Test for API credentials 143 | """ 144 | client_id = os.environ.get('GOOGLE_OAUTH_CLIENT_ID') 145 | client_secret = os.environ.get('GOOGLE_OAUTH_CONSUMER_SECRET') 146 | salt = os.environ.get('AUTHOMATIC_SALT') 147 | return bool(client_id and client_secret and salt) 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dailygraphics", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "less": "~3.5.3", 6 | "ogr2ogr": "^1.3.0", 7 | "topojson": "^1.6.27" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /render_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import codecs 4 | from datetime import datetime 5 | import imp 6 | import json 7 | import time 8 | import urllib 9 | import subprocess 10 | import sys 11 | 12 | from flask import Markup, g, render_template, request 13 | from slimit import minify 14 | from smartypants import smartypants 15 | from jinja2 import contextfunction, Template 16 | 17 | import app_config 18 | import copytext 19 | 20 | class BetterJSONEncoder(json.JSONEncoder): 21 | """ 22 | A JSON encoder that intelligently handles datetimes. 23 | """ 24 | def default(self, obj): 25 | if isinstance(obj, datetime): 26 | encoded_object = obj.isoformat() 27 | else: 28 | encoded_object = json.JSONEncoder.default(self, obj) 29 | 30 | return encoded_object 31 | 32 | class Includer(object): 33 | """ 34 | Base class for Javascript and CSS psuedo-template-tags. 35 | 36 | See `make_context` for an explanation of `asset_depth`. 37 | """ 38 | def __init__(self, asset_depth=0, root_path='www'): 39 | self.includes = [] 40 | self.tag_string = None 41 | self.asset_depth = asset_depth 42 | self.root_path = root_path 43 | 44 | def push(self, path): 45 | self.includes.append(path) 46 | 47 | return '' 48 | 49 | def _compress(self): 50 | raise NotImplementedError() 51 | 52 | def _relativize_path(self, path): 53 | relative_path = path 54 | depth = len(request.path.split('/')) - (2 + self.asset_depth) 55 | 56 | while depth > 0: 57 | relative_path = '../%s' % relative_path 58 | depth -= 1 59 | 60 | return relative_path 61 | 62 | def render(self, path): 63 | if getattr(g, 'compile_includes', False): 64 | if path in g.compiled_includes: 65 | timestamp_path = g.compiled_includes[path] 66 | else: 67 | # Add a querystring to the rendered filename to prevent caching 68 | timestamp_path = '%s?%i' % (path, int(time.time())) 69 | 70 | out_path = '%s/%s' % (self.root_path, path) 71 | 72 | if path not in g.compiled_includes: 73 | print 'Rendering %s' % out_path 74 | 75 | with codecs.open(out_path, 'w', encoding='utf-8') as f: 76 | f.write(self._compress()) 77 | 78 | # See "fab render" 79 | g.compiled_includes[path] = timestamp_path 80 | 81 | markup = Markup(self.tag_string % self._relativize_path(timestamp_path)) 82 | else: 83 | response = ','.join(self.includes) 84 | 85 | response = '\n'.join([ 86 | self.tag_string % self._relativize_path(src) for src in self.includes 87 | ]) 88 | 89 | markup = Markup(response) 90 | 91 | del self.includes[:] 92 | 93 | return markup 94 | 95 | class JavascriptIncluder(Includer): 96 | """ 97 | Psuedo-template tag that handles collecting Javascript and serving appropriate clean or compressed versions. 98 | """ 99 | def __init__(self, *args, **kwargs): 100 | Includer.__init__(self, *args, **kwargs) 101 | 102 | self.tag_string = '' 103 | 104 | def _compress(self): 105 | output = [] 106 | src_paths = [] 107 | 108 | for src in self.includes: 109 | src_paths.append('%s/%s' % (self.root_path, src)) 110 | 111 | with codecs.open('%s/%s' % (self.root_path, src), encoding='utf-8') as f: 112 | if not src.endswith('.min.js'): 113 | print '- compressing %s' % src 114 | output.append(minify(f.read())) 115 | else: 116 | print '- appending already compressed %s' % src 117 | output.append(f.read()) 118 | 119 | context = make_context() 120 | context['paths'] = src_paths 121 | 122 | return '\n'.join(output) 123 | 124 | class CSSIncluder(Includer): 125 | """ 126 | Psuedo-template tag that handles collecting CSS and serving appropriate clean or compressed versions. 127 | """ 128 | def __init__(self, *args, **kwargs): 129 | Includer.__init__(self, *args, **kwargs) 130 | 131 | self.tag_string = '' 132 | 133 | def _compress(self): 134 | output = [] 135 | 136 | src_paths = [] 137 | 138 | for src in self.includes: 139 | css_path = '%s/%s' % (self.root_path, src) 140 | 141 | src_paths.append(css_path) 142 | 143 | try: 144 | compressed_src = subprocess.check_output(["node_modules/less/bin/lessc", "-x", css_path]) 145 | output.append(compressed_src) 146 | except: 147 | print 'It looks like "lessc" isn\'t installed. Try running: "npm install"' 148 | raise 149 | 150 | context = make_context() 151 | context['paths'] = src_paths 152 | 153 | return '\n'.join(output) 154 | 155 | def load_graphic_config(graphic_path, base_paths=[]): 156 | """ 157 | Load the Python configuration module for a graphic. 158 | """ 159 | for path in base_paths: 160 | sys.path.insert(0, path) 161 | 162 | sys.path.insert(0, graphic_path) 163 | 164 | paths = [graphic_path] + base_paths 165 | 166 | try: 167 | f, path, desc = imp.find_module('graphic_config', paths) 168 | graphic_config = imp.load_module('graphic_config', f, path, desc) 169 | f.close() 170 | except ImportError: 171 | class EmptyConfig: 172 | pass 173 | graphic_config = EmptyConfig() 174 | 175 | sys.path.pop(0) 176 | 177 | for path in base_paths: 178 | sys.path.pop(0) 179 | 180 | return graphic_config 181 | 182 | def flatten_app_config(): 183 | """ 184 | Returns a copy of app_config containing only 185 | configuration variables. 186 | """ 187 | config = {} 188 | 189 | # Only all-caps [constant] vars get included 190 | for k, v in app_config.__dict__.items(): 191 | if k.upper() == k: 192 | config[k] = v 193 | 194 | return config 195 | 196 | def make_context(asset_depth=0, root_path='www'): 197 | """ 198 | Create a base-context for rendering views. 199 | Includes app_config and JS/CSS includers. 200 | 201 | `asset_depth` indicates how far into the url hierarchy 202 | the assets are hosted. If 0, then they are at the root. 203 | If 1 then at /foo/, etc. 204 | """ 205 | context = flatten_app_config() 206 | 207 | context['JS'] = JavascriptIncluder( 208 | asset_depth=asset_depth, 209 | root_path=root_path 210 | ) 211 | context['CSS'] = CSSIncluder( 212 | asset_depth=asset_depth, 213 | root_path=root_path 214 | ) 215 | 216 | return context 217 | 218 | def urlencode_filter(s): 219 | """ 220 | Filter to urlencode strings. 221 | """ 222 | if type(s) == 'Markup': 223 | s = s.unescape() 224 | 225 | # Evaulate COPY elements 226 | if type(s) is not unicode: 227 | s = unicode(s) 228 | 229 | s = s.encode('utf8') 230 | s = urllib.quote_plus(s) 231 | 232 | return Markup(s) 233 | 234 | def smarty_filter(s): 235 | """ 236 | Filter to smartypants strings. 237 | """ 238 | if type(s) == 'Markup': 239 | s = s.unescape() 240 | 241 | # Evaulate COPY elements 242 | if type(s) is not unicode: 243 | s = unicode(s) 244 | 245 | 246 | s = s.encode('utf-8') 247 | s = smartypants(s) 248 | 249 | try: 250 | return Markup(s) 251 | except: 252 | print 'This string failed to encode: %s' % s 253 | return Markup(s) 254 | 255 | @contextfunction 256 | def render_with_context(context, text): 257 | """ 258 | Render a template within a template! 259 | """ 260 | template = Template(text.__unicode__()) 261 | 262 | return template.render(**context) 263 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Authomatic==0.1.0.post1 2 | Fabric==1.14.0 3 | Flask==0.9 4 | boto==2.48.0 5 | copytext==0.1.9 6 | cssmin==0.2.0 7 | docutils==0.11 8 | gunicorn==19.8.1 9 | requests==2.18.4 10 | slimit==0.8.1 11 | ply==3.4 12 | smartypants==1.8.6 13 | 14 | # Requirements for testing 15 | # Since this Python package is only used for development, 16 | # not as a dependency, don't worry about splitting these out 17 | # into a separate `requirements-dev.txt` file 18 | nose==1.2.1 19 | selenium==3.3.3 20 | 21 | # added for later support 22 | cryptography==3.3.2 23 | werkzeug==0.16.0 -------------------------------------------------------------------------------- /templates/copyedit/graphic.txt: -------------------------------------------------------------------------------- 1 | 2 | ---- GRAPHIC {{graphic.graphic_number}} ---- 3 | 4 | Spreadsheet URL: https://docs.google.com/spreadsheets/d/{{graphic.spreadsheet_id}}/edit#gid=0 5 | Production URL: https://apps.npr.org/dailygraphics/graphics/{{graphic.app_id}}/#desktop 6 | {% for row in graphic.sheet %} 7 | {{row[0]}}: {{row[1]}} 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /templates/copyedit/note.txt: -------------------------------------------------------------------------------- 1 | {% trans count=graphics|length %} 2 | This graphic accompanies __AUTHOR__'s story, running __TIME__, about __SUBJECT__. 3 | {% pluralize %} 4 | These graphics accompany __AUTHOR__'s story, running __TIME__, about __SUBJECT__. 5 | {% endtrans %} 6 | Story URL (not yet published): http://www.npr.org/templates/story/story.php?storyId=__SEAMUS_ID__&live=1 7 | 8 | Expected run date: __TIME__ 9 | 10 | Primary graphics contact: __GRAPHICS_CONTACT__ 11 | Primary editorial contact: __EDITORIAL_CONTACT__ 12 | 13 | {% for graphic in graphics %}{% include 'copyedit/graphic.txt' %}{% endfor %} 14 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Graphic Embed : NPR 5 | 6 | 7 | 8 | 68 | 69 | 70 | 71 | 72 |

There are {{ graphics_count }} graphics:

73 | 74 |
    {% for graphic in graphics %} 75 |
  1. {{ graphic }}
  2. 76 | {% endfor %}
77 | 78 |

There are {{ templates_count }} templates:

79 | 80 |
    {% for template in templates %} 81 |
  1. {{ template }}
  2. 82 | {% endfor %}
83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /templates/oauth/_oauth_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}App Template OAuth{% endblock %} 6 | 7 | 8 | 9 |
10 |
11 |
12 | {% block content %} 13 | {% endblock content %} 14 |
15 |
16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/oauth/authenticate.html: -------------------------------------------------------------------------------- 1 | {% extends 'oauth/_oauth_base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% if result.error %} 6 |

Oh no, there was an error!

7 |

Error message: {{ result.error.message }}

8 | {% endif %} 9 | 10 | {% if result.user %} 11 |

Success!

12 | 13 |

You are now authenticated.

14 | 15 | {% if request.url_root.endswith('8888/') %} 16 |

You can close this window.

17 | {% else %} 18 |

Take me back!

19 | {% endif %} 20 | 21 | {% endif %} 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/oauth/oauth.html: -------------------------------------------------------------------------------- 1 | {% extends 'oauth/_oauth_base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% if email %} 6 |

Hey! You're already authenticated.

7 |

You're authenticated with {{ email }}.

8 |

RE-AUTHENTICATE ME

9 |

Re-authenticate if you need to switch Google accounts or to debug this workflow.

10 | {% else %} 11 |

Hey! You haven't authenticated with Google yet.

12 |

Don't worry. We'll take you through it.

13 |

AUTHENTICATE ME

14 | {% endif %} 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/oauth/warning.html: -------------------------------------------------------------------------------- 1 | {% extends 'oauth/_oauth_base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Whoops, some configuration needed

6 |

You'll need to set these environment variables before you continue:

7 |
    8 |
  • GOOGLE_OAUTH_CLIENT_ID
  • 9 |
  • GOOGLE_OAUTH_CONSUMER_SECRET
  • 10 |
  • AUTHOMATIC_SALT
  • 11 |
12 |

See the NPR Visuals blog post for more details.

13 |
14 | {% endblock %} 15 | --------------------------------------------------------------------------------