finflow-sankey 0.1.10__tar.gz
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.
- finflow_sankey-0.1.10/PKG-INFO +239 -0
- finflow_sankey-0.1.10/README.md +210 -0
- finflow_sankey-0.1.10/finflow_sankey/__init__.py +100 -0
- finflow_sankey-0.1.10/finflow_sankey/core/__init__.py +1 -0
- finflow_sankey-0.1.10/finflow_sankey/core/exceptions.py +107 -0
- finflow_sankey-0.1.10/finflow_sankey/core/graph.py +58 -0
- finflow_sankey-0.1.10/finflow_sankey/core/mapper.py +78 -0
- finflow_sankey-0.1.10/finflow_sankey/core/normalizer.py +86 -0
- finflow_sankey-0.1.10/finflow_sankey/core/palette.py +202 -0
- finflow_sankey-0.1.10/finflow_sankey/core/pipeline.py +295 -0
- finflow_sankey-0.1.10/finflow_sankey/core/schema.py +79 -0
- finflow_sankey-0.1.10/finflow_sankey/core/validator.py +184 -0
- finflow_sankey-0.1.10/finflow_sankey/examples/cash_flow_example.py +46 -0
- finflow_sankey-0.1.10/finflow_sankey/examples/income_statement_example.py +70 -0
- finflow_sankey-0.1.10/finflow_sankey/mappings/default_balance_sheet.yaml +24 -0
- finflow_sankey-0.1.10/finflow_sankey/mappings/default_cash_flow.yaml +27 -0
- finflow_sankey-0.1.10/finflow_sankey/mappings/default_income_statement.yaml +27 -0
- finflow_sankey-0.1.10/finflow_sankey/palettes/colorblind_safe.yaml +31 -0
- finflow_sankey-0.1.10/finflow_sankey/palettes/dark.yaml +31 -0
- finflow_sankey-0.1.10/finflow_sankey/palettes/default.yaml +31 -0
- finflow_sankey-0.1.10/finflow_sankey/palettes/minimal.yaml +31 -0
- finflow_sankey-0.1.10/finflow_sankey/palettes/monochrome.yaml +31 -0
- finflow_sankey-0.1.10/finflow_sankey/py.typed +0 -0
- finflow_sankey-0.1.10/finflow_sankey/renderers/__init__.py +1 -0
- finflow_sankey-0.1.10/finflow_sankey/renderers/base.py +17 -0
- finflow_sankey-0.1.10/finflow_sankey/renderers/plotly_renderer.py +203 -0
- finflow_sankey-0.1.10/finflow_sankey/templates/__init__.py +1 -0
- finflow_sankey-0.1.10/finflow_sankey/templates/balance_sheet.py +216 -0
- finflow_sankey-0.1.10/finflow_sankey/templates/base.py +25 -0
- finflow_sankey-0.1.10/finflow_sankey/templates/cash_flow.py +147 -0
- finflow_sankey-0.1.10/finflow_sankey/templates/income_statement.py +157 -0
- finflow_sankey-0.1.10/finflow_sankey/templates/multi_period.py +129 -0
- finflow_sankey-0.1.10/finflow_sankey.egg-info/PKG-INFO +239 -0
- finflow_sankey-0.1.10/finflow_sankey.egg-info/SOURCES.txt +48 -0
- finflow_sankey-0.1.10/finflow_sankey.egg-info/dependency_links.txt +1 -0
- finflow_sankey-0.1.10/finflow_sankey.egg-info/requires.txt +11 -0
- finflow_sankey-0.1.10/finflow_sankey.egg-info/top_level.txt +1 -0
- finflow_sankey-0.1.10/pyproject.toml +49 -0
- finflow_sankey-0.1.10/setup.cfg +4 -0
- finflow_sankey-0.1.10/tests/test_balance_sheet.py +63 -0
- finflow_sankey-0.1.10/tests/test_basic.py +82 -0
- finflow_sankey-0.1.10/tests/test_cash_flow.py +95 -0
- finflow_sankey-0.1.10/tests/test_dark_theme.py +27 -0
- finflow_sankey-0.1.10/tests/test_edge_cases.py +112 -0
- finflow_sankey-0.1.10/tests/test_export.py +53 -0
- finflow_sankey-0.1.10/tests/test_group_minor.py +69 -0
- finflow_sankey-0.1.10/tests/test_hover.py +67 -0
- finflow_sankey-0.1.10/tests/test_layout.py +62 -0
- finflow_sankey-0.1.10/tests/test_mapping.py +64 -0
- finflow_sankey-0.1.10/tests/test_multi_period.py +43 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finflow-sankey
|
|
3
|
+
Version: 0.1.10
|
|
4
|
+
Summary: Polars-first financial statement Sankey visualization library
|
|
5
|
+
Author: FinFlow Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/finflow/finflow-sankey
|
|
8
|
+
Project-URL: Repository, https://github.com/finflow/finflow-sankey
|
|
9
|
+
Keywords: sankey,finance,visualization,polars,plotly
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: polars>=0.20.0
|
|
21
|
+
Requires-Dist: plotly>=5.18.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
26
|
+
Provides-Extra: docs
|
|
27
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
28
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
29
|
+
|
|
30
|
+
# FinFlow Sankey
|
|
31
|
+
|
|
32
|
+
Polars-first financial statement Sankey visualization library.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Polars-first**: Native support for `pl.DataFrame` and `pl.LazyFrame`
|
|
37
|
+
- **Accounting-aware validation**: Period/currency checks, reconciliation validation
|
|
38
|
+
- **Role-based color palette**: Revenue, costs, profit, cash flow each have distinct colors
|
|
39
|
+
- **Customizable themes**: default, monochrome, colorblind-safe, minimal, dark, plus custom YAML palettes
|
|
40
|
+
- **Runtime palette override**: Change colors via dict or YAML without modifying source
|
|
41
|
+
- **Account mapping**: Map raw account names to standard sections via dict or YAML
|
|
42
|
+
- **Consistent line styles**: Link widths proportional to values, but stroke widths uniform
|
|
43
|
+
- **Node layout**: Level-based horizontal positioning, including reconciliation side-by-side view
|
|
44
|
+
- **Rich hover metadata**: Original accounts, validation status, period, currency
|
|
45
|
+
- **HTML export**: One-line export helper
|
|
46
|
+
- **Multi-period comparison**: Compare two periods in a single Sankey
|
|
47
|
+
- **Plotly renderer**: Returns `plotly.graph_objects.Figure` for Jupyter, Streamlit, Dash, HTML export
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import polars as pl
|
|
53
|
+
from finflow_sankey import FinancialSankey
|
|
54
|
+
|
|
55
|
+
df = pl.DataFrame({
|
|
56
|
+
"account": ["Revenue", "Cost of Revenue", "Operating Expenses", "Tax", "Net Income"],
|
|
57
|
+
"value": [100_000_000.0, -40_000_000.0, -30_000_000.0, -10_000_000.0, 20_000_000.0],
|
|
58
|
+
"period": ["FY2025"] * 5,
|
|
59
|
+
"currency": ["USD"] * 5,
|
|
60
|
+
"statement": ["income_statement"] * 5,
|
|
61
|
+
"section": ["revenue", "cost_of_revenue", "operating_expenses", "tax", "profit"],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
fig = (
|
|
65
|
+
FinancialSankey
|
|
66
|
+
.income_statement(df, period="FY2025", currency="USD")
|
|
67
|
+
.validate()
|
|
68
|
+
.render(title="FY2025 Income Statement Flow")
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
fig.show()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Cash Flow Statement
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
df = pl.DataFrame({
|
|
78
|
+
"account": [
|
|
79
|
+
"Beginning Cash",
|
|
80
|
+
"Operating Cash Flow",
|
|
81
|
+
"Investing Cash Flow",
|
|
82
|
+
"Financing Cash Flow",
|
|
83
|
+
"FX Effect",
|
|
84
|
+
"Ending Cash",
|
|
85
|
+
],
|
|
86
|
+
"value": [50_000_000.0, 25_000_000.0, -10_000_000.0, -5_000_000.0, 2_000_000.0, 62_000_000.0],
|
|
87
|
+
"period": ["FY2025"] * 6,
|
|
88
|
+
"currency": ["USD"] * 6,
|
|
89
|
+
"statement": ["cash_flow_statement"] * 6,
|
|
90
|
+
"section": [
|
|
91
|
+
"beginning_cash",
|
|
92
|
+
"operating_cash_flow",
|
|
93
|
+
"investing_cash_flow",
|
|
94
|
+
"financing_cash_flow",
|
|
95
|
+
"fx_effect",
|
|
96
|
+
"ending_cash",
|
|
97
|
+
],
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
fig = (
|
|
101
|
+
FinancialSankey
|
|
102
|
+
.cash_flow_statement(df, period="FY2025", currency="USD")
|
|
103
|
+
.validate()
|
|
104
|
+
.render(title="FY2025 Cash Flow Bridge")
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Balance Sheet Reconciliation
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
df = pl.DataFrame({
|
|
112
|
+
"account": [
|
|
113
|
+
"Current Assets",
|
|
114
|
+
"Non-current Assets",
|
|
115
|
+
"Current Liabilities",
|
|
116
|
+
"Non-current Liabilities",
|
|
117
|
+
"Equity",
|
|
118
|
+
],
|
|
119
|
+
"value": [60_000_000.0, 40_000_000.0, 30_000_000.0, 20_000_000.0, 50_000_000.0],
|
|
120
|
+
"period": ["2025-12-31"] * 5,
|
|
121
|
+
"currency": ["USD"] * 5,
|
|
122
|
+
"statement": ["balance_sheet"] * 5,
|
|
123
|
+
"section": [
|
|
124
|
+
"current_asset",
|
|
125
|
+
"non_current_asset",
|
|
126
|
+
"current_liability",
|
|
127
|
+
"non_current_liability",
|
|
128
|
+
"equity",
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
fig = (
|
|
133
|
+
FinancialSankey
|
|
134
|
+
.balance_sheet_reconciliation(df, as_of="2025-12-31", currency="USD")
|
|
135
|
+
.validate()
|
|
136
|
+
.render(title="Balance Sheet Reconciliation")
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Multi-Period Comparison
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
df = pl.DataFrame({
|
|
144
|
+
"account": ["Revenue", "Revenue", "Operating Expenses", "Operating Expenses"],
|
|
145
|
+
"value": [100_000_000.0, 120_000_000.0, -40_000_000.0, -50_000_000.0],
|
|
146
|
+
"period": ["FY2024", "FY2025", "FY2024", "FY2025"],
|
|
147
|
+
"currency": ["USD"] * 4,
|
|
148
|
+
"statement": ["income_statement"] * 4,
|
|
149
|
+
"section": ["revenue", "revenue", "expense", "expense"],
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
fig = (
|
|
153
|
+
FinancialSankey
|
|
154
|
+
.multi_period_compare(df, currency="USD")
|
|
155
|
+
.validate()
|
|
156
|
+
.render(title="FY2024 vs FY2025")
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Account Mapping
|
|
161
|
+
|
|
162
|
+
Map raw account names to standard sections via dict or YAML:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
mapping = {
|
|
166
|
+
"revenue": ["Net Sales", "Sales Revenue"],
|
|
167
|
+
"cost_of_revenue": ["COGS", "Cost of Goods Sold"],
|
|
168
|
+
"operating_expenses": ["SG&A", "R&D"],
|
|
169
|
+
"tax": ["Income Tax Expense"],
|
|
170
|
+
"profit": ["Net Income"],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fig = FinancialSankey.income_statement(df, mapping=mapping).validate().render()
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Themes & Palettes
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# Built-in theme
|
|
180
|
+
fig = FinancialSankey.income_statement(df).validate().render(theme="colorblind_safe")
|
|
181
|
+
|
|
182
|
+
# Dark mode
|
|
183
|
+
fig = FinancialSankey.income_statement(df).validate().render(theme="dark")
|
|
184
|
+
|
|
185
|
+
# Custom YAML palette
|
|
186
|
+
fig = FinancialSankey.income_statement(df).validate().render(palette="./my_palette.yaml")
|
|
187
|
+
|
|
188
|
+
# Runtime dict override
|
|
189
|
+
fig = FinancialSankey.income_statement(df).validate().render(
|
|
190
|
+
palette={"revenue": "#0055FF", "profit": "#00AA55"}
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## HTML Export
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
(
|
|
198
|
+
FinancialSankey
|
|
199
|
+
.income_statement(df)
|
|
200
|
+
.validate()
|
|
201
|
+
.export_html("income_statement.html", title="FY2025 Income Statement")
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Installation
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pip install finflow-sankey
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install -e ".[dev]"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
python -m pytest tests/ -v
|
|
221
|
+
ruff check finflow_sankey tests
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Project Structure
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
finflow_sankey/
|
|
228
|
+
core/ # schema, validation, normalization, graph, palette, mapper
|
|
229
|
+
templates/ # income_statement, cash_flow, balance_sheet, multi_period
|
|
230
|
+
renderers/ # plotly renderer
|
|
231
|
+
palettes/ # YAML color palettes
|
|
232
|
+
mappings/ # YAML account mappings
|
|
233
|
+
examples/ # usage examples
|
|
234
|
+
tests/ # pytest suite
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# FinFlow Sankey
|
|
2
|
+
|
|
3
|
+
Polars-first financial statement Sankey visualization library.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Polars-first**: Native support for `pl.DataFrame` and `pl.LazyFrame`
|
|
8
|
+
- **Accounting-aware validation**: Period/currency checks, reconciliation validation
|
|
9
|
+
- **Role-based color palette**: Revenue, costs, profit, cash flow each have distinct colors
|
|
10
|
+
- **Customizable themes**: default, monochrome, colorblind-safe, minimal, dark, plus custom YAML palettes
|
|
11
|
+
- **Runtime palette override**: Change colors via dict or YAML without modifying source
|
|
12
|
+
- **Account mapping**: Map raw account names to standard sections via dict or YAML
|
|
13
|
+
- **Consistent line styles**: Link widths proportional to values, but stroke widths uniform
|
|
14
|
+
- **Node layout**: Level-based horizontal positioning, including reconciliation side-by-side view
|
|
15
|
+
- **Rich hover metadata**: Original accounts, validation status, period, currency
|
|
16
|
+
- **HTML export**: One-line export helper
|
|
17
|
+
- **Multi-period comparison**: Compare two periods in a single Sankey
|
|
18
|
+
- **Plotly renderer**: Returns `plotly.graph_objects.Figure` for Jupyter, Streamlit, Dash, HTML export
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import polars as pl
|
|
24
|
+
from finflow_sankey import FinancialSankey
|
|
25
|
+
|
|
26
|
+
df = pl.DataFrame({
|
|
27
|
+
"account": ["Revenue", "Cost of Revenue", "Operating Expenses", "Tax", "Net Income"],
|
|
28
|
+
"value": [100_000_000.0, -40_000_000.0, -30_000_000.0, -10_000_000.0, 20_000_000.0],
|
|
29
|
+
"period": ["FY2025"] * 5,
|
|
30
|
+
"currency": ["USD"] * 5,
|
|
31
|
+
"statement": ["income_statement"] * 5,
|
|
32
|
+
"section": ["revenue", "cost_of_revenue", "operating_expenses", "tax", "profit"],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
fig = (
|
|
36
|
+
FinancialSankey
|
|
37
|
+
.income_statement(df, period="FY2025", currency="USD")
|
|
38
|
+
.validate()
|
|
39
|
+
.render(title="FY2025 Income Statement Flow")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
fig.show()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Cash Flow Statement
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
df = pl.DataFrame({
|
|
49
|
+
"account": [
|
|
50
|
+
"Beginning Cash",
|
|
51
|
+
"Operating Cash Flow",
|
|
52
|
+
"Investing Cash Flow",
|
|
53
|
+
"Financing Cash Flow",
|
|
54
|
+
"FX Effect",
|
|
55
|
+
"Ending Cash",
|
|
56
|
+
],
|
|
57
|
+
"value": [50_000_000.0, 25_000_000.0, -10_000_000.0, -5_000_000.0, 2_000_000.0, 62_000_000.0],
|
|
58
|
+
"period": ["FY2025"] * 6,
|
|
59
|
+
"currency": ["USD"] * 6,
|
|
60
|
+
"statement": ["cash_flow_statement"] * 6,
|
|
61
|
+
"section": [
|
|
62
|
+
"beginning_cash",
|
|
63
|
+
"operating_cash_flow",
|
|
64
|
+
"investing_cash_flow",
|
|
65
|
+
"financing_cash_flow",
|
|
66
|
+
"fx_effect",
|
|
67
|
+
"ending_cash",
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
fig = (
|
|
72
|
+
FinancialSankey
|
|
73
|
+
.cash_flow_statement(df, period="FY2025", currency="USD")
|
|
74
|
+
.validate()
|
|
75
|
+
.render(title="FY2025 Cash Flow Bridge")
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Balance Sheet Reconciliation
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
df = pl.DataFrame({
|
|
83
|
+
"account": [
|
|
84
|
+
"Current Assets",
|
|
85
|
+
"Non-current Assets",
|
|
86
|
+
"Current Liabilities",
|
|
87
|
+
"Non-current Liabilities",
|
|
88
|
+
"Equity",
|
|
89
|
+
],
|
|
90
|
+
"value": [60_000_000.0, 40_000_000.0, 30_000_000.0, 20_000_000.0, 50_000_000.0],
|
|
91
|
+
"period": ["2025-12-31"] * 5,
|
|
92
|
+
"currency": ["USD"] * 5,
|
|
93
|
+
"statement": ["balance_sheet"] * 5,
|
|
94
|
+
"section": [
|
|
95
|
+
"current_asset",
|
|
96
|
+
"non_current_asset",
|
|
97
|
+
"current_liability",
|
|
98
|
+
"non_current_liability",
|
|
99
|
+
"equity",
|
|
100
|
+
],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
fig = (
|
|
104
|
+
FinancialSankey
|
|
105
|
+
.balance_sheet_reconciliation(df, as_of="2025-12-31", currency="USD")
|
|
106
|
+
.validate()
|
|
107
|
+
.render(title="Balance Sheet Reconciliation")
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Multi-Period Comparison
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
df = pl.DataFrame({
|
|
115
|
+
"account": ["Revenue", "Revenue", "Operating Expenses", "Operating Expenses"],
|
|
116
|
+
"value": [100_000_000.0, 120_000_000.0, -40_000_000.0, -50_000_000.0],
|
|
117
|
+
"period": ["FY2024", "FY2025", "FY2024", "FY2025"],
|
|
118
|
+
"currency": ["USD"] * 4,
|
|
119
|
+
"statement": ["income_statement"] * 4,
|
|
120
|
+
"section": ["revenue", "revenue", "expense", "expense"],
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
fig = (
|
|
124
|
+
FinancialSankey
|
|
125
|
+
.multi_period_compare(df, currency="USD")
|
|
126
|
+
.validate()
|
|
127
|
+
.render(title="FY2024 vs FY2025")
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Account Mapping
|
|
132
|
+
|
|
133
|
+
Map raw account names to standard sections via dict or YAML:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
mapping = {
|
|
137
|
+
"revenue": ["Net Sales", "Sales Revenue"],
|
|
138
|
+
"cost_of_revenue": ["COGS", "Cost of Goods Sold"],
|
|
139
|
+
"operating_expenses": ["SG&A", "R&D"],
|
|
140
|
+
"tax": ["Income Tax Expense"],
|
|
141
|
+
"profit": ["Net Income"],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fig = FinancialSankey.income_statement(df, mapping=mapping).validate().render()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Themes & Palettes
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Built-in theme
|
|
151
|
+
fig = FinancialSankey.income_statement(df).validate().render(theme="colorblind_safe")
|
|
152
|
+
|
|
153
|
+
# Dark mode
|
|
154
|
+
fig = FinancialSankey.income_statement(df).validate().render(theme="dark")
|
|
155
|
+
|
|
156
|
+
# Custom YAML palette
|
|
157
|
+
fig = FinancialSankey.income_statement(df).validate().render(palette="./my_palette.yaml")
|
|
158
|
+
|
|
159
|
+
# Runtime dict override
|
|
160
|
+
fig = FinancialSankey.income_statement(df).validate().render(
|
|
161
|
+
palette={"revenue": "#0055FF", "profit": "#00AA55"}
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## HTML Export
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
(
|
|
169
|
+
FinancialSankey
|
|
170
|
+
.income_statement(df)
|
|
171
|
+
.validate()
|
|
172
|
+
.export_html("income_statement.html", title="FY2025 Income Statement")
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Installation
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install finflow-sankey
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pip install -e ".[dev]"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
python -m pytest tests/ -v
|
|
192
|
+
ruff check finflow_sankey tests
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Project Structure
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
finflow_sankey/
|
|
199
|
+
core/ # schema, validation, normalization, graph, palette, mapper
|
|
200
|
+
templates/ # income_statement, cash_flow, balance_sheet, multi_period
|
|
201
|
+
renderers/ # plotly renderer
|
|
202
|
+
palettes/ # YAML color palettes
|
|
203
|
+
mappings/ # YAML account mappings
|
|
204
|
+
examples/ # usage examples
|
|
205
|
+
tests/ # pytest suite
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""FinFlow Sankey: Polars-first financial statement Sankey visualization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
from finflow_sankey.core.mapper import AccountMapper
|
|
11
|
+
from finflow_sankey.core.pipeline import SankeyPipeline
|
|
12
|
+
from finflow_sankey.templates.income_statement import IncomeStatementTemplate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FinancialSankey:
|
|
16
|
+
"""Main entry point for FinFlow Sankey."""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def income_statement(
|
|
20
|
+
cls,
|
|
21
|
+
data: pl.DataFrame | pl.LazyFrame,
|
|
22
|
+
*,
|
|
23
|
+
period: str | None = None,
|
|
24
|
+
currency: str | None = None,
|
|
25
|
+
mapping: AccountMapper | dict[str, Any] | str | Path | None = None,
|
|
26
|
+
) -> SankeyPipeline:
|
|
27
|
+
"""Create an income statement Sankey pipeline."""
|
|
28
|
+
template = IncomeStatementTemplate()
|
|
29
|
+
return SankeyPipeline(
|
|
30
|
+
data=data,
|
|
31
|
+
template=template,
|
|
32
|
+
period=period,
|
|
33
|
+
currency=currency,
|
|
34
|
+
mapping=mapping,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def cash_flow_statement(
|
|
39
|
+
cls,
|
|
40
|
+
data: pl.DataFrame | pl.LazyFrame,
|
|
41
|
+
*,
|
|
42
|
+
period: str | None = None,
|
|
43
|
+
currency: str | None = None,
|
|
44
|
+
mapping: AccountMapper | dict[str, Any] | str | Path | None = None,
|
|
45
|
+
) -> SankeyPipeline:
|
|
46
|
+
"""Create a cash flow statement Sankey pipeline."""
|
|
47
|
+
from finflow_sankey.templates.cash_flow import CashFlowStatementTemplate
|
|
48
|
+
|
|
49
|
+
template = CashFlowStatementTemplate()
|
|
50
|
+
return SankeyPipeline(
|
|
51
|
+
data=data,
|
|
52
|
+
template=template,
|
|
53
|
+
period=period,
|
|
54
|
+
currency=currency,
|
|
55
|
+
mapping=mapping,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def balance_sheet_reconciliation(
|
|
60
|
+
cls,
|
|
61
|
+
data: pl.DataFrame | pl.LazyFrame,
|
|
62
|
+
*,
|
|
63
|
+
as_of: str | None = None,
|
|
64
|
+
currency: str | None = None,
|
|
65
|
+
mapping: AccountMapper | dict[str, Any] | str | Path | None = None,
|
|
66
|
+
) -> SankeyPipeline:
|
|
67
|
+
"""Create a balance sheet reconciliation Sankey pipeline."""
|
|
68
|
+
from finflow_sankey.templates.balance_sheet import BalanceSheetReconciliationTemplate
|
|
69
|
+
|
|
70
|
+
template = BalanceSheetReconciliationTemplate()
|
|
71
|
+
return SankeyPipeline(
|
|
72
|
+
data=data,
|
|
73
|
+
template=template,
|
|
74
|
+
period=as_of,
|
|
75
|
+
currency=currency,
|
|
76
|
+
mapping=mapping,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def multi_period_compare(
|
|
81
|
+
cls,
|
|
82
|
+
data: pl.DataFrame | pl.LazyFrame,
|
|
83
|
+
*,
|
|
84
|
+
currency: str | None = None,
|
|
85
|
+
mapping: AccountMapper | dict[str, Any] | str | Path | None = None,
|
|
86
|
+
) -> SankeyPipeline:
|
|
87
|
+
"""Create a multi-period comparison Sankey pipeline."""
|
|
88
|
+
from finflow_sankey.templates.multi_period import MultiPeriodComparisonTemplate
|
|
89
|
+
|
|
90
|
+
template = MultiPeriodComparisonTemplate()
|
|
91
|
+
return SankeyPipeline(
|
|
92
|
+
data=data,
|
|
93
|
+
template=template,
|
|
94
|
+
period=None,
|
|
95
|
+
currency=currency,
|
|
96
|
+
mapping=mapping,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["FinancialSankey", "SankeyPipeline"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core modules for FinFlow Sankey."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""FinFlow Sankey custom exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FinFlowError(Exception):
|
|
7
|
+
"""Base exception for FinFlow Sankey."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SchemaError(FinFlowError):
|
|
13
|
+
"""Raised when input schema is invalid."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MissingColumnError(SchemaError):
|
|
19
|
+
"""Raised when a required column is missing."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, column: str):
|
|
22
|
+
self.column = column
|
|
23
|
+
super().__init__(f"Required column '{column}' is missing from input data.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PeriodMismatchError(FinFlowError):
|
|
27
|
+
"""Raised when multiple periods are detected."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CurrencyMismatchError(FinFlowError):
|
|
33
|
+
"""Raised when multiple currencies are detected."""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MissingAccountError(FinFlowError):
|
|
39
|
+
"""Raised when a required account role is missing."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, role: str, statement: str, available: list[str] | None = None):
|
|
42
|
+
self.role = role
|
|
43
|
+
self.statement = statement
|
|
44
|
+
self.available = available or []
|
|
45
|
+
msg = f"Required role '{role}' is missing for statement '{statement}'."
|
|
46
|
+
if self.available:
|
|
47
|
+
msg += f" Available accounts: {', '.join(self.available)}"
|
|
48
|
+
super().__init__(msg)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ReconciliationError(FinFlowError):
|
|
52
|
+
"""Raised when financial reconciliation fails."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
rule: str,
|
|
57
|
+
expected: float,
|
|
58
|
+
actual: float,
|
|
59
|
+
difference: float,
|
|
60
|
+
period: str | None = None,
|
|
61
|
+
currency: str | None = None,
|
|
62
|
+
tolerance: float = 0.01,
|
|
63
|
+
):
|
|
64
|
+
self.rule = rule
|
|
65
|
+
self.expected = expected
|
|
66
|
+
self.actual = actual
|
|
67
|
+
self.difference = difference
|
|
68
|
+
self.period = period
|
|
69
|
+
self.currency = currency
|
|
70
|
+
self.tolerance = tolerance
|
|
71
|
+
super().__init__(
|
|
72
|
+
f"Reconciliation failed: {rule}\n"
|
|
73
|
+
f" Expected: {expected:,.2f}\n"
|
|
74
|
+
f" Actual: {actual:,.2f}\n"
|
|
75
|
+
f" Difference: {difference:,.2f}\n"
|
|
76
|
+
f" Tolerance: {tolerance}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class NullValueError(FinFlowError):
|
|
81
|
+
"""Raised when null values are found in required fields."""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DuplicateAccountError(FinFlowError):
|
|
87
|
+
"""Raised when duplicate accounts are detected."""
|
|
88
|
+
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class InvalidColorError(FinFlowError):
|
|
93
|
+
"""Raised when an invalid color is provided."""
|
|
94
|
+
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MissingRoleColorError(FinFlowError):
|
|
99
|
+
"""Raised when a required role color is missing from palette."""
|
|
100
|
+
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class InvalidOpacityError(FinFlowError):
|
|
105
|
+
"""Raised when opacity is out of valid range."""
|
|
106
|
+
|
|
107
|
+
pass
|