Skip to content

Commit 5618620

Browse files
authored
Merge pull request #131 from statisticsnorway/gen_vis
moved repo
2 parents 8a7576a + 541782d commit 5618620

File tree

10 files changed

+1192
-0
lines changed

10 files changed

+1192
-0
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies = [
2929
dash = [
3030
"dash>=4.0.0",
3131
"plotly>=6.5.2",
32+
"polars>=1.39.0",
3233
"ssb-dash-components>=0.11.1",
3334
]
3435

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""Dash component for generic visualization at SSB.
2+
3+
This module provides a dash all-in-one component that are intended to replace
4+
some functions of myfame for generic visualizations.
5+
It will only work in SSBs-developer environments.
6+
7+
The module expects the path to JSON-configuration file.
8+
9+
"dataset_1": {
10+
"glob_pattern": "glob/to/files/*.parquet",
11+
"index_col": "period", // The columns that contains the periods.
12+
"index_pattern": "%Y-%m", // The pattern of the period column. See: https://strftime.org/
13+
"groupby_col": "nar" // OPTIONAL: Columns that are supposed to be subset
14+
"agg_type": "AVERAGE" // OPTIONAL: For now, only "AVERAGE" is allowed.
15+
},
16+
"dataset_2": ...
17+
18+
Example:
19+
```
20+
from ssb_konjunk.dash.gen_vis import GenVis
21+
layout = GenVis("./data_setup.json")
22+
# Or
23+
24+
from dash import html
25+
26+
layout = html.Div(
27+
children = [
28+
GenVis("./data_setup.json")
29+
]
30+
)
31+
```
32+
33+
34+
"""
35+
36+
import uuid
37+
38+
from dash import (
39+
Input,
40+
Output,
41+
callback,
42+
html,
43+
)
44+
45+
from ssb_dash_components import Button
46+
47+
from .internal.loading_test import load_datasets
48+
from .internal.tab_selector import TabSelector
49+
from .internal.series_selector import SeriesSelector
50+
from .internal.series_settings_display import SeriesSettingsDisplay
51+
from .internal.graph_display import GraphDisplay
52+
from .internal.graph_settings_display import GraphSettingsDisplay
53+
54+
class GenVis(html.Div):
55+
56+
class ids:
57+
dropdown = lambda aio_id: {
58+
"component": "MarkdownWithColorAIO",
59+
"subcomponent": "dropdown",
60+
"aio_id": aio_id,
61+
}
62+
markdown = lambda aio_id: {
63+
"component": "MarkdownWithColorAIO",
64+
"subcomponent": "markdown",
65+
"aio_id": aio_id,
66+
}
67+
68+
# Make the ids class a public class
69+
ids = ids
70+
71+
# Define the arguments of the All-in-One component
72+
def __init__(self, config_path: str, aio_id=None):
73+
"""
74+
Returns a component that can be used as its own page or as a component in other layouts.
75+
76+
Args:
77+
config_path (str): Path to the config file.
78+
aio_id (str | None): An optional id. Will be randomised if not provided.
79+
"""
80+
if aio_id is None:
81+
aio_id = str(uuid.uuid4())
82+
83+
datasets = load_datasets(config_path)
84+
select_data = {}
85+
for key, value in datasets.items():
86+
select_data[key] = value.get_entries()
87+
88+
super().__init__(
89+
[
90+
html.Div(
91+
[
92+
html.Div(
93+
children=[
94+
html.Div(
95+
children=[
96+
Button(
97+
"Oppdater datasett",
98+
id="update-dataset-button",
99+
),
100+
],
101+
),
102+
html.Div(
103+
TabSelector(datasets, aio_id=aio_id, height="200px"),
104+
),
105+
],
106+
),
107+
html.Div(
108+
children=[
109+
html.H3("Velg serier"),
110+
html.Div(
111+
children=[SeriesSelector(datasets, aio_id=aio_id)],
112+
),
113+
],
114+
),
115+
html.Div(
116+
children=[GraphSettingsDisplay(aio_id)],
117+
),
118+
],
119+
style={
120+
"display": "grid",
121+
"gridTemplateColumns": "40% 45% 15%",
122+
},
123+
),
124+
html.Div(
125+
children=[
126+
html.Div(
127+
html.Div(
128+
children=[
129+
SeriesSettingsDisplay(datasets, aio_id),
130+
GraphDisplay(datasets, aio_id),
131+
],
132+
id="graph-display",
133+
style={
134+
"height": "100px",
135+
"width": "100%",
136+
"display": "grid",
137+
"gridTemplateColumns": "30% 70%",
138+
},
139+
)
140+
),
141+
],
142+
style={
143+
"gap": "30px",
144+
},
145+
),
146+
],
147+
style={"gap": "10px"},
148+
)
149+
150+
@callback(
151+
Output(SeriesSelector.ids.store(aio_id), "data"),
152+
Input(TabSelector.ids.store(aio_id), "data"),
153+
)
154+
def update_selected(selected):
155+
"""Move data from the file selector to the series selector"""
156+
return selected
157+
158+
@callback(
159+
Output(SeriesSettingsDisplay.ids.store(aio_id), "data"),
160+
Input(SeriesSelector.ids.selected_store(aio_id), "data"),
161+
)
162+
def update_display(selected):
163+
"""Move data from the series selcetor to series settings display"""
164+
return selected
165+
166+
@callback(
167+
Output(GraphDisplay.ids.series_store(aio_id), "data"),
168+
Input(SeriesSettingsDisplay.ids.settings_store(aio_id), "data"),
169+
)
170+
def update_graph_series(selected):
171+
"""Moves series settings to the graph display"""
172+
return selected
173+
174+
@callback(
175+
Output(GraphDisplay.ids.settings_store(aio_id), "data"),
176+
Input(GraphSettingsDisplay.ids.settings_store(aio_id), "data"),
177+
)
178+
def update_graph_settings(settings):
179+
"""Move general graph settings to the graph display"""
180+
return settings

src/ssb_konjunk/dash/components/internal/__init__.py

Whitespace-only changes.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from typing import Literal
2+
from datetime import date
3+
import polars as pl
4+
5+
from .loading_test import AGG_TYPES
6+
7+
class DataSource:
8+
def __init__(self, filename: str, index_col: str, date_pattern: str) -> None:
9+
self.dt_colname = "dt"
10+
self.data = pl.read_parquet(filename).with_columns(
11+
dt=pl.col(index_col).cast(pl.String).str.to_date(date_pattern)
12+
)
13+
14+
def filter_dt(self, lower: date, highest: date):
15+
return self.data.filter(pl.col(self.dt_colname).is_between(lower, highest))
16+
17+
def get_unique_dates(self) -> list[date]:
18+
return self.data.get_column(self.dt_colname).unique().to_list()
19+
20+
def get_unique_groupby(self, groupby_col: str):
21+
return self.data.get_column(groupby_col).unique().to_list()
22+
23+
def subset_group(self, groupby_col: str, filter_val):
24+
return self.data.filter(pl.col(groupby_col) == filter_val)
25+
26+
def _set_base_year(
27+
self,
28+
data: pl.DataFrame,
29+
year: str,
30+
col: str,
31+
method: Literal["discrete", "none"],
32+
agg_type: AGG_TYPES,
33+
):
34+
# print(data)
35+
filtered = data.filter(
36+
(pl.col(self.dt_colname) < date(int(year), 12, 31))
37+
& (pl.col(self.dt_colname) >= date(int(year), 1, 1))
38+
)
39+
40+
selected_col = filtered.get_column(col).cast(pl.Float64)
41+
if method == "discrete":
42+
if agg_type == "AVERAGE":
43+
index_factor = selected_col.mean()
44+
else:
45+
index_factor = selected_col.sum()
46+
else:
47+
index_factor = selected_col.mean()
48+
49+
altered = data.with_columns(
50+
**{col: (pl.col(col).cast(pl.Float64) / index_factor) * 100}
51+
)
52+
# print(altered)
53+
return altered
54+
55+
def set_base_year(
56+
self,
57+
data: pl.DataFrame,
58+
year: str,
59+
col: str,
60+
group_col: str | None,
61+
method: Literal["discrete", "none"],
62+
agg_mapping: AGG_TYPES | dict[str, AGG_TYPES],
63+
) -> pl.DataFrame:
64+
65+
if isinstance(agg_mapping, dict):
66+
agg_type = agg_mapping.get(col, "AVERAGE")
67+
elif isinstance(agg_mapping, str):
68+
agg_type = agg_mapping
69+
else:
70+
raise ValueError("")
71+
72+
if group_col is not None:
73+
return data.group_by(group_col).map_groups(
74+
lambda x: self._set_base_year(x, year, col, method, agg_type)
75+
)
76+
else:
77+
return self._set_base_year(data, year, col, method, agg_type)

0 commit comments

Comments
 (0)