forked from enviPath/enviPy
[Feature] Timeseries Pathway view (#319)
**Warning depends on Timeseries feature to be merged** Implements a way to display OECD 301F data on the pathway view. This is mostly a PoC and needs to be improved once the pathway rendering is updated.  Co-authored-by: jebus <lorsbach@envipath.com> Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#319 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
This commit is contained in:
@ -2189,6 +2189,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
"uncovered_functional_groups": False,
|
"uncovered_functional_groups": False,
|
||||||
},
|
},
|
||||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||||
|
"timeseries": self.get_timeseries_data(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -2226,6 +2227,13 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
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):
|
def get_app_domain_assessment_data(self):
|
||||||
data = self.kv.get("app_domain_assessment", None)
|
data = self.kv.get("app_domain_assessment", None)
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ dependencies = [
|
|||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" }
|
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-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" }
|
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@ -361,6 +361,83 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
function node_popup(n) {
|
function node_popup(n) {
|
||||||
popupContent = "";
|
popupContent = "";
|
||||||
|
|
||||||
|
if (timeseriesViewEnabled && n.timeseries && n.timeseries.measurements) {
|
||||||
|
for (var s of n.scenarios) {
|
||||||
|
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
||||||
|
}
|
||||||
|
|
||||||
|
popupContent += '<div style="width:100%;height:120px"><canvas id="ts-popover-canvas"></canvas></div>';
|
||||||
|
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) {
|
if (n.stereo_removed) {
|
||||||
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,6 +61,11 @@
|
|||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has_timeseries {
|
||||||
|
stroke: #3b82f6;
|
||||||
|
stroke-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.highlighted {
|
.highlighted {
|
||||||
stroke: red;
|
stroke: red;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
@ -134,7 +139,6 @@
|
|||||||
{% include "actions/objects/pathway.html" %}
|
{% include "actions/objects/pathway.html" %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if pathway.setting.model.app_domain %}
|
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
||||||
<svg
|
<svg
|
||||||
@ -158,6 +162,7 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
||||||
>
|
>
|
||||||
|
{% if pathway.setting.model.app_domain %}
|
||||||
<li>
|
<li>
|
||||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||||
<svg
|
<svg
|
||||||
@ -181,9 +186,28 @@
|
|||||||
App Domain View
|
App Domain View
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a id="timeseries-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="timeseries-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||||
|
</svg>
|
||||||
|
OECD 301F View
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none gap-2">
|
<div class="flex-none gap-2">
|
||||||
<button
|
<button
|
||||||
@ -415,6 +439,8 @@
|
|||||||
<script>
|
<script>
|
||||||
// Global switch for app domain view
|
// Global switch for app domain view
|
||||||
var appDomainViewEnabled = false;
|
var appDomainViewEnabled = false;
|
||||||
|
// Global switch for timeseries view
|
||||||
|
var timeseriesViewEnabled = false;
|
||||||
|
|
||||||
function goFullscreen(id) {
|
function goFullscreen(id) {
|
||||||
var element = document.getElementById(id);
|
var element = document.getElementById(id);
|
||||||
@ -508,6 +534,35 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timeseries toggle
|
||||||
|
const timeseriesBtn = document.getElementById("timeseries-toggle-button");
|
||||||
|
if (timeseriesBtn) {
|
||||||
|
timeseriesBtn.addEventListener("click", function () {
|
||||||
|
timeseriesViewEnabled = !timeseriesViewEnabled;
|
||||||
|
const icon = document.getElementById("timeseries-icon");
|
||||||
|
|
||||||
|
if (timeseriesViewEnabled) {
|
||||||
|
icon.innerHTML =
|
||||||
|
'<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" style="stroke:#3b82f6"/>';
|
||||||
|
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
if (x.timeseries) {
|
||||||
|
d3.select(x.el)
|
||||||
|
.select("circle")
|
||||||
|
.classed("has_timeseries", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
icon.innerHTML =
|
||||||
|
'<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>';
|
||||||
|
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
d3.select(x.el).select("circle").classed("has_timeseries", false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show actions button if there are actions
|
// Show actions button if there are actions
|
||||||
const actionsButton = document.getElementById("actionsButton");
|
const actionsButton = document.getElementById("actionsButton");
|
||||||
const actionsList = actionsButton?.querySelector("ul");
|
const actionsList = actionsButton?.querySelector("ul");
|
||||||
|
|||||||
6
uv.lock
generated
6
uv.lock
generated
@ -712,7 +712,7 @@ requires-dist = [
|
|||||||
{ name = "django-polymorphic", specifier = ">=4.1.0" },
|
{ name = "django-polymorphic", specifier = ">=4.1.0" },
|
||||||
{ name = "django-stubs", marker = "extra == 'dev'", specifier = ">=5.2.4" },
|
{ name = "django-stubs", marker = "extra == 'dev'", specifier = ">=5.2.4" },
|
||||||
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.4" },
|
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.4" },
|
||||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.2.0" },
|
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.4.0" },
|
||||||
{ name = "envipy-ambit", git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" },
|
{ name = "envipy-ambit", git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" },
|
||||||
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
||||||
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
||||||
@ -741,8 +741,8 @@ provides-extras = ["ms-login", "dev"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.2.0"
|
version = "0.4.0"
|
||||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.2.0#02e3ae64b2ff42a5d2b723a76727c8f6755c9a90" }
|
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.4.0#c4ff23980bbd378a6d6b5bef778aa893310608e3" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user