dataface 0.1.2__py3-none-any.whl

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 (455) hide show
  1. d3_format/__init__.py +14 -0
  2. d3_format/errors.py +19 -0
  3. d3_format/format.py +551 -0
  4. d3_format/spec.py +159 -0
  5. dataface/DATAFACE_SYNTAX.md +1135 -0
  6. dataface/__init__.py +93 -0
  7. dataface/_docs_site.py +20 -0
  8. dataface/_install_hint.py +26 -0
  9. dataface/agent_api/__init__.py +79 -0
  10. dataface/agent_api/_init_templates/__init__.py +0 -0
  11. dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
  12. dataface/agent_api/_init_templates/dataface.yml +15 -0
  13. dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
  14. dataface/agent_api/_init_templates/index.md +24 -0
  15. dataface/agent_api/_paths.py +118 -0
  16. dataface/agent_api/_project_agents_md.py +43 -0
  17. dataface/agent_api/_session_store.py +486 -0
  18. dataface/agent_api/_state.py +28 -0
  19. dataface/agent_api/chat.py +221 -0
  20. dataface/agent_api/dashboards.py +257 -0
  21. dataface/agent_api/describe.py +366 -0
  22. dataface/agent_api/describe_query.py +120 -0
  23. dataface/agent_api/docs/__init__.py +25 -0
  24. dataface/agent_api/docs/_loader.py +292 -0
  25. dataface/agent_api/docs/yaml-reference.md +2757 -0
  26. dataface/agent_api/file_refs.py +118 -0
  27. dataface/agent_api/init.py +126 -0
  28. dataface/agent_api/inspect.py +128 -0
  29. dataface/agent_api/mcp_install.py +170 -0
  30. dataface/agent_api/query.py +274 -0
  31. dataface/agent_api/schema.py +658 -0
  32. dataface/agent_api/schema_search.py +284 -0
  33. dataface/agent_api/search.py +270 -0
  34. dataface/agent_api/skill_install.py +141 -0
  35. dataface/agent_api/skill_render.py +90 -0
  36. dataface/agent_api/skills.py +293 -0
  37. dataface/agent_api/surface_aliases.yaml +128 -0
  38. dataface/agent_api/validate.py +175 -0
  39. dataface/agent_api/validate_query.py +84 -0
  40. dataface/ai/__init__.py +39 -0
  41. dataface/ai/agent.py +139 -0
  42. dataface/ai/context.py +45 -0
  43. dataface/ai/events.py +62 -0
  44. dataface/ai/external_mcp.py +610 -0
  45. dataface/ai/generate_sql.py +96 -0
  46. dataface/ai/llm.py +403 -0
  47. dataface/ai/mcp/__init__.py +51 -0
  48. dataface/ai/mcp/server.py +289 -0
  49. dataface/ai/memories.py +85 -0
  50. dataface/ai/prompts.py +177 -0
  51. dataface/ai/schema_context.py +138 -0
  52. dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
  53. dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
  54. dataface/ai/skills/dashboard-build/SKILL.md +212 -0
  55. dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
  56. dataface/ai/skills/dashboard-design/SKILL.md +182 -0
  57. dataface/ai/skills/dashboard-review/SKILL.md +113 -0
  58. dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
  59. dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
  60. dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
  61. dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
  62. dataface/ai/skills/drill-down-link/SKILL.md +112 -0
  63. dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
  64. dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
  65. dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
  66. dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
  67. dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
  68. dataface/ai/skills/kpi-row/SKILL.md +101 -0
  69. dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
  70. dataface/ai/skills/report-design/SKILL.md +184 -0
  71. dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
  72. dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
  73. dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
  74. dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
  75. dataface/ai/skills/time-series-trend/SKILL.md +93 -0
  76. dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
  77. dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
  78. dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
  79. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
  80. dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
  81. dataface/ai/tool_schemas.py +132 -0
  82. dataface/ai/tools/__init__.py +312 -0
  83. dataface/ai/yaml_utils.py +57 -0
  84. dataface/cli/__init__.py +3 -0
  85. dataface/cli/_console.py +48 -0
  86. dataface/cli/_error_format.py +83 -0
  87. dataface/cli/_extras.py +190 -0
  88. dataface/cli/_json_output.py +8 -0
  89. dataface/cli/_parsing.py +17 -0
  90. dataface/cli/_version_info.py +56 -0
  91. dataface/cli/commands/__init__.py +3 -0
  92. dataface/cli/commands/_agent_input.py +205 -0
  93. dataface/cli/commands/_agent_server.py +115 -0
  94. dataface/cli/commands/chat.py +645 -0
  95. dataface/cli/commands/describe.py +107 -0
  96. dataface/cli/commands/docs.py +131 -0
  97. dataface/cli/commands/extension.py +179 -0
  98. dataface/cli/commands/init.py +240 -0
  99. dataface/cli/commands/inspect.py +94 -0
  100. dataface/cli/commands/mcp_init.py +167 -0
  101. dataface/cli/commands/query.py +386 -0
  102. dataface/cli/commands/render.py +291 -0
  103. dataface/cli/commands/schema.py +411 -0
  104. dataface/cli/commands/search.py +49 -0
  105. dataface/cli/commands/serve.py +114 -0
  106. dataface/cli/commands/skills.py +133 -0
  107. dataface/cli/commands/skills_init.py +161 -0
  108. dataface/cli/commands/validate.py +63 -0
  109. dataface/cli/main.py +1501 -0
  110. dataface/core/__init__.py +75 -0
  111. dataface/core/compile/__init__.py +244 -0
  112. dataface/core/compile/_jinja_helpers.py +78 -0
  113. dataface/core/compile/channel.py +222 -0
  114. dataface/core/compile/chart_focus.py +101 -0
  115. dataface/core/compile/chart_resolved.py +169 -0
  116. dataface/core/compile/chart_type_detection.py +489 -0
  117. dataface/core/compile/chart_update.py +261 -0
  118. dataface/core/compile/colors.py +64 -0
  119. dataface/core/compile/compiler.py +904 -0
  120. dataface/core/compile/config.py +823 -0
  121. dataface/core/compile/custom_chart_types.py +208 -0
  122. dataface/core/compile/data_table_attachment.py +1287 -0
  123. dataface/core/compile/detect.py +110 -0
  124. dataface/core/compile/errors.py +302 -0
  125. dataface/core/compile/filter_injection.py +319 -0
  126. dataface/core/compile/introspection.py +527 -0
  127. dataface/core/compile/jinja.py +511 -0
  128. dataface/core/compile/labels_env.py +52 -0
  129. dataface/core/compile/markdown.py +154 -0
  130. dataface/core/compile/meta.py +388 -0
  131. dataface/core/compile/models/__init__.py +0 -0
  132. dataface/core/compile/models/chart/__init__.py +0 -0
  133. dataface/core/compile/models/chart/authored.py +2137 -0
  134. dataface/core/compile/models/chart/compiled.py +398 -0
  135. dataface/core/compile/models/config.py +347 -0
  136. dataface/core/compile/models/face/__init__.py +0 -0
  137. dataface/core/compile/models/face/authored.py +659 -0
  138. dataface/core/compile/models/face/compiled.py +522 -0
  139. dataface/core/compile/models/factories.py +201 -0
  140. dataface/core/compile/models/markers.py +40 -0
  141. dataface/core/compile/models/palette.py +36 -0
  142. dataface/core/compile/models/primitives.py +415 -0
  143. dataface/core/compile/models/query/__init__.py +0 -0
  144. dataface/core/compile/models/query/authored.py +246 -0
  145. dataface/core/compile/models/query/compiled.py +710 -0
  146. dataface/core/compile/models/refs.py +137 -0
  147. dataface/core/compile/models/source.py +611 -0
  148. dataface/core/compile/models/style/__init__.py +0 -0
  149. dataface/core/compile/models/style/authored.py +481 -0
  150. dataface/core/compile/models/style/compiled.py +3399 -0
  151. dataface/core/compile/models/style/merged.py +1682 -0
  152. dataface/core/compile/models/theme.py +362 -0
  153. dataface/core/compile/models/variable/__init__.py +0 -0
  154. dataface/core/compile/models/variable/authored.py +254 -0
  155. dataface/core/compile/models/vega_lite/__init__.py +0 -0
  156. dataface/core/compile/models/vega_lite/config.py +510 -0
  157. dataface/core/compile/models/vega_lite/contracts.py +171 -0
  158. dataface/core/compile/normalize_charts.py +494 -0
  159. dataface/core/compile/normalize_layout.py +1000 -0
  160. dataface/core/compile/normalize_queries.py +297 -0
  161. dataface/core/compile/normalize_variables.py +489 -0
  162. dataface/core/compile/normalizer.py +543 -0
  163. dataface/core/compile/palette.py +1100 -0
  164. dataface/core/compile/parameterized.py +658 -0
  165. dataface/core/compile/parser.py +228 -0
  166. dataface/core/compile/schema.py +20 -0
  167. dataface/core/compile/schema_renderers/__init__.py +0 -0
  168. dataface/core/compile/schema_renderers/json_schema.py +163 -0
  169. dataface/core/compile/schema_renderers/prompt.py +152 -0
  170. dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
  171. dataface/core/compile/sizing.py +2126 -0
  172. dataface/core/compile/sources.py +518 -0
  173. dataface/core/compile/sql_authoring_lint.py +56 -0
  174. dataface/core/compile/style_cascade.py +471 -0
  175. dataface/core/compile/typography.py +299 -0
  176. dataface/core/compile/validator.py +301 -0
  177. dataface/core/compile/variables.py +53 -0
  178. dataface/core/compile/vega_config.py +98 -0
  179. dataface/core/compile/vega_lite/__init__.py +6 -0
  180. dataface/core/compile/vega_lite/validation.py +95 -0
  181. dataface/core/compile/yaml_error_formatter.py +838 -0
  182. dataface/core/connections.py +38 -0
  183. dataface/core/dashboard.py +358 -0
  184. dataface/core/defaults/default_config.yml +101 -0
  185. dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
  186. dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
  187. dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
  188. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
  189. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
  190. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
  191. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
  192. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
  193. dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
  194. dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
  195. dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
  196. dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
  197. dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
  198. dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
  199. dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
  200. dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
  201. dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
  202. dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
  203. dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
  204. dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
  205. dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
  206. dataface/core/defaults/palettes/sequential/amber.yml +22 -0
  207. dataface/core/defaults/palettes/sequential/blue.yml +22 -0
  208. dataface/core/defaults/palettes/sequential/brown.yml +22 -0
  209. dataface/core/defaults/palettes/sequential/gray.yml +22 -0
  210. dataface/core/defaults/palettes/sequential/green.yml +22 -0
  211. dataface/core/defaults/palettes/sequential/purple.yml +22 -0
  212. dataface/core/defaults/palettes/sequential/rust.yml +22 -0
  213. dataface/core/defaults/palettes/sequential/teal.yml +22 -0
  214. dataface/core/defaults/palettes/tone/negative.yml +32 -0
  215. dataface/core/defaults/palettes/tone/positive.yml +22 -0
  216. dataface/core/defaults/palettes/tone/warning.yml +22 -0
  217. dataface/core/defaults/themes/_base.yaml +786 -0
  218. dataface/core/defaults/themes/bi.yaml +16 -0
  219. dataface/core/defaults/themes/carbong100.yaml +41 -0
  220. dataface/core/defaults/themes/cream.yaml +122 -0
  221. dataface/core/defaults/themes/dark.yaml +40 -0
  222. dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
  223. dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
  224. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
  225. dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
  226. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
  227. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
  228. dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
  229. dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
  230. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
  231. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
  232. dataface/core/defaults/themes/editorial.yaml +147 -0
  233. dataface/core/defaults/themes/light.yaml +30 -0
  234. dataface/core/defaults/themes/looker.yaml +17 -0
  235. dataface/core/defaults/themes/stark.yaml +134 -0
  236. dataface/core/errors/__init__.py +67 -0
  237. dataface/core/errors/codes_compile.py +56 -0
  238. dataface/core/errors/codes_execute.py +177 -0
  239. dataface/core/errors/codes_render.py +106 -0
  240. dataface/core/errors/codes_unknown.py +15 -0
  241. dataface/core/errors/hints.py +74 -0
  242. dataface/core/errors/registry.py +42 -0
  243. dataface/core/errors/structured.py +92 -0
  244. dataface/core/execute/__init__.py +91 -0
  245. dataface/core/execute/adapters/__init__.py +49 -0
  246. dataface/core/execute/adapters/adapter_registry.py +400 -0
  247. dataface/core/execute/adapters/base.py +245 -0
  248. dataface/core/execute/adapters/csv_adapter.py +239 -0
  249. dataface/core/execute/adapters/dbt_adapter.py +283 -0
  250. dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
  251. dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
  252. dataface/core/execute/adapters/dbt_utils.py +150 -0
  253. dataface/core/execute/adapters/http_adapter.py +224 -0
  254. dataface/core/execute/adapters/metricflow_adapter.py +94 -0
  255. dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
  256. dataface/core/execute/adapters/sql_adapter.py +710 -0
  257. dataface/core/execute/adapters/values_adapter.py +58 -0
  258. dataface/core/execute/batch.py +744 -0
  259. dataface/core/execute/cache_backend.py +135 -0
  260. dataface/core/execute/cache_keys.py +66 -0
  261. dataface/core/execute/dbt_jinja.py +21 -0
  262. dataface/core/execute/dialects/__init__.py +121 -0
  263. dataface/core/execute/dialects/athena.py +75 -0
  264. dataface/core/execute/dialects/base.py +302 -0
  265. dataface/core/execute/dialects/bigquery.py +38 -0
  266. dataface/core/execute/dialects/databricks.py +68 -0
  267. dataface/core/execute/dialects/duckdb.py +35 -0
  268. dataface/core/execute/dialects/mysql.py +68 -0
  269. dataface/core/execute/dialects/postgres.py +39 -0
  270. dataface/core/execute/dialects/redshift.py +12 -0
  271. dataface/core/execute/dialects/snowflake.py +51 -0
  272. dataface/core/execute/dialects/sqlserver.py +92 -0
  273. dataface/core/execute/duckdb_cache.py +712 -0
  274. dataface/core/execute/duckdb_config.py +26 -0
  275. dataface/core/execute/errors.py +213 -0
  276. dataface/core/execute/executor.py +1249 -0
  277. dataface/core/execute/parallel.py +162 -0
  278. dataface/core/execute/setup_sql.py +58 -0
  279. dataface/core/execute/source_registry.py +72 -0
  280. dataface/core/execute/source_resolver.py +255 -0
  281. dataface/core/execute/sql_guard.py +387 -0
  282. dataface/core/execute/sql_literals.py +199 -0
  283. dataface/core/fonts.py +52 -0
  284. dataface/core/inspect/__init__.py +32 -0
  285. dataface/core/inspect/cache_factory.py +98 -0
  286. dataface/core/inspect/db_types.py +162 -0
  287. dataface/core/inspect/dbt_schema.py +96 -0
  288. dataface/core/inspect/defaults.yml +37 -0
  289. dataface/core/inspect/fanout_risk.py +109 -0
  290. dataface/core/inspect/manifest_utils.py +77 -0
  291. dataface/core/inspect/partials/categorical.yml +40 -0
  292. dataface/core/inspect/partials/date.yml +40 -0
  293. dataface/core/inspect/partials/numeric.yml +55 -0
  294. dataface/core/inspect/partition_types.py +38 -0
  295. dataface/core/inspect/query_validator.py +975 -0
  296. dataface/core/inspect/renderer.py +354 -0
  297. dataface/core/inspect/resolver.py +808 -0
  298. dataface/core/inspect/search.py +461 -0
  299. dataface/core/inspect/sources/__init__.py +32 -0
  300. dataface/core/inspect/sources/dbt.py +738 -0
  301. dataface/core/inspect/sources/duckdb_utils.py +66 -0
  302. dataface/core/inspect/templates/__init__.py +1 -0
  303. dataface/core/inspect/templates/categorical_column.yml +196 -0
  304. dataface/core/inspect/templates/charts.yml +109 -0
  305. dataface/core/inspect/templates/date_column.yml +248 -0
  306. dataface/core/inspect/templates/model.yml +138 -0
  307. dataface/core/inspect/templates/numeric_column.yml +261 -0
  308. dataface/core/inspect/templates/quality.yml +80 -0
  309. dataface/core/inspect/templates/string_column.yml +263 -0
  310. dataface/core/project_roots.py +165 -0
  311. dataface/core/render/__init__.py +87 -0
  312. dataface/core/render/board_links.py +176 -0
  313. dataface/core/render/chart/__init__.py +27 -0
  314. dataface/core/render/chart/arc_attached_table.py +251 -0
  315. dataface/core/render/chart/artifacts.py +16 -0
  316. dataface/core/render/chart/callout.py +225 -0
  317. dataface/core/render/chart/decisions.py +358 -0
  318. dataface/core/render/chart/geo.py +700 -0
  319. dataface/core/render/chart/kpi.py +916 -0
  320. dataface/core/render/chart/labels.py +76 -0
  321. dataface/core/render/chart/pipeline.py +818 -0
  322. dataface/core/render/chart/presentation.py +36 -0
  323. dataface/core/render/chart/profile.py +3438 -0
  324. dataface/core/render/chart/render_single.py +347 -0
  325. dataface/core/render/chart/renderers.py +193 -0
  326. dataface/core/render/chart/rendering.py +565 -0
  327. dataface/core/render/chart/serialization.py +90 -0
  328. dataface/core/render/chart/spark.py +496 -0
  329. dataface/core/render/chart/spark_bar.py +370 -0
  330. dataface/core/render/chart/spec_builders.py +154 -0
  331. dataface/core/render/chart/standard_renderer.py +2645 -0
  332. dataface/core/render/chart/table.py +2957 -0
  333. dataface/core/render/chart/table_support.py +1452 -0
  334. dataface/core/render/chart/tick_values.py +66 -0
  335. dataface/core/render/chart/time_unit_detect.py +809 -0
  336. dataface/core/render/chart/title_overflow.py +157 -0
  337. dataface/core/render/chart/type_inference.py +122 -0
  338. dataface/core/render/chart/validation.py +99 -0
  339. dataface/core/render/chart/vega_lite.py +125 -0
  340. dataface/core/render/chart/vega_lite_types.py +268 -0
  341. dataface/core/render/chart/vl_field_maps.py +346 -0
  342. dataface/core/render/chart_interactivity.py +24 -0
  343. dataface/core/render/control_registry.py +287 -0
  344. dataface/core/render/converters/__init__.py +24 -0
  345. dataface/core/render/converters/chart.py +276 -0
  346. dataface/core/render/converters/html.py +98 -0
  347. dataface/core/render/converters/pdf.py +40 -0
  348. dataface/core/render/converters/png.py +41 -0
  349. dataface/core/render/errors.py +144 -0
  350. dataface/core/render/face_api.py +160 -0
  351. dataface/core/render/faces.py +1194 -0
  352. dataface/core/render/font_measurement.py +48 -0
  353. dataface/core/render/font_support.py +197 -0
  354. dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
  355. dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
  356. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
  357. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
  358. dataface/core/render/fonts/InterVariable.ttf +0 -0
  359. dataface/core/render/fonts/InterVariable.woff2 +0 -0
  360. dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
  361. dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
  362. dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
  363. dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
  364. dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
  365. dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
  366. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
  367. dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
  368. dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
  369. dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
  370. dataface/core/render/fonts/_emoji_font_face.css +43 -0
  371. dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
  372. dataface/core/render/format_utils.py +329 -0
  373. dataface/core/render/geo_defaults.yml +28 -0
  374. dataface/core/render/json_format.py +146 -0
  375. dataface/core/render/layout_sizing.py +865 -0
  376. dataface/core/render/layouts.py +541 -0
  377. dataface/core/render/markdown_defaults.yml +16 -0
  378. dataface/core/render/missing_vars_prompt.py +79 -0
  379. dataface/core/render/placeholder.py +389 -0
  380. dataface/core/render/render_result.py +14 -0
  381. dataface/core/render/renderer.py +467 -0
  382. dataface/core/render/script_embedding.py +16 -0
  383. dataface/core/render/svg_utils.py +212 -0
  384. dataface/core/render/template_loader.py +69 -0
  385. dataface/core/render/templates/controls/_styles.css +606 -0
  386. dataface/core/render/templates/controls/checkbox.html +16 -0
  387. dataface/core/render/templates/controls/date.html +16 -0
  388. dataface/core/render/templates/controls/number.html +19 -0
  389. dataface/core/render/templates/controls/readonly.html +9 -0
  390. dataface/core/render/templates/controls/select.html +21 -0
  391. dataface/core/render/templates/controls/slider.html +22 -0
  392. dataface/core/render/templates/controls/text.html +16 -0
  393. dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
  394. dataface/core/render/templates/scripts/variables.js +976 -0
  395. dataface/core/render/templates/svg/grid_pattern.svg +3 -0
  396. dataface/core/render/templates/svg/styles.css +51 -0
  397. dataface/core/render/terminal.py +311 -0
  398. dataface/core/render/terminal_charts.py +563 -0
  399. dataface/core/render/terminal_defaults.yml +2 -0
  400. dataface/core/render/terminal_layouts.py +299 -0
  401. dataface/core/render/terminal_text.py +31 -0
  402. dataface/core/render/text/__init__.py +1 -0
  403. dataface/core/render/text/case.py +113 -0
  404. dataface/core/render/text_format.py +129 -0
  405. dataface/core/render/utils.py +106 -0
  406. dataface/core/render/variable_controls.py +946 -0
  407. dataface/core/render/variable_input_refinement.py +140 -0
  408. dataface/core/render/warnings/__init__.py +15 -0
  409. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
  410. dataface/core/render/warnings/base.py +44 -0
  411. dataface/core/render/warnings/fanout_risk.py +15 -0
  412. dataface/core/render/warnings/from_query_diagnostic.py +56 -0
  413. dataface/core/render/warnings/missing_join_predicate.py +13 -0
  414. dataface/core/render/warnings/query_parse_error.py +14 -0
  415. dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
  416. dataface/core/render/warnings/reaggregation.py +14 -0
  417. dataface/core/render/warnings/registry.py +45 -0
  418. dataface/core/render/warnings/suppression.py +46 -0
  419. dataface/core/render/warnings/temporal_single_point.py +63 -0
  420. dataface/core/render/warnings/unreferenced_chart.py +15 -0
  421. dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
  422. dataface/core/render/yaml_format.py +167 -0
  423. dataface/core/resolve_face.py +195 -0
  424. dataface/core/schema/__init__.py +0 -0
  425. dataface/core/schema/guidance.py +151 -0
  426. dataface/core/scoped_paths.py +59 -0
  427. dataface/core/serve/__init__.py +14 -0
  428. dataface/core/serve/bootstrap.py +39 -0
  429. dataface/core/serve/embedded.py +57 -0
  430. dataface/core/serve/port.py +129 -0
  431. dataface/core/serve/server.py +938 -0
  432. dataface/core/serve/templates/__init__.py +0 -0
  433. dataface/core/serve/templates/directory.yml +6 -0
  434. dataface/core/serve/templates/error.html.j2 +217 -0
  435. dataface/core/utils.py +121 -0
  436. dataface/core/validate.py +64 -0
  437. dataface/integrations/__init__.py +0 -0
  438. dataface/integrations/highlighting.py +351 -0
  439. dataface/integrations/markdown.py +537 -0
  440. dataface/py.typed +0 -0
  441. dataface-0.1.2.dist-info/METADATA +375 -0
  442. dataface-0.1.2.dist-info/RECORD +455 -0
  443. dataface-0.1.2.dist-info/WHEEL +4 -0
  444. dataface-0.1.2.dist-info/entry_points.txt +2 -0
  445. dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
  446. mdsvg/__init__.py +168 -0
  447. mdsvg/fonts.py +656 -0
  448. mdsvg/images.py +299 -0
  449. mdsvg/parser.py +629 -0
  450. mdsvg/playground.py +284 -0
  451. mdsvg/py.typed +2 -0
  452. mdsvg/renderer.py +1623 -0
  453. mdsvg/style.py +355 -0
  454. mdsvg/types.py +200 -0
  455. mdsvg/utils.py +86 -0
