NPSChart.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
  2. <template>
  3. <q-card v-bind="$attrs" class="q-px-md">
  4. <q-card-section class="row justify-between">
  5. <div>
  6. <div class="text-h6">{{ props.title }}</div>
  7. <span>{{ props.subTitle }}</span>
  8. </div>
  9. <q-btn
  10. icon="mdi-tray-arrow-down"
  11. dense
  12. flat
  13. class="q-my-auto"
  14. @click="downloadImage"
  15. />
  16. </q-card-section>
  17. <q-separator dark inset />
  18. <div class="graph-container">
  19. <div class="column">
  20. <div class="col-4 row">
  21. <div
  22. class="bg-nps-green"
  23. style="width: 1rem; min-height: max-content"
  24. />
  25. <div class="column q-pa-md">
  26. <span class="text-bold text-h6 text-nps-green">
  27. {{ $t("charts.nps.promotion_zone") }}
  28. </span>
  29. <span>{{ $t("charts.nps.promotion_zone_range") }}</span>
  30. </div>
  31. </div>
  32. <div class="col-4 row">
  33. <div
  34. class="bg-nps-green-light"
  35. style="width: 1rem; min-height: max-content"
  36. />
  37. <div class="column q-pa-md">
  38. <span class="text-bold text-h6 text-nps-green-light">
  39. {{ $t("charts.nps.quality_zone") }}
  40. </span>
  41. <span>{{ $t("charts.nps.quality_zone_range") }}</span>
  42. </div>
  43. </div>
  44. <div class="col-4 row">
  45. <div
  46. class="bg-nps-yellow"
  47. style="width: 1rem; min-height: max-content"
  48. />
  49. <div class="column q-pa-md">
  50. <span class="text-bold text-h6 text-nps-yellow">
  51. {{ $t("charts.nps.refinement_zone") }}
  52. </span>
  53. <span>{{ $t("charts.nps.refinement_zone_range") }}</span>
  54. </div>
  55. </div>
  56. <div class="col-4 row">
  57. <div
  58. class="bg-nps-red"
  59. style="width: 1rem; min-height: max-content"
  60. />
  61. <div class="column q-pa-md">
  62. <span class="text-bold text-h6 text-nps-red">
  63. {{ $t("charts.nps.critical_zone") }}</span
  64. >
  65. <span>{{ $t("charts.nps.critical_zone_range") }}</span>
  66. </div>
  67. </div>
  68. </div>
  69. <div style="display: flex; flex-direction: column; align-items: center">
  70. <div style="max-height: 500px">
  71. <Doughnut
  72. ref="chart_ref"
  73. :options="chartOptions"
  74. :data="chartData"
  75. :plugins="[ChartDataLabels, gaugeNeedle]"
  76. />
  77. </div>
  78. <span>{{ titulo }}</span>
  79. </div>
  80. <div class="column q-col-gutter-md">
  81. <div class="column">
  82. <span>
  83. {{ $t("charts.nps.promoters") }} -
  84. {{ ((data.promotores / data.total) * 100).toFixed(0) }} %
  85. </span>
  86. <q-linear-progress
  87. :value="data.promotores / data.total"
  88. rounded
  89. size="20px"
  90. color="nps-green"
  91. style="min-width: 200px"
  92. />
  93. </div>
  94. <div class="column">
  95. <span
  96. >{{ $t("charts.nps.passives") }} -
  97. {{ ((data.neutros / data.total) * 100).toFixed(0) }} %</span
  98. >
  99. <q-linear-progress
  100. :value="data.neutros / data.total"
  101. rounded
  102. size="20px"
  103. color="nps-yellow"
  104. style="min-width: 200px"
  105. />
  106. </div>
  107. <div class="column">
  108. <span
  109. >{{ $t("charts.nps.detractors") }} -
  110. {{ ((data.detratores / data.total) * 100).toFixed(0) }} %</span
  111. >
  112. <q-linear-progress
  113. :value="data.detratores / data.total"
  114. rounded
  115. size="20px"
  116. color="nps-red"
  117. style="min-width: 200px"
  118. />
  119. </div>
  120. </div>
  121. </div>
  122. </q-card>
  123. </template>
  124. <script setup>
  125. import { ref } from "vue";
  126. import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
  127. import { Doughnut } from "vue-chartjs";
  128. import ChartDataLabels from "chartjs-plugin-datalabels";
  129. import { base64ToJPEG } from "src/helpers/convertBase64Image";
  130. const props = defineProps({
  131. title: {
  132. type: String,
  133. required: true,
  134. },
  135. value: {
  136. type: Number,
  137. required: true,
  138. },
  139. subTitle: {
  140. type: String,
  141. default: null,
  142. },
  143. data: {
  144. type: Object,
  145. required: true,
  146. },
  147. });
  148. const chart_ref = ref(null);
  149. const titulo = ref(`Valor: ${props.data?.nps}`);
  150. const gaugeNeedle = {
  151. id: "gaugeNeedle",
  152. afterDatasetsDraw(chart) {
  153. const { ctx, data } = chart;
  154. ctx.save();
  155. const needleValue = data.datasets[0].needleValue;
  156. const xCenter = chart.getDatasetMeta(0).data[0].x;
  157. const yCenter = chart.getDatasetMeta(0).data[0].y;
  158. const outerRadius = chart.getDatasetMeta(0).data[0].outerRadius - 40;
  159. const angle = Math.PI;
  160. let circumference =
  161. (chart.getDatasetMeta(0).data[0].circumference /
  162. Math.PI /
  163. data.datasets[0].data[0]) *
  164. needleValue;
  165. const needleAngleValue = circumference + 1.5;
  166. ctx.translate(xCenter, yCenter);
  167. ctx.rotate(angle * needleAngleValue);
  168. // Draw the needle
  169. ctx.beginPath();
  170. ctx.strokeStyle = "grey";
  171. ctx.fillStyle = "grey";
  172. ctx.moveTo(0 - 4, 0);
  173. ctx.lineTo(0, -outerRadius);
  174. ctx.lineTo(0 + 4, 0);
  175. ctx.stroke();
  176. ctx.fill();
  177. ctx.beginPath();
  178. ctx.arc(0, 0, 8, 0, 2 * Math.PI);
  179. ctx.fillStyle = "grey";
  180. ctx.fill();
  181. ctx.restore();
  182. },
  183. };
  184. ChartJS.register(ArcElement, Tooltip, Legend);
  185. const chartData = ref({
  186. datasets: [
  187. {
  188. backgroundColor: ["#d43333", "#ffbe00", "#40c56c", "#0d733e"],
  189. data: [100, 50, 25, 25],
  190. needleValue: Number(props.data?.nps) + 100,
  191. borderColor: "transparent",
  192. },
  193. ],
  194. });
  195. const chartOptions = ref({
  196. rotation: 270,
  197. circumference: 180,
  198. cutout: "50%",
  199. plugins: {
  200. tooltip: {
  201. enabled: false,
  202. },
  203. datalabels: {
  204. color: "white",
  205. font: {
  206. size: 14,
  207. weight: "bold",
  208. },
  209. formatter: (value, ctx) => {
  210. let valor = "";
  211. switch (ctx.dataIndex) {
  212. case 0:
  213. valor = "-100 a 0";
  214. break;
  215. case 1:
  216. valor = "0 a 50";
  217. break;
  218. case 2:
  219. valor = "50 a 75";
  220. break;
  221. case 3:
  222. valor = "75 a 100";
  223. break;
  224. default:
  225. valor = "";
  226. break;
  227. }
  228. return valor;
  229. },
  230. },
  231. },
  232. });
  233. addEventListener("resize", () => {
  234. if (chart_ref.value) {
  235. chart_ref.value.chart.update();
  236. }
  237. });
  238. const downloadImage = () => {
  239. const image = chart_ref.value.chart?.toBase64Image("image/jpeg", 1);
  240. base64ToJPEG(image, props.title);
  241. };
  242. </script>
  243. <style scoped>
  244. .graph-container {
  245. display: grid;
  246. grid-template-columns: 1fr 1fr 1fr;
  247. align-items: center;
  248. justify-content: space-around;
  249. gap: 1rem;
  250. width: 100%;
  251. max-width: 100vw;
  252. padding: 1rem;
  253. }
  254. @media screen and (max-width: 1024px) {
  255. .graph-container {
  256. grid-template-columns: 1fr 1fr;
  257. }
  258. }
  259. @media screen and (max-width: 768px) {
  260. .graph-container {
  261. grid-template-columns: 1fr;
  262. }
  263. }
  264. </style>