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