nph-client 0.1.2__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.
- nph_client-0.1.2/PKG-INFO +91 -0
- nph_client-0.1.2/README.md +71 -0
- nph_client-0.1.2/pyproject.toml +32 -0
- nph_client-0.1.2/setup.cfg +4 -0
- nph_client-0.1.2/src/nph_client/__init__.py +26 -0
- nph_client-0.1.2/src/nph_client/charts.py +150 -0
- nph_client-0.1.2/src/nph_client/client.py +257 -0
- nph_client-0.1.2/src/nph_client/plotting.py +309 -0
- nph_client-0.1.2/src/nph_client/thresholds.py +36 -0
- nph_client-0.1.2/src/nph_client.egg-info/PKG-INFO +91 -0
- nph_client-0.1.2/src/nph_client.egg-info/SOURCES.txt +13 -0
- nph_client-0.1.2/src/nph_client.egg-info/dependency_links.txt +1 -0
- nph_client-0.1.2/src/nph_client.egg-info/requires.txt +10 -0
- nph_client-0.1.2/src/nph_client.egg-info/top_level.txt +1 -0
- nph_client-0.1.2/tests/test_graficos.py +64 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nph-client
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Biblioteca de visualização de dados oceanográficos e meteorológicos do NPH-UNISANTA.
|
|
5
|
+
Author: NPH UNISANTA
|
|
6
|
+
Keywords: nph,nph-client,oceanografia,meteorologia,plotly,graficos,nivel-do-mar,chuva,unisanta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: pandas>=1.5.0
|
|
12
|
+
Requires-Dist: plotly>=5.0.0
|
|
13
|
+
Requires-Dist: requests>=2.25.0
|
|
14
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
15
|
+
Requires-Dist: numpy>=1.20.0
|
|
16
|
+
Requires-Dist: nbformat>=4.2.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: kaleido>=0.2.1; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# nph-client 🌊
|
|
22
|
+
|
|
23
|
+
Biblioteca em Python para geração padronizada e simplificada de gráficos oceanográficos e meteorológicos do **NPH-UNISANTA** (Núcleo de Pesquisas Hidrodinâmicas da Unisanta).
|
|
24
|
+
|
|
25
|
+
## Instalação
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install nph-client
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Principais Funções
|
|
32
|
+
|
|
33
|
+
### 1. Nível do Mar (`plot_nivel_mar`)
|
|
34
|
+
Plota o gráfico de linha de nível do mar com picos destacados e limiares do **PPDC Ressacas**:
|
|
35
|
+
- **OBSERVAÇÃO**: `< 1.8m`
|
|
36
|
+
- **ATENÇÃO**: `1.8m a 2.0m`
|
|
37
|
+
- **ALERTA**: `≥ 2.0m`
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from nph_client import NPHClient, plot_nivel_mar
|
|
41
|
+
|
|
42
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
43
|
+
df_nivel = client.get_measurements(stations=349, start_date="2026-08-05", end_date="2026-08-07")
|
|
44
|
+
|
|
45
|
+
fig = plot_nivel_mar(df=df_nivel, title="Nível do Mar - Santos")
|
|
46
|
+
fig.show()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Dica**: Para ocultar as linhas e faixas dos limiares PPDC, passe `show_thresholds=False`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 2. Precipitação Pluviométrica / Chuva (`plot_chuva`)
|
|
54
|
+
Plota o gráfico de barras para chuva (mm):
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from nph_client import NPHClient, plot_chuva
|
|
58
|
+
|
|
59
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
60
|
+
df_chuva = client.get_measurements(stations=26, start_date="2026-08-05", end_date="2026-08-07")
|
|
61
|
+
|
|
62
|
+
fig = plot_chuva(df=df_chuva, title="Precipitação Pluviométrica - Portão 40")
|
|
63
|
+
fig.show()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 3. Hidrograma Combinado (`plot_nivel_e_chuva`)
|
|
69
|
+
Reúne Nível do Mar e Chuva no mesmo gráfico com eixo secundário:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from nph_client import NPHClient, plot_nivel_e_chuva
|
|
73
|
+
|
|
74
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
75
|
+
df_nivel = client.get_measurements(stations=349, start_date="2026-08-05", end_date="2026-08-07")
|
|
76
|
+
df_chuva = client.get_measurements(stations=26, start_date="2026-08-05", end_date="2026-08-07")
|
|
77
|
+
|
|
78
|
+
fig = plot_nivel_e_chuva(df_nivel=df_nivel, df_chuva=df_chuva, title="Hidrograma")
|
|
79
|
+
fig.show()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Requisitos
|
|
83
|
+
- `pandas`
|
|
84
|
+
- `plotly`
|
|
85
|
+
- `requests`
|
|
86
|
+
- `python-dateutil`
|
|
87
|
+
- `numpy`
|
|
88
|
+
- `nbformat`
|
|
89
|
+
|
|
90
|
+
## Licença
|
|
91
|
+
Licenciado sob a licença MIT.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# nph-client 🌊
|
|
2
|
+
|
|
3
|
+
Biblioteca em Python para geração padronizada e simplificada de gráficos oceanográficos e meteorológicos do **NPH-UNISANTA** (Núcleo de Pesquisas Hidrodinâmicas da Unisanta).
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install nph-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Principais Funções
|
|
12
|
+
|
|
13
|
+
### 1. Nível do Mar (`plot_nivel_mar`)
|
|
14
|
+
Plota o gráfico de linha de nível do mar com picos destacados e limiares do **PPDC Ressacas**:
|
|
15
|
+
- **OBSERVAÇÃO**: `< 1.8m`
|
|
16
|
+
- **ATENÇÃO**: `1.8m a 2.0m`
|
|
17
|
+
- **ALERTA**: `≥ 2.0m`
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from nph_client import NPHClient, plot_nivel_mar
|
|
21
|
+
|
|
22
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
23
|
+
df_nivel = client.get_measurements(stations=349, start_date="2026-08-05", end_date="2026-08-07")
|
|
24
|
+
|
|
25
|
+
fig = plot_nivel_mar(df=df_nivel, title="Nível do Mar - Santos")
|
|
26
|
+
fig.show()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Dica**: Para ocultar as linhas e faixas dos limiares PPDC, passe `show_thresholds=False`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### 2. Precipitação Pluviométrica / Chuva (`plot_chuva`)
|
|
34
|
+
Plota o gráfico de barras para chuva (mm):
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from nph_client import NPHClient, plot_chuva
|
|
38
|
+
|
|
39
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
40
|
+
df_chuva = client.get_measurements(stations=26, start_date="2026-08-05", end_date="2026-08-07")
|
|
41
|
+
|
|
42
|
+
fig = plot_chuva(df=df_chuva, title="Precipitação Pluviométrica - Portão 40")
|
|
43
|
+
fig.show()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### 3. Hidrograma Combinado (`plot_nivel_e_chuva`)
|
|
49
|
+
Reúne Nível do Mar e Chuva no mesmo gráfico com eixo secundário:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from nph_client import NPHClient, plot_nivel_e_chuva
|
|
53
|
+
|
|
54
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
55
|
+
df_nivel = client.get_measurements(stations=349, start_date="2026-08-05", end_date="2026-08-07")
|
|
56
|
+
df_chuva = client.get_measurements(stations=26, start_date="2026-08-05", end_date="2026-08-07")
|
|
57
|
+
|
|
58
|
+
fig = plot_nivel_e_chuva(df_nivel=df_nivel, df_chuva=df_chuva, title="Hidrograma")
|
|
59
|
+
fig.show()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Requisitos
|
|
63
|
+
- `pandas`
|
|
64
|
+
- `plotly`
|
|
65
|
+
- `requests`
|
|
66
|
+
- `python-dateutil`
|
|
67
|
+
- `numpy`
|
|
68
|
+
- `nbformat`
|
|
69
|
+
|
|
70
|
+
## Licença
|
|
71
|
+
Licenciado sob a licença MIT.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nph-client"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Biblioteca de visualização de dados oceanográficos e meteorológicos do NPH-UNISANTA."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "NPH UNISANTA" }
|
|
12
|
+
]
|
|
13
|
+
keywords = ["nph", "nph-client", "oceanografia", "meteorologia", "plotly", "graficos", "nivel-do-mar", "chuva", "unisanta"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"pandas>=1.5.0",
|
|
21
|
+
"plotly>=5.0.0",
|
|
22
|
+
"requests>=2.25.0",
|
|
23
|
+
"python-dateutil>=2.8.0",
|
|
24
|
+
"numpy>=1.20.0",
|
|
25
|
+
"nbformat>=4.2.0"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0.0",
|
|
31
|
+
"kaleido>=0.2.1"
|
|
32
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .charts import plot_nivel_mar, plot_chuva, plot_nivel_e_chuva
|
|
2
|
+
from .client import NPHClient, MODELS_GLOSSARY, PARAMS_GLOSSARY
|
|
3
|
+
from .thresholds import (
|
|
4
|
+
Threshold,
|
|
5
|
+
get_ppdc_sea_level_thresholds,
|
|
6
|
+
PPDC_THRESHOLD_ATENCAO,
|
|
7
|
+
PPDC_THRESHOLD_ALERTA
|
|
8
|
+
)
|
|
9
|
+
from .plotting import AxisConfig, plot_base_series
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"plot_nivel_mar",
|
|
13
|
+
"plot_chuva",
|
|
14
|
+
"plot_nivel_e_chuva",
|
|
15
|
+
"NPHClient",
|
|
16
|
+
"Threshold",
|
|
17
|
+
"get_ppdc_sea_level_thresholds",
|
|
18
|
+
"PPDC_THRESHOLD_ATENCAO",
|
|
19
|
+
"PPDC_THRESHOLD_ALERTA",
|
|
20
|
+
"AxisConfig",
|
|
21
|
+
"plot_base_series",
|
|
22
|
+
"MODELS_GLOSSARY",
|
|
23
|
+
"PARAMS_GLOSSARY"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.2"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from typing import Optional, List, Union, Tuple
|
|
4
|
+
import plotly.graph_objects as go
|
|
5
|
+
|
|
6
|
+
from .thresholds import Threshold, get_ppdc_sea_level_thresholds
|
|
7
|
+
from .plotting import AxisConfig, plot_base_series
|
|
8
|
+
from .client import NPHClient
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def plot_nivel_mar(
|
|
14
|
+
df: Optional[pd.DataFrame] = None,
|
|
15
|
+
stations: Optional[Union[List[Union[str, int]], str, int]] = None,
|
|
16
|
+
start_date: Optional[str] = None,
|
|
17
|
+
end_date: Optional[str] = None,
|
|
18
|
+
api_key: Optional[str] = None,
|
|
19
|
+
client: Optional[NPHClient] = None,
|
|
20
|
+
columns: Optional[List[str]] = None,
|
|
21
|
+
title: str = "Nível do Mar - PPDC Ressacas (NPH-UNISANTA)",
|
|
22
|
+
thresholds: Optional[List[Threshold]] = None,
|
|
23
|
+
show_thresholds: bool = True,
|
|
24
|
+
show_peaks: bool = True,
|
|
25
|
+
figsize: Tuple[int, int] = (12, 6),
|
|
26
|
+
save_path: Optional[str] = None
|
|
27
|
+
) -> go.Figure:
|
|
28
|
+
if df is None:
|
|
29
|
+
cli = client or NPHClient(api_key=api_key)
|
|
30
|
+
df = cli.get_measurements(stations=stations, start_date=start_date, end_date=end_date)
|
|
31
|
+
if df.empty:
|
|
32
|
+
df = cli.get_forecasts(stations=stations, start_date=start_date, end_date=end_date, param_id="Nível do Mar")
|
|
33
|
+
|
|
34
|
+
if df.empty:
|
|
35
|
+
raise ValueError("Nenhum dado encontrado para gerar o gráfico de Nível do Mar.")
|
|
36
|
+
|
|
37
|
+
if not show_thresholds:
|
|
38
|
+
thresholds_to_use = []
|
|
39
|
+
elif thresholds is not None:
|
|
40
|
+
thresholds_to_use = thresholds
|
|
41
|
+
else:
|
|
42
|
+
thresholds_to_use = get_ppdc_sea_level_thresholds()
|
|
43
|
+
|
|
44
|
+
primary_conf = AxisConfig(
|
|
45
|
+
label="Nível do Mar (m)",
|
|
46
|
+
columns=columns,
|
|
47
|
+
plot_type='line',
|
|
48
|
+
show_peaks=show_peaks,
|
|
49
|
+
thresholds=thresholds_to_use,
|
|
50
|
+
color_palette='Ocean'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return plot_base_series(
|
|
54
|
+
df=df,
|
|
55
|
+
primary_conf=primary_conf,
|
|
56
|
+
title=title,
|
|
57
|
+
figsize=figsize,
|
|
58
|
+
save_path=save_path
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def plot_chuva(
|
|
63
|
+
df: Optional[pd.DataFrame] = None,
|
|
64
|
+
stations: Optional[Union[List[Union[str, int]], str, int]] = None,
|
|
65
|
+
start_date: Optional[str] = None,
|
|
66
|
+
end_date: Optional[str] = None,
|
|
67
|
+
api_key: Optional[str] = None,
|
|
68
|
+
client: Optional[NPHClient] = None,
|
|
69
|
+
columns: Optional[List[str]] = None,
|
|
70
|
+
title: str = "Precipitação Pluviométrica (NPH-UNISANTA)",
|
|
71
|
+
figsize: Tuple[int, int] = (12, 6),
|
|
72
|
+
save_path: Optional[str] = None
|
|
73
|
+
) -> go.Figure:
|
|
74
|
+
if df is None:
|
|
75
|
+
cli = client or NPHClient(api_key=api_key)
|
|
76
|
+
df = cli.get_measurements(stations=stations, start_date=start_date, end_date=end_date)
|
|
77
|
+
if df.empty:
|
|
78
|
+
df = cli.get_forecasts(stations=stations, start_date=start_date, end_date=end_date, param_id="Precipitação")
|
|
79
|
+
|
|
80
|
+
if df.empty:
|
|
81
|
+
raise ValueError("Nenhum dado encontrado para gerar o gráfico de Chuva.")
|
|
82
|
+
|
|
83
|
+
primary_conf = AxisConfig(
|
|
84
|
+
label="Precipitação (mm)",
|
|
85
|
+
columns=columns,
|
|
86
|
+
plot_type='bar',
|
|
87
|
+
custom_colors=['#1f77b4', '#0096c7', '#03045e'],
|
|
88
|
+
plot_kwargs={'alpha': 0.8, 'width': 0.04}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return plot_base_series(
|
|
92
|
+
df=df,
|
|
93
|
+
primary_conf=primary_conf,
|
|
94
|
+
title=title,
|
|
95
|
+
figsize=figsize,
|
|
96
|
+
save_path=save_path
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def plot_nivel_e_chuva(
|
|
101
|
+
df_nivel: pd.DataFrame,
|
|
102
|
+
df_chuva: pd.DataFrame,
|
|
103
|
+
title: str = "Hidrograma: Nível do Mar e Precipitação (NPH-UNISANTA)",
|
|
104
|
+
level_columns: Optional[List[str]] = None,
|
|
105
|
+
precip_columns: Optional[List[str]] = None,
|
|
106
|
+
thresholds: Optional[List[Threshold]] = None,
|
|
107
|
+
show_thresholds: bool = True,
|
|
108
|
+
show_peaks: bool = True,
|
|
109
|
+
invert_rain_axis: bool = True,
|
|
110
|
+
figsize: Tuple[int, int] = (12, 6),
|
|
111
|
+
save_path: Optional[str] = None
|
|
112
|
+
) -> go.Figure:
|
|
113
|
+
if df_nivel.empty:
|
|
114
|
+
raise ValueError("O DataFrame de nível do mar está vazio.")
|
|
115
|
+
if df_chuva.empty:
|
|
116
|
+
raise ValueError("O DataFrame de chuva está vazio.")
|
|
117
|
+
|
|
118
|
+
if not show_thresholds:
|
|
119
|
+
thresholds_to_use = []
|
|
120
|
+
elif thresholds is not None:
|
|
121
|
+
thresholds_to_use = thresholds
|
|
122
|
+
else:
|
|
123
|
+
thresholds_to_use = get_ppdc_sea_level_thresholds()
|
|
124
|
+
|
|
125
|
+
primary_conf = AxisConfig(
|
|
126
|
+
label="Nível do Mar (m)",
|
|
127
|
+
columns=level_columns,
|
|
128
|
+
plot_type='line',
|
|
129
|
+
show_peaks=show_peaks,
|
|
130
|
+
thresholds=thresholds_to_use,
|
|
131
|
+
color_palette='Ocean'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
secondary_conf = AxisConfig(
|
|
135
|
+
label="Precipitação (mm)",
|
|
136
|
+
columns=precip_columns,
|
|
137
|
+
plot_type='bar',
|
|
138
|
+
invert_yaxis=invert_rain_axis,
|
|
139
|
+
custom_colors=['#2b5c8f', '#4682b4'],
|
|
140
|
+
plot_kwargs={'alpha': 0.65, 'width': 0.04}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return plot_base_series(
|
|
144
|
+
df=[df_nivel, df_chuva],
|
|
145
|
+
primary_conf=primary_conf,
|
|
146
|
+
secondary_conf=secondary_conf,
|
|
147
|
+
title=title,
|
|
148
|
+
figsize=figsize,
|
|
149
|
+
save_path=save_path
|
|
150
|
+
)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import time
|
|
5
|
+
from datetime import date, datetime, timedelta
|
|
6
|
+
from dateutil.relativedelta import relativedelta
|
|
7
|
+
from typing import List, Dict, Union, Optional, Any
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
_STATION_COLUMNS = [
|
|
12
|
+
"id", "station_prefix_id", "station_prefix", "station_name",
|
|
13
|
+
"latitude", "longitude", "station_type", "city_name",
|
|
14
|
+
"station_owner", "station_operator", "ugrhi_name",
|
|
15
|
+
"subugrhi_name", "station_status"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
_STATION_RENAME_MAP = {
|
|
19
|
+
"station_prefix_id": "id_sibh",
|
|
20
|
+
"station_prefix": "prefixo",
|
|
21
|
+
"station_name": "nome_da_estacao",
|
|
22
|
+
"latitude": "latitude",
|
|
23
|
+
"longitude": "longitude",
|
|
24
|
+
"station_type": "tipo_da_estacao",
|
|
25
|
+
"city_name": "cidade",
|
|
26
|
+
"station_owner": "proprietario",
|
|
27
|
+
"station_operator": "operador",
|
|
28
|
+
"ugrhi_name": "ugrhi",
|
|
29
|
+
"sub_ugrhi_name": "sub_ugrhi",
|
|
30
|
+
"station_status": "status"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
MODELS_GLOSSARY = {
|
|
34
|
+
"WRF 3km": 1,
|
|
35
|
+
"WRF 1km": 2,
|
|
36
|
+
"ECMWF 9km": 3,
|
|
37
|
+
"WRF 7km": 13,
|
|
38
|
+
"GFS 25km": 15,
|
|
39
|
+
"MOHID": 4,
|
|
40
|
+
"Delft - FLOW (BAIXADA)": 5,
|
|
41
|
+
"Delft - WAVE (BAIXADA)": 6,
|
|
42
|
+
"Delft - FLOW (SESSV)": 7,
|
|
43
|
+
"Delft - WAVE (SESSV)": 8,
|
|
44
|
+
"SWAN v1": 9,
|
|
45
|
+
"SWAN v2": 10,
|
|
46
|
+
"CMS - Hydro Hourly": 11,
|
|
47
|
+
"CMS - Wave": 12,
|
|
48
|
+
"NALA 10min": 17,
|
|
49
|
+
"NALA 1h": 18,
|
|
50
|
+
"NALA 3h": 19,
|
|
51
|
+
"NALA Best": 20
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
PARAMS_GLOSSARY = {
|
|
55
|
+
"Temperatura": 1,
|
|
56
|
+
"Precipitação": 2,
|
|
57
|
+
"Pressão": 3,
|
|
58
|
+
"Rajada de Vento": 4,
|
|
59
|
+
"Velocidade do Vento": 5,
|
|
60
|
+
"Direção do Vento": 6,
|
|
61
|
+
"CAPE": 7,
|
|
62
|
+
"LIFT": 8,
|
|
63
|
+
"Umidade Relativa": 9,
|
|
64
|
+
"Cobertura Total de Nuvens": 10,
|
|
65
|
+
"Ponto de Orvalho": 11,
|
|
66
|
+
"Precipitação Convectiva": 12,
|
|
67
|
+
"Nível do Mar": 13,
|
|
68
|
+
"Direção da Corrente": 14,
|
|
69
|
+
"Velocidade da Corrente": 15,
|
|
70
|
+
"Altura Significativa das Ondas (Hs)": 16,
|
|
71
|
+
"Direção média das ondas": 17,
|
|
72
|
+
"Período de Pico (Tp)": 18,
|
|
73
|
+
"Direção de Pico das Ondas": 19
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class NPHClient:
|
|
78
|
+
|
|
79
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
80
|
+
self.api_key = api_key
|
|
81
|
+
self.base_url = 'https://nph.unisanta.br/ssbs/api'
|
|
82
|
+
|
|
83
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
84
|
+
return {
|
|
85
|
+
'apiKey': self.api_key or '',
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _resolve_value_col(status: int) -> str:
|
|
91
|
+
if status == 0:
|
|
92
|
+
return 'primary_value'
|
|
93
|
+
elif status == 1:
|
|
94
|
+
return 'secondary_value'
|
|
95
|
+
elif status == 2:
|
|
96
|
+
return 'extra_values'
|
|
97
|
+
else:
|
|
98
|
+
return 'primary_value'
|
|
99
|
+
|
|
100
|
+
def _fetch_paginated_data(
|
|
101
|
+
self,
|
|
102
|
+
url: str,
|
|
103
|
+
start_dt: Optional[Union[datetime, bool]] = None,
|
|
104
|
+
end_dt: Optional[Union[datetime, bool]] = None,
|
|
105
|
+
base_params: Optional[Dict[str, Any]] = None
|
|
106
|
+
) -> pd.DataFrame:
|
|
107
|
+
base_params = base_params or {}
|
|
108
|
+
|
|
109
|
+
if not start_dt or not end_dt:
|
|
110
|
+
try:
|
|
111
|
+
response = requests.get(url=url, headers=self._get_headers(), params=base_params)
|
|
112
|
+
response.raise_for_status()
|
|
113
|
+
data = response.json()
|
|
114
|
+
return pd.DataFrame(data) if data else pd.DataFrame()
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error(f"Erro ao buscar dados: {e}")
|
|
117
|
+
return pd.DataFrame()
|
|
118
|
+
|
|
119
|
+
all_dataframes = []
|
|
120
|
+
current_start = start_dt
|
|
121
|
+
|
|
122
|
+
while current_start < end_dt:
|
|
123
|
+
chunk_end = min(current_start + relativedelta(years=1), end_dt)
|
|
124
|
+
params = base_params.copy()
|
|
125
|
+
params['startDate'] = current_start.strftime('%Y-%m-%d %H:%M:%S')
|
|
126
|
+
params['endDate'] = chunk_end.strftime('%Y-%m-%d %H:%M:%S')
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
response = requests.get(url=url, headers=self._get_headers(), params=params)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
data = response.json()
|
|
132
|
+
if data:
|
|
133
|
+
all_dataframes.append(pd.DataFrame(data))
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"Erro ao buscar intervalo {params['startDate']}: {e}")
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
current_start = chunk_end
|
|
139
|
+
if current_start < end_dt:
|
|
140
|
+
time.sleep(0.05)
|
|
141
|
+
|
|
142
|
+
if not all_dataframes:
|
|
143
|
+
return pd.DataFrame()
|
|
144
|
+
|
|
145
|
+
return pd.concat(all_dataframes, ignore_index=True)
|
|
146
|
+
|
|
147
|
+
def get_stations(self) -> pd.DataFrame:
|
|
148
|
+
url = f"{self.base_url}/stations"
|
|
149
|
+
try:
|
|
150
|
+
response = requests.get(url=url, headers=self._get_headers())
|
|
151
|
+
response.raise_for_status()
|
|
152
|
+
data = response.json()
|
|
153
|
+
if not data:
|
|
154
|
+
return pd.DataFrame()
|
|
155
|
+
|
|
156
|
+
df = pd.DataFrame(data)
|
|
157
|
+
cols_to_keep = [c for c in _STATION_COLUMNS if c in df.columns]
|
|
158
|
+
return df[cols_to_keep].set_index('id').rename(columns=_STATION_RENAME_MAP)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Erro ao buscar estações: {e}")
|
|
161
|
+
return pd.DataFrame()
|
|
162
|
+
|
|
163
|
+
def get_measurements(
|
|
164
|
+
self,
|
|
165
|
+
stations: Optional[Union[List[Union[str, int]], str, int]] = None,
|
|
166
|
+
start_date: Optional[Union[str, date]] = None,
|
|
167
|
+
end_date: Optional[Union[str, datetime]] = None,
|
|
168
|
+
return_value: int = 0,
|
|
169
|
+
time_offset: Optional[timedelta] = timedelta(hours=-3)
|
|
170
|
+
) -> pd.DataFrame:
|
|
171
|
+
start_dt = pd.to_datetime(start_date or (date.today() - timedelta(days=7)))
|
|
172
|
+
end_dt = pd.to_datetime(end_date or datetime.now())
|
|
173
|
+
|
|
174
|
+
base_params = {}
|
|
175
|
+
if stations:
|
|
176
|
+
if isinstance(stations, (str, int)):
|
|
177
|
+
base_params['stationId'] = str(stations)
|
|
178
|
+
elif isinstance(stations, list):
|
|
179
|
+
base_params['stationId'] = ','.join(map(str, stations))
|
|
180
|
+
|
|
181
|
+
df = self._fetch_paginated_data(
|
|
182
|
+
url=f"{self.base_url}/measurements",
|
|
183
|
+
start_dt=start_dt,
|
|
184
|
+
end_dt=end_dt,
|
|
185
|
+
base_params=base_params
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if df.empty:
|
|
189
|
+
return df
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
df['datetime'] = pd.to_datetime(df['datetime'])
|
|
193
|
+
value_col = self._resolve_value_col(return_value)
|
|
194
|
+
|
|
195
|
+
if time_offset:
|
|
196
|
+
df['datetime'] = df['datetime'] + time_offset
|
|
197
|
+
|
|
198
|
+
col_name = 'station_name' if 'station_name' in df.columns else 'station_id'
|
|
199
|
+
df_pivot = df.pivot_table(index='datetime', columns=col_name, values=value_col, aggfunc='mean')
|
|
200
|
+
df_pivot = df_pivot.sort_index()
|
|
201
|
+
return df_pivot
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"Erro ao processar medições: {e}")
|
|
204
|
+
return pd.DataFrame()
|
|
205
|
+
|
|
206
|
+
def get_forecasts(
|
|
207
|
+
self,
|
|
208
|
+
stations: Optional[Union[List[Union[str, int]], str, int]] = None,
|
|
209
|
+
start_date: Optional[Union[str, date]] = None,
|
|
210
|
+
end_date: Optional[Union[str, datetime]] = None,
|
|
211
|
+
model_id: Union[int, str] = 1,
|
|
212
|
+
param_id: Union[int, str] = "Nível do Mar",
|
|
213
|
+
time_offset: Optional[timedelta] = timedelta(hours=-3)
|
|
214
|
+
) -> pd.DataFrame:
|
|
215
|
+
start_dt = pd.to_datetime(start_date or (date.today() - timedelta(days=7)))
|
|
216
|
+
end_dt = pd.to_datetime(end_date or (datetime.now() + timedelta(days=3)))
|
|
217
|
+
|
|
218
|
+
if isinstance(model_id, str):
|
|
219
|
+
model_id = MODELS_GLOSSARY.get(model_id, 1)
|
|
220
|
+
|
|
221
|
+
if isinstance(param_id, str):
|
|
222
|
+
param_id = PARAMS_GLOSSARY.get(param_id, 13)
|
|
223
|
+
|
|
224
|
+
base_params = {
|
|
225
|
+
'modelId': str(model_id),
|
|
226
|
+
'parameterId': str(param_id)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if stations:
|
|
230
|
+
if isinstance(stations, (str, int)):
|
|
231
|
+
base_params['stationId'] = str(stations)
|
|
232
|
+
elif isinstance(stations, list):
|
|
233
|
+
base_params['stationId'] = ','.join(map(str, stations))
|
|
234
|
+
|
|
235
|
+
df = self._fetch_paginated_data(
|
|
236
|
+
url=f"{self.base_url}/forecasts",
|
|
237
|
+
start_dt=start_dt,
|
|
238
|
+
end_dt=end_dt,
|
|
239
|
+
base_params=base_params
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if df.empty:
|
|
243
|
+
return df
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
df['datetime'] = pd.to_datetime(df['datetime'])
|
|
247
|
+
if time_offset:
|
|
248
|
+
df['datetime'] = df['datetime'] + time_offset
|
|
249
|
+
|
|
250
|
+
col_name = 'station_name' if 'station_name' in df.columns else 'station_id'
|
|
251
|
+
value_col = 'value' if 'value' in df.columns else df.columns[-1]
|
|
252
|
+
df_pivot = df.pivot_table(index='datetime', columns=col_name, values=value_col, aggfunc='mean')
|
|
253
|
+
df_pivot = df_pivot.sort_index()
|
|
254
|
+
return df_pivot
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"Erro ao processar previsões: {e}")
|
|
257
|
+
return pd.DataFrame()
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import List, Optional, Tuple, Dict, Any, Literal, Union
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
from plotly.subplots import make_subplots
|
|
8
|
+
import plotly.colors as pcolors
|
|
9
|
+
|
|
10
|
+
from .thresholds import Threshold
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
LINESTYLE_MAP = {
|
|
15
|
+
'-': 'solid',
|
|
16
|
+
'--': 'dash',
|
|
17
|
+
':': 'dot',
|
|
18
|
+
'-.': 'dashdot'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
PALETTE_MAP = {
|
|
22
|
+
'tab10': pcolors.qualitative.T10,
|
|
23
|
+
'Set1': pcolors.qualitative.Set1,
|
|
24
|
+
'Dark2': pcolors.qualitative.Dark2,
|
|
25
|
+
'Plotly': pcolors.qualitative.Plotly,
|
|
26
|
+
'Ocean': ['#0077b6', '#00b4d8', '#90e0ef', '#03045e', '#0096c7']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AxisConfig:
|
|
32
|
+
label: str = "Valor"
|
|
33
|
+
columns: Optional[List[str]] = None
|
|
34
|
+
plot_type: Literal['line', 'scatter', 'bar', 'area'] = 'line'
|
|
35
|
+
min_val: Optional[float] = None
|
|
36
|
+
max_val: Optional[float] = None
|
|
37
|
+
step: Optional[float] = None
|
|
38
|
+
show_peaks: bool = False
|
|
39
|
+
color_palette: str = 'tab10'
|
|
40
|
+
invert_yaxis: bool = False
|
|
41
|
+
custom_colors: Optional[List[str]] = None
|
|
42
|
+
plot_kwargs: Dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
thresholds: List[Threshold] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_palette_colors(palette_name: str) -> List[str]:
|
|
47
|
+
return PALETTE_MAP.get(palette_name, pcolors.qualitative.T10)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def hex_to_rgba(hex_str: str, alpha: float) -> str:
|
|
51
|
+
try:
|
|
52
|
+
hex_str = hex_str.lstrip('#')
|
|
53
|
+
if len(hex_str) == 3:
|
|
54
|
+
hex_str = ''.join([c * 2 for c in hex_str])
|
|
55
|
+
r, g, b = (int(hex_str[i:i+2], 16) for i in (0, 2, 4))
|
|
56
|
+
return f"rgba({r}, {g}, {b}, {alpha})"
|
|
57
|
+
except Exception:
|
|
58
|
+
return f"rgba(31, 119, 180, {alpha})"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def annotate_peaks(fig: go.Figure, series: pd.Series, color: str, secondary_y: bool):
|
|
62
|
+
clean_series = series.dropna()
|
|
63
|
+
if clean_series.empty:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
max_val = clean_series.max()
|
|
67
|
+
max_date = clean_series.idxmax()
|
|
68
|
+
max_date_str = max_date.isoformat() if hasattr(max_date, 'isoformat') else str(max_date)
|
|
69
|
+
|
|
70
|
+
min_val = clean_series.min()
|
|
71
|
+
min_date = clean_series.idxmin()
|
|
72
|
+
min_date_str = min_date.isoformat() if hasattr(min_date, 'isoformat') else str(min_date)
|
|
73
|
+
|
|
74
|
+
fig.add_trace(
|
|
75
|
+
go.Scatter(
|
|
76
|
+
x=[max_date_str],
|
|
77
|
+
y=[max_val],
|
|
78
|
+
mode='markers+text',
|
|
79
|
+
marker=dict(color=color, size=10, line=dict(color='black', width=1.5)),
|
|
80
|
+
text=[f"Máx: {max_val:.2f}"],
|
|
81
|
+
textposition="top center",
|
|
82
|
+
textfont=dict(color='black', size=11, family="Arial"),
|
|
83
|
+
showlegend=False,
|
|
84
|
+
hoverinfo='skip'
|
|
85
|
+
),
|
|
86
|
+
secondary_y=secondary_y
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if min_date != max_date:
|
|
90
|
+
fig.add_trace(
|
|
91
|
+
go.Scatter(
|
|
92
|
+
x=[min_date_str],
|
|
93
|
+
y=[min_val],
|
|
94
|
+
mode='markers+text',
|
|
95
|
+
marker=dict(color=color, size=9, line=dict(color='black', width=1)),
|
|
96
|
+
text=[f"Mín: {min_val:.2f}"],
|
|
97
|
+
textposition="bottom center",
|
|
98
|
+
textfont=dict(color='#555555', size=10, family="Arial"),
|
|
99
|
+
showlegend=False,
|
|
100
|
+
hoverinfo='skip'
|
|
101
|
+
),
|
|
102
|
+
secondary_y=secondary_y
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def draw_thresholds(fig: go.Figure, config: AxisConfig, secondary_y: bool, y_range: Tuple[float, float]):
|
|
107
|
+
yref = 'y2' if secondary_y else 'y'
|
|
108
|
+
y_min, y_max = y_range
|
|
109
|
+
|
|
110
|
+
for t in config.thresholds:
|
|
111
|
+
dash_style = LINESTYLE_MAP.get(t.linestyle, 'dash')
|
|
112
|
+
|
|
113
|
+
fig.add_hline(
|
|
114
|
+
y=t.value,
|
|
115
|
+
line=dict(color=t.color, width=1.8, dash=dash_style),
|
|
116
|
+
annotation_text=f" <b>{t.label}</b> ({t.value:.2f}m)",
|
|
117
|
+
annotation_position="top left",
|
|
118
|
+
annotation_font=dict(color=t.color, size=11, family="Arial"),
|
|
119
|
+
yref=yref
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
y0 = t.value if t.fill_above else y_min
|
|
123
|
+
y1 = (t.max_fill_value if t.max_fill_value is not None else y_max) if t.fill_above else t.value
|
|
124
|
+
|
|
125
|
+
fig.add_hrect(
|
|
126
|
+
y0=y0,
|
|
127
|
+
y1=y1,
|
|
128
|
+
fillcolor=t.color,
|
|
129
|
+
opacity=t.alpha,
|
|
130
|
+
layer="below",
|
|
131
|
+
line_width=0,
|
|
132
|
+
yref=yref
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def add_traces(fig: go.Figure, df: pd.DataFrame, config: AxisConfig, secondary_y: bool):
|
|
137
|
+
cols_to_plot = config.columns if config.columns else df.columns
|
|
138
|
+
colors = config.custom_colors or get_palette_colors(config.color_palette)
|
|
139
|
+
|
|
140
|
+
for i, col in enumerate(cols_to_plot):
|
|
141
|
+
if col not in df.columns:
|
|
142
|
+
logger.warning(f"Coluna '{col}' não encontrada no DataFrame.")
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
color = colors[i % len(colors)]
|
|
146
|
+
clean_kwargs = {k: v for k, v in config.plot_kwargs.items() if k not in ['alpha', 'width', 'dash', 'size']}
|
|
147
|
+
|
|
148
|
+
hovertemplate = (
|
|
149
|
+
f"<b>{col}</b><br>"
|
|
150
|
+
"<b>Data/Hora:</b> %{x|%d/%m/%Y %H:%M}<br>"
|
|
151
|
+
f"<b>{config.label}:</b> %{{y:.2f}}<extra></extra>"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if config.plot_type == 'bar':
|
|
155
|
+
opacity = config.plot_kwargs.get('alpha', 0.75)
|
|
156
|
+
bar_width = None
|
|
157
|
+
if 'width' in config.plot_kwargs:
|
|
158
|
+
w = config.plot_kwargs['width']
|
|
159
|
+
bar_width = w * 86400000 if isinstance(df.index, pd.DatetimeIndex) else w
|
|
160
|
+
|
|
161
|
+
trace = go.Bar(
|
|
162
|
+
x=df.index,
|
|
163
|
+
y=df[col],
|
|
164
|
+
name=f"{col} (Chuva)",
|
|
165
|
+
marker_color=color,
|
|
166
|
+
opacity=opacity,
|
|
167
|
+
width=bar_width,
|
|
168
|
+
hovertemplate=hovertemplate,
|
|
169
|
+
**clean_kwargs
|
|
170
|
+
)
|
|
171
|
+
elif config.plot_type == 'scatter':
|
|
172
|
+
marker_size = config.plot_kwargs.get('size', 8)
|
|
173
|
+
trace = go.Scatter(
|
|
174
|
+
x=df.index,
|
|
175
|
+
y=df[col],
|
|
176
|
+
name=col,
|
|
177
|
+
mode='markers',
|
|
178
|
+
marker=dict(color=color, size=marker_size),
|
|
179
|
+
hovertemplate=hovertemplate,
|
|
180
|
+
**clean_kwargs
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
line_dict = dict(color=color, width=2.5)
|
|
184
|
+
if 'dash' in config.plot_kwargs:
|
|
185
|
+
line_dict['dash'] = config.plot_kwargs['dash']
|
|
186
|
+
trace = go.Scatter(
|
|
187
|
+
x=df.index,
|
|
188
|
+
y=df[col],
|
|
189
|
+
name=col,
|
|
190
|
+
mode='lines',
|
|
191
|
+
line=line_dict,
|
|
192
|
+
hovertemplate=hovertemplate,
|
|
193
|
+
**clean_kwargs
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
fig.add_trace(trace, secondary_y=secondary_y)
|
|
197
|
+
|
|
198
|
+
if config.show_peaks and config.plot_type != 'bar':
|
|
199
|
+
annotate_peaks(fig, df[col], color, secondary_y)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def plot_base_series(
|
|
203
|
+
df: Union[pd.DataFrame, List[pd.DataFrame]],
|
|
204
|
+
primary_conf: AxisConfig,
|
|
205
|
+
secondary_conf: Optional[AxisConfig] = None,
|
|
206
|
+
title: str = "Série Temporal NPH",
|
|
207
|
+
figsize: Tuple[int, int] = (12, 6),
|
|
208
|
+
save_path: Optional[str] = None
|
|
209
|
+
) -> go.Figure:
|
|
210
|
+
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
211
|
+
|
|
212
|
+
if isinstance(df, list):
|
|
213
|
+
df_primary = df[0]
|
|
214
|
+
df_secondary = df[1] if len(df) > 1 else df[0]
|
|
215
|
+
else:
|
|
216
|
+
df_primary = df
|
|
217
|
+
df_secondary = df
|
|
218
|
+
|
|
219
|
+
add_traces(fig, df_primary, primary_conf, secondary_y=False)
|
|
220
|
+
|
|
221
|
+
if secondary_conf:
|
|
222
|
+
add_traces(fig, df_secondary, secondary_conf, secondary_y=True)
|
|
223
|
+
|
|
224
|
+
primary_cols = [c for c in (primary_conf.columns or df_primary.columns) if c in df_primary.columns]
|
|
225
|
+
|
|
226
|
+
if primary_cols and not df_primary[primary_cols].empty:
|
|
227
|
+
p_min = df_primary[primary_cols].min().min()
|
|
228
|
+
p_max = df_primary[primary_cols].max().max()
|
|
229
|
+
p_min, p_max = (0.0, 1.0) if pd.isna(p_min) or pd.isna(p_max) else (p_min, p_max)
|
|
230
|
+
else:
|
|
231
|
+
p_min, p_max = 0.0, 1.0
|
|
232
|
+
|
|
233
|
+
all_p_vals = [p_min, p_max] + [t.value for t in primary_conf.thresholds]
|
|
234
|
+
y1_min_val, y1_max_val = min(all_p_vals), max(all_p_vals)
|
|
235
|
+
pad1 = (y1_max_val - y1_min_val) * 0.15 if y1_max_val != y1_min_val else 1.0
|
|
236
|
+
|
|
237
|
+
y1_min = primary_conf.min_val if primary_conf.min_val is not None else y1_min_val - pad1
|
|
238
|
+
y1_max = primary_conf.max_val if primary_conf.max_val is not None else y1_max_val + pad1
|
|
239
|
+
y1_range = (y1_min, y1_max)
|
|
240
|
+
|
|
241
|
+
y1_layout = dict(
|
|
242
|
+
title=dict(text=primary_conf.label, font=dict(family="Arial", size=13, color="#111111")),
|
|
243
|
+
gridcolor="rgba(200, 200, 200, 0.25)",
|
|
244
|
+
showgrid=True,
|
|
245
|
+
range=[y1_max, y1_min] if primary_conf.invert_yaxis else [y1_min, y1_max]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
y2_range = (0.0, 10.0)
|
|
249
|
+
y2_layout = dict(showgrid=False)
|
|
250
|
+
|
|
251
|
+
if secondary_conf:
|
|
252
|
+
scols = [c for c in (secondary_conf.columns or df_secondary.columns) if c in df_secondary.columns]
|
|
253
|
+
if scols and not df_secondary[scols].empty:
|
|
254
|
+
s_min = df_secondary[scols].min().min()
|
|
255
|
+
s_max = df_secondary[scols].max().max()
|
|
256
|
+
s_min, s_max = (0.0, 10.0) if pd.isna(s_min) or pd.isna(s_max) else (s_min, s_max)
|
|
257
|
+
else:
|
|
258
|
+
s_min, s_max = 0.0, 10.0
|
|
259
|
+
|
|
260
|
+
y2_min = secondary_conf.min_val if secondary_conf.min_val is not None else 0.0
|
|
261
|
+
y2_max = secondary_conf.max_val if secondary_conf.max_val is not None else max(s_max * 2.5, 10.0)
|
|
262
|
+
y2_range = (y2_min, y2_max)
|
|
263
|
+
|
|
264
|
+
y2_layout.update(dict(
|
|
265
|
+
title=dict(text=secondary_conf.label, font=dict(family="Arial", size=13, color="#111111")),
|
|
266
|
+
range=[y2_max, y2_min] if secondary_conf.invert_yaxis else [y2_min, y2_max]
|
|
267
|
+
))
|
|
268
|
+
|
|
269
|
+
if primary_conf.thresholds:
|
|
270
|
+
draw_thresholds(fig, primary_conf, secondary_y=False, y_range=y1_range)
|
|
271
|
+
if secondary_conf and secondary_conf.thresholds:
|
|
272
|
+
draw_thresholds(fig, secondary_conf, secondary_y=True, y_range=y2_range)
|
|
273
|
+
|
|
274
|
+
fig.update_layout(
|
|
275
|
+
title=dict(text=f"<b>{title}</b>", x=0.5, xanchor='center', font=dict(family="Arial", size=16)),
|
|
276
|
+
xaxis=dict(
|
|
277
|
+
title=dict(text="Data / Hora", font=dict(family="Arial", size=12)),
|
|
278
|
+
gridcolor="rgba(200, 200, 200, 0.25)",
|
|
279
|
+
showgrid=True
|
|
280
|
+
),
|
|
281
|
+
yaxis=y1_layout,
|
|
282
|
+
yaxis2=y2_layout,
|
|
283
|
+
width=figsize[0] * 80,
|
|
284
|
+
height=figsize[1] * 80,
|
|
285
|
+
plot_bgcolor='#ffffff',
|
|
286
|
+
paper_bgcolor='#ffffff',
|
|
287
|
+
showlegend=True,
|
|
288
|
+
legend=dict(
|
|
289
|
+
orientation="h",
|
|
290
|
+
yanchor="bottom",
|
|
291
|
+
y=1.02,
|
|
292
|
+
xanchor="center",
|
|
293
|
+
x=0.5
|
|
294
|
+
),
|
|
295
|
+
hovermode="x unified",
|
|
296
|
+
margin=dict(l=60, r=60, t=90, b=60)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if save_path:
|
|
300
|
+
try:
|
|
301
|
+
if save_path.endswith('.html'):
|
|
302
|
+
fig.write_html(save_path)
|
|
303
|
+
else:
|
|
304
|
+
fig.write_image(save_path)
|
|
305
|
+
logger.info(f"Gráfico salvo em {save_path}")
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Erro ao salvar gráfico em {save_path}: {e}")
|
|
308
|
+
|
|
309
|
+
return fig
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Threshold:
|
|
6
|
+
value: float
|
|
7
|
+
color: str
|
|
8
|
+
label: str
|
|
9
|
+
fill_above: bool = True
|
|
10
|
+
max_fill_value: Optional[float] = None
|
|
11
|
+
alpha: float = 0.12
|
|
12
|
+
linestyle: str = '--'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PPDC_THRESHOLD_ATENCAO = Threshold(
|
|
16
|
+
value=1.8,
|
|
17
|
+
color="#f39c12",
|
|
18
|
+
label="ATENÇÃO",
|
|
19
|
+
fill_above=True,
|
|
20
|
+
max_fill_value=2.0,
|
|
21
|
+
alpha=0.15,
|
|
22
|
+
linestyle="--"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
PPDC_THRESHOLD_ALERTA = Threshold(
|
|
26
|
+
value=2.0,
|
|
27
|
+
color="#e74c3c",
|
|
28
|
+
label="ALERTA",
|
|
29
|
+
fill_above=True,
|
|
30
|
+
max_fill_value=None,
|
|
31
|
+
alpha=0.20,
|
|
32
|
+
linestyle="--"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def get_ppdc_sea_level_thresholds() -> List[Threshold]:
|
|
36
|
+
return [PPDC_THRESHOLD_ATENCAO, PPDC_THRESHOLD_ALERTA]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nph-client
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Biblioteca de visualização de dados oceanográficos e meteorológicos do NPH-UNISANTA.
|
|
5
|
+
Author: NPH UNISANTA
|
|
6
|
+
Keywords: nph,nph-client,oceanografia,meteorologia,plotly,graficos,nivel-do-mar,chuva,unisanta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: pandas>=1.5.0
|
|
12
|
+
Requires-Dist: plotly>=5.0.0
|
|
13
|
+
Requires-Dist: requests>=2.25.0
|
|
14
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
15
|
+
Requires-Dist: numpy>=1.20.0
|
|
16
|
+
Requires-Dist: nbformat>=4.2.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: kaleido>=0.2.1; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# nph-client 🌊
|
|
22
|
+
|
|
23
|
+
Biblioteca em Python para geração padronizada e simplificada de gráficos oceanográficos e meteorológicos do **NPH-UNISANTA** (Núcleo de Pesquisas Hidrodinâmicas da Unisanta).
|
|
24
|
+
|
|
25
|
+
## Instalação
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install nph-client
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Principais Funções
|
|
32
|
+
|
|
33
|
+
### 1. Nível do Mar (`plot_nivel_mar`)
|
|
34
|
+
Plota o gráfico de linha de nível do mar com picos destacados e limiares do **PPDC Ressacas**:
|
|
35
|
+
- **OBSERVAÇÃO**: `< 1.8m`
|
|
36
|
+
- **ATENÇÃO**: `1.8m a 2.0m`
|
|
37
|
+
- **ALERTA**: `≥ 2.0m`
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from nph_client import NPHClient, plot_nivel_mar
|
|
41
|
+
|
|
42
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
43
|
+
df_nivel = client.get_measurements(stations=349, start_date="2026-08-05", end_date="2026-08-07")
|
|
44
|
+
|
|
45
|
+
fig = plot_nivel_mar(df=df_nivel, title="Nível do Mar - Santos")
|
|
46
|
+
fig.show()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Dica**: Para ocultar as linhas e faixas dos limiares PPDC, passe `show_thresholds=False`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 2. Precipitação Pluviométrica / Chuva (`plot_chuva`)
|
|
54
|
+
Plota o gráfico de barras para chuva (mm):
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from nph_client import NPHClient, plot_chuva
|
|
58
|
+
|
|
59
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
60
|
+
df_chuva = client.get_measurements(stations=26, start_date="2026-08-05", end_date="2026-08-07")
|
|
61
|
+
|
|
62
|
+
fig = plot_chuva(df=df_chuva, title="Precipitação Pluviométrica - Portão 40")
|
|
63
|
+
fig.show()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 3. Hidrograma Combinado (`plot_nivel_e_chuva`)
|
|
69
|
+
Reúne Nível do Mar e Chuva no mesmo gráfico com eixo secundário:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from nph_client import NPHClient, plot_nivel_e_chuva
|
|
73
|
+
|
|
74
|
+
client = NPHClient(api_key="SUA_API_KEY")
|
|
75
|
+
df_nivel = client.get_measurements(stations=349, start_date="2026-08-05", end_date="2026-08-07")
|
|
76
|
+
df_chuva = client.get_measurements(stations=26, start_date="2026-08-05", end_date="2026-08-07")
|
|
77
|
+
|
|
78
|
+
fig = plot_nivel_e_chuva(df_nivel=df_nivel, df_chuva=df_chuva, title="Hidrograma")
|
|
79
|
+
fig.show()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Requisitos
|
|
83
|
+
- `pandas`
|
|
84
|
+
- `plotly`
|
|
85
|
+
- `requests`
|
|
86
|
+
- `python-dateutil`
|
|
87
|
+
- `numpy`
|
|
88
|
+
- `nbformat`
|
|
89
|
+
|
|
90
|
+
## Licença
|
|
91
|
+
Licenciado sob a licença MIT.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/nph_client/__init__.py
|
|
4
|
+
src/nph_client/charts.py
|
|
5
|
+
src/nph_client/client.py
|
|
6
|
+
src/nph_client/plotting.py
|
|
7
|
+
src/nph_client/thresholds.py
|
|
8
|
+
src/nph_client.egg-info/PKG-INFO
|
|
9
|
+
src/nph_client.egg-info/SOURCES.txt
|
|
10
|
+
src/nph_client.egg-info/dependency_links.txt
|
|
11
|
+
src/nph_client.egg-info/requires.txt
|
|
12
|
+
src/nph_client.egg-info/top_level.txt
|
|
13
|
+
tests/test_graficos.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nph_client
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import plotly.graph_objects as go
|
|
5
|
+
|
|
6
|
+
from nph_client import (
|
|
7
|
+
plot_nivel_mar,
|
|
8
|
+
plot_chuva,
|
|
9
|
+
plot_nivel_e_chuva,
|
|
10
|
+
get_ppdc_sea_level_thresholds,
|
|
11
|
+
NPHClient
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestNPHClient(unittest.TestCase):
|
|
16
|
+
|
|
17
|
+
def setUp(self):
|
|
18
|
+
dates = pd.date_range(start="2026-08-01", periods=48, freq="h")
|
|
19
|
+
np.random.seed(42)
|
|
20
|
+
|
|
21
|
+
nivel = 1.6 + 0.5 * np.sin(np.linspace(0, 4 * np.pi, 48)) + np.random.normal(0, 0.1, 48)
|
|
22
|
+
self.df_nivel = pd.DataFrame({"Praticagem de Santos": nivel}, index=dates)
|
|
23
|
+
|
|
24
|
+
chuva = np.random.exponential(scale=2.0, size=48)
|
|
25
|
+
self.df_chuva = pd.DataFrame({"Portão 40": chuva}, index=dates)
|
|
26
|
+
|
|
27
|
+
def test_plot_nivel_mar(self):
|
|
28
|
+
fig = plot_nivel_mar(self.df_nivel, title="Nível do Mar")
|
|
29
|
+
self.assertIsInstance(fig, go.Figure)
|
|
30
|
+
self.assertEqual(fig.layout.title.text, "<b>Nível do Mar</b>")
|
|
31
|
+
|
|
32
|
+
def test_plot_nivel_mar_opcoes(self):
|
|
33
|
+
fig = plot_nivel_mar(self.df_nivel, show_thresholds=False, show_peaks=False)
|
|
34
|
+
self.assertIsInstance(fig, go.Figure)
|
|
35
|
+
|
|
36
|
+
def test_plot_chuva(self):
|
|
37
|
+
fig = plot_chuva(self.df_chuva, title="Precipitação")
|
|
38
|
+
self.assertIsInstance(fig, go.Figure)
|
|
39
|
+
self.assertEqual(fig.layout.title.text, "<b>Precipitação</b>")
|
|
40
|
+
|
|
41
|
+
def test_plot_nivel_e_chuva(self):
|
|
42
|
+
fig = plot_nivel_e_chuva(self.df_nivel, self.df_chuva, title="Hidrograma")
|
|
43
|
+
self.assertIsInstance(fig, go.Figure)
|
|
44
|
+
self.assertEqual(fig.layout.title.text, "<b>Hidrograma</b>")
|
|
45
|
+
|
|
46
|
+
def test_plot_nivel_e_chuva_sem_limiares(self):
|
|
47
|
+
fig = plot_nivel_e_chuva(self.df_nivel, self.df_chuva, show_thresholds=False)
|
|
48
|
+
self.assertIsInstance(fig, go.Figure)
|
|
49
|
+
|
|
50
|
+
def test_client_init(self):
|
|
51
|
+
client = NPHClient(api_key="teste_key")
|
|
52
|
+
self.assertEqual(client.api_key, "teste_key")
|
|
53
|
+
|
|
54
|
+
def test_ppdc_thresholds(self):
|
|
55
|
+
thresholds = get_ppdc_sea_level_thresholds()
|
|
56
|
+
self.assertEqual(len(thresholds), 2)
|
|
57
|
+
self.assertEqual(thresholds[0].value, 1.8)
|
|
58
|
+
self.assertEqual(thresholds[0].label, "ATENÇÃO")
|
|
59
|
+
self.assertEqual(thresholds[1].value, 2.0)
|
|
60
|
+
self.assertEqual(thresholds[1].label, "ALERTA")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|