@fui-org/fui-cli 2.1.4 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +16 -7
  2. package/dist/fui-xg17j3f9.js +396 -0
  3. package/dist/fui.js +1 -1
  4. package/package.json +1 -1
  5. package/skills/fui/SKILL.md +152 -39
  6. package/skills/{fui-skill → fui}/assets/projectdefaultstyle.css +489 -489
  7. package/skills/{fui-skill → fui}/examples/component.vue +162 -162
  8. package/skills/{fui-skill → fui}/references/INDEX.md +4 -5
  9. package/skills/{fui-skill → fui}/references/advanced-techniques.md +170 -168
  10. package/skills/{fui-skill → fui}/references/component-design.md +2 -2
  11. package/skills/{fui-skill → fui}/references/component-quickref.md +78 -78
  12. package/skills/{fui-skill → fui}/references/component-table.md +287 -287
  13. package/skills/{fui-skill → fui}/references/components-dialog.md +191 -191
  14. package/skills/{fui-skill → fui}/references/components-display.md +141 -141
  15. package/skills/{fui-skill → fui}/references/components-echart.md +316 -316
  16. package/skills/{fui-skill → fui}/references/components-input.md +335 -335
  17. package/skills/{fui-skill → fui}/references/controls-patterns.md +2 -2
  18. package/skills/{fui-skill → fui}/references/controls-styling-vocabulary.md +173 -173
  19. package/skills/{fui-skill → fui}/references/db-workflow.md +9 -9
  20. package/skills/{fui-skill → fui}/references/default-function.md +456 -456
  21. package/skills/{fui-skill → fui}/references/design-modes.md +57 -57
  22. package/skills/{fui-skill → fui}/references/echart-templates.md +489 -489
  23. package/skills/{fui-skill → fui}/references/fastproject.md +99 -99
  24. package/skills/{fui-skill → fui}/references/fullstack-workflow.md +314 -313
  25. package/skills/{fui-skill → fui}/references/module-data-patterns.md +117 -117
  26. package/skills/{fui-skill → fui}/references/module-json-anatomy.md +132 -132
  27. package/skills/{fui-skill → fui}/references/module-structure.md +144 -144
  28. package/skills/{fui-skill → fui}/references/new-session.md +3 -20
  29. package/skills/{fui-skill → fui}/references/pdfmake.md +60 -60
  30. package/skills/{fui-skill → fui}/references/permission-system.md +1 -1
  31. package/skills/{fui-skill → fui}/references/platform-architecture.md +269 -269
  32. package/skills/{fui-skill → fui}/references/project-config.md +303 -303
  33. package/skills/{fui-skill → fui}/references/project-provisioning.md +4 -4
  34. package/skills/{fui-skill → fui}/references/script-map.md +262 -262
  35. package/skills/{fui-skill → fui}/references/sql-clr-functions.md +225 -225
  36. package/skills/{fui-skill → fui}/references/system-design.md +3 -3
  37. package/skills/{fui-skill → fui}/references/tapi-permission-patterns.md +156 -156
  38. package/skills/{fui-skill → fui}/references/tapi-reference.md +474 -474
  39. package/skills/fui/references/tools-registry.md +214 -0
  40. package/skills/{fui-skill → fui}/references/ui-crosswindow-patterns.md +321 -321
  41. package/skills/{fui-skill → fui}/references/ui-dialog-patterns.md +255 -255
  42. package/skills/{fui-skill → fui}/references/ui-layout-patterns.md +176 -176
  43. package/skills/{fui-skill → fui}/references/ui-patterns.md +303 -303
  44. package/skills/{fui-skill → fui}/references/ui-screenshot-review.md +95 -95
  45. package/skills/{fui-skill → fui}/references/ui-table-cell-patterns.md +318 -318
  46. package/skills/{fui-skill → fui}/references/verification.md +18 -18
  47. package/skills/{fui-skill → fui}/references/watcher-patterns.md +163 -163
  48. package/skills/{fui-skill → fui}/references/websocket-realtime.md +271 -271
  49. package/skills/{fui-skill → fui}/scripts/component-3.0.js +2715 -2715
  50. package/skills/{fui-skill → fui}/scripts/component.js +3147 -3147
  51. package/skills/{fui-skill → fui}/scripts/componentTable-3.0.js +1038 -1038
  52. package/skills/{fui-skill → fui}/scripts/componentTable.js +891 -891
  53. package/skills/{fui-skill → fui}/scripts/defaultfunction-3.0.js +866 -866
  54. package/skills/{fui-skill → fui}/scripts/defaultfunction.js +1051 -1051
  55. package/skills/{fui-skill → fui}/scripts/fastproject-3.0.js +870 -870
  56. package/skills/{fui-skill → fui}/scripts/fastproject.js +828 -828
  57. package/dist/fui-h4z7zhk2.js +0 -381
  58. package/skills/fui-skill/SKILL.md +0 -139
  59. package/skills/fui-skill/references/tools-registry.md +0 -84
  60. /package/skills/{fui-skill → fui}/assets/projectdefaultstyle-3.0.css +0 -0
  61. /package/skills/{fui-skill → fui}/design-md/airbnb/DESIGN.md +0 -0
  62. /package/skills/{fui-skill → fui}/design-md/airbnb/README.md +0 -0
  63. /package/skills/{fui-skill → fui}/design-md/airtable/DESIGN.md +0 -0
  64. /package/skills/{fui-skill → fui}/design-md/airtable/README.md +0 -0
  65. /package/skills/{fui-skill → fui}/design-md/apple/DESIGN.md +0 -0
  66. /package/skills/{fui-skill → fui}/design-md/apple/README.md +0 -0
  67. /package/skills/{fui-skill → fui}/design-md/asu/DESIGN.md +0 -0
  68. /package/skills/{fui-skill → fui}/design-md/asu/README.md +0 -0
  69. /package/skills/{fui-skill → fui}/design-md/binance/DESIGN.md +0 -0
  70. /package/skills/{fui-skill → fui}/design-md/binance/README.md +0 -0
  71. /package/skills/{fui-skill → fui}/design-md/bmw/DESIGN.md +0 -0
  72. /package/skills/{fui-skill → fui}/design-md/bmw/README.md +0 -0
  73. /package/skills/{fui-skill → fui}/design-md/bmw-m/DESIGN.md +0 -0
  74. /package/skills/{fui-skill → fui}/design-md/bmw-m/README.md +0 -0
  75. /package/skills/{fui-skill → fui}/design-md/bugatti/DESIGN.md +0 -0
  76. /package/skills/{fui-skill → fui}/design-md/bugatti/README.md +0 -0
  77. /package/skills/{fui-skill → fui}/design-md/cal/DESIGN.md +0 -0
  78. /package/skills/{fui-skill → fui}/design-md/cal/README.md +0 -0
  79. /package/skills/{fui-skill → fui}/design-md/claude/DESIGN.md +0 -0
  80. /package/skills/{fui-skill → fui}/design-md/claude/README.md +0 -0
  81. /package/skills/{fui-skill → fui}/design-md/clay/DESIGN.md +0 -0
  82. /package/skills/{fui-skill → fui}/design-md/clay/README.md +0 -0
  83. /package/skills/{fui-skill → fui}/design-md/clickhouse/DESIGN.md +0 -0
  84. /package/skills/{fui-skill → fui}/design-md/clickhouse/README.md +0 -0
  85. /package/skills/{fui-skill → fui}/design-md/cohere/DESIGN.md +0 -0
  86. /package/skills/{fui-skill → fui}/design-md/cohere/README.md +0 -0
  87. /package/skills/{fui-skill → fui}/design-md/coinbase/DESIGN.md +0 -0
  88. /package/skills/{fui-skill → fui}/design-md/coinbase/README.md +0 -0
  89. /package/skills/{fui-skill → fui}/design-md/composio/DESIGN.md +0 -0
  90. /package/skills/{fui-skill → fui}/design-md/composio/README.md +0 -0
  91. /package/skills/{fui-skill → fui}/design-md/cursor/DESIGN.md +0 -0
  92. /package/skills/{fui-skill → fui}/design-md/cursor/README.md +0 -0
  93. /package/skills/{fui-skill → fui}/design-md/elevenlabs/DESIGN.md +0 -0
  94. /package/skills/{fui-skill → fui}/design-md/elevenlabs/README.md +0 -0
  95. /package/skills/{fui-skill → fui}/design-md/expo/DESIGN.md +0 -0
  96. /package/skills/{fui-skill → fui}/design-md/expo/README.md +0 -0
  97. /package/skills/{fui-skill → fui}/design-md/ferrari/DESIGN.md +0 -0
  98. /package/skills/{fui-skill → fui}/design-md/ferrari/README.md +0 -0
  99. /package/skills/{fui-skill → fui}/design-md/figma/DESIGN.md +0 -0
  100. /package/skills/{fui-skill → fui}/design-md/figma/README.md +0 -0
  101. /package/skills/{fui-skill → fui}/design-md/framer/DESIGN.md +0 -0
  102. /package/skills/{fui-skill → fui}/design-md/framer/README.md +0 -0
  103. /package/skills/{fui-skill → fui}/design-md/fui/DESIGN.md +0 -0
  104. /package/skills/{fui-skill → fui}/design-md/hashicorp/DESIGN.md +0 -0
  105. /package/skills/{fui-skill → fui}/design-md/hashicorp/README.md +0 -0
  106. /package/skills/{fui-skill → fui}/design-md/ibm/DESIGN.md +0 -0
  107. /package/skills/{fui-skill → fui}/design-md/ibm/README.md +0 -0
  108. /package/skills/{fui-skill → fui}/design-md/intercom/DESIGN.md +0 -0
  109. /package/skills/{fui-skill → fui}/design-md/intercom/README.md +0 -0
  110. /package/skills/{fui-skill → fui}/design-md/kraken/DESIGN.md +0 -0
  111. /package/skills/{fui-skill → fui}/design-md/kraken/README.md +0 -0
  112. /package/skills/{fui-skill → fui}/design-md/lamborghini/DESIGN.md +0 -0
  113. /package/skills/{fui-skill → fui}/design-md/lamborghini/README.md +0 -0
  114. /package/skills/{fui-skill → fui}/design-md/linear.app/DESIGN.md +0 -0
  115. /package/skills/{fui-skill → fui}/design-md/linear.app/README.md +0 -0
  116. /package/skills/{fui-skill → fui}/design-md/lovable/DESIGN.md +0 -0
  117. /package/skills/{fui-skill → fui}/design-md/lovable/README.md +0 -0
  118. /package/skills/{fui-skill → fui}/design-md/mastercard/DESIGN.md +0 -0
  119. /package/skills/{fui-skill → fui}/design-md/mastercard/README.md +0 -0
  120. /package/skills/{fui-skill → fui}/design-md/meta/DESIGN.md +0 -0
  121. /package/skills/{fui-skill → fui}/design-md/meta/README.md +0 -0
  122. /package/skills/{fui-skill → fui}/design-md/minimax/DESIGN.md +0 -0
  123. /package/skills/{fui-skill → fui}/design-md/minimax/README.md +0 -0
  124. /package/skills/{fui-skill → fui}/design-md/mintlify/DESIGN.md +0 -0
  125. /package/skills/{fui-skill → fui}/design-md/mintlify/README.md +0 -0
  126. /package/skills/{fui-skill → fui}/design-md/miro/DESIGN.md +0 -0
  127. /package/skills/{fui-skill → fui}/design-md/miro/README.md +0 -0
  128. /package/skills/{fui-skill → fui}/design-md/mistral.ai/DESIGN.md +0 -0
  129. /package/skills/{fui-skill → fui}/design-md/mistral.ai/README.md +0 -0
  130. /package/skills/{fui-skill → fui}/design-md/mongodb/DESIGN.md +0 -0
  131. /package/skills/{fui-skill → fui}/design-md/mongodb/README.md +0 -0
  132. /package/skills/{fui-skill → fui}/design-md/nike/DESIGN.md +0 -0
  133. /package/skills/{fui-skill → fui}/design-md/nike/README.md +0 -0
  134. /package/skills/{fui-skill → fui}/design-md/notion/DESIGN.md +0 -0
  135. /package/skills/{fui-skill → fui}/design-md/notion/README.md +0 -0
  136. /package/skills/{fui-skill → fui}/design-md/nvidia/DESIGN.md +0 -0
  137. /package/skills/{fui-skill → fui}/design-md/nvidia/README.md +0 -0
  138. /package/skills/{fui-skill → fui}/design-md/ollama/DESIGN.md +0 -0
  139. /package/skills/{fui-skill → fui}/design-md/ollama/README.md +0 -0
  140. /package/skills/{fui-skill → fui}/design-md/opencode.ai/DESIGN.md +0 -0
  141. /package/skills/{fui-skill → fui}/design-md/opencode.ai/README.md +0 -0
  142. /package/skills/{fui-skill → fui}/design-md/pinterest/DESIGN.md +0 -0
  143. /package/skills/{fui-skill → fui}/design-md/pinterest/README.md +0 -0
  144. /package/skills/{fui-skill → fui}/design-md/playstation/DESIGN.md +0 -0
  145. /package/skills/{fui-skill → fui}/design-md/playstation/README.md +0 -0
  146. /package/skills/{fui-skill → fui}/design-md/posthog/DESIGN.md +0 -0
  147. /package/skills/{fui-skill → fui}/design-md/posthog/README.md +0 -0
  148. /package/skills/{fui-skill → fui}/design-md/raycast/DESIGN.md +0 -0
  149. /package/skills/{fui-skill → fui}/design-md/raycast/README.md +0 -0
  150. /package/skills/{fui-skill → fui}/design-md/renault/DESIGN.md +0 -0
  151. /package/skills/{fui-skill → fui}/design-md/renault/README.md +0 -0
  152. /package/skills/{fui-skill → fui}/design-md/replicate/DESIGN.md +0 -0
  153. /package/skills/{fui-skill → fui}/design-md/replicate/README.md +0 -0
  154. /package/skills/{fui-skill → fui}/design-md/resend/DESIGN.md +0 -0
  155. /package/skills/{fui-skill → fui}/design-md/resend/README.md +0 -0
  156. /package/skills/{fui-skill → fui}/design-md/revolut/DESIGN.md +0 -0
  157. /package/skills/{fui-skill → fui}/design-md/revolut/README.md +0 -0
  158. /package/skills/{fui-skill → fui}/design-md/runwayml/DESIGN.md +0 -0
  159. /package/skills/{fui-skill → fui}/design-md/runwayml/README.md +0 -0
  160. /package/skills/{fui-skill → fui}/design-md/sanity/DESIGN.md +0 -0
  161. /package/skills/{fui-skill → fui}/design-md/sanity/README.md +0 -0
  162. /package/skills/{fui-skill → fui}/design-md/sentry/DESIGN.md +0 -0
  163. /package/skills/{fui-skill → fui}/design-md/sentry/README.md +0 -0
  164. /package/skills/{fui-skill → fui}/design-md/shopify/DESIGN.md +0 -0
  165. /package/skills/{fui-skill → fui}/design-md/shopify/README.md +0 -0
  166. /package/skills/{fui-skill → fui}/design-md/slack/DESIGN.md +0 -0
  167. /package/skills/{fui-skill → fui}/design-md/spacex/DESIGN.md +0 -0
  168. /package/skills/{fui-skill → fui}/design-md/spacex/README.md +0 -0
  169. /package/skills/{fui-skill → fui}/design-md/spotify/DESIGN.md +0 -0
  170. /package/skills/{fui-skill → fui}/design-md/spotify/README.md +0 -0
  171. /package/skills/{fui-skill → fui}/design-md/starbucks/DESIGN.md +0 -0
  172. /package/skills/{fui-skill → fui}/design-md/starbucks/README.md +0 -0
  173. /package/skills/{fui-skill → fui}/design-md/stripe/DESIGN.md +0 -0
  174. /package/skills/{fui-skill → fui}/design-md/stripe/README.md +0 -0
  175. /package/skills/{fui-skill → fui}/design-md/supabase/DESIGN.md +0 -0
  176. /package/skills/{fui-skill → fui}/design-md/supabase/README.md +0 -0
  177. /package/skills/{fui-skill → fui}/design-md/superhuman/DESIGN.md +0 -0
  178. /package/skills/{fui-skill → fui}/design-md/superhuman/README.md +0 -0
  179. /package/skills/{fui-skill → fui}/design-md/tesla/DESIGN.md +0 -0
  180. /package/skills/{fui-skill → fui}/design-md/tesla/README.md +0 -0
  181. /package/skills/{fui-skill → fui}/design-md/theverge/DESIGN.md +0 -0
  182. /package/skills/{fui-skill → fui}/design-md/theverge/README.md +0 -0
  183. /package/skills/{fui-skill → fui}/design-md/together.ai/DESIGN.md +0 -0
  184. /package/skills/{fui-skill → fui}/design-md/together.ai/README.md +0 -0
  185. /package/skills/{fui-skill → fui}/design-md/uber/DESIGN.md +0 -0
  186. /package/skills/{fui-skill → fui}/design-md/uber/README.md +0 -0
  187. /package/skills/{fui-skill → fui}/design-md/vercel/DESIGN.md +0 -0
  188. /package/skills/{fui-skill → fui}/design-md/vercel/README.md +0 -0
  189. /package/skills/{fui-skill → fui}/design-md/vodafone/DESIGN.md +0 -0
  190. /package/skills/{fui-skill → fui}/design-md/vodafone/README.md +0 -0
  191. /package/skills/{fui-skill → fui}/design-md/voltagent/DESIGN.md +0 -0
  192. /package/skills/{fui-skill → fui}/design-md/voltagent/README.md +0 -0
  193. /package/skills/{fui-skill → fui}/design-md/warp/DESIGN.md +0 -0
  194. /package/skills/{fui-skill → fui}/design-md/warp/README.md +0 -0
  195. /package/skills/{fui-skill → fui}/design-md/webflow/DESIGN.md +0 -0
  196. /package/skills/{fui-skill → fui}/design-md/webflow/README.md +0 -0
  197. /package/skills/{fui-skill → fui}/design-md/wired/DESIGN.md +0 -0
  198. /package/skills/{fui-skill → fui}/design-md/wired/README.md +0 -0
  199. /package/skills/{fui-skill → fui}/design-md/wise/DESIGN.md +0 -0
  200. /package/skills/{fui-skill → fui}/design-md/wise/README.md +0 -0
  201. /package/skills/{fui-skill → fui}/design-md/x.ai/DESIGN.md +0 -0
  202. /package/skills/{fui-skill → fui}/design-md/x.ai/README.md +0 -0
  203. /package/skills/{fui-skill → fui}/design-md/zapier/DESIGN.md +0 -0
  204. /package/skills/{fui-skill → fui}/design-md/zapier/README.md +0 -0
  205. /package/skills/{fui-skill → fui}/examples/f-table-patterns.json +0 -0
  206. /package/skills/{fui-skill → fui}/examples/module-patterns.json +0 -0
  207. /package/skills/{fui-skill → fui}/examples/project-patterns.json +0 -0
  208. /package/skills/{fui-skill → fui}/references/coding-standards.md +0 -0
  209. /package/skills/{fui-skill → fui}/references/db-table-design.md +0 -0
  210. /package/skills/{fui-skill → fui}/references/fsheet.md +0 -0
  211. /package/skills/{fui-skill → fui}/references/tapi-file-api.md +0 -0
  212. /package/skills/{fui-skill → fui}/references/ui-templates.md +0 -0
  213. /package/skills/{fui-skill → fui}/scripts/fechart.js +0 -0
  214. /package/skills/{fui-skill → fui}/scripts/fsheet.js +0 -0
