DoughnutChart.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <template>
  2. <div v-bind="$attrs" class="chart-wrapper full-width full-height">
  3. <q-resize-observer @resize="onResize" />
  4. <div v-if="hasData" class="chart-container">
  5. <Doughnut
  6. ref="chart_ref"
  7. :options="chartPieOptions"
  8. :data="chartPieData"
  9. :plugins="[ChartDataLabels]"
  10. />
  11. </div>
  12. <div v-else class="no-data-container">
  13. <span :class="textColor">{{ $t("http.errors.no_records_found") }}</span>
  14. </div>
  15. </div>
  16. </template>
  17. <script setup>
  18. import { ref, computed } from "vue";
  19. import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
  20. import { Doughnut } from "vue-chartjs";
  21. import ChartDataLabels from "chartjs-plugin-datalabels";
  22. import { base64ToJPEG } from "src/helpers/convertBase64Image";
  23. import { useQuasar, colors, getCssVar } from "quasar";
  24. ChartJS.register(ArcElement, Tooltip, Legend);
  25. const $q = useQuasar();
  26. const { lighten } = colors;
  27. const props = defineProps({
  28. data: {
  29. type: Object,
  30. default: () => ({
  31. chart_data: [],
  32. }),
  33. },
  34. backgroundColors: {
  35. type: Array,
  36. default: null,
  37. },
  38. title: {
  39. type: String,
  40. default: "doughnut-chart",
  41. },
  42. dataSetLabel: {
  43. type: String,
  44. default: "Dados",
  45. },
  46. });
  47. const chart_ref = ref(null);
  48. const onResize = () => {
  49. if (chart_ref.value?.chart) {
  50. setTimeout(() => {
  51. chart_ref.value.chart.resize();
  52. }, 50);
  53. }
  54. };
  55. const textColor = computed(() => {
  56. return $q.dark.isActive ? "text-white" : "text-black";
  57. });
  58. const labelColor = computed(() => {
  59. return $q.dark.isActive ? "#ffffff" : "#000000";
  60. });
  61. const dataLabelColor = computed(() => {
  62. return $q.dark.isActive ? "#ffffff" : "#000000";
  63. });
  64. const hasData = computed(() => {
  65. return props.data?.chart_data && props.data.chart_data.length > 0;
  66. });
  67. const chartLabels = computed(() => {
  68. return props.data?.chart_data?.map((item) => item.label) || [];
  69. });
  70. const chartValues = computed(() => {
  71. return props.data?.chart_data?.map((item) => item.value) || [];
  72. });
  73. const chartPercentages = computed(() => {
  74. const total = props.data?.current_total || 0;
  75. if (total === 0) return [];
  76. return (
  77. props.data?.chart_data?.map((item) =>
  78. Math.round((item.value / total) * 100),
  79. ) || []
  80. );
  81. });
  82. const chartThemeColors = computed(() => {
  83. if (props.backgroundColors) {
  84. return props.backgroundColors;
  85. }
  86. const primaryColor = getCssVar("primary");
  87. if (!primaryColor) return [];
  88. const numColors = chartValues.value.length;
  89. const step = numColors > 0 ? 50 / numColors : 0;
  90. return Array.from({ length: numColors }, (_, i) =>
  91. lighten(primaryColor, i * step),
  92. );
  93. });
  94. const chartPieData = computed(() => ({
  95. labels: chartLabels.value,
  96. datasets: [
  97. {
  98. label: props.dataSetLabel,
  99. data: chartPercentages.value,
  100. backgroundColor: chartThemeColors.value,
  101. },
  102. ],
  103. }));
  104. const chartPieOptions = computed(() => ({
  105. responsive: true,
  106. maintainAspectRatio: false,
  107. plugins: {
  108. legend: {
  109. position: "bottom",
  110. labels: {
  111. color: labelColor.value,
  112. font: {
  113. size: 12,
  114. },
  115. padding: 20,
  116. },
  117. },
  118. datalabels: {
  119. color: dataLabelColor.value,
  120. font: {
  121. size: 12,
  122. weight: "bold",
  123. },
  124. formatter: (value) => {
  125. return value > 0 ? value + "%" : "";
  126. },
  127. },
  128. tooltip: {
  129. backgroundColor: $q.dark.isActive
  130. ? "rgba(0, 0, 0, 0.8)"
  131. : "rgba(255, 255, 255, 0.9)",
  132. titleColor: labelColor.value,
  133. bodyColor: labelColor.value,
  134. borderColor: labelColor.value,
  135. borderWidth: 1,
  136. callbacks: {
  137. label: (context) => {
  138. const label = context.label || "";
  139. const percentage = context.parsed;
  140. const actualValue = chartValues.value[context.dataIndex];
  141. return `${label}: ${actualValue} (${percentage}%)`;
  142. },
  143. },
  144. },
  145. },
  146. }));
  147. const downloadImage = () => {
  148. const image = chart_ref.value.chart?.toBase64Image("image/jpeg", 1);
  149. base64ToJPEG(image, props.title || "pie-chart");
  150. };
  151. defineExpose({
  152. downloadImage,
  153. chart_ref,
  154. });
  155. </script>
  156. <style scoped>
  157. .chart-wrapper {
  158. position: relative;
  159. }
  160. .chart-container {
  161. position: absolute;
  162. top: 10px;
  163. left: 0;
  164. right: 0;
  165. bottom: 0;
  166. width: 100%;
  167. height: 100%;
  168. }
  169. .no-data-container {
  170. position: absolute;
  171. top: 0;
  172. left: 0;
  173. right: 0;
  174. bottom: 0;
  175. display: flex;
  176. align-items: center;
  177. justify-content: center;
  178. padding: 1.5rem;
  179. }
  180. </style>