PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.7.0
Independent Analytics – WordPress Analytics Plugin v2.7.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 pruner_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
419 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 && this.secondaryChartMetricIdValue !== 'no_comparison'
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 currencyDisplay: 'narrowSymbol',
100 minimumFractionDigits: 0,
101 maximumFractionDigits: 0,
102 }).format(value);
103 case 'currency':
104 return new Intl.NumberFormat(this.localeValue, {
105 style: 'currency',
106 currency: this.currencyValue,
107 currencyDisplay: 'narrowSymbol',
108 minimumFractionDigits: 2,
109 maximumFractionDigits: 2,
110 }).format(value);
111 case 'percent':
112 return new Intl.NumberFormat(this.localeValue, {
113 style: 'percent',
114 maximumFractionDigits: 2,
115 }).format(value / 100);
116 case 'time':
117 const minutes = Math.floor(value / 60);
118 const seconds = value % 60
119
120 return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
121 case 'int':
122 return new Intl.NumberFormat(this.localeValue, {
123 maximumFractionDigits: 0
124 }).format(value);
125 case 'float':
126 return new Intl.NumberFormat(this.localeValue, {
127 maximumFractionDigits: 2
128 }).format(value);
129 default:
130 return value
131 }
132 }
133
134 tooltipLabel = (tooltip) => {
135 if (typeof tooltip.dataset.tooltipLabel === 'function') {
136 return tooltip.dataset.tooltipLabel(tooltip)
137 }
138
139 return tooltip.dataset.label + ': ' + this.formatValueForMetric(tooltip.dataset.id, tooltip.raw)
140 }
141
142 tickText(value) {
143 const label = JSON.parse(this.getLabelForValue(value))
144
145 return label.tick
146 }
147
148 /**
149 * This works because we have a separate hidden select with a single option. When we set the newly
150 * selected option as it's only option, we can see exactly how long the select needs to be
151 */
152 updateMetricSelectWidth(element) {
153 const option = element.options[element.selectedIndex]
154
155 this.adaptiveWidthSelectTarget[0].innerHTML = option.innerText
156 element.style.width = this.adaptiveWidthSelectTarget.getBoundingClientRect().width + 'px'
157 element.parentElement.classList.add('visible');
158 }
159
160 hasSharedAxis(metricId, otherMetricId) {
161 const group = this.getGroupByMetricId(metricId)
162 const otherGroup = this.getGroupByMetricId(otherMetricId)
163
164 return JSON.stringify(group) === JSON.stringify(otherGroup)
165 }
166
167 connect() {
168 if(!this.isPreviewValue) {
169 this.updateMetricSelectWidth(this.primaryMetricSelectTarget)
170 this.updateMetricSelectWidth(this.secondaryMetricSelectTarget)
171 }
172 this.createChart()
173 this.updateChart()
174 }
175
176 changePrimaryMetric(e) {
177 const element = e.target
178 this.primaryChartMetricIdValue = element.value
179 this.primaryChartMetricNameValue = element.options[element.selectedIndex].innerText
180 this.updateMetricSelectWidth(element)
181 this.updateChart()
182
183 Array.from(this.secondaryMetricSelectTarget.querySelectorAll('option')).forEach((option) => {
184 option.toggleAttribute('disabled', option.value === element.value)
185 })
186
187 document.dispatchEvent(
188 new CustomEvent('iawp:changePrimaryChartMetric', {
189 detail: {
190 primaryChartMetricId: element.value
191 }
192 })
193 )
194 }
195
196 changeSecondaryMetric(e) {
197 const element = e.target
198 const hasSelectedSecondaryMetric = element.value !== ''
199
200 if (hasSelectedSecondaryMetric) {
201 this.secondaryChartMetricIdValue = element.value
202 this.secondaryChartMetricNameValue = element.options[element.selectedIndex].innerText
203 } else {
204 this.secondaryChartMetricIdValue = ''
205 this.secondaryChartMetricNameValue = ''
206 }
207
208 this.updateMetricSelectWidth(element)
209 this.updateChart()
210
211 Array.from(this.primaryMetricSelectTarget.querySelectorAll('option')).forEach((option) => {
212 option.toggleAttribute('disabled', option.value === element.value)
213 })
214
215 document.dispatchEvent(
216 new CustomEvent('iawp:changeSecondaryChartMetric', {
217 detail: {
218 secondaryChartMetricId: hasSelectedSecondaryMetric ? element.value : null
219 }
220 })
221 )
222 }
223
224 updateChart() {
225 const primaryDataset = window.iawp_chart.data.datasets[0]
226
227 primaryDataset.id = this.primaryChartMetricIdValue
228 primaryDataset.data = this.dataValue[this.primaryChartMetricIdValue]
229 primaryDataset.label = this.primaryChartMetricNameValue
230
231 const isEmptyPrimaryDataset = primaryDataset.data.every((value) => value === 0)
232 window.iawp_chart.options.scales['y'].suggestedMax = isEmptyPrimaryDataset ? 10 : null
233 window.iawp_chart.options.scales['y'].beginAtZero = primaryDataset.id !== 'bounce_rate'
234
235 // Always start by removing the secondary dataset
236 if (window.iawp_chart.data.datasets.length > 1) {
237 window.iawp_chart.data.datasets.pop()
238 }
239
240 if (this.hasSecondaryMetric()) {
241 const id = this.secondaryChartMetricIdValue
242 const name = this.secondaryChartMetricNameValue
243 const data = this.dataValue[id]
244 const axisId = this.hasSharedAxis(this.primaryChartMetricIdValue, id) ? 'y' : 'defaultRight'
245
246 window.iawp_chart.data.datasets.push(
247 this.makeDataset(id, name, data, axisId, 'rgba(246,157,10)')
248 )
249
250 const isEmptySecondaryDataset = data.every((value) => value === 0)
251 window.iawp_chart.options.scales['defaultRight'].suggestedMax = isEmptySecondaryDataset ? 10 : null
252 window.iawp_chart.options.scales['defaultRight'].beginAtZero = id !== 'bounce_rate'
253 }
254
255 window.iawp_chart.update();
256 }
257
258 makeDataset(id, name, data, axisId, colorValue, isPrimary = false) {
259 const accentColor = color(colorValue)
260
261 return {
262 id: id,
263 label: name,
264 data: data,
265 borderColor: accentColor.string(),
266 backgroundColor: accentColor.alpha(.1).string(),
267 pointBackgroundColor: accentColor.string(),
268 tension: 0.4,
269 yAxisID: axisId,
270 fill: true,
271 order: isPrimary ? 1 : 0, // Stack orange on top of purple
272 }
273 }
274
275 shouldUseDarkMode() {
276 return document.body.classList.contains('iawp-dark-mode') && !this.isPreviewValue
277 }
278
279 createChart() {
280 Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
281 const element = document.getElementById('myChart');
282 const labels = this.labelsValue
283
284 const primaryMetricDataset = this.makeDataset(
285 this.primaryChartMetricIdValue,
286 this.primaryChartMetricNameValue,
287 this.dataValue[this.primaryChartMetricIdValue],
288 'y',
289 'rgba(108,70,174)',
290 true
291 )
292
293 const data = {
294 labels: labels,
295 datasets: ([
296 primaryMetricDataset
297 ]).filter((dataset) => dataset !== null)
298 };
299
300 const options = {
301 locale: this.getLocale(),
302 interaction: {
303 intersect: false,
304 mode: 'index'
305 },
306 scales: {
307 y: {
308 grid: {
309 color: this.shouldUseDarkMode() ? '#676173' : '#DEDAE6',
310 borderColor: '#DEDAE6',
311 tickColor: '#DEDAE6',
312 display: true,
313 drawOnChartArea: true,
314 borderDash: [2, 4]
315 },
316 beginAtZero: true,
317 suggestedMax: null,
318 ticks: {
319 color: this.shouldUseDarkMode() ? '#ffffff' : '#6D6A73',
320 font: {
321 size: 14,
322 weight: 400,
323 },
324 precision: 0,
325 callback: (value, index, values) => {
326 return this.formatValueForMetric(this.primaryChartMetricIdValue, value)
327 },
328 },
329 },
330 defaultRight: {
331 position: 'right',
332 display: 'auto',
333 grid: {
334 color: this.shouldUseDarkMode() ? '#9a95a6' : '#DEDAE6',
335 borderColor: '#DEDAE6',
336 tickColor: '#DEDAE6',
337 display: true,
338 drawOnChartArea: false,
339 borderDash: [2, 4]
340 },
341 beginAtZero: true,
342 suggestedMax: null,
343 ticks: {
344 color: this.shouldUseDarkMode() ? '#ffffff' : '#6D6A73',
345 font: {
346 size: 14,
347 weight: 400,
348 },
349 precision: 0,
350 callback: (value, index, values) => {
351 if (this.hasSecondaryMetric()) {
352 return this.formatValueForMetric(this.secondaryChartMetricIdValue, value)
353 } else {
354 return value
355 }
356 },
357 },
358 },
359 x: {
360 grid: {
361 borderColor: '#DEDAE6',
362 tickColor: '#DEDAE6',
363 display: true,
364 drawOnChartArea: false,
365 },
366 ticks: {
367 color: this.shouldUseDarkMode() ? '#ffffff' : '#6D6A73',
368 autoSkip: true,
369 autoSkipPadding: 16,
370 maxRotation: 0,
371 // maxTicksLimit: 20,
372 font: {
373 size: 14,
374 weight: 400
375 },
376 callback: this.tickText
377 },
378 },
379 },
380 plugins: {
381 mode: String, // 'light' or 'dark'
382 legend: {
383 display: false
384 },
385 corsair: {
386 dash: [2, 4],
387 color: '#777',
388 width: 1
389 },
390 tooltip: {
391 itemSort: (a, b) => {
392 return a.datasetIndex < b.datasetIndex ? -1 : 1
393 },
394 callbacks: {
395 title: this.tooltipTitle,
396 label: this.tooltipLabel,
397 }
398 }
399 },
400 elements: {
401 point: {
402 radius: 4
403 }
404 }
405 }
406
407 const config = {
408 type: 'line',
409 data: data,
410 options: options,
411 plugins: [
412 corsairPlugin
413
414 ],
415 };
416
417 window.iawp_chart = new Chart(element, config);
418 }
419 }