├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── bower.json ├── dist ├── assets │ ├── i18n │ │ └── en_GB.json │ └── images │ │ ├── angular.png │ │ ├── bootstrap.png │ │ ├── browsersync.png │ │ ├── gulp.png │ │ ├── jasmine.png │ │ ├── karma.png │ │ ├── node-sass.png │ │ ├── protractor.png │ │ ├── ui-bootstrap.png │ │ └── yeoman.png ├── bower_components │ └── c3 │ │ └── c3.css ├── config.yaml ├── default.config.yaml ├── favicon.ico ├── index.html ├── styles │ └── vendor-8671a67c1a.css └── themes │ ├── axismaker.config.yaml │ ├── default.css │ ├── sundaytimes.config.yaml │ ├── sundaytimes.css │ ├── thetimes.config.yaml │ ├── thetimes.css │ └── wordpress.config.yaml ├── e2e ├── .jshintrc ├── main.po.js └── main.spec.js ├── gulp ├── .jshintrc ├── build.js ├── conf.js ├── e2e-tests.js ├── inject.js ├── scripts.js ├── server.js ├── styles.js ├── unit-tests.js └── watch.js ├── gulpfile.js ├── inch.json ├── karma.conf.js ├── package.json ├── protractor.conf.js └── src ├── app ├── components │ ├── addColorDataAttributes │ │ ├── addColorDataAttributes.directive.js │ │ └── addColorDataAttributes.directive.spec.js │ ├── chart │ │ ├── buildChart │ │ │ ├── buildChart.directive.js │ │ │ └── buildChart.directive.spec.js │ │ ├── c3 │ │ │ ├── c3.service.js │ │ │ └── c3.service.spec.js │ │ ├── chart.html │ │ ├── chartService │ │ │ ├── chartService.service.js │ │ │ └── chartService.service.spec.js │ │ └── exportChart │ │ │ ├── exportChart.directive.js │ │ │ └── exportChart.directive.spec.js │ ├── config │ │ ├── configChooser │ │ │ ├── configChooser.html │ │ │ ├── configChooser.service.js │ │ │ └── configChooser.service.spec.js │ │ └── configLoader │ │ │ ├── configLoader.provider.js │ │ │ └── configLoader.provider.spec.js │ ├── input │ │ ├── csv │ │ │ ├── csvInput.html │ │ │ ├── csvInput.service.js │ │ │ ├── csvInput.service.spec.js │ │ │ ├── maintainFocus.directive.js │ │ │ └── maintainFocus.directive.spec.js │ │ ├── feed │ │ │ ├── feedInput.html │ │ │ ├── feedInput.service.js │ │ │ └── feedInput.service.spec.js │ │ ├── financial │ │ │ ├── financialInput.html │ │ │ ├── financialInput.service.js │ │ │ └── financialInput.service.spec.js │ │ ├── generic │ │ │ ├── genericInput.service.js │ │ │ └── genericInput.service.spec.js │ │ ├── inputChooser │ │ │ ├── inputChooser.directive.js │ │ │ ├── inputChooser.directive.spec.js │ │ │ └── inputChooser.html │ │ ├── inputService │ │ │ ├── inputService.service.js │ │ │ └── inputService.service.spec.js │ │ └── spreadsheet │ │ │ ├── spreadsheetInput.directive.html │ │ │ ├── spreadsheetInput.directive.js │ │ │ ├── spreadsheetInput.directive.spec.js │ │ │ ├── spreadsheetInput.html │ │ │ ├── spreadsheetInput.service.js │ │ │ └── spreadsheetInput.service.spec.js │ ├── output │ │ ├── axis │ │ │ ├── axisOutput.service.js │ │ │ └── axisOutput.service.spec.js │ │ ├── embedcode │ │ │ ├── embedcode.modal.html │ │ │ ├── embedcodeOutput.service.js │ │ │ └── embedcodeOutput.service.spec.js │ │ ├── generic │ │ │ ├── genericOutput.service.js │ │ │ └── genericOutput.service.spec.js │ │ ├── outputService │ │ │ ├── outputService.service.js │ │ │ └── outputService.service.spec.js │ │ ├── pdf │ │ │ ├── pdfOutput.service.js │ │ │ └── pdfOutput.service.spec.js │ │ ├── png │ │ │ ├── pngOutput.service.js │ │ │ └── pngOutput.service.spec.js │ │ ├── svg │ │ │ ├── svgOutput.service.js │ │ │ └── svgOutput.service.spec.js │ │ └── wordpress │ │ │ ├── wordpressOutput.service.js │ │ │ └── wordpressOutput.service.spec.js │ └── saveButton │ │ ├── saveButton.directive.js │ │ ├── saveButton.directive.spec.js │ │ └── saveButton.html ├── head │ ├── head.controller.js │ └── head.controller.spec.js ├── index.config.js ├── index.constants.js ├── index.module.js ├── index.route.js ├── index.run.js ├── index.scss └── main │ ├── main.controller.js │ ├── main.controller.spec.js │ ├── main.html │ └── partials │ ├── axes.html │ ├── input.html │ ├── options.chart.html │ ├── options.data.html │ └── options.output.html ├── assets ├── i18n │ └── en_GB.json └── images │ ├── angular.png │ ├── bootstrap.png │ ├── browsersync.png │ ├── gulp.png │ ├── jasmine.png │ ├── karma.png │ ├── node-sass.png │ ├── protractor.png │ ├── ui-bootstrap.png │ └── yeoman.png ├── config.yaml ├── default.config.yaml ├── favicon.ico ├── index.html └── themes ├── axismaker.config.yaml ├── default.css ├── sundaytimes.config.yaml ├── sundaytimes.css ├── thetimes.config.yaml ├── thetimes.css └── wordpress.config.yaml /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | !dist/bower_components/ 4 | .sass-cache/ 5 | .idea/ 6 | .tmp/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "bitwise": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "latedef": false, 7 | "noarg": true, 8 | "undef": true, 9 | "unused": true, 10 | "validthis": true, 11 | "jasmine": true, 12 | "browser": true, 13 | "node": true, 14 | "globals": { 15 | "angular": false, 16 | "inject": false, 17 | "module": false, 18 | "console": true, 19 | "$": true, 20 | "dump": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '0.12' 5 | before_script: 6 | - "export DISPLAY=:99.0" 7 | - "sh -e /etc/init.d/xvfb start" 8 | - 'npm install -g bower gulp-cli karma' 9 | - 'bower install' 10 | after_success: 11 | - npm run codecov 12 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-gulp-angular": { 3 | "version": "0.12.1", 4 | "props": { 5 | "angularVersion": "~1.4.0", 6 | "angularModules": [ 7 | { 8 | "key": "animate", 9 | "module": "ngAnimate" 10 | }, 11 | { 12 | "key": "cookies", 13 | "module": "ngCookies" 14 | }, 15 | { 16 | "key": "touch", 17 | "module": "ngTouch" 18 | }, 19 | { 20 | "key": "sanitize", 21 | "module": "ngSanitize" 22 | } 23 | ], 24 | "jQuery": { 25 | "key": "jquery2" 26 | }, 27 | "resource": { 28 | "key": "none", 29 | "module": null 30 | }, 31 | "router": { 32 | "key": "ui-router", 33 | "module": "ui.router" 34 | }, 35 | "ui": { 36 | "key": "bootstrap", 37 | "module": null 38 | }, 39 | "bootstrapComponents": { 40 | "key": "ui-bootstrap", 41 | "module": "ui.bootstrap" 42 | }, 43 | "cssPreprocessor": { 44 | "key": "node-sass", 45 | "extension": "scss" 46 | }, 47 | "jsPreprocessor": { 48 | "key": "none", 49 | "extension": "js", 50 | "srcExtension": "js" 51 | }, 52 | "htmlPreprocessor": { 53 | "key": "none", 54 | "extension": "html" 55 | }, 56 | "foundationComponents": { 57 | "name": null, 58 | "version": null, 59 | "key": null, 60 | "module": null 61 | }, 62 | "paths": { 63 | "src": "src", 64 | "dist": "dist", 65 | "e2e": "e2e", 66 | "tmp": ".tmp" 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [year] [fullname] 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AxisJS [![Build Status](https://travis-ci.org/times/axisJS.svg?branch=master)](https://travis-ci.org/times/axisJS) [![codecov.io](http://codecov.io/github/times/axisJS/coverage.svg?branch=master)](http://codecov.io/github/times/axisJS?branch=master) [![Documentation status](http://inch-ci.org/github/times/axisJS.svg?branch=master)](http://inch-ci.org/github/times/axisJS) [![Join the chat at https://gitter.im/times/axisJS](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/times/axisJS?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | ### 2014 [Ændrew Rininsland](http://www.github.com/aendrew) for [The Times and Sunday Times](http://www.github.com/times) 3 | 4 | AxisJS is a light [Angular](http://angularjs.org)-based app for generating charts. It combines with 5 | the [AxisWP](http://www.github.com/times/Axis) WordPress plugin to 6 | add rich charting capabilities to WordPress. 7 | 8 | **[Live demo of AxisJS here](http://times.github.io/axisJS/#/)** 9 | 10 | AxisJS is built atop the [Yeoman](http://github.com/yeoman) [Gulp-Angular](http://github.com/Swiip/generator-gulp-angular) 11 | generator and currently makes use of C3 to build charts. 12 | 13 | AxisJS owes a huge debt of gratitude to [Quartz](http://www.qz.com)'s [ChartBuilder](http://quartz.github.io/ChartBuilder), 14 | from where much of the PNG/SVG output code is taken (in addition to some of the interface design). However, Axis is more extensible and intended to be used for online interactive graphics. 15 | 16 | ### Bower 17 | 18 | `bower install axisjs` 19 | 20 | ### Project goals 21 | 22 | 1. Enable easy integration of various D3-based frameworks into a simple interface 23 | 2. Enable a wide array of data input methods 24 | 3. Be modular enough to allow charting frameworks to easily be replaced 25 | 4. Allow for straight-forward customisation and styling 26 | 5. Allow for easy integration into existing content management systems 27 | 6. Allow journalists to easily create charts that are embeddable across a wide array of devices and media 28 | 29 | 30 | ### To build 31 | 32 | 1. `npm install` 33 | 2. `bower install` 34 | 3. `gulp build` 35 | 36 | ### Modifying 37 | 38 | The source is in the `src/` folder, which gets built to `dist/` when you do `gulp build`. 39 | When working on it, run `gulp serve` to invoke a light HTTP server that auto-reloads the page 40 | when you save a file. Styles are in Sass at `src/app/index.scss`. 41 | 42 | ### Contributing 43 | 44 | Please do a new feature branch when forking and squash your commits before 45 | making a pull request. Pull requests welcomed and encouraged. I especially welcome 46 | any documentation or unit testing PRs! 47 | 48 | ### API Docs 49 | 50 | Inline documentation is in ngDoc format and gets built to `docs` during `gulp build`. 51 | View API docs online [here](http://times.github.io/axisJS/docs/). 52 | 53 | ### Roadmap/ToDos 54 | 55 | - [x] Abstract chart configuration into a provider so that `app/scripts/directives/buildchart.js` 56 | and `app/scripts/directives/exportchart.js` aren't so tightly bound to C3 57 | - [x] **ALL** the unit tests 58 | - [x] Documentation and cleanup of `buildchart.js` and `exportchart.js` 59 | - [x] Abstract each output format into factories so more can be modularly added 60 | - [ ] Abstract out vendor functionality — i.e., make the colour picker replaceable 61 | - [x] Create an external config file with settings like colour scheme 62 | - [x] Improve inline documentation 63 | - [x] Make adding categorical axes more straight-forward 64 | - [ ] Create adapters for [nvd3](http://www.nvd3.org) and other SVG-based charting libraries. 65 | - [ ] Componentise so it can be easily dropped into any CMS or app 66 | 67 | ### Contributors 68 | 69 | #### Lead developer: Ændrew Rininsland ([@aendrew](http://github.com/aendrew)) 70 | 71 | #### Design contributions 72 | * Ben Rixon ([@WiseOgre](http://github.com/wiseogre)) 73 | * Samantha Boelhouwer ([@mataulvr](https://github.com/mataulvr)) 74 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axisJS", 3 | "description": "Modular, Angular-based framework for creating basic charts", 4 | "main": [ 5 | "dist/scripts/scripts.js" 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "app/", 10 | "test/", 11 | "Gruntfile.js", 12 | ".editorconfig", 13 | ".jshintrc", 14 | ".travis.yml", 15 | ".gitignore", 16 | "bower_components/", 17 | "docs/", 18 | "coverage/" 19 | ], 20 | "keywords": [ 21 | "charts", 22 | "d3", 23 | "c3", 24 | "graphs", 25 | "svg", 26 | "png" 27 | ], 28 | "authors": [ 29 | "Ændrew Rininsland (http://aendrew.com)" 30 | ], 31 | "homepage": "http://times.github.io/axisJS", 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/times/axisJS.git" 35 | }, 36 | "dependencies": { 37 | "css-layout": "facebook/css-layout#~1.1.1" 38 | }, 39 | "devDependencies": { 40 | "angular-animate": "~1.4.0", 41 | "angular-touch": "~1.4.0", 42 | "angular-sanitize": "~1.4.0", 43 | "jquery": "~2.1.4", 44 | "angular-ui-router": "~0.2.15", 45 | "bootstrap-sass-official": "~3.3.4", 46 | "toastr": "~2.1.1", 47 | "moment": "~2.10.3", 48 | "animate.css": "~3.3.0", 49 | "angular": "~1.4.0", 50 | "js-yaml": "~3.3.1", 51 | "angular-resource": "~1.4.1", 52 | "c3": "aendrew/c3#title_options", 53 | "papa-parse": "~4.1.1", 54 | "bootstrap-colorselector": "*", 55 | "canvg": "~1.3.0", 56 | "angular-minicolors": "~0.0.5", 57 | "jsonfn": "vkiryukhin/jsonfn", 58 | "jspdf": "~1.0.272", 59 | "angular-local-storage": "~0.2.2", 60 | "angular-aside": "~1.1.3", 61 | "angular-translate-loader-static-files": "~2.7.2", 62 | "angular-bootstrap": "~0.12.0", 63 | "angular-mocks": "~1.4.0", 64 | "ngHandsontable": "0.7.0-beta2" 65 | }, 66 | "resolutions": { 67 | "jquery": "~2.1.4", 68 | "angular": "~1.4.0", 69 | "angular-bootstrap": "0.12.1", 70 | "moment": "~2.10.3", 71 | "handsontable": "~0.18.0", 72 | "ngHandsontable": "0.7.0-beta2" 73 | }, 74 | "appPath": "src", 75 | "overrides": { 76 | "css-layout": { 77 | "main": "dist/css-layout.js" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /dist/assets/i18n/en_GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEAD_APP_TITLE": "Axis » streamlined chart creation tool", 3 | "HEAD_META_DESCRIPTION": "Axis » streamlined chart creation tool by The Times", 4 | "PANEL_HEADING_DATA_INPUT": "Data input", 5 | "INPUT_MALFORMED_CSV_ERROR": "Seems like you've entered some malformed CSV data...", 6 | "FINANCIAL_INPUT_START_DATE_LABEL": "Start date", 7 | "FINANCIAL_INPUT_END_DATE_LABEL": "End date", 8 | "FINANCIAL_INPUT_SYMBOL_LABEL": "Symbol(s)", 9 | "FEED_INPUT_START_DATE_LABEL": "Start date", 10 | "FEED_INPUT_END_DATE_LABEL": "End date", 11 | 12 | "PANEL_HEADING_DATA_OPTIONS": "Data options", 13 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_SERIES": "series", 14 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_PIE": "pie", 15 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_DONUT": "donut", 16 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_GAUGE": "gauge", 17 | "DATA_OPTIONS_DATUM_TYPE_SELECT_LINE": "line", 18 | "DATA_OPTIONS_DATUM_TYPE_SELECT_STEP": "step", 19 | "DATA_OPTIONS_DATUM_TYPE_SELECT_AREA": "area", 20 | "DATA_OPTIONS_DATUM_TYPE_SELECT_AREA-STEP": "area-step", 21 | "DATA_OPTIONS_DATUM_TYPE_SELECT_SCATTER": "scatter", 22 | "DATA_OPTIONS_DATUM_TYPE_SELECT_BAR": "bar", 23 | "DATA_OPTIONS_DATUM_TYPE_SELECT_SPLINE": "spline", 24 | 25 | "PANEL_HEADING_AXES": "Axes", 26 | "AXES_SECTION_HEADING": "{{axisPosition}} Axis", 27 | "AXES_COLUMN_SELECT_LABEL": "Ordinal?", 28 | "AXES_LABEL_FIELD_LABEL": "Label", 29 | "AXES_PREFIX_FIELD_LABEL": "Prefix", 30 | "AXES_SUFFIX_FIELD_LABEL": "Suffix", 31 | "AXES_MIN_FIELD_LABEL": "Min.", 32 | "AXES_MAX_FIELD_LABEL": "Max.", 33 | "AXES_ACCURACY_SELECT_LABEL": "Accuracy", 34 | "AXES_ACCURACY_SELECT_OPTION_TEXT": "{{accuracy}} decimal places", 35 | "AXES_CULLING_FIELD_LABEL": "Culling", 36 | "AXES_COUNT_FIELD_LABEL": "Count", 37 | "AXES_TIMESERIES_FIELD_LABEL": "Timeseries?", 38 | "AXES_DATETIME_FORMAT_FIELD_LABEL": "Datetime format", 39 | "AXES_INVERT": "Invert", 40 | 41 | "PANEL_HEADING_CHART_OPTIONS": "Design options", 42 | "CHART_OPTIONS_PRESETS_LABEL": "Presets", 43 | "CHART_OPTIONS_LEGEND_SELECT_LABEL": "Legend", 44 | "CHART_OPTIONS_LEGEND_SELECT_OPTION_BOTTOM": "bottom", 45 | "CHART_OPTIONS_LEGEND_SELECT_OPTION_RIGHT": "right", 46 | "CHART_OPTIONS_TITLE_LABEL": "Title", 47 | "CHART_OPTIONS_CREDIT_LABEL": "Credit", 48 | "CHART_OPTIONS_CREDIT_FIELD_PLACEHOLDER": "Your Name/The Times", 49 | "CHART_OPTIONS_SOURCE_LABEL": "Source", 50 | "CHART_OPTIONS_SOURCE_FIELD_PLACEHOLDER": "YouGov", 51 | "CHART_OPTIONS_TITLES_POSITION": "Title position", 52 | "CHART_OPTIONS_TITLES_POSITION_SELECT_OPTION_TOP_LEFT": "top-left", 53 | "CHART_OPTIONS_TITLES_POSITION_SELECT_OPTION_TOP_CENTER": "top-centre", 54 | "CHART_OPTIONS_TITLES_POSITION_SELECT_OPTION_TOP_RIGHT": "top-right", 55 | "CHART_OPTIONS_WIDTH_LABEL": "Width", 56 | "CHART_OPTIONS_HEIGHT_LABEL": "Height", 57 | "CHART_OPTIONS_TRANSITION_LABEL": "Transition", 58 | "CHART_OPTIONS_SELECT_LABEL": "Accuracy", 59 | "CHART_OPTIONS_SELECT_OPTION_TEXT": "{{accuracy}} decimal places", 60 | "CHART_OPTIONS_ZERO_BASED": "Zero-based areas", 61 | "CHART_OPTIONS_ROTATED_LABEL": "Rotated", 62 | "CHART_OPTIONS_BACKGROUND_LABEL": "Background", 63 | "CHART_OPTIONS_ZOOMABLE_LABEL": "Zoomable", 64 | "CHART_OPTIONS_SUBCHART_LABEL": "Subchart", 65 | "CHART_OPTIONS_INTERACTION_LABEL": "Interaction", 66 | "CHART_OPTIONS_X_GRID_LABEL": "X Grid", 67 | "CHART_OPTIONS_Y_GRID_LABEL": "Y Grid", 68 | "CHART_OPTIONS_DATA_LABELS_LABEL": "Datum labels", 69 | 70 | "PANEL_HEADING_OUTPUT": "Output", 71 | "OUTPUT_EXPORT_BUTTON_LABEL": "Export to...", 72 | "OUTPUT_SAVE_BUTTON_LABEL": "Save image...", 73 | 74 | "CREDIT_TEXT": "A project by Ændrew Rininsland, Times Digital Development | issues | GitHub", 75 | 76 | "POPOVER_TEXT_CHART_GLOBAL_TYPE": "Choose the main chart style here", 77 | "POPOVER_TEXT_DATUM_TYPE": "Select the style of this column from the following options", 78 | "POPOVER_TEXT_LEGEND_CHECKBOX": "Turn the chart legend on or off with this checkbox", 79 | "POPOVER_TEXT_ROTATED_CHECKBOX": "Rotate the chart 90 degrees", 80 | "POPOVER_TEXT_BACKGROUND_CHECKBOX": "Adds a solid background to image outputs", 81 | "POPOVER_TEXT_COLUMN_SELECT": "Choose a column here to make it a ordinal/categorical axis", 82 | "POPOVER_TEXT_ACCURACY_CHECKBOX": "Toggle grouping zeroes with commas", 83 | "POPOVER_TEXT_CULLING_FIELD": "Set the number of axis labels", 84 | "POPOVER_TEXT_COUNT_FIELD": "Set the number of axis ticks", 85 | "POPOVER_TEXT_ZOOMABLE_CHECKBOX": "Check to make chart zoomable", 86 | "POPOVER_TEXT_SUBCHART_CHECKBOX": "Check to add a subchart" 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /dist/assets/images/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/angular.png -------------------------------------------------------------------------------- /dist/assets/images/bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/bootstrap.png -------------------------------------------------------------------------------- /dist/assets/images/browsersync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/browsersync.png -------------------------------------------------------------------------------- /dist/assets/images/gulp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/gulp.png -------------------------------------------------------------------------------- /dist/assets/images/jasmine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/jasmine.png -------------------------------------------------------------------------------- /dist/assets/images/karma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/karma.png -------------------------------------------------------------------------------- /dist/assets/images/node-sass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/node-sass.png -------------------------------------------------------------------------------- /dist/assets/images/protractor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/protractor.png -------------------------------------------------------------------------------- /dist/assets/images/ui-bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/ui-bootstrap.png -------------------------------------------------------------------------------- /dist/assets/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/assets/images/yeoman.png -------------------------------------------------------------------------------- /dist/bower_components/c3/c3.css: -------------------------------------------------------------------------------- 1 | /*-- Chart --*/ 2 | .c3 svg { 3 | font: 10px sans-serif; 4 | -webkit-tap-highlight-color: transparent; } 5 | 6 | .c3 path, .c3 line { 7 | fill: none; 8 | stroke: #000; } 9 | 10 | .c3 text { 11 | -webkit-user-select: none; 12 | -moz-user-select: none; 13 | user-select: none; } 14 | 15 | .c3-legend-item-tile, .c3-xgrid-focus, .c3-ygrid, .c3-event-rect, .c3-bars path { 16 | shape-rendering: crispEdges; } 17 | 18 | .c3-chart-arc path { 19 | stroke: #fff; } 20 | 21 | .c3-chart-arc text { 22 | fill: #fff; 23 | font-size: 13px; } 24 | 25 | /*-- Axis --*/ 26 | /*-- Grid --*/ 27 | .c3-grid line { 28 | stroke: #aaa; } 29 | 30 | .c3-grid text { 31 | fill: #aaa; } 32 | 33 | .c3-xgrid, .c3-ygrid { 34 | stroke-dasharray: 3 3; } 35 | 36 | /*-- Text on Chart --*/ 37 | .c3-text.c3-empty { 38 | fill: #808080; 39 | font-size: 2em; } 40 | 41 | /*-- Line --*/ 42 | .c3-line { 43 | stroke-width: 1px; } 44 | 45 | /*-- Point --*/ 46 | .c3-circle._expanded_ { 47 | stroke-width: 1px; 48 | stroke: white; } 49 | 50 | .c3-selected-circle { 51 | fill: white; 52 | stroke-width: 2px; } 53 | 54 | /*-- Bar --*/ 55 | .c3-bar { 56 | stroke-width: 0; } 57 | 58 | .c3-bar._expanded_ { 59 | fill-opacity: 0.75; } 60 | 61 | /*-- Focus --*/ 62 | .c3-target.c3-focused { 63 | opacity: 1; } 64 | 65 | .c3-target.c3-focused path.c3-line, .c3-target.c3-focused path.c3-step { 66 | stroke-width: 2px; } 67 | 68 | .c3-target.c3-defocused { 69 | opacity: 0.3 !important; } 70 | 71 | /*-- Region --*/ 72 | .c3-region { 73 | fill: steelblue; 74 | fill-opacity: .1; } 75 | 76 | /*-- Brush --*/ 77 | .c3-brush .extent { 78 | fill-opacity: .1; } 79 | 80 | /*-- Select - Drag --*/ 81 | /*-- Legend --*/ 82 | .c3-legend-item { 83 | font-size: 12px; } 84 | 85 | .c3-legend-item-hidden { 86 | opacity: 0.15; } 87 | 88 | .c3-legend-background { 89 | opacity: 0.75; 90 | fill: white; 91 | stroke: lightgray; 92 | stroke-width: 1; } 93 | 94 | /*-- Title --*/ 95 | .c3-title { 96 | font: 14px sans-serif; } 97 | 98 | .c3-title-author { 99 | font-weight: bold; } 100 | 101 | .c3-title-source { 102 | font-style: italic; } 103 | 104 | /*-- Tooltip --*/ 105 | .c3-tooltip-container { 106 | z-index: 10; } 107 | 108 | .c3-tooltip { 109 | border-collapse: collapse; 110 | border-spacing: 0; 111 | background-color: #fff; 112 | empty-cells: show; 113 | -webkit-box-shadow: 7px 7px 12px -9px #777777; 114 | -moz-box-shadow: 7px 7px 12px -9px #777777; 115 | box-shadow: 7px 7px 12px -9px #777777; 116 | opacity: 0.9; } 117 | 118 | .c3-tooltip tr { 119 | border: 1px solid #CCC; } 120 | 121 | .c3-tooltip th { 122 | background-color: #aaa; 123 | font-size: 14px; 124 | padding: 2px 5px; 125 | text-align: left; 126 | color: #FFF; } 127 | 128 | .c3-tooltip td { 129 | font-size: 13px; 130 | padding: 3px 6px; 131 | background-color: #fff; 132 | border-left: 1px dotted #999; } 133 | 134 | .c3-tooltip td > span { 135 | display: inline-block; 136 | width: 10px; 137 | height: 10px; 138 | margin-right: 6px; } 139 | 140 | .c3-tooltip td.value { 141 | text-align: right; } 142 | 143 | /*-- Area --*/ 144 | .c3-area { 145 | stroke-width: 0; 146 | opacity: 0.2; } 147 | 148 | /*-- Arc --*/ 149 | .c3-chart-arcs-title { 150 | dominant-baseline: middle; 151 | font-size: 1.3em; } 152 | 153 | .c3-chart-arcs .c3-chart-arcs-background { 154 | fill: #e0e0e0; 155 | stroke: none; } 156 | 157 | .c3-chart-arcs .c3-chart-arcs-gauge-unit { 158 | fill: #000; 159 | font-size: 16px; } 160 | 161 | .c3-chart-arcs .c3-chart-arcs-gauge-max { 162 | fill: #777; } 163 | 164 | .c3-chart-arcs .c3-chart-arcs-gauge-min { 165 | fill: #777; } 166 | 167 | .c3-chart-arc .c3-gauge-value { 168 | fill: #000; 169 | /* font-size: 28px !important;*/ } 170 | -------------------------------------------------------------------------------- /dist/config.yaml: -------------------------------------------------------------------------------- 1 | stylesheet: 'themes/default.css' 2 | -------------------------------------------------------------------------------- /dist/default.config.yaml: -------------------------------------------------------------------------------- 1 | # axisJS default config 2 | --- 3 | renderer: 'c3' 4 | 5 | input: 6 | - 'spreadsheet' 7 | - 'csv' 8 | # - 'financial' # Disabled until financial data becomes more open. :( 9 | 10 | export: 11 | - 'Embed code' 12 | - 'pdf' 13 | 14 | save: 15 | - 'png' 16 | - 'svg' 17 | 18 | colors: 19 | - label: 'neutral 1' 20 | value: '#78B8DF' 21 | - label: 'neutral 2' 22 | value: '#AFCBCE' 23 | - label: 'neutral 3' 24 | value: '#23393D' 25 | - label: 'neutral 4' 26 | value: '#87AF9C' 27 | - label: 'yes' 28 | value: '#0EBF00' 29 | - label: 'no' 30 | value: '#ED1B24' 31 | - label: 'maybe' 32 | value: '#808080' 33 | - label: 'Labour' 34 | value: '#ED1B24' 35 | - label: 'Conservative' 36 | value: '#022397' 37 | - label: 'LibDem' 38 | value: '#FDBB30' 39 | - label: 'Ukip' 40 | value: '#722889' 41 | - label: 'Green' 42 | value: '#6AB023' 43 | - label: '#1f77b4' # begin c3.js defaults 44 | value: '#1f77b4' 45 | - label: '#aec7e8' 46 | value: '#aec7e8' 47 | - label: '#ff7f0e' 48 | value: '#ff7f0e' 49 | - label: '#ffbb78' 50 | value: '#ffbb78' 51 | - label: '#2ca02c' 52 | value: '#2ca02c' 53 | - label: '#98df8a' 54 | value: '#98df8a' 55 | - label: '#d62728' 56 | value: '#d62728' 57 | - label: '#ff9896' 58 | value: '#ff9896' 59 | - label: '#9467bd' 60 | value: '#9467bd' 61 | - label: '#c5b0d5' 62 | value: '#c5b0d5' 63 | - label: '#8c564b' 64 | value: '#8c564b' 65 | - label: '#c49c94' 66 | value: '#c49c94' 67 | - label: '#e377c2' 68 | value: '#e377c2' 69 | - label: '#f7b6d2' 70 | value: '#f7b6d2' 71 | - label: '#7f7f7f' 72 | value: '#7f7f7f' 73 | - label: '#c7c7c7' 74 | value: '#c7c7c7' 75 | - label: '#bcbd22' 76 | value: '#bcbd22' 77 | - label: '#dbdb8d' 78 | value: '#dbdb8d' 79 | - label: '#17becf' 80 | value: '#17becf' 81 | - label: '#9edae5' 82 | value: '#9edae5' 83 | 84 | background colors: 85 | - label: 'white' 86 | value: '#ffffff' 87 | 88 | defaults: 89 | grid x: false 90 | grid y: false 91 | axis y: true 92 | axis y2: false 93 | axis x: true 94 | legend: true 95 | point: false 96 | y axis: 'y' 97 | axis x padding left: 98 | axis x padding right: 99 | axis y padding top: 100 | axis y padding bottom: 101 | padding left: 25 102 | padding right: 25 103 | padding top: 25 104 | padding bottom: 25 105 | title text: 106 | title author: 107 | title source: 108 | title position: top-left 109 | charts: 110 | series: 111 | grid: 112 | y: 113 | show: true 114 | x: 115 | show: true 116 | pie: 117 | grid: 118 | y: 119 | show: false 120 | x: 121 | show: false 122 | donut: 123 | y: 124 | show: false 125 | x: 126 | show: false 127 | gauge: 128 | y: 129 | show: false 130 | x: 131 | show: false 132 | 133 | backgroundColor: 'white' 134 | 135 | colorPicker: 'simple' 136 | 137 | financialService: 'yahoo' 138 | 139 | themes: 140 | - name: 'Default (Red Box)' 141 | file: 'config.yaml' 142 | - name: 'The Times' 143 | file: 'themes/thetimes.config.yaml' 144 | - name: 'Sunday Times' 145 | file: 'themes/sundaytimes.config.yaml' 146 | -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/dist/favicon.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | Axis » streamlined chart creation tool
-------------------------------------------------------------------------------- /dist/themes/axismaker.config.yaml: -------------------------------------------------------------------------------- 1 | # Axis Server default config 2 | --- 3 | renderer: 'c3' 4 | 5 | input: 6 | - 'spreadsheet' 7 | - 'csv' 8 | # - 'financial' # Disabled until financial data becomes more open. :( 9 | 10 | export: 11 | - 'axis' 12 | - 'Embed code' 13 | 14 | save: 15 | - 'png' 16 | - 'svg' 17 | 18 | colors: 19 | - label: 'neutral 1' 20 | value: '#78B8DF' 21 | - label: 'neutral 2' 22 | value: '#AFCBCE' 23 | - label: 'neutral 3' 24 | value: '#23393D' 25 | - label: 'neutral 4' 26 | value: '#87AF9C' 27 | - label: 'yes' 28 | value: '#0EBF00' 29 | - label: 'no' 30 | value: '#ED1B24' 31 | - label: 'maybe' 32 | value: '#808080' 33 | - label: 'Labour' 34 | value: '#ED1B24' 35 | - label: 'Conservative' 36 | value: '#022397' 37 | - label: 'LibDem' 38 | value: '#FDBB30' 39 | - label: 'Ukip' 40 | value: '#722889' 41 | - label: 'Green' 42 | value: '#6AB023' 43 | - label: '#1f77b4' # begin c3.js defaults 44 | value: '#1f77b4' 45 | - label: '#aec7e8' 46 | value: '#aec7e8' 47 | - label: '#ff7f0e' 48 | value: '#ff7f0e' 49 | - label: '#ffbb78' 50 | value: '#ffbb78' 51 | - label: '#2ca02c' 52 | value: '#2ca02c' 53 | - label: '#98df8a' 54 | value: '#98df8a' 55 | - label: '#d62728' 56 | value: '#d62728' 57 | - label: '#ff9896' 58 | value: '#ff9896' 59 | - label: '#9467bd' 60 | value: '#9467bd' 61 | - label: '#c5b0d5' 62 | value: '#c5b0d5' 63 | - label: '#8c564b' 64 | value: '#8c564b' 65 | - label: '#c49c94' 66 | value: '#c49c94' 67 | - label: '#e377c2' 68 | value: '#e377c2' 69 | - label: '#f7b6d2' 70 | value: '#f7b6d2' 71 | - label: '#7f7f7f' 72 | value: '#7f7f7f' 73 | - label: '#c7c7c7' 74 | value: '#c7c7c7' 75 | - label: '#bcbd22' 76 | value: '#bcbd22' 77 | - label: '#dbdb8d' 78 | value: '#dbdb8d' 79 | - label: '#17becf' 80 | value: '#17becf' 81 | - label: '#9edae5' 82 | value: '#9edae5' 83 | 84 | background colors: 85 | - label: 'white' 86 | value: '#ffffff' 87 | 88 | defaults: 89 | grid x: false 90 | grid y: false 91 | axis y: true 92 | axis y2: false 93 | axis x: true 94 | legend: true 95 | point: false 96 | y axis: 'y' 97 | title text: 98 | title author: 99 | title source: 100 | title position: top-left 101 | charts: 102 | series: 103 | grid: 104 | y: 105 | show: true 106 | x: 107 | show: true 108 | pie: 109 | grid: 110 | y: 111 | show: false 112 | x: 113 | show: false 114 | donut: 115 | y: 116 | show: false 117 | x: 118 | show: false 119 | gauge: 120 | y: 121 | show: false 122 | x: 123 | show: false 124 | 125 | backgroundColor: 'white' 126 | 127 | colorPicker: 'simple' 128 | 129 | financialService: 'yahoo' 130 | 131 | themes: 132 | - name: 'Default (Red Box)' 133 | file: 'config.yaml' 134 | - name: 'The Times' 135 | file: 'themes/thetimes.config.yaml' 136 | - name: 'Sunday Times' 137 | file: 'themes/sundaytimes.config.yaml' 138 | -------------------------------------------------------------------------------- /dist/themes/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Axis styles, based on Times Red Box. 3 | * 4 | * Working renderers: C3 5 | */ 6 | 7 | 8 | /** C3 **/ 9 | path.c3-line { 10 | stroke-width: 2; 11 | } 12 | -------------------------------------------------------------------------------- /dist/themes/sundaytimes.config.yaml: -------------------------------------------------------------------------------- 1 | # Sunday Times axisJS configuration 2 | --- 3 | renderer: 'c3' 4 | 5 | export: 6 | - 'Embed code' 7 | - 'pdf' 8 | 9 | output: 10 | - 'png' 11 | - 'svg' 12 | 13 | colors: 14 | - label: 'Appointments' 15 | value: '#A8C6D6' 16 | - label: 'Business' 17 | value: '#0076A3' 18 | - label: 'Culture' 19 | value: '#C2DBE8' 20 | - label: 'Comment' 21 | value: '#6398B0' 22 | - label: 'Driving' 23 | value: '#F58220' 24 | - label: 'Focus' 25 | value: '#DA2128' 26 | - label: 'Home' 27 | value: '#00A88E' 28 | - label: 'Money' 29 | value: '#00ACCD' 30 | - label: 'News' 31 | value: '#286FB7' 32 | - label: 'News Review' 33 | value: '#0083CA' 34 | - label: 'Sport' 35 | value: '#39B54A' 36 | - label: 'Sport Dark Blue' 37 | value: '#2B4C5A' 38 | - label: 'Sunday' 39 | value: '#005B7F' 40 | - label: 'Travel' 41 | value: '#00AEEF' 42 | 43 | stylesheet: 'themes/sundaytimes.css' 44 | 45 | screen margins: # in pixels 46 | top: '12px' 47 | bottom: '12px' 48 | left: '12px' 49 | right: '12px' 50 | 51 | print margins: # in points 52 | top: '5' 53 | bottom: '5' 54 | left: '5' 55 | right: '5' 56 | 57 | defaults: 58 | grid x: false 59 | grid y: true 60 | axis y: false 61 | axis y2: true 62 | axis x: true 63 | legend: false 64 | legend position: 'bottom' 65 | point: false 66 | y axis: 'y2' 67 | 68 | fonts: 69 | - 'http://www.thetimes.co.uk/fonts/Solido-Bold.css' 70 | - 'http://www.thetimes.co.uk/fonts/Solido-ExtraBold.css' 71 | - 'http://www.thetimes.co.uk/fonts/Solido-Book-Italic.css' 72 | 73 | # This is to prevent Adobe Illustrator from borking due to non-standard font names. 74 | # Format — 'webfont name': 'local font name' 75 | font replacements: 76 | 'Solido-Bold': 'SOLIDO-BOLD' 77 | 'Solido-ExtraBold': 'SOLIDO-EXTRABOLD' 78 | 'Solido-Book-Italic': 'SOLIDO-BOOKITALIC' 79 | 80 | colorPicker: 'simple' 81 | -------------------------------------------------------------------------------- /dist/themes/sundaytimes.css: -------------------------------------------------------------------------------- 1 | /** Sunday Times styles **/ 2 | 3 | /** C3 **/ 4 | g.c3-axis-y2 g.tick > line { 5 | display: none; 6 | } 7 | 8 | g.c3-axis-y2 g.tick > text { 9 | font-family: 'Solido-ExtraBold', Times, serif; 10 | } 11 | 12 | g.c3-axis-x g.tick > text { 13 | font-family: 'Solido-Book-Italic', Times, serif; 14 | } 15 | 16 | g.c3-axis-y2 .domain { 17 | display: none; 18 | } 19 | 20 | g.c3-grid g.c3-ygrids line { 21 | stroke-dasharray: 1, 1; 22 | } 23 | 24 | .chartTitle { 25 | font-family: 'Solido-ExtraBold', Times, serif; 26 | } 27 | 28 | .chartSource { 29 | font-family: 'Solido-Book-Italic', Times, serif; 30 | } 31 | -------------------------------------------------------------------------------- /dist/themes/thetimes.config.yaml: -------------------------------------------------------------------------------- 1 | # The Times axisJS configuration 2 | --- 3 | renderer: 'c3' 4 | 5 | export: 6 | - 'Embed code' 7 | - 'pdf' 8 | 9 | output: 10 | - 'png' 11 | - 'svg' 12 | 13 | colors: 14 | - label: 'Appointments' 15 | value: '#A8C6D6' 16 | - label: 'Business' 17 | value: '#0076A3' 18 | - label: 'Culture' 19 | value: '#C2DBE8' 20 | - label: 'Comment' 21 | value: '#6398B0' 22 | - label: 'Driving' 23 | value: '#F58220' 24 | - label: 'Focus' 25 | value: '#DA2128' 26 | - label: 'Home' 27 | value: '#00A88E' 28 | - label: 'Money' 29 | value: '#00ACCD' 30 | - label: 'News' 31 | value: '#286FB7' 32 | - label: 'News Review' 33 | value: '#0083CA' 34 | - label: 'Sport' 35 | value: '#39B54A' 36 | - label: 'Sport Dark Blue' 37 | value: '#2B4C5A' 38 | - label: 'Sunday' 39 | value: '#005B7F' 40 | - label: 'Travel' 41 | value: '#00AEEF' 42 | 43 | background colors: 44 | - label: 'normal' 45 | value: '#F3F2E5' 46 | - label: 'tempus' 47 | value: '#E4EEEF' 48 | - label: 'white' 49 | value: '#ffffff' 50 | 51 | stylesheet: 'themes/thetimes.css' 52 | 53 | screen margins: # in pixels 54 | top: '12px' 55 | bottom: '12px' 56 | left: '12px' 57 | right: '12px' 58 | 59 | print margins: # in points 60 | top: '5' 61 | bottom: '5' 62 | left: '5' 63 | right: '5' 64 | 65 | defaults: 66 | grid x: true 67 | grid y: true 68 | axis y: false 69 | axis y2: true 70 | axis x: true 71 | legend show: false 72 | legend position: 'bottom' 73 | point: false 74 | y axis: 'y2' 75 | series type: 'area' 76 | padding left: 50 77 | padding right: 50 78 | padding top: 100 79 | padding bottom: 10 80 | 81 | 82 | presets: 83 | - label: 'Need To Know' 84 | class: 'need-to-know' 85 | settings: 86 | background: true 87 | backgroundColor: '#F3F2E5' 88 | 89 | - label: 'Graph Of The Day' 90 | class: 'gotd' 91 | settings: 92 | background: true 93 | backgroundColor: '#F3F2E5' 94 | 95 | - label: 'Home News' 96 | class: 'home-news' 97 | settings: 98 | background: true 99 | backgroundColor: '#F3F2E5' 100 | 101 | - label: 'Tempus' 102 | class: 'tempus' 103 | settings: 104 | background: true 105 | backgroundColor: '#E4EEEF' 106 | axis y show: false 107 | axis y2 show: false 108 | axis x show: false 109 | 110 | fonts: 111 | - 'http://www.thetimes.co.uk/fonts/TimesClassicText-Bold-Italic.css' 112 | - 'http://www.thetimes.co.uk/fonts/TimesClassicText-Medium.css' 113 | - 'http://www.thetimes.co.uk/fonts/StagSans-SemiBold.css' 114 | - 'http://www.thetimes.co.uk/fonts/StagSans-Medium.css' 115 | - 'http://www.thetimes.co.uk/fonts/StagSans-Book.css' 116 | 117 | # This is to prevent Adobe Illustrator from borking due to non-standard font names. 118 | # Format — 'webfont name': 'local font name' 119 | font replacements: 120 | 'TimesClassicText-Bold-Italic': 'TIMESCLASSICTEXTBOLDITALIC' 121 | 'TimesClassicText-Medium': 'TIMESCLASSICTEXT-MD' 122 | 'StagSans-SemiBold': 'STAGSANS-SEMIBOLD' 123 | 'StagSans-Medium': 'STAGSANS-MEDIUM' 124 | 'StagSans-Book': 'STAGSANS-BOOK' 125 | 126 | colorPicker: 'simple' 127 | -------------------------------------------------------------------------------- /dist/themes/thetimes.css: -------------------------------------------------------------------------------- 1 | /** The Times styles **/ 2 | 3 | /** C3 **/ 4 | .c3-grid line { 5 | stroke: #ddd !important; 6 | stroke-dasharray: 1, 1; 7 | } 8 | 9 | path.c3-line { 10 | stroke: #bf1f10 !important; 11 | stroke-width: 2; 12 | } 13 | 14 | path.c3-area { 15 | fill: #cfdfef 16 | } 17 | 18 | .c3-axis .tick text { 19 | font-family: 'StagSans-Book', sans-serif; 20 | } 21 | 22 | #chart.home-news .chart-bg, #chart.need-to-know .chart-bg, #chart.gotd .chart-bg { 23 | fill: #F3F2E5; 24 | } 25 | 26 | #chart.tempus .chart-bg { 27 | fill: #E4EEEF; 28 | } 29 | 30 | .gotd path.c3-line { 31 | stroke: #004668 !important; 32 | stroke-width: 2; 33 | } 34 | 35 | .need-to-know .c3-axis-x .tick text { 36 | font-family: 'StagSans-Medium', sans-serif; 37 | } 38 | 39 | .need-to-know .c3-axis-y2 .tick text, .need-to-know .c3-axis-y .tick text { 40 | font-family: 'StagSans-Book', sans-serif; 41 | } 42 | 43 | .gotd .c3-axis-x .tick text { 44 | font-family: 'StagSans-Medium', sans-serif; 45 | } 46 | 47 | .gotd .c3-axis-y2 .tick text, .gotd .c3-axis-y .tick text { 48 | font-family: 'StagSans-Book', sans-serif; 49 | } 50 | 51 | .tempus .c3-axis-x .tick text { 52 | font-family: 'StagSans-Book', sans-serif; 53 | } 54 | 55 | .tempus .c3-axis-y2 .tick text, .tempus .c3-axis-y .tick text { 56 | font-family: 'StagSans-Medium', sans-serif; 57 | } 58 | 59 | .home-news .c3-axis-x .tick text { 60 | font-family: 'StagSans-Book', sans-serif; 61 | } 62 | 63 | .home-news .c3-axis-y2 .tick text, .home-news .c3-axis-y .tick text { 64 | font-family: 'StagSans-Medium', sans-serif; 65 | } 66 | -------------------------------------------------------------------------------- /dist/themes/wordpress.config.yaml: -------------------------------------------------------------------------------- 1 | # Axis WordPress default config 2 | --- 3 | renderer: 'c3' 4 | 5 | input: 6 | - 'spreadsheet' 7 | - 'csv' 8 | # - 'financial' # Disabled until financial data becomes more open. :( 9 | 10 | export: 11 | - 'wordpress' 12 | 13 | save: 14 | - 'png' 15 | - 'svg' 16 | 17 | colors: 18 | - label: 'neutral 1' 19 | value: '#78B8DF' 20 | - label: 'neutral 2' 21 | value: '#AFCBCE' 22 | - label: 'neutral 3' 23 | value: '#23393D' 24 | - label: 'neutral 4' 25 | value: '#87AF9C' 26 | - label: 'yes' 27 | value: '#0EBF00' 28 | - label: 'no' 29 | value: '#ED1B24' 30 | - label: 'maybe' 31 | value: '#808080' 32 | - label: 'Labour' 33 | value: '#ED1B24' 34 | - label: 'Conservative' 35 | value: '#022397' 36 | - label: 'LibDem' 37 | value: '#FDBB30' 38 | - label: 'Ukip' 39 | value: '#722889' 40 | - label: 'Green' 41 | value: '#6AB023' 42 | - label: '#1f77b4' # begin c3.js defaults 43 | value: '#1f77b4' 44 | - label: '#aec7e8' 45 | value: '#aec7e8' 46 | - label: '#ff7f0e' 47 | value: '#ff7f0e' 48 | - label: '#ffbb78' 49 | value: '#ffbb78' 50 | - label: '#2ca02c' 51 | value: '#2ca02c' 52 | - label: '#98df8a' 53 | value: '#98df8a' 54 | - label: '#d62728' 55 | value: '#d62728' 56 | - label: '#ff9896' 57 | value: '#ff9896' 58 | - label: '#9467bd' 59 | value: '#9467bd' 60 | - label: '#c5b0d5' 61 | value: '#c5b0d5' 62 | - label: '#8c564b' 63 | value: '#8c564b' 64 | - label: '#c49c94' 65 | value: '#c49c94' 66 | - label: '#e377c2' 67 | value: '#e377c2' 68 | - label: '#f7b6d2' 69 | value: '#f7b6d2' 70 | - label: '#7f7f7f' 71 | value: '#7f7f7f' 72 | - label: '#c7c7c7' 73 | value: '#c7c7c7' 74 | - label: '#bcbd22' 75 | value: '#bcbd22' 76 | - label: '#dbdb8d' 77 | value: '#dbdb8d' 78 | - label: '#17becf' 79 | value: '#17becf' 80 | - label: '#9edae5' 81 | value: '#9edae5' 82 | 83 | background colors: 84 | - label: 'white' 85 | value: '#ffffff' 86 | 87 | defaults: 88 | grid x: false 89 | grid y: false 90 | axis y: true 91 | axis y2: false 92 | axis x: true 93 | legend: true 94 | point: false 95 | y axis: 'y' 96 | title text: 97 | title author: 98 | title source: 99 | title position: top-left 100 | charts: 101 | series: 102 | grid: 103 | y: 104 | show: true 105 | x: 106 | show: true 107 | pie: 108 | grid: 109 | y: 110 | show: false 111 | x: 112 | show: false 113 | donut: 114 | y: 115 | show: false 116 | x: 117 | show: false 118 | gauge: 119 | y: 120 | show: false 121 | x: 122 | show: false 123 | 124 | backgroundColor: 'white' 125 | 126 | colorPicker: 'simple' 127 | 128 | financialService: 'yahoo' 129 | 130 | themes: 131 | - name: 'Default (Red Box)' 132 | file: 'config.yaml' 133 | - name: 'The Times' 134 | file: 'themes/thetimes.config.yaml' 135 | - name: 'Sunday Times' 136 | file: 'themes/sundaytimes.config.yaml' 137 | -------------------------------------------------------------------------------- /e2e/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "globals": { 4 | "browser": false, 5 | "element": false, 6 | "by": false, 7 | "$": false, 8 | "$$": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /e2e/main.po.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file uses the Page Object pattern to define the main page for tests 3 | * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var MainPage = function() { 9 | this.jumbEl = element(by.css('.jumbotron')); 10 | this.h1El = this.jumbEl.element(by.css('h1')); 11 | this.imgEl = this.jumbEl.element(by.css('img')); 12 | this.thumbnailEls = element(by.css('body')).all(by.repeater('awesomeThing in main.awesomeThings')); 13 | }; 14 | 15 | module.exports = new MainPage(); 16 | -------------------------------------------------------------------------------- /e2e/main.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('The main view', function () { 4 | var page; 5 | 6 | beforeEach(function () { 7 | browser.get('/index.html'); 8 | page = require('./main.po'); 9 | }); 10 | 11 | it('should include jumbotron with correct data', function() { 12 | expect(page.h1El.getText()).toBe('\'Allo, \'Allo!'); 13 | expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/); 14 | expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman'); 15 | }); 16 | 17 | it('should list more than 5 awesome things', function () { 18 | expect(page.thumbnailEls.count()).toBeGreaterThan(5); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /gulp/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "node": true 4 | } 5 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | var runSequence = require('run-sequence'); 7 | 8 | var $ = require('gulp-load-plugins')({ 9 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 10 | }); 11 | 12 | gulp.task('partials', function () { 13 | return gulp.src([ 14 | path.join(conf.paths.src, '/app/**/*.html'), 15 | path.join(conf.paths.tmp, '/serve/app/**/*.html') 16 | ]) 17 | .pipe($.minifyHtml({ 18 | empty: true, 19 | spare: true, 20 | quotes: true 21 | })) 22 | .pipe($.angularTemplatecache('templateCacheHtml.js', { 23 | module: 'axis', 24 | root: 'app' 25 | })) 26 | .pipe(gulp.dest(conf.paths.tmp + '/partials/')); 27 | }); 28 | 29 | gulp.task('html', ['inject', 'partials'], function () { 30 | var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), { read: false }); 31 | var partialsInjectOptions = { 32 | starttag: '', 33 | ignorePath: path.join(conf.paths.tmp, '/partials'), 34 | addRootSlash: false 35 | }; 36 | 37 | var htmlFilter = $.filter('*.html'); 38 | var jsFilter = $.filter('**/*.js'); 39 | var cssFilter = $.filter('**/*.css'); 40 | var assets; 41 | 42 | return gulp.src(path.join(conf.paths.tmp, '/serve/*.html')) 43 | .pipe($.inject(partialsInjectFile, partialsInjectOptions)) 44 | .pipe(assets = $.useref.assets()) 45 | .pipe($.rev()) 46 | .pipe(jsFilter) 47 | .pipe($.ngAnnotate()) 48 | // .pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify')) 49 | .pipe(jsFilter.restore()) 50 | .pipe(cssFilter) 51 | .pipe($.replace('../../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/', '../fonts/')) 52 | .pipe($.csso()) 53 | .pipe(cssFilter.restore()) 54 | .pipe(assets.restore()) 55 | .pipe($.useref()) 56 | .pipe($.revReplace()) 57 | .pipe(htmlFilter) 58 | .pipe($.minifyHtml({ 59 | empty: true, 60 | spare: true, 61 | quotes: true, 62 | conditionals: true 63 | })) 64 | .pipe(htmlFilter.restore()) 65 | .pipe(gulp.dest(path.join(conf.paths.dist, '/'))) 66 | .pipe($.size({ title: path.join(conf.paths.dist, '/'), showFiles: true })); 67 | }); 68 | 69 | // Only applies for fonts from bower dependencies 70 | // Custom fonts are handled by the "other" task 71 | gulp.task('fonts', function () { 72 | return gulp.src($.mainBowerFiles()) 73 | .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}')) 74 | .pipe($.flatten()) 75 | .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/'))); 76 | }); 77 | 78 | gulp.task('extraCSS', function(){ 79 | var c3CSS = function() { 80 | return gulp.src([ 81 | 'bower_components/c3/c3.css' 82 | ]).pipe(gulp.dest(path.join(conf.paths.dist, '/bower_components/c3'))); 83 | }; 84 | 85 | var themeCSS = function() { 86 | return gulp.src([ 87 | 'src/themes/*.css' 88 | ]).pipe(gulp.dest(path.join(conf.paths.dist, '/themes'))); 89 | }; 90 | 91 | return c3CSS().on('end', themeCSS); 92 | }); 93 | 94 | gulp.task('other', function () { 95 | var fileFilter = $.filter(function (file) { 96 | return file.stat.isFile(); 97 | }); 98 | 99 | return gulp.src([ 100 | path.join(conf.paths.src, '/**/*'), 101 | path.join('!' + conf.paths.src, '/**/*.{html,css,js,scss}') 102 | ]) 103 | .pipe(fileFilter) 104 | .pipe(gulp.dest(path.join(conf.paths.dist, '/'))); 105 | }); 106 | 107 | gulp.task('clean', function (done) { 108 | $.del([path.join(conf.paths.dist, '/'), path.join(conf.paths.tmp, '/')], done); 109 | }); 110 | 111 | gulp.task('build', function(done){ 112 | runSequence('clean', ['html', 'fonts', 'extraCSS', 'other'], done); 113 | }); 114 | -------------------------------------------------------------------------------- /gulp/conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the variables used in other gulp files 3 | * which defines tasks 4 | * By design, we only put there very generic config values 5 | * which are used in several places to keep good readability 6 | * of the tasks 7 | */ 8 | 9 | var gutil = require('gulp-util'); 10 | 11 | /** 12 | * The main paths of your project handle these with care 13 | */ 14 | exports.paths = { 15 | src: 'src', 16 | dist: 'dist', 17 | tmp: '.tmp', 18 | e2e: 'e2e' 19 | }; 20 | 21 | /** 22 | * Wiredep is the lib which inject bower dependencies in your project 23 | * Mainly used to inject script tags in the index.html but also used 24 | * to inject css preprocessor deps and js files in karma 25 | */ 26 | exports.wiredep = { 27 | devDependencies: true, 28 | exclude: [ 29 | /bootstrap\.js$/, 30 | /bootstrap\.css/ 31 | ], 32 | directory: 'bower_components', 33 | overrides: { 34 | jspdf: { 35 | main: 'dist/jspdf.debug.js' 36 | }, 37 | 'bootstrap-colorselector': { 38 | main: [ 39 | 'lib/bootstrap-colorselector-0.2.0/css/bootstrap-colorselector.css', 40 | 'lib/bootstrap-colorselector-0.2.0/js/bootstrap-colorselector.js' 41 | ] 42 | }, 43 | 'bootstrap-sass-official': { 44 | main: [ 45 | 'assets/javascripts/bootstrap/dropdown.js', //override needed for bootstrap-colorselector. 46 | ] 47 | }, 48 | } 49 | }; 50 | 51 | /** 52 | * Common implementation for an error handler of a Gulp plugin 53 | */ 54 | exports.errorHandler = function(title) { 55 | 'use strict'; 56 | 57 | return function(err) { 58 | gutil.log(gutil.colors.red('[' + title + ']'), err.toString()); 59 | this.emit('end'); 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /gulp/e2e-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | // Downloads the selenium webdriver 12 | gulp.task('webdriver-update', $.protractor.webdriver_update); 13 | 14 | gulp.task('webdriver-standalone', $.protractor.webdriver_standalone); 15 | 16 | function runProtractor (done) { 17 | var params = process.argv; 18 | var args = params.length > 3 ? [params[3], params[4]] : []; 19 | 20 | gulp.src(path.join(conf.paths.e2e, '/**/*.js')) 21 | .pipe($.protractor.protractor({ 22 | configFile: 'protractor.conf.js', 23 | args: args 24 | })) 25 | .on('error', function (err) { 26 | // Make sure failed tests cause gulp to exit non-zero 27 | throw err; 28 | }) 29 | .on('end', function () { 30 | // Close browser sync server 31 | browserSync.exit(); 32 | done(); 33 | }); 34 | } 35 | 36 | gulp.task('protractor', ['protractor:src']); 37 | gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor); 38 | gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor); 39 | -------------------------------------------------------------------------------- /gulp/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var $ = require('gulp-load-plugins')(); 8 | 9 | var wiredep = require('wiredep').stream; 10 | var _ = require('lodash'); 11 | 12 | gulp.task('inject', ['scripts', 'styles'], function () { 13 | var injectStyles = gulp.src([ 14 | path.join(conf.paths.tmp, '/serve/app/**/*.css'), 15 | path.join('!' + conf.paths.tmp, '/serve/app/vendor.css') 16 | ], { read: false }); 17 | 18 | var injectScripts = gulp.src([ 19 | path.join(conf.paths.src, '/app/**/*.module.js'), 20 | path.join(conf.paths.src, '/app/**/*.js'), 21 | path.join('!' + conf.paths.src, '/app/**/*.spec.js'), 22 | path.join('!' + conf.paths.src, '/app/**/*.mock.js') 23 | ]) 24 | .pipe($.angularFilesort()).on('error', conf.errorHandler('AngularFilesort')); 25 | 26 | var injectOptions = { 27 | ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], 28 | addRootSlash: false 29 | }; 30 | 31 | return gulp.src(path.join(conf.paths.src, '/*.html')) 32 | .pipe($.inject(injectStyles, injectOptions)) 33 | .pipe($.inject(injectScripts, injectOptions)) 34 | .pipe(wiredep(_.extend({}, conf.wiredep))) 35 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve'))); 36 | }); 37 | -------------------------------------------------------------------------------- /gulp/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | gulp.task('scripts', function () { 12 | return gulp.src(path.join(conf.paths.src, '/app/**/*.js')) 13 | // .pipe($.jshint()) 14 | // .pipe($.jshint.reporter('jshint-stylish')) 15 | .pipe(browserSync.reload({ stream: true })) 16 | // .pipe($.size()); 17 | }); 18 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | var browserSyncSpa = require('browser-sync-spa'); 9 | 10 | var util = require('util'); 11 | 12 | var proxyMiddleware = require('http-proxy-middleware'); 13 | 14 | function browserSyncInit(baseDir, browser) { 15 | browser = browser === undefined ? 'default' : browser; 16 | 17 | var routes = null; 18 | if(baseDir === conf.paths.src || (util.isArray(baseDir) && baseDir.indexOf(conf.paths.src) !== -1)) { 19 | routes = { 20 | '/bower_components': 'bower_components' 21 | }; 22 | } 23 | 24 | var server = { 25 | baseDir: baseDir, 26 | routes: routes 27 | }; 28 | 29 | /* 30 | * You can add a proxy to your backend by uncommenting the line bellow. 31 | * You just have to configure a context which will we redirected and the target url. 32 | * Example: $http.get('/users') requests will be automatically proxified. 33 | * 34 | * For more details and option, https://github.com/chimurai/http-proxy-middleware/blob/v0.0.5/README.md 35 | */ 36 | // server.middleware = proxyMiddleware('/users', {target: 'http://jsonplaceholder.typicode.com', proxyHost: 'jsonplaceholder.typicode.com'}); 37 | 38 | browserSync.instance = browserSync.init({ 39 | startPath: '/', 40 | server: server, 41 | browser: browser 42 | }); 43 | } 44 | 45 | browserSync.use(browserSyncSpa({ 46 | selector: '[ng-app]'// Only needed for angular apps 47 | })); 48 | 49 | gulp.task('serve', ['watch'], function () { 50 | browserSyncInit([path.join(conf.paths.tmp, '/serve'), conf.paths.src]); 51 | }); 52 | 53 | gulp.task('serve:test', ['watch', 'test'], function () { 54 | browserSyncInit([path.join(conf.paths.tmp, '/serve'), conf.paths.src]); 55 | }); 56 | 57 | gulp.task('serve:dist', ['build'], function () { 58 | browserSyncInit(conf.paths.dist); 59 | }); 60 | 61 | gulp.task('serve:e2e', ['inject'], function () { 62 | browserSyncInit([conf.paths.tmp + '/serve', conf.paths.src], []); 63 | }); 64 | 65 | gulp.task('serve:e2e-dist', ['build'], function () { 66 | browserSyncInit(conf.paths.dist, []); 67 | }); 68 | -------------------------------------------------------------------------------- /gulp/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | var wiredep = require('wiredep').stream; 12 | var _ = require('lodash'); 13 | 14 | gulp.task('styles', function () { 15 | var sassOptions = { 16 | style: 'expanded' 17 | }; 18 | 19 | var injectFiles = gulp.src([ 20 | path.join(conf.paths.src, '/app/**/*.scss'), 21 | path.join('!' + conf.paths.src, '/app/index.scss') 22 | ], { read: false }); 23 | 24 | var injectOptions = { 25 | transform: function(filePath) { 26 | filePath = filePath.replace(conf.paths.src + '/app/', ''); 27 | return '@import "' + filePath + '";'; 28 | }, 29 | starttag: '// injector', 30 | endtag: '// endinjector', 31 | addRootSlash: false 32 | }; 33 | 34 | 35 | return gulp.src([ 36 | path.join(conf.paths.src, '/app/index.scss') 37 | ]) 38 | .pipe($.inject(injectFiles, injectOptions)) 39 | .pipe(wiredep(_.extend({}, conf.wiredep))) 40 | .pipe($.sourcemaps.init()) 41 | .pipe($.sass(sassOptions)).on('error', conf.errorHandler('Sass')) 42 | .pipe($.autoprefixer()).on('error', conf.errorHandler('Autoprefixer')) 43 | .pipe($.sourcemaps.write()) 44 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve/app/'))) 45 | .pipe(browserSync.reload({ stream: true })); 46 | }); 47 | -------------------------------------------------------------------------------- /gulp/unit-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var karma = require('karma'); 8 | 9 | function runTests (singleRun, done) { 10 | var reporters, preprocessors; 11 | 12 | if (singleRun) { 13 | reporters = ['progress', 'coverage']; 14 | preprocessors = { 15 | 'src/**/*.html': ['ng-html2js'], 16 | 'src/**/*.directive.js': ['coverage'], 17 | 'src/**/*.service.js': ['coverage'], 18 | 'src/**/*.controller.js': ['coverage'], 19 | 'src/**/*.provider.js': ['coverage'], 20 | 'src/*.js': ['coverage'] 21 | }; 22 | } else { 23 | reporters = ['nyan']; 24 | preprocessors = { 25 | 'src/**/*.html': ['ng-html2js'] 26 | }; 27 | } 28 | 29 | karma.server.start({ 30 | configFile: path.join(__dirname, '/../karma.conf.js'), 31 | singleRun: singleRun, 32 | autoWatch: !singleRun, 33 | reporters: reporters, 34 | preprocessors: preprocessors 35 | }, function(exitCode) { 36 | done(exitCode ? new Error('Karma has exited with ' + exitCode) : null); 37 | }); 38 | } 39 | 40 | gulp.task('test', ['scripts'], function(done) { 41 | runTests(true, done); 42 | }); 43 | 44 | gulp.task('test:auto', ['watch'], function(done) { 45 | runTests(false, done); 46 | }); 47 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | function isOnlyChange(event) { 10 | return event.type === 'changed'; 11 | } 12 | 13 | gulp.task('watch', ['inject'], function () { 14 | 15 | gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject']); 16 | 17 | gulp.watch([ 18 | path.join(conf.paths.src, '/app/**/*.css'), 19 | path.join(conf.paths.src, '/app/**/*.scss') 20 | ], function(event) { 21 | if(isOnlyChange(event)) { 22 | gulp.start('styles'); 23 | } else { 24 | gulp.start('inject'); 25 | } 26 | }); 27 | 28 | gulp.watch(path.join(conf.paths.src, '/app/**/*.js'), function(event) { 29 | if(isOnlyChange(event)) { 30 | // gulp.start('scripts'); 31 | browserSync.reload(event.path); 32 | } else { 33 | gulp.start('inject'); 34 | } 35 | }); 36 | 37 | gulp.watch(path.join(conf.paths.src, '/app/**/*.html'), function(event) { 38 | browserSync.reload(event.path); 39 | }); 40 | 41 | gulp.watch(path.join(conf.paths.src, '/assets/i18n/*.json'), function(event) { 42 | browserSync.reload(event.path); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your gulpfile! 3 | * The gulp tasks are splitted in several files in the gulp directory 4 | * because putting all here was really too long 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var gulp = require('gulp'); 10 | var wrench = require('wrench'); 11 | 12 | /** 13 | * This will load all js or coffee files in the gulp directory 14 | * in order to load all gulp tasks 15 | */ 16 | wrench.readdirSyncRecursive('./gulp').filter(function(file) { 17 | return (/\.(js|coffee)$/i).test(file); 18 | }).map(function(file) { 19 | require('./gulp/' + file); 20 | }); 21 | 22 | 23 | /** 24 | * Default task clean temporaries directories and launch the 25 | * main optimization build task 26 | */ 27 | gulp.task('default', ['clean'], function () { 28 | gulp.start('build'); 29 | }); 30 | -------------------------------------------------------------------------------- /inch.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "included": [ 4 | "src/**/*.js" 5 | ], 6 | "excluded": [ 7 | "src/**/*.spec.js" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var conf = require('./gulp/conf'); 5 | 6 | var _ = require('lodash'); 7 | var wiredep = require('wiredep'); 8 | 9 | function listFiles() { 10 | var wiredepOptions = _.extend({}, conf.wiredep, { 11 | dependencies: true, 12 | devDependencies: true 13 | }); 14 | 15 | return wiredep(wiredepOptions).js 16 | .concat([ 17 | path.join(conf.paths.src, '/app/**/*.module.js'), 18 | path.join(conf.paths.src, '/app/**/*.js'), 19 | path.join(conf.paths.src, '/**/*.spec.js'), 20 | path.join(conf.paths.src, '/**/*.mock.js'), 21 | path.join(conf.paths.src, '/**/*.html'), 22 | path.join('bower_components/bootstrap-sass-official/assets/javascripts/bootstrap/dropdown.js'), 23 | path.join('bower_components/bootstrap-colorselector/lib/bootstrap-colorselector-0.2.0/js/bootstrap-colorselector.js') 24 | ]); 25 | } 26 | 27 | module.exports = function(config) { 28 | 29 | var configuration = { 30 | files: listFiles(), 31 | 32 | client: { 33 | captureConsole: true 34 | }, 35 | 36 | singleRun: true, 37 | 38 | autoWatch: false, 39 | 40 | frameworks: ['jasmine', 'angular-filesort'], 41 | 42 | angularFilesort: { 43 | whitelist: [path.join(conf.paths.src, '/**/!(*.html|*.spec|*.mock).js')] 44 | }, 45 | 46 | ngHtml2JsPreprocessor: { 47 | stripPrefix: 'src/', 48 | moduleName: 'axis' 49 | }, 50 | 51 | browsers : ['PhantomJS'], 52 | 53 | plugins : [ 54 | 'karma-phantomjs-launcher', 55 | 'karma-angular-filesort', 56 | 'karma-jasmine', 57 | 'karma-ng-html2js-preprocessor', 58 | 'karma-nyan-reporter', 59 | 'karma-coverage' 60 | ], 61 | 62 | preprocessors: { 63 | 'src/**/*.html': ['ng-html2js'], 64 | 'src/**/!(*.spec).js': ['coverage'] 65 | }, 66 | 67 | reporters: ['nyan', 'coverage'], 68 | 69 | coverageReporter: { 70 | reporters: [ 71 | {type: 'lcov'}, 72 | {type: 'text-summary'} 73 | ], 74 | instrumenterOptions: { 75 | istanbul: { noCompact: true } 76 | } 77 | } 78 | }; 79 | 80 | // This block is needed to execute Chrome on Travis 81 | // If you ever plan to use Chrome and Travis, you can keep it 82 | // If not, you can safely remove it 83 | // https://github.com/karma-runner/karma/issues/1144#issuecomment-53633076 84 | if(configuration.browsers[0] === 'Chrome' && process.env.TRAVIS) { 85 | configuration.customLaunchers = { 86 | 'chrome-travis-ci': { 87 | base: 'Chrome', 88 | flags: ['--no-sandbox'] 89 | } 90 | }; 91 | configuration.browsers = ['chrome-travis-ci']; 92 | } 93 | 94 | config.set(configuration); 95 | }; 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AxisJS", 3 | "version": "1.1.0", 4 | "dependencies": {}, 5 | "scripts": { 6 | "test": "gulp test", 7 | "codecov": "cat coverage/*/lcov.info | codecov" 8 | }, 9 | "devDependencies": { 10 | "browser-sync": "^2.7.13", 11 | "browser-sync-spa": "^1.0.3", 12 | "chalk": "~1.0.0", 13 | "codecov.io": "^0.1.6", 14 | "concat-stream": "~1.5.0", 15 | "del": "~1.2.0", 16 | "gulp": "~3.9.0", 17 | "gulp-angular-filesort": "~1.1.1", 18 | "gulp-angular-templatecache": "~1.6.0", 19 | "gulp-autoprefixer": "~2.3.1", 20 | "gulp-csso": "~1.0.0", 21 | "gulp-filter": "~2.0.2", 22 | "gulp-flatten": "~0.0.4", 23 | "gulp-inject": "~1.3.1", 24 | "gulp-jshint": "~1.11.0", 25 | "gulp-load-plugins": "~0.10.0", 26 | "gulp-minify-html": "~1.0.3", 27 | "gulp-ng-annotate": "~1.0.0", 28 | "gulp-protractor": "~1.0.0", 29 | "gulp-rename": "~1.2.2", 30 | "gulp-replace": "~0.5.3", 31 | "gulp-rev": "~5.0.0", 32 | "gulp-rev-replace": "~0.4.2", 33 | "gulp-sass": "~2.0.1", 34 | "gulp-size": "~1.2.1", 35 | "gulp-sourcemaps": "~1.5.2", 36 | "gulp-uglify": "~1.2.0", 37 | "gulp-useref": "~1.2.0", 38 | "gulp-util": "~3.0.5", 39 | "http-proxy-middleware": "~0.0.5", 40 | "jasmine-core": "^2.3.4", 41 | "jshint-stylish": "~2.0.0", 42 | "karma": "~0.12.36", 43 | "karma-angular-filesort": "~0.1.0", 44 | "karma-coverage": "^0.5.2", 45 | "karma-jasmine": "^0.3.6", 46 | "karma-ng-html2js-preprocessor": "~0.1.2", 47 | "karma-nyan-reporter": "^0.2.2", 48 | "karma-phantomjs-launcher": "~0.2.0", 49 | "lodash": "~3.9.3", 50 | "main-bower-files": "~2.8.0", 51 | "merge-stream": "~0.1.7", 52 | "phantomjs": "just-boris/phantomjs", 53 | "require-dir": "~0.3.0", 54 | "run-sequence": "^1.1.4", 55 | "uglify-save-license": "~0.4.1", 56 | "wiredep": "~2.2.2", 57 | "wrench": "~1.5.8" 58 | }, 59 | "engines": { 60 | "node": ">=0.12.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var paths = require('./.yo-rc.json')['generator-gulp-angular'].props.paths; 4 | 5 | // An example configuration file. 6 | exports.config = { 7 | // The address of a running selenium server. 8 | //seleniumAddress: 'http://localhost:4444/wd/hub', 9 | //seleniumServerJar: deprecated, this should be set on node_modules/protractor/config.json 10 | 11 | // Capabilities to be passed to the webdriver instance. 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | 16 | baseUrl: 'http://localhost:3000', 17 | 18 | // Spec patterns are relative to the current working directly when 19 | // protractor is called. 20 | specs: [paths.e2e + '/**/*.js'], 21 | 22 | // Options to be passed to Jasmine-node. 23 | jasmineNodeOpts: { 24 | showColors: true, 25 | defaultTimeoutInterval: 30000 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/components/addColorDataAttributes/addColorDataAttributes.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name AxisJS.directive:addColorDataAttributes 4 | * @description 5 | * Lame hack to add data attributes to select options, for Bootstrap Color Picker. 6 | * Might be doable with $watch instead of $timeout. Feels sloppy doing it that way... 7 | */ 8 | (function(){ 9 | 'use strict'; 10 | 11 | angular 12 | .module('axis') 13 | .directive('addColorDataAttributes', addColorDataAttributes); 14 | 15 | /** @ngInject */ 16 | function addColorDataAttributes($timeout) { 17 | return { 18 | restrict: 'A', 19 | link: function postLink(scope, element) { 20 | $timeout(function(){ 21 | element.children('option').each(function(){ 22 | var elm = angular.element(this); 23 | elm.attr('data-color', elm.text()); 24 | }); 25 | element.colorselector(); 26 | }, 500); 27 | } 28 | }; 29 | } 30 | })(); -------------------------------------------------------------------------------- /src/app/components/addColorDataAttributes/addColorDataAttributes.directive.spec.js: -------------------------------------------------------------------------------- 1 | describe('Directive: addColors', function () { 2 | 'use strict'; 3 | 4 | // load the directive's module 5 | beforeEach(module('axis')); 6 | 7 | var scope; 8 | 9 | // Initialize the controller and a mock scope 10 | beforeEach(inject(function ($rootScope) { 11 | scope = $rootScope.$new(); 12 | })); 13 | 14 | afterEach(function(){ 15 | angular.element('body').empty(); 16 | }); 17 | 18 | it('should add data attributes to select options', inject(function ($compile, $timeout, $httpBackend) { 19 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 20 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 21 | var element = angular.element(''); 22 | element = $compile(element)(scope); 23 | angular.element('body').append(element); 24 | scope.$apply(); 25 | $timeout.flush(500); 26 | expect(angular.element('select > option').eq(0).attr('data-color')).toBe('#ffcc00'); 27 | })); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/components/chart/c3/c3.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: c3Service', function () { 2 | 'use strict'; 3 | 4 | // load the service's module 5 | beforeEach(module('axis')); 6 | 7 | // instantiate service 8 | var c3Service, 9 | scope, 10 | element, 11 | appConfig = { 12 | renderer: 'c3', 13 | colors: [ 14 | 'red', 15 | 'blue' 16 | ], 17 | defaults: { 18 | 'grid x': true, 19 | 'grid y': true 20 | } 21 | }; 22 | 23 | beforeEach(inject(function ($rootScope, _c3Service_) { 24 | scope = $rootScope.$new(); 25 | c3Service = _c3Service_; 26 | 27 | element = angular.element('
'); 28 | angular.element('body').append(element); 29 | var configJSON = '{"data":{"x":"","y":"","y2":"","columns":[["dogs","10","20","40","60"],["bears","10","15","20","25"],["llamas","15","40","70","80"],["ducks","20","10","30","70"],["cows","30","20","10","60"],["sheep","40","25","35","50"],["orangutans","20","30","10","40"]],"axes":{},"groups":{},"type":"","types":{"data1":"line","data2":"line","dogs":"line","bears":"step","llamas":"area","ducks":"area-step","cows":"scatter","sheep":"bar","orangutans":"spline"},"colors":{"data1":"#78B8DF","data2":"#AFCBCE","dogs":"#1f77b4","bears":"#ff7f0e","llamas":"#2ca02c","ducks":"#d62728","cows":"#9467bd","sheep":"#8c564b","orangutans":"#e377c2"}},"axis":{"x":{"show":true,"accuracy":0,"prefix":"","suffix":"","tick":{}},"y":{"show":true,"accuracy":0,"prefix":"","suffix":"","tick":{}},"y2":{"show":false,"accuracy":0,"prefix":"","suffix":"","tick":{}}},"point":{"show":false},"groups":{},"defaultColors":["#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5"],"chartTitle":"","chartCredit":"","chartSource":"","chartWidth":1000,"chartGlobalType":"series","chartAccuracy":1,"cms":false,"pie":{"label":{}},"donut":{"label":{}},"gauge":{"label":{}}}'; 30 | scope.config = angular.fromJson(configJSON); 31 | angular.element('body').empty(); 32 | })); 33 | 34 | it('should produce a C3 chart', function () { 35 | var chart = c3Service.generate('chart', scope.config); 36 | expect(chart.data()[0].id).toBe('dogs'); 37 | expect(angular.element('svg')).toBeTruthy(); 38 | }); 39 | 40 | it('should generate a config object', function() { 41 | expect(c3Service.getConfig(appConfig)).toBeTruthy(); 42 | }); 43 | 44 | it('should throw a C3ServiceException on C3 error'); 45 | 46 | describe('C3 formatter functions', function(){ 47 | it('should return a formatter if specified'); 48 | it('should return the datum if no formatter specified'); 49 | }); 50 | 51 | it('should be able to set the global chart type (via setGlobalType)'); 52 | it('should be able to put data into groups (via setGroups)'); 53 | }); 54 | -------------------------------------------------------------------------------- /src/app/components/chart/chart.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/app/components/chart/chartService/chartService.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.chartService 4 | * @description 5 | * # chartService 6 | * Injects the correct chart renderer service based on YAML config. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .service('chartService', chartService); 15 | 16 | /** @ngInject */ 17 | function chartService($injector) { 18 | return function(appConfig) { 19 | var renderer = $injector.get(appConfig.renderer + 'Service'); 20 | var chart = renderer.getConfig(appConfig); 21 | 22 | chart.getConfig = function() { 23 | return renderer.getConfig(appConfig).config; 24 | }; 25 | 26 | chart.generate = renderer.generate; 27 | chart.watchers = renderer.watchers.map(function(v){ 28 | return 'main.config.' + v; 29 | }); 30 | 31 | chart.saveCallbacks = renderer.saveCallbacks; 32 | chart.restoreCallbacks = renderer.restoreCallbacks; 33 | 34 | return chart; 35 | }; 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /src/app/components/chart/chartService/chartService.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: chartProvider', function () { 2 | 'use strict'; 3 | 4 | // load the service's module 5 | beforeEach(module('axis')); 6 | 7 | var chartProvider, 8 | chartService, 9 | result, 10 | appConfig = { 11 | renderer: 'c3', 12 | colors: [ 13 | 'red', 14 | 'blue' 15 | ], 16 | defaults: { 17 | 'grid x': true, 18 | 'grid y': true 19 | } 20 | }; 21 | 22 | beforeEach(inject(function (_chartService_) { 23 | chartProvider = _chartService_; 24 | chartService = chartProvider(appConfig); 25 | })); 26 | 27 | it('should load c3Service', function () { 28 | expect(chartService.dependencies.js[1]).toBe('//cdnjs.cloudflare.com/ajax/libs/c3/0.4.7/c3.min.js'); 29 | }); 30 | 31 | it('should be able to get configuration from the renderer', function(){ 32 | result = chartService.getConfig(); 33 | expect(result).toBeDefined(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/components/chart/exportChart/exportChart.directive.spec.js: -------------------------------------------------------------------------------- 1 | describe('Directive: exportChart', function () { 2 | 'use strict'; 3 | 4 | // load the directive's module 5 | beforeEach(module('axis')); 6 | 7 | var MainController, 8 | element, 9 | scope, 10 | body, 11 | c3; 12 | 13 | // Initialize the controller and a mock MainController scope 14 | beforeEach(inject(function ($controller, $rootScope, $httpBackend, $window) { 15 | c3 = $window.c3; 16 | body = angular.element('body'); 17 | body.empty(); // clean up previous tests 18 | 19 | body.append(angular.element('png')); 20 | body.append(angular.element('svg')); 21 | body.append(angular.element('')); 22 | body.append(angular.element('
')); 23 | 24 | c3.generate({data: {columns: [['data1', 1, 2, 3], ['data2', 4, 5, 6]]}}); 25 | 26 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 27 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 28 | $httpBackend.expectGET('default.config.yaml'); 29 | $httpBackend.whenGET('default.config.yaml').respond(''); 30 | $httpBackend.expectGET('config.yaml'); 31 | $httpBackend.whenGET('config.yaml').respond(''); 32 | $httpBackend.expectGET('partials/configChooser.html'); 33 | $httpBackend.whenGET('partials/configChooser.html').respond(''); 34 | 35 | scope = $rootScope.$new(); 36 | MainController = $controller('MainController as main', { 37 | $scope: scope, 38 | appConfig: { 39 | renderer: 'c3', 40 | input: 'csv', 41 | save: [ 42 | 'png', 43 | 'svg' 44 | ], 45 | export: [ 46 | 'Embed code' 47 | ], 48 | colors: [ 49 | {value: 'blue'}, 50 | {value: 'red'} 51 | ], 52 | defaults: {}, 53 | } 54 | }); 55 | 56 | })); 57 | 58 | it('should create chart images if the "save" button is clicked', inject(function ($compile) { 59 | // Arrange 60 | element = angular.element(''); 61 | element = $compile(element)(scope); 62 | scope.$apply(); 63 | 64 | // Act 65 | element.trigger('click'); 66 | 67 | // Assert 68 | expect(angular.element('canvas').length).toBe(1); 69 | })); 70 | 71 | it('should save a PNG if the "Save to PNG" button is clicked', inject(function ($compile) { 72 | // Arrange 73 | element = angular.element(''); 74 | element = $compile(element)(scope); 75 | scope.$apply(); 76 | 77 | // Act 78 | element.trigger('click'); 79 | 80 | // Assert 81 | expect(angular.element('.savePNG').attr('href')).toMatch(/base64/); 82 | })); 83 | 84 | it('should save a SVG if the "Save to SVG" button is clicked', inject(function ($compile) { 85 | // Arrange 86 | element = angular.element(''); 87 | element = $compile(element)(scope); 88 | scope.$apply(); 89 | 90 | // Act 91 | element.trigger('click'); 92 | 93 | // Assert 94 | expect(angular.element('.saveSVG').attr('href')).toMatch(/\?xml/); 95 | })); 96 | 97 | it('should set image dimensions if specified'); 98 | it('should have a test for whatever the heck it is doing on ln 70'); 99 | it('should create a chart style object from C3 css'); 100 | it('should change elements with 0 opacity or visibility: hidden to display: none'); 101 | it('should inline all CSS rules'); 102 | it('should change .c3-line path fill CSS to an attribute'); 103 | it('should create a style object'); 104 | 105 | 106 | describe('a spec with tests intended to prevent regression on closed issues', function() { 107 | it('should not add cruft that prevents Illustrator from opening (#31)', inject(function ($compile) { 108 | // Arrange 109 | element = angular.element(''); 110 | element = $compile(element)(scope); 111 | scope.$apply(); 112 | 113 | // Act 114 | element.trigger('click'); 115 | var svg = angular.element('.saveSVG').attr('href'); 116 | 117 | // Assert 118 | expect(svg).toMatch(/\?xml/); 119 | expect(svg).not.toMatch(/\sfont-.*?: .*?;/gi); 120 | expect(svg).not.toMatch(/\sclip-.*?="url\(http:\/\/.*?\)"/gi); // This one is particularly important. 121 | expect(svg).not.toMatch(/\stransform="scale\(2\)"/gi); 122 | })); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/app/components/config/configChooser/configChooser.html: -------------------------------------------------------------------------------- 1 | 4 | 11 | 14 | -------------------------------------------------------------------------------- /src/app/components/config/configChooser/configChooser.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.configChooser 4 | * @description 5 | * # configChooser 6 | * Opens an off-canvas configuration picker, using options defined in YAML. 7 | * When a new config is chosen, it's saved to localStorage and the app is reloaded. 8 | */ 9 | 10 | (function(){ 11 | 'use strict'; 12 | 13 | angular 14 | .module('axis') 15 | .factory('configChooser', configChooser); 16 | 17 | /** @ngInject */ 18 | function configChooser($aside, configProvider) { 19 | return function() { 20 | // This is ignored by Istanbul for the same reason as the modal in EmbedcodeOutput 21 | /* istanbul ignore next */ 22 | $aside.open({ 23 | placement: 'right', 24 | backdrop: true, 25 | controller: 'ConfigChooserController as ConfChooseCtrl', 26 | templateUrl: 'app/components/config/configChooser/configChooser.html', 27 | resolve: { 28 | conf: function(){ return configProvider; } 29 | } 30 | }); 31 | }; 32 | } 33 | 34 | angular 35 | .module('axis') 36 | .controller('ConfigChooserController', ConfigChooserController); 37 | 38 | /** @ngInject **/ 39 | function ConfigChooserController($scope, localStorageService, $window, conf, $modalInstance) { 40 | var vm = this; 41 | vm.name = 'Choose Configuration'; 42 | vm.themes = conf.themes; 43 | 44 | vm.cancel = function(e){ 45 | $modalInstance.dismiss(); 46 | e.stopPropagation(); 47 | }; 48 | 49 | vm.setConfig = function(config) { 50 | localStorageService.set('config', config); 51 | $window.location.reload(); 52 | }; 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /src/app/components/config/configChooser/configChooser.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for configChooserService 3 | */ 4 | 5 | describe('Service: configChooser', function () { 6 | 'use strict'; 7 | 8 | // load the service's module 9 | beforeEach(module('axis')); 10 | 11 | // instantiate service 12 | var configChooser, 13 | ConfChooseCtrl, 14 | scope, 15 | localStorageService, 16 | $modalInstance, 17 | $window; 18 | 19 | beforeEach(inject(function (_configChooser_, $controller, $rootScope, _$window_) { 20 | scope = $rootScope.$new(); 21 | configChooser = _configChooser_; 22 | 23 | // Mocks 24 | $modalInstance = { 25 | dismiss: jasmine.createSpy('dismiss') 26 | }; 27 | 28 | localStorageService = { 29 | set: jasmine.createSpy('set') 30 | }; 31 | 32 | $window = _$window_; 33 | $window.location.reload = jasmine.createSpy('reload'); 34 | 35 | // Controllers 36 | ConfChooseCtrl = $controller('ConfigChooserController', { 37 | $scope: scope, 38 | localStorageService: localStorageService, 39 | $modalInstance: $modalInstance, 40 | $window: $window, 41 | conf: { 42 | renderer: 'c3', 43 | input: ['csv', 'spreadsheet'], 44 | colors: [ 45 | {value: 'blue'}, 46 | {value: 'red'} 47 | ], 48 | themes: [ 49 | { 50 | name: 'test1', 51 | file: 'test1.yaml' 52 | }, 53 | { 54 | name: 'test2', 55 | file: 'test2.yaml' 56 | }, 57 | ], 58 | defaults: {} 59 | } 60 | }); 61 | })); 62 | 63 | it('should have a list of themes', function(){ 64 | expect(ConfChooseCtrl.themes.length).toBe(2); 65 | }); 66 | 67 | it('should dismiss the aside on cancel', function(){ 68 | var e = {stopPropagation: jasmine.createSpy('stopPropagation')}; 69 | ConfChooseCtrl.cancel(e); 70 | 71 | expect($modalInstance.dismiss).toHaveBeenCalled(); 72 | expect(e.stopPropagation).toHaveBeenCalled(); 73 | }); 74 | 75 | it('should save the config to localStorage on selection', function(){ 76 | ConfChooseCtrl.setConfig('test1.yaml'); 77 | 78 | expect(localStorageService.set).toHaveBeenCalled(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/app/components/config/configLoader/configLoader.provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.configProvider 4 | * @description 5 | * # configProvider 6 | * Loads default.config.yaml and overrides values from that based on user config. 7 | * The (empty) config.yaml is used if no config option is set in localStorage. 8 | * Otherwise it attempts to load whatever value is stored as "config" in localStorage. 9 | */ 10 | 11 | (function(){ 12 | 'use strict'; 13 | 14 | angular 15 | .module('axis') 16 | .provider('configProvider', configProvider); 17 | 18 | /** @ngInject */ 19 | function configProvider() { 20 | return { 21 | $get: function($http, $q, localStorageService, $window) { 22 | var jsyaml = $window.jsyaml; 23 | 24 | var defaultConfigFile = localStorageService.get('defaultConfig') ? localStorageService.get('defaultConfig') : 'default.config.yaml'; 25 | var defaultConfig = $http.get(defaultConfigFile).then( 26 | function(response){ 27 | return response; 28 | }, 29 | function(response){ 30 | if (response.status === 404) { 31 | return $http.get('default.config.yaml'); 32 | } else { 33 | return $q.reject(response); 34 | } 35 | } 36 | ); 37 | 38 | var userConfigFile = localStorageService.get('config') ? localStorageService.get('config') : 'config.yaml'; 39 | var userConfig = $http.get(userConfigFile).then( 40 | function(response){ 41 | return response; 42 | }, 43 | function(response){ 44 | if (response.status === 404) { 45 | response = {}; 46 | return response; 47 | } else { 48 | return $q.reject(response); 49 | } 50 | } 51 | ); 52 | 53 | return $q.all([defaultConfig, userConfig]).then(function(values){ 54 | var defaultConfigYaml = jsyaml.safeLoad(values[0].data); 55 | var userConfigYaml = jsyaml.safeLoad(values[1].data); 56 | var axisConfig; 57 | 58 | // Oddly, js-yaml returns string 'undefined' on fail and not type undefined 59 | userConfigYaml = userConfigYaml !== 'undefined' ? userConfigYaml : {}; 60 | axisConfig = angular.extend({}, defaultConfigYaml, userConfigYaml); 61 | 62 | axisConfig.framework = axisConfig.renderer; // Needed for backwards compat. 63 | return axisConfig; 64 | }, 65 | function(err){ // Return default on failure. 66 | console.dir(err); 67 | }); 68 | } 69 | }; 70 | } 71 | })(); 72 | -------------------------------------------------------------------------------- /src/app/components/config/configLoader/configLoader.provider.spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: configLoader', function () { 2 | 'use strict'; 3 | 4 | // load the service's module 5 | beforeEach(module('axis')); 6 | 7 | // instantiate service 8 | var configProvider, config; 9 | beforeEach(inject(function (_configProvider_) { 10 | configProvider = _configProvider_; 11 | config = undefined; 12 | })); 13 | 14 | it('should load the default config', inject(function ($httpBackend) { 15 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 16 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 17 | $httpBackend.whenGET('default.config.yaml').respond('colors:\n - label: "neutral 1"\n value: "#78B8DF"\n - label: "neutral 2"\n value: "#AFCBCE"'); 18 | $httpBackend.whenGET('config.yaml').respond('colors:\n - label: "neutral 1"\n value: "#78B8DF"\n - label: "neutral 2"\n value: "#AFCBCE"'); 19 | $httpBackend.expectGET('default.config.yaml'); 20 | $httpBackend.expectGET('config.yaml'); 21 | 22 | configProvider.then(function(data){ // TODO figure out why this is needed. 23 | config = data; 24 | }); 25 | 26 | $httpBackend.flush(); 27 | 28 | expect(config.colors.length).toBe(2); 29 | })); 30 | 31 | // TODO make this spec work. 32 | xit('should return an empty object if request 404s', inject(function ($httpBackend) { 33 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 34 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 35 | $httpBackend.expectGET('default.config.yaml'); 36 | $httpBackend.whenGET('default.config.yaml').respond(404, '{}'); 37 | $httpBackend.expectGET('config.yaml'); 38 | $httpBackend.whenGET('config.yaml').respond(404, '{}'); 39 | 40 | configProvider.then(function(data){ // TODO figure out why this is needed. 41 | dump(data); 42 | config = data; 43 | }); 44 | 45 | $httpBackend.flush(); 46 | 47 | expect(config).toBe({}); 48 | })); 49 | 50 | // TODO decide what to do when promise is rejected. 51 | xit('should reject the promise on any other HTTP error code', inject(function ($httpBackend) { 52 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 53 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 54 | $httpBackend.expectGET('default.config.yaml'); 55 | $httpBackend.whenGET('default.config.yaml').respond(500, ''); 56 | $httpBackend.expectGET('config.yaml'); 57 | $httpBackend.whenGET('config.yaml').respond(500, ''); 58 | 59 | configProvider.then(function(data){ // TODO figure out why this is needed. 60 | config = data; 61 | }); 62 | 63 | $httpBackend.flush(); 64 | })); 65 | }); 66 | -------------------------------------------------------------------------------- /src/app/components/input/csv/csvInput.html: -------------------------------------------------------------------------------- 1 | 2 | 11 |
14 | Whoops! {{ 'INPUT_MALFORMED_CSV_ERROR' | translate }} 15 |
16 | -------------------------------------------------------------------------------- /src/app/components/input/csv/csvInput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.csvInput 4 | * @description 5 | * # csvInput 6 | * Provides a text box to input and parse CSV data. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('csvInput', csvInput); 15 | 16 | /** @ngInject */ 17 | function csvInput($window) { 18 | function CsvInputServiceException(message) { 19 | this.name = 'CsvInputServiceException'; 20 | this.message = 'Input has failed: ' + message; 21 | } 22 | CsvInputServiceException.prototype = new Error(); 23 | CsvInputServiceException.prototype.constructor = CsvInputServiceException; 24 | 25 | 26 | var Papa = $window.Papa; 27 | 28 | var validateCSV = function (value) { 29 | var csv, noDelimiter; 30 | var parserConfig = { 31 | header: true, 32 | dynamicTyping: true 33 | }; 34 | 35 | // Detect TSV; fallback to auto-detection. @see #39. 36 | if (value.match('\t')) { 37 | parserConfig.delimiter = '\t'; 38 | } 39 | 40 | try { 41 | csv = Papa.parse(value, parserConfig); 42 | } catch(e) { 43 | throw new CsvInputServiceException(e); 44 | } 45 | 46 | noDelimiter = /^[^,\t\s]*\n[^,\t\s]*$/gm; // Edge-case for gauge charts (one column of data) 47 | 48 | return (csv.errors.length > 0 && !value.match(noDelimiter) ? false : true); 49 | }; 50 | 51 | var defaultCSV = 'data1\tdata2\n30\t50\n200\t20\n100\t10\n400\t40\n150\t15\n250\t25'; 52 | 53 | var parseCSV = function (scope) { 54 | if (scope.inputs.inputData) { 55 | try { 56 | scope.chartData = []; // Empty, or else new column names will break ng-grid 57 | scope.columns = []; // Clear existing 58 | scope.config.data.columns = []; 59 | var parserConfig = { 60 | header: true, 61 | dynamicTyping: true 62 | }; 63 | 64 | // Detect TSV; fallback to auto-detection. @see #39. 65 | if (scope.inputs.inputData.match('\t')) { 66 | parserConfig.delimiter = '\t'; 67 | } 68 | 69 | scope.chartData = Papa.parse(scope.inputs.inputData, parserConfig).data; 70 | } catch(e) { 71 | throw new CsvInputServiceException(e); 72 | } 73 | 74 | // Convert objects into arrays. Might be better long-term to use C3's JSON input. 75 | // Lots of this stuff is C3-specific. TODO move to c3Service. 76 | if (scope.chartData.length > 0) { 77 | scope.columns = Object.keys(scope.chartData[0]); 78 | angular.forEach(scope.columns, function(colName) { 79 | var column = []; 80 | column.push(colName); 81 | angular.forEach(scope.chartData, function(datum) { 82 | // Remove commas from numbers 83 | if (!/(\d)(?=(\d{3})+(?!\d))/g.test(datum[colName]) && typeof datum[colName] === 'string') { 84 | datum[colName] = datum[colName].replace(/,/g, ''); 85 | } 86 | 87 | column.push(datum[colName]); 88 | }); 89 | 90 | scope.config.data.columns.push(column); 91 | }); 92 | } 93 | } 94 | 95 | return scope; 96 | }; 97 | 98 | var convertColsToCSV = function(columns) { 99 | var data = []; 100 | var headers = []; 101 | var output; 102 | for (var i = 0; i < columns.length; i++) { 103 | headers.push(columns[i].shift()); 104 | for (var j = 0; j < columns[i].length; j++) { 105 | if (!data[j]) { 106 | data[j] = []; 107 | } 108 | data[j][i] = columns[i][j]; 109 | } 110 | } 111 | 112 | try { 113 | output = Papa.unparse({fields: headers, data: data}, {delimiter: '\t'}); 114 | } catch(e) { 115 | throw new CsvInputServiceException(e); 116 | } 117 | 118 | return output; 119 | }; 120 | 121 | // Public API here 122 | return { 123 | /** 124 | * Service name. Useful for tests. 125 | * @type {String} 126 | */ 127 | name: 'csvInputService', 128 | 129 | /** 130 | * Validates the CSV input. 131 | * @param {string} value The raw CSV input 132 | * @return {boolean} The validation result 133 | */ 134 | validate: function(value) { 135 | return validateCSV(value); 136 | }, 137 | 138 | /** 139 | * Default CSV data to populate input with. 140 | * @type {string} 141 | */ 142 | defaultData: defaultCSV, 143 | 144 | /** 145 | * Parses the raw CSV into an array of arrays. 146 | * @param {object} scope Axis scope object 147 | * @return {object} Updated scope object 148 | */ 149 | input: function(scope) { 150 | return parseCSV(scope); 151 | }, 152 | 153 | /** 154 | * Converts an array into a CSV string. 155 | * @param {array} columns Parsed CSV data 156 | * @return {string} Converted CSV string 157 | */ 158 | convert: function(columns) { 159 | return convertColsToCSV(columns); 160 | } 161 | }; 162 | } 163 | })(); 164 | -------------------------------------------------------------------------------- /src/app/components/input/csv/csvInput.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the csvInput service. 3 | */ 4 | 5 | describe('Service: csvInput', function () { 6 | 'use strict'; 7 | 8 | // load the service's module 9 | beforeEach(module('axis')); 10 | 11 | var csvInput, 12 | result, 13 | MainCtrl, 14 | scope; 15 | 16 | // instantiate service 17 | beforeEach(inject(function (_csvInput_, $controller, $rootScope, $httpBackend) { 18 | csvInput = _csvInput_; 19 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 20 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 21 | $httpBackend.expectGET('default.config.yaml'); 22 | $httpBackend.whenGET('default.config.yaml').respond({}); 23 | $httpBackend.expectGET('config.yaml'); 24 | $httpBackend.whenGET('config.yaml').respond({}); 25 | 26 | scope = $rootScope.$new(); 27 | MainCtrl = $controller('MainController as main', { 28 | $scope: scope, 29 | appConfig: { 30 | renderer: 'c3', 31 | input: 'csv', 32 | save: [ 33 | 'png', 34 | 'svg' 35 | ], 36 | export: [ 37 | 'Embed code' 38 | ], 39 | colors: [ 40 | {value: 'blue'}, 41 | {value: 'red'} 42 | ], 43 | defaults: {}, 44 | } 45 | }); 46 | })); 47 | 48 | describe('basic functionality', function(){ 49 | it('should have default data', function(){ 50 | expect(csvInput.defaultData).toBeDefined(); 51 | }); 52 | 53 | it('should validate CSV', function(){ 54 | result = csvInput.validate('llamas\tducks\n55\t9001'); 55 | expect(result).toBeTruthy(); 56 | }); 57 | 58 | // This is a dumb test due to inputService.input passing full scopes around. 59 | // @TODO improve this test. 60 | it('should parse a CSV into an array of columns', function(){ 61 | result = csvInput.input(scope.main); 62 | expect(result).toBeDefined(); 63 | }); 64 | 65 | it('should be able to convert an array of columns into a CSV string', function(){ 66 | result = csvInput.convert([['llamas', 55], ['ducks', 9001]]); 67 | 68 | expect(result).toBe('llamas\tducks\r\n55\t9001'); // N.b., Papa Parse newline is \r\n 69 | }); 70 | }); 71 | 72 | describe('edge cases', function(){ 73 | 74 | // @TODO make these throw actual CsvInputServiceExceptions. 75 | // Given that Papa Parse returns an error object instead of throwing, may need rewriting. 76 | it('should throw a CsvInputServiceException upon problem', function() { 77 | // These need to be wrapped in an anonymous function to catch the exception. 78 | expect(function(){ 79 | csvInput.input('hurrrrr'); 80 | }).toThrow();//.toThrowError(/CsvInputServiceException/); 81 | 82 | expect(function(){ 83 | csvInput.validate(false); 84 | }).toThrow();//.toThrowError(/CsvInputServiceException/); 85 | 86 | expect(function(){ 87 | csvInput.convert('this is a string'); 88 | }).toThrow();//.toThrowError(/CsvInputServiceException/); 89 | }); 90 | 91 | it('should remove separator commas from numbers', function(){ 92 | scope.main.inputs.inputData = 'llamas\r\n"9,001"'; 93 | result = csvInput.input(scope.main); 94 | expect(result.config.data.columns[0][1]).toBe('9001'); 95 | }); 96 | 97 | it('should be able to validate CSVs with only one column', function(){ 98 | scope.main.inputs.inputData = 'llamas\r\nducks'; 99 | result = csvInput.input(scope.main); 100 | expect(result.config.data.columns[0][0]).toBe('llamas'); 101 | expect(result.config.data.columns[0][1]).toBe('ducks'); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/app/components/input/csv/maintainFocus.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name axis.directive:maintainFocus 4 | * @description 5 | * # maintainFocus 6 | * Prevents tab key from changing focus and instead inserts a tab into the 7 | * focused element. 8 | */ 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .directive('maintainFocus', maintainFocus); 15 | 16 | /** @ngInject */ 17 | function maintainFocus() { 18 | return { 19 | restrict: 'A', 20 | link: function postLink(scope, element) { 21 | element.on('keydown', function(e) { 22 | if (e.keyCode === 9) { // tab was pressed 23 | // get caret position/selection 24 | var val = this.value, 25 | start = this.selectionStart, 26 | end = this.selectionEnd; 27 | 28 | // set textarea value to: text before caret + tab + text after caret 29 | this.value = val.substring(0, start) + '\t' + val.substring(end); 30 | 31 | // put caret at right position again 32 | this.selectionStart = this.selectionEnd = start + 1; 33 | 34 | // prevent the focus lose 35 | return false; 36 | } 37 | }); 38 | } 39 | }; 40 | } 41 | })(); -------------------------------------------------------------------------------- /src/app/components/input/csv/maintainFocus.directive.spec.js: -------------------------------------------------------------------------------- 1 | describe('Directive: maintainFocus', function () { 2 | 'use strict'; 3 | 4 | // load the directive's module 5 | beforeEach(module('axis')); 6 | 7 | var element, 8 | scope; 9 | 10 | beforeEach(inject(function ($rootScope, $compile) { 11 | scope = $rootScope.$new(); 12 | element = angular.element(''); 13 | element = $compile(element)(scope); 14 | angular.element('body').append(element); 15 | angular.element('#data-input').focus(); // grant focus 16 | angular.element.event.trigger('keydown', {keyCode: 9}); // emulate pressing tab 17 | })); 18 | 19 | /** 20 | * N.b., the following test passes, but triggering keyCode 9 programmatically 21 | * doesn't seem to unfocus it. Better test needed. 22 | */ 23 | 24 | it('should maintain focus when tab is pressed', function() { 25 | expect(document.activeElement).toBe(angular.element('#data-input')[0]); 26 | }); 27 | 28 | it('should output a tab character when tab is pressed'); 29 | 30 | it('should move the caret one character forward after tab is pressed'); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/components/input/feed/feedInput.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |

6 | 15 | 16 | 21 | 22 |

23 |
24 |
25 | 26 |

27 | 37 | 38 | 44 | 45 |

46 |
47 |
48 | -------------------------------------------------------------------------------- /src/app/components/input/feed/feedInput.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @TODO convert this from csvInput to feedInput 3 | */ 4 | 5 | 'use strict'; 6 | 7 | xdescribe('Service: feedInput', function() { 8 | 9 | // load the service's module 10 | beforeEach(module('axis')); 11 | 12 | // instantiate service 13 | var csvInput, 14 | sampleTSV = 'data1\tdata2\n30\t50\n200\t20\n100\t10\n400\t40\n150\t15\n250\t25', 15 | sampleCSV = 'data1,data2\n30,50\n200,20\n100,10\n400,40\n150,15\n250,25', 16 | sampleBroken = 'llama llama duck'; 17 | 18 | var testScope = { 19 | inputs: { 20 | inputData: sampleTSV 21 | }, 22 | config: { 23 | data: { 24 | types: {} 25 | } 26 | }, 27 | }; 28 | 29 | beforeEach(inject(function(_csvInput_) { 30 | csvInput = _csvInput_; 31 | })); 32 | 33 | it('should include default data', function() { 34 | expect(csvInput.defaultData).toEqual(sampleTSV); 35 | }); 36 | 37 | it('should validate compliant CSV', function() { 38 | expect(csvInput.validate(sampleTSV)).toBe(true); 39 | }); 40 | 41 | it('should not validate non-compliant CSV', function() { 42 | expect(csvInput.validate(sampleBroken)).toBe(false); 43 | }); 44 | 45 | it('parse TSV and return a Papaparse Object', function() { 46 | expect(csvInput.input(testScope).config.data.columns.length).toBe(2); 47 | }); 48 | 49 | it('should recognise CSV and parse it properly', function() { 50 | testScope.inputs.csvData = sampleCSV; 51 | expect(csvInput.input(testScope).config.data.columns.length).toBe(2); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/app/components/input/financial/financialInput.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |

6 | 15 | 16 | 21 | 22 |

23 |
24 |
25 | 26 |

27 | 37 | 38 | 44 | 45 |

46 |
47 |
48 | 49 |
50 |
51 | 52 | 59 |
60 |
61 | -------------------------------------------------------------------------------- /src/app/components/input/financial/financialInput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.financialInput 4 | * @description 5 | * # financialInput 6 | * Enables a fancy user-friendly financial data picker. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('financialInput', financialInput); 15 | 16 | /** @ngInject */ 17 | function financialInput($q, $http, $window) { 18 | /** 19 | * Creates a bunch of promises to query Yahoo! Finance 20 | * @param {object} inputs Object containing symbol, start date and end date. 21 | * @return {array} Array of promises 22 | */ 23 | var getData = function(inputs) { 24 | var symbols = inputs.symbol ? inputs.symbol.split(/[,\s]/) : ['NWS']; 25 | var dateStart = inputs.dateStart ? inputs.dateStart : $window.moment().subtract(31, 'days').format('YYYY-MM-DD'); 26 | var dateEnd = inputs.dateEnd ? inputs.dateEnd : $window.moment().format('YYYY-MM-DD'); 27 | var endpoint; 28 | var financialData = []; 29 | 30 | for (var i = 0; i < symbols.length; i++) { 31 | endpoint = 'https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20yahoo.finance.historicaldata%20where%20symbol%20%3D%20%22' + symbols[i] + '%22%20and%20startDate%20%3D%20%22' + dateStart + '%22%20and%20endDate%20%3D%20%22' + dateEnd + '%22&format=json&diagnostics=false&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys'; 32 | financialData.push($http.get(endpoint)); // Add promise to array, to resolve in parseData 33 | } 34 | 35 | return financialData; 36 | }; 37 | 38 | var parseData = function(scope) { 39 | if (scope.inputs.inputData) { 40 | var financialData = getData({ 41 | symbol: scope.inputs.inputData.symbol, 42 | dateStart: angular.isString(scope.inputs.inputData.dateStart) ? scope.inputs.inputData.dateStart : $window.moment(scope.inputs.inputData.dateStart).format('YYYY-MM-DD'), 43 | dateEnd: angular.isString(scope.inputs.inputData.dateEnd) ? scope.inputs.inputData.dateEnd : $window.moment(scope.inputs.inputData.dateEnd).format('YYYY-MM-DD') 44 | }); 45 | 46 | // Resolve all the promises and populate the chart 47 | $q.all(financialData).then(function(res){ 48 | scope.chartData = []; // Empty, or else new column names will break ng-grid 49 | scope.columns = []; // Clear existing 50 | scope.config.data.columns = []; 51 | var cols = []; 52 | var colNames = []; 53 | 54 | angular.forEach(res, function(item, index){ 55 | var stockData = item.data.query.results.quote; 56 | if (index === 0) { 57 | var dates = stockData.map(function(v){ 58 | return v.Date; 59 | }); 60 | dates.unshift('Date'); 61 | cols.push(dates); 62 | colNames.push('Date'); 63 | } 64 | 65 | if (stockData.length > 0) { 66 | var symbol = stockData[0].Symbol; 67 | var col = stockData.map(function(v){ 68 | return v.Adj_Close; 69 | }); 70 | col.unshift(symbol); // Add symbol name as item header 71 | cols.push(col); 72 | colNames.push(symbol); 73 | } 74 | }); 75 | 76 | if (cols.length > 0) { 77 | scope.chartData = cols; 78 | scope.columns = colNames; 79 | scope.config.data.columns = cols; 80 | scope.config.data.x = 'Date'; 81 | scope.config.financial = scope.chartData; // Needed to repopulate on load; unique to this. 82 | scope.config.title.source = 'Source: Yahoo'; 83 | scope.config.axis.x.tick = scope.config.axis.x.tick ? scope.config.axis.x.tick : {format: '%Y-%m-%d'}; 84 | scope.config.axis.x.type = 'timeseries'; 85 | // scope.updateData(); // This might be needed somehow. 86 | } 87 | }); 88 | } 89 | 90 | return scope; 91 | }; 92 | 93 | var populateInput = function(columns) { // This doesn't do anything yet. 94 | 95 | }; 96 | 97 | // Public API here 98 | return { 99 | /** 100 | * Service name. 101 | * @type {String} 102 | */ 103 | name: 'financialInputService', 104 | 105 | /** 106 | * Validate spreadsheet input 107 | * @param {array} value A financial picker symbol 108 | * @return {boolean} True if validates, false if not. 109 | */ 110 | validate: function(value) { 111 | return value instanceof Array; // TODO write a real validator. 112 | }, 113 | 114 | /** 115 | * The default data to populate Axis with. 116 | * @type {array} 117 | */ 118 | defaultData: { 119 | symbol: 'NWS', 120 | dateStart: $window.moment().subtract(31, 'days').format('YYYY-MM-DD'), 121 | dateEnd: $window.moment().format('YYYY-MM-DD') 122 | }, 123 | 124 | /** 125 | * Grabs the symbol data and applies to scope 126 | * @param {object} scope The AxisJS scope object. 127 | * @return {object} The updated scope object. 128 | */ 129 | input: function(scope) { 130 | return parseData(scope); 131 | }, 132 | 133 | /** 134 | * Convert loaded data to date ranges and symbol 135 | * @param {array} data An array of array columns. 136 | * @return {array} Array with header column values as first element. 137 | */ 138 | convert: function(data) { 139 | return populateInput(data); 140 | } 141 | }; 142 | } 143 | })(); 144 | -------------------------------------------------------------------------------- /src/app/components/input/financial/financialInput.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: financialInput', function() { 2 | 'use strict'; 3 | 4 | // load the service's module 5 | beforeEach(module('axis')); 6 | 7 | var financialInput, 8 | testScope, 9 | baseTime; 10 | 11 | baseTime = new Date('2015-10-14'); 12 | jasmine.clock().mockDate(baseTime); 13 | 14 | beforeEach(inject(function(_financialInput_, $rootScope) { 15 | financialInput = _financialInput_; 16 | testScope = $rootScope.$new(); 17 | 18 | testScope.inputs = { 19 | symbol: 'NWS', 20 | dateStart: '2015-09-13', 21 | dateEnd: '2015-10-14' 22 | }; 23 | testScope.config = {}; 24 | })); 25 | 26 | it('should identify itself', function(){ 27 | expect(financialInput.name).toBe('financialInputService'); 28 | }); 29 | 30 | it('should return the default data', function(){ 31 | var defaultData = financialInput.defaultData; 32 | 33 | expect(defaultData.symbol).toBe('NWS'); 34 | expect(defaultData.dateStart).toBe('2015-09-13'); 35 | expect(defaultData.dateEnd).toBe('2015-10-14'); 36 | }); 37 | 38 | it('should validate picker symbols', function(){ 39 | expect(financialInput.validate([])).toBeTruthy(); // This isn't a real test because the validator doesn't work yet. 40 | }); 41 | 42 | xit('should parse the data', inject(function($httpBackend){ 43 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 44 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 45 | 46 | var output = financialInput.input(testScope); // This resolves a promise inside the function and is thus borked. 47 | testScope.$digest(); 48 | $httpBackend.flush(); 49 | $httpBackend.expectGET('default.config.yaml'); 50 | $httpBackend.whenGET('default.config.yaml').respond(''); 51 | $httpBackend.expectGET('config.yaml'); 52 | $httpBackend.whenGET('config.yaml').respond(''); 53 | 54 | expect(output.chartData).toBeDefined(); 55 | })); 56 | 57 | it('should convert loaded data'); // Not sure if convert is functional yet. 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/components/input/generic/genericInput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.genericInput 4 | * @description 5 | * # genericInput 6 | * Input service primitive. Useless as-is; meant to be extended. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('genericInput', genericInput); 15 | 16 | /** @ngInject */ 17 | function genericInput() { 18 | var defaultData = [ 19 | ['data1', 'data2'], 20 | [30, 50], 21 | [200, 20], 22 | [100, 10], 23 | [400, 40], 24 | [150, 15], 25 | [250, 25] 26 | ]; 27 | 28 | // Public API here 29 | return { 30 | /** 31 | * Service name 32 | * @type {String} 33 | */ 34 | name: 'genericInputService', 35 | 36 | /** 37 | * Validate input 38 | * @param {array} value Some output 39 | * @return {boolean} True if validates, false if not. 40 | */ 41 | validate: function(value) { 42 | return value ? true : false; 43 | }, 44 | 45 | /** 46 | * The default data to populate AxisJS with. 47 | * @type {array} 48 | */ 49 | defaultData: defaultData, 50 | 51 | /** 52 | * Parses data into columns. Called whenever data updated. 53 | * @param {object} scope The AxisJS scope object. 54 | * @return {object} The updated scope object. 55 | */ 56 | input: function(scope) { 57 | return scope; 58 | }, 59 | 60 | /** 61 | * Convert data from something to something. 62 | * @param {array} data An array of array columns. 63 | * @return {array} Array with header column values as first element. 64 | */ 65 | convert: function(data) { 66 | return data; 67 | } 68 | }; 69 | } 70 | })(); 71 | -------------------------------------------------------------------------------- /src/app/components/input/generic/genericInput.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @TODO write a better spec for genericInput 3 | */ 4 | 5 | describe('Service: genericInput', function () { 6 | 'use strict'; 7 | 8 | // load the service's module 9 | beforeEach(module('axis')); 10 | 11 | // instantiate service 12 | var genericInput; 13 | beforeEach(inject(function (_genericInput_) { 14 | genericInput = _genericInput_; 15 | })); 16 | 17 | it('should validate() as true if any value given'); 18 | it('should validate() as false if no value given'); 19 | it('should return the scope object on input()'); 20 | it('should return the scope object on convert()'); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/components/input/inputChooser/inputChooser.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name AxisJS.directive:inputChooser 4 | * @description 5 | * Directive allowing the user choose an input method 6 | */ 7 | (function(){ 8 | 'use strict'; 9 | 10 | angular 11 | .module('axis') 12 | .directive('inputChooser', inputChooser); 13 | 14 | function inputChooser() { 15 | return { 16 | templateUrl: 'app/components/input/inputChooser/inputChooser.html', 17 | restrict: 'E', 18 | scope: { 19 | 'main': '=config' 20 | }, 21 | controllerAs: 'inputCtrl', 22 | controller: 'InputChooserController' 23 | }; 24 | } 25 | 26 | angular 27 | .module('axis') 28 | .controller('InputChooserController', InputChooserController); 29 | 30 | /** @ngInject **/ 31 | function InputChooserController($scope, inputService) { 32 | var main = $scope.main; 33 | var vm = this; 34 | 35 | vm.isArray = angular.isArray; 36 | vm.template = false; 37 | vm.setTemplate = function(type) { 38 | vm.template = type; 39 | main.appConfig.input = type; // Replace array with string form. 40 | main.setInput(type); // Set input in MainController to this input provider. 41 | main.resetConfig(type); // Reset chart config to default 42 | main.inputs.inputData = inputService(main.appConfig).defaultData; // Replace default data 43 | main.config.inputType = type; // Set type in config object to restore on load. 44 | main.updateData(); 45 | }; 46 | } 47 | })(); 48 | -------------------------------------------------------------------------------- /src/app/components/input/inputChooser/inputChooser.directive.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for input chooser directive 3 | */ 4 | describe('Directive: inputChooser', function () { 5 | 'use strict'; 6 | 7 | // load the directive's module 8 | beforeEach(module('axis')); 9 | 10 | var scope, 11 | inputCtrl, 12 | main, 13 | element; 14 | 15 | describe('a single input method', function(){ 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function ($rootScope, $controller, $compile, $httpBackend) { 18 | scope = $rootScope.$new(); 19 | main = $controller('MainController as main', { 20 | $scope: scope, 21 | appConfig: { 22 | renderer: 'c3', 23 | input: 'csv', 24 | colors: [ 25 | {value: 'blue'}, 26 | {value: 'red'} 27 | ], 28 | defaults: {} 29 | } 30 | }); 31 | 32 | inputCtrl = $controller('InputChooserController', { 33 | '$scope': scope 34 | }); 35 | 36 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 37 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 38 | $httpBackend.expectGET('default.config.yaml'); 39 | $httpBackend.whenGET('default.config.yaml').respond(''); 40 | $httpBackend.expectGET('config.yaml'); 41 | $httpBackend.whenGET('config.yaml').respond('input: "csv"'); 42 | 43 | element = angular.element(''); 44 | element = $compile(element)(scope); 45 | scope.$apply(); 46 | })); 47 | 48 | afterEach(function(){ 49 | angular.element('body').empty(); 50 | }); 51 | 52 | it('should load the first input method if there\'s only one', function() { 53 | expect(element.html()).toMatch(/id="csvInput"/); 54 | }); 55 | }); 56 | 57 | describe('multiple input methods', function(){ 58 | // Initialize the controller and a mock scope 59 | beforeEach(inject(function ($rootScope, $controller, $compile, $httpBackend) { 60 | scope = $rootScope.$new(); 61 | main = $controller('MainController as main', { 62 | $scope: scope, 63 | appConfig: { 64 | renderer: 'c3', 65 | input: ['csv', 'spreadsheet'], 66 | colors: [ 67 | {value: 'blue'}, 68 | {value: 'red'} 69 | ], 70 | defaults: {} 71 | } 72 | }); 73 | 74 | inputCtrl = $controller('InputChooserController', { 75 | '$scope': scope 76 | }); 77 | 78 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 79 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 80 | $httpBackend.expectGET('default.config.yaml'); 81 | $httpBackend.whenGET('default.config.yaml').respond(''); 82 | $httpBackend.expectGET('config.yaml'); 83 | $httpBackend.whenGET('config.yaml').respond('input: "csv"'); 84 | 85 | element = angular.element(''); 86 | element = $compile(element)(scope); 87 | scope.$apply(); 88 | })); 89 | 90 | afterEach(function(){ 91 | angular.element('body').empty(); 92 | }); 93 | 94 | it('should show buttons for each input method', function() { 95 | var buttons = element.find('button'); 96 | 97 | expect(buttons.eq(0).text().trim()).toBe('csv'); 98 | expect(buttons.eq(1).text().trim()).toBe('spreadsheet'); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/app/components/input/inputChooser/inputChooser.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | 6 |
7 |
8 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/app/components/input/inputService/inputService.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.inputService 4 | * @description 5 | * # inputService 6 | * Service that pulls in input services specified in config.yaml. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .service('inputService', inputService); 15 | 16 | /** @ngInject */ 17 | function inputService(configProvider, $injector) { 18 | return function(appConfig){ 19 | if (!angular.isUndefined(appConfig) && !angular.isUndefined(appConfig.input)) { 20 | if (appConfig.input.constructor === Array) { 21 | return $injector.get(appConfig.input[0] + 'Input'); // If multiple, use the first to populate data. 22 | } else { 23 | return $injector.get(appConfig.input + 'Input'); 24 | } 25 | } 26 | }; 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /src/app/components/input/inputService/inputService.service.spec.js: -------------------------------------------------------------------------------- 1 | // @TODO fill out the pending specs. 2 | 3 | describe('Service: inputService', function () { 4 | 'use strict'; 5 | 6 | // load the service's module 7 | beforeEach(module('axis')); 8 | 9 | // instantiate service 10 | var inputService, 11 | inputProvider, 12 | appConfig; 13 | 14 | beforeEach(inject(function (_inputService_) { 15 | inputProvider = _inputService_; 16 | })); 17 | 18 | 19 | describe('one input service listed', function(){ 20 | beforeEach(function(){ 21 | appConfig = { 22 | input: 'csv' 23 | }; 24 | 25 | inputService = inputProvider(appConfig); 26 | }); 27 | 28 | it('should return an injected input service', function(){ 29 | expect(inputService.name).toBe('csvInputService'); 30 | }); 31 | }); 32 | 33 | describe('multiple inputs listed', function(){ 34 | beforeEach(function(){ 35 | appConfig = { 36 | input: ['spreadsheet', 'csv'] 37 | }; 38 | 39 | inputService = inputProvider(appConfig); 40 | }); 41 | 42 | it('should return the first input service if there are multiple listed', function(){ 43 | expect(inputService.name).toBe('spreadsheetInputService'); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/components/input/spreadsheet/spreadsheetInput.directive.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/components/input/spreadsheet/spreadsheetInput.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name AxisJS.directive:inputChooser 4 | * @description 5 | * Directive allowing the user choose an input method 6 | */ 7 | (function(){ 8 | 'use strict'; 9 | 10 | angular 11 | .module('axis') 12 | .directive('spreadsheetInput', spreadsheetInput); 13 | 14 | /** @ngInject */ 15 | function spreadsheetInput() { 16 | return { 17 | templateUrl: 'app/components/input/spreadsheet/spreadsheetInput.directive.html', 18 | restrict: 'E', 19 | scope: { 20 | main: '=config' 21 | }, 22 | controllerAs: 'sheet', 23 | controller: function($scope) { 24 | var main = $scope.main; 25 | var vm = this; 26 | 27 | vm.inputs = main.inputs; 28 | 29 | /** 30 | * Clears HOT if data is from pasting. 31 | * @param {array} changes From HOT, the changes being made to table. 32 | * @param {string} source From HOT, the source of the changes. 33 | * @return {void} 34 | */ 35 | vm.clearOnPaste = function(changes, source){ 36 | if (source === 'paste') { 37 | this.clear(); 38 | } 39 | }; 40 | } 41 | }; 42 | } 43 | })(); 44 | -------------------------------------------------------------------------------- /src/app/components/input/spreadsheet/spreadsheetInput.directive.spec.js: -------------------------------------------------------------------------------- 1 | // @TODO fill out the pending specs. 2 | 3 | describe('Directive: spreadsheetInput', function () { 4 | 'use strict'; 5 | 6 | // load the directive's module 7 | beforeEach(module('axis')); 8 | 9 | var element, 10 | scope; 11 | 12 | beforeEach(inject(function ($rootScope, $compile) { 13 | // scope = $rootScope.$new(); 14 | // element = angular.element(''); 15 | // element = $compile(element)(scope); 16 | // angular.element('body').append(element); 17 | // angular.element('#data-input').focus(); // grant focus 18 | // angular.element.event.trigger('keydown', {keyCode: 9}); // emulate pressing tab 19 | })); 20 | 21 | it('should attach the input provider from main controller'); 22 | it('should clear HandsOnTable before adding data if data is pasted'); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/input/spreadsheet/spreadsheetInput.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/components/input/spreadsheet/spreadsheetInput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.spreadsheetInput 4 | * @description 5 | * # spreadsheetInput 6 | * Enables a fancy user-friendly spreadsheet input. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('spreadsheetInput', spreadsheetInput); 15 | 16 | /** @ngInject */ 17 | function spreadsheetInput() { 18 | var defaultSheet = [ 19 | ['data1', 'data2'], 20 | [30, 50], 21 | [200, 20], 22 | [100, 10], 23 | [400, 40], 24 | [150, 15], 25 | [250, 25] 26 | ]; 27 | 28 | var parseSheet = function(scope) { 29 | if (scope.inputs.inputData) { 30 | scope.chartData = []; // Empty, or else new column names will break ng-grid 31 | scope.columns = []; // Clear existing 32 | scope.config.data.columns = []; 33 | scope.chartData = angular.copy(scope.inputs.inputData); 34 | var cols = []; 35 | 36 | // Convert objects into arrays. Might be better long-term to use C3's JSON input. 37 | // Lots of this stuff is C3-specific. TODO move to c3Service. 38 | if (scope.chartData.length > 0) { 39 | scope.columns = scope.chartData[0].filter(function(v) { 40 | return v != undefined && v !== ''; /* jshint ignore:line */ 41 | }); 42 | 43 | scope.chartData.shift(); 44 | angular.forEach(scope.columns, function(colName, index) { 45 | var column = []; 46 | column.push(colName); 47 | angular.forEach(scope.chartData, function(datum) { 48 | if (datum[index]) { 49 | column.push(datum[index]); 50 | } 51 | }); 52 | 53 | cols.push(column); 54 | }); 55 | 56 | scope.config.data.columns = cols; 57 | } 58 | } 59 | 60 | return scope; 61 | }; 62 | 63 | var convertColsToRows = function(columns) { 64 | var data = []; 65 | var headers = []; 66 | for (var i = 0; i < columns.length; i++) { 67 | headers.push(columns[i].shift()); 68 | for (var j = 0; j < columns[i].length; j++) { 69 | if (!data[j]) { 70 | data[j] = []; 71 | } 72 | 73 | data[j][i] = columns[i][j]; 74 | } 75 | } 76 | 77 | return [headers].concat(data); 78 | }; 79 | 80 | // Public API here 81 | return { 82 | /** 83 | * Service name 84 | * @type {String} 85 | */ 86 | name: 'spreadsheetInputService', 87 | 88 | /** 89 | * Validate spreadsheet input 90 | * @param {array} value The output from HOT.getData() 91 | * @return {boolean} True if validates, false if not. 92 | */ 93 | validate: function(value) { 94 | return value instanceof Array; // TODO write a real validator. 95 | }, 96 | 97 | /** 98 | * The default sheet to populate AxisJS with. 99 | * @type {array} 100 | */ 101 | defaultData: defaultSheet, 102 | 103 | /** 104 | * Parses sheet into columns. Called whenever sheet updated. 105 | * @param {object} scope The AxisJS scope object. 106 | * @return {object} The updated scope object. 107 | */ 108 | input: function(scope) { 109 | return parseSheet(scope); 110 | }, 111 | 112 | /** 113 | * Convert array columns to Handsontable-compatible array. 114 | * @param {array} data An array of array columns. 115 | * @return {array} Array with header column values as first element. 116 | */ 117 | convert: function(data) { 118 | return convertColsToRows(data); 119 | } 120 | }; 121 | } 122 | })(); 123 | -------------------------------------------------------------------------------- /src/app/components/input/spreadsheet/spreadsheetInput.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for spreadsheetInputService 3 | */ 4 | describe('Service: spreadsheetInput', function() { 5 | 'use strict'; 6 | 7 | // load the service's module 8 | beforeEach(module('axis')); 9 | 10 | // instantiate service 11 | var spreadsheetInput, 12 | testScope, 13 | output; 14 | 15 | beforeEach(inject(function(_spreadsheetInput_, $rootScope) { 16 | spreadsheetInput = _spreadsheetInput_; 17 | testScope = $rootScope.$new(); 18 | testScope.inputs = { 19 | inputData: [ 20 | ['header1', 'header2'], 21 | ['data1', 'data2'] 22 | ] 23 | }; 24 | testScope.config = { 25 | data: { 26 | types: {} 27 | } 28 | }; 29 | })); 30 | 31 | it('should properly identify itself', function(){ 32 | expect(spreadsheetInput.name).toBe('spreadsheetInputService'); 33 | }); 34 | 35 | it('should be able to validate an input spreadsheet', function(){ 36 | expect(spreadsheetInput.validate([])).toBeTruthy(); // TODO write real validator for HOT. 37 | }); 38 | 39 | it('should be able input a spreadsheet', function(){ 40 | output = spreadsheetInput.input(testScope); 41 | 42 | expect(output.columns).toEqual(['header1', 'header2']); 43 | expect(output.chartData).toEqual([ ['data1', 'data2'] ]); 44 | }); 45 | 46 | it('should be able to convert columnar data to HOT\'s row structure', function(){ 47 | output = spreadsheetInput.convert([['data1', 1, 2, 3], ['data2', 4, 5, 6]]); 48 | 49 | expect(output[0]).toEqual(['data1', 'data2']); 50 | expect(output[1]).toEqual([1, 4]); 51 | expect(output[2]).toEqual([2, 5]); 52 | expect(output[3]).toEqual([3, 6]); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/app/components/output/axis/axisOutput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.axisOutput 4 | * @description 5 | * # axisOutput 6 | * Service for outputting to Axis Server or AxisMaker. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('axisOutput', axisOutput); 15 | 16 | /** @ngInject */ 17 | function axisOutput(genericOutput, $window, chartService) { 18 | var maker = angular.copy(genericOutput); 19 | var parent = $window.parent; 20 | 21 | maker.serviceConfig = { 22 | type: 'export', // Options: 'save' and 'export'. 23 | label: 'Save' // Label to use on button. 24 | }; 25 | 26 | maker.preprocess = function(scope) { 27 | var chartConfig = angular.copy(scope.config); 28 | chartConfig = chartService(scope.appConfig).saveCallbacks(chartConfig); 29 | var chartTitle = chartConfig.hasOwnProperty('chartTitle') ? chartConfig.chartTitle : 'A Chart'; 30 | var axisConfig = String(angular.toJson(chartConfig)); 31 | var axisChart = String(angular.element('.savePNG').attr('href')); 32 | return { 33 | config: axisConfig, 34 | png: axisChart, 35 | title: chartTitle 36 | }; 37 | }; 38 | 39 | maker.process = function(payload) { 40 | // Have use PostMessage to push outside the iframe 41 | parent.postMessage( 42 | JSON.stringify(payload), 43 | '*' // The intended origin, in this example we use the any origin wildcard. 44 | ); 45 | }; 46 | 47 | /* istanbul ignore next */ 48 | maker.complete = function() { 49 | console.log('Complete.'); 50 | }; 51 | 52 | maker.export = function(scope) { 53 | var payload = maker.preprocess(scope); 54 | maker.process(payload); 55 | maker.complete(); 56 | }; 57 | 58 | maker.import = function(inputService, appConfig) { 59 | if (typeof parent.axisConfig !== 'undefined') { 60 | var importData = {}; 61 | importData.config = angular.fromJson(parent.axisConfig); 62 | importData.inputData = inputService.convert(importData.config.data.columns); 63 | 64 | var config = importData.config; 65 | importData.config = chartService(appConfig).restoreCallbacks(importData.config); 66 | 67 | if (importData.config && importData.inputData) { 68 | return importData; 69 | } 70 | } 71 | }; 72 | 73 | return maker; 74 | } 75 | })(); 76 | -------------------------------------------------------------------------------- /src/app/components/output/axis/axisOutput.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a basic test suite for axisOutput. It would be good to decouple it from MainController 3 | * because there is some weird testing behaviour as a result. 4 | */ 5 | 6 | describe('Service: axisOutput', function () { 7 | 'use strict'; 8 | 9 | // load the directive's module 10 | beforeEach(module('axis')); 11 | 12 | var MainCtrl, 13 | element, 14 | scope, 15 | body, 16 | c3, 17 | configObjectString, 18 | parent, 19 | spy, 20 | imported; 21 | 22 | 23 | // Initialize the controller and a mock MainCtrl scope 24 | beforeEach(inject(function ($controller, $rootScope, $httpBackend, $window, $compile) { 25 | c3 = $window.c3; 26 | body = angular.element('body'); 27 | body.empty(); // clean up previous tests 28 | body.append(angular.element('')); 29 | body.append(angular.element('
')); 30 | 31 | c3.generate({data: {columns: [['data1', 1, 2, 3], ['data2', 4, 5, 6]]}}); 32 | 33 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 34 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 35 | $httpBackend.expectGET('default.config.yaml'); 36 | $httpBackend.whenGET('default.config.yaml').respond({}); 37 | $httpBackend.expectGET('config.yaml'); 38 | $httpBackend.whenGET('config.yaml').respond({}); 39 | 40 | scope = $rootScope.$new(); 41 | MainCtrl = $controller('MainController as main', { 42 | $scope: scope, 43 | appConfig: { 44 | renderer: 'c3', 45 | input: 'csv', 46 | save: [ 47 | 'png', 48 | 'svg' 49 | ], 50 | export: [ 51 | 'Embed code' 52 | ], 53 | colors: [ 54 | {value: 'blue'}, 55 | {value: 'red'} 56 | ], 57 | defaults: {}, 58 | } 59 | }); 60 | 61 | configObjectString = 'eyJkYXRhIjp7IngiOiIiLCJ5IjoiIiwieTIiOiIiLCJjb2x1bW5zIjpbWyJkYXRhMSIsIjMwIiwiMjAwIiwiMTAwIiwiNDAwIiwiMTUwIiwiMjUwIl0sWyJkYXRhMiIsIjUwIiwiMjAiLCIxMCIsIjQwIiwiMTUiLCIyNSJdXSwiYXhlcyI6e30sImdyb3VwcyI6e30sInR5cGUiOiIiLCJ0eXBlcyI6eyJkYXRhMSI6ImxpbmUiLCJkYXRhMiI6ImxpbmUifSwiY29sb3JzIjp7ImRhdGExIjoiIzc4QjhERiIsImRhdGEyIjoiI0FGQ0JDRSJ9fSwiYXhpcyI6eyJ4Ijp7InNob3ciOnRydWUsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX0sInkiOnsic2hvdyI6dHJ1ZSwiYWNjdXJhY3kiOjAsInByZWZpeCI6IiIsInN1ZmZpeCI6IiIsInRpY2siOnt9fSwieTIiOnsic2hvdyI6ZmFsc2UsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX19LCJwb2ludCI6eyJzaG93IjpmYWxzZX0sImdyb3VwcyI6e30sImRlZmF1bHRDb2xvcnMiOlsiIzFmNzdiNCIsIiNhZWM3ZTgiLCIjZmY3ZjBlIiwiI2ZmYmI3OCIsIiMyY2EwMmMiLCIjOThkZjhhIiwiI2Q2MjcyOCIsIiNmZjk4OTYiLCIjOTQ2N2JkIiwiI2M1YjBkNSIsIiM4YzU2NGIiLCIjYzQ5Yzk0IiwiI2UzNzdjMiIsIiNmN2I2ZDIiLCIjN2Y3ZjdmIiwiI2M3YzdjNyIsIiNiY2JkMjIiLCIjZGJkYjhkIiwiIzE3YmVjZiIsIiM5ZWRhZTUiXSwiY2hhcnRUaXRsZSI6InRlc3RpbmciLCJjaGFydENyZWRpdCI6IiIsImNoYXJ0U291cmNlIjoiIiwiY2hhcnRXaWR0aCI6MTAwMCwiY2hhcnRHbG9iYWxUeXBlIjoic2VyaWVzIiwiY2hhcnRBY2N1cmFjeSI6MSwiY21zIjpmYWxzZSwicGllIjp7ImxhYmVsIjp7fX0sImRvbnV0Ijp7ImxhYmVsIjp7fX0sImdhdWdlIjp7ImxhYmVsIjp7fX19'; 62 | 63 | element = angular.element(''); 64 | element = $compile(element)(scope); 65 | scope.$apply(); 66 | 67 | body.append(element); 68 | angular.element('[export-chart="save"]').eq(0).trigger('click'); 69 | 70 | parent = $window.parent = {}; 71 | spy = jasmine.createSpy('postmessage'); 72 | parent.postMessage = spy; 73 | parent.axisConfig = atob('eyJkYXRhIjp7IngiOiIiLCJ5IjoiIiwieTIiOiIiLCJjb2x1bW5zIjpbWyJkYXRhMSIsIjMwIiwiMjAwIiwiMTAwIiwiNDAwIiwiMTUwIiwiMjUwIl0sWyJkYXRhMiIsIjUwIiwiMjAiLCIxMCIsIjQwIiwiMTUiLCIyNSJdXSwiYXhlcyI6e30sImdyb3VwcyI6e30sInR5cGUiOiIiLCJ0eXBlcyI6eyJkYXRhMSI6ImxpbmUiLCJkYXRhMiI6ImxpbmUifSwiY29sb3JzIjp7ImRhdGExIjoiIzc4QjhERiIsImRhdGEyIjoiI0FGQ0JDRSJ9fSwiYXhpcyI6eyJ4Ijp7InNob3ciOnRydWUsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX0sInkiOnsic2hvdyI6dHJ1ZSwiYWNjdXJhY3kiOjAsInByZWZpeCI6IiIsInN1ZmZpeCI6IiIsInRpY2siOnt9fSwieTIiOnsic2hvdyI6ZmFsc2UsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX19LCJwb2ludCI6eyJzaG93IjpmYWxzZX0sImdyb3VwcyI6e30sImRlZmF1bHRDb2xvcnMiOlsiIzFmNzdiNCIsIiNhZWM3ZTgiLCIjZmY3ZjBlIiwiI2ZmYmI3OCIsIiMyY2EwMmMiLCIjOThkZjhhIiwiI2Q2MjcyOCIsIiNmZjk4OTYiLCIjOTQ2N2JkIiwiI2M1YjBkNSIsIiM4YzU2NGIiLCIjYzQ5Yzk0IiwiI2UzNzdjMiIsIiNmN2I2ZDIiLCIjN2Y3ZjdmIiwiI2M3YzdjNyIsIiNiY2JkMjIiLCIjZGJkYjhkIiwiIzE3YmVjZiIsIiM5ZWRhZTUiXSwiY2hhcnRUaXRsZSI6InRlc3RpbmciLCJjaGFydENyZWRpdCI6IiIsImNoYXJ0U291cmNlIjoiIiwiY2hhcnRXaWR0aCI6MTAwMCwiY2hhcnRHbG9iYWxUeXBlIjoic2VyaWVzIiwiY2hhcnRBY2N1cmFjeSI6MSwiY21zIjpmYWxzZSwicGllIjp7ImxhYmVsIjp7fX0sImRvbnV0Ijp7ImxhYmVsIjp7fX0sImdhdWdlIjp7ImxhYmVsIjp7fX19'); 74 | })); 75 | 76 | 77 | it('should return an object with config, base64 PNG and title on preprocess()', inject(function(axisOutput){ 78 | var preprocessed = axisOutput.preprocess(scope); 79 | expect(typeof preprocessed.config).toBe('string'); 80 | expect(preprocessed.png.indexOf('data:image/png;base64,')).toBe(0); 81 | expect(preprocessed.title).toBeUndefined(); 82 | })); 83 | 84 | 85 | it('should push a postMessage to parent iframe on process()', inject(function(axisOutput){ 86 | axisOutput.process({}); 87 | expect(spy).toHaveBeenCalled(); 88 | })); 89 | 90 | it('should run preprocess, process and complete on axisOutput.export()', inject(function(axisOutput){ 91 | spyOn(axisOutput, 'preprocess'); 92 | spyOn(axisOutput, 'process'); 93 | spyOn(axisOutput, 'complete'); 94 | axisOutput.export(scope); 95 | 96 | expect(axisOutput.preprocess).toHaveBeenCalled(); 97 | expect(axisOutput.process).toHaveBeenCalled(); 98 | expect(axisOutput.complete).toHaveBeenCalled(); 99 | })); 100 | 101 | it('should return an object with config and input data on axisOutput.import()', inject(function(axisOutput, inputService){ 102 | var input = inputService(scope.main.appConfig); 103 | imported = axisOutput.import(input); 104 | expect(imported.inputData.split('\t').length).toBe(8); 105 | })); 106 | }); 107 | -------------------------------------------------------------------------------- /src/app/components/output/embedcode/embedcode.modal.html: -------------------------------------------------------------------------------- 1 |

2 | Embed code:
3 | Copy and paste this into a new HTML document 4 |

5 | 6 |
7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/components/output/embedcode/embedcodeOutput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.embedcodeOutput 4 | * @description 5 | * # embedcodeOutput 6 | * Factory to generate embed codes via a modal window. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('embedcodeOutput', embedcodeOutput); 15 | 16 | /** @ngInject */ 17 | function embedcodeOutput(genericOutput, chartService, $modal, $window) { 18 | var embed = angular.copy(genericOutput); 19 | var JSONfn = $window.JSONfn; 20 | 21 | /** 22 | * Service name 23 | * @type {String} 24 | */ 25 | embed.name = 'embedcodeOutputService'; 26 | 27 | /** 28 | * Service config. This isn't really used yet. 29 | * @type {Object} 30 | */ 31 | embed.serviceConfig = { 32 | type: 'export', // Options: 'save' and 'export'. 33 | label: 'Generate embed code' // Label to use on button. 34 | }; 35 | 36 | /** 37 | * Preprocess scope 38 | * @param {Object} scope Axis MainCtrl scope 39 | * @return {object} Chart config and deps. 40 | */ 41 | embed.preprocess = function(mainScope){ 42 | var chartConfig = angular.copy(mainScope.config); // Needs to copy or else scope breaks. See #45. 43 | chartConfig.bindto = '#chart-' + Math.floor((Math.random()*10000)+1); 44 | return { 45 | config: chartConfig, 46 | dependencies: chartService(mainScope.appConfig).dependencies 47 | }; 48 | }; 49 | 50 | /** 51 | * Process request. 52 | * @param {Object} payload Needs config and dependencies. 53 | * @return {Object} Embed code output with depenencies (complete) and not (partial). 54 | */ 55 | embed.process = function(payload) { 56 | var output = {}; 57 | var deps = []; 58 | var code = []; 59 | var config = JSONfn.stringify(payload.config); 60 | 61 | // Needs to be above script declarations. 62 | deps.push('
'); 63 | 64 | angular.forEach(payload.dependencies.css, function(v) { 65 | deps.push(''); 66 | }); 67 | 68 | angular.forEach(payload.dependencies.js, function(v) { 69 | deps.push(''); 70 | }); 71 | 72 | code.push( 73 | '' 79 | ); 80 | 81 | output.complete = deps.concat(code).join('\n'); // TODO figure out how to pretty-print. 82 | output.partial = [deps[0]].concat(code).join('\n'); 83 | 84 | return output; 85 | }; 86 | 87 | /** 88 | * Open a modal with the complete embed code. 89 | * @param {string} output Rendered output 90 | * @return {embedModal} Embed Modal 91 | * 92 | * @NB Istanbul instrumentation disabled until I can figure how to mock $modal here. 93 | */ 94 | embed.complete = function(output) { 95 | /* istanbul ignore next */ 96 | $modal.open({ 97 | templateUrl: 'app/components/output/embedcode/embedcode.modal.html', 98 | controller: 'EmbedcodeOutputController as embed', 99 | resolve: { 100 | output: function(){ 101 | return output; 102 | } 103 | } 104 | }); 105 | }; 106 | 107 | return embed; 108 | } 109 | 110 | 111 | angular 112 | .module('axis') 113 | .controller('EmbedcodeOutputController', EmbedcodeOutputController); 114 | 115 | function EmbedcodeOutputController(output) { 116 | var vm = this; 117 | vm.includeDeps = true; 118 | vm.output = vm.includeDeps ? output.complete : output.partial; 119 | vm.updateOutput = function(deps) { 120 | if (deps) { 121 | vm.output = output.complete; 122 | } else { 123 | vm.output = output.partial; 124 | } 125 | }; 126 | } 127 | })(); 128 | -------------------------------------------------------------------------------- /src/app/components/output/embedcode/embedcodeOutput.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: embedcodeOutput', function() { 2 | 'use strict'; 3 | 4 | // load the service's module 5 | beforeEach(module('axis')); 6 | 7 | // instantiate service 8 | var embedcodeOutput, 9 | scope, 10 | MainCtrl, 11 | EmbedCtrl, 12 | embed, 13 | output; 14 | 15 | beforeEach(inject(function(_embedcodeOutput_, $controller, $rootScope, $httpBackend) { 16 | embedcodeOutput = _embedcodeOutput_; 17 | scope = $rootScope.$new(); 18 | $httpBackend.expectGET('default.config.yaml'); 19 | $httpBackend.whenGET('default.config.yaml').respond(''); 20 | $httpBackend.expectGET('config.yaml'); 21 | $httpBackend.whenGET('config.yaml').respond(''); 22 | $httpBackend.expectGET('partials/configChooser.html'); // due to angular-off-canvas. 23 | $httpBackend.whenGET('partials/configCh dooser.html').respond(''); 24 | $httpBackend.expectGET('partials/outputModal.html'); // due to angular-off-canvas. 25 | $httpBackend.whenGET('partials/outputModal.html').respond(''); 26 | 27 | MainCtrl = $controller('MainController as main', { 28 | $scope: scope, 29 | appConfig: { 30 | renderer: 'c3', 31 | input: 'csv', 32 | export: ['embedcode'], 33 | colors: [ 34 | {value: 'blue'}, 35 | {value: 'red'} 36 | ], 37 | defaults: {} 38 | } 39 | }); 40 | })); 41 | 42 | it('should properly identify itself', function(){ 43 | expect(embedcodeOutput.name).toBe('embedcodeOutputService'); 44 | }); 45 | 46 | it('should return the chart config and dependencies on preprocess()', function(){ 47 | output = embedcodeOutput.preprocess(scope); 48 | expect(output.config).toBeDefined(); 49 | expect(output.dependencies).toBeDefined(); 50 | }); 51 | 52 | it('should return an output object on process()', function(){ 53 | var preprocessed = embedcodeOutput.preprocess(scope); 54 | output = embedcodeOutput.process(preprocessed); 55 | 56 | expect(output.complete).toBeDefined(); 57 | expect(output.partial).toBeDefined(); 58 | expect(typeof output.complete).toBe('string'); 59 | expect(typeof output.partial).toBe('string'); 60 | }); 61 | 62 | describe('embedcodeOutput.complete() method', function(){ 63 | var pre, proc; 64 | beforeEach(inject(function($controller){ 65 | pre = embedcodeOutput.preprocess(scope); 66 | proc = embedcodeOutput.process(pre); 67 | EmbedCtrl = $controller('EmbedcodeOutputController as EmbedCtrl', { 68 | 'output': proc, 69 | '$scope': scope 70 | }); 71 | 72 | embed = scope.EmbedCtrl; 73 | })); 74 | 75 | it('should default to including dependencies', function(){ 76 | expect(embed.includeDeps).toBeTruthy(); 77 | }); 78 | 79 | it('should output complete JS + deps if includeDeps is true', function(){ 80 | embed.includeDeps = true; 81 | embed.updateOutput(embed.includeDeps); 82 | 83 | expect(embed.output).toMatch(/d3\.min\.js/); 84 | expect(embed.output).toMatch(/var configJSON/); 85 | }); 86 | 87 | it('should output JS and no deps if includeDeps is false', function(){ 88 | embed.includeDeps = false; 89 | embed.updateOutput(embed.includeDeps); 90 | 91 | expect(embed.output).not.toMatch(/d3\.min\.js/); 92 | expect(embed.output).toMatch(/var configJSON/); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/app/components/output/generic/genericOutput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.genericOutput 4 | * @description 5 | * # genericOutput 6 | * Output service primitive. Useless as-is; meant to be extended. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('genericOutput', genericOutput); 15 | 16 | function genericOutput() { 17 | /** 18 | * Output service configuration options (currently unused) 19 | * @type {Object} 20 | */ 21 | this.serviceConfig = { 22 | type: 'save', // Options: 'save' and 'export' 23 | label: '' 24 | }; 25 | 26 | /** 27 | * Preprocess the data before sending somewhere 28 | * @param {Object} scope Axis scope object. 29 | * @return {Object} Updated scope object. 30 | */ 31 | this.preprocess = function(scope){ 32 | return scope; 33 | }; 34 | 35 | /** 36 | * Sends output from preprocess somewhere 37 | * @param {Object} payload Payload for server or whatever. 38 | * @return {Object} Response from server or whatever. 39 | */ 40 | this.process = function(payload){ 41 | return payload; 42 | }; 43 | 44 | /** 45 | * Fires once process is complete. 46 | * @param {Object} output Response from server or whatever 47 | * @return {Object} Whatever final transformations before passing back 48 | */ 49 | this.complete = function(output){ 50 | return output; // This generally doesn't return anything, but is doing so for tests in this case. 51 | }; 52 | 53 | /** 54 | * Passes a scope into the service, manipulates it, sends it somewhere, etc. 55 | * @param {Object} scope Axis scope object. 56 | * @return {Object} Output from the genericOutput.complete stage. 57 | */ 58 | this.export = function(scope){ 59 | var payload = this.preprocess(scope); 60 | var output = this.process(payload); 61 | return this.complete(output); 62 | }; 63 | 64 | return this; 65 | } 66 | })(); -------------------------------------------------------------------------------- /src/app/components/output/generic/genericOutput.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @TODO write a spec for genericOutput 3 | */ 4 | 5 | describe('Service: outputService', function () { 6 | 'use strict'; 7 | 8 | // load the service's module 9 | beforeEach(module('axis')); 10 | 11 | // instantiate service 12 | var outputService, 13 | genericOutput; 14 | 15 | beforeEach(inject(function (_outputService_) { 16 | outputService = _outputService_; 17 | genericOutput = outputService({'llama': 'duck'}, 'generic'); 18 | })); 19 | 20 | it('should return identity on preprocess()'); 21 | it('should return identity on process()'); 22 | it('should return identity on complete()'); 23 | it('should run preprocess, process and complete on export(), returning identity'); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/output/outputService/outputService.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name Axis.outputService 4 | * @description 5 | * # outputProvider 6 | * Service that pulls in output services specified in config.yaml. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .service('outputService', outputService); 15 | 16 | /** @ngInject */ 17 | function outputService(configProvider, $injector) { 18 | return function(scope, type){ 19 | var output = $injector.get(type + 'Output'); 20 | return output.export(scope); 21 | }; 22 | } 23 | })(); -------------------------------------------------------------------------------- /src/app/components/output/outputService/outputService.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: outputService', function () { 2 | 'use strict'; 3 | 4 | // load the service's module 5 | beforeEach(module('axis')); 6 | 7 | // instantiate service 8 | var outputService, 9 | genericOutput; 10 | 11 | beforeEach(inject(function (_outputService_) { 12 | outputService = _outputService_; 13 | genericOutput = outputService({'llama': 'duck'}, 'generic'); 14 | })); 15 | 16 | it('should return the above object', function () { 17 | expect(genericOutput.llama).toBe('duck'); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/app/components/output/pdf/pdfOutput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.pdfOutput 4 | * @description 5 | * # pdfOutput 6 | * Produces PDFs of the chart. 7 | */ 8 | /* global jsPDF*/ 9 | 10 | (function(){ 11 | 'use strict'; 12 | 13 | angular 14 | .module('axis') 15 | .service('pdfOutput', pdfOutput); 16 | 17 | /** @ngInject */ 18 | function pdfOutput(genericOutput) { 19 | var pdf = angular.copy(genericOutput); 20 | 21 | pdf.name = 'pdfOutputService'; 22 | 23 | pdf.preprocess = function(scope) { 24 | return { 25 | data: document.getElementById('canvas').toDataURL(), 26 | margins: scope.appConfig['print margins'] ? scope.appConfig['print margins'] : undefined 27 | }; 28 | }; 29 | 30 | pdf.process = function(payload){ 31 | var doc = new jsPDF('l', 'pt'); 32 | doc.addImage(payload.data, 'PNG', 0, 0); // TODO add margins 33 | return doc; 34 | }; 35 | 36 | pdf.complete = function(output) { 37 | output.save('axis.pdf'); // provided by jsPDF 38 | }; 39 | 40 | return pdf; 41 | } 42 | })(); 43 | -------------------------------------------------------------------------------- /src/app/components/output/png/pngOutput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.pngOutput 4 | * @description 5 | * # pngOutput 6 | * Service to output PNGs. 7 | * @TODO move the PNG creation logic here. 8 | */ 9 | // angular.module('axis') 10 | // .service('pngOutput', function pngOutput() { 11 | // 'use strict'; 12 | // // AngularJS will instantiate a singleton by calling "new" on this function 13 | // }); 14 | -------------------------------------------------------------------------------- /src/app/components/output/png/pngOutput.service.spec.js: -------------------------------------------------------------------------------- 1 | // Disabling until pngOutput service is populated. 2 | xdescribe('Service: pngService', function () { 3 | 'use strict'; 4 | 5 | // load the service's module 6 | beforeEach(module('axis')); 7 | 8 | // instantiate service 9 | var pngService; 10 | beforeEach(inject(function (_pngService_) { 11 | pngService = _pngService_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!pngService).toBe(true); 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /src/app/components/output/svg/svgOutput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.svgOutput 4 | * @description 5 | * # svgOutput 6 | * Service to generate SVGs. 7 | * @TODO move the SVG creation code from exportChart directive to here. 8 | */ 9 | // angular.module('axis') 10 | // .service('svgOutput', function svgOutput() { 11 | // 'use strict'; 12 | // // AngularJS will instantiate a singleton by calling "new" on this function 13 | // }); 14 | -------------------------------------------------------------------------------- /src/app/components/output/svg/svgOutput.service.spec.js: -------------------------------------------------------------------------------- 1 | // Disabled until svgOutput service is populated. 2 | xdescribe('Service: svgOutput', function () { 3 | 'use strict'; 4 | 5 | // load the service's module 6 | beforeEach(module('axis')); 7 | 8 | // instantiate service 9 | var svgOutput; 10 | beforeEach(inject(function (_svgOutput_) { 11 | svgOutput = _svgOutput_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!svgOutput).toBe(true); 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /src/app/components/output/wordpress/wordpressOutput.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name axis.wordpressOutput 4 | * @description 5 | * # wordpressOutput 6 | * Handles migrating data to and from AxisWP. 7 | */ 8 | 9 | (function(){ 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .factory('wordpressOutput', wordpressOutput); 15 | 16 | /** @ngInject */ 17 | function wordpressOutput(genericOutput, $http, $window) { 18 | function WordPressOutputServiceException(data, status, headers, config) { 19 | this.name = 'WordPressOutputServiceException'; 20 | this.message = 'WordPressOutputServiceException: ' + status; 21 | this.data = data; 22 | this.status = status; 23 | this.headers = headers; 24 | this.config = config; 25 | } 26 | WordPressOutputServiceException.prototype = new Error(); 27 | WordPressOutputServiceException.prototype.constructor = WordPressOutputServiceException; 28 | 29 | var wordpress = angular.copy(genericOutput); 30 | 31 | wordpress.serviceConfig = { 32 | type: 'export', // Options: 'save' and 'export'. 33 | label: 'Save to WordPress' // Label to use on button. 34 | }; 35 | 36 | wordpress.preprocess = function(scope){ 37 | var chartConfig = angular.copy(scope.config); 38 | chartConfig.axis.x.tick.format = chartConfig.axis.x.tick.format.toString(); 39 | chartConfig.axis.y.tick.format = chartConfig.axis.y.tick.format.toString(); 40 | chartConfig.axis.y2.tick.format = chartConfig.axis.y2.tick.format.toString(); 41 | chartConfig.pie.label.format = chartConfig.pie.label.format.toString(); 42 | chartConfig.donut.label.format = chartConfig.donut.label.format.toString(); 43 | chartConfig.gauge.label.format = chartConfig.gauge.label.format.toString(); 44 | var axisConfig = String(angular.toJson(chartConfig)); 45 | var axisChart = String(angular.element('.savePNG').attr('href')); 46 | var axisWP = parent.tinymce.activeEditor.windowManager.getParams().axisWP; 47 | return { 48 | action: 'insert_axis_attachment', 49 | axisConfig: axisConfig, 50 | axisChart: axisChart, 51 | parentID: axisWP.parentID 52 | }; 53 | }; 54 | 55 | wordpress.process = function(payload){ 56 | // Have WordPress process the data-URI PNG and return some config data 57 | $http.post(parent.ajaxurl, payload, { 58 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}, // $http sends as JSON, WP expects form. 59 | transformRequest: function(obj) { // Via http://stackoverflow.com/a/19270196/467760 60 | var str = []; 61 | for (var key in obj) { 62 | /* istanbul ignore next */ 63 | if (obj[key] instanceof Array) { // Transform array into flat object. 64 | for(var idx in obj[key]){ 65 | var subObj = obj[key][idx]; 66 | for(var subKey in subObj){ 67 | str.push(encodeURIComponent(key) + '[' + idx + '][' + encodeURIComponent(subKey) + ']=' + encodeURIComponent(subObj[subKey])); 68 | } 69 | } 70 | } else { // Already flat; just push key-value pairs. 71 | str.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])); 72 | } 73 | } 74 | return str.join('&'); 75 | } 76 | }) 77 | .success(function(res){ 78 | res = angular.fromJson(res); 79 | parent.tinymce.activeEditor.insertContent('

'); 80 | parent.tinymce.activeEditor.windowManager.close(); 81 | }) 82 | .error(function(data, status, headers, config){ 83 | throw new WordPressOutputServiceException(data, status, headers, config); 84 | }); 85 | }; 86 | 87 | wordpress.export = function(scope){ 88 | var payload = wordpress.preprocess(scope); 89 | wordpress.process(payload); 90 | wordpress.complete(); 91 | }; 92 | 93 | wordpress.import = function(inputService){ 94 | if (typeof parent.tinymce !== 'undefined' && typeof parent.tinymce.activeEditor.windowManager.getParams().axisJS !== 'undefined') { 95 | var importData = {}; 96 | importData.config = angular.fromJson($window.atob(parent.tinymce.activeEditor.windowManager.getParams().axisJS)); 97 | importData.inputData = inputService.convert(importData.config.data.columns); 98 | 99 | var config = importData.config; 100 | /* jshint ignore:start */ // Whatever Crockford, all the cool kids use eval. 101 | importData.config.axis.x.tick.format = eval('(' + importData.config.axis.x.tick.format + ')'); 102 | importData.config.axis.y.tick.format = eval('(' + importData.config.axis.y.tick.format + ')'); 103 | importData.config.axis.y2.tick.format = eval('(' + importData.config.axis.y2.tick.format + ')'); 104 | importData.config.donut.label.format = eval('(' + importData.config.donut.label.format + ')'); 105 | importData.config.pie.label.format = eval('(' + importData.config.pie.label.format + ')'); 106 | importData.config.gauge.label.format = eval('(' + importData.config.gauge.label.format + ')'); 107 | /* jshint ignore:end */ // WHAT EVAL LURKS IN THE HEARTS OF MEN?! 108 | 109 | if (importData.config && importData.inputData) { 110 | return importData; 111 | } 112 | } 113 | }; 114 | 115 | return wordpress; 116 | } 117 | })(); 118 | -------------------------------------------------------------------------------- /src/app/components/output/wordpress/wordpressOutput.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: wordpressOutput', function () { 2 | 'use strict'; 3 | 4 | // load the directive's module 5 | beforeEach(module('axis')); 6 | 7 | var MainCtrl, 8 | insertContent, 9 | element, 10 | scope, 11 | body, 12 | c3; 13 | 14 | // Initialize the controller and a mock MainCtrl scope 15 | beforeEach(inject(function ($controller, $rootScope, $httpBackend, $window) { 16 | c3 = $window.c3; 17 | body = angular.element('body'); 18 | body.empty(); // clean up previous tests 19 | body.append(angular.element('png')); 20 | body.append(angular.element('svg')); 21 | body.append(angular.element('')); 22 | body.append(angular.element('
')); 23 | 24 | c3.generate({data: {columns: [['data1', 1, 2, 3], ['data2', 4, 5, 6]]}}); 25 | 26 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 27 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 28 | $httpBackend.expectGET('default.config.yaml'); 29 | $httpBackend.whenGET('default.config.yaml').respond({}); 30 | $httpBackend.expectGET('config.yaml'); 31 | $httpBackend.whenGET('config.yaml').respond({}); 32 | 33 | scope = $rootScope.$new(); 34 | MainCtrl = $controller('MainController as main', { 35 | $scope: scope, 36 | appConfig: { 37 | renderer: 'c3', 38 | input: 'csv', 39 | save: [ 40 | 'png', 41 | 'svg' 42 | ], 43 | export: [ 44 | 'Embed code' 45 | ], 46 | colors: [ 47 | {value: 'blue'}, 48 | {value: 'red'} 49 | ], 50 | defaults: {}, 51 | } 52 | }); 53 | 54 | var configObjectString = 'eyJkYXRhIjp7IngiOiIiLCJ5IjoiIiwieTIiOiIiLCJjb2x1bW5zIjpbWyJkYXRhMSIsIjMwIiwiMjAwIiwiMTAwIiwiNDAwIiwiMTUwIiwiMjUwIl0sWyJkYXRhMiIsIjUwIiwiMjAiLCIxMCIsIjQwIiwiMTUiLCIyNSJdXSwiYXhlcyI6e30sImdyb3VwcyI6e30sInR5cGUiOiIiLCJ0eXBlcyI6eyJkYXRhMSI6ImxpbmUiLCJkYXRhMiI6ImxpbmUifSwiY29sb3JzIjp7ImRhdGExIjoiIzc4QjhERiIsImRhdGEyIjoiI0FGQ0JDRSJ9fSwiYXhpcyI6eyJ4Ijp7InNob3ciOnRydWUsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX0sInkiOnsic2hvdyI6dHJ1ZSwiYWNjdXJhY3kiOjAsInByZWZpeCI6IiIsInN1ZmZpeCI6IiIsInRpY2siOnt9fSwieTIiOnsic2hvdyI6ZmFsc2UsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX19LCJwb2ludCI6eyJzaG93IjpmYWxzZX0sImdyb3VwcyI6e30sImRlZmF1bHRDb2xvcnMiOlsiIzFmNzdiNCIsIiNhZWM3ZTgiLCIjZmY3ZjBlIiwiI2ZmYmI3OCIsIiMyY2EwMmMiLCIjOThkZjhhIiwiI2Q2MjcyOCIsIiNmZjk4OTYiLCIjOTQ2N2JkIiwiI2M1YjBkNSIsIiM4YzU2NGIiLCIjYzQ5Yzk0IiwiI2UzNzdjMiIsIiNmN2I2ZDIiLCIjN2Y3ZjdmIiwiI2M3YzdjNyIsIiNiY2JkMjIiLCIjZGJkYjhkIiwiIzE3YmVjZiIsIiM5ZWRhZTUiXSwiY2hhcnRUaXRsZSI6InRlc3RpbmciLCJjaGFydENyZWRpdCI6IiIsImNoYXJ0U291cmNlIjoiIiwiY2hhcnRXaWR0aCI6MTAwMCwiY2hhcnRHbG9iYWxUeXBlIjoic2VyaWVzIiwiY2hhcnRBY2N1cmFjeSI6MSwiY21zIjpmYWxzZSwicGllIjp7ImxhYmVsIjp7fX0sImRvbnV0Ijp7ImxhYmVsIjp7fX0sImdhdWdlIjp7ImxhYmVsIjp7fX19'; 55 | 56 | parent.ajaxurl = '/test-wordpress'; 57 | 58 | parent.tinymce = { 59 | activeEditor: { 60 | insertContent: function(val) { 61 | insertContent = val; 62 | }, 63 | windowManager: { 64 | getParams: function() { 65 | return { 66 | axisJS: configObjectString, 67 | axisWP: { 68 | parentID: 55 69 | } 70 | }; 71 | }, 72 | close: function() { 73 | return true; 74 | } 75 | } 76 | } 77 | }; 78 | 79 | spyOn(parent.tinymce.activeEditor, 'insertContent').and.callThrough(); 80 | spyOn(parent.tinymce.activeEditor.windowManager, 'close'); 81 | 82 | })); 83 | 84 | it('should export data back to WordPress if the "Copy to CMS" button is clicked', inject(function ($compile, $httpBackend){ 85 | // Arrange 86 | element = angular.element(''); 87 | element = $compile(element)(scope); 88 | scope.$apply(); 89 | 90 | element.trigger('click'); 91 | $httpBackend.whenPOST('/test-wordpress').respond({attachmentURL: '#'}); 92 | $httpBackend.flush(); 93 | 94 | expect(parent.tinymce.activeEditor.insertContent).toHaveBeenCalled(); 95 | expect(parent.tinymce.activeEditor.windowManager.close).toHaveBeenCalled(); 96 | expect(insertContent.length).toBeGreaterThan(1); 97 | })); 98 | 99 | describe('Edge cases', function(){ 100 | it('should throw a WordPressOutputServiceException on failure', inject(function($httpBackend, $compile){ 101 | // Arrange 102 | element = angular.element(''); 103 | element = $compile(element)(scope); 104 | scope.$apply(); 105 | expect(function(){ 106 | element.trigger('click'); 107 | $httpBackend.whenPOST('/test-wordpress').respond(500, ''); 108 | $httpBackend.flush(); 109 | }).toThrowError(/WordPressOutputServiceException/); 110 | })); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/app/components/saveButton/saveButton.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name AxisJS.directive:saveButton 4 | * @description 5 | * # saveButton 6 | */ 7 | 8 | (function(){ 9 | 'use strict'; 10 | 11 | angular.module('axis') 12 | .directive('saveButton', saveButton); 13 | 14 | function saveButton() { 15 | return { 16 | templateUrl: 'app/components/saveButton/saveButton.html', 17 | scope: true, 18 | link: function postLink(scope, element, attrs) { 19 | var main = scope.main; 20 | scope.buttonType = attrs.type; 21 | scope.items = attrs.type === 'export' ? main.appConfig.export : main.appConfig.save; 22 | } 23 | }; 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /src/app/components/saveButton/saveButton.directive.spec.js: -------------------------------------------------------------------------------- 1 | // @TODO write a saveButton test spec. 2 | 3 | describe('Directive: saveButton', function () { 4 | 'use strict'; 5 | 6 | // load the directive's module 7 | beforeEach(module('axis')); 8 | 9 | var element, 10 | scope; 11 | 12 | beforeEach(inject(function ($rootScope) { 13 | scope = $rootScope.$new(); 14 | })); 15 | 16 | it('should show an export button if type attribute is "export"'); 17 | it('should show a save button if type attribute is not "export"'); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/components/saveButton/saveButton.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 41 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/app/head/head.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc function 3 | * @name AxisJS.controller:HeadController 4 | * @description 5 | * # HeadController 6 | * Adds stylesheets, fonts and other stuff in the `` section. 7 | */ 8 | 9 | (function() { 10 | 'use strict'; 11 | 12 | angular 13 | .module('axis') 14 | .controller('HeadController', HeadController); 15 | 16 | /** @ngInject */ 17 | function HeadController($scope, configProvider) { 18 | var head = this; 19 | 20 | configProvider.then(function(appConfig){ 21 | head.conf = appConfig; 22 | head.stylesheet = typeof appConfig.stylesheet !== 'undefined' ? appConfig.stylesheet : ''; 23 | head.fonts = typeof appConfig.fonts !== 'undefined' ? appConfig.fonts : []; 24 | }); 25 | } 26 | })(); 27 | -------------------------------------------------------------------------------- /src/app/head/head.controller.spec.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | 4 | describe('Controller: HeadController', function () { 5 | 6 | // load the controller's module 7 | beforeEach(function(){ 8 | // This test suite doesn't seem to be actually loading test.config.yaml. 9 | // TODO make this test suite more robust. 10 | var store = { 11 | 'theme': 'test' 12 | }; 13 | 14 | spyOn(localStorage, 'getItem').and.callFake(function (key) { 15 | return store[key]; 16 | }); 17 | spyOn(localStorage, 'setItem').and.callFake(function (key, value) { 18 | store[key] = value + ''; 19 | return store[key]; 20 | }); 21 | spyOn(localStorage, 'clear').and.callFake(function () { 22 | store = {}; 23 | }); 24 | 25 | module('axis'); 26 | }); 27 | 28 | var HeadController, 29 | scope; 30 | 31 | // Initialize the controller and a mock scope 32 | beforeEach(inject(function ($controller, $rootScope, $httpBackend) { 33 | scope = $rootScope.$new(); 34 | $httpBackend.expectGET('assets/i18n/en_GB.json'); 35 | $httpBackend.whenGET('assets/i18n/en_GB.json').respond('{}'); 36 | 37 | $httpBackend.expectGET('default.config.yaml'); 38 | $httpBackend.whenGET('default.config.yaml').respond('stylesheet: "themes/default.css"'); 39 | 40 | $httpBackend.expectGET('config.yaml'); 41 | $httpBackend.whenGET('config.yaml').respond('stylesheet: "themes/test.css"\nfonts:\n - "http://www.thetimes.co.uk/fonts/Solido-Bold.css"\n - "http://www.thetimes.co.uk/fonts/Solido-ExtraBold.css"\n - "http://www.thetimes.co.uk/fonts/Solido-Book-Italic.css"'); 42 | 43 | HeadController = $controller('HeadController as head', { 44 | $scope: scope 45 | }); 46 | scope.$digest(); 47 | $httpBackend.flush(); 48 | })); 49 | 50 | it('should attach a stylesheet from config', function () { 51 | expect(scope.head.stylesheet).toBe('themes/test.css'); 52 | }); 53 | 54 | it('should load an array of font URLs from config', function () { 55 | expect(scope.head.fonts.length).toBe(3); 56 | }); 57 | }); 58 | })(); 59 | -------------------------------------------------------------------------------- /src/app/index.config.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('axis') 6 | .config(config); 7 | 8 | // angular 9 | // .module('axis') 10 | // .factory('$exceptionHandler', exceptionHandlerOverride); 11 | 12 | /** 13 | * Main application config. 14 | * @TODO decide whether to use Toastr or not. 15 | */ 16 | /** @ngInject */ 17 | function config($logProvider, toastr, $translateProvider, $tooltipProvider) { 18 | // Enable log 19 | $logProvider.debugEnabled(true); 20 | 21 | // Toastr options (currently unused) 22 | toastr.options.timeOut = 3000; 23 | toastr.options.positionClass = 'toast-top-right'; 24 | toastr.options.preventDuplicates = true; 25 | toastr.options.progressBar = true; 26 | 27 | // Translation options 28 | $translateProvider.useStaticFilesLoader({ 29 | prefix: 'assets/i18n/', 30 | suffix: '.json' 31 | }); 32 | $translateProvider.preferredLanguage('en_GB'); 33 | $translateProvider.useSanitizeValueStrategy('sanitize'); 34 | 35 | // Popover options 36 | $tooltipProvider.options({trigger: 'mouseenter'}); 37 | } 38 | 39 | /** 40 | * This overrides the default Angular uncaught exception handling. 41 | * It basically writes chart config to localStorage so that it doesn't get lost. 42 | * 43 | * @TODO put this somewhere more intelligent. 44 | * @return {void} 45 | */ 46 | /** @ngInject **/ 47 | function exceptionHandlerOverride($log) { 48 | return function(exception, cause) { 49 | exception.message += ' (caused by "' + cause + '")'; 50 | $log.error(exception); 51 | 52 | if (angular.isDefined(exception.config)) { // Save current config to localStorage. 53 | try { 54 | localStorage.setItem('ls.backup_' + Date.now(), exception.config); 55 | $log.info('localStorage backup successful'); 56 | } catch(e) { 57 | $log.warn('Warning: localStorage backup failed'); 58 | } 59 | } 60 | }; 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /src/app/index.constants.js: -------------------------------------------------------------------------------- 1 | /* global toastr:false, moment:false */ 2 | (function() { 3 | 'use strict'; 4 | 5 | angular 6 | .module('axis') 7 | .constant('toastr', toastr) 8 | .constant('moment', moment); 9 | })(); 10 | -------------------------------------------------------------------------------- /src/app/index.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('axis', [ 6 | 'ngAnimate', 7 | 'ngResource', 8 | 'ngTouch', 9 | 'ngSanitize', 10 | 'ui.router', 11 | 'ui.bootstrap', 12 | 'minicolors', 13 | 'ngAside', 14 | 'ngHandsontable', 15 | 'LocalStorageModule', 16 | 'pascalprecht.translate' 17 | ]); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /src/app/index.route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc overview 3 | * @name AxisJS 4 | * @description 5 | * # AxisJS Routes 6 | * 7 | * Main route module of the application. Bootstraps config via ui-router. 8 | */ 9 | 10 | (function() { 11 | 'use strict'; 12 | 13 | angular 14 | .module('axis') 15 | .config(routeConfig); 16 | 17 | /** @ngInject */ 18 | function routeConfig($stateProvider, $urlRouterProvider) { 19 | $urlRouterProvider.otherwise('/'); 20 | $stateProvider 21 | .state('index', { 22 | url: '/', 23 | templateUrl: 'app/main/main.html', 24 | controller: 'MainController as main', 25 | resolve: { 26 | appConfig: function(configProvider, $rootScope) { /* istanbul ignore next */ 27 | $rootScope.version = '1.1.0'; 28 | return configProvider; 29 | } 30 | } 31 | }); 32 | } 33 | 34 | })(); 35 | -------------------------------------------------------------------------------- /src/app/index.run.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('axis') 6 | .run(runBlock); 7 | 8 | /** @ngInject */ 9 | function runBlock($log) { 10 | 11 | $log.debug('Axis loaded.'); 12 | } 13 | 14 | })(); 15 | -------------------------------------------------------------------------------- /src/app/index.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * If you want to override some bootstrap variables, you have to change values here. 3 | * The list of variables are listed here bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/_variables.scss 4 | */ 5 | $navbar-inverse-link-color: #5AADBB; 6 | $icon-font-path: "../../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/"; 7 | 8 | /** 9 | * Do not remove this comments bellow. It's the markers used by wiredep to inject 10 | * sass dependencies when defined in the bower.json of your dependencies 11 | */ 12 | // bower:scss 13 | // endbower 14 | 15 | html, body { 16 | height: 100%; 17 | overflow-x: hidden; 18 | } 19 | 20 | .rendering { 21 | #chart { 22 | height: 100%; 23 | margin-left: -10px; 24 | margin-top: 35px; 25 | position: fixed !important; /* !important needed to prevent C3 from overriding */ 26 | width: 100%; 27 | } 28 | canvas { 29 | display: none !important; 30 | } 31 | 32 | .svgClone { 33 | // display: none; 34 | } 35 | } 36 | 37 | .chartTitle, .chartCredit, .chartSource { 38 | text-align: center; 39 | } 40 | 41 | #chartControls { 42 | /* height: 100%; 43 | overflow: scroll; */ 44 | } 45 | 46 | .input-group.chartWidth-group { 47 | padding-left:13px !important; 48 | padding-right:13px !important; 49 | } 50 | 51 | .input-group.chartHeight-group { 52 | padding-left: 13px !important; 53 | padding-right: 13px !important; 54 | } 55 | 56 | #chartGlobalType { 57 | margin-bottom: 15px; 58 | } 59 | 60 | #csvInput { 61 | margin-bottom: 20px; 62 | white-space: pre; 63 | tab-size: 20; 64 | } 65 | 66 | #dataGrid { 67 | max-height: 100px; 68 | overflow: scroll; 69 | /* .ngViewport { 70 | max-height: 100px; 71 | } */ 72 | } 73 | 74 | .input-buttons { 75 | text-align: center; 76 | button { 77 | /* margin: 0 .5em; */ 78 | } 79 | } 80 | 81 | // Color picker 82 | .minicolors { 83 | .minicolors-swatch { 84 | width: 21px !important; 85 | height: 21px; 86 | } 87 | } 88 | 89 | .input-group-addon select { 90 | border: 0px; 91 | } 92 | 93 | .panel-body h4 { 94 | margin-bottom: 25px; 95 | } 96 | 97 | .panel-heading { 98 | cursor: pointer; 99 | 100 | &:after { 101 | font-family:'Glyphicons Halflings'; 102 | content: '\e080'; 103 | float: right; 104 | color: grey; 105 | } 106 | 107 | &.collapsed:after { 108 | content: '\e114'; 109 | } 110 | } 111 | 112 | .browsehappy { 113 | margin: 0.2em 0; 114 | background: #ccc; 115 | color: #000; 116 | padding: 0.2em 0; 117 | } 118 | 119 | .input-group > select { 120 | -webkit-appearance: none; 121 | -webkit-border-radius: 0px; 122 | } 123 | 124 | .config { 125 | /* position: absolute; 126 | right: 10px; 127 | top: 10px; */ 128 | float: right; 129 | margin-top: 10px; 130 | } 131 | 132 | .btn-circle { 133 | width: 30px; 134 | height: 30px; 135 | text-align: center; 136 | padding: 6px 0; 137 | font-size: 12px; 138 | line-height: 1.428571429; 139 | border-radius: 15px; 140 | border: 0; 141 | 142 | &.btn-lg { 143 | width: 50px; 144 | height: 50px; 145 | padding: 10px 16px; 146 | font-size: 18px; 147 | line-height: 1.33; 148 | border-radius: 25px; 149 | } 150 | 151 | &.btn-xl { 152 | width: 70px; 153 | height: 70px; 154 | padding: 10px 16px; 155 | font-size: 24px; 156 | line-height: 1.33; 157 | border-radius: 35px; 158 | } 159 | } 160 | 161 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 162 | display: none !important; 163 | } 164 | 165 | 166 | .modal-backdrop { 167 | height: 100%; 168 | } 169 | 170 | .credit { 171 | small { 172 | float: right; 173 | padding-right: 1em; 174 | } 175 | } 176 | 177 | .input-chooser { 178 | .btn-lg { 179 | border: 0; 180 | margin-left: 5px; 181 | } 182 | } 183 | 184 | .background-color-picker { 185 | float: right; 186 | margin-left: 1em; 187 | } 188 | 189 | /** 190 | * Do not remove this comments bellow. It's the markers used by gulp-inject to inject 191 | * all your sass files automatically 192 | */ 193 | // injector 194 | // endinjector 195 | -------------------------------------------------------------------------------- /src/app/main/main.controller.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | describe('Controller: MainController -- WITHOUT external data', function () { 5 | 6 | // load the controller's module 7 | beforeEach( 8 | module('axis') 9 | ); 10 | 11 | var MainController, 12 | scope, 13 | configChooser; 14 | 15 | // Initialize the controller and a mock scope 16 | beforeEach(inject(function ($controller, $rootScope, $injector) { 17 | scope = $rootScope.$new(); 18 | MainController = $controller('MainController as main', { 19 | $scope: scope, 20 | appConfig: { 21 | renderer: 'c3', 22 | input: 'csv', 23 | colors: [ 24 | {value: 'blue'}, 25 | {value: 'red'} 26 | ], 27 | defaults: {} 28 | } 29 | }); 30 | 31 | configChooser = $injector.get('configChooser'); 32 | })); 33 | 34 | it('should attach a list of config options to the scope', function() { 35 | expect(typeof scope.main.config).not.toBe('undefined'); 36 | }); 37 | 38 | // Move to CSVInput 39 | it('should validate if the data is only one column', function() { 40 | var gaugeCSV = 'llamas\n70'; 41 | expect(scope.main.validateData(gaugeCSV)).toBeTruthy(); 42 | }); 43 | 44 | // Move to CSVInput 45 | it('should validate non-string-delimited TSV input with commas (#39)', function() { 46 | var wordyTSV = 'The PM should pay up The PM should try to reduce the bill, but if unsuccessful, should still pay The PM should try to reduce the bill, and refuse to pay if unsuccessful Don\'t know\n9 25 54 12'; 47 | expect(scope.main.validateData(wordyTSV)).toBeTruthy(); 48 | }); 49 | 50 | // Move to CSVInput 51 | it('should also still validate CSV (#39)', function() { 52 | var wordyCSV = '"The PM should pay up","The PM should try to reduce the bill, but if unsuccessful, should still pay","The PM should try to reduce the bill, and refuse to pay if unsuccessful","Don\'t know"\n9,25,54,12'; 53 | expect(scope.main.validateData(wordyCSV)).toBeTruthy(); 54 | }); 55 | 56 | it('should be able to reset config to standard', function(){ 57 | expect(scope.main.config.llama).not.toBeDefined(); 58 | scope.main.config.llama = 'hurr'; 59 | MainController.resetConfig(); 60 | 61 | expect(scope.main.config.llama).not.toBeDefined(); 62 | }); 63 | 64 | it('should be able to set the global chart type', function(){ 65 | expect(scope.main.config.data.types.data1).toBe('line'); 66 | expect(scope.main.config.data.types.data2).toBe('line'); 67 | 68 | MainController.setGlobalType('pie'); 69 | expect(scope.main.config.data.types.data1).toBe('pie'); 70 | expect(scope.main.config.data.types.data2).toBe('pie'); 71 | }); 72 | 73 | it('should be able to set data groups for stacked charts'); 74 | it('should be able to set the input service'); 75 | it('should be able to detect if a chart type uses areas'); 76 | }); 77 | 78 | describe('Controller: MainController -- WITH external data', function() { 79 | 80 | // load the controller's module 81 | beforeEach(module('axis')); 82 | 83 | var MainController, 84 | scope; 85 | 86 | // Initialize the controller and a mock scope 87 | beforeEach(inject(function ($controller, $rootScope, $window) { 88 | var configObjectString = 'eyJkYXRhIjp7IngiOiIiLCJ5IjoiIiwieTIiOiIiLCJjb2x1bW5zIjpbWyJkYXRhMSIsIjMwIiwiMjAwIiwiMTAwIiwiNDAwIiwiMTUwIiwiMjUwIl0sWyJkYXRhMiIsIjUwIiwiMjAiLCIxMCIsIjQwIiwiMTUiLCIyNSJdXSwiYXhlcyI6e30sImdyb3VwcyI6e30sInR5cGUiOiIiLCJ0eXBlcyI6eyJkYXRhMSI6ImxpbmUiLCJkYXRhMiI6ImxpbmUifSwiY29sb3JzIjp7ImRhdGExIjoiIzc4QjhERiIsImRhdGEyIjoiI0FGQ0JDRSJ9fSwiYXhpcyI6eyJ4Ijp7InNob3ciOnRydWUsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX0sInkiOnsic2hvdyI6dHJ1ZSwiYWNjdXJhY3kiOjAsInByZWZpeCI6IiIsInN1ZmZpeCI6IiIsInRpY2siOnt9fSwieTIiOnsic2hvdyI6ZmFsc2UsImFjY3VyYWN5IjowLCJwcmVmaXgiOiIiLCJzdWZmaXgiOiIiLCJ0aWNrIjp7fX19LCJwb2ludCI6eyJzaG93IjpmYWxzZX0sImdyb3VwcyI6e30sImRlZmF1bHRDb2xvcnMiOlsiIzFmNzdiNCIsIiNhZWM3ZTgiLCIjZmY3ZjBlIiwiI2ZmYmI3OCIsIiMyY2EwMmMiLCIjOThkZjhhIiwiI2Q2MjcyOCIsIiNmZjk4OTYiLCIjOTQ2N2JkIiwiI2M1YjBkNSIsIiM4YzU2NGIiLCIjYzQ5Yzk0IiwiI2UzNzdjMiIsIiNmN2I2ZDIiLCIjN2Y3ZjdmIiwiI2M3YzdjNyIsIiNiY2JkMjIiLCIjZGJkYjhkIiwiIzE3YmVjZiIsIiM5ZWRhZTUiXSwiY2hhcnRUaXRsZSI6InRlc3RpbmciLCJjaGFydENyZWRpdCI6IiIsImNoYXJ0U291cmNlIjoiIiwiY2hhcnRXaWR0aCI6MTAwMCwiY2hhcnRHbG9iYWxUeXBlIjoic2VyaWVzIiwiY2hhcnRBY2N1cmFjeSI6MSwiY21zIjpmYWxzZSwicGllIjp7ImxhYmVsIjp7fX0sImRvbnV0Ijp7ImxhYmVsIjp7fX0sImdhdWdlIjp7ImxhYmVsIjp7fX19'; 89 | $window.parent = { 90 | tinymce: { 91 | activeEditor: { 92 | windowManager: { 93 | getParams: function() { 94 | return { 95 | axisJS: configObjectString 96 | }; 97 | } 98 | } 99 | } 100 | } 101 | }; 102 | 103 | scope = $rootScope.$new(); 104 | MainController = $controller('MainController as main', { 105 | $scope: scope, 106 | appConfig: { 107 | renderer: 'c3', 108 | input: 'csv', 109 | export: [ 110 | 'wordpress' 111 | ], 112 | colors: [ 113 | {value: 'blue'}, 114 | {value: 'red'} 115 | ], 116 | defaults: {}, 117 | } 118 | }); 119 | })); 120 | 121 | it('should populate the chart title if given config from WordPress', function() { 122 | expect(scope.main.config.chartTitle).toBe('testing', 'Config title not populating properly...'); 123 | }); 124 | 125 | it('should populate chartData if given config from WordPress', function(){ 126 | expect(scope.main.chartData.length).toBe(6, 'Config data not populating properly...'); 127 | }); 128 | 129 | it('should populate columns if given config from WordPress', function(){ 130 | expect(scope.main.columns.length).toBe(2, 'Config columns not populating properly...'); 131 | }); 132 | 133 | }); 134 | })(); 135 | -------------------------------------------------------------------------------- /src/app/main/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | 8 | 9 |
10 | 13 |

Axis {{version}}

14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /src/app/main/partials/input.html: -------------------------------------------------------------------------------- 1 |
2 |
5 | {{ 'PANEL_HEADING_DATA_INPUT' | translate }} 6 |
7 | 8 |
9 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/main/partials/options.data.html: -------------------------------------------------------------------------------- 1 |
2 |
5 | {{ 'PANEL_HEADING_DATA_OPTIONS' | translate }} 6 |
7 | 8 |
9 | 20 |
21 | 22 |
23 | 24 | 30 |
31 | 32 |
33 |
34 | 35 | 43 | 44 | 45 | 48 | 49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /src/app/main/partials/options.output.html: -------------------------------------------------------------------------------- 1 |
2 |
5 | {{ 'PANEL_HEADING_OUTPUT' | translate }} 6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/assets/i18n/en_GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEAD_APP_TITLE": "Axis » streamlined chart creation tool", 3 | "HEAD_META_DESCRIPTION": "Axis » streamlined chart creation tool by The Times", 4 | "PANEL_HEADING_DATA_INPUT": "Data input", 5 | "INPUT_MALFORMED_CSV_ERROR": "Seems like you've entered some malformed CSV data...", 6 | "FINANCIAL_INPUT_START_DATE_LABEL": "Start date", 7 | "FINANCIAL_INPUT_END_DATE_LABEL": "End date", 8 | "FINANCIAL_INPUT_SYMBOL_LABEL": "Symbol(s)", 9 | "FEED_INPUT_START_DATE_LABEL": "Start date", 10 | "FEED_INPUT_END_DATE_LABEL": "End date", 11 | 12 | "PANEL_HEADING_DATA_OPTIONS": "Data options", 13 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_SERIES": "series", 14 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_PIE": "pie", 15 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_DONUT": "donut", 16 | "DATA_OPTIONS_GLOBAL_CHART_TYPE_SELECT_GAUGE": "gauge", 17 | "DATA_OPTIONS_DATUM_TYPE_SELECT_LINE": "line", 18 | "DATA_OPTIONS_DATUM_TYPE_SELECT_STEP": "step", 19 | "DATA_OPTIONS_DATUM_TYPE_SELECT_AREA": "area", 20 | "DATA_OPTIONS_DATUM_TYPE_SELECT_AREA-STEP": "area-step", 21 | "DATA_OPTIONS_DATUM_TYPE_SELECT_SCATTER": "scatter", 22 | "DATA_OPTIONS_DATUM_TYPE_SELECT_BAR": "bar", 23 | "DATA_OPTIONS_DATUM_TYPE_SELECT_SPLINE": "spline", 24 | 25 | "PANEL_HEADING_AXES": "Axes", 26 | "AXES_SECTION_HEADING": "{{axisPosition}} Axis", 27 | "AXES_COLUMN_SELECT_LABEL": "Ordinal?", 28 | "AXES_LABEL_FIELD_LABEL": "Label", 29 | "AXES_PREFIX_FIELD_LABEL": "Prefix", 30 | "AXES_SUFFIX_FIELD_LABEL": "Suffix", 31 | "AXES_MIN_FIELD_LABEL": "Min.", 32 | "AXES_MAX_FIELD_LABEL": "Max.", 33 | "AXES_ACCURACY_SELECT_LABEL": "Accuracy", 34 | "AXES_ACCURACY_SELECT_OPTION_TEXT": "{{accuracy}} decimal places", 35 | "AXES_CULLING_FIELD_LABEL": "Culling", 36 | "AXES_COUNT_FIELD_LABEL": "Count", 37 | "AXES_TIMESERIES_FIELD_LABEL": "Timeseries?", 38 | "AXES_DATETIME_FORMAT_FIELD_LABEL": "Datetime format", 39 | "AXES_INVERT": "Invert", 40 | 41 | "PANEL_HEADING_CHART_OPTIONS": "Design options", 42 | "CHART_OPTIONS_PRESETS_LABEL": "Presets", 43 | "CHART_OPTIONS_LEGEND_SELECT_LABEL": "Legend", 44 | "CHART_OPTIONS_LEGEND_SELECT_OPTION_BOTTOM": "bottom", 45 | "CHART_OPTIONS_LEGEND_SELECT_OPTION_RIGHT": "right", 46 | "CHART_OPTIONS_TITLE_LABEL": "Title", 47 | "CHART_OPTIONS_CREDIT_LABEL": "Credit", 48 | "CHART_OPTIONS_CREDIT_FIELD_PLACEHOLDER": "Your Name/The Times", 49 | "CHART_OPTIONS_SOURCE_LABEL": "Source", 50 | "CHART_OPTIONS_SOURCE_FIELD_PLACEHOLDER": "YouGov", 51 | "CHART_OPTIONS_TITLES_POSITION": "Title position", 52 | "CHART_OPTIONS_TITLES_POSITION_SELECT_OPTION_TOP_LEFT": "top-left", 53 | "CHART_OPTIONS_TITLES_POSITION_SELECT_OPTION_TOP_CENTER": "top-centre", 54 | "CHART_OPTIONS_TITLES_POSITION_SELECT_OPTION_TOP_RIGHT": "top-right", 55 | "CHART_OPTIONS_WIDTH_LABEL": "Width", 56 | "CHART_OPTIONS_HEIGHT_LABEL": "Height", 57 | "CHART_OPTIONS_TRANSITION_LABEL": "Transition", 58 | "CHART_OPTIONS_SELECT_LABEL": "Accuracy", 59 | "CHART_OPTIONS_SELECT_OPTION_TEXT": "{{accuracy}} decimal places", 60 | "CHART_OPTIONS_ZERO_BASED": "Zero-based areas", 61 | "CHART_OPTIONS_ROTATED_LABEL": "Rotated", 62 | "CHART_OPTIONS_BACKGROUND_LABEL": "Background", 63 | "CHART_OPTIONS_ZOOMABLE_LABEL": "Zoomable", 64 | "CHART_OPTIONS_SUBCHART_LABEL": "Subchart", 65 | "CHART_OPTIONS_INTERACTION_LABEL": "Interaction", 66 | "CHART_OPTIONS_X_GRID_LABEL": "X Grid", 67 | "CHART_OPTIONS_Y_GRID_LABEL": "Y Grid", 68 | "CHART_OPTIONS_DATA_LABELS_LABEL": "Datum labels", 69 | 70 | "PANEL_HEADING_OUTPUT": "Output", 71 | "OUTPUT_EXPORT_BUTTON_LABEL": "Export to...", 72 | "OUTPUT_SAVE_BUTTON_LABEL": "Save image...", 73 | 74 | "CREDIT_TEXT": "A project by Ændrew Rininsland, Times Digital Development | issues | GitHub", 75 | 76 | "POPOVER_TEXT_CHART_GLOBAL_TYPE": "Choose the main chart style here", 77 | "POPOVER_TEXT_DATUM_TYPE": "Select the style of this column from the following options", 78 | "POPOVER_TEXT_LEGEND_CHECKBOX": "Turn the chart legend on or off with this checkbox", 79 | "POPOVER_TEXT_ROTATED_CHECKBOX": "Rotate the chart 90 degrees", 80 | "POPOVER_TEXT_BACKGROUND_CHECKBOX": "Adds a solid background to image outputs", 81 | "POPOVER_TEXT_COLUMN_SELECT": "Choose a column here to make it a ordinal/categorical axis", 82 | "POPOVER_TEXT_ACCURACY_CHECKBOX": "Toggle grouping zeroes with commas", 83 | "POPOVER_TEXT_CULLING_FIELD": "Set the number of axis labels", 84 | "POPOVER_TEXT_COUNT_FIELD": "Set the number of axis ticks", 85 | "POPOVER_TEXT_ZOOMABLE_CHECKBOX": "Check to make chart zoomable", 86 | "POPOVER_TEXT_SUBCHART_CHECKBOX": "Check to add a subchart" 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/assets/images/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/angular.png -------------------------------------------------------------------------------- /src/assets/images/bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/bootstrap.png -------------------------------------------------------------------------------- /src/assets/images/browsersync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/browsersync.png -------------------------------------------------------------------------------- /src/assets/images/gulp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/gulp.png -------------------------------------------------------------------------------- /src/assets/images/jasmine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/jasmine.png -------------------------------------------------------------------------------- /src/assets/images/karma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/karma.png -------------------------------------------------------------------------------- /src/assets/images/node-sass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/node-sass.png -------------------------------------------------------------------------------- /src/assets/images/protractor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/protractor.png -------------------------------------------------------------------------------- /src/assets/images/ui-bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/ui-bootstrap.png -------------------------------------------------------------------------------- /src/assets/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/assets/images/yeoman.png -------------------------------------------------------------------------------- /src/config.yaml: -------------------------------------------------------------------------------- 1 | stylesheet: 'themes/default.css' 2 | -------------------------------------------------------------------------------- /src/default.config.yaml: -------------------------------------------------------------------------------- 1 | # axisJS default config 2 | --- 3 | renderer: 'c3' 4 | 5 | input: 6 | - 'spreadsheet' 7 | - 'csv' 8 | # - 'financial' # Disabled until financial data becomes more open. :( 9 | 10 | export: 11 | - 'Embed code' 12 | - 'pdf' 13 | 14 | save: 15 | - 'png' 16 | - 'svg' 17 | 18 | colors: 19 | - label: 'neutral 1' 20 | value: '#78B8DF' 21 | - label: 'neutral 2' 22 | value: '#AFCBCE' 23 | - label: 'neutral 3' 24 | value: '#23393D' 25 | - label: 'neutral 4' 26 | value: '#87AF9C' 27 | - label: 'yes' 28 | value: '#0EBF00' 29 | - label: 'no' 30 | value: '#ED1B24' 31 | - label: 'maybe' 32 | value: '#808080' 33 | - label: 'Labour' 34 | value: '#ED1B24' 35 | - label: 'Conservative' 36 | value: '#022397' 37 | - label: 'LibDem' 38 | value: '#FDBB30' 39 | - label: 'Ukip' 40 | value: '#722889' 41 | - label: 'Green' 42 | value: '#6AB023' 43 | - label: '#1f77b4' # begin c3.js defaults 44 | value: '#1f77b4' 45 | - label: '#aec7e8' 46 | value: '#aec7e8' 47 | - label: '#ff7f0e' 48 | value: '#ff7f0e' 49 | - label: '#ffbb78' 50 | value: '#ffbb78' 51 | - label: '#2ca02c' 52 | value: '#2ca02c' 53 | - label: '#98df8a' 54 | value: '#98df8a' 55 | - label: '#d62728' 56 | value: '#d62728' 57 | - label: '#ff9896' 58 | value: '#ff9896' 59 | - label: '#9467bd' 60 | value: '#9467bd' 61 | - label: '#c5b0d5' 62 | value: '#c5b0d5' 63 | - label: '#8c564b' 64 | value: '#8c564b' 65 | - label: '#c49c94' 66 | value: '#c49c94' 67 | - label: '#e377c2' 68 | value: '#e377c2' 69 | - label: '#f7b6d2' 70 | value: '#f7b6d2' 71 | - label: '#7f7f7f' 72 | value: '#7f7f7f' 73 | - label: '#c7c7c7' 74 | value: '#c7c7c7' 75 | - label: '#bcbd22' 76 | value: '#bcbd22' 77 | - label: '#dbdb8d' 78 | value: '#dbdb8d' 79 | - label: '#17becf' 80 | value: '#17becf' 81 | - label: '#9edae5' 82 | value: '#9edae5' 83 | 84 | background colors: 85 | - label: 'white' 86 | value: '#ffffff' 87 | 88 | defaults: 89 | grid x: false 90 | grid y: false 91 | axis y: true 92 | axis y2: false 93 | axis x: true 94 | legend: true 95 | point: false 96 | y axis: 'y' 97 | axis x padding left: 98 | axis x padding right: 99 | axis y padding top: 100 | axis y padding bottom: 101 | padding left: 25 102 | padding right: 25 103 | padding top: 25 104 | padding bottom: 25 105 | title text: 106 | title author: 107 | title source: 108 | title position: top-left 109 | charts: 110 | series: 111 | grid: 112 | y: 113 | show: true 114 | x: 115 | show: true 116 | pie: 117 | grid: 118 | y: 119 | show: false 120 | x: 121 | show: false 122 | donut: 123 | y: 124 | show: false 125 | x: 126 | show: false 127 | gauge: 128 | y: 129 | show: false 130 | x: 131 | show: false 132 | 133 | backgroundColor: 'white' 134 | 135 | colorPicker: 'simple' 136 | 137 | financialService: 'yahoo' 138 | 139 | themes: 140 | - name: 'Default (Red Box)' 141 | file: 'config.yaml' 142 | - name: 'The Times' 143 | file: 'themes/thetimes.config.yaml' 144 | - name: 'Sunday Times' 145 | file: 'themes/sundaytimes.config.yaml' 146 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/times/axisJS/0a655661321f1739574c93f31630b5ab20fc2892/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Axis » streamlined chart creation tool 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/themes/axismaker.config.yaml: -------------------------------------------------------------------------------- 1 | # Axis Server default config 2 | --- 3 | renderer: 'c3' 4 | 5 | input: 6 | - 'spreadsheet' 7 | - 'csv' 8 | # - 'financial' # Disabled until financial data becomes more open. :( 9 | 10 | export: 11 | - 'axis' 12 | - 'Embed code' 13 | 14 | save: 15 | - 'png' 16 | - 'svg' 17 | 18 | colors: 19 | - label: 'neutral 1' 20 | value: '#78B8DF' 21 | - label: 'neutral 2' 22 | value: '#AFCBCE' 23 | - label: 'neutral 3' 24 | value: '#23393D' 25 | - label: 'neutral 4' 26 | value: '#87AF9C' 27 | - label: 'yes' 28 | value: '#0EBF00' 29 | - label: 'no' 30 | value: '#ED1B24' 31 | - label: 'maybe' 32 | value: '#808080' 33 | - label: 'Labour' 34 | value: '#ED1B24' 35 | - label: 'Conservative' 36 | value: '#022397' 37 | - label: 'LibDem' 38 | value: '#FDBB30' 39 | - label: 'Ukip' 40 | value: '#722889' 41 | - label: 'Green' 42 | value: '#6AB023' 43 | - label: '#1f77b4' # begin c3.js defaults 44 | value: '#1f77b4' 45 | - label: '#aec7e8' 46 | value: '#aec7e8' 47 | - label: '#ff7f0e' 48 | value: '#ff7f0e' 49 | - label: '#ffbb78' 50 | value: '#ffbb78' 51 | - label: '#2ca02c' 52 | value: '#2ca02c' 53 | - label: '#98df8a' 54 | value: '#98df8a' 55 | - label: '#d62728' 56 | value: '#d62728' 57 | - label: '#ff9896' 58 | value: '#ff9896' 59 | - label: '#9467bd' 60 | value: '#9467bd' 61 | - label: '#c5b0d5' 62 | value: '#c5b0d5' 63 | - label: '#8c564b' 64 | value: '#8c564b' 65 | - label: '#c49c94' 66 | value: '#c49c94' 67 | - label: '#e377c2' 68 | value: '#e377c2' 69 | - label: '#f7b6d2' 70 | value: '#f7b6d2' 71 | - label: '#7f7f7f' 72 | value: '#7f7f7f' 73 | - label: '#c7c7c7' 74 | value: '#c7c7c7' 75 | - label: '#bcbd22' 76 | value: '#bcbd22' 77 | - label: '#dbdb8d' 78 | value: '#dbdb8d' 79 | - label: '#17becf' 80 | value: '#17becf' 81 | - label: '#9edae5' 82 | value: '#9edae5' 83 | 84 | background colors: 85 | - label: 'white' 86 | value: '#ffffff' 87 | 88 | defaults: 89 | grid x: false 90 | grid y: false 91 | axis y: true 92 | axis y2: false 93 | axis x: true 94 | legend: true 95 | point: false 96 | y axis: 'y' 97 | title text: 98 | title author: 99 | title source: 100 | title position: top-left 101 | charts: 102 | series: 103 | grid: 104 | y: 105 | show: true 106 | x: 107 | show: true 108 | pie: 109 | grid: 110 | y: 111 | show: false 112 | x: 113 | show: false 114 | donut: 115 | y: 116 | show: false 117 | x: 118 | show: false 119 | gauge: 120 | y: 121 | show: false 122 | x: 123 | show: false 124 | 125 | backgroundColor: 'white' 126 | 127 | colorPicker: 'simple' 128 | 129 | financialService: 'yahoo' 130 | 131 | themes: 132 | - name: 'Default (Red Box)' 133 | file: 'config.yaml' 134 | - name: 'The Times' 135 | file: 'themes/thetimes.config.yaml' 136 | - name: 'Sunday Times' 137 | file: 'themes/sundaytimes.config.yaml' 138 | -------------------------------------------------------------------------------- /src/themes/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Axis styles, based on Times Red Box. 3 | * 4 | * Working renderers: C3 5 | */ 6 | 7 | 8 | /** C3 **/ 9 | path.c3-line { 10 | stroke-width: 2; 11 | } 12 | -------------------------------------------------------------------------------- /src/themes/sundaytimes.config.yaml: -------------------------------------------------------------------------------- 1 | # Sunday Times axisJS configuration 2 | --- 3 | renderer: 'c3' 4 | 5 | export: 6 | - 'Embed code' 7 | - 'pdf' 8 | 9 | output: 10 | - 'png' 11 | - 'svg' 12 | 13 | colors: 14 | - label: 'Appointments' 15 | value: '#A8C6D6' 16 | - label: 'Business' 17 | value: '#0076A3' 18 | - label: 'Culture' 19 | value: '#C2DBE8' 20 | - label: 'Comment' 21 | value: '#6398B0' 22 | - label: 'Driving' 23 | value: '#F58220' 24 | - label: 'Focus' 25 | value: '#DA2128' 26 | - label: 'Home' 27 | value: '#00A88E' 28 | - label: 'Money' 29 | value: '#00ACCD' 30 | - label: 'News' 31 | value: '#286FB7' 32 | - label: 'News Review' 33 | value: '#0083CA' 34 | - label: 'Sport' 35 | value: '#39B54A' 36 | - label: 'Sport Dark Blue' 37 | value: '#2B4C5A' 38 | - label: 'Sunday' 39 | value: '#005B7F' 40 | - label: 'Travel' 41 | value: '#00AEEF' 42 | 43 | stylesheet: 'themes/sundaytimes.css' 44 | 45 | screen margins: # in pixels 46 | top: '12px' 47 | bottom: '12px' 48 | left: '12px' 49 | right: '12px' 50 | 51 | print margins: # in points 52 | top: '5' 53 | bottom: '5' 54 | left: '5' 55 | right: '5' 56 | 57 | defaults: 58 | grid x: false 59 | grid y: true 60 | axis y: false 61 | axis y2: true 62 | axis x: true 63 | legend: false 64 | legend position: 'bottom' 65 | point: false 66 | y axis: 'y2' 67 | 68 | fonts: 69 | - 'http://www.thetimes.co.uk/fonts/Solido-Bold.css' 70 | - 'http://www.thetimes.co.uk/fonts/Solido-ExtraBold.css' 71 | - 'http://www.thetimes.co.uk/fonts/Solido-Book-Italic.css' 72 | 73 | # This is to prevent Adobe Illustrator from borking due to non-standard font names. 74 | # Format — 'webfont name': 'local font name' 75 | font replacements: 76 | 'Solido-Bold': 'SOLIDO-BOLD' 77 | 'Solido-ExtraBold': 'SOLIDO-EXTRABOLD' 78 | 'Solido-Book-Italic': 'SOLIDO-BOOKITALIC' 79 | 80 | colorPicker: 'simple' 81 | -------------------------------------------------------------------------------- /src/themes/sundaytimes.css: -------------------------------------------------------------------------------- 1 | /** Sunday Times styles **/ 2 | 3 | /** C3 **/ 4 | g.c3-axis-y2 g.tick > line { 5 | display: none; 6 | } 7 | 8 | g.c3-axis-y2 g.tick > text { 9 | font-family: 'Solido-ExtraBold', Times, serif; 10 | } 11 | 12 | g.c3-axis-x g.tick > text { 13 | font-family: 'Solido-Book-Italic', Times, serif; 14 | } 15 | 16 | g.c3-axis-y2 .domain { 17 | display: none; 18 | } 19 | 20 | g.c3-grid g.c3-ygrids line { 21 | stroke-dasharray: 1, 1; 22 | } 23 | 24 | .chartTitle { 25 | font-family: 'Solido-ExtraBold', Times, serif; 26 | } 27 | 28 | .chartSource { 29 | font-family: 'Solido-Book-Italic', Times, serif; 30 | } 31 | -------------------------------------------------------------------------------- /src/themes/thetimes.config.yaml: -------------------------------------------------------------------------------- 1 | # The Times axisJS configuration 2 | --- 3 | renderer: 'c3' 4 | 5 | export: 6 | - 'Embed code' 7 | - 'pdf' 8 | 9 | output: 10 | - 'png' 11 | - 'svg' 12 | 13 | colors: 14 | - label: 'Appointments' 15 | value: '#A8C6D6' 16 | - label: 'Business' 17 | value: '#0076A3' 18 | - label: 'Culture' 19 | value: '#C2DBE8' 20 | - label: 'Comment' 21 | value: '#6398B0' 22 | - label: 'Driving' 23 | value: '#F58220' 24 | - label: 'Focus' 25 | value: '#DA2128' 26 | - label: 'Home' 27 | value: '#00A88E' 28 | - label: 'Money' 29 | value: '#00ACCD' 30 | - label: 'News' 31 | value: '#286FB7' 32 | - label: 'News Review' 33 | value: '#0083CA' 34 | - label: 'Sport' 35 | value: '#39B54A' 36 | - label: 'Sport Dark Blue' 37 | value: '#2B4C5A' 38 | - label: 'Sunday' 39 | value: '#005B7F' 40 | - label: 'Travel' 41 | value: '#00AEEF' 42 | 43 | background colors: 44 | - label: 'normal' 45 | value: '#F3F2E5' 46 | - label: 'tempus' 47 | value: '#E4EEEF' 48 | - label: 'white' 49 | value: '#ffffff' 50 | 51 | stylesheet: 'themes/thetimes.css' 52 | 53 | screen margins: # in pixels 54 | top: '12px' 55 | bottom: '12px' 56 | left: '12px' 57 | right: '12px' 58 | 59 | print margins: # in points 60 | top: '5' 61 | bottom: '5' 62 | left: '5' 63 | right: '5' 64 | 65 | defaults: 66 | grid x: true 67 | grid y: true 68 | axis y: false 69 | axis y2: true 70 | axis x: true 71 | legend show: false 72 | legend position: 'bottom' 73 | point: false 74 | y axis: 'y2' 75 | series type: 'area' 76 | padding left: 50 77 | padding right: 50 78 | padding top: 100 79 | padding bottom: 10 80 | 81 | 82 | presets: 83 | - label: 'Need To Know' 84 | class: 'need-to-know' 85 | settings: 86 | background: true 87 | backgroundColor: '#F3F2E5' 88 | 89 | - label: 'Graph Of The Day' 90 | class: 'gotd' 91 | settings: 92 | background: true 93 | backgroundColor: '#F3F2E5' 94 | 95 | - label: 'Home News' 96 | class: 'home-news' 97 | settings: 98 | background: true 99 | backgroundColor: '#F3F2E5' 100 | 101 | - label: 'Tempus' 102 | class: 'tempus' 103 | settings: 104 | background: true 105 | backgroundColor: '#E4EEEF' 106 | axis y show: false 107 | axis y2 show: false 108 | axis x show: false 109 | 110 | fonts: 111 | - 'http://www.thetimes.co.uk/fonts/TimesClassicText-Bold-Italic.css' 112 | - 'http://www.thetimes.co.uk/fonts/TimesClassicText-Medium.css' 113 | - 'http://www.thetimes.co.uk/fonts/StagSans-SemiBold.css' 114 | - 'http://www.thetimes.co.uk/fonts/StagSans-Medium.css' 115 | - 'http://www.thetimes.co.uk/fonts/StagSans-Book.css' 116 | 117 | # This is to prevent Adobe Illustrator from borking due to non-standard font names. 118 | # Format — 'webfont name': 'local font name' 119 | font replacements: 120 | 'TimesClassicText-Bold-Italic': 'TIMESCLASSICTEXTBOLDITALIC' 121 | 'TimesClassicText-Medium': 'TIMESCLASSICTEXT-MD' 122 | 'StagSans-SemiBold': 'STAGSANS-SEMIBOLD' 123 | 'StagSans-Medium': 'STAGSANS-MEDIUM' 124 | 'StagSans-Book': 'STAGSANS-BOOK' 125 | 126 | colorPicker: 'simple' 127 | -------------------------------------------------------------------------------- /src/themes/thetimes.css: -------------------------------------------------------------------------------- 1 | /** The Times styles **/ 2 | 3 | /** C3 **/ 4 | .c3-grid line { 5 | stroke: #ddd !important; 6 | stroke-dasharray: 1, 1; 7 | } 8 | 9 | path.c3-line { 10 | stroke: #bf1f10 !important; 11 | stroke-width: 2; 12 | } 13 | 14 | path.c3-area { 15 | fill: #cfdfef 16 | } 17 | 18 | .c3-axis .tick text { 19 | font-family: 'StagSans-Book', sans-serif; 20 | } 21 | 22 | #chart.home-news .chart-bg, #chart.need-to-know .chart-bg, #chart.gotd .chart-bg { 23 | fill: #F3F2E5; 24 | } 25 | 26 | #chart.tempus .chart-bg { 27 | fill: #E4EEEF; 28 | } 29 | 30 | .gotd path.c3-line { 31 | stroke: #004668 !important; 32 | stroke-width: 2; 33 | } 34 | 35 | .need-to-know .c3-axis-x .tick text { 36 | font-family: 'StagSans-Medium', sans-serif; 37 | } 38 | 39 | .need-to-know .c3-axis-y2 .tick text, .need-to-know .c3-axis-y .tick text { 40 | font-family: 'StagSans-Book', sans-serif; 41 | } 42 | 43 | .gotd .c3-axis-x .tick text { 44 | font-family: 'StagSans-Medium', sans-serif; 45 | } 46 | 47 | .gotd .c3-axis-y2 .tick text, .gotd .c3-axis-y .tick text { 48 | font-family: 'StagSans-Book', sans-serif; 49 | } 50 | 51 | .tempus .c3-axis-x .tick text { 52 | font-family: 'StagSans-Book', sans-serif; 53 | } 54 | 55 | .tempus .c3-axis-y2 .tick text, .tempus .c3-axis-y .tick text { 56 | font-family: 'StagSans-Medium', sans-serif; 57 | } 58 | 59 | .home-news .c3-axis-x .tick text { 60 | font-family: 'StagSans-Book', sans-serif; 61 | } 62 | 63 | .home-news .c3-axis-y2 .tick text, .home-news .c3-axis-y .tick text { 64 | font-family: 'StagSans-Medium', sans-serif; 65 | } 66 | -------------------------------------------------------------------------------- /src/themes/wordpress.config.yaml: -------------------------------------------------------------------------------- 1 | # Axis WordPress default config 2 | --- 3 | renderer: 'c3' 4 | 5 | input: 6 | - 'spreadsheet' 7 | - 'csv' 8 | # - 'financial' # Disabled until financial data becomes more open. :( 9 | 10 | export: 11 | - 'wordpress' 12 | 13 | save: 14 | - 'png' 15 | - 'svg' 16 | 17 | colors: 18 | - label: 'neutral 1' 19 | value: '#78B8DF' 20 | - label: 'neutral 2' 21 | value: '#AFCBCE' 22 | - label: 'neutral 3' 23 | value: '#23393D' 24 | - label: 'neutral 4' 25 | value: '#87AF9C' 26 | - label: 'yes' 27 | value: '#0EBF00' 28 | - label: 'no' 29 | value: '#ED1B24' 30 | - label: 'maybe' 31 | value: '#808080' 32 | - label: 'Labour' 33 | value: '#ED1B24' 34 | - label: 'Conservative' 35 | value: '#022397' 36 | - label: 'LibDem' 37 | value: '#FDBB30' 38 | - label: 'Ukip' 39 | value: '#722889' 40 | - label: 'Green' 41 | value: '#6AB023' 42 | - label: '#1f77b4' # begin c3.js defaults 43 | value: '#1f77b4' 44 | - label: '#aec7e8' 45 | value: '#aec7e8' 46 | - label: '#ff7f0e' 47 | value: '#ff7f0e' 48 | - label: '#ffbb78' 49 | value: '#ffbb78' 50 | - label: '#2ca02c' 51 | value: '#2ca02c' 52 | - label: '#98df8a' 53 | value: '#98df8a' 54 | - label: '#d62728' 55 | value: '#d62728' 56 | - label: '#ff9896' 57 | value: '#ff9896' 58 | - label: '#9467bd' 59 | value: '#9467bd' 60 | - label: '#c5b0d5' 61 | value: '#c5b0d5' 62 | - label: '#8c564b' 63 | value: '#8c564b' 64 | - label: '#c49c94' 65 | value: '#c49c94' 66 | - label: '#e377c2' 67 | value: '#e377c2' 68 | - label: '#f7b6d2' 69 | value: '#f7b6d2' 70 | - label: '#7f7f7f' 71 | value: '#7f7f7f' 72 | - label: '#c7c7c7' 73 | value: '#c7c7c7' 74 | - label: '#bcbd22' 75 | value: '#bcbd22' 76 | - label: '#dbdb8d' 77 | value: '#dbdb8d' 78 | - label: '#17becf' 79 | value: '#17becf' 80 | - label: '#9edae5' 81 | value: '#9edae5' 82 | 83 | background colors: 84 | - label: 'white' 85 | value: '#ffffff' 86 | 87 | defaults: 88 | grid x: false 89 | grid y: false 90 | axis y: true 91 | axis y2: false 92 | axis x: true 93 | legend: true 94 | point: false 95 | y axis: 'y' 96 | title text: 97 | title author: 98 | title source: 99 | title position: top-left 100 | charts: 101 | series: 102 | grid: 103 | y: 104 | show: true 105 | x: 106 | show: true 107 | pie: 108 | grid: 109 | y: 110 | show: false 111 | x: 112 | show: false 113 | donut: 114 | y: 115 | show: false 116 | x: 117 | show: false 118 | gauge: 119 | y: 120 | show: false 121 | x: 122 | show: false 123 | 124 | backgroundColor: 'white' 125 | 126 | colorPicker: 'simple' 127 | 128 | financialService: 'yahoo' 129 | 130 | themes: 131 | - name: 'Default (Red Box)' 132 | file: 'config.yaml' 133 | - name: 'The Times' 134 | file: 'themes/thetimes.config.yaml' 135 | - name: 'Sunday Times' 136 | file: 'themes/sundaytimes.config.yaml' 137 | --------------------------------------------------------------------------------