Skip to content

Commit 2c48d31

Browse files
authored
fix: Fix and refactor Timeline component in trame (#157)
* start including start date logic * solution to small extend tasks * more frequency and only available Categories * task deletion * output time in seconds * fix freq * fix input conversion problem * add new widget version
1 parent 1ac2be7 commit 2c48d31

12 files changed

Lines changed: 247 additions & 88 deletions

File tree

.github/workflows/typing-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
# working-directory: ./${{ matrix.package-name }}
3131
run: |
3232
python -m pip install --upgrade pip
33-
python -m pip install mypy ruff types-PyYAML
33+
python -m pip install mypy ruff types-PyYAML types-pytz
3434
3535
- name: Typing check with mypy
3636
# working-directory: ./${{ matrix.package-name }}

geos-trame/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ version = "1.0.0"
88
description = "Geos Simulation Modeler"
99
authors = [{name = "GEOS Contributors" }]
1010
maintainers = [{name = "Alexandre Benedicto", email = "alexandre.benedicto@external.totalenergies.com" },
11-
{name = "Paloma Martinez", email = "paloma.martinez@external.totalenergies.com" }]
11+
{name = "Paloma Martinez", email = "paloma.martinez@external.totalenergies.com" },
12+
{name = "Jacques Franc", email= "jacques.franc@external.totalenergies.com" }]
1213
license = {text = "Apache-2.0"}
1314
classifiers = [
1415
"Development Status :: 4 - Beta",
@@ -40,14 +41,14 @@ dependencies = [
4041
"matplotlib==3.9.4",
4142
"trame-matplotlib==2.0.3",
4243
"trame-components==2.4.2",
43-
"trame-gantt==0.1.5",
4444
"mpld3<0.5.11",
4545
"xsdata==24.5",
4646
"xsdata-pydantic[lxml]==24.5",
4747
"pyvista==0.45.2",
4848
"dpath==2.2.0",
4949
"colorcet==3.1.0",
5050
"funcy==2.0",
51+
"pytz==2025.2",
5152
"typing_inspect==0.9.0",
5253
"typing_extensions>=4.12",
5354
"PyYAML",

geos-trame/src/geos/trame/app/core.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3-
# SPDX-FileContributor: Lionel Untereiner
3+
# SPDX-FileContributor: Lionel Untereiner, Jacques Franc
44

55
from trame.ui.vuetify3 import VAppLayout
66
from trame.decorators import TrameApp
@@ -111,7 +111,7 @@ def deck_ui( self ) -> None:
111111
cols=10,
112112
order=2,
113113
):
114-
self.timelineEditor = TimelineEditor( source=self.tree, classes="ma-2", style="height: 40%" )
114+
self.timelineEditor = TimelineEditor( source=self.tree, classes="ma-2", style="height: 60%" )
115115
with vuetify.VRow( classes="mb-6 fill-height", ):
116116

117117
with vuetify.VCol(
@@ -121,7 +121,7 @@ def deck_ui( self ) -> None:
121121
self.deckEditor = DeckEditor(
122122
source=self.tree,
123123
classes="ma-2",
124-
style="flex: 1; height: 100%;",
124+
style="height: 100%;",
125125
)
126126

127127
with vuetify.VCol(
@@ -133,13 +133,13 @@ def deck_ui( self ) -> None:
133133
region_viewer=self.region_viewer,
134134
well_viewer=self.well_viewer,
135135
classes="ma-2",
136-
style="flex: 1; height: 60%; width: 100%;",
136+
style="height: 60%; width: 100%;",
137137
)
138138

139139
self.deckPlotting = DeckPlotting(
140140
source=self.tree,
141141
classes="ma-2",
142-
style="flex: 1; height: 40%; width: 100%;",
142+
style="height: 40%; width: 100%;",
143143
)
144144

145145
def build_ui( self ) -> None:

geos-trame/src/geos/trame/app/deck/tree.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
import os
55
from collections import defaultdict
66
from typing import Any
7+
from datetime import timedelta, datetime
8+
79
import dpath
810
import funcy
911
from pydantic import BaseModel
1012

1113
from xsdata.formats.dataclass.parsers.config import ParserConfig
1214
from xsdata.formats.dataclass.serializers.config import SerializerConfig
1315
from xsdata.utils import text
14-
from xsdata_pydantic.bindings import DictDecoder, XmlContext, XmlSerializer
16+
from xsdata_pydantic.bindings import DictDecoder, XmlContext, XmlSerializer, DictEncoder
1517

1618
from trame_server.controller import Controller
1719
from trame_simput import get_simput_manager
@@ -21,6 +23,12 @@
2123
from geos.trame.schema_generated.schema_mod import Problem, Included, File, Functions
2224
from geos.trame.app.utils.file_utils import normalize_path, format_xml
2325

26+
import logging
27+
28+
date_fmt = "%Y-%m-%d"
29+
logger = logging.getLogger( "tree" )
30+
logger.setLevel( logging.ERROR )
31+
2432

2533
class DeckTree( object ):
2634
"""A tree that represents a deck file along with all the available blocks and parameters."""
@@ -36,6 +44,8 @@ def __init__( self, sm_id: str | None = None, ctrl: Controller = None, **kwargs:
3644
self.input_has_errors = False
3745
self._sm_id = sm_id
3846
self._ctrl = ctrl
47+
self.world_origin_time = datetime( 1924, 3, 28 ).strftime( date_fmt ) # Total start date !!
48+
self.registered_targets: dict = {}
3949

4050
def set_input_file( self, input_filename: str ) -> None:
4151
"""Set a new input file.
@@ -79,6 +89,12 @@ def update( self, path: str, key: str, value: Any ) -> None:
7989
assert self.input_file is not None and self.input_file.pb_dict is not None
8090
self.input_file.pb_dict = funcy.set_in( self.input_file.pb_dict, new_path, value )
8191

92+
def drop( self, path: str ) -> None:
93+
"""Remove in the tree."""
94+
new_path = [ int( x ) if x.isdigit() else x for x in path.split( "/" ) ]
95+
assert self.input_file is not None and self.input_file.pb_dict is not None
96+
self.input_file.pb_dict = funcy.del_in( self.input_file.pb_dict, new_path )
97+
8298
def _search( self, path: str ) -> list | None:
8399
new_path = path.split( "/" )
84100
if self.input_file is None:
@@ -99,6 +115,17 @@ def decode( self, path: str ) -> BaseModel | None:
99115
decoder = DictDecoder( context=context, config=ParserConfig() )
100116
return decoder.decode( data[ 0 ] )
101117

118+
@staticmethod
119+
def encode_data( data: BaseModel ) -> dict:
120+
"""Convert a data to a xml serializable file."""
121+
context = XmlContext(
122+
element_name_generator=text.pascal_case,
123+
attribute_name_generator=text.camel_case,
124+
)
125+
encoder = DictEncoder( context=context, config=SerializerConfig( indent=" " ) )
126+
nodeDict: dict = encoder.encode( data )
127+
return nodeDict
128+
102129
@staticmethod
103130
def decode_data( data: dict ) -> Problem:
104131
"""Convert a data to a xml serializable file."""
@@ -133,12 +160,28 @@ def timeline( self ) -> list[ dict ] | None:
133160
timeline = []
134161
# list root events
135162
global_id = 0
136-
for e in self.input_file.problem.events[ 0 ].periodic_event:
163+
all_periodic_events = self.input_file.problem.events[ 0 ].periodic_event
164+
max_time = self.input_file.problem.events[ 0 ].max_time
165+
for e in all_periodic_events:
166+
self.registered_targets[ e.target.split( '/' )[ -1 ] ] = e.target
167+
e.end_time = max_time if float( e.end_time ) > float( max_time ) else e.end_time
168+
#note here float conversion is used to correctly interpret scientific format
137169
item: dict[ str, str | int ] = {
138-
"id": global_id,
139-
"summary": e.name,
140-
"start_date": e.begin_time,
170+
"id":
171+
global_id,
172+
"name":
173+
e.name,
174+
"start": ( datetime.strptime( self.world_origin_time, date_fmt ) +
175+
timedelta( seconds=float( e.begin_time ) ) ).strftime( date_fmt ),
176+
"end": ( datetime.strptime( self.world_origin_time, date_fmt ) +
177+
timedelta( seconds=float( e.end_time ) ) ).strftime( date_fmt ),
178+
"duration":
179+
str( timedelta( seconds=( float( e.end_time ) - float( e.begin_time ) ) ).days ),
180+
"category":
181+
e.target.split( '/' )[ -1 ],
141182
}
183+
if ( int( float( e.time_frequency ) ) > 0 ):
184+
item[ "freq" ] = timedelta( seconds=float( e.time_frequency ) ).days
142185
timeline.append( item )
143186
global_id = global_id + 1
144187

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from trame_client.utils.version import get_version
2+
3+
__version__ = get_version( "gantt-chart" )
4+
5+
__all__ = [
6+
"__version__",
7+
]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pathlib import Path
2+
3+
# Compute local path to serve
4+
serve_path = str( Path( __file__ ).with_name( "serve" ).resolve() )
5+
6+
# Serve directory for JS/CSS files
7+
serve = { "__gantt_chart": serve_path }
8+
9+
# List of JS files to load (usually from the serve path above)
10+
scripts = [ "__gantt_chart/gantt-chart.umd.js" ]
11+
12+
# List of CSS files to load (usually from the serve path above)
13+
# styles = ["__geos_trame/style.css"]
14+
15+
# List of Vue plugins to install/load
16+
vue_use = [ "GanttLib" ]
17+
18+
# Uncomment to add entries to the shared state
19+
# state = {}
20+
21+
22+
# Optional if you want to execute custom initialization at module load
23+
def setup( app, **kwargs ): # noqa
24+
"""Method called at initialization with possibly some custom keyword arguments."""
25+
pass

geos-trame/src/geos/trame/app/gantt_chart/module/serve/gantt-chart.umd.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

geos-trame/src/geos/trame/app/gantt_chart/module/serve/style.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from trame_client.widgets.core import AbstractElement
2+
from .. import module
3+
4+
__all__ = [
5+
"Gantt",
6+
]
7+
8+
9+
#will eventually be a dependency, so we'll skip some type checks
10+
class HtmlElement( AbstractElement ):
11+
12+
def __init__( self, _elem_name, children=None, **kwargs ) -> None: # noqa
13+
super().__init__( _elem_name, children, **kwargs ) # noqa
14+
if self.server:
15+
self.server.enable_module( module )
16+
17+
18+
class Gantt( HtmlElement ):
19+
"""Gantt Editor component.
20+
21+
Properties:
22+
tasks
23+
availableCategoriesList
24+
25+
Emit:
26+
taskUpdated.
27+
"""
28+
29+
def __init__( self, **kwargs ) -> None: #noqa
30+
super().__init__(
31+
"GanttChart",
32+
**kwargs,
33+
)
34+
self._attr_names += [ "tasks", "availableCategoriesList" ]
35+
self._event_names += [ "taskUpdated" ]

geos-trame/src/geos/trame/app/ui/inspector.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ def _on_change( topic: str, ids: list | None = None ) -> None:
6363
if ids is not None and topic == "changed":
6464
for obj_id in ids:
6565
proxy = self.simput_manager.proxymanager.get( obj_id )
66-
#self.tree.decode( obj_id ) # if const function and return not used why ?? to decode context ??
6766
for prop in proxy.edited_property_names:
6867
self.tree.update( obj_id, text.camel_case( prop ), proxy.get_property( prop ) )
6968

0 commit comments

Comments
 (0)