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