├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── ARCHITECTURE.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── addon ├── .gitkeep ├── components │ ├── plaid-area │ │ ├── component.js │ │ └── template.hbs │ ├── plaid-axis │ │ └── component.js │ ├── plaid-bar │ │ └── component.js │ ├── plaid-donut │ │ ├── component.js │ │ └── template.hbs │ ├── plaid-line │ │ ├── component.js │ │ └── template.hbs │ ├── plaid-plot.js │ ├── plaid-scatter │ │ ├── component.js │ │ └── template.hbs │ ├── plaid-symbol.js │ └── plaid-text │ │ ├── component.js │ │ └── template.hbs ├── helpers │ ├── area.js │ ├── curve.js │ ├── extent.js │ ├── format-fn.js │ ├── format.js │ ├── linear-scale.js │ └── pair-by.js ├── mixins │ ├── coordinates.js │ ├── dimensions.js │ ├── global-resize.js │ ├── group-element.js │ └── plot-area.js ├── templates │ └── components │ │ ├── plaid-plot.hbs │ │ └── plaid-symbol.hbs └── utils │ ├── box-expression.js │ └── computed-extent.js ├── app ├── .gitkeep ├── components │ ├── plaid-area │ │ └── component.js │ ├── plaid-axis │ │ └── component.js │ ├── plaid-bar │ │ └── component.js │ ├── plaid-donut │ │ └── component.js │ ├── plaid-line │ │ └── component.js │ ├── plaid-plot.js │ ├── plaid-scatter │ │ └── component.js │ ├── plaid-symbol.js │ └── plaid-text │ │ └── component.js ├── helpers │ ├── area.js │ ├── curve.js │ ├── extent.js │ ├── format-fn.js │ ├── format.js │ ├── linear-scale.js │ └── pair-by.js └── utils │ ├── box-expression.js │ └── computed-extent.js ├── blueprints ├── .jshintrc └── maximum-plaid │ └── index.js ├── bower.json ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── logo ├── line-chart-screenshot.png └── maximum-plaid-logo.png ├── maximum-plaid.sublime-project ├── package.json ├── testem.js ├── tests ├── .eslintrc.js ├── .jshintrc ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── styles │ │ │ └── app.scss │ │ └── templates │ │ │ ├── application.hbs │ │ │ ├── charts │ │ │ └── -line-chart.hbs │ │ │ ├── components │ │ │ └── .gitkeep │ │ │ ├── index.hbs │ │ │ └── loading.hbs │ ├── config │ │ └── environment.js │ ├── data │ │ └── time-series.json │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── destroy-app.js │ ├── module-for-acceptance.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── integration │ ├── .gitkeep │ └── components │ │ ├── plaid-area │ │ └── component-test.js │ │ ├── plaid-axis │ │ └── component-test.js │ │ ├── plaid-bar │ │ └── component-test.js │ │ ├── plaid-donut │ │ └── component-test.js │ │ ├── plaid-plot-test.js │ │ ├── plaid-symbol-test.js │ │ └── plaid-text │ │ └── component-test.js ├── test-helper.js └── unit │ ├── .gitkeep │ ├── helpers │ ├── area-test.js │ ├── curve-test.js │ ├── extent-test.js │ ├── format-fn-test.js │ ├── format-test.js │ ├── linear-scale-test.js │ └── pair-by-test.js │ ├── mixins │ ├── coordinates-test.js │ ├── dimensions-test.js │ ├── global-resize-test.js │ ├── group-element-test.js │ └── plot-area-test.js │ └── utils │ ├── box-expression-test.js │ └── computed-extent-test.js └── vendor └── .gitkeep /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 7, 5 | sourceType: 'module' 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:ember-suave/recommended' 10 | ], 11 | env: { 12 | 'browser': true, 13 | 'es6': true 14 | }, 15 | rules: { 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | .DS_Store 19 | *.sublime-workspace 20 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ember-suave" 3 | } 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esversion": 6, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .jshintrc 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "stable" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - $HOME/.npm 11 | - $HOME/.cache # includes bowers cache 12 | 13 | env: 14 | # we recommend testing LTS's and latest stable release (bonus points to beta/canary) 15 | - EMBER_TRY_SCENARIO=ember-lts-2.4 16 | - EMBER_TRY_SCENARIO=ember-lts-2.8 17 | - EMBER_TRY_SCENARIO=ember-release 18 | - EMBER_TRY_SCENARIO=ember-beta 19 | - EMBER_TRY_SCENARIO=ember-canary 20 | 21 | matrix: 22 | fast_finish: true 23 | allow_failures: 24 | - env: EMBER_TRY_SCENARIO=ember-canary 25 | 26 | before_install: 27 | - npm config set spin false 28 | - npm install -g bower 29 | - bower --version 30 | - npm install phantomjs-prebuilt 31 | - node_modules/phantomjs-prebuilt/bin/phantomjs --version 32 | 33 | install: 34 | - npm install 35 | - bower install 36 | 37 | script: 38 | # Usually, it's ok to finish the test scenario without reverting 39 | # to the addon's original dependency state, skipping "cleanup". 40 | - ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup 41 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Maximum Plaid Architecture 2 | 3 | Maximum Plaid is a visualisation toolset, designed to be a consistent and easy to use high level grammar for specifying data visualisations. It is not designed to be a mapping directly to D3 or to SVG with nice Ember syntax sugar on top, instead it uses D3 as helpers for transforming and scaling data, and presenting the visualisation. 4 | 5 | To understand how this works, we must first break down a visualisation in to smaller building blocks or layers, based on the [Grammar of Graphics](http://www.springer.com/us/book/9780387245447): 6 | 7 | - Data `DATA`: A set of operations which create variables from datasets, 8 | - Transformations `TRANS`: Variable transformations to something our chart can render, 9 | - Scale `SCALE`: Scale transformations (linear, log, etc.), 10 | - Coordinates `COORD`: A coordinate system e.g. Polar, Cartesian Plane, 11 | - Elements `ELEMENT`: graphs (e.g., points) and their aesthetic attributes (e.g., color), 12 | - Guides `GUIDE`: one or more guides (axes, legends, etc.). 13 | 14 | We'll include two additional layers to make things more interesting: 15 | 16 | - Interaction `INT`: interaction controls (e.g zoom, select, pan), 17 | - Animation `ANIM`: animations as data transitions from one state to another to provide data constancy. 18 | 19 | These layers will be tied together to build charts consisting of the following types of Elements: 20 | 21 | - Lines 22 | - Areas 23 | - Pies / Donuts 24 | - Bars 25 | - Scatter plots 26 | - Stacked/Stream based layouts for bars and areas. 27 | 28 | D3 also provides methods for calculating Histograms from data to easily produce binned data for each bar, amongst other transformations. 29 | 30 | ## SVG or Canvas? 31 | On a technical level, D3 supports outputting path data for lines and areas for SVG, as well as calling the drawing APIs of `Canvas` to produce the same shapes in a Canvas context. 32 | 33 | Because of this, we’d like to make it a design goal of supporting both rendering contexts, and not produce a library which simply wraps Ember components around SVG elements. Of course if you’re exclusively doing an SVG chart you may prefer to rendering SVG elements for certain markings within your visualisation. 34 | 35 | ## Syntax 36 | Ember provides an incredibly powerful templating language with composable helpers and components. 37 | 38 | To best explain how this works with data visualisation, lets see an example: 39 | 40 | Assume we have some time series data: 41 | 42 | ```js 43 | let dataPoints = [{timestamp: 1460864483048, value: 1288}, …] 44 | ``` 45 | 46 | We could draw a simple line chart like so: 47 | 48 | ```hbs 49 | {{#plaid-plot 50 | xScale=(linear-scale (extent (map-by 'timestamp' dataPoints)) (extent plotArea.width)) 51 | yScale=(linear-scale (extent (map-by 'value' dataPoints)) (extent plotArea.height)) 52 | as |plot|}} 53 | 54 | {{plot.line dataPoints}} 55 | {{/plaid-plot}} 56 | ``` 57 | 58 | First we're initialising a `plot` component which will make it easy and straight forward to apply additional layers within this context. It requires an `xScale` and `yScale` to help position Elements and Guides. You can easily compute both scales using helpers for the respective scaling function you need. 59 | 60 | This is already supported. However, we'll often need more complexity than this. Let’s say we need the line to slide left when new data is appended to the `dataPoints` array. We could add transition support to the `line` component, but this might not always be the behaviour you want and it would limit the usefulness of the `line` component. So we need to _decorate_ this component with some more abilities. 61 | 62 | ```hbs 63 | {{#plaid-plot 64 | xScale=(linear-scale (extent (map-by 'timestamp' dataPoints)) (extent plotArea.width)) 65 | yScale=(linear-scale (extent (map-by 'value' dataPoints)) (extent plotArea.height)) 66 | as |plot|}} 67 | 68 | {{#plot.transition duration=250 easing=(ease-in-out) as |plot|}} 69 | {{plot.line dataPoints}} 70 | {{/plot.transition-group}} 71 | {{/plaid-plot}} 72 | ``` 73 | 74 | Under the hood, if this line is rendering to SVG it will use D3 transition to interpolate its path data over `duration` from current value to future value, and apply that value for each tick of a timer. 75 | 76 | > Potentially this could use `liquid-fire` directly or learn some tricks for how this works and apply it to D3 and Ember. 77 | 78 | To draw an area chart similar to that seen in Skylight, we could use both an area and a line: 79 | 80 | ```hbs 81 | {{#plaid-plot 82 | xScale=(linear-scale (extent (map-by 'timestamp' dataPoints)) (extent plotArea.width)) 83 | yScale=(linear-scale (extent (map-by 'value' dataPoints)) (extent plotArea.height)) 84 | as |plot|}} 85 | 86 | {{#plot.transition duration=250 easing=(ease-in-out) as |plot|}} 87 | {{plot.area dataPoints fill="#CE93D8"}} 88 | {{plot.line dataPoints stroke="#9C27B0"}} 89 | {{/plot.transition-group}} 90 | {{/plaid-plot}} 91 | ``` 92 | 93 | # Interaction 94 | Static charts aren't very useful in ambitious applications, which is why we need some way to interact with them. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # maximum plaid changelog 2 | 3 | ### HEAD (Dec 06, 2016) 4 | 5 | - Updated to Ember 2.10 6 | 7 | ### 0.1.2 (May 30, 2016) 8 | 9 | - Version bumped because of NPM conflict. 10 | 11 | ### 0.1.1 (May 30, 2016) 12 | 13 | - [#7](https://github.com/ivanvanderbyl/maximum-plaid/pull/7) Upgraded to Ember CLI 2.5 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Deprecated: Please see (and use) [ember-primer](https://github.com/ember-visualization/ember-primer) instead, as this is no longer maintained** 2 | 3 | ![Maximum Plaid](/logo/maximum-plaid-logo.png) 4 | 5 | [![Build Status](https://travis-ci.org/ivanvanderbyl/maximum-plaid.svg?branch=master)](https://travis-ci.org/ivanvanderbyl/maximum-plaid) 6 | [![Ember Observer Score](http://emberobserver.com/badges/maximum-plaid.svg)](http://emberobserver.com/addons/maximum-plaid) 7 | 8 | Template driven data visualisation for ambitious applications. 9 | 10 | # Design 11 | 12 | Maximum Plaid is designed to be a high level visualisation library built exclusively for Ember. It uses Ember's templating language to efficiently and declaratively produce easily maintained and tested visualisations. 13 | 14 | We're building this because traditional charting libraries often times have poor separation of concerns from data and presentation, which leads to poor maintainability and code reuse, as well as exhibiting poor performance with medium to large datasets (10K to 100K data points). 15 | 16 | We desire something which exposes abstractions for efficiently expressing a wide variety of visualisations, while balancing expressiveness such that you're not locked in to one particular style of visualisation which doesn't quite express the discoveries you've made in your data. 17 | 18 | # Proposed API 19 | 20 | Visualisations are composed in layers, starting with data transformation down to visualisation and interaction. 21 | 22 | ## Data Transformation 23 | 24 | In order to provide a consistent API for each visualisation layer, we do the data setup ahead of time. After you've loaded your data source, you should transform it to the necessary shape for the type of visualisation you're rendering. 25 | 26 | We provide a few helpers to make this easy (more soon). 27 | 28 | ### Series Transforms 29 | 30 | # `pair-by`(attr, [attr2,] series) 31 | 32 | > Basically a multi-property `map-by`. 33 | 34 | Takes an array of objects containing discrete properties for each axis of your visualisations, and returns an array of tuples containing only the values for each attribute. 35 | 36 | Given the series: 37 | 38 | ```js 39 | let timeSeriesData = [ 40 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 41 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 42 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 43 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 44 | { timestamp: 1450345960000, value: 5, projectId: 200 } 45 | ]; 46 | ``` 47 | 48 | Using `pair-by` in your template: 49 | 50 | ```hbs 51 | {{pair-by "timestamp" "value" timeSeriesData}} 52 | ``` 53 | 54 | Would produce: 55 | 56 | ```js 57 | [ 58 | [1450345920000, 1], 59 | [1450345930000, 2], 60 | [1450345940000, 3], 61 | [1450345950000, 4], 62 | [1450345960000, 5] 63 | ] 64 | ``` 65 | 66 | Which is ideal for using as the `values` argument to `plaid.line` and `plaid.area`. 67 | 68 | ## Coordinates 69 | 70 | SVG doesn't use the same coordinate space as CSS, so we have to calculate margins manually. To help out with this we've included an `area` helper, which returns an object containing the positioning data for your chart. 71 | 72 | # `area`(width height [margin="TOP RIGHT BOTTOM LEFT"]) 73 | 74 | The margin string follows the typical box model margin convention of TOP, RIGHT, BOTTOM, LEFT. All values are unitless and inherit their units from the coordinate space with the SVG context. 75 | 76 | ## Presentation 77 | 78 | On their own, components for even the simplest elements in a visualisation can quickly require complicated APIs. To solve this, Ember contextual components can provide lower level primitive components with the necessary inputs for scaling and positioning. This reduces the API surface for the user to quickly produce visualisations in very few lines of code. 79 | 80 | ```hbs 81 | {{#plaid-plot xScale yScale plotArea as |plot|}} 82 | {{plot.right-axis}} 83 | {{plot.line values}} 84 | {{/plaid-plot}} 85 | ``` 86 | 87 | A more complete example, using scales from [ember-d3-scale](https://github.com/spencer516/ember-d3-scale#linear-scale) and helpers from [ember-composable-helpers](https://github.com/DockYard/ember-composable-helpers) 88 | 89 | ```hbs 90 | {{#with (area 720 220 margin="0 50 72 50") as |plotArea|}} 91 | {{#plaid-plot 92 | (time-scale (extent (map-by "timestamp" responseTimeMean)) (array 0 plotArea.width)) 93 | (linear-scale (extent (map-by "value" responseTimeMean) toZero=true) (array plotArea.height 0)) 94 | plotArea as |plot|}} 95 | 96 | {{#with (pair-by "timestamp" "value" responseTimeMean) as |values|}} 97 | 98 | {{plot.line values stroke="#673AB7" strokeWidth="2" curve=(curve "basis")}} 99 | {{plot.area values fill="#D1C4E9" curve=(curve "basis")}} 100 | {{plot.bottom-axis values ticks=10}} 101 | {{plot.left-axis values tickFormat=(format-fn "0.1s" suffix="ms") ticks=2}} 102 | {{/with}} 103 | {{/plaid-plot}} 104 | {{/with}} 105 | ``` 106 | 107 | This will produce a pretty simple line + area chart: 108 | 109 | [![Maximum Plaid Line Chart](/logo/line-chart-screenshot.png)](http://maximum-plaid.com) 110 | 111 | # Components 112 | 113 | ### `plaid-symbol` 114 | 115 | Provides an easy to use and straight forward interface to `d3-shape`'s symbol 116 | generators. `primitive-symbol` can be used in the same way as any Ember component 117 | and will render as a `` tag containing the path data for the specified 118 | symbol type. 119 | 120 | #### Options 121 | 122 | - `type`: Symbol to render, can be any of `circle`, `diamond`, `cross`, 123 | `square`, `star`, `triangle`, `wye`. 124 | - `size`: Specifies the symbol render size. This is the area of the 125 | symbol, which typicall equates to 1/4th of the actual width or height, depending 126 | on the shape. 127 | - `fill`: SVG path `fill` property. 128 | - `stroke`: SVG path `stroke` property. 129 | - `strokeWidth`: SVG path `stroke-width` property. 130 | - `top`: Top offset (applied using transform). 131 | - `left`: Left offset (applied using transform). 132 | 133 | 134 | # Mixins 135 | 136 | ## `PlotArea` 137 | 138 | Provides simple calculations for specifying the position of the main graphic in 139 | a visualisation. 140 | 141 | ### Example: 142 | 143 | ```js 144 | import { PlotArea } from 'maximum-plaid/mixins/plot-area'; 145 | 146 | export default Component.extend(PlotArea, { 147 | margin: '16 24', 148 | 149 | didRender() { 150 | const { top, left } = this.get('plotArea'); 151 | select(this.element).select('g.plot').attr('transform', `translate(${left},${top})`); 152 | } 153 | }); 154 | ``` 155 | 156 | # Utils 157 | 158 | ## `computedExtent` 159 | 160 | Creates a computed property which calculates the extent of all inputs provided 161 | using `extent` from d3-array. 162 | 163 | ## Installation 164 | 165 | **NOTE: `maximum-plaid` requires Ember v2.3+** 166 | 167 | ember install maximum-plaid 168 | 169 | ## Running 170 | 171 | * `ember server` 172 | * Visit your app at http://localhost:4200. 173 | 174 | ## Running Tests 175 | 176 | * `npm test` (Runs `ember try:testall` to test your addon against multiple Ember versions) 177 | * `ember test` 178 | * `ember test --server` 179 | 180 | ## Building 181 | 182 | * `ember build` 183 | 184 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 185 | 186 | # FAQ 187 | 188 | ### Why `maximum-plaid`? 189 | 190 | **Ivan**: Initially I was searching name which represented both a 191 | type of fabric for the visualisation metaphore, but also I've been told that friends 192 | have lost me in crowds walking around Williamsburg because I wear so much plaid. Also, the goal 193 | of this project is to make data visualisation easy and efficient, so in keeping with 194 | the promised performance mode of the next [Tesla Roadster](http://mashable.com/2015/07/17/new-tesla-roadster/#3NCT_4NpL8qU), I found "maximum plaid" to be 195 | fitting. 196 | 197 | ### You spelt visualization wrong 198 | 199 | That's not a question. 200 | 201 | As the main author of this project is Australian — a country which speaks a 202 | variation of British English, words such as `visualisation` are spelt with an `s` 203 | instead of a `z`. Another word you may find incorrectly spelt is `colour`. Please 204 | don't issue Pull Requests to fix this. 205 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/addon/.gitkeep -------------------------------------------------------------------------------- /addon/components/plaid-area/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import layout from './template'; 3 | import { area } from 'd3-shape'; 4 | import GroupElement from '../../mixins/group-element'; 5 | import computed from 'ember-computed'; 6 | 7 | const { 8 | isPresent, 9 | String: { dasherize, w } 10 | } = Ember; 11 | 12 | const AreaComponent = Ember.Component.extend(GroupElement, { 13 | layout, 14 | 15 | /** 16 | * xScale function 17 | * 18 | * @public 19 | * @type {D3 Scale} 20 | */ 21 | xScale: null, 22 | 23 | /** 24 | * yScale function 25 | * 26 | * @public 27 | * @type {D3 Scale} 28 | */ 29 | yScale: null, 30 | 31 | /** 32 | * Values to render line from. These should be the same as those used 33 | * for the domains of the scaling functions. 34 | * 35 | * @public 36 | * @type {Array} 37 | */ 38 | values: [], 39 | 40 | fill: 'black', 41 | 42 | fillOpacity: 1.0, 43 | 44 | didRender() { 45 | let pathAttrs = this.getProperties(w('fill fillOpacity')); 46 | let area = this.selection.select('path.area'); 47 | Object.keys(pathAttrs).forEach((attr) => { 48 | let value = pathAttrs[attr]; 49 | 50 | if (isPresent(value)) { 51 | area.attr(dasherize(attr), value); 52 | } 53 | }); 54 | }, 55 | 56 | pathData: computed('values.[]', 'xScale', 'yScale', 'curve', { 57 | get() { 58 | let { values, xScale, yScale, curve } = 59 | this.getProperties('values', 'xScale', 'yScale', 'curve'); 60 | 61 | let areaFn = area() 62 | .x((d) => xScale(d[0])) 63 | .y1((d) => yScale(d[1])) 64 | .y0(yScale(0)); 65 | 66 | if (curve) { 67 | areaFn.curve(curve); 68 | } 69 | 70 | return areaFn(values); 71 | } 72 | }) 73 | }); 74 | 75 | AreaComponent.reopenClass({ 76 | positionalParams: ['values'] 77 | }); 78 | 79 | export default AreaComponent; 80 | -------------------------------------------------------------------------------- /addon/components/plaid-area/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addon/components/plaid-axis/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import GroupElement from '../../mixins/group-element'; 3 | import { axisTop, axisRight, axisBottom, axisLeft } from 'd3-axis'; 4 | 5 | const { 6 | Component, 7 | run: { scheduleOnce } 8 | } = Ember; 9 | 10 | const AXIS_MAP = { 11 | top: axisTop, 12 | right: axisRight, 13 | bottom: axisBottom, 14 | left: axisLeft 15 | }; 16 | 17 | export default Component.extend(GroupElement, { 18 | layout: null, 19 | classNames: [ 'axis', 'Plaid-axis' ], 20 | classNameBindings: [ 'orientation' ], 21 | 22 | y: 0, 23 | 24 | x: 0, 25 | 26 | ticks: 3, 27 | 28 | /** 29 | * Represents the axis orientation. You should always declare this. 30 | * 31 | * @public 32 | * @type {String} 33 | */ 34 | orientation: 'top', 35 | 36 | /** 37 | * A scaling function used for this axis. 38 | * 39 | * @public 40 | * @type {Function} 41 | */ 42 | scale: null, 43 | 44 | /** 45 | * The format used for the ticks for this axis. 46 | * [See D3 docs for more details](https://github.com/d3/d3-axis#axis_tickFormat) 47 | * 48 | * @public 49 | * @type {Function} 50 | */ 51 | tickFormat: null, 52 | 53 | /** 54 | * The inner tick size for the ticks for this axis. 55 | * [See D3 docs for more details](https://github.com/d3/d3-axis#axis_tickSizeInner) 56 | * 57 | * @public 58 | * @type {Number} 59 | */ 60 | tickSizeInner: 4, 61 | 62 | /** 63 | * The outer tick size for the ticks for this axis. 64 | * [See D3 docs for more details](https://github.com/d3/d3-axis#axis_tickSizeOuter) 65 | * 66 | * @public 67 | * @type {Number} 68 | */ 69 | tickSizeOuter: 8, 70 | 71 | /** 72 | * Explicit tick values for this axis. 73 | * [See D3 docs for more details](https://github.com/d3/d3-axis#axis_tickValues) 74 | * 75 | * @public 76 | * @type {Array} 77 | */ 78 | tickValues: null, 79 | 80 | xOffset: 0, 81 | 82 | yOffset: 0, 83 | 84 | didReceiveAttrs() { 85 | scheduleOnce('afterRender', this, this.drawAxis); 86 | }, 87 | 88 | drawAxis() { 89 | let { y, x, xOffset, yOffset, scale, orientation, tickFormat, ticks, tickSizeInner, tickSizeOuter, tickValues } = 90 | this.getProperties('y', 'x', 'xOffset', 'yOffset', 'scale', 'orientation', 'tickFormat', 'ticks', 'tickSizeInner', 'tickSizeOuter', 'tickValues'); 91 | 92 | let axis = this.createAxis(orientation, scale); 93 | 94 | axis.tickFormat(tickFormat); 95 | axis.tickSize(tickSizeInner, tickSizeOuter); 96 | axis.tickValues(tickValues); 97 | axis.scale(scale); 98 | 99 | if (ticks) { 100 | axis.ticks(ticks); 101 | } 102 | 103 | this.selection.call(axis); 104 | this.selection.attr('transform', `translate(${x + xOffset}, ${y + yOffset})`); 105 | }, 106 | 107 | createAxis(orient, scale) { 108 | return AXIS_MAP[orient](scale); 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /addon/components/plaid-bar/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import GroupElement from '../../mixins/group-element'; 3 | import { max, min } from 'd3-array'; 4 | 5 | const { 6 | assert, 7 | Component, 8 | getProperties, 9 | run: { scheduleOnce }, 10 | typeOf 11 | } = Ember; 12 | 13 | const PlaidBarComponent = Component.extend(GroupElement, { 14 | layout: null, 15 | 16 | /** 17 | * Represents the bar orientation. May be "vertical" or "horizontal" 18 | * 19 | * @public 20 | * @type {String} 21 | */ 22 | orientation: 'vertical', 23 | 24 | /** 25 | xScale function 26 | 27 | @public 28 | @type {D3 Scale} 29 | */ 30 | xScale: null, 31 | 32 | /** 33 | yScale function 34 | 35 | @public 36 | @type {D3 Scale} 37 | */ 38 | yScale: null, 39 | 40 | /** 41 | Values to render bars from. These should be the same as those used 42 | for the domains of the scaling functions. 43 | 44 | @public 45 | @type {Array} 46 | */ 47 | values: [], 48 | 49 | fill: 'black', 50 | 51 | fillOpacity: 1.0, 52 | 53 | didReceiveAttrs() { 54 | this._super(...arguments); 55 | 56 | let orientation = this.get('orientation'); 57 | 58 | assert(`bar chart orientation must be in {vertical,horizontal}, was "${orientation}"`, 59 | orientation === 'vertical' || orientation === 'horizontal'); 60 | 61 | let checkScale = orientation === 'vertical' ? 'xScale' : 'yScale'; 62 | 63 | assert(`${checkScale} must be a band-scale for ${orientation} bar charts`, typeOf(this.get(checkScale).bandwidth) === 'function'); 64 | 65 | scheduleOnce('afterRender', this, this.drawBars); 66 | }, 67 | 68 | drawBars() { 69 | let { values, xScale, yScale, fill, fillOpacity, orientation } = 70 | getProperties(this, 'values', 'xScale', 'yScale', 'fill', 'fillOpacity', 'orientation'); 71 | 72 | let x, width, y, height; 73 | 74 | if (orientation === 'vertical') { 75 | let maxHeight = max(yScale.range()); 76 | x = (d) => xScale(d[0]); 77 | width = xScale.bandwidth(); 78 | y = (d) => yScale(d[1]); 79 | height = (d) => maxHeight - yScale(d[1]); 80 | } else { 81 | x = min(xScale.range()); 82 | width = (d) => xScale(d[0]); 83 | y = (d) => yScale(d[1]); 84 | height = yScale.bandwidth(); 85 | } 86 | 87 | // JOIN new data with old elements 88 | let bars = this.selection.selectAll('.bar').data(values); 89 | 90 | // EXIT old elements not present in new data 91 | bars.exit().remove(); 92 | 93 | // ENTER new elements present in new data 94 | let enterBars = bars.enter().append('rect').attr('class', 'bar'); 95 | 96 | // MERGE the existing and with the entered and UPDATE 97 | bars.merge(enterBars) 98 | .attr('class', 'bar') 99 | .attr('x', x) 100 | .attr('width', width) 101 | .attr('y', y) 102 | .attr('height', height) 103 | .attr('fill', fill) 104 | .attr('fillOpacity', fillOpacity); 105 | } 106 | }); 107 | 108 | PlaidBarComponent.reopenClass({ 109 | positionalParams: [ 'values' ] 110 | }); 111 | 112 | export default PlaidBarComponent; 113 | -------------------------------------------------------------------------------- /addon/components/plaid-donut/component.js: -------------------------------------------------------------------------------- 1 | import { arc, pie } from 'd3-shape'; 2 | import Ember from 'ember'; 3 | import layout from './template'; 4 | import GroupElement from '../../mixins/group-element'; 5 | import { interpolateInferno, scaleSequential } from 'd3-scale'; 6 | import { color } from 'd3-color'; 7 | const { 8 | Component, 9 | computed, 10 | get, 11 | getProperties, 12 | run, 13 | run: { scheduleOnce } 14 | } = Ember; 15 | 16 | const DonutComponent = Component.extend(GroupElement, { 17 | layout, 18 | 19 | radius: computed('width', 'height', function() { 20 | let { width, height } = getProperties(this, 'width', 'height'); 21 | return Math.min(width, height) / 2; 22 | }), 23 | 24 | transform: computed('width', 'height', function() { 25 | let { width, height } = getProperties(this, 'width', 'height'); 26 | 27 | return `translate(${width / 2},${height / 2})`; 28 | }), 29 | 30 | innerRadius: computed('radius', function() { 31 | return get(this, 'radius') - 32; 32 | }), 33 | 34 | outerRadius: computed('radius', function() { 35 | return get(this, 'radius'); 36 | }), 37 | 38 | cornerRadius: 5, 39 | colorInterpolator: interpolateInferno, 40 | padDegrees: 5, 41 | 42 | drawnValues: [], 43 | 44 | didReceiveAttrs() { 45 | this._super(...arguments); 46 | 47 | scheduleOnce('afterRender', this, this.draw); 48 | }, 49 | 50 | pie: computed('padDegrees', function() { 51 | return pie() 52 | .padAngle(get(this, 'padDegrees') / 360) 53 | .value((d) => d[1]); 54 | }), 55 | 56 | piedValues: computed('pie', 'values.[]', function() { 57 | let { values, pie } = getProperties(this, 'values', 'pie'); 58 | 59 | return pie(values); 60 | }), 61 | 62 | arc: computed('cornerRadius', 'innerRadius', 'outerRadius', function() { 63 | let { cornerRadius, innerRadius, outerRadius } = 64 | getProperties(this, 'cornerRadius', 'innerRadius', 'outerRadius'); 65 | 66 | return arc() 67 | .cornerRadius(cornerRadius) 68 | .innerRadius(innerRadius) 69 | .outerRadius(outerRadius); 70 | }), 71 | 72 | draw() { 73 | let { piedValues: values, arc, colorInterpolator } = getProperties(this, 'piedValues', 'arc', 'colorInterpolator'); 74 | let arcs = this.selection.selectAll('g.arc'); 75 | let join = arcs.data(values); 76 | // DATA REMOVE 77 | join.exit().remove(); 78 | 79 | // Use a colour interpolator to render the colour for each bar. 80 | let arcColorScale = scaleSequential(colorInterpolator).domain([0, values.length]); 81 | 82 | // DATA ENTER 83 | let enterJoin = join.enter().append('g').attr('class', 'arc'); 84 | enterJoin.append('path'); 85 | 86 | // DATA MERGE + UPDATE 87 | enterJoin.merge(join) 88 | .attr('data-title', (d) => d.data[0]) 89 | .select('path') 90 | .attr('d', arc) 91 | .attr('fill', (d,i) => { 92 | let c = color(arcColorScale(i)); 93 | c.opacity = 0.54; 94 | return c.toString(); 95 | }) 96 | .attr('stroke', (d,i) => arcColorScale(i)) 97 | .on('click', (d) => run(this, this.sendAction, 'on-click', d.data[0])) 98 | .on('mouseenter', (d) => run(this, this.sendAction, 'on-enter', d.data[0])) 99 | .on('mouseleave', (d) => run(this, this.sendAction, 'on-leave', d.data[0])); 100 | 101 | } 102 | }); 103 | 104 | DonutComponent.reopenClass({ 105 | positionalParams: [ 'values' ] 106 | }); 107 | 108 | export default DonutComponent; 109 | -------------------------------------------------------------------------------- /addon/components/plaid-donut/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /addon/components/plaid-line/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import layout from './template'; 3 | import GroupElement from '../../mixins/group-element'; 4 | import { line, curveMonotoneX } from 'd3-shape'; 5 | 6 | const { 7 | computed, 8 | isPresent, 9 | String: { dasherize, w } 10 | } = Ember; 11 | 12 | const PlaidLineComponent = Ember.Component.extend(GroupElement, { 13 | layout, 14 | 15 | /** 16 | * xScale function 17 | * 18 | * @public 19 | * @type {D3 Scale} 20 | */ 21 | xScale: null, 22 | 23 | /** 24 | * yScale function 25 | * 26 | * @public 27 | * @type {D3 Scale} 28 | */ 29 | yScale: null, 30 | 31 | /** 32 | * Values to render line from. These should be the same as those used 33 | * for the domains of the scaling functions. 34 | * 35 | * @public 36 | * @type {Array} 37 | */ 38 | values: [], 39 | 40 | stroke: 'black', 41 | strokeWidth: 2, 42 | strokeOpacity: 1.0, 43 | 44 | fill: 'none', 45 | fillOpacity: null, 46 | 47 | curve: curveMonotoneX, 48 | 49 | didRender() { 50 | let pathAttrs = this.getProperties(w('stroke strokeWidth strokeOpacity fill fillOpacity')); 51 | let line = this.selection.select('path.line'); 52 | Object.keys(pathAttrs).forEach((attr) => { 53 | let value = pathAttrs[attr]; 54 | 55 | if (isPresent(value)) { 56 | line.attr(dasherize(attr), value); 57 | } 58 | }); 59 | }, 60 | 61 | pathData: computed('values.[]', 'xScale', 'yScale', 'curve', { 62 | get() { 63 | let { values, xScale, yScale, curve } = 64 | this.getProperties('values', 'xScale', 'yScale', 'curve'); 65 | 66 | let lineFn = line() 67 | .curve(curve) 68 | .x((d) => xScale(d[0])) 69 | .y((d) => yScale(d[1])); 70 | 71 | if (curve) { 72 | lineFn.curve(curve); 73 | } 74 | 75 | return lineFn(values); 76 | } 77 | }) 78 | }); 79 | 80 | PlaidLineComponent.reopenClass({ 81 | positionalParams: ['values'] 82 | }); 83 | 84 | export default PlaidLineComponent; 85 | -------------------------------------------------------------------------------- /addon/components/plaid-line/template.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/components/plaid-plot.js: -------------------------------------------------------------------------------- 1 | import layout from '../templates/components/plaid-plot'; 2 | import Coordinates from 'maximum-plaid/mixins/coordinates'; 3 | import Component from 'ember-component'; 4 | 5 | let PlotComponent = Component.extend(Coordinates, { 6 | layout, 7 | 8 | tagName: 'svg', 9 | 10 | classNames: ['plaid-plot'], 11 | 12 | attributeBindings: ['plotArea.outerWidth:width', 'plotArea.outerHeight:height'], 13 | 14 | /** 15 | * Represents the xScale, if used. 16 | * 17 | * @public 18 | * @type {D3 Scale} 19 | */ 20 | xScale: null, 21 | 22 | /** 23 | * Represents the yScale, if used. 24 | * 25 | * @public 26 | * @type {D3 Scale} 27 | */ 28 | yScale: null 29 | 30 | /** 31 | * Represents the coordinates to render the main graphic. 32 | * 33 | * This is typically the output of the Coordinates mixin: 34 | * 35 | * {top, right, bottom, left, width, height, outerWidth, outerHeight} 36 | * 37 | * @public 38 | * @type {Object} 39 | */ 40 | // plotArea: {} 41 | 42 | }); 43 | 44 | PlotComponent.reopenClass({ 45 | positionalParams: ['xScale', 'yScale', 'plotArea'] 46 | }); 47 | 48 | export default PlotComponent; 49 | -------------------------------------------------------------------------------- /addon/components/plaid-scatter/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import layout from './template'; 3 | import GroupElement from '../../mixins/group-element'; 4 | const { computed } = Ember; 5 | 6 | const ScatterComponent = Ember.Component.extend(GroupElement, { 7 | layout, 8 | 9 | xScale: null, 10 | 11 | yScale: null, 12 | 13 | values: [], 14 | 15 | positions: computed('values.[]', 'xScale', 'yScale', { 16 | get() { 17 | let { values, xScale, yScale } 18 | = this.getProperties('xScale', 'yScale', 'values'); 19 | 20 | return values.map(([timestamp, value]) => { 21 | return { 22 | x: xScale(timestamp), 23 | y: yScale(value) 24 | }; 25 | }); 26 | } 27 | }) 28 | }); 29 | 30 | ScatterComponent.reopenClass({ 31 | positionalParams: ['values'] 32 | }); 33 | 34 | export default ScatterComponent; 35 | -------------------------------------------------------------------------------- /addon/components/plaid-scatter/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each positions as |position|}} 2 | {{yield position.x position.y}} 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /addon/components/plaid-symbol.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Component from 'ember-component'; 3 | 4 | import { 5 | symbol, 6 | symbolCircle, 7 | symbolDiamond, 8 | symbolCross, 9 | symbolSquare, 10 | symbolStar, 11 | symbolTriangle, 12 | symbolWye 13 | } from 'd3-shape'; 14 | 15 | const { 16 | computed, 17 | assert, 18 | isPresent, 19 | typeOf 20 | } = Ember; 21 | 22 | /** 23 | * Generates the path data for a symbol as a tag. 24 | * 25 | * @public 26 | * Usage: 27 | * 28 | * {{plaid-symbol "TYPE" x y}} 29 | */ 30 | 31 | const SymbolComponent = Component.extend({ 32 | tagName: 'path', 33 | attributeBindings: [ 34 | 'symbolData:d', 35 | 'transform', 36 | 'stroke', 37 | 'fill', 38 | 'strokeWidth:stroke-width' 39 | ], 40 | classNames: ['symbol'], 41 | classNameBindings: ['type'], 42 | 43 | /** 44 | * Symbol size in area 45 | * 46 | * @public 47 | * @type {Number} 48 | */ 49 | size: 16, 50 | 51 | /** 52 | * Symbol to render 53 | * 54 | * Can either be a string or a symbol function. 55 | * 56 | * @public 57 | * @type {String} 58 | */ 59 | type: 'diamond', 60 | 61 | /** 62 | * Fill color or style 63 | * 64 | * @public 65 | * @type {String} 66 | */ 67 | fill: 'black', 68 | 69 | stroke: 'none', 70 | 71 | strokeWidth: 0, 72 | 73 | /** 74 | * Positioning offset from y 75 | * 76 | * @public 77 | * @type {Number} 78 | */ 79 | y: 0, 80 | 81 | /** 82 | * Positioning offset from x 83 | * 84 | * @public 85 | * @type {Number} 86 | */ 87 | x: 0, 88 | 89 | /** 90 | * Generates the SVG path data for the given symbol 91 | * 92 | * @public 93 | * @return {String} 94 | */ 95 | symbolData: computed('size', 'type', { 96 | get() { 97 | let { size, type } = this.getProperties('size', 'type'); 98 | let data; 99 | 100 | if (typeOf(type) !== 'string') { 101 | data = type; 102 | } else { 103 | data = { 104 | 'circle': symbolCircle, 105 | 'diamond': symbolDiamond, 106 | 'cross': symbolCross, 107 | 'square': symbolSquare, 108 | 'star': symbolStar, 109 | 'triangle': symbolTriangle, 110 | 'wye': symbolWye 111 | }[type.toLowerCase()]; 112 | assert(`Not a valid symbol type "${type}"`, isPresent(data)); 113 | } 114 | 115 | let fn = symbol(); 116 | fn.type(data); 117 | fn.size(size); 118 | 119 | return fn(); 120 | } 121 | }), 122 | 123 | transform: computed('y', 'x', { 124 | get() { 125 | let { y, x } = this.getProperties('y', 'x'); 126 | return `translate(${x},${y})`; 127 | } 128 | }) 129 | }); 130 | 131 | SymbolComponent.reopenClass({ 132 | positionalParams: ['type', 'x', 'y'] 133 | }); 134 | 135 | export default SymbolComponent; 136 | -------------------------------------------------------------------------------- /addon/components/plaid-text/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import layout from './template'; 3 | 4 | const { 5 | computed, 6 | Component, 7 | getProperties 8 | } = Ember; 9 | 10 | const PlaidTextComponent = Component.extend({ 11 | tagName: 'text', 12 | layout, 13 | classNames: [ 'plaid-text' ], 14 | attributeBindings: [ 'transform', 'fill', 'dx', 'dy', 'rotate', 'textLength', 'lengthAdjust', 'textAnchor:text-anchor' ], 15 | 16 | textAnchor: 'middle', 17 | 18 | x: 0, 19 | y: 0, 20 | fill: '#000', 21 | 22 | dx: null, 23 | dy: null, 24 | rotate: null, 25 | textLength: null, 26 | lengthAdjust: null, 27 | 28 | value: null, 29 | textRotate: 0, 30 | 31 | transform: computed('x', 'y', 'textRotate', { 32 | get() { 33 | let { x, y, textRotate } = getProperties(this, 'x', 'y', 'textRotate'); 34 | return `translate(${x},${y}) rotate(${textRotate})`; 35 | } 36 | }) 37 | }); 38 | 39 | PlaidTextComponent.reopenClass({ 40 | positionalParams: [ 'value' ] 41 | }); 42 | 43 | export default PlaidTextComponent; 44 | -------------------------------------------------------------------------------- /addon/components/plaid-text/template.hbs: -------------------------------------------------------------------------------- 1 | {{value}} -------------------------------------------------------------------------------- /addon/helpers/area.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import box from '../utils/box-expression'; 3 | const { Helper } = Ember; 4 | 5 | export function area(params, hash) { 6 | let [width, height] = params.slice(); 7 | hash = Object.assign({}, hash); 8 | let margin = hash.margin ? box(hash.margin) : { top: 0, right: 0, bottom: 0, left: 0 }; 9 | 10 | return { 11 | top: margin.top, 12 | left: margin.left, 13 | bottom: height - margin.top, 14 | right: width - margin.right, 15 | height: height - margin.top - margin.bottom, 16 | width: width - margin.left - margin.right, 17 | outerWidth: width, 18 | outerHeight: height, 19 | margin: { 20 | top: margin.top, 21 | left: margin.left, 22 | bottom: margin.bottom, 23 | right: margin.right 24 | } 25 | }; 26 | } 27 | 28 | export default Helper.helper(area); 29 | -------------------------------------------------------------------------------- /addon/helpers/curve.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { Helper, isPresent, assert, String: { camelize } } = Ember; 3 | 4 | import { 5 | curveBasisClosed, 6 | curveBasisOpen, 7 | curveBasis, 8 | curveBundle, 9 | curveCardinalClosed, 10 | curveCardinalOpen, 11 | curveCardinal, 12 | curveCatmullRomClosed, 13 | curveCatmullRomOpen, 14 | curveCatmullRom, 15 | curveLinearClosed, 16 | curveLinear, 17 | curveNatural, 18 | curveStep, 19 | curveMonotoneX, 20 | curveMonotoneY 21 | } from 'd3-shape'; 22 | 23 | export function curve([curveName], hash) { 24 | if (!hash) { 25 | hash = {}; 26 | } 27 | 28 | let curves = { 29 | basisClosed: curveBasisClosed, 30 | basisOpen: curveBasisOpen, 31 | basis: curveBasis, 32 | bundle: curveBundle, 33 | cardinalClosed: curveCardinalClosed, 34 | cardinalOpen: curveCardinalOpen, 35 | cardinal: curveCardinal, 36 | catmullRomClosed: curveCatmullRomClosed, 37 | catmullRomOpen: curveCatmullRomOpen, 38 | catmullRom: curveCatmullRom, 39 | linearClosed: curveLinearClosed, 40 | linear: curveLinear, 41 | natural: curveNatural, 42 | step: curveStep, 43 | monotone: curveMonotoneX, 44 | monotoneX: curveMonotoneX, 45 | monotoneY: curveMonotoneY 46 | }; 47 | 48 | let curveFn = curves[camelize(curveName)]; 49 | 50 | assert(`No curve with name ${curveName} is available`, isPresent(curveFn)); 51 | 52 | Object.keys(hash).forEach((key) => { 53 | if (typeof curveFn[key] === 'function') { 54 | curveFn[key](hash[key]); 55 | } 56 | }); 57 | 58 | return curveFn; 59 | } 60 | 61 | export default Helper.helper(curve); 62 | -------------------------------------------------------------------------------- /addon/helpers/extent.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { extent as arrayExtent, max } from 'd3-array'; 3 | const { Helper, get, isPresent } = Ember; 4 | 5 | export function extent([array, accessor], options) { 6 | 7 | if (isPresent(accessor)) { 8 | array = array.map((d) => get(d, accessor)); 9 | } 10 | 11 | if (options && options.toZero === true) { 12 | return [0, max(array)]; 13 | } else { 14 | return arrayExtent(array); 15 | } 16 | } 17 | 18 | export default Helper.helper(extent); 19 | -------------------------------------------------------------------------------- /addon/helpers/format-fn.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { format } from './format'; 3 | 4 | const { Helper } = Ember; 5 | 6 | export function formatFn(params, hash) { 7 | return function formatFnHelper(value) { 8 | hash = Object.assign({}, hash); 9 | hash.format = params[0]; 10 | return format([value], hash); 11 | }; 12 | } 13 | 14 | export default Helper.helper(formatFn); 15 | -------------------------------------------------------------------------------- /addon/helpers/format.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { format as d3Format } from 'd3-format'; 3 | const { Helper } = Ember; 4 | 5 | export function format([value], hash) { 6 | hash = Object.assign({}, hash); 7 | 8 | let result; 9 | if (!hash.format) { 10 | hash.format = ','; 11 | } 12 | 13 | if (value < 1 && hash.ignoreSmallValues) { 14 | result = '< 1'; 15 | } else { 16 | result = d3Format(hash.format)(value); 17 | } 18 | 19 | if (hash.suffix) { 20 | result = `${result} ${hash.suffix}`; 21 | } 22 | 23 | if (hash.prefix) { 24 | result = `${hash.prefix}${result}`; 25 | } 26 | 27 | return result; 28 | } 29 | 30 | export default Helper.helper(format); 31 | -------------------------------------------------------------------------------- /addon/helpers/linear-scale.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { scaleLinear } from 'd3-scale'; 3 | const { Helper } = Ember; 4 | 5 | export function linearScale(params, hash = {}) { 6 | params = params.slice(); 7 | let [domain, range] = params; 8 | hash = Object.assign({}, hash); 9 | 10 | let scale = scaleLinear().domain(domain); 11 | if (hash && hash.round) { 12 | scale.rangeRound(range); 13 | } else { 14 | scale.range(range); 15 | } 16 | 17 | return scale; 18 | } 19 | 20 | export default Helper.helper(linearScale); 21 | -------------------------------------------------------------------------------- /addon/helpers/pair-by.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { Helper, isArray, assert } = Ember; 4 | 5 | /** 6 | * @pairBy(params); 7 | * 8 | * Creates a pair of values from an array of objects. 9 | * 10 | * Usage: 11 | * 12 | * (pair-by "timestamp" "value" someArrayOfObjects) 13 | * // => [[1450345920000, 1885], [1450345920000, 1885], ...] 14 | * 15 | * @param {Array[...keys, data]} params 16 | * @public 17 | * @return {Array[Array[2]]} 18 | */ 19 | export function pairBy(args = []) { 20 | let params = args.slice(); 21 | assert('pair-by requires at least 2 arguments: key, data', params.length >= 2); 22 | let data = params.pop(); 23 | assert('last argument must be an array of objects', isArray(data)); 24 | 25 | let [...keys] = params; 26 | 27 | return data.map((d) => { 28 | return keys.reduce((acc, k, index) => { 29 | acc[index] = d[k]; 30 | return acc; 31 | }, []); 32 | }); 33 | } 34 | 35 | export default Helper.helper(pairBy); 36 | -------------------------------------------------------------------------------- /addon/mixins/coordinates.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Dimensions from './dimensions'; 3 | import PlotArea from './plot-area'; 4 | 5 | const { Mixin } = Ember; 6 | 7 | export default Mixin.create(Dimensions, PlotArea, { 8 | }); 9 | -------------------------------------------------------------------------------- /addon/mixins/dimensions.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import GlobalResize from 'maximum-plaid/mixins/global-resize'; 3 | 4 | const { 5 | Mixin, 6 | K, 7 | run: { 8 | throttle, 9 | next 10 | }, 11 | on 12 | } = Ember; 13 | 14 | export default Mixin.create(GlobalResize, { 15 | 16 | width: 1, 17 | height: 1, 18 | 19 | prepare: on('didInsertElement', function() { 20 | next(this, this.measureDimensions); 21 | }), 22 | 23 | didMeasureDimensions: K, 24 | 25 | didResize() { 26 | // window.requestAnimationFrame(this.measureDimensions.bind(this)); 27 | throttle(this, this.measureDimensions, 100); 28 | }, 29 | 30 | measureDimensions() { 31 | if (!this.element) { 32 | return; 33 | } 34 | 35 | let rect = this.element.getBoundingClientRect(); 36 | next(this, function() { 37 | this.setProperties({ 38 | width: rect.width, 39 | height: rect.height 40 | }); 41 | 42 | this.trigger('didMeasureDimensions'); 43 | }); 44 | } 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /addon/mixins/global-resize.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { Mixin, on, $, K, run } = Ember; 4 | 5 | export default Mixin.create({ 6 | _setupResizeListener: on('didInsertElement', function() { 7 | $(window).on(`resize.${this.elementId}`, (event) => { 8 | run.next(this, this.didResize, event); 9 | }); 10 | }), 11 | 12 | _teardownResizeListener: on('willDestroyElement', function() { 13 | $(window).off(`resize.${this.elementId}`); 14 | }), 15 | 16 | didResize: K 17 | }); 18 | -------------------------------------------------------------------------------- /addon/mixins/group-element.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { select } from 'd3-selection'; 3 | 4 | const { computed, Mixin } = Ember; 5 | 6 | /** 7 | * @public 8 | * GroupElement mixin sets the component's tag to ``, and assigns an instance 9 | * variable of `this.selection` to a D3 selection of itself. 10 | */ 11 | 12 | export default Mixin.create({ 13 | tagName: 'g', 14 | 15 | x: 0, 16 | 17 | y: 0, 18 | 19 | attributeBindings: ['transform'], 20 | 21 | didInsertElement() { 22 | this.selection = select(this.element); 23 | }, 24 | 25 | transform: computed('x', 'y', { 26 | get() { 27 | let { x, y } = this.getProperties('x', 'y'); 28 | return `translate(${x},${y})`; 29 | } 30 | }) 31 | }); 32 | -------------------------------------------------------------------------------- /addon/mixins/plot-area.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import box from '../utils/box-expression'; 3 | 4 | const { computed, Mixin } = Ember; 5 | 6 | export default Mixin.create({ 7 | 8 | /** 9 | * Specifies them margin for the main graph, such that you can position 10 | * guides (axis) outside of it. 11 | * 12 | * Accepts either an object specifying {top, right, bottom, left} in units, 13 | * or a CSS equivalent margin string without units. 14 | * 15 | * E.g. "24 10" == {top: 24, right: 10, bottom: 24, left: 10} 16 | * 17 | * @public 18 | * @type {String || Object} 19 | */ 20 | margin: '0', 21 | 22 | /** 23 | * Computed dimensions for the actual plot. 24 | * 25 | * @public 26 | * @return {Object} 27 | */ 28 | plotArea: computed('width', 'height', 'margin.[]', { 29 | get() { 30 | let height = this.getWithDefault('height', 0); 31 | let width = this.getWithDefault('width', 0); 32 | let margin = box(this.get('margin')); 33 | 34 | return { 35 | top: margin.top, 36 | left: margin.left, 37 | bottom: height - margin.top, 38 | right: width - margin.right, 39 | height: height - margin.top - margin.bottom, 40 | width: width - margin.left - margin.right, 41 | outerWidth: width, 42 | outerHeight: height, 43 | margin: { 44 | top: margin.top, 45 | left: margin.left, 46 | bottom: margin.bottom, 47 | right: margin.right 48 | } 49 | }; 50 | } 51 | }) 52 | }); 53 | -------------------------------------------------------------------------------- /addon/templates/components/plaid-plot.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash 2 | line=(component "plaid-line" xScale=xScale yScale=yScale x=plotArea.left y=plotArea.top) 3 | area=(component "plaid-area" xScale=xScale yScale=yScale x=plotArea.left y=plotArea.top) 4 | scatter=(component "plaid-scatter" xScale=xScale yScale=yScale x=plotArea.left y=plotArea.top) 5 | symbol=(component "plaid-symbol" x=plotArea.left y=plotArea.top) 6 | bar=(component "plaid-bar" xScale=xScale yScale=yScale x=plotArea.left y=plotArea.top) 7 | donut=(component "plaid-donut" x=plotArea.left y=plotArea.top width=plotArea.width height=plotArea.height) 8 | text=(component "plaid-text") 9 | donut=(component "plaid-donut" width=plotArea.width height=plotArea.height) 10 | 11 | top-axis=(component "plaid-axis" orientation="top" scale=xScale y=plotArea.top x=plotArea.left) 12 | right-axis=(component "plaid-axis" orientation="right" scale=yScale y=plotArea.top x=plotArea.right) 13 | bottom-axis=(component "plaid-axis" orientation="bottom" scale=xScale y=(add plotArea.height plotArea.top) x=plotArea.left) 14 | left-axis=(component "plaid-axis" orientation="left" scale=yScale y=plotArea.top x=plotArea.left) 15 | )}} 16 | -------------------------------------------------------------------------------- /addon/templates/components/plaid-symbol.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/addon/templates/components/plaid-symbol.hbs -------------------------------------------------------------------------------- /addon/utils/box-expression.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { assert } = Ember; 3 | 4 | export default function box(expr) { 5 | if (typeof expr !== 'object') { 6 | expr = String(expr).split(/\s+/).map(Number); 7 | } else { 8 | return ['left', 'right', 'top', 'bottom'].reduce((accum, dir) => { 9 | accum[dir] = Number(expr[dir]) || 0; 10 | 11 | return accum; 12 | }, {}); 13 | } 14 | 15 | assert('Box expr must be have 1-4 numbers', !expr.filter(isNaN).length); 16 | 17 | switch (expr.length) { 18 | // 1 value = all four sides 19 | case 1: return { left: expr[0], right: expr[0], top: expr[0], bottom: expr[0] }; 20 | // 2 values = top/bottom, right/left 21 | case 2: return { left: expr[1], right: expr[1], top: expr[0], bottom: expr[0] }; 22 | // 3 values = top, both sides, bottom 23 | case 3: return { left: expr[1], right: expr[1], top: expr[0], bottom: expr[2] }; 24 | // 4 values = top, right, bottom, left 25 | case 4: return { left: expr[3], right: expr[1], top: expr[0], bottom: expr[2] }; 26 | } 27 | 28 | return { 29 | left: 0, 30 | top: 0, 31 | bottom: 0, 32 | right: 0 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /addon/utils/computed-extent.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { computed, expandProperties } = Ember; 3 | import { extent } from 'd3-array'; 4 | 5 | function expandPropertyList(propertyList) { 6 | return propertyList.reduce((newPropertyList, property) => { 7 | let atEachIndex = property.indexOf('.@each'); 8 | if (atEachIndex !== -1) { 9 | return newPropertyList.concat(property.slice(0, atEachIndex)); 10 | } else if (property.slice(-2) === '[]') { 11 | return newPropertyList.concat(property.slice(0, -3)); 12 | } 13 | 14 | expandProperties(property, (expandedProperties) => { 15 | newPropertyList = newPropertyList.concat(expandedProperties); 16 | }); 17 | 18 | return newPropertyList; 19 | }, []); 20 | } 21 | 22 | function extractConstantValues(dependencies) { 23 | return dependencies.filter((property) => { 24 | return Ember.typeOf(property) !== 'string'; 25 | }); 26 | } 27 | 28 | function rejectConstantValues(dependencies) { 29 | return dependencies.filter((property) => { 30 | return Ember.typeOf(property) === 'string'; 31 | }); 32 | } 33 | 34 | export default function computedExtent(...dependencies) { 35 | let userSuppliedConstants = extractConstantValues(dependencies) || []; 36 | let expandedParams = expandPropertyList(rejectConstantValues(dependencies)); 37 | 38 | let computedFn = computed.apply(this, [...rejectConstantValues(dependencies), { 39 | get() { 40 | let values = []; 41 | values = values.concat(userSuppliedConstants); 42 | 43 | let paramValues = expandedParams.map((p) => this.get(p)); 44 | paramValues.forEach((expandedProperty) => { 45 | if (Ember.typeOf(expandedProperty) === 'array') { 46 | values = values.concat(expandedProperty); 47 | } else { 48 | values.push(expandedProperty); 49 | } 50 | }); 51 | 52 | return extent(values); 53 | } 54 | }]); 55 | 56 | return computedFn; 57 | } 58 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/app/.gitkeep -------------------------------------------------------------------------------- /app/components/plaid-area/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-area/component'; -------------------------------------------------------------------------------- /app/components/plaid-axis/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-axis/component'; -------------------------------------------------------------------------------- /app/components/plaid-bar/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-bar/component'; 2 | -------------------------------------------------------------------------------- /app/components/plaid-donut/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-donut/component'; -------------------------------------------------------------------------------- /app/components/plaid-line/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-line/component'; -------------------------------------------------------------------------------- /app/components/plaid-plot.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-plot'; -------------------------------------------------------------------------------- /app/components/plaid-scatter/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-scatter/component'; -------------------------------------------------------------------------------- /app/components/plaid-symbol.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-symbol'; -------------------------------------------------------------------------------- /app/components/plaid-text/component.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/components/plaid-text/component'; -------------------------------------------------------------------------------- /app/helpers/area.js: -------------------------------------------------------------------------------- 1 | export { default, area } from 'maximum-plaid/helpers/area'; 2 | -------------------------------------------------------------------------------- /app/helpers/curve.js: -------------------------------------------------------------------------------- 1 | export { default, curve } from 'maximum-plaid/helpers/curve'; 2 | -------------------------------------------------------------------------------- /app/helpers/extent.js: -------------------------------------------------------------------------------- 1 | export { default, extent } from 'maximum-plaid/helpers/extent'; 2 | -------------------------------------------------------------------------------- /app/helpers/format-fn.js: -------------------------------------------------------------------------------- 1 | export { default, formatFn } from 'maximum-plaid/helpers/format-fn'; 2 | -------------------------------------------------------------------------------- /app/helpers/format.js: -------------------------------------------------------------------------------- 1 | export { default, format } from 'maximum-plaid/helpers/format'; 2 | -------------------------------------------------------------------------------- /app/helpers/linear-scale.js: -------------------------------------------------------------------------------- 1 | export { default, linearScale } from 'maximum-plaid/helpers/linear-scale'; 2 | -------------------------------------------------------------------------------- /app/helpers/pair-by.js: -------------------------------------------------------------------------------- 1 | export { default, pairBy } from 'maximum-plaid/helpers/pair-by'; 2 | -------------------------------------------------------------------------------- /app/utils/box-expression.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/utils/box-expression'; 2 | -------------------------------------------------------------------------------- /app/utils/computed-extent.js: -------------------------------------------------------------------------------- 1 | export { default } from 'maximum-plaid/utils/computed-extent'; 2 | -------------------------------------------------------------------------------- /blueprints/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "console" 4 | ], 5 | "strict": false 6 | } 7 | -------------------------------------------------------------------------------- /blueprints/maximum-plaid/index.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | description: 'Installation blueprint for maximum plaid', 4 | normalizeEntityName: function () {}, 5 | beforeInstall: function() { 6 | return this.addAddonsToProject({ 7 | packages: [ 8 | 'ember-cli-d3-shape@^0.8.5', 9 | 'ember-math-helpers@^1.0.0' 10 | ] 11 | }); 12 | } 13 | 14 | // locals: function(options) { 15 | // // Return custom template variables here. 16 | // return { 17 | // foo: options.entity.options.foo 18 | // }; 19 | // } 20 | 21 | // afterInstall: function(options) { 22 | // // Perform extra work here. 23 | // } 24 | }; 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maximum-plaid", 3 | "dependencies": { 4 | "ember": "~2.10.0", 5 | "ember-cli-shims": "0.1.3", 6 | "bourbon": "4.2.6", 7 | "firebase": "^2.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | scenarios: [ 4 | { 5 | name: 'ember-lts-2.4', 6 | bower: { 7 | dependencies: { 8 | 'ember': 'components/ember#lts-2-4' 9 | }, 10 | resolutions: { 11 | 'ember': 'lts-2-4' 12 | } 13 | } 14 | }, 15 | { 16 | name: 'ember-lts-2.8', 17 | bower: { 18 | dependencies: { 19 | 'ember': 'components/ember#lts-2-8' 20 | }, 21 | resolutions: { 22 | 'ember': 'lts-2-8' 23 | } 24 | } 25 | }, 26 | { 27 | name: 'ember-release', 28 | bower: { 29 | dependencies: { 30 | 'ember': 'components/ember#release' 31 | }, 32 | resolutions: { 33 | 'ember': 'release' 34 | } 35 | } 36 | }, 37 | { 38 | name: 'ember-beta', 39 | bower: { 40 | dependencies: { 41 | 'ember': 'components/ember#beta' 42 | }, 43 | resolutions: { 44 | 'ember': 'beta' 45 | } 46 | } 47 | }, 48 | { 49 | name: 'ember-canary', 50 | bower: { 51 | dependencies: { 52 | 'ember': 'components/ember#canary' 53 | }, 54 | resolutions: { 55 | 'ember': 'canary' 56 | } 57 | } 58 | } 59 | ] 60 | }; 61 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 'use strict'; 3 | 4 | module.exports = function(/* environment, appConfig */) { 5 | return { }; 6 | }; 7 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'maximum-plaid' 6 | }; 7 | -------------------------------------------------------------------------------- /logo/line-chart-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/logo/line-chart-screenshot.png -------------------------------------------------------------------------------- /logo/maximum-plaid-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/logo/maximum-plaid-logo.png -------------------------------------------------------------------------------- /maximum-plaid.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "name": "MaximumPlaid", 7 | "folder_exclude_patterns": ["node_modules", "bower_components", "tmp"] 8 | } 9 | ], 10 | "settings": { 11 | "tab_size": 2, 12 | "translate_tabs_to_spaces": true 13 | }, 14 | "build_systems": [ 15 | { 16 | "name": "Ember-CLI Server", 17 | "shell_cmd": "ember s" 18 | }, 19 | { 20 | "name": "Ember-CLI Tests", 21 | "shell_cmd": "ember test ci" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maximum-plaid", 3 | "version": "0.1.3", 4 | "description": "Template driven data visualisation for ambitious applications", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "license": "MIT", 10 | "author": "Ivan Vanderbyl", 11 | "repository": "ivanvanderbyl/plaid", 12 | "scripts": { 13 | "build": "ember build", 14 | "start": "ember server", 15 | "test": "ember try:each", 16 | "deploy": "ember github-pages:commit --message \"Deploy gh-pages from commit $(git rev-parse HEAD)\"; git push; git checkout -" 17 | }, 18 | "dependencies": { 19 | "ember-cli-babel": "^5.1.7", 20 | "ember-math-helpers": "1.0.0", 21 | "ember-d3-helpers": ">=0.5.2", 22 | "ember-cli-htmlbars": "^1.0.10", 23 | "ember-composable-helpers": "^1.1.2", 24 | "ember-d3": "ivanvanderbyl/ember-d3", 25 | "d3": "^4.4.0" 26 | }, 27 | "devDependencies": { 28 | "broccoli-asset-rev": "^2.4.5", 29 | "ember-ajax": "^2.4.1", 30 | "ember-cli": "2.10.0", 31 | "ember-cli-app-version": "^2.0.0", 32 | "ember-cli-bourbon": "1.2.2", 33 | "ember-cli-dependency-checker": "^1.3.0", 34 | "ember-cli-eslint": "3.0.0", 35 | "ember-cli-htmlbars-inline-precompile": "^0.3.3", 36 | "ember-cli-inject-live-reload": "^1.4.1", 37 | "ember-cli-jshint": "^2.0.1", 38 | "ember-cli-qunit": "^3.0.1", 39 | "ember-cli-release": "^0.2.9", 40 | "ember-cli-sass": "5.6.0", 41 | "ember-cli-sri": "^2.1.0", 42 | "ember-cli-test-loader": "^1.1.0", 43 | "ember-cli-uglify": "^1.2.0", 44 | "ember-data": "^2.10.0", 45 | "ember-disable-prototype-extensions": "^1.1.0", 46 | "ember-export-application-global": "^1.0.5", 47 | "ember-load-initializers": "^0.5.1", 48 | "ember-resolver": "^2.0.3", 49 | "emberfire": "1.6.6", 50 | "eslint-plugin-ember-suave": "^1.0.0", 51 | "loader.js": "^4.0.10" 52 | }, 53 | "keywords": [ 54 | "ember-addon", 55 | "d3", 56 | "maximum-plaid", 57 | "data-visualization", 58 | "visualisation", 59 | "charts", 60 | "charting-package" 61 | ], 62 | "engines": { 63 | "node": ">= 4.6" 64 | }, 65 | "ember-addon": { 66 | "configPath": "tests/dummy/config" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "PhantomJS" 8 | ], 9 | "launch_in_dev": [ 10 | "PhantomJS", 11 | "Chrome" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | 'embertest': true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "-Promise", 9 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName" 25 | ], 26 | "node": false, 27 | "browser": false, 28 | "boss": true, 29 | "curly": true, 30 | "debug": false, 31 | "devel": false, 32 | "eqeqeq": true, 33 | "evil": true, 34 | "forin": false, 35 | "immed": false, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": false, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": false, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": false, 49 | "eqnull": true, 50 | "esversion": 6, 51 | "unused": true 52 | } 53 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Controller from 'ember-controller'; 2 | 3 | export default Controller.extend({ 4 | 5 | responseTimeMean: [], 6 | 7 | donutData: [ 8 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 9 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 10 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 11 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 12 | { timestamp: 1450345960000, value: 5, projectId: 200 } 13 | ] 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Route from 'ember-route'; 3 | 4 | const { 5 | inject, 6 | RSVP: { Promise }, 7 | String: { camelize } 8 | } = Ember; 9 | 10 | export default Route.extend({ 11 | firebase: inject.service(), 12 | 13 | model() { 14 | let ref = this.get('firebase'); 15 | 16 | return new Promise((resolve, reject) => { 17 | ref.once('value', function(snapshot) { 18 | let value = snapshot.val(); 19 | let data = {}; 20 | Object.keys(value).forEach((key) => { 21 | data[camelize(key)] = value[key]; 22 | }); 23 | 24 | resolve(data); 25 | }, reject); 26 | }); 27 | 28 | }, 29 | 30 | setupController(controller, { responseTimeMean }) { 31 | controller.setProperties({ responseTimeMean }); 32 | } 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import "bourbon"; 2 | 3 | html, body { 4 | font-family: system, sans-serif; 5 | font-size: 16px; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | .Plaid-axis { 11 | path.domain, .tick line { 12 | stroke-width: 1; 13 | stroke: black; 14 | stroke-opacity: 0.54; 15 | fill: none; 16 | } 17 | 18 | .tick text { 19 | font-size: 12px; 20 | } 21 | 22 | .axis-title { 23 | font-size: 20px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/charts/-line-chart.hbs: -------------------------------------------------------------------------------- 1 | {{#with (area 664 220 margin="0 75 72 16") as |plotArea|}} 2 | {{#plaid-plot 3 | (time-scale (extent (map-by "timestamp" responseTimeMean)) (array 0 plotArea.width)) 4 | (linear-scale (extent (map-by "value" responseTimeMean) toZero=true) (array plotArea.height 0)) 5 | plotArea as |plot|}} 6 | 7 | {{#with (pair-by "timestamp" "value" responseTimeMean) as |values|}} 8 | {{plot.line values stroke="#673AB7" strokeWidth="2" curve=(curve "basis")}} 9 | {{plot.area values fill="#D1C4E9" curve=(curve "basis")}} 10 | {{#plot.bottom-axis values ticks=10}} 11 | {{plaid-text 'Timestamps' class='axis-title' x=(div plotArea.width 2) y=40}} 12 | {{/plot.bottom-axis}} 13 | {{#plot.right-axis values tickFormat=(format-fn "0.1s" suffix="ms") ticks=2}} 14 | {{plaid-text 'Values' class='axis-title' x=50 y=(div plotArea.height 2) textRotate=90}} 15 | {{/plot.right-axis}} 16 | 17 | {{!-- {{#plot.scatter values as |x y|}} 18 | 19 | 20 | {{/plot.scatter}} --}} 21 | {{/with}} 22 | {{/plaid-plot}} 23 | {{/with}} 24 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |

