1<template>
2 <div class="w-full h-full overflow-hidden">
3 <CardBase
4 :title="title"
5 :text="chartData.text"
6 :percentageChange="chartData.percentageChange"
7 :chartConfig="chartConfig"
8 :currentDuration="currentDuration"
9 :orientation="orientation"
10 :timelineFilter="timelineFilter"
11 @changeDuration="changeDuration"
12 />
13 </div>
14</template>
15
16<script setup lang="ts">
17import { __ } from "@/translation";
18import { buildPercentageChange, formatTime } from "@/utils";
19import { EChartsOption } from "echarts";
20import { createResource } from "frappe-ui";
21import { computed, onMounted, ref, type PropType } from "vue";
22import CardBase from "./ChartCardBase.vue";
23
24interface AverageResponseData {
25 percentage_change: number;
26 average?: number;
27 total?: number;
28
29 data: { date: string; avg_time?: number; count?: number }[];
30}
31
32const props = defineProps({
33 title: {
34 type: String,
35 required: true,
36 },
37 apiUrl: {
38 type: String,
39 required: false,
40 },
41 timelineFilter: {
42 type: Boolean,
43 default: true,
44 },
45 data: {
46 type: Object as PropType<AverageResponseData>,
47 required: true,
48 },
49 chartColor: {
50 type: Object as PropType<{
51 lineColor: string;
52 gradientColor: { start: string; end: string };
53 }>,
54 required: true,
55 },
56 dt: {
57 type: String,
58 required: false,
59 },
60 dn: {
61 type: String,
62 required: false,
63 },
64 orientation: {
65 type: String as PropType<"vertical" | "horizontal">,
66 default: "vertical",
67 },
68 type: {
69 type: String as PropType<"Time" | "Count">,
70 default: "Time",
71 },
72});
73
74// the untranslated key: it is lowercased straight into the API period
75const currentDuration = ref("Last month");
76
77const chartData = computed(() => {
78 const isDataFetched = resource.fetched;
79 const _data: AverageResponseData = isDataFetched ? resource.data : props.data;
80
81 const dates = _data?.data?.map((item) => item.date) || [];
82
83 const seriesData =
84 _data?.data?.map((item) =>
85 props.type === "Time" ? item.avg_time : item.count
86 ) || [];
87 const _percentageChange = _data?.percentage_change || 0;
88 const percentageChange = buildPercentageChange(_percentageChange);
89
90 // for time take average and for count take total
91 const text =
92 props.type === "Time"
93 ? formatTime(_data?.average || 0, {
94 day: true,
95 hour: true,
96 minute: true,
97 maxUnits: 2,
98 }) || "0m"
99 : _data?.total || 0;
100
101 return {
102 data: seriesData,
103 dates,
104 percentageChange,
105 text,
106 };
107});
108
109const chartConfig = computed<EChartsOption>(() => {
110 const color = props.chartColor.lineColor;
111 const gradientColor = props.chartColor.gradientColor;
112 return {
113 xAxis: {
114 type: "category",
115 data: chartData.value.dates,
116 show: false,
117 boundaryGap: false,
118 },
119 yAxis: {
120 type: "value",
121 show: false,
122 },
123 series: [
124 {
125 data: chartData.value.data,
126 type: "line",
127 symbol: "none",
128 lineStyle: {
129 width: 1.25,
130 },
131 areaStyle: {
132 opacity: 0.8,
133 color: {
134 type: "linear",
135 x: 0,
136 y: 0,
137 x2: 0,
138 y2: 1,
139 colorStops: [
140 {
141 offset: 0,
142 color: gradientColor.start,
143 },
144 {
145 offset: 1,
146 color: gradientColor.end,
147 },
148 ],
149 global: false,
150 },
151 },
152 },
153 ],
154 color: color,
155 grid: {
156 left: 2,
157 right: 2,
158 top: 2,
159 bottom: 2,
160 },
161 tooltip: {
162 show: true,
163 trigger: "axis",
164 appendToBody: true,
165 // position: (point: number[]) => [point[0] + 12, point[1] - 50],
166 axisPointer: { type: "none" },
167 formatter: (params: any) => {
168 const p = Array.isArray(params) ? params[0] : params;
169 if (p.value === undefined || p.value === null) return "";
170 const value =
171 props.type === "Time"
172 ? formatTime(Number(p.value) || 0, {
173 day: true,
174 hour: true,
175 minute: true,
176 maxUnits: 2,
177 }) || "0m"
178 : p.value;
179 return `<span style="font-size:12px;color:#6b7280">${p.name}: <b style="color:#374151">${value}</b></span>`;
180 },
181 },
182 };
183});
184
185const resource = createResource({
186 url: props.apiUrl || "",
187 type: "GET",
188 makeParams: () => {
189 const params: Record<string, any> = {
190 period: currentDuration.value.toLowerCase(),
191 };
192 if (props.dt && props.dn) {
193 params.dt = props.dt;
194 params.dn = props.dn;
195 }
196 return params;
197 },
198});
199
200const changeDuration = (period: string) => {
201 currentDuration.value = period;
202 if (props.apiUrl) resource.submit();
203};
204
205onMounted(() => {
206 if (props.timelineFilter && props.apiUrl && !props.data?.data) {
207 resource.submit();
208 }
209});
210</script>
211