d3_format/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """d3-format — Python implementation of d3-format spec parsing and formatting.
2
+
3
+ Public API:
4
+ format(spec_str)(value) -> str
5
+ format(spec_str, value) -> str
6
+ parse(spec_str) -> FormatSpec
7
+ D3FormatError
8
+ """
9
+
10
+ from d3_format.errors import D3FormatError
11
+ from d3_format.format import format
12
+ from d3_format.spec import FormatSpec, parse
13
+
14
+ __all__ = ["D3FormatError", "FormatSpec", "format", "parse"]
d3_format/errors.py ADDED
@@ -0,0 +1,19 @@
1
+ """D3FormatError — raised for unsupported or invalid format specs."""
2
+
3
+
4
+ class D3FormatError(ValueError):
5
+ """Raised when a format spec is invalid or uses an unsupported feature.
6
+
7
+ Attributes:
8
+ spec: The full format spec string that failed.
9
+ position: 0-based position in spec where the problem was detected.
10
+ reason: Human-readable explanation of the problem.
11
+ """
12
+
13
+ def __init__(self, spec: str, position: int, reason: str) -> None:
14
+ self.spec = spec
15
+ self.position = position
16
+ self.reason = reason
17
+ super().__init__(
18
+ f"d3-format parse error at position {position} in {spec!r}: {reason}"
19
+ )
d3_format/format.py ADDED
@@ -0,0 +1,551 @@
1
+ """d3-format Python implementation.
2
+
3
+ Implements the d3-format spec with byte-for-byte parity to d3.js.
4
+ Key deviations from Python's standard number formatting:
5
+ - Negative sign is U+2212 MINUS SIGN, not U+002D HYPHEN-MINUS.
6
+ - Rounding uses round-half-up (JavaScript Math.round), not banker's rounding.
7
+ - SI type uses significant-digit precision (not decimal-place precision).
8
+ - `d` type rounds to nearest integer using round-half-up.
9
+ - `n` type is locale-aware `g` with grouping.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ from collections.abc import Callable
16
+ from decimal import ROUND_HALF_UP, Decimal
17
+
18
+ from d3_format.spec import FormatSpec, parse
19
+
20
+ # U+2212 MINUS SIGN — d3 uses this, not hyphen-minus.
21
+ _MINUS = "−"
22
+ # U+221E INFINITY SYMBOL — d3's d/,d use this for infinite values.
23
+ _INF_SYMBOL = "∞"
24
+
25
+ # SI prefixes: index 8 = '' (10^0), 9 = 'k' (10^3), etc.
26
+ _SI_PREFIXES = [
27
+ "y",
28
+ "z",
29
+ "a",
30
+ "f",
31
+ "p",
32
+ "n",
33
+ "µ",
34
+ "m",
35
+ "",
36
+ "k",
37
+ "M",
38
+ "G",
39
+ "T",
40
+ "P",
41
+ "E",
42
+ "Z",
43
+ "Y",
44
+ ]
45
+ _SI_PREFIX_OFFSET = 8 # index of the '' (10^0) entry
46
+
47
+
48
+ def _round_half_up(x: float, decimals: int) -> float:
49
+ """Round x to `decimals` decimal places using round-half-up (like JS Math.round).
50
+
51
+ Python's built-in round() uses banker's rounding; d3/JS always rounds 0.5 up.
52
+ We use Decimal for exact midpoint detection.
53
+ """
54
+ if not math.isfinite(x) or math.isnan(x):
55
+ return x
56
+ quant = Decimal(
57
+ "1e-" + str(decimals)
58
+ ) # works for any int, including 0 and negative
59
+ # Use Decimal(x) — the exact IEEE-754 binary value — so rounding matches d3/JS.
60
+ # Decimal(repr(x)) would round "2.55" up to "2.6", but d3 rounds it down to "2.5"
61
+ # because the binary value is 2.5499..., not 2.55.
62
+ return float(Decimal(x).quantize(quant, rounding=ROUND_HALF_UP))
63
+
64
+
65
+ def _fmt_fixed(abs_val: float, decimals: int) -> str:
66
+ """Format abs_val to `decimals` decimal places with round-half-up."""
67
+ rounded = _round_half_up(abs_val, decimals)
68
+ return f"{rounded:.{decimals}f}"
69
+
70
+
71
+ def _format_group(digits: str, separator: str) -> str:
72
+ """Apply thousands grouping (groups of 3) to a digit string."""
73
+ n = len(digits)
74
+ first = n % 3 or 3
75
+ parts = [digits[:first]]
76
+ i = first
77
+ while i < n:
78
+ parts.append(digits[i : i + 3])
79
+ i += 3
80
+ return separator.join(parts)
81
+
82
+
83
+ def _apply_grouping(body: str, separator: str, decimal_pt: str) -> str:
84
+ """Apply thousands grouping to the integer part of a formatted number.
85
+
86
+ Handles plain integers, decimals, and exponential notation.
87
+ """
88
+ if "e" in body:
89
+ mantissa, exp_part = body.split("e", 1)
90
+ if decimal_pt in mantissa:
91
+ int_part, frac = mantissa.split(decimal_pt, 1)
92
+ return (
93
+ _format_group(int_part, separator) + decimal_pt + frac + "e" + exp_part
94
+ )
95
+ return _format_group(mantissa, separator) + "e" + exp_part
96
+ if decimal_pt in body:
97
+ int_part, frac = body.split(decimal_pt, 1)
98
+ return _format_group(int_part, separator) + decimal_pt + frac
99
+ return _format_group(body, separator)
100
+
101
+
102
+ def _apply_trim(body: str) -> str:
103
+ """Remove trailing zeros (and trailing decimal point) from a formatted number."""
104
+ if "e" in body:
105
+ mantissa, exp_part = body.split("e", 1)
106
+ if "." in mantissa:
107
+ mantissa = mantissa.rstrip("0").rstrip(".")
108
+ return mantissa + "e" + exp_part
109
+ if "." in body:
110
+ return body.rstrip("0").rstrip(".")
111
+ return body
112
+
113
+
114
+ def _format_decimal_parts(x: float, precision: int) -> tuple[str, int] | None:
115
+ """Replicate d3's formatDecimalParts(x, p).
116
+
117
+ x must be positive and finite (non-zero).
118
+ Returns (coefficient_digits, exponent) where:
119
+ - coefficient_digits: the p significant-figure string (may be longer if carry)
120
+ - exponent: position of the leading digit (i.e. value ≈ 0.coefficient × 10^(exp+1))
121
+
122
+ e.g. formatDecimalParts(1.23, 3) → ("123", 0)
123
+ formatDecimalParts(1234.5, 3) → ("123", 3) (rounded to 3 sig figs)
124
+
125
+ Uses Decimal(x) — the exact IEEE-754 binary value — with ROUND_HALF_UP so that
126
+ rounding matches JavaScript's toExponential (d3 rounds the binary, not repr).
127
+ """
128
+ if not math.isfinite(x) or x == 0:
129
+ return None
130
+ p = max(1, precision)
131
+ d = Decimal(x) # exact binary value — matches JS's toExponential rounding
132
+ adj = d.adjusted() # exponent of leading digit = floor(log10(|d|))
133
+ # Scale so rounding to nearest integer gives p significant figures.
134
+ scale_exp = p - 1 - adj
135
+ scaled = d * (Decimal(10) ** scale_exp)
136
+ rounded_int = scaled.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
137
+ coefficient = str(abs(int(rounded_int)))
138
+ # If rounding caused a carry (e.g. 9.5 → 10 at p=1), exp_out increases.
139
+ exp_out = adj + (len(coefficient) - p)
140
+ # d3's toExponential(p-1) always returns exactly p significant-digit chars.
141
+ # When a carry adds a digit (e.g. '100' from p=2), truncate to match d3's shape.
142
+ if len(coefficient) > p:
143
+ coefficient = coefficient[:p]
144
+ return coefficient, exp_out
145
+
146
+
147
+ def _format_si(abs_val: float, precision: int) -> tuple[str, str]:
148
+ """Format abs_val with SI prefix to `precision` significant digits.
149
+
150
+ Replicates d3's formatPrefixAuto(x, p) algorithm exactly:
151
+ 1. Round to p significant digits using exponential notation.
152
+ 2. Determine SI prefix exponent from the rounded exponent.
153
+ 3. Build body string from coefficient positioned relative to prefix.
154
+
155
+ Returns (number_body, prefix_char).
156
+ """
157
+ if abs_val == 0.0:
158
+ # d3 calls x.toPrecision(p) for zero (formatDecimalParts returns null).
159
+ # JS toPrecision(p) on 0 = "0." + "0"*(p-1) for p>=2, "0" for p<=1.
160
+ body = ("0." + "0" * (precision - 1)) if precision > 1 else "0"
161
+ return body, ""
162
+
163
+ coefficient, exponent = _format_decimal_parts(abs_val, precision) # type: ignore[misc]
164
+
165
+ # Clamp the level (not the product) to [-8, 8] matching d3-format formatPrefixAuto.js.
166
+ # Clamping the product (-24..24) instead of the level (-8..8) would allow exponents
167
+ # beyond ±24 that exceed the _SI_PREFIXES array bounds → IndexError.
168
+ prefix_exp = max(-8, min(8, math.floor(exponent / 3))) * 3
169
+ prefix = _SI_PREFIXES[prefix_exp // 3 + _SI_PREFIX_OFFSET]
170
+
171
+ # Position the decimal point in coefficient.
172
+ # i = exponent - prefix_exp + 1 = number of digits before the decimal point.
173
+ i = exponent - prefix_exp + 1
174
+ n = len(coefficient)
175
+
176
+ if i == n:
177
+ body = coefficient
178
+ elif i > n:
179
+ body = coefficient + "0" * (i - n)
180
+ elif i > 0:
181
+ body = coefficient[:i] + "." + coefficient[i:]
182
+ else:
183
+ # i <= 0: value is smaller than 1 in the chosen prefix unit.
184
+ # d3: "0." + new Array(1-i).join("0") + formatDecimalParts(x, max(0,p+i-1))[0]
185
+ # new Array(k).join("0") produces k-1 zeros, so (1-i)-1 = -i zeros.
186
+ new_prec = max(1, precision + i - 1)
187
+ d2 = _format_decimal_parts(abs_val, new_prec)
188
+ coeff2 = d2[0] if d2 else "0"
189
+ body = "0." + "0" * (-i) + coeff2
190
+
191
+ return body, prefix
192
+
193
+
194
+ def _js_to_precision(abs_val: float, precision: int) -> str:
195
+ """Replicate JavaScript's Number.prototype.toPrecision(p).
196
+
197
+ Uses exponential notation when exponent >= precision or exponent < -6.
198
+ Otherwise uses fixed notation. Rounds using round-half-up.
199
+
200
+ This is the core of d3's 'g' and 'n' type formatting.
201
+ """
202
+ # d3 treats toPrecision(0) as toPrecision(1) — g/n types use max(1, p).
203
+ p = max(1, precision)
204
+ if abs_val == 0.0:
205
+ # JS: (0).toPrecision(p) = "0." + "0"*(p-1) for p>=2, "0" for p=1
206
+ return ("0." + "0" * (p - 1)) if p > 1 else "0"
207
+
208
+ coefficient, exponent = _format_decimal_parts(abs_val, p) # type: ignore[misc]
209
+
210
+ # JS uses e notation if exponent >= p or exponent < -6.
211
+ if exponent >= p or exponent < -6:
212
+ # Exponential form: coefficient[0].coefficient[1:]e+exponent
213
+ if len(coefficient) > 1:
214
+ mantissa = coefficient[0] + "." + coefficient[1:]
215
+ else:
216
+ mantissa = coefficient[0]
217
+ exp_sign = "+" if exponent >= 0 else "-"
218
+ exp_str = str(abs(exponent))
219
+ return f"{mantissa}e{exp_sign}{exp_str}"
220
+ else:
221
+ # Fixed form: position decimal based on exponent.
222
+ i = exponent + 1 # digits before decimal point
223
+ n = len(coefficient)
224
+ if i >= n:
225
+ # All digits before decimal, may need trailing zeros.
226
+ return coefficient + "0" * (i - n)
227
+ elif i > 0:
228
+ return coefficient[:i] + "." + coefficient[i:]
229
+ else:
230
+ # 0 < abs_val < 1: prepend "0." and leading zeros.
231
+ return "0." + "0" * (-i) + coefficient
232
+
233
+
234
+ def _format_rounded(abs_val: float, precision: int) -> str:
235
+ """Replicate d3's formatRounded(x, p) for 'r' type.
236
+
237
+ Always uses fixed notation (no e), rounds to p significant digits.
238
+ """
239
+ if abs_val == 0.0:
240
+ return "0"
241
+
242
+ coefficient, exponent = _format_decimal_parts(abs_val, precision) # type: ignore[misc]
243
+
244
+ i = exponent + 1 # digits before decimal
245
+ n = len(coefficient)
246
+ if i <= 0:
247
+ return "0." + "0" * (-i) + coefficient
248
+ elif n > i:
249
+ return coefficient[:i] + "." + coefficient[i:]
250
+ else:
251
+ return coefficient + "0" * (i - n)
252
+
253
+
254
+ def _format_general(abs_val: float, precision: int) -> str:
255
+ """Format abs_val using d3's 'g' rule (x.toPrecision(p))."""
256
+ return _js_to_precision(abs_val, precision)
257
+
258
+
259
+ def _body_is_zero(body: str) -> bool:
260
+ """True if `body` contains no nonzero digit (e.g. '0', '0.0', '0.00e+0')."""
261
+ return not any(c.isdigit() and c != "0" for c in body)
262
+
263
+
264
+ def _format_number(spec: FormatSpec, v: float) -> tuple[str, str, str]:
265
+ """Format a finite, non-NaN float (v may be negative).
266
+
267
+ Returns (sign_char, body, suffix) where:
268
+ - sign_char: "−" | "+" | " " | ""
269
+ - body: formatted digit string
270
+ - suffix: SI prefix char, "%" or ""
271
+ """
272
+ is_neg = math.copysign(1.0, v) < 0
273
+ abs_val = abs(v)
274
+
275
+ # d3: negative values get minus; but -0 loses its minus unless sign is "+".
276
+ # (equivalent to d3's "if valueNegative && +formattedValue === 0 && sign !== '+'" logic)
277
+ # We handle this after formatting for non-zero; for zero it's always suppressed unless sign="+".
278
+ is_neg_zero = is_neg and abs_val == 0.0
279
+ if is_neg_zero and spec.sign != "+":
280
+ is_neg = False
281
+
282
+ if is_neg:
283
+ sign_char = _MINUS
284
+ elif spec.sign == "+":
285
+ sign_char = "+"
286
+ elif spec.sign == " ":
287
+ sign_char = " "
288
+ else:
289
+ sign_char = ""
290
+
291
+ t = spec.type
292
+ precision = spec.precision
293
+
294
+ if t == "f":
295
+ prec = precision if precision is not None else 6
296
+ # JS Number.prototype.toFixed() falls back to toString() for |x| >= 10^21.
297
+ body = str(abs_val) if abs_val >= 1e21 else _fmt_fixed(abs_val, prec)
298
+ suffix = ""
299
+
300
+ elif t == "%":
301
+ prec = precision if precision is not None else 6
302
+ scaled = abs_val * 100.0
303
+ # toFixed fallback: |x * 100| >= 10^21 → toString(x * 100)
304
+ body = str(scaled) if scaled >= 1e21 else _fmt_fixed(scaled, prec)
305
+ suffix = "%"
306
+
307
+ elif t == "e":
308
+ prec = precision if precision is not None else 6
309
+ if abs_val == 0.0:
310
+ body = ("0." + "0" * prec if prec > 0 else "0") + "e+0"
311
+ else:
312
+ d = _format_decimal_parts(abs_val, prec + 1)
313
+ coefficient, exponent = d # type: ignore[misc]
314
+ # Pad to prec+1 digits in case trailing zeros were stripped
315
+ coefficient = coefficient.ljust(prec + 1, "0")
316
+ if prec > 0:
317
+ mantissa = coefficient[0] + "." + coefficient[1 : prec + 1]
318
+ else:
319
+ mantissa = coefficient[0]
320
+ exp_sign = "+" if exponent >= 0 else "-"
321
+ body = f"{mantissa}e{exp_sign}{abs(exponent)}"
322
+ suffix = ""
323
+
324
+ elif t in ("g", ""):
325
+ # d3's formatDefault (empty type) uses toPrecision(12) as the default precision.
326
+ # 'g' type uses the spec precision (default 6).
327
+ prec = precision if precision is not None else (12 if t == "" else 6)
328
+ body = _format_general(abs_val, prec)
329
+ suffix = ""
330
+
331
+ elif t == "r":
332
+ prec = precision if precision is not None else 6
333
+ body = _format_rounded(abs_val, prec)
334
+ suffix = ""
335
+
336
+ elif t == "s":
337
+ prec = precision if precision is not None else 6
338
+ body, si_suffix = _format_si(abs_val, prec)
339
+ suffix = si_suffix
340
+
341
+ elif t == "d":
342
+ if abs_val >= 1e21:
343
+ # JS formatDecimal uses x.toLocaleString("en") for |x| >= 1e21.
344
+ # str(x) gives the shortest round-trip repr; Decimal(str()) expands
345
+ # exponential form to the "clean" integer string matching JS.
346
+ s = str(abs_val)
347
+ body = str(int(Decimal(s))) if ("e" in s or "E" in s) else str(int(abs_val))
348
+ else:
349
+ body = str(int(_round_half_up(abs_val, 0)))
350
+ suffix = ""
351
+
352
+ else: # t == "n"
353
+ # n type: alias for ",g" — locale-aware g (grouping handled later).
354
+ prec = precision if precision is not None else 6
355
+ body = _js_to_precision(abs_val, prec)
356
+ suffix = ""
357
+
358
+ # d3: "if (valueNegative && +formattedValue === 0 && sign !== '+') valueNegative = false"
359
+ # Drop the minus sign when the formatted body rounds to zero.
360
+ if is_neg and spec.sign != "+" and _body_is_zero(body):
361
+ is_neg = False
362
+ sign_char = " " if spec.sign == " " else ""
363
+
364
+ return sign_char, body, suffix
365
+
366
+
367
+ def _assemble(
368
+ spec: FormatSpec,
369
+ sign_char: str,
370
+ body: str,
371
+ suffix: str,
372
+ ) -> str:
373
+ """Assemble sign + symbol + body + suffix, apply width/fill/align."""
374
+ currency = "$" if spec.symbol == "$" else ""
375
+
376
+ # Parens sign mode.
377
+ if spec.sign == "(" and sign_char == _MINUS:
378
+ paren_open = "("
379
+ paren_close = ")"
380
+ sign_char = ""
381
+ else:
382
+ paren_open = ""
383
+ paren_close = ""
384
+
385
+ # Assemble the non-padded string.
386
+ if paren_open:
387
+ full = paren_open + currency + body + suffix + paren_close
388
+ else:
389
+ full = sign_char + currency + body + suffix
390
+
391
+ if spec.width is None:
392
+ return full
393
+
394
+ w = spec.width
395
+ current = len(full)
396
+ if current >= w:
397
+ return full
398
+ needed = w - current
399
+
400
+ if spec.zero:
401
+ # Zero-pad: sign+currency, then zeros, then body+suffix.
402
+ # parens+zero is rejected at parse time, so paren_open is never set here.
403
+ prefix = sign_char + currency
404
+ rest = body + suffix
405
+ return prefix + "0" * needed + rest
406
+
407
+ if spec.align == "=":
408
+ prefix_str = (paren_open + currency) if paren_open else (sign_char + currency)
409
+ rest = body + suffix + paren_close
410
+ return prefix_str + spec.fill * needed + rest
411
+
412
+ if spec.align == "<":
413
+ return full + spec.fill * needed
414
+ elif spec.align == ">":
415
+ return spec.fill * needed + full
416
+ else: # "^"
417
+ left = needed // 2
418
+ right = needed - left
419
+ return spec.fill * left + full + spec.fill * right
420
+
421
+
422
+ def _assemble_special(
423
+ spec: FormatSpec,
424
+ text: str,
425
+ is_neg: bool,
426
+ ) -> str:
427
+ """Assemble NaN or Infinity string with sign/currency/width.
428
+
429
+ NaN: sign chars from spec apply (+ shows +, space shows space).
430
+ Infinity: actual sign from is_neg.
431
+ """
432
+ if is_neg:
433
+ sign_char = _MINUS
434
+ elif spec.sign == "+":
435
+ sign_char = "+"
436
+ elif spec.sign == " ":
437
+ sign_char = " "
438
+ else:
439
+ sign_char = ""
440
+
441
+ currency = "$" if spec.symbol == "$" else ""
442
+
443
+ # Parens mode for negative infinity.
444
+ if spec.sign == "(" and is_neg:
445
+ full = "(" + currency + text + ")"
446
+ sign_char = ""
447
+ else:
448
+ full = sign_char + currency + text
449
+
450
+ if spec.width is None:
451
+ return full
452
+
453
+ w = spec.width
454
+ current = len(full)
455
+ if current >= w:
456
+ return full
457
+ needed = w - current
458
+
459
+ # For special values, zero-pad uses "0" as fill char.
460
+ if spec.zero:
461
+ prefix = sign_char + currency
462
+ return prefix + "0" * needed + text
463
+
464
+ if spec.align == "<":
465
+ return full + spec.fill * needed
466
+ elif spec.align in (">", "="):
467
+ return spec.fill * needed + full
468
+ else: # "^"
469
+ left = needed // 2
470
+ right = needed - left
471
+ return spec.fill * left + full + spec.fill * right
472
+
473
+
474
+ def _apply_spec(spec: FormatSpec, v: float | int | Decimal) -> str:
475
+ """Apply a parsed FormatSpec to a numeric value."""
476
+ v = float(v) # coerce int, Decimal, etc.; preserves -0 sign via IEEE 754
477
+
478
+ # d and ,d type: use ∞ symbol for infinite values.
479
+ use_inf_symbol = spec.type in ("d",)
480
+
481
+ # The % type appends "%" even to NaN/Infinity.
482
+ type_suffix = "%" if spec.type == "%" else ""
483
+
484
+ # NaN.
485
+ if math.isnan(v):
486
+ return _assemble_special(spec, "NaN" + type_suffix, is_neg=False)
487
+
488
+ # Infinity.
489
+ if math.isinf(v):
490
+ is_neg = v < 0
491
+ text = (_INF_SYMBOL if use_inf_symbol else "Infinity") + type_suffix
492
+ return _assemble_special(spec, text, is_neg=is_neg)
493
+
494
+ # Normal number.
495
+ sign_char, body, suffix = _format_number(spec, v)
496
+
497
+ # Trim trailing zeros: explicit ~ flag, or unconditionally for empty type.
498
+ # d3's formatDefault (empty type) always strips trailing zeros regardless of ~.
499
+ if spec.trim or spec.type == "":
500
+ body = _apply_trim(body)
501
+
502
+ # Comma grouping: for 'n' type always group; for all others respect spec.comma.
503
+ # For 's' type, comma is applied to the mantissa (matters for very large SI bodies).
504
+ use_comma = spec.comma or spec.type == "n"
505
+ if use_comma:
506
+ if spec.zero and spec.width is not None:
507
+ # d3 applies comma grouping to the zero-padded digits.
508
+ # Pre-pad body to the largest digit count that fits the target width.
509
+ sign_len = len(sign_char) + (1 if spec.symbol == "$" else 0)
510
+ available = spec.width - sign_len - len(suffix)
511
+ d = len(body)
512
+ while True:
513
+ nd = d + 1
514
+ if nd + (nd - 1) // 3 > available:
515
+ break
516
+ d = nd
517
+ body = body.zfill(d)
518
+ body = _apply_grouping(body, ",", ".")
519
+
520
+ return _assemble(spec, sign_char, body, suffix)
521
+
522
+
523
+ def format( # noqa: A001
524
+ spec_str: str,
525
+ value: float | int | Decimal | None = None,
526
+ ) -> Callable[[float | int | Decimal], str] | str:
527
+ """Parse spec and return a formatter, or apply immediately.
528
+
529
+ Two calling conventions::
530
+
531
+ d3_format(",.2f")(1234.56) → "1,234.56"
532
+ d3_format(",.2f", 1234.56) → "1,234.56"
533
+
534
+ Args:
535
+ spec_str: d3-format spec string.
536
+ value: Optional value to format immediately.
537
+
538
+ Returns:
539
+ A ``Callable[[float], str]`` if value is None, else the formatted string.
540
+
541
+ Raises:
542
+ D3FormatError: If spec_str is invalid or uses unsupported features.
543
+ """
544
+ spec = parse(spec_str)
545
+
546
+ def _fmt(v: float | int | Decimal) -> str:
547
+ return _apply_spec(spec, v)
548
+
549
+ if value is None:
550
+ return _fmt
551
+ return _fmt(value)