diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 3ea0540..6417467 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -27,18 +27,50 @@ b { font-weight: 600 !important; } +h1, +h2, +h3, +h4, +h5 { + font-weight: 600; +} + h3 { margin: 36px 0 20px 0; } -/* Base Button Styles */ +/* Base Button Styles button { font-weight: 400; background-color: #fff; border-radius: 3px; appearance: auto; border: solid var(--primary-color) 1px; - /* couleur thème foncée */ +} */ + +button.btn.btn-primary, +button.show-hide { + display: block; + border-radius: 3px; + outline: 0; + color: #fff; + border: 0; + height: 30px; + padding-top: 2px; + background-image: linear-gradient( + rgb(209, 96, 73), + rgb(179, 56, 33) 26%, + rgb(159, 36, 22) + ); +} + +button.btn.btn-primary:hover, +button.show-hide:hover { + background-image: linear-gradient( + rgb(239, 126, 103), + rgb(209, 86, 63) 26%, + rgb(189, 66, 52) + ); } button[disabled] { @@ -160,12 +192,13 @@ p.version > a { .table-menu { font-size: 16px; margin: 12px 0 12px 0; - height: 36px; + height: 50px; + display: flex; + align-items: baseline; } .table-menu > * { margin: 8px 16px 8px 0; - float: left; } #source_table { @@ -213,11 +246,11 @@ table.cell-table th { font-family: "Inter", sans-serif; text-align: left; font-weight: 600; - padding-right: 12px; + padding: 2px 12px 4px 2px; border: 1px solid rgb(179, 56, 33) !important; background-color: rgb(179, 56, 33); border-bottom: none !important; - height: 34px; + height: 32px; } .dash-table-container diff --git a/src/figures.py b/src/figures.py index 5ad50ca..a505576 100644 --- a/src/figures.py +++ b/src/figures.py @@ -6,7 +6,7 @@ import plotly.graph_objects as go import polars as pl from dash import dash_table, dcc, html -from src.utils import format_number +from src.utils import data_schema, format_number def get_map_count_marches(df: pl.DataFrame): @@ -91,11 +91,8 @@ def get_yearly_statistics(statistics, today_str) -> html.Div: page_size=10, sort_action="none", filter_action="none", - style_header={ - "border": "solid 1px rgb(179, 56, 33)", - "backgroundColor": "rgb(179, 56, 33)", - "color": "white", - }, + style_header={"fontFamily": "Inter", "fontSize": "16px"}, + style_cell={"fontFamily": "Inter", "fontSize": "16px"}, ) return html.Div(children=table, className="marches_table") @@ -210,11 +207,8 @@ def get_sources_tables(source_path) -> html.Div: ], sort_action="native", markdown_options={"html": True}, - style_header={ - "border": "solid 1px rgb(179, 56, 33)", - "backgroundColor": "rgb(179, 56, 33)", - "color": "white", - }, + style_header={"fontFamily": "Inter", "fontSize": "16px"}, + style_cell={"fontFamily": "Inter", "fontSize": "16px"}, ) return html.Div(children=datatable) @@ -295,20 +289,18 @@ class DataTable(dash_table.DataTable): "lineHeight": "18px", "whiteSpace": "normal", }, - { - "if": {"column_id": "montant"}, - "textAlign": "right", - }, - { - "if": {"column_id": "dureeMois"}, - "textAlign": "right", - }, - { - "if": {"column_id": "titulaire_distance"}, - "textAlign": "right", - }, ] + for key in data_schema.keys(): + field = data_schema[key] + if field["type"] in ["number", "integer"]: + rule = { + "if": {"column_id": field["name"]}, + "textAlign": "right", + # "fontFamily": "Fira Code", + } + style_cell_conditional.append(rule) + # Initialisation de la classe parente avec les arguments super().__init__( id=dtid, @@ -330,6 +322,8 @@ class DataTable(dash_table.DataTable): style_cell_conditional=style_cell_conditional, data_timestamp=0, markdown_options={"html": True}, + style_header={"fontFamily": "Inter", "fontSize": "16px"}, + style_cell={"fontFamily": "Inter", "fontSize": "16px"}, tooltip_duration=8000, tooltip_delay=350, hidden_columns=hidden_columns, @@ -360,13 +354,6 @@ def get_duplicate_matrix() -> html.Div: Passez votre souris sur une case pour avoir les pourcentages exacts. À noter que ces statistiques sont produites avant le dédoublonnement qui a lieu avant la publication en Open Data et sur ce site.""") - # Assuming result_df is your DataFrame with structure: - # | sourceDataset | unique | dataset1 | dataset2 | dataset3 | - # |---------------|--------|----------|----------|----------| - # | dataset1 | 0.8 | | 0.15 | 0.2 | - # | dataset2 | 0.75 | 0.15 | | 0.12 | - # | dataset3 | 0.85 | 0.2 | 0.12 | | - # Extract data z_data = result_df.select(pl.all().exclude("sourceDataset")).fill_null(0).to_numpy() x_labels = result_df.columns[1:] # columns after "sourceDataset" @@ -378,12 +365,6 @@ def get_duplicate_matrix() -> html.Div: z=z_data, x=x_labels, y=y_labels, - # colorscale=[ - # [0, "white"], # 0% → white - # [0.10, "lightblue"], # 1% → light blue (soft start) - # [0.50, "steelblue"], # 50% → medium blue - # [1, "darkblue"], # 100% → dark blue - # ], colorscale=[ [0.0, "white"], # 0% → white [0.10, "lightsalmon"], # 10% → light warm tone @@ -391,8 +372,6 @@ def get_duplicate_matrix() -> html.Div: ], zmin=0, zmax=1, - # texttemplate="%{z:.0%}", # Format as percentage - # textfont={"size": 10, "color": "black"}, # Smaller font hoverongaps=False, showscale=True, hovertemplate=( diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 5d0694f..6f5e5e2 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -71,7 +71,7 @@ layout = [ className="org_year", children=dcc.Dropdown( id="acheteur_year", - options=["Toutes"] + options=["Toutes les années"] + [ str(year) for year in range( @@ -109,6 +109,7 @@ layout = [ html.Button( "Téléchargement au format Excel", id="btn-download-data-acheteur", + className="btn btn-primary", ), dcc.Download(id="download-data-acheteur"), ], @@ -136,6 +137,7 @@ layout = [ html.Button( "Téléchargement désactivé au-delà de 65 000 lignes", id="btn-download-filtered-data-acheteur", + className="btn btn-primary", disabled=True, ), dcc.Download(id="acheteur-download-filtered-data"), @@ -229,7 +231,7 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> tuple: acheteur_siret = url.split("/")[-1] lff = df.lazy() lff = lff.filter(pl.col("acheteur_id") == acheteur_siret) - if acheteur_year and acheteur_year != "Toutes": + if acheteur_year and acheteur_year != "Toutes les années": acheteur_year = int(acheteur_year) lff = lff.filter(pl.col("dateNotification").dt.year() == acheteur_year) lff = lff.sort(["dateNotification", "uid"], descending=True, nulls_last=True) @@ -290,7 +292,7 @@ def download_acheteur_data( def to_bytes(buffer): df_to_download.write_excel( - buffer, worksheet="DECP" if annee in ["Toutes", None] else annee + buffer, worksheet="DECP" if annee in ["Toutes les années", None] else annee ) date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") diff --git a/src/pages/recherche.py b/src/pages/recherche.py index b402c63..44b1271 100644 --- a/src/pages/recherche.py +++ b/src/pages/recherche.py @@ -46,20 +46,20 @@ layout = html.Div( "width": "500px", "border": "1px solid #ccc", "borderRight": "none", - "borderRadius": "4px 0 0 4px", + "borderRadius": "3px 0 0 3px", "padding": "5px 10px", "outline": "none", + "height": "34px", }, ), html.Button( - "🔍", + "=>", id="search-button", + className="btn btn-primary", style={ "border": "1px solid #ccc", - "borderRadius": "0 4px 4px 0", + "borderRadius": "0 3px 3px 0", "marginLeft": "0", - "backgroundColor": "#f0f0f0", - "cursor": "pointer", "height": "auto", # Ensure it matches input height if necessary, often relying on padding/line-height }, ), diff --git a/src/pages/tableau.py b/src/pages/tableau.py index 707820f..20edcf1 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -4,6 +4,7 @@ import urllib.parse import uuid from datetime import datetime +import dash_bootstrap_components as dbc import polars as pl from dash import ( ClientsideFunction, @@ -125,14 +126,15 @@ layout = [ style={"maxWidth": "1000px"}, ), html.Div( - html.Details( - children=[ - html.Summary( - html.H4("Mode d'emploi", style={"textDecoration": "underline"}), - ), - dcc.Markdown( - dangerously_allow_html=True, - children=f""" + [ + dbc.Button("Mode d'emploi", id="tableau_help_open"), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Header")), + dbc.ModalBody( + dcc.Markdown( + dangerously_allow_html=True, + children=f""" ##### Définition des colonnes Pour voir la définition d'une colonne, passez votre souris sur son en-tête. @@ -144,11 +146,9 @@ layout = [ - Champs textuels : la recherche retourne les valeurs qui contiennent le texte recherché et n'est pas sensible à la casse (majuscules/minuscules). - Exemple : `rennes` retourne "RENNES METROPOLE". - Les guillemets simples (apostrophe du 4) doivent être prédédées d'une barre oblique (AltGr + 8). Exemple : `services d\\\'assurances` - - Lorsque vous ouvrez une URL de vue (voir "Partager une vue" plus bas), le format équivalent `icontains rennes` est utilisé. Mais dans vos filtres pas besoin de taper `icontains` ! - Champs numériques (Durée en mois, Montant, ...) : vous pouvez... - soit taper un nombre pour trouver les valeurs strictement égales. Exemple : `12` ne retourne que des 12 - soit le précéder de **>** ou **<** pour filtrer les valeurs supérieures ou inférieures. Exemple pour les offres reçues : `> 4` retourne les marchés ayant reçu plus de 4 offres. - - lorsque vous ouvrez une URL de vue (voir "Partager une vue" plus bas), le format équivalent `i<` ou `i>` est utilisé, mais c'est un bug : vous n'avez pas besoin de taper le `i` pour appliquer ce filtre. - Champs date (Date de notification, ...) : vous pouvez également utiliser **>** ou **<**. Exemples : - `< 2024-01-31` pour "avant le 31 janvier 2024" - `2024` pour "en 2024", `> 2022` pour "à partir de 2022". @@ -188,10 +188,24 @@ layout = [ (informations, marchés attribués/remportés, etc.) """, - ), - ], - id="instructions", - ), + ), + ), + dbc.ModalFooter( + dbc.Button( + "Close", + id="tableau_help_close", + className="ms-auto", + n_clicks=0, + ) + ), + ], + id="tableau_help", + is_open=False, + fullscreen="md-down", + scrollable=True, + size="lg", + ), + ], id="header", ), # html.Div( @@ -210,7 +224,7 @@ layout = [ html.P("lignes", id="nb_rows"), html.Div(id="copy-container"), dcc.Input(id="share-url", readOnly=True, style={"display": "none"}), - html.Button( + dbc.Button( "Téléchargement désactivé au-delà de 65 000 lignes", id="btn-download-data", disabled=True, @@ -389,3 +403,14 @@ def show_confirmation(n_clicks): style={"color": "green", "fontWeight": "bold", "marginLeft": "10px"}, ) return no_update + + +@callback( + Output("tableau_help", "is_open"), + [Input("tableau_help_open", "n_clicks"), Input("tableau_help_close", "n_clicks")], + [State("tableau_help", "is_open")], +) +def toggle_tableau_help(click_open, click_close, is_open): + if click_open or click_close: + return not is_open + return is_open diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index 834ce68..84ec606 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -71,7 +71,7 @@ layout = [ className="org_year", children=dcc.Dropdown( id="titulaire_year", - options=["Toutes"] + options=["Toutes les années"] + [ str(year) for year in range( @@ -235,7 +235,7 @@ def get_titulaire_marches_data(url, titulaire_year: str) -> tuple: (pl.col("titulaire_id") == titulaire_siret) & (pl.col("titulaire_typeIdentifiant") == "SIRET") ) - if titulaire_year and titulaire_year != "Toutes": + if titulaire_year and titulaire_year != "Toutes les années": lff = lff.filter( pl.col("dateNotification").cast(pl.String).str.starts_with(titulaire_year) ) @@ -305,7 +305,7 @@ def download_titulaire_data( def to_bytes(buffer): df_to_download.write_excel( - buffer, worksheet="DECP" if annee in ["Toutes", None] else annee + buffer, worksheet="DECP" if annee in ["Toutes les années", None] else annee ) date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") diff --git a/src/utils.py b/src/utils.py index f9361f9..0cf908f 100644 --- a/src/utils.py +++ b/src/utils.py @@ -284,7 +284,7 @@ def filter_table_data( lff: pl.LazyFrame, filter_query: str, filter_source: str ) -> pl.LazyFrame: _schema = lff.collect_schema() - track_search(f"{filter_source}: {filter_query}") + track_search(filter_query, filter_source) filtering_expressions = filter_query.split(" && ") for filter_part in filtering_expressions: col_name, operator, filter_value = split_filter_part(filter_part) @@ -464,21 +464,15 @@ def get_data_schema() -> dict: return new_schema -def track_search(query): - if ( - len(query) >= 4 - and os.getenv("DEVELOPMENT").lower != "true" - and os.getenv("MATOMO_DOMAIN") - ): - if os.getenv("DEVELOPMENT").lower() == "true": - url = "https://test.decp.info" - else: - url = "https://decp.info" +def track_search(query, category): + if len(query) >= 4 and not development and os.getenv("MATOMO_DOMAIN"): + url = "https://decp.info" params = { "idsite": os.getenv("MATOMO_ID_SITE"), "url": url, "rec": "1", - "action_name": "front_page_search", + "action_name": "search" if category == "home_page_search" else "filter", + "search_cat": category, "rand": uuid.uuid4().hex, "apiv": "1", "h": localtime().tm_hour, @@ -506,7 +500,7 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: return dff.select(pl.lit(False).alias("matches")) # Enregistrement des recherche dans Matomo - track_search(query) + track_search(query, "home_page_search") # Normalize query normalized_query = unidecode(query.strip()).upper()