create-yss-spec 3.5.2 → 3.5.3

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 (438) hide show
  1. package/package.json +1 -1
  2. package/src/template/asset-runtime.js +3 -1
  3. package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
  4. package/template/.agents/skills/codebase-design/SKILL.md +4 -0
  5. package/template/{.codex/skills/improve-codebase-architecture/SKILL.md → .agents/skills/codebase-design/references/architecture-audit.md} +3 -9
  6. package/template/.agents/skills/frontend-commit/SKILL.md +4 -93
  7. package/template/.agents/skills/git-commit-core/SKILL.md +18 -0
  8. package/template/.agents/skills/java-backend-commit/SKILL.md +4 -96
  9. package/template/.agents/skills/llm-wiki/SKILL.md +12 -13
  10. package/template/.agents/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  11. package/template/.agents/skills/llm-wiki/references/compile.md +21 -114
  12. package/template/.agents/skills/llm-wiki/references/ingest.md +8 -33
  13. package/template/.agents/skills/llm-wiki/references/lint.md +9 -46
  14. package/template/.agents/skills/llm-wiki/references/query.md +9 -18
  15. package/template/.agents/skills/llm-wiki/references/schema.md +56 -29
  16. package/template/.agents/skills/llm-wiki/references/transactions.md +48 -0
  17. package/template/.agents/skills/llm-wiki/references/writing.md +4 -4
  18. package/template/.agents/skills/llm-wiki/scripts/advise.mjs +46 -58
  19. package/template/.agents/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  20. package/template/.agents/skills/llm-wiki/scripts/core.mjs +262 -0
  21. package/template/.agents/skills/llm-wiki/scripts/extract.mjs +35 -18
  22. package/template/.agents/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  23. package/template/.agents/skills/llm-wiki/scripts/feedback.mjs +51 -0
  24. package/template/.agents/skills/llm-wiki/scripts/inventory.mjs +43 -171
  25. package/template/.agents/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  26. package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  27. package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  28. package/template/.agents/skills/llm-wiki/scripts/migrate.mjs +51 -0
  29. package/template/.agents/skills/llm-wiki/scripts/query.mjs +149 -0
  30. package/template/.agents/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  31. package/template/.agents/skills/llm-wiki/scripts/sources.mjs +166 -0
  32. package/template/.agents/skills/llm-wiki/scripts/transaction.mjs +384 -0
  33. package/template/.agents/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  34. package/template/.agents/skills/prototype/LOGIC.md +1 -1
  35. package/template/.agents/skills/prototype/SKILL.md +5 -19
  36. package/template/.agents/skills/setup-matt-pocock-skills/SKILL.md +3 -3
  37. package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  38. package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
  39. package/template/.agents/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  40. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +8 -8
  41. package/template/.agents/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  42. package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
  43. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
  44. package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +2 -2
  45. package/template/.agents/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  46. package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +1 -1
  47. package/template/.agents/skills/yss-research/SKILL.md +4 -0
  48. package/template/.codex/skills/codebase-design/SKILL.md +4 -0
  49. package/template/{.pi/skills/improve-codebase-architecture/SKILL.md → .codex/skills/codebase-design/references/architecture-audit.md} +3 -9
  50. package/template/.codex/skills/frontend-commit/SKILL.md +4 -93
  51. package/template/.codex/skills/git-commit-core/SKILL.md +18 -0
  52. package/template/.codex/skills/java-backend-commit/SKILL.md +4 -96
  53. package/template/.codex/skills/llm-wiki/SKILL.md +12 -13
  54. package/template/.codex/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  55. package/template/.codex/skills/llm-wiki/references/compile.md +21 -114
  56. package/template/.codex/skills/llm-wiki/references/ingest.md +8 -33
  57. package/template/.codex/skills/llm-wiki/references/lint.md +9 -46
  58. package/template/.codex/skills/llm-wiki/references/query.md +9 -18
  59. package/template/.codex/skills/llm-wiki/references/schema.md +56 -29
  60. package/template/.codex/skills/llm-wiki/references/transactions.md +48 -0
  61. package/template/.codex/skills/llm-wiki/references/writing.md +4 -4
  62. package/template/.codex/skills/llm-wiki/scripts/advise.mjs +46 -58
  63. package/template/.codex/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  64. package/template/.codex/skills/llm-wiki/scripts/core.mjs +262 -0
  65. package/template/.codex/skills/llm-wiki/scripts/extract.mjs +35 -18
  66. package/template/.codex/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  67. package/template/.codex/skills/llm-wiki/scripts/feedback.mjs +51 -0
  68. package/template/.codex/skills/llm-wiki/scripts/inventory.mjs +43 -171
  69. package/template/.codex/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  70. package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  71. package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  72. package/template/.codex/skills/llm-wiki/scripts/migrate.mjs +51 -0
  73. package/template/.codex/skills/llm-wiki/scripts/query.mjs +149 -0
  74. package/template/.codex/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  75. package/template/.codex/skills/llm-wiki/scripts/sources.mjs +166 -0
  76. package/template/.codex/skills/llm-wiki/scripts/transaction.mjs +384 -0
  77. package/template/.codex/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  78. package/template/.codex/skills/prototype/LOGIC.md +1 -1
  79. package/template/.codex/skills/prototype/SKILL.md +5 -19
  80. package/template/.codex/skills/setup-matt-pocock-skills/SKILL.md +3 -3
  81. package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  82. package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
  83. package/template/.codex/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  84. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +8 -8
  85. package/template/.codex/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  86. package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
  87. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
  88. package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +2 -2
  89. package/template/.codex/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  90. package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +1 -1
  91. package/template/.codex/skills/yss-research/SKILL.md +4 -0
  92. package/template/.cursor/skills/codebase-design/SKILL.md +4 -0
  93. package/template/.cursor/skills/{improve-codebase-architecture/SKILL.md → codebase-design/references/architecture-audit.md} +3 -9
  94. package/template/.cursor/skills/frontend-commit/SKILL.md +4 -93
  95. package/template/.cursor/skills/git-commit-core/SKILL.md +18 -0
  96. package/template/.cursor/skills/java-backend-commit/SKILL.md +4 -96
  97. package/template/.cursor/skills/llm-wiki/SKILL.md +12 -13
  98. package/template/.cursor/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  99. package/template/.cursor/skills/llm-wiki/references/compile.md +21 -114
  100. package/template/.cursor/skills/llm-wiki/references/ingest.md +8 -33
  101. package/template/.cursor/skills/llm-wiki/references/lint.md +9 -46
  102. package/template/.cursor/skills/llm-wiki/references/query.md +9 -18
  103. package/template/.cursor/skills/llm-wiki/references/schema.md +56 -29
  104. package/template/.cursor/skills/llm-wiki/references/transactions.md +48 -0
  105. package/template/.cursor/skills/llm-wiki/references/writing.md +4 -4
  106. package/template/.cursor/skills/llm-wiki/scripts/advise.mjs +46 -58
  107. package/template/.cursor/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  108. package/template/.cursor/skills/llm-wiki/scripts/core.mjs +262 -0
  109. package/template/.cursor/skills/llm-wiki/scripts/extract.mjs +35 -18
  110. package/template/.cursor/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  111. package/template/.cursor/skills/llm-wiki/scripts/feedback.mjs +51 -0
  112. package/template/.cursor/skills/llm-wiki/scripts/inventory.mjs +43 -171
  113. package/template/.cursor/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  114. package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  115. package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  116. package/template/.cursor/skills/llm-wiki/scripts/migrate.mjs +51 -0
  117. package/template/.cursor/skills/llm-wiki/scripts/query.mjs +149 -0
  118. package/template/.cursor/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  119. package/template/.cursor/skills/llm-wiki/scripts/sources.mjs +166 -0
  120. package/template/.cursor/skills/llm-wiki/scripts/transaction.mjs +384 -0
  121. package/template/.cursor/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  122. package/template/.cursor/skills/prototype/LOGIC.md +1 -1
  123. package/template/.cursor/skills/prototype/SKILL.md +5 -19
  124. package/template/.cursor/skills/setup-matt-pocock-skills/SKILL.md +3 -3
  125. package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  126. package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
  127. package/template/.cursor/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  128. package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +8 -8
  129. package/template/.cursor/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  130. package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
  131. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
  132. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +2 -2
  133. package/template/.cursor/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  134. package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +1 -1
  135. package/template/.cursor/skills/yss-research/SKILL.md +4 -0
  136. package/template/.pi/skills/codebase-design/SKILL.md +4 -0
  137. package/template/{.agents/skills/improve-codebase-architecture/SKILL.md → .pi/skills/codebase-design/references/architecture-audit.md} +3 -9
  138. package/template/.pi/skills/frontend-commit/SKILL.md +4 -93
  139. package/template/.pi/skills/git-commit-core/SKILL.md +18 -0
  140. package/template/.pi/skills/java-backend-commit/SKILL.md +4 -96
  141. package/template/.pi/skills/llm-wiki/SKILL.md +12 -13
  142. package/template/.pi/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  143. package/template/.pi/skills/llm-wiki/references/compile.md +21 -114
  144. package/template/.pi/skills/llm-wiki/references/ingest.md +8 -33
  145. package/template/.pi/skills/llm-wiki/references/lint.md +9 -46
  146. package/template/.pi/skills/llm-wiki/references/query.md +9 -18
  147. package/template/.pi/skills/llm-wiki/references/schema.md +56 -29
  148. package/template/.pi/skills/llm-wiki/references/transactions.md +48 -0
  149. package/template/.pi/skills/llm-wiki/references/writing.md +4 -4
  150. package/template/.pi/skills/llm-wiki/scripts/advise.mjs +46 -58
  151. package/template/.pi/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  152. package/template/.pi/skills/llm-wiki/scripts/core.mjs +262 -0
  153. package/template/.pi/skills/llm-wiki/scripts/extract.mjs +35 -18
  154. package/template/.pi/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  155. package/template/.pi/skills/llm-wiki/scripts/feedback.mjs +51 -0
  156. package/template/.pi/skills/llm-wiki/scripts/inventory.mjs +43 -171
  157. package/template/.pi/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  158. package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  159. package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  160. package/template/.pi/skills/llm-wiki/scripts/migrate.mjs +51 -0
  161. package/template/.pi/skills/llm-wiki/scripts/query.mjs +149 -0
  162. package/template/.pi/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  163. package/template/.pi/skills/llm-wiki/scripts/sources.mjs +166 -0
  164. package/template/.pi/skills/llm-wiki/scripts/transaction.mjs +384 -0
  165. package/template/.pi/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  166. package/template/.pi/skills/prototype/LOGIC.md +1 -1
  167. package/template/.pi/skills/prototype/SKILL.md +5 -19
  168. package/template/.pi/skills/setup-matt-pocock-skills/SKILL.md +3 -3
  169. package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  170. package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +2 -3
  171. package/template/.pi/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  172. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +8 -8
  173. package/template/.pi/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  174. package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +5 -5
  175. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +20 -4
  176. package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +2 -2
  177. package/template/.pi/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  178. package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +1 -1
  179. package/template/.pi/skills/yss-research/SKILL.md +4 -0
  180. package/template/.template-spec/agents/README.md +1 -1
  181. package/template/.template-spec/agents/digital-human-roles.yaml +1 -1
  182. package/template/.template-spec/agents/skill-migrations.md +15 -0
  183. package/template/.template-spec/agents/yss-skill-registry.yaml +10 -29
  184. package/template/.template-spec/architecture/README.md +3 -34
  185. package/template/.template-spec/plan/templates/competitive-analysis-template.md +2 -2
  186. package/template/.template-spec/process/contract-reading.md +24 -0
  187. package/template/.template-spec/process/lifecycle-registry-baseline.json +3 -1
  188. package/template/.template-spec/process/lifecycle-registry.yaml +6 -0
  189. package/template/.template-spec/process/research-completion.md +23 -0
  190. package/template/.template-spec/process/schemas/digital-human-task-package.schema.json +202 -12
  191. package/template/.template-spec/process/schemas/frontend-implementation-evidence.schema.json +7 -15
  192. package/template/.template-spec/process/subagent-collaboration.md +8 -0
  193. package/template/.template-spec/process/templates/frontend-implementation-verification-template.yaml +15 -1
  194. package/template/AGENTS.md +1 -1
  195. package/template/CONTEXT.md +5 -1
  196. package/template/README.md +12 -24
  197. package/template/scripts/contract +5 -5
  198. package/template/scripts/lib/contract-views.mjs +57 -2
  199. package/template/scripts/lib/execution-evidence.mjs +51 -0
  200. package/template/scripts/lib/harness-execution-scope.mjs +1 -1
  201. package/template/scripts/lib/implementation-contract-compiler.mjs +2 -0
  202. package/template/scripts/lib/lifecycle-context-query.mjs +15 -3
  203. package/template/scripts/lib/lifecycle-status.mjs +32 -6
  204. package/template/scripts/lib/lifecycle-transition.mjs +19 -1
  205. package/template/scripts/lib/maintenance-research.mjs +63 -0
  206. package/template/scripts/lib/read-only-intake.mjs +132 -0
  207. package/template/scripts/lib/skill-registry.mjs +2 -1
  208. package/template/scripts/lib/skill-supply-chain.mjs +4 -0
  209. package/template/scripts/lib/slice-task-package.mjs +1 -1
  210. package/template/scripts/lib/task-package.mjs +9 -2
  211. package/template/scripts/lifecycle-status +2 -2
  212. package/template/scripts/prepare-read-only-intake +16 -0
  213. package/template/scripts/run-read-only-intake +14 -0
  214. package/template/scripts/verify-digital-human-task-package +2 -2
  215. package/template/scripts/verify-frontend-implementation-evidence +24 -1
  216. package/template/scripts/verify-maintenance-research +13 -0
  217. package/template/skills-lock.json +19 -76
  218. package/template.manifest.json +118 -29
  219. package/template.snapshot.json +5 -6
  220. package/template/.agents/skills/grill-with-docs/SKILL.md +0 -20
  221. package/template/.agents/skills/grill-with-docs/agents/openai.yaml +0 -5
  222. package/template/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  223. package/template/.agents/skills/prototype/UI.md +0 -112
  224. package/template/.agents/skills/setup-matt-pocock-skills/domain.md +0 -37
  225. package/template/.agents/skills/to-questionnaire/SKILL.md +0 -55
  226. package/template/.agents/skills/to-questionnaire/agents/openai.yaml +0 -5
  227. package/template/.agents/skills/wait-what/SKILL.md +0 -7
  228. package/template/.agents/skills/wait-what/agents/openai.yaml +0 -5
  229. package/template/.codex/skills/data-analytics/.app.json +0 -84
  230. package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +0 -66
  231. package/template/.codex/skills/data-analytics/.mcp.json +0 -30
  232. package/template/.codex/skills/data-analytics/AGENTS.md +0 -20
  233. package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +0 -27
  234. package/template/.codex/skills/data-analytics/README.md +0 -60
  235. package/template/.codex/skills/data-analytics/__yss_dotfile__.gitignore +0 -2
  236. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +0 -16
  237. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +0 -1
  238. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +0 -1
  239. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +0 -1
  240. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +0 -1
  241. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +0 -1
  242. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +0 -1
  243. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +0 -16
  244. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +0 -1
  245. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +0 -1
  246. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +0 -1
  247. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +0 -1
  248. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +0 -1
  249. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +0 -1
  250. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +0 -1
  251. package/template/.codex/skills/data-analytics/assets/datascience-small.svg +0 -5
  252. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +0 -16
  253. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +0 -1
  254. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +0 -1
  255. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +0 -1
  256. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +0 -1
  257. package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
  258. package/template/.codex/skills/data-analytics/assets/datascience.svg +0 -10
  259. package/template/.codex/skills/data-analytics/mcp/server.cjs +0 -2964
  260. package/template/.codex/skills/data-analytics/package-lock.json +0 -3048
  261. package/template/.codex/skills/data-analytics/package.json +0 -43
  262. package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +0 -75
  263. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +0 -137
  264. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +0 -6
  265. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/references/quality-checks.md +0 -29
  266. package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +0 -148
  267. package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +0 -6
  268. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +0 -18
  269. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +0 -24
  270. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +0 -71
  271. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +0 -85
  272. package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +0 -207
  273. package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +0 -6
  274. package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +0 -70
  275. package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +0 -66
  276. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +0 -108
  277. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +0 -6
  278. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +0 -1
  279. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +0 -52
  280. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +0 -61
  281. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +0 -362
  282. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +0 -829
  283. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +0 -30
  284. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +0 -127
  285. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +0 -374
  286. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +0 -613
  287. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +0 -54
  288. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +0 -16
  289. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +0 -9
  290. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/tests/test_delivery_plan.py +0 -45
  291. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +0 -77
  292. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +0 -6
  293. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +0 -2379
  294. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +0 -88
  295. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +0 -6
  296. package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +0 -97
  297. package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +0 -59
  298. package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +0 -75
  299. package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +0 -103
  300. package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +0 -6
  301. package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +0 -68
  302. package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +0 -6
  303. package/template/.codex/skills/data-analytics/skills/index/SKILL.md +0 -251
  304. package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +0 -6
  305. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +0 -131
  306. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +0 -6
  307. package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +0 -141
  308. package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +0 -6
  309. package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +0 -32
  310. package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +0 -106
  311. package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +0 -6
  312. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +0 -130
  313. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +0 -6
  314. package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +0 -141
  315. package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +0 -6
  316. package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +0 -178
  317. package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +0 -9
  318. package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
  319. package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +0 -31
  320. package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +0 -466
  321. package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +0 -99
  322. package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +0 -197
  323. package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +0 -6
  324. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +0 -26
  325. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +0 -51
  326. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +0 -66
  327. package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +0 -69
  328. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +0 -204
  329. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +0 -87
  330. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +0 -497
  331. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +0 -74
  332. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +0 -65
  333. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +0 -160
  334. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +0 -75
  335. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +0 -96
  336. package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +0 -263
  337. package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +0 -1462
  338. package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +0 -128
  339. package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +0 -101
  340. package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +0 -499
  341. package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +0 -978
  342. package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +0 -126
  343. package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +0 -6
  344. package/template/.codex/skills/data-analytics/skills/validate-data/references/validation-methods.md +0 -70
  345. package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +0 -157
  346. package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +0 -6
  347. package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +0 -774
  348. package/template/.codex/skills/data-analytics/src/DESIGN.md +0 -222
  349. package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +0 -3666
  350. package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +0 -136
  351. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +0 -54
  352. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +0 -96
  353. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +0 -1647
  354. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +0 -245
  355. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +0 -462
  356. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +0 -107
  357. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +0 -164
  358. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +0 -192
  359. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +0 -203
  360. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +0 -619
  361. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +0 -402
  362. package/template/.codex/skills/data-analytics/src/analytics-app/fonts/SystemSansVariableVF.woff2 +0 -0
  363. package/template/.codex/skills/data-analytics/src/analytics-app/imageExport.ts +0 -370
  364. package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +0 -536
  365. package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +0 -681
  366. package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +0 -164
  367. package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +0 -21
  368. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +0 -472
  369. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +0 -75
  370. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +0 -142
  371. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +0 -142
  372. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +0 -377
  373. package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +0 -3468
  374. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +0 -44
  375. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +0 -639
  376. package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +0 -327
  377. package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +0 -225
  378. package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +0 -204
  379. package/template/.codex/skills/data-analytics/src/analytics-app-core.md +0 -85
  380. package/template/.codex/skills/data-analytics/src/codex-style-contract.md +0 -30
  381. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +0 -12
  382. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +0 -1008
  383. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +0 -2117
  384. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +0 -122
  385. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +0 -3588
  386. package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +0 -483
  387. package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +0 -32
  388. package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +0 -333
  389. package/template/.codex/skills/data-analytics/src/mcp-host.js +0 -153
  390. package/template/.codex/skills/data-analytics/src/recharts-config.js +0 -315
  391. package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +0 -294
  392. package/template/.codex/skills/data-analytics/src/sql-source-view.css +0 -33
  393. package/template/.codex/skills/data-analytics/src/sql-source-view.js +0 -157
  394. package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +0 -121
  395. package/template/.codex/skills/data-analytics/src/table-renderer.jsx +0 -28
  396. package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +0 -87
  397. package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +0 -105
  398. package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +0 -233
  399. package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +0 -1500
  400. package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +0 -219
  401. package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +0 -144
  402. package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +0 -11
  403. package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +0 -54
  404. package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +0 -321
  405. package/template/.codex/skills/data-analytics/tsconfig.json +0 -19
  406. package/template/.codex/skills/data-analytics/vite.config.ts +0 -46
  407. package/template/.codex/skills/grill-with-docs/SKILL.md +0 -20
  408. package/template/.codex/skills/grill-with-docs/agents/openai.yaml +0 -5
  409. package/template/.codex/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  410. package/template/.codex/skills/prototype/UI.md +0 -112
  411. package/template/.codex/skills/setup-matt-pocock-skills/domain.md +0 -37
  412. package/template/.codex/skills/to-questionnaire/SKILL.md +0 -55
  413. package/template/.codex/skills/to-questionnaire/agents/openai.yaml +0 -5
  414. package/template/.codex/skills/wait-what/SKILL.md +0 -7
  415. package/template/.codex/skills/wait-what/agents/openai.yaml +0 -5
  416. package/template/.cursor/skills/grill-with-docs/SKILL.md +0 -20
  417. package/template/.cursor/skills/grill-with-docs/agents/openai.yaml +0 -5
  418. package/template/.cursor/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  419. package/template/.cursor/skills/prototype/UI.md +0 -112
  420. package/template/.cursor/skills/setup-matt-pocock-skills/domain.md +0 -37
  421. package/template/.cursor/skills/to-questionnaire/SKILL.md +0 -55
  422. package/template/.cursor/skills/to-questionnaire/agents/openai.yaml +0 -5
  423. package/template/.cursor/skills/wait-what/SKILL.md +0 -7
  424. package/template/.cursor/skills/wait-what/agents/openai.yaml +0 -5
  425. package/template/.pi/skills/grill-with-docs/SKILL.md +0 -20
  426. package/template/.pi/skills/grill-with-docs/agents/openai.yaml +0 -5
  427. package/template/.pi/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  428. package/template/.pi/skills/prototype/UI.md +0 -112
  429. package/template/.pi/skills/setup-matt-pocock-skills/domain.md +0 -37
  430. package/template/.pi/skills/to-questionnaire/SKILL.md +0 -55
  431. package/template/.pi/skills/to-questionnaire/agents/openai.yaml +0 -5
  432. package/template/.pi/skills/wait-what/SKILL.md +0 -7
  433. package/template/.pi/skills/wait-what/agents/openai.yaml +0 -5
  434. package/template/.template-spec/agents/domain.md +0 -31
  435. /package/template/.agents/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  436. /package/template/.codex/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  437. /package/template/.cursor/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  438. /package/template/.pi/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