@@ -1,489 +1,489 @@
1
- # f-echart — Chart Type Templates & Real-World Patterns
2
-
3
- > Owns: **`option` templates per chart type + matching module.json patterns**. Load once you know which chart to draw. **Read the core first** — import/props/config/theme/DataView/errors: [components-echart.md](components-echart.md).
4
-
5
- ---
6
-
7
- ## Templates — Chart types
8
-
9
- > All templates: declare `grid` sides only for non-default layout (engine reserves title/legend space); `grid` vs `boundaryGap`: [components-echart.md](components-echart.md) §Grid padding vs boundaryGap. `legend: {}` in `option` **anchors top**; for bottom use `legend: { bottom: 0 }` or `config.legend: true`.
10
-
11
- ### Bar chart
12
-
13
- ```js
14
- // script.js — callback sau fetch
15
- vueData.chartOpt = {
16
- xAxis: { type: "category", data: d.data.map((r) => r.thang) },
17
- yAxis: { type: "value" },
18
- series: [{ type: "bar", data: d.data.map((r) => r.doanhThu), name: "Doanh thu" }],
19
- tooltip: { trigger: "axis" },
20
- legend: {},
21
- };
22
- vueData.rows = d.data;
23
- ```
24
-
25
- ### Line chart (smooth)
26
-
27
- ```js
28
- vueData.chartOpt = {
29
- xAxis: { type: "category", data: d.data.map((r) => r.thang) },
30
- yAxis: { type: "value" },
31
- series: [{ type: "line", data: d.data.map((r) => r.val), smooth: true, areaStyle: {} }],
32
- tooltip: { trigger: "axis" },
33
- };
34
- ```
35
-
36
- ### Multi-series + Dual Y-axis
37
-
38
- ```js
39
- vueData.chartOpt = {
40
- xAxis: { type: "category", data: d.data.map((r) => r.thang) },
41
- yAxis: [{ name: "VNĐ" }, { name: "%", position: "right" }],
42
- series: [
43
- { type: "bar", name: "Doanh thu", data: d.data.map((r) => r.doanhThu), yAxisIndex: 0 },
44
- { type: "line", name: "Tăng trưởng", data: d.data.map((r) => r.tangTruong), yAxisIndex: 1, smooth: true },
45
- ],
46
- tooltip: { trigger: "axis" },
47
- legend: {},
48
- };
49
- ```
50
-
51
- ### Stacked bar
52
-
53
- ```js
54
- vueData.chartOpt = {
55
- xAxis: { type: "category", data: d.data.map((r) => r.thang) },
56
- yAxis: { type: "value" },
57
- series: [
58
- { type: "bar", name: "SP A", data: d.data.map((r) => r.spA), stack: "total" },
59
- { type: "bar", name: "SP B", data: d.data.map((r) => r.spB), stack: "total" },
60
- { type: "bar", name: "SP C", data: d.data.map((r) => r.spC), stack: "total" },
61
- ],
62
- tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
63
- legend: {},
64
- };
65
- ```
66
-
67
- ### Pie chart
68
-
69
- ```js
70
- vueData.chartOpt = {
71
- series: [
72
- {
73
- type: "pie",
74
- data: d.data.map((r) => ({ name: r.khoa, value: r.soLuong })),
75
- radius: "70%",
76
- label: { formatter: "{b}: {d}%" },
77
- },
78
- ],
79
- tooltip: { trigger: "item" },
80
- legend: { bottom: 0 },
81
- };
82
- ```
83
-
84
- Doughnut: `radius: ['40%', '70%']`
85
-
86
- ### Scatter plot
87
-
88
- ```js
89
- vueData.chartOpt = {
90
- xAxis: { type: "value", name: "Điểm thi" },
91
- yAxis: { type: "value", name: "GPA" },
92
- series: [{ type: "scatter", data: d.data.map((r) => [r.diemThi, r.gpa, r.hoTen]), symbolSize: 8 }],
93
- tooltip: {
94
- formatter: (p) => `${p.value[2]}<br>Điểm: ${p.value[0]} — GPA: ${p.value[1]}`,
95
- },
96
- };
97
- ```
98
-
99
- ### Gauge
100
-
101
- ```js
102
- vueData.chartOpt = {
103
- series: [
104
- {
105
- type: "gauge",
106
- min: 0,
107
- max: 100,
108
- data: [{ value: vueData.cpuPercent, name: "CPU %" }],
109
- detail: { formatter: "{value}%" },
110
- axisLabel: { color: "auto" },
111
- },
112
- ],
113
- tooltip: {},
114
- };
115
- ```
116
-
117
- ### Radar
118
-
119
- ```js
120
- vueData.chartOpt = {
121
- radar: {
122
- indicator: [
123
- { name: "Toán", max: 10 },
124
- { name: "Lý", max: 10 },
125
- { name: "Hóa", max: 10 },
126
- { name: "Văn", max: 10 },
127
- { name: "Anh", max: 10 },
128
- ],
129
- },
130
- series: [
131
- {
132
- type: "radar",
133
- data: d.data.map((r) => ({
134
- name: r.hoTen,
135
- value: [r.toan, r.ly, r.hoa, r.van, r.anh],
136
- })),
137
- },
138
- ],
139
- tooltip: {},
140
- legend: {},
141
- };
142
- ```
143
-
144
- ### Heatmap (Cartesian)
145
-
146
- ```js
147
- // data: [{x: 'Mon', y: '8h', value: 5}, ...]
148
- const xData = [...new Set(d.data.map((r) => r.x))];
149
- const yData = [...new Set(d.data.map((r) => r.y))];
150
- vueData.chartOpt = {
151
- xAxis: { type: "category", data: xData },
152
- yAxis: { type: "category", data: yData },
153
- visualMap: { min: 0, max: 10, calculable: true },
154
- series: [
155
- {
156
- type: "heatmap",
157
- data: d.data.map((r) => [xData.indexOf(r.x), yData.indexOf(r.y), r.value]),
158
- },
159
- ],
160
- tooltip: {},
161
- };
162
- ```
163
-
164
- ### Candlestick (OHLC)
165
-
166
- ```js
167
- vueData.chartOpt = {
168
- xAxis: { type: "category", data: d.data.map((r) => r.date) },
169
- yAxis: { type: "value", scale: true },
170
- series: [
171
- {
172
- type: "candlestick",
173
- data: d.data.map((r) => [r.open, r.close, r.low, r.high]),
174
- },
175
- ],
176
- tooltip: { trigger: "axis" },
177
- };
178
- ```
179
-
180
- ### Funnel
181
-
182
- ```js
183
- vueData.chartOpt = {
184
- series: [
185
- {
186
- type: "funnel",
187
- data: d.data.map((r) => ({ name: r.buoc, value: r.soLuong })).sort((a, b) => b.value - a.value),
188
- label: { formatter: "{b}: {d}%" },
189
- },
190
- ],
191
- tooltip: { trigger: "item" },
192
- legend: {},
193
- };
194
- ```
195
-
196
- ### Treemap
197
-
198
- ```js
199
- // data là hierarchical: { name, value, children: [...] }
200
- vueData.chartOpt = {
201
- series: [
202
- {
203
- type: "treemap",
204
- data: vueData.budgetTree, // hierarchical array
205
- roam: false,
206
- leafDepth: 2,
207
- label: { show: true },
208
- },
209
- ],
210
- tooltip: {},
211
- };
212
- ```
213
-
214
- ### Tree (org chart)
215
-
216
- ```js
217
- vueData.chartOpt = {
218
- series: [
219
- {
220
- type: "tree",
221
- data: [vueData.orgTree], // single root node
222
- orient: "TB", // 'LR' | 'TB' | 'RL' | 'BT'
223
- expandAndCollapse: true,
224
- initialTreeDepth: 2,
225
- label: { position: "top" },
226
- leaves: { label: { position: "bottom" } },
227
- },
228
- ],
229
- tooltip: { trigger: "item" },
230
- };
231
- ```
232
-
233
- ### Sankey
234
-
235
- ```js
236
- // nodes: [{name: '...'}], links: [{source: '...', target: '...', value: N}]
237
- vueData.chartOpt = {
238
- series: [
239
- {
240
- type: "sankey",
241
- data: vueData.sankeyNodes, // [{name: 'A'}, {name: 'B'}, ...]
242
- links: vueData.sankeyLinks, // [{source: 'A', target: 'B', value: 5}, ...]
243
- emphasis: { focus: "adjacency" },
244
- lineStyle: { color: "gradient" },
245
- },
246
- ],
247
- tooltip: { trigger: "item" },
248
- };
249
- ```
250
-
251
- ### Graph (network)
252
-
253
- ```js
254
- vueData.chartOpt = {
255
- series: [
256
- {
257
- type: "graph",
258
- layout: "force", // 'force' | 'circular' | 'none'
259
- data: vueData.nodes, // [{name, value, category}, ...]
260
- links: vueData.edges, // [{source, target}, ...]
261
- categories: vueData.categories, // [{name}, ...]
262
- roam: true,
263
- label: { show: true },
264
- force: { repulsion: 1000 },
265
- },
266
- ],
267
- legend: {},
268
- tooltip: {},
269
- };
270
- ```
271
-
272
- ### Calendar heatmap
273
-
274
- ```js
275
- // data: [{date: '2024-01-01', count: 5}, ...]
276
- vueData.chartOpt = {
277
- calendar: { range: "2024", cellSize: ["auto", 20] },
278
- visualMap: { min: 0, max: 30, type: "piecewise", orient: "horizontal", left: "center" },
279
- series: [
280
- {
281
- type: "heatmap",
282
- coordinateSystem: "calendar",
283
- data: d.data.map((r) => [r.date, r.count]),
284
- },
285
- ],
286
- tooltip: { formatter: (p) => `${p.value[0]}: ${p.value[1]}` },
287
- };
288
- ```
289
-
290
- ---
291
-
292
- ## Zoom + Tooltip format
293
-
294
- ```js
295
- // Zoom (trong option)
296
- vueData.chartOpt = {
297
- ...baseOpt,
298
- dataZoom: [{ type: "inside" }, { type: "slider", height: 20 }],
299
- };
300
- // Hoặc dùng config shortcut:
301
- // config: { zoom: true }
302
- ```
303
-
304
- ```js
305
- // Tooltip tiền tệ — dùng config shortcut
306
- // config: { tooltipFormat: 'currency', symbol: '₫', decimals: 0 }
307
-
308
- // Hoặc custom formatter trong option
309
- tooltip: {
310
- formatter: (p) => {
311
- const fmt = FEchartFormatter.build("currency", { symbol: "₫", decimals: 0 });
312
- return `${p.name}: ${fmt(p.value)}`;
313
- };
314
- }
315
- ```
316
-
317
- ---
318
-
319
- ## Dataset + encode (arbitrary column mapping)
320
-
321
- ```js
322
- // script.js — build option dùng dataset (không map thủ công)
323
- vueData.chartOpt = {
324
- dataset: { source: vueData.rows },
325
- series: [
326
- { type: "bar", name: "Doanh thu", encode: { x: "thang", y: "doanhThu" } },
327
- { type: "line", name: "Chi phí", encode: { x: "thang", y: "chiPhi" }, yAxisIndex: 1 },
328
- ],
329
- xAxis: { type: "category" },
330
- yAxis: [{}, { position: "right" }],
331
- tooltip: { trigger: "axis" },
332
- };
333
- ```
334
-
335
- ---
336
-
337
- ## Real-world patterns
338
-
339
- | Pattern | When |
340
- | ----------------------------------------------- | ------------------------------------------------------------------- |
341
- | **Inline** — `option` written directly in attr | Simple chart, data already in `vueData`, no intermediate variable |
342
- | **vueData ref** — `option` built in `script.js` | Data fetched from API; complex option; sort/transform before render |
343
-
344
- ### Pattern 1 — Inline pie (no script.js)
345
-
346
- Data already in `vueData.rows` → `option` in the attr, no `chartOpt`/script.js.
347
-
348
- ```json
349
- {
350
- "el": "f-echart", "w": "6",
351
- "attr": {
352
- ":config": "{ title: 'Cơ cấu sinh viên', height: 350 }",
353
- ":option": "{
354
- series: [{
355
- type: 'pie',
356
- data: vueData.rows.map(r => ({ name: r.khoa, value: r.soLuong })),
357
- radius: ['40%', '70%'],
358
- label: { formatter: '{b}: {d}%' }
359
- }],
360
- tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
361
- legend: { bottom: 0 }
362
- }"
363
- }
364
- }
365
- ```
366
-
367
- > `vueData.rows` must be assigned first (e.g. from another control's fetch callback in the same module).
368
-
369
- ### Pattern 2 — Inline horizontal bar
370
-
371
- ```json
372
- {
373
- "el": "f-echart", "w": "12",
374
- "attr": {
375
- ":config": "{ height: Math.max(300, vueData.rows.length * 36) }",
376
- ":option": "{
377
- xAxis: { type: 'value' },
378
- yAxis: {
379
- type: 'category',
380
- data: vueData.rows.map(r => r.tenKhoa),
381
- inverse: true
382
- },
383
- grid: { containLabel: true },
384
- series: [{
385
- type: 'bar',
386
- data: vueData.rows.map(r => r.soLuong),
387
- name: 'Sinh viên',
388
- label: { show: true, position: 'right' }
389
- }],
390
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }
391
- }"
392
- }
393
- }
394
- ```
395
-
396
- - `grid.containLabel: true` — required so `yAxis` labels aren't clipped; other `grid` sides only if the user asks.
397
- - `inverse: true` → largest on top.
398
- - `height: Math.max(300, rows.length * 36)` scales with rows.
399
-
400
- ### Pattern 3 — vueData ref + dynamic height (API fetch)
401
-
402
- ```json
403
- {
404
- "el": "f-echart",
405
- "w": "12",
406
- "attr": {
407
- ":config": "{ title: 'Doanh thu theo tháng', height: 400, dataView: true }",
408
- ":option": "vueData.chartOpt"
409
- }
410
- }
411
- ```
412
-
413
- **script.js:**
414
-
415
- ```js
416
- // Trong callback fetch data
417
- vueData.rows = d.data.slice().sort((a, b) => a.thang.localeCompare(b.thang));
418
- vueData.chartOpt = {
419
- xAxis: { type: "category", data: vueData.rows.map((r) => r.thang) },
420
- yAxis: [
421
- { type: "value", name: "Triệu đồng", axisLabel: { formatter: (v) => FEchartFormatter.formatShort(v) } },
422
- { type: "value", name: "%", position: "right", max: 100 },
423
- ],
424
- series: [
425
- { type: "bar", name: "Doanh thu", data: vueData.rows.map((r) => r.doanhThu), yAxisIndex: 0 },
426
- { type: "line", name: "Tăng trưởng", data: vueData.rows.map((r) => r.tangTruong), yAxisIndex: 1, smooth: true },
427
- ],
428
- grid: { containLabel: true },
429
- tooltip: { trigger: "axis" },
430
- legend: {},
431
- };
432
- ```
433
-
434
- ### Pattern 4 — Conditional option (null guard)
435
-
436
- Several data sources → guard against rendering empty data.
437
-
438
- ```json
439
- {
440
- "el": "f-echart",
441
- "w": "12",
442
- "attr": {
443
- ":config": "{ height: 350 }",
444
- ":option": "vueData.baseOpt ? Object.assign({}, vueData.baseOpt, { series: vueData.series }) : null"
445
- }
446
- }
447
- ```
448
-
449
- **script.js:**
450
-
451
- ```js
452
- // Base option — khởi tạo một lần
453
- vueData.baseOpt = {
454
- xAxis: { type: "category", data: [] },
455
- yAxis: { type: "value" },
456
- tooltip: { trigger: "axis" },
457
- legend: {},
458
- grid: { containLabel: true },
459
- };
460
-
461
- // Khi fetch data xong — reactive update
462
- vueData.baseOpt.xAxis.data = d.data.map((r) => r.thang);
463
- vueData.series = d.data.map((r, i) => ({
464
- type: "bar",
465
- name: r.name,
466
- data: r.values,
467
- stack: "total",
468
- }));
469
- // Object.assign trong :option tạo object mới → Vue trigger re-render
470
- ```
471
-
472
- > `Object.assign({}, base, override)` in `:option` yields a new object each evaluation, so Vue detects the change.
473
-
474
- ### Pattern 5 — No DataView (most common)
475
-
476
- Most production charts omit `dataView`. There is no `:data` prop.
477
-
478
- ```json
479
- {
480
- "el": "f-echart",
481
- "w": "6",
482
- "attr": {
483
- ":config": "{ height: 300 }",
484
- ":option": "vueData.pieOpt"
485
- }
486
- }
487
- ```
488
-
489
- Add `dataView: true` only when the user needs the data table (auto-extracted from `option`).
1
+ # f-echart — Chart Type Templates & Real-World Patterns
2
+
3
+ > Owns: **`option` templates per chart type + matching module.json patterns**. Load once you know which chart to draw. **Read the core first** — import/props/config/theme/DataView/errors: [components-echart.md](components-echart.md).
4
+
5
+ ---
6
+
7
+ ## Templates — Chart types
8
+
9
+ > All templates: declare `grid` sides only for non-default layout (engine reserves title/legend space); `grid` vs `boundaryGap`: [components-echart.md](components-echart.md) §Grid padding vs boundaryGap. `legend: {}` in `option` **anchors top**; for bottom use `legend: { bottom: 0 }` or `config.legend: true`.
10
+
11
+ ### Bar chart
12
+
13
+ ```js
14
+ // script.js — callback sau fetch
15
+ vueData.chartOpt = {
16
+ xAxis: { type: "category", data: d.data.map((r) => r.thang) },
17
+ yAxis: { type: "value" },
18
+ series: [{ type: "bar", data: d.data.map((r) => r.doanhThu), name: "Doanh thu" }],
19
+ tooltip: { trigger: "axis" },
20
+ legend: {},
21
+ };
22
+ vueData.rows = d.data;
23
+ ```
24
+
25
+ ### Line chart (smooth)
26
+
27
+ ```js
28
+ vueData.chartOpt = {
29
+ xAxis: { type: "category", data: d.data.map((r) => r.thang) },
30
+ yAxis: { type: "value" },
31
+ series: [{ type: "line", data: d.data.map((r) => r.val), smooth: true, areaStyle: {} }],
32
+ tooltip: { trigger: "axis" },
33
+ };
34
+ ```
35
+
36
+ ### Multi-series + Dual Y-axis
37
+
38
+ ```js
39
+ vueData.chartOpt = {
40
+ xAxis: { type: "category", data: d.data.map((r) => r.thang) },
41
+ yAxis: [{ name: "VNĐ" }, { name: "%", position: "right" }],
42
+ series: [
43
+ { type: "bar", name: "Doanh thu", data: d.data.map((r) => r.doanhThu), yAxisIndex: 0 },
44
+ { type: "line", name: "Tăng trưởng", data: d.data.map((r) => r.tangTruong), yAxisIndex: 1, smooth: true },
45
+ ],
46
+ tooltip: { trigger: "axis" },
47
+ legend: {},
48
+ };
49
+ ```
50
+
51
+ ### Stacked bar
52
+
53
+ ```js
54
+ vueData.chartOpt = {
55
+ xAxis: { type: "category", data: d.data.map((r) => r.thang) },
56
+ yAxis: { type: "value" },
57
+ series: [
58
+ { type: "bar", name: "SP A", data: d.data.map((r) => r.spA), stack: "total" },
59
+ { type: "bar", name: "SP B", data: d.data.map((r) => r.spB), stack: "total" },
60
+ { type: "bar", name: "SP C", data: d.data.map((r) => r.spC), stack: "total" },
61
+ ],
62
+ tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
63
+ legend: {},
64
+ };
65
+ ```
66
+
67
+ ### Pie chart
68
+
69
+ ```js
70
+ vueData.chartOpt = {
71
+ series: [
72
+ {
73
+ type: "pie",
74
+ data: d.data.map((r) => ({ name: r.khoa, value: r.soLuong })),
75
+ radius: "70%",
76
+ label: { formatter: "{b}: {d}%" },
77
+ },
78
+ ],
79
+ tooltip: { trigger: "item" },
80
+ legend: { bottom: 0 },
81
+ };
82
+ ```
83
+
84
+ Doughnut: `radius: ['40%', '70%']`
85
+
86
+ ### Scatter plot
87
+
88
+ ```js
89
+ vueData.chartOpt = {
90
+ xAxis: { type: "value", name: "Điểm thi" },
91
+ yAxis: { type: "value", name: "GPA" },
92
+ series: [{ type: "scatter", data: d.data.map((r) => [r.diemThi, r.gpa, r.hoTen]), symbolSize: 8 }],
93
+ tooltip: {
94
+ formatter: (p) => `${p.value[2]}<br>Điểm: ${p.value[0]} — GPA: ${p.value[1]}`,
95
+ },
96
+ };
97
+ ```
98
+
99
+ ### Gauge
100
+
101
+ ```js
102
+ vueData.chartOpt = {
103
+ series: [
104
+ {
105
+ type: "gauge",
106
+ min: 0,
107
+ max: 100,
108
+ data: [{ value: vueData.cpuPercent, name: "CPU %" }],
109
+ detail: { formatter: "{value}%" },
110
+ axisLabel: { color: "auto" },
111
+ },
112
+ ],
113
+ tooltip: {},
114
+ };
115
+ ```
116
+
117
+ ### Radar
118
+
119
+ ```js
120
+ vueData.chartOpt = {
121
+ radar: {
122
+ indicator: [
123
+ { name: "Toán", max: 10 },
124
+ { name: "Lý", max: 10 },
125
+ { name: "Hóa", max: 10 },
126
+ { name: "Văn", max: 10 },
127
+ { name: "Anh", max: 10 },
128
+ ],
129
+ },
130
+ series: [
131
+ {
132
+ type: "radar",
133
+ data: d.data.map((r) => ({
134
+ name: r.hoTen,
135
+ value: [r.toan, r.ly, r.hoa, r.van, r.anh],
136
+ })),
137
+ },
138
+ ],
139
+ tooltip: {},
140
+ legend: {},
141
+ };
142
+ ```
143
+
144
+ ### Heatmap (Cartesian)
145
+
146
+ ```js
147
+ // data: [{x: 'Mon', y: '8h', value: 5}, ...]
148
+ const xData = [...new Set(d.data.map((r) => r.x))];
149
+ const yData = [...new Set(d.data.map((r) => r.y))];
150
+ vueData.chartOpt = {
151
+ xAxis: { type: "category", data: xData },
152
+ yAxis: { type: "category", data: yData },
153
+ visualMap: { min: 0, max: 10, calculable: true },
154
+ series: [
155
+ {
156
+ type: "heatmap",
157
+ data: d.data.map((r) => [xData.indexOf(r.x), yData.indexOf(r.y), r.value]),
158
+ },
159
+ ],
160
+ tooltip: {},
161
+ };
162
+ ```
163
+
164
+ ### Candlestick (OHLC)
165
+
166
+ ```js
167
+ vueData.chartOpt = {
168
+ xAxis: { type: "category", data: d.data.map((r) => r.date) },
169
+ yAxis: { type: "value", scale: true },
170
+ series: [
171
+ {
172
+ type: "candlestick",
173
+ data: d.data.map((r) => [r.open, r.close, r.low, r.high]),
174
+ },
175
+ ],
176
+ tooltip: { trigger: "axis" },
177
+ };
178
+ ```
179
+
180
+ ### Funnel
181
+
182
+ ```js
183
+ vueData.chartOpt = {
184
+ series: [
185
+ {
186
+ type: "funnel",
187
+ data: d.data.map((r) => ({ name: r.buoc, value: r.soLuong })).sort((a, b) => b.value - a.value),
188
+ label: { formatter: "{b}: {d}%" },
189
+ },
190
+ ],
191
+ tooltip: { trigger: "item" },
192
+ legend: {},
193
+ };
194
+ ```
195
+
196
+ ### Treemap
197
+
198
+ ```js
199
+ // data là hierarchical: { name, value, children: [...] }
200
+ vueData.chartOpt = {
201
+ series: [
202
+ {
203
+ type: "treemap",
204
+ data: vueData.budgetTree, // hierarchical array
205
+ roam: false,
206
+ leafDepth: 2,
207
+ label: { show: true },
208
+ },
209
+ ],
210
+ tooltip: {},
211
+ };
212
+ ```
213
+
214
+ ### Tree (org chart)
215
+
216
+ ```js
217
+ vueData.chartOpt = {
218
+ series: [
219
+ {
220
+ type: "tree",
221
+ data: [vueData.orgTree], // single root node
222
+ orient: "TB", // 'LR' | 'TB' | 'RL' | 'BT'
223
+ expandAndCollapse: true,
224
+ initialTreeDepth: 2,
225
+ label: { position: "top" },
226
+ leaves: { label: { position: "bottom" } },
227
+ },
228
+ ],
229
+ tooltip: { trigger: "item" },
230
+ };
231
+ ```
232
+
233
+ ### Sankey
234
+
235
+ ```js
236
+ // nodes: [{name: '...'}], links: [{source: '...', target: '...', value: N}]
237
+ vueData.chartOpt = {
238
+ series: [
239
+ {
240
+ type: "sankey",
241
+ data: vueData.sankeyNodes, // [{name: 'A'}, {name: 'B'}, ...]
242
+ links: vueData.sankeyLinks, // [{source: 'A', target: 'B', value: 5}, ...]
243
+ emphasis: { focus: "adjacency" },
244
+ lineStyle: { color: "gradient" },
245
+ },
246
+ ],
247
+ tooltip: { trigger: "item" },
248
+ };
249
+ ```
250
+
251
+ ### Graph (network)
252
+
253
+ ```js
254
+ vueData.chartOpt = {
255
+ series: [
256
+ {
257
+ type: "graph",
258
+ layout: "force", // 'force' | 'circular' | 'none'
259
+ data: vueData.nodes, // [{name, value, category}, ...]
260
+ links: vueData.edges, // [{source, target}, ...]
261
+ categories: vueData.categories, // [{name}, ...]
262
+ roam: true,
263
+ label: { show: true },
264
+ force: { repulsion: 1000 },
265
+ },
266
+ ],
267
+ legend: {},
268
+ tooltip: {},
269
+ };
270
+ ```
271
+
272
+ ### Calendar heatmap
273
+
274
+ ```js
275
+ // data: [{date: '2024-01-01', count: 5}, ...]
276
+ vueData.chartOpt = {
277
+ calendar: { range: "2024", cellSize: ["auto", 20] },
278
+ visualMap: { min: 0, max: 30, type: "piecewise", orient: "horizontal", left: "center" },
279
+ series: [
280
+ {
281
+ type: "heatmap",
282
+ coordinateSystem: "calendar",
283
+ data: d.data.map((r) => [r.date, r.count]),
284
+ },
285
+ ],
286
+ tooltip: { formatter: (p) => `${p.value[0]}: ${p.value[1]}` },
287
+ };
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Zoom + Tooltip format
293
+
294
+ ```js
295
+ // Zoom (trong option)
296
+ vueData.chartOpt = {
297
+ ...baseOpt,
298
+ dataZoom: [{ type: "inside" }, { type: "slider", height: 20 }],
299
+ };
300
+ // Hoặc dùng config shortcut:
301
+ // config: { zoom: true }
302
+ ```
303
+
304
+ ```js
305
+ // Tooltip tiền tệ — dùng config shortcut
306
+ // config: { tooltipFormat: 'currency', symbol: '₫', decimals: 0 }
307
+
308
+ // Hoặc custom formatter trong option
309
+ tooltip: {
310
+ formatter: (p) => {
311
+ const fmt = FEchartFormatter.build("currency", { symbol: "₫", decimals: 0 });
312
+ return `${p.name}: ${fmt(p.value)}`;
313
+ };
314
+ }
315
+ ```
316
+
317
+ ---
318
+
319
+ ## Dataset + encode (arbitrary column mapping)
320
+
321
+ ```js
322
+ // script.js — build option dùng dataset (không map thủ công)
323
+ vueData.chartOpt = {
324
+ dataset: { source: vueData.rows },
325
+ series: [
326
+ { type: "bar", name: "Doanh thu", encode: { x: "thang", y: "doanhThu" } },
327
+ { type: "line", name: "Chi phí", encode: { x: "thang", y: "chiPhi" }, yAxisIndex: 1 },
328
+ ],
329
+ xAxis: { type: "category" },
330
+ yAxis: [{}, { position: "right" }],
331
+ tooltip: { trigger: "axis" },
332
+ };
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Real-world patterns
338
+
339
+ | Pattern | When |
340
+ | ----------------------------------------------- | ------------------------------------------------------------------- |
341
+ | **Inline** — `option` written directly in attr | Simple chart, data already in `vueData`, no intermediate variable |
342
+ | **vueData ref** — `option` built in `script.js` | Data fetched from API; complex option; sort/transform before render |
343
+
344
+ ### Pattern 1 — Inline pie (no script.js)
345
+
346
+ Data already in `vueData.rows` → `option` in the attr, no `chartOpt`/script.js.
347
+
348
+ ```json
349
+ {
350
+ "el": "f-echart", "w": "6",
351
+ "attr": {
352
+ ":config": "{ title: 'Cơ cấu sinh viên', height: 350 }",
353
+ ":option": "{
354
+ series: [{
355
+ type: 'pie',
356
+ data: vueData.rows.map(r => ({ name: r.khoa, value: r.soLuong })),
357
+ radius: ['40%', '70%'],
358
+ label: { formatter: '{b}: {d}%' }
359
+ }],
360
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
361
+ legend: { bottom: 0 }
362
+ }"
363
+ }
364
+ }
365
+ ```
366
+
367
+ > `vueData.rows` must be assigned first (e.g. from another control's fetch callback in the same module).
368
+
369
+ ### Pattern 2 — Inline horizontal bar
370
+
371
+ ```json
372
+ {
373
+ "el": "f-echart", "w": "12",
374
+ "attr": {
375
+ ":config": "{ height: Math.max(300, vueData.rows.length * 36) }",
376
+ ":option": "{
377
+ xAxis: { type: 'value' },
378
+ yAxis: {
379
+ type: 'category',
380
+ data: vueData.rows.map(r => r.tenKhoa),
381
+ inverse: true
382
+ },
383
+ grid: { containLabel: true },
384
+ series: [{
385
+ type: 'bar',
386
+ data: vueData.rows.map(r => r.soLuong),
387
+ name: 'Sinh viên',
388
+ label: { show: true, position: 'right' }
389
+ }],
390
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }
391
+ }"
392
+ }
393
+ }
394
+ ```
395
+
396
+ - `grid.containLabel: true` — required so `yAxis` labels aren't clipped; other `grid` sides only if the user asks.
397
+ - `inverse: true` → largest on top.
398
+ - `height: Math.max(300, rows.length * 36)` scales with rows.
399
+
400
+ ### Pattern 3 — vueData ref + dynamic height (API fetch)
401
+
402
+ ```json
403
+ {
404
+ "el": "f-echart",
405
+ "w": "12",
406
+ "attr": {
407
+ ":config": "{ title: 'Doanh thu theo tháng', height: 400, dataView: true }",
408
+ ":option": "vueData.chartOpt"
409
+ }
410
+ }
411
+ ```
412
+
413
+ **script.js:**
414
+
415
+ ```js
416
+ // Trong callback fetch data
417
+ vueData.rows = d.data.slice().sort((a, b) => a.thang.localeCompare(b.thang));
418
+ vueData.chartOpt = {
419
+ xAxis: { type: "category", data: vueData.rows.map((r) => r.thang) },
420
+ yAxis: [
421
+ { type: "value", name: "Triệu đồng", axisLabel: { formatter: (v) => FEchartFormatter.formatShort(v) } },
422
+ { type: "value", name: "%", position: "right", max: 100 },
423
+ ],
424
+ series: [
425
+ { type: "bar", name: "Doanh thu", data: vueData.rows.map((r) => r.doanhThu), yAxisIndex: 0 },
426
+ { type: "line", name: "Tăng trưởng", data: vueData.rows.map((r) => r.tangTruong), yAxisIndex: 1, smooth: true },
427
+ ],
428
+ grid: { containLabel: true },
429
+ tooltip: { trigger: "axis" },
430
+ legend: {},
431
+ };
432
+ ```
433
+
434
+ ### Pattern 4 — Conditional option (null guard)
435
+
436
+ Several data sources → guard against rendering empty data.
437
+
438
+ ```json
439
+ {
440
+ "el": "f-echart",
441
+ "w": "12",
442
+ "attr": {
443
+ ":config": "{ height: 350 }",
444
+ ":option": "vueData.baseOpt ? Object.assign({}, vueData.baseOpt, { series: vueData.series }) : null"
445
+ }
446
+ }
447
+ ```
448
+
449
+ **script.js:**
450
+
451
+ ```js
452
+ // Base option — khởi tạo một lần
453
+ vueData.baseOpt = {
454
+ xAxis: { type: "category", data: [] },
455
+ yAxis: { type: "value" },
456
+ tooltip: { trigger: "axis" },
457
+ legend: {},
458
+ grid: { containLabel: true },
459
+ };
460
+
461
+ // Khi fetch data xong — reactive update
462
+ vueData.baseOpt.xAxis.data = d.data.map((r) => r.thang);
463
+ vueData.series = d.data.map((r, i) => ({
464
+ type: "bar",
465
+ name: r.name,
466
+ data: r.values,
467
+ stack: "total",
468
+ }));
469
+ // Object.assign trong :option tạo object mới → Vue trigger re-render
470
+ ```
471
+
472
+ > `Object.assign({}, base, override)` in `:option` yields a new object each evaluation, so Vue detects the change.
473
+
474
+ ### Pattern 5 — No DataView (most common)
475
+
476
+ Most production charts omit `dataView`. There is no `:data` prop.
477
+
478
+ ```json
479
+ {
480
+ "el": "f-echart",
481
+ "w": "6",
482
+ "attr": {
483
+ ":config": "{ height: 300 }",
484
+ ":option": "vueData.pieOpt"
485
+ }
486
+ }
487
+ ```
488
+
489
+ Add `dataView: true` only when the user needs the data table (auto-extracted from `option`).