1+ const indexBubbleChart = document . getElementById ( 'index-bubble-chart' ) ;
2+
3+ /* Calculates the difference, in months, between two Date objects */
4+ const dateDiff = pastDate => Math . floor ( ( new Date ( ) - pastDate ) / 2592000000 ) ;
5+
6+ /* Updates the index page bubble chart with the current values of the list size,
7+ open rate, and date created field */
8+ const updateChart = speed => {
9+ const
10+ userSubscribers = parseInt ( document . getElementById ( 'enter-list-size' )
11+ . value . replace ( / , / g, '' ) ) ,
12+ userOpenRate = + ( document . getElementById ( 'enter-open-rate' )
13+ . value . replace ( '%' , '' ) ) ,
14+ userListCreated = document . getElementById ( 'enter-list-age' ) . value ;
15+ if ( isNaN ( userSubscribers ) || userSubscribers < 0 ||
16+ isNaN ( userOpenRate ) || userOpenRate < 0 || userOpenRate > 100 )
17+ return ;
18+ const
19+ userListAge = dateDiff (
20+ new Date ( new Date ( userListCreated ) . toUTCString ( ) ) ) ,
21+ openRateFormatted = userOpenRate . toFixed ( 1 ) ,
22+ animation = Plotly . animate ( indexBubbleChart , {
23+ data : [
24+ { x : [ userListAge ] ,
25+ y : [ openRateFormatted ] ,
26+ text : [ 'Age: ' + userListAge + ' months<br>' +
27+ 'Open Rate: ' + openRateFormatted + '%<br>' +
28+ 'Subscribers: ' + userSubscribers . toLocaleString ( ) ] ,
29+ marker : {
30+ size : [ userSubscribers ] ,
31+ }
32+ }
33+ ] ,
34+ traces : [ 1 ]
35+ } , {
36+ transition : {
37+ duration : speed ,
38+ easing : 'ease' ,
39+ } ,
40+ frame : {
41+ duration : speed
42+ }
43+ } ) ;
44+ return animation ;
45+ }
46+
47+ if ( indexBubbleChart ) {
48+ const
49+ subscribers = JSON . parse ( indexBubbleChart . getAttribute ( 'data-subscribers' ) ) ,
50+ openRates = JSON . parse (
51+ indexBubbleChart . getAttribute ( 'data-open-rates' ) )
52+ . map ( val => Math . round ( 1000 * val ) / 10 ) ,
53+ listAges = JSON . parse ( indexBubbleChart . getAttribute ( 'data-ages' ) ) ,
54+ janFirstDate = new Date (
55+ new Date ( new Date ( ) . getFullYear ( ) - 5 , 0 , 1 ) . toUTCString ( ) ) ;
56+
57+ // Bubble chart data from the database
58+ const dbData = {
59+ x : listAges ,
60+ y : openRates ,
61+ text : Array . from (
62+ { length : listAges . length } ,
63+ ( v , i ) =>
64+ 'Age: ' + listAges [ i ] + ' months<br>' +
65+ 'Open Rate: ' + openRates [ i ] + '%<br>' +
66+ 'Subscribers: ' + subscribers [ i ] . toLocaleString ( )
67+ ) ,
68+ hoverinfo : 'text' ,
69+ hoverlabel : {
70+ bgcolor : 'rgba(167, 25, 48, .85)' ,
71+ font : {
72+ family : 'Montserrat, sans-serif' ,
73+ size : 12
74+ } ,
75+
76+ } ,
77+ mode : 'markers' ,
78+ marker : {
79+ size : subscribers ,
80+ sizeref : 2.0 * Math . max ( ...subscribers ) / ( 60 ** 2 ) ,
81+ sizemode : 'area' ,
82+ color : new Array ( subscribers . length ) . fill ( 'rgba(167, 25, 48, .85)' )
83+ }
84+ } ;
85+
86+ // Prepopulated dummy 'user' data
87+ const userData = {
88+ x : [ dateDiff ( janFirstDate ) ] ,
89+ y : [ 7.5 ] ,
90+ text : [ 'Age: ' + dateDiff ( janFirstDate ) +
91+ ' months<br>Open Rate: 7.5%<br>Subscribers: 5,000' ] ,
92+ hoverinfo : 'text' ,
93+ hoverlabel : {
94+ bgcolor : 'rgba(215, 164, 45, 0.85)' ,
95+ font : {
96+ color : 'white' ,
97+ family : 'Montserrat, sans-serif' ,
98+ size : 12
99+ } ,
100+ bordercolor : 'white'
101+ } ,
102+ mode : 'markers' ,
103+ marker : {
104+ size : [ 5000 ] ,
105+ sizeref : 2.0 * Math . max ( ...subscribers ) / ( 60 ** 2 ) ,
106+ sizemode : 'area' ,
107+ color : [ 'rgba(215, 164, 45, 0.85)' ]
108+ }
109+ } ;
110+
111+ const data = [ dbData , userData ] ;
112+
113+ // Bubble chart visual appearance
114+ const layout = {
115+ font : {
116+ family : 'Montserrat, sans-serif' ,
117+ size : 16 ,
118+ } ,
119+ yaxis : {
120+ range : [ 0 , ( 1.25 * Math . max ( ...openRates ) > 100 ) ? 100 :
121+ 1.25 * Math . max ( ...openRates ) ] ,
122+ color : '#aaa' ,
123+ tickfont : {
124+ color : '#555'
125+ } ,
126+ tickprefix : ' ' ,
127+ ticksuffix : '% ' ,
128+ title : 'List Open Rate' ,
129+ titlefont : {
130+ color : '#555'
131+ } ,
132+ automargin : true ,
133+ fixedrange : true
134+ } ,
135+ xaxis : {
136+ range : [ 0 , 1.15 * Math . max ( ...listAges ) ] ,
137+ color : '#aaa' ,
138+ tickfont : {
139+ color : '#555'
140+ } ,
141+ tickformat : ',' ,
142+ title : 'List Age (Months)' ,
143+ titlefont : {
144+ color : '#555'
145+ } ,
146+ fixedrange : true
147+ } ,
148+ showlegend : false ,
149+ height : 525 ,
150+ margin : {
151+ t : 5 ,
152+ b : 105
153+ } ,
154+ hovermode : 'closest'
155+ } ;
156+
157+ const config = {
158+ responsive : true ,
159+ displayModeBar : false
160+ } ;
161+
162+ Plotly . newPlot ( indexBubbleChart , data , layout , config ) ;
163+
164+ // Instantiate a flatpickr date picker widget on the list age field
165+ flatpickr ( '#enter-list-age' , {
166+ defaultDate : janFirstDate ,
167+ maxDate : 'today' ,
168+ dateFormat : 'm/d/Y'
169+ } ) ;
170+
171+ const enterStatsFields = document . querySelectorAll ( '.enter-stats input' ) ;
172+ for ( let i = 0 ; i < enterStatsFields . length ; ++ i ) {
173+ const elt = enterStatsFields [ i ] ;
174+ elt . addEventListener ( 'change' , ( ) => updateChart ( 450 ) ) ;
175+ }
176+
177+ /* Event listener which triggers an animation when the chart comes into view */
178+ const chartVisibleHandler = ( ) => {
179+ const
180+ rect = indexBubbleChart . getBoundingClientRect ( ) ,
181+ top = rect . top ,
182+ bottom = rect . bottom - 45 ;
183+ if ( top >= 0 && bottom <= window . innerHeight ) {
184+ const
185+ listSizeField = document . getElementById ( 'enter-list-size' ) ,
186+ openRateField = document . getElementById ( 'enter-open-rate' ) ;
187+ listSizeField . value = '25,000' ;
188+ openRateField . value = '30%' ;
189+ updateChart ( 1500 ) ;
190+ document . removeEventListener ( 'scroll' , debouncedChartHandler ) ;
191+ }
192+ }
193+
194+ const debouncedChartHandler = debounced ( 50 , chartVisibleHandler ) ;
195+
196+ document . addEventListener ( 'scroll' , debouncedChartHandler ) ;
197+ }
0 commit comments