Coverage for src / lexigram / admin / dashboard / chart_widget.py: 21%

38 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 22:14 +0800

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.ui import ( 

6 AreaChart, 

7 BarChart, 

8 ChartConfig, 

9 ChartDataPoint, 

10 ChartType, 

11 Component, 

12 LineChart, 

13 PieChart, 

14 el, 

15) 

16 

17_CHART_TYPE_MAP: dict[ChartType, type[Component]] = { 

18 ChartType.BAR: BarChart, 

19 ChartType.LINE: LineChart, 

20 ChartType.PIE: PieChart, 

21 ChartType.AREA: AreaChart, 

22} 

23 

24_COL_SPAN_MAP = {1: "", 2: "lg:col-span-2", 3: "lg:col-span-3", 4: "lg:col-span-4"} 

25 

26 

27class ChartWidget(Component): 

28 def __init__( 

29 self, 

30 title: str, 

31 chart_type: ChartType, 

32 data: list[ChartDataPoint] | None = None, 

33 *, 

34 chart_config: ChartConfig | None = None, 

35 data_source: str | None = None, 

36 refresh_interval: int | None = None, 

37 description: str = "", 

38 col_span: int = 1, 

39 ) -> None: 

40 super().__init__() 

41 self.title = title 

42 self.chart_type = chart_type 

43 self.data = data or [] 

44 self.chart_config = chart_config or ChartConfig() 

45 self.data_source = data_source 

46 self.refresh_interval = refresh_interval 

47 self.description = description 

48 self.col_span = col_span 

49 

50 def render(self) -> Any: 

51 span = _COL_SPAN_MAP.get(self.col_span, "") 

52 

53 header = [ 

54 el( 

55 "h3", 

56 self.title, 

57 class_="text-sm font-semibold text-foreground", 

58 ), 

59 ] 

60 if self.description: 

61 header.append( 

62 el( 

63 "p", 

64 self.description, 

65 class_="text-xs text-muted-foreground mt-0.5", 

66 ) 

67 ) 

68 

69 hx_attrs: dict[str, Any] = {} 

70 if self.data_source: 

71 triggers = ["load"] 

72 if self.refresh_interval and self.refresh_interval > 0: 

73 triggers.append(f"every {self.refresh_interval * 1000}ms") 

74 hx_attrs["hx-get"] = self.data_source 

75 hx_attrs["hx-trigger"] = ", ".join(triggers) 

76 hx_attrs["hx-swap"] = "innerHTML" 

77 

78 body: Component 

79 if self.data: 

80 chart_cls = _CHART_TYPE_MAP.get(self.chart_type) 

81 if chart_cls: 

82 body = chart_cls(self.data, self.chart_config) 

83 else: 

84 body = el( 

85 "div", 

86 "Unsupported chart type", 

87 class_="text-sm text-muted-foreground text-center py-8", 

88 ) 

89 elif self.data_source: 

90 body = el( 

91 "div", 

92 el("div", class_="h-4 bg-muted rounded w-3/4 mb-2"), 

93 el("div", class_="h-4 bg-muted rounded w-1/2 mb-2"), 

94 class_="animate-pulse py-2", 

95 ) 

96 else: 

97 body = el( 

98 "div", 

99 "No data", 

100 class_="text-sm text-muted-foreground text-center py-8", 

101 ) 

102 

103 return el( 

104 "div", 

105 el("div", *header, class_="mb-4"), 

106 body, 

107 class_=f"bg-card rounded-xl shadow-sm border border-border p-5 {span}".strip(), 

108 **hx_attrs, 

109 )