diff --git a/epdb/models.py b/epdb/models.py index 00fcd175..cdbcd64c 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -2189,6 +2189,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin): "uncovered_functional_groups": False, }, "is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False), + "timeseries": self.get_timeseries_data(), } @staticmethod @@ -2226,6 +2227,13 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin): def as_svg(self): return IndigoUtils.mol_to_svg(self.default_node_label.smiles) + def get_timeseries_data(self): + for scenario in self.scenarios.all(): + for ai in scenario.get_additional_information(): + if ai.__class__.__name__ == "OECD301FTimeSeries": + return ai.model_dump(mode="json") + return None + def get_app_domain_assessment_data(self): data = self.kv.get("app_domain_assessment", None) diff --git a/pyproject.toml b/pyproject.toml index a19f10d5..c2b14a82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ [tool.uv.sources] enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" } envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" } -envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.2.0" } +envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.4.0" } envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" } [project.optional-dependencies] diff --git a/static/js/pw.js b/static/js/pw.js index c06e8668..e3639a03 100644 --- a/static/js/pw.js +++ b/static/js/pw.js @@ -361,6 +361,83 @@ function draw(pathway, elem) { function node_popup(n) { popupContent = ""; + + if (timeseriesViewEnabled && n.timeseries && n.timeseries.measurements) { + for (var s of n.scenarios) { + popupContent += "" + s.name + "
"; + } + + popupContent += '
'; + const tsMeasurements = n.timeseries.measurements; + setTimeout(() => { + const canvas = document.getElementById('ts-popover-canvas'); + if (canvas && window.Chart) { + const valid = tsMeasurements + .filter(m => m.timestamp != null && m.value != null) + .map(m => ({ ...m, timestamp: typeof m.timestamp === 'number' ? m.timestamp : new Date(m.timestamp).getTime() })) + .sort((a, b) => a.timestamp - b.timestamp); + + const datasets = []; + + // Error band (lower + upper with fill between) + const withErrors = valid.filter(m => m.error != null && m.error > 0); + if (withErrors.length > 0) { + datasets.push({ + data: withErrors.map(m => ({ x: m.timestamp, y: m.value - m.error })), + borderColor: 'rgba(59,130,246,0.3)', + backgroundColor: 'rgba(59,130,246,0.15)', + pointRadius: 0, + fill: false, + tension: 0.1, + }); + datasets.push({ + data: withErrors.map(m => ({ x: m.timestamp, y: m.value + m.error })), + borderColor: 'rgba(59,130,246,0.3)', + backgroundColor: 'rgba(59,130,246,0.15)', + pointRadius: 0, + fill: '-1', + tension: 0.1, + }); + } + + // Main value line + datasets.push({ + data: valid.map(m => ({ x: m.timestamp, y: m.value })), + borderColor: 'rgb(59,130,246)', + pointRadius: 0, + tension: 0.1, + fill: false, + }); + + new Chart(canvas.getContext('2d'), { + type: 'line', + data: { datasets }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { enabled: false }, + }, + scales: { + x: { + type: 'linear', + ticks: { font: { size: 10 } }, + title: { display: false }, + }, + y: { + ticks: { font: { size: 10 } }, + title: { display: false }, + }, + }, + }, + }); + } + }, 0); + + return popupContent; + } + if (n.stereo_removed) { popupContent += "Removed stereochemistry for prediction"; } diff --git a/templates/objects/pathway.html b/templates/objects/pathway.html index 549a6aae..ca7ecd5a 100644 --- a/templates/objects/pathway.html +++ b/templates/objects/pathway.html @@ -61,6 +61,11 @@ stroke-opacity: 0.6; } + .has_timeseries { + stroke: #3b82f6; + stroke-width: 3px; + } + .highlighted { stroke: red; stroke-width: 3px; @@ -134,30 +139,30 @@ {% include "actions/objects/pathway.html" %} - {% if pathway.setting.model.app_domain %} - - {% endif %} + {% endif %} +
  • + + + + + OECD 301F View + +
  • + +