├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bower.json
├── composer.json
├── dist
├── css
│ ├── epoch.css
│ └── epoch.min.css
└── js
│ ├── epoch.js
│ └── epoch.min.js
├── gulpfile.js
├── package.json
├── sass
├── _core.scss
├── epoch.scss
└── themes
│ ├── _dark.scss
│ └── _default.scss
├── src
├── adapters.coffee
├── adapters
│ ├── MooTools.coffee
│ ├── jQuery.coffee
│ └── zepto.coffee
├── basic.coffee
├── basic
│ ├── area.coffee
│ ├── bar.coffee
│ ├── histogram.coffee
│ ├── line.coffee
│ ├── pie.coffee
│ └── scatter.coffee
├── core
│ ├── chart.coffee
│ ├── context.coffee
│ ├── css.coffee
│ ├── d3.coffee
│ ├── format.coffee
│ └── util.coffee
├── data.coffee
├── epoch.coffee
├── model.coffee
├── time.coffee
└── time
│ ├── area.coffee
│ ├── bar.coffee
│ ├── gauge.coffee
│ ├── heatmap.coffee
│ └── line.coffee
└── tests
├── render
├── basic
│ ├── area.html
│ ├── bar.html
│ ├── histogram.html
│ ├── line.html
│ ├── model.html
│ ├── options.html
│ ├── pie.html
│ └── scatter.html
├── css
│ └── tests.css
├── index.html
├── js
│ └── data.js
├── real-time
│ ├── area.html
│ ├── bar.html
│ ├── gauge.html
│ ├── heatmap.html
│ ├── line.html
│ ├── model.html
│ └── options.html
└── themes
│ ├── dark.html
│ └── default.html
└── unit
├── core
├── charts.coffee
├── copy.coffee
├── css.coffee
├── d3.coffee
├── events.coffee
├── format.coffee
├── is.coffee
└── util.coffee
├── data
├── array_format.coffee
├── chart.coffee
├── keyvalue_format.coffee
└── tuple_format.coffee
├── init.coffee
├── time.coffee
└── time
└── line.coffee
/.gitignore:
--------------------------------------------------------------------------------
1 | .stamp*
2 | /codo-doc/
3 | /doc/
4 | /css/
5 | /js/
6 | /build/
7 | npm-debug.log
8 | node_modules/*
9 | .DS_Store
10 | sass/.sass-cache
11 | test.html
12 | /_site/
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4.1.1"
4 | sudo: false
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Epoch Changelog
2 |
3 | ## 0.8.4 - October 30th, 2015
4 | ### Bug Fixes
5 | * Fixed bower css path (@ftaiolivista)
6 |
7 | ## 0.8.3 - October 17th, 2015
8 | ### Enhancements / Features
9 | * Added `redraw` method for clearing styles on canvas based charts (#196, @woozyking)
10 |
11 | ## 0.8.2 - October 13th, 2015
12 | ### Enhancements / Features
13 | * Charts now auto draw on construction (#195)
14 |
15 | ## 0.8.1 - October 13th, 2015
16 | ### Enhancements / Features
17 | * Added packagist/composer package manager support (#202)
18 |
19 | ### Bug Fixes
20 | * Real-time charts no-longer error when pushing first data point after initialized
21 | with empty data layers. (#203)
22 |
23 | ## 0.8.0 - October 10th, 2015
24 | ### Enhancements / Features
25 | * Multi-axis support for basic and real-time line plots
26 | * Added new gulp build-system (for development)
27 |
28 | ## 0.7.1 - October 4th, 2015
29 | * Moved minified source to `dist/js` and `dist/css` respectively
30 | * Added non-minified source to aforementioned directories
31 |
32 | ## 0.7.0 - October 4th, 2015
33 |
34 | ### Enhancements / Features
35 | * New basic chart: Histogram
36 | * New Feature: Data formatters
37 | * Chart layers can now be hidden/shown
38 |
39 | ### Bug Fixes
40 | * Ticks now working for ordinal scaled bar charts
41 | * Fixed CSS builds by updating NPM sass-node package
42 | * Removed versions from minified release files (@RyanNielson)
43 | * Time based graphs can now have fixed ranges (@willwhitney)
44 | * NPM Package: epoch-charting (@sompylasar)
45 | * Right axes now using correct formatters (@Dav1dde)
46 | * Add 'main' attribute enabling webpack support. (@WRidder)
47 | * Fixed Bower D3 Dependencies (@loopj)
48 | * Fixed CSS errors by using `transparent` instead of `none` (@mwsmith2)
49 | * Fixed bower "version" property (@kkirsche)
50 |
51 | ## 0.6.0 - July 21st, 2014
52 |
53 | ### Enhancements / Features
54 |
55 | * Source code restructure for easier programming
56 | * Replaced Compass with node-sass
57 | * Removed put.js from the repository
58 | * Removed dependency on jQuery
59 | * Added CSS controlled themes
60 | * New "Dark" theme for dark backgrounds
61 | * Registered with bower
62 | * Added option accessor / mutator to all charts (making them adaptive)
63 | * Added bubble charts (special case of scatter plots)
64 | * Added MooTools and Zepto Adapters
65 | * Added Core Library Unit Testing
66 | * New `domain` and `range` options for basic charts
67 |
68 | ### Bug Fixes
69 |
70 | * Event `.off` method was completely busted, fixed
71 | * Swapped terminology for horizontal and vertical bar plots
72 | * Removed `isVisible` and related rendering hacks (caused all sorts of woe)
73 |
74 |
75 | ## 0.5.2 - June 24th, 2014
76 |
77 | ### Enhancements / Features
78 |
79 | * #36 - Fixed the readme to focus on development
80 | * #54 - Added vertical orientation option to the basic bar chart
81 |
82 | ## 0.5.1 - June 23rd, 2014
83 |
84 | ### Bug Fixes
85 |
86 | * #52 - Replaced instances of `$` with `jQuery` (ambiguous, otherwise)
87 |
88 | ## 0.5.0 - June 23rd, 2014
89 |
90 | ### Enhancements / Features
91 |
92 | * #32 - QueryCSS greatly enhanced - now builds a full DOM context when computing styles
93 | * #42 - Heat map now allows for painting of "zero" values via a new `paintZeroValues` option
94 | * #43 - Heat map color computation abstracted out of `_paintEntry` (makes it easier to extend)
95 |
96 | ### Bug Fixes
97 |
98 | * #22 - Fixed an issue with pie chart transitions
99 | * #30 - Layers without labels now correctly render on a various basic charts
100 | * #31 - Real-time Line Chart thickness fixed by taking pixel ratio into account
101 | * #41 - Fixed bucketing issues with the Heat Map
102 | * #46 - Removed default black stroke from the Real-Time Area chart
103 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Fastly, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Epoch
2 | By Ryan Sandor Richards
3 |
4 | [](https://travis-ci.org/epochjs/epoch)
5 | [](https://david-dm.org/epochjs/epoch)
6 | [](https://david-dm.org/epochjs/epoch#info=devDependencies)
7 |
8 | Epoch is a general purpose charting library for application developers and visualization designers. It focuses on two different aspects of visualization programming: **basic charts** for creating historical reports, and **real-time charts** for displaying frequently updating timeseries data.
9 |
10 | To get started using Epoch, please refer to the [Epoch Project Site](http://epochjs.github.io/epoch/). There you can find full documentation and guides to help you start using Epoch right away.
11 |
12 | ### Installation
13 | Epoch can be easily installed via the following package managers:
14 |
15 | * [npm](https://www.npmjs.com/package/epoch-charting)
16 | * [bower](http://bower.io/search/?q=epoch)
17 | * [packagist](https://packagist.org/packages/epochjs/epoch)
18 |
19 | If you don't see your favorite package manager in the list above feel free to
20 | [open up an issue](https://github.com/epochjs/epoch/issues/new) and let us know.
21 | Finally, you can download any release of the library from the
22 | [project releases page](https://github.com/epochjs/epoch/releases).
23 |
24 | **Important:** Epoch requires [d3](https://github.com/mbostock/d3). In order to
25 | work properly your page must load d3 before epoch.
26 |
27 | #### Public CDN URLs
28 | If you don't want to host the files yourself, you can use
29 | [jsDelivr](http://www.jsdelivr.com/) to serve the files:
30 |
31 | 1. Visit [epoch page on jsDelvr](http://www.jsdelivr.com/projects/epoch).
32 | 2. Copy the provided URL's and link to them in your project.
33 |
34 | ### Developing Epoch
35 |
36 | Developing Epoch is a reasonably straight forward process. In this section we'll
37 | cover the basic on how to develop Epoch by detailing common build task, exploring
38 | how the source is arranged, and finally show how to use rendering tests to aid
39 | development.
40 |
41 | #### Configuring Development Environment
42 |
43 | Epoch requires the following for development:
44 |
45 | 1. [Node.js](https://nodejs.org/en/) (v4.1.1+)
46 | 2. [NPM](https://www.npmjs.com/) (v2.1.0+)
47 |
48 | Once both are installed on your machine you will need to run `npm install` from
49 | the repository's root directory in order to install the npm packages required
50 | to develop epoch.
51 |
52 | Once you have installed the required npm packages you can use `gulp build` to
53 | fully rebuild the source (see more information about gulp tasks below).
54 |
55 |
56 | #### Basic Development Process
57 |
58 | The best way to start contributing to Epoch is to follow these steps:
59 |
60 | 1. Change to the source directory for the project
61 | 2. Run `gulp watch` to recompile the project after source files change
62 | 3. Make changes in a source file (either in `src/` or `sass/`)
63 | 4. In a web browser open the `test/index.html` and browse the rendering tests
64 | 5. Use the rendering tests to see if your changes had the desired result
65 | 6. Ensure unit tests with pass `npm test`
66 |
67 | #### Testing
68 |
69 | Epoch uses two types of testing to ensure that changes do not cause unintended
70 | side effects. The first, unit tests, ensure that the core functional components
71 | of the library work as expected. The second, rendering tests, allow you to
72 | ensure that charts and graphs are correctly rendered.
73 |
74 | It is important to keep both unit test and rendering tests up-to-date! When
75 | developing, use the following guidelines:
76 |
77 | * When adding new features make sure to add new tests
78 | * When changing existing functionality, ensure that the appropriate both types
79 | of tests still pass
80 | * If you want to make a new type of chart, add a whole new test suite for that
81 | chart!
82 |
83 | Keeping the tests current makes it easier for others to review your code and
84 | spot issues. Also, pull requests without appropriate testing will not be
85 | merged.
86 |
87 |
88 | #### Gulp Tasks
89 |
90 | Epoch uses [gulp](https://github.com/gulpjs/gulp) to perform various tasks. The
91 | `gulpfile.js` file defines the following tasks:
92 |
93 | * `gulp clean` - Cleans the `dist/` directory.
94 | * `gulp build` - Builds the CoffeeScript and Sass source into the `dist/`
95 | directory.
96 | * `gulp watch` - Starts a watch script to recompile CoffeeScript and Sass when
97 | any files change.
98 |
99 | #### Source Structure
100 |
101 | The directory structure for the Epoch project follows some basic guidelines, here's an overview of how it is structured:
102 |
103 | ```
104 | dist/ - Compiled JavaScript and CSS source
105 | src/ - Main source directory
106 | core/ - Core Epoch Library Files
107 | util.coffee - Library Utility Routines
108 | d3.coffee - d3 Extensions
109 | format.coffee - Data formatters
110 | chart.coffee - Base Chart Classes
111 | css.coffee - CSS Querying Engine
112 | adapters/ - 3rd Party Library Adapters (currently only jQuery)
113 | basic/ - Basic Chart Classes
114 | time/ - Real-time Chart Classes
115 | adapters.coffee - Options / Global Classes for Adapter Implementations
116 | basic.coffee - Base Classes for Basic Charts
117 | data.coffee - Data Formatting
118 | epoch.coffee - Main source file, defines name spaces, etc.
119 | model.coffee - Data Model
120 | time.coffee - Base Classes for Real-Time Charts
121 | sass/ - Scss source for the default epoch stylesheet
122 | tests/
123 | render/ - Rendering tests
124 | basic/ - Basic chart rendering tests
125 | real-time/ - Real-time rendering tests
126 | unit/ - Unit tests
127 | ```
128 |
129 | ### Release Checklist
130 |
131 | - Run `npm test` and ensure all tests pass
132 | - Run `npm version [major|minor|patch]`
133 | - Run `npm publish`
134 | - Update CHANGELOG.md with the changes since last release
135 | - Update the `gh-pages` branch's library version in `_config.yml`
136 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "epoch",
3 | "description": "A general purpose, real-time visualization library.",
4 | "main": [
5 | "dist/js/epoch.min.js",
6 | "dist/css/epoch.min.css"
7 | ],
8 | "license": "MIT",
9 | "ignore": [
10 | "**/.*",
11 | "/src",
12 | "/sass",
13 | "/tests",
14 | "/Cakefile",
15 | "/CHANGELOG.md",
16 | "/package.json"
17 | ],
18 | "authors": [
19 | "Ryan Sandor Richards Plot a random selection of points from the Beta(2, 5) distribution as a histogram. Plot a random selection of points from Beta(2, 5) and display in a horizontal histogram. Plot Beta(2, 2) and change number of buckets on the fly.
87 |
88 |
89 |
90 |
91 | |
92 |
93 | Plot beta(2, 5) and change the bucket range on the fly.
120 |
121 |
122 | |
123 |
124 | |
125 |
126 |
27 |
28 |
29 |
30 |
31 | Correctly add and remove axes when options are set.
32 |
70 | Correctly resize margins when options are set.
71 |
107 | Correctly resize margins when options are set.
108 |
179 | Correctly resize the chart when the width and height options are set.
180 |
212 |
213 |
214 |
241 |
242 |
243 |
36 | Correctly render a pie chart with three categories:
37 |
63 | Correctly render a donut chart with three categories:
64 |
91 | Correctly transition between set A:
92 |
106 |
107 |
108 |
136 | Correctly transition between set A:
137 |
150 |
151 |
152 |
179 | Override the colors as such:
180 |
212 | Correctly transition between different categorical colors sets.
213 |
216 |
217 |
218 |
219 |
220 |
252 | Correctly render a pie chart with three categories:
253 |
277 |
278 |
279 |
300 |
301 |
302 |
303 |
324 |
325 |
326 |
327 |
16 | true
if the chart has an axis with a given name, false
otherwise.
85 | # @param [String] axis Name of axis to check.
86 | hasAxis: (axis) ->
87 | @options.axes.indexOf(axis) > -1
88 |
89 | # @return [Number] Width of the visualization portion of the chart (width - margins).
90 | innerWidth: ->
91 | @width - (@margins.left + @margins.right)
92 |
93 | # @return [Number] Height of the visualization portion of the chart (height - margins).
94 | innerHeight: ->
95 | @height - (@margins.top + @margins.bottom)
96 |
97 | # @return [Function] The x scale for the visualization.
98 | x: ->
99 | domain = @options.domain ? @extent((d) -> d.x)
100 | d3.scale.linear()
101 | .domain(domain)
102 | .range([0, @innerWidth()])
103 |
104 | # @return [Function] The y scale for the visualization.
105 | y: (givenDomain) ->
106 | d3.scale.linear()
107 | .domain(@_getScaleDomain(givenDomain))
108 | .range([@innerHeight(), 0])
109 |
110 | # @return [Function] d3 axis to use for the bottom of the visualization.
111 | bottomAxis: ->
112 | d3.svg.axis().scale(@x()).orient('bottom')
113 | .ticks(@options.ticks.bottom)
114 | .tickFormat(@options.tickFormats.bottom)
115 |
116 | # @return [Function] d3 axis to use for the top of the visualization.
117 | topAxis: ->
118 | d3.svg.axis().scale(@x()).orient('top')
119 | .ticks(@options.ticks.top)
120 | .tickFormat(@options.tickFormats.top)
121 |
122 | # @return [Function] d3 axis to use on the left of the visualization.
123 | leftAxis: ->
124 | range = if @options.range then @options.range.left else null
125 | d3.svg.axis().scale(@y(range)).orient('left')
126 | .ticks(@options.ticks.left)
127 | .tickFormat(@options.tickFormats.left)
128 |
129 | # @return [Function] d3 axis to use on the right of the visualization.
130 | rightAxis: ->
131 | range = if @options.range then @options.range.right else null
132 | d3.svg.axis().scale(@y(range)).orient('right')
133 | .ticks(@options.ticks.right)
134 | .tickFormat(@options.tickFormats.right)
135 |
136 | # Renders the axes for the visualization (subclasses must implement specific
137 | # drawing routines).
138 | draw: ->
139 | if @_axesDrawn
140 | @_redrawAxes()
141 | else
142 | @_drawAxes()
143 | super()
144 |
145 | # Redraws the axes for the visualization.
146 | _redrawAxes: ->
147 | if @hasAxis('bottom')
148 | @g.selectAll('.x.axis.bottom').transition()
149 | .duration(500)
150 | .ease('linear')
151 | .call(@bottomAxis())
152 | if @hasAxis('top')
153 | @g.selectAll('.x.axis.top').transition()
154 | .duration(500)
155 | .ease('linear')
156 | .call(@topAxis())
157 | if @hasAxis('left')
158 | @g.selectAll('.y.axis.left').transition()
159 | .duration(500)
160 | .ease('linear')
161 | .call(@leftAxis())
162 | if @hasAxis('right')
163 | @g.selectAll('.y.axis.right').transition()
164 | .duration(500)
165 | .ease('linear')
166 | .call(@rightAxis())
167 |
168 | # Draws the initial axes for the visualization.
169 | _drawAxes: ->
170 | if @hasAxis('bottom')
171 | @g.append("g")
172 | .attr("class", "x axis bottom")
173 | .attr("transform", "translate(0, #{@innerHeight()})")
174 | .call(@bottomAxis())
175 | if @hasAxis('top')
176 | @g.append("g")
177 | .attr('class', 'x axis top')
178 | .call(@topAxis())
179 | if @hasAxis('left')
180 | @g.append("g")
181 | .attr("class", "y axis left")
182 | .call(@leftAxis())
183 | if @hasAxis('right')
184 | @g.append('g')
185 | .attr('class', 'y axis right')
186 | .attr('transform', "translate(#{@innerWidth()}, 0)")
187 | .call(@rightAxis())
188 | @_axesDrawn = true
189 |
190 | dimensionsChanged: ->
191 | super()
192 | @g.selectAll('.axis').remove()
193 | @_axesDrawn = false
194 | @draw()
195 |
196 | # Updates margins in response to a option:margin.*
event.
197 | marginsChanged: ->
198 | return unless @options.margins?
199 | for own pos, size of @options.margins
200 | unless size?
201 | @margins[pos] = 6
202 | else
203 | @margins[pos] = size
204 |
205 | @g.transition()
206 | .duration(750)
207 | .attr("transform", "translate(#{@margins.left}, #{@margins.top})")
208 |
209 | @draw()
210 |
211 | # Updates axes in response to a option:axes
event.
212 | axesChanged: ->
213 | # Remove default axis margins
214 | for pos in ['top', 'right', 'bottom', 'left']
215 | continue if @options.margins? and @options.margins[pos]?
216 | if @hasAxis(pos)
217 | @margins[pos] = defaultAxisMargins[pos]
218 | else
219 | @margins[pos] = 6
220 |
221 | # Update the margin offset
222 | @g.transition()
223 | .duration(750)
224 | .attr("transform", "translate(#{@margins.left}, #{@margins.top})")
225 |
226 | # Remove the axes and redraw
227 | @g.selectAll('.axis').remove()
228 | @_axesDrawn = false
229 | @draw()
230 |
231 | # Updates ticks in response to a option:ticks.*
event.
232 | ticksChanged: -> @draw()
233 |
234 | # Updates tick formats in response to a option:tickFormats.*
event.
235 | tickFormatsChanged: -> @draw()
236 |
237 | # Updates chart in response to a option:domain
event.
238 | domainChanged: -> @draw()
239 |
240 | # Updates chart in response to a option:range
event.
241 | rangeChanged: -> @draw()
242 |
243 | # "They will see us waving from such great heights, come down now..." - The Postal Service
244 |
--------------------------------------------------------------------------------
/src/basic/area.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Static stacked area chart implementation using d3.
3 | class Epoch.Chart.Area extends Epoch.Chart.Plot
4 | constructor: (@options={}) ->
5 | @options.type ?= 'area'
6 | super(@options)
7 | @draw()
8 |
9 | # Generates a scale needed to appropriately render the stacked visualization.
10 | # @return [Function] The y scale for the visualization.
11 | y: ->
12 | a = []
13 | for layer in @getVisibleLayers()
14 | for own k, v of layer.values
15 | a[k] += v.y if a[k]?
16 | a[k] = v.y unless a[k]?
17 | d3.scale.linear()
18 | .domain(@options.range ? [0, d3.max(a)])
19 | .range([@height - @margins.top - @margins.bottom, 0])
20 |
21 | # Renders the SVG elements needed to display the stacked area chart.
22 | draw: ->
23 | [x, y, layers] = [@x(), @y(), @getVisibleLayers()]
24 |
25 | @g.selectAll('.layer').remove()
26 | return if layers.length == 0
27 |
28 | area = d3.svg.area()
29 | .x((d) -> x(d.x))
30 | .y0((d) -> y(d.y0))
31 | .y1((d) -> y(d.y0 + d.y))
32 |
33 | stack = d3.layout.stack()
34 | .values((d) -> d.values)
35 |
36 | data = stack layers
37 |
38 | layer = @g.selectAll('.layer')
39 | .data(layers, (d) -> d.category)
40 |
41 | layer.select('.area')
42 | .attr('d', (d) -> area(d.values))
43 |
44 | layer.enter().append('g')
45 | .attr('class', (d) -> d.className)
46 |
47 | layer.append('path')
48 | .attr('class', 'area')
49 | .attr('d', (d) -> area(d.values))
50 |
51 | super()
52 |
--------------------------------------------------------------------------------
/src/basic/bar.coffee:
--------------------------------------------------------------------------------
1 | # Static bar chart implementation (using d3).
2 | class Epoch.Chart.Bar extends Epoch.Chart.Plot
3 | defaults =
4 | type: 'bar'
5 | style: 'grouped'
6 | orientation: 'vertical'
7 | padding:
8 | bar: 0.08
9 | group: 0.1
10 | outerPadding:
11 | bar: 0.08
12 | group: 0.1
13 |
14 | horizontal_specific =
15 | tickFormats:
16 | top: Epoch.Formats.si
17 | bottom: Epoch.Formats.si
18 | left: Epoch.Formats.regular
19 | right: Epoch.Formats.regular
20 |
21 | horizontal_defaults = Epoch.Util.defaults(horizontal_specific, defaults)
22 |
23 | optionListeners =
24 | 'option:orientation': 'orientationChanged'
25 | 'option:padding': 'paddingChanged'
26 | 'option:outerPadding': 'paddingChanged'
27 | 'option:padding:bar': 'paddingChanged'
28 | 'option:padding:group': 'paddingChanged'
29 | 'option:outerPadding:bar': 'paddingChanged'
30 | 'option:outerPadding:group': 'paddingChanged'
31 |
32 | constructor: (@options={}) ->
33 | if @_isHorizontal()
34 | @options = Epoch.Util.defaults(@options, horizontal_defaults)
35 | else
36 | @options = Epoch.Util.defaults(@options, defaults)
37 | super(@options)
38 | @onAll optionListeners
39 | @draw()
40 |
41 | # @return [Boolean] True if the chart is vertical, false otherwise
42 | _isVertical: ->
43 | @options.orientation == 'vertical'
44 |
45 | # @return [Boolean] True if the chart is horizontal, false otherwise
46 | _isHorizontal: ->
47 | @options.orientation == 'horizontal'
48 |
49 | # @return [Function] The scale used to generate the chart's x scale.
50 | x: ->
51 | if @_isVertical()
52 | d3.scale.ordinal()
53 | .domain(Epoch.Util.domain(@getVisibleLayers()))
54 | .rangeRoundBands([0, @innerWidth()], @options.padding.group, @options.outerPadding.group)
55 | else
56 | extent = @extent((d) -> d.y)
57 | extent[0] = Math.min(0, extent[0])
58 | d3.scale.linear()
59 | .domain(extent)
60 | .range([0, @width - @margins.left - @margins.right])
61 |
62 | # @return [Function] The x scale used to render the horizontal bar chart.
63 | x1: (x0) ->
64 | d3.scale.ordinal()
65 | .domain((layer.category for layer in @getVisibleLayers()))
66 | .rangeRoundBands([0, x0.rangeBand()], @options.padding.bar, @options.outerPadding.bar)
67 |
68 | # @return [Function] The y scale used to render the bar chart.
69 | y: ->
70 | if @_isVertical()
71 | extent = @extent((d) -> d.y)
72 | extent[0] = Math.min(0, extent[0])
73 | d3.scale.linear()
74 | .domain(extent)
75 | .range([@height - @margins.top - @margins.bottom, 0])
76 | else
77 | d3.scale.ordinal()
78 | .domain(Epoch.Util.domain(@getVisibleLayers()))
79 | .rangeRoundBands([0, @innerHeight()], @options.padding.group, @options.outerPadding.group)
80 |
81 | # @return [Function] The x scale used to render the vertical bar chart.
82 | y1: (y0) ->
83 | d3.scale.ordinal()
84 | .domain((layer.category for layer in @getVisibleLayers()))
85 | .rangeRoundBands([0, y0.rangeBand()], @options.padding.bar, @options.outerPadding.bar)
86 |
87 | # Remaps the bar chart data into a form that is easier to display.
88 | # @return [Array] The reorganized data.
89 | _remapData: ->
90 | map = {}
91 | for layer in @getVisibleLayers()
92 | className = 'bar ' + layer.className.replace(/\s*layer\s*/, '')
93 | for entry in layer.values
94 | map[entry.x] ?= []
95 | map[entry.x].push { label: layer.category, y: entry.y, className: className }
96 | ({group: k, values: v} for own k, v of map)
97 |
98 | # Draws the bar char.
99 | draw: ->
100 | if @_isVertical()
101 | @_drawVertical()
102 | else
103 | @_drawHorizontal()
104 | super()
105 |
106 | # Draws the bar chart with a vertical orientation
107 | _drawVertical: ->
108 | [x0, y] = [@x(), @y()]
109 | x1 = @x1(x0)
110 | height = @height - @margins.top - @margins.bottom
111 | data = @_remapData()
112 |
113 | # 1) Join
114 | layer = @g.selectAll(".layer")
115 | .data(data, (d) -> d.group)
116 |
117 | # 2) Update
118 | layer.transition().duration(750)
119 | .attr("transform", (d) -> "translate(#{x0(d.group)}, 0)")
120 |
121 | # 3) Enter / Create
122 | layer.enter().append("g")
123 | .attr('class', 'layer')
124 | .attr("transform", (d) -> "translate(#{x0(d.group)}, 0)")
125 |
126 | rects = layer.selectAll('rect')
127 | .data((group) -> group.values)
128 |
129 | rects.attr('class', (d) -> d.className)
130 |
131 | rects.transition().duration(600)
132 | .attr('x', (d) -> x1(d.label))
133 | .attr('y', (d) -> y(d.y))
134 | .attr('width', x1.rangeBand())
135 | .attr('height', (d) -> height - y(d.y))
136 |
137 | rects.enter().append('rect')
138 | .attr('class', (d) -> d.className)
139 | .attr('x', (d) -> x1(d.label))
140 | .attr('y', (d) -> y(d.y))
141 | .attr('width', x1.rangeBand())
142 | .attr('height', (d) -> height - y(d.y))
143 |
144 | rects.exit().transition()
145 | .duration(150)
146 | .style('opacity', '0')
147 | .remove()
148 |
149 | # 4) Update new and existing
150 |
151 | # 5) Exit / Remove
152 | layer.exit()
153 | .transition()
154 | .duration(750)
155 | .style('opacity', '0')
156 | .remove()
157 |
158 | # Draws the bar chart with a horizontal orientation
159 | _drawHorizontal: ->
160 | [x, y0] = [@x(), @y()]
161 | y1 = @y1(y0)
162 | width = @width - @margins.left - @margins.right
163 | data = @_remapData()
164 |
165 | # 1) Join
166 | layer = @g.selectAll(".layer")
167 | .data(data, (d) -> d.group)
168 |
169 | # 2) Update
170 | layer.transition().duration(750)
171 | .attr("transform", (d) -> "translate(0, #{y0(d.group)})")
172 |
173 | # 3) Enter / Create
174 | layer.enter().append("g")
175 | .attr('class', 'layer')
176 | .attr("transform", (d) -> "translate(0, #{y0(d.group)})")
177 |
178 | rects = layer.selectAll('rect')
179 | .data((group) -> group.values)
180 |
181 | rects.attr('class', (d) -> d.className)
182 |
183 | rects.transition().duration(600)
184 | .attr('x', (d) -> 0)
185 | .attr('y', (d) -> y1(d.label))
186 | .attr('height', y1.rangeBand())
187 | .attr('width', (d) -> x(d.y))
188 |
189 | rects.enter().append('rect')
190 | .attr('class', (d) -> d.className)
191 | .attr('x', (d) -> 0)
192 | .attr('y', (d) -> y1(d.label))
193 | .attr('height', y1.rangeBand())
194 | .attr('width', (d) -> x(d.y))
195 |
196 | rects.exit().transition()
197 | .duration(150)
198 | .style('opacity', '0')
199 | .remove()
200 |
201 | # 4) Update new and existing
202 |
203 | # 5) Exit / Remove
204 | layer.exit()
205 | .transition()
206 | .duration(750)
207 | .style('opacity', '0')
208 | .remove()
209 |
210 | # Generates specific tick marks to emulate d3's linear scale axis ticks
211 | # for ordinal scales. Note: this should only be called if the user has
212 | # defined a set number of ticks for a given axis.
213 | # @param [Number] numTicks Number of ticks to generate
214 | # @param [String] dataKey Property name of a datum to use for the tick value
215 | # @return [Array] The ticks for the given axis
216 | _getTickValues: (numTicks, dataKey='x') ->
217 | return [] unless @data[0]?
218 | total = @data[0].values.length
219 | step = Math.ceil(total / numTicks)|0
220 | tickValues = (@data[0].values[i].x for i in [0...total] by step)
221 |
222 | # @return [Function] d3 axis to use for the bottom of the visualization.
223 | bottomAxis: ->
224 | axis = d3.svg.axis().scale(@x()).orient('bottom')
225 | .ticks(@options.ticks.bottom)
226 | .tickFormat(@options.tickFormats.bottom)
227 | if @_isVertical() and @options.ticks.bottom?
228 | axis.tickValues @_getTickValues(@options.ticks.bottom)
229 | axis
230 |
231 | # @return [Function] d3 axis to use for the top of the visualization.
232 | topAxis: ->
233 | axis = d3.svg.axis().scale(@x()).orient('top')
234 | .ticks(@options.ticks.top)
235 | .tickFormat(@options.tickFormats.top)
236 | if @_isVertical() and @options.ticks.top?
237 | axis.tickValues @_getTickValues(@options.ticks.top)
238 | axis
239 |
240 | # @return [Function] d3 axis to use on the left of the visualization.
241 | leftAxis: ->
242 | axis = d3.svg.axis().scale(@y()).orient('left')
243 | .ticks(@options.ticks.left)
244 | .tickFormat(@options.tickFormats.left)
245 | if @_isHorizontal() and @options.ticks.left?
246 | axis.tickValues @_getTickValues(@options.ticks.left)
247 | axis
248 |
249 | # @return [Function] d3 axis to use on the right of the visualization.
250 | rightAxis: ->
251 | axis = d3.svg.axis().scale(@y()).orient('right')
252 | .ticks(@options.ticks.right)
253 | .tickFormat(@options.tickFormats.right)
254 | if @_isHorizontal() and @options.ticks.right?
255 | axis.tickValues @_getTickValues(@options.ticks.right)
256 | axis
257 |
258 | # Updates orientation in response option:orientation
.
259 | orientationChanged: ->
260 | top = @options.tickFormats.top
261 | bottom = @options.tickFormats.bottom
262 | left = @options.tickFormats.left
263 | right = @options.tickFormats.right
264 |
265 | @options.tickFormats.left = top
266 | @options.tickFormats.right = bottom
267 | @options.tickFormats.top = left
268 | @options.tickFormats.bottom = right
269 |
270 | @draw()
271 |
272 | # Updates padding in response to option:padding:*
and option:outerPadding:*
.
273 | paddingChanged: -> @draw()
274 |
--------------------------------------------------------------------------------
/src/basic/histogram.coffee:
--------------------------------------------------------------------------------
1 | class Epoch.Chart.Histogram extends Epoch.Chart.Bar
2 | defaults =
3 | type: 'histogram'
4 | domain: [0, 100]
5 | bucketRange: [0, 100]
6 | buckets: 10
7 | cutOutliers: false
8 |
9 | optionListeners =
10 | 'option:bucketRange': 'bucketRangeChanged'
11 | 'option:buckets': 'bucketsChanged'
12 | 'option:cutOutliers': 'cutOutliersChanged'
13 |
14 | constructor: (@options={}) ->
15 | super(@options = Epoch.Util.defaults(@options, defaults))
16 | @onAll optionListeners
17 | @draw()
18 |
19 | # Prepares data by sorting it into histogram buckets as instructed by the chart options.
20 | # @param [Array] data Data to prepare for rendering.
21 | # @return [Array] The data prepared to be displayed as a histogram.
22 | _prepareData: (data) ->
23 | bucketSize = (@options.bucketRange[1] - @options.bucketRange[0]) / @options.buckets
24 |
25 | prepared = []
26 | for layer in data
27 | buckets = (0 for i in [0...@options.buckets])
28 | for point in layer.values
29 | index = parseInt((point.x - @options.bucketRange[0]) / bucketSize)
30 |
31 | if @options.cutOutliers and ((index < 0) or (index >= @options.buckets))
32 | continue
33 | if index < 0
34 | index = 0
35 | else if index >= @options.buckets
36 | index = @options.buckets - 1
37 |
38 | buckets[index] += parseInt point.y
39 |
40 | preparedLayer = { values: (buckets.map (d, i) -> {x: parseInt(i) * bucketSize, y: d}) }
41 | for own k, v of layer
42 | preparedLayer[k] = v unless k == 'values'
43 |
44 | prepared.push preparedLayer
45 |
46 | return prepared
47 |
48 | # Called when options change, this prepares the raw data for the chart according to the new
49 | # options, sets it, and renders the chart.
50 | resetData: ->
51 | @setData @rawData
52 | @draw()
53 |
54 | # Updates the chart in response to an option:bucketRange
event.
55 | bucketRangeChanged: -> @resetData()
56 |
57 | # Updates the chart in response to an option:buckets
event.
58 | bucketsChanged: -> @resetData()
59 |
60 | # Updates the chart in response to an option:cutOutliers
event.
61 | cutOutliersChanged: -> @resetData()
62 |
--------------------------------------------------------------------------------
/src/basic/line.coffee:
--------------------------------------------------------------------------------
1 | # Static line chart implementation (using d3).
2 | class Epoch.Chart.Line extends Epoch.Chart.Plot
3 | constructor: (@options={}) ->
4 | @options.type ?= 'line'
5 | super(@options)
6 | @draw()
7 |
8 | # @return [Function] The line generator used to construct the plot.
9 | line: (layer) ->
10 | [x, y] = [@x(), @y(layer.range)]
11 | d3.svg.line()
12 | .x((d) -> x(d.x))
13 | .y((d) -> y(d.y))
14 |
15 | # Draws the line chart.
16 | draw: ->
17 | [x, y, layers] = [@x(), @y(), @getVisibleLayers()]
18 |
19 | # Zero visible layers, just drop all and get out
20 | if layers.length == 0
21 | return @g.selectAll('.layer').remove()
22 |
23 | # 1) Join
24 | layer = @g.selectAll('.layer')
25 | .data(layers, (d) -> d.category)
26 |
27 | # 2) Update (only existing)
28 | layer.select('.line').transition().duration(500)
29 | .attr('d', (l) => @line(l)(l.values))
30 |
31 | # 3) Enter (Create)
32 | layer.enter().append('g')
33 | .attr('class', (l) -> l.className)
34 | .append('path')
35 | .attr('class', 'line')
36 | .attr('d', (l) => @line(l)(l.values))
37 |
38 | # 4) Update (existing & new)
39 | # Nuuupp
40 |
41 | # 5) Exit (Remove)
42 | layer.exit().transition().duration(750)
43 | .style('opacity', '0')
44 | .remove()
45 |
46 | super()
47 |
--------------------------------------------------------------------------------
/src/basic/pie.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Static Pie Chart implementation (using d3).
3 | class Epoch.Chart.Pie extends Epoch.Chart.SVG
4 | defaults =
5 | type: 'pie'
6 | margin: 10
7 | inner: 0
8 |
9 | # Creates a new pie chart.
10 | # @param [Object] options Options for the pie chart.
11 | # @option options [Number] margin Margins to add around the pie chart (default: 10).
12 | # @option options [Number] inner The inner radius for the chart (default: 0).
13 | constructor: (@options={}) ->
14 | super(@options = Epoch.Util.defaults(@options, defaults))
15 | @pie = d3.layout.pie().sort(null)
16 | .value (d) -> d.value
17 | @arc = d3.svg.arc()
18 | .outerRadius(=> (Math.max(@width, @height) / 2) - @options.margin)
19 | .innerRadius(=> @options.inner)
20 | @g = @svg.append('g')
21 | .attr("transform", "translate(#{@width/2}, #{@height/2})")
22 | @on 'option:margin', 'marginChanged'
23 | @on 'option:inner', 'innerChanged'
24 | @draw()
25 |
26 | # Draws the pie chart
27 | draw: ->
28 | @g.selectAll('.arc').remove()
29 |
30 | arcs = @g.selectAll(".arc")
31 | .data(@pie(@getVisibleLayers()), (d) -> d.data.category)
32 |
33 | arcs.enter().append('g')
34 | .attr('class', (d) -> "arc pie " + d.data.className)
35 |
36 | arcs.select('path')
37 | .attr('d', @arc)
38 |
39 | arcs.select('text')
40 | .attr("transform", (d) => "translate(#{@arc.centroid(d)})")
41 | .text((d) -> d.data.label or d.data.category)
42 |
43 | path = arcs.append("path")
44 | .attr("d", @arc)
45 | .each((d) -> @._current = d)
46 |
47 | text = arcs.append("text")
48 | .attr("transform", (d) => "translate(#{@arc.centroid(d)})")
49 | .attr("dy", ".35em")
50 | .style("text-anchor", "middle")
51 | .text((d) -> d.data.label or d.data.category)
52 |
53 | super()
54 |
55 | # Updates margins in response to an option:margin
event.
56 | marginChanged: -> @draw()
57 |
58 | # Updates inner margin in response to an option:inner
event.
59 | innerChanged: -> @draw()
60 |
--------------------------------------------------------------------------------
/src/basic/scatter.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Static scatter plot implementation (using d3).
3 | class Epoch.Chart.Scatter extends Epoch.Chart.Plot
4 | defaults =
5 | type: 'scatter'
6 | radius: 3.5
7 | axes: ['top', 'bottom', 'left', 'right']
8 |
9 | # Creates a new scatter plot.
10 | # @param [Object] options Options for the plot.
11 | # @option options [Number] radius The default radius to use for the points in
12 | # the plot (default 3.5). This can be overrwitten by individual points.
13 | constructor: (@options={}) ->
14 | super(@options = Epoch.Util.defaults(@options, defaults))
15 | @on 'option:radius', 'radiusChanged'
16 | @draw()
17 |
18 | # Draws the scatter plot.
19 | draw: ->
20 | [x, y, layers] = [@x(), @y(), @getVisibleLayers()]
21 | radius = @options.radius
22 |
23 | if layers.length == 0
24 | return @g.selectAll('.layer').remove()
25 |
26 | layer = @g.selectAll('.layer')
27 | .data(layers, (d) -> d.category)
28 |
29 | layer.enter().append('g')
30 | .attr('class', (d) -> d.className)
31 |
32 | dots = layer.selectAll('.dot')
33 | .data((l) -> l.values)
34 |
35 | dots.transition().duration(500)
36 | .attr("r", (d) -> d.r ? radius)
37 | .attr("cx", (d) -> x(d.x))
38 | .attr("cy", (d) -> y(d.y))
39 |
40 | dots.enter().append('circle')
41 | .attr('class', 'dot')
42 | .attr("r", (d) -> d.r ? radius)
43 | .attr("cx", (d) -> x(d.x))
44 | .attr("cy", (d) -> y(d.y))
45 |
46 | dots.exit().transition()
47 | .duration(750)
48 | .style('opacity', 0)
49 | .remove()
50 |
51 | layer.exit().transition()
52 | .duration(750)
53 | .style('opacity', 0)
54 | .remove()
55 |
56 | super()
57 |
58 | # Updates radius in response to an option:radius
event.
59 | radiusChanged: -> @draw()
60 |
--------------------------------------------------------------------------------
/src/core/context.coffee:
--------------------------------------------------------------------------------
1 | # Rendering context used for unit testing.
2 | class Epoch.TestContext
3 | VOID_METHODS = [
4 | 'arc', 'arcTo', 'beginPath', 'bezierCurveTo', 'clearRect',
5 | 'clip', 'closePath', 'drawImage', 'fill', 'fillRect', 'fillText',
6 | 'moveTo', 'quadraticCurveTo', 'rect', 'restore', 'rotate', 'save',
7 | 'scale', 'scrollPathIntoView', 'setLineDash', 'setTransform',
8 | 'stroke', 'strokeRect', 'strokeText', 'transform', 'translate', 'lineTo'
9 | ]
10 |
11 | # Creates a new test rendering context.
12 | constructor: ->
13 | @_log = []
14 | @_makeFauxMethod(method) for method in VOID_METHODS
15 |
16 | # Creates a fake method with the given name that logs the method called
17 | # and arguments passed when executed.
18 | # @param name Name of the fake method to create.
19 | _makeFauxMethod: (name) ->
20 | @[name] = -> @_log.push "#{name}(#{(arg.toString() for arg in arguments).join(',')})"
21 |
22 | # Faux method that emulates the "getImageData" method
23 | getImageData: ->
24 | @_log.push "getImageData(#{(arg.toString() for arg in arguments).join(',')})"
25 | return { width: 0, height: 0, resolution: 1.0, data: [] }
26 |
--------------------------------------------------------------------------------
/src/core/css.coffee:
--------------------------------------------------------------------------------
1 | # Singelton class used to query CSS styles by way of reference elements.
2 | # This allows canvas based visualizations to use the same styles as their
3 | # SVG counterparts.
4 | class QueryCSS
5 | # Reference container id
6 | REFERENCE_CONTAINER_ID = '_canvas_css_reference'
7 |
8 | # Container Hash Attribute
9 | CONTAINER_HASH_ATTR = 'data-epoch-container-id'
10 |
11 | # Handles automatic container id generation
12 | containerCount = 0
13 | nextContainerId = -> "epoch-container-#{containerCount++}"
14 |
15 | # Expression used to derive tag name, id, and class names from
16 | # selectors given the the put method.
17 | PUT_EXPR = /^([^#. ]+)?(#[^. ]+)?(\.[^# ]+)?$/
18 |
19 | # Whether or not to log full selector lists
20 | logging = false
21 |
22 | # Converts selectors into actual dom elements (replaces put.js)
23 | # Limited the functionality to what Epoch actually needs to
24 | # operate correctly. We detect class names, ids, and element
25 | # tag names.
26 | put = (selector) ->
27 | match = selector.match(PUT_EXPR)
28 | return Epoch.error('Query CSS cannot match given selector: ' + selector) unless match?
29 | [whole, tag, id, classNames] = match
30 | tag = (tag ? 'div').toUpperCase()
31 |
32 | element = document.createElement(tag)
33 | element.id = id.substr(1) if id?
34 | if classNames?
35 | element.className = classNames.substr(1).replace(/\./g, ' ')
36 |
37 | return element
38 |
39 | # Lets the user set whether or not to log selector lists and resulting DOM trees.
40 | # Useful for debugging QueryCSS itself.
41 | @log: (b) ->
42 | logging = b
43 |
44 | # Key-Value cache for computed styles that we found using this class.
45 | @cache = {}
46 |
47 | # List of styles to pull from the full list of computed styles
48 | @styleList = ['fill', 'stroke', 'stroke-width']
49 |
50 | # The svg reference container
51 | @container = null
52 |
53 | # Purges the selector to style cache
54 | @purge: ->
55 | QueryCSS.cache = {}
56 |
57 | # Gets the reference element container.
58 | @getContainer: ->
59 | return QueryCSS.container if QueryCSS.container?
60 | container = document.createElement('DIV')
61 | container.id = REFERENCE_CONTAINER_ID
62 | document.body.appendChild(container)
63 | QueryCSS.container = d3.select(container)
64 |
65 | # @return [String] A unique identifier for the given container and selector.
66 | # @param [String] selector Selector from which to derive the styles
67 | # @param container The containing element for a chart.
68 | @hash: (selector, container) ->
69 | containerId = container.attr(CONTAINER_HASH_ATTR)
70 | unless containerId?
71 | containerId = nextContainerId()
72 | container.attr(CONTAINER_HASH_ATTR, containerId)
73 | return "#{containerId}__#{selector}"
74 |
75 | # @return The computed styles for the given selector in the given container element.
76 | # @param [String] selector Selector from which to derive the styles.
77 | # @param container HTML containing element in which to place the reference SVG.
78 | @getStyles: (selector, container) ->
79 | # 0) Check for cached styles
80 | cacheKey = QueryCSS.hash(selector, container)
81 | cache = QueryCSS.cache[cacheKey]
82 | return cache if cache?
83 |
84 | # 1) Build a full reference tree (parents, container, and selector elements)
85 | parents = []
86 | parentNode = container.node().parentNode
87 |
88 | while parentNode? and parentNode.nodeName.toLowerCase() != 'body'
89 | parents.unshift parentNode
90 | parentNode = parentNode.parentNode
91 | parents.push container.node()
92 |
93 | selectorList = []
94 | for element in parents
95 | sel = element.nodeName.toLowerCase()
96 | if element.id? and element.id.length > 0
97 | sel += '#' + element.id
98 | if element.className? and element.className.length > 0
99 | sel += '.' + Epoch.Util.trim(element.className).replace(/\s+/g, '.')
100 | selectorList.push sel
101 |
102 | selectorList.push('svg')
103 |
104 | for subSelector in Epoch.Util.trim(selector).split(/\s+/)
105 | selectorList.push(subSelector)
106 |
107 | console.log(selectorList) if logging
108 |
109 | parent = root = put(selectorList.shift())
110 | while selectorList.length
111 | el = put(selectorList.shift())
112 | parent.appendChild el
113 | parent = el
114 |
115 | console.log(root) if logging
116 |
117 | # 2) Place the reference tree and fetch styles given the selector
118 | QueryCSS.getContainer().node().appendChild(root)
119 |
120 | ref = d3.select('#' + REFERENCE_CONTAINER_ID + ' ' + selector)
121 | styles = {}
122 | for name in QueryCSS.styleList
123 | styles[name] = ref.style(name)
124 | QueryCSS.cache[cacheKey] = styles
125 |
126 | # 3) Cleanup and return the styles
127 | QueryCSS.getContainer().html('')
128 | return styles
129 |
130 |
131 | Epoch.QueryCSS = QueryCSS
--------------------------------------------------------------------------------
/src/core/d3.coffee:
--------------------------------------------------------------------------------
1 | # Gets the width of the first node, or sets the width of all the nodes
2 | # in a d3 selection.
3 | # @param value [Number, String] (optional) Width to set for all the nodes in the selection.
4 | # @return The selection if setting the width of the nodes, or the width
5 | # in pixels of the first node in the selection.
6 | d3.selection::width = (value) ->
7 | if value? and Epoch.isString(value)
8 | @style('width', value)
9 | else if value? and Epoch.isNumber(value)
10 | @style('width', "#{value}px")
11 | else
12 | +Epoch.Util.getComputedStyle(@node(), null).width.replace('px', '')
13 |
14 | # Gets the height of the first node, or sets the height of all the nodes
15 | # in a d3 selection.
16 | # @param value (optional) Height to set for all the nodes in the selection.
17 | # @return The selection if setting the height of the nodes, or the height
18 | # in pixels of the first node in the selection.
19 | d3.selection::height = (value) ->
20 | if value? and Epoch.isString(value)
21 | @style('height', value)
22 | else if value? and Epoch.isNumber(value)
23 | @style('height', "#{value}px")
24 | else
25 | +Epoch.Util.getComputedStyle(@node(), null).height.replace('px', '')
--------------------------------------------------------------------------------
/src/core/format.coffee:
--------------------------------------------------------------------------------
1 | # Tick formatter identity.
2 | Epoch.Formats.regular = (d) -> d
3 |
4 | # Tick formatter that formats the numbers using standard SI postfixes.
5 | Epoch.Formats.si = (d) -> Epoch.Util.formatSI(d)
6 |
7 | # Tick formatter for percentages.
8 | Epoch.Formats.percent = (d) -> (d*100).toFixed(1) + "%"
9 |
10 | # Tick formatter for seconds from timestamp data.
11 | Epoch.Formats.seconds = (t) -> d3Seconds(new Date(t*1000))
12 | d3Seconds = d3.time.format('%I:%M:%S %p')
13 |
14 | # Tick formatter for bytes
15 | Epoch.Formats.bytes = (d) -> Epoch.Util.formatBytes(d)
16 |
--------------------------------------------------------------------------------
/src/core/util.coffee:
--------------------------------------------------------------------------------
1 | typeFunction = (objectName) -> (v) ->
2 | Object::toString.call(v) == "[object #{objectName}]"
3 |
4 | # @return [Boolean] true
if the given value is an array, false
otherwise.
5 | # @param v Value to test.
6 | Epoch.isArray = Array.isArray ? typeFunction('Array')
7 |
8 | # @return [Boolean] true
if the given value is an object, false
otherwise.
9 | # @param v Value to test.
10 | Epoch.isObject = typeFunction('Object')
11 |
12 | # @return [Boolean] true
if the given value is a string, false
otherwise.
13 | # @param v Value to test.
14 | Epoch.isString = typeFunction('String')
15 |
16 | # @return [Boolean] true
if the given value is a function, false
otherwise.
17 | # @param v Value to test.
18 | Epoch.isFunction = typeFunction('Function')
19 |
20 | # @return [Boolean] true
if the given value is a number, false
otherwise.
21 | # @param v Value to test.
22 | Epoch.isNumber = typeFunction('Number')
23 |
24 | # Attempts to determine if a given value represents a DOM element. The result is always correct if the
25 | # browser implements DOM Level 2, but one can fool it on certain versions of IE. Adapted from:
26 | # Stack Overflow #384286.
27 | # @return [Boolean] true
if the given value is a DOM element, false
otherwise.
28 | # @param v Value to test.
29 | Epoch.isElement = (v) ->
30 | if HTMLElement?
31 | v instanceof HTMLElement
32 | else
33 | v? and Epoch.isObject(v) and v.nodeType == 1 and Epoch.isString(v.nodeName)
34 |
35 | # Determines if a given value is a non-empty array.
36 | # @param v Value to test.
37 | # @return [Boolean] true
if the given value is an array with at least one element.
38 | Epoch.isNonEmptyArray = (v) ->
39 | Epoch.isArray(v) and v.length > 0
40 |
41 | # Generates shallow copy of an object.
42 | # @return A shallow copy of the given object.
43 | # @param [Object] original Object for which to make the shallow copy.
44 | Epoch.Util.copy = (original) ->
45 | return null unless original?
46 | copy = {}
47 | copy[k] = v for own k, v of original
48 | return copy
49 |
50 | # Creates a deep copy of the given options filling in missing defaults.
51 | # @param [Object] options Options to copy.
52 | # @param [Object] defaults Default values for the options.
53 | Epoch.Util.defaults = (options, defaults) ->
54 | result = Epoch.Util.copy(options)
55 | for own k, v of defaults
56 | opt = options[k]
57 | def = defaults[k]
58 | bothAreObjects = Epoch.isObject(opt) and Epoch.isObject(def)
59 |
60 | if opt? and def?
61 | if bothAreObjects and not Epoch.isArray(opt)
62 | result[k] = Epoch.Util.defaults(opt, def)
63 | else
64 | result[k] = opt
65 | else if opt?
66 | result[k] = opt
67 | else
68 | result[k] = def
69 |
70 | return result
71 |
72 | # Formats numbers with standard postfixes (e.g. K, M, G)
73 | # @param [Number] v Value to format.
74 | # @param [Integer] fixed Number of floating point digits to fix after conversion.
75 | # @param [Boolean] fixIntegers Whether or not to add floating point digits to non-floating point results.
76 | # @example Formatting a very large number
77 | # Epoch.Util.formatSI(1120000) == "1.1 M"
78 | Epoch.Util.formatSI = (v, fixed=1, fixIntegers=false) ->
79 | if v < 1000
80 | q = v
81 | q = q.toFixed(fixed) unless (q|0) == q and !fixIntegers
82 | return q
83 |
84 | for own i, label of ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
85 | base = Math.pow(10, ((i|0)+1)*3)
86 | if v >= base and v < Math.pow(10, ((i|0)+2)*3)
87 | q = v/base
88 | q = q.toFixed(fixed) unless (q % 1) == 0 and !fixIntegers
89 | return "#{q} #{label}"
90 |
91 | # Formats large bandwidth and disk space usage numbers with byte postfixes (e.g. KB, MB, GB, etc.)
92 | # @param [Number] v Value to format.
93 | # @param [Integer] fixed Number of floating point digits to fix after conversion.
94 | # @param [Boolean] fixIntegers Whether or not to add floating point digits to non-floating point results.
95 | # @example Formatting a large number of bytes
96 | # Epoch.Util.formatBytes(5.21 * Math.pow(2, 20)) == "5.2 MB"
97 | Epoch.Util.formatBytes = (v, fixed=1, fix_integers=false) ->
98 | if v < 1024
99 | q = v
100 | q = q.toFixed(fixed) unless (q % 1) == 0 and !fix_integers
101 | return "#{q} B"
102 |
103 | for own i, label of ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
104 | base = Math.pow(1024, (i|0)+1)
105 | if v >= base and v < Math.pow(1024, (i|0)+2)
106 | q = v/base
107 | q = q.toFixed(fixed) unless (q % 1) == 0 and !fix_integers
108 | return "#{q} #{label}"
109 |
110 | # @return a "dasherized" css class names from a given string
111 | # @example Using dasherize
112 | # Epoch.Util.dasherize('My Awesome Name') == 'my-awesome-name'
113 | Epoch.Util.dasherize = (str) ->
114 | Epoch.Util.trim(str).replace("\n", '').replace(/\s+/g, '-').toLowerCase()
115 |
116 | # @return the full domain of a given variable from an array of layers
117 | # @param [Array] layers Layered plot data.
118 | # @param [String] key The key name of the value at on each entry in the layers.
119 | Epoch.Util.domain = (layers, key='x') ->
120 | set = {}
121 | domain = []
122 | for layer in layers
123 | for entry in layer.values
124 | continue if set[entry[key]]?
125 | domain.push(entry[key])
126 | set[entry[key]] = true
127 | return domain
128 |
129 | # Strips whitespace from the beginning and end of a string.
130 | # @param [String] string String to trim.
131 | # @return [String] The string without leading or trailing whitespace.
132 | # Returns null if the given parameter was not a string.
133 | Epoch.Util.trim = (string) ->
134 | return null unless Epoch.isString(string)
135 | string.replace(/^\s+/g, '').replace(/\s+$/g, '')
136 |
137 | # Returns the computed styles of an element in the document
138 | # @param [HTMLElement] Element for which to fetch the styles.
139 | # @param [String] pseudoElement Pseudo selectors on which to search for the element.
140 | # @return [Object] The styles for the given element.
141 | Epoch.Util.getComputedStyle = (element, pseudoElement) ->
142 | if Epoch.isFunction(window.getComputedStyle)
143 | window.getComputedStyle(element, pseudoElement)
144 | else if element.currentStyle?
145 | element.currentStyle
146 |
147 | # Converts a CSS color string into an RGBA string with the given opacity
148 | # @param [String] color Color string to convert into an rgba
149 | # @param [Number] opacity Opacity to use for the resulting color.
150 | # @return the resulting rgba color string.
151 | Epoch.Util.toRGBA = (color, opacity) ->
152 | if (parts = color.match /^rgba\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*[0-9\.]+\)/)
153 | [all, r, g, b] = parts
154 | result = "rgba(#{r},#{g},#{b},#{opacity})"
155 | else if (v = d3.rgb color)
156 | result = "rgba(#{v.r},#{v.g},#{v.b},#{opacity})"
157 | return result
158 |
159 | # Obtains a graphics context for the given canvas node. Nice to have
160 | # this abstracted out in case we want to support WebGL in the future.
161 | # Also allows us to setup a special context when unit testing, as
162 | # jsdom doesn't have canvas support, and node-canvas is a pain in the
163 | # butt to install properly across different platforms.
164 | Epoch.Util.getContext = (node, type='2d') ->
165 | node.getContext(type)
166 |
167 | # Basic eventing base class for all Epoch classes.
168 | class Epoch.Events
169 | constructor: ->
170 | @_events = {}
171 |
172 | # Registers a callback to a given event.
173 | # @param [String] name Name of the event.
174 | # @param [Function, String] callback Either a closure to call when the event fires
175 | # or a string that denotes a method name to call on this object.
176 | on: (name, callback) ->
177 | return unless callback?
178 | @_events[name] ?= []
179 | @_events[name].push callback
180 |
181 | # Registers a map of event names to given callbacks. This method calls .on
182 | # directly for each of the events given.
183 | # @param [Object] map A map of event names to callbacks.
184 | onAll: (map) ->
185 | return unless Epoch.isObject(map)
186 | @on(name, callback) for own name, callback of map
187 |
188 | # Removes a specific callback listener or all listeners for a given event.
189 | # @param [String] name Name of the event.
190 | # @param [Function, String] callback (Optional) Callback to remove from the listener list.
191 | # If this parameter is not provided then all listeners will be removed for the event.
192 | off: (name, callback) ->
193 | return unless Epoch.isArray(@_events[name])
194 | return delete(@_events[name]) unless callback?
195 | while (i = @_events[name].indexOf(callback)) >= 0
196 | @_events[name].splice(i, 1)
197 |
198 | # Removes a set of callback listeners for all events given in the map or array of strings.
199 | # This method calls .off
directly for each event and callback to remove.
200 | # @param [Object, Array] mapOrList Either a map that associates event names to specific callbacks
201 | # or an array of event names for which to completely remove listeners.
202 | offAll: (mapOrList) ->
203 | if Epoch.isArray(mapOrList)
204 | @off(name) for name in mapOrList
205 | else if Epoch.isObject(mapOrList)
206 | @off(name, callback) for own name, callback of mapOrList
207 |
208 | # Triggers an event causing all active listeners to be executed.
209 | # @param [String] name Name of the event to fire.
210 | trigger: (name) ->
211 | return unless @_events[name]?
212 | args = (arguments[i] for i in [1...arguments.length])
213 | for callback in @_events[name]
214 | fn = null
215 | if Epoch.isString(callback)
216 | fn = @[callback]
217 | else if Epoch.isFunction(callback)
218 | fn = callback
219 | unless fn?
220 | Epoch.exception "Callback for event '#{name}' is not a function or reference to a method."
221 | fn.apply @, args
222 |
223 | # Performs a single pass flatten on a multi-array
224 | # @param [Array] multiarray A deep multi-array to flatten
225 | # @returns [Array] A single pass flatten of the multi-array
226 | Epoch.Util.flatten = (multiarray) ->
227 | if !Array.isArray(multiarray)
228 | throw new Error('Epoch.Util.flatten only accepts arrays')
229 | result = []
230 | for array in multiarray
231 | if Array.isArray(array)
232 | for item in array
233 | result.push item
234 | else
235 | result.push array
236 | result
237 |
--------------------------------------------------------------------------------
/src/epoch.coffee:
--------------------------------------------------------------------------------
1 | window.Epoch ?= {}
2 | window.Epoch.Chart ?= {}
3 | window.Epoch.Time ?= {}
4 | window.Epoch.Util ?= {}
5 | window.Epoch.Formats ?= {}
6 |
7 | # Sends a warning to the developer console with the given message.
8 | # @param [String] msg Message for the warning.
9 | Epoch.warn = (msg) ->
10 | (console.warn or console.log)("Epoch Warning: #{msg}")
11 |
12 | # Raises an exception with the given message (with the 'Epoch Error:' preamble).
13 | # @param [String] msg Specific message for the exception.
14 | Epoch.exception = (msg) ->
15 | throw "Epoch Error: #{msg}"
16 |
17 | # "I think, baby, I was born just a little late!" -- Middle Class Rut
18 |
--------------------------------------------------------------------------------
/src/model.coffee:
--------------------------------------------------------------------------------
1 | # Data model for epoch charts. By instantiating a model and passing it to each
2 | # of the charts on a page the application programmer can update data once and
3 | # have each of the charts respond accordingly.
4 | #
5 | # In addition to setting basic / historical data via the setData method, the
6 | # model also supports the push method, which when used will cause real-time
7 | # plots to automatically update and animate.
8 | class Epoch.Model extends Epoch.Events
9 | defaults =
10 | dataFormat: null
11 |
12 | # Creates a new Model.
13 | # @option options dataFormat The default data fromat for the model.
14 | # @option data Initial data for the model.
15 | constructor: (options={}) ->
16 | super()
17 | options = Epoch.Util.defaults options, defaults
18 | @dataFormat = options.dataFormat
19 | @data = options.data
20 | @loading = false
21 |
22 | # Sets the model's data.
23 | # @param data Data to set for the model.
24 | # @event data:updated Instructs listening charts that new data is available.
25 | setData: (data) ->
26 | @data = data
27 | @trigger 'data:updated'
28 |
29 | # Pushes a new entry into the model.
30 | # @param entry Entry to push.
31 | # @event data:push Instructs listening charts that a new data entry is available.
32 | push: (entry) ->
33 | @entry = entry
34 | @trigger 'data:push'
35 |
36 | # Determines if the model has data.
37 | # @return true if the model has data, false otherwise.
38 | hasData: ->
39 | @data?
40 |
41 | # Retrieves and formats adata for the specific chart type and data format.
42 | # @param [String] type Type of the chart for which to fetch the data.
43 | # @param [String, Object] dataFormat (optional) Used to override the model's default data format.
44 | # @return The model's data formatted based the parameters.
45 | getData: (type, dataFormat) ->
46 | dataFormat ?= @dataFormat
47 | Epoch.Data.formatData @data, type, dataFormat
48 |
49 | # Retrieves the latest data entry that was pushed into the model.
50 | # @param [String] type Type of the chart for which to fetch the data.
51 | # @param [String, Object] dataFormat (optional) Used to override the model's default data format.
52 | # @return The model's next data entry formatted based the parameters.
53 | getNext: (type, dataFormat) ->
54 | dataFormat ?= @dataFormat
55 | Epoch.Data.formatEntry @entry, type, dataFormat
56 |
--------------------------------------------------------------------------------
/src/time/area.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Real-time stacked area chart implementation.
3 | class Epoch.Time.Area extends Epoch.Time.Stack
4 | constructor: (@options={}) ->
5 | @options.type ?= 'time.area'
6 | super(@options)
7 | @draw()
8 |
9 | # Sets the appropriate styles to the graphics context given a particular layer.
10 | # @param [Object] layer Layer for which to set the styles.
11 | setStyles: (layer) ->
12 | if layer? && layer.className?
13 | styles = @getStyles "g.#{layer.className.replace(/\s/g,'.')} path.area"
14 | else
15 | styles = @getStyles "g path.area"
16 | @ctx.fillStyle = styles.fill
17 | if styles.stroke?
18 | @ctx.strokeStyle = styles.stroke
19 | if styles['stroke-width']?
20 | @ctx.lineWidth = styles['stroke-width'].replace('px', '')
21 |
22 | # Draws areas for the chart
23 | _drawAreas: (delta=0) ->
24 | [y, w, layers] = [@y(), @w(), @getVisibleLayers()]
25 |
26 | for i in [layers.length-1..0]
27 | continue unless (layer = layers[i])
28 |
29 | @setStyles layer
30 | @ctx.beginPath()
31 |
32 | [j, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
33 | firstX = null
34 | while (--j >= -2) and (--k >= 0)
35 | entry = layer.values[k]
36 | args = [(j+1)*w+delta, y(entry.y + entry.y0)]
37 | args[0] += w if trans
38 | if i == @options.windowSize - 1
39 | @ctx.moveTo.apply @ctx, args
40 | else
41 | @ctx.lineTo.apply @ctx, args
42 |
43 | if trans
44 | borderX = (j+3)*w+delta
45 | else
46 | borderX = (j+2)*w+delta
47 |
48 | @ctx.lineTo(borderX, @innerHeight())
49 | @ctx.lineTo(@width*@pixelRatio+w+delta, @innerHeight())
50 | @ctx.closePath()
51 | @ctx.fill()
52 |
53 | # Draws strokes for the chart
54 | _drawStrokes: (delta=0) ->
55 | [y, w, layers] = [@y(), @w(), @getVisibleLayers()]
56 |
57 | for i in [layers.length-1..0]
58 | continue unless (layer = layers[i])
59 | @setStyles layer
60 | @ctx.beginPath()
61 |
62 | [i, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
63 | firstX = null
64 | while (--i >= -2) and (--k >= 0)
65 | entry = layer.values[k]
66 | args = [(i+1)*w+delta, y(entry.y + entry.y0)]
67 | args[0] += w if trans
68 | if i == @options.windowSize - 1
69 | @ctx.moveTo.apply @ctx, args
70 | else
71 | @ctx.lineTo.apply @ctx, args
72 |
73 | @ctx.stroke()
74 |
75 | # Draws the area chart.
76 | draw: (delta=0) ->
77 | @clear()
78 | @_drawAreas(delta)
79 | @_drawStrokes(delta)
80 | super()
81 |
--------------------------------------------------------------------------------
/src/time/bar.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Real-time Bar Chart implementation.
3 | class Epoch.Time.Bar extends Epoch.Time.Stack
4 | constructor: (@options={}) ->
5 | @options.type ?= 'time.bar'
6 | super(@options)
7 | @draw()
8 |
9 | # @return [Number] An offset used to align the ticks to the center of the rendered bars.
10 | _offsetX: ->
11 | 0.5 * @w() / @pixelRatio
12 |
13 | # Sets the styles for the graphics context given a layer class name.
14 | # @param [String] className The class name to use when deriving the styles.
15 | setStyles: (className) ->
16 | styles = @getStyles "rect.bar.#{className.replace(/\s/g,'.')}"
17 | @ctx.fillStyle = styles.fill
18 |
19 | if !styles.stroke? or styles.stroke == 'none'
20 | @ctx.strokeStyle = 'transparent'
21 | else
22 | @ctx.strokeStyle = styles.stroke
23 |
24 | if styles['stroke-width']?
25 | @ctx.lineWidth = styles['stroke-width'].replace('px', '')
26 |
27 | # Draws the stacked bar chart.
28 | draw: (delta=0) ->
29 | @clear()
30 | [y, w] = [@y(), @w()]
31 |
32 | for layer in @getVisibleLayers()
33 | continue unless Epoch.isNonEmptyArray(layer.values)
34 | @setStyles(layer.className)
35 |
36 | [i, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
37 | iBoundry = if trans then -1 else 0
38 |
39 | while (--i >= iBoundry) and (--k >= 0)
40 | entry = layer.values[k]
41 | [ex, ey, ey0] = [i*w+delta, entry.y, entry.y0]
42 | ex += w if trans
43 | args = [ex+1, y(ey+ey0), w-2, @innerHeight()-y(ey)+0.5*@pixelRatio]
44 |
45 | @ctx.fillRect.apply(@ctx, args)
46 | @ctx.strokeRect.apply(@ctx, args)
47 |
48 | super()
49 |
--------------------------------------------------------------------------------
/src/time/gauge.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Real-time Gauge Visualization. Note: Looks best with a 4:3 aspect ratio (w:h)
3 | class Epoch.Time.Gauge extends Epoch.Chart.Canvas
4 | defaults =
5 | type: 'time.gauge'
6 | domain: [0, 1]
7 | ticks: 10
8 | tickSize: 5
9 | tickOffset: 5
10 | fps: 34
11 | format: Epoch.Formats.percent
12 |
13 | optionListeners =
14 | 'option:domain': 'domainChanged'
15 | 'option:ticks': 'ticksChanged'
16 | 'option:tickSize': 'tickSizeChanged'
17 | 'option:tickOffset': 'tickOffsetChanged'
18 | 'option:format': 'formatChanged'
19 |
20 | # Creates the new gauge chart.
21 | # @param [Object] options Options for the gauge chart.
22 | # @option options [Array] domain The domain to use when rendering values (default: [0, 1]).
23 | # @option options [Integer] ticks Number of ticks to render (default: 10).
24 | # @option options [Integer] tickSize The length (in pixels) for each tick (default: 5).
25 | # @option options [Integer] tickOffset The number of pixels by which to offset ticks from the outer arc (default: 5).
26 | # @option options [Integer] fps The number of animation frames to render per second (default: 34).
27 | # @option options [Function] format The formatting function to use when rendering the gauge label
28 | # (default: Epoch.Formats.percent).
29 | constructor: (@options={}) ->
30 | super(@options = Epoch.Util.defaults(@options, defaults))
31 | @value = @options.value or 0
32 |
33 | if @options.model
34 | @options.model.on 'data:push', => @pushFromModel()
35 |
36 | # SVG Labels Overlay
37 | if @el.style('position') != 'absolute' and @el.style('position') != 'relative'
38 | @el.style('position', 'relative')
39 |
40 | @svg = @el.insert('svg', ':first-child')
41 | .attr('width', @width)
42 | .attr('height', @height)
43 | .attr('class', 'gauge-labels')
44 |
45 | @svg.style
46 | 'position': 'absolute'
47 | 'z-index': '1'
48 |
49 | @svg.append('g')
50 | .attr('transform', "translate(#{@textX()}, #{@textY()})")
51 | .append('text')
52 | .attr('class', 'value')
53 | .text(@options.format(@value))
54 |
55 | # Animations
56 | @animation =
57 | interval: null
58 | active: false
59 | delta: 0
60 | target: 0
61 |
62 | @_animate = =>
63 | if Math.abs(@animation.target - @value) < Math.abs(@animation.delta)
64 | @value = @animation.target
65 | clearInterval @animation.interval
66 | @animation.active = false
67 | else
68 | @value += @animation.delta
69 |
70 | @svg.select('text.value').text(@options.format(@value))
71 | @draw()
72 |
73 | @onAll optionListeners
74 | @draw()
75 |
76 | # Sets the value for the gauge to display and begins animating the guage.
77 | # @param [Number] value Value to set for the gauge.
78 | update: (value) ->
79 | @animation.target = value
80 | @animation.delta = (value - @value) / @options.fps
81 | unless @animation.active
82 | @animation.interval = setInterval @_animate, (1000/@options.fps)
83 | @animation.active = true
84 |
85 | # Alias for the update()
method.
86 | # @param [Number] value Value to set for the gauge.
87 | push: (value) ->
88 | @update value
89 |
90 | # Responds to a model's 'data:push' event.
91 | pushFromModel: ->
92 | next = @options.model.getNext(@options.type, @options.dataFormat)
93 | @update next
94 |
95 | # @return [Number] The radius for the gauge.
96 | radius: -> @getHeight() / 1.58
97 |
98 | # @return [Number] The center position x-coordinate for the gauge.
99 | centerX: -> @getWidth() / 2
100 |
101 | # @return [Number] The center position y-coordinate for the gauge.
102 | centerY: -> 0.68 * @getHeight()
103 |
104 | # @return [Number] The x-coordinate for the gauge text display.
105 | textX: -> @width / 2
106 |
107 | # @return [Number] The y-coordinate for the gauge text display.
108 | textY: -> 0.48 * @height
109 |
110 | # @return [Number] The angle to set for the needle given a value within the domain.
111 | # @param [Number] value Value to translate into a needle angle.
112 | getAngle: (value) ->
113 | [a, b] = @options.domain
114 | ((value - a) / (b - a)) * (Math.PI + 2*Math.PI/8) - Math.PI/2 - Math.PI/8
115 |
116 | # Sets context styles given a particular selector.
117 | # @param [String] selector The selector to use when setting the styles.
118 | setStyles: (selector) ->
119 | styles = @getStyles selector
120 | @ctx.fillStyle = styles.fill
121 | @ctx.strokeStyle = styles.stroke
122 | @ctx.lineWidth = styles['stroke-width'].replace('px', '') if styles['stroke-width']?
123 |
124 | # Draws the gauge.
125 | draw: ->
126 | [cx, cy, r] = [@centerX(), @centerY(), @radius()]
127 | [tickOffset, tickSize] = [@options.tickOffset, @options.tickSize]
128 |
129 | @clear()
130 |
131 | # Draw Ticks
132 | t = d3.scale.linear()
133 | .domain([0, @options.ticks])
134 | .range([ -(9/8)*Math.PI, Math.PI/8 ])
135 |
136 | @setStyles '.epoch .gauge .tick'
137 | @ctx.beginPath()
138 | for i in [0..@options.ticks]
139 | a = t(i)
140 | [c, s] = [Math.cos(a), Math.sin(a)]
141 |
142 | x1 = c * (r-tickOffset) + cx
143 | y1 = s * (r-tickOffset) + cy
144 | x2 = c * (r-tickOffset-tickSize) + cx
145 | y2 = s * (r-tickOffset-tickSize) + cy
146 |
147 | @ctx.moveTo x1, y1
148 | @ctx.lineTo x2, y2
149 |
150 | @ctx.stroke()
151 |
152 | # Outer arc
153 | @setStyles '.epoch .gauge .arc.outer'
154 | @ctx.beginPath()
155 | @ctx.arc cx, cy, r, -(9/8)*Math.PI, (1/8)*Math.PI, false
156 | @ctx.stroke()
157 |
158 | # Inner arc
159 | @setStyles '.epoch .gauge .arc.inner'
160 | @ctx.beginPath()
161 | @ctx.arc cx, cy, r-10, -(9/8)*Math.PI, (1/8)*Math.PI, false
162 | @ctx.stroke()
163 |
164 | @drawNeedle()
165 |
166 | super()
167 |
168 | # Draws the needle.
169 | drawNeedle: ->
170 | [cx, cy, r] = [@centerX(), @centerY(), @radius()]
171 | ratio = @value / @options.domain[1]
172 |
173 | @setStyles '.epoch .gauge .needle'
174 | @ctx.beginPath()
175 | @ctx.save()
176 | @ctx.translate cx, cy
177 | @ctx.rotate @getAngle(@value)
178 |
179 | @ctx.moveTo 4 * @pixelRatio, 0
180 | @ctx.lineTo -4 * @pixelRatio, 0
181 | @ctx.lineTo -1 * @pixelRatio, 19-r
182 | @ctx.lineTo 1, 19-r
183 | @ctx.fill()
184 |
185 | @setStyles '.epoch .gauge .needle-base'
186 | @ctx.beginPath()
187 | @ctx.arc 0, 0, (@getWidth() / 25), 0, 2*Math.PI
188 | @ctx.fill()
189 |
190 | @ctx.restore()
191 |
192 | # Correctly responds to an option:
193 | domainChanged: -> @draw()
194 |
195 | # Correctly responds to an option:
196 | ticksChanged: -> @draw()
197 |
198 | # Correctly responds to an option:
199 | tickSizeChanged: -> @draw()
200 |
201 | # Correctly responds to an option:
202 | tickOffsetChanged: -> @draw()
203 |
204 | # Correctly responds to an option:
205 | formatChanged: -> @svg.select('text.value').text(@options.format(@value))
206 |
207 |
208 |
209 | # "The mother of a million sons... CIVILIZATION!" -- Justice
210 |
--------------------------------------------------------------------------------
/src/time/heatmap.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Real-time Heatmap Implementation.
3 | class Epoch.Time.Heatmap extends Epoch.Time.Plot
4 | defaults =
5 | type: 'time.heatmap'
6 | buckets: 10
7 | bucketRange: [0, 100]
8 | opacity: 'linear'
9 | bucketPadding: 2
10 | paintZeroValues: false
11 | cutOutliers: false
12 |
13 | # Easy to use "named" color functions
14 | colorFunctions =
15 | root: (value, max) -> Math.pow(value/max, 0.5)
16 | linear: (value, max) -> value / max
17 | quadratic: (value, max) -> Math.pow(value/max, 2)
18 | cubic: (value, max) -> Math.pow(value/max, 3)
19 | quartic: (value, max) -> Math.pow(value/max, 4)
20 | quintic: (value, max) -> Math.pow(value/max, 5)
21 |
22 | optionListeners =
23 | 'option:buckets': 'bucketsChanged'
24 | 'option:bucketRange': 'bucketRangeChanged'
25 | 'option:opacity': 'opacityChanged'
26 | 'option:bucketPadding': 'bucketPaddingChanged'
27 | 'option:paintZeroValues': 'paintZeroValuesChanged'
28 | 'option:cutOutliers': 'cutOutliersChanged'
29 |
30 | # Creates a new heatmap.
31 | # @param [Object] options Options for the heatmap.
32 | # @option options [Integer] buckets Number of vertical buckets to use when normalizing the
33 | # incoming histogram data for visualization in the heatmap (default: 10).
34 | # @option options [Array] bucketRange A range of acceptable values to be bucketed (default: [0, 100]).
35 | # @option options [String, Function] opacity The opacity coloring function to use when rendering buckets
36 | # in a column. The built-in functions (referenced by string) are: 'root', 'linear', 'quadratic', 'cubic',
37 | # 'quartic', and 'quintic'. A custom function can be supplied given it accepts two parameters (value, max)
38 | # and returns a numeric value from 0 to 1. Default: linear.
39 | # @option options [Number] bucketPadding Amount of padding to apply around buckets (default: 2).
40 | constructor: (@options={}) ->
41 | super(@options = Epoch.Util.defaults(@options, defaults))
42 | @_setOpacityFunction()
43 | @_setupPaintCanvas()
44 | @onAll optionListeners
45 | @draw()
46 |
47 | _setOpacityFunction: ->
48 | if Epoch.isString(@options.opacity)
49 | @_opacityFn = colorFunctions[@options.opacity]
50 | Epoch.exception "Unknown coloring function provided '#{@options.opacity}'" unless @_opacityFn?
51 | else if Epoch.isFunction(@options.opacity)
52 | @_opacityFn = @options.opacity
53 | else
54 | Epoch.exception "Unknown type for provided coloring function."
55 |
56 | # Prepares initially set data for rendering.
57 | # @param [Array] data Layered histogram data for the visualization.
58 | setData: (data) ->
59 | super(data)
60 | for layer in @data
61 | layer.values = layer.values.map((entry) => @_prepareEntry(entry))
62 |
63 | # Distributes the full histogram in the entry into the defined buckets
64 | # for the visualization.
65 | # @param [Object] entry Entry to prepare for visualization.
66 | _getBuckets: (entry) ->
67 | prepared =
68 | time: entry.time
69 | max: 0
70 | buckets: (0 for i in [0...@options.buckets])
71 |
72 | # Bucket size = (Range[1] - Range[0]) / number of buckets
73 | bucketSize = (@options.bucketRange[1] - @options.bucketRange[0]) / @options.buckets
74 |
75 | for own value, count of entry.histogram
76 | index = parseInt((value - @options.bucketRange[0]) / bucketSize)
77 |
78 | # Remove outliers from the preprared buckets if instructed to do so
79 | if @options.cutOutliers and ((index < 0) or (index >= @options.buckets))
80 | continue
81 |
82 | # Bound the histogram to the range (aka, handle out of bounds values)
83 | if index < 0
84 | index = 0
85 | else if index >= @options.buckets
86 | index = @options.buckets - 1
87 |
88 | prepared.buckets[index] += parseInt count
89 |
90 | for i in [0...prepared.buckets.length]
91 | prepared.max = Math.max(prepared.max, prepared.buckets[i])
92 |
93 | return prepared
94 |
95 | # @return [Function] The y scale for the heatmap.
96 | y: ->
97 | d3.scale.linear()
98 | .domain(@options.bucketRange)
99 | .range([@innerHeight(), 0])
100 |
101 | # @return [Function] The y scale for the svg portions of the heatmap.
102 | ySvg: ->
103 | d3.scale.linear()
104 | .domain(@options.bucketRange)
105 | .range([@innerHeight() / @pixelRatio, 0])
106 |
107 | # @return [Number] The height to render each bucket in a column (disregards padding).
108 | h: ->
109 | @innerHeight() / @options.buckets
110 |
111 | # @return [Number] The offset needed to center ticks at the middle of each column.
112 | _offsetX: ->
113 | 0.5 * @w() / @pixelRatio
114 |
115 | # Creates the painting canvas which is used to perform all the actual drawing. The contents
116 | # of the canvas are then copied into the actual display canvas and through some image copy
117 | # trickery at the end of a transition the illusion of motion over time is preserved.
118 | #
119 | # Using two canvases in this way allows us to render an incredible number of buckets in the
120 | # visualization and animate them at high frame rates without smashing the cpu.
121 | _setupPaintCanvas: ->
122 | # Size the paint canvas to have a couple extra columns so we can perform smooth transitions
123 | @paintWidth = (@options.windowSize + 1) * @w()
124 | @paintHeight = @height * @pixelRatio
125 |
126 | # Create the "memory only" canvas and nab the drawing context
127 | @paint = document.createElement('CANVAS')
128 | @paint.width = @paintWidth
129 | @paint.height = @paintHeight
130 | @p = Epoch.Util.getContext @paint
131 |
132 | # Paint the initial data (rendering backwards from just before the fixed paint position)
133 | @redraw()
134 |
135 | # Hook into the events to paint the next row after it's been shifted into the data
136 | @on 'after:shift', '_paintEntry'
137 |
138 | # At the end of a transition we must reset the paint canvas by shifting the viewable
139 | # buckets to the left (this allows for a fixed cut point and single renders below in @draw)
140 | @on 'transition:end', '_shiftPaintCanvas'
141 | @on 'transition:end', => @draw(@animation.frame * @animation.delta())
142 |
143 | # Redraws the entire heatmap for the current data.
144 | redraw: ->
145 | return unless Epoch.isNonEmptyArray(@data) and Epoch.isNonEmptyArray(@data[0].values)
146 | entryIndex = @data[0].values.length
147 | drawColumn = @options.windowSize
148 |
149 | # This addresses a strange off-by-one issue when the chart is transitioning
150 | drawColumn++ if @inTransition()
151 |
152 | while (--entryIndex >= 0) and (--drawColumn >= 0)
153 | @_paintEntry(entryIndex, drawColumn)
154 | @draw(@animation.frame * @animation.delta())
155 |
156 | # Computes the correct color for a given bucket.
157 | # @param [Integer] value Normalized value at the bucket.
158 | # @param [Integer] max Normalized maximum for the column.
159 | # @param [String] color Computed base color for the bucket.
160 | _computeColor: (value, max, color) ->
161 | Epoch.Util.toRGBA(color, @_opacityFn(value, max))
162 |
163 | # Paints a single entry column on the paint canvas at the given column.
164 | # @param [Integer] entryIndex Index of the entry to paint.
165 | # @param [Integer] drawColumn Column on the paint canvas to place the visualized entry.
166 | _paintEntry: (entryIndex=null, drawColumn=null) ->
167 | [w, h] = [@w(), @h()]
168 |
169 | entryIndex ?= @data[0].values.length - 1
170 | drawColumn ?= @options.windowSize
171 |
172 | entries = []
173 | bucketTotals = (0 for i in [0...@options.buckets])
174 | maxTotal = 0
175 |
176 | for layer in @getVisibleLayers()
177 | entry = @_getBuckets( layer.values[entryIndex] )
178 | for own bucket, count of entry.buckets
179 | bucketTotals[bucket] += count
180 | maxTotal += entry.max
181 | styles = @getStyles ".#{layer.className.split(' ').join('.')} rect.bucket"
182 | entry.color = styles.fill
183 | entries.push entry
184 |
185 | xPos = drawColumn * w
186 |
187 | @p.clearRect xPos, 0, w, @paintHeight
188 |
189 | j = @options.buckets
190 |
191 | for own bucket, sum of bucketTotals
192 | color = @_avgLab(entries, bucket)
193 | max = 0
194 | for entry in entries
195 | max += (entry.buckets[bucket] / sum) * maxTotal
196 | if sum > 0 or @options.paintZeroValues
197 | @p.fillStyle = @_computeColor(sum, max, color)
198 | @p.fillRect xPos, (j-1) * h, w-@options.bucketPadding, h-@options.bucketPadding
199 | j--
200 |
201 | # This shifts the image contents of the paint canvas to the left by 1 column width.
202 | # It is called after a transition has ended (yay, slight of hand).
203 | _shiftPaintCanvas: ->
204 | data = @p.getImageData @w(), 0, @paintWidth-@w(), @paintHeight
205 | @p.putImageData data, 0, 0
206 |
207 | # Performs an averaging of the colors for muli-layer heatmaps using the lab color space.
208 | # @param [Array] entries The layers for which the colors are to be averaged.
209 | # @param [Number] bucket The bucket in the entries that must be averaged.
210 | # @return [String] The css color code for the average of all the layer colors.
211 | _avgLab: (entries, bucket) ->
212 | [l, a, b, total] = [0, 0, 0, 0]
213 | for entry in entries
214 | continue unless entry.buckets[bucket]?
215 | total += entry.buckets[bucket]
216 |
217 | for own i, entry of entries
218 | if entry.buckets[bucket]?
219 | value = entry.buckets[bucket]|0
220 | else
221 | value = 0
222 | ratio = value / total
223 | color = d3.lab(entry.color)
224 | l += ratio * color.l
225 | a += ratio * color.a
226 | b += ratio * color.b
227 |
228 | d3.lab(l, a, b).toString()
229 |
230 | # Copies the paint canvas onto the display canvas, thus rendering the heatmap.
231 | draw: (delta=0) ->
232 | @clear()
233 | @ctx.drawImage @paint, delta, 0
234 | super()
235 |
236 | # Changes the number of buckets in response to an option:buckets
event.
237 | bucketsChanged: -> @redraw()
238 |
239 | # Changes the range of the buckets in response to an option:bucketRange
event.
240 | bucketRangeChanged: ->
241 | @_transitionRangeAxes()
242 | @redraw()
243 |
244 | # Changes the opacity function in response to an option:opacity
event.
245 | opacityChanged: ->
246 | @_setOpacityFunction()
247 | @redraw()
248 |
249 | # Changes the bucket padding in response to an option:bucketPadding
event.
250 | bucketPaddingChanged: -> @redraw()
251 |
252 | # Changes whether or not to paint zeros in response to an option:paintZeroValues
event.
253 | paintZeroValuesChanged: -> @redraw()
254 |
255 | # Changes whether or not to cut outliers when bucketing in response to an
256 | # option:cutOutliers
event.
257 | cutOutliersChanged: -> @redraw()
258 |
259 | layerChanged: -> @redraw()
260 |
261 | # "Audio... Audio... Audio... Video Disco..." - Justice
262 |
--------------------------------------------------------------------------------
/src/time/line.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Real-time line chart implementation
3 | class Epoch.Time.Line extends Epoch.Time.Plot
4 | constructor: (@options={}) ->
5 | @options.type ?= 'time.line'
6 | super(@options)
7 | @draw()
8 |
9 | # Sets the graphics context styles based ont he given layer class name.
10 | # @param [String] className The class name of the layer for which to set the styles.
11 | setStyles: (className) ->
12 | styles = @getStyles "g.#{className.replace(/\s/g,'.')} path.line"
13 | @ctx.fillStyle = styles.fill
14 | @ctx.strokeStyle = styles.stroke
15 | @ctx.lineWidth = @pixelRatio * styles['stroke-width'].replace('px', '')
16 |
17 | # Draws the line chart.
18 | draw: (delta=0) ->
19 | @clear()
20 | w = @w()
21 | for layer in @getVisibleLayers()
22 | continue unless Epoch.isNonEmptyArray(layer.values)
23 | @setStyles(layer.className)
24 | @ctx.beginPath()
25 | y = @y(layer.range)
26 | [i, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
27 |
28 | while (--i >= -2) and (--k >= 0)
29 | entry = layer.values[k]
30 | args = [(i+1)*w+delta, y(entry.y)]
31 | args[0] += w if trans
32 | if i == @options.windowSize - 1
33 | @ctx.moveTo.apply @ctx, args
34 | else
35 | @ctx.lineTo.apply @ctx, args
36 |
37 | @ctx.stroke()
38 |
39 | super()
40 |
--------------------------------------------------------------------------------
/tests/render/basic/histogram.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
24 |
25 |
26 |
27 |
28 | Basic Bar Chart Test
29 |
30 |
31 |
32 |
37 |
38 |
39 | 1. Beta(2, 5)
41 | 2. Beta(2, 5) Horizontal
62 | 3. Option: buckets
84 | 4. Options: bucketRange & cutOutliers
117 | Basic Chart Model / Data Test
24 |
25 |
26 | Basic Chart Options and Events
17 |
18 |
26 |
27 |
28 | 1. Axes
30 | 2. Margins
69 | 3. Ticks and Tick Formats
106 | 4. Resize
178 | 6. Option: domain
210 |
211 | 7. Option: range
239 |
240 | Basic Pie Chart Test
18 |
19 |
20 |
31 |
32 |
33 | Basic Pie Test
35 |
38 |
42 |
43 |
44 | 2. Basic Donut Test
62 |
65 |
69 |
70 |
71 | 3. Pie Tranisition I
90 |
93 |
96 | and set B:
97 |
98 |
102 | Use the buttons below the chart to initiate the transitions.
103 |
104 |
105 | 4. Pie Tranisition II
135 |
138 |
141 | and set B:
142 |
143 |
146 | Use the buttons below the chart to initiate the transitions.
147 |
148 |
149 | 5. Color Override
178 |
181 |
185 |
186 |
187 | 6. Categorical Colors
211 | 7. Pie Chart Layers without Labels
251 |
254 |
258 | when the layers are not provided labels.
259 |
260 |
261 | 8. Margin Changes
275 |
276 | 9. Inner Changes
298 |
299 | 10. Show/Hide Layers
322 |
323 | Epoch Chart Rendering Tests
13 |
14 | Basic Charts
15 |
31 |
39 | 40 | 41 |43 |
47 | 48 | 49 |51 |
Display a single value of 25%
37 | 38 |Display value of 0% and transition to a random value when the button is pressed.
50 | 51 | 52 |Display value of 0% and transition to a random value every second when the button is pressed.
71 | 72 | 73 |Display the four built-in gauge sizes in this order: tiny, small, medium, large.
104 | 105 | 106 | 107 | 108 |122 | Override the basic gauge styles with the following 123 |
223 | | 224 | 225 | 226 |
227 |235 | | 236 | 237 | 238 | 239 | 240 |
241 |249 | | 250 | 251 | 252 | 253 | 254 |
255 |263 | | 264 | 265 | 266 | 267 |
268 |276 | | 277 | 278 | 279 |
280 |Correctly Resize a Real-time Chart.
30 | 31 |32 | | 33 | 34 | 35 | 36 |
37 |142 | | 143 | 144 | 145 |
146 |192 | | 193 | 194 | 195 |
196 |252 | | 253 | 254 | 255 | 256 |
257 |