1import { Chart, registerables } from 'chart.js'; 2 3Chart.register(...registerables); 4 5import { LineWithErrorBarsChart, ScatterWithErrorBarsChart } from 'chartjs-chart-error-bars'; 6 7import regression from 'regression'; 8 9import ChartDataLabels from 'chartjs-plugin-datalabels'; 10Chart.register(ChartDataLabels); 11 12import annotationPlugin from 'chartjs-plugin-annotation'; 13Chart.register(annotationPlugin); 14 15import { Controller } from '@hotwired/stimulus'; 16 17export default class extends Controller { 18 static values = { 19 config: String 20 }; 21 22 connect() { 23 var e = this.element 24 25 const config = JSON.parse(this.configValue); 26 27 switch (config.tipo) { 28 29 case 'bar': 30 case 'line': 31 var chart = new Chart(e, { 32 type: config.tipo, 33 data: { 34 labels: config.labels, 35 }, 36 options: { 37 aspectRatio: config.aspect, 38 maintainAspectRatio: false, 39 responsive: true, 40 layout: { 41 padding: { 42 top: 20, 43 bottom: 20, 44 left: 0, 45 right: 0, 46 }, 47 }, 48 indexAxis: config.flip? 'y': 'x', 49 scales: { 50 x: { 51 stacked: config.stacked, 52 display: !config.thumb, 53 max: config.xMax ?? undefined, 54 grid: { 55 color: ({ tick }) => tick?.value == 0 ? (config.flip? config.gridZeroYColor: config.gridZeroXColor) : (config.flip? config.gridYColor: config.gridXColor), 56 }, 57 title: { 58 display: config.scaleXLabel != '', 59 text: config.scaleXLabel, 60 }, 61 }, 62 y: { 63 stacked: config.stacked, 64 display: !config.thumb, 65 max: config.yMax ?? undefined, 66 grid: { 67 color: ({ tick }) => tick?.value == 0 ? (config.flip? config.gridZeroXColor: config.gridZeroYColor) : (config.flip? config.gridXColor: config.gridYColor), 68 }, 69 border: { 70 color: config.flip? config.gridEdgeXColor: config.gridEdgeYColor, 71 }, 72 title: { 73 display: config.scaleYLabel != '', 74 text: config.scaleYLabel, 75 }, 76 }, 77 }, 78 plugins: { 79 title: { 80 display: config.title != null, 81 text: config.title, 82 padding: { bottom: 30 }, 83 }, 84 legend: { 85 display: !config.thumb && ( 86 config.legend || config.stacked || config.series.length > 1 87 ), 88 position: config.legendPosition, 89 labels: { 90 usePointStyle: true, 91 generateLabels: (chart) => { 92 return chart.data.datasets.map((dataset, i) => { 93 const meta = chart.getDatasetMeta(i); 94 const backgroundColor = Array.isArray(dataset.backgroundColor)? dataset.backgroundColor[i]: dataset.backgroundColor; 95 const borderColor = Array.isArray(dataset.borderColor)? dataset.borderColor[i]: dataset.borderColor; 96 const borderWidth = Array.isArray(dataset.borderWidth)? dataset.borderWidth[i]: dataset.borderWidth; 97 98 var ps = 'rect'; 99 if (config.subtipos[i] == 'line') { 100 var ps = 'line'; 101 if (config.pointRadius[i % config.pointRadius.length] > 0) { 102 var svg; 103 if (config.pointStyle[i % config.pointStyle.length] != 'circle') { 104 svg = ` 105 <svg width="20" height="50" xmlns="http://www.w3.org/2000/svg"> 106 <line x1="0" y1="25" x2="32" y2="25" stroke="${borderColor}" stroke-width="${borderWidth}"/> 107 <rect x="7" y="22" width="6" height="6" fill="${backgroundColor}" stroke="${borderColor}" stroke-width="1"/> 108 </svg>`; 109 } else { 110 svg = ` 111 <svg width="20" height="50" xmlns="http://www.w3.org/2000/svg"> 112 <line x1="0" y1="25" x2="32" y2="25" stroke="${borderColor}" stroke-width="${borderWidth}"/> 113 <circle cx="10" cy="25" r="3" fill="${backgroundColor}" stroke="${borderColor}" stroke-width="1"/> 114 </svg>`; 115 } 116 let blob = new Blob([svg], {type: 'image/svg+xml'}); 117 let url = URL.createObjectURL(blob); 118 119 var ps = document.createElement('img'); 120 ps.src = url; 121 ps.addEventListener('load', () => URL.revokeObjectURL(url), {once: true}); 122 } 123 } 124 125 // https://www.chartjs.org/docs/latest/configuration/legend.html#legend-item-interface 126 return { 127 // Base 128 datasetIndex: i, 129 hidden: meta.hidden, 130 strokeStyle: borderColor, 131 fillStyle: backgroundColor, 132 text: dataset.label, 133 fontColor: '#666', 134 // Custom style 135 pointStyle: ps, 136 lineDash: config.borderDash[i % config.borderDash.length], 137 lineDashOffset: 4, 138 lineWidth: dataset.borderWidth, 139 }; 140 }); 141 }, 142 }, 143 }, 144 datalabels: { 145 display: config.datalabels && !(config.stacked) && !(config.thumb), 146 clamp: true, 147 color: '#666', 148 anchor: 'end', 149 align: config.flip? '10': 'top', 150 formatter: function (val, ctx) { 151 if (val == null) { 152 return null; 153 } 154 if (config.unit != '') { 155 return val + ' ' + config.unit; 156 } 157 return val; 158 }, 159 }, 160 tooltip: { 161 callbacks: { 162 label: function(ctx) { 163 if (config.unit != '') { 164 return ctx.formattedValue + ' ' + config.unit; 165 } 166 } 167 } 168 },
169 beforeInit: function(chart, options) { 170 chart.legend.afterFit = function() { 171 this.height = this.height + 200; 172 }; 173 } 174 }, 175 animation: { 176 onComplete: function(ctx) { 177 const scales = ctx.chart.scales 178 179 // Fondos sombreados 180 if (ctx.initial && config.shadesX.length > 0) { 181 config.shadesX.forEach(function(j, serie) { 182 ctx.chart.options.plugins.annotation.annotations['' + serie] = { 183 type: 'box', 184 xMin: j[0], 185 xMax: j[1], 186 yMin: scales.y.min, 187 yMax: scales.y.max, 188 backgroundColor: '#ECF1F4', 189 borderWidth: 0, 190 drawTime: 'beforeDraw', 191 } 192 }) 193 194 chart.update(); 195 } 196 // Flechas 197 if (ctx.initial && config.arrows.length > 0) { 198 config.arrows.forEach(function(j, serie) { 199 j.forEach(function(i, el) { 200 const ratio_x = (scales.x.max - scales.x.min) / scales.x.width 201 const ratio_y = (scales.y.max - scales.y.min) / scales.y.height 202 203 const arrow_pos = Number(i[0]) - .5 + (serie * (1 / config.bySerie.length)) + (.5 / config.bySerie.length) 204 // ^ valor ^ al margen izq del valor ^ al centro del valor 205 // ^ al margen izq de la serie 206 207 //console.log("serie: " + config.series[serie] + " (" + serie + ")", "valor: " + i[0], "pos: " + x_pos) 208 209 ctx.chart.options.plugins.annotation.annotations['' + serie + el] = { 210 type: 'line', 211 arrowHeads: { 212 start: { display: i[1] < 0 }, 213 end: { display: i[1] > 0 }, 214 }, 215 borderColor: i[1] > 0? '#22B14C': '#FF7F27', 216 borderWidth: 3, 217 218 xMin: config.flip? 219 Number(config.bySerie[serie][i[0]]) + ratio_x * 40: 220 arrow_pos, 221 222 xMax: config.flip? 223 Number(config.bySerie[serie][i[0]]) + ratio_x * 70: 224 arrow_pos, 225 226 yMin: config.flip? 227 arrow_pos: 228 Number(config.bySerie[serie][i[0]]) + ratio_y * 30, 229 230 yMax: config.flip? 231 arrow_pos: 232 Number(config.bySerie[serie][i[0]]) + ratio_y * 60, 233 } 234 }) 235 }) 236
237 chart.update(); 238 } 239 }, 240 }, 241 onClick: (event, elements, chart) => { 242 if (config.link != '') { 243 window.location = config.link; 244 } 245 }, 246 }, 247 plugins: [{ 248 beforeInit: function(chart) { 249 chart.data.labels.forEach(function(e, i, a) { 250 a[i] = e.split('|'); 251 }); 252 }, 253 }], 254 }) 255 256 if (config.disableAnimation) { 257 chart.options.animation = false; 258 } 259 260 for (var i = 0; i < config.bySerie.length; i++) { 261 262 chart.data.datasets.push({ 263 barPercentage: .9, 264 categoryPercentage: .975, 265 type: config.subtipos[i], 266 label: config.series[i], 267 data: config.bySerie[i], 268 lineTension: 0, 269 spanGaps: false, 270 backgroundColor: config.backgroundColor[i % config.backgroundColor.length], 271 borderColor: config.borderColor[i % config.borderColor.length], 272 borderWidth: config.borderWidth[i % config.borderWidth.length], 273 pointStyle: config.pointStyle[i % config.pointStyle.length], 274 pointRadius: config.pointRadius[i % config.pointRadius.length],
275 pointBackgroundColor: config.pointBackgroundColor[i % config.pointBackgroundColor.length ], 276 pointBorderWidth: config.pointBorderWidth[i % config.pointBorderWidth.length], 277 borderDash: config.borderDash[i % config.borderDash.length], 278 fill: config.fill? 'origin': false, 279 order: config.order? config.order[i]: i, 280 stack: (config.subtipos[i] == 'bar' || config.fill )? 'stack_group': 'stack_' + i, 281 }) } 282 283 chart.update(); 284 285 break; 286 287 case 'pie': 288 case 'doughnut': 289 290 var chart = new Chart(e, { 291 type: config.tipo, 292 data: { 293 labels: config.labels, 294 }, 295 options: { 296 aspectRatio: config.aspect, 297 maintainAspectRatio: false, 298 responsive: true, 299 layout: { 300 padding: { 301 top: 20, 302 bottom: 20, 303 left: 70, 304 right: 70, 305 }, 306 }, 307 plugins: { 308 title: { 309 display: config.title != null, 310 text: (config.title), 311 padding: { bottom: 30 }, 312 }, 313 legend: { 314 display: config.legend || config.stacked, 315 position: config.legendPosition, 316 }, 317 datalabels: { 318 display: config.datalabels, 319 clamp: true, 320 color: '#666', 321 anchor: 'end', 322 align: 'end', 323 formatter: function (val, ctx) { 324 return val == 0? null: ctx.chart.data.labels[ctx.dataIndex]; 325 }, 326 }, 327 tooltip: { 328 callbacks: { 329 label: function(ctx) { 330 if (config.unit != '') { 331 return ctx.formattedValue + ' ' + config.unit; 332 } 333 if ((typeof config.unit) == 'undefined') { 334 return ctx.parsed + ' %'; 335 } 336 return ctx.parsed; 337 } 338 } 339 } 340 }, 341 }, 342 plugins: [{ 343 beforeInit: function(chart) { 344 chart.data.labels.forEach(function(e, i, a) { 345 a[i] = e.split('|'); 346 }); 347 }, 348 }], 349 }) 350 351 if (config.disableAnimation) { 352 chart.options.animation = false; 353 } 354 355 for (var i = 0; i < config.bySerie.length; i++) { 356 chart.data.datasets.push({ 357 label: config.series[i], 358 data: config.bySerie[i], 359 backgroundColor: Array.isArray(config.backgroundColor[0])? config.backgroundColor[i]: config.backgroundColor, 360 borderColor: config.borderColor, 361 borderWidth: config.borderWidth, 362 fill: false, 363 pointRadius: 1, 364 lineTension: 0, 365 spanGaps: false, 366 }); 367 }
368 chart.update(); 369 370 break; 371 372 case 'lineWithErrorBars': 373 case 'scatterWithErrorBars': 374 375 var errorData = [] 376 config.series.forEach(function(el, i) { 377 errorData.push([]) 378 Object.keys(config.raw).forEach(function(j, el) { 379 if (config.flip) { 380 errorData[i].push({ 381 x: (config.raw[j][0 + i * 3] == null)? null: Number(config.raw[j][0 + i * 3]), 382 xMin: (config.raw[j][0 + i * 3] == null)? null: Number(config.raw[j][1 + i * 3]), 383 xMax: (config.raw[j][0 + i * 3] == null)? null: Number(config.raw[j][2 + i * 3]), 384 }) 385 } else { 386 errorData[i].push({ 387 y: (config.raw[j][0 + i * 3] == null)? null: Number(config.raw[j][0 + i * 3]), 388 yMin: (config.raw[j][0 + i * 3] == null)? null: Number(config.raw[j][1 + i * 3]), 389 yMax: (config.raw[j][0 + i * 3] == null)? null: Number(config.raw[j][2 + i * 3]), 390 }) 391 } 392 }) 393 }) 394 395 var chart = new LineWithErrorBarsChart(e, { 396 type: 'lineWithErrorBars', 397 data: { 398 labels: config.labels, 399 }, 400 options: { 401 aspectRatio: config.aspect, 402 maintainAspectRatio: false, 403 responsive: true, 404 layout: { 405 padding: { 406 top: 30, 407 bottom: 0, 408 left: 20, 409 right: 30, 410 }, 411 }, 412 indexAxis: config.flip? 'y': 'x', 413 plugins: { 414 title: { 415 display: config.title != null, 416 text: (config.title), 417 }, 418 legend: { 419 display: true, 420 position: 'bottom', 421 }, 422 datalabels: { 423 display: config.datalabels, 424 }, 425 tooltip: { 426 callbacks: { 427 label: function(ctx) { 428 if (config.flip) { 429 return ctx.dataset.label + ': ' + ctx.parsed.x + ' (' + ctx.parsed.xMin + ' ⦠' + ctx.parsed.xMax + ')'; 430 } else { 431 return ctx.dataset.label + ': ' + ctx.parsed.y + ' (' + ctx.parsed.yMin + ' ⦠' + ctx.parsed.yMax + ')'; 432 } 433 } 434 } 435 } 436 }, 437 }, 438 }) 439 440 if (config.disableAnimation) { 441 chart.options.animation = false; 442 } 443 444 config.series.forEach(function(el, i) { 445 chart.data.datasets.push({ 446 label: config.series[i], 447 data: errorData[i], 448 backgroundColor: config.backgroundColor[i % config.backgroundColor.length], 449 borderColor: config.borderColor[i % config.borderColor.length], 450 borderWidth: config.borderWidth[i % config.borderWidth.length], 451 showLine: false, 452 }); 453 }) 454 chart.update(); 455 456 break; 457 458 case 'scatter': 459 var data = [];
460 Object.keys(config.raw).forEach(function(x, i) { 461 config.raw[x].forEach(function(y, j) { 462 data.push({ 463 x: x, 464 y: y, 465 }) 466 }) 467 }) 468 469 var chart = new Chart(e, { 470 type: config.tipo, 471 data: { 472 datasets: [ 473 { 474 data: data, 475 backgroundColor: config.backgroundColor[0], 476 label: config.series[0] ?? null, 477 }, 478 ], 479 }, 480 options: { 481 aspectRatio: config.aspect, 482 maintainAspectRatio: false, 483 responsive: true, 484 indexAxis: config.flip? 'y': 'x', 485 scales: { 486 x: { 487 title: { 488 display: config.x != '', 489 text: config.x, 490 }, 491 }, 492 y: { 493 title: { 494 display: config.y != '', 495 text: config.y, 496 }, 497 }, 498 }, 499 plugins: { 500 title: { 501 display: config.title != null, 502 text: (config.title), 503 }, 504 legend: { 505 display: config.legend && ((config.series[0] ?? '') != ''), 506 position: 'top', 507 labels: { 508 filter: function(labelItem) { 509 return labelItem.datasetIndex < 1 510 } 511 } 512 }, 513 datalabels: { 514 display: config.datalabels, 515 }, 516 tooltip: { 517 filter: function(tooltipItem) { 518 return tooltipItem.datasetIndex < 1 519 }, 520 }, 521 }, 522 }, 523 }) 524 525 if (config.disableAnimation) { 526 chart.options.animation = false; 527 } 528 529 if (config.regression) { 530 var data_reg = []; 531 Object.keys(config.raw).forEach(function(x, i) { 532 config.raw[x].forEach(function(y, j) { 533 data_reg.push([Number(x), Number(y)]) 534 }) 535 }) 536 537 const reg = regression.linear(data_reg) 538 539 chart.data.datasets.push({ 540 type: 'line', 541 data: reg.points, 542 pointRadius: 0, 543 label: '', 544 plugins: { 545 tooltip: { 546 display: false, 547 }, 548 } 549 }); 550 551 chart.update(); 552 } 553 break; 554 555 } 556 557 } 558}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.