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