-
Notifications
You must be signed in to change notification settings - Fork 16
Description
Hey guys, I'm noticing a bunch of failing tests on master. Is this known? I was hoping to try and get a test to fail for the odd segment drag behavior I'm seeing but I'm a total newb when it comes to writing JS tests using browserify, testling, babel-tape-runner, etc.
Also, I really want to be working off the develop branch and the tests don't seem to be running at all??
I'm seeing the following differences in the package.json scripts
MASTER BRANCH
"test": "browserify -t [ babelify --blacklist regenerator ] tests/*/*.js | testling -u --no-show",
"coverage": "browserify -t [ babelify --blacklist regenerator ] -t coverify tests/*/*.js | testling -u --no-show | coverify --json -o coverage.json && node ./bin/scripts --cover-report -i coverage.json"
DEVELOP BRANCH
"test": "babel-tape-runner tests/**.test.js",
The coverage command seems to have been killed in the develop branch. The test command in develop doesn't print anything.
Also, all the tests in the develop branch still include from the es6 directory which doesn't exist.
import AnnotatedMarkerLayer from '../es6/helpers/annotated-marker-layer';
$ MASTER BRANCH yarn coverage output below
⚡ yarn coverage
yarn run v1.2.1
$ browserify -t [ babelify --blacklist regenerator ] -t coverify tests/*/*.js | testling -u --no-show | coverify --json -o coverage.json && node ./bin/scripts --cover-report -i coverage.json
http://localhost:54242/__testling?show=false
TAP version 13
# Edit Breakpoint Behavior
ok 1 should be equal
ok 2 should be equal
# MarkerBehavior
ok 3 should be equal
# SegmentBehavior
ok 4 should be equal
ok 5 should be equal
ok 6 should be equal
ok 7 should be equal
ok 8 should be equal
ok 9 should be equal
ok 10 should be equal
ok 11 should be equal
# TimeContextBehavior should edit shape accordingly
ok 12 should be equal
ok 13 should be equal
ok 14 should be equal
ok 15 should be equal
ok 16 should be equal
ok 17 should be equal
ok 18 should be equal
ok 19 should be equal
ok 20 should be equal
ok 21 should be equal
ok 22 should be equal
ok 23 should be equal
ok 24 should be equal
ok 25 should be equal
ok 26 should be equal
# TimeContextBehavior should edit shape accordingly v2
ok 27 should be equal
ok 28 should be equal
ok 29 should be equal
ok 30 should be equal
ok 31 should be equal
ok 32 should be equal
ok 33 should be equal
ok 34 should be equal
ok 35 should be equal
ok 36 should be equal
ok 37 should be equal
ok 38 should be equal
# TimeContextBehavior should stretch behavior correctly
ok 39 should be equal
ok 40 should be equal
ok 41 should be equal
ok 42 should be equal
ok 43 should be equal
ok 44 should be equal
ok 45 should be truthy
ok 46 should be equal
ok 47 should be equal
ok 48 should be equal
ok 49 should be truthy
# TraceBehavior
ok 50 should be equal
ok 51 should be equal
ok 52 should be equal
ok 53 should be equal
ok 54 should be equal
ok 55 should be equal
ok 56 should be equal
ok 57 should be equal
ok 58 should be equal
ok 59 should be equal
ok 60 should be equal
ok 61 should be equal
# LayerTimeContext get default values
ok 62 Default layerTimeContext startis 0 second
ok 63 Default layerTimeContext start is 10 seconds
ok 64 Default layerTimeContext offset 0 seconds
ok 65 Default stretchRatio is 1
# Layer with default params
ok 66 should be equal
ok 67 should be equal
# TimelineTimeContext get default values
ok 68 Initial offset is 0
ok 69 Initial pixelsPerSecond is 0
ok 70 Initial zoom is 1
ok 71 Initial visibleWidth is 1000 pixels
ok 72 Initial visibleDuration is 10 seconds
ok 73 Initial maintainVisibleDuration is false
# TimelineTimeContext set values
ok 74 visibleDuration is 1 second
ok 75 zoom is unchanged
ok 76 pps is unchanged
ok 77 0.5 seconds
# Timeline get default window view values over tracks
ok 78 Initial offset is 0
ok 79 Initial zoom is 1
ok 80 Initial pixelsPerSecond is 100
ok 81 Initial visibleWidth is 1000
ok 82 Initial visibleDuration is 10 seconds
# Timeline set window view values over tracks
ok 83 Offset is set to 10 seconds
ok 84 Zoom is set to 1
ok 85 pixelsPerSecond is set to 200
ok 86 visibleWidth is set to 2000
# Create a track from the timeline
ok 87 Timeline has one track
ok 88 The timeline track is the one just added
ok 89 should be equal
ok 90 should be equal
# Add a track from the timeline
ok 91 Can't add a track already added to the timeline
ok 92 Timeline has two tracks
ok 93 The timeline second track is the one just added
# Add a layer to a track from the timeline, and remove it
ok 94 should be equivalent
ok 95 The entire timeline has one, and just one layer
ok 96 The track we created contains one layer
ok 97 The track layer is the right one
ok 98 The entire timeline has no more layers
ok 99 The track we created doesn't contain any layer
ok 100 We can retrieve a track with a specific id
ok 101 We can get all the layers that belong to a certain group
ok 102 We can get all groupedLayers
ok 103 Can't add a track with a trackId already added
# TrackCollection methods
ok 104 should be equal
ok 105 should be equivalent
ok 106 update event is emitted
ok 107 update:containers event is emitted
ok 108 update:layers event is emitted
ok 109 render event is emitted
# Track - instanciation
ok 110 Default height is 100
ok 111 should be equal
ok 112 should be equal
ok 113 should be equal
ok 114 should be equal
ok 115 should be equal
ok 116 should be equal
ok 117 should be equal
ok 118 should be equal
ok 119 should be equal
ok 120 should be equal
ok 121 should be equal
ok 122 should be equal
ok 123 should be equal
ok 124 should be equal
ok 125 should be equal
ok 126 should be equal
ok 127 should be equal
ok 128 should be equal
ok 129 should be equal
ok 130 When set to 200, height is 200px
# Track - add/remove Layer
ok 131 should be equal
ok 132 should be equal
ok 133 should be equal
ok 134 should be equal
ok 135 should be equal
ok 136 should be equal
ok 137 should be equal
ok 138 should be equal
ok 139 should be equal
ok 140 should be equal
ok 141 should be equal
ok 142 should be equal
ok 143 should be equal
ok 144 should be equal
ok 145 should be equal
ok 146 should be equal
ok 147 should be equal
ok 148 should be equal
ok 149 should be equal
ok 150 should be equal
ok 151 should be equal
ok 152 should be equal
ok 153 should be equal
ok 154 should be equal
ok 155 should be equal
ok 156 should be equal
ok 157 should be equal
ok 158 should be equal
ok 159 should be equal
ok 160 should be equal
ok 161 should be equal
ok 162 should be equivalent
# Track - update container
ok 163 should be equal
ok 164 should be equal
ok 165 should be equal
# KeyBoard keydown
not ok 166 should be equal
---
operator: equal
expected: 'A'
actual: '\x00'
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Keyboard.<anonymous> (http://localhost:54242/__testling?show=false:22104:53)
at Keyboard.EventEmitter.emit (http://localhost:54242/__testling?show=false:9095:20)
at HTMLDocument.onKeyDown (http://localhost:54242/__testling?show=false:18852:58)
at Test.<anonymous> (http://localhost:54242/__testling?show=false:22131:53)
at Test.bound [as _cb] (http://localhost:54242/__testling?show=false:13499:32)
at Test.run (http://localhost:54242/__testling?show=false:13518:10)
...
ok 167 should be equal
ok 168 should be equal
ok 169 should be equal
ok 170 should be equal
ok 171 should be equal
not ok 172 should be equal
---
operator: equal
expected: 'A'
actual: '\x00'
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Keyboard.<anonymous> (http://localhost:54242/__testling?show=false:22104:53)
at Keyboard.EventEmitter.emit (http://localhost:54242/__testling?show=false:9095:20)
at HTMLDocument.onKeyUp (http://localhost:54242/__testling?show=false:18860:58)
at Test.<anonymous> (http://localhost:54242/__testling?show=false:22133:53)
at Test.bound [as _cb] (http://localhost:54242/__testling?show=false:13499:32)
at Test.run (http://localhost:54242/__testling?show=false:13518:10)
...
ok 173 should be equal
ok 174 should be equal
ok 175 should be equal
ok 176 should be equal
ok 177 should be equal
# Surface, default instance attributes
not ok 178 should be equal
---
operator: equal
expected: null
actual: undefined
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Test.<anonymous> (http://localhost:54242/__testling?show=false:22161:51)
at Test.bound [as _cb] (http://localhost:54242/__testling?show=false:13499:32)
at Test.run (http://localhost:54242/__testling?show=false:13518:10)
at Test.bound [as run] (http://localhost:54242/__testling?show=false:13499:32)
at next (http://localhost:54242/__testling?show=false:13311:15)
at Item.run (http://localhost:54242/__testling?show=false:10218:14)
...
not ok 179 should be equal
---
operator: equal
expected: null
actual: undefined
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Test.<anonymous> (http://localhost:54242/__testling?show=false:22163:51)
at Test.bound [as _cb] (http://localhost:54242/__testling?show=false:13499:32)
at Test.run (http://localhost:54242/__testling?show=false:13518:10)
at Test.bound [as run] (http://localhost:54242/__testling?show=false:13499:32)
at next (http://localhost:54242/__testling?show=false:13311:15)
at Item.run (http://localhost:54242/__testling?show=false:10218:14)
...
# Surface, trigger events
ok 180 should be equal
not ok 181 should be equal
---
operator: equal
expected: true
actual: undefined
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22192:53)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
at Surface.EventEmitter.emit (http://localhost:54242/__testling?show=false:9073:17)
at HTMLBodyElement.onMouseDown (http://localhost:54242/__testling?show=false:19088:58)
at Test.<anonymous> (http://localhost:54242/__testling?show=false:22244:49)
at Test.bound [as _cb] (http://localhost:54242/__testling?show=false:13499:32)
...
ok 182 should be equal
ok 183 should be equal
ok 184 should be equal
not ok 185 should be equal
---
operator: equal
expected: true
actual: undefined
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22211:55)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
at Surface.EventEmitter.emit (http://localhost:54242/__testling?show=false:9073:17)
at onMouseMove (http://localhost:54242/__testling?show=false:19102:58)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22240:51)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
...
ok 186 should be equal
not ok 187 should be equal
---
operator: equal
expected: false
actual: undefined
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22226:57)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
at Surface.EventEmitter.emit (http://localhost:54242/__testling?show=false:9073:17)
at onMouseUp (http://localhost:54242/__testling?show=false:19123:58)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22236:53)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
...
not ok 188 should be equal
---
operator: equal
expected: null
actual: undefined
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22228:57)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
at Surface.EventEmitter.emit (http://localhost:54242/__testling?show=false:9073:17)
at onMouseUp (http://localhost:54242/__testling?show=false:19123:58)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22236:53)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
...
not ok 189 should be equal
---
operator: equal
expected: null
actual: undefined
at: Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13651:17)
stack: |-
Error: should be equal
at Test.assert [as _assert] (http://localhost:54242/__testling?show=false:13647:54)
at Test.bound [as _assert] (http://localhost:54242/__testling?show=false:13499:32)
at Test.equal.Test.equals.Test.isEqual.Test.is.Test.strictEqual.Test.strictEquals (http://localhost:54242/__testling?show=false:13782:10)
at Test.bound [as equal] (http://localhost:54242/__testling?show=false:13499:32)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22230:57)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
at Surface.EventEmitter.emit (http://localhost:54242/__testling?show=false:9073:17)
at onMouseUp (http://localhost:54242/__testling?show=false:19123:58)
at Surface.<anonymous> (http://localhost:54242/__testling?show=false:22236:53)
at Surface.g (http://localhost:54242/__testling?show=false:9165:16)
...
ok 190 should be equal
ok 191 should be equal
# Annotated Marker
ok 192 should contain the right text
ok 193 should contain the right text
# Dot
ok 194 Dot is well positioned 1
ok 195 Dot is well positioned 2
# Marker
ok 196 should be equal
ok 197 should be equal
# Segment instanciation
ok 198 should be equal
ok 199 should be equal
ok 200 should be equal
ok 201 should be equal
# Segment navigation zoom and move
ok 202 should be equal
ok 203 should be equal
ok 204 should be equal
# TraceDots
ok 205 should be equal
ok 206 should be equal
# OrthogonalData
ok 207 Correctly tranforms cols to rows
ok 208 Correctly tranforms rows to cols
1..208
# tests 208
# pass 199
# fail 9
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/core/layer.js
1 import events from 'events';
2 import ns from './namespace';
3 import scales from '../utils/scales';
4 import Segment from '../shapes/segment';
5 import TimeContextBehavior from '../behaviors/time-context-behavior';
6
7 // time context bahevior
8 let timeContextBehavior = null;
9 let timeContextBehaviorCtor = TimeContextBehavior;
10
11 /**
12 * The layer class is the main visualization class. It is mainly defines by its
13 * related `LayerTimeContext` which determines its position in the overall
14 * timeline (through the `start`, `duration`, `offset` and `stretchRatio`
15 * attributes) and by it's registered Shape which defines how to display the
16 * data associated to the layer. Each created layer must be inserted into a
17 * `Track` instance in order to be displayed.
18 *
19 * _Note: in the context of the layer, an __item__ is the SVG element
20 * returned by a `Shape` instance and associated with a particular __datum__._
21 *
22 * ### Layer DOM structure
23 * ```
24 * <g class="layer" transform="translate(${start}, 0)">
25 * <svg class="bounding-box" width="${duration}">
26 * <g class="offset" transform="translate(${offset, 0})">
27 * <!-- background -->
28 * <rect class="background"></rect>
29 * <!-- shapes and common shapes are inserted here -->
30 * </g>
31 * <g class="interactions"><!-- for feedback --></g>
32 * </svg>
33 * </g>
34 * ```
35 */
36 export default class Layer extends events.EventEmitter {
37 /**
38 * @param {String} dataType - Defines how the layer should look at the data.
39 * Can be 'entity' or 'collection'.
40 * @param {(Array|Object)} data - The data associated to the layer.
41 * @param {Object} options - Configures the layer.
42 * @param {Number} [options.height=100] - Defines the height of the layer.
43 * @param {Number} [options.top=0] - Defines the top position of the layer.
44 * @param {Number} [options.opacity=1] - Defines the opacity of the layer.
45 * @param {Number} [options.yDomain=[0,1]] - Defines boundaries of the data
46 * values in y axis (for exemple to display an audio buffer, this attribute
47 * should be set to [-1, 1].
48 * @param {String} [options.className=null] - An optionnal class to add to each
49 * created shape.
50 * @param {String} [options.className='selected'] - The class to add to a shape
51 * when selected.
52 * @param {Number} [options.contextHandlerWidth=2] - The width of the handlers
53 * displayed to edit the layer.
54 * @param {Number} [options.hittable=false] - Defines if the layer can be interacted
55 * with. Basically, the layer is not returned by `BaseState.getHitLayers` when
56 * set to false (a common use case is a layer that contains a cursor)
57 */
58 constructor(dataType, data, options = {}) {
59 super();
60
61 const defaults = {
62 height: 100,
63 top: 0,
64 opacity: 1,
65 yDomain: [0, 1],
66 className: null,
67 selectedClassName: 'selected',
68 contextHandlerWidth: 2,
69 hittable: true, // when false the layer is not returned by `BaseState.getHitLayers`
70 id: '', // used ?
71 overflow: 'hidden', // usefull ?
72 };
73
74 /**
75 * Parameters of the layers, `defaults` overrided with options.
76 * @type {Object}
77 */
78 this.params = Object.assign({}, defaults, options);
79 /**
80 * Defines how the layer should look at the data (`'entity'` or `'collection'`).
81 * @type {String}
82 */
83 this.dataType = dataType; // 'entity' || 'collection';
84 /** @type {LayerTimeContext} */
85 this.timeContext = null;
86 /** @type {Element} */
87 this.$el = null;
88 /** @type {Element} */
89 this.$background = null;
90 /** @type {Element} */
91 this.$boundingBox = null;
92 /** @type {Element} */
93 this.$offset = null;
94 /** @type {Element} */
95 this.$interactions = null;
96 /**
97 * A Segment instanciated to interact with the Layer itself.
98 * @type {Segment}
99 */
100 this.contextShape = null;
101
102 this._shapeConfiguration = null; // { ctor, accessors, options }
103 this._commonShapeConfiguration = null; // { ctor, accessors, options }
104 this._$itemShapeMap = new Map();
105 this._$itemDataMap = new Map();
106 this._$itemCommonShapeMap = new Map();
107
108 this._isContextEditable = false;
109 this._behavior = null;
110
111 this.data = data;
112
113 this._valueToPixel = scales.linear()
114 .domain(this.params.yDomain)
115 .range([0, this.params.height]);
116
117 // initialize timeContext layout
118 this._renderContainer();
119 // creates the timeContextBehavior for all layers
120 if (timeContextBehavior === null) {
121 timeContextBehavior = new timeContextBehaviorCtor();
122 }
123 }
124
125 /**
126 * Destroy the layer, clear all references.
127 */
128 destroy() {
129 this.timeContext = null;
130 this.data = null;
131 this.params = null;
132 this._behavior = null;
133
134 this._$itemShapeMap.clear();
135 this._$itemDataMap.clear();
136 this._$itemCommonShapeMap.clear();
137
138 this.removeAllListeners();
139 }
140
141 /**
142 * Allows to override default the `TimeContextBehavior` used to edit the layer.
143 *
144 * @param {Object} ctor
145 */
146 static configureTimeContextBehavior(ctor) {
147 timeContextBehaviorCtor = ctor;
148 }
149
150 /**
151 * Returns `LayerTimeContext`'s `start` time domain value.
152 *
153 * @type {Number}
154 */
155 get start() {
156 return this.timeContext.start;
157 }
158
159 /**
160 * Sets `LayerTimeContext`'s `start` time domain value.
161 *
162 * @type {Number}
163 */
164 set start(value) {
165 this.timeContext.start = value;
166 }
167
168 /**
169 * Returns `LayerTimeContext`'s `offset` time domain value.
170 *
171 * @type {Number}
172 */
173 get offset() {
174 return this.timeContext.offset;
175 }
176
177 /**
178 * Sets `LayerTimeContext`'s `offset` time domain value.
179 *
180 * @type {Number}
181 */
182 set offset(value) {
183 this.timeContext.offset = value;
184 }
185
186 /**
187 * Returns `LayerTimeContext`'s `duration` time domain value.
188 *
189 * @type {Number}
190 */
191 get duration() {
192 return this.timeContext.duration;
193 }
194
195 /**
196 * Sets `LayerTimeContext`'s `duration` time domain value.
197 *
198 * @type {Number}
199 */
200 set duration(value) {
201 this.timeContext.duration = value;
202 }
203
204 /**
205 * Returns `LayerTimeContext`'s `stretchRatio` time domain value.
206 *
207 * @type {Number}
208 */
209 get stretchRatio() {
210 return this.timeContext.stretchRatio;
211 }
212
213 /**
214 * Sets `LayerTimeContext`'s `stretchRatio` time domain value.
215 *
216 * @type {Number}
217 */
218 set stretchRatio(value) {
219 this.timeContext.stretchRatio = value;
220 }
221
222 /**
223 * Set the domain boundaries of the data for the y axis.
224 *
225 * @type {Array}
226 */
227 set yDomain(domain) {
228 this.params.yDomain = domain;
229 this._valueToPixel.domain(domain);
230 }
231
232 /**
233 * Returns the domain boundaries of the data for the y axis.
234 *
235 * @type {Array}
236 */
237 get yDomain() {
238 return this.params.yDomain;
239 }
240
241 /**
242 * Sets the opacity of the whole layer.
243 *
244 * @type {Number}
245 */
246 set opacity(value) {
247 this.params.opacity = value;
248 }
249
250 /**
251 * Returns the opacity of the whole layer.
252 *
253 * @type {Number}
254 */
255 get opacity() {
256 return this.params.opacity;
257 }
258
259 /**
260 * Returns the transfert function used to display the data in the x axis.
261 *
262 * @type {Number}
263 */
264 get timeToPixel() {
265 return this.timeContext.timeToPixel;
266 }
267
268 /**
269 * Returns the transfert function used to display the data in the y axis.
270 *
271 * @type {Number}
272 */
273 get valueToPixel() {
274 return this._valueToPixel;
275 }
276
277 /**
278 * Returns an array containing all the displayed items.
279 *
280 * @type {Array<Element>}
281 */
282 get items() {
283 return Array.from(this._$itemDataMap.keys());
284 }
285
286 /**
287 * Returns the data associated to the layer.
288 *
289 * @type {Object[]}
290 */
291 get data() { return this._data; }
292
293 /**
294 * Sets the data associated with the layer.
295 *
296 * @type {Object|Object[]}
297 */
298 set data(data) {
299 switch (this.dataType) {
300 case 'entity':
301 if (this._data) { // if data already exists, reuse the reference
302 this._data[0] = data;
303 } else {
304 this._data = [data];
305 }
306 break;
307 case 'collection':
308 this._data = data;
309 break;
310 }
311 }
312
313 // --------------------------------------
314 // Initialization
315 // --------------------------------------
316
317 /**
318 * Renders the DOM in memory on layer creation to be able to use it before
319 * the layer is actually inserted in the DOM.
320 */
321 _renderContainer() {
322 // wrapper group for `start, top and context flip matrix
323 this.$el = document.createElementNS(ns, 'g');
324 this.$el.classList.add('layer');
325 if (this.params.className !== null) {
326 this.$el.classList.add(this.params.className);
327 }
328 // clip the context with a `svg` element
329 this.$boundingBox = document.createElementNS(ns, 'svg');
330 this.$boundingBox.classList.add('bounding-box');
331 this.$boundingBox.style.overflow = this.params.overflow;
332 // group to apply offset
333 this.$offset = document.createElementNS(ns, 'g');
334 this.$offset.classList.add('offset', 'items');
335 // layer background
336 this.$background = document.createElementNS(ns, 'rect');
337 this.$background.setAttributeNS(null, 'height', '100%');
338 this.$background.setAttributeNS(null, 'width', '100%');
339 this.$background.classList.add('background');
340 this.$background.style.fillOpacity = 0;
341 this.$background.style.pointerEvents = 'none';
342 // context interactions
343 this.$interactions = document.createElementNS(ns, 'g');
344 this.$interactions.classList.add('interactions');
345 this.$interactions.style.display = 'none';
346 // @NOTE: works but king of ugly... should be cleaned
347 this.contextShape = new Segment();
348 this.contextShape.install({
349 opacity: () => 0.1,
350 color : () => '#787878',
351 width : () => this.timeContext.duration,
352 height : () => this._renderingContext.valueToPixel.domain()[1],
353 y : () => this._renderingContext.valueToPixel.domain()[0]
354 });
355
356 this.$interactions.appendChild(this.contextShape.render());
357 // create the DOM tree
358 this.$el.appendChild(this.$boundingBox);
359 this.$boundingBox.appendChild(this.$offset);
360 this.$offset.appendChild(this.$background);
361 this.$boundingBox.appendChild(this.$interactions);
362 }
363
364 // --------------------------------------
365 // Component Configuration
366 // --------------------------------------
367
368 /**
369 * Sets the context of the layer, thus defining its `start`, `duration`,
370 * `offset` and `stretchRatio`.
371 *
372 * @param {TimeContext} timeContext - The timeContext in which the layer is displayed.
373 */
374 setTimeContext(timeContext) {
375 this.timeContext = timeContext;
376 // create a mixin to pass to the shapes
377 this._renderingContext = {};
378 this._updateRenderingContext();
379 }
380
381 /**
382 * Register a shape and its configuration to use in order to render the data.
383 *
384 * @param {BaseShape} ctor - The constructor of the shape to be used.
385 * @param {Object} [accessors={}] - Defines how the shape should adapt to a particular data struture.
386 * @param {Object} [options={}] - Global configuration for the shapes, is specific to each `Shape`.
387 */
388 configureShape(ctor, accessors = {}, options = {}) {
389 this._shapeConfiguration = { ctor, accessors, options };
390 }
391
392 /**
393 * Optionnaly register a shape to be used accros the entire collection.
394 *
395 * @param {BaseShape} ctor - The constructor of the shape to be used.
396 * @param {Object} [accessors={}] - Defines how the shape should adapt to a particular data struture.
397 * @param {Object} [options={}] - Global configuration for the shapes, is specific to each `Shape`.
398 */
399 configureCommonShape(ctor, accessors = {}, options = {}) {
400 this._commonShapeConfiguration = { ctor, accessors, options };
401 }
402
403 /**
404 * Register the behavior to use when interacting with a shape.
405 *
406 * @param {BaseBehavior} behavior
407 */
408 setBehavior(behavior) {
409 behavior.initialize(this);
410 this._behavior = behavior;
411 }
412
413 /**
414 * Updates the values stored int the `_renderingContext` passed to shapes
415 * for rendering and updating.
416 */
417 _updateRenderingContext() {
418 this._renderingContext.timeToPixel = this.timeContext.timeToPixel;
419 this._renderingContext.valueToPixel = this._valueToPixel;
420
421 this._renderingContext.height = this.params.height;
422 this._renderingContext.width = this.timeContext.timeToPixel(this.timeContext.duration);
423 // for foreign object issue in chrome
424 this._renderingContext.offsetX = this.timeContext.timeToPixel(this.timeContext.offset);
425 this._renderingContext.startX = this.timeContext.parent.timeToPixel(this.timeContext.start);
426
427 // @todo replace with `minX` and `maxX` representing the visible pixels in which
428 // the shapes should be rendered, could allow to not update the DOM of shapes
429 // who are not in this area.
430 this._renderingContext.trackOffsetX = this.timeContext.parent.timeToPixel(this.timeContext.parent.offset);
431 this._renderingContext.visibleWidth = this.timeContext.parent.visibleWidth;
432 }
433
434 // --------------------------------------
435 // Behavior Accessors
436 // --------------------------------------
437
438 /**
439 * Returns the items marked as selected.
440 *
441 * @type {Array<Element>}
442 */
443 get selectedItems() {
444 return this._behavior ? this._behavior.selectedItems : [];
445 }
446
447 /**
448 * Mark item(s) as selected.
449 *
450 * @param {Element|Element[]} $items
451 */
452 select(...$items) {
453 if (!this._behavior) { return; }
454 if (!$items.length) { $items = this._$itemDataMap.keys(); }
455 if (Array.isArray($items[0])) { $items = $items[0]; }
456
457 for (let $item of $items) {
458 const datum = this._$itemDataMap.get($item);
459 this._behavior.select($item, datum);
460 this._toFront($item);
461 }
462 }
463
464 /**
465 * Removes item(s) from selected items.
466 *
467 * @param {Element|Element[]} $items
468 */
469 unselect(...$items) {
470 if (!this._behavior) { return; }
471 if (!$items.length) { $items = this._$itemDataMap.keys(); }
472 if (Array.isArray($items[0])) { $items = $items[0]; }
473
474 for (let $item of $items) {
475 const datum = this._$itemDataMap.get($item);
476 this._behavior.unselect($item, datum);
477 }
478 }
479
480 /**
481 * Toggle item(s) selection state according to their current state.
482 *
483 * @param {Element|Element[]} $items
484 */
485 toggleSelection(...$items) {
486 if (!this._behavior) { return; }
487 if (!$items.length) { $items = this._$itemDataMap.keys(); }
488 if (Array.isArray($items[0])) { $items = $items[0]; }
489
490 for (let $item of $items) {
491 const datum = this._$itemDataMap.get($item);
492 this._behavior.toggleSelection($item, datum);
493 }
494 }
495
496 /**
497 * Edit item(s) according to the `edit` defined in the registered `Behavior`.
498 *
499 * @param {Element|Element[]} $items - The item(s) to edit.
500 * @param {Number} dx - The modification to apply in the x axis (in pixels).
501 * @param {Number} dy - The modification to apply in the y axis (in pixels).
502 * @param {Element} $target - The target of the interaction (for example, left
503 * handler DOM element in a segment).
504 */
505 edit($items, dx, dy, $target) {
506 if (!this._behavior) { return; }
507 $items = !Array.isArray($items) ? [$items] : $items;
508
509 for (let $item of $items) {
510 const shape = this._$itemShapeMap.get($item);
511 const datum = this._$itemDataMap.get($item);
512
513 this._behavior.edit(this._renderingContext, shape, datum, dx, dy, $target);
514 this.emit('edit', shape, datum);
515 }
516 }
517
518 /**
519 * Defines if the `Layer`, and thus the `LayerTimeContext` is editable or not.
520 *
521 * @params {Boolean} [bool=true]
522 */
523 setContextEditable(bool = true) {
524 const display = bool ? 'block' : 'none';
525 this.$interactions.style.display = display;
526 this._isContextEditable = bool;
527 }
528
529 /**
530 * Edit the layer and thus its related `LayerTimeContext` attributes.
531 *
532 * @param {Number} dx - The modification to apply in the x axis (in pixels).
533 * @param {Number} dy - The modification to apply in the y axis (in pixels).
534 * @param {Element} $target - The target of the event of the interaction.
535 */
536 editContext(dx, dy, $target) {
537 timeContextBehavior.edit(this, dx, dy, $target);
538 }
539
540 /**
541 * Stretch the layer and thus its related `LayerTimeContext` attributes.
542 *
543 * @param {Number} dx - The modification to apply in the x axis (in pixels).
544 * @param {Number} dy - The modification to apply in the y axis (in pixels).
545 * @param {Element} $target - The target of the event of the interaction.
546 */
547 stretchContext(dx, dy, $target) {
548 timeContextBehavior.stretch(this, dx, dy, $target);
549 }
550
551 // --------------------------------------
552 // Helpers
553 // --------------------------------------
554
555 /**
556 * Returns an item from a DOM element related to the shape, null otherwise.
557 *
558 * @param {Element} $el - the element to be tested
559 * @return {Element|null}
560 */
561 getItemFromDOMElement($el) {
562 let $item;
563
564 do {
565 if ($el.classList && $el.classList.contains('item')) {
566 $item = $el;
567 break;
568 }
569
570 $el = $el.parentNode;
571 } while ($el !== null);
572
573 return this.hasItem($item) ? $item : null;
574 }
575
576 /**
577 * Returns the datum associated to a specific item.
578 *
579 * @param {Element} $item
580 * @return {Object|Array|null}
581 */
582 getDatumFromItem($item) {
583 const datum = this._$itemDataMap.get($item);
584 return datum ? datum : null;
585 }
586
587 /**
588 * Returns the datum associated to a specific item from any DOM element
589 * composing the shape. Basically a shortcut for `getItemFromDOMElement` and
590 * `getDatumFromItem` methods.
591 *
592 * @param {Element} $el
593 * @return {Object|Array|null}
594 */
595 getDatumFromDOMElement($el) {
596 var $item = this.getItemFromDOMElement($el);
597 if ($item === null) { return null; }
598 return this.getDatumFromItem($item);
599 }
600
601 /**
602 * Tests if the given DOM element is an item of the layer.
603 *
604 * @param {Element} $item - The item to be tested.
605 * @return {Boolean}
606 */
607 hasItem($item) {
608 return this._$itemDataMap.has($item);
609 }
610
611 /**
612 * Defines if a given element belongs to the layer. Is more general than
613 * `hasItem`, can mostly used to check interactions elements.
614 *
615 * @param {Element} $el - The DOM element to be tested.
616 * @return {bool}
617 */
618 hasElement($el) {
619 do {
620 if ($el === this.$el) {
621 return true;
622 }
623
624 $el = $el.parentNode;
625 } while ($el !== null);
626
627 return false;
628 }
629
630 /**
631 * Retrieve all the items in a given area as defined in the registered `Shape~inArea` method.
632 *
633 * @param {Object} area - The area in which to find the elements
634 * @param {Number} area.top
635 * @param {Number} area.left
636 * @param {Number} area.width
637 * @param {Number} area.height
638 * @return {Array} - list of the items presents in the area
639 */
640 getItemsInArea(area) {
641 const start = this.timeContext.parent.timeToPixel(this.timeContext.start);
642 const duration = this.timeContext.timeToPixel(this.timeContext.duration);
643 const offset = this.timeContext.timeToPixel(this.timeContext.offset);
644 const top = this.params.top;
645 // be aware af context's translations - constrain in working view
646 let x1 = Math.max(area.left, start);
647 let x2 = Math.min(area.left + area.width, start + duration);
648 x1 -= (start + offset);
649 x2 -= (start + offset);
650 // keep consistent with context y coordinates system
651 let y1 = this.params.height - (area.top + area.height);
652 let y2 = this.params.height - area.top;
653
654 y1 += this.params.top;
655 y2 += this.params.top;
656
657 const $filteredItems = [];
658
659 for (let [$item, datum] of this._$itemDataMap.entries()) {
660 const shape = this._$itemShapeMap.get($item);
661 const inArea = shape.inArea(this._renderingContext, datum, x1, y1, x2, y2);
662
663 if (inArea) { $filteredItems.push($item); }
664 }
665
666 return $filteredItems;
667 }
668
669 // --------------------------------------
670 // Rendering / Display methods
671 // --------------------------------------
672
673 /**
674 * Moves an item to the end of the layer to display it front of its
675 * siblings (svg z-index...).
676 *
677 * @param {Element} $item - The item to be moved.
678 */
679 _toFront($item) {
680 this.$offset.appendChild($item);
681 }
682
683 /**
684 * Create the DOM structure of the shapes according to the given data. Inspired
685 * from the `enter` and `exit` d3.js paradigm, this method should be called
686 * each time a datum is added or removed from the data. While the DOM is
687 * created the `update` method must be called in order to update the shapes
688 * attributes and thus place them where they should.
689 */
690 render() {
691 // render `commonShape` only once
692 if (
693 this._commonShapeConfiguration !== null &&
694 this._$itemCommonShapeMap.size === 0
695 ) {
696 const { ctor, accessors, options } = this._commonShapeConfiguration;
697 const $group = document.createElementNS(ns, 'g');
698 const shape = new ctor(options);
699
700 shape.install(accessors);
701 $group.appendChild(shape.render());
702 $group.classList.add('item', 'common', shape.getClassName());
703
704 this._$itemCommonShapeMap.set($group, shape);
705 this.$offset.appendChild($group);
706 }
707
708 // append elements all at once
709 const fragment = document.createDocumentFragment();
710 const values = this._$itemDataMap.values(); // iterator
711
712 // enter
713 this.data.forEach((datum) => {
714 for (let value of values) { if (value === datum) { return; } }
715
716 const { ctor, accessors, options } = this._shapeConfiguration;
717 const shape = new ctor(options);
718 shape.install(accessors);
719
720 const $el = shape.render(this._renderingContext);
721 $el.classList.add('item', shape.getClassName());
722
723 this._$itemShapeMap.set($el, shape);
724 this._$itemDataMap.set($el, datum);
725
726 fragment.appendChild($el);
727 });
728
729 this.$offset.appendChild(fragment);
730
731 // remove
732 for (let [$item, datum] of this._$itemDataMap.entries()) {
733 if (this.data.indexOf(datum) !== -1) { continue; }
734
735 const shape = this._$itemShapeMap.get($item);
736
737 this.$offset.removeChild($item);
738 shape.destroy();
739 // a removed item cannot be selected
740 if (this._behavior) {
741 this._behavior.unselect($item, datum);
742 }
743
744 this._$itemDataMap.delete($item);
745 this._$itemShapeMap.delete($item);
746 }
747 }
748
749 /**
750 * Updates the container of the layer and the attributes of the existing shapes.
751 */
752 update() {
753 this.updateContainer();
754 this.updateShapes();
755 }
756
757 /**
758 * Updates the container of the layer.
759 */
760 updateContainer() {
761 this._updateRenderingContext();
762
763 const timeContext = this.timeContext;
764 const width = timeContext.timeToPixel(timeContext.duration);
765 // x is relative to timeline's timeContext
766 const x = timeContext.parent.timeToPixel(timeContext.start);
767 const offset = timeContext.timeToPixel(timeContext.offset);
768 const top = this.params.top;
769 const height = this.params.height;
770 // matrix to invert the coordinate system
771 const translateMatrix = `matrix(1, 0, 0, -1, ${x}, ${top + height})`;
772
773 this.$el.setAttributeNS(null, 'transform', translateMatrix);
774
775 this.$boundingBox.setAttributeNS(null, 'width', width);
776 this.$boundingBox.setAttributeNS(null, 'height', height);
777 this.$boundingBox.style.opacity = this.params.opacity;
778
779 this.$offset.setAttributeNS(null, 'transform', `translate(${offset}, 0)`);
780 // maintain context shape
781 this.contextShape.update(this._renderingContext, this.timeContext, 0);
782 }
783
784 /**
785 * Updates the attributes of all the `Shape` instances rendered into the layer.
786 *
787 * @todo - allow to filter which shape(s) should be updated.
788 */
789 updateShapes() {
790 this._updateRenderingContext();
791 // update common shapes
792 this._$itemCommonShapeMap.forEach((shape, $item) => {
793 shape.update(this._renderingContext, this.data);
794 });
795
796 for (let [$item, datum] of this._$itemDataMap.entries()) {
797 const shape = this._$itemShapeMap.get($item);
798 shape.update(this._renderingContext, datum);
799 }
800 }
801 }
802
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/utils/scales.js
1 /**
2 * Lightweight scales mimicing the `d3.js` functionnal API.
3 */
4 export default {
5 /**
6 * A linear scale interpolating values between a `domain` and a `range`.
7 * @return {Function}
8 */
9 linear() {
10 let _domain = [0, 1];
11 let _range = [0, 1];
12
13 let _slope = 1;
14 let _intercept = 0;
15
16 function _updateCoefs() {
17 _slope = (_range[1] - _range[0]) / (_domain[1] - _domain[0]);
18 _intercept = _range[0] - (_slope * _domain[0]);
19 }
20
21 function scale (value) {
22 return (_slope * value) + _intercept;
23 }
24
25 scale.invert = function(value) {
26 return (value - _intercept) / _slope;
27 };
28
29 scale.domain = function(arr = null) {
30 if (arr === null) { return _domain; }
31
32 _domain = arr;
33 _updateCoefs();
34
35 return scale;
36 };
37
38 scale.range = function(arr = null) {
39 if (arr === null) { return _range; }
40
41 _range = arr;
42 _updateCoefs();
43
44 return scale;
45 };
46
47 return scale;
48 }
49 };
50
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/shapes/segment.js
1 import BaseShape from './base-shape';
2
3
4 /**
5 * A shape to display a segment.
6 *
7 * [example usage](./examples/layer-segment.html)
8 */
9 export default class Segment extends BaseShape {
10 getClassName() { return 'segment'; }
11
12 _getAccessorList() {
13 return { x: 0, y: 0, width: 0, height: 1, color: '#000000', opacity: 1 };
14 }
15
16 _getDefaults() {
17 return {
18 displayHandlers: true,
19 handlerWidth: 2,
20 handlerOpacity: 0.8,
21 opacity: 0.6
22 };
23 }
24
25 render(renderingContext) {
26 if (this.$el) { return this.$el; }
27
28 this.$el = document.createElementNS(this.ns, 'g');
29
30 this.$segment = document.createElementNS(this.ns, 'rect');
31 this.$segment.classList.add('segment');
32 this.$segment.style.opacity = this.params.opacity;
33 this.$segment.setAttributeNS(null, 'shape-rendering', 'crispEdges');
34
35 this.$el.appendChild(this.$segment);
36
37 if (this.params.displayHandlers) {
38 this.$leftHandler = document.createElementNS(this.ns, 'rect');
39 this.$leftHandler.classList.add('left', 'handler');
40 this.$leftHandler.setAttributeNS(null, 'width', this.params.handlerWidth);
41 this.$leftHandler.setAttributeNS(null, 'shape-rendering', 'crispEdges');
42 this.$leftHandler.style.opacity = this.params.handlerOpacity;
43 this.$leftHandler.style.cursor = 'ew-resize';
44
45 this.$rightHandler = document.createElementNS(this.ns, 'rect');
46 this.$rightHandler.classList.add('right', 'handler');
47 this.$rightHandler.setAttributeNS(null, 'width', this.params.handlerWidth);
48 this.$rightHandler.setAttributeNS(null, 'shape-rendering', 'crispEdges');
49 this.$rightHandler.style.opacity = this.params.handlerOpacity;
50 this.$rightHandler.style.cursor = 'ew-resize';
51
52 this.$el.appendChild(this.$leftHandler);
53 this.$el.appendChild(this.$rightHandler);
54 }
55
56 return this.$el;
57 }
58
59 update(renderingContext, datum) {
60 const x = renderingContext.timeToPixel(this.x(datum));
61 const y = renderingContext.valueToPixel(this.y(datum));
62
63 const width = renderingContext.timeToPixel(this.width(datum));
64 const height = renderingContext.valueToPixel(this.height(datum));
65 const color = this.color(datum);
66 const opacity = this.opacity(datum);
67
68 this.$el.setAttributeNS(null, 'transform', `translate(${x}, ${y})`);
69 this.$el.style.opacity = opacity;
70
71 this.$segment.setAttributeNS(null, 'width', Math.max(width, 0));
72 this.$segment.setAttributeNS(null, 'height', height);
73 this.$segment.style.fill = color;
74
75 if (this.params.displayHandlers) {
76 // display handlers
77 this.$leftHandler.setAttributeNS(null, 'height', height);
78 this.$leftHandler.setAttributeNS(null, 'transform', 'translate(0, 0)');
79 this.$leftHandler.style.fill = color;
80
81 const rightHandlerTranslate = `translate(${width - this.params.handlerWidth}, 0)`;
82 this.$rightHandler.setAttributeNS(null, 'height', height);
83 this.$rightHandler.setAttributeNS(null, 'transform', rightHandlerTranslate);
84 this.$rightHandler.style.fill = color;
85 }
86 }
87
88 inArea(renderingContext, datum, x1, y1, x2, y2) {
89 const shapeX1 = renderingContext.timeToPixel(this.x(datum));
90 const shapeX2 = renderingContext.timeToPixel(this.x(datum) + this.width(datum));
91 const shapeY1 = renderingContext.valueToPixel(this.y(datum));
92 const shapeY2 = renderingContext.valueToPixel(this.y(datum) + this.height(datum));
93
94 // http://jsfiddle.net/uthyZ/ - check overlaping area
95 const xOverlap = Math.max(0, Math.min(x2, shapeX2) - Math.max(x1, shapeX1));
96 const yOverlap = Math.max(0, Math.min(y2, shapeY2) - Math.max(y1, shapeY1));
97 const area = xOverlap * yOverlap;
98
99 return area > 0;
100 }
101 }
102
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/shapes/base-shape.js
1 import ns from '../core/namespace';
2
3
4 /**
5 * Is an abstract class or interface to be overriden in order to define new
6 * shapes. Shapes define the way a given datum should be rendered, they are
7 * the smallest unit of rendering into a timeline.
8 *
9 * All the life cycle of `Shape` instances is handled into the `Layer` instance
10 * they are attach to. As a consequence, they should be mainly considered as
11 * private objects. The only place they should be interacted with is in `Behavior`
12 * definitions, to test which element of the shape is the target of the
13 * interaction and define the interaction according to that test.
14 *
15 * Depending of its implementation a `Shape` can be used along with `entity` or
16 * `collection` data type. Some shapes are then created to use data considered
17 * as a single entity (Waveform, TracePath, Line), while others are defined to
18 * be used with data seen as a collection, each shape rendering a single entry
19 * of the collection. The shapes working with entity type data should therefore
20 * be used in an `entity` configured `Layer`. Note that if they are registered
21 * as "commonShape" in a `collection` type `Layer`, they will behave the exact
22 * same way. These kind of shapes are noted: "entity shape".
23 *
24 * ### Available `collection` shapes:
25 * - Marker / Annotated Marker
26 * - Segment / Annotated Segment
27 * - Dot
28 * - TraceDots
29 *
30 * ### Available `entity` shapes:
31 * - Line
32 * - Tick (for axis)
33 * - Waveform
34 * - TracePath
35 */
36 export default class BaseShape {
37 /**
38 * @param {Object} options - override default configuration
39 */
40 constructor(options = {}) {
41 /** @type {Element} - Svg element to be returned by the `render` method. */
42 this.$el = null;
43 /** @type {String} - Svg namespace. */
44 this.ns = ns;
45 /** @type {Object} - Object containing the global parameters of the shape */
46 this.params = Object.assign({}, this._getDefaults(), options);
47 // create accessors methods and set default accessor functions
48 const accessors = this._getAccessorList();
49 this._createAccessors(accessors);
50 this._setDefaultAccessors(accessors);
51 }
52
53 /**
54 * Destroy the shape and clean references. Interface method called from the `layer`.
55 */
56 destroy() {
57 // this.group = null;
58 this.$el = null;
59 }
60
61 /**
62 * Interface method to override when extending this base class. The method
63 * is called by the `Layer~render` method. Returns the name of the shape,
64 * used as a class in the element group (defaults to `'shape'`).
65 *
66 * @return {String}
67 */
68 getClassName() { return 'shape'; }
69
70 /**
71 * @todo not implemented
72 * allow to install defs in the track svg element. Should be called when
73 * adding the `Layer` to the `Track`.
74 */
75 // setSvgDefinition(defs) {}
76
77 /**
78 * Returns the defaults for global configuration of the shape.
79 * @protected
80 * @return {Object}
81 */
82 _getDefaults() {
83 return {};
84 }
85
86 /**
87 * Returns an object where keys are the accessors methods names to create
88 * and values are the default values for each given accessor.
89 *
90 * @protected
91 * @todo rename ?
92 * @return {Object}
93 */
94 _getAccessorList() { return {}; }
95
96
97 /**
98 * Interface method called by Layer when creating a shape. Install the
99 * given accessors on the shape, overriding the default accessors.
100 *
101 * @param {Object<String, function>} accessors
102 */
103 install(accessors) {
104 for (let key in accessors) { this[key] = accessors[key]; }
105 }
106
107 /**
108 * Generic method to create accessors. Adds getters en setters to the
109 * prototype if not already present.
110 */
111 _createAccessors(accessors) {
112 this._accessors = {};
113 // add it to the prototype
114 const proto = Object.getPrototypeOf(this);
115 // create a getter / setter for each accessors
116 // setter : `this.x = callback`
117 // getter : `this.x(datum)`
118 Object.keys(accessors).forEach((name) => {
119 if (proto.hasOwnProperty(name)) { return; }
120
121 Object.defineProperty(proto, name, {
122 get: function() { return this._accessors[name]; },
123 set: function(func) {
124 this._accessors[name] = func;
125 }
126 });
127 });
128 }
129
130 /**
131 * Create a function to be used as a default accessor for each accesors
132 */
133 _setDefaultAccessors(accessors) {
134 Object.keys(accessors).forEach((name) => {
135 const defaultValue = accessors[name];
136 let accessor = function(d, v = null) {
137 if (v === null) { return d[name] || defaultValue; }
138 d[name] = v;
139 };
140 // set accessor as the default one
141 this[name] = accessor;
142 });
143 }
144
145 /**
146 * Interface method called by `Layer~render`. Creates the DOM structure of
147 * the shape.
148 *
149 * @param {Object} renderingContext - the renderingContext of the layer
150 * which owns this shape.
151 * @return {Element} - the DOM element to insert in the item's group.
152 */
153 render(renderingContext) {}
154
155 /**
156 * Interface method called by `Layer~update`. Updates the DOM structure of the shape.
157 *
158 * @param {Object} renderingContext - The `renderingContext` of the layer
159 * which owns this shape.
160 * @param {Object|Array} datum - The datum associated to the shape.
161 */
162 update(renderingContext, datum) {}
163
164 /**
165 * Interface method to override called by `Layer~getItemsInArea`. Defines if
166 * the shape is considered to be the given area. Arguments are passed in pixel domain.
167 *
168 * @param {Object} renderingContext - the renderingContext of the layer which
169 * owns this shape.
170 * @param {Object|Array} datum - The datum associated to the shape.
171 * @param {Number} x1 - The x component of the top-left corner of the area to test.
172 * @param {Number} y1 - The y component of the top-left corner of the area to test.
173 * @param {Number} x2 - The x component of the bottom-right corner of the area to test.
174 * @param {Number} y2 - The y component of the bottom-right corner of the area to test.
175 * @return {Boolean} - Returns `true` if the is considered to be in the given area, `false` otherwise.
176 */
177 inArea(renderingContext, datum, x1, y1, x2, y2) {}
178 }
179
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/shapes/dot.js
1 import BaseShape from './base-shape';
2
3
4 /**
5 * A shape to display a dot.
6 *
7 * [example usage](./examples/layer-breakpoint.html)
8 */
9 export default class Dot extends BaseShape {
10 getClassName() { return 'dot'; }
11
12 // @TODO rename : confusion between accessors and meta-accessors
13 _getAccessorList() {
14 return { cx: 0, cy: 0, r: 3, color: '#000000' };
15 }
16
17 render() {
18 if (this.$el) { return this.$el; }
19
20 this.$el = document.createElementNS(this.ns, 'circle');
21
22 return this.$el;
23 }
24
25 update(renderingContext, datum) {
26 const cx = renderingContext.timeToPixel(this.cx(datum));
27 const cy = renderingContext.valueToPixel(this.cy(datum));
28 const r = this.r(datum);
29 const color = this.color(datum);
30
31 this.$el.setAttributeNS(null, 'transform', `translate(${cx}, ${cy})`);
32 this.$el.setAttributeNS(null, 'r', r);
33 this.$el.style.fill = color;
34 }
35
36 // x1, x2, y1, y2 => in pixel domain
37 inArea(renderingContext, datum, x1, y1, x2, y2) {
38 const cx = renderingContext.timeToPixel(this.cx(datum));
39 const cy = renderingContext.valueToPixel(this.cy(datum));
40
41 if ((cx > x1 && cx < x2) && (cy > y1 && cy < y2)) {
42 return true;
43 }
44
45 return false;
46 }
47 }
48
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/core/layer-time-context.js
1 import scales from '../utils/scales';
2
3
4 /**
5 * A `LayerTimeContext` instance represents a time segment into a `TimelineTimeContext`.
6 * It must be attached to a `TimelineTimeContext` (the one of the timeline it
7 * belongs to). It relies on its parent's `timeToPixel` (time to pixel transfert
8 * function) to create the time to pixel representation of the Layer (the view) it is attached to.
9 *
10 * The `layerTimeContext` has four important attributes:
11 * - `start` represent the time at which temporal data must be represented
12 * in the timeline (for instance the begining of a soundfile in a DAW).
13 * - `offset` represents offset time of the data in the context of a Layer.
14 * (@TODO give a use case example here "crop ?", and/or explain that it's not a common use case).
15 * - `duration` is the duration of the view on the data.
16 * - `stretchRatio` is the stretch applyed to the temporal data contained in
17 * the view (this value can be seen as a local zoom on the data, or as a stretch
18 * on the time components of the data). When applyed, the stretch ratio maintain
19 * the start position of the view in the timeline.
20 *
21 * ```
22 * + timeline -----------------------------------------------------------------
23 * 0 5 10 15 20 25 30 seconds
24 * +---+*****************+------------------------------------------+*******+--
25 * |*** soundfile ***|Layer (view on the sound file) |*******|
26 * +*****************+------------------------------------------+*******+
27 *
28 * <---- offset ----><--------------- duration ----------------->
29 * <-------- start ----->
30 *
31 * The parts of the sound file represented with '*' are hidden from the view
32 * ```
33 *
34 * [example usage](./examples/time-contexts.html)
35 */
36 export default class LayerTimeContext {
37 /**
38 * @param {TimelineTimeContext} parent - The `TimelineTimeContext` instance of the timeline.
39 */
40 constructor(parent) {
41 if (!parent) { throw new Error('LayerTimeContext must have a parent'); }
42
43 /**
44 * The `TimelineTimeContext` instance of the timeline.
45 *
46 * @type {TimelineTimeContext}
47 */
48 this.parent = parent;
49
50 this._timeToPixel = null;
51 this._start = 0;
52 this._duration = parent.visibleDuration;
53 this._offset = 0;
54 this._stretchRatio = 1;
55 // register into the timeline's TimeContext
56 this.parent._children.push(this);
57 }
58
59 /**
60 * Creates a clone of the current time context.
61 *
62 * @return {LayerTimeContext}
63 */
64 clone() {
65 const ctx = new this();
66
67 ctx.parent = this.parent;
68 ctx.start = this.start;
69 ctx.duration = this.duration;
70 ctx.offset = this.offset;
71 ctx.stretchRatio = this.stretchRatio; // creates the local scale if needed
72
73 return ctx;
74 }
75
76 /**
77 * Returns the start position of the time context (in seconds).
78 *
79 * @type {Number}
80 */
81 get start() {
82 return this._start;
83 }
84
85 /**
86 * Sets the start position of the time context (in seconds).
87 *
88 * @type {Number}
89 */
90 set start(value) {
91 this._start = value;
92 }
93
94 /**
95 * Returns the duration of the time context (in seconds).
96 *
97 * @type {Number}
98 */
99 get duration() {
100 return this._duration;
101 }
102
103 /**
104 * Sets the duration of the time context (in seconds).
105 *
106 * @type {Number}
107 */
108 set duration(value) {
109 this._duration = value;
110 }
111
112 /**
113 * Returns the offset of the time context (in seconds).
114 *
115 * @type {Number}
116 */
117 get offset() {
118 return this._offset;
119 }
120
121 /**
122 * Sets the offset of the time context (in seconds).
123 *
124 * @type {Number}
125 */
126 set offset(value) {
127 this._offset = value;
128 }
129
130 /**
131 * Returns the stretch ratio of the time context.
132 *
133 * @type {Number}
134 */
135 get stretchRatio() {
136 return this._stretchRatio;
137 }
138
139 /**
140 * Sets the stretch ratio of the time context.
141 *
142 * @type {Number}
143 */
144 set stretchRatio(value) {
145 // remove local scale if ratio = 1
146 if (value === 1) {
147 this._timeToPixel = null;
148 return;
149 }
150 // reuse previsously created local scale if exists
151 const timeToPixel = this._timeToPixel ?
152 this._timeToPixel : scales.linear().domain([0, 1]);
153
154 timeToPixel.range([0, this.parent.computedPixelsPerSecond * value]);
155
156 this._timeToPixel = timeToPixel;
157 this._stretchRatio = value;
158 }
159
160 /**
161 * Returns the time to pixel transfert function of the time context. If
162 * the `stretchRatio` attribute is equal to 1, this function is the global
163 * one from the `TimelineTimeContext` instance.
164 *
165 * @type {Function}
166 */
167 get timeToPixel() {
168 if (!this._timeToPixel) {
169 return this.parent.timeToPixel;
170 }
171
172 return this._timeToPixel;
173 }
174
175 /**
176 * Helper function to convert pixel to time.
177 *
178 * @param {Number} px
179 * @return {Number}
180 */
181 pixelToTime(px) {
182 if (!this._timeToPixel) {
183 return this.parent.timeToPixel.invert(px);
184 }
185
186 return this._timeToPixel.invert(px);
187 }
188 }
189
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/behaviors/base-behavior.js
1 /**
2 * Is an abstract class or interface to be overriden in order to define the way
3 * a given shape should behave when selected or edited by a user. Instances of
4 * `BaseBehavior` are internally used by `Layer` instances to modify the data
5 * according to a user interaction and a given shape. A single instance of
6 * `Behavior` is created in one given shape.
7 *
8 * By default, the only method to override to define a new behavior for a
9 * shape is the `edit` method. However, if needed in special cases, all the
10 * selection handling can be overriden too.
11 *
12 * The flow is the following:
13 * `Event` - (forwarded to) -> `Layer` - (command) -> `Behavior` - (modify) -> `data` - (upates) -> `Shape`
14 *
15 * The behavior responsability is then to modify the data according to the
16 * user interactions, while shapes are always a view of the current state of the
17 * data.
18 */
19 export default class BaseBehavior {
20 constructor() {
21 this._selectedItems = new Set(); // no duplicate in Set
22 this._selectedClass = null;
23 this._layer = null;
24 }
25
26 initialize(layer) {
27 this._layer = layer;
28 this._selectedClass = layer.params.selectedClassName;
29 }
30
31 /**
32 * Destroy the references to the selected items.
33 *
34 * @type {String}
35 * @todo - rename to `clearSelection` (removing the class) ?
36 */
37 destroy() {
38 this._selectedItems.clear();
39 }
40
41 /**
42 * The class to add to the shapes when selected.
43 *
44 * @type {String}
45 */
46 set selectedClass(value) {
47 this._selectedClass = value;
48 }
49
50 /**
51 * The class to add to the shapes when selected.
52 *
53 * @type {String}
54 */
55 get selectedClass() {
56 return this._selectedClass;
57 }
58
59 /**
60 * An array containing all the selected items of the layer.
61 *
62 * @type {Array}
63 */
64 get selectedItems() {
65 return [...this._selectedItems];
66 }
67
68 /**
69 * @param {Element} $item - The item to select.
70 * @param {Object} datum - Not used in this implementation. Could be
71 * used to mark the data as selected.
72 * @todo - Pass the shape object to get the accessors ?
73 */
74 select($item, datum) {
75 $item.classList.add(this.selectedClass);
76 this._selectedItems.add($item);
77 }
78
79 /**
80 * @param {Element} $item - The item to unselect.
81 * @param {Object} datum - Not used in this implementation. Could be
82 * used to mark the data as selected.
83 * @todo - Pass the shape object to get the accessors ?
84 */
85 unselect($item, datum) {
86 $item.classList.remove(this.selectedClass);
87 this._selectedItems.delete($item);
88 }
89
90 /**
91 * @param {Element} $item - The item to toggle selection.
92 * @param {Object} datum - Not used in this implementation. Could be
93 * used to mark the data as selected.
94 * @todo - Pass the shape object to get the accessors ?
95 */
96 toggleSelection($item, datum) {
97 const method = this._selectedItems.has($item) ? 'unselect' : 'select';
98 this[method]($item);
99 }
100
101 /**
102 * Interface method to override in order to define its particular behavior when
103 * interacted with.
104 *
105 * @param {Object} renderingContext - The layer rendering context.
106 * @param {BaseShape} shape - The shape object to be edited.
107 * @param {Object|Array} datum - The related datum to modify.
108 * @param {Number} dx - The value of the interaction in the x axis (in pixels).
109 * @param {Number} dy - The value of the interaction in the y axis (in pixels).
110 * @param {Element} $target - The target DOM element of the interaction.
111 */
112 edit(renderingContext, shape, datum, dx, dy, $target) {
113 // must be implemented in children
114 }
115 }
116
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/core/timeline.js
1 import events from 'events';
2
3 import Keyboard from '../interactions/keyboard';
4 import LayerTimeContext from './layer-time-context';
5 import Surface from '../interactions/surface';
6 import TimelineTimeContext from './timeline-time-context';
7 import Track from './track';
8 import TrackCollection from './track-collection';
9
10
11 /**
12 * Is the main entry point to create a temporal visualization.
13 *
14 * A `timeline` instance mainly provides the context for any visualization of
15 * temporal data and maintains the hierarchy of `Track`, `Layer` and `Shape`
16 * over the entiere visualisation.
17 *
18 * Its main responsabilites are:
19 * - maintaining the temporal consistency accross the visualisation through
20 * its `timeContext` property (instance of `TimelineTimeContext`).
21 * - handling interactions to its current state (acting here as a simple
22 * state machine).
23 *
24 * @TODO insert figure
25 *
26 * It also contains a reference to all the register track allowing to `render`
27 * or `update` all the layer from a single entry point.
28 *
29 * ## Example Usage
30 *
31 * ```js
32 * const visibleWidth = 500; // default width in pixels for all created `Track`
33 * const duration = 10; // the visible area represents 10 seconds
34 * const pixelsPerSeconds = visibleWidth / duration;
35 * const timeline = new ui.core.Timeline(pixelsPerSecond, width);
36 * ```
37 */
38 export default class Timeline extends events.EventEmitter {
39 /**
40 * @param {Number} [pixelsPerSecond=100] - the default scaling between time and pixels.
41 * @param {Number} [visibleWidth=1000] - the default visible area for all registered tracks.
42 */
43 constructor(pixelsPerSecond = 100, visibleWidth = 1000, {
44 registerKeyboard = true
45 } = {}) {
46
47 super();
48
49 this._tracks = new TrackCollection(this);
50 this._state = null;
51
52 // default interactions
53 this._surfaceCtor = Surface;
54
55 if (registerKeyboard) {
56 this.createInteraction(Keyboard, document);
57 }
58
59 // stores
60 this._trackById = {};
61 this._groupedLayers = {};
62
63 /** @type {TimelineTimeContext} - master time context for the visualization. */
64 this.timeContext = new TimelineTimeContext(pixelsPerSecond, visibleWidth);
65 }
66
67 /**
68 * Returns `TimelineTimeContext`'s `offset` time domain value.
69 *
70 * @type {Number} [offset=0]
71 */
72 get offset() {
73 return this.timeContext.offset;
74 }
75
76 /**
77 * Updates `TimelineTimeContext`'s `offset` time domain value.
78 *
79 * @type {Number} [offset=0]
80 */
81 set offset(value) {
82 this.timeContext.offset = value;
83 }
84
85 /**
86 * Returns the `TimelineTimeContext`'s `zoom` value.
87 *
88 * @type {Number} [offset=0]
89 */
90 get zoom() {
91 return this.timeContext.zoom;
92 }
93
94 /**
95 * Updates the `TimelineTimeContext`'s `zoom` value.
96 *
97 * @type {Number} [offset=0]
98 */
99 set zoom(value) {
100 this.timeContext.zoom = value;
101 }
102
103 /**
104 * Returns the `TimelineTimeContext`'s `pixelsPerSecond` ratio.
105 *
106 * @type {Number} [offset=0]
107 */
108 get pixelsPerSecond() {
109 return this.timeContext.pixelsPerSecond;
110 }
111
112 /**
113 * Updates the `TimelineTimeContext`'s `pixelsPerSecond` ratio.
114 *
115 * @type {Number} [offset=0]
116 */
117 set pixelsPerSecond(value) {
118 this.timeContext.pixelsPerSecond = value;
119 }
120
121 /**
122 * Returns the `TimelineTimeContext`'s `visibleWidth` pixel domain value.
123 *
124 * @type {Number} [offset=0]
125 */
126 get visibleWidth() {
127 return this.timeContext.visibleWidth;
128 }
129
130 /**
131 * Updates the `TimelineTimeContext`'s `visibleWidth` pixel domain value.
132 *
133 * @type {Number} [offset=0]
134 */
135 set visibleWidth(value) {
136 this.timeContext.visibleWidth = value;
137 }
138
139 /**
140 * Returns `TimelineTimeContext`'s `timeToPixel` transfert function.
141 *
142 * @type {Function}
143 */
144 get timeToPixel() {
145 return this.timeContext.timeToPixel;
146 }
147
148 /**
149 * Returns `TimelineTimeContext`'s `visibleDuration` helper value.
150 *
151 * @type {Number}
152 */
153 get visibleDuration() {
154 return this.timeContext.visibleDuration;
155 }
156
157 /**
158 * Updates the `TimelineTimeContext`'s `maintainVisibleDuration` value.
159 * Defines if the duration of the visible area should be maintain when
160 * the `visibleWidth` attribute is updated.
161 *
162 * @type {Boolean}
163 */
164 set maintainVisibleDuration(bool) {
165 this.timeContext.maintainVisibleDuration = bool;
166 }
167
168 /**
169 * Returns `TimelineTimeContext`'s `maintainVisibleDuration` current value.
170 *
171 * @type {Boolean}
172 */
173 get maintainVisibleDuration() {
174 return this.timeContext.maintainVisibleDuration;
175 }
176
177 /**
178 * Object maintaining arrays of `Layer` instances ordered by their `groupId`.
179 * Is used internally by the `TrackCollection` instance.
180 *
181 * @type {Object}
182 */
183 get groupedLayers() {
184 return this._groupedLayers;
185 }
186
187 /**
188 * Overrides the default `Surface` that is instanciated on each `Track`
189 * instance. This methos should be called before adding any `Track` instance
190 * to the current `timeline`.
191 *
192 * @param {EventSource} ctor - The constructor to use in order to catch mouse
193 * events on each `Track` instances.
194 */
195 configureSurface(ctor) {
196 this._surfaceCtor = ctor;
197 }
198
199 /**
200 * Factory method to add interaction modules the timeline should listen to.
201 * By default, the timeline instanciate a global `Keyboard` instance and a
202 * `Surface` instance on each container.
203 * Should be used to install new interactions implementing the `EventSource` interface.
204 *
205 * @param {EventSource} ctor - The contructor of the interaction module to instanciate.
206 * @param {Element} $el - The DOM element which will be binded to the `EventSource` module.
207 * @param {Object} [options={}] - Options to be applied to the `ctor`.
208 */
209 createInteraction(ctor, $el, options = {}) {
210 const interaction = new ctor($el, options);
211 interaction.on('event', (e) => this._handleEvent(e));
212 }
213
214 /**
215 * Returns a list of the layers situated under the position of a `WaveEvent`.
216 *
217 * @param {WavesEvent} e - An event triggered by a `WaveEvent`
218 * @return {Array} - Matched layers
219 */
220 getHitLayers(e) {
221 const clientX = e.originalEvent.clientX;
222 const clientY = e.originalEvent.clientY;
223 let layers = [];
224
225 this.layers.forEach((layer) => {
226 if (!layer.params.hittable) { return; }
227 const br = layer.$el.getBoundingClientRect();
228
229 if (
230 clientX > br.left && clientX < br.right &&
231 clientY > br.top && clientY < br.bottom
232 ) {
233 layers.push(layer);
234 }
235 });
236
237 return layers;
238 }
239
240 /**
241 * The callback that is used to listen to interactions modules.
242 *
243 * @param {WaveEvent} e - An event generated by an interaction modules (`EventSource`).
244 */
245 _handleEvent(e) {
246 const hitLayers = (e.source === 'surface') ?
247 this.getHitLayers(e) : null;
248 // emit event as a middleware
249 this.emit('event', e, hitLayers);
250 // propagate to the state
251 if (!this._state) { return; }
252 this._state.handleEvent(e, hitLayers);
253 }
254
255 /**
256 * Updates the state of the timeline.
257 *
258 * @type {BaseState}
259 */
260 set state(state) {
261 if (this._state) { this._state.exit(); }
262 this._state = state;
263 if (this._state) { this._state.enter(); }
264 }
265
266 /**
267 * Returns the current state of the timeline.
268 *
269 * @type {BaseState}
270 */
271 get state() {
272 return this._state;
273 }
274
275 /**
276 * Returns the `TrackCollection` instance.
277 *
278 * @type {TrackCollection}
279 */
280 get tracks() {
281 return this._tracks;
282 }
283
284 /**
285 * Returns the list of all registered layers.
286 *
287 * @type {Array}
288 */
289 get layers() {
290 return this._tracks.layers;
291 }
292
293 /**
294 * Adds a new track to the timeline.
295 *
296 * @param {Track} track - The new track to be registered in the timeline.
297 * @param {String} [trackId=null] - Optionnal unique id to associate with
298 * the track, this id only exists in timeline's context and should be used
299 * in conjonction with `addLayer` method.
300 */
301 add(track, trackId = null) {
302 if (this.tracks.indexOf(track) !== -1) {
303 throw new Error('track already added to the timeline');
304 }
305
306 this._registerTrackId(track, trackId);
307 track.configure(this.timeContext);
308
309 this.tracks.push(track);
310 this.createInteraction(this._surfaceCtor, track.$el);
311 }
312
313 /**
314 * Removes a track from the timeline.
315 *
316 * @param {Track} track - the track to remove from the timeline.
317 * @todo not implemented.
318 */
319 remove(track) {
320 // should destroy interaction too, avoid ghost eventListeners
321 }
322
323 /**
324 * Helper to create a new `Track` instance. The `track` is added,
325 * rendered and updated before being returned.
326 *
327 * @param {Element} $el - The DOM element where the track should be inserted.
328 * @param {Number} trackHeight - The height of the newly created track.
329 * @param {String} [trackId=null] - Optionnal unique id to associate with
330 * the track, this id only exists in timeline's context and should be used in
331 * conjonction with `addLayer` method.
332 * @return {Track}
333 */
334 createTrack($el, trackHeight = 100, trackId = null) {
335 const track = new Track($el, trackHeight);
336 // Add track to the timeline
337 this.add(track, trackId);
338 track.render();
339 track.update();
340
341 return track;
342 }
343
344 /**
345 * If track id is defined, associate a track with a unique id.
346 */
347 _registerTrackId(track, trackId) {
348 if (trackId !== null) {
349 if (this._trackById[trackId] !== undefined) {
350 throw new Error(`trackId: "${trackId}" is already used`);
351 }
352
353 this._trackById[trackId] = track;
354 }
355 }
356
357 /**
358 * Helper to add a `Layer` instance into a given `Track`. Is designed to be
359 * used in conjonction with the `Timeline~getLayersByGroup` method. The
360 * layer is internally rendered and updated.
361 *
362 * @param {Layer} layer - The `Layer` instance to add into the visualization.
363 * @param {(Track|String)} trackOrTrackId - The `Track` instance (or its `id`
364 * as defined in the `createTrack` method) where the `Layer` instance should be inserted.
365 * @param {String} [groupId='default'] - An optionnal group id in which the
366 * `Layer` should be inserted.
367 * @param {Boolean} [isAxis] - Set to `true` if the added `layer` is an
368 * instance of `AxisLayer` (these layers shares the `TimlineTimeContext` instance
369 * of the timeline).
370 */
371 addLayer(layer, trackOrTrackId, groupId = 'default', isAxis = false) {
372 let track = trackOrTrackId;
373
374 if (typeof trackOrTrackId === 'string') {
375 track = this.getTrackById(trackOrTrackId);
376 }
377
378 // creates the `LayerTimeContext` if not present
379 if (!layer.timeContext) {
380 const timeContext = isAxis ?
381 this.timeContext : new LayerTimeContext(this.timeContext);
382
383 layer.setTimeContext(timeContext);
384 }
385
386 // we should have a Track instance at this point
387 track.add(layer);
388
389 if (!this._groupedLayers[groupId]) {
390 this._groupedLayers[groupId] = [];
391 }
392
393 this._groupedLayers[groupId].push(layer);
394
395 layer.render();
396 layer.update();
397 }
398
399 /**
400 * Removes a layer from its track. The layer is detatched from the DOM but
401 * can still be reused later.
402 *
403 * @param {Layer} layer - The layer to remove.
404 */
405 removeLayer(layer) {
406 this.tracks.forEach(function(track) {
407 const index = track.layers.indexOf(layer);
408 if (index !== -1) { track.remove(layer); }
409 });
410
411 // clean references in helpers
412 for (let groupId in this._groupedLayers) {
413 const group = this._groupedLayers[groupId];
414 const index = group.indexOf(layer);
415
416 if (index !== -1) { group.splice(layer, 1); }
417
418 if (!group.length) {
419 delete this._groupedLayers[groupId];
420 }
421 }
422 }
423
424 /**
425 * Returns a `Track` instance from it's given id.
426 *
427 * @param {String} trackId
428 * @return {Track}
429 */
430 getTrackById(trackId) {
431 return this._trackById[trackId];
432 }
433
434 /**
435 * Returns the track containing a given DOM Element, returns null if no match found.
436 *
437 * @param {Element} $el - The DOM Element to be tested.
438 * @return {Track}
439 */
440 getTrackFromDOMElement($el) {
441 let $svg = null;
442 let track = null;
443 // find the closest `.track` element
444 do {
445 if ($el.classList.contains('track')) {
446 $svg = $el;
447 }
448 $el = $el.parentNode;
449 } while ($svg === null);
450 // find the related `Track`
451 this.tracks.forEach(function(_track) {
452 if (_track.$svg === $svg) { track = _track; }
453 });
454
455 return track;
456 }
457
458 /**
459 * Returns an array of layers from their given group id.
460 *
461 * @param {String} groupId - The id of the group as defined in `addLayer`.
462 * @return {(Array|undefined)}
463 */
464 getLayersByGroup(groupId) {
465 return this._groupedLayers[groupId];
466 }
467
468 /**
469 * Iterates through the added tracks.
470 */
471 *[Symbol.iterator]() {
472 yield* this.tracks[Symbol.iterator]();
473 }
474 }
475
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/interactions/surface.js
1 import EventSource from './event-source';
2 import WaveEvent from './wave-event';
3
4
5 /**
6 * Normalizes mouse user interactions with the timeline upon the DOM
7 * container element of `Track` instances. As soon as a `track` is added to a
8 * `timeline`, its attached `Surface` instance will emit the mouse events.
9 */
10 export default class Surface extends EventSource {
11 /**
12 * @param {DOMElement} el - The DOM element to listen.
13 * @todo - Add some padding to the surface.
14 */
15 constructor($el) {
16 super($el);
17
18 /**
19 * The name of the event source.
20 * @type {String}
21 */
22 this.sourceName = 'surface';
23 this._mouseDownEvent = null;
24 this._lastEvent = null;
25 }
26
27 /**
28 * Factory method for `Event` class
29 */
30 _createEvent(type, e) {
31 const event = new WaveEvent(this.sourceName, type, e);
32
33 const pos = this._getRelativePosition(e);
34 event.x = pos.x;
35 event.y = pos.y;
36
37 return event;
38 }
39
40 /**
41 * Returns the x, y coordinates coordinates relative to the surface element.
42 *
43 * @param {Event} e - Raw event from listener.
44 * @return {Object}
45 * @todo - handle padding.
46 */
47 _getRelativePosition(e) {
48 // @TODO: should be able to ignore padding
49 let x = 0;
50 let y = 0;
51 const clientRect = this.$el.getBoundingClientRect();
52 const scrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft;
53 const scrollTop = document.body.scrollTop + document.documentElement.scrollTop;
54
55 // Adapted from http://www.quirksmode.org/js/events_properties.html#position
56 if (e.pageX || e.pageY) {
57 x = e.pageX;
58 y = e.pageY;
59 } else if (e.clientX || e.clientY) {
60 // Normalize to pageX, pageY
61 x = e.clientX + scrollLeft;
62 y = e.clientY + scrollTop;
63 }
64
65 // clientRect refers to the client, not to the page
66 x = x - (clientRect.left + scrollLeft);
67 y = y - (clientRect.top + scrollTop );
68
69 return { x, y };
70 }
71
72 _defineArea(e, mouseDownEvent, lastEvent) {
73 if (!mouseDownEvent || !lastEvent) { return; }
74 e.dx = e.x - lastEvent.x;
75 e.dy = e.y - lastEvent.y;
76
77 const left = mouseDownEvent.x < e.x ? mouseDownEvent.x : e.x;
78 const top = mouseDownEvent.y < e.y ? mouseDownEvent.y : e.y;
79 const width = Math.abs(Math.round(e.x - mouseDownEvent.x));
80 const height = Math.abs(Math.round(e.y - mouseDownEvent.y));
81
82 e.area = { left, top, width, height };
83 }
84
85 /**
86 * Keep this private to avoid double event binding. Main logic of the surface
87 * is here. Should be extended with needed events (mouseenter, mouseleave,
88 * wheel ...).
89 */
90 _bindEvents() {
91 const onMouseDown = (e) => {
92 // By removing the previous selection we prevent bypassing the mousemove events coming from SVG in Firefox.
93 window.getSelection().removeAllRanges();
94 const event = this._createEvent('mousedown', e);
95
96
97 this._mouseDownEvent = event;
98 this._lastEvent = event;
99 // Register mousemove and mouseup listeners on window
100 window.addEventListener('mousemove', onMouseMove, false);
101 window.addEventListener('mouseup', onMouseUp, false);
102
103 this.emit('event', event);
104 };
105
106 const onMouseMove = (e) => {
107 let event = this._createEvent('mousemove', e);
108 this._defineArea(event, this._mouseDownEvent, this._lastEvent);
109 // Update `lastEvent` for next call
110 this._lastEvent = event;
111
112 this.emit('event', event);
113 };
114
115 const onMouseUp = (e) => {
116 let event = this._createEvent('mouseup', e);
117 this._defineArea(event, this._mouseDownEvent, this._lastEvent);
118
119
120 this._mouseDownEvent = null;
121 this._lastEvent = null;
122 // Remove mousemove and mouseup listeners on window
123 window.removeEventListener('mousemove', onMouseMove);
124 window.removeEventListener('mouseup', onMouseUp);
125
126 this.emit('event', event);
127 };
128
129 const onClick = (e) => {
130 let event = this._createEvent('click', e);
131 this.emit('event', event);
132 };
133
134 const onDblClick = (e) => {
135 let event = this._createEvent('dblclick', e);
136 this.emit('event', event);
137 };
138
139 const onMouseOver = (e) => {
140 let event = this._createEvent('mouseover', e);
141 this.emit('event', event);
142 };
143
144 const onMouseOut = (e) => {
145 let event = this._createEvent('mouseout', e);
146 this.emit('event', event);
147 };
148
149 // Bind callbacks
150 this.$el.addEventListener('mousedown', onMouseDown, false);
151 this.$el.addEventListener('click', onClick, false);
152 this.$el.addEventListener('dblclick', onDblClick, false);
153 this.$el.addEventListener('mouseover', onMouseOver, false);
154 this.$el.addEventListener('mouseout', onMouseOut, false);
155 }
156 }
157
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/core/timeline-time-context.js
1 import scales from '../utils/scales';
2
3
4 /**
5 * Defines and maintains global aspects of the visualization concerning the
6 * relations between time and pixels.
7 *
8 * The `TimelineTimeContext` instance (unique across a visualization) keeps the
9 * main reference on how many pixels should be used to represent one second
10 * though its `timeToPixel` method. The attributes `zoom`, `offset` (i.e. from
11 * origin) and `visibleWidth` allow for navigating in time and for maintaining
12 * view consistency upon the DOM structure (`<svg>` and `<g>` tags) created by
13 * the registered tracks.
14 *
15 * It also maintain an array of all references to `LayerTimeContext` instances
16 * to propagate to `layers`, changes made on the time to pixel representation.
17 *
18 * [example usage](./examples/time-contexts.html)
19 */
20 export default class TimelineTimeContext {
21 /**
22 * @param {Number} pixelsPerSecond - The number of pixels that should be
23 * used to display one second.
24 * @param {Number} visibleWidth - The default with of the visible area
25 * displayed in `tracks` (in pixels).
26 */
27 constructor(pixelsPerSecond, visibleWidth) {
28 this._children = [];
29
30 this._timeToPixel = null;
31 this._offset = 0;
32 this._zoom = 1;
33 this._computedPixelsPerSecond = pixelsPerSecond;
34 // params
35 this._visibleWidth = visibleWidth;
36 this._maintainVisibleDuration = false;
37
38 // create the timeToPixel scale
39 const scale = scales.linear()
40 .domain([0, 1])
41 .range([0, pixelsPerSecond]);
42
43 this._timeToPixel = scale;
44
45 this._originalPixelsPerSecond = this._computedPixelsPerSecond;
46 }
47
48 /**
49 * Returns the number of pixels per seconds ignoring the current zoom value.
50 *
51 * @type {Number}
52 */
53 get pixelsPerSecond() {
54 return this._originalPixelsPerSecond;
55 }
56
57 /**
58 * Updates all the caracteristics of this object according to the new
59 * given value of pixels per seconds. Propagates the changes to the
60 * `LayerTimeContext` children.
61 *
62 * @type {Number}
63 */
64 set pixelsPerSecond(value) {
65 this._computedPixelsPerSecond = value * this.zoom;
66 this._originalPixelsPerSecond = value;
67 this._updateTimeToPixelRange();
68
69 // force children scale update
70 this._children.forEach(function(child) {
71 if (child.stretchRatio === 1) { return; }
72 child.stretchRatio = child.stretchRatio;
73 });
74 }
75
76 /**
77 * Returns the number of pixels per seconds including the current zoom value.
78 *
79 * @type {Number}
80 */
81 get computedPixelsPerSecond() {
82 return this._computedPixelsPerSecond;
83 }
84
85 /**
86 * Returns the current offset applied to the registered `Track` instances
87 * from origin (in seconds).
88 *
89 * @type {Number}
90 */
91 get offset() {
92 return this._offset;
93 }
94
95 /**
96 * Sets the offset to apply to the registered `Track` instances from origin
97 * (in seconds).
98 *
99 * @type {Number}
100 */
101 set offset(value) {
102 this._offset = value;
103 }
104
105 /**
106 * Returns the current zoom level applied to the whole visualization.
107 *
108 * @type {Number}
109 */
110 get zoom() {
111 return this._zoom;
112 }
113
114 /**
115 * Sets the zoom ratio for the whole visualization.
116 *
117 * @type {Number}
118 */
119 set zoom(value) {
120 // Compute change to propagate to children who have their own timeToPixel
121 const ratioChange = value / this._zoom;
122 this._zoom = value;
123 this._computedPixelsPerSecond = this._originalPixelsPerSecond * value;
124 this._updateTimeToPixelRange();
125
126 this._children.forEach(function(child) {
127 if (child.stretchRatio === 1) { return; }
128 child.stretchRatio = child.stretchRatio * ratioChange;
129 });
130 }
131
132 /**
133 * Returns the visible width of the `Track` instances.
134 *
135 * @type {Number}
136 */
137 get visibleWidth() {
138 return this._visibleWidth;
139 }
140
141 /**
142 * Sets the visible width of the `Track` instances.
143 *
144 * @type {Number}
145 */
146 set visibleWidth(value) {
147 const widthRatio = value / this.visibleWidth;
148 this._visibleWidth = value;
149
150 if (this.maintainVisibleDuration) {
151 this.pixelsPerSecond = this._computedPixelsPerSecond * widthRatio;
152 }
153 }
154
155 /**
156 * Returns the duration displayed by `Track` instances.
157 *
158 * @type {Number}
159 */
160 get visibleDuration() {
161 return this.visibleWidth / this._computedPixelsPerSecond;
162 }
163
164 /**
165 * Returns if the duration displayed by tracks should be maintained when
166 * their width is updated.
167 *
168 * @type {Number}
169 */
170 get maintainVisibleDuration() {
171 return this._maintainVisibleDuration;
172 }
173
174 /**
175 * Defines if the duration displayed by tracks should be maintained when
176 * their width is updated.
177 *
178 * @type {Boolean}
179 */
180 set maintainVisibleDuration(bool) {
181 this._maintainVisibleDuration = bool;
182 }
183
184 /**
185 * Returns the time to pixel trasfert function.
186 *
187 * @type {Function}
188 */
189 get timeToPixel() {
190 return this._timeToPixel;
191 }
192
193 _updateTimeToPixelRange() {
194 this.timeToPixel.range([0, this._computedPixelsPerSecond]);
195 }
196 }
197
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/core/track.js
1 import ns from './namespace';
2
3
4 /**
5 * Acts as a placeholder to organize the vertical layout of the visualization
6 * and the horizontal alignement to an abscissa that correspond to a common
7 * time reference. It basically offer a view on the overall timeline.
8 *
9 * Tracks are inserted into a given DOM element, allowing to create DAW like
10 * representations. Each `Track` instance can host multiple `Layer` instances.
11 * A track must be added to a timeline before being updated.
12 *
13 * ### A timeline with 3 tracks:
14 *
15 * ```
16 * 0 6 16
17 * +- - - - - - - - -+-------------------------------+- - - - - - -
18 * | |x track 1 xxxxxxxxxxxxxxxxxxxxx|
19 * +- - - - - - - - -+-------------------------------+- - - - - - -
20 * | |x track 2 xxxxxxxxxxxxxxxxxxxxx|
21 * +- - - - - - - - -+-------------------------------+- - - - - - -
22 * | |x track 3 xxxxxxxxxxxxxxxxxxxxx|
23 * +- - - - - - - - ---------------------------------+- - - - - - -
24 * +----------------->
25 * timeline.timeContext.timeToPixel(timeline.timeContext.offset)
26 *
27 * <------------------------------->
28 * timeline's tracks defaults to 1000px
29 * with a default pixelsPerSecond of 100px/s.
30 * and a default `stretchRatio = 1`
31 * track1 shows 10 seconds of the timeline
32 * ```
33 *
34 * ### Track DOM structure
35 *
36 * ```html
37 * <svg width="${visibleWidth}">
38 * <!-- background -->
39 * <rect><rect>
40 * <!-- main view -->
41 * <g class="offset" transform="translate(${offset}, 0)">
42 * <g class="layout">
43 * <!-- layers -->
44 * </g>
45 * </g>
46 * <g class="interactions"><!-- for feedback --></g>
47 * </svg>
48 * ```
49 */
50 export default class Track {
51 /**
52 * @param {DOMElement} $el
53 * @param {Number} [height = 100]
54 */
55 constructor($el, height = 100) {
56 this._height = height;
57
58 /**
59 * The DOM element in which the track is created.
60 * @type {Element}
61 */
62 this.$el = $el;
63 /**
64 * A placeholder to add shapes for interactions feedback.
65 * @type {Element}
66 */
67 this.$interactions = null;
68 /** @type {Element} */
69 this.$layout = null;
70 /** @type {Element} */
71 this.$offset = null;
72 /** @type {Element} */
73 this.$svg = null;
74 /** @type {Element} */
75 this.$background = null;
76
77 /**
78 * An array of all the layers belonging to the track.
79 * @type {Array<Layer>}
80 */
81 this.layers = [];
82 /**
83 * The context used to maintain the DOM structure of the track.
84 * @type {TimelineTimeContext}
85 */
86 this.renderingContext = null;
87
88 this._createContainer();
89 }
90
91 /**
92 * Returns the height of the track.
93 *
94 * @type {Number}
95 */
96 get height() {
97 return this._height;
98 }
99
100 /**
101 * Sets the height of the track.
102 *
103 * @todo propagate to layers, keeping ratio? could be handy for vertical
104 * resize. This is why a set/get is implemented here.
105 * @type {Number}
106 */
107 set height(value) {
108 this._height = value;
109 }
110
111 /**
112 * This method is called when the track is added to the timeline. The
113 * track cannot be updated without being added to a timeline.
114 *
115 * @private
116 * @param {TimelineTimeContext} renderingContext
117 */
118 configure(renderingContext) {
119 this.renderingContext = renderingContext;
120 }
121
122 /**
123 * Destroy the track. The layers from this track can still be reused elsewhere.
124 */
125 destroy() {
126 // Detach everything from the DOM
127 this.$el.removeChild(this.$svg);
128 this.layers.forEach((layer) => this.$layout.removeChild(layer.$el));
129 // clean references
130 this.$el = null;
131 this.renderingContext = null;
132 this.layers.length = 0;
133 }
134
135 /**
136 * Creates the DOM structure of the track.
137 */
138 _createContainer() {
139 const $svg = document.createElementNS(ns, 'svg');
140 $svg.setAttributeNS(null, 'shape-rendering', 'optimizeSpeed');
141 $svg.setAttributeNS(null, 'height', this.height);
142 $svg.setAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
143 $svg.classList.add('track');
144
145 const $background = document.createElementNS(ns, 'rect');
146 $background.setAttributeNS(null, 'height', '100%');
147 $background.setAttributeNS(null, 'width', '100%');
148 $background.style.fillOpacity = 0;
149 // $background.style.pointerEvents = 'none';
150
151 const $defs = document.createElementNS(ns, 'defs');
152
153 const $offsetGroup = document.createElementNS(ns, 'g');
154 $offsetGroup.classList.add('offset');
155
156 const $layoutGroup = document.createElementNS(ns, 'g');
157 $layoutGroup.classList.add('layout');
158
159 const $interactionsGroup = document.createElementNS(ns, 'g');
160 $interactionsGroup.classList.add('interactions');
161
162 $offsetGroup.appendChild($layoutGroup);
163 $svg.appendChild($defs);
164 $svg.appendChild($background);
165 $svg.appendChild($offsetGroup);
166 $svg.appendChild($interactionsGroup);
167 this.$el.appendChild($svg);
168 // removes additionnal height added who knows why...
169 this.$el.style.fontSize = 0;
170 // fixes one of the (many ?) weird canvas rendering bugs in Chrome
171 this.$el.style.transform = 'translateZ(0)';
172
173 this.$layout = $layoutGroup;
174 this.$offset = $offsetGroup;
175 this.$interactions = $interactionsGroup;
176 this.$svg = $svg;
177 this.$background = $background;
178 }
179
180 /**
181 * Adds a layer to the track.
182 *
183 * @param {Layer} layer - the layer to add to the track.
184 */
185 add(layer) {
186 this.layers.push(layer);
187 // Create a default renderingContext for the layer if missing
188 this.$layout.appendChild(layer.$el);
189 }
190
191 /**
192 * Removes a layer from the track. The layer can be reused elsewhere.
193 *
194 * @param {Layer} layer - the layer to remove from the track.
195 */
196 remove(layer) {
197 this.layers.splice(this.layers.indexOf(layer), 1);
198 // Removes layer from its container
199 this.$layout.removeChild(layer.$el);
200 }
201
202 /**
203 * Tests if a given element belongs to the track.
204 *
205 * @param {Element} $el
206 * @return {bool}
207 */
208 hasElement($el) {
209 do {
210 if ($el === this.$el) {
211 return true;
212 }
213
214 $el = $el.parentNode;
215 } while ($el !== null);
216
217 return false;
218 }
219
220 /**
221 * Render all the layers added to the track.
222 */
223 render() {
224 for (let layer of this) { layer.render(); }
225 }
226
227 /**
228 * Updates the track DOM structure and updates the layers.
229 *
230 * @param {Array<Layer>} [layers=null] - if not null, a subset of the layers to update.
231 */
232 update(layers = null) {
233 this.updateContainer();
234 this.updateLayers(layers);
235 }
236
237 /**
238 * Updates the track DOM structure.
239 */
240 updateContainer() {
241 const $svg = this.$svg;
242 const $offset = this.$offset;
243 // Should be in some update layout
244 const renderingContext = this.renderingContext;
245 const height = this.height;
246 const width = Math.round(renderingContext.visibleWidth);
247 const offsetX = Math.round(renderingContext.timeToPixel(renderingContext.offset));
248 const translate = `translate(${offsetX}, 0)`;
249
250 $svg.setAttributeNS(null, 'height', height);
251 $svg.setAttributeNS(null, 'width', width);
252 $svg.setAttributeNS(null, 'viewbox', `0 0 ${width} ${height}`);
253
254 $offset.setAttributeNS(null, 'transform', translate);
255 }
256
257 /**
258 * Updates the layers.
259 *
260 * @param {Array<Layer>} [layers=null] - if not null, a subset of the layers to update.
261 */
262 updateLayers(layers = null) {
263 layers = (layers === null) ? this.layers : layers;
264
265 layers.forEach((layer) => {
266 if (this.layers.indexOf(layer) === -1) { return; }
267 layer.update();
268 });
269 }
270
271 /**
272 * Iterates through the added layers.
273 */
274 *[Symbol.iterator]() {
275 yield* this.layers[Symbol.iterator]();
276 }
277 }
278
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/core/track-collection.js
1 import Layer from './layer';
2
3
4 /**
5 * Collection hosting all the `Track` instances registered into the timeline.
6 * It provides shorcuts to trigger `render` / `update` methods on tracks or
7 * layers. Extend built-in Array
8 */
9 export default class TrackCollection extends Array {
10 constructor(timeline) {
11 super();
12
13 this._timeline = timeline;
14 }
15
16 // @note - should be in the timeline ?
17 // @todo - allow to pass an array of layers
18 _getLayersOrGroups(layerOrGroup = null) {
19 let layers = null;
20
21 if (typeof layerOrGroup === 'string') {
22 layers = this._timeline.groupedLayers[layerOrGroup];
23 } else if (layerOrGroup instanceof Layer) {
24 layers = [layerOrGroup];
25 } else {
26 layers = this.layers;
27 }
28
29 return layers;
30 }
31
32 // @NOTE keep this ?
33 // could prepare some vertical resizing ability
34 // this should be able to modify the layers yScale to be really usefull
35
36 /**
37 * @type {Number} - Updates the height of all tracks at once.
38 * @todo - Propagate to layers, not usefull for now.
39 */
40 set height(value) {
41 this.forEach((track) => track.height = value);
42 }
43
44 get height() {
45 return track.height;
46 }
47
48 /**
49 * An array of all registered layers.
50 *
51 * @type {Array<Layer>}
52 */
53 get layers() {
54 let layers = [];
55 this.forEach((track) => layers = layers.concat(track.layers));
56
57 return layers;
58 }
59
60 /**
61 * Render all tracks and layers. When done, the timeline triggers a `render` event.
62 */
63 render() {
64 this.forEach((track) => track.render());
65 this._timeline.emit('render');
66 }
67
68 /**
69 * Updates all tracks and layers. When done, the timeline triggers a
70 * `update` event.
71 *
72 * @param {Layer|String} layerOrGroup - Filter the layers to update by
73 * passing the `Layer` instance to update or a `groupId`
74 */
75 update(layerOrGroup) {
76 const layers = this._getLayersOrGroups(layerOrGroup);
77 this.forEach((track) => track.update(layers));
78 this._timeline.emit('update', layers);
79 }
80
81 /**
82 * Updates all `Track` containers, layers are not updated with this method.
83 * When done, the timeline triggers a `update:containers` event.
84 */
85 updateContainer(/* trackOrTrackIds */) {
86 this.forEach((track) => track.updateContainer());
87 this._timeline.emit('update:containers');
88 }
89
90 /**
91 * Updates all layers. When done, the timeline triggers a `update:layers` event.
92 *
93 * @param {Layer|String} layerOrGroup - Filter the layers to update by
94 * passing the `Layer` instance to update or a `groupId`
95 */
96 updateLayers(layerOrGroup) {
97 const layers = this._getLayersOrGroups(layerOrGroup);
98 this.forEach((track) => track.updateLayers(layers));
99 this._timeline.emit('update:layers', layers);
100 }
101 }
102
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/shapes/marker.js
1 import BaseShape from './base-shape';
2
3
4 /**
5 * A shape to display a marker.
6 *
7 * [example usage](./examples/layer-marker.html)
8 */
9 export default class Marker extends BaseShape {
10 getClassName() { return 'marker'; }
11
12 _getAccessorList() {
13 return { x: 0, color: '#ff0000' };
14 }
15
16 _getDefaults() {
17 return {
18 handlerWidth: 7,
19 handlerHeight: 10,
20 displayHandlers: true,
21 opacity: 1,
22 color: 'red',
23 };
24 }
25
26 render(renderingContext) {
27 if (this.$el) { return this.$el; }
28
29 const height = renderingContext.height;
30
31 this.$el = document.createElementNS(this.ns, 'g');
32 this.$line = document.createElementNS(this.ns, 'line');
33
34 // draw line
35 this.$line.setAttributeNS(null, 'x', 0);
36 this.$line.setAttributeNS(null, 'y1', 0);
37 this.$line.setAttributeNS(null, 'y2', height);
38 this.$line.setAttributeNS(null, 'shape-rendering', 'crispEdges');
39
40 this.$el.appendChild(this.$line);
41
42 if (this.params.displayHandlers) {
43 this.$handler = document.createElementNS(this.ns, 'rect');
44
45 this.$handler.setAttributeNS(null, 'x', -((this.params.handlerWidth) / 2 ));
46 this.$handler.setAttributeNS(null, 'y', renderingContext.height - this.params.handlerHeight);
47 this.$handler.setAttributeNS(null, 'width', this.params.handlerWidth);
48 this.$handler.setAttributeNS(null, 'height', this.params.handlerHeight);
49 this.$handler.setAttributeNS(null, 'shape-rendering', 'crispEdges');
50
51 this.$el.appendChild(this.$handler);
52 }
53
54 this.$el.style.opacity = this.params.opacity;
55
56 return this.$el;
57 }
58
59 update(renderingContext, datum) {
60 const x = renderingContext.timeToPixel(this.x(datum)) - 0.5;
61 const color = this.color(datum);
62
63 this.$el.setAttributeNS(null, 'transform', `translate(${x}, 0)`);
64 this.$line.style.stroke = color;
65
66 if (this.params.displayHandlers) {
67 this.$handler.style.fill = color;
68 }
69 }
70
71 inArea(renderingContext, datum, x1, y1, x2, y2) {
72 // handlers only are selectable
73 const x = renderingContext.timeToPixel(this.x(datum));
74 const shapeX1 = x - (this.params.handlerWidth - 1) / 2;
75 const shapeX2 = shapeX1 + this.params.handlerWidth;
76 const shapeY1 = renderingContext.height - this.params.handlerHeight;
77 const shapeY2 = renderingContext.height;
78
79 const xOverlap = Math.max(0, Math.min(x2, shapeX2) - Math.max(x1, shapeX1));
80 const yOverlap = Math.max(0, Math.min(y2, shapeY2) - Math.max(y1, shapeY1));
81 const area = xOverlap * yOverlap;
82
83 return area > 0;
84 }
85 }
86
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/behaviors/segment-behavior.js
1 import BaseBehavior from './base-behavior';
2
3
4 /**
5 * Defines the default behavior for a segment.
6 *
7 * [example usage](./examples/layer-marker.html)
8 */
9 export default class SegmentBehavior extends BaseBehavior {
10 edit(renderingContext, shape, datum, dx, dy, target) {
11 const classList = target.classList;
12 let action = 'move';
13
14 if (classList.contains('handler') && classList.contains('left')) {
15 action = 'resizeLeft';
16 } else if (classList.contains('handler') && classList.contains('right')) {
17 action = 'resizeRight';
18 }
19
20 this[`_${action}`](renderingContext, shape, datum, dx, dy, target);
21 }
22
23 _move(renderingContext, shape, datum, dx, dy, target) {
24 const layerHeight = renderingContext.height;
25 // current values
26 const x = renderingContext.timeToPixel(shape.x(datum));
27 const y = renderingContext.valueToPixel(shape.y(datum));
28 const height = renderingContext.valueToPixel(shape.height(datum));
29 // target values
30 let targetX = Math.max(x + dx, 0);
31 let targetY = y - dy;
32
33 // lock in layer's y axis
34 if (targetY < 0) {
35 targetY = 0;
36 } else if (targetY + height > layerHeight) {
37 targetY = layerHeight - height;
38 }
39
40 shape.x(datum, renderingContext.timeToPixel.invert(targetX));
41 shape.y(datum, renderingContext.valueToPixel.invert(targetY));
42 }
43
44 _resizeLeft(renderingContext, shape, datum, dx, dy, target) {
45 // current values
46 const x = renderingContext.timeToPixel(shape.x(datum));
47 const width = renderingContext.timeToPixel(shape.width(datum));
48 // target values
49 let maxTargetX = x + width;
50 let targetX = x + dx < maxTargetX ? Math.max(x + dx, 0) : x;
51 let targetWidth = targetX !== 0 ? Math.max(width - dx, 1) : width;
52
53 shape.x(datum, renderingContext.timeToPixel.invert(targetX));
54 shape.width(datum, renderingContext.timeToPixel.invert(targetWidth));
55 }
56
57 _resizeRight(renderingContext, shape, datum, dx, dy, target) {
58 // current values
59 const width = renderingContext.timeToPixel(shape.width(datum));
60 // target values
61 let targetWidth = Math.max(width + dx, 1);
62
63 shape.width(datum, renderingContext.timeToPixel.invert(targetWidth));
64 }
65 }
66
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/shapes/trace-dots.js
1 import BaseShape from './base-shape';
2
3
4 /**
5 * A shape to display dots in a trace visualization (mean / range).
6 *
7 * [example usage](./examples/layer-trace.html)
8 */
9 export default class TraceDots extends BaseShape {
10 getClassName() { return 'trace-dots'; }
11
12 _getAccessorList() {
13 return { x: 0, mean: 0, range: 0 };
14 }
15
16 _getDefaults() {
17 return {
18 meanRadius: 3,
19 rangeRadius: 3,
20 meanColor: '#232323',
21 rangeColor: 'steelblue'
22 };
23 }
24
25 render(renderingContext) {
26 if (this.$el) { return this.$el; }
27 // container
28 this.$el = document.createElementNS(this.ns, 'g');
29 // draw mean dot
30 this.$mean = document.createElementNS(this.ns, 'circle');
31 this.$mean.setAttributeNS(null, 'r', this.params.meanRadius);
32 this.$mean.setAttributeNS(null, 'stroke', this.params.meanColor);
33 this.$mean.setAttributeNS(null, 'fill', 'transparent');
34 this.$mean.classList.add('mean');
35 // range dots (0 => top, 1 => bottom)
36 this.$max = document.createElementNS(this.ns, 'circle');
37 this.$max.setAttributeNS(null, 'r', this.params.meanRadius);
38 this.$max.setAttributeNS(null, 'stroke', this.params.rangeColor);
39 this.$max.setAttributeNS(null, 'fill', 'transparent');
40 this.$max.classList.add('max');
41
42 this.$min = document.createElementNS(this.ns, 'circle');
43 this.$min.setAttributeNS(null, 'r', this.params.meanRadius);
44 this.$min.setAttributeNS(null, 'stroke', this.params.rangeColor);
45 this.$min.setAttributeNS(null, 'fill', 'transparent');
46 this.$min.classList.add('min');
47
48 this.$el.appendChild(this.$mean);
49 this.$el.appendChild(this.$max);
50 this.$el.appendChild(this.$min);
51
52 return this.$el;
53 }
54
55 // @TODO use accessors
56 update(renderingContext, datum) {
57 const mean = this.mean(datum);
58 const range = this.range(datum);
59 const x = this.x(datum);
60 // y positions
61 const meanPos = `${renderingContext.valueToPixel(mean)}`;
62 this.$mean.setAttributeNS(null, 'transform', `translate(0, ${meanPos})`);
63
64 const halfRange = range / 2;
65 const max = renderingContext.valueToPixel(mean + halfRange);
66 const min = renderingContext.valueToPixel(mean - halfRange);
67 const xPos = renderingContext.timeToPixel(x);
68
69 this.$max.setAttributeNS(null, 'transform', `translate(0, ${max})`);
70 this.$min.setAttributeNS(null, 'transform', `translate(0, ${min})`);
71 this.$el.setAttributeNS(null, 'transform', `translate(${xPos}, 0)`);
72 }
73
74 inArea(renderingContext, datum, x1, y1, x2, y2) {
75 const x = renderingContext.timeToPixel(this.x(datum));
76 const mean = renderingContext.valueToPixel(this.mean(datum));
77 const range = renderingContext.valueToPixel(this.range(datum));
78 const min = mean - (range / 2);
79 const max = mean + (range / 2);
80
81 if (x > x1 && x < x2 && (min > y1 || max < y2)) {
82 return true;
83 }
84
85 return false;
86 }
87 }
88
> coverage of: /Users/justinwinter/Sites/local/docroot/waves/waves-ui/src/utils/orthogonal-data.js
1 /**
2 * OrthogonalData transforms an object of arrays `{foo: [1, 2], bar: [3, 4]}`
3 * to or from an array of objects `[{foo: 1, bar: 3}, {foo: 2, bar: 4}]`
4 */
5 export default class OrthogonalData {
6 constructor() {
7 this._cols = null; // Object of arrays
8 this._rows = null; // Array of objects
9 }
10
11 /**
12 * Check the consistency of the data.
13 */
14 _checkConsistency() {
15 let size = null;
16
17 for (let key in this._cols) {
18 const col = this._cols[key];
19 const colLength = col.length;
20
21 if (size !== null && size !== colLength) {
22 throw new Error(`${this.prototype.constructor.name}: inconsistent data`);
23 } else if (size === null) {
24 size = colLength;
25 }
26 }
27 }
28
29 /**
30 * Updates array of objects from object of arrays.
31 */
32 updateFromCols() {
33 let keys = Object.keys(this._cols);
34
35 keys.forEach((key, i) => {
36 const col = this._cols[key];
37
38 col.forEach((value, index) => {
39 if (this._rows[index] === undefined) this._rows[index] = {};
40 this._rows[index][key] = value;
41 });
42 });
43
44 this._checkConsistency();
45 }
46
47 /**
48 * Updates object of arrays from array of objects.
49 */
50 updateFromRows() {
51 this._rows.forEach((obj, index) => {
52 for (let key in obj) {
53 if (index === 0) this._cols[key] = [];
54 this._cols[key].push(obj[key]);
55 }
56 });
57
58 this._checkConsistency();
59 }
60
61 /**
62 * Sets an object of arrays.
63 *
64 * @type {Object<String, Array>}
65 */
66 set cols(obj) {
67 this._cols = obj;
68 this._rows = [];
69
70 this.updateFromCols();
71 }
72
73 /**
74 * Returns an object of arrays.
75 *
76 * @type {Object<String, Array>}
77 */
78 get cols() {
79 return this._cols;
80 }
81
82 /**
83 * Sets an array of objects.
84 *
85 * @type {Array<Object>}
86 */
87 set rows(arr) {
88 this._rows = arr;
89 this._cols = {};
90
91 this.updateFromRows();
92 }
93
94 /**
95 * Returns an array of objects.
96 *
97 * @type {Array<Object>}
98 */
99 get rows() {
100 return this._rows;
101 }
102 }
103
✨ Done in 58.93s.