@@ -1,3588 +0,0 @@
1
- import { connectMcpWidgetHost } from "./mcp-host.js";
2
- import {
3
- CANONICAL_CHART_TYPES,
4
- canonicalVisualizationType,
5
- chartPickerSections,
6
- resolveCssColors,
7
- } from "./recharts-config.js";
8
- import { destroyRechartsChart, renderRechartsChart } from "./recharts-renderer.jsx";
9
- import { destroyDataTable, renderDataTable } from "./table-renderer.jsx";
10
- import { renderSourceCode, sourceCodeLanguage, sourceCodeText, sourceQueryFromSource } from "./sql-source-view.js";
11
- import "./styles/codex-theme.css";
12
- import "./analytics-app/tokens.css";
13
- import "./sql-source-view.css";
14
- import "./datascience-chart-widget.css";
15
-
16
- connectMcpWidgetHost({
17
- name: "Data Analytics Chart Widget",
18
- version: "0.1.1",
19
- availableDisplayModes: ["inline", "fullscreen"],
20
- });
21
-
22
-
23
- function round(value, digits = 1) {
24
- const factor = 10 ** digits;
25
- return Math.round(value * factor) / factor;
26
- }
27
-
28
- function isoDateFromDay(startMs, dayIndex) {
29
- return new Date(startMs + dayIndex * 86400000).toISOString().slice(0, 10);
30
- }
31
-
32
- function weekdayName(startMs, dayIndex) {
33
- return new Date(startMs + dayIndex * 86400000).toLocaleDateString(undefined, { weekday: "short", timeZone: "UTC" });
34
- }
35
-
36
- function buildFallbackRows() {
37
- const startMs = Date.UTC(2026, 0, 1);
38
- const weekdayLift = [0.62, 1.05, 1.1, 1.08, 1.12, 1.0, 0.58];
39
- const regions = [
40
- { region: "North America", multiplier: 1.22, accountMultiplier: 1.18, productOffset: 0 },
41
- { region: "EMEA", multiplier: 0.82, accountMultiplier: 0.9, productOffset: 1 },
42
- ];
43
- const plans = [
44
- { plan: "Self-serve", channel: "Organic", baseRevenue: 0.78, weeklyGrowth: 0.055, baseAccounts: 3.1, accountGrowth: 0.09, activation: 43, conversion: 7.8, margin: 61 },
45
- { plan: "Team", channel: "Partner", baseRevenue: 1.18, weeklyGrowth: 0.085, baseAccounts: 1.85, accountGrowth: 0.055, activation: 56, conversion: 12.4, margin: 66 },
46
- { plan: "Enterprise", channel: "Sales-led", baseRevenue: 1.72, weeklyGrowth: 0.13, baseAccounts: 0.72, accountGrowth: 0.025, activation: 69, conversion: 18.2, margin: 72 },
47
- ];
48
- const products = ["Chat", "API", "Agents", "Voice"];
49
- const rows = [];
50
- for (let dayIndex = 0; dayIndex < 91; dayIndex += 1) {
51
- const weekIndex = Math.floor(dayIndex / 7);
52
- const weekday = new Date(startMs + dayIndex * 86400000).getUTCDay();
53
- for (const [regionIndex, region] of regions.entries()) {
54
- for (const [planIndex, plan] of plans.entries()) {
55
- const wave = Math.sin((dayIndex + 1) * (planIndex + 1.7) + regionIndex * 2.1) * 0.07;
56
- const lift = weekdayLift[weekday] * (1 + wave);
57
- const revenue = (plan.baseRevenue + weekIndex * plan.weeklyGrowth) * region.multiplier * lift;
58
- const activeAccounts = (plan.baseAccounts + weekIndex * plan.accountGrowth) * region.accountMultiplier * lift;
59
- rows.push({
60
- date: isoDateFromDay(startMs, dayIndex),
61
- weekday: weekdayName(startMs, dayIndex),
62
- region: region.region,
63
- plan: plan.plan,
64
- acquisition_channel: plan.channel,
65
- product_area: products[(planIndex + region.productOffset + weekIndex) % products.length],
66
- lifecycle_stage: dayIndex < 28 ? "New" : planIndex === 0 ? "Retention" : "Expansion",
67
- revenue_m: round(revenue, 2),
68
- active_accounts_k: round(activeAccounts, 1),
69
- activation_rate_pct: round(plan.activation + weekIndex * 0.35 + regionIndex * 0.8 + wave * 18, 1),
70
- conversion_rate_pct: round(plan.conversion + weekIndex * 0.18 + regionIndex * 0.4 + wave * 6, 1),
71
- pipeline_m: round(revenue * (planIndex === 2 ? 1.75 : planIndex === 1 ? 1.32 : 0.92), 2),
72
- spend_m: round(revenue * (planIndex === 0 ? 0.19 : planIndex === 1 ? 0.16 : 0.13), 2),
73
- margin_pct: round(plan.margin + weekIndex * 0.18 - (weekday === 0 || weekday === 6 ? 1.4 : 0) + wave * 7, 1),
74
- });
75
- }
76
- }
77
- }
78
- return rows;
79
- }
80
-
81
- const fallbackRows = buildFallbackRows();
82
- const fallbackPayload = {
83
- ok: true,
84
- widget_type: "chart",
85
- title: "Daily product growth demo by plan and region",
86
- source: {
87
- query: {
88
- engine: "static-demo",
89
- id: "widget-demo-growth-configs",
90
- executed_at: "2026-05-01T00:00:00Z",
91
- sql:
92
- "SELECT date, weekday, region, plan, acquisition_channel, product_area, lifecycle_stage, revenue_m, active_accounts_k, activation_rate_pct, conversion_rate_pct, pipeline_m, spend_m, margin_pct FROM demo.widget_growth_configurations_daily",
93
- },
94
- },
95
- table: {
96
- columns: [
97
- { key: "date", label: "Date", type: "date" },
98
- { key: "weekday", label: "Weekday", type: "text" },
99
- { key: "region", label: "Region", type: "text" },
100
- { key: "plan", label: "Plan", type: "text" },
101
- { key: "acquisition_channel", label: "Acquisition channel", type: "text" },
102
- { key: "product_area", label: "Product area", type: "text" },
103
- { key: "lifecycle_stage", label: "Lifecycle stage", type: "text" },
104
- { key: "revenue_m", label: "Revenue", type: "number", unit: "$M" },
105
- { key: "active_accounts_k", label: "Active accounts", type: "number", unit: "K" },
106
- { key: "activation_rate_pct", label: "Activation rate", type: "number", unit: "%" },
107
- { key: "conversion_rate_pct", label: "Conversion rate", type: "number", unit: "%" },
108
- { key: "pipeline_m", label: "Pipeline", type: "number", unit: "$M" },
109
- { key: "spend_m", label: "Spend", type: "number", unit: "$M" },
110
- { key: "margin_pct", label: "Margin", type: "number", unit: "%" },
111
- ],
112
- rows: fallbackRows,
113
- row_count: fallbackRows.length,
114
- truncated: false,
115
- },
116
- chart: {
117
- type: "bar",
118
- fields: {
119
- x: { field: "date", type: "temporal", time_unit: "week" },
120
- y: { field: "revenue_m", type: "quantitative", aggregate: "sum", unit: "$M" },
121
- color: { field: "plan", type: "nominal" },
122
- },
123
- },
124
- display: {
125
- unit: "$M",
126
- controls: true,
127
- },
128
- };
129
-
130
- const colors = [
131
- "var(--ds-chart-series-blue)",
132
- "var(--ds-chart-series-orange)",
133
- "var(--ds-chart-series-green)",
134
- "var(--ds-chart-series-purple)",
135
- "var(--ds-chart-series-red)",
136
- "var(--ds-chart-series-pink)",
137
- "var(--ds-chart-series-yellow)",
138
- "var(--ds-chart-series-neutral)",
139
- ];
140
- const colorEdges = colors;
141
- const visualizationTypes = CANONICAL_CHART_TYPES;
142
- const aggregationOptions = ["Sum", "Average", "Minimum", "Maximum", "Count"];
143
- const fieldMenuSearchThreshold = 12;
144
- const measureNamesField = "__measure_names__";
145
- let payload = fallbackPayload;
146
- let activeVisualizationType = "bar";
147
- let chartSettings = {
148
- barOrientation: "vertical",
149
- barGroupMode: "grouped",
150
- };
151
- let viewMode = "both";
152
- let dataMode = "table";
153
- let chartConfig = {
154
- xField: "x",
155
- yField: "y",
156
- colorField: "series",
157
- lineStyleField: "",
158
- labelField: "",
159
- sizeField: "",
160
- timeUnit: "none",
161
- yAggregation: "sum",
162
- };
163
- let visibleSeries = new Set();
164
- let visibleSeriesSignature = "";
165
- let revealAllSeriesAfterGroupingChange = false;
166
- let chartInstance = null;
167
- let lastPayloadSignature = "";
168
- let hasExternalPayload = false;
169
- let displayMode = "inline";
170
- let selectedDataFilters = {};
171
- let shareMenuOpen = false;
172
- const splitStorageKeyPrefix = "datascience-chart-split:";
173
- const defaultSplitFraction = 0.618;
174
- const minSplitFraction = 0.35;
175
- const maxSplitFraction = 0.78;
176
- let splitFraction = defaultSplitFraction;
177
- let resizeFrame = 0;
178
-
179
- function isDetailDisplayMode(mode = displayMode) {
180
- return mode === "fullscreen" || mode === "modal";
181
- }
182
-
183
- function decodePayload(raw) {
184
- if (typeof raw !== "string") return raw;
185
- try {
186
- return JSON.parse(raw);
187
- } catch {
188
- return null;
189
- }
190
- }
191
-
192
- function readUrlWidgetInstanceId() {
193
- try {
194
- const params = new URLSearchParams(window.location.search);
195
- const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ""));
196
- return (
197
- params.get("widgetInstanceId") ||
198
- params.get("widgetId") ||
199
- hashParams.get("widgetInstanceId") ||
200
- hashParams.get("widgetId") ||
201
- ""
202
- );
203
- } catch {
204
- return "";
205
- }
206
- }
207
-
208
- const urlWidgetInstanceId = readUrlWidgetInstanceId();
209
-
210
- function readUrlDisplayMode() {
211
- try {
212
- const params = new URLSearchParams(window.location.search);
213
- const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ""));
214
- const raw = (
215
- params.get("displayMode") ||
216
- params.get("display_mode") ||
217
- params.get("preview") ||
218
- hashParams.get("displayMode") ||
219
- hashParams.get("display_mode") ||
220
- hashParams.get("preview") ||
221
- ""
222
- ).toLowerCase();
223
- if (raw === "fullscreen" || raw === "full" || raw === "expanded") return "fullscreen";
224
- if (raw === "modal" || raw === "dialog") return "modal";
225
- if (raw === "inline" || raw === "compact") return "inline";
226
- return "";
227
- } catch {
228
- return "";
229
- }
230
- }
231
-
232
- const urlDisplayMode = readUrlDisplayMode();
233
-
234
- function firstString(...values) {
235
- for (const value of values) {
236
- if (value == null || value === "") continue;
237
- return String(value);
238
- }
239
- return "";
240
- }
241
-
242
- function currentWidgetInstanceId() {
243
- const hostApi = window.openai || {};
244
- const hostContext = hostApi.hostContext || {};
245
- const view = hostApi.view || {};
246
- return firstString(
247
- hostApi.widgetInstanceId,
248
- hostApi.widgetId,
249
- hostApi.widget_instance_id,
250
- hostApi.widget_id,
251
- hostContext.widgetInstanceId,
252
- hostContext.widgetId,
253
- view.widgetInstanceId,
254
- view.widgetId,
255
- urlWidgetInstanceId,
256
- );
257
- }
258
-
259
- function targetWidgetInstanceId(raw) {
260
- raw = decodePayload(raw);
261
- if (!raw || typeof raw !== "object") return "";
262
- const meta = raw._meta || raw.meta || {};
263
- const globals = raw.globals || {};
264
- const hostContext = raw.hostContext || {};
265
- const view = raw.view || {};
266
- return firstString(
267
- raw.targetWidgetInstanceId,
268
- raw.targetWidgetId,
269
- raw.target_widget_instance_id,
270
- raw.target_widget_id,
271
- meta.targetWidgetInstanceId,
272
- meta.targetWidgetId,
273
- globals.targetWidgetInstanceId,
274
- globals.targetWidgetId,
275
- hostContext.targetWidgetInstanceId,
276
- hostContext.targetWidgetId,
277
- view.targetWidgetInstanceId,
278
- view.targetWidgetId,
279
- );
280
- }
281
-
282
- function messageTargetsThisWidget(raw) {
283
- const target = targetWidgetInstanceId(raw);
284
- if (!target) return true;
285
- return target === currentWidgetInstanceId();
286
- }
287
-
288
- function pickPayload(raw) {
289
- raw = decodePayload(raw);
290
- if (!raw || typeof raw !== "object") return null;
291
- if (Array.isArray(raw.content)) {
292
- for (const item of raw.content) {
293
- const found = pickPayload(item && (item.text || item));
294
- if (found) return found;
295
- }
296
- }
297
- if (raw.structuredContent && typeof raw.structuredContent === "object") return pickPayload(raw.structuredContent);
298
- if (raw.payload && typeof raw.payload === "object") return pickPayload(raw.payload);
299
- if (
300
- Array.isArray(raw.data) ||
301
- raw.table ||
302
- raw.chart ||
303
- raw.result_table ||
304
- raw.visualization_spec ||
305
- raw.widget_type === "table" ||
306
- Array.isArray(raw.rows)
307
- ) {
308
- return raw;
309
- }
310
- return null;
311
- }
312
-
313
- function currentHostPayload() {
314
- return (
315
- pickPayload(window.openai && window.openai.toolOutput) ||
316
- pickPayload(window.openai && window.openai.toolResponseMetadata)
317
- );
318
- }
319
-
320
- function currentHostDisplayMode() {
321
- const hostApi = window.openai || {};
322
- return (
323
- urlDisplayMode ||
324
- hostApi.displayMode ||
325
- (hostApi.view && hostApi.view.displayMode) ||
326
- (hostApi.hostContext && hostApi.hostContext.displayMode) ||
327
- (hostApi.host && hostApi.host.displayMode) ||
328
- displayMode
329
- );
330
- }
331
-
332
- function hostSupportsDisplayMode() {
333
- const hostApi = window.openai || {};
334
- const availableDisplayModes =
335
- hostApi.availableDisplayModes ||
336
- (hostApi.hostContext && hostApi.hostContext.availableDisplayModes) ||
337
- (hostApi.view && hostApi.view.availableDisplayModes);
338
- if (Array.isArray(availableDisplayModes) && availableDisplayModes.includes("fullscreen")) {
339
- return true;
340
- }
341
- return Boolean(window.openai) || typeof hostApi.requestDisplayMode === "function";
342
- }
343
-
344
- function hostedEmptyPayload() {
345
- return { title: "Data Analytics chart", data: [] };
346
- }
347
-
348
- function payloadSignature(value) {
349
- try {
350
- return JSON.stringify(value);
351
- } catch {
352
- return String(Date.now());
353
- }
354
- }
355
-
356
- function canApplyPayload(raw, routingContext = raw) {
357
- if (!hasExternalPayload) return true;
358
- const target = targetWidgetInstanceId(routingContext);
359
- return Boolean(target && target === currentWidgetInstanceId());
360
- }
361
-
362
- function applyPayload(raw, routingContext = raw) {
363
- if (!canApplyPayload(raw, routingContext)) return false;
364
- const next = pickPayload(raw);
365
- if (!next) return false;
366
- const signature = payloadSignature(next);
367
- hasExternalPayload = true;
368
- if (signature === lastPayloadSignature) return true;
369
- lastPayloadSignature = signature;
370
- normalizePayload(next);
371
- render();
372
- return true;
373
- }
374
-
375
- function applyHostState(raw) {
376
- const next = decodePayload(raw);
377
- if (!next || typeof next !== "object") return;
378
- const mode =
379
- next.displayMode ||
380
- next.display_mode ||
381
- (next.view && next.view.displayMode) ||
382
- (next.hostContext && next.hostContext.displayMode) ||
383
- (next.globals && (next.globals.displayMode || (next.globals.view && next.globals.view.displayMode)));
384
- if (mode === "inline" || mode === "fullscreen" || mode === "modal") {
385
- setDisplayMode(mode);
386
- }
387
- }
388
-
389
- async function requestDisplayMode(mode) {
390
- const hostApi = window.openai || {};
391
- setDisplayMode(mode);
392
- if (typeof hostApi.requestDisplayMode !== "function") {
393
- return { mode };
394
- }
395
-
396
- try {
397
- const result = await hostApi.requestDisplayMode({ mode });
398
- if (result && (result.mode === "inline" || result.mode === "fullscreen" || result.mode === "modal")) {
399
- setDisplayMode(result.mode);
400
- return result;
401
- }
402
- } catch {
403
- setDisplayMode(currentHostDisplayMode());
404
- }
405
- return { mode: displayMode };
406
- }
407
-
408
- function setDisplayMode(mode) {
409
- if (mode !== "inline" && mode !== "fullscreen" && mode !== "modal") return;
410
- const changed = displayMode !== mode;
411
- displayMode = mode;
412
- const widget = document.querySelector(".widget");
413
- if (widget) {
414
- widget.dataset.displayMode = mode;
415
- const instanceId = currentWidgetInstanceId();
416
- if (instanceId) widget.dataset.widgetInstanceId = instanceId;
417
- }
418
- document.documentElement.dataset.displayMode = mode;
419
- document.body.dataset.displayMode = mode;
420
- applySplitLayout();
421
- const button = document.getElementById("display-mode-button");
422
- if (!button) return;
423
- button.hidden = !hostSupportsDisplayMode();
424
- const label = isDetailDisplayMode(mode) ? "Done" : "Edit";
425
- button.title = isDetailDisplayMode(mode) ? "Return chart inline" : "Edit chart";
426
- button.setAttribute("aria-label", isDetailDisplayMode(mode) ? "Return chart inline" : "Edit chart");
427
- const labelElement = document.createElement("span");
428
- labelElement.className = "display-mode-button-label";
429
- labelElement.textContent = label;
430
- if (isDetailDisplayMode(mode)) {
431
- button.replaceChildren(labelElement);
432
- } else {
433
- button.replaceChildren(labelElement, exploreIcon());
434
- }
435
- const modalCloseButton = document.getElementById("modal-title-close-button");
436
- if (modalCloseButton) modalCloseButton.hidden = mode !== "modal";
437
- if (changed) render();
438
- }
439
-
440
- function exploreIcon() {
441
- const icon = document.createElement("span");
442
- icon.className = "display-mode-button-icon";
443
- const svg = svgEl("svg", {
444
- viewBox: "0 0 16 16",
445
- "aria-hidden": "true",
446
- focusable: "false",
447
- });
448
- const path = svgEl("path", {
449
- d: "M11.949 3.47949C12.0997 3.46465 12.2553 3.51279 12.3709 3.62793C12.4863 3.74328 12.5338 3.89898 12.5193 4.0498C12.5206 4.06633 12.5251 4.08275 12.5252 4.09961V10.667C12.525 10.9565 12.2902 11.191 12.0007 11.1914C11.7109 11.1914 11.4755 10.9568 11.4754 10.667V5.2666L4.37184 12.376C4.16684 12.5807 3.83365 12.5808 3.62867 12.376C3.42385 12.1711 3.42396 11.8388 3.62867 11.6338L10.7332 4.52539H5.33375C5.0438 4.52539 4.80836 4.28995 4.80836 4C4.80836 3.71005 5.0438 3.47461 5.33375 3.47461H11.9002C11.9167 3.47462 11.9328 3.47822 11.949 3.47949Z",
450
- fill: "currentColor",
451
- });
452
- svg.appendChild(path);
453
- icon.appendChild(svg);
454
- return icon;
455
- }
456
-
457
- function setViewMode(mode) {
458
- viewMode = canonicalViewMode(mode);
459
- const widget = document.querySelector(".widget");
460
- if (widget) widget.dataset.viewMode = viewMode;
461
- document.querySelectorAll("[data-view-mode-choice]").forEach((button) => {
462
- button.setAttribute("aria-pressed", String(button.dataset.viewModeChoice === viewMode));
463
- });
464
- applySplitLayout();
465
- }
466
-
467
- function clampSplitFraction(value) {
468
- const parsed = Number(value);
469
- if (!Number.isFinite(parsed)) return defaultSplitFraction;
470
- return Math.min(maxSplitFraction, Math.max(minSplitFraction, parsed));
471
- }
472
-
473
- function splitStorageKey() {
474
- const title = text((activeDataset() && activeDataset().title) || payload.title || "default").slice(0, 80);
475
- return `${splitStorageKeyPrefix}${currentWidgetInstanceId() || "local"}:${title}`;
476
- }
477
-
478
- function readStoredSplitFraction() {
479
- try {
480
- return clampSplitFraction(window.localStorage.getItem(splitStorageKey()) || defaultSplitFraction);
481
- } catch {
482
- return defaultSplitFraction;
483
- }
484
- }
485
-
486
- function writeStoredSplitFraction() {
487
- try {
488
- window.localStorage.setItem(splitStorageKey(), String(splitFraction));
489
- } catch {
490
- // Ignore storage failures; the current drag still applies for this session.
491
- }
492
- }
493
-
494
- function shouldUseSplitLayout() {
495
- return false;
496
- }
497
-
498
- function splitMetrics() {
499
- const appMain = document.querySelector(".app-main");
500
- const controls = document.getElementById("controls");
501
- const resizer = document.getElementById("split-resizer");
502
- if (!appMain || !resizer) return null;
503
- appMain.style.gridTemplateRows = "";
504
- const controlsVisible = controls && !controls.hidden && window.getComputedStyle(controls).display !== "none";
505
- const controlsHeight = controlsVisible ? controls.getBoundingClientRect().height : 0;
506
- const resizerHeight = resizer.getBoundingClientRect().height || 12;
507
- const availableHeight = appMain.clientHeight - controlsHeight - resizerHeight;
508
- if (availableHeight <= 0) return null;
509
- return { appMain, resizer, controlsHeight, resizerHeight, availableHeight };
510
- }
511
-
512
- function resetSplitLayout() {
513
- const appMain = document.querySelector(".app-main");
514
- if (appMain) appMain.style.gridTemplateRows = "";
515
- resizeChartInstance();
516
- }
517
-
518
- function applySplitLayout() {
519
- if (resizeFrame) window.cancelAnimationFrame(resizeFrame);
520
- resizeFrame = window.requestAnimationFrame(() => {
521
- resizeFrame = 0;
522
- if (!shouldUseSplitLayout()) {
523
- resetSplitLayout();
524
- return;
525
- }
526
- const metrics = splitMetrics();
527
- if (!metrics) return;
528
- const minChartHeight = Math.min(260, metrics.availableHeight * 0.5);
529
- const minBottomHeight = Math.min(190, metrics.availableHeight * 0.4);
530
- const lowerBound = Math.max(minSplitFraction, minChartHeight / metrics.availableHeight);
531
- const upperBound = Math.min(maxSplitFraction, 1 - minBottomHeight / metrics.availableHeight);
532
- const effectiveFraction = Math.min(Math.max(splitFraction, lowerBound), Math.max(lowerBound, upperBound));
533
- const chartHeight = Math.round(metrics.availableHeight * effectiveFraction);
534
- const bottomHeight = Math.max(0, metrics.availableHeight - chartHeight);
535
- metrics.appMain.style.gridTemplateRows = `${Math.round(metrics.controlsHeight)}px ${chartHeight}px ${Math.round(metrics.resizerHeight)}px ${bottomHeight}px`;
536
- metrics.resizer.setAttribute("aria-valuenow", String(Math.round(effectiveFraction * 100)));
537
- resizeChartInstance();
538
- });
539
- }
540
-
541
- function setSplitFraction(nextFraction, options = {}) {
542
- splitFraction = clampSplitFraction(nextFraction);
543
- if (options.persist) writeStoredSplitFraction();
544
- applySplitLayout();
545
- }
546
-
547
- function setSplitFractionFromPointer(clientY, options = {}) {
548
- const metrics = splitMetrics();
549
- if (!metrics) return;
550
- const rect = metrics.appMain.getBoundingClientRect();
551
- const chartHeight = clientY - rect.top - metrics.controlsHeight - metrics.resizerHeight / 2;
552
- setSplitFraction(chartHeight / metrics.availableHeight, options);
553
- }
554
-
555
- function setupSplitResizer() {
556
- const resizer = document.getElementById("split-resizer");
557
- const widget = document.querySelector(".widget");
558
- if (!resizer) return;
559
- let activePointerId = null;
560
-
561
- resizer.addEventListener("pointerdown", (event) => {
562
- if (!shouldUseSplitLayout()) return;
563
- activePointerId = event.pointerId;
564
- resizer.setPointerCapture?.(event.pointerId);
565
- widget?.classList.add("is-resizing");
566
- setSplitFractionFromPointer(event.clientY);
567
- event.preventDefault();
568
- });
569
-
570
- resizer.addEventListener("pointermove", (event) => {
571
- if (activePointerId !== event.pointerId) return;
572
- setSplitFractionFromPointer(event.clientY);
573
- });
574
-
575
- const finishPointerDrag = (event) => {
576
- if (activePointerId !== event.pointerId) return;
577
- activePointerId = null;
578
- widget?.classList.remove("is-resizing");
579
- writeStoredSplitFraction();
580
- };
581
- resizer.addEventListener("pointerup", finishPointerDrag);
582
- resizer.addEventListener("pointercancel", finishPointerDrag);
583
- resizer.addEventListener("lostpointercapture", () => {
584
- activePointerId = null;
585
- widget?.classList.remove("is-resizing");
586
- });
587
-
588
- resizer.addEventListener("dblclick", () => {
589
- setSplitFraction(defaultSplitFraction, { persist: true });
590
- });
591
-
592
- resizer.addEventListener("keydown", (event) => {
593
- if (!shouldUseSplitLayout()) return;
594
- if (event.key === "ArrowUp") {
595
- setSplitFraction(splitFraction - 0.03, { persist: true });
596
- event.preventDefault();
597
- } else if (event.key === "ArrowDown") {
598
- setSplitFraction(splitFraction + 0.03, { persist: true });
599
- event.preventDefault();
600
- } else if (event.key === "Home") {
601
- setSplitFraction(defaultSplitFraction, { persist: true });
602
- event.preventDefault();
603
- } else if (event.key === "End") {
604
- setSplitFraction(maxSplitFraction, { persist: true });
605
- event.preventDefault();
606
- }
607
- });
608
- }
609
-
610
- function setupViewModeControls() {
611
- document.querySelectorAll("[data-view-mode-choice]").forEach((button) => {
612
- button.addEventListener("click", () => {
613
- setViewMode(button.dataset.viewModeChoice);
614
- });
615
- });
616
- setViewMode(viewMode);
617
- }
618
-
619
- function canonicalDataMode(value) {
620
- const raw = text(value || "").toLowerCase();
621
- return raw === "query" || raw === "sql" || raw === "python" || raw === "code" ? "query" : "table";
622
- }
623
-
624
- function setDataMode(mode) {
625
- dataMode = canonicalDataMode(mode);
626
- const widget = document.querySelector(".widget");
627
- const bottomSplit = document.getElementById("bottom-split");
628
- if (widget) widget.dataset.dataMode = dataMode;
629
- if (bottomSplit) bottomSplit.dataset.dataMode = dataMode;
630
- document.querySelectorAll(".data-mode-segmented").forEach((control) => {
631
- control.dataset.activeMode = dataMode;
632
- });
633
- document.querySelectorAll("[data-data-mode-choice]").forEach((button) => {
634
- button.setAttribute("aria-pressed", String(button.dataset.dataModeChoice === dataMode));
635
- });
636
- }
637
-
638
- function setupDataModeControls() {
639
- document.querySelectorAll("[data-data-mode-choice]").forEach((button) => {
640
- button.addEventListener("click", () => {
641
- setDataMode(button.dataset.dataModeChoice);
642
- });
643
- });
644
- setDataMode(dataMode);
645
- }
646
-
647
- function setupDisplayModeControls() {
648
- const button = document.getElementById("display-mode-button");
649
- const backButton = document.getElementById("detail-back-button");
650
- const modalCloseButton = document.getElementById("modal-title-close-button");
651
- const toggleDisplayMode = () => {
652
- if (!button) return;
653
- button.disabled = true;
654
- const nextMode = isDetailDisplayMode() ? "inline" : "fullscreen";
655
- requestDisplayMode(nextMode).finally(() => {
656
- button.disabled = false;
657
- });
658
- };
659
- if (button) button.addEventListener("click", toggleDisplayMode);
660
- if (backButton) {
661
- backButton.addEventListener("click", () => {
662
- requestDisplayMode("inline");
663
- });
664
- }
665
- if (modalCloseButton) {
666
- modalCloseButton.addEventListener("click", () => {
667
- requestDisplayMode("inline");
668
- });
669
- }
670
- setDisplayMode(currentHostDisplayMode());
671
- }
672
-
673
- function asArray(value) {
674
- return Array.isArray(value) ? value : [];
675
- }
676
-
677
- function text(value) {
678
- return value == null ? "" : String(value);
679
- }
680
-
681
- function number(value) {
682
- const parsed = Number(value);
683
- return Number.isFinite(parsed) ? parsed : null;
684
- }
685
-
686
- function valueFor(dataset, key, fallback = null) {
687
- return dataset && dataset[key] != null ? dataset[key] : payload[key] != null ? payload[key] : fallback;
688
- }
689
-
690
- function objectFor(dataset, key) {
691
- const value = valueFor(dataset, key);
692
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
693
- }
694
-
695
- function resultTableFor(dataset) {
696
- return objectFor(dataset, "table") || objectFor(dataset, "result_table") || null;
697
- }
698
-
699
- function sourceQueryFor(dataset) {
700
- return sourceQueryFromSource(objectFor(dataset, "source"));
701
- }
702
-
703
- function sourceLabelFor(dataset) {
704
- const source = valueFor(dataset, "source", payload.source);
705
- if (source && typeof source === "object" && !Array.isArray(source)) return text(source.label);
706
- return text(source);
707
- }
708
-
709
- function sqlTextFor(dataset) {
710
- return sourceCodeText(sourceQueryFor(dataset));
711
- }
712
-
713
- function queryLanguageFor(dataset) {
714
- return sourceCodeLanguage(sourceQueryFor(dataset));
715
- }
716
-
717
- function compactList(values) {
718
- return values.map((value) => text(value).trim()).filter(Boolean);
719
- }
720
-
721
- function listFromValue(value) {
722
- if (Array.isArray(value)) {
723
- return compactList(value.map((item) => {
724
- if (item && typeof item === "object") {
725
- const label = item.label || item.name || item.table || item.field || item.metric || item.id;
726
- const detail = item.description || item.definition || item.value || item.expression;
727
- return label && detail ? `${label}: ${detail}` : label || detail;
728
- }
729
- return item;
730
- }));
731
- }
732
- if (value && typeof value === "object") {
733
- return compactList(Object.entries(value).map(([key, item]) => `${key}: ${item}`));
734
- }
735
- return compactList([value]);
736
- }
737
-
738
- function firstListFromObjects(objects, keys) {
739
- for (const object of objects) {
740
- if (!object) continue;
741
- for (const key of keys) {
742
- const values = listFromValue(object[key]);
743
- if (values.length) return values;
744
- }
745
- }
746
- return [];
747
- }
748
-
749
- function isLikelySourceTableName(value) {
750
- const tableName = text(value).trim().replace(/^[`"\[]|[`"\]]$/g, "");
751
- return /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){1,3}$/.test(tableName);
752
- }
753
-
754
- function sourceTableNamesFromMetadata(objects) {
755
- return firstListFromObjects(objects, [
756
- "tables_used",
757
- "tablesUsed",
758
- "source_tables",
759
- "sourceTables",
760
- "tables",
761
- ]).filter(isLikelySourceTableName);
762
- }
763
-
764
- function extractTablesFromSql(sql) {
765
- const tables = [];
766
- const seen = new Set();
767
- const pattern = /\b(?:from|join)\s+([`"\[]?[\w.-]+(?:\.[\w.-]+){0,3}[`"\]]?)/gi;
768
- let match;
769
- while ((match = pattern.exec(sql))) {
770
- const table = match[1].replace(/^[`"\[]|[`"\]]$/g, "");
771
- const normalized = table.toLowerCase();
772
- if (!table || seen.has(normalized)) continue;
773
- seen.add(normalized);
774
- tables.push(table);
775
- }
776
- return tables;
777
- }
778
-
779
- function sourceMetadataObjects(dataset) {
780
- return [
781
- objectFor(dataset, "source"),
782
- sourceQueryFor(dataset),
783
- objectFor(dataset, "display"),
784
- objectFor(dataset, "chart"),
785
- dataset && typeof dataset === "object" ? dataset : null,
786
- payload && typeof payload === "object" ? payload : null,
787
- ];
788
- }
789
-
790
- function metricDefinitionList(dataset) {
791
- const explicit = firstListFromObjects(sourceMetadataObjects(dataset), [
792
- "metric_definitions",
793
- "metricDefinitions",
794
- "metrics_definition",
795
- "metricDefinition",
796
- ]);
797
- if (explicit.length) return explicit;
798
- const table = resultTableFor(dataset) || {};
799
- const columns = asArray(table.columns);
800
- const yFields = asArray(visualizationSpecFor(dataset).encodings?.y?.fields);
801
- const yField = visualizationSpecFor(dataset).encodings?.y?.field || chartConfig.yField;
802
- const metricFields = new Set(compactList([yField, ...yFields]));
803
- return compactList(columns
804
- .filter((column) => column && (!metricFields.size || metricFields.has(column.key)))
805
- .map((column) => `${column.label || column.key}: displayed from ${column.key}${column.unit ? ` (${column.unit})` : ""}`))
806
- .slice(0, 6);
807
- }
808
-
809
- function sourceDetailText(values, fallback = "Not declared") {
810
- const items = compactList(Array.isArray(values) ? values : [values]);
811
- return items.length ? items.join("; ") : fallback;
812
- }
813
-
814
- function sourceDetailList(values, fallback = "Not declared") {
815
- const items = compactList(Array.isArray(values) ? values : [values]);
816
- return items.length ? items : [fallback];
817
- }
818
-
819
- function sourceFieldsForDataset(dataset) {
820
- const spec = visualizationSpecFor(dataset);
821
- const fields = new Set();
822
- function addField(field) {
823
- if (field) fields.add(field);
824
- }
825
- addField(spec.encodings?.x?.field);
826
- addField(spec.encodings?.y?.field);
827
- for (const field of spec.encodings?.y?.fields ?? []) addField(field);
828
- addField(spec.encodings?.color?.field);
829
- return Array.from(fields);
830
- }
831
-
832
- function visualizationSpecFor(dataset) {
833
- const chart = objectFor(dataset, "chart");
834
- if (chart) {
835
- const fields = chart.fields && typeof chart.fields === "object" && !Array.isArray(chart.fields)
836
- ? chart.fields
837
- : {};
838
- const options = chart.options && typeof chart.options === "object" && !Array.isArray(chart.options)
839
- ? chart.options
840
- : {};
841
- const display = objectFor(dataset, "display") || {};
842
- return {
843
- visualization_type: chart.type,
844
- encodings: fields,
845
- settings: {
846
- ...(options.orientation != null ? { orientation: options.orientation } : {}),
847
- ...(options.grouping != null ? { group_mode: options.grouping } : {}),
848
- ...(options.points != null ? { show_points: options.points } : {}),
849
- ...(options.multi_measure_series != null
850
- ? { multi_measure_series: options.multi_measure_series }
851
- : {}),
852
- },
853
- presentation: {
854
- ...(display.unit != null ? { unit: display.unit } : {}),
855
- ...(display.baseline != null ? { baseline: display.baseline } : {}),
856
- ...(display.x_axis_title != null ? { x_axis_title: display.x_axis_title } : {}),
857
- ...(display.y_axis_title != null ? { y_axis_title: display.y_axis_title } : {}),
858
- ...(display.controls != null ? { show_controls: display.controls } : {}),
859
- },
860
- };
861
- }
862
- return objectFor(dataset, "visualization_spec") || {};
863
- }
864
-
865
- function chartSpecFor(dataset) {
866
- return objectFor(dataset, "chart_spec") || {};
867
- }
868
-
869
- function specEncodings(dataset) {
870
- const encodings = visualizationSpecFor(dataset).encodings;
871
- return encodings && typeof encodings === "object" ? encodings : {};
872
- }
873
-
874
- function specEncoding(dataset, role) {
875
- const encoding = specEncodings(dataset)[role];
876
- return encoding && typeof encoding === "object" ? encoding : {};
877
- }
878
-
879
- function specPresentation(dataset) {
880
- const presentation = visualizationSpecFor(dataset).presentation;
881
- return presentation && typeof presentation === "object" ? presentation : {};
882
- }
883
-
884
- function specSettings(dataset) {
885
- const settings = visualizationSpecFor(dataset).settings;
886
- if (settings && typeof settings === "object" && !Array.isArray(settings)) return settings;
887
- return objectFor(dataset, "settings") || {};
888
- }
889
-
890
- function specValue(dataset, key, fallback = null) {
891
- const presentation = specPresentation(dataset);
892
- return presentation[key] != null ? presentation[key] : valueFor(dataset, key, fallback);
893
- }
894
-
895
- function currentWidgetSpec() {
896
- const dataset = activeDataset();
897
- return {
898
- version: "1",
899
- visualization_type: activeVisualizationType,
900
- encodings: {
901
- x: {
902
- field: chartConfig.xField,
903
- type: fieldLooksDateLike(dataset, chartConfig.xField) ? "temporal" : "nominal",
904
- ...(chartConfig.timeUnit && chartConfig.timeUnit !== "none" ? { time_unit: chartConfig.timeUnit } : {}),
905
- },
906
- y: {
907
- aggregate: chartConfig.yAggregation,
908
- field: chartConfig.yField,
909
- type: "quantitative",
910
- },
911
- ...(chartConfig.colorField ? { color: { field: chartConfig.colorField, type: "nominal" } } : {}),
912
- ...(chartConfig.lineStyleField ? { lineStyle: { field: chartConfig.lineStyleField, type: "nominal" } } : {}),
913
- ...(chartConfig.labelField ? { label: { field: chartConfig.labelField, type: "text" } } : {}),
914
- ...(chartConfig.sizeField ? { size: { field: chartConfig.sizeField, type: "quantitative" } } : {}),
915
- },
916
- presentation: {
917
- data_mode: dataMode,
918
- unit: unitFor(dataset),
919
- view_mode: viewMode,
920
- },
921
- settings: {
922
- orientation: chartSettings.barOrientation,
923
- group_mode: chartSettings.barGroupMode,
924
- },
925
- };
926
- }
927
-
928
- function syntheticSeriesField(value, index) {
929
- const slug = text(value || "value").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
930
- return `__series_${index}_${slug || "value"}`;
931
- }
932
-
933
- function uniqueProjectedSeriesValues(rows) {
934
- const values = [];
935
- const seen = new Set();
936
- for (const row of rows) {
937
- const value = text((row && row.series) || "Value");
938
- if (seen.has(value)) continue;
939
- seen.add(value);
940
- values.push(value);
941
- }
942
- return values;
943
- }
944
-
945
- function projectedRowsUseColorEncoding(rows) {
946
- return Boolean(chartConfig.colorField) || usesMeasureNames() || uniqueProjectedSeriesValues(rows).length > 1;
947
- }
948
-
949
- function projectedRenderRows(rows) {
950
- return rows.map((row) => {
951
- const output = {};
952
- for (const [key, value] of Object.entries(row || {})) {
953
- if (key === "raw") continue;
954
- output[key] = value;
955
- }
956
- return output;
957
- });
958
- }
959
-
960
- function projectedRenderColumns(rows, dataset) {
961
- const columns = [
962
- {
963
- key: "x",
964
- label: fieldLabel(dataset, chartConfig.xField, "X"),
965
- type: fieldLooksDateLike(dataset, chartConfig.xField) ? "date" : "text",
966
- },
967
- {
968
- key: "y",
969
- label: fieldLabel(dataset, chartConfig.yField, "Value"),
970
- type: "number",
971
- unit: unitFor(dataset),
972
- },
973
- ];
974
- if (projectedRowsUseColorEncoding(rows)) {
975
- columns.splice(1, 0, { key: "series", label: "Series", type: "text" });
976
- }
977
- if (rows.some((row) => row && row.size != null)) {
978
- columns.push({ key: "size", label: fieldLabel(dataset, chartConfig.sizeField, "Size"), type: "number" });
979
- }
980
- if (rows.some((row) => row && row.lineStyle != null)) {
981
- columns.push({ key: "lineStyle", label: "Line style", type: "text" });
982
- }
983
- if (rows.some((row) => row && row.label != null)) {
984
- columns.push({ key: "label", label: fieldLabel(dataset, chartConfig.labelField, "Label"), type: "text" });
985
- }
986
- return columns;
987
- }
988
-
989
- function projectedChartSpec(dataset, type, rows) {
990
- const existing = chartSpecFor(dataset);
991
- const sourceSettings = specSettings(dataset);
992
- const useColor = projectedRowsUseColorEncoding(rows);
993
- const xAxisTitle = specValue(dataset, "x_axis_title", existing.xAxisTitle);
994
- const yAxisTitle = specValue(dataset, "y_axis_title", existing.yAxisTitle);
995
- return {
996
- id: text(existing.id || valueFor(dataset, "id", "default")) || "default",
997
- title: text(valueFor(dataset, "title", payload.title || "Data Analytics chart")),
998
- subtitle: specValue(dataset, "subtitle", payload.subtitle),
999
- type,
1000
- dataset: text(valueFor(dataset, "dataset", valueFor(dataset, "id", "default"))) || "default",
1001
- encodings: {
1002
- x: {
1003
- field: "x",
1004
- label: fieldLabel(dataset, chartConfig.xField, "X"),
1005
- type: fieldLooksDateLike(dataset, chartConfig.xField) ? "temporal" : "nominal",
1006
- ...(chartConfig.timeUnit && chartConfig.timeUnit !== "none" ? { time_unit: chartConfig.timeUnit } : {}),
1007
- },
1008
- y: {
1009
- aggregate: "none",
1010
- field: "y",
1011
- label: fieldLabel(dataset, chartConfig.yField, "Value"),
1012
- type: "quantitative",
1013
- unit: unitFor(dataset),
1014
- },
1015
- ...(useColor ? { color: { field: "series", label: "Series", type: "nominal" } } : {}),
1016
- ...(rows.some((row) => row && row.lineStyle != null)
1017
- ? { lineStyle: { field: "lineStyle", label: "Line style", type: "nominal" } }
1018
- : {}),
1019
- ...(rows.some((row) => row && row.label != null)
1020
- ? { label: { field: "label", label: fieldLabel(dataset, chartConfig.labelField, "Label"), type: "text" } }
1021
- : {}),
1022
- ...(rows.some((row) => row && row.size != null)
1023
- ? { size: { field: "size", label: fieldLabel(dataset, chartConfig.sizeField, "Size"), type: "quantitative" } }
1024
- : {}),
1025
- },
1026
- ...(xAxisTitle != null ? { xAxisTitle } : {}),
1027
- ...(yAxisTitle != null ? { yAxisTitle } : {}),
1028
- unit: unitFor(dataset),
1029
- valueFormat: existing.valueFormat,
1030
- settings: {
1031
- orientation: chartSettings.barOrientation,
1032
- groupMode: chartSettings.barGroupMode,
1033
- group_mode: chartSettings.barGroupMode,
1034
- ...(sourceSettings.showPoints != null || sourceSettings.show_points != null
1035
- ? {
1036
- showPoints: sourceSettings.showPoints ?? sourceSettings.show_points,
1037
- show_points: sourceSettings.show_points ?? sourceSettings.showPoints,
1038
- }
1039
- : {}),
1040
- },
1041
- surface: {
1042
- ...(existing.surface || {}),
1043
- surface: isDetailDisplayMode() ? "explorer" : "compact",
1044
- showControls: Boolean(specValue(dataset, "show_controls", payload.show_controls)),
1045
- },
1046
- };
1047
- }
1048
-
1049
- function notifyChartSpecChange() {
1050
- if (typeof window.parent?.postMessage !== "function" || window.parent === window) return;
1051
- window.parent.postMessage(
1052
- {
1053
- type: "datascience-chart-widget-spec-change",
1054
- widgetInstanceId: currentWidgetInstanceId(),
1055
- visualization_spec: currentWidgetSpec(),
1056
- },
1057
- "*",
1058
- );
1059
- }
1060
-
1061
- function notifyChartSpecReset() {
1062
- if (typeof window.parent?.postMessage !== "function" || window.parent === window) return;
1063
- window.parent.postMessage(
1064
- {
1065
- type: "datascience-chart-widget-spec-reset",
1066
- widgetInstanceId: currentWidgetInstanceId(),
1067
- },
1068
- "*",
1069
- );
1070
- }
1071
-
1072
- function widgetVisualizationType(type) {
1073
- return canonicalVisualizationType(type);
1074
- }
1075
-
1076
- function canonicalBarOrientation(value, fallback = "vertical") {
1077
- const raw = text(value || "").trim().toLowerCase().replaceAll("_", "").replaceAll("-", "");
1078
- if (raw === "vertical" || raw === "column") return "vertical";
1079
- if (raw === "horizontal") return "horizontal";
1080
- if (raw === "auto" || raw === "automatic") return fallback;
1081
- return fallback;
1082
- }
1083
-
1084
- function canonicalBarGroupMode(value, fallback = "grouped") {
1085
- const raw = text(value || "").trim().toLowerCase().replaceAll("_", "").replaceAll("-", "").replaceAll("%", "100");
1086
- if (raw === "single") return "single";
1087
- if (raw === "grouped" || raw === "group") return "grouped";
1088
- if (raw === "stacked" || raw === "stack") return "stacked";
1089
- if (raw === "stacked100" || raw === "stack100" || raw === "100" || raw === "100stacked") return "stacked100";
1090
- return fallback;
1091
- }
1092
-
1093
- function resetChartSettings(dataset = activeDataset(), requestedType = activeVisualizationType) {
1094
- const settings = specSettings(dataset);
1095
- chartSettings = {
1096
- barOrientation: canonicalBarOrientation(
1097
- settings.orientation ?? settings.bar_orientation ?? valueFor(dataset, "bar_orientation"),
1098
- "vertical",
1099
- ),
1100
- barGroupMode: canonicalBarGroupMode(
1101
- settings.group_mode ?? settings.groupMode ?? settings.bar_group_mode ?? valueFor(dataset, "bar_group_mode"),
1102
- "grouped",
1103
- ),
1104
- };
1105
- }
1106
-
1107
- function rendererBarOrientation() {
1108
- return chartSettings.barOrientation === "horizontal" ? "horizontal" : "vertical";
1109
- }
1110
-
1111
- function rendererVisualizationType(type = activeVisualizationType) {
1112
- const canonical = widgetVisualizationType(type);
1113
- return canonical;
1114
- }
1115
-
1116
- function applyChartSettingsAttributes() {
1117
- const widget = document.querySelector(".widget");
1118
- if (!widget) return;
1119
- widget.dataset.barOrientation = chartSettings.barOrientation;
1120
- widget.dataset.barGroupMode = chartSettings.barGroupMode;
1121
- }
1122
-
1123
- function unitFor(dataset) {
1124
- const selectedMeasure = asArray(resultTableFor(dataset)?.columns).find(
1125
- (column) => column && column.key === chartConfig.yField,
1126
- );
1127
- if (selectedMeasure?.unit != null && text(selectedMeasure.unit).trim()) {
1128
- return text(selectedMeasure.unit).trim();
1129
- }
1130
- return specValue(dataset, "unit", payload.unit);
1131
- }
1132
-
1133
- function activeDataset() {
1134
- return payload;
1135
- }
1136
-
1137
- function isTablePayload(dataset) {
1138
- return (
1139
- valueFor(dataset, "widget_type", payload.widget_type) === "table" ||
1140
- visualizationSpecFor(dataset).visualization_type === "table"
1141
- );
1142
- }
1143
-
1144
- function setQueryStatus(message) {
1145
- const status = document.getElementById("query-status");
1146
- if (status) status.textContent = message || "";
1147
- }
1148
-
1149
- function formatRunTime(value) {
1150
- const raw = text(value);
1151
- if (!raw) return "";
1152
- const date = new Date(raw);
1153
- if (!Number.isFinite(date.getTime())) return raw;
1154
- return new Intl.DateTimeFormat(undefined, {
1155
- day: "numeric",
1156
- hour: "numeric",
1157
- minute: "2-digit",
1158
- month: "short",
1159
- year: "numeric",
1160
- }).format(date);
1161
- }
1162
-
1163
- function renderDetailHeader(dataset, title, subtitle) {
1164
- const detailTitle = document.getElementById("detail-title");
1165
- const detailSubtitle = document.getElementById("detail-subtitle");
1166
- const runTime = document.getElementById("detail-run-time");
1167
- const sourceQuery = sourceQueryFor(dataset) || {};
1168
- if (detailTitle) detailTitle.textContent = text(title);
1169
- if (detailSubtitle) {
1170
- detailSubtitle.textContent = text(subtitle);
1171
- detailSubtitle.hidden = !subtitle;
1172
- }
1173
- if (runTime) {
1174
- const formatted = formatRunTime(sourceQuery.executed_at || valueFor(dataset, "executed_at", payload.executed_at));
1175
- runTime.textContent = formatted;
1176
- const refreshButton = document.getElementById("detail-refresh-button");
1177
- if (refreshButton) refreshButton.hidden = !formatted;
1178
- }
1179
- }
1180
-
1181
- function renderQueryControls(dataset) {
1182
- if (displayMode !== "fullscreen") {
1183
- setQueryStatus("");
1184
- return;
1185
- }
1186
- const sourceQuery = sourceQueryFor(dataset);
1187
- const resultTable = resultTableFor(dataset);
1188
- const label =
1189
- (sourceQuery && (sourceQuery.label || sourceQuery.id || sourceQuery.engine));
1190
- const rowCount = resultTable && Number.isFinite(Number(resultTable.row_count)) ? Number(resultTable.row_count) : null;
1191
- const suffix = rowCount != null ? ` · ${formatNumber(rowCount, 0)} rows${resultTable.truncated ? " sampled" : ""}` : "";
1192
- setQueryStatus(label ? `${label}${suffix}` : "");
1193
- }
1194
-
1195
- function setupMenuDismissal() {
1196
- document.addEventListener("click", () => {
1197
- closeFieldMenus();
1198
- closeChartPicker();
1199
- closeDataFilterMenus();
1200
- closeShareMenu();
1201
- });
1202
- document.addEventListener("keydown", (event) => {
1203
- if (event.key === "Escape") {
1204
- closeFieldMenus();
1205
- closeChartPicker();
1206
- closeDataFilterMenus();
1207
- closeShareMenu();
1208
- }
1209
- });
1210
- }
1211
-
1212
- function canonicalViewMode(value) {
1213
- const raw = text(value || "").toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
1214
- if (raw === "vis" || raw === "viz" || raw === "chart") return "visualization";
1215
- if (raw === "table" || raw === "data") return "table";
1216
- if (raw === "both" || raw === "split") return "both";
1217
- return "both";
1218
- }
1219
-
1220
- function normalizePayload(nextPayload) {
1221
- payload = nextPayload || fallbackPayload;
1222
- const requestedVisualizationType =
1223
- visualizationSpecFor(activeDataset()).visualization_type;
1224
- activeVisualizationType = widgetVisualizationType(requestedVisualizationType);
1225
- resetChartSettings(activeDataset(), requestedVisualizationType);
1226
- viewMode = canonicalViewMode(viewMode);
1227
- dataMode = canonicalDataMode(specValue(activeDataset(), "data_mode", payload.data_mode || dataMode));
1228
- selectedDataFilters = {};
1229
- splitFraction = readStoredSplitFraction();
1230
- resetChartConfig(activeDataset());
1231
- resetVisibleSeries();
1232
- }
1233
-
1234
- function resetVisibleSeries() {
1235
- const names = rechartsLegendNamesForDataset(projectedDataset(activeDataset()), activeVisualizationType);
1236
- visibleSeries = new Set(names);
1237
- visibleSeriesSignature = names.join("\u0000");
1238
- }
1239
-
1240
- function orderedVisibleSeries(seriesNames, selectedSeries) {
1241
- const selected = selectedSeries instanceof Set ? selectedSeries : new Set(asArray(selectedSeries).map(text));
1242
- return new Set(asArray(seriesNames).map(text).filter((series) => selected.has(series)));
1243
- }
1244
-
1245
- function reconcileVisibleSeries(seriesNames) {
1246
- const names = asArray(seriesNames).map(text);
1247
- const signature = names.join("\u0000");
1248
- if (revealAllSeriesAfterGroupingChange) {
1249
- visibleSeries = new Set(names);
1250
- visibleSeriesSignature = signature;
1251
- revealAllSeriesAfterGroupingChange = false;
1252
- return;
1253
- }
1254
- if (signature === visibleSeriesSignature) return;
1255
- const available = new Set(names);
1256
- const stillVisible = [...visibleSeries].filter((series) => available.has(series));
1257
- visibleSeries = stillVisible.length ? orderedVisibleSeries(names, new Set(stillVisible)) : new Set(names);
1258
- visibleSeriesSignature = signature;
1259
- }
1260
-
1261
- const COMPACT_VALUE_THRESHOLD = 100000;
1262
-
1263
- function compactDigits(value) {
1264
- const absolute = Math.abs(value);
1265
- if (absolute >= COMPACT_VALUE_THRESHOLD) return 1;
1266
- return 2;
1267
- }
1268
-
1269
- function formatNumber(value, compact = false) {
1270
- const numeric = number(value);
1271
- if (numeric == null) return "";
1272
- const absolute = Math.abs(numeric);
1273
- return new Intl.NumberFormat(undefined, {
1274
- maximumFractionDigits: compactDigits(numeric),
1275
- notation: compact && absolute >= COMPACT_VALUE_THRESHOLD ? "compact" : "standard",
1276
- }).format(numeric);
1277
- }
1278
-
1279
- function isUsdUnit(unit) {
1280
- return unit === "$" || text(unit).toUpperCase() === "USD";
1281
- }
1282
-
1283
- function unitScaleSuffix(unit) {
1284
- const normalized = text(unit).toLowerCase();
1285
- if (normalized === "usd millions" || normalized === "usd million") return "M";
1286
- if (normalized === "usd billions" || normalized === "usd billion") return "B";
1287
- if (normalized === "usd thousands" || normalized === "usd thousand") return "K";
1288
- if (/^\$[kmbt]$/i.test(text(unit))) return text(unit).slice(1).toUpperCase();
1289
- return null;
1290
- }
1291
-
1292
- function formatCurrency(value) {
1293
- const numeric = number(value);
1294
- if (numeric == null) return "";
1295
- const absolute = Math.abs(numeric);
1296
- return new Intl.NumberFormat(undefined, {
1297
- currency: "USD",
1298
- maximumFractionDigits: compactDigits(numeric),
1299
- notation: absolute >= COMPACT_VALUE_THRESHOLD ? "compact" : "standard",
1300
- style: "currency",
1301
- }).format(numeric);
1302
- }
1303
-
1304
- function formatValue(value, unit) {
1305
- const cleanUnit = text(unit);
1306
- if (!cleanUnit) return formatNumber(value, true);
1307
- if (cleanUnit === "%" || cleanUnit.startsWith("%")) return `${formatNumber(value)}%`;
1308
- if (isUsdUnit(cleanUnit)) return formatCurrency(value);
1309
- const scaleSuffix = unitScaleSuffix(cleanUnit);
1310
- if (scaleSuffix) return `$${formatNumber(value)}${scaleSuffix}`;
1311
- if (cleanUnit.startsWith("$")) return `$${formatNumber(value)}${cleanUnit.slice(1)}`;
1312
- return `${formatNumber(value, true)} ${cleanUnit}`;
1313
- }
1314
-
1315
- function formatTableCell(value, type) {
1316
- if (value == null) return "";
1317
- const numeric = number(value);
1318
- if (type === "number" && numeric != null) return formatNumber(numeric, true);
1319
- if (type === "percent" && numeric != null) return `${formatNumber(numeric * 100)}%`;
1320
- if (type === "currency" && numeric != null) return formatCurrency(numeric);
1321
- return text(value);
1322
- }
1323
-
1324
- function tableColumns(dataset, rows) {
1325
- const resultTable = resultTableFor(dataset);
1326
- const columns = asArray(resultTable && resultTable.columns);
1327
- if (columns.length) return columns;
1328
- const keys = Object.keys(rows[0] || {});
1329
- return keys.map((key) => ({ key, label: key }));
1330
- }
1331
-
1332
- function rawChartRows(dataset) {
1333
- const resultTable = resultTableFor(dataset);
1334
- const resultRows = resultTable && Array.isArray(resultTable.rows) ? resultTable.rows : null;
1335
- if (resultRows) {
1336
- return resultRows.map((row) => (row && typeof row === "object" ? { ...row } : {}));
1337
- }
1338
- return asArray(dataset.data).map((point) => {
1339
- const row = point && typeof point === "object" ? { ...point } : {};
1340
- row.x = text(point && point.x);
1341
- row.series = text((point && point.series) || "Value");
1342
- row.y = number(point && point.y);
1343
- if (point && point.label != null) row.label = point.label;
1344
- return row;
1345
- });
1346
- }
1347
-
1348
- function dataFilterKey(dataset, fieldKey) {
1349
- return `${text(dataset && dataset.id) || "default"}\u0000${fieldKey}`;
1350
- }
1351
-
1352
- function selectedDataFilterValue(dataset, fieldKey) {
1353
- return selectedDataFilters[dataFilterKey(dataset, fieldKey)] || "all";
1354
- }
1355
-
1356
- function uniqueFilterValues(rows, fieldKey) {
1357
- const seen = new Set();
1358
- const values = [];
1359
- for (const row of rows) {
1360
- const value = text(row && row[fieldKey]);
1361
- if (!value || seen.has(value)) continue;
1362
- seen.add(value);
1363
- values.push(value);
1364
- }
1365
- return values.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
1366
- }
1367
-
1368
- function filterRowsExcluding(dataset, rows, excludedFieldKey) {
1369
- const specs = dataFilterSpecs(dataset);
1370
- if (!specs.length) return rows;
1371
- return rows.filter((row) =>
1372
- specs.every((spec) => {
1373
- if (spec.key === excludedFieldKey) return true;
1374
- const selected = selectedDataFilterValue(dataset, spec.key);
1375
- return selected === "all" || text(row && row[spec.key]) === selected;
1376
- }),
1377
- );
1378
- }
1379
-
1380
- function filterOptionsForSpec(dataset, spec) {
1381
- return uniqueFilterValues(filterRowsExcluding(dataset, rawChartRows(dataset), spec.key), spec.key);
1382
- }
1383
-
1384
- function pruneDataFilters(dataset) {
1385
- let changed = true;
1386
- while (changed) {
1387
- changed = false;
1388
- for (const spec of dataFilterSpecs(dataset)) {
1389
- const key = dataFilterKey(dataset, spec.key);
1390
- const selected = selectedDataFilters[key];
1391
- if (!selected) continue;
1392
- if (!filterOptionsForSpec(dataset, spec).includes(selected)) {
1393
- delete selectedDataFilters[key];
1394
- changed = true;
1395
- }
1396
- }
1397
- }
1398
- }
1399
-
1400
- function dataFilterSpecs(dataset) {
1401
- const rows = rawChartRows(dataset);
1402
- return availableFields(dataset)
1403
- .filter((field) => {
1404
- if (field.synthetic || field.numeric || field.type === "date") return false;
1405
- if (field.key === chartConfig.yField || field.key === measureNamesField) return false;
1406
- if (fieldLooksDateLike(dataset, field.key)) return false;
1407
- const values = uniqueFilterValues(rows, field.key);
1408
- return values.length > 1 && values.length <= 24;
1409
- })
1410
- .map((field) => ({
1411
- ...field,
1412
- options: uniqueFilterValues(rows, field.key),
1413
- }));
1414
- }
1415
-
1416
- function applyDataFilters(dataset, rows) {
1417
- const specs = dataFilterSpecs(dataset);
1418
- if (!specs.length) return rows;
1419
- return rows.filter((row) =>
1420
- specs.every((spec) => {
1421
- const selected = selectedDataFilterValue(dataset, spec.key);
1422
- return selected === "all" || text(row && row[spec.key]) === selected;
1423
- }),
1424
- );
1425
- }
1426
-
1427
- function chartRows(dataset) {
1428
- return applyDataFilters(dataset, rawChartRows(dataset));
1429
- }
1430
-
1431
- function chartColumns(rows, dataset = activeDataset()) {
1432
- const resultTable = resultTableFor(dataset);
1433
- const declared = asArray(resultTable && resultTable.columns)
1434
- .map((column) => column && column.key)
1435
- .filter((key) => typeof key === "string" && key);
1436
- if (declared.length) return declared;
1437
- const preferred = resultTable ? [] : ["series", "x", "y", "label"];
1438
- const seen = new Set();
1439
- const keys = [];
1440
- for (const key of preferred) {
1441
- if (rows.some((row) => row[key] != null && row[key] !== "")) {
1442
- seen.add(key);
1443
- keys.push(key);
1444
- }
1445
- }
1446
- for (const row of rows) {
1447
- for (const key of Object.keys(row)) {
1448
- if (seen.has(key)) continue;
1449
- seen.add(key);
1450
- keys.push(key);
1451
- }
1452
- }
1453
- return keys;
1454
- }
1455
-
1456
- function columnLabel(key, dataset = activeDataset()) {
1457
- if (key === measureNamesField) return "Measure names";
1458
- const resultTable = resultTableFor(dataset);
1459
- const declared = asArray(resultTable && resultTable.columns).find((column) => column && column.key === key);
1460
- if (declared && declared.label) return text(declared.label);
1461
- const labels = {
1462
- x: "X",
1463
- y: "Y",
1464
- series: "Series",
1465
- label: "Label",
1466
- };
1467
- return labels[key] || key.replaceAll("_", " ").replace(/\b\w/g, (char) => char.toUpperCase());
1468
- }
1469
-
1470
- function columnType(dataset, key) {
1471
- const resultTable = resultTableFor(dataset);
1472
- const declared = asArray(resultTable && resultTable.columns).find((column) => column && column.key === key);
1473
- return declared && declared.type ? text(declared.type) : "";
1474
- }
1475
-
1476
- function columnUnit(dataset, key) {
1477
- const resultTable = resultTableFor(dataset);
1478
- const declared = asArray(resultTable && resultTable.columns).find((column) => column && column.key === key);
1479
- return declared && declared.unit != null ? text(declared.unit).trim() : "";
1480
- }
1481
-
1482
- function isNumericColumn(rows, key, dataset = activeDataset()) {
1483
- const type = columnType(dataset, key);
1484
- if (["number", "percent", "currency"].includes(type)) return true;
1485
- if (["text", "date"].includes(type)) return false;
1486
- return rows.some((row) => number(row[key]) != null);
1487
- }
1488
-
1489
- function availableFields(dataset) {
1490
- const rows = rawChartRows(dataset);
1491
- return chartColumns(rows, dataset).map((key) => ({
1492
- key,
1493
- label: columnLabel(key, dataset),
1494
- numeric: isNumericColumn(rows, key, dataset),
1495
- type: columnType(dataset, key),
1496
- }));
1497
- }
1498
-
1499
- function hasField(dataset, key) {
1500
- if (key === measureNamesField) return compatibleMeasureNameFields(dataset).length > 1;
1501
- return availableFields(dataset).some((field) => field.key === key);
1502
- }
1503
-
1504
- function fieldLabel(dataset, key, fallback = "None") {
1505
- const field = availableFields(dataset).find((item) => item.key === key);
1506
- return field ? field.label : fallback;
1507
- }
1508
-
1509
- function colorGroupingRoleLabel(type = activeVisualizationType) {
1510
- type = widgetVisualizationType(type);
1511
- if (type === "pie") return "Slice by";
1512
- if (type === "heatmap") return "Y-axis";
1513
- if ((type === "bar" && (chartSettings.barGroupMode === "stacked" || chartSettings.barGroupMode === "stacked100")) || type === "stackedArea") return "Stack by";
1514
- if (type === "line" || type === "area") return "Series by";
1515
- if (type === "bar") return "Group by";
1516
- return "Color by";
1517
- }
1518
-
1519
- function chartUsesSingleSeriesOnly(type = activeVisualizationType) {
1520
- return ["funnel", "leaderboard", "waterfall"].includes(widgetVisualizationType(type));
1521
- }
1522
-
1523
- function chartRoleSpecs(type = activeVisualizationType) {
1524
- type = widgetVisualizationType(type);
1525
- if (type === "funnel") {
1526
- return [
1527
- { role: "x", label: "Stage", fallback: "Stage", required: true, aggregate: false, time: false },
1528
- { role: "y", label: "Value", fallback: "Value", required: true, aggregate: true, time: false },
1529
- ];
1530
- }
1531
- if (type === "waterfall") {
1532
- return [
1533
- { role: "x", label: "Step", fallback: "Step", required: true, aggregate: false, time: false },
1534
- { role: "y", label: "Change", fallback: "Change", required: true, aggregate: true, time: false },
1535
- ];
1536
- }
1537
- if (type === "leaderboard") {
1538
- return [
1539
- { role: "x", label: "Category", fallback: "Category", required: true, aggregate: false, time: false },
1540
- { role: "y", label: "Value", fallback: "Value", required: true, aggregate: true, time: false },
1541
- ];
1542
- }
1543
- if (type === "pie") {
1544
- return [
1545
- { role: "color", label: colorGroupingRoleLabel(type), fallback: "Slice", required: true, aggregate: false, time: false },
1546
- { role: "y", label: "Value", fallback: "Value", required: true, aggregate: true, time: false },
1547
- ];
1548
- }
1549
- if (type === "histogram") {
1550
- return [
1551
- { role: "y", label: "Value", fallback: "Value", required: true, aggregate: false, time: false },
1552
- ];
1553
- }
1554
- if (type === "scatter") {
1555
- return [
1556
- { role: "x", label: "X-axis", fallback: "X-axis", required: true, aggregate: false, time: false, numeric: true },
1557
- { role: "y", label: "Y-axis", fallback: "Y-axis", required: true, aggregate: false, time: false, numeric: true },
1558
- { role: "size", label: "Size by", fallback: "None", required: false, aggregate: false, time: false, numeric: true },
1559
- { role: "label", label: "Label by", fallback: "None", required: false, aggregate: false, time: false },
1560
- { role: "color", label: colorGroupingRoleLabel(type), fallback: "None", required: false, aggregate: false, time: false },
1561
- ];
1562
- }
1563
- if (type === "heatmap") {
1564
- return [
1565
- { role: "x", label: "X-axis", fallback: "X-axis", required: true, aggregate: false, time: true },
1566
- { role: "color", label: "Y-axis", fallback: "Y-axis", required: true, aggregate: false, time: false },
1567
- { role: "y", label: "Value", fallback: "Value", required: true, aggregate: true, time: false },
1568
- ];
1569
- }
1570
- return [
1571
- { role: "x", label: "X-axis", fallback: "X-axis", required: true, aggregate: false, time: true },
1572
- { role: "y", label: "Y-axis", fallback: "Y-axis", required: true, aggregate: true, time: false },
1573
- { role: "color", label: colorGroupingRoleLabel(type), fallback: "None", required: false, aggregate: false, time: false },
1574
- ];
1575
- }
1576
-
1577
- function chartRoleSpec(role, type = activeVisualizationType) {
1578
- return chartRoleSpecs(type).find((spec) => spec.role === role) || { role, label: role, fallback: "None", required: false };
1579
- }
1580
-
1581
- function detailControlLabel(spec) {
1582
- if (spec && spec.detailLabel) return spec.detailLabel;
1583
- if (spec && spec.label && !/^x-axis$|^y-axis$/i.test(spec.label)) {
1584
- return text(spec.label).replace("-axis", " axis");
1585
- }
1586
- if (spec && spec.role === "color" && /^y-axis$/i.test(spec.label || "")) return "Y axis";
1587
- if (spec && spec.role === "x") return "X axis";
1588
- if (spec && spec.role === "y") return "Y axis";
1589
- if (spec && spec.role === "size") return "Size by";
1590
- if (spec && spec.role === "label") return "Label by";
1591
- if (spec && spec.role === "color") return "Stack by";
1592
- if (spec && spec.role === "time") return "Time period";
1593
- return text((spec && spec.label) || "").replace("-axis", " axis");
1594
- }
1595
-
1596
- function fieldKeyForRole(role) {
1597
- if (role === "x") return chartConfig.xField;
1598
- if (role === "y") return chartConfig.yField;
1599
- if (role === "size") return chartConfig.sizeField;
1600
- if (role === "label") return chartConfig.labelField;
1601
- if (role === "color") return chartConfig.colorField;
1602
- return "";
1603
- }
1604
-
1605
- function compatibleFieldsForRole(dataset, role, type = activeVisualizationType) {
1606
- const fields = availableFields(dataset);
1607
- const spec = chartRoleSpec(role, type);
1608
- let compatible = fields;
1609
- if (["funnel", "leaderboard", "waterfall"].includes(widgetVisualizationType(type)) && role === "x") {
1610
- compatible = fields.filter((field) => !field.numeric);
1611
- }
1612
- if (role === "y" || role === "size" || spec.numeric) compatible = fields.filter((field) => field.numeric);
1613
- if (role === "color") {
1614
- const dimensions = fields.filter((field) => !field.numeric && field.key !== chartConfig.xField);
1615
- const measures = compatibleMeasureNameFields(dataset);
1616
- compatible = [
1617
- ...dimensions,
1618
- ...(measures.length > 1 ? [{ key: measureNamesField, label: "Measure names", numeric: false, type: "text", synthetic: true }] : []),
1619
- ];
1620
- }
1621
- return compatible.length ? compatible : fields;
1622
- }
1623
-
1624
- function canonicalTimeUnit(value) {
1625
- const raw = text(value || "").toLowerCase();
1626
- return ["year", "quarter", "month", "week", "day", "hour", "minute", "second"].includes(raw) ? raw : "none";
1627
- }
1628
-
1629
- function canonicalAggregation(value) {
1630
- const raw = text(value || "").toLowerCase();
1631
- const aliases = { average: "avg", mean: "avg", minimum: "min", maximum: "max" };
1632
- const next = aliases[raw] || raw;
1633
- return ["sum", "avg", "min", "max", "count"].includes(next) ? next : "sum";
1634
- }
1635
-
1636
- function aggregationLabel(value) {
1637
- const labels = { sum: "Sum", avg: "Avg", min: "Min", max: "Max", count: "Count" };
1638
- return labels[canonicalAggregation(value)] || "Sum";
1639
- }
1640
-
1641
- function timeUnitLabel(value) {
1642
- const unit = canonicalTimeUnit(value);
1643
- return unit === "none" ? "None" : unit.replace(/^\w/, (char) => char.toUpperCase());
1644
- }
1645
-
1646
- function timeUnitOptionsForDataset(dataset, fieldKey = chartConfig.xField) {
1647
- if (!fieldLooksDateLike(dataset, fieldKey)) return [];
1648
- const values = rawChartRows(dataset).map((row) => text(row && row[fieldKey])).filter(Boolean);
1649
- const hasClockTime = values.some((value) => /[T\s]\d{1,2}:\d{2}/.test(value));
1650
- const hasSeconds = values.some((value) => /[T\s]\d{1,2}:\d{2}:\d{2}/.test(value));
1651
- const options = ["None", "Year", "Quarter", "Month", "Week", "Day"];
1652
- if (hasClockTime) options.push("Hour", "Minute");
1653
- if (hasSeconds) options.push("Second");
1654
- return options;
1655
- }
1656
-
1657
- function parseDateValue(value) {
1658
- if (value instanceof Date && Number.isFinite(value.getTime())) return value;
1659
- if (typeof value !== "string") return null;
1660
- const raw = text(value);
1661
- if (!raw) return null;
1662
- if (!/^\d{4}[-/]\d{1,2}(?:[-/]\d{1,2})?(?:[T\s]\d{1,2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/.test(raw)) return null;
1663
- const parsed = new Date(raw);
1664
- return Number.isFinite(parsed.getTime()) ? parsed : null;
1665
- }
1666
-
1667
- function formatTimeValue(value, unit) {
1668
- unit = canonicalTimeUnit(unit);
1669
- if (unit === "none") return text(value);
1670
- const date = parseDateValue(value);
1671
- if (!date) return text(value);
1672
- const year = date.getUTCFullYear();
1673
- const month = date.getUTCMonth() + 1;
1674
- const pad = (next) => String(next).padStart(2, "0");
1675
- if (unit === "year") return String(year);
1676
- if (unit === "quarter") return `${year} Q${Math.floor((month - 1) / 3) + 1}`;
1677
- if (unit === "month") return `${year}-${pad(month)}`;
1678
- if (unit === "week") {
1679
- const first = Date.UTC(year, 0, 1);
1680
- const current = Date.UTC(year, date.getUTCMonth(), date.getUTCDate());
1681
- const week = Math.floor((current - first) / (7 * 24 * 60 * 60 * 1000)) + 1;
1682
- return `${year} W${pad(week)}`;
1683
- }
1684
- if (unit === "day") return `${year}-${pad(month)}-${pad(date.getUTCDate())}`;
1685
- if (unit === "hour") return `${year}-${pad(month)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:00`;
1686
- if (unit === "minute") return `${year}-${pad(month)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
1687
- return `${year}-${pad(month)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`;
1688
- }
1689
-
1690
- function resetChartConfig(dataset = activeDataset()) {
1691
- const fields = availableFields(dataset);
1692
- const rows = rawChartRows(dataset);
1693
- const xEncoding = specEncoding(dataset, "x");
1694
- const yEncoding = specEncoding(dataset, "y");
1695
- const sizeEncoding = specEncoding(dataset, "size");
1696
- const colorEncoding = specEncoding(dataset, "color");
1697
- const lineStyleEncoding = specEncoding(dataset, "lineStyle");
1698
- const labelEncoding = specEncoding(dataset, "label");
1699
- const firstDimension = fields.find((field) => !field.numeric)?.key || "x";
1700
- const firstMeasure = fields.find((field) => field.numeric)?.key || "y";
1701
- const hasResultTable = Boolean(resultTableFor(dataset));
1702
- const fallbackColor = hasResultTable ? "" : fields.find((field) => field.key === "series")?.key || "";
1703
- const nextXField = text(xEncoding.field || chartConfig.xField || firstDimension) || firstDimension;
1704
- const nextYField = text(yEncoding.field || chartConfig.yField || firstMeasure) || firstMeasure;
1705
- const requestedTimeUnit = xEncoding.time_unit;
1706
- const explicitColor = colorEncoding.field || (multiMeasureSeriesEnabled(dataset) ? measureNamesField : "");
1707
- const explicitLineStyle = lineStyleEncoding.field;
1708
- const explicitLabel = labelEncoding.field;
1709
- const explicitSize = sizeEncoding.field;
1710
- chartConfig = {
1711
- xField: nextXField,
1712
- yField: nextYField,
1713
- colorField: text(explicitColor || fallbackColor) || fallbackColor,
1714
- lineStyleField: text(explicitLineStyle || chartConfig.lineStyleField || "") || "",
1715
- labelField: text(explicitLabel || chartConfig.labelField || "") || "",
1716
- sizeField: text(explicitSize || chartConfig.sizeField || "") || "",
1717
- timeUnit: requestedTimeUnit != null ? canonicalTimeUnit(requestedTimeUnit) : canonicalTimeUnit(chartConfig.timeUnit || "none"),
1718
- yAggregation: canonicalAggregation(yEncoding.aggregate || chartConfig.yAggregation),
1719
- };
1720
- if (!hasField(dataset, chartConfig.xField)) chartConfig.xField = firstDimension;
1721
- if (!hasField(dataset, chartConfig.yField)) chartConfig.yField = firstMeasure;
1722
- if (!hasField(dataset, chartConfig.colorField)) chartConfig.colorField = fallbackColor;
1723
- if (!hasField(dataset, chartConfig.lineStyleField)) chartConfig.lineStyleField = "";
1724
- if (!hasField(dataset, chartConfig.labelField)) chartConfig.labelField = "";
1725
- if (!hasField(dataset, chartConfig.sizeField)) chartConfig.sizeField = "";
1726
- if (!fieldLooksDateLike(dataset, chartConfig.xField)) chartConfig.timeUnit = "none";
1727
- }
1728
-
1729
- function normalizeChartConfigForType(dataset = activeDataset(), type = activeVisualizationType) {
1730
- if (!chartUsesSingleSeriesOnly(type)) return;
1731
- const compatibleStageFields = compatibleFieldsForRole(dataset, "x", type);
1732
- const compatibleValueFields = compatibleFieldsForRole(dataset, "y", type);
1733
- if (!compatibleStageFields.some((field) => field.key === chartConfig.xField)) {
1734
- chartConfig.xField = compatibleStageFields[0]?.key || chartConfig.xField;
1735
- }
1736
- if (!compatibleValueFields.some((field) => field.key === chartConfig.yField)) {
1737
- chartConfig.yField = compatibleValueFields[0]?.key || chartConfig.yField;
1738
- }
1739
- chartConfig.colorField = "";
1740
- chartConfig.timeUnit = "none";
1741
- }
1742
-
1743
- function fieldLooksDateLike(dataset, fieldKey) {
1744
- if (!fieldKey) return false;
1745
- const field = availableFields(dataset).find((candidate) => candidate.key === fieldKey);
1746
- if (field && field.type === "date") return true;
1747
- if (field && field.numeric) return false;
1748
- return rawChartRows(dataset).some((row) => row[fieldKey] != null && parseDateValue(row[fieldKey]));
1749
- }
1750
-
1751
- function measureNameFields(dataset) {
1752
- return availableFields(dataset).filter((field) => field.numeric && field.key !== chartConfig.xField);
1753
- }
1754
-
1755
- function multiMeasureSeriesEnabled(dataset) {
1756
- return specSettings(dataset).multi_measure_series === true;
1757
- }
1758
-
1759
- function measureCompatibilityKey(dataset, field) {
1760
- if (!field) return "";
1761
- const unit = columnUnit(dataset, field.key);
1762
- if (unit) return `unit:${unit.toLowerCase()}`;
1763
- return field.type === "percent" ? "type:percent" : "";
1764
- }
1765
-
1766
- function compatibleMeasureNameFields(dataset) {
1767
- const measures = measureNameFields(dataset);
1768
- if (multiMeasureSeriesEnabled(dataset)) return measures;
1769
- const selectedMeasure = measures.find((field) => field.key === chartConfig.yField);
1770
- const compatibilityKey = measureCompatibilityKey(dataset, selectedMeasure);
1771
- if (!compatibilityKey) return [];
1772
- return measures.filter((field) => measureCompatibilityKey(dataset, field) === compatibilityKey);
1773
- }
1774
-
1775
- function usesMeasureNames() {
1776
- return chartConfig.colorField === measureNamesField;
1777
- }
1778
-
1779
- function appendAggregatedValue(groups, x, series, y, row, lineStyle = null) {
1780
- const key = `${x}\u0000${series}`;
1781
- if (y == null) return;
1782
- if (!groups.has(key)) {
1783
- groups.set(key, { x, series, lineStyle, values: [], raw: row });
1784
- }
1785
- if (lineStyle && !groups.get(key).lineStyle) groups.get(key).lineStyle = lineStyle;
1786
- groups.get(key).values.push(y);
1787
- }
1788
-
1789
- function aggregateRows(rows, dataset = activeDataset()) {
1790
- const groups = new Map();
1791
- for (const row of rows) {
1792
- const x = formatTimeValue(row[chartConfig.xField], chartConfig.timeUnit);
1793
- if (usesMeasureNames()) {
1794
- for (const field of compatibleMeasureNameFields(dataset)) {
1795
- appendAggregatedValue(groups, x, field.label, number(row[field.key]), row);
1796
- }
1797
- continue;
1798
- }
1799
- const series = chartConfig.colorField ? text(row[chartConfig.colorField] || "Value") : "Value";
1800
- const y = chartConfig.yAggregation === "count" ? 1 : number(row[chartConfig.yField]);
1801
- const lineStyle = chartConfig.lineStyleField ? text(row[chartConfig.lineStyleField] || "") : "";
1802
- appendAggregatedValue(groups, x, series, y, row, lineStyle || null);
1803
- }
1804
- const output = [...groups.values()].map((group) => {
1805
- let y = 0;
1806
- if (chartConfig.yAggregation === "avg") y = group.values.reduce((sum, value) => sum + value, 0) / group.values.length;
1807
- else if (chartConfig.yAggregation === "min") y = Math.min(...group.values);
1808
- else if (chartConfig.yAggregation === "max") y = Math.max(...group.values);
1809
- else y = group.values.reduce((sum, value) => sum + value, 0);
1810
- return { x: group.x, y, series: group.series, ...(group.lineStyle ? { lineStyle: group.lineStyle } : {}), raw: group.raw };
1811
- });
1812
- return output;
1813
- }
1814
-
1815
- function rawProjectedRows(rows, dataset = activeDataset()) {
1816
- const output = [];
1817
- for (const row of rows) {
1818
- if (usesMeasureNames()) {
1819
- for (const field of compatibleMeasureNameFields(dataset)) {
1820
- const y = number(row[field.key]);
1821
- if (y == null) continue;
1822
- output.push({
1823
- x: row[chartConfig.xField],
1824
- y,
1825
- size: chartConfig.sizeField ? number(row[chartConfig.sizeField]) : null,
1826
- series: field.label,
1827
- ...(chartConfig.labelField ? { label: text(row[chartConfig.labelField] || "") } : {}),
1828
- raw: row,
1829
- });
1830
- }
1831
- continue;
1832
- }
1833
- const y = number(row[chartConfig.yField]);
1834
- if (y == null) continue;
1835
- output.push({
1836
- x: row[chartConfig.xField],
1837
- y,
1838
- size: chartConfig.sizeField ? number(row[chartConfig.sizeField]) : null,
1839
- series: chartConfig.colorField ? text(row[chartConfig.colorField] || "Value") : "Value",
1840
- ...(chartConfig.lineStyleField ? { lineStyle: text(row[chartConfig.lineStyleField] || "") } : {}),
1841
- ...(chartConfig.labelField ? { label: text(row[chartConfig.labelField] || "") } : {}),
1842
- raw: row,
1843
- });
1844
- }
1845
- return output;
1846
- }
1847
-
1848
- function projectedDataset(dataset) {
1849
- const type = widgetVisualizationType(activeVisualizationType);
1850
- const rows = projectedDataForType(dataset, type);
1851
- const renderRows = projectedRenderRows(rows);
1852
- return {
1853
- ...dataset,
1854
- chart_spec: projectedChartSpec(dataset, type, rows),
1855
- data: renderRows,
1856
- table: {
1857
- columns: projectedRenderColumns(rows, dataset),
1858
- rows: renderRows,
1859
- row_count: renderRows.length,
1860
- truncated: false,
1861
- },
1862
- };
1863
- }
1864
-
1865
- function projectedDataForType(dataset, type = activeVisualizationType) {
1866
- type = widgetVisualizationType(type);
1867
- const rows = chartRows(dataset);
1868
- return type === "scatter" || type === "histogram" || type === "boxPlot" ? rawProjectedRows(rows, dataset) : aggregateRows(rows, dataset);
1869
- }
1870
-
1871
- function hasNonNegativeProjectedValues(data) {
1872
- let observed = false;
1873
- for (const point of data) {
1874
- const value = number(point && point.y);
1875
- if (value == null) continue;
1876
- observed = true;
1877
- if (value < 0) return false;
1878
- }
1879
- return observed;
1880
- }
1881
-
1882
- function projectedSeriesCount(data) {
1883
- return groupData(data).size;
1884
- }
1885
-
1886
- function projectedXCount(data) {
1887
- return uniqueX(data).length;
1888
- }
1889
-
1890
- function numericValuesForField(dataset, fieldKey) {
1891
- if (!fieldKey) return [];
1892
- return rawChartRows(dataset)
1893
- .map((row) => number(row && row[fieldKey]))
1894
- .filter((value) => value != null);
1895
- }
1896
-
1897
- function hasDistributionSample(dataset, fieldKey, minimumValues = 10) {
1898
- const values = numericValuesForField(dataset, fieldKey);
1899
- return values.length >= minimumValues && new Set(values.map((value) => String(value))).size >= 3;
1900
- }
1901
-
1902
- function maxNumericValuesPerCategory(dataset, categoryField, valueField) {
1903
- if (!categoryField || !valueField) return 0;
1904
- const counts = new Map();
1905
- for (const row of rawChartRows(dataset)) {
1906
- if (number(row && row[valueField]) == null) continue;
1907
- const category = text(row && row[categoryField]);
1908
- counts.set(category, (counts.get(category) || 0) + 1);
1909
- }
1910
- return Math.max(0, ...counts.values());
1911
- }
1912
-
1913
- function compatibleChartPickerSections(dataset = activeDataset()) {
1914
- return chartPickerSections
1915
- .map((section) => ({
1916
- ...section,
1917
- options: section.options
1918
- .filter((option, index, options) => {
1919
- const canonicalType = widgetVisualizationType(option.type);
1920
- return options.findIndex((candidate) => widgetVisualizationType(candidate.type) === canonicalType) === index;
1921
- })
1922
- .map((option) => ({ ...option, type: widgetVisualizationType(option.type) })),
1923
- }))
1924
- .filter((section) => section.options.length);
1925
- }
1926
-
1927
- function ensureActiveChartTypeIsSupported(dataset = activeDataset()) {
1928
- activeVisualizationType = widgetVisualizationType(activeVisualizationType || "bar");
1929
- normalizeChartConfigForType(dataset, activeVisualizationType);
1930
- }
1931
-
1932
- function renderFieldList(dataset) {
1933
- const root = document.getElementById("field-list");
1934
- if (!root) return;
1935
- root.innerHTML = "";
1936
- const rows = rawChartRows(dataset);
1937
- const keys = chartColumns(rows, dataset);
1938
- const measures = keys.filter((key) => isNumericColumn(rows, key, dataset));
1939
- const dimensions = keys.filter((key) => !measures.includes(key));
1940
- const activeKeys = new Set([chartConfig.xField, chartConfig.yField, chartConfig.colorField, chartConfig.lineStyleField, chartConfig.labelField, chartConfig.sizeField]);
1941
- const filter = text(document.querySelector(".field-filter")?.value).trim().toLowerCase();
1942
-
1943
- function appendGroup(title, fields) {
1944
- fields = fields.filter((key) => !filter || columnLabel(key, dataset).toLowerCase().includes(filter) || key.toLowerCase().includes(filter));
1945
- if (!fields.length) return;
1946
- const group = document.createElement("div");
1947
- group.className = "field-group";
1948
- const heading = document.createElement("p");
1949
- heading.className = "field-group-title";
1950
- heading.textContent = title;
1951
- const list = document.createElement("ul");
1952
- list.className = "field-list";
1953
- for (const key of fields) {
1954
- const item = document.createElement("li");
1955
- item.className = `field-item${activeKeys.has(key) ? " is-active" : ""}`;
1956
- const icon = document.createElement("span");
1957
- icon.className = "field-icon";
1958
- icon.appendChild(lucideIcon(measures.includes(key) ? "hash" : "type", { width: "14", height: "14" }));
1959
- const label = document.createElement("span");
1960
- label.className = "field-label";
1961
- label.textContent = columnLabel(key, dataset);
1962
- item.append(icon, label);
1963
- list.appendChild(item);
1964
- }
1965
- group.append(heading, list);
1966
- root.appendChild(group);
1967
- }
1968
-
1969
- appendGroup("Measures", measures);
1970
- appendGroup("Columns", dimensions);
1971
- }
1972
-
1973
- function setupAppChromeControls() {
1974
- const fieldFilter = document.querySelector(".field-filter");
1975
- if (!fieldFilter) return;
1976
- fieldFilter.addEventListener("input", () => renderFieldList(activeDataset()));
1977
- }
1978
-
1979
- function renderDataPreview(dataset) {
1980
- const root = document.getElementById("data-preview");
1981
- if (!root) return;
1982
- destroyDataTable(root);
1983
- root.innerHTML = "";
1984
- const rows = chartRows(dataset);
1985
- renderDataTable(root, {
1986
- columns: tableColumns(dataset, rows),
1987
- emptyLabel: "No rows match the selected filters.",
1988
- maxRows: 80,
1989
- pageSize: isDetailDisplayMode() ? 12 : 8,
1990
- rows,
1991
- });
1992
- }
1993
-
1994
- function appendSourceRow(root, label, value, options = {}) {
1995
- if (value == null || value === "") return;
1996
- if (options.code) {
1997
- const details = document.createElement("details");
1998
- details.className = "source-disclosure";
1999
- const summary = document.createElement("summary");
2000
- summary.textContent = label;
2001
- const code = document.createElement("pre");
2002
- code.className = "source-code";
2003
- code.dataset.language = options.language || label;
2004
- renderSourceCode(code, value, code.dataset.language);
2005
- details.append(summary, code);
2006
- root.appendChild(details);
2007
- return;
2008
- }
2009
- const row = document.createElement("div");
2010
- row.className = "source-row";
2011
- const labelEl = document.createElement("span");
2012
- labelEl.textContent = label;
2013
- const valueEl = document.createElement("strong");
2014
- if (options.multiline) {
2015
- valueEl.className = "source-row-list";
2016
- for (const item of sourceDetailList(value)) {
2017
- const line = document.createElement("span");
2018
- line.textContent = text(item);
2019
- valueEl.appendChild(line);
2020
- }
2021
- }
2022
- else {
2023
- valueEl.textContent = text(value);
2024
- }
2025
- row.append(labelEl, valueEl);
2026
- root.appendChild(row);
2027
- }
2028
-
2029
- function appendSourcePreviewRows(root, dataset) {
2030
- const table = resultTableFor(dataset) || {};
2031
- const rows = asArray(table.rows).filter((row) => row && typeof row === "object").slice(0, 10);
2032
- if (!rows.length) {
2033
- appendSourceRow(root, "Preview rows", "No preview rows available");
2034
- return;
2035
- }
2036
- const columns = [];
2037
- const seen = new Set();
2038
- for (const column of asArray(table.columns)) {
2039
- const key = column && (column.key || column.field);
2040
- if (key && !seen.has(key)) {
2041
- seen.add(key);
2042
- columns.push(key);
2043
- }
2044
- }
2045
- for (const row of rows) {
2046
- for (const key of Object.keys(row)) {
2047
- if (!seen.has(key)) {
2048
- seen.add(key);
2049
- columns.push(key);
2050
- }
2051
- }
2052
- }
2053
- const wrapper = document.createElement("div");
2054
- wrapper.className = "source-row source-row-table";
2055
- const labelEl = document.createElement("span");
2056
- labelEl.textContent = "Preview rows";
2057
- const tableShell = document.createElement("div");
2058
- tableShell.className = "source-preview-shell";
2059
- const previewTable = document.createElement("table");
2060
- previewTable.className = "source-preview-table";
2061
- const thead = document.createElement("thead");
2062
- const headRow = document.createElement("tr");
2063
- for (const column of columns) {
2064
- const th = document.createElement("th");
2065
- th.textContent = column;
2066
- headRow.appendChild(th);
2067
- }
2068
- thead.appendChild(headRow);
2069
- const tbody = document.createElement("tbody");
2070
- for (const row of rows) {
2071
- const tr = document.createElement("tr");
2072
- for (const column of columns) {
2073
- const td = document.createElement("td");
2074
- td.textContent = text(row[column]);
2075
- tr.appendChild(td);
2076
- }
2077
- tbody.appendChild(tr);
2078
- }
2079
- previewTable.append(thead, tbody);
2080
- tableShell.appendChild(previewTable);
2081
- wrapper.append(labelEl, tableShell);
2082
- root.appendChild(wrapper);
2083
- }
2084
-
2085
- function renderSourceDetails(dataset) {
2086
- const root = document.getElementById("source-details");
2087
- if (!root) return;
2088
- root.innerHTML = "";
2089
- const sourceQuery = sourceQueryFor(dataset) || {};
2090
- const metadataObjects = sourceMetadataObjects(dataset);
2091
- const queryCode = sqlTextFor(dataset);
2092
- const queryLanguage = queryLanguageFor(dataset);
2093
- const explicitTables = sourceTableNamesFromMetadata(metadataObjects);
2094
- const inferredTables = extractTablesFromSql(queryCode);
2095
- const tables = explicitTables.length ? explicitTables : inferredTables;
2096
- const explicitFilters = firstListFromObjects(metadataObjects, [
2097
- "filters",
2098
- "filter_descriptions",
2099
- "filterDescriptions",
2100
- "filter_description",
2101
- "filterDescription",
2102
- ]);
2103
- const activeFilters = activeDataFilterSummary(dataset);
2104
- const filters = explicitFilters.length ? explicitFilters : activeFilters === "None" ? ["None declared"] : [activeFilters];
2105
- const metrics = metricDefinitionList(dataset);
2106
- appendSourceRow(root, "Dataset", valueFor(dataset, "dataset", payload.dataset) || sourceLabelFor(dataset) || "Not declared");
2107
- if (sourceQuery.description) appendSourceRow(root, "Description", sourceQuery.description);
2108
- appendSourceRow(root, "Tables used", sourceDetailList(tables.length ? tables : ["Not declared"]), { multiline: true });
2109
- appendSourceRow(root, "Filters", sourceDetailText(filters));
2110
- appendSourceRow(root, "Metric definitions", sourceDetailList(metrics.length ? metrics : ["Displayed directly from source columns"]), { multiline: true });
2111
- appendSourceRow(root, "Snapshot", sourceQuery.executed_at || valueFor(dataset, "executed_at", payload.executed_at) || "Not declared");
2112
- if (queryCode) appendSourceRow(root, "Source query", queryCode, { code: true, language: queryLanguage });
2113
- appendSourcePreviewRows(root, dataset);
2114
- if (!root.childElementCount) {
2115
- const empty = document.createElement("p");
2116
- empty.className = "source-note";
2117
- empty.textContent = "Static reviewed data.";
2118
- root.appendChild(empty);
2119
- }
2120
- }
2121
-
2122
- function renderSqlPreview(dataset) {
2123
- const root = document.getElementById("sql-preview");
2124
- if (!root) return;
2125
- root.innerHTML = "";
2126
- const bottomSplit = document.getElementById("bottom-split");
2127
- const sql = sqlTextFor(dataset);
2128
- if (bottomSplit) bottomSplit.classList.toggle("has-sql", Boolean(sql));
2129
- if (!sql) {
2130
- const empty = document.createElement("div");
2131
- empty.className = "empty";
2132
- empty.setAttribute("role", "status");
2133
- const heading = document.createElement("strong");
2134
- heading.textContent = "No query available";
2135
- const detail = document.createElement("span");
2136
- detail.textContent = "This chart payload did not include SQL or Python source code.";
2137
- empty.append(heading, detail);
2138
- root.appendChild(empty);
2139
- return;
2140
- }
2141
-
2142
- const code = document.createElement("pre");
2143
- code.className = "source-code";
2144
- code.dataset.language = queryLanguageFor(dataset);
2145
- renderSourceCode(code, sql, code.dataset.language);
2146
- root.appendChild(code);
2147
- }
2148
-
2149
- function renderAppChrome(dataset) {
2150
- renderSourceDetails(dataset);
2151
- renderFieldList(dataset);
2152
- renderDataPreview(dataset);
2153
- renderSqlPreview(dataset);
2154
- }
2155
-
2156
- function groupData(data) {
2157
- const groups = new Map();
2158
- for (const point of data) {
2159
- const y = number(point && point.y);
2160
- if (y == null) continue;
2161
- const series = text(point.series || "Value");
2162
- if (!groups.has(series)) groups.set(series, []);
2163
- groups.get(series).push({ x: text(point.x), y, label: point.label, raw: point });
2164
- }
2165
- return groups;
2166
- }
2167
-
2168
- function rechartsLegendNamesForDataset(dataset, type) {
2169
- if (type === "histogram" || type === "heatmap" || type === "boxPlot") return [];
2170
- const chartSpec = chartSpecFor(dataset);
2171
- const encodings = chartSpec && typeof chartSpec.encodings === "object" && !Array.isArray(chartSpec.encodings)
2172
- ? chartSpec.encodings
2173
- : {};
2174
- const yFields = asArray(encodings.y && encodings.y.fields).map(text).filter(Boolean);
2175
- if (yFields.length) return yFields;
2176
- const colorField = text(encodings.color && encodings.color.field);
2177
- if (colorField) {
2178
- return uniqueProjectedSeriesValues(chartRows(dataset)).map((value, index) => syntheticSeriesField(value, index));
2179
- }
2180
- const yField = text(encodings.y && encodings.y.field);
2181
- return yField ? [yField] : [];
2182
- }
2183
-
2184
- function uniqueX(data) {
2185
- const seen = new Set();
2186
- const values = [];
2187
- for (const point of data) {
2188
- const x = text(point && point.x);
2189
- if (seen.has(x)) continue;
2190
- seen.add(x);
2191
- values.push(x);
2192
- }
2193
- return values;
2194
- }
2195
-
2196
- function svgEl(name, attrs = {}) {
2197
- const element = document.createElementNS("http://www.w3.org/2000/svg", name);
2198
- for (const [key, value] of Object.entries(attrs)) {
2199
- if (value != null) element.setAttribute(key, String(value));
2200
- }
2201
- return element;
2202
- }
2203
-
2204
- function lucideIcon(name, attrs = {}) {
2205
- const svg = svgEl("svg", {
2206
- viewBox: "0 0 24 24",
2207
- fill: "none",
2208
- "aria-hidden": "true",
2209
- focusable: "false",
2210
- ...attrs,
2211
- });
2212
- svg.setAttribute("stroke", "currentColor");
2213
- svg.setAttribute("stroke-width", "2");
2214
- svg.setAttribute("stroke-linecap", "round");
2215
- svg.setAttribute("stroke-linejoin", "round");
2216
- const icons = {
2217
- check: [["path", { d: "M20 6 9 17l-5-5" }]],
2218
- chevronDown: [["path", { d: "m6 9 6 6 6-6" }]],
2219
- chevronRight: [["path", { d: "m9 18 6-6-6-6" }]],
2220
- fileDown: [
2221
- ["path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }],
2222
- ["path", { d: "M14 2v4a2 2 0 0 0 2 2h4" }],
2223
- ["path", { d: "M12 18v-6" }],
2224
- ["path", { d: "m9 15 3 3 3-3" }],
2225
- ],
2226
- fileText: [
2227
- ["path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }],
2228
- ["path", { d: "M14 2v4a2 2 0 0 0 2 2h4" }],
2229
- ["path", { d: "M10 9H8" }],
2230
- ["path", { d: "M16 13H8" }],
2231
- ["path", { d: "M16 17H8" }],
2232
- ],
2233
- printer: [
2234
- ["path", { d: "M6 9V2h12v7" }],
2235
- ["path", { d: "M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" }],
2236
- ["path", { d: "M6 14h12v8H6z" }],
2237
- ],
2238
- hash: [
2239
- ["line", { x1: "4", x2: "20", y1: "9", y2: "9" }],
2240
- ["line", { x1: "4", x2: "20", y1: "15", y2: "15" }],
2241
- ["line", { x1: "10", x2: "8", y1: "3", y2: "21" }],
2242
- ["line", { x1: "16", x2: "14", y1: "3", y2: "21" }],
2243
- ],
2244
- calendar: [
2245
- ["path", { d: "M8 2v4" }],
2246
- ["path", { d: "M16 2v4" }],
2247
- ["rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }],
2248
- ["path", { d: "M3 10h18" }],
2249
- ],
2250
- maximize2: [
2251
- ["polyline", { points: "15 3 21 3 21 9" }],
2252
- ["polyline", { points: "9 21 3 21 3 15" }],
2253
- ["line", { x1: "21", x2: "14", y1: "3", y2: "10" }],
2254
- ["line", { x1: "3", x2: "10", y1: "21", y2: "14" }],
2255
- ],
2256
- minimize2: [
2257
- ["polyline", { points: "4 14 10 14 10 20" }],
2258
- ["polyline", { points: "20 10 14 10 14 4" }],
2259
- ["line", { x1: "14", x2: "21", y1: "10", y2: "3" }],
2260
- ["line", { x1: "3", x2: "10", y1: "21", y2: "14" }],
2261
- ],
2262
- presentation: [
2263
- ["path", { d: "M2 3h20" }],
2264
- ["path", { d: "M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3" }],
2265
- ["path", { d: "m7 21 5-5 5 5" }],
2266
- ],
2267
- refreshCw: [
2268
- ["path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }],
2269
- ["path", { d: "M21 3v5h-5" }],
2270
- ["path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }],
2271
- ["path", { d: "M8 16H3v5" }],
2272
- ],
2273
- type: [
2274
- ["polyline", { points: "4 7 4 4 20 4 20 7" }],
2275
- ["line", { x1: "9", x2: "15", y1: "20", y2: "20" }],
2276
- ["line", { x1: "12", x2: "12", y1: "4", y2: "20" }],
2277
- ],
2278
- };
2279
- for (const [tag, attrs] of icons[name] || icons.chevronDown) {
2280
- svg.appendChild(svgEl(tag, attrs));
2281
- }
2282
- return svg;
2283
- }
2284
-
2285
- function renderEmpty(message, title = "Chart cannot be rendered") {
2286
- destroyChartInstance();
2287
- const chartRoot = document.getElementById("chart");
2288
- const empty = document.createElement("div");
2289
- empty.className = "empty";
2290
- empty.setAttribute("role", "status");
2291
- const heading = document.createElement("strong");
2292
- heading.textContent = title;
2293
- const detail = document.createElement("span");
2294
- detail.textContent = message;
2295
- empty.append(heading, detail);
2296
- chartRoot.appendChild(empty);
2297
- }
2298
-
2299
- function destroyChartInstance() {
2300
- destroyRechartsChart(document.getElementById("chart"));
2301
- destroyDataTable(document.getElementById("chart"));
2302
- chartInstance = null;
2303
- }
2304
-
2305
- function hideExternalLegend() {
2306
- const root = document.getElementById("legend");
2307
- const legendShell = document.getElementById("legend-shell");
2308
- if (root) root.innerHTML = "";
2309
- if (legendShell) {
2310
- legendShell.hidden = true;
2311
- legendShell.classList.add("is-empty");
2312
- legendShell.setAttribute("aria-hidden", "true");
2313
- }
2314
- }
2315
-
2316
- function chartPalette() {
2317
- return resolveCssColors(colors).filter(Boolean);
2318
- }
2319
-
2320
- function chartBorderPalette() {
2321
- return resolveCssColors(colorEdges).filter(Boolean);
2322
- }
2323
-
2324
- function resizeChartInstance() {
2325
- // Recharts responds to the container resize; the layout code only needs
2326
- // to reserve the region, not recreate the chart.
2327
- }
2328
-
2329
- function renderChart() {
2330
- const dataset = projectedDataset(activeDataset());
2331
- const chartRoot = document.getElementById("chart");
2332
- destroyChartInstance();
2333
- chartRoot.innerHTML = "";
2334
- activeVisualizationType = widgetVisualizationType(activeVisualizationType || "bar");
2335
- applyChartSettingsAttributes();
2336
- const type = rendererVisualizationType(activeVisualizationType);
2337
- const legendNames = rechartsLegendNamesForDataset(dataset, type);
2338
- reconcileVisibleSeries(legendNames);
2339
- try {
2340
- renderRechartsChart(chartRoot, dataset, type, {
2341
- colors: chartPalette(),
2342
- borderColors: chartBorderPalette(),
2343
- height: isDetailDisplayMode() ? "100%" : undefined,
2344
- onVisibleSeriesChange(nextVisibleSeries) {
2345
- visibleSeries = orderedVisibleSeries(legendNames, nextVisibleSeries);
2346
- renderChart();
2347
- },
2348
- visibleSeries,
2349
- timeUnit: chartConfig.timeUnit,
2350
- unit: unitFor(dataset),
2351
- baseline: number(specValue(dataset, "baseline", payload.baseline)),
2352
- settings: {
2353
- orientation: chartSettings.barOrientation,
2354
- group_mode: chartSettings.barGroupMode,
2355
- },
2356
- surface: isDetailDisplayMode() ? "explorer" : "compact",
2357
- title: text(valueFor(dataset, "title", payload.title || "Data Analytics chart")),
2358
- });
2359
- hideExternalLegend();
2360
- } catch (error) {
2361
- chartRoot.innerHTML = "";
2362
- renderEmpty(error && error.message ? `Choose compatible fields or reset the chart configuration. ${error.message}` : "Choose compatible fields or reset the chart configuration.");
2363
- hideExternalLegend();
2364
- }
2365
- }
2366
-
2367
- function renderTableFallback(dataset) {
2368
- destroyChartInstance();
2369
- const chartRoot = document.getElementById("chart");
2370
- const controls = document.getElementById("controls");
2371
- const legendShell = document.getElementById("legend-shell");
2372
- const dataPreview = document.getElementById("data-preview");
2373
- controls.hidden = true;
2374
- if (legendShell) legendShell.hidden = true;
2375
- if (dataPreview) {
2376
- destroyDataTable(dataPreview);
2377
- dataPreview.innerHTML = "";
2378
- }
2379
- destroyDataTable(chartRoot);
2380
- chartRoot.innerHTML = "";
2381
-
2382
- const resultTable = resultTableFor(dataset);
2383
- const rows = resultTable ? chartRows(dataset) : [];
2384
- if (!rows.length) {
2385
- renderEmpty("No table rows to render.", "No table data");
2386
- return;
2387
- }
2388
-
2389
- const maxRows = Math.max(1, Number(valueFor(dataset, "max_rows", payload.max_rows)) || 50);
2390
- const columns = tableColumns(dataset, rows);
2391
- renderDataTable(chartRoot, {
2392
- columns,
2393
- emptyLabel: "No table rows to render.",
2394
- maxRows,
2395
- pageSize: isDetailDisplayMode() ? 12 : 8,
2396
- rows,
2397
- });
2398
- }
2399
-
2400
- function renderLegend(groupNames) {
2401
- const root = document.getElementById("legend");
2402
- const legendShell = document.getElementById("legend-shell");
2403
- root.innerHTML = "";
2404
- if (legendShell) {
2405
- const empty = groupNames.length <= 1;
2406
- legendShell.hidden = empty && !isDetailDisplayMode();
2407
- legendShell.classList.toggle("is-empty", empty);
2408
- legendShell.setAttribute("aria-hidden", String(empty));
2409
- }
2410
- if (!groupNames.length || groupNames.length <= 1) return;
2411
- groupNames.forEach((name, index) => {
2412
- const item = document.createElement("li");
2413
- item.className = "legend-item";
2414
- item.style.color = colors[index % colors.length];
2415
- const button = document.createElement("button");
2416
- button.type = "button";
2417
- button.className = "legend-button";
2418
- button.setAttribute("aria-pressed", String(visibleSeries.has(name)));
2419
- button.addEventListener("click", () => {
2420
- if (visibleSeries.has(name)) {
2421
- if (visibleSeries.size === 1) return;
2422
- visibleSeries.delete(name);
2423
- }
2424
- else visibleSeries.add(name);
2425
- visibleSeries = orderedVisibleSeries(groupNames, visibleSeries);
2426
- renderChart();
2427
- });
2428
- const swatch = document.createElement("span");
2429
- swatch.className = "swatch";
2430
- const label = document.createElement("span");
2431
- label.textContent = name;
2432
- button.append(swatch, label);
2433
- item.appendChild(button);
2434
- root.appendChild(item);
2435
- });
2436
- }
2437
-
2438
- function chartTypeLabel(type) {
2439
- for (const section of chartPickerSections) {
2440
- for (const option of section.options) {
2441
- if (option.type === type) return option.label;
2442
- }
2443
- }
2444
- return text(type).replaceAll("_", " ");
2445
- }
2446
-
2447
- function activeDataFilterSummary(dataset = activeDataset()) {
2448
- const active = dataFilterSpecs(dataset)
2449
- .map((spec) => {
2450
- const value = selectedDataFilterValue(dataset, spec.key);
2451
- return value === "all" ? null : `${spec.label}: ${value}`;
2452
- })
2453
- .filter(Boolean);
2454
- return active.length ? active.join(", ") : "None";
2455
- }
2456
-
2457
- function promptSourceLines(dataset = activeDataset()) {
2458
- const sourceQuery = sourceQueryFor(dataset) || {};
2459
- const resultTable = resultTableFor(dataset) || {};
2460
- const lines = [];
2461
- if (sourceQuery.label || sourceQuery.id) lines.push(`Source query: ${sourceQuery.label || sourceQuery.id}`);
2462
- if (sourceQuery.engine) lines.push(`Source: ${sourceQuery.engine}`);
2463
- if (sourceQuery.executed_at) lines.push(`Executed at: ${sourceQuery.executed_at}`);
2464
- if (sourceQuery.description) lines.push(`Description: ${sourceQuery.description}`);
2465
- if (Number.isFinite(Number(resultTable.row_count))) {
2466
- lines.push(`Rows: ${formatNumber(Number(resultTable.row_count), 0)}${resultTable.truncated ? " sampled" : ""}`);
2467
- }
2468
- if (sourceQuery.sql) lines.push(`Query:\n${sourceQuery.sql}`);
2469
- return lines.length ? lines.join("\n") : "Source query: not declared in the widget payload.";
2470
- }
2471
-
2472
- function promptContext(dataset = activeDataset()) {
2473
- const title = text(valueFor(dataset, "title", payload.title || "Data Analytics chart"));
2474
- return [
2475
- `Widget URL: ${window.location.href}`,
2476
- `Chart title: ${title}`,
2477
- `Chart type: ${chartTypeLabel(activeVisualizationType)}`,
2478
- `X field: ${fieldLabel(dataset, chartConfig.xField, chartConfig.xField)}`,
2479
- `Y field: ${fieldLabel(dataset, chartConfig.yField, chartConfig.yField)}`,
2480
- `Grouping field: ${chartConfig.colorField ? fieldLabel(dataset, chartConfig.colorField, chartConfig.colorField) : "None"}`,
2481
- `Filters: ${activeDataFilterSummary(dataset)}`,
2482
- promptSourceLines(dataset),
2483
- ].join("\n");
2484
- }
2485
-
2486
- function refreshPrompt(dataset = activeDataset()) {
2487
- return `Refresh this inline chart using the same declared query and the latest available time frame.
2488
-
2489
- Re-run the existing source, update the widget data, preserve the current chart structure unless the latest data makes it invalid, and verify the refreshed chart in the inline and detail views.
2490
-
2491
- ${promptContext(dataset)}`;
2492
- }
2493
-
2494
- function exportPrompt(target, dataset = activeDataset()) {
2495
- const context = promptContext(dataset);
2496
- if (target === "html") {
2497
- return `Export this inline chart detail view as a static, portable HTML artifact. Include the chart title, chart, table preview, active filters, query/source details, and caveats. Omit the interactive top bar and app-only controls from the exported artifact. Verify the HTML before delivery.
2498
-
2499
- ${context}`;
2500
- }
2501
- if (target === "document") {
2502
- return `Create a polished document artifact from this inline chart. Preserve the chart, table preview, active filters, query/source details, and caveats, then verify the document.
2503
-
2504
- ${context}`;
2505
- }
2506
- if (target === "slides") {
2507
- return `Create an executive-ready slide artifact from this inline chart. Preserve the core takeaway, chart, active filters, table preview, and source details, then verify the deck.
2508
-
2509
- ${context}`;
2510
- }
2511
- return `Use the existing data-analytics:report-to-pdf workflow to create a PDF artifact from this inline chart detail view. Use the static HTML/export path when available, preserve the chart title, chart, table preview, active filters, caveats, and source details, omit the interactive top bar, share menus, edit controls, and app-only controls, then verify the PDF before delivery.
2512
-
2513
- ${context}`;
2514
- }
2515
-
2516
- function codexPromptUrl(prompt) {
2517
- const url = new URL("codex://new");
2518
- url.searchParams.set("prompt", prompt);
2519
- url.searchParams.set("originUrl", window.location.href);
2520
- return url.toString();
2521
- }
2522
-
2523
- function sendCodexPromptToHost(prompt) {
2524
- const hostApi = window.openai || {};
2525
- const payload = {
2526
- originUrl: window.location.href,
2527
- prompt,
2528
- };
2529
- const launchers = [hostApi.sendFollowUpMessage, hostApi.openCodexPrompt];
2530
- for (const launch of launchers) {
2531
- if (typeof launch !== "function") continue;
2532
- try {
2533
- Promise.resolve(launch.call(hostApi, payload)).catch(() => {});
2534
- return true;
2535
- } catch (error) {
2536
- // Try the next host launcher if one exists.
2537
- }
2538
- }
2539
- return false;
2540
- }
2541
-
2542
- async function copyTextToClipboard(value) {
2543
- if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") return false;
2544
- await navigator.clipboard.writeText(value);
2545
- return true;
2546
- }
2547
-
2548
- function isLocalPreviewHost() {
2549
- return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
2550
- }
2551
-
2552
- function launchCodexDeepLink(url) {
2553
- if (isLocalPreviewHost()) return false;
2554
- let linkClicked = false;
2555
- try {
2556
- const link = document.createElement("a");
2557
- link.href = url;
2558
- link.target = "_top";
2559
- link.rel = "noopener noreferrer";
2560
- link.style.display = "none";
2561
- document.body.appendChild(link);
2562
- link.click();
2563
- link.remove();
2564
- linkClicked = true;
2565
- } catch (error) {
2566
- // Continue to the top-level location path below when available.
2567
- }
2568
- try {
2569
- if (window.top === window) {
2570
- window.location.assign(url);
2571
- return true;
2572
- }
2573
- } catch (fallbackError) {
2574
- // Ignore navigation failures; the copied prompt is the fallback.
2575
- }
2576
- return linkClicked;
2577
- }
2578
-
2579
- function openCodexPrompt(prompt) {
2580
- const url = codexPromptUrl(prompt);
2581
- void copyTextToClipboard(prompt).catch(() => {});
2582
- const sentToHost = sendCodexPromptToHost(prompt);
2583
- if (!sentToHost) launchCodexDeepLink(url);
2584
- }
2585
-
2586
- function removeMenuSurface(menu, { immediate = false } = {}) {
2587
- if (!menu) return;
2588
- if (immediate) {
2589
- menu.remove();
2590
- return;
2591
- }
2592
- menu.classList.remove("opening");
2593
- menu.classList.add("closing");
2594
- window.setTimeout(() => menu.remove(), 100);
2595
- }
2596
-
2597
- function closeShareMenu(options = {}) {
2598
- const menu = document.getElementById("detail-share-menu-list");
2599
- const button = document.getElementById("detail-share-button");
2600
- removeMenuSurface(menu, options);
2601
- if (button) button.setAttribute("aria-expanded", "false");
2602
- shareMenuOpen = false;
2603
- }
2604
-
2605
- function shareMenuItem(label, iconName, onClick) {
2606
- const button = document.createElement("button");
2607
- button.type = "button";
2608
- button.className = "export-menu-item";
2609
- button.setAttribute("role", "menuitem");
2610
- button.append(lucideIcon(iconName, { width: "16", height: "16" }), document.createTextNode(label));
2611
- button.addEventListener("click", (event) => {
2612
- event.stopPropagation();
2613
- closeShareMenu();
2614
- onClick();
2615
- });
2616
- return button;
2617
- }
2618
-
2619
- function openShareMenu() {
2620
- const root = document.getElementById("detail-share-menu");
2621
- const button = document.getElementById("detail-share-button");
2622
- if (!root || !button) return;
2623
- closeFieldMenus();
2624
- closeChartPicker();
2625
- closeDataFilterMenus();
2626
- closeShareMenu({ immediate: true });
2627
- shareMenuOpen = true;
2628
- button.setAttribute("aria-expanded", "true");
2629
- const menu = document.createElement("div");
2630
- menu.id = "detail-share-menu-list";
2631
- menu.className = "export-menu-list menu-surface opening";
2632
- menu.setAttribute("role", "menu");
2633
- menu.append(
2634
- shareMenuItem("Create HTML file", "fileText", () => openCodexPrompt(exportPrompt("html"))),
2635
- shareMenuItem("Create PDF", "fileDown", () => openCodexPrompt(exportPrompt("pdf"))),
2636
- shareMenuItem("Create Google Doc", "fileText", () => openCodexPrompt(exportPrompt("document"))),
2637
- shareMenuItem("Create Google Slides", "presentation", () => openCodexPrompt(exportPrompt("slides"))),
2638
- );
2639
- menu.addEventListener("click", (event) => event.stopPropagation());
2640
- root.appendChild(menu);
2641
- }
2642
-
2643
- function setupDetailRefreshButton() {
2644
- const button = document.getElementById("detail-refresh-button");
2645
- if (!button) return;
2646
- button.addEventListener("click", (event) => {
2647
- event.stopPropagation();
2648
- closeShareMenu();
2649
- openCodexPrompt(refreshPrompt());
2650
- });
2651
- }
2652
-
2653
- function setupDetailShareMenu() {
2654
- const button = document.getElementById("detail-share-button");
2655
- if (!button) return;
2656
- const label = document.createElement("span");
2657
- label.textContent = "Export";
2658
- button.replaceChildren(label, caretIcon());
2659
- button.addEventListener("click", (event) => {
2660
- event.stopPropagation();
2661
- if (shareMenuOpen) closeShareMenu();
2662
- else openShareMenu();
2663
- });
2664
- }
2665
-
2666
- function chartPreviewSvg(kind) {
2667
- const stroke = "currentColor";
2668
- const fill = "color-mix(in srgb, currentColor 28%, transparent)";
2669
- const fillStrong = "color-mix(in srgb, currentColor 48%, transparent)";
2670
- const grid = `<path d="M8 8v48h80" fill="none" stroke="${stroke}" stroke-width="3" stroke-linecap="round" opacity=".44"/>`;
2671
- const frame = (body) => `<svg viewBox="0 0 96 64" aria-hidden="true" fill="none">${grid}${body}</svg>`;
2672
- const rect = (attrs) => `<rect ${attrs} rx="2" fill="${fill}" stroke="${stroke}" stroke-width="2.5"/>`;
2673
- const segment = (attrs, strong = false) => `<rect ${attrs} rx="1.5" fill="${strong ? fillStrong : fill}" stroke="${stroke}" stroke-width="2.2"/>`;
2674
- if (kind === "bar") {
2675
- return frame(
2676
- `${rect('x="18" y="34" width="7" height="22"')}${rect('x="28" y="24" width="7" height="32"')}` +
2677
- `${rect('x="47" y="42" width="7" height="14"')}${rect('x="57" y="18" width="7" height="38"')}` +
2678
- `${rect('x="76" y="29" width="7" height="27"')}`,
2679
- );
2680
- }
2681
- if (kind === "stacked-bar" || kind === "stacked-bar-100") {
2682
- return frame(
2683
- `${segment('x="20" y="38" width="12" height="18"')}${segment('x="20" y="24" width="12" height="14"', true)}` +
2684
- `${segment('x="45" y="32" width="12" height="24"')}${segment('x="45" y="15" width="12" height="17"', true)}` +
2685
- `${segment('x="70" y="43" width="12" height="13"')}${segment('x="70" y="28" width="12" height="15"', true)}`,
2686
- );
2687
- }
2688
- if (kind === "horizontal-bar") {
2689
- return frame(
2690
- `${rect('x="18" y="16" width="22" height="6"')}${rect('x="18" y="25" width="36" height="6"')}` +
2691
- `${rect('x="18" y="40" width="28" height="6"')}${rect('x="18" y="49" width="53" height="6"')}`,
2692
- );
2693
- }
2694
- if (kind === "horizontal-stacked-bar" || kind === "horizontal-stacked-bar-100") {
2695
- return frame(
2696
- `${segment('x="18" y="18" width="30" height="9"')}${segment('x="48" y="18" width="22" height="9"', true)}` +
2697
- `${segment('x="18" y="39" width="22" height="9"')}${segment('x="40" y="39" width="38" height="9"', true)}`,
2698
- );
2699
- }
2700
- if (kind === "line" || kind === "sparkline") {
2701
- return frame(
2702
- `<path d="M18 45 34 29l14 9 17-21 17 15" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>` +
2703
- `<circle cx="18" cy="45" r="3" fill="${fillStrong}" stroke="${stroke}" stroke-width="2"/><circle cx="34" cy="29" r="3" fill="${fillStrong}" stroke="${stroke}" stroke-width="2"/><circle cx="65" cy="17" r="3" fill="${fillStrong}" stroke="${stroke}" stroke-width="2"/><circle cx="82" cy="32" r="3" fill="${fillStrong}" stroke="${stroke}" stroke-width="2"/>`,
2704
- );
2705
- }
2706
- if (kind === "area" || kind === "stacked-area") {
2707
- return frame(
2708
- `<path d="M18 47 34 35l14 8 18-23 17 10v26H18Z" fill="${fill}"/>` +
2709
- `<path d="M18 47 34 35l14 8 18-23 17 10" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>`,
2710
- );
2711
- }
2712
- if (kind === "histogram") {
2713
- return frame(
2714
- [16, 27, 38, 49, 60, 71].map((x, i) => {
2715
- const heights = [12, 22, 35, 29, 18, 9];
2716
- return segment(`x="${x}" y="${56 - heights[i]}" width="9" height="${heights[i]}"`, i === 2 || i === 3);
2717
- }).join(""),
2718
- );
2719
- }
2720
- if (kind === "scatter") {
2721
- return frame(
2722
- [[20, 45], [31, 34], [42, 40], [51, 25], [63, 31], [74, 18], [82, 28]]
2723
- .map(([cx, cy]) => `<circle cx="${cx}" cy="${cy}" r="4" fill="${fillStrong}" stroke="${stroke}" stroke-width="2.5"/>`)
2724
- .join(""),
2725
- );
2726
- }
2727
- if (kind === "heatmap") {
2728
- return frame(
2729
- [0, 1, 2].map((row) =>
2730
- [0, 1, 2, 3].map((col) => {
2731
- const strong = (row + col) % 3 === 0;
2732
- const opacity = [".18", ".3", ".46", ".62"][(row * 2 + col) % 4];
2733
- return `<rect x="${18 + col * 16}" y="${16 + row * 12}" width="13" height="10" rx="1.5" fill="${strong ? fillStrong : fill}" opacity="${opacity}" stroke="${stroke}" stroke-width="1.8"/>`;
2734
- }).join("")
2735
- ).join(""),
2736
- );
2737
- }
2738
- if (kind === "pie") {
2739
- return `<svg viewBox="0 0 96 64" aria-hidden="true" fill="none"><path d="M48 8a24 24 0 1 1-20.8 36" fill="${fill}" stroke="${stroke}" stroke-width="3" stroke-linejoin="round"/><path d="M48 8v24l20.8 12A24 24 0 0 0 48 8Z" fill="${fillStrong}" stroke="${stroke}" stroke-width="3" stroke-linejoin="round"/><path d="M48 32 27.2 44" stroke="${stroke}" stroke-width="3" stroke-linecap="round"/></svg>`;
2740
- }
2741
- if (kind === "funnel") {
2742
- return `<svg viewBox="0 0 96 64" aria-hidden="true" fill="none"><path d="M18 10h60L68 24H28Z" fill="${fillStrong}" stroke="${stroke}" stroke-width="3"/><path d="M30 28h36L58 42H38Z" fill="${fill}" stroke="${stroke}" stroke-width="3"/><path d="M40 46h16l-4 10h-8Z" fill="${fill}" stroke="${stroke}" stroke-width="3"/></svg>`;
2743
- }
2744
- if (kind === "waterfall") {
2745
- return frame(
2746
- `${segment('x="18" y="40" width="12" height="16"')}${segment('x="38" y="26" width="12" height="30"', true)}${segment('x="58" y="34" width="12" height="22"')}${segment('x="78" y="20" width="8" height="36"', true)}`,
2747
- );
2748
- }
2749
- if (kind === "leaderboard") {
2750
- return frame(
2751
- `${segment('x="18" y="17" width="58" height="8"', true)}${segment('x="18" y="31" width="44" height="8"')}${segment('x="18" y="45" width="30" height="8"')}`,
2752
- );
2753
- }
2754
- if (kind === "box-plot") {
2755
- return frame(
2756
- `<path d="M22 24h52M22 42h42" stroke="${stroke}" stroke-width="3" stroke-linecap="round"/><rect x="34" y="18" width="24" height="12" rx="2" fill="${fill}" stroke="${stroke}" stroke-width="2.5"/><rect x="30" y="36" width="22" height="12" rx="2" fill="${fillStrong}" stroke="${stroke}" stroke-width="2.5"/>`,
2757
- );
2758
- }
2759
- return frame(rect('x="20" y="22" width="52" height="28"'));
2760
- }
2761
-
2762
- function caretIcon(extraClass = "") {
2763
- const caret = document.createElement("span");
2764
- caret.className = `dropdown-caret${extraClass ? ` ${extraClass}` : ""}`;
2765
- caret.setAttribute("aria-hidden", "true");
2766
- caret.appendChild(lucideIcon("chevronDown"));
2767
- return caret;
2768
- }
2769
-
2770
- function closeChartPicker() {
2771
- const popover = document.getElementById("chart-picker-popover");
2772
- const button = document.getElementById("chart-picker-button");
2773
- if (popover) {
2774
- popover.hidden = true;
2775
- popover.classList.remove("opening");
2776
- }
2777
- if (button) button.setAttribute("aria-expanded", "false");
2778
- }
2779
-
2780
- function renderChartPicker(root) {
2781
- const picker = document.createElement("div");
2782
- picker.className = "chart-picker";
2783
- const button = document.createElement("button");
2784
- button.id = "chart-picker-button";
2785
- button.className = "chart-picker-button";
2786
- button.type = "button";
2787
- button.setAttribute("aria-haspopup", "true");
2788
- button.setAttribute("aria-expanded", "false");
2789
- button.setAttribute("aria-label", `Chart type ${chartTypeLabel(activeVisualizationType)}`);
2790
- const buttonLabel = document.createElement("span");
2791
- buttonLabel.textContent =
2792
- isDetailDisplayMode()
2793
- ? "Chart type"
2794
- : chartTypeLabel(activeVisualizationType);
2795
- button.append(buttonLabel, caretIcon());
2796
-
2797
- const popover = document.createElement("div");
2798
- popover.id = "chart-picker-popover";
2799
- popover.className = "chart-picker-popover menu-surface";
2800
- popover.hidden = true;
2801
- for (const section of compatibleChartPickerSections(activeDataset())) {
2802
- const sectionEl = document.createElement("section");
2803
- sectionEl.className = "chart-picker-section";
2804
- if (section.title) {
2805
- const heading = document.createElement("h2");
2806
- heading.className = "chart-picker-heading";
2807
- heading.textContent = section.title;
2808
- sectionEl.appendChild(heading);
2809
- }
2810
- const grid = document.createElement("div");
2811
- grid.className = "chart-option-grid";
2812
- for (const option of section.options) {
2813
- const optionButton = document.createElement("button");
2814
- optionButton.type = "button";
2815
- optionButton.className = "chart-option";
2816
- optionButton.setAttribute("aria-pressed", String(option.type === activeVisualizationType));
2817
- const preview = document.createElement("span");
2818
- preview.className = "chart-option-preview";
2819
- preview.innerHTML = chartPreviewSvg(option.preview);
2820
- const label = document.createElement("span");
2821
- label.className = "chart-option-label";
2822
- label.textContent = option.label;
2823
- const check = document.createElement("span");
2824
- check.className = "chart-option-check";
2825
- check.appendChild(lucideIcon("check"));
2826
- optionButton.append(preview, label, check);
2827
- optionButton.addEventListener("click", () => {
2828
- activeVisualizationType = option.type;
2829
- if (viewMode === "table") setViewMode("both");
2830
- closeChartPicker();
2831
- render();
2832
- notifyChartSpecChange();
2833
- });
2834
- grid.appendChild(optionButton);
2835
- }
2836
- sectionEl.appendChild(grid);
2837
- popover.appendChild(sectionEl);
2838
- }
2839
-
2840
- button.addEventListener("click", (event) => {
2841
- event.stopPropagation();
2842
- closeFieldMenus();
2843
- closeDataFilterMenus();
2844
- closeShareMenu();
2845
- const nextHidden = !popover.hidden;
2846
- popover.hidden = nextHidden;
2847
- popover.classList.toggle("opening", !nextHidden);
2848
- button.setAttribute("aria-expanded", String(!nextHidden));
2849
- });
2850
- picker.addEventListener("click", (event) => {
2851
- event.stopPropagation();
2852
- });
2853
- picker.append(button, popover);
2854
- root.appendChild(picker);
2855
- }
2856
-
2857
- function closeFieldMenus(options = {}) {
2858
- document.querySelectorAll(".field-menu").forEach((menu) => removeMenuSurface(menu, options));
2859
- document.querySelectorAll(".field-pill[aria-expanded='true']").forEach((button) => button.setAttribute("aria-expanded", "false"));
2860
- }
2861
-
2862
- function closeDataFilterMenus(options = {}) {
2863
- document.querySelectorAll(".filter-menu-list").forEach((menu) => removeMenuSurface(menu, options));
2864
- document.querySelectorAll(".filter-menu-button[aria-expanded='true']").forEach((button) => button.setAttribute("aria-expanded", "false"));
2865
- }
2866
-
2867
- function setDataFilter(dataset, fieldKey, value) {
2868
- const key = dataFilterKey(dataset, fieldKey);
2869
- if (!value || value === "all") delete selectedDataFilters[key];
2870
- else selectedDataFilters[key] = value;
2871
- pruneDataFilters(dataset);
2872
- closeDataFilterMenus();
2873
- resetVisibleSeries();
2874
- render();
2875
- }
2876
-
2877
- function filterOptionLabel(value) {
2878
- return value === "all" ? "All" : value;
2879
- }
2880
-
2881
- function activeDataFilterCount(dataset = activeDataset()) {
2882
- return dataFilterSpecs(dataset).filter((spec) => selectedDataFilterValue(dataset, spec.key) !== "all").length;
2883
- }
2884
-
2885
- function positionDataFilterMenu(anchor, menu) {
2886
- const boundsLeft = 12;
2887
- const boundsRight = window.innerWidth - 12;
2888
- menu.style.left = "0px";
2889
- menu.style.right = "auto";
2890
- const rect = menu.getBoundingClientRect();
2891
- let leftOffset = 0;
2892
- if (rect.right > boundsRight) leftOffset -= rect.right - boundsRight;
2893
- if (rect.left + leftOffset < boundsLeft) leftOffset += boundsLeft - (rect.left + leftOffset);
2894
- menu.style.left = `${Math.round(leftOffset)}px`;
2895
- }
2896
-
2897
- function openDataFilterMenu(anchor, spec) {
2898
- const existing = anchor.parentElement.querySelector(".filter-menu-list");
2899
- closeFieldMenus();
2900
- closeChartPicker();
2901
- if (existing) {
2902
- closeDataFilterMenus();
2903
- closeShareMenu();
2904
- return;
2905
- }
2906
- closeDataFilterMenus({ immediate: true });
2907
- closeShareMenu({ immediate: true });
2908
- anchor.setAttribute("aria-expanded", "true");
2909
- const menu = document.createElement("div");
2910
- menu.className = "filter-menu-list menu-surface opening";
2911
- menu.setAttribute("role", "menu");
2912
- const current = selectedDataFilterValue(activeDataset(), spec.key);
2913
- const options = ["all", ...filterOptionsForSpec(activeDataset(), spec)];
2914
- for (const option of options) {
2915
- const item = document.createElement("button");
2916
- const selected = option === current;
2917
- item.type = "button";
2918
- item.className = "filter-menu-item";
2919
- item.setAttribute("role", "menuitemradio");
2920
- item.setAttribute("aria-checked", String(selected));
2921
- const label = document.createElement("span");
2922
- label.textContent = filterOptionLabel(option);
2923
- item.appendChild(label);
2924
- if (selected) item.appendChild(lucideIcon("check", { width: "14", height: "14" }));
2925
- item.addEventListener("click", (event) => {
2926
- event.stopPropagation();
2927
- setDataFilter(activeDataset(), spec.key, option);
2928
- });
2929
- menu.appendChild(item);
2930
- }
2931
- menu.addEventListener("click", (event) => event.stopPropagation());
2932
- anchor.parentElement.appendChild(menu);
2933
- positionDataFilterMenu(anchor, menu);
2934
- }
2935
-
2936
- function dataFilterChip(spec) {
2937
- const wrapper = document.createElement("div");
2938
- wrapper.className = "filter-menu";
2939
- const button = document.createElement("button");
2940
- button.type = "button";
2941
- button.className = "filter-menu-button";
2942
- button.setAttribute("aria-haspopup", "menu");
2943
- button.setAttribute("aria-expanded", "false");
2944
- const label = document.createElement("span");
2945
- label.className = "filter-menu-label";
2946
- label.textContent = spec.label;
2947
- const value = document.createElement("span");
2948
- value.className = "filter-menu-value";
2949
- const selectedValue = selectedDataFilterValue(activeDataset(), spec.key);
2950
- if (selectedValue !== "all") {
2951
- label.textContent = `${spec.label}:`;
2952
- value.textContent = filterOptionLabel(selectedValue);
2953
- button.dataset.hasFilter = "true";
2954
- button.append(label, caretIcon());
2955
- } else {
2956
- button.append(label, caretIcon());
2957
- }
2958
- button.addEventListener("click", (event) => {
2959
- event.stopPropagation();
2960
- openDataFilterMenu(button, spec);
2961
- });
2962
- wrapper.appendChild(button);
2963
- return wrapper;
2964
- }
2965
-
2966
- function dataFilterOptionsPanel(spec) {
2967
- const panel = document.createElement("div");
2968
- panel.className = "menu-submenu-panel menu-surface";
2969
- panel.setAttribute("role", "menu");
2970
- const current = selectedDataFilterValue(activeDataset(), spec.key);
2971
- const options = ["all", ...filterOptionsForSpec(activeDataset(), spec)];
2972
- for (const option of options) {
2973
- const selected = option === current;
2974
- const row = menuRow(filterOptionLabel(option), selected ? lucideIcon("check", { width: "14", height: "14" }) : "", () => {
2975
- setDataFilter(activeDataset(), spec.key, option);
2976
- }, { active: selected });
2977
- row.setAttribute("role", "menuitemradio");
2978
- row.setAttribute("aria-checked", String(selected));
2979
- panel.appendChild(row);
2980
- }
2981
- return panel;
2982
- }
2983
-
2984
- function openFiltersMenu(anchor, specs) {
2985
- const existing = anchor.parentElement.querySelector(".filter-menu-list");
2986
- closeFieldMenus();
2987
- closeChartPicker();
2988
- if (existing) {
2989
- closeDataFilterMenus();
2990
- closeShareMenu();
2991
- return;
2992
- }
2993
- closeDataFilterMenus({ immediate: true });
2994
- closeShareMenu({ immediate: true });
2995
- anchor.setAttribute("aria-expanded", "true");
2996
- const menu = document.createElement("div");
2997
- menu.className = "filter-menu-list menu-surface opening";
2998
- menu.setAttribute("role", "menu");
2999
- for (const spec of specs) {
3000
- const selected = selectedDataFilterValue(activeDataset(), spec.key);
3001
- menu.appendChild(
3002
- menuSubmenu(
3003
- spec.label,
3004
- selected !== "all" ? filterOptionLabel(selected) : "",
3005
- dataFilterOptionsPanel(spec),
3006
- { active: selected !== "all" },
3007
- ),
3008
- );
3009
- }
3010
- const count = activeDataFilterCount(activeDataset());
3011
- if (count) {
3012
- menu.appendChild(document.createElement("div")).className = "menu-divider";
3013
- menu.appendChild(menuRow("Reset filters", "", () => {
3014
- selectedDataFilters = {};
3015
- closeDataFilterMenus();
3016
- resetVisibleSeries();
3017
- render();
3018
- }));
3019
- }
3020
- menu.addEventListener("click", (event) => event.stopPropagation());
3021
- anchor.parentElement.appendChild(menu);
3022
- positionDataFilterMenu(anchor, menu);
3023
- }
3024
-
3025
- function filtersMenuChip(specs) {
3026
- const wrapper = document.createElement("div");
3027
- wrapper.className = "filter-menu";
3028
- const button = document.createElement("button");
3029
- button.type = "button";
3030
- button.className = "filter-menu-button";
3031
- button.setAttribute("aria-haspopup", "menu");
3032
- button.setAttribute("aria-expanded", "false");
3033
- const label = document.createElement("span");
3034
- label.className = "filter-menu-label";
3035
- label.textContent = "Filters";
3036
- const count = activeDataFilterCount(activeDataset());
3037
- const value = document.createElement("span");
3038
- value.className = "filter-menu-value";
3039
- value.textContent = activeDataFilterSummary(activeDataset());
3040
- value.title = value.textContent;
3041
- button.setAttribute("aria-label", `Filters ${value.textContent}`);
3042
- if (count) button.dataset.hasFilter = "true";
3043
- button.append(label, caretIcon());
3044
- button.addEventListener("click", (event) => {
3045
- event.stopPropagation();
3046
- openFiltersMenu(button, specs);
3047
- });
3048
- wrapper.appendChild(button);
3049
- return wrapper;
3050
- }
3051
-
3052
- function menuRow(label, value, onClick, options = {}) {
3053
- const button = document.createElement("button");
3054
- button.type = "button";
3055
- button.className = `menu-row${options.active ? " is-active" : ""}${options.danger ? " is-danger" : ""}`;
3056
- const labelEl = document.createElement("span");
3057
- labelEl.textContent = label;
3058
- button.appendChild(labelEl);
3059
- if (value) {
3060
- const valueEl = document.createElement("span");
3061
- valueEl.className = "menu-row-value";
3062
- if (value instanceof Node) valueEl.appendChild(value);
3063
- else valueEl.textContent = value;
3064
- button.appendChild(valueEl);
3065
- }
3066
- button.addEventListener("click", (event) => {
3067
- event.stopPropagation();
3068
- onClick?.();
3069
- });
3070
- return button;
3071
- }
3072
-
3073
- function menuValue(value, iconName = "chevronRight") {
3074
- const wrapper = document.createElement("span");
3075
- wrapper.className = "menu-row-value-with-icon";
3076
- if (value) {
3077
- const valueEl = document.createElement("span");
3078
- valueEl.textContent = value;
3079
- wrapper.appendChild(valueEl);
3080
- }
3081
- wrapper.appendChild(lucideIcon(iconName));
3082
- return wrapper;
3083
- }
3084
-
3085
- function positionSubmenuPanel(wrapper) {
3086
- const panel = wrapper.querySelector(".menu-submenu-panel");
3087
- if (!panel) return;
3088
- panel.style.left = "calc(100% + 8px)";
3089
- panel.style.right = "auto";
3090
- panel.style.visibility = "hidden";
3091
- panel.style.display = "grid";
3092
- const rect = panel.getBoundingClientRect();
3093
- if (rect.right > window.innerWidth - 12) {
3094
- panel.style.left = "auto";
3095
- panel.style.right = "calc(100% + 8px)";
3096
- }
3097
- panel.style.display = "";
3098
- panel.style.visibility = "";
3099
- }
3100
-
3101
- function menuSubmenu(label, value, panel, options = {}) {
3102
- const wrapper = document.createElement("div");
3103
- wrapper.className = "menu-submenu";
3104
- const trigger = menuRow(label, menuValue(value), null, options);
3105
- trigger.classList.add("menu-submenu-trigger");
3106
- trigger.setAttribute("aria-haspopup", "menu");
3107
- trigger.setAttribute("aria-expanded", "false");
3108
- trigger.addEventListener("click", (event) => {
3109
- event.stopPropagation();
3110
- const open = !wrapper.classList.contains("is-open");
3111
- wrapper.parentElement?.querySelectorAll(".menu-submenu.is-open").forEach((item) => {
3112
- if (item === wrapper) return;
3113
- item.classList.remove("is-open");
3114
- item.querySelector(".menu-submenu-trigger")?.setAttribute("aria-expanded", "false");
3115
- });
3116
- wrapper.classList.toggle("is-open", open);
3117
- trigger.setAttribute("aria-expanded", String(open));
3118
- if (open) positionSubmenuPanel(wrapper);
3119
- });
3120
- wrapper.addEventListener("mouseenter", () => positionSubmenuPanel(wrapper));
3121
- wrapper.addEventListener("focusin", () => positionSubmenuPanel(wrapper));
3122
- wrapper.append(trigger, panel);
3123
- return wrapper;
3124
- }
3125
-
3126
- function menuHeader(label, value = "") {
3127
- const row = document.createElement("div");
3128
- row.className = "menu-heading";
3129
- const labelEl = document.createElement("span");
3130
- labelEl.textContent = label;
3131
- row.appendChild(labelEl);
3132
- if (value) {
3133
- const valueEl = document.createElement("span");
3134
- valueEl.textContent = value;
3135
- row.appendChild(valueEl);
3136
- }
3137
- return row;
3138
- }
3139
-
3140
- function setChartField(role, key) {
3141
- if (role === "x") chartConfig.xField = key;
3142
- if (role === "y") chartConfig.yField = key;
3143
- if (role === "size") chartConfig.sizeField = key;
3144
- if (role === "lineStyle") chartConfig.lineStyleField = key;
3145
- if (role === "label") chartConfig.labelField = key;
3146
- if (role === "color") {
3147
- chartConfig.colorField = key;
3148
- revealAllSeriesAfterGroupingChange = true;
3149
- }
3150
- if (role === "x" && !fieldLooksDateLike(activeDataset(), key)) chartConfig.timeUnit = "none";
3151
- if (
3152
- role === "y" &&
3153
- chartConfig.colorField === measureNamesField &&
3154
- compatibleMeasureNameFields(activeDataset()).length <= 1
3155
- ) {
3156
- chartConfig.colorField = "";
3157
- }
3158
- resetVisibleSeries();
3159
- closeFieldMenus();
3160
- render();
3161
- notifyChartSpecChange();
3162
- }
3163
-
3164
- function clearChartFields() {
3165
- selectedDataFilters = {};
3166
- chartConfig = { xField: "x", yField: "y", colorField: "series", lineStyleField: "", labelField: "", sizeField: "", timeUnit: "none", yAggregation: "sum" };
3167
- resetChartSettings(activeDataset());
3168
- resetChartConfig(activeDataset());
3169
- visibleSeries = {};
3170
- resetVisibleSeries();
3171
- closeFieldMenus();
3172
- closeDataFilterMenus();
3173
- closeChartPicker();
3174
- render();
3175
- notifyChartSpecReset();
3176
- }
3177
-
3178
- function fieldIconName(dataset, field) {
3179
- if (fieldLooksDateLike(dataset, field.key)) return "calendar";
3180
- if (field.numeric) return "hash";
3181
- return "type";
3182
- }
3183
-
3184
- function fieldGroupLabel(field) {
3185
- if (field.synthetic) return "Calculated";
3186
- return field.numeric ? "Measures" : "Dimensions";
3187
- }
3188
-
3189
- function fieldIsSelectedForRole(role, key) {
3190
- if (role === "x") return chartConfig.xField === key;
3191
- if (role === "y") return chartConfig.yField === key;
3192
- if (role === "size") return chartConfig.sizeField === key;
3193
- if (role === "label") return chartConfig.labelField === key;
3194
- if (role === "color") return chartConfig.colorField === key;
3195
- return false;
3196
- }
3197
-
3198
- function fieldMenuRow(dataset, role, field, options = {}) {
3199
- const row = menuRow("", "", () => setChartField(role, field.key), {
3200
- active: fieldIsSelectedForRole(role, field.key),
3201
- });
3202
- row.innerHTML = "";
3203
- const label = document.createElement("span");
3204
- label.className = "menu-field-label";
3205
- if (options.icons !== false) {
3206
- const icon = document.createElement("span");
3207
- icon.className = "menu-field-icon";
3208
- icon.appendChild(lucideIcon(fieldIconName(dataset, field), { width: "14", height: "14" }));
3209
- label.appendChild(icon);
3210
- }
3211
- const textEl = document.createElement("span");
3212
- textEl.textContent = field.label;
3213
- label.appendChild(textEl);
3214
- row.appendChild(label);
3215
- return row;
3216
- }
3217
-
3218
- function renderFieldRows(panel, role, options = {}) {
3219
- const dataset = activeDataset();
3220
- const fields = compatibleFieldsForRole(dataset, role);
3221
- const scroll = document.createElement("div");
3222
- scroll.className = "menu-scroll";
3223
- const showSearch = options.search !== false && fields.length > fieldMenuSearchThreshold;
3224
- if (showSearch) {
3225
- const search = document.createElement("input");
3226
- search.className = "menu-search";
3227
- search.type = "search";
3228
- search.placeholder = "Filter...";
3229
- panel.append(search);
3230
- search.addEventListener("input", () => paint(search.value));
3231
- }
3232
- panel.append(scroll);
3233
-
3234
- const paint = (query = "") => {
3235
- const filter = query.trim().toLowerCase();
3236
- scroll.innerHTML = "";
3237
- let lastGroup = "";
3238
- if (options.includeNone) {
3239
- const noneRow = menuRow("None", "", () => setChartField(role, ""), {
3240
- active: !fieldKeyForRole(role),
3241
- });
3242
- scroll.appendChild(noneRow);
3243
- }
3244
- for (const field of fields) {
3245
- if (filter && !field.label.toLowerCase().includes(filter) && !field.key.toLowerCase().includes(filter)) continue;
3246
- const group = fieldGroupLabel(field);
3247
- if (options.grouped !== false && group !== lastGroup) {
3248
- scroll.appendChild(menuHeader(group));
3249
- lastGroup = group;
3250
- }
3251
- scroll.appendChild(fieldMenuRow(dataset, role, field, options));
3252
- }
3253
- };
3254
- paint();
3255
- }
3256
-
3257
- function appendTimeGrainRows(panel, dataset) {
3258
- for (const option of timeUnitOptionsForDataset(dataset)) {
3259
- panel.appendChild(
3260
- menuRow(option, canonicalTimeUnit(option) === chartConfig.timeUnit ? lucideIcon("check", { width: "14", height: "14" }) : "", () => {
3261
- chartConfig.timeUnit = canonicalTimeUnit(option);
3262
- closeFieldMenus();
3263
- render();
3264
- notifyChartSpecChange();
3265
- }, { active: canonicalTimeUnit(option) === chartConfig.timeUnit }),
3266
- );
3267
- }
3268
- }
3269
-
3270
- function appendAggregationRows(panel) {
3271
- for (const option of aggregationOptions) {
3272
- panel.appendChild(
3273
- menuRow(option, canonicalAggregation(option) === chartConfig.yAggregation ? lucideIcon("check", { width: "14", height: "14" }) : "", () => {
3274
- chartConfig.yAggregation = canonicalAggregation(option);
3275
- closeFieldMenus();
3276
- render();
3277
- notifyChartSpecChange();
3278
- }, { active: canonicalAggregation(option) === chartConfig.yAggregation }),
3279
- );
3280
- }
3281
- }
3282
-
3283
- function positionFieldMenu(anchor, menu) {
3284
- const panel = document.querySelector(".app-main");
3285
- const panelRect = panel?.getBoundingClientRect();
3286
- const boundsLeft = Math.max(12, panelRect ? panelRect.left + 12 : 12);
3287
- const boundsRight = Math.min(window.innerWidth - 12, panelRect ? panelRect.right - 12 : window.innerWidth - 12);
3288
- menu.style.left = "0px";
3289
- menu.style.right = "auto";
3290
- menu.style.maxWidth = `${Math.max(180, Math.min(240, boundsRight - boundsLeft))}px`;
3291
-
3292
- const menuRect = menu.getBoundingClientRect();
3293
- let leftOffset = 0;
3294
- if (menuRect.right > boundsRight) {
3295
- leftOffset -= menuRect.right - boundsRight;
3296
- }
3297
- if (menuRect.left + leftOffset < boundsLeft) {
3298
- leftOffset += boundsLeft - (menuRect.left + leftOffset);
3299
- }
3300
- menu.style.left = `${Math.round(leftOffset)}px`;
3301
- }
3302
-
3303
- function openFieldMenu(anchor, spec) {
3304
- const role = spec.role;
3305
- const existing = anchor.parentElement.querySelector(".field-menu");
3306
- closeFieldMenus();
3307
- closeChartPicker();
3308
- closeDataFilterMenus();
3309
- closeShareMenu();
3310
- if (existing) return;
3311
- anchor.setAttribute("aria-expanded", "true");
3312
- const menu = document.createElement("div");
3313
- menu.className = "field-menu menu-surface opening";
3314
- menu.addEventListener("click", (event) => {
3315
- event.stopPropagation();
3316
- });
3317
- const panel = document.createElement("div");
3318
- panel.className = "field-menu-panel";
3319
- const dataset = activeDataset();
3320
-
3321
- if (role === "x") {
3322
- const supportsTime = spec.time !== false;
3323
- renderFieldRows(panel, role, { grouped: false, icons: false });
3324
- if (supportsTime && fieldLooksDateLike(dataset, chartConfig.xField)) {
3325
- panel.appendChild(document.createElement("div")).className = "menu-divider";
3326
- panel.appendChild(menuHeader("Time grain"));
3327
- appendTimeGrainRows(panel, dataset);
3328
- }
3329
- } else if (role === "y") {
3330
- const supportsAggregation = spec.aggregate !== false;
3331
- renderFieldRows(panel, role, { grouped: false, icons: false });
3332
- if (supportsAggregation) {
3333
- panel.appendChild(document.createElement("div")).className = "menu-divider";
3334
- panel.appendChild(menuHeader("Aggregation"));
3335
- appendAggregationRows(panel);
3336
- }
3337
- } else {
3338
- renderFieldRows(panel, role, {
3339
- grouped: false,
3340
- icons: false,
3341
- includeNone: !spec.required,
3342
- search: false,
3343
- });
3344
- }
3345
- menu.appendChild(panel);
3346
- anchor.parentElement.appendChild(menu);
3347
- positionFieldMenu(anchor, menu);
3348
- }
3349
-
3350
- function fieldPill(spec) {
3351
- const role = spec.role;
3352
- const dataset = activeDataset();
3353
- const detailChrome = isDetailDisplayMode();
3354
- const well = document.createElement("div");
3355
- well.className = "field-well";
3356
- const button = document.createElement("button");
3357
- button.type = "button";
3358
- button.className = "field-pill";
3359
- button.setAttribute("aria-haspopup", "menu");
3360
- button.setAttribute("aria-expanded", "false");
3361
- const roleEl = document.createElement("span");
3362
- roleEl.className = "field-pill-role";
3363
- roleEl.textContent = detailChrome ? detailControlLabel(spec) : spec.label;
3364
- button.appendChild(roleEl);
3365
- const field = document.createElement("span");
3366
- field.className = "field-pill-field";
3367
- field.textContent =
3368
- role === "x" ? fieldLabel(dataset, chartConfig.xField, spec.fallback) :
3369
- role === "y" ? fieldLabel(dataset, chartConfig.yField, spec.fallback) :
3370
- role === "size" ? chartConfig.sizeField ? fieldLabel(dataset, chartConfig.sizeField, spec.fallback) : spec.fallback :
3371
- role === "label" ? chartConfig.labelField ? fieldLabel(dataset, chartConfig.labelField, spec.fallback) : spec.fallback :
3372
- role === "time" ? timeUnitLabel(chartConfig.timeUnit) :
3373
- chartConfig.colorField ? fieldLabel(dataset, chartConfig.colorField, spec.fallback) : spec.fallback;
3374
- button.setAttribute("aria-label", `${roleEl.textContent}: ${field.textContent}`);
3375
- if (!detailChrome) button.appendChild(field);
3376
- const modifier = document.createElement("span");
3377
- modifier.className = "field-pill-modifier";
3378
- if (role === "x" && spec.time !== false && fieldLooksDateLike(dataset, chartConfig.xField)) {
3379
- modifier.textContent = timeUnitLabel(chartConfig.timeUnit === "none" ? "" : chartConfig.timeUnit);
3380
- }
3381
- if (role === "y" && spec.aggregate !== false) modifier.textContent = aggregationLabel(chartConfig.yAggregation);
3382
- if (modifier.textContent && !detailChrome) button.appendChild(modifier);
3383
- button.appendChild(caretIcon("field-pill-caret"));
3384
- button.addEventListener("click", (event) => {
3385
- event.stopPropagation();
3386
- openFieldMenu(button, spec);
3387
- });
3388
- well.appendChild(button);
3389
- return well;
3390
- }
3391
-
3392
- function barSettingsHaveSeries(dataset = activeDataset()) {
3393
- if (!chartConfig.colorField) return false;
3394
- return projectedSeriesCount(projectedDataForType(dataset, "bar")) > 1;
3395
- }
3396
-
3397
- function openSettingMenu(anchor, label, value, options, onChange) {
3398
- const existing = anchor.parentElement.querySelector(".field-menu");
3399
- closeFieldMenus();
3400
- closeChartPicker();
3401
- closeDataFilterMenus();
3402
- closeShareMenu();
3403
- if (existing) return;
3404
- anchor.setAttribute("aria-expanded", "true");
3405
- const menu = document.createElement("div");
3406
- menu.className = "field-menu menu-surface opening";
3407
- menu.addEventListener("click", (event) => {
3408
- event.stopPropagation();
3409
- });
3410
- const panel = document.createElement("div");
3411
- panel.className = "field-menu-panel";
3412
- for (const option of options) {
3413
- const selected = option.value === value;
3414
- const row = menuRow(option.label, selected ? lucideIcon("check", { width: "14", height: "14" }) : "", () => {
3415
- onChange(option.value);
3416
- render();
3417
- closeFieldMenus({ immediate: true });
3418
- notifyChartSpecChange();
3419
- }, { active: selected });
3420
- row.setAttribute("role", "menuitemradio");
3421
- row.setAttribute("aria-checked", String(selected));
3422
- panel.appendChild(row);
3423
- }
3424
- menu.appendChild(panel);
3425
- anchor.parentElement.appendChild(menu);
3426
- positionFieldMenu(anchor, menu);
3427
- }
3428
-
3429
- function settingDropdownChip(label, value, options, onChange) {
3430
- const selected = options.find((option) => option.value === value) || options[0];
3431
- const well = document.createElement("div");
3432
- well.className = "field-well chart-setting-control";
3433
- const button = document.createElement("button");
3434
- button.type = "button";
3435
- button.className = "field-pill chart-setting-option";
3436
- button.setAttribute("aria-haspopup", "menu");
3437
- button.setAttribute("aria-expanded", "false");
3438
- const labelEl = document.createElement("span");
3439
- labelEl.className = "field-pill-role";
3440
- labelEl.textContent = label;
3441
- const valueEl = document.createElement("span");
3442
- valueEl.className = "field-pill-field";
3443
- valueEl.textContent = selected?.label || "";
3444
- button.setAttribute("aria-label", `${label}: ${valueEl.textContent}`);
3445
- button.append(labelEl, caretIcon("field-pill-caret"));
3446
- button.addEventListener("click", (event) => {
3447
- event.stopPropagation();
3448
- openSettingMenu(button, label, value, options, onChange);
3449
- });
3450
- well.appendChild(button);
3451
- return well;
3452
- }
3453
-
3454
- function renderExpandedSettingsControls(root) {
3455
- if (!isDetailDisplayMode()) return;
3456
- const wells = document.createElement("div");
3457
- wells.className = "chart-settings-wells";
3458
- if (activeVisualizationType === "bar") {
3459
- wells.appendChild(
3460
- settingDropdownChip(
3461
- "Orientation",
3462
- chartSettings.barOrientation,
3463
- [
3464
- { label: "Vertical", value: "vertical" },
3465
- { label: "Horizontal", value: "horizontal" },
3466
- ],
3467
- (value) => {
3468
- chartSettings.barOrientation = canonicalBarOrientation(value);
3469
- },
3470
- ),
3471
- );
3472
- if (barSettingsHaveSeries()) {
3473
- const selectedMode = chartSettings.barGroupMode === "single" ? "grouped" : chartSettings.barGroupMode;
3474
- wells.appendChild(
3475
- settingDropdownChip(
3476
- "Mode",
3477
- selectedMode,
3478
- [
3479
- { label: "Grouped", value: "grouped" },
3480
- { label: "Stacked", value: "stacked" },
3481
- { label: "100%", value: "stacked100" },
3482
- ],
3483
- (value) => {
3484
- chartSettings.barGroupMode = canonicalBarGroupMode(value);
3485
- },
3486
- ),
3487
- );
3488
- }
3489
- }
3490
- root.appendChild(wells);
3491
- }
3492
-
3493
- function renderControls() {
3494
- const root = document.getElementById("controls");
3495
- const showControls = Boolean(specValue(activeDataset(), "show_controls", payload.show_controls));
3496
- const appMode = isDetailDisplayMode();
3497
- pruneDataFilters(activeDataset());
3498
- root.innerHTML = "";
3499
- root.hidden = !appMode && !showControls;
3500
-
3501
- if (showControls || appMode) {
3502
- const typeGroup = document.createElement("div");
3503
- typeGroup.className = "control-group";
3504
- const typeLabel = document.createElement("span");
3505
- typeLabel.className = "control-label";
3506
- typeLabel.textContent = "View";
3507
- typeGroup.appendChild(typeLabel);
3508
- renderChartPicker(typeGroup);
3509
- root.appendChild(typeGroup);
3510
-
3511
- const wells = document.createElement("div");
3512
- wells.className = "field-wells";
3513
- wells.append(...chartRoleSpecs().map((spec) => fieldPill(spec)));
3514
- root.appendChild(wells);
3515
-
3516
- renderExpandedSettingsControls(root);
3517
-
3518
- const clear = document.createElement("button");
3519
- clear.type = "button";
3520
- clear.className = "clear-button";
3521
- clear.textContent = appMode ? "Reset" : "Clear all";
3522
- clear.addEventListener("click", clearChartFields);
3523
- root.appendChild(clear);
3524
- }
3525
- }
3526
-
3527
- function render() {
3528
- const dataset = activeDataset();
3529
- ensureActiveChartTypeIsSupported(dataset);
3530
- const subtitle = specValue(dataset, "subtitle", payload.subtitle);
3531
- const title = specValue(dataset, "title", payload.title || "Data Analytics chart");
3532
- document.getElementById("title").textContent = text(title);
3533
- document.getElementById("subtitle").textContent = text(subtitle);
3534
- document.getElementById("subtitle").hidden = !subtitle;
3535
- renderDetailHeader(dataset, title, subtitle);
3536
- setViewMode(viewMode);
3537
- setDataMode(dataMode);
3538
- renderQueryControls(dataset);
3539
-
3540
- if (isTablePayload(dataset)) {
3541
- renderTableFallback(dataset);
3542
- } else {
3543
- renderAppChrome(dataset);
3544
- renderControls();
3545
- renderChart();
3546
- }
3547
- applySplitLayout();
3548
- }
3549
-
3550
- window.addEventListener("message", (event) => {
3551
- if (!messageTargetsThisWidget(event.data)) return;
3552
- applyHostState(event.data);
3553
- applyPayload(event.data, event.data);
3554
- });
3555
-
3556
- window.addEventListener("openai:set_globals", (event) => {
3557
- const globals = event.detail && event.detail.globals;
3558
- if (!messageTargetsThisWidget(globals)) return;
3559
- applyHostState(globals);
3560
- if (globals && applyPayload(globals.toolOutput, globals)) return;
3561
- if (!hasExternalPayload) applyPayload(currentHostPayload(), null);
3562
- });
3563
-
3564
- setupDisplayModeControls();
3565
- setupViewModeControls();
3566
- setupDataModeControls();
3567
- setupMenuDismissal();
3568
- setupAppChromeControls();
3569
- setupDetailRefreshButton();
3570
- setupDetailShareMenu();
3571
- setupSplitResizer();
3572
- window.addEventListener("resize", applySplitLayout);
3573
- if (!applyPayload(currentHostPayload())) {
3574
- const initialPayload = window.openai ? hostedEmptyPayload() : fallbackPayload;
3575
- normalizePayload(initialPayload);
3576
- lastPayloadSignature = payloadSignature(initialPayload);
3577
- render();
3578
- }
3579
-
3580
- let hostPollAttempts = 0;
3581
- const hostPoll = window.setInterval(() => {
3582
- hostPollAttempts += 1;
3583
- setDisplayMode(currentHostDisplayMode());
3584
- if (!hasExternalPayload) {
3585
- applyPayload(currentHostPayload(), null);
3586
- }
3587
- if (hostPollAttempts >= 80) window.clearInterval(hostPoll);
3588
- }, 250);