Skip to content

Commit b891ff4

Browse files
committed
Add "live-colors" example
1 parent d93fb27 commit b891ff4

10 files changed

Lines changed: 445 additions & 83 deletions

File tree

addon/components/plot-ly.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { A } from '@ember/array';
21
import Component from '@ember/component';
32
import EmberObject, { observer } from '@ember/object';
43
import { computed } from '@ember-decorators/object';
@@ -97,7 +96,6 @@ export default class PlotlyComponent extends Component.extend({
9796
scheduleOnce('render', this, '_react');
9897
})
9998
}) {
100-
10199
constructor(...args) {
102100
super(...args);
103101
this.set('layout', layout);
@@ -114,6 +112,7 @@ export default class PlotlyComponent extends Component.extend({
114112
// Consumers should override this if they want to handle plotly_events
115113
onPlotlyEvent(eventName, ...args) {
116114
log('onPlotlyEvent fired (does nothing since it was not overridden)', eventName, ...args);
115+
117116
}
118117

119118
// Lifecycle hooks
@@ -138,8 +137,8 @@ export default class PlotlyComponent extends Component.extend({
138137
@computed('chartData', 'chartLayout', 'chartConfig', 'isResponsive', 'plotlyEvents')
139138
get _parameters() {
140139
const parameters = Object.assign({}, {
141-
chartData: this.get('chartData') || A(),
142-
chartLayout: this.get('chartLayout') || EmberObject.create(),
140+
chartData: this.get('chartData'),
141+
chartLayout: this.get('chartLayout') || document.getElementById(this.elementId).layout || EmberObject.create({ datarevision: 0 }),
143142
chartConfig: Object.assign(defaultConfig, this.get('chartConfig')),
144143
isResponsive: !!this.get('isResponsive'),
145144
plotlyEvents: this.get('plotlyEvents') || []
@@ -195,18 +194,21 @@ export default class PlotlyComponent extends Component.extend({
195194
log('newPlot finished');
196195
this._bindPlotlyEventListeners();
197196
// TODO: Hook
197+
}).catch((e, ...args) => {
198+
warn(`Plotly.newPlot resulted in rejected promise`, e, ...args);
198199
});
199200
}
200201

201202
_react() {
202203
if (this._isDomElementBad()) {
203-
warn(`_updateChart aborting since element (or its ID) is not available or component is (being) destroyed.`);
204+
warn(`_react aborting since element (or its ID) is not available or component is (being) destroyed.`);
204205
return;
205206
}
206207
const id = this.elementId;
207208
const { chartData, chartLayout, chartConfig } = this.get('_parameters');
208-
log('About to call Plotly.react');
209-
chartLayout.datarevision = chartLayout.datarevision + 1; // Force update
209+
// Force update
210+
chartLayout.datarevision += 1;
211+
log('About to call Plotly.react', chartData, chartLayout, chartConfig);
210212
Plotly.react(id, chartData, chartLayout, chartConfig).then(() => {
211213
log('react finished');
212214
// TODO: Hook
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import Controller from '@ember/controller';
2+
import { later } from '@ember/runloop';
3+
import { action, computed } from '@ember-decorators/object';
4+
5+
import getNormalDistPDF from 'dummy/utils/get-normal-dist-pdf';
6+
7+
import * as debug from 'debug';
8+
const log = debug('ember-cli-plotly:dummy:live-color');
9+
10+
//const interval = 0; // (go as fast as the plot can update)
11+
const interval = 200;
12+
13+
const activeColor = '#01a0e1';
14+
const passiveColor = '#959595';
15+
const highlightColor = '#d474ff';
16+
17+
// Data to plot
18+
const n = 101;
19+
const x = new Array(n).fill(0).map((z,i) => 5*(2*i/(n-1) - 1)); // [-5, ..., 5]
20+
const sourceData = [{
21+
name: "Group 1 (0,1)",
22+
mu: 0,
23+
sigma: 1,
24+
traces: 4,
25+
noiseFunction: y => y + 0.2*(2*(Math.random() ** 10) - 1),
26+
scaleFactor: 50
27+
}, {
28+
name: "Group 2 (2,2)",
29+
mu: 2,
30+
sigma: 2,
31+
traces: 10,
32+
noiseFunction: (y, i) => y + 0.1*Math.sin(2*Math.PI*50*Math.random()*i/n),
33+
scaleFactor: 25
34+
}].map(({ name, mu, sigma, traces, noiseFunction, scaleFactor }) => {
35+
const array = [];
36+
for (let i=0; i < traces; i++) {
37+
array.push({
38+
name: `${(i+1)} (${name})`,
39+
x,
40+
y: x.map(getNormalDistPDF(mu, sigma))
41+
.map(y => scaleFactor*y)
42+
.map(noiseFunction),
43+
mode: 'lines'
44+
});
45+
}
46+
return array;
47+
}).reduce((array, set) => {
48+
set.forEach(s => array.push(s));
49+
return array;
50+
}, []);
51+
52+
export default class ExamplesLiveColorsController extends Controller {
53+
_isHighlighted = sourceData.map(() => false);
54+
plotlyEvents = ['plotly_legenddoubleclick'];
55+
currentTrace = 1; // Start on second trace
56+
currentIndex = 0;
57+
_updating = false; // Don't start the timer until user clicks button
58+
_revision = 0;
59+
60+
_triggerUpdate() {
61+
log(`_triggerUpdate incrementing _revision (${this._revision})`);
62+
//this.set('chartData.triggerUpdate', (this.get('chartData.triggerUpdate') || 0) + 1);
63+
this.incrementProperty('_revision');
64+
}
65+
66+
update() {
67+
log('update firing');
68+
if (!this.get('_updating')) {
69+
return;
70+
}
71+
72+
const currentTrace = this.get('currentTrace');
73+
const currentIndex = this.get('currentIndex');
74+
log(`Update called: currentTrace=${currentTrace}, currentIndex=${currentIndex}`, this.get(`chartData`));
75+
76+
// Prepare to do next point
77+
if (currentIndex >= sourceData[currentTrace].x.length) {
78+
// Stop when we've plotted all the data
79+
if (currentTrace >= sourceData.length) {
80+
log(`currentIndex (${currentIndex}) >= sourceData[currentTrace].x.length (${currentIndex >= sourceData[currentTrace].x.length})`);
81+
this.set('_updating', false);
82+
}
83+
else {
84+
this.set('currentTrace', currentTrace + 1);
85+
this.set('currentIndex', 0);
86+
}
87+
}
88+
else {
89+
this.set('currentIndex', 1 + currentIndex);
90+
}
91+
92+
this._triggerUpdate();
93+
if (this.get('_updating')) {
94+
later(this, 'update', interval);
95+
}
96+
}
97+
98+
_toggleHighlighting(curveNumber) {
99+
log(`_toggleHighlighting(${curveNumber}) changing ${this._isHighlighted[curveNumber]} -> ${!this._isHighlighted[curveNumber]}`);
100+
this._isHighlighted[curveNumber] = !this._isHighlighted[curveNumber];
101+
this._triggerUpdate();
102+
}
103+
104+
@computed('currentTrace', 'currentIndex', '_isHighlighted.[]', '_revision')
105+
get chartData() {
106+
const currentTrace = (() => {
107+
// FIXME: Shouldn't need to sanity check this
108+
let ct = this.get('currentTrace');
109+
if (ct >= sourceData.length) {
110+
ct = sourceData.length - 1;
111+
}
112+
return ct;
113+
})();
114+
const currentIndex = this.get('currentIndex');
115+
log(`Computing chartData (currentTrace=${currentTrace}, currentIndex=${currentIndex})`);
116+
// We're going to copy sourceData (don't modify it!) into our own var here where we can set colors, slice to animate, etc.
117+
// For improved performance we could maintain this state instead of rebuilding it every time
118+
const chartData = JSON.parse(JSON.stringify(sourceData)).slice(0, currentTrace + 1); // FIXME: sloppy
119+
chartData[currentTrace].x = chartData[currentTrace].x.slice(0, currentIndex + 1);
120+
chartData[currentTrace].y = chartData[currentTrace].y.slice(0, currentIndex + 1);
121+
122+
// Apply styling
123+
chartData.forEach((trace, i, array) => {
124+
trace.line = trace.line || {};
125+
126+
// Active/passive coloring
127+
if (i === array.length - 1) {
128+
// Last trace (= active trace)
129+
trace.line.color = activeColor;
130+
}
131+
else {
132+
trace.line.color = passiveColor;
133+
}
134+
135+
// Highlight selected traces
136+
if (this._isHighlighted[i]) {
137+
trace.line.color = highlightColor;
138+
//trace.line.width = 3;
139+
}
140+
});
141+
142+
// TODO: See if there's a cleaner way to update than by manually re-triggering when chartData is computed
143+
chartData.triggerUpdate = this._revision;
144+
145+
return chartData;
146+
}
147+
148+
@action
149+
clear() {
150+
log(`Clear clicked`);
151+
this.setProperties({
152+
currentTrace: 0,
153+
currentIndex: 0
154+
});
155+
}
156+
157+
@action
158+
start() {
159+
log(`Start clicked`, this.get('_updating'));
160+
if (this.get('_updating') === false) {
161+
this.set('_updating', true);
162+
later(this, 'update', interval);
163+
}
164+
}
165+
166+
@action
167+
stop() {
168+
log(`Stop clicked`);
169+
this.set('_updating', false);
170+
}
171+
172+
@action
173+
onPlotlyEvent(eventName, eventData) {
174+
log(`onPlotlyEvent got ${eventName} -->`, eventData);
175+
if (typeof this.plotlyEventHandlers[eventName] === 'function') {
176+
return this.plotlyEventHandlers[eventName].call(this, eventData);
177+
}
178+
}
179+
180+
plotlyEventHandlers = {
181+
plotly_legenddoubleclick(eventData) {
182+
this._toggleHighlighting(eventData.curveNumber);
183+
return false; // prevent default behavior (hiding all other traces)
184+
}
185+
};
186+
}

tests/dummy/app/controllers/examples/responsive.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import Controller from '@ember/controller';
22

3+
import getNormalDistPDF from 'dummy/utils/get-normal-dist-pdf';
4+
35
const n = 1001;
46
const x = new Array(n).fill(0).map((z,i) => 10*(2*i/(n-1) - 1)); // [-10, ..., 10]
5-
const getNormalDistPDF = (mu, sigma) => {
6-
// See PDF equation here: https://en.wikipedia.org/wiki/Log-normal_distribution
7-
const k1 = 1 / (Math.sqrt(2*Math.PI*sigma*sigma));
8-
const k2 = 2 * sigma * sigma;
9-
return x => k1*Math.exp(-Math.pow(x-mu, 2) / k2);
10-
};
117
const chartData = [{
128
x, y: x.map(getNormalDistPDF(0, 1))
139
}, {

tests/dummy/app/router.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Router.map(function() {
1616
this.route('legend-events');
1717
this.route('responsive');
1818
this.route('live-data');
19+
this.route('live-colors');
1920
});
2021
});
2122

tests/dummy/app/routes/examples.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export default Route.extend({
2020
}, {
2121
id: 'live-data',
2222
name: 'Update chart as "live" data is generated'
23+
}, {
24+
id: 'live-colors',
25+
name: 'Change colors of traces dynamically'
2326
}, {
2427
id: 'legend-events',
2528
name: 'Handle legend click/doubleclick events'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Route from '@ember/routing/route';
2+
3+
export default class ExamplesLiveColorsRoute extends Route {
4+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{plot-ly
2+
chartData=chartData
3+
chartLayout=chartLayout
4+
chartConfig=chartConfig
5+
plotlyEvents=plotlyEvents
6+
onPlotlyEvent=(action 'onPlotlyEvent')
7+
isResponsive=true
8+
}}
9+
10+
<button onclick={{action 'start'}}>Start</button>
11+
<button onclick={{action 'stop'}}>Stop</button>
12+
<button onclick={{action 'clear'}}>Clear</button>
13+
14+
currentTrace: {{currentTrace}}, currentIndex: {{currentIndex}}

tests/dummy/app/utils/datasets.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,25 @@ export default function generateDataSets(n, r) {
1212
x,
1313
y: x.map(() => -5)
1414
}, {
15-
name: 'y = 2x-1',
16-
x,
17-
y: x.map(x => 2 * x - 1)
18-
}, {
19-
name: 'y = 1.5x',
20-
x,
21-
y: x.map(x => 1.5 * x)
22-
}, {
23-
name: 'y = -1.8x + noise',
24-
x,
25-
y: x.map(x => -1.8 * x + 2 * (1 - Math.random()))
26-
}, {
27-
name: 'y = 1/x',
28-
x,
29-
y: x.map(x => 1 / x),
30-
}, {
31-
name: 'y = x*sin(2*x)',
32-
x,
33-
y: x.map(x => x * Math.sin(2 * x)),
34-
}
15+
name: 'y = 2x-1',
16+
x,
17+
y: x.map(x => 2 * x - 1)
18+
}, {
19+
name: 'y = 1.5x',
20+
x,
21+
y: x.map(x => 1.5 * x)
22+
}, {
23+
name: 'y = -1.8x + noise',
24+
x,
25+
y: x.map(x => -1.8 * x + 2 * (1 - Math.random()))
26+
}, {
27+
name: 'y = 1/x',
28+
x,
29+
y: x.map(x => 1 / x),
30+
}, {
31+
name: 'y = x*sin(2*x)',
32+
x,
33+
y: x.map(x => x * Math.sin(2 * x)),
34+
}
3535
];
3636
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function getNormalDistPDF(mu, sigma) {
2+
// See PDF equation here: https://en.wikipedia.org/wiki/Log-normal_distribution
3+
const k1 = 1 / (Math.sqrt(2*Math.PI*sigma*sigma));
4+
const k2 = 2 * sigma * sigma;
5+
return x => k1*Math.exp(-Math.pow(x-mu, 2) / k2);
6+
}

0 commit comments

Comments
 (0)