{"version":3,"file":"combination_chart_controller-CROBlZVZ.js","sources":["../../../app/javascript/entrypoints/controllers/combination_chart_controller.jsx"],"sourcesContent":["/** @jsx renderer.create */\nimport { Controller } from \"@hotwired/stimulus\";\nimport dayjs from \"dayjs\";\n\nimport {\n Chart,\n CategoryScale,\n LinearScale,\n PointElement,\n LineElement,\n Title,\n Tooltip,\n Legend,\n} from \"chart.js\";\nimport ChartDataLabels from \"chartjs-plugin-datalabels\";\n\nimport _, { first } from \"lodash\";\n\nimport { useWindowResize } from \"stimulus-use\";\nimport { useResize } from \"stimulus-use\";\nimport currency from \"currency.js\";\n\nimport numbro from \"numbro\";\n\nimport { CommonDOMRenderer } from \"render-jsx/dom\";\n\nimport { numberFormatter, ticksCallback } from \"../utils\";\n\nconst tooltipWidth = 350;\n\nChart.register(\n CategoryScale,\n LinearScale,\n PointElement,\n LineElement,\n Title,\n Tooltip,\n Legend,\n ChartDataLabels\n);\n\nexport default class extends Controller {\n chart = null;\n initial = false;\n values = [];\n labels = [];\n actualLabels = [];\n\n static targets = [\"chart\"];\n\n static values = {\n jsonData: Object,\n };\n\n initialize() {\n this.resize = _.debounce(this.resize, 250).bind(this);\n this.windowResize = _.debounce(this.windowResize, 250).bind(this);\n\n Chart.defaults.plugins.legend.onHover = function () {\n document.body.style.cursor = \"pointer\";\n };\n\n Chart.defaults.plugins.legend.onLeave = function () {\n document.body.style.cursor = \"unset\";\n };\n }\n\n chartTargetDisconnect(element) {\n const chart = Chart.getChart(element.id);\n if (chart) {\n chart.destroy();\n }\n }\n\n parseData() {\n const parsedData = this.jsonDataValue;\n const parsedEntries = parsedData.entries.map((entry) => JSON.parse(entry));\n const colors = parsedEntries.map((entry) => entry.color);\n\n const datasets = parsedEntries.map((entry, index) => ({\n type: \"line\",\n label: entry.name,\n data: Object.values(entry.rows),\n backgroundColor: entry.color,\n segmentColor: entry.color,\n borderColor: entry.color,\n ...this.baseDatalabelOptions(entry.color),\n yAxisID: `y${index == 0 ? \"\" : index}`,\n }));\n\n const firstDataset = JSON.parse(parsedData.entries[0]);\n const data = firstDataset.rows;\n\n this.values = datasets;\n\n this.sameScale = firstDataset.same_scale;\n this.dataType = firstDataset.data_type;\n this.decimalPlaces = firstDataset.decimal_places;\n this.tracking = firstDataset.tracking;\n this.dateRange = firstDataset.date_range;\n this.colors = parsedEntries.map((entry) => entry.color);\n this.reversed = parsedEntries.map((entry) => entry.upside_down);\n\n this.min = parsedEntries.map((entry) => entry.min);\n this.max = parsedEntries.map((entry) => entry.max);\n\n if (this.sameScale) {\n this.min = Math.min(...this.min);\n this.max = Math.max(...this.max);\n }\n\n if (this.tracking == \"week\") {\n this.labels = Object.keys(firstDataset.rows).map(\n (key) => firstDataset.end_of_week[key]\n );\n\n this.actualLabels = parsedEntries.map((entry) =>\n Object.keys(entry.rows).map((key) => entry.end_of_week[key])\n );\n } else {\n this.labels = Object.keys(firstDataset.rows) || [];\n this.actualLabels = parsedEntries.map((entry) => Object.keys(entry.rows));\n }\n\n return { data, colors };\n }\n\n jsonDataValueChanged() {\n if (!this.initial) {\n this.initial = true;\n } else {\n let chart = Chart.getChart(\"canvas-element\");\n\n const { data, colors } = this.parseData();\n\n if (chart) {\n chart.data.labels = this.labels;\n chart.data.datasets = this.values;\n\n console.log(\"options\", this.options);\n chart.options = this.options(data, colors);\n\n chart.update(\"reset\");\n chart.update(\"normal\");\n } else {\n const ctx = document.getElementById(\"canvas-element\").getContext(\"2d\");\n this.createChart(ctx, data, this.element, colors);\n }\n }\n }\n\n chartTargetConnected(element) {\n useWindowResize(this);\n useResize(this);\n\n const parsedData = this.jsonDataValue;\n\n if (_.isEmpty(parsedData)) {\n return;\n }\n\n const ctx = document.getElementById(element.id).getContext(\"2d\");\n const { data, colors } = this.parseData();\n\n if (_.isEmpty(this.labels) || _.isEmpty(this.values)) {\n return;\n }\n\n this.createChart(ctx, data, element, colors);\n }\n\n baseDatalabelOptions = (color) => ({\n datalabels: {\n display: true,\n color: color,\n textStrokeWidth: 0.5,\n align: \"top\",\n offset: 12,\n font: {\n size: 14,\n },\n },\n });\n\n createChart = (ctx, data, element, colors) => {\n let delayed;\n\n new Chart(ctx, {\n type: \"line\",\n data: {\n labels: this.labels,\n datasets: this.values,\n },\n options: {\n animation: {\n onComplete: () => {\n delayed = true;\n },\n delay: (context) => {\n let delay = 0;\n if (\n context.type === \"data\" &&\n context.mode === \"default\" &&\n !delayed\n ) {\n delay = context.dataIndex * 50 + context.datasetIndex * 25;\n }\n\n return delay;\n },\n },\n font: {\n family: \"Inter var\",\n size: 14,\n },\n ...this.options(data, colors),\n },\n });\n };\n\n resize() {\n [...this.chartTargets].forEach((element) => {\n const chart = Chart.getChart(element.id);\n if (chart) {\n chart.resize();\n }\n });\n }\n\n windowResize({ width, height, event }) {\n [...this.chartTargets].forEach((element) => {\n const chart = Chart.getChart(element.id);\n if (chart) {\n chart.resize();\n }\n });\n }\n\n options = (data, colors) => {\n return {\n responsive: true,\n maintainAspectRatio: true,\n stacked: false,\n interaction: {\n mode: \"index\",\n },\n plugins: {\n legend: {\n position: \"top\",\n maxWidth: \"100vw\",\n title: {\n text: \"\",\n color: \"black\",\n display: false,\n font: { size: 12, weight: \"normal\" },\n },\n labels: {\n usePointStyle: true,\n color: \"black\",\n padding: 20,\n font: {\n size: 16,\n family: \"Inter var\",\n },\n },\n },\n tooltip: {\n enabled: false,\n position: \"nearest\",\n external: this.externalTooltipHandler,\n },\n datalabels: {\n formatter: (value, context) => {\n try {\n if (context.dataset.data[context.dataIndex] == \"NR\") {\n return context.dataset.data[context.dataIndex];\n }\n\n if (this.dataType === \"currency\") {\n return currency(context.dataset.data[context.dataIndex] || 0, {\n precision: this.decimalPlaces,\n }).format();\n }\n\n if (this.dataType === \"percentage\") {\n return `${context.dataset.data[context.dataIndex] || 0}%`;\n }\n\n return numbro(\n context.dataset.data[context.dataIndex] || 0\n ).format({\n thousandSeparated: true,\n mantissa: this.decimalPlaces,\n });\n } catch (e) {\n console.log(\"error\");\n console.log(e.message);\n return context.dataset.data[context.dataIndex];\n }\n },\n },\n },\n scales: {\n x: {\n grid: {\n color: \"rgb(0, 0, 0, .20)\",\n },\n ticks: {\n includeBounds: false,\n padding: 20,\n autoSkip: false,\n color: this.dateRange ? this.colors[0] : \"rgb(0, 0, 0)\",\n font: {\n family: \"Inter var\",\n size: 14,\n weight: \"bold\",\n },\n callback: (val, index) =>\n ticksCallback(index, this.tracking, this.actualLabels[0]),\n },\n },\n ...this.additionalXAxes(),\n y: {\n ...this.defaultYSettings(),\n suggestedMin: this.sameScale ? this.min : this.min[0],\n suggestedMax: this.sameScale ? this.max : this.max[0],\n ticks: {\n ...this.defaultYSettings().ticks,\n color: this.sameScale ? \"rgb(0, 0, 0)\" : colors[0],\n reversed: this.reversed[0],\n },\n },\n ...this.additionalYAxes(colors),\n },\n };\n };\n\n additionalXAxes = () => {\n const additional = {};\n\n if (!this.dateRange) return additional;\n\n this.actualLabels.slice(1).map((_, index) => {\n additional[`x${index + 2}`] = {\n axis: \"x\",\n position: \"bottom\",\n grid: {\n color: \"rgb(0, 0, 0, .20)\",\n },\n ticks: {\n includeBounds: false,\n padding: 20,\n reverse: true,\n autoSkip: false,\n color: this.dateRange ? this.colors[index + 1] : \"rgb(0, 0, 0)\",\n font: {\n family: \"Inter var\",\n size: 14,\n weight: \"bold\",\n },\n callback: (val, idx) =>\n ticksCallback(idx, this.tracking, this.actualLabels[index + 1]),\n },\n };\n });\n\n return additional;\n };\n\n additionalYAxes = (colors) => {\n const additional = {};\n\n colors.slice(1).map((color, index) => {\n additional[`y${index + 1}`] = {\n ...this.defaultYSettings(),\n reverse: this.reversed[index + 1],\n suggestedMin: this.sameScale ? this.min : this.min[index + 1],\n suggestedMax: this.sameScale ? this.max : this.max[index + 1],\n type: \"linear\",\n axis: \"y\",\n display: !this.sameScale,\n position: index % 2 === 0 ? \"right\" : \"left\",\n grid: {\n drawOnChartArea: false,\n },\n ticks: {\n ...this.defaultYSettings().ticks,\n position: index % 2 === 0 ? \"right\" : \"left\",\n color,\n },\n };\n });\n\n return additional;\n };\n\n defaultYSettings = () => {\n return {\n type: \"linear\",\n suggestedMin: this.min || 0,\n suggestedMax: this.max || null,\n position: \"left\",\n ticks: {\n position: \"left\",\n precision: this.decimalPlaces,\n font: {\n family: \"Inter var\",\n size: 14,\n weight: \"bold\",\n },\n padding: 20,\n color: \"rgb(0, 0, 0)\",\n callback: (value, index, values) => {\n if (this.dataType === \"currency\") {\n try {\n return currency(value, {\n precision: this.decimalPlaces,\n }).format();\n } catch (_) {\n return `$${value}`;\n }\n }\n\n if (this.dataType == \"percentage\") {\n return `${value}%`;\n }\n\n return numbro(value).format({\n thousandSeparated: true,\n mantissa: this.decimalPlaces,\n });\n },\n },\n };\n };\n\n getOrCreateTooltip = (chart) => {\n let tooltipEl = chart.canvas.parentNode.querySelector(\"div\");\n\n if (!tooltipEl) {\n tooltipEl = document.createElement(\"div\");\n\n tooltipEl.classList.add(\n \"bg-gray-100\",\n \"text-black\",\n \"rounded\",\n \"shadow-lg\",\n \"border\"\n );\n\n tooltipEl.style.opacity = 1;\n tooltipEl.style.pointerEvents = \"none\";\n tooltipEl.style.position = \"absolute\";\n tooltipEl.style.transform = \"translate(-50%, 0)\";\n tooltipEl.style.transition = \"all .1s ease\";\n tooltipEl.style.zIndex = \"9999999999999999999999\";\n\n const table = document.createElement(\"table\");\n table.style.margin = \"0px\";\n\n tooltipEl.appendChild(table);\n chart.canvas.parentNode.appendChild(tooltipEl);\n }\n\n return tooltipEl;\n };\n\n externalTooltipHandler = (context) => {\n const { chart, tooltip } = context;\n const tooltipEl = this.getOrCreateTooltip(chart);\n\n tooltipEl.innerHTML = \"\";\n\n if (tooltip.opacity === 0) {\n tooltipEl.style.opacity = 0;\n return;\n }\n\n if (tooltip.body) {\n const bodyLines = tooltip.body.map((b) => b.lines);\n const titleLines = tooltip.title || [];\n const renderer = new CommonDOMRenderer();\n\n renderer\n .render(\n