Maximum Plaid

2 | 3 |

Line

4 | 5 | {{partial "charts/line-chart"}} 6 | 7 |

Donut

8 | {{#with (area 664 220) as |plotArea|}} 9 | {{#plaid-plot plotArea=plotArea as |plot|}} 10 | 11 | {{plot.donut (pair-by "timestamp" "value" donutData)}} 12 | 13 | {{/plaid-plot}} 14 | {{/with}} -------------------------------------------------------------------------------- /tests/dummy/app/templates/loading.hbs: -------------------------------------------------------------------------------- 1 |

2 | Loading... 3 |

-------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | } 14 | }, 15 | 16 | APP: { 17 | // Here you can pass flags/options to your application instance 18 | // when it is created 19 | }, 20 | 21 | firebase: 'https://maximum-plaid.firebaseio.com/', 22 | }; 23 | 24 | if (environment === 'development') { 25 | // ENV.APP.LOG_RESOLVER = true; 26 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 27 | // ENV.APP.LOG_TRANSITIONS = true; 28 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 29 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 30 | } 31 | 32 | if (environment === 'test') { 33 | // Testem prefers this... 34 | ENV.rootURL = '/'; 35 | ENV.locationType = 'none'; 36 | 37 | // keep test console output quieter 38 | ENV.APP.LOG_ACTIVE_GENERATION = false; 39 | ENV.APP.LOG_VIEW_LOOKUPS = false; 40 | 41 | ENV.APP.rootElement = '#ember-testing'; 42 | } 43 | 44 | if (environment === 'production') { 45 | ENV.locationType = 'hash'; 46 | ENV.rootURL = '/maximum-plaid/'; 47 | 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | let application; 7 | 8 | // use defaults, but you can override 9 | let attributes = Ember.assign({}, config.APP, attrs); 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/components/plaid-area/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('plaid-area', 'Integration | Component | plaid area', { 5 | integration: true 6 | }); 7 | 8 | test('it a an area chart', function(assert) { 9 | 10 | this.set('timeSeriesData', [ 11 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 12 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 13 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 14 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 15 | { timestamp: 1450345960000, value: 5, projectId: 200 } 16 | ]); 17 | 18 | this.render(hbs` 19 | {{#with (area 664 220 margin="0 50 72 16") as |plotArea|}} 20 | {{#plaid-plot 21 | (time-scale (extent (map-by "timestamp" timeSeriesData)) (array 0 plotArea.width)) 22 | (linear-scale (extent (map-by "value" timeSeriesData) toZero=true) (array plotArea.height 0)) 23 | plotArea as |plot|}} 24 | 25 | {{plot.area (pair-by "timestamp" "value" timeSeriesData) fill="#D1C4E9"}} 26 | 27 | {{/plaid-plot}} 28 | {{/with}} 29 | `); 30 | 31 | assert.equal(this.$('path.area').attr('d'), 'M0,118.4L149.5,88.8L299,59.2L448.5,29.599999999999994L598,0L598,148L448.5,148L299,148L149.5,148L0,148Z'); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/integration/components/plaid-axis/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('plaid-axis', 'Integration | Component | plaid axis', { 5 | integration: true 6 | }); 7 | 8 | test('it renders an axis', function(assert) { 9 | this.set('timeSeriesData', [ 10 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 11 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 12 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 13 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 14 | { timestamp: 1450345960000, value: 5, projectId: 200 } 15 | ]); 16 | 17 | this.render(hbs` 18 | {{#with (area 664 220 margin="0 50 72 16") as |plotArea|}} 19 | {{#plaid-plot 20 | (time-scale (extent (map-by "timestamp" timeSeriesData)) (array 0 plotArea.width)) 21 | (linear-scale (extent (map-by "value" timeSeriesData) toZero=true) (array plotArea.height 0)) 22 | plotArea as |plot|}} 23 | 24 | {{#with (pair-by "timestamp" "value" timeSeriesData) as |values|}} 25 | {{plot.left-axis values ticks=values.length}} 26 | {{/with}} 27 | {{/plaid-plot}} 28 | {{/with}} 29 | `); 30 | 31 | assert.equal(this.$('.axis').length, 1, 'number of axes'); 32 | 33 | let ticks = this.$('.tick'); 34 | assert.equal(ticks.length, this.get('timeSeriesData.length') + 1, 'number of ticks'); 35 | 36 | assert.equal(this.$(ticks[0]).find('text').text(), 0, 'tick 0 text'); 37 | 38 | for (let i = 1; i < ticks.length; ++i) { 39 | assert.equal(this.$(ticks[i]).find('text').text(), this.get(`timeSeriesData.${i - 1}.value`), `tick ${i} text`); 40 | } 41 | }); 42 | 43 | test('it should bind the orientation as a class', function(assert) { 44 | this.set('timeSeriesData', [ 45 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 46 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 47 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 48 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 49 | { timestamp: 1450345960000, value: 5, projectId: 200 } 50 | ]); 51 | 52 | this.render(hbs` 53 | {{#with (area 664 220 margin="0 50 72 16") as |plotArea|}} 54 | {{#plaid-plot 55 | (time-scale (extent (map-by "timestamp" timeSeriesData)) (array 0 plotArea.width)) 56 | (linear-scale (extent (map-by "value" timeSeriesData) toZero=true) (array plotArea.height 0)) 57 | plotArea as |plot|}} 58 | 59 | {{#with (pair-by "timestamp" "value" timeSeriesData) as |values|}} 60 | {{plot.left-axis values ticks=values.length}} 61 | {{/with}} 62 | {{/plaid-plot}} 63 | {{/with}} 64 | `); 65 | 66 | assert.ok(this.$('.axis.left').length, 'axis is there'); 67 | }); 68 | 69 | test('it should allow tick size to be customized', function(assert) { 70 | this.set('timeSeriesData', [ 71 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 72 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 73 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 74 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 75 | { timestamp: 1450345960000, value: 5, projectId: 200 } 76 | ]); 77 | 78 | this.render(hbs` 79 | {{#with (area 664 220 margin="0 50 72 16") as |plotArea|}} 80 | {{#plaid-plot 81 | (time-scale (extent (map-by "timestamp" timeSeriesData)) (array 0 plotArea.width)) 82 | (linear-scale (extent (map-by "value" timeSeriesData) toZero=true) (array plotArea.height 0)) 83 | plotArea as |plot|}} 84 | 85 | {{#with (pair-by "timestamp" "value" timeSeriesData) as |values|}} 86 | {{plot.left-axis values ticks=values.length tickSizeInner=(mult -1 plotArea.width)}} 87 | {{/with}} 88 | {{/plaid-plot}} 89 | {{/with}} 90 | `); 91 | 92 | assert.equal(this.$('.axis').length, 1, 'number of axes'); 93 | 94 | let ticks = this.$('.tick'); 95 | assert.equal(ticks.length, this.get('timeSeriesData.length') + 1, 'number of ticks'); 96 | 97 | for (let i = 0; i < ticks.length; ++i) { 98 | // TODO: would be nice to not magic number the width of 598 99 | assert.equal(this.$(ticks[i]).find('line').attr('x2'), 598, `tick ${i} width`); 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /tests/integration/components/plaid-bar/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import wait from 'ember-test-helpers/wait'; 4 | 5 | moduleForComponent('plaid-bar', 'Integration | Component | plaid bar', { 6 | integration: true 7 | }); 8 | 9 | test('it renders a vertical bar chart', function(assert) { 10 | this.set('fuelEconomy', [ 11 | { mpg: 12, vehicles: 580 }, 12 | { mpg: 15, vehicles: 420 }, 13 | { mpg: 18, vehicles: 1000 }, 14 | { mpg: 21, vehicles: 805 }, 15 | { mpg: 24, vehicles: 640 }, 16 | { mpg: 27, vehicles: 400 }, 17 | { mpg: 30, vehicles: 380 }, 18 | { mpg: 33, vehicles: 240 }, 19 | { mpg: 36, vehicles: 210 }, 20 | { mpg: 39, vehicles: 180 }, 21 | { mpg: 42, vehicles: 205 } 22 | ]); 23 | 24 | this.render(hbs` 25 | {{#with (area 110 1000) as |plotArea|}} 26 | {{#plaid-plot 27 | xScale=(band-scale (map-by 'mpg' fuelEconomy) (array 0 plotArea.width)) 28 | yScale=(linear-scale (extent (map-by 'vehicles' fuelEconomy) toZero=true) (array plotArea.height 0)) 29 | plotArea=plotArea as |plot|}} 30 | 31 | {{#with (pair-by 'mpg' 'vehicles' fuelEconomy) as |values|}} 32 | 33 | {{plot.bar values orientation='vertical'}} 34 | 35 | {{/with}} 36 | {{/plaid-plot}} 37 | {{/with}}`); 38 | 39 | return wait() 40 | .then(() => { 41 | let bars = this.$('.bar'); 42 | 43 | assert.ok(bars, 'bars'); 44 | assert.equal(bars.length, this.get('fuelEconomy.length'), 'number of bars'); 45 | 46 | for (let i = 0; i < bars.length; ++i) { 47 | let bar = bars[i]; 48 | let barVehicles = this.get(`fuelEconomy.${i}.vehicles`); 49 | 50 | assert.equal(bar.getAttribute('y'), 1000 - barVehicles, `bar ${i} y`); 51 | assert.equal(bar.getAttribute('height'), barVehicles, `bar ${i} height`); 52 | 53 | assert.equal(bar.getAttribute('x'), i * 10, `bar ${i} x`); 54 | assert.equal(bar.getAttribute('width'), 10, `bar ${i} width`); 55 | } 56 | }); 57 | }); 58 | 59 | test('it renders correctly on resize', function(assert) { 60 | this.set('fuelEconomy', [ 61 | { mpg: 12, vehicles: 580 }, 62 | { mpg: 15, vehicles: 420 }, 63 | { mpg: 18, vehicles: 1000 }, 64 | { mpg: 21, vehicles: 805 }, 65 | { mpg: 24, vehicles: 640 }, 66 | { mpg: 27, vehicles: 400 }, 67 | { mpg: 30, vehicles: 380 }, 68 | { mpg: 33, vehicles: 240 }, 69 | { mpg: 36, vehicles: 210 }, 70 | { mpg: 39, vehicles: 180 }, 71 | { mpg: 42, vehicles: 205 } 72 | ]); 73 | 74 | this.setProperties({ 75 | width: 55, 76 | height: 500 77 | }); 78 | 79 | this.render(hbs` 80 | {{#with (area width height) as |plotArea|}} 81 | {{#plaid-plot 82 | xScale=(band-scale (map-by 'mpg' fuelEconomy) (array 0 plotArea.width)) 83 | yScale=(linear-scale (extent (map-by 'vehicles' fuelEconomy) toZero=true) (array plotArea.height 0)) 84 | plotArea=plotArea as |plot|}} 85 | 86 | {{#with (pair-by 'mpg' 'vehicles' fuelEconomy) as |values|}} 87 | 88 | {{plot.bar values orientation='vertical'}} 89 | 90 | {{/with}} 91 | {{/plaid-plot}} 92 | {{/with}}`); 93 | 94 | return wait() 95 | .then(() => { 96 | let bars = this.$('.bar'); 97 | 98 | assert.ok(bars, 'bars'); 99 | assert.equal(bars.length, this.get('fuelEconomy.length'), 'number of bars'); 100 | 101 | this.setProperties({ 102 | width: 110, 103 | height: 1000 104 | }); 105 | 106 | return wait(); 107 | }) 108 | .then(() => { 109 | let bars = this.$('.bar'); 110 | 111 | assert.ok(bars, 'bars'); 112 | assert.equal(bars.length, this.get('fuelEconomy.length'), 'number of bars'); 113 | 114 | for (let i = 0; i < bars.length; ++i) { 115 | let bar = bars[i]; 116 | let barVehicles = this.get(`fuelEconomy.${i}.vehicles`); 117 | 118 | assert.equal(bar.getAttribute('y'), 1000 - barVehicles, `bar ${i} y`); 119 | assert.equal(bar.getAttribute('height'), barVehicles, `bar ${i} height`); 120 | 121 | assert.equal(bar.getAttribute('x'), i * 10, `bar ${i} x`); 122 | assert.equal(bar.getAttribute('width'), 10, `bar ${i} width`); 123 | } 124 | }); 125 | }); 126 | 127 | test('it renders a horizontal bar chart', function(assert) { 128 | this.set('fuelEconomy', [ 129 | { mpg: 12, vehicles: 580 }, 130 | { mpg: 15, vehicles: 420 }, 131 | { mpg: 18, vehicles: 1000 }, 132 | { mpg: 21, vehicles: 805 }, 133 | { mpg: 24, vehicles: 640 }, 134 | { mpg: 27, vehicles: 400 }, 135 | { mpg: 30, vehicles: 380 }, 136 | { mpg: 33, vehicles: 240 }, 137 | { mpg: 36, vehicles: 210 }, 138 | { mpg: 39, vehicles: 180 }, 139 | { mpg: 42, vehicles: 205 } 140 | ]); 141 | 142 | this.render(hbs` 143 | {{#with (area 1000 110) as |plotArea|}} 144 | {{#plaid-plot 145 | xScale=(linear-scale (extent (map-by 'vehicles' fuelEconomy) toZero=true) (array 0 plotArea.width)) 146 | yScale=(band-scale (map-by 'mpg' fuelEconomy) (array 0 plotArea.height)) 147 | plotArea=plotArea as |plot|}} 148 | 149 | {{#with (pair-by 'vehicles' 'mpg' fuelEconomy) as |values|}} 150 | 151 | {{plot.bar values orientation='horizontal'}} 152 | 153 | {{/with}} 154 | {{/plaid-plot}} 155 | {{/with}}`); 156 | 157 | return wait() 158 | .then(() => { 159 | let bars = this.$('.bar'); 160 | 161 | assert.ok(bars, 'bars'); 162 | assert.equal(bars.length, this.get('fuelEconomy.length'), 'number of bars'); 163 | 164 | for (let i = 0; i < bars.length; ++i) { 165 | let bar = bars[i]; 166 | let barVehicles = this.get(`fuelEconomy.${i}.vehicles`); 167 | 168 | assert.equal(bar.getAttribute('y'), i * 10, `bar ${i} y`); 169 | assert.equal(bar.getAttribute('height'), 10, `bar ${i} height`); 170 | 171 | assert.equal(bar.getAttribute('x'), 0, `bar ${i} x`); 172 | assert.equal(bar.getAttribute('width'), barVehicles, `bar ${i} width`); 173 | } 174 | }); 175 | }); 176 | 177 | test('it defaults to vertical orientation', function(assert) { 178 | this.set('fuelEconomy', [ 179 | { mpg: 12, vehicles: 580 }, 180 | { mpg: 15, vehicles: 420 }, 181 | { mpg: 18, vehicles: 1000 }, 182 | { mpg: 21, vehicles: 805 }, 183 | { mpg: 24, vehicles: 640 }, 184 | { mpg: 27, vehicles: 400 }, 185 | { mpg: 30, vehicles: 380 }, 186 | { mpg: 33, vehicles: 240 }, 187 | { mpg: 36, vehicles: 210 }, 188 | { mpg: 39, vehicles: 180 }, 189 | { mpg: 42, vehicles: 205 } 190 | ]); 191 | 192 | this.render(hbs` 193 | {{#with (area 110 1000) as |plotArea|}} 194 | {{#plaid-plot 195 | xScale=(band-scale (map-by 'mpg' fuelEconomy) (array 0 plotArea.width)) 196 | yScale=(linear-scale (extent (map-by 'vehicles' fuelEconomy) toZero=true) (array plotArea.height 0)) 197 | plotArea=plotArea as |plot|}} 198 | 199 | {{#with (pair-by 'mpg' 'vehicles' fuelEconomy) as |values|}} 200 | 201 | {{plot.bar values}} 202 | 203 | {{/with}} 204 | {{/plaid-plot}} 205 | {{/with}}`); 206 | 207 | return wait() 208 | .then(() => { 209 | let bars = this.$('.bar'); 210 | 211 | assert.ok(bars, 'bars'); 212 | assert.equal(bars.length, this.get('fuelEconomy.length'), 'number of bars'); 213 | 214 | for (let i = 0; i < bars.length; ++i) { 215 | let bar = bars[i]; 216 | let barVehicles = this.get(`fuelEconomy.${i}.vehicles`); 217 | 218 | assert.equal(bar.getAttribute('y'), 1000 - barVehicles, `bar ${i} y`); 219 | assert.equal(bar.getAttribute('height'), barVehicles, `bar ${i} height`); 220 | 221 | assert.equal(bar.getAttribute('x'), i * 10, `bar ${i} x`); 222 | assert.equal(bar.getAttribute('width'), 10, `bar ${i} width`); 223 | } 224 | }); 225 | }); 226 | 227 | test('it errors if orientation is not "vertical" or "horizontal"', function(assert) { 228 | this.set('fuelEconomy', [ 229 | { mpg: 12, vehicles: 580 }, 230 | { mpg: 15, vehicles: 420 }, 231 | { mpg: 18, vehicles: 1000 }, 232 | { mpg: 21, vehicles: 805 }, 233 | { mpg: 24, vehicles: 640 }, 234 | { mpg: 27, vehicles: 400 }, 235 | { mpg: 30, vehicles: 380 }, 236 | { mpg: 33, vehicles: 240 }, 237 | { mpg: 36, vehicles: 210 }, 238 | { mpg: 39, vehicles: 180 }, 239 | { mpg: 42, vehicles: 205 } 240 | ]); 241 | 242 | assert.throws(() => { 243 | this.render(hbs` 244 | {{#with (area 1000 110) as |plotArea|}} 245 | {{#plaid-plot 246 | xScale=(linear-scale (extent (map-by 'vehicles' fuelEconomy) toZero=true) (array 0 plotArea.width)) 247 | yScale=(band-scale (map-by 'mpg' fuelEconomy) (array 0 plotArea.height)) 248 | plotArea=plotArea as |plot|}} 249 | 250 | {{#with (pair-by 'vehicles' 'mpg' fuelEconomy) as |values|}} 251 | 252 | {{plot.bar values orientation='bad-orientation'}} 253 | 254 | {{/with}} 255 | {{/plaid-plot}} 256 | {{/with}}`); 257 | }, 'bar chart orientation must be in {vertical,horizontal}, was "bad-orientation"'); 258 | }); 259 | 260 | test('it errors if orientation is "vertical" and the xScale is not a band-scale', function(assert) { 261 | this.set('fuelEconomy', [ 262 | { mpg: 12, vehicles: 580 }, 263 | { mpg: 15, vehicles: 420 }, 264 | { mpg: 18, vehicles: 1000 }, 265 | { mpg: 21, vehicles: 805 }, 266 | { mpg: 24, vehicles: 640 }, 267 | { mpg: 27, vehicles: 400 }, 268 | { mpg: 30, vehicles: 380 }, 269 | { mpg: 33, vehicles: 240 }, 270 | { mpg: 36, vehicles: 210 }, 271 | { mpg: 39, vehicles: 180 }, 272 | { mpg: 42, vehicles: 205 } 273 | ]); 274 | 275 | assert.throws(() => { 276 | this.render(hbs` 277 | {{#with (area 1000 110) as |plotArea|}} 278 | {{#plaid-plot 279 | xScale=(linear-scale (extent (map-by 'vehicles' fuelEconomy) toZero=true) (array 0 plotArea.width)) 280 | yScale=(band-scale (map-by 'mpg' fuelEconomy) (array 0 plotArea.height)) 281 | plotArea=plotArea as |plot|}} 282 | 283 | {{#with (pair-by 'vehicles' 'mpg' fuelEconomy) as |values|}} 284 | 285 | {{plot.bar values orientation='vertical'}} 286 | 287 | {{/with}} 288 | {{/plaid-plot}} 289 | {{/with}}`); 290 | }, 'xScale must be a band-scale for vertical bar charts'); 291 | }); 292 | 293 | test('it errors if orientation is "horizontal" and the yScale is not a band-scale', function(assert) { 294 | this.set('fuelEconomy', [ 295 | { mpg: 12, vehicles: 580 }, 296 | { mpg: 15, vehicles: 420 }, 297 | { mpg: 18, vehicles: 1000 }, 298 | { mpg: 21, vehicles: 805 }, 299 | { mpg: 24, vehicles: 640 }, 300 | { mpg: 27, vehicles: 400 }, 301 | { mpg: 30, vehicles: 380 }, 302 | { mpg: 33, vehicles: 240 }, 303 | { mpg: 36, vehicles: 210 }, 304 | { mpg: 39, vehicles: 180 }, 305 | { mpg: 42, vehicles: 205 } 306 | ]); 307 | 308 | assert.throws(() => { 309 | this.render(hbs` 310 | {{#with (area 110 1000) as |plotArea|}} 311 | {{#plaid-plot 312 | xScale=(band-scale (map-by 'mpg' fuelEconomy) (array 0 plotArea.width)) 313 | yScale=(linear-scale (extent (map-by 'vehicles' fuelEconomy) toZero=true) (array plotArea.height 0)) 314 | plotArea=plotArea as |plot|}} 315 | 316 | {{#with (pair-by 'mpg' 'vehicles' fuelEconomy) as |values|}} 317 | 318 | {{plot.bar values orientation='horizontal'}} 319 | 320 | {{/with}} 321 | {{/plaid-plot}} 322 | {{/with}}`); 323 | }, 'yScale must be a band-scale for horizontal bar charts'); 324 | }); 325 | -------------------------------------------------------------------------------- /tests/integration/components/plaid-donut/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('plaid-donut', 'Integration | Component | plaid donut', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.set('timeSeriesData', [ 13 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 14 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 15 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 16 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 17 | { timestamp: 1450345960000, value: 5, projectId: 200 } 18 | ]); 19 | 20 | this.render(hbs` 21 | {{#with (area 664 220) as |plotArea|}} 22 | {{#plaid-plot plotArea=plotArea as |plot|}} 23 | 24 | {{plot.donut (pair-by "timestamp" "value" timeSeriesData)}} 25 | 26 | {{/plaid-plot}} 27 | {{/with}} 28 | `); 29 | 30 | assert.equal(this.$('.arc').length, this.get('timeSeriesData.length'), 'number of arcs'); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/integration/components/plaid-plot-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('plaid-plot', 'Integration | Component | plaid plot', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{plaid-plot}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#plaid-plot}} 19 | template block text 20 | {{/plaid-plot}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/plaid-symbol-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import Ember from 'ember'; 4 | import {symbolCircle } from 'd3-shape'; 5 | 6 | moduleForComponent('plaid-symbol', 'Integration | Component | plaid symbol', { 7 | integration: true 8 | }); 9 | 10 | test('it renders a diamond', function(assert) { 11 | this.render(hbs`{{plaid-symbol type="diamond" size="48"}}`); 12 | assert.equal(this.$('path').attr('d'), 'M0,-6.4474195909412515L3.7224194364083982,0L0,6.4474195909412515L-3.7224194364083982,0Z'); 13 | assert.ok(Ember.A(this.$('path').attr('class').split(' ')).contains('diamond'), 'has diamond class'); 14 | assert.ok(Ember.A(this.$('path').attr('class').split(' ')).contains('symbol'), 'has symbol class'); 15 | }); 16 | 17 | test('positioning', function(assert) { 18 | this.render(hbs`{{plaid-symbol type="diamond" size="48" x="24" y="100"}}`); 19 | assert.equal(this.$('path').attr('transform'), 'translate(24,100)'); 20 | }); 21 | 22 | test('fill, stroke, and stroke-width', function(assert) { 23 | this.render(hbs`{{plaid-symbol fill="red" stroke="black" strokeWidth="2"}}`); 24 | assert.equal(this.$('path.symbol').attr('fill'), 'red'); 25 | assert.equal(this.$('path.symbol').attr('stroke'), 'black'); 26 | assert.equal(this.$('path.symbol').attr('stroke-width'), '2'); 27 | }); 28 | 29 | test('type as function', function(assert) { 30 | this.set('symbol', symbolCircle); 31 | 32 | this.render(hbs`{{plaid-symbol type=symbol size="48"}}`); 33 | assert.equal(this.$('path').attr('d'), 'M3.9088200952233594,0A3.9088200952233594,3.9088200952233594,0,1,1,-3.9088200952233594,0A3.9088200952233594,3.9088200952233594,0,1,1,3.9088200952233594,0'); 34 | }); 35 | 36 | test('shorthand', function(assert) { 37 | this.render(hbs`{{plaid-symbol "diamond" 24 100}}`); 38 | assert.equal(this.$('path').attr('transform'), 'translate(24,100)'); 39 | assert.equal(this.$('path').attr('d'), 'M0,-3.7224194364083987L2.149139863647084,0L0,3.7224194364083987L-2.149139863647084,0Z'); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/integration/components/plaid-text/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('plaid-text', 'Integration | Component | plaid text', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | this.render(hbs`{{plaid-text "Label Value"}}`); 10 | 11 | assert.equal(this.$().text().trim(), 'Label Value'); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/helpers/area-test.js: -------------------------------------------------------------------------------- 1 | import { area } from 'dummy/helpers/area'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | area'); 5 | 6 | test('it returns an area hash', function(assert) { 7 | let result = area([640, 320], { margin: '24 16' }); 8 | assert.deepEqual(result, { 9 | 'bottom': 296, 10 | 'height': 272, 11 | 'left': 16, 12 | 'outerHeight': 320, 13 | 'outerWidth': 640, 14 | 'right': 624, 15 | 'top': 24, 16 | 'width': 608, 17 | 'margin': { 'top': 24, 'left': 16, 'bottom': 24, 'right': 16 } 18 | }, 'contains correct area attributes'); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/unit/helpers/curve-test.js: -------------------------------------------------------------------------------- 1 | import { curve } from 'dummy/helpers/curve'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | curve'); 5 | 6 | function isFunction(thing) { 7 | return typeof thing === 'function'; 8 | } 9 | 10 | test('it returns a curve function by name', function(assert) { 11 | let fns = [ 12 | 'basisClosed', 13 | 'basisOpen', 14 | 'basis', 15 | 'bundle', 16 | 'cardinalClosed', 17 | 'cardinalOpen', 18 | 'cardinal', 19 | 'catmullRomClosed', 20 | 'catmullRomOpen', 21 | 'catmullRom', 22 | 'linearClosed', 23 | 'linear', 24 | 'natural', 25 | 'step', 26 | 'monotone', 27 | 'monotoneX', 28 | 'monotoneY' 29 | ]; 30 | 31 | assert.expect(fns.length); 32 | 33 | fns.forEach((name) => { 34 | assert.ok(isFunction(curve([name])), `${name} is available`); 35 | }); 36 | }); 37 | 38 | test('it accepts dasherized named', function(assert) { 39 | assert.ok(isFunction(curve(['catmull-rom-closed'])), `catmull-rom-closed is available`); 40 | }); 41 | 42 | test('it tests is curve is present', function(assert) { 43 | assert.throws(function() { 44 | curve(['not-a-curve']); 45 | }, 'curve is not available'); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/unit/helpers/extent-test.js: -------------------------------------------------------------------------------- 1 | import { extent } from 'dummy/helpers/extent'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | extent'); 5 | 6 | test('extent of flat array', function(assert) { 7 | let result = extent([[1,2,3,4]]); 8 | assert.deepEqual(result, [1,4]); 9 | }); 10 | 11 | test('extent of array of objects', function(assert) { 12 | let result = extent([[ 13 | { value: 1 }, 14 | { value: 2 }, 15 | { value: 3 }, 16 | { value: 4 }, 17 | { value: 5 } 18 | ], 'value']); 19 | assert.deepEqual(result, [1,5]); 20 | }); 21 | 22 | test('toZero option ensures min is zero', function(assert) { 23 | let result = extent([[ 24 | { value: 2 }, 25 | { value: 3 }, 26 | { value: 4 }, 27 | { value: 5 } 28 | ], 'value'], { toZero: true }); 29 | assert.deepEqual(result, [0,5]); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/unit/helpers/format-fn-test.js: -------------------------------------------------------------------------------- 1 | import { formatFn } from 'dummy/helpers/format-fn'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | format fn'); 5 | 6 | test('it returns a function representing a format', function(assert) { 7 | let result = formatFn(['$0.3s'])(12345); 8 | 9 | assert.deepEqual(result, '$12.3k', 'calling format returns a formatted value'); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/helpers/format-test.js: -------------------------------------------------------------------------------- 1 | import { format } from 'dummy/helpers/format'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | format'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = format([42]); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/helpers/linear-scale-test.js: -------------------------------------------------------------------------------- 1 | import { linearScale } from 'dummy/helpers/linear-scale'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | linear scale'); 5 | 6 | test('creates a scale helper', function(assert) { 7 | let domain = [0,100]; 8 | let range = [100, 1000]; 9 | 10 | let result = linearScale([domain, range], {})(50); 11 | assert.equal(result, 550, 'computed correct linear scale value'); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/helpers/pair-by-test.js: -------------------------------------------------------------------------------- 1 | import { pairBy } from 'dummy/helpers/pair-by'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | pair by'); 5 | 6 | test('it returns an array with values for each attribute', function(assert) { 7 | let data = [ 8 | { timestamp: 1450345920000, value: 1, projectId: 200 }, 9 | { timestamp: 1450345930000, value: 2, projectId: 200 }, 10 | { timestamp: 1450345940000, value: 3, projectId: 200 }, 11 | { timestamp: 1450345950000, value: 4, projectId: 200 }, 12 | { timestamp: 1450345960000, value: 5, projectId: 200 } 13 | ]; 14 | 15 | let result = pairBy(['timestamp', 'value', data]); 16 | assert.deepEqual(result, [ 17 | [1450345920000, 1], 18 | [1450345930000, 2], 19 | [1450345940000, 3], 20 | [1450345950000, 4], 21 | [1450345960000, 5] 22 | ]); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/unit/mixins/coordinates-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import CoordinatesMixin from 'maximum-plaid/mixins/coordinates'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | coordinates'); 6 | 7 | // Replace this with your real tests. 8 | test('it works', function(assert) { 9 | let CoordinatesObject = Ember.Object.extend(CoordinatesMixin); 10 | let subject = CoordinatesObject.create(); 11 | assert.ok(subject); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/mixins/dimensions-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import DimensionsMixin from 'maximum-plaid/mixins/dimensions'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | dimensions'); 6 | 7 | // Replace this with your real tests. 8 | test('it works', function(assert) { 9 | let DimensionsObject = Ember.Object.extend(DimensionsMixin); 10 | let subject = DimensionsObject.create(); 11 | assert.ok(subject); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/mixins/global-resize-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import GlobalResizeMixin from 'maximum-plaid/mixins/global-resize'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | global resize'); 6 | 7 | // Replace this with your real tests. 8 | test('it works', function(assert) { 9 | let GlobalResizeObject = Ember.Object.extend(GlobalResizeMixin); 10 | let subject = GlobalResizeObject.create(); 11 | assert.ok(subject); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/mixins/group-element-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import GroupElementMixin from 'maximum-plaid/mixins/group-element'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | group element'); 6 | 7 | // Replace this with your real tests. 8 | test('it works', function(assert) { 9 | let GroupElementObject = Ember.Object.extend(GroupElementMixin); 10 | let subject = GroupElementObject.create(); 11 | assert.ok(subject); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/mixins/plot-area-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import PlotAreaMixin from 'maximum-plaid/mixins/plot-area'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | plot area'); 6 | 7 | // Replace this with your real tests. 8 | test('returns computed area for plot', function(assert) { 9 | let PlotAreaObject = Ember.Object.extend(PlotAreaMixin, { 10 | margin: '10 20' 11 | }); 12 | 13 | let subject = PlotAreaObject.create(); 14 | assert.equal(subject.get('plotArea.top'), 10, 'plotArea.top'); 15 | assert.equal(subject.get('plotArea.left'), 20, 'plotArea.left'); 16 | }); 17 | 18 | test('accepts object with margin', function(assert) { 19 | let PlotAreaObject = Ember.Object.extend(PlotAreaMixin, { 20 | margin: { top: 10, left: 20 }, 21 | height: 200, 22 | width: 200 23 | }); 24 | 25 | let subject = PlotAreaObject.create(); 26 | assert.equal(subject.get('plotArea.top'), 10, 'plotArea.top'); 27 | assert.equal(subject.get('plotArea.left'), 20, 'plotArea.left'); 28 | assert.equal(subject.get('plotArea.right'), 200, 'plotArea.right'); 29 | assert.equal(subject.get('plotArea.bottom'), 190, 'plotArea.bottom'); 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /tests/unit/utils/box-expression-test.js: -------------------------------------------------------------------------------- 1 | import box from 'dummy/utils/box-expression'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Utility | box expression'); 5 | 6 | test('parsing string to object', function(assert) { 7 | let result = box('10 20'); 8 | assert.deepEqual(result, { 9 | 'bottom': 10, 10 | 'left': 20, 11 | 'right': 20, 12 | 'top': 10 13 | }, 'Accepts top and left margin convention'); 14 | }); 15 | 16 | test('parsing object to object', function(assert) { 17 | let result = box({ 18 | 'bottom': 10, 19 | 'left': 20, 20 | 'right': 20, 21 | 'top': 10 22 | }); 23 | 24 | assert.deepEqual(result, { 25 | 'bottom': 10, 26 | 'left': 20, 27 | 'right': 20, 28 | 'top': 10 29 | }, 'Accepts top and left margin convention'); 30 | }); 31 | 32 | test('parsing object with missing sides', function(assert) { 33 | let result = box({ 34 | 'bottom': 10, 35 | 'left': 20 36 | }); 37 | 38 | assert.deepEqual(result, { 39 | 'bottom': 10, 40 | 'left': 20, 41 | 'right': 0, 42 | 'top': 0 43 | }, 'Accepts top and left margin convention'); 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /tests/unit/utils/computed-extent-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import computedExtent from 'dummy/utils/computed-extent'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Utility | computed extent'); 6 | 7 | // Replace this with your real tests. 8 | test('it computes the extent of an array of numbers', function(assert) { 9 | let TestSubject = Ember.Object.extend({ 10 | values: [1,2,3,4,5,6,7,8,9,10], 11 | 12 | extentOfNumbers: computedExtent('values.[]') 13 | }); 14 | 15 | let subject = TestSubject.create(); 16 | assert.deepEqual(subject.get('extentOfNumbers'), [1,10], 'includes start and end'); 17 | }); 18 | 19 | test('it can accept a primitive', function(assert) { 20 | let TestSubject = Ember.Object.extend({ 21 | width: 510, 22 | 23 | xRange: computedExtent(0, 'width') 24 | }); 25 | 26 | let subject = TestSubject.create(); 27 | assert.deepEqual(subject.get('xRange'), [0, 510], 'includes 0 to width'); 28 | }); 29 | 30 | test('it can accept multiple dependent keys', function(assert) { 31 | let TestSubject = Ember.Object.extend({ 32 | x1: 100, 33 | x2: 150, 34 | 35 | xRange: computedExtent(0, 'x1', 'x2') 36 | }); 37 | 38 | let subject = TestSubject.create(); 39 | assert.deepEqual(subject.get('xRange'), [0, 150], 'includes 0 to x2'); 40 | }); 41 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvanderbyl/maximum-plaid/1ee6508917384f3083ec85170ffc5b1c707e0182/vendor/.gitkeep --------------------------------------------------------------------------------