GainLossChart.svelte 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. <script>
  2. let { data = [], onPointSelect = () => {}, selectedPeriod = null } = $props();
  3. const chartWidth = 760;
  4. const chartHeight = 320;
  5. const padding = { top: 30, right: 20, bottom: 50, left: 50 };
  6. const innerWidth = chartWidth - padding.left - padding.right;
  7. const innerHeight = chartHeight - padding.top - padding.bottom;
  8. const maxValue = $derived(Math.max(1, ...data.map((item) => Math.max(item.gains, item.losses))));
  9. const points = $derived(
  10. data.map((item, index) => {
  11. const xStep = data.length > 1 ? innerWidth / (data.length - 1) : 0;
  12. const x = padding.left + index * xStep;
  13. const gainsY = padding.top + (1 - item.gains / maxValue) * innerHeight;
  14. const lossesY = padding.top + (1 - item.losses / maxValue) * innerHeight;
  15. return { ...item, x, gainsY, lossesY };
  16. })
  17. );
  18. const gainsPath = $derived(points.map((point) => `${point.x},${point.gainsY}`).join(' '));
  19. const lossesPath = $derived(points.map((point) => `${point.x},${point.lossesY}`).join(' '));
  20. const yAxisTicks = $derived([0, 0.25, 0.5, 0.75, 1].map((tick) => Math.round(maxValue * tick)));
  21. </script>
  22. <section class="h-[460px] flex flex-col rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#1e293b] p-5 shadow-sm transition-colors duration-200">
  23. <div class="mb-6 flex items-center justify-between border-b border-slate-100 dark:border-slate-800/50 pb-3 shrink-0">
  24. <h2 class="text-sm font-bold tracking-wider text-slate-500 dark:text-slate-400 uppercase">
  25. Opinião do Público
  26. </h2>
  27. <div class="flex items-center gap-4 text-xs font-bold">
  28. <span class="flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
  29. <i class="h-2.5 w-2.5 rounded-full bg-emerald-500"></i>Positivo
  30. </span>
  31. <span class="flex items-center gap-1.5 text-red-600 dark:text-red-400">
  32. <i class="h-2.5 w-2.5 rounded-full bg-red-500"></i>Negativo
  33. </span>
  34. </div>
  35. </div>
  36. <div class="flex-1 w-full flex items-center justify-center pt-2">
  37. <svg viewBox={`0 0 ${chartWidth} ${chartHeight}`} class="h-full w-full overflow-visible">
  38. <!-- Background Grid -->
  39. {#each yAxisTicks as tick}
  40. {@const y = padding.top + (1 - tick / maxValue) * innerHeight}
  41. <line
  42. x1={padding.left}
  43. y1={y}
  44. x2={chartWidth - padding.right}
  45. y2={y}
  46. class="stroke-slate-200 dark:stroke-slate-700/80"
  47. stroke-dasharray="4 4"
  48. />
  49. <text x={padding.left - 12} y={y + 4} text-anchor="end" class="fill-slate-500 dark:fill-slate-400 text-xs font-medium">
  50. {tick}
  51. </text>
  52. {/each}
  53. <!-- Lines -->
  54. <polyline fill="none" points={gainsPath} stroke="#10b981" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="drop-shadow-sm" />
  55. <polyline fill="none" points={lossesPath} stroke="#ef4444" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="drop-shadow-sm" />
  56. <!-- Points -->
  57. {#each points as point}
  58. <g
  59. role="button"
  60. tabindex="0"
  61. aria-label={`Selecionar periodo ${point.period}`}
  62. onclick={() => onPointSelect(point)}
  63. onkeydown={(event) => (event.key === 'Enter' || event.key === ' ') && onPointSelect(point)}
  64. class="cursor-pointer group outline-none"
  65. >
  66. <!-- Gain Point -->
  67. <circle
  68. cx={point.x}
  69. cy={point.gainsY}
  70. r={selectedPeriod === point.period ? 6 : 4}
  71. class="fill-emerald-500 stroke-white dark:stroke-[#1e293b] stroke-2 transition-all duration-200 group-hover:r-6"
  72. />
  73. <!-- Loss Point -->
  74. <circle
  75. cx={point.x}
  76. cy={point.lossesY}
  77. r={selectedPeriod === point.period ? 6 : 4}
  78. class="fill-red-500 stroke-white dark:stroke-[#1e293b] stroke-2 transition-all duration-200 group-hover:r-6"
  79. />
  80. </g>
  81. <text x={point.x} y={chartHeight - 10} text-anchor="middle" class="fill-slate-500 dark:fill-slate-400 text-xs font-semibold">
  82. {point.period}
  83. </text>
  84. {/each}
  85. </svg>
  86. </div>
  87. </section>