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