microsegments 0.2.2__tar.gz → 0.2.3__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.
- {microsegments-0.2.2 → microsegments-0.2.3}/PKG-INFO +1 -1
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/_version.py +2 -2
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/html/export.py +10 -4
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/html/template.html +135 -52
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/plot.py +22 -8
- {microsegments-0.2.2 → microsegments-0.2.3}/.github/workflows/publish.yml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/.github/workflows/test.yml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/.gitignore +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/LICENSE +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/README.md +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/docs/report.jpg +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/examples/gtfsrt_csv.toml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/examples/make_synth.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/examples/stib1_platform.toml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/examples/stib55.toml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/examples/stib55_full.toml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/examples/stib71_platform.toml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/examples/synth.toml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/pyproject.toml +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/__init__.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/aggregate.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/cli.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/compare.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/config.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/contract.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/hotspots.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/html/__init__.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/html/leaflet.min.css +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/__init__.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/coverage.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/events.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/gtfsrt.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/ids.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/stib.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/tabular.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/io/timeutil.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/locate/__init__.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/locate/common.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/locate/linear.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/locate/mapmatch.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/locate/resample.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/metrics.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/network/__init__.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/network/calendar.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/network/geometry.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/network/keys.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/network/patterns.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/pipeline.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/py.typed +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/report.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/scale.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/schema.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/segments.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/simulate.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/tracks.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/src/microsegments/tune.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/buckets.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/drops.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/flen.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/obs_day.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/obs_dow.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/passages.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/summary.json +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/golden/validation.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/agency.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/calendar.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/calendar_dates.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/routes.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/shapes.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/stop_times.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/stops.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/trips.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/punctuality/route55.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/speed55_ref.json +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/vd55/20250318.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/vd55/20250318.snaps.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/vd55/20250319.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/vd55/20250319.snaps.parquet +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/gtfs_fixtures.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/regression/make_golden.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/t4_fixtures.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_aggregate.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_compare.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_coverage.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_hotspots.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_hotspots_criteria.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_io_events.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_io_gtfsrt.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_io_stib.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_io_tabular.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_locate_linear.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_locate_mapmatch.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_metrics.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_network_geometry.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_network_patterns.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_pipeline_cli.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_regression_stib55.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_report_html.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_scaffold.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_segments.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_simulate.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_tracks.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_tune.py +0 -0
- {microsegments-0.2.2 → microsegments-0.2.3}/tests/test_versions_merge.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: microsegments
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Where do transit vehicles linger? Count vehicle position observations per micro-segment of a line, from GTFS + AVL / GTFS-RT positions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/GaspardMerten/microsegments
|
|
6
6
|
Project-URL: Repository, https://github.com/GaspardMerten/microsegments
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -49,12 +49,16 @@ def _default_title(c: dict, lang: str) -> str:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def render(analysis_or_contract, *, title: str | None = None, lang: str = "fr", tiles: bool = True,
|
|
52
|
-
scales: dict | None = None, description: str | None = None, **contract_kw) -> str:
|
|
52
|
+
scales: dict | None = None, description: str | None = None, show_hotspots: bool = True, **contract_kw) -> str:
|
|
53
53
|
"""The full HTML document as a string. ``contract_kw`` go to ``to_contract`` when an Analysis /
|
|
54
54
|
RunResult is given. The colour scale is fixed for every page (``microsegments.scale``); ``scales`` is
|
|
55
|
-
deprecated and ignored.
|
|
55
|
+
deprecated and ignored. ``show_hotspots=False`` hides every hotspot element of the one-period view
|
|
56
|
+
(list, map markers and outlines, tile, method paragraph); the page reads it from the contract key
|
|
57
|
+
``show_hotspots``, so a page fed with its data later can set that key instead."""
|
|
56
58
|
c = dict(_contract(analysis_or_contract, **contract_kw))
|
|
57
59
|
c["tiles"] = bool(tiles)
|
|
60
|
+
if not show_hotspots:
|
|
61
|
+
c["show_hotspots"] = False
|
|
58
62
|
if scales:
|
|
59
63
|
warnings.warn("render(scales=...) is ignored: the colour scale is fixed (microsegments.scale)",
|
|
60
64
|
DeprecationWarning, stacklevel=2)
|
|
@@ -79,11 +83,13 @@ def render(analysis_or_contract, *, title: str | None = None, lang: str = "fr",
|
|
|
79
83
|
|
|
80
84
|
|
|
81
85
|
def export(analysis_or_contract, path: str | Path, *, title: str | None = None, lang: str = "fr",
|
|
82
|
-
standalone: bool = True, tiles: bool = True, scales: dict | None = None,
|
|
86
|
+
standalone: bool = True, tiles: bool = True, scales: dict | None = None, show_hotspots: bool = True,
|
|
87
|
+
**contract_kw) -> Path:
|
|
83
88
|
"""Write the report to ``path``. ``analysis_or_contract``: an ``Analysis``, a ``RunResult`` or a
|
|
84
89
|
contract dict (``report.to_contract``). ``standalone`` (default): a complete HTML document; False:
|
|
85
90
|
only what goes inside ``<body>`` (head elements included inline), to embed in another page."""
|
|
86
|
-
page = render(analysis_or_contract, title=title, lang=lang, tiles=tiles, scales=scales,
|
|
91
|
+
page = render(analysis_or_contract, title=title, lang=lang, tiles=tiles, scales=scales, show_hotspots=show_hotspots,
|
|
92
|
+
**contract_kw)
|
|
87
93
|
if not standalone:
|
|
88
94
|
head = re.search(r"<head>(.*)</head>", page, re.DOTALL).group(1)
|
|
89
95
|
body = re.search(r"<body>(.*)</body>", page, re.DOTALL).group(1)
|
|
@@ -32,6 +32,7 @@ h1 .badge{display:inline-block;background:var(--acc);color:#111;font:700 .62em/1
|
|
|
32
32
|
h2{font:700 24px/1.2 var(--serif);margin:0 0 6px}
|
|
33
33
|
p.lede{font-size:clamp(17px,1.6vw,19px);color:var(--ink-2);max-width:68ch;margin:0}
|
|
34
34
|
.tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--line);border:1px solid var(--line);border-radius:14px;overflow:hidden;margin-top:28px}
|
|
35
|
+
.tiles.t3{grid-template-columns:repeat(3,1fr)}
|
|
35
36
|
.tile{background:var(--surface);padding:18px 18px 16px;min-width:0}
|
|
36
37
|
.tile b{display:block;font:700 clamp(24px,3vw,32px)/1 var(--serif);font-variant-numeric:tabular-nums}
|
|
37
38
|
.tile span{display:block;margin-top:8px;color:var(--ink-2);font-size:14px;line-height:1.4}
|
|
@@ -78,6 +79,15 @@ input[type=range]{width:100%;accent-color:var(--acc)}
|
|
|
78
79
|
.legend .ramp .tk span:last-child{transform:translateX(-100%)}
|
|
79
80
|
.legend .txt{flex:1 1 260px}
|
|
80
81
|
.sw{display:inline-block;width:14px;height:8px;border-radius:2px;vertical-align:middle;margin-right:5px}
|
|
82
|
+
.sw.hatch{background:repeating-linear-gradient(45deg,#5d6a88 0 2px,#18213a 2px 5px)}
|
|
83
|
+
/* short label + '*' that opens a plain-language explanation (hover, tap, Esc / outside click closes) */
|
|
84
|
+
.qm{font:700 13px/1 var(--sans);color:var(--acc);background:transparent;border:1px solid var(--line);border-radius:50%;width:20px;height:20px;padding:0;margin-left:4px;cursor:pointer;vertical-align:middle}
|
|
85
|
+
.qm:hover,.qm[aria-expanded=true]{border-color:var(--acc)}
|
|
86
|
+
.qm:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
|
|
87
|
+
#pop{position:fixed;z-index:3100;max-width:min(340px,calc(100vw - 24px));background:#0f1830;color:var(--ink);border:1px solid var(--line);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.5);font:13px/1.5 var(--sans);padding:9px 12px}
|
|
88
|
+
body.nohs .grid{grid-template-columns:minmax(0,1fr)}
|
|
89
|
+
body.nohs .grid .side{display:none}
|
|
90
|
+
body.nohs #map{height:min(70vh,640px)}
|
|
81
91
|
.legend .hk{display:inline-block;width:16px;height:8px;border:2px solid var(--hot);border-radius:2px;vertical-align:middle;margin-right:5px}
|
|
82
92
|
.side{padding:16px 16px 10px}
|
|
83
93
|
.side .note{font-size:13px;color:var(--ink-3);margin:0 0 10px}
|
|
@@ -114,11 +124,12 @@ svg{display:block}
|
|
|
114
124
|
.cal .calleg{display:flex;gap:6px 14px;flex-wrap:wrap;font-size:12px;color:var(--ink-2)}
|
|
115
125
|
.calwrap{overflow-x:auto;max-width:100%}
|
|
116
126
|
.vtag{font:500 10px var(--mono);letter-spacing:.06em;text-transform:uppercase;color:#c9b36a;border:1px solid #6b5d2e;border-radius:4px;padding:1px 5px;white-space:nowrap;margin-left:6px}
|
|
117
|
-
@media (max-width:900px){.grid{grid-template-columns:minmax(0,1fr)}#map{height:440px}.tiles{grid-template-columns:repeat(2,1fr)}}
|
|
127
|
+
@media (max-width:900px){.grid{grid-template-columns:minmax(0,1fr)}#map,body.nohs #map{height:440px}.tiles{grid-template-columns:repeat(2,1fr)}.tiles.t3{grid-template-columns:repeat(2,1fr)}.tiles.t3 .tile:last-child{grid-column:1/-1}}
|
|
118
128
|
@media (max-width:600px){.wrap{padding-left:16px;padding-right:16px}header{padding-top:24px}.clock{font-size:24px;min-width:90px}}
|
|
119
129
|
</style>
|
|
120
130
|
</head>
|
|
121
131
|
<body>
|
|
132
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><defs><pattern id="mshatch" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)"><rect width="6" height="6" fill="#18213a"/><rect width="2.4" height="6" fill="#5d6a88"/></pattern></defs></svg>
|
|
122
133
|
<header class="wrap">
|
|
123
134
|
<div class="top"><div class="kicker" id="kicker"></div><div class="seg" id="langs" role="group" aria-label="Language"></div></div>
|
|
124
135
|
<h1 id="h1"></h1>
|
|
@@ -178,6 +189,7 @@ svg{display:block}
|
|
|
178
189
|
</main>
|
|
179
190
|
<footer class="wrap" id="footer"></footer>
|
|
180
191
|
<div id="tip" role="tooltip"></div>
|
|
192
|
+
<div id="pop" role="tooltip" hidden></div>
|
|
181
193
|
<script id="ms-data" type="application/json">__DATA__</script>
|
|
182
194
|
<script>
|
|
183
195
|
"use strict";
|
|
@@ -216,7 +228,7 @@ const T = {
|
|
|
216
228
|
towards: t => `vers ${t}`,
|
|
217
229
|
play: '▶ Journée', pause: '❚❚ Pause', mean: 'moyenne',
|
|
218
230
|
tiles: {
|
|
219
|
-
days: (n, x) => `jours analysés${x ?
|
|
231
|
+
days: (n, x) => `jours analysés${x ? ` ; ${x}` : ''}`,
|
|
220
232
|
segs: (n, L) => `microsegments d'environ ${L} m, dans les deux sens`,
|
|
221
233
|
obs: 'positions comptées sur la période',
|
|
222
234
|
hs: (n) => D.hotspots_status === 'not_computed' ? 'points chauds : pas assez de jours pour les repérer' : n ? 'points chauds : des retards répétés d\'un jour à l\'autre' : 'aucun point chaud net sur la période',
|
|
@@ -232,12 +244,18 @@ const T = {
|
|
|
232
244
|
zone: {stop: 'à l\'arrêt', running: 'entre les arrêts'},
|
|
233
245
|
perveh: m1 => `obs. par ${m1}`, ci: (a, b) => `IC 95 % : ${a} à ${b}`, daysPct: p => `${p} % des jours`, secs: s => `≈ ${s} s`,
|
|
234
246
|
legend: {
|
|
235
|
-
oph: (m, u) => `Observations par heure
|
|
236
|
-
opp: (m1, u) => `
|
|
237
|
-
ex: (m1, u) =>
|
|
238
|
-
exl: (m1, u) =>
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
oph: (m, u) => `Observations par heure, pour ${u}`,
|
|
248
|
+
opp: (m1, u) => `Secondes par ${m1}, pour ${u}`,
|
|
249
|
+
ex: (m1, u) => `Écart avec le soir, s par ${m1} pour ${u}`,
|
|
250
|
+
exl: (m1, u) => `Écart avec la ligne, s par ${m1} pour ${u}`,
|
|
251
|
+
help: {
|
|
252
|
+
oph: (m1, m) => `Toutes les ${TICK} s, on note où se trouve chaque ${m1}. Ici : combien de fois, en une heure, un ${m1} est vu sur ce bout de voie. Plus il y en a, plus les ${m} y passent de temps (1 observation ≈ ${TICK} s).`,
|
|
253
|
+
opp: (m1, m) => `Toutes les ${TICK} s, on note où se trouve chaque ${m1}. Plus un bout de voie collecte d'observations, plus les ${m} y restent : vert = ça roule, rouge = ça bloque. 1 observation ≈ ${TICK} s.`,
|
|
254
|
+
ex: (m1, m) => `Le temps que chaque ${m1} perd ici par rapport au soir (${REFH[0]}–${REFH[1]} h), quand la circulation est fluide. Vert : comme le soir ; rouge : nettement plus lent.`,
|
|
255
|
+
exl: (m1, m) => `Le temps que chaque ${m1} perd ici par rapport à son allure habituelle sur la ligne, entre deux arrêts. Fait ressortir un feu ou un carrefour lent à toute heure, même le soir.`,
|
|
256
|
+
},
|
|
257
|
+
masked: 'zone d\'arrêt masquée', nodata: 'pas assez de données', hot: 'point chaud', evening: ' (le soir est la référence)',
|
|
258
|
+
what: 'Explication',
|
|
241
259
|
},
|
|
242
260
|
tip: {
|
|
243
261
|
after: (a, b, s) => `${a} à ${b} m après ${s}`, towards: t => `vers ${t}`,
|
|
@@ -247,15 +265,19 @@ const T = {
|
|
|
247
265
|
exl: (v, s) => `${v} par rapport à la ligne (≈ ${s} s)`, freq: (v, m) => `${v} ${m} par heure`,
|
|
248
266
|
},
|
|
249
267
|
mxtitle: 'Toute la journée d\'un coup d\'œil',
|
|
250
|
-
mxsub: (f, t, d, what, stops) => `De ${f} à ${t}, ${d}
|
|
268
|
+
mxsub: (f, t, d, what, stops, hs) => [`De ${f} à ${t}, ${d}.`, `En colonnes, la ligne découpée en microsegments (traits sombres : arrêts) ; en lignes, les heures. Couleur : ${what}, comme sur la carte${stops ? '' : ' ; hachures : zones d\'arrêt masquées'}.${hs ? ' Contours jaunes : points chauds.' : ''} Cliquez une heure pour l'afficher sur la carte.`],
|
|
251
269
|
full: '⛶ Plein écran', unfull: '✕ Quitter le plein écran',
|
|
252
270
|
proftitle: 'Le long de la ligne, microsegment par microsegment',
|
|
253
|
-
profsub: (f, t, w, ref, m) => `De ${f} à ${t}, ${w}
|
|
271
|
+
profsub: (f, t, w, ref, m) => [`De ${f} à ${t}, ${w}.`, `Hauteur : la mesure choisie dans chaque microsegment. Traits verticaux : arrêts.${ref ? ` Trait blanc : le soir (${REFH[0]}–${REFH[1]} h).` : ''} Pointillés jaunes : ${m} par heure (échelle de droite).`],
|
|
254
272
|
perh: m => `${m}/h`,
|
|
255
273
|
covtitle: 'Les jours retenus',
|
|
256
|
-
covsub: (n, x) => `Une case par jour. Un jour est écarté quand le flux de positions manque ou se fige sur plus d'un quart des heures de ${DAYH[0]} à ${DAYH[1]} h ; une heure est écartée sous 80 % de relevés.
|
|
274
|
+
covsub: (n, x) => [`${n} jours retenus${x ? ` ; ${x}` : ''}.`, `Une case par jour. Un jour est écarté quand le flux de positions manque ou se fige sur plus d'un quart des heures de ${DAYH[0]} à ${DAYH[1]} h ; une heure est écartée sous 80 % de relevés. Survolez une case pour le détail.`],
|
|
257
275
|
cal: {inc: 'retenu', exc: 'écarté', off: 'hors sélection', none: 'pas de données'},
|
|
258
|
-
reasons: {low_coverage: 'trop peu de relevés', frozen: 'flux figé', line_absent: 'ligne absente du flux', deviation: 'trajets hors itinéraire', vehicle_ratio: 'nombre de véhicules anormal', no_data: 'aucune donnée', excluded: 'écarté à la demande', not_selected: 'hors sélection'},
|
|
276
|
+
reasons: {low_coverage: 'trop peu de relevés', frozen: 'flux figé', line_absent: 'ligne absente du flux', deviation: 'trajets hors itinéraire', vehicle_ratio: 'nombre de véhicules anormal', no_data: 'aucune donnée', excluded: 'écarté à la demande', holiday: 'jour férié', not_selected: 'hors sélection'},
|
|
277
|
+
// excluded days per reason, short (tiles, calendar caption): [one, several]
|
|
278
|
+
xsum: (n, parts) => `${n} ${n > 1 ? 'jours écartés' : 'jour écarté'} : ${parts}`,
|
|
279
|
+
xr: {holiday: ['férié', 'fériés'], feed: ['flux incomplet', 'flux incomplets'], line_absent: ['sans la ligne dans le flux', 'sans la ligne dans le flux'],
|
|
280
|
+
deviation: ['hors itinéraire', 'hors itinéraire'], vehicle_ratio: ['nombre de véhicules anormal', 'nombre de véhicules anormal'], excluded: ['à la demande', 'à la demande']},
|
|
259
281
|
covday: (c) => `relevés : ${c} % des heures ${DAYH[0]}–${DAYH[1]} h`,
|
|
260
282
|
vers: (a, b, n) => `${a} – ${b} · ${n} j`, verMain: 'principale', verNote: (r, a) => `${r} tronçon(s) en moins, ${a} en plus que la version principale`,
|
|
261
283
|
mtitle: 'Méthode',
|
|
@@ -267,7 +289,7 @@ const T = {
|
|
|
267
289
|
`<b>Écart avec le soir.</b> Observations par passage à l'heure choisie, moins celles de ${REFH[0]} à ${REFH[1]} h, sur les mêmes jours.`,
|
|
268
290
|
`<b>Écart avec la ligne.</b> Une seconde référence : le niveau habituel de la ligne dans ce sens, soit la médiane des observations par passage et par mètre des microsegments entre arrêts, de 6 à 23 h, multipliée par la longueur du microsegment. Elle fait ressortir ce qui ralentit à toute heure, soir compris.${agreeText('fr')}`,
|
|
269
291
|
`<b>Zones d'arrêt.</b> De ${(P.stop_zone || [30, 60])[0]} m avant à ${(P.stop_zone || [30, 60])[1]} m après chaque arrêt. Les masquer fait ressortir les feux et les carrefours ; les couleurs ne changent pas.`,
|
|
270
|
-
`<b>Échelle.</b>
|
|
292
|
+
`<b>Échelle.</b> Les mêmes couleurs pour toutes les lignes, périodes, jours, heures et options. Chaque mesure est ramenée à 30 m de voie, puis lue sur l'échelle de vitesse de la STIB, du vert (fluide) au rouge (lent) : un passage de 30 m en 4 s fait 27 km/h (vert foncé), en 14 s, 7,5 km/h (rouge), en 24 s ou plus, 4,5 km/h ou moins (rouge foncé). Les écarts (avec le soir, avec la ligne) partagent la même échelle : un écart de +x s se lit comme un passage fluide de 3 s rallongé de x s, donc 0 s en vert, +10 s en rouge, +21 s ou plus en rouge foncé. Les observations par heure se lisent à raison de 2 s par observation (≈ 10 passages par heure) : 1,5 en vert foncé, 12 ou plus en rouge foncé. Afficher « 10 m » ne change que les nombres (divisés par 3), pas les couleurs. Ces repères viennent des données 2025 de plusieurs lignes de tram et de bus : entre les arrêts, un passage prend en médiane 3 à 4,5 s pour 30 m et, au 98e centile, 12 à 23 s.`,
|
|
271
293
|
`<b>Jours.</b> Chaque valeur est un rapport de sommes sur les jours choisis. Moins il y a de jours, plus les valeurs sont bruitées.`,
|
|
272
294
|
`<b>Changements de tracé.</b> Un microsegment n'est compté que les jours où il fait partie de l'itinéraire principal. La carte suit la version la plus fréquente ; les autres versions s'affichent avec le sélecteur.`,
|
|
273
295
|
`<b>Points chauds.</b> Deux critères. Comparé au soir : à certaines heures (au moins deux de suite, ou toute une pointe), chaque ${m1} est observé plus souvent qu'entre ${REFH[0]} et ${REFH[1]} h. Comparé au reste de la ligne, entre les arrêts seulement : sur toute la journée, soirée comprise, plus d'observations par mètre que le niveau habituel de la ligne dans ce sens (la médiane des microsegments entre arrêts) ; c'est ce qui fait ressortir un feu ou un carrefour lent à toute heure, que la comparaison au soir efface. Dans les deux cas : au moins ${HTH} s de plus par ${m1} avec 95 % de confiance (en rééchantillonnant les jours), un écart positif au moins 60 % des jours, et une correction pour les tests multiples. Les microsegments voisins forment un tronçon ; les tronçons sont classés par observations en plus par jour. « Toute la journée » : le retard ne disparaît pas en soirée ; « aux heures de pointe » : il est surtout là aux pointes ; « mixte » : un peu des deux.`,
|
|
@@ -293,7 +315,7 @@ const T = {
|
|
|
293
315
|
towards: t => `to ${t}`,
|
|
294
316
|
play: '▶ Day', pause: '❚❚ Pause', mean: 'average',
|
|
295
317
|
tiles: {
|
|
296
|
-
days: (n, x) => `days analysed${x ?
|
|
318
|
+
days: (n, x) => `days analysed${x ? `; ${x}` : ''}`,
|
|
297
319
|
segs: (n, L) => `micro-segments of about ${L} m, both directions`,
|
|
298
320
|
obs: 'positions counted over the period',
|
|
299
321
|
hs: (n) => D.hotspots_status === 'not_computed' ? 'hotspots: too few days to find them' : n ? 'hotspots: delays that recur day after day' : 'no clear hotspot over the period',
|
|
@@ -309,12 +331,18 @@ const T = {
|
|
|
309
331
|
zone: {stop: 'at a stop', running: 'between stops'},
|
|
310
332
|
perveh: m1 => `obs. per ${m1}`, ci: (a, b) => `95 % CI: ${a} to ${b}`, daysPct: p => `${p} % of days`, secs: s => `≈ ${s} s`,
|
|
311
333
|
legend: {
|
|
312
|
-
oph: (m, u) => `Observations per hour
|
|
313
|
-
opp: (m1, u) => `
|
|
314
|
-
ex: (m1, u) => `
|
|
315
|
-
exl: (m1, u) => `
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
oph: (m, u) => `Observations per hour, per ${u}`,
|
|
335
|
+
opp: (m1, u) => `Seconds per ${m1}, per ${u}`,
|
|
336
|
+
ex: (m1, u) => `Excess over the evening, s per ${m1} per ${u}`,
|
|
337
|
+
exl: (m1, u) => `Excess over the line, s per ${m1} per ${u}`,
|
|
338
|
+
help: {
|
|
339
|
+
oph: (m1, m) => `Every ${TICK} s, we note where each ${m1} is. Here: how many times in an hour a ${m1} is seen on this bit of track. The more, the longer the ${m} spend there (1 observation ≈ ${TICK} s).`,
|
|
340
|
+
opp: (m1, m) => `Every ${TICK} s, we note where each ${m1} is. The more observations a bit of track collects, the longer the ${m} stay there: green = moving, red = stuck. 1 observation ≈ ${TICK} s.`,
|
|
341
|
+
ex: (m1, m) => `The time each ${m1} loses here compared with the evening (${REFH[0]}–${REFH[1]} h), when traffic flows. Green: like the evening; red: clearly slower.`,
|
|
342
|
+
exl: (m1, m) => `The time each ${m1} loses here compared with its usual pace on the line, between stops. Brings out a signal or junction that is slow at every hour, evening included.`,
|
|
343
|
+
},
|
|
344
|
+
masked: 'stop zone hidden', nodata: 'not enough data', hot: 'hotspot', evening: ' (the evening is the reference)',
|
|
345
|
+
what: 'Explanation',
|
|
318
346
|
},
|
|
319
347
|
tip: {
|
|
320
348
|
after: (a, b, s) => `${a} to ${b} m after ${s}`, towards: t => `to ${t}`,
|
|
@@ -324,15 +352,18 @@ const T = {
|
|
|
324
352
|
exl: (v, s) => `${v} vs the line (≈ ${s} s)`, freq: (v, m) => `${v} ${m} per hour`,
|
|
325
353
|
},
|
|
326
354
|
mxtitle: 'The whole day at a glance',
|
|
327
|
-
mxsub: (f, t, d, what, stops) => `From ${f} to ${t}, ${d}
|
|
355
|
+
mxsub: (f, t, d, what, stops, hs) => [`From ${f} to ${t}, ${d}.`, `Columns: the line cut into micro-segments (dark lines: stops); rows: hours. Colour: ${what}, as on the map${stops ? '' : '; hatching: stop zones hidden'}.${hs ? ' Yellow outlines: hotspots.' : ''} Click an hour to show it on the map.`],
|
|
328
356
|
full: '⛶ Full screen', unfull: '✕ Exit full screen',
|
|
329
357
|
proftitle: 'Along the line, micro-segment by micro-segment',
|
|
330
|
-
profsub: (f, t, w, ref, m) => `From ${f} to ${t}, ${w}
|
|
358
|
+
profsub: (f, t, w, ref, m) => [`From ${f} to ${t}, ${w}.`, `Height: the chosen measure in each micro-segment. Vertical lines: stops.${ref ? ` White line: the evening (${REFH[0]}–${REFH[1]} h).` : ''} Yellow dots: ${m} per hour (right scale).`],
|
|
331
359
|
perh: m => `${m}/h`,
|
|
332
360
|
covtitle: 'Days used',
|
|
333
|
-
covsub: (n, x) => `One square per day. A day is dropped when the position feed is missing or frozen for more than a quarter of the hours from ${DAYH[0]} to ${DAYH[1]} h; an hour is dropped below 80 % of polls.
|
|
361
|
+
covsub: (n, x) => [`${n} days used${x ? `; ${x}` : ''}.`, `One square per day. A day is dropped when the position feed is missing or frozen for more than a quarter of the hours from ${DAYH[0]} to ${DAYH[1]} h; an hour is dropped below 80 % of polls. Hover a square for details.`],
|
|
334
362
|
cal: {inc: 'used', exc: 'dropped', off: 'not selected', none: 'no data'},
|
|
335
|
-
reasons: {low_coverage: 'too few polls', frozen: 'frozen feed', line_absent: 'line missing from the feed', deviation: 'off-route trips', vehicle_ratio: 'abnormal vehicle count', no_data: 'no data', excluded: 'excluded on request', not_selected: 'not selected'},
|
|
363
|
+
reasons: {low_coverage: 'too few polls', frozen: 'frozen feed', line_absent: 'line missing from the feed', deviation: 'off-route trips', vehicle_ratio: 'abnormal vehicle count', no_data: 'no data', excluded: 'excluded on request', holiday: 'public holiday', not_selected: 'not selected'},
|
|
364
|
+
xsum: (n, parts) => `${n} ${n > 1 ? 'days' : 'day'} dropped: ${parts}`,
|
|
365
|
+
xr: {holiday: ['public holiday', 'public holidays'], feed: ['incomplete feed', 'incomplete feed'], line_absent: ['line missing from the feed', 'line missing from the feed'],
|
|
366
|
+
deviation: ['off-route trips', 'off-route trips'], vehicle_ratio: ['abnormal vehicle count', 'abnormal vehicle count'], excluded: ['on request', 'on request']},
|
|
336
367
|
covday: (c) => `polls: ${c} % of ${DAYH[0]}–${DAYH[1]} h`,
|
|
337
368
|
vers: (a, b, n) => `${a} – ${b} · ${n} d`, verMain: 'main', verNote: (r, a) => `${r} stretch(es) fewer, ${a} more than the main version`,
|
|
338
369
|
mtitle: 'Method',
|
|
@@ -344,7 +375,7 @@ const T = {
|
|
|
344
375
|
`<b>Excess over evening.</b> Observations per passage at the chosen hour minus those from ${REFH[0]} to ${REFH[1]} h, on the same days.`,
|
|
345
376
|
`<b>Excess over the line.</b> A second reference: the line's usual level in that direction, the median observations per passage and per metre of the micro-segments between stops, from 6 to 23 h, times the micro-segment's length. It brings out what is slow at every hour, evening included.${agreeText('en')}`,
|
|
346
377
|
`<b>Stop zones.</b> From ${(P.stop_zone || [30, 60])[0]} m before to ${(P.stop_zone || [30, 60])[1]} m after each stop. Hiding them brings out signals and junctions; the colours do not change.`,
|
|
347
|
-
`<b>Scale.</b>
|
|
378
|
+
`<b>Scale.</b> The same colours for every line, period, day, hour and option. Each measure is brought to 30 m of track, then read on the STIB speed scale, from green (fluid) to red (slow): 30 m in 4 s is 27 km/h (dark green), in 14 s 7.5 km/h (red), in 24 s or more 4.5 km/h or less (dark red). Both excesses (vs the evening, vs the line) share that scale: an excess of +x s reads as a fluid 3 s passage made x s longer, so 0 s is green, +10 s red, +21 s or more dark red. Observations per hour are read at 2 s per observation (≈ 10 passages an hour): 1.5 is dark green, 12 or more dark red. Showing "10 m" only changes the numbers (divided by 3), not the colours. These marks come from 2025 data of several tram and bus lines: between stops a passage takes a median 3 to 4.5 s per 30 m and, at the 98th percentile, 12 to 23 s.`,
|
|
348
379
|
`<b>Days.</b> Each value is a ratio of sums over the chosen days. Fewer days means noisier values.`,
|
|
349
380
|
`<b>Route changes.</b> A micro-segment only counts on days when it is part of the main route. The map follows the most frequent version; the others can be picked with the selector.`,
|
|
350
381
|
`<b>Hotspots.</b> Two criteria. Vs the evening: at some hours (at least two in a row, or a whole peak), each ${m1} is observed more often than from ${REFH[0]} to ${REFH[1]} h. Vs the rest of the line, between stops only: over the whole day, evening included, more observations per metre than the line's usual level in that direction (the median of the micro-segments between stops); this brings out a signal or a slow junction present at every hour, which the evening comparison cancels. Both need at least ${HTH} s more per ${m1} with 95 % confidence (resampling days), a positive excess on at least 60 % of days, and a multiple-testing correction. Neighbouring micro-segments form a stretch; stretches are ranked by extra observations per day. "All day": the delay does not go away in the evening; "at peak hours": it is mostly there at peaks; "mixed": a bit of both.`,
|
|
@@ -372,14 +403,16 @@ const CT = {
|
|
|
372
403
|
perveh: m1 => `s par ${m1}`, per30: m1 => `s par ${m1}, pour 30 m`, perday: v => `${v} min par jour au total`,
|
|
373
404
|
freq: (a, b, m) => `${a} → ${b} ${m}/h`, freqflag: 'fréquence modifiée : une partie de l\'écart peut venir de l\'horaire',
|
|
374
405
|
ci: (a, b) => `IC 95 % : ${a} à ${b} s`,
|
|
375
|
-
legend: {d: m1 => `Différence
|
|
376
|
-
dx: m1 => `Différence de l'écart
|
|
406
|
+
legend: {d: m1 => `Différence, s par ${m1} pour 30 m`,
|
|
407
|
+
dx: m1 => `Différence de l'écart au soir, s par ${m1} pour 30 m`,
|
|
408
|
+
help: {d: m1 => `Pour chaque bout de voie : le temps par ${m1} après, moins le temps avant. Vert : plus rapide qu'avant ; rouge : plus lent ; pâle : différence incertaine.`,
|
|
409
|
+
dx: m1 => `Comme la différence, mais calculée sur l'écart avec le soir : si le soir lui-même a changé, ce changement n'est pas compté. Vert : plus rapide ; rouge : plus lent ; pâle : incertain.`},
|
|
377
410
|
faster: 'plus rapide', slower: 'plus lent', pale: 'pâle : différence incertaine', absent: 'absent avant', chg: 'changement retenu'},
|
|
378
411
|
tip: {a: (v, s) => `avant : ${v} obs. par passage (≈ ${s} s)`, b: (v, s) => `après : ${v} (≈ ${s} s)`,
|
|
379
412
|
d: (v, m1) => `<b>${v} s par ${m1}</b>`, unsure: 'différence incertaine', absent: 'absent de la première période',
|
|
380
413
|
freq: (a, b, m) => `${a} → ${b} ${m} par heure`},
|
|
381
|
-
mxsub: (f, t, a, b, m1, stops) => `De ${f} à ${t}
|
|
382
|
-
profsub: (f, t, w, m1, m) => `De ${f} à ${t}, ${w}
|
|
414
|
+
mxsub: (f, t, a, b, m1, stops) => [`De ${f} à ${t}.`, `Chaque case : la différence de temps par ${m1} entre ${a} et ${b}, à cette heure. Vert : plus rapide ; rouge : plus lent ; pâle : incertain${stops ? '' : ' ; hachures : zones d\'arrêt masquées'}. Contours jaunes : changements retenus. Cliquez une heure pour l'afficher sur la carte.`],
|
|
415
|
+
profsub: (f, t, w, m1, m) => [`De ${f} à ${t}, ${w}.`, `Au-dessus de zéro, chaque ${m1} met plus de temps qu'avant ; en dessous, moins. Pointillés jaunes : ${m} par heure après (échelle de droite).`],
|
|
383
416
|
days: 'mêmes jours de semaine que chaque période',
|
|
384
417
|
method: (m1, m, a, b) => `<b>Comparaison de deux périodes.</b> ${a} (avant) et ${b} (après), chacune sur ses propres jours et avec sa propre référence. Les microsegments sont appariés d'une période à l'autre ; ceux qui n'existent que dans une période sont écartés. La différence porte sur les observations par passage, donc le temps par ${m1}, ce qui la rend indépendante de la fréquence. L'incertitude vient d'un rééchantillonnage des jours de chaque période. Quand la fréquence change de plus de 20 %, le tronçon est signalé : un horaire différent (plus de ${m}, plus de files) peut expliquer une partie de l'écart.`,
|
|
385
418
|
},
|
|
@@ -397,14 +430,16 @@ const CT = {
|
|
|
397
430
|
perveh: m1 => `s per ${m1}`, per30: m1 => `s per ${m1}, per 30 m`, perday: v => `${v} min per day in total`,
|
|
398
431
|
freq: (a, b, m) => `${a} → ${b} ${m}/h`, freqflag: 'frequency changed: part of the difference may come from the timetable',
|
|
399
432
|
ci: (a, b) => `95 % CI: ${a} to ${b} s`,
|
|
400
|
-
legend: {d: m1 => `Difference
|
|
401
|
-
dx: m1 => `Difference
|
|
433
|
+
legend: {d: m1 => `Difference, s per ${m1} per 30 m`,
|
|
434
|
+
dx: m1 => `Difference of the excess over the evening, s per ${m1} per 30 m`,
|
|
435
|
+
help: {d: m1 => `For each bit of track: the time per ${m1} after, minus the time before. Green: faster than before; red: slower; pale: uncertain difference.`,
|
|
436
|
+
dx: m1 => `Like the difference, but computed on the excess over the evening: if the evening itself changed, that change is left out. Green: faster; red: slower; pale: uncertain.`},
|
|
402
437
|
faster: 'faster', slower: 'slower', pale: 'pale: uncertain difference', absent: 'absent before', chg: 'retained change'},
|
|
403
438
|
tip: {a: (v, s) => `before: ${v} obs. per passage (≈ ${s} s)`, b: (v, s) => `after: ${v} (≈ ${s} s)`,
|
|
404
439
|
d: (v, m1) => `<b>${v} s per ${m1}</b>`, unsure: 'uncertain difference', absent: 'absent from the first period',
|
|
405
440
|
freq: (a, b, m) => `${a} → ${b} ${m} per hour`},
|
|
406
|
-
mxsub: (f, t, a, b, m1, stops) => `From ${f} to ${t}
|
|
407
|
-
profsub: (f, t, w, m1, m) => `From ${f} to ${t}, ${w}
|
|
441
|
+
mxsub: (f, t, a, b, m1, stops) => [`From ${f} to ${t}.`, `Each cell: the difference in time per ${m1} between ${a} and ${b}, at that hour. Green: faster; red: slower; pale: uncertain${stops ? '' : '; hatching: stop zones hidden'}. Yellow outlines: retained changes. Click an hour to show it on the map.`],
|
|
442
|
+
profsub: (f, t, w, m1, m) => [`From ${f} to ${t}, ${w}.`, `Above zero, each ${m1} takes longer than before; below, less. Yellow dots: ${m} per hour after (right scale).`],
|
|
408
443
|
days: 'same weekdays as each period',
|
|
409
444
|
method: (m1, m, a, b) => `<b>Comparing two periods.</b> ${a} (before) and ${b} (after), each on its own days and with its own reference. Micro-segments are matched from one period to the other; those that exist in one period only are left out. The difference is in observations per passage, so in time per ${m1}, which makes it independent of the frequency. The uncertainty comes from resampling the days of each period. When the frequency changes by more than 20 %, the stretch is flagged: a different timetable (more ${m}, more queues) may explain part of the difference.`,
|
|
410
445
|
},
|
|
@@ -445,7 +480,8 @@ function colorOf(v) {
|
|
|
445
480
|
// is 3-4.4 s per 30 m, p90 6.5-10 s, p98 12-23 s; stop zones p98 33-46 s; positive excess over the evening:
|
|
446
481
|
// median ~1 s, p98 5-17 s. So typical values stay green and hotspots (10 s and more per 30 m) turn red.
|
|
447
482
|
const SC = {L: 30, T0: 3, T1: 24, TICKS: [3, 6, 9, 12, 15, 24], BASE: 3, OPH: 2, CMP: 10, CTICKS: [-10, -3, 0, 3, 10]};
|
|
448
|
-
|
|
483
|
+
// no data: flat discreet grey; masked stop zone: grey hatching (SVG pattern #mshatch, legend swatch .sw.hatch)
|
|
484
|
+
const NULLC = '#3a4560', MASKC = 'url(#mshatch)';
|
|
449
485
|
function timeColor(T) {
|
|
450
486
|
if (T == null || !isFinite(T)) return NULLC;
|
|
451
487
|
return colorOf(108 / Math.max(T, 1e-9));
|
|
@@ -467,7 +503,7 @@ function divColor(v, pale) {
|
|
|
467
503
|
// ---------------------------------------------------------------- state
|
|
468
504
|
const qs = new URLSearchParams(location.search);
|
|
469
505
|
const st = {lang: T[qs.get('lang')] ? qs.get('lang') : (T[document.documentElement.lang] ? document.documentElement.lang : 'fr'),
|
|
470
|
-
dir: null, ver: null, band: null, hour: null, metric: 'oph', per10: false, stops: false, days: [], hot: null,
|
|
506
|
+
dir: null, ver: null, band: null, hour: null, metric: 'oph', per10: false, stops: qs.get('stops') !== 'false', days: [], hot: null,
|
|
471
507
|
mode: D.compare ? 'cmp' : 'one', cmetric: 'd', chg: null};
|
|
472
508
|
const tx = () => T[st.lang];
|
|
473
509
|
const noun = (pl) => { const n = NOUN[st.lang][D.mode] || NOUN[st.lang]._; return pl ? n[1] : n[0]; };
|
|
@@ -585,8 +621,35 @@ function showTip(e, html) { tipEl.innerHTML = html; tipEl.style.display = 'block
|
|
|
585
621
|
tipEl.style.left = Math.min(window.innerWidth - w - 8, e.clientX + 14) + 'px'; tipEl.style.top = (e.clientY + 16 + hh > window.innerHeight ? e.clientY - hh - 10 : e.clientY + 16) + 'px'; }
|
|
586
622
|
const hideTip = () => { tipEl.style.display = 'none'; };
|
|
587
623
|
const HOT_TOP = 8;
|
|
624
|
+
// masked stop zone in the profiles: a hatched band over the plot height
|
|
625
|
+
const maskBar = (x, b, m, H) => `<rect x="${x(b.x0).toFixed(1)}" y="${m.t}" width="${Math.max(1, x(b.x0 + b.len) - x(b.x0) - .5).toFixed(1)}" height="${H - m.t - m.b}" fill="${MASKC}" opacity=".55"/>`;
|
|
626
|
+
// short text + '*' button; the button carries the plain-language explanation for the popover
|
|
627
|
+
const helpBtn = t => `<button type="button" class="qm" aria-expanded="false" aria-controls="pop" aria-label="${esc(tx().legend.what)}" data-help="${esc(t)}">*</button>`;
|
|
628
|
+
function setSub(el, parts) { el.innerHTML = esc(parts[0]) + helpBtn(parts[1]); }
|
|
629
|
+
const popEl = document.getElementById('pop');
|
|
630
|
+
let popBtn = null, popPinned = false;
|
|
631
|
+
function openPop(b, pin) {
|
|
632
|
+
if (popBtn && popBtn !== b) popBtn.setAttribute('aria-expanded', 'false');
|
|
633
|
+
popBtn = b; popPinned = pin; popEl.textContent = b.dataset.help; popEl.hidden = false; b.setAttribute('aria-expanded', 'true');
|
|
634
|
+
const r = b.getBoundingClientRect(), w = popEl.offsetWidth, h = popEl.offsetHeight;
|
|
635
|
+
popEl.style.left = Math.max(12, Math.min(window.innerWidth - w - 12, r.left + r.width / 2 - w / 2)) + 'px';
|
|
636
|
+
popEl.style.top = (r.bottom + 8 + h > window.innerHeight && r.top - h - 8 > 0 ? r.top - h - 8 : r.bottom + 8) + 'px';
|
|
637
|
+
}
|
|
638
|
+
function closePop() { if (popBtn) popBtn.setAttribute('aria-expanded', 'false'); popBtn = null; popPinned = false; popEl.hidden = true; }
|
|
639
|
+
document.addEventListener('click', e => {
|
|
640
|
+
const b = e.target.closest && e.target.closest('.qm');
|
|
641
|
+
if (b) { e.preventDefault(); if (popBtn === b && popPinned) closePop(); else openPop(b, true); return; }
|
|
642
|
+
if (popBtn && !popEl.contains(e.target)) closePop();
|
|
643
|
+
});
|
|
644
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape' && popBtn) { const b = popBtn; closePop(); b.focus(); } });
|
|
645
|
+
const canHover = window.matchMedia && matchMedia('(hover: hover)').matches;
|
|
646
|
+
document.addEventListener('mouseover', e => { const b = canHover && e.target.closest && e.target.closest('.qm'); if (b && !popPinned) openPop(b, false); });
|
|
647
|
+
document.addEventListener('mouseout', e => { const b = e.target.closest && e.target.closest('.qm'); if (b && popBtn === b && !popPinned) closePop(); });
|
|
648
|
+
document.addEventListener('focusin', e => { const b = e.target.closest && e.target.closest('.qm'); if (b && !popPinned && b.matches(':focus-visible')) openPop(b, false); });
|
|
649
|
+
window.addEventListener('scroll', () => { if (popBtn) { if (popBtn.isConnected) openPop(popBtn, popPinned); else closePop(); } }, {passive: true});
|
|
588
650
|
// stop-zone hotspots are listed only when stop zones are shown
|
|
589
|
-
const
|
|
651
|
+
const hsOn = () => isCmp() || D.show_hotspots !== false;
|
|
652
|
+
const hotDir = () => !hsOn() ? [] : (D.hotspots || []).filter(h => (h.dir ?? h.direction_id) === st.dir && (!h.pattern_uid || h.pattern_uid === (dirOf() || {}).pattern_uid)).sort((a, b) => a.rank - b.rank);
|
|
590
653
|
const hotAll = () => hotDir().filter(h => st.stops || h.zone !== 'stop');
|
|
591
654
|
// the list (and the map markers) show the top hotspots unless "show all" is on
|
|
592
655
|
const hotOfDir = () => { const a = hotAll(); return st.hotAll ? a : a.slice(0, HOT_TOP); };
|
|
@@ -613,7 +676,7 @@ function agreeText(lang) {
|
|
|
613
676
|
if (!ag.length) return '';
|
|
614
677
|
const rho = ag.map(a => nf(a.spearman_day, 2)).join(lang === 'fr' ? ' et ' : ' and ');
|
|
615
678
|
const nh = ag.reduce((t, a) => t + (a.n_hotspots || 0), 0), nb = ag.reduce((t, a) => t + (a.n_both || 0), 0);
|
|
616
|
-
const hs = nh ? (lang === 'fr' ? ` ${nb} des ${nh} points chauds ressortent avec les deux références.` : ` ${nb} of the ${nh} hotspots stand out against both references.`) : '';
|
|
679
|
+
const hs = nh && D.show_hotspots !== false ? (lang === 'fr' ? ` ${nb} des ${nh} points chauds ressortent avec les deux références.` : ` ${nb} of the ${nh} hotspots stand out against both references.`) : '';
|
|
617
680
|
return lang === 'fr'
|
|
618
681
|
? ` En journée, entre les arrêts, les deux écarts classent les microsegments avec une corrélation de rang de ${rho}${ag.length > 1 ? ' selon le sens' : ''} (1 : même classement).${hs}`
|
|
619
682
|
: ` In the daytime, between stops, the two excesses rank the micro-segments with a rank correlation of ${rho}${ag.length > 1 ? ' by direction' : ''} (1: same ranking).${hs}`;
|
|
@@ -664,9 +727,9 @@ function cmpLegend() {
|
|
|
664
727
|
const bar = Array.from({length: 41}, (_, i) => { const t = (i - 20) / 20; return divColor(Math.sign(t) * M * t * t, false); }).join(',');
|
|
665
728
|
const tk = SC.CTICKS.map(v => `<span style="left:${pos(v)}%">${v === M ? '≥ ' : v === -M ? '≤ ' : ''}${v ? signed(v, 0) + ' s' : '0'}</span>`).join('');
|
|
666
729
|
const html = `<div class="ramp"><div class="bar" style="background:linear-gradient(90deg,${bar})"></div><div class="tk">${tk}</div></div>` +
|
|
667
|
-
`<span class="txt">${c[mm](noun(false))}
|
|
730
|
+
`<span class="txt">${esc(c[mm](noun(false)))}${helpBtn(c.help[mm](noun(false)))}</span>` +
|
|
668
731
|
`<span><i class="sw" style="background:${NULLC}"></i>${c.absent}</span>` +
|
|
669
|
-
(st.stops ? '' : `<span><i class="sw
|
|
732
|
+
(st.stops ? '' : `<span><i class="sw hatch"></i>${tx().legend.masked}</span>`) +
|
|
670
733
|
(chgDir().length ? `<span><i class="hk"></i>${c.chg}</span>` : '');
|
|
671
734
|
$('legend').innerHTML = html; $('mxlegend').innerHTML = html;
|
|
672
735
|
}
|
|
@@ -726,6 +789,7 @@ function cmpProfile() {
|
|
|
726
789
|
s += `<line x1="${xx}" x2="${xx}" y1="${m.t}" y2="${H - m.b}" stroke="#eef1f6" stroke-opacity=".15"/>`;
|
|
727
790
|
if (xx - last >= 13) { last = xx; s += `<text x="${xx}" y="${H - m.b + 14}" transform="rotate(-40 ${xx} ${H - m.b + 14})" text-anchor="end" fill="var(--ink-3)" font-size="10" font-family="var(--sans)">${esc(t.name)}</text>`; }
|
|
728
791
|
}
|
|
792
|
+
v.forEach(b => { if (!shown(b)) s += maskBar(x, b, m, H); });
|
|
729
793
|
v.forEach(b => { const u = cSec(b); if (u == null || !shown(b)) return; const ys = y(u), y0 = y(0);
|
|
730
794
|
s += `<rect x="${x(b.x0).toFixed(1)}" y="${Math.min(ys, y0).toFixed(1)}" width="${Math.max(1, x(b.x0 + b.len) - x(b.x0) - .5).toFixed(1)}" height="${Math.max(.5, Math.abs(y0 - ys)).toFixed(1)}" fill="${divColor(u, !b.sig)}"/>`; });
|
|
731
795
|
s += freqLine(v.map(b => b.fb == null ? null : b.fb), v, x, H, m, W);
|
|
@@ -734,7 +798,7 @@ function cmpProfile() {
|
|
|
734
798
|
svg.onmousemove = e => { const r = svg.getBoundingClientRect(), u = (e.clientX - r.left - m.l) / (W - m.l - m.r) * Ltot; if (u < 0 || u > Ltot) return hideTip();
|
|
735
799
|
let i = v.findIndex(b => u >= b.x0 && u < b.x0 + b.len); if (i < 0) i = v.length - 1; showTip(e, cTipHtml(E, v[i])); };
|
|
736
800
|
svg.onmouseleave = hideTip;
|
|
737
|
-
$('profsub')
|
|
801
|
+
setSub($('profsub'), c.profsub(E.from, E.to, whenLabel(), noun(false), noun(true)));
|
|
738
802
|
}
|
|
739
803
|
|
|
740
804
|
// ---------------------------------------------------------------- static texts
|
|
@@ -759,7 +823,7 @@ function texts() {
|
|
|
759
823
|
document.getElementById('proftitle').textContent = l.proftitle;
|
|
760
824
|
document.getElementById('covtitle').textContent = l.covtitle;
|
|
761
825
|
document.getElementById('mtitle').textContent = l.mtitle;
|
|
762
|
-
document.getElementById('method').innerHTML = l.method(noun(true), noun(false)).concat(CMP ? [ctx().method(noun(false), noun(true), perLabel(CMP.a), perLabel(CMP.b))] : []).map(x => `<li>${x}</li>`).join('');
|
|
826
|
+
document.getElementById('method').innerHTML = l.method(noun(true), noun(false)).filter(x => hsOn() || !/^<b>(Points chauds|Hotspots)\./.test(x)).concat(CMP ? [ctx().method(noun(false), noun(true), perLabel(CMP.a), perLabel(CMP.b))] : []).map(x => `<li>${x}</li>`).join('');
|
|
763
827
|
document.getElementById('footer').textContent = l.footer(D.source);
|
|
764
828
|
document.getElementById('play').textContent = timer ? l.pause : l.play;
|
|
765
829
|
document.getElementById('mxfull').textContent = mxCard.classList.contains('full') ? l.unfull : l.full;
|
|
@@ -767,25 +831,40 @@ function texts() {
|
|
|
767
831
|
const hs = D.hours, tk = [hs[0], 8, 12, 17, hs[hs.length - 1]].filter((h, i, a) => hs.includes(h) && a.indexOf(h) === i);
|
|
768
832
|
document.getElementById('hticks').innerHTML = tk.map(h => `<span>${h} h</span>`).join('');
|
|
769
833
|
}
|
|
834
|
+
// "4 jours écartés : 2 fériés, 2 flux incomplets" (missing, frozen and empty feed hours count as one reason)
|
|
835
|
+
const FEED_REASONS = ['low_coverage', 'frozen', 'no_data'];
|
|
836
|
+
function exclSummary(excluded) {
|
|
837
|
+
const l = tx(), ex = excluded || [];
|
|
838
|
+
if (!ex.length) return '';
|
|
839
|
+
const counts = new Map();
|
|
840
|
+
for (const e of ex) { const k = FEED_REASONS.includes(e.reason) ? 'feed' : e.reason; counts.set(k, (counts.get(k) || 0) + 1); }
|
|
841
|
+
const parts = [...counts].sort((a, b) => b[1] - a[1]).map(([k, n]) => {
|
|
842
|
+
const w = l.xr[k] || [l.reasons[k] || k, l.reasons[k] || k];
|
|
843
|
+
return `${nf(n, 0)} ${w[n > 1 ? 1 : 0]}`;
|
|
844
|
+
});
|
|
845
|
+
return l.xsum(ex.length, parts.join(', '));
|
|
846
|
+
}
|
|
770
847
|
function tiles() {
|
|
771
|
-
const l = tx()
|
|
848
|
+
const l = tx();
|
|
772
849
|
const nseg = DIRS.reduce((a, d) => a + d.seg.key.length, 0);
|
|
773
850
|
let tot = 0;
|
|
774
851
|
for (const d of DIRS) for (const w of PRESENT) for (const row of d.wk.obs[w]) for (const v of row) tot += v;
|
|
775
852
|
const nh = (D.hotspots || []).length;
|
|
776
853
|
if (isCmp()) {
|
|
777
854
|
const c = ctx().tiles, ch = CMP.changes || [];
|
|
855
|
+
document.getElementById('tiles').classList.remove('t3');
|
|
778
856
|
document.getElementById('tiles').innerHTML = [[nf(CMP.a.days, 0), `${c.a} (${perLabel(CMP.a)})`], [nf(CMP.b.days, 0), `${c.b} (${perLabel(CMP.b)})`],
|
|
779
857
|
[nf(ch.filter(x => x.sign > 0).length, 0), c.slower], [nf(ch.filter(x => x.sign < 0).length, 0), c.faster]]
|
|
780
858
|
.map(([b, s]) => `<div class="tile"><b>${b}</b><span>${esc(s)}</span></div>`).join('');
|
|
781
859
|
return;
|
|
782
860
|
}
|
|
783
861
|
const big = v => v >= 1e6 ? nf(v / 1e6, 1) + (st.lang === 'fr' ? ' M' : 'M') : v >= 1e4 ? nf(v / 1e3, 0) + (st.lang === 'fr' ? ' k' : 'k') : nf(v, 0);
|
|
862
|
+
document.getElementById('tiles').classList.toggle('t3', !hsOn());
|
|
784
863
|
document.getElementById('tiles').innerHTML = [
|
|
785
|
-
[nf(D.period.days, 0), l.tiles.days(D.period.days,
|
|
864
|
+
[nf(D.period.days, 0), esc(l.tiles.days(D.period.days, exclSummary(D.period.excluded)))],
|
|
786
865
|
[nf(nseg, 0), l.tiles.segs(nseg, nf(P.segment_m || 30, 0))],
|
|
787
866
|
[big(tot), l.tiles.obs],
|
|
788
|
-
[nf(nh, 0), l.tiles.hs(nh)],
|
|
867
|
+
...(hsOn() ? [[nf(nh, 0), l.tiles.hs(nh)]] : []),
|
|
789
868
|
].map(([b, s]) => `<div class="tile"><b>${b}</b><span>${s}</span></div>`).join('');
|
|
790
869
|
}
|
|
791
870
|
|
|
@@ -925,11 +1004,11 @@ function legend() {
|
|
|
925
1004
|
const lab = (T, i) => { const x = tickVal(T, m), d = Math.abs(x - Math.round(x)) < 1e-9 ? 0 : 1;
|
|
926
1005
|
return (i === n - 1 ? '≥ ' : i === 0 && !isEx(m) ? '≤ ' : '') + (isEx(m) && x ? '+' : '') + nf(x, d) + (m === 'oph' ? '' : ' s'); };
|
|
927
1006
|
const ticks = SC.TICKS.map((T, i) => `<span style="left:${tPos(T) * 100}%">${lab(T, i)}</span>`).join('');
|
|
928
|
-
const txt = (m === 'oph' ? l.oph(noun(false), u) : m === 'opp' ? l.opp(noun(false), u) : m === 'exl' ? l.exl(noun(false), u)
|
|
929
|
-
: l.ex(noun(false), u) + (st.band === 'evening' || (!st.band && st.hour >= REFH[0] && st.hour < REFH[1]) ? l.evening : '')) +
|
|
1007
|
+
const txt = esc((m === 'oph' ? l.oph(noun(false), u) : m === 'opp' ? l.opp(noun(false), u) : m === 'exl' ? l.exl(noun(false), u)
|
|
1008
|
+
: l.ex(noun(false), u) + (st.band === 'evening' || (!st.band && st.hour >= REFH[0] && st.hour < REFH[1]) ? l.evening : ''))) + helpBtn(l.help[m](noun(false), noun(true)));
|
|
930
1009
|
$('legend').innerHTML = `<div class="ramp"><div class="bar" style="background:linear-gradient(90deg,${bar})"></div><div class="tk">${ticks}</div></div>` +
|
|
931
1010
|
`<span class="txt">${txt}</span><span><i class="sw" style="background:${NULLC}"></i>${l.nodata}</span>` +
|
|
932
|
-
(st.stops ? '' : `<span><i class="sw
|
|
1011
|
+
(st.stops ? '' : `<span><i class="sw hatch"></i>${l.masked}</span>`) +
|
|
933
1012
|
(hotOfDir().length && isMain() ? `<span><i class="hk"></i>${l.hot}</span>` : '');
|
|
934
1013
|
$('mxlegend').innerHTML = $('legend').innerHTML;
|
|
935
1014
|
}
|
|
@@ -1000,7 +1079,7 @@ function drawMatrix() {
|
|
|
1000
1079
|
svg.innerHTML = s;
|
|
1001
1080
|
MX = {E, rows, m, RH, W, Ltot, tip: cm ? cTipHtml : tipHtml};
|
|
1002
1081
|
const what = l.metrics[st.metric].toLowerCase() + (st.per10 ? ' (10 m)' : ' (30 m)');
|
|
1003
|
-
$('mxsub')
|
|
1082
|
+
setSub($('mxsub'), cm ? ctx().mxsub(E.from, E.to, perLabel(CMP.a), perLabel(CMP.b), noun(false), st.stops) : l.mxsub(E.from, E.to, daysLabel(), what, st.stops, hsOn() && hotOfDir().length > 0));
|
|
1004
1083
|
}
|
|
1005
1084
|
$('mx').addEventListener('mousemove', e => {
|
|
1006
1085
|
if (!MX) return;
|
|
@@ -1035,6 +1114,7 @@ function drawProfile() {
|
|
|
1035
1114
|
s += `<line x1="${xx}" x2="${xx}" y1="${m.t}" y2="${H - m.b}" stroke="#eef1f6" stroke-opacity=".15"/>`;
|
|
1036
1115
|
if (xx - last >= 13) { last = xx; s += `<text x="${xx}" y="${H - m.b + 14}" transform="rotate(-40 ${xx} ${H - m.b + 14})" text-anchor="end" fill="var(--ink-3)" font-size="10" font-family="var(--sans)">${esc(t.name)}</text>`; }
|
|
1037
1116
|
}
|
|
1117
|
+
v.forEach(b => { if (!shown(b)) s += maskBar(x, b, m, H); });
|
|
1038
1118
|
v.forEach(b => { const u = pv(b); if (u != null && shown(b)) s += `<rect x="${x(b.x0).toFixed(1)}" y="${y(u).toFixed(1)}" width="${Math.max(1, x(b.x0 + b.len) - x(b.x0) - .5).toFixed(1)}" height="${(y(0) - y(u)).toFixed(1)}" fill="${colorFor(b)}"/>`; });
|
|
1039
1119
|
if (evening) { let pth = ''; evening.forEach(b => { const u = pv(b); if (u != null && shown(b)) pth += `M${x(b.x0).toFixed(1)},${y(u).toFixed(1)}H${x(b.x0 + b.len).toFixed(1)}`; });
|
|
1040
1120
|
s += `<path d="${pth}" stroke="#eef1f6" stroke-width="1.5" fill="none" opacity=".7"/>`; }
|
|
@@ -1044,7 +1124,7 @@ function drawProfile() {
|
|
|
1044
1124
|
svg.onmousemove = e => { const r = svg.getBoundingClientRect(), u = (e.clientX - r.left - m.l) / (W - m.l - m.r) * Ltot; if (u < 0 || u > Ltot) return hideTip();
|
|
1045
1125
|
let i = v.findIndex(b => u >= b.x0 && u < b.x0 + b.len); if (i < 0) i = v.length - 1; showTip(e, tipHtml(E, v[i])); };
|
|
1046
1126
|
svg.onmouseleave = hideTip;
|
|
1047
|
-
$('profsub')
|
|
1127
|
+
setSub($('profsub'), l.profsub(E.from, E.to, whenLabel(), !!evening, noun(true)));
|
|
1048
1128
|
}
|
|
1049
1129
|
|
|
1050
1130
|
// ---------------------------------------------------------------- coverage calendar
|
|
@@ -1053,6 +1133,7 @@ function drawCalendar() {
|
|
|
1053
1133
|
const dates = cv.dates || [];
|
|
1054
1134
|
if (!dates.length) { svg.innerHTML = ''; $('covsub').textContent = ''; return; }
|
|
1055
1135
|
const exc = Object.fromEntries((D.period.excluded || []).map(e => [e.date, e.reason]));
|
|
1136
|
+
const xlab = Object.fromEntries((D.period.excluded || []).filter(e => e.label).map(e => [e.date, e.label]));
|
|
1056
1137
|
const dayIdx = D.hours.map((h, j) => [h, j]).filter(([h]) => h >= DAYH[0] && h < DAYH[1]).map(([, j]) => j);
|
|
1057
1138
|
const t0 = new Date(dates[0] + 'T12:00:00Z'), dow0 = (t0.getUTCDay() + 6) % 7;
|
|
1058
1139
|
const C = 15, G = 3, top = 16, left = 26;
|
|
@@ -1071,15 +1152,14 @@ function drawCalendar() {
|
|
|
1071
1152
|
let fill, status;
|
|
1072
1153
|
if (inc === true) { fill = st.days.includes(w) ? colorOf(14 + 8 * (mc == null ? 1 : mc)) : '#24324f'; status = st.days.includes(w) ? l.cal.inc : l.cal.off; }
|
|
1073
1154
|
else if (inc === 'not_selected') { fill = '#16223d'; status = l.cal.off; }
|
|
1074
|
-
else { fill = inc === 'no_data' ? '#3a4560' : '#c2414b'; status = `${l.cal.exc} : ${l.reasons[inc] || inc}`; }
|
|
1155
|
+
else { fill = inc === 'no_data' ? '#3a4560' : '#c2414b'; status = `${l.cal.exc} : ${l.reasons[inc] || inc}${xlab[d] ? ` (${xlab[d]})` : ''}`; }
|
|
1075
1156
|
const tip = `<b>${esc(fmtDate(d, {weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'}))}</b><br>${esc(status)}${mc != null ? '<br>' + esc(l.covday(nf(mc * 100, 0))) : ''}`;
|
|
1076
1157
|
s += `<rect x="${left + col * (C + G)}" y="${top + w * (C + G)}" width="${C}" height="${C}" rx="3" fill="${fill}" data-tip="${esc(tip)}" opacity="${selected ? 1 : .55}"/>`;
|
|
1077
1158
|
});
|
|
1078
1159
|
svg.setAttribute('width', left + nW * (C + G)); svg.setAttribute('height', top + 7 * (C + G));
|
|
1079
1160
|
svg.innerHTML = s;
|
|
1080
1161
|
svg.querySelectorAll('rect').forEach(r => { r.onmousemove = e => showTip(e, r.dataset.tip); r.onmouseleave = hideTip; });
|
|
1081
|
-
|
|
1082
|
-
$('covsub').textContent = l.covsub(D.period.days, nx);
|
|
1162
|
+
setSub($('covsub'), l.covsub(D.period.days, exclSummary(D.period.excluded)));
|
|
1083
1163
|
$('calleg').innerHTML = [[colorOf(22), l.cal.inc], ['#c2414b', l.cal.exc], ['#24324f', l.cal.off], ['#3a4560', l.cal.none]]
|
|
1084
1164
|
.map(([c, t]) => `<span><i class="sw" style="background:${c};width:12px;height:12px"></i>${esc(t)}</span>`).join('');
|
|
1085
1165
|
}
|
|
@@ -1101,6 +1181,9 @@ function render() {
|
|
|
1101
1181
|
const dl = isCmp() ? ctx().days : daysLabel();
|
|
1102
1182
|
$('clock').innerHTML = st.band ? `${BANDDEF[st.band][0]}–${BANDDEF[st.band][1]} h<small>${tx().mean}, ${esc(dl)}</small>` : `${String(st.hour).padStart(2, '0')}:00<small>${esc(dl)}</small>`;
|
|
1103
1183
|
hourEl.value = st.hour;
|
|
1184
|
+
const nohs = !hsOn();
|
|
1185
|
+
if (document.body.classList.contains('nohs') !== nohs) { document.body.classList.toggle('nohs', nohs); if (map) setTimeout(() => map.invalidateSize(), 0); }
|
|
1186
|
+
if (popBtn) closePop();
|
|
1104
1187
|
recolor(); legend(); drawHotMap(); drawHotList(); drawMatrix(); drawProfile(); drawCalendar();
|
|
1105
1188
|
}
|
|
1106
1189
|
function drawAll() { buildVersions(); drawMap(); render(); }
|
|
@@ -32,7 +32,8 @@ _ALIASES = {"oph": "obs_per_h", "oph10": "obs_per_h_10m", "opp": "obs_per_passag
|
|
|
32
32
|
"excess": "excess_per_passage"}
|
|
33
33
|
_RAMP = list(S.RAMP) # the STIB speed ramp (km/h stops), as on the page
|
|
34
34
|
NULL_COLOR = "#3a4560"
|
|
35
|
-
MASK_COLOR = "#
|
|
35
|
+
MASK_COLOR = "#18213a" # masked stop zones: hatched (background + stripes), distinct from flat no-data grey
|
|
36
|
+
MASK_HATCH = "#5d6a88"
|
|
36
37
|
BG = "#0b1224"
|
|
37
38
|
|
|
38
39
|
|
|
@@ -163,6 +164,14 @@ def _frame(an: Analysis, direction_id: int, metric: str, pattern_uid: str | None
|
|
|
163
164
|
return r.with_columns(v.cast(pl.Float64).alias("value"))
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
def _hatch_spans(ax, segs: pl.DataFrame, y0: float, y1: float, alpha: float = 1.0) -> None:
|
|
168
|
+
"""Masked stop zones: hatched rectangles over [x0, x0 + len] x [y0, y1]."""
|
|
169
|
+
from matplotlib.patches import Rectangle
|
|
170
|
+
for x0, ln in zip(segs["x0_m"].to_list(), segs["len_m"].to_list()):
|
|
171
|
+
ax.add_patch(Rectangle((x0, min(y0, y1)), ln, abs(y1 - y0), facecolor=MASK_COLOR, edgecolor=MASK_HATCH,
|
|
172
|
+
hatch="////", lw=0, alpha=alpha, zorder=1.5))
|
|
173
|
+
|
|
174
|
+
|
|
166
175
|
def auto_vmax(analysis=None, metric: str = "obs_per_h", stops: bool = True, q: float = 0.98) -> float:
|
|
167
176
|
"""Top of the fixed colour scale in the units of ``metric`` per 30 m (per 10 m for ``obs_per_h_10m``).
|
|
168
177
|
It does not depend on the analysis (kept for compatibility; the arguments other than ``metric`` are
|
|
@@ -229,7 +238,6 @@ def matrix(analysis, direction_id: int = 0, metric: str = "obs_per_h", days=None
|
|
|
229
238
|
for h, s, v in f.select("hour", "seg_idx", "T").iter_rows():
|
|
230
239
|
if h in hi and v is not None and np.isfinite(v):
|
|
231
240
|
Z[hi[h], si[s]] = v
|
|
232
|
-
zone = np.array(segs["zone"].to_list())
|
|
233
241
|
x_edges = np.concatenate([segs["x0_m"].to_numpy(), [segs["x0_m"][-1] + segs["len_m"][-1]]]) if segs.height else np.array([0, 1])
|
|
234
242
|
y_edges = np.arange(len(hours) + 1) - 0.5 + hours[0] if hours else np.array([0, 1])
|
|
235
243
|
fig, ax = (ax.figure, ax) if ax is not None else plt.subplots(figsize=figsize, layout="constrained")
|
|
@@ -237,9 +245,7 @@ def matrix(analysis, direction_id: int = 0, metric: str = "obs_per_h", days=None
|
|
|
237
245
|
cm = cmap()
|
|
238
246
|
ax.pcolormesh(x_edges, y_edges, np.ma.masked_invalid(Z), cmap=cm, norm=norm(), shading="flat")
|
|
239
247
|
if not stops and segs.height:
|
|
240
|
-
|
|
241
|
-
from matplotlib.colors import ListedColormap
|
|
242
|
-
ax.pcolormesh(x_edges, y_edges, np.ma.masked_invalid(M), cmap=ListedColormap([MASK_COLOR]), shading="flat")
|
|
248
|
+
_hatch_spans(ax, segs.filter(pl.col("zone") == "stop"), y_edges[0], y_edges[-1])
|
|
243
249
|
xs, names = _stops(an, direction_id, links, pattern_uid)
|
|
244
250
|
for x in xs:
|
|
245
251
|
ax.axvline(x, color=BG, lw=1.2)
|
|
@@ -294,6 +300,8 @@ def profile(analysis, direction_id: int = 0, hour: int | str | None = None, *, b
|
|
|
294
300
|
colors = cmap()(norm()(np.nan_to_num(T, nan=S.T_MIN)))
|
|
295
301
|
colors[np.isnan(T)] = (0, 0, 0, 0)
|
|
296
302
|
ax.bar(cur["x0_m"].to_numpy(), np.nan_to_num(v), width=cur["len_m"].to_numpy() * 0.96, align="edge", color=colors)
|
|
303
|
+
if not stops:
|
|
304
|
+
_hatch_spans(ax, f.filter((pl.col("hour") == code) & (pl.col("zone") == "stop")), 0, top * 1.05, alpha=0.55)
|
|
297
305
|
if reference and code != BAND_HOUR["evening"] and not m.startswith("excess") and m != "log2_ratio":
|
|
298
306
|
ev = f.filter(pl.col("hour") == BAND_HOUR["evening"])
|
|
299
307
|
if not stops:
|
|
@@ -335,8 +343,7 @@ def map(analysis, direction_id: int = 0, hour: int | str | None = None, *, band:
|
|
|
335
343
|
T = _seconds(an, m, g["value"].fill_null(np.nan).to_numpy(), g["len_m"].to_numpy())
|
|
336
344
|
colors = cmap()(norm()(np.nan_to_num(T, nan=S.T_MIN)))
|
|
337
345
|
colors[np.isnan(T)] = matplotlib_color(NULL_COLOR)
|
|
338
|
-
if not stops
|
|
339
|
-
colors[np.array(g["zone"].to_list()) == "stop"] = matplotlib_color(MASK_COLOR)
|
|
346
|
+
masked = np.array(g["zone"].to_list()) == "stop" if not stops else np.zeros(len(lines), bool)
|
|
340
347
|
fig, ax = (ax.figure, ax) if ax is not None else plt.subplots(figsize=figsize, layout="constrained")
|
|
341
348
|
if hotspots is not None and hotspots.height and uid == an.dirs[direction_id].display_uid:
|
|
342
349
|
x0 = g["x0_m"].to_numpy()
|
|
@@ -349,7 +356,14 @@ def map(analysis, direction_id: int = 0, hour: int | str | None = None, *, band:
|
|
|
349
356
|
mid = hl[len(hl) // 2][len(hl[len(hl) // 2]) // 2]
|
|
350
357
|
ax.annotate(str(r["rank"]), mid, xytext=(8, 8), textcoords="offset points", fontsize=8,
|
|
351
358
|
bbox=dict(boxstyle="circle", fc="#ffe600", ec="none"), zorder=4)
|
|
352
|
-
|
|
359
|
+
keep = ~masked
|
|
360
|
+
ax.add_collection(LineCollection([c for c, k in zip(lines, keep) if k], colors=colors[keep], linewidths=linewidth,
|
|
361
|
+
capstyle="butt", zorder=2))
|
|
362
|
+
if masked.any(): # masked stop zones: striped (dashes over a dark casing), unlike the flat no-data grey
|
|
363
|
+
ml = [c for c, k in zip(lines, masked) if k]
|
|
364
|
+
ax.add_collection(LineCollection(ml, colors=MASK_COLOR, linewidths=linewidth, capstyle="butt", zorder=2))
|
|
365
|
+
ax.add_collection(LineCollection(ml, colors=MASK_HATCH, linewidths=linewidth, linestyles=(0, (1, 1.5)),
|
|
366
|
+
capstyle="butt", zorder=2))
|
|
353
367
|
xs, names = _stops(an, direction_id, links, uid)
|
|
354
368
|
if links is not None:
|
|
355
369
|
lk = links.filter(pl.col("pattern_uid") == uid).sort("link_idx")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/gtfs/calendar_dates.parquet
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/punctuality/route55.parquet
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/vd55/20250318.snaps.parquet
RENAMED
|
File without changes
|
|
File without changes
|
{microsegments-0.2.2 → microsegments-0.2.3}/tests/fixtures/stib55/vd55/20250319.snaps.parquet
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|