PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.6.0
Independent Analytics – WordPress Analytics Plugin v2.6.0
2.15.5 2.15.4 2.15.3 2.15.2 2.15.1 2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / javascript-source / controllers / chart_controller.js
independent-analytics / javascript-source / controllers Last commit date
campaign_builder_controller.js 2 years ago chart_controller.js 2 years ago chart_geo_controller.js 2 years ago chart_interval_controller.js 2 years ago clipboard_controller.js 2 years ago copy_report_controller.js 2 years ago create_report_controller.js 2 years ago dates_controller.js 2 years ago delete_data_controller.js 2 years ago delete_report_controller.js 2 years ago easepick_controller.js 2 years ago export_reports_controller.js 2 years ago filters_controller.js 2 years ago group_controller.js 2 years ago import_reports_controller.js 2 years ago migration_redirect_controller.js 2 years ago modal_controller.js 2 years ago plugin_group_options_controller.js 2 years ago quick_stats_controller.js 2 years ago real_time_controller.js 2 years ago rename_report_controller.js 2 years ago report_controller.js 2 years ago reset_analytics_controller.js 2 years ago save_report_controller.js 2 years ago select_input_controller.js 2 years ago set_favorite_report_controller.js 2 years ago sort_controller.js 2 years ago sortable_reports_controller.js 2 years ago table_columns_controller.js 2 years ago
chart_controller.js
387 lines
1 import {Controller} from "@hotwired/stimulus"
2 import corsairPlugin from '../chart_plugins/corsair_plugin'
3 import {Chart, registerables} from 'chart.js'
4 import color from 'color'
5
6 Chart.register(...registerables);
7
8 export default class extends Controller {
9 static targets = ['primaryMetricSelect', 'secondaryMetricSelect', 'adaptiveWidthSelect'];
10
11 static values = {
12 labels: Array,
13 data: Object,
14 locale: String,
15 currency: {
16 type: String,
17 default: 'USD'
18 },
19 isPreview: Boolean,
20 isUsingWooCommerce: Boolean,
21 primaryChartMetricId: String,
22 primaryChartMetricName: String,
23 secondaryChartMetricId: String,
24 secondaryChartMetricName: String,
25 }
26
27 metricGroups = [{
28 metrics: ['views', 'visitors', 'sessions'],
29 format: 'int'
30 }, {
31 metrics: ['average_session_duration'],
32 format: 'time'
33 }, {
34 metrics: ['bounce_rate'],
35 format: 'percent'
36 }, {
37 metrics: ['views_per_session'],
38 format: 'float'
39 }, {
40 metrics: ['wc_orders', 'wc_refunds'],
41 format: 'int'
42 }, {
43 metrics: ['wc_gross_sales', 'wc_refunded_amount', 'wc_net_sales'],
44 format: 'whole_currency'
45 }, {
46 metrics: ['wc_conversion_rate'],
47 format: 'percent'
48 }, {
49 metrics: ['wc_earnings_per_visitor'],
50 format: 'currency'
51 }, {
52 metrics: ['wc_average_order_volume'],
53 format: 'whole_currency'
54 }, {
55 metrics: ['form_submissions'],
56 prefix_to_include: 'form_submissions_for_',
57 format: 'int'
58 }, {
59 metrics: ['form_conversion_rate'],
60 prefix_to_include: 'form_conversion_rate_for_',
61 format: 'percent'
62 }]
63
64 getLocale() {
65 // Validate the locale
66 try {
67 new Intl.NumberFormat(this.localeValue)
68
69 return this.localeValue
70 } catch (e) {
71 return 'en-US'
72 }
73 }
74
75 hasSecondaryMetric() {
76 return this.hasSecondaryChartMetricIdValue && this.secondaryChartMetricIdValue
77 }
78
79 tooltipTitle(tooltip) {
80 const label = JSON.parse(tooltip[0].label)
81
82 return label.tooltipLabel
83 }
84
85 getGroupByMetricId(metricId) {
86 return this.metricGroups.find(group => {
87 return group.metrics.includes(metricId) || (group.prefix_to_include && metricId.startsWith(group.prefix_to_include))
88 })
89 }
90
91 formatValueForMetric(metricId, value) {
92 const group = this.getGroupByMetricId(metricId)
93
94 switch (group.format) {
95 case 'whole_currency':
96 return new Intl.NumberFormat(this.localeValue, {
97 style: 'currency',
98 currency: this.currencyValue,
99 minimumFractionDigits: 0,
100 maximumFractionDigits: 0,
101 }).format(value);
102 case 'percent':
103 return new Intl.NumberFormat(this.localeValue, {
104 style: 'percent',
105 }).format(value / 100);
106 case 'time':
107 const minutes = Math.floor(value / 60);
108 const seconds = value % 60
109
110 return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
111 default:
112 return value
113 }
114 }
115
116 tooltipLabel = (tooltip) => {
117 if (typeof tooltip.dataset.tooltipLabel === 'function') {
118 return tooltip.dataset.tooltipLabel(tooltip)
119 }
120
121 return tooltip.dataset.label + ': ' + this.formatValueForMetric(tooltip.dataset.id, tooltip.raw)
122 }
123
124 tickText(value) {
125 const label = JSON.parse(this.getLabelForValue(value))
126
127 return label.tick
128 }
129
130 /**
131 * This works because we have a separate hidden select with a single option. When we set the newly
132 * selected option as it's only option, we can see exactly how long the select needs to be
133 */
134 updateMetricSelectWidth(element) {
135 const option = element.options[element.selectedIndex]
136
137 this.adaptiveWidthSelectTarget[0].innerHTML = option.innerText
138 element.style.width = this.adaptiveWidthSelectTarget.getBoundingClientRect().width + 'px'
139 element.parentElement.classList.add('visible');
140 }
141
142 hasSharedAxis(metricId, otherMetricId) {
143 const group = this.getGroupByMetricId(metricId)
144 const otherGroup = this.getGroupByMetricId(otherMetricId)
145
146 return JSON.stringify(group) === JSON.stringify(otherGroup)
147 }
148
149 connect() {
150 if(!this.isPreviewValue) {
151 this.updateMetricSelectWidth(this.primaryMetricSelectTarget)
152 this.updateMetricSelectWidth(this.secondaryMetricSelectTarget)
153 }
154 this.createChart()
155 this.updateChart()
156 }
157
158 changePrimaryMetric(e) {
159 const element = e.target
160 this.primaryChartMetricIdValue = element.value
161 this.primaryChartMetricNameValue = element.options[element.selectedIndex].innerText
162 this.updateMetricSelectWidth(element)
163 this.updateChart()
164
165 Array.from(this.secondaryMetricSelectTarget.querySelectorAll('option')).forEach((option) => {
166 option.toggleAttribute('disabled', option.value === element.value)
167 })
168
169 document.dispatchEvent(
170 new CustomEvent('iawp:changePrimaryChartMetric', {
171 detail: {
172 primaryChartMetricId: element.value
173 }
174 })
175 )
176 }
177
178 changeSecondaryMetric(e) {
179 const element = e.target
180 const hasSelectedSecondaryMetric = element.value !== ''
181
182 if (hasSelectedSecondaryMetric) {
183 this.secondaryChartMetricIdValue = element.value
184 this.secondaryChartMetricNameValue = element.options[element.selectedIndex].innerText
185 } else {
186 this.secondaryChartMetricIdValue = ''
187 this.secondaryChartMetricNameValue = ''
188 }
189
190 this.updateMetricSelectWidth(element)
191 this.updateChart()
192
193 Array.from(this.primaryMetricSelectTarget.querySelectorAll('option')).forEach((option) => {
194 option.toggleAttribute('disabled', option.value === element.value)
195 })
196
197 document.dispatchEvent(
198 new CustomEvent('iawp:changeSecondaryChartMetric', {
199 detail: {
200 secondaryChartMetricId: hasSelectedSecondaryMetric ? element.value : null
201 }
202 })
203 )
204 }
205
206 updateChart() {
207 const primaryDataset = window.iawp_chart.data.datasets[0]
208
209 primaryDataset.id = this.primaryChartMetricIdValue
210 primaryDataset.data = this.dataValue[this.primaryChartMetricIdValue]
211 primaryDataset.label = this.primaryChartMetricNameValue
212
213 // Always start by removing the secondary dataset
214 if (window.iawp_chart.data.datasets.length > 1) {
215 window.iawp_chart.data.datasets.pop()
216 }
217
218 if (this.hasSecondaryMetric()) {
219 const id = this.secondaryChartMetricIdValue
220 const name = this.secondaryChartMetricNameValue
221 const data = this.dataValue[id]
222 const axisId = this.hasSharedAxis(this.primaryChartMetricIdValue, id) ? 'y' : 'defaultRight'
223
224 window.iawp_chart.data.datasets.push(
225 this.makeDataset(id, name, data, axisId, 'rgba(246,157,10)')
226 )
227 }
228
229 window.iawp_chart.update();
230 }
231
232 makeDataset(id, name, data, axisId, colorValue, isPrimary = false) {
233 const accentColor = color(colorValue)
234
235 return {
236 id: id,
237 label: name,
238 data: data,
239 borderColor: accentColor.string(),
240 backgroundColor: accentColor.alpha(.1).string(),
241 pointBackgroundColor: accentColor.string(),
242 tension: 0.4,
243 yAxisID: axisId,
244 fill: true,
245 order: isPrimary ? 1 : 0, // Stack orange on top of purple
246 }
247 }
248
249 createChart() {
250 Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
251 const element = document.getElementById('myChart');
252 const labels = this.labelsValue
253
254 const primaryMetricDataset = this.makeDataset(
255 this.primaryChartMetricIdValue,
256 this.primaryChartMetricNameValue,
257 this.dataValue[this.primaryChartMetricIdValue],
258 'y',
259 'rgba(108,70,174)',
260 true
261 )
262
263 const data = {
264 labels: labels,
265 datasets: ([
266 primaryMetricDataset
267 ]).filter((dataset) => dataset !== null)
268 };
269
270 const options = {
271 locale: this.getLocale(),
272 interaction: {
273 intersect: false,
274 mode: 'index'
275 },
276 scales: {
277 y: {
278 grid: {
279 borderColor: '#DEDAE6',
280 tickColor: '#DEDAE6',
281 display: true,
282 drawOnChartArea: true,
283 borderDash: [2, 4]
284 },
285 beginAtZero: true,
286 suggestedMax: 10,
287 ticks: {
288 color: document.body.classList.contains('iawp-dark-mode') ? '#ffffff' : '#6D6A73',
289 font: {
290 size: 14,
291 weight: 400,
292 },
293 precision: 0,
294 callback: (value, index, values) => {
295 return this.formatValueForMetric(this.primaryChartMetricIdValue, value)
296 },
297 },
298 },
299 defaultRight: {
300 position: 'right',
301 display: 'auto',
302 grid: {
303 borderColor: '#DEDAE6',
304 tickColor: '#DEDAE6',
305 display: true,
306 drawOnChartArea: false,
307 borderDash: [2, 4]
308 },
309 beginAtZero: true,
310 suggestedMax: 10,
311 ticks: {
312 color: document.body.classList.contains('iawp-dark-mode') ? '#ffffff' : '#6D6A73',
313 font: {
314 size: 14,
315 weight: 400,
316 },
317 precision: 0,
318 callback: (value, index, values) => {
319 if (this.hasSecondaryMetric()) {
320 return this.formatValueForMetric(this.secondaryChartMetricIdValue, value)
321 } else {
322 return value
323 }
324 },
325 },
326 },
327 x: {
328 grid: {
329 borderColor: '#DEDAE6',
330 tickColor: '#DEDAE6',
331 display: true,
332 drawOnChartArea: false,
333 },
334 ticks: {
335 color: document.body.classList.contains('iawp-dark-mode') ? '#ffffff' : '#6D6A73',
336 autoSkip: true,
337 autoSkipPadding: 16,
338 maxRotation: 0,
339 // maxTicksLimit: 20,
340 font: {
341 size: 14,
342 weight: 400
343 },
344 callback: this.tickText
345 },
346 },
347 },
348 plugins: {
349 mode: String, // 'light' or 'dark'
350 legend: {
351 display: false
352 },
353 corsair: {
354 dash: [2, 4],
355 color: '#777',
356 width: 1
357 },
358 tooltip: {
359 itemSort: (a, b) => {
360 return a.datasetIndex < b.datasetIndex ? -1 : 1
361 },
362 callbacks: {
363 title: this.tooltipTitle,
364 label: this.tooltipLabel,
365 }
366 }
367 },
368 elements: {
369 point: {
370 radius: 4
371 }
372 }
373 }
374
375 const config = {
376 type: 'line',
377 data: data,
378 options: options,
379 plugins: [
380 corsairPlugin
381
382 ],
383 };
384
385 window.iawp_chart = new Chart(element, config);
386 }
387 }