diff --git a/docker/ontop-reload.sh b/docker/ontop-reload.sh new file mode 100755 index 000000000..91193b0e1 --- /dev/null +++ b/docker/ontop-reload.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Restart the dev ontop container and verify that what it serves matches +# docker/serviceConfigs/ontop/mapping.obda. +# +# ontop reads the OBDA mapping ONLY at startup: after a branch switch or a +# mapping edit, a running container silently keeps serving the old mapping +# (newly mapped tables return 0 rows). Run this after any mapping change. +# +# Usage: +# ./docker/ontop-reload.sh restart, wait for the endpoint, verify +# ./docker/ontop-reload.sh --check verify only (no restart) +# +# Environment overrides: ONTOP_CONTAINER (default: ontop), +# ONTOP_ENDPOINT (default: http://localhost:8080/sparql), ONTOP_TIMEOUT (120s). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONTAINER="${ONTOP_CONTAINER:-ontop}" +ENDPOINT="${ONTOP_ENDPOINT:-http://localhost:8080/sparql}" +MAPPING="$SCRIPT_DIR/serviceConfigs/ontop/mapping.obda" +# table-name predicate emitted by every row anchor (see oekg/registry) +TABLE_PRED="https://openenergyplatform.org/ontology/oeo/OEO_00000504" +TIMEOUT="${ONTOP_TIMEOUT:-120}" + +if [[ "${1:-}" != "--check" ]]; then + echo "Restarting '$CONTAINER'..." + docker restart "$CONTAINER" >/dev/null +fi + +printf 'Waiting for %s ' "$ENDPOINT" +deadline=$((SECONDS + TIMEOUT)) +until curl -sf -o /dev/null --data-urlencode 'query=ASK {}' "$ENDPOINT"; do + if ((SECONDS >= deadline)); then + printf '\nERROR: endpoint not answering after %ss\n' "$TIMEOUT" >&2 + exit 1 + fi + printf '.' + sleep 3 +done +printf ' up.\n' + +# Tables the mapping file on disk maps (FROM "schema"."table" in the source SQL). +mapfile -t expected < <(grep -oE 'FROM[[:space:]]+"[^"]+"\."[^"]+"' "$MAPPING" | + sed -E 's/.*\."([^"]+)"$/\1/' | sort -u) +if ((${#expected[@]} == 0)); then + echo "ERROR: no mapped tables found in $MAPPING" >&2 + exit 1 +fi + +# Tables the running endpoint actually serves, with row counts. +query="SELECT ?t (COUNT(?s) AS ?c) WHERE { ?s <$TABLE_PRED> ?t } GROUP BY ?t ORDER BY ?t" +csv="$(curl -sf -H 'Accept: text/csv' --data-urlencode "query=$query" "$ENDPOINT")" + +status=0 +echo "Mapped tables (mapping.obda on disk vs SPARQL endpoint):" +for t in "${expected[@]}"; do + count="$(printf '%s\n' "$csv" | tr -d '\r' | awk -F, -v t="$t" '$1 == t {print $2}')" + if [[ -n "${count:-}" && "$count" != "0" ]]; then + printf ' OK %-45s %s rows\n' "$t" "$count" + else + printf ' STALE %-45s not served (0 rows)\n' "$t" + status=1 + fi +done +if ((status != 0)); then + echo "Verification FAILED: the container serves an older mapping (or the table is empty in Postgres). Re-run without --check to restart." >&2 +fi +exit $status diff --git a/docker/serviceConfigs/ontop/README.md b/docker/serviceConfigs/ontop/README.md index 07ca09f81..7361b2b75 100644 --- a/docker/serviceConfigs/ontop/README.md +++ b/docker/serviceConfigs/ontop/README.md @@ -5,3 +5,30 @@ Download the database JDBC driver for ontop: - Add the file postgresql.jar to this directory. + +## Reloading the mapping (dev) + +ontop reads `mapping.obda` **only at startup**. If the file changes while the +container is running — a mapping edit, or a `git checkout` that rewrites it — +the container silently keeps serving the old mapping: newly mapped tables return +0 rows in SPARQL and the comparison frontend shows no data. + +After **any** change to `mapping.obda` (including branch switches), run: + +```sh +./docker/ontop-reload.sh +``` + +It restarts the `ontop` container, waits for the SPARQL endpoint, and then +verifies that every table mapped in `mapping.obda` on disk is actually served +(row count > 0 via the table-name predicate `oeo:OEO_00000504`). + +To only check whether the running container matches the file on disk, without +restarting: + +```sh +./docker/ontop-reload.sh --check +``` + +A `STALE` line means the container loaded an older mapping (restart it) — or the +table exists in the mapping but is empty in Postgres. diff --git a/docker/serviceConfigs/ontop/mapping.obda b/docker/serviceConfigs/ontop/mapping.obda index 77ab37ad4..b63d9f91c 100644 --- a/docker/serviceConfigs/ontop/mapping.obda +++ b/docker/serviceConfigs/ontop/mapping.obda @@ -8,7 +8,7 @@ foaf: http://xmlns.com/foaf/0.1/ obda: https://w3id.org/obda/vocabulary# rdfs: http://www.w3.org/2000/01/rdf-schema# oeo: https://openenergyplatform.org/ontology/oeo/ -oekg: http://openenergy-platform.org/ontology/oeo/oekg/ +oekg: https://openenergyplatform.org/ontology/oeo/oekg/ llc: https://www.omg.org/spec/LCC/Countries/ISO3166-1-CountryCodes/ [MappingDeclaration] @collection [[ @@ -18,183 +18,179 @@ target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} a oeo:IAO source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" mappingId eu_leg_data_2021_rep_table_1_spatial_region_AT -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Austria . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'AT' mappingId eu_leg_data_2021_rep_table_1_spatial_region_BE -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Belgium . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'BE' mappingId eu_leg_data_2021_rep_table_1_spatial_region_BG -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Bulgaria . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'BG' mappingId eu_leg_data_2021_rep_table_1_spatial_region_CH -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Switzerland . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'CH' mappingId eu_leg_data_2021_rep_table_1_spatial_region_CY -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Cyprus . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'CY' mappingId eu_leg_data_2021_rep_table_1_spatial_region_CZ -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Czechia . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'CZ' mappingId eu_leg_data_2021_rep_table_1_spatial_region_DE -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Germany . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'DE' mappingId eu_leg_data_2021_rep_table_1_spatial_region_DK -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Denmark . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'DK' mappingId eu_leg_data_2021_rep_table_1_spatial_region_EE -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Estonia . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'EE' mappingId eu_leg_data_2021_rep_table_1_spatial_region_EL -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Greece . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'EL' mappingId eu_leg_data_2021_rep_table_1_spatial_region_ES -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Spain . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'ES' mappingId eu_leg_data_2021_rep_table_1_spatial_region_FI -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Finland . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'FI' mappingId eu_leg_data_2021_rep_table_1_spatial_region_FR -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:France . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'FR' mappingId eu_leg_data_2021_rep_table_1_spatial_region_GB -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:UnitedKingdomOfGreatBritainAndNorthernIreland . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'GB' mappingId eu_leg_data_2021_rep_table_1_spatial_region_GR -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Greece . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'GR' mappingId eu_leg_data_2021_rep_table_1_spatial_region_HR -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Croatia . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'HR' mappingId eu_leg_data_2021_rep_table_1_spatial_region_HU -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Hungary . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'HU' mappingId eu_leg_data_2021_rep_table_1_spatial_region_IE -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Ireland . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'IE' mappingId eu_leg_data_2021_rep_table_1_spatial_region_IS -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Iceland . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'IS' mappingId eu_leg_data_2021_rep_table_1_spatial_region_IT -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Italy . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'IT' mappingId eu_leg_data_2021_rep_table_1_spatial_region_LT -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Lithuania . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'LT' mappingId eu_leg_data_2021_rep_table_1_spatial_region_LU -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Luxembourg . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'LU' mappingId eu_leg_data_2021_rep_table_1_spatial_region_LV -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Latvia . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'LV' mappingId eu_leg_data_2021_rep_table_1_spatial_region_MT -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Malta . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'MT' mappingId eu_leg_data_2021_rep_table_1_spatial_region_NL -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Netherlands . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'NL' mappingId eu_leg_data_2021_rep_table_1_spatial_region_NO -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Norway . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'NO' mappingId eu_leg_data_2021_rep_table_1_spatial_region_PL -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Poland . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'PL' mappingId eu_leg_data_2021_rep_table_1_spatial_region_PT -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Portugal . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'PT' mappingId eu_leg_data_2021_rep_table_1_spatial_region_RO -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Romania . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'RO' mappingId eu_leg_data_2021_rep_table_1_spatial_region_SE -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Sweden . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'SE' mappingId eu_leg_data_2021_rep_table_1_spatial_region_SI -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Slovenia . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'SI' mappingId eu_leg_data_2021_rep_table_1_spatial_region_SK -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:Slovakia . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'SK' mappingId eu_leg_data_2021_rep_table_1_spatial_region_UK -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020221 llc:UnitedKingdomOfGreatBritainAndNorthernIreland . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "country_code" = 'UK' -mappingId eu_leg_data_2021_rep_table_1_crf_based_sector_division -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division "{category}" . -source SELECT "id", "category" FROM "data"."eu_leg_data_2021_rep_table_1" - mappingId eu_leg_data_2021_rep_table_1_scenario_year target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020224 "{year}" . source SELECT "id", "year" FROM "data"."eu_leg_data_2021_rep_table_1" mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_CH4 -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 oeo:OEO_00000025 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "gas" = 'CH4' mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_CO2 -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 oeo:OEO_00000006 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "gas" = 'CO2' mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_N2O -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 oeo:OEO_00000027 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "gas" = 'N2O' mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_NF3 -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 oeo:OEO_00000026 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "gas" = 'NF3' mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_HFC -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 oeo:OEO_00000219 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "gas" = 'HFC' mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_PFC -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 oeo:OEO_00000322 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "gas" = 'PFC' mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_SF6 -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00010121 oeo:OEO_00000038 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "gas" = 'SF6' mappingId eu_leg_data_2021_rep_table_1_scenario_WEM -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020226 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020226 oeo:OEO_00020311 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "scenario" = 'WEM' mappingId eu_leg_data_2021_rep_table_1_scenario_WAM -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020226 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020226 oeo:OEO_00020312 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "scenario" = 'WAM' mappingId eu_leg_data_2021_rep_table_1_scenario_WOM -target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020226 . +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00020226 oeo:OEO_00020310 . source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "scenario" = 'WOM' mappingId eu_leg_data_2021_rep_table_1_unit_of_measurement @@ -205,4 +201,516 @@ mappingId eu_leg_data_2021_rep_table_1_greenhouse_gas_emission_value target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:OEO_00140178 "{value}" . source SELECT "id", "value" FROM "data"."eu_leg_data_2021_rep_table_1" +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010038 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010038 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1 Energy' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010039 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010039 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A Fuel combustion' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010040 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010040 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.1 Energy industries' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010158 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010158 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.1.a Public electricity and heat production' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010159 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010159 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.1.b Petroleum refining' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010160 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010160 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.1.c Manufacture of solid fuels and other energy industries' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010041 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010041 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.2 Manufacturing industries and construction' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010042 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010042 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.3 Transport' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010059 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010059 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.3.a Domestic aviation' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010060 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010060 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.3.b Road transportation' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010061 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010061 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.3.c Railways' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010062 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010062 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.3.d Domestic navigation' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010063 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010063 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.3.e Other transportation' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010043 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010043 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.4 Other sectors' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010052 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010052 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.4.a Commercial/Institutional' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010053 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010053 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.4.b Residential' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010054 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010054 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.4.c Agriculture/Forestry/Fishing' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010044 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010044 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.A.5 Other' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010057 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010057 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.B Fugitive emissions from fuels' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010161 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010161 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.B.1 Solid fuels' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010162 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010162 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.B.2 Oil and natural gas and other emissions from energy production' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010058 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010058 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '1.C CO2 transport and storage' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010046 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010046 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2 Industrial processes' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010164 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010164 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.A Mineral Industry' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010165 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010165 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.A.1 Cement production' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010166 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010166 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.B Chemical industry' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010167 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010167 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.C Metal industry' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010168 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010168 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.C.1 Iron and steel production' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010169 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010169 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.D Non-energy products from fuels and solvent use' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010170 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010170 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.E Electronics industry' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010171 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010171 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.F Product uses as substitutes for ODS' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010172 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010172 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.G Other product manufacture and use' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010173 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010173 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '2.H Other' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010047 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010047 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3 Agriculture' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010179 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010179 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.A Enteric fermentation' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010180 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010180 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.B Manure management' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010181 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010181 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.C Rice cultivation' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010182 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010182 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.D Agricultural soils' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010183 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010183 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.E Prescribed burning of savannahs' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010184 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010184 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.F Field burning of agricultural residues' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010185 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010185 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.G Liming' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010186 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010186 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.H Urea application' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010187 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010187 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.I Other carbon-containing fertilizers' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010188 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010188 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '3.J Other' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010048 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010048 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '4 Land Use, Land-Use Change and Forestry' + +mappingId eu_leg_data_2021_rep_table_1_category_OEO_00010189 +target oekg:data-descriptor/eu_leg_data_2021_rep_table_1/{id} oeo:has_sector_division oeo:OEO_00010189 . +source SELECT "id" FROM "data"."eu_leg_data_2021_rep_table_1" WHERE "category" = '4.A Forest land' + +mappingId ariadne2_data_with_labels_TargetClass +target oekg:data-descriptor/ariadne2_data_with_labels/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "ariadne2_data_with_labels"^^xsd:string . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" + +mappingId ariadne2_data_with_labels_scenario_year +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:OEO_00020224 "{scenario_year}" . +source SELECT "id", "scenario_year" FROM "data"."ariadne2_data_with_labels" + +mappingId ariadne2_data_with_labels_quantity_value +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:OEO_00140178 "{value}" . +source SELECT "id", "value" FROM "data"."ariadne2_data_with_labels" + +mappingId ariadne2_data_with_labels_unit +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:OEO_00040010 "{unit}" . +source SELECT "id", "unit" FROM "data"."ariadne2_data_with_labels" + +mappingId ariadne2_data_with_labels_scenario +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oekg:scenario "{scenario}" . +source SELECT "id", "scenario" FROM "data"."ariadne2_data_with_labels" + +mappingId ariadne2_data_with_labels_quantity_kind +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oekg:quantity_kind "{quantity_kind}" . +source SELECT "id", split_part("iamc_full_string", ' | ', 1) AS quantity_kind FROM "data"."ariadne2_data_with_labels" + +mappingId ariadne2_data_with_labels_spatial_region_DEU +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:OEO_00020221 llc:Germany . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE "region" = 'DEU' + +mappingId ariadne2_data_with_labels_sector_transportation +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00000422 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | transportation | %' + +mappingId ariadne2_data_with_labels_transport_mode_bus +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010277 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | bus | %' + +mappingId ariadne2_data_with_labels_transport_mode_rail +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010280 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | rail | %' + +mappingId ariadne2_data_with_labels_transport_mode_truck +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010278 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | truck | %' + +mappingId ariadne2_data_with_labels_transport_mode_domestic_aviation +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010059 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | domestic aviation | %' + +mappingId ariadne2_data_with_labels_transport_mode_domestic_navigation +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010062 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | domestic navigation | %' + +mappingId ariadne2_data_with_labels_technology_bev +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010024 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | bev | %' + +mappingId ariadne2_data_with_labels_technology_fcev +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010025 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | fcev | %' + +mappingId ariadne2_data_with_labels_technology_ice +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00000240 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | ice | %' + +mappingId ariadne2_data_with_labels_technology_phev +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00010030 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | phev | %' + +mappingId ariadne2_data_with_labels_technology_overhead_line +target oekg:data-descriptor/ariadne2_data_with_labels/{id} oeo:IAO_0000136 oeo:OEO_00000047 . +source SELECT "id" FROM "data"."ariadne2_data_with_labels" WHERE (' | ' || "iamc_full_string" || ' | ') LIKE '% | overhead line | %' + +mappingId amiris_germany2019_biogas_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_biogas/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_biogas"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_biogas" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_biogas_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_biogas/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_biogas"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_biogas" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_biogas_received_money_in_eur +target oekg:data-descriptor/amiris_germany2019_biogas/received_money_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_biogas"^^xsd:string ; oeo:OEO_00140178 "{received_money_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020128 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_money_in_eur", "time_step" FROM "data"."amiris_germany2019_biogas" WHERE "received_money_in_eur" IS NOT NULL + +mappingId amiris_germany2019_biogas_variable_costs_in_eur +target oekg:data-descriptor/amiris_germany2019_biogas/variable_costs_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_biogas"^^xsd:string ; oeo:OEO_00140178 "{variable_costs_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020145 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "variable_costs_in_eur", "time_step" FROM "data"."amiris_germany2019_biogas" WHERE "variable_costs_in_eur" IS NOT NULL + +mappingId amiris_germany2019_biogas_fixed_costs_in_eur +target oekg:data-descriptor/amiris_germany2019_biogas/fixed_costs_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_biogas"^^xsd:string ; oeo:OEO_00140178 "{fixed_costs_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020168 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "fixed_costs_in_eur", "time_step" FROM "data"."amiris_germany2019_biogas" WHERE "fixed_costs_in_eur" IS NOT NULL + +mappingId amiris_germany2019_biogas_investment_annuity_in_eur +target oekg:data-descriptor/amiris_germany2019_biogas/investment_annuity_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_biogas"^^xsd:string ; oeo:OEO_00140178 "{investment_annuity_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020167 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "investment_annuity_in_eur", "time_step" FROM "data"."amiris_germany2019_biogas" WHERE "investment_annuity_in_eur" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_co2_emissions_in_t +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/co2_emissions_in_t/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{co2_emissions_in_t}" ; oeo:OEO_00040010 "t" ; oeo:IAO_0000136 oeo:OEO_00260007 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "co2_emissions_in_t", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "co2_emissions_in_t" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_fuel_consumption_in_thermal_mwh +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/fuel_consumption_in_thermal_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{fuel_consumption_in_thermal_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00010210 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "fuel_consumption_in_thermal_mwh", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "fuel_consumption_in_thermal_mwh" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_received_money_in_eur +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/received_money_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{received_money_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020128 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_money_in_eur", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "received_money_in_eur" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_variable_costs_in_eur +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/variable_costs_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{variable_costs_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020145 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "variable_costs_in_eur", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "variable_costs_in_eur" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_fixed_costs_in_eur +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/fixed_costs_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{fixed_costs_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020168 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "fixed_costs_in_eur", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "fixed_costs_in_eur" IS NOT NULL + +mappingId amiris_germany2019_conventional_plant_operator_investment_annuity_in_eur +target oekg:data-descriptor/amiris_germany2019_conventional_plant_operator/investment_annuity_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_plant_operator"^^xsd:string ; oeo:OEO_00140178 "{investment_annuity_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020167 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "investment_annuity_in_eur", "time_step" FROM "data"."amiris_germany2019_conventional_plant_operator" WHERE "investment_annuity_in_eur" IS NOT NULL + +mappingId amiris_germany2019_conventional_trader_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_conventional_trader/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_conventional_trader" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_conventional_trader_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_conventional_trader/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_conventional_trader" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_conventional_trader_requested_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_conventional_trader/requested_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_conventional_trader"^^xsd:string ; oeo:OEO_00140178 "{requested_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "requested_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_conventional_trader" WHERE "requested_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_day_ahead_market_single_zone_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_day_ahead_market_single_zone/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_day_ahead_market_single_zone"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_day_ahead_market_single_zone" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_day_ahead_market_single_zone_electricity_price_in_eur_per_mwh +target oekg:data-descriptor/amiris_germany2019_day_ahead_market_single_zone/electricity_price_in_eur_per_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_day_ahead_market_single_zone"^^xsd:string ; oeo:OEO_00140178 "{electricity_price_in_eur_per_mwh}" ; oeo:OEO_00040010 "EUR/MWh" ; oeo:IAO_0000136 oeo:OEO_00020117 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "electricity_price_in_eur_per_mwh", "time_step" FROM "data"."amiris_germany2019_day_ahead_market_single_zone" WHERE "electricity_price_in_eur_per_mwh" IS NOT NULL + +mappingId amiris_germany2019_day_ahead_market_single_zone_dispatch_system_cost_in_eur +target oekg:data-descriptor/amiris_germany2019_day_ahead_market_single_zone/dispatch_system_cost_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_day_ahead_market_single_zone"^^xsd:string ; oeo:OEO_00140178 "{dispatch_system_cost_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020116 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "dispatch_system_cost_in_eur", "time_step" FROM "data"."amiris_germany2019_day_ahead_market_single_zone" WHERE "dispatch_system_cost_in_eur" IS NOT NULL + +mappingId amiris_germany2019_demand_trader_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_demand_trader/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_demand_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_demand_trader" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_demand_trader_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_demand_trader/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_demand_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_demand_trader" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_demand_trader_requested_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_demand_trader/requested_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_demand_trader"^^xsd:string ; oeo:OEO_00140178 "{requested_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "requested_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_demand_trader" WHERE "requested_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_received_money_in_eur +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/received_money_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{received_money_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020128 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_money_in_eur", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "received_money_in_eur" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_offered_charge_price_in_eur_per_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/offered_charge_price_in_eur_per_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_charge_price_in_eur_per_mwh}" ; oeo:OEO_00040010 "EUR/MWh" ; oeo:IAO_0000136 oeo:OEO_00020117 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_charge_price_in_eur_per_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "offered_charge_price_in_eur_per_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_offered_discharge_price_in_eur_per_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/offered_discharge_price_in_eur_per_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_discharge_price_in_eur_per_mwh}" ; oeo:OEO_00040010 "EUR/MWh" ; oeo:IAO_0000136 oeo:OEO_00020117 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_discharge_price_in_eur_per_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "offered_discharge_price_in_eur_per_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_awarded_charge_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/awarded_charge_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_charge_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_charge_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "awarded_charge_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_awarded_discharge_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/awarded_discharge_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_discharge_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_discharge_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "awarded_discharge_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_stored_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/stored_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{stored_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "stored_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "stored_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_variable_costs_in_eur +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/variable_costs_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{variable_costs_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020145 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "variable_costs_in_eur", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "variable_costs_in_eur" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_dispatch_multiplier +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/dispatch_multiplier/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{dispatch_multiplier}" ; oeo:OEO_00040010 "1" ; oeo:IAO_0000136 oeo:OEO_00240016 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "dispatch_multiplier", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "dispatch_multiplier" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_electricity_price_prediction_in_eur_per_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/electricity_price_prediction_in_eur_per_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{electricity_price_prediction_in_eur_per_mwh}" ; oeo:OEO_00040010 "EUR/MWh" ; oeo:IAO_0000136 oeo:OEO_00020117 ; oeo:IAO_0000136 oeo:OEO_00000063 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "electricity_price_prediction_in_eur_per_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "electricity_price_prediction_in_eur_per_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_generic_flexibility_trader_requested_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_generic_flexibility_trader/requested_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_generic_flexibility_trader"^^xsd:string ; oeo:OEO_00140178 "{requested_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "requested_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_generic_flexibility_trader" WHERE "requested_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_no_support_trader_received_support_in_eur +target oekg:data-descriptor/amiris_germany2019_no_support_trader/received_support_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_no_support_trader"^^xsd:string ; oeo:OEO_00140178 "{received_support_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020125 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_support_in_eur", "time_step" FROM "data"."amiris_germany2019_no_support_trader" WHERE "received_support_in_eur" IS NOT NULL + +mappingId amiris_germany2019_no_support_trader_refunded_support_in_eur +target oekg:data-descriptor/amiris_germany2019_no_support_trader/refunded_support_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_no_support_trader"^^xsd:string ; oeo:OEO_00140178 "{refunded_support_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020125 ; oeo:IAO_0000136 oeo:OEO_00020124 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "refunded_support_in_eur", "time_step" FROM "data"."amiris_germany2019_no_support_trader" WHERE "refunded_support_in_eur" IS NOT NULL + +mappingId amiris_germany2019_no_support_trader_received_market_revenues +target oekg:data-descriptor/amiris_germany2019_no_support_trader/received_market_revenues/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_no_support_trader"^^xsd:string ; oeo:OEO_00140178 "{received_market_revenues}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020128 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_market_revenues", "time_step" FROM "data"."amiris_germany2019_no_support_trader" WHERE "received_market_revenues" IS NOT NULL + +mappingId amiris_germany2019_no_support_trader_true_generation_potential_in_mwh +target oekg:data-descriptor/amiris_germany2019_no_support_trader/true_generation_potential_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_no_support_trader"^^xsd:string ; oeo:OEO_00140178 "{true_generation_potential_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140139 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "true_generation_potential_in_mwh", "time_step" FROM "data"."amiris_germany2019_no_support_trader" WHERE "true_generation_potential_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_no_support_trader_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_no_support_trader/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_no_support_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_no_support_trader" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_no_support_trader_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_no_support_trader/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_no_support_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_no_support_trader" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_no_support_trader_requested_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_no_support_trader/requested_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_no_support_trader"^^xsd:string ; oeo:OEO_00140178 "{requested_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "requested_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_no_support_trader" WHERE "requested_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_renewable_trader_received_support_in_eur +target oekg:data-descriptor/amiris_germany2019_renewable_trader/received_support_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_renewable_trader"^^xsd:string ; oeo:OEO_00140178 "{received_support_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020125 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_support_in_eur", "time_step" FROM "data"."amiris_germany2019_renewable_trader" WHERE "received_support_in_eur" IS NOT NULL + +mappingId amiris_germany2019_renewable_trader_refunded_support_in_eur +target oekg:data-descriptor/amiris_germany2019_renewable_trader/refunded_support_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_renewable_trader"^^xsd:string ; oeo:OEO_00140178 "{refunded_support_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020125 ; oeo:IAO_0000136 oeo:OEO_00020124 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "refunded_support_in_eur", "time_step" FROM "data"."amiris_germany2019_renewable_trader" WHERE "refunded_support_in_eur" IS NOT NULL + +mappingId amiris_germany2019_renewable_trader_received_market_revenues +target oekg:data-descriptor/amiris_germany2019_renewable_trader/received_market_revenues/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_renewable_trader"^^xsd:string ; oeo:OEO_00140178 "{received_market_revenues}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020128 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_market_revenues", "time_step" FROM "data"."amiris_germany2019_renewable_trader" WHERE "received_market_revenues" IS NOT NULL + +mappingId amiris_germany2019_renewable_trader_true_generation_potential_in_mwh +target oekg:data-descriptor/amiris_germany2019_renewable_trader/true_generation_potential_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_renewable_trader"^^xsd:string ; oeo:OEO_00140178 "{true_generation_potential_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140139 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "true_generation_potential_in_mwh", "time_step" FROM "data"."amiris_germany2019_renewable_trader" WHERE "true_generation_potential_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_renewable_trader_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_renewable_trader/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_renewable_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_renewable_trader" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_renewable_trader_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_renewable_trader/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_renewable_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_renewable_trader" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_renewable_trader_requested_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_renewable_trader/requested_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_renewable_trader"^^xsd:string ; oeo:OEO_00140178 "{requested_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "requested_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_renewable_trader" WHERE "requested_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_sensitivity_forecaster_awarded_energy_forecast_in_mwh +target oekg:data-descriptor/amiris_germany2019_sensitivity_forecaster/awarded_energy_forecast_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_sensitivity_forecaster"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_forecast_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00010411 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_forecast_in_mwh", "time_step" FROM "data"."amiris_germany2019_sensitivity_forecaster" WHERE "awarded_energy_forecast_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_sensitivity_forecaster_electricity_price_forecast_in_eur_per_mwh +target oekg:data-descriptor/amiris_germany2019_sensitivity_forecaster/electricity_price_forecast_in_eur_per_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_sensitivity_forecaster"^^xsd:string ; oeo:OEO_00140178 "{electricity_price_forecast_in_eur_per_mwh}" ; oeo:OEO_00040010 "EUR/MWh" ; oeo:IAO_0000136 oeo:OEO_00020117 ; oeo:IAO_0000136 oeo:OEO_00010411 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "electricity_price_forecast_in_eur_per_mwh", "time_step" FROM "data"."amiris_germany2019_sensitivity_forecaster" WHERE "electricity_price_forecast_in_eur_per_mwh" IS NOT NULL + +mappingId amiris_germany2019_system_operator_trader_received_support_in_eur +target oekg:data-descriptor/amiris_germany2019_system_operator_trader/received_support_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_system_operator_trader"^^xsd:string ; oeo:OEO_00140178 "{received_support_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020125 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_support_in_eur", "time_step" FROM "data"."amiris_germany2019_system_operator_trader" WHERE "received_support_in_eur" IS NOT NULL + +mappingId amiris_germany2019_system_operator_trader_refunded_support_in_eur +target oekg:data-descriptor/amiris_germany2019_system_operator_trader/refunded_support_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_system_operator_trader"^^xsd:string ; oeo:OEO_00140178 "{refunded_support_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020125 ; oeo:IAO_0000136 oeo:OEO_00020124 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "refunded_support_in_eur", "time_step" FROM "data"."amiris_germany2019_system_operator_trader" WHERE "refunded_support_in_eur" IS NOT NULL + +mappingId amiris_germany2019_system_operator_trader_received_market_revenues +target oekg:data-descriptor/amiris_germany2019_system_operator_trader/received_market_revenues/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_system_operator_trader"^^xsd:string ; oeo:OEO_00140178 "{received_market_revenues}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020128 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_market_revenues", "time_step" FROM "data"."amiris_germany2019_system_operator_trader" WHERE "received_market_revenues" IS NOT NULL + +mappingId amiris_germany2019_system_operator_trader_true_generation_potential_in_mwh +target oekg:data-descriptor/amiris_germany2019_system_operator_trader/true_generation_potential_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_system_operator_trader"^^xsd:string ; oeo:OEO_00140178 "{true_generation_potential_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140139 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "true_generation_potential_in_mwh", "time_step" FROM "data"."amiris_germany2019_system_operator_trader" WHERE "true_generation_potential_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_system_operator_trader_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_system_operator_trader/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_system_operator_trader"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_system_operator_trader" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_system_operator_trader_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_system_operator_trader/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_system_operator_trader"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_system_operator_trader" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_system_operator_trader_requested_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_system_operator_trader/requested_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_system_operator_trader"^^xsd:string ; oeo:OEO_00140178 "{requested_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "requested_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_system_operator_trader" WHERE "requested_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_variable_renewable_operator_awarded_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_variable_renewable_operator/awarded_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_variable_renewable_operator"^^xsd:string ; oeo:OEO_00140178 "{awarded_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140122 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "awarded_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_variable_renewable_operator" WHERE "awarded_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_variable_renewable_operator_offered_energy_in_mwh +target oekg:data-descriptor/amiris_germany2019_variable_renewable_operator/offered_energy_in_mwh/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_variable_renewable_operator"^^xsd:string ; oeo:OEO_00140178 "{offered_energy_in_mwh}" ; oeo:OEO_00040010 "MWh" ; oeo:IAO_0000136 oeo:OEO_00000139 ; oeo:IAO_0000136 oeo:OEO_00140121 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "offered_energy_in_mwh", "time_step" FROM "data"."amiris_germany2019_variable_renewable_operator" WHERE "offered_energy_in_mwh" IS NOT NULL + +mappingId amiris_germany2019_variable_renewable_operator_received_money_in_eur +target oekg:data-descriptor/amiris_germany2019_variable_renewable_operator/received_money_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_variable_renewable_operator"^^xsd:string ; oeo:OEO_00140178 "{received_money_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020128 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "received_money_in_eur", "time_step" FROM "data"."amiris_germany2019_variable_renewable_operator" WHERE "received_money_in_eur" IS NOT NULL + +mappingId amiris_germany2019_variable_renewable_operator_variable_costs_in_eur +target oekg:data-descriptor/amiris_germany2019_variable_renewable_operator/variable_costs_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_variable_renewable_operator"^^xsd:string ; oeo:OEO_00140178 "{variable_costs_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020145 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "variable_costs_in_eur", "time_step" FROM "data"."amiris_germany2019_variable_renewable_operator" WHERE "variable_costs_in_eur" IS NOT NULL + +mappingId amiris_germany2019_variable_renewable_operator_fixed_costs_in_eur +target oekg:data-descriptor/amiris_germany2019_variable_renewable_operator/fixed_costs_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_variable_renewable_operator"^^xsd:string ; oeo:OEO_00140178 "{fixed_costs_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020168 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "fixed_costs_in_eur", "time_step" FROM "data"."amiris_germany2019_variable_renewable_operator" WHERE "fixed_costs_in_eur" IS NOT NULL + +mappingId amiris_germany2019_variable_renewable_operator_investment_annuity_in_eur +target oekg:data-descriptor/amiris_germany2019_variable_renewable_operator/investment_annuity_in_eur/{id} a oeo:IAO_0000027 ; oeo:OEO_00000504 "amiris_germany2019_variable_renewable_operator"^^xsd:string ; oeo:OEO_00140178 "{investment_annuity_in_eur}" ; oeo:OEO_00040010 "EUR" ; oeo:IAO_0000136 oeo:OEO_00020167 ; oekg:time_step "{time_step}"^^xsd:dateTime ; oeo:OEO_00020224 "2019" . +source SELECT "id", "investment_annuity_in_eur", "time_step" FROM "data"."amiris_germany2019_variable_renewable_operator" WHERE "investment_annuity_in_eur" IS NOT NULL + ]] diff --git a/factsheet/frontend/src/components/comparison/RegistryComparison.jsx b/factsheet/frontend/src/components/comparison/RegistryComparison.jsx new file mode 100644 index 000000000..ab26c8aca --- /dev/null +++ b/factsheet/frontend/src/components/comparison/RegistryComparison.jsx @@ -0,0 +1,519 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Registry-driven quantitative comparison (beta). Works for ANY annotated table: +// dimensions, predicates, value IRIs and labels come from /oekg/registry/, with +// ontology labels/definitions loaded from the TIB Terminology Service. +// +// UX: +// * Only dimensions/presets the table actually populates are shown (discovery). +// * Presets give one-click → result; a plain-language customiser with ontology +// labels (not variable keys). +// * Unit selector — values are only summed within one unit (mixing is wrong). +// * "How it works" decomposition reveal tucked into an accordion. + +import React, { useEffect, useMemo, useState } from "react"; +import ReactECharts from "echarts-for-react"; +import axios from "axios"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import Stack from "@mui/material/Stack"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import Chip from "@mui/material/Chip"; +import Tooltip from "@mui/material/Tooltip"; +import Link from "@mui/material/Link"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import MenuItem from "@mui/material/MenuItem"; +import Alert from "@mui/material/Alert"; +import LinearProgress from "@mui/material/LinearProgress"; +import Accordion from "@mui/material/Accordion"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; + +import conf from "../../conf.json"; +import CSRFToken from "../csrfToken.js"; +import useRegistry from "./useRegistry.js"; +import { + buildComparisonQuery, labelForIri, expandCurie, + dimensionAskQuery, valueFrequencyQuery, +} from "./registryQuery.js"; +import { resolveTerms } from "./tibTerms.js"; + +const PALETTE = [ + "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", + "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf", +]; +const DELIM = " | "; +const ROWS_SCHEMA = "model_draft"; + +const titleCase = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +const shorten = (s) => (s && s.startsWith("http") ? s.split("/").pop() : s); +const cellValue = (registry, dim, row) => { + const b = row[dim.key]; + if (!b) return null; + return dim.object_kind === "iri" ? labelForIri(registry, dim, b.value) : b.value; +}; + +function TermChip({ info, fallbackLabel, fullIri, color }) { + const label = info?.label || shorten(fallbackLabel); + const desc = info?.description || "Loading definition…"; + return ( + {desc}}> + + + ); +} + +export default function RegistryComparison() { + const { registry, loading: registryLoading, error: registryError } = useRegistry(); + + const [table, setTable] = useState("ariadne2_data_with_labels"); + const [xKey, setXKey] = useState("scenario_year"); + const [stackKey, setStackKey] = useState("technology"); + const [unitOptions, setUnitOptions] = useState([]); + const [unit, setUnit] = useState(""); + const [quantityOptions, setQuantityOptions] = useState([]); // [{value,count}] most common first + const [primaryQuantity, setPrimaryQuantity] = useState(""); // "" = all quantities + const [chartType, setChartType] = useState("stacked"); // stacked | grouped | line + const [availableKeys, setAvailableKeys] = useState(null); // Set | null(=show all) + const [discovering, setDiscovering] = useState(false); + const [rawString, setRawString] = useState(null); + const [segments, setSegments] = useState([]); + const [rows, setRows] = useState(null); + const [terms, setTerms] = useState({}); + const [dimTerms, setDimTerms] = useState({}); + const [running, setRunning] = useState(false); + const [err, setErr] = useState(null); + const [lastQuery, setLastQuery] = useState(""); + + const allAxisDims = useMemo( + () => (registry?.dimensions || []).filter((d) => !["quantity_value", "unit"].includes(d.key)), + [registry] + ); + const axisDims = useMemo( + () => (availableKeys ? allAxisDims.filter((d) => availableKeys.has(d.key)) : allAxisDims), + [allAxisDims, availableKeys] + ); + const dimLabel = (d) => (d?.concept && dimTerms[d.concept]?.label) || titleCase(d?.key || ""); + + // short inline hint: how rows qualify for this dimension + const dimSourceShort = (d) => + !d ? "" : + d.value_space === "iamc_tokens" ? "matched from variable-string tokens" : + d.object_kind === "literal" ? "raw value of the column" : + "controlled value → ontology IRI"; + // full hover: how rows qualify + which predicate + ontology definition + const dimTooltip = (d) => { + if (!d) return ""; + const how = + d.value_space === "iamc_tokens" + ? `Qualifies rows whose variable string contains a ${titleCase(d.key)} token (e.g. ${(d.values || []).slice(0, 4).map((v) => v.code).join(", ")}).` + : d.object_kind === "literal" + ? `Uses the raw value of the “${titleCase(d.key)}” column.` + : `Qualifies rows whose value maps to an ontology IRI (controlled vocabulary).`; + const def = d.concept && dimTerms[d.concept]?.description; + return `${how} — predicate ${d.predicate}.${def ? " · " + def : ""}`; + }; + + const tokenIndex = useMemo(() => { + const idx = {}; + for (const d of registry?.dimensions || []) { + if (d.value_space !== "iamc_tokens") continue; + for (const v of d.values || []) idx[v.code.toLowerCase()] = { ...v, dimension: d.key }; + } + return idx; + }, [registry]); + + const postSparql = async (query) => { + const res = await axios.post(conf.obdi, query, { + headers: { + "X-CSRFToken": CSRFToken(), + Accept: "application/sparql-results+json", + "Content-Type": "application/sparql-query", + }, + }); + return res.data; + }; + + const timeDim = useMemo( + () => axisDims.find((d) => d.key === "scenario_year") || + axisDims.find((d) => (d.datatype || "").includes("gYear")) || null, + [axisDims] + ); + // dimensions offered as breakdowns/axes (quantity is the primary filter, not a breakdown) + const breakdownDims = useMemo( + () => axisDims.filter((d) => !(primaryQuantity && d.key === "quantity_kind")), + [axisDims, primaryQuantity] + ); + const presets = useMemo(() => { + if (!timeDim) return []; + return breakdownDims + .filter((d) => d.key !== timeDim.key && + (d.object_kind !== "iri" || (d.values || []).length > 0)) // literal dims always groupable + .map((d) => ({ name: `${titleCase(d.key)} over ${titleCase(timeDim.key)}`, x: timeDim.key, stack: d.key })); + }, [breakdownDims, timeDim]); + + // dimension concept labels (for the controls) + useEffect(() => { + if (!registry) return; + let active = true; + resolveTerms((registry.dimensions || []).map((d) => d.concept).filter(Boolean)) + .then((m) => active && setDimTerms(m)); + return () => { active = false; }; + }, [registry]); + + // DISCOVERY: which dimensions/units does THIS table populate? + useEffect(() => { + if (!registry) return; + let active = true; + setDiscovering(true); setAvailableKeys(null); setRows(null); + (async () => { + const t = table.trim(); + // availability via ASK per dimension + let avail = null; + try { + const res = await Promise.all(allAxisDims.map(async (dm) => { + try { const r = await postSparql(dimensionAskQuery({ registry, table: t, dim: dm })); return [dm.key, !!r.boolean]; } + catch { return [dm.key, true]; } + })); + avail = new Set(res.filter(([, ok]) => ok).map(([k]) => k)); + } catch (e) { avail = null; } + // primary quantity options (the first IAMC segment), most common first + let quants = []; + try { + const qd = allAxisDims.find((d) => d.key === "quantity_kind"); + if (qd) { + const r = await postSparql(valueFrequencyQuery({ registry, table: t, dim: qd })); + quants = (r.results?.bindings || []).map((b) => ({ value: b.v.value, count: +(b.c?.value || 0) })); + } + } catch (e) { /* ignore */ } + if (!active) return; + setAvailableKeys(avail); + setQuantityOptions(quants); + setPrimaryQuantity((cur) => (quants.find((q) => q.value === cur) ? cur : (quants[0]?.value || ""))); + if (avail) { + // quantity_kind is the PRIMARY filter, not a breakdown + const keys = [...avail].filter((k) => k !== "quantity_kind"); + setXKey((cur) => (avail.has(cur) ? cur : (avail.has("scenario_year") ? "scenario_year" : keys[0])) || cur); + setStackKey((cur) => (avail.has(cur) && cur !== "quantity_kind" ? cur : (keys.find((k) => k !== "scenario_year") || keys[0])) || cur); + } + setDiscovering(false); + })(); + return () => { active = false; }; + }, [registry, table, allAxisDims]); + + // UNITS — follow the chosen quantity (the unit is determined by the quantity) + useEffect(() => { + if (!registry) return; + let active = true; + (async () => { + const unitDim = (registry.dimensions || []).find((d) => d.key === "unit"); + const qkDim = allAxisDims.find((d) => d.key === "quantity_kind"); + if (!unitDim) return; + try { + const d = await postSparql(valueFrequencyQuery({ + registry, table: table.trim(), dim: unitDim, + scopeDim: primaryQuantity ? qkDim : null, scopeValue: primaryQuantity || null, + })); + const us = (d.results?.bindings || []).map((b) => b.v.value); + if (!active) return; + setUnitOptions(us); + setUnit((cur) => (us.includes(cur) ? cur : (us[0] || ""))); + } catch (e) { if (active) { setUnitOptions([]); setUnit(""); } } + })(); + return () => { active = false; }; + }, [registry, table, primaryQuantity, allAxisDims]); + + // sample row + IAMC decomposition (only if present) + useEffect(() => { + if (!registry) return; + let active = true; + (async () => { + try { + const res = await axios.get(`/api/v0/schema/${ROWS_SCHEMA}/tables/${table.trim()}/rows/?limit=80`); + const data = Array.isArray(res.data) ? res.data : []; + const withStr = data.filter((r) => r.iamc_full_string); + if (!withStr.length) { if (active) { setRawString(null); setSegments([]); } return; } + const best = withStr.sort((a, b) => b.iamc_full_string.split("|").length - a.iamc_full_string.split("|").length)[0]; + const raw = best.iamc_full_string; + const segs = raw.split(DELIM).map((s) => s.trim()).filter(Boolean) + .map((seg) => ({ seg, match: tokenIndex[seg.toLowerCase()] || null })); + if (!active) return; + setRawString(raw); setSegments(segs); + const map = await resolveTerms(segs.map((s) => s.match?.iri).filter(Boolean)); + if (active) setTerms((p) => ({ ...p, ...map })); + } catch (e) { if (active) { setRawString(null); setSegments([]); } } + })(); + return () => { active = false; }; + }, [registry, table, tokenIndex]); + + const run = async (xk = xKey, sk = stackKey, u = unit, q = primaryQuantity) => { + setXKey(xk); setStackKey(sk); + setRunning(true); setErr(null); setRows(null); + try { + const filters = {}; + const dimset = new Set([xk, sk]); + if (u) { dimset.add("unit"); filters.unit = [u]; } + // scope to the primary quantity (unless it IS an axis here) + if (q && xk !== "quantity_kind" && sk !== "quantity_kind") { + dimset.add("quantity_kind"); filters.quantity_kind = [q]; + } + const query = buildComparisonQuery({ registry, tables: [table.trim()], dims: [...dimset], filters }); + setLastQuery(query); + const data = await postSparql(query); + const bindings = data?.results?.bindings || []; + setRows(bindings); + const stackDim = allAxisDims.find((d) => d.key === sk); + if (stackDim?.object_kind === "iri") { + const map = await resolveTerms(bindings.map((r) => r[sk]?.value).filter(Boolean)); + setTerms((p) => ({ ...p, ...map })); + } + } catch (e) { setErr(e?.message || "Query failed"); } finally { setRunning(false); } + }; + + // preset: pick the breakdown AND the most relevant unit (for the current + // primary quantity), then run scoped to that quantity. + const runPreset = async (p) => { + setXKey(p.x); setStackKey(p.stack); + const unitDim = (registry.dimensions || []).find((d) => d.key === "unit"); + const qkDim = allAxisDims.find((d) => d.key === "quantity_kind"); + let u = unit; + try { + const d = await postSparql(valueFrequencyQuery({ + registry, table: table.trim(), dim: unitDim, + scopeDim: primaryQuantity ? qkDim : null, scopeValue: primaryQuantity || null, + })); + const us = (d.results?.bindings || []).map((b) => b.v.value); + setUnitOptions(us); + u = us[0] || ""; + setUnit(u); + } catch (e) { /* keep current unit */ } + run(p.x, p.stack, u, primaryQuantity); + }; + + const seriesLabel = (fullIri, fallback) => { + const lbl = (fullIri && terms[fullIri]?.label) || fallback; + return shorten(lbl); + }; + + const { option, stackValues } = useMemo(() => { + if (!rows || !registry) return { option: null, stackValues: [] }; + const xDim = allAxisDims.find((d) => d.key === xKey); + const stackDim = allAxisDims.find((d) => d.key === stackKey); + if (!xDim || !stackDim) return { option: null, stackValues: [] }; + const xVals = [...new Set(rows.map((r) => cellValue(registry, xDim, r)).filter((v) => v != null))].sort(); + const stacks = []; const matrix = {}; + for (const r of rows) { + const x = cellValue(registry, xDim, r); + const sFull = stackDim.object_kind === "iri" ? r[stackDim.key]?.value : null; + const sLabel = seriesLabel(sFull, cellValue(registry, stackDim, r)); + const val = parseFloat(r.value?.value); + if (x == null || sLabel == null || Number.isNaN(val)) continue; + if (!stacks.find((s) => s.label === sLabel)) stacks.push({ label: sLabel, fullIri: sFull }); + matrix[sLabel] = matrix[sLabel] || {}; matrix[sLabel][x] = (matrix[sLabel][x] || 0) + val; + } + const isLine = chartType === "line"; + const stackId = chartType === "stacked" ? "total" : undefined; + const opt = { + tooltip: { trigger: "axis", axisPointer: { type: isLine ? "line" : "shadow" } }, + legend: { type: "scroll", top: 0 }, + grid: { left: 80, right: 20, bottom: 40, top: 40 }, + xAxis: { type: "category", data: xVals, name: titleCase(xDim.key) }, + yAxis: { type: "value", name: unit || "value" }, + series: stacks.map((s, i) => ({ + name: s.label, type: isLine ? "line" : "bar", + ...(stackId ? { stack: stackId } : {}), + emphasis: { focus: "series" }, smooth: isLine, + itemStyle: { color: PALETTE[i % PALETTE.length] }, data: xVals.map((x) => matrix[s.label]?.[x] ?? 0), + })), + }; + return { option: opt, stackValues: stacks }; + }, [rows, registry, allAxisDims, xKey, stackKey, unit, terms, chartType]); + + if (registryLoading) return ; + if (registryError) return Could not load the registry from {conf.dimensionRegistry}.; + + const xDimObj = allAxisDims.find((d) => d.key === xKey); + const stackDimObj = allAxisDims.find((d) => d.key === stackKey); + + return ( + + {/* PRIMARY VARIABLE — the top-level measured quantity; scopes everything + below. Only shown when the dataset actually has a quantity dimension + (so single-variable / typed tables aren't cluttered with it). */} + {quantityOptions.length > 0 && ( + + + + + { + const v = e.target.value; + setPrimaryQuantity(v); + if (v && stackKey === "quantity_kind") { + const alt = axisDims.find((d) => d.key !== "quantity_kind" && d.key !== xKey); + if (alt) setStackKey(alt.key); + } + }}> + (all variables — mixed) + {quantityOptions.map((q) => ( + {titleCase(q.value)}{q.count ? ` (${q.count})` : ""} + ))} + + + + + {primaryQuantity + ? <>Scoped to {titleCase(primaryQuantity)} — pick a unit + a breakdown below. + : <>All variables (mixed units) — pick one to focus the analysis and shrink the options.} + + + + + + )} + + {/* PRESETS */} + Quick comparisons (one click): + + {discovering && } + {!discovering && presets.length === 0 && No presets for this table.} + {presets.map((p) => ( + + ))} + + + {/* CUSTOMISER */} + + + + Show the total value{unit ? <> in {unit} : null} across{" "} + {dimLabel(xDimObj)} + , grouped by{" "} + {dimLabel(stackDimObj)}. + + + + setTable(e.target.value)} fullWidth size="small" helperText="one dataset at a time" /> + + + setXKey(e.target.value)}> + {breakdownDims.map((d) => {dimLabel(d)})} + + + + setStackKey(e.target.value)}> + {breakdownDims.map((d) => {dimLabel(d)})} + + + + 1 ? `${unitOptions.length} units — pick one` : "data unit"} + onChange={(e) => setUnit(e.target.value)}> + {unitOptions.map((u) => {u})} + + + + + + + {unitOptions.length > 1 && ( + + Only values in {unit} are summed — mixing units would be meaningless. + + )} + + + + {running && } + {err && {err}} + {rows && rows.length === 0 && ( + No data for this combination{unit ? <> in {unit} : null}. Try a preset or another unit. + )} + {option && ( + + setChartType(e.target.value)} sx={{ minWidth: 170 }}> + Stacked bars (composition) + Grouped bars (compare) + Lines (trend) + + + )} + {option && } + + {/* group definitions */} + {stackValues.length > 0 && ( + + What each “{dimLabel(stackDimObj)}” means (ontology / TS): + + {stackValues.map((s, i) => { + const info = s.fullIri ? terms[s.fullIri] : null; + return ( + + + + {info?.label || s.label} + {info?.description || "…"} + {s.fullIri && ontology term ↗} + + + + ); + })} + + + )} + + {/* HOW IT WORKS */} + + }> + How this works — from raw values to ontology terms + + + + Dimensions, predicates and value IRIs come from /oekg/registry/; labels and + definitions are loaded from the TIB Terminology Service. Works for any annotated table. + + {rawString ? ( + <> + Example: one opaque iamc_full_string value — + {rawString} + …decomposed into ontology terms (hover for definition, click to open): + + {segments.map(({ seg, match }, i) => ( + + {match ? titleCase(match.dimension) : "unmapped"} + {match && match.iri + ? + : } + + ))} + + + ) : ( + + This dataset has no packed IAMC string; its dimensions come directly from annotated columns. + + )} + {lastQuery && ( +
+ SPARQL (what ran under the hood) +
{lastQuery}
+
+ )} +
+
+
+ ); +} diff --git a/factsheet/frontend/src/components/comparison/comparability.js b/factsheet/frontend/src/components/comparison/comparability.js new file mode 100644 index 000000000..0e1f3654f --- /dev/null +++ b/factsheet/frontend/src/components/comparison/comparability.js @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// The comparability contract (wayfinder WF-12, amended by WF-13): a pure, +// unit-testable verdict over DECLARED facts only — registry contract + +// SPARQL-discovered facts per table. No chart heuristics. +// +// A *series* is one (table, measure, unit, granularity) tuple: +// { table, space, measure, measureLabel?, unit, granularity, aggregation } +// space: measure space — "substance" (IRI values) | "quantity_kind" +// (IAMC literal values). Spaces NEVER cross (WF-12 decision 1). +// unit: literal unit string (WF-05); equality is the merge guard. +// granularity: native temporal granularity on the WF-06 ladder, or null +// when the table declares no temporal anchor. +// aggregation: "sum" | "mean" | null — the registry's per-substance hint +// (WF-06); null means re-scaling this series is NOT declared +// valid and coarser rungs are unreachable (report-don't-guess). +// +// Verdicts: merge | aggregate_first | blocked(reason), reasons drawn from the +// fixed WF-12 vocabulary (+ "incommensurable" added by WF-13). + +export const LADDER = ["hour", "day", "week", "month", "year"]; + +// Which coarser rungs a native granularity can be aggregated onto. Weeks cross +// month and year boundaries (ISO), so "week" reaches nothing and nothing on the +// calendar chain reaches "week" from month upward. +const REACHABLE = { + hour: ["hour", "day", "week", "month", "year"], + day: ["day", "week", "month", "year"], + week: ["week"], + month: ["month", "year"], + year: ["year"], +}; + +export const REASONS = { + SPACES: "measure spaces not aligned", + MEASURE: "measure mismatch", + UNIT: "unit mismatch", + INCOMMENSURABLE: "incommensurable", + GRANULARITY: "granularity unreachable", + MISSING_UNIT: "missing declaration (unit)", + MISSING_TEMPORAL: "missing declaration (temporal)", + MISSING_AGGREGATION: "missing declaration (aggregation validity)", +}; + +const blocked = (reason, detail) => ({ kind: "blocked", reason, detail }); + +// The rungs a series may be displayed at: its native rung plus — only when the +// registry declares aggregation validity — every reachable coarser rung. +export function reachableLevels(series) { + if (!series.granularity) return []; + const levels = REACHABLE[series.granularity] || []; + return series.aggregation ? levels : [series.granularity]; +} + +// WF-13 seam — unit leg beyond string equality. `units` is the (future) +// registry-served map keyed by literal unit string: +// { [unitString]: { iri, quantity_kind, si_factor?, si_offset? } } +// Same quantity kind + both SI factors present => convertible (auto-convert + +// chart annotation). Differing quantity kinds => incommensurable (kt CO2e vs +// kt is a quantity-kind mismatch, not a factor away). Unknown strings keep +// today's blocked(unit mismatch). The registry does not serve `units:` yet, +// so with units == null string equality is the whole leg (WF-12 / WF-05). +function unitLeg(a, b, units) { + if (!a.unit || !b.unit) { + const missing = [a, b].filter((s) => !s.unit).map((s) => s.table); + return blocked( + REASONS.MISSING_UNIT, + `${missing.join(", ")} declares no unit for this measure` + ); + } + if (a.unit === b.unit) return { ok: true }; + const ua = units && units[a.unit]; + const ub = units && units[b.unit]; + if (ua && ub) { + if (ua.quantity_kind !== ub.quantity_kind) { + return blocked( + REASONS.INCOMMENSURABLE, + `${a.unit} (${ua.quantity_kind}) and ${b.unit} (${ub.quantity_kind}) measure different quantity kinds` + ); + } + if (ua.si_factor != null && ub.si_factor != null) { + return { + ok: true, + conversion: { + from: b.unit, + to: a.unit, + factor: ub.si_factor / ua.si_factor, + }, + }; + } + } + return blocked( + REASONS.UNIT, + `${a.table} reports ${a.unit} · ${b.table} reports ${b.unit}` + ); +} + +// The WF-12 pairwise predicate. +export function compareSeries(a, b, { units = null } = {}) { + if (a.space !== b.space) { + return blocked( + REASONS.SPACES, + `${a.table} annotates ${a.space} · ${b.table} annotates ${b.space}` + ); + } + if (a.measure !== b.measure) { + return blocked( + REASONS.MEASURE, + `${a.table} measures ${a.measureLabel || a.measure} · ${b.table} measures ${b.measureLabel || b.measure}` + ); + } + const u = unitLeg(a, b, units); + if (u.kind === "blocked") return u; + if (!a.granularity || !b.granularity) { + const missing = [a, b].filter((s) => !s.granularity).map((s) => s.table); + return blocked( + REASONS.MISSING_TEMPORAL, + `${missing.join(", ")} declares no temporal granularity` + ); + } + if (a.granularity === b.granularity) { + return { kind: "merge", target: a.granularity, conversion: u.conversion }; + } + const common = reachableLevels(a).filter((l) => + reachableLevels(b).includes(l) + ); + if (!common.length) { + const noAgg = [a, b].filter((s) => !s.aggregation); + if (noAgg.length) { + return blocked( + REASONS.MISSING_AGGREGATION, + `${noAgg.map((s) => s.table).join(", ")}: no aggregation validity declared for ${a.measureLabel || a.measure}` + ); + } + return blocked( + REASONS.GRANULARITY, + `${a.granularity} and ${b.granularity} share no reachable rung` + ); + } + return { + kind: "aggregate_first", + target: common[0], + conversion: u.conversion, + }; +} + +// WF-12 corollary over N series. `entries` may carry pre-failed tables +// (series construction already knows a table lacks the chosen measure): +// [{ table, series } | { table, series: null, reason, detail }] +// A selection merges iff every pair merges or aggregate_firsts; the first +// blocked pair blocks the whole selection and is named in the verdict. +export function selectionVerdict(entries, { units = null } = {}) { + const failed = entries.find((e) => !e.series); + if (failed) { + return { + kind: "blocked", + reason: failed.reason, + detail: failed.detail, + pair: [failed.table], + }; + } + const series = entries.map((e) => e.series); + // ladder options valid for the WHOLE selection (drives the UI control) + let levels = series.length ? reachableLevels(series[0]) : []; + for (const s of series.slice(1)) + levels = levels.filter((l) => reachableLevels(s).includes(l)); + + let kind = "merge"; + const conversions = []; + for (let i = 0; i < series.length; i++) { + for (let j = i + 1; j < series.length; j++) { + const v = compareSeries(series[i], series[j], { units }); + if (v.kind === "blocked") + return { ...v, pair: [series[i].table, series[j].table] }; + if (v.kind === "aggregate_first") kind = "aggregate_first"; + if (v.conversion) conversions.push(v.conversion); + } + } + if (series.length > 1 && !levels.length) { + return { + kind: "blocked", + reason: REASONS.GRANULARITY, + detail: "no granularity fits every selected source", + }; + } + return { kind, levels, target: levels[0] || null, conversions }; +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/HowItWorks.jsx b/factsheet/frontend/src/components/comparison/prototype_multisource/HowItWorks.jsx new file mode 100644 index 000000000..50c07fb2c --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/HowItWorks.jsx @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07, reaction round 6) — content for the previously +// dead "How it works?" button on the comparison board. Explains each tab in +// user language; the Registry section walks through the semantic-layer flow +// the workbench is built on, so users understand WHY something merges, +// aggregates or blocks — nothing here is decoration, every claim mirrors an +// implemented rule. + +import React from "react"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogActions from "@mui/material/DialogActions"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; + +const Step = ({ n, title, children }) => ( + + + {n}. {title} + + + {children} + + +); + +function RegistryContent() { + return ( + <> + + The Registry (beta) view compares{" "} + quantitative data across data sources — different scenarios, + models and reporting datasets — and only ever merges what the data + itself declares comparable. Everything you see is driven by the + platform's semantic layer (the table annotations published with + each dataset), not by hand-maintained configuration. + + + Every table whose annotations are mapped into the knowledge graph + appears here automatically — publishing a well-annotated table is all it + takes to show up. The colored dot in front of each source tells you what + would happen if you added it to your current selection: green merges + directly, blue is comparable after aggregation to a common time + resolution, red would block the comparison (the tooltip names the exact + reason), grey holds no data for the chosen measure. A warning triangle + means some columns of that table could not be annotated — values from + those columns may be missing. The eye icon shows the first raw rows so + you can inspect what is actually in a table. + + + A measure is what the values mean: an ontology-annotated substance (e.g. + electricity price, CO2 emission) or an IAMC-style variable. The picker + shows how many sources provide each measure and from which scenario + families; measures held by only one source are summarized away by + default, because a single scenario dataset has nothing to be compared + against. "Select all providers" pulls every source that + reports the measure into the selection at once. Substance-based and + IAMC-based measures are separate vocabularies and are never mixed + silently. + + + Before anything runs, the tool checks every pair of selected sources + against a fixed contract: same measure, same declared unit, and a + reachable common time resolution. The result is always one of{" "} + merge (plotted as stored), aggregate first (finer series + are rolled up to the coarsest common resolution) or blocked with + a named reason. A blocked selection stays fully selectable — the chart + area explains what blocks and which two sources clash, so you can fix + the selection instead of guessing. + + + The hour / day / month / year ladder offers only resolutions every + selected source can reach. When rolling up, the aggregation function + (sum vs. average) comes from the dataset's own declaration — an + energy amount is summed, a price is averaged; the chart never chooses. + Yearly values use the year each dataset declares for its scenario, not a + year extracted from timestamps. + + + All sources land in one merged result; source is a dimension like + any other and the default grouping when several are selected. Values + from different sources are never stacked or summed together. The + generated title states exactly what is plotted, and the line under it + states whether any calculation was applied to the stored values. Legend + entries resolve to ontology terms — hover for the definition, click the + chips under the chart to open the term. Long series can be zoomed with + the mouse wheel or the range slider. + + + In short: annotate your data well and it becomes comparable here by + itself — the view adds no interpretation of its own. + + + ); +} + +const CONTENT = { + Registry: { + title: "How the Registry (beta) comparison works", + body: , + }, + Qualitative: { + title: "How the qualitative comparison works", + body: ( + + The qualitative view compares the scenario bundles you selected + on the listing page: study context, scenario descriptions, interacting + regions, input and output datasets, and the other facts recorded in the + Open Energy Knowledge Graph. It puts the bundles side by side so + differences in scope and assumptions are visible before any numbers are + compared — use it to judge whether two scenarios are about the same + question at all. + + ), + }, + Quantitative: { + title: "How the quantitative comparison works", + body: ( + + The quantitative view charts data of the selected scenarios' linked + tables. It predates the Registry (beta) view and works per table; the + Registry tab is its successor and adds multi-source selection with + declared-comparability checking. + + ), + }, +}; + +export default function HowItWorksDialog({ open, onClose, alignment }) { + const c = CONTENT[alignment] || CONTENT.Registry; + return ( + + {c.title} + + {c.body} + + + + + ); +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/MultiSourcePrototype.jsx b/factsheet/frontend/src/components/comparison/prototype_multisource/MultiSourcePrototype.jsx new file mode 100644 index 000000000..58587f33a --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/MultiSourcePrototype.jsx @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — multi-source selection for the Registry +// (beta) view. Three structurally different variants on the existing route, +// switchable via ?variant= (A|B|C, plus 0 = today's single-source view for +// contrast). Shared state lives in useMultiSource so switching variants +// keeps the selection. Dev builds only — production falls back to the +// current RegistryComparison. +// +// Plan: "Three variants of multi-source selection, switchable via ?variant=, +// mounted on the Registry (beta) tab of the comparison route." + +import React from "react"; +import { useSearchParams } from "react-router-dom"; +import Alert from "@mui/material/Alert"; +import LinearProgress from "@mui/material/LinearProgress"; +import RegistryComparison from "../RegistryComparison.jsx"; +import useMultiSource from "./useMultiSource.js"; +import PrototypeSwitcher from "./PrototypeSwitcher.jsx"; +import VariantA, { VARIANT_NAME as NAME_A } from "./VariantA_Gallery.jsx"; +import VariantB, { VARIANT_NAME as NAME_B } from "./VariantB_Rail.jsx"; +import VariantC, { VARIANT_NAME as NAME_C } from "./VariantC_Sentence.jsx"; + +const VARIANTS = ["A", "B", "C", "0"]; +const NAMES = { + A: NAME_A, + B: NAME_B, + C: NAME_C, + 0: "today's single-source view", +}; + +export default function MultiSourcePrototype() { + const [params, setParams] = useSearchParams(); + const variant = params.get("variant") || "A"; + const ms = useMultiSource(); + + // stray-merge safety: outside dev builds this IS the current view + if (!import.meta.env.DEV) return ; + + const setVariant = (v) => { + const next = new URLSearchParams(params); + next.set("variant", v); + setParams(next, { replace: true }); + }; + + let body = null; + if (variant === "0") body = ; + else if (ms.registryLoading) body = ; + else if (ms.registryError) + body = ( + Could not load the registry contract. + ); + else if (variant === "B") body = ; + else if (variant === "C") body = ; + else body = ; + + return ( + <> + {body} + + + ); +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/PROTOTYPE-NOTES.md b/factsheet/frontend/src/components/comparison/prototype_multisource/PROTOTYPE-NOTES.md new file mode 100644 index 000000000..240260e4d --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/PROTOTYPE-NOTES.md @@ -0,0 +1,263 @@ +# PROTOTYPE — Multi-source selection UI (wayfinder WF-07) + +**Question:** How should selecting multiple data sources look and behave in the +Registry (beta) view? + +**Plan:** Three structurally different variants on the existing Registry (beta) +tab of the comparison route, switchable via `?variant=` (floating bar, dev +builds only). All variants share one state machine (`useMultiSource.js`) — the +selection survives switching — and implement the decided semantics: + +- WF-02 — merged result set, _source_ as first-class dimension (default group-by + when >1 source), unit filter spans all sources, no stacking across sources. +- WF-05 — registry `unmapped_columns` → incompleteness badge on cards + footnote + on charts. +- WF-06 — hour/day/week/month/year ladder; yearly buckets use the DECLARED + `scenario_year`, never `EXTRACT(YEAR)`; aggregation function comes from the + registry's per-substance hint. `week` is visible but disabled (no `WEEK()` + through ontop; ISO weeks cross month/year bounds). +- WF-12 — pure verdict function in `../comparability.js` + (`merge | aggregate_first | blocked(reason)`, fixed reason vocabulary); + blocked selections stay selectable, the chart is replaced by a structured + reason. **Keep `comparability.js` when deleting this prototype** — it is the + decided contract, not throwaway. +- WF-13 — the convertible-unit leg is implemented as a seam in + `comparability.js` (`units` map param); inert until the registry serves + `units:` entries. String equality is the live guard. + +## Run + +```bash +python manage.py runserver # Django on :8000 (registry, rows API, /api/oevkg-query) +./docker/ontop-reload.sh --check # make sure ontop serves the current mapping +npm run dev # vite; open the comparison page → Registry (beta) tab +``` + +Variants: `?variant=A` gallery (browse-first) · `?variant=B` workbench rail +(comparison-first) · `?variant=C` sentence builder (guided) · `?variant=0` +today's single-source view for contrast. ←/→ keys cycle. + +Demo selections against the live test bench: + +- `day_ahead_market_single_zone` + `generic_flexibility_trader` + + `sensitivity_forecaster`, measure _electricity price_ → **merge** (hourly, + EUR/MWh); ladder to month/year shows the registry `mean` hint at work. +- any AMIRIS table + `ariadne2_data_with_labels` → **blocked: measure spaces not + aligned** (substance vs quantity_kind — WF-12 decision 1). +- any AMIRIS table + `eu_leg_data_2021_rep_table_1` → **blocked** under the + prototype assumption below. +- `biogas` + `conventional_plant_operator`, measure _electrical energy_ → + **merge**, grouped by source; regroup by `transaction_role` for the facet + view. + +## Prototype assumptions to react to (not pinned by prior tickets) + +1. **A table with NO measure dimension (eu_leg live) is treated as its own + measure space** → always blocked against other sources, reason "measure + spaces not aligned … declares no measure dimension". _Reacted (2026-07-12):_ + the declaration exists in the metadata (value column + `isAbout OEO_00140082 greenhouse gas emission value`, species per row via + `gas` valueReferences) but the hand-written eu_leg mapping never emits it and + the substance enum lacks the concept — charted as wayfinder ticket WF-21. + Once fixed, this UI picks the measure up automatically (it discovers measures + from the VKG). +2. **Dataset family grouping on the cards is a frontend heuristic** + (`familyOf()` prefix match). Should come from the semantic layer (Datasets + feature / registry) eventually. +3. Sub-year buckets use `YEAR(?ts)/MONTH(?ts)/DAY(?ts)` on `time_step` — the + WF-06 bookings-in-period caveat is shown as a chart footnote whenever + day/month granularity includes an AMIRIS source. +4. When a table reports several units for the chosen measure (per-row-unit + tables), the chosen unit filters rows (WF-12: "one series per selected + unit"); the series unit shown in the verdict is the chosen unit if the table + has it, else the table's most frequent one. + +## Reaction round 1 (2026-07-13) — Variant B wins, built out + +Maintainer: **B is the direction** ("very comprehensive overview"); C has other +use cases (kept, not built out); variant 0 gets re-homed near the data view +(wayfinder WF-22). Implemented in this round: + +- **Full width** — the Registry tab drops the `lg2` container + (`comparisonBoardMain.tsx`), the rail grows to viewport height. +- **Comparability-aware selection window** — every rail row carries a + contract-computed dot for the chosen measure: ● merges · ● aggregate first · ● + would block (tooltip names the WF-12 reason) · ● no data. Same pure function + as the verdict, evaluated against the current selection; legend at the rail + bottom. +- **Measure-first flow** — the measure select is now catalog-wide ("start + here"), each option says how many sources provide it; picking a measure lights + up the rail dots. The group-by select flags dimensions not present in every + selected source ("some sources only" + warning helper) instead of silently + dropping sources. +- **Stale-chart UX** — the hook tracks the parameters each run used; any change + dims the chart under a "Parameters changed — Update chart" overlay (one click + re-runs; auto-run was considered but hourly-scale queries make eager refetch + jumpy — revisit if the overlay still feels clunky). +- Scenario-first selection noted as a seam in the rail header (needs the WF-14 + bundle-link harvest). + +Still open for the next round: comparability beyond unit + substance +(agent-based-modelling context — map fog, feeds WF-14), preset prominence +(WF-18: possibly preset-only entry), eu_leg substance fix (WF-21, decided: +species-conditional `co2_emission`). + +## Reaction round 2 (2026-07-13) — measure bar + +- **Measure bar on top of the graph filters** — the measure select moved out of + the toolbar into its own bar above them (it is the initially required + selection), with per-scenario provider info ("Provided by 3 sources: AMIRIS + Germany 2019 (2) · Ariadne (1) — 1 of 2 selected provide it") and a **"Select + all N providers"** button (`ms.selectProviders`). +- **Ontology-hierarchy measure grouping** (maintainer idea, charted as wayfinder + WF-23, not built): OEO verifiably groups per-species emission rates under + `OEO_00140082` _greenhouse gas emission rate_ — subclass traversal could let + sources annotating different subclasses meet at a parent class. Discovery + grouping is the safe near-term use; a verdict leg needs double-counting and + CO2e-weighting guardrails first. + +## Reaction round 3 (2026-07-13) — no-data feedback + raw-data transparency + +- **No blank graphs** — an empty result now renders a "No data for this query" + panel naming the exact slice (measure, unit, granularity, number of sources) + and suggesting what to change; all three variants. +- **Table peek** — every rail row carries a preview icon: a dialog with the + first 8 raw rows of the table plus an "Open table page" link + (`/dataedit/view/model_draft/`), without leaving the composition. +- eu_leg awareness confirmed on the ticket: it blocks against everything because + its species live in the `greenhouse_gas` DIMENSION while the measure concept + is never emitted — WF-21 (decided) fixes the CO2 slice, WF-23 generalizes. + +## Reaction round 4 (2026-07-13) — legend term resolution + zoom + +- **No raw OEO ids in the legend** — IRI-valued group values resolve through the + TIB Terminology Service (`../tibTerms.js`, same cache as the single-table + view): TIB label first, registry enum label second, shortened IRI last. Legend + entries show the ontology definition on hover, and a term-chip row under the + chart links each series to its ontology term. +- **Zoom / range selection** — echarts `dataZoom` (wheel/drag inside the plot, + range slider, toolbox zoom/restore) activates whenever the x-axis has more + than 31 buckets — hourly and daily series over long periods are now navigable. + +## Reaction round 5 (2026-07-13) — feedback pause; entry point charted + +"Prototype looks good for now" — no build changes. The remaining issue is bigger +than this prototype: the comparison board is only reachable via scenario-bundles +listing → badge-select ≥2 scenarios → "Compare scenarios", although the Registry +tab never uses the selected scenario uids (only the Qualitative view does). +Charted as wayfinder WF-24 (entry-point rework, affects qualitative comparison +too); "okay for now" per the maintainer, so this prototype stays reachable +through the existing flow. + +## Reaction round 6 (2026-07-13) — title, computation statement, measure picker, How it works + +- **Generated chart title** — the chart writes out what is plotted ("Electricity + price in EUR/MWh per month, grouped by source" + source list), built from a + run-time snapshot (`ranSummary`) so it stays truthful while the stale overlay + is up. The single-table view's title misses the measure — noted on WF-22. +- **Computation statement** — under the title, every run states what was done to + the stored values: "✓ Plotted as stored — no aggregation and no unit + conversion" for a pure merge, else per-source aggregation lines (function + + registry-hint provenance) and, once WF-13 units exist, conversion lines. The + live aggregation notices moved into this snapshot; the FAME bookings caveat + stays a chart footnote. +- **Measure picker rework (Variant B)** — searchable Autocomplete, 460px wide, + sort toggle (most sources first ↔ A–Z), and single-source measures summarized + away by default with an explanatory caption + "show and search them anyway" + toggle; group headers split "Comparable across sources" from "Single source + only". +- **"How it works?" content** — the dead button on the comparison board toolbar + now opens a per-tab dialog (`HowItWorks.jsx`); the Registry walkthrough + explains rail dots, measure spaces, the verdict contract, the ladder + + registry-hinted aggregation and the chart rules in user language. +- **Multi-measure charts** (not built) — charted as wayfinder WF-25: co-display + of two+ measures (dual axes), pairing suggested semantically (part–whole, + price×volume); cross-measure arithmetic stays out of scope. + +## Reaction round 7 (2026-07-13) — picker controls into the dropdown, toolbar alignment + +- **Measure picker controls moved inside the dropdown** — sort (most sources ↔ + A–Z) and the single-source show/hide live in a header of the Autocomplete + popup, right where the user searches (custom `PaperComponent`; + `onMouseDown preventDefault` keeps the input focused so using them doesn't + close the popup). Nothing measure-related sits outside the field anymore. + _Follow-up (same day):_ the controls are now **small circular icon buttons + with hover help**, the usual filter idiom — sort-by-sources, sort-A–Z, and an + eye toggle for the hidden single-source measures (badge shows how many are + hidden; the tooltip explains why they are). +- **No more helper text under toolbar fields** — the "stacking is off while + grouped by source" caption elevated the Chart style field and broke the row + baseline. Replaced by an in-field lock indicator (visible whenever grouping by + source blocks stacking; tooltip explains why and how to unblock) plus a "— + locked by group-by" annotation on the disabled menu item. The Grouped-by + field's "not in every selected source" helper got the same treatment (warning + icon in the field, tooltip with the consequence). + +## Reaction round 8 (2026-07-13) — facet semantics, enum-isolation hole, tooltip order + +- **Enum-isolation hole closed** — the `qualifier` registry dimension's only + enum value has `iri: null`; the isolation filter (built from enum IRIs) + silently vanished, so (a) qualifier was OFFERED as a group-by for every table + with any is-about triple and (b) grouping by it bound EVERY annotation — + award, bid, and the substance _electrical energy_ itself (the irritating + three-series plot, where "electrical energy" ≈ award + bid double-counted). + Fix: a shared-predicate dimension without enum IRIs is un-isolatable → + `askDimension` never offers it, `buildMergedQuery` refuses to group by it. +- **Facet-conflation guard** (the deeper WF-04 point: the MEANING is the + combination of annotations) — verified live that biogas "electrical energy" = + 8,761 bid + 8,761 award observations: grouped by source they were summed into + one series. Now the hook discovers which facet values the chosen measure + spreads over in the selected sources (`facetValuesForMeasure`, one + enum-isolated query); if a facet has ≥2 values and isn't the group-by, Variant + B shows a warning ("bid · award are summed into one series") with a one-click + "Group by Transaction Role" fix, and the chart carries a footnote (all + variants). +- **Extraction verified faithful** — the "empty" investment-cost chart was + correct data: AMIRIS Germany2019 reports investment/fixed/variable cost as + all-zero (8,761 hourly rows × 0 per agent; market revenue €1.5e9 and energy + 8.09e7 MWh check out). The chart now says so: an info note "every value in + this slice is exactly 0 — read correctly, nothing non-zero to see", and a + separate guard replaces the empty coordinate system when rows carry NO + readable numbers at all. +- **Tooltip ordering** — the hover listing sorts by value, largest first + (`tooltip.order: "valueDesc"`), matching the visual order of the lines. +- A stale-HMR `ToggleButtonGroup is not defined` error was reported once (module + timestamp predated the round-7 rework); no reference remains — hard reload + clears it. + +## Reaction round 9 (2026-07-13) — facet filters + scenario-mode rail + +- **Facet filters** — the conflation guard alone made every group-by warn + (electrical energy spreads over transaction_role AND data_role). Each spread + facet now gets a toolbar select: `all (summed)` · one value (e.g. award) · + `without this facet` (FILTER NOT EXISTS — excludes e.g. forecasts). Pinning a + facet clears its warning; active filters appear in the chart heading and are + part of stale detection + the run snapshot. +- **Scenario-mode rail** — toggle at the rail top (round icon buttons): table + list ↔ scenario list. Scenario mode collapses each dataset family to one row + with a tri-state checkbox (one tick = the whole scenario's tables), expandable + to members; family checkboxes + n/m counts work in both modes. Scenario = + dataset family FOR NOW (maintainer decision); the OEKG scenario bundle becomes + the source of definition once WF-14 harvests bundle links. +- **Trader/operator mixing recorded** — 11 AMIRIS sources are agent-role + perspectives on the same flows (same MWh at operator → trader → market); the + contract has no declaration to tell them apart. Sharpened on the map's + "comparability beyond unit + substance" fog entry (WF-14 interplay). + +## Reaction round 10 (2026-07-13) — provenance both ways + +- **Rail rows list their measures** — each table row gets a second caption line + with every measure the table provides (from the same VKG-discovered options + the picker uses); the currently chosen measure is bolded, overflow ellipsized + with the full list on hover. +- **Measure options show their scenarios** — every entry in the measure dropdown + now says which scenario(s) the providing sources belong to ("from AMIRIS + Germany 2019 (3) · Ariadne (1)"), matching the breakdown the measure bar + already showed for the selected measure. + +## Verdict + +**Variant B (workbench rail).** Remaining before this directory dies: further +reaction rounds on the B build-out, then fold B into the real Registry view +(rewrite, not promote), delete A/C/the switcher/the `comparisonBoardMain.tsx` +mount — and keep `../comparability.js`. diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/PrototypeSwitcher.jsx b/factsheet/frontend/src/components/comparison/prototype_multisource/PrototypeSwitcher.jsx new file mode 100644 index 000000000..3aa6a7ab7 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/PrototypeSwitcher.jsx @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — floating variant switcher. Obviously not part +// of the design under evaluation; dev builds only. ←/→ also cycle (unless an +// input is focused). + +import React, { useEffect } from "react"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; + +export default function PrototypeSwitcher({ + variants, + current, + names, + onChange, +}) { + const idx = Math.max(0, variants.indexOf(current)); + const go = (delta) => + onChange(variants[(idx + delta + variants.length) % variants.length]); + + useEffect(() => { + const onKey = (e) => { + const t = e.target; + if ( + t && + (t.tagName === "INPUT" || + t.tagName === "TEXTAREA" || + t.isContentEditable) + ) + return; + if (e.key === "ArrowLeft") go(-1); + if (e.key === "ArrowRight") go(1); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }); + + return ( + + go(-1)}> + + + + PROTOTYPE {current} — {names[current] || ""} + + go(1)}> + + + + ); +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/VariantA_Gallery.jsx b/factsheet/frontend/src/components/comparison/prototype_multisource/VariantA_Gallery.jsx new file mode 100644 index 000000000..dc52c0874 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/VariantA_Gallery.jsx @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — Variant A "Source gallery": browse-first. +// The free-text table field becomes a metadata-rich card gallery (title, +// description, keywords, badges) grouped by dataset family; the analysis +// controls sit below the gallery, chart at the bottom. Closest in spirit to +// today's layout — the cards simply replace the TextField. + +import React from "react"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import Card from "@mui/material/Card"; +import CardActionArea from "@mui/material/CardActionArea"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import TextField from "@mui/material/TextField"; +import MenuItem from "@mui/material/MenuItem"; +import Button from "@mui/material/Button"; +import LinearProgress from "@mui/material/LinearProgress"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import { + UnmappedBadge, + GranularityChip, + GranularityLadder, + VerdictPanel, + MergedChart, + ChartTypeSelect, + NoDataAlert, + titleCase, +} from "./shared.jsx"; + +export const VARIANT_NAME = "Source gallery (browse-first)"; + +export default function VariantA({ ms }) { + if (!ms.catalog) return ; + const families = [...new Set(ms.catalog.map((c) => c.family))]; + + return ( + + {/* SOURCE GALLERY */} + + Pick one or more data sources — source becomes a dimension of the + comparison: + + {families.map((fam) => ( + + + {fam} + + + {ms.catalog + .filter((c) => c.family === fam) + .map((c) => { + const on = ms.selected.includes(c.table); + return ( + + + ms.toggle(c.table)} + sx={{ height: "100%" }} + > + + + {on && ( + + )} + + {c.title} + + + + {c.description || c.table} + + + + + {c.keywords.slice(0, 3).map((k) => ( + + ))} + + + + + + ); + })} + + + ))} + + {/* CONTROLS */} + + + + Compare {ms.measure?.label || "…"} + {ms.unit ? ( + <> + {" "} + in {ms.unit} + + ) : null}{" "} + per {ms.granularity}, grouped by{" "} + + {titleCase(ms.groupKey === "source" ? "source" : ms.groupKey)} + {" "} + across {ms.selected.length} source + {ms.selected.length !== 1 ? "s" : ""}. + + + + ms.setMeasureId(e.target.value)} + helperText="union over the selected sources" + > + {ms.measureOptions.map((o) => ( + + {o.label} · {o.selectedProviders}/{ms.selected.length}{" "} + selected sources + + ))} + + + + ms.setUnit(e.target.value)} + helperText="spans all selected sources" + > + {ms.unitOptions.map((u) => ( + + {u} + + ))} + + + + + + + ms.setGroupKey(e.target.value)} + > + {ms.groupOptions.map((o) => ( + + {o.label} + + ))} + + + + + + + + + + {/* VERDICT / CHART */} + {ms.running && } + + {ms.err && {ms.err}} + {ms.verdict?.kind !== "blocked" && ms.rows && ms.rows.length === 0 && ( + + )} + {ms.verdict?.kind !== "blocked" && ms.rows && ms.rows.length > 0 && ( + <> + + + + + + )} + + ); +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/VariantB_Rail.jsx b/factsheet/frontend/src/components/comparison/prototype_multisource/VariantB_Rail.jsx new file mode 100644 index 000000000..47e115db9 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/VariantB_Rail.jsx @@ -0,0 +1,757 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — Variant B "Workbench rail": comparison-first. +// A persistent left rail holds the source catalog (searchable, compact +// checkbox rows grouped by family, mini-badges); the right side is dominated +// by the chart with a slim toolbar and a COMPARABILITY STRIP above it — the +// verdict is always visible while composing, before anything runs. + +import React, { useMemo, useState } from "react"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import Typography from "@mui/material/Typography"; +import Checkbox from "@mui/material/Checkbox"; +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import TextField from "@mui/material/TextField"; +import MenuItem from "@mui/material/MenuItem"; +import Button from "@mui/material/Button"; +import Divider from "@mui/material/Divider"; +import Tooltip from "@mui/material/Tooltip"; +import LinearProgress from "@mui/material/LinearProgress"; +import Alert from "@mui/material/Alert"; +import List from "@mui/material/List"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete"; +import Paper from "@mui/material/Paper"; +import InputAdornment from "@mui/material/InputAdornment"; +import IconButton from "@mui/material/IconButton"; +import Badge from "@mui/material/Badge"; +import Collapse from "@mui/material/Collapse"; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; +import SortIcon from "@mui/icons-material/Sort"; +import SortByAlphaIcon from "@mui/icons-material/SortByAlpha"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import ViewListIcon from "@mui/icons-material/ViewList"; +import AccountTreeIcon from "@mui/icons-material/AccountTree"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; +import { + GranularityLadder, + VerdictPanel, + VerdictChip, + MergedChart, + ChartTypeSelect, + CandidateDot, + NoDataAlert, + TablePeek, + PALETTE, +} from "./shared.jsx"; + +export const VARIANT_NAME = "Workbench rail (comparison-first)"; + +const measureFilter = createFilterOptions({ + stringify: (o) => `${o.label} ${o.value} ${o.space}`, +}); + +// round icon-button filter idiom (round 7) +const roundBtn = (on) => ({ + width: 28, + height: 28, + border: "1px solid", + borderColor: on ? "primary.main" : "divider", + bgcolor: on ? "primary.main" : "transparent", + color: on ? "primary.contrastText" : "text.secondary", + "&:hover": { bgcolor: on ? "primary.dark" : "action.hover" }, +}); + +export default function VariantB({ ms }) { + const [filter, setFilter] = useState(""); + const [verdictOpen, setVerdictOpen] = useState(false); + // rail mode (round 9): browse tables flat, or lead with the SCENARIO — + // for now scenario = dataset family; the OEKG scenario bundle becomes the + // source of definition once WF-14 harvests the links + const [railMode, setRailMode] = useState("tables"); + const [expandedFams, setExpandedFams] = useState(() => new Set()); + const toggleExpand = (fam) => + setExpandedFams((prev) => { + const n = new Set(prev); + if (n.has(fam)) n.delete(fam); + else n.add(fam); + return n; + }); + // measure picker controls (reaction round 6): search, sort, and a summary + // of single-source measures instead of flooding the list with them + const [measureSort, setMeasureSort] = useState("sources"); + const [includeSingle, setIncludeSingle] = useState(false); + const singleCount = useMemo( + () => ms.measureOptions.filter((o) => o.providers.length === 1).length, + [ms.measureOptions] + ); + const measureChoices = useMemo(() => { + // default pool: measures ≥2 sources can compare — plus the current + // choice, so the field never holds a value missing from its own list + const pool = includeSingle + ? ms.measureOptions + : ms.measureOptions.filter( + (o) => + o.providers.length > 1 || + (ms.measure && + o.space === ms.measure.space && + o.value === ms.measure.value) + ); + const byLabel = (a, b) => a.label.localeCompare(b.label); + const sorted = [...pool].sort( + measureSort === "alpha" + ? byLabel + : (a, b) => b.providers.length - a.providers.length || byLabel(a, b) + ); + // keep multi-provider options ahead of single-provider ones so the + // Autocomplete group headers appear once each + return [ + ...sorted.filter((o) => o.providers.length > 1), + ...sorted.filter((o) => o.providers.length === 1), + ]; + }, [ms.measureOptions, ms.measure, includeSingle, measureSort]); + + // Sort + single-source controls live INSIDE the dropdown, next to the + // search (reaction round 7) — as small round icon buttons with hover help, + // the way filter controls are usually shown (round 7 follow-up). + // onMouseDown preventDefault keeps the input focused so clicking them + // doesn't close the popup. + const MeasurePaper = useMemo(() => { + return function MeasurePaper({ children, ...rest }) { + return ( + + e.preventDefault()} + sx={{ + px: 1.5, + py: 0.75, + borderBottom: "1px solid", + borderColor: "divider", + }} + > + + setMeasureSort("sources")} + sx={roundBtn(measureSort === "sources")} + > + + + + + setMeasureSort("alpha")} + sx={roundBtn(measureSort === "alpha")} + > + + + + + {singleCount > 0 && ( + + )} + + {children} + + ); + }; + }, [measureSort, includeSingle, singleCount]); + if (!ms.catalog) return ; + + // round 10: measures per rail row + scenario provenance per measure option + const measuresOf = (table) => + ms.measureOptions.filter((o) => o.providers.includes(table)); + const famBreakdown = (o) => { + const counts = {}; + for (const t of o.providers) { + const f = ms.catalog.find((c) => c.table === t)?.family || "other"; + counts[f] = (counts[f] || 0) + 1; + } + return Object.entries(counts) + .map(([f, n]) => `${f} (${n})`) + .join(" · "); + }; + + const visible = ms.catalog.filter( + (c) => + !filter || + c.table.toLowerCase().includes(filter.toLowerCase()) || + c.title.toLowerCase().includes(filter.toLowerCase()) || + c.keywords.some((k) => k.toLowerCase().includes(filter.toLowerCase())) + ); + const families = [...new Set(visible.map((c) => c.family))]; + + return ( + + {/* SOURCE RAIL */} + + + + + Data sources + + + setRailMode("tables")} + sx={{ ...roundBtn(railMode === "tables"), mr: 0.5 }} + > + + + + + setRailMode("scenarios")} + sx={roundBtn(railMode === "scenarios")} + > + + + + + setFilter(e.target.value)} + /> + + + + {families.map((fam) => { + const famTables = ms.catalog + .filter((c) => c.family === fam) + .map((c) => c.table); + const selCount = famTables.filter((t) => + ms.selected.includes(t) + ).length; + const open = railMode === "tables" || expandedFams.has(fam); + return ( + + {/* scenario row (round 9): one tick selects the whole dataset + family — the scenario stand-in until WF-14 */} + + 0 && selCount < famTables.length} + onChange={(e) => + ms.toggleFamily(famTables, e.target.checked) + } + /> + toggleExpand(fam) + : undefined + } + > + {fam} ({selCount}/{famTables.length}) + + {railMode === "scenarios" && ( + toggleExpand(fam)}> + {open ? ( + + ) : ( + + )} + + )} + + + + {visible + .filter((c) => c.family === fam) + .map((c) => { + const on = ms.selected.includes(c.table); + return ( + ms.toggle(c.table)} + > + + + + + {c.title} + + {c.unmapped.length > 0 && ( + `${u.column} — ${u.reason}`) + .join(" · ")} + > + + + )} + + + } + secondary={ + <> + + {c.granularity + ? c.granularity === "hour" + ? "hourly" + : "yearly" + : "no temporal declaration"}{" "} + · {c.table} + + {/* measures this table provides (round 10); + the chosen one is bolded */} + o.label) + .join(" · ")} + > + {measuresOf(c.table).length + ? measuresOf(c.table).map((o, i) => ( + + {i > 0 && " · "} + {ms.measure && + o.space === ms.measure.space && + o.value === ms.measure.value ? ( + {o.label} + ) : ( + o.label + )} + + )) + : "no measures declared"} + + + } + secondaryTypographyProps={{ + component: "div", + fontSize: 11, + }} + /> + + ); + })} + + + + ); + })} + + + + + {ms.selected.length} source{ms.selected.length !== 1 ? "s" : ""}{" "} + selected + {ms.selected.length > 1 ? " — grouped by source by default" : ""} + + + For {ms.measure?.label || "…"}:{" "} + merges ·{" "} + aggregate first ·{" "} + would block ·{" "} + no data — from the + registry contract, not heuristics + + + + + {/* WORKBENCH */} + + {/* MEASURE BAR — the initially required choice sits on top of the + graph filters (reaction round 2): pick a measure, see how many + sources from which scenario provide it, select them all at once */} + + + o && ms.setMeasureId(`${o.space}:${o.value}`)} + disableClearable + getOptionLabel={(o) => o.label || ""} + isOptionEqualToValue={(o, v) => + o.space === v.space && o.value === v.value + } + groupBy={(o) => + o.providers.length > 1 + ? "Comparable across sources" + : "Single source only" + } + renderOption={(props, o) => ( +
  • + + + {o.space === "substance" + ? "substance" + : "IAMC quantity"}{" "} + · {o.providers.length} source + {o.providers.length !== 1 ? "s" : ""} provide + {o.providers.length === 1 ? "s" : ""} it + {o.selectedProviders + ? ` (${o.selectedProviders} selected)` + : ""} + + {/* scenario provenance (round 10): which scenario(s) + the providing sources belong to */} + + from {famBreakdown(o)} + + + } + secondaryTypographyProps={{ component: "div" }} + /> +
  • + )} + renderInput={(params) => ( + + )} + /> + {ms.measure && ( + + Provided by {ms.measure.providers.length} source + {ms.measure.providers.length !== 1 ? "s" : ""}:{" "} + {Object.entries( + ms.measure.providers.reduce((acc, t) => { + const fam = + ms.catalog.find((c) => c.table === t)?.family || "other"; + acc[fam] = (acc[fam] || 0) + 1; + return acc; + }, {}) + ) + .map(([fam, n]) => `${fam} (${n})`) + .join(" · ")}{" "} + — {ms.measure.selectedProviders} of{" "} + {ms.selected.length || "none"} selected provide it + + )} + + {ms.measure && ( + + )} +
    +
    + + {/* toolbar */} + + ms.setUnit(e.target.value)} + > + {ms.unitOptions.map((u) => ( + + {u} + + ))} + + + ms.setGroupKey(e.target.value)} + InputProps={ + ms.groupOptions.find((o) => o.key === ms.groupKey)?.shared === + false + ? { + startAdornment: ( + + + + + + ), + } + : undefined + } + > + {ms.groupOptions.map((o) => ( + + {o.label} + {o.shared === false ? " — some sources only" : ""} + + ))} + + {/* facet filters (round 9): a spread facet can be pinned to one + value instead of grouped by — "everything summed all the time" + stops being the only alternative */} + {Object.entries(ms.facetSpread) + .filter(([, vals]) => vals.length > 1) + .map(([key, vals]) => ( + ms.setFacetFilter(key, e.target.value)} + > + all (summed) + {vals.map((v) => ( + + {v.label} + + ))} + without this facet + + ))} + + + + + {/* FACET-CONFLATION GUARD (round 8): the meaning of a value is the + combination of its annotations — never sum bid+award style facets + into one series without saying so */} + {ms.conflations.map((c) => ( + ms.setGroupKey(c.key)} + > + Group by {c.label} + + } + > + The selected sources annotate {ms.measure?.label} values with + several {c.label} facets ({c.values.join(" · ")}). Grouped by{" "} + {ms.groupKey === "source" ? "source" : ms.groupKey}, these different + things are summed into one series — group by {c.label}, or + pin one value with the {c.label.toLowerCase()} filter in the + toolbar. + + ))} + + {/* COMPARABILITY STRIP — always visible while composing */} + + + {ms.selectedEntries.map((c, i) => ( + ms.toggle(c.table)} + /> + ))} + + setVerdictOpen((v) => !v)} + /> + + {(verdictOpen || ms.verdict?.kind === "blocked") && ( + + + + )} + + + {/* CHART */} + {ms.running && } + {ms.err && {ms.err}} + {ms.verdict?.kind !== "blocked" && ms.rows && ms.rows.length === 0 && ( + + )} + {ms.verdict?.kind !== "blocked" && ms.rows && ms.rows.length > 0 && ( + + )} + {!ms.rows && !ms.running && ms.verdict?.kind !== "blocked" && ( + + Compose a comparison on the left, then hit Compare. + + )} +
    +
    + ); +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/VariantC_Sentence.jsx b/factsheet/frontend/src/components/comparison/prototype_multisource/VariantC_Sentence.jsx new file mode 100644 index 000000000..046681d45 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/VariantC_Sentence.jsx @@ -0,0 +1,314 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — Variant C "Sentence builder": guided, +// contract-forward. The whole comparison is composed as one plain-language +// sentence with dropdown slots; the comparability verdict is a LIVE line of +// narrative directly under the sentence — the user reads WHY something merges, +// aggregates or blocks before ever running it. Users learn the contract by +// composing sentences. + +import React from "react"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import ListItemText from "@mui/material/ListItemText"; +import Checkbox from "@mui/material/Checkbox"; +import Button from "@mui/material/Button"; +import LinearProgress from "@mui/material/LinearProgress"; +import { + UnmappedBadge, + GranularityChip, + MergedChart, + ChartTypeSelect, + NoDataAlert, + titleCase, +} from "./shared.jsx"; + +export const VARIANT_NAME = "Sentence builder (guided)"; + +const Slot = ({ children }) => ( + + {children} + +); +const slotSx = { + fontWeight: 700, + "& .MuiSelect-select": { py: 0.25 }, + borderBottom: "2px dotted", + borderColor: "primary.main", + "&::before, &::after": { display: "none" }, +}; + +export default function VariantC({ ms }) { + if (!ms.catalog) return ; + const titleOf = (t) => ms.catalog.find((c) => c.table === t)?.title || t; + + // the live narrative under the sentence + const narrative = (() => { + const v = ms.verdict; + if (!ms.selected.length) + return { tone: "info", text: "Pick at least one source." }; + if (!v) return { tone: "info", text: "Pick a measure." }; + if (v.kind === "blocked") { + return { + tone: "error", + text: `⛔ Blocked: ${v.reason} — ${v.detail}${v.pair ? ` (${v.pair.join(" · ")})` : ""}. Nothing is hidden — change a slot and the sentence becomes comparable.`, + }; + } + if (v.kind === "aggregate_first") { + const finer = ms.verdictInput.filter( + (e) => e.series && e.series.granularity !== ms.granularity + ); + return { + tone: "info", + text: `⟲ Comparable after alignment: ${finer.map((e) => `${titleOf(e.table)} (${e.series.granularity}ly)`).join(", ")} will be ${ms.measure?.aggregation === "mean" ? "averaged" : "summed"} to ${ms.granularity} — the aggregation function comes from the registry, not the chart.`, + }; + } + return { + tone: "success", + text: "✓ These sources merge: same measure, same unit, same granularity.", + }; + })(); + + return ( + + {/* THE SENTENCE */} + + + + Compare + + + + on + + + + {ms.unitOptions.length > 0 && ( + <> + in + + + + + )} + per + + + + , grouped by + + + + . + + + {/* LIVE VERDICT NARRATIVE */} + + {narrative.text} + + + + + + {ms.selected.length > 1 && ms.groupKey === "source" && ( + + )} + + + + + {/* CHART */} + {ms.running && } + {ms.err && {ms.err}} + {ms.verdict?.kind !== "blocked" && ms.rows && ms.rows.length === 0 && ( + + )} + {ms.verdict?.kind !== "blocked" && ms.rows && ms.rows.length > 0 && ( + + )} + {ms.verdict?.kind !== "blocked" && !ms.rows && !ms.running && ( + + The sentence above IS the query — run it to see the merged chart. + + )} + + What each part of the sentence means: sources come from the VKG + mapping; the measure is a declared substance or IAMC quantity; + the unit filter spans all sources; the per-granularity is + the WF-06 ladder (week needs WEEK() support — coming); grouping by{" "} + source keeps cross-source values side by side, never summed + together. + + + ); +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/protoData.js b/factsheet/frontend/src/components/comparison/prototype_multisource/protoData.js new file mode 100644 index 000000000..9e9ad575e --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/protoData.js @@ -0,0 +1,327 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — throwaway data plumbing for the multi-source +// selection variants. SPARQL builders + fetchers shared by all variants. +// The decided semantics live in ../comparability.js (keep); this file dies +// with the prototype. + +import axios from "axios"; +import conf from "../../../conf.json"; +import CSRFToken from "../../csrfToken.js"; +import { + prefixHeader, + sparqlTerm, + sharedPredicates, + expandCurie, +} from "../registryQuery.js"; + +export const TABLE_PRED = "oeo:OEO_00000504"; +export const ROWS_SCHEMA = "model_draft"; + +export async function postSparql(query) { + const res = await axios.post(conf.obdi, query, { + headers: { + "X-CSRFToken": CSRFToken(), + Accept: "application/sparql-results+json", + "Content-Type": "application/sparql-query", + }, + }); + return res.data; +} + +const bindings = (d) => d?.results?.bindings || []; + +// ---- catalog --------------------------------------------------------------- + +// All tables present in the VKG — the card list is grounded in the semantic +// layer (mapped tables only), not in a hand-kept list. +export async function fetchMappedTables(registry) { + const q = `${prefixHeader(registry)} +SELECT DISTINCT ?t WHERE { ?s ${TABLE_PRED} ?t } ORDER BY ?t`; + return bindings(await postSparql(q)).map((b) => b.t.value); +} + +// oemetadata for one table (title/description/keywords for the rich cards). +export async function fetchTableMeta(table) { + try { + const res = await axios.get( + `/api/v0/schema/${ROWS_SCHEMA}/tables/${table}/meta/` + ); + const d = res.data || {}; + const r = (d.resources || [])[0] || {}; + return { + title: r.title || d.title || table, + description: r.description || d.description || "", + keywords: r.keywords || d.keywords || [], + subject: (r.subject || d.subject || []) + .map((s) => s?.name) + .filter(Boolean), + }; + } catch (e) { + return { title: table, description: "", keywords: [], subject: [] }; + } +} + +// PROTOTYPE heuristic: dataset family for grouping cards. Should come from the +// Datasets feature (semantic layer) eventually — flagged for the maintainer. +export function familyOf(table) { + if (table.startsWith("amiris_")) return "AMIRIS Germany 2019"; + if (table.startsWith("ariadne")) return "Ariadne (IAMC)"; + if (table.startsWith("eu_leg")) return "EU emission reporting (SIROP)"; + return "Other"; +} + +// ---- per-table facts (feed the verdict) ------------------------------------ + +// Which tables populate a literal dimension at all (one query for ALL tables: +// cheaper than per-table ASKs for catalog-wide facts like time_step). +export async function tablesWithDimension(registry, dim) { + const q = `${prefixHeader(registry)} +SELECT DISTINCT ?t WHERE { ?s ${TABLE_PRED} ?t . ?s ${dim.predicate} ?v }`; + return new Set(bindings(await postSparql(q)).map((b) => b.t.value)); +} + +// Distinct values of a measure dimension per table, across the whole VKG. +// Enum isolation for the shared is-about predicate (substance). +export async function measuresByTable(registry, dim) { + let iso = ""; + if ( + dim.object_kind === "iri" && + sharedPredicates(registry).has(dim.predicate) + ) { + const set = (dim.values || []) + .filter((v) => v.iri) + .map((v) => sparqlTerm(v.iri)); + if (set.length) iso = `FILTER(?v IN (${set.join(", ")})) .`; + } + const q = `${prefixHeader(registry)} +SELECT DISTINCT ?t ?v WHERE { ?s ${TABLE_PRED} ?t . ?s ${dim.predicate} ?v . ${iso} }`; + const out = {}; + for (const b of bindings(await postSparql(q))) { + (out[b.t.value] = out[b.t.value] || []).push(b.v.value); + } + return out; +} + +// Units present per table FOR one chosen measure (unit follows the measure; +// the unit filter spans all selected sources — WF-02). +export async function unitsByTableForMeasure({ registry, tables, measure }) { + const unitDim = (registry.dimensions || []).find((d) => d.key === "unit"); + const scope = measureScope(registry, measure); + const q = `${prefixHeader(registry)} +SELECT ?t ?v (COUNT(?s) AS ?c) WHERE { + ?s ${TABLE_PRED} ?t . FILTER(?t IN (${tables.map((t) => `"${t}"`).join(", ")})) + ?s ${unitDim.predicate} ?v . ${scope} +} GROUP BY ?t ?v ORDER BY DESC(?c)`; + const out = {}; + for (const b of bindings(await postSparql(q))) { + (out[b.t.value] = out[b.t.value] || []).push(b.v.value); + } + return out; +} + +// dimension availability per table (the WF-07 "union of per-table ASKs") +export async function askDimension(registry, table, dim) { + let iso = ""; + if ( + dim.object_kind === "iri" && + sharedPredicates(registry).has(dim.predicate) + ) { + const set = (dim.values || []) + .filter((v) => v.iri) + .map((v) => sparqlTerm(v.iri)); + // a shared-predicate dimension with NO enum IRIs cannot be isolated from + // the other is-about facets — never offer it (round 8: the `qualifier` + // dim has iri:null, which silently dropped the filter and let grouping + // bind EVERY annotation incl. the substance itself) + if (!set.length) return false; + iso = ` FILTER(?v IN (${set.join(", ")}))`; + } + const q = `${prefixHeader(registry)} +ASK { ?s ${TABLE_PRED} ?t . FILTER(?t = "${table}") ?s ${dim.predicate} ?v .${iso} }`; + try { + return !!(await postSparql(q)).boolean; + } catch (e) { + return true; // fail open, like the existing view + } +} + +// Which facet values (transaction_role, data_role, …) the chosen measure's +// observations carry within the selected tables — one query over the shared +// is-about predicate, bucketed back per dimension via the enums. Feeds the +// facet-conflation guard (round 8): if a measure spreads over ≥2 values of a +// facet the chart is NOT grouped by, those values are summed into one series. +export async function facetValuesForMeasure({ registry, tables, measure }) { + if (!tables.length || !measure) return {}; + const dims = (registry.dimensions || []).filter( + (d) => + d.key !== "substance" && + d.object_kind === "iri" && + sharedPredicates(registry).has(d.predicate) && + (d.values || []).some((v) => v.iri) + ); + if (!dims.length) return {}; + const pred = dims[0].predicate; // all share the is-about predicate + const all = dims.flatMap((d) => + (d.values || []).filter((v) => v.iri).map((v) => sparqlTerm(v.iri)) + ); + const q = `${prefixHeader(registry)} +SELECT DISTINCT ?v WHERE { + ?s ${TABLE_PRED} ?t . FILTER(?t IN (${tables.map((t) => `"${t}"`).join(", ")})) . + ${measureScope(registry, measure)} + ?s ${pred} ?v . FILTER(?v IN (${all.join(", ")})) . +}`; + const found = bindings(await postSparql(q)).map((b) => b.v.value); + const out = {}; + for (const d of dims) { + const hits = (d.values || []).filter( + (v) => + v.iri && + found.some((f) => f === expandCurie(registry, v.iri) || f === v.iri) + ); + if (hits.length) + out[d.key] = hits.map((v) => ({ iri: v.iri, label: v.label || v.iri })); + } + return out; +} + +// ---- the merged, aggregated comparison query ------------------------------- + +function measureScope(registry, measure) { + if (!measure || measure.space === "none") return ""; + const byKey = Object.fromEntries( + (registry.dimensions || []).map((d) => [d.key, d]) + ); + if (measure.space === "substance") { + const dim = byKey.substance; + return `?s ${dim.predicate} ?measure . FILTER(?measure = ${sparqlTerm(measure.value)}) .`; + } + const dim = byKey.quantity_kind; + const esc = String(measure.value).replace(/"/g, '\\"'); + return `?s ${dim.predicate} ?measure . FILTER(STR(?measure) = "${esc}") .`; +} + +// Time-bucket patterns per WF-06/WF-08: +// * year → the DECLARED ?scenario_year constant, never EXTRACT(YEAR) — the +// AMIRIS December settlement lands on the 2020-01-01 fencepost. +// * hour → the raw ?time_step (one row per hour). +// * day/month → BIND(...) on ?time_step; ontop rejects expressions inside +// GROUP BY, so every bucket part gets its own BIND (WF-08 gotcha). +// * week → not buildable in SPARQL (no WEEK()); disabled in the UI. +const BUCKETS = { + year: { + patterns: (yDim) => [`?s ${yDim.predicate} ?bucket_y .`], + vars: ["?bucket_y"], + }, + month: { + patterns: (yDim, tsDim) => [ + `?s ${tsDim.predicate} ?ts .`, + `BIND(YEAR(?ts) AS ?bucket_y) BIND(MONTH(?ts) AS ?bucket_m)`, + ], + vars: ["?bucket_y", "?bucket_m"], + }, + day: { + patterns: (yDim, tsDim) => [ + `?s ${tsDim.predicate} ?ts .`, + `BIND(YEAR(?ts) AS ?bucket_y) BIND(MONTH(?ts) AS ?bucket_m) BIND(DAY(?ts) AS ?bucket_d)`, + ], + vars: ["?bucket_y", "?bucket_m", "?bucket_d"], + }, + hour: { + patterns: (yDim, tsDim) => [`?s ${tsDim.predicate} ?bucket_ts .`], + vars: ["?bucket_ts"], + }, +}; + +export function bucketLabel(row, granularity) { + const p2 = (v) => String(v).padStart(2, "0"); + const g = (k) => row[k]?.value; + if (granularity === "year") return g("bucket_y"); + if (granularity === "month") return `${g("bucket_y")}-${p2(g("bucket_m"))}`; + if (granularity === "day") + return `${g("bucket_y")}-${p2(g("bucket_m"))}-${p2(g("bucket_d"))}`; + return (g("bucket_ts") || "").replace("T", " ").slice(0, 16); +} + +// One merged query across all selected tables: GROUP BY (bucket, source, +// group-dim), aggregation function from the registry hint — the chart never +// chooses it (WF-06). xsd:double cast: quantity_value is a plain literal. +export function buildMergedQuery({ + registry, + tables, + measure, + unit, + granularity, + groupKey, + agg, + facetFilters = {}, +}) { + const byKey = Object.fromEntries( + (registry.dimensions || []).map((d) => [d.key, d]) + ); + const valueDim = byKey.quantity_value; + const unitDim = byKey.unit; + const bucket = BUCKETS[granularity] || BUCKETS.year; + + const patterns = [ + `?s ${valueDim.predicate} ?raw_value .`, + `?s ${TABLE_PRED} ?table_name .`, + `FILTER(?table_name IN (${tables.map((t) => `"${t}"`).join(", ")})) .`, + measureScope(registry, measure), + ...bucket.patterns(byKey.scenario_year, byKey.time_step), + ]; + if (unit) { + patterns.push( + `?s ${unitDim.predicate} ?unit . FILTER(STR(?unit) = "${unit.replace(/"/g, '\\"')}") .` + ); + } + // facet filters (round 9): pin one value of a spread facet — or exclude the + // facet entirely ("none" → observations NOT annotated with any enum value). + // Enum-scoped so the shared is-about predicate stays isolated. + for (const [key, choice] of Object.entries(facetFilters)) { + if (!choice || choice === "all") continue; + const fd = byKey[key]; + if (!fd) continue; + const set = (fd.values || []) + .filter((v) => v.iri) + .map((v) => sparqlTerm(v.iri)); + if (!set.length) continue; + if (choice === "none") { + // ontop rejects (NOT) EXISTS — negation via OPTIONAL + !BOUND instead + patterns.push( + `OPTIONAL { ?s ${fd.predicate} ?no_${key} . FILTER(?no_${key} IN (${set.join(", ")})) }`, + `FILTER(!BOUND(?no_${key})) .` + ); + } else { + patterns.push( + `?s ${fd.predicate} ?f_${key} . FILTER(?f_${key} = ${sparqlTerm(choice)}) .` + ); + } + } + const groupVars = ["?table_name", ...bucket.vars]; + if (groupKey && groupKey !== "source") { + const gd = byKey[groupKey]; + const shared = + gd && + gd.object_kind === "iri" && + sharedPredicates(registry).has(gd.predicate); + const set = shared + ? (gd.values || []).filter((v) => v.iri).map((v) => sparqlTerm(v.iri)) + : []; + // shared-predicate dim without enum IRIs is un-isolatable — fall back to + // grouping by source instead of binding every is-about annotation + if (gd && !(shared && !set.length)) { + patterns.push(`?s ${gd.predicate} ?${gd.key} .`); + if (shared) patterns.push(`FILTER(?${gd.key} IN (${set.join(", ")})) .`); + groupVars.push(`?${gd.key}`); + } + } + const fn = (agg || "sum").toLowerCase() === "mean" ? "AVG" : "SUM"; + return `${prefixHeader(registry)} +PREFIX xsd: +SELECT ${groupVars.join(" ")} (${fn}(xsd:double(?raw_value)) AS ?value) WHERE { + ${patterns.filter(Boolean).join("\n ")} +} GROUP BY ${groupVars.join(" ")}`; +} diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/shared.jsx b/factsheet/frontend/src/components/comparison/prototype_multisource/shared.jsx new file mode 100644 index 000000000..cf277ac03 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/shared.jsx @@ -0,0 +1,732 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — shared presentational atoms. The variants +// differ in layout/hierarchy; these atoms keep the decided semantics +// (badge wording, verdict reasons, ladder, chart correctness) identical +// across them so the maintainer reacts to STRUCTURE, not to copy drift. + +import React, { useEffect, useMemo, useState } from "react"; +import ReactECharts from "echarts-for-react"; +import axios from "axios"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Chip from "@mui/material/Chip"; +import Alert from "@mui/material/Alert"; +import AlertTitle from "@mui/material/AlertTitle"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import ToggleButton from "@mui/material/ToggleButton"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import MenuItem from "@mui/material/MenuItem"; +import TextField from "@mui/material/TextField"; +import IconButton from "@mui/material/IconButton"; +import InputAdornment from "@mui/material/InputAdornment"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogActions from "@mui/material/DialogActions"; +import CircularProgress from "@mui/material/CircularProgress"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Stack from "@mui/material/Stack"; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; +import PreviewIcon from "@mui/icons-material/Preview"; +import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; +import LaunchIcon from "@mui/icons-material/Launch"; +import { labelForIri } from "../registryQuery.js"; +import { resolveTerms } from "../tibTerms.js"; +import { LADDER } from "../comparability.js"; +import { bucketLabel, ROWS_SCHEMA } from "./protoData.js"; + +export const PALETTE = [ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", +]; +export const titleCase = (k) => + String(k) + .replace(/_/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); + +// WF-05 incompleteness badge: registry-reported unmapped columns. +export function UnmappedBadge({ unmapped, size = "small" }) { + if (!unmapped?.length) return null; + return ( + + {unmapped.map((u, i) => ( + + {u.column} — {u.reason} + + ))} + + } + > + } + size={size} + color="warning" + variant="outlined" + label={`${unmapped.length} column${unmapped.length > 1 ? "s" : ""} not comparable`} + /> + + ); +} + +export function GranularityChip({ granularity }) { + if (!granularity) + return ( + + ); + return ( + + ); +} + +// The WF-06 hour/day/week/month/year ladder. Week stays visible but disabled +// (no WEEK() through ontop; ISO weeks cross month/year bounds). +export function GranularityLadder({ + ladder, + granularity, + setGranularity, + size = "small", +}) { + return ( + v && setGranularity(v)} + > + {ladder.map((l) => ( + + {l.level} + + ))} + + ); +} + +// WF-12 decision 4: blocked selections keep everything selectable; the chart +// is REPLACED by a structured reason naming the offending declarations. +export function VerdictPanel({ verdict, dense = false }) { + if (!verdict) return null; + if (verdict.kind === "blocked") { + return ( + + + Blocked: {verdict.reason} + + {verdict.detail} + {verdict.pair && ( + + offending pair: {verdict.pair.join(" · ")} + + )} + + Nothing is hidden — adjust the selection or the measure; the contract + only merges what is declared comparable. + + + ); + } + if (verdict.kind === "aggregate_first") { + return ( + + Sources report at different granularities — the finer series are + auto-aligned to the coarsest common rung ({verdict.target}) per + the registry's aggregation hint. + + ); + } + return null; +} + +export function VerdictChip({ verdict, onClick }) { + if (!verdict) return null; + const map = { + merge: { color: "success", label: "✓ comparable — merge" }, + aggregate_first: { color: "info", label: "⟲ comparable — aggregate first" }, + blocked: { color: "error", label: `⛔ blocked: ${verdict.reason}` }, + }; + const m = map[verdict.kind]; + return ( + + ); +} + +// Empty result ≠ blank graph (reaction round 3): say WHAT returned nothing, +// so the user knows the sources hold the measure but not this exact slice. +export function NoDataAlert({ measure, unit, granularity, nSources }) { + return ( + + No data for this query + + {measure?.label || "the chosen measure"} + {unit ? ( + <> + {" "} + in {unit} + + ) : null}{" "} + per {granularity} across {nSources} source + {nSources !== 1 ? "s" : ""} returned no rows. The sources declare the + measure, but not in this unit/granularity slice — try another unit or a + coarser granularity, or peek at the raw data via the rail. + + + ); +} + +// Transparency (reaction round 3): a quick look at the ACTUAL rows behind a +// source + a link to its table page — without leaving the composition. +export function TablePeek({ table, title }) { + const [open, setOpen] = useState(false); + const [rows, setRows] = useState(null); + const [err, setErr] = useState(null); + useEffect(() => { + if (!open || rows) return; + let active = true; + axios + .get(`/api/v0/schema/${ROWS_SCHEMA}/tables/${table}/rows/?limit=8`) + .then((res) => active && setRows(Array.isArray(res.data) ? res.data : [])) + .catch((e) => active && setErr(e?.message || "could not load rows")); + return () => { + active = false; + }; + }, [open, rows, table]); + const cols = rows?.length ? Object.keys(rows[0]) : []; + return ( + <> + + { + e.stopPropagation(); + setOpen(true); + }} + > + + + + setOpen(false)} + maxWidth="lg" + onClick={(e) => e.stopPropagation()} + > + + {title || table} + + first 8 rows of {ROWS_SCHEMA}.{table} + + + + {err && {err}} + {!rows && !err && ( + + + + )} + {rows && ( + +
    + + + {cols.map((c) => ( + + {c} + + ))} + + + + {rows.map((r, i) => ( + + {cols.map((c) => ( + + {String(r[c] ?? "")} + + ))} + + ))} + +
    + + )} + + + + + + + + ); +} + +// Rail indicator: what would happen if this table joined the selection — +// computed from the pure contract (reaction round 1, items 2+3). +export function CandidateDot({ status }) { + if (!status) return null; + const map = { + merge: { + color: "#2e7d32", + label: "comparable — merges with the selection", + }, + aggregate_first: { + color: "#0288d1", + label: "comparable after aggregation to a common granularity", + }, + blocked: { color: "#d32f2f", label: `would block: ${status.reason}` }, + no_data: { + color: "#9e9e9e", + label: `no data for this measure (${status.reason})`, + }, + }; + const m = map[status.kind] || map.no_data; + return ( + + + + ); +} + +// Generated chart title (reaction round 6): write out WHAT the user sees, +// constructed from the run snapshot — mirrors the single-table view's +// generated title, plus the measure that view is missing. +export function ChartHeading({ summary }) { + if (!summary) return null; + const names = summary.sources.map((s) => s.title || s.table); + const shown = names.slice(0, 4); + const more = names.length - shown.length; + return ( + + + {summary.measureLabel || "Value"} + {summary.unit ? ` in ${summary.unit}` : ""} per {summary.granularity}, + grouped by {summary.groupLabel} + + + {names.length} source{names.length !== 1 ? "s" : ""}:{" "} + {shown.join(" · ")} + {more > 0 ? ` · +${more} more` : ""} + {(summary.filters || []).map((f) => ` — ${f.label}: ${f.value}`)} + + + ); +} + +// Computation statement (reaction round 6): after a green merge the user must +// know whether the tool calculated anything or plotted values as stored. +export function ComputationNote({ summary }) { + if (!summary) return null; + const { transforms = [], conversions = [] } = summary; + if (!transforms.length && !conversions.length) { + return ( + + ✓ Plotted as stored — no aggregation and no unit conversion was applied + to any source. + + ); + } + return ( + + {transforms.map((t, i) => ( + + ⟲ {t.table}: {t.from}ly values {t.fn} to {t.to} —{" "} + {t.hinted + ? "aggregation function from the registry hint" + : "sum fallback (no registry hint)"} + + ))} + {conversions.map((c, i) => ( + + ⇄ {c.table}: values converted {c.from} → {c.to} (×{c.factor}) + + ))} + + {conversions.length === 0 + ? `No unit conversion — every source declares ${summary.unit || "the same unit"} verbatim.` + : null} + + + ); +} + +// One merged chart over the GROUP BY result rows. Source is a first-class +// dimension: grouped by table_name unless another group dim is chosen. +// `stale` dims the chart once parameters diverge from the run (item 4). +export function MergedChart({ + registry, + rows, + groupKey, + unit, + chartType, + granularity, + catalog = [], + notices = [], + unmappedFootnotes = [], + stale = false, + onRerun = null, + running = false, + summary = null, +}) { + // Legends must never show raw OEO ids (reaction round 4): resolve IRI-valued + // group values through the TIB Terminology Service — same resolution (and + // cache) the single-table view uses. + const [terms, setTerms] = useState({}); + useEffect(() => { + if (!rows?.length || !registry) return undefined; + const byKey = Object.fromEntries( + (registry.dimensions || []).map((d) => [d.key, d]) + ); + const gd = groupKey !== "source" ? byKey[groupKey] : null; + if (!gd || gd.object_kind !== "iri") return undefined; + let active = true; + resolveTerms([ + ...new Set(rows.map((r) => r[gd.key]?.value).filter(Boolean)), + ]) + .then((m) => active && setTerms((p) => ({ ...p, ...m }))) + .catch(() => {}); + return () => { + active = false; + }; + }, [rows, registry, groupKey]); + + const { option, stacks, numeric, allZero } = useMemo(() => { + if (!rows || !registry) + return { option: null, stacks: [], numeric: 0, allZero: false }; + const byKey = Object.fromEntries( + (registry.dimensions || []).map((d) => [d.key, d]) + ); + const groupDim = groupKey !== "source" ? byKey[groupKey] : null; + const titleOf = (t) => catalog.find((c) => c.table === t)?.title || t; + const shorten = (s) => + s && String(s).startsWith("http") ? String(s).split("/").pop() : s; + // TIB label first, registry enum label second, shortened IRI last — + // the raw IRI never reaches the legend + const seriesOf = (r) => { + if (!groupDim) return { name: titleOf(r.table_name?.value), iri: null }; + const raw = r[groupDim.key]?.value; + if (raw == null) return { name: null, iri: null }; + if (groupDim.object_kind !== "iri") return { name: raw, iri: null }; + const enumLabel = labelForIri(registry, groupDim, raw); + return { + name: + terms[raw]?.label || (enumLabel !== raw ? enumLabel : shorten(raw)), + iri: raw, + }; + }; + const xVals = [ + ...new Set(rows.map((r) => bucketLabel(r, granularity))), + ].sort(); + const stackList = []; + const matrix = {}; + let numericCount = 0; + let nonZero = false; + for (const r of rows) { + const s = seriesOf(r); + const x = bucketLabel(r, granularity); + const v = parseFloat(r.value?.value); + if (s.name == null || Number.isNaN(v)) continue; + numericCount += 1; + if (v !== 0) nonZero = true; + if (!stackList.find((k) => k.name === s.name)) stackList.push(s); + matrix[s.name] = matrix[s.name] || {}; + matrix[s.name][x] = (matrix[s.name][x] || 0) + v; + } + const isLine = chartType === "line"; + // WF-02 guardrail mirrored here: never stack when source is the group-by + const stacked = chartType === "stacked" && groupKey !== "source"; + // zoom + range selection (reaction round 4): essential for hourly/daily + // series with thousands of steps — wheel/drag zoom inside the plot plus a + // range slider; only shown when the axis is long enough to need it + const zoomable = xVals.length > 31; + const opt = { + tooltip: { + trigger: "axis", + axisPointer: { type: isLine ? "line" : "shadow" }, + // hover listing sorted by value, largest first — matches the visual + // order of the lines at that x position (round 8) + order: "valueDesc", + }, + legend: { + type: "scroll", + top: 0, + tooltip: { + show: true, + formatter: (p) => { + const s = stackList.find((k) => k.name === p.name); + const d = s?.iri && terms[s.iri]?.description; + return d ? `${p.name}
    ${d}` : p.name; + }, + }, + }, + grid: { left: 80, right: 20, bottom: zoomable ? 70 : 40, top: 40 }, + ...(zoomable + ? { + dataZoom: [ + { type: "inside", throttle: 50 }, + { type: "slider", height: 22, bottom: 10 }, + ], + toolbox: { + right: 10, + feature: { + dataZoom: { yAxisIndex: "none" }, + restore: {}, + }, + }, + } + : {}), + xAxis: { type: "category", data: xVals, name: titleCase(granularity) }, + yAxis: { type: "value", name: unit || "value" }, + series: stackList.map((s, i) => ({ + name: s.name, + type: isLine ? "line" : "bar", + ...(stacked ? { stack: "total" } : {}), + showSymbol: xVals.length < 100, + smooth: false, + emphasis: { focus: "series" }, + itemStyle: { color: PALETTE[i % PALETTE.length] }, + data: xVals.map((x) => matrix[s.name]?.[x] ?? null), + })), + }; + return { + option: opt, + stacks: stackList, + numeric: numericCount, + allZero: numericCount > 0 && !nonZero, + }; + }, [rows, registry, groupKey, unit, chartType, granularity, catalog, terms]); + + if (!option) return null; + // rows came back but none carried a readable number — say so instead of + // rendering an empty coordinate system (round 8) + if (numeric === 0) { + return ( + + + + The query returned {rows.length} row{rows.length !== 1 ? "s" : ""}, + but none carried a numeric value that could be plotted — the values + may be empty in the underlying table. Peek at the raw data via the + rail to check. + + + ); + } + return ( + + + + {allZero && ( + + Every value in this slice is exactly 0 — the sources report the + measure, but record zero throughout (the flat line sits on the + x-axis). The data was read correctly; there is just nothing non-zero + to see here. + + )} + + + + + {stale && ( + + + {running ? "…" : "Update chart"} + + ) + } + > + Parameters changed — the chart still shows the previous + configuration. + + + )} + + {/* what each series means — TIB label, description on hover, click → + ontology term (mirrors the single-table view's group definitions) */} + {stacks.some((s) => s.iri) && ( + + {stacks.map( + (s, i) => + s.iri && ( + + + + ) + )} + + )} + {(notices.length > 0 || unmappedFootnotes.length > 0) && ( + + {notices.map((n, i) => ( + + ⟲ {n} + + ))} + {unmappedFootnotes.map((f, i) => ( + + ⚠ {f.table}: {f.count} column{f.count > 1 ? "s" : ""} not + comparable — values may be missing ( + {f.details.map((d) => d.column).join(", ")}) + + ))} + + )} + + ); +} + +// WF-02 guardrail: stacking across sources is never offered while source is +// the group-by — a cross-source stack reads as a sum nobody asked for. +// The blocked state shows as an in-field lock indicator + tooltip instead of +// helper text below the field, which misaligned the toolbar row (round 7). +export function ChartTypeSelect({ chartType, setChartType, groupKey, sx }) { + const stackBlocked = groupKey === "source"; + return ( + setChartType(e.target.value)} + sx={{ minWidth: 180, ...sx }} + InputProps={ + stackBlocked + ? { + startAdornment: ( + + + + + + ), + } + : undefined + } + > + Lines (trend) + Grouped bars (compare) + + Stacked bars{stackBlocked ? " — locked by group-by" : " (composition)"} + + + ); +} + +export { LADDER }; diff --git a/factsheet/frontend/src/components/comparison/prototype_multisource/useMultiSource.js b/factsheet/frontend/src/components/comparison/prototype_multisource/useMultiSource.js new file mode 100644 index 000000000..f4a270c1b --- /dev/null +++ b/factsheet/frontend/src/components/comparison/prototype_multisource/useMultiSource.js @@ -0,0 +1,620 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// PROTOTYPE (wayfinder WF-07) — one shared state machine behind all three UI +// variants, so flipping variants keeps the selection. The variants only +// differ in presentation; the semantics here are the decided ones: +// WF-02 merged result set, source as first-class dimension (default +// group-by when >1 source), unit filter spans all sources; +// WF-05 incompleteness report (registry unmapped_columns) → badges; +// WF-06 granularity ladder, aggregation function from the registry hint; +// WF-12 pure comparability verdict (../comparability.js), blocked +// selections stay selectable and the chart explains why. + +import { useEffect, useMemo, useState } from "react"; +import useRegistry from "../useRegistry.js"; +import { labelForIri, expandCurie } from "../registryQuery.js"; +import { compareSeries, selectionVerdict, LADDER } from "../comparability.js"; +import { + postSparql, + fetchMappedTables, + fetchTableMeta, + familyOf, + tablesWithDimension, + measuresByTable, + unitsByTableForMeasure, + askDimension, + buildMergedQuery, + bucketLabel, + facetValuesForMeasure, +} from "./protoData.js"; + +const titleCase = (k) => + String(k) + .replace(/_/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); + +// dimensions that are machinery, not group-by candidates +const NON_GROUP = new Set([ + "quantity_value", + "unit", + "substance", + "quantity_kind", + "time_step", + "scenario_year", +]); + +export default function useMultiSource() { + const { + registry, + loading: registryLoading, + error: registryError, + } = useRegistry(); + + // ---- catalog: mapped tables + oemetadata + facts + unmapped report ---- + const [catalog, setCatalog] = useState(null); + useEffect(() => { + if (!registry) return; + let active = true; + (async () => { + const byKey = Object.fromEntries( + (registry.dimensions || []).map((d) => [d.key, d]) + ); + const [tables, hasTs, hasYear, subst, qk] = await Promise.all([ + fetchMappedTables(registry), + tablesWithDimension(registry, byKey.time_step), + tablesWithDimension(registry, byKey.scenario_year), + measuresByTable(registry, byKey.substance), + measuresByTable(registry, byKey.quantity_kind), + ]); + const metas = await Promise.all(tables.map(fetchTableMeta)); + if (!active) return; + setCatalog( + tables.map((t, i) => ({ + table: t, + family: familyOf(t), + ...metas[i], + unmapped: (registry.unmapped_columns || {})[t] || [], + substances: subst[t] || [], + quantityKinds: qk[t] || [], + // native granularity: hourly rows if time_step is mapped, else the + // declared scenario_year, else no temporal declaration at all + granularity: hasTs.has(t) ? "hour" : hasYear.has(t) ? "year" : null, + })) + ); + })(); + return () => { + active = false; + }; + }, [registry]); + + // ---- selection ---- + const [selected, setSelected] = useState([ + "amiris_germany2019_day_ahead_market_single_zone", + ]); + const toggle = (t) => + setSelected((cur) => + cur.includes(t) ? cur.filter((x) => x !== t) : [...cur, t] + ); + // measure-first shortcut (reaction round 2): select every source that + // provides the chosen measure + const selectProviders = (providers) => setSelected([...providers]); + // scenario-mode shortcut (reaction round 9): (de)select a whole dataset + // family — the stand-in for a scenario until WF-14 harvests bundle links + const toggleFamily = (tables, on) => + setSelected((cur) => + on + ? [...new Set([...cur, ...tables])] + : cur.filter((t) => !tables.includes(t)) + ); + const entriesFor = (tables) => + (catalog || []).filter((c) => tables.includes(c.table)); + const selectedEntries = useMemo( + () => entriesFor(selected), + [catalog, selected] + ); + + // ---- measure options: CATALOG-wide (measure-first flow, reaction round 1 + // item 3) — pick a measure, then see which sources provide it; measures + // the current selection provides sort first ---- + const substanceDim = useMemo( + () => (registry?.dimensions || []).find((d) => d.key === "substance"), + [registry] + ); + const measureOptions = useMemo(() => { + const opts = []; + const add = (space, value, table, label, aggregation) => { + let o = opts.find((x) => x.space === space && x.value === value); + if (!o) { + o = { space, value, label, aggregation, providers: [] }; + opts.push(o); + } + o.providers.push(table); + }; + for (const e of catalog || []) { + for (const iri of e.substances) { + const enumVal = (substanceDim?.values || []).find( + (v) => expandCurie(registry, v.iri) === iri || v.iri === iri + ); + add( + "substance", + iri, + e.table, + enumVal?.label || labelForIri(registry, substanceDim || {}, iri), + enumVal?.aggregation || null + ); + } + for (const v of e.quantityKinds) + add("quantity_kind", v, e.table, titleCase(v), null); + } + return opts + .map((o) => ({ + ...o, + selectedProviders: o.providers.filter((t) => selected.includes(t)) + .length, + })) + .sort( + (a, b) => + b.selectedProviders - a.selectedProviders || + b.providers.length - a.providers.length + ); + }, [catalog, selected, registry, substanceDim]); + + const [measureId, setMeasureId] = useState(""); + const measure = useMemo( + () => + measureOptions.find((o) => `${o.space}:${o.value}` === measureId) || null, + [measureOptions, measureId] + ); + useEffect(() => { + // keep a valid measure selected; prefer one the selection provides + if (measure) return; + const best = + measureOptions.find((o) => o.selectedProviders > 0) || measureOptions[0]; + setMeasureId(best ? `${best.space}:${best.value}` : ""); + }, [measureOptions, measure]); + + // ---- units per (table, measure) for the WHOLE catalog (the rail's + // would-it-be-comparable indicators need unselected tables' units); + // the unit SELECT stays scoped to the selection (WF-02) ---- + const [unitsByTable, setUnitsByTable] = useState({}); + const [unit, setUnit] = useState(""); + useEffect(() => { + if (!registry || !catalog?.length || !measure) { + setUnitsByTable({}); + return; + } + let active = true; + (async () => { + try { + const u = await unitsByTableForMeasure({ + registry, + tables: catalog.map((c) => c.table), + measure, + }); + if (active) setUnitsByTable(u); + } catch (e) { + if (active) setUnitsByTable({}); + } + })(); + return () => { + active = false; + }; + }, [registry, catalog, measure]); + const unitOptions = useMemo( + () => [...new Set(selected.flatMap((t) => unitsByTable[t] || []))], + [unitsByTable, selected] + ); + useEffect(() => { + setUnit((cur) => (unitOptions.includes(cur) ? cur : unitOptions[0] || "")); + }, [unitOptions]); + + // ---- group-by discovery: union of per-table ASKs (WF-07 spec) ---- + const [availByTable, setAvailByTable] = useState({}); + useEffect(() => { + if (!registry || !selected.length) return; + let active = true; + const dims = (registry.dimensions || []).filter( + (d) => !NON_GROUP.has(d.key) + ); + (async () => { + const perTable = await Promise.all( + selected.map(async (t) => { + const asks = await Promise.all( + dims.map(async (d) => [d.key, await askDimension(registry, t, d)]) + ); + return [t, new Set(asks.filter(([, ok]) => ok).map(([k]) => k))]; + }) + ); + if (active) setAvailByTable(Object.fromEntries(perTable)); + })(); + return () => { + active = false; + }; + }, [registry, selected]); + + const groupOptions = useMemo(() => { + const union = new Set(); + for (const t of selected) + for (const k of availByTable[t] || []) union.add(k); + const dims = (registry?.dimensions || []).filter((d) => union.has(d.key)); + // shared = every selected source populates the dimension — grouping by a + // partial dimension silently drops the sources that lack it (transparency + // ask, reaction round 1 item 3) + const sharedAll = (k) => + selected.length > 0 && + selected.every((t) => (availByTable[t] || new Set()).has(k)); + return [ + { key: "source", label: "Source (table)", shared: true, isSource: true }, + ...dims.map((d) => ({ + key: d.key, + label: titleCase(d.key), + shared: sharedAll(d.key), + })), + ]; + }, [registry, selected, availByTable]); + + const [groupKey, setGroupKey] = useState("source"); + useEffect(() => { + // WF-02: source becomes the default group-by whenever the selection grows + // past one source (the user may regroup afterwards) + if (selected.length > 1) setGroupKey("source"); + }, [selected]); + useEffect(() => { + // repair a group key the new selection no longer offers + setGroupKey((cur) => + groupOptions.find((o) => o.key === cur) + ? cur + : groupOptions[1]?.key || "source" + ); + }, [groupOptions]); + + // ---- series + verdict (pure — ../comparability.js) ---- + // one catalog entry's series for the chosen measure — or null + WF-12 reason + const entrySeries = (e) => { + const declaresSubstance = e.substances.length > 0; + const declaresQk = e.quantityKinds.length > 0; + if (!declaresSubstance && !declaresQk) { + // eu_leg live case: metadata declares the measure but the mapping never + // emits it — treated as its own measure space until WF-21 lands. + return { + table: e.table, + series: null, + reason: "measure spaces not aligned", + detail: `${e.table} declares no measure dimension (neither substance nor quantity_kind)`, + }; + } + const inSpace = + measure.space === "substance" ? e.substances : e.quantityKinds; + if (!inSpace.length) { + return { + table: e.table, + series: null, + reason: "measure spaces not aligned", + detail: `${e.table} annotates ${declaresSubstance ? "substance" : "quantity_kind"}, the chosen measure lives in ${measure.space}`, + }; + } + if (!inSpace.includes(measure.value)) { + return { + table: e.table, + series: null, + reason: "measure mismatch", + detail: `${e.table} does not report ${measure.label}`, + }; + } + const tableUnits = unitsByTable[e.table] || []; + return { + table: e.table, + series: { + table: e.table, + space: measure.space, + measure: measure.value, + measureLabel: measure.label, + unit: tableUnits.includes(unit) ? unit : tableUnits[0] || null, + granularity: e.granularity, + aggregation: measure.aggregation, + }, + }; + }; + + const verdictInput = useMemo( + () => (measure ? selectedEntries.map(entrySeries) : []), + [selectedEntries, measure, unitsByTable, unit] // eslint-disable-line react-hooks/exhaustive-deps + ); + + const verdict = useMemo( + () => + verdictInput.length + ? selectionVerdict(verdictInput, { units: registry?.units || null }) + : null, + [verdictInput, registry] + ); + + // ---- candidate indicators (reaction round 1, items 2+3): for EVERY catalog + // table, what would happen if it joined the current selection — same + // pure contract, surfaced in the selection window ---- + const candidates = useMemo(() => { + if (!measure || !catalog) return {}; + const selSeries = verdictInput.filter((x) => x.series).map((x) => x.series); + const out = {}; + for (const c of catalog) { + const entry = entrySeries(c); + if (!entry.series) { + out[c.table] = { + kind: "no_data", + reason: entry.reason, + detail: entry.detail, + }; + continue; + } + let kind = "merge"; + let reason = null; + let detail = null; + for (const s of selSeries) { + if (s.table === c.table) continue; + const v = compareSeries(entry.series, s, { + units: registry?.units || null, + }); + if (v.kind === "blocked") { + kind = "blocked"; + reason = v.reason; + detail = v.detail; + break; + } + if (v.kind === "aggregate_first") kind = "aggregate_first"; + } + out[c.table] = { kind, reason, detail }; + } + return out; + }, [catalog, measure, verdictInput, unitsByTable, unit, registry]); // eslint-disable-line react-hooks/exhaustive-deps + + // ---- facet-conflation guard (reaction round 8): a value's meaning is the + // COMBINATION of its annotations (WF-04). If the chosen measure's + // observations spread over ≥2 values of a facet dimension the chart is + // not grouped by, those values are summed into one series — warn and + // offer the fixing group-by instead of plotting silently. ---- + const [facetSpread, setFacetSpread] = useState({}); + useEffect(() => { + if (!registry || !selected.length || !measure) { + setFacetSpread({}); + return; + } + let active = true; + facetValuesForMeasure({ registry, tables: selected, measure }) + .then((s) => active && setFacetSpread(s)) + .catch(() => active && setFacetSpread({})); + return () => { + active = false; + }; + }, [registry, selected, measure]); + // facet filters (round 9): pin a spread facet to one value ("all" sums, a + // value filters, "none" keeps only observations without the facet) — the + // alternative to grouping by it, so not every group-by choice warns + const [facetFilters, setFacetFilters] = useState({}); + const setFacetFilter = (key, choice) => + setFacetFilters((cur) => ({ ...cur, [key]: choice })); + useEffect(() => { + // drop filters whose facet/value the new measure/selection no longer has + setFacetFilters((cur) => { + const next = {}; + for (const [k, v] of Object.entries(cur)) { + const vals = facetSpread[k] || []; + if (v === "all" || v === "none" || vals.some((x) => x.iri === v)) + next[k] = v; + } + return next; + }); + }, [facetSpread]); + const conflations = useMemo( + () => + Object.entries(facetSpread) + .filter( + ([key, vals]) => + vals.length > 1 && + key !== groupKey && + (facetFilters[key] || "all") === "all" + ) + .map(([key, vals]) => ({ + key, + label: titleCase(key), + values: vals.map((v) => v.label), + })), + [facetSpread, groupKey, facetFilters] + ); + + // ---- granularity ladder ---- + const ladder = useMemo(() => { + const ok = + verdict?.kind && verdict.kind !== "blocked" ? verdict.levels : []; + return LADDER.map((l) => ({ + level: l, + enabled: ok.includes(l) && l !== "week", // no WEEK() through ontop — see protoData.js + })); + }, [verdict]); + const [granularity, setGranularity] = useState("year"); + useEffect(() => { + const enabled = ladder.filter((l) => l.enabled).map((l) => l.level); + if (enabled.length && !enabled.includes(granularity)) + setGranularity(enabled[enabled.length - 1]); + }, [ladder, granularity]); + + // ---- run ---- + const [rows, setRows] = useState(null); + const [running, setRunning] = useState(false); + const [err, setErr] = useState(null); + const [lastQuery, setLastQuery] = useState(""); + const [chartType, setChartType] = useState("line"); + + // stale-chart detection (reaction round 1, item 4): the chart remembers the + // parameters it was run with; any change dims it until re-run + const paramsKey = JSON.stringify({ + selected: [...selected].sort(), + measureId, + unit, + granularity, + groupKey, + facetFilters, + }); + const [ranKey, setRanKey] = useState(null); + const [ranGranularity, setRanGranularity] = useState(null); + const [ranSummary, setRanSummary] = useState(null); + const stale = !!rows && ranKey !== paramsKey; + + const run = async () => { + if (!registry || !selected.length || verdict?.kind === "blocked") return; + setRunning(true); + setErr(null); + setRows(null); + try { + const q = buildMergedQuery({ + registry, + tables: selected, + measure, + unit, + granularity, + groupKey, + agg: measure?.aggregation, + facetFilters, + }); + setLastQuery(q); + const data = await postSparql(q); + setRows(data?.results?.bindings || []); + setRanKey(paramsKey); + setRanGranularity(granularity); + // snapshot of WHAT this run plotted and WHAT was done to the data — + // feeds the generated chart title + the computation statement (round 6); + // snapshotted so title/statement stay truthful while parameters drift + setRanSummary({ + measureLabel: measure?.label || null, + space: measure?.space || null, + unit: unit || null, + granularity, + groupKey, + groupLabel: + groupKey === "source" + ? "source" + : titleCase( + groupOptions.find((o) => o.key === groupKey)?.label || groupKey + ), + sources: selectedEntries.map((e) => ({ + table: e.table, + title: e.title, + })), + // per-source transformation story: aggregation is the only + // calculation the tool performs today (unit conversion is the WF-13 + // seam and stays inert until the registry serves units:) + transforms: verdictInput + .filter( + (e) => + e.series && + e.series.granularity && + e.series.granularity !== granularity + ) + .map((e) => ({ + table: e.table, + from: e.series.granularity, + to: granularity, + fn: + measure?.aggregation === "mean" + ? "averaged (AVG)" + : "summed (SUM)", + hinted: !!measure?.aggregation, + })), + conversions: verdict?.conversions || [], + filters: Object.entries(facetFilters) + .filter(([, v]) => v && v !== "all") + .map(([k, v]) => ({ + label: titleCase(k), + value: + v === "none" + ? "without this facet" + : (facetSpread[k] || []).find((x) => x.iri === v)?.label || v, + })), + }); + } catch (e) { + setErr(e?.message || "Query failed"); + } finally { + setRunning(false); + } + }; + + // notices: standing caveats stated on the chart (WF-06). Per-source + // aggregation statements moved into ranSummary.transforms (round 6) so the + // computation story is run-snapshotted, not live-drifting. + const notices = useMemo(() => { + const out = []; + for (const c of conflations) { + out.push( + `Mixed ${c.label} values (${c.values.join(" · ")}) are summed within each series — group by ${c.label} to keep them apart.` + ); + } + if ( + ["day", "month"].includes(granularity) && + selectedEntries.some((e) => e.family.startsWith("AMIRIS")) + ) { + out.push( + "FAME settlement bookings (730-h months) don't align with calendar periods — sparse money series show bookings-in-period (WF-06 caveat)." + ); + } + return out; + }, [granularity, selectedEntries, conflations]); + + const unmappedFootnotes = useMemo( + () => + selectedEntries + .filter((e) => e.unmapped.length) + .map((e) => ({ + table: e.table, + count: e.unmapped.length, + details: e.unmapped, + })), + [selectedEntries] + ); + + return { + registry, + registryLoading, + registryError, + catalog, + selected, + toggle, + selectProviders, + toggleFamily, + selectedEntries, + measureOptions, + measure, + measureId, + setMeasureId, + unitOptions, + unitsByTable, + unit, + setUnit, + groupOptions, + groupKey, + setGroupKey, + verdict, + verdictInput, + candidates, + conflations, + facetSpread, + facetFilters, + setFacetFilter, + ladder, + granularity, + setGranularity, + run, + running, + rows, + err, + lastQuery, + chartType, + setChartType, + stale, + ranGranularity, + ranSummary, + notices, + unmappedFootnotes, + bucketLabel, + }; +} diff --git a/factsheet/frontend/src/components/comparison/registryQuery.js b/factsheet/frontend/src/components/comparison/registryQuery.js new file mode 100644 index 000000000..63e01b4c1 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/registryQuery.js @@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// P1/P2 of the registry-driven refactor (see Obsidian "10 - Frontend Refactor +// Plan"). Pure helpers — no React — that build SPARQL from the Dimension +// Property Registry contract served by GET /oekg/registry/. Unit-testable. +// +// Contract shape (oekg/registry/loader.py): +// { namespaces, row_anchor, generic_super_property, dimensions: [ +// { key, concept, predicate, object_kind, datatype, value_space, +// values: [{ code, iri, label }] } ] } +// +// KEY IDEA — disambiguating shared predicates: +// IAMC dimensions currently share the generic `is about` predicate +// (oeo:IAO_0000136). Selecting `?s ?technology` alone would bind to +// EVERY annotated concept on the row. Because the token dictionary assigns +// each value to exactly one dimension, we isolate a dimension by constraining +// its variable to that dimension's enum IRIs: +// ?s oeo:IAO_0000136 ?technology . +// FILTER(?technology IN ( <…technology IRIs…> )) +// This needs the token IRIs filled (resolve_terms.py) but NO new predicates. + +const TABLE_PRED = "oeo:OEO_00000504"; // table-name predicate (row anchor) +const VALUE_KEY = "quantity_value"; // dimension carrying the numeric measure + +export function prefixHeader(registry) { + return Object.entries(registry.namespaces || {}) + .map(([p, iri]) => `PREFIX ${p}: <${iri}>`) + .join("\n"); +} + +export function expandCurie(registry, curie) { + if (!curie || curie.startsWith("http") || !curie.includes(":")) return curie; + const i = curie.indexOf(":"); + const prefix = curie.slice(0, i); + const local = curie.slice(i + 1); + const base = (registry.namespaces || {})[prefix]; + return base ? `${base}${local}` : curie; +} + +// Full http IRI -> <...>; CURIE (prefix declared in the header) -> verbatim. +export function sparqlTerm(iriOrCurie) { + if (!iriOrCurie) return iriOrCurie; + return iriOrCurie.startsWith("http") ? `<${iriOrCurie}>` : iriOrCurie; +} + +// Predicates used by >1 iri-dimension (i.e. the generic `is about`) — these +// need enum isolation. +export function sharedPredicates(registry) { + const counts = {}; + for (const d of registry.dimensions || []) { + if (d.object_kind !== "iri") continue; + counts[d.predicate] = (counts[d.predicate] || 0) + 1; + } + return new Set(Object.keys(counts).filter((p) => counts[p] > 1)); +} + +const enumTerms = (dim) => + (dim.values || []).filter((v) => v.iri).map((v) => sparqlTerm(v.iri)); + +const tableFilter = (tables) => + tables && tables.length + ? `FILTER(?table_name IN (${tables.map((t) => `"${t}"`).join(", ")})) .` + : ""; + +// Distinct values of one dimension actually present in the selected tables. +export function dimensionValuesQuery({ registry, dim, tables = [] }) { + let isolate = ""; + if (dim.object_kind === "iri" && sharedPredicates(registry).has(dim.predicate)) { + const set = enumTerms(dim); + if (set.length) isolate = `FILTER(?v IN (${set.join(", ")})) .`; + } + return `${prefixHeader(registry)} +SELECT DISTINCT ?v ?table_name WHERE { + ?s ${dim.predicate} ?v . ?s ${TABLE_PRED} ?table_name . + ${isolate} + ${tableFilter(tables)} +}`; +} + +// Units present in a table, most common first. If `dim` is given, only units +// that CO-OCCUR with that dimension are returned (e.g. units that actually apply +// when breaking down by technology) — keeps the unit list relevant + small. +export function unitFrequencyQuery({ registry, table, dim }) { + const unitDim = (registry.dimensions || []).find((d) => d.key === "unit"); + const pred = unitDim ? unitDim.predicate : "oeo:OEO_00040010"; + let cond = ""; + if (dim && dim.key !== "unit") { + let iso = ""; + if (dim.object_kind === "iri" && sharedPredicates(registry).has(dim.predicate)) { + const set = enumTerms(dim); + if (set.length) iso = ` FILTER(?dv IN (${set.join(", ")}))`; + } + cond = ` ?s ${dim.predicate} ?dv .${iso}`; + } + return `${prefixHeader(registry)} +SELECT ?v (COUNT(?s) AS ?c) WHERE { + ?s ${TABLE_PRED} ?t . FILTER(?t = "${table}") ?s ${pred} ?v .${cond} +} GROUP BY ?v ORDER BY DESC(?c)`; +} + +// Distinct values of a dimension by frequency (most common first), optionally +// scoped by another (literal) dimension = value — e.g. quantities in a table, or +// the units that occur FOR a chosen quantity (unit follows the quantity). +export function valueFrequencyQuery({ registry, table, dim, scopeDim, scopeValue }) { + let scope = ""; + if (scopeDim && scopeValue) { + const esc = String(scopeValue).replace(/"/g, '\\"'); + scope = ` ?s ${scopeDim.predicate} ?sv . FILTER(STR(?sv) = "${esc}")`; + } + return `${prefixHeader(registry)} +SELECT ?v (COUNT(?s) AS ?c) WHERE { + ?s ${TABLE_PRED} ?t . FILTER(?t = "${table}") ?s ${dim.predicate} ?v .${scope} +} GROUP BY ?v ORDER BY DESC(?c)`; +} + +// Cheap existence check: does this table populate this dimension at all? +// Used to show only dimensions/presets that will actually return data. +export function dimensionAskQuery({ registry, table, dim }) { + let iso = ""; + if (dim.object_kind === "iri" && sharedPredicates(registry).has(dim.predicate)) { + const set = enumTerms(dim); + if (set.length) iso = ` FILTER(?v IN (${set.join(", ")}))`; + } + return `${prefixHeader(registry)} +ASK { ?s ${TABLE_PRED} ?t . FILTER(?t = "${table}") ?s ${dim.predicate} ?v .${iso} }`; +} + +// The comparison query: select the numeric value + the chosen dimension axes, +// filtered by the user's selections. +// dims: array of dimension keys to project (e.g. ["technology","scenario_year"]) +// filters: { [dimKey]: code[] } user selections +export function buildComparisonQuery({ registry, tables = [], filters = {}, dims = [] }) { + const shared = sharedPredicates(registry); + const byKey = Object.fromEntries((registry.dimensions || []).map((d) => [d.key, d])); + const selected = dims.map((k) => byKey[k]).filter(Boolean); + + const valueDim = byKey[VALUE_KEY]; + const patterns = [ + `?s ${valueDim ? valueDim.predicate : "oeo:OEO_00140178"} ?value .`, + `?s ${TABLE_PRED} ?table_name .`, + ]; + const vars = []; + const filterLines = []; + + const tf = tableFilter(tables); + if (tf) filterLines.push(tf); + + for (const d of selected) { + if (d.key === VALUE_KEY) continue; + const v = `?${d.key}`; + vars.push(v); + patterns.push(`?s ${d.predicate} ${v} .`); + + // isolate shared-predicate (generic is-about) dims by their enum set + if (d.object_kind === "iri" && shared.has(d.predicate)) { + const set = enumTerms(d); + if (set.length) patterns.push(`FILTER(${v} IN (${set.join(", ")})) .`); + } + + const codes = filters[d.key]; + if (codes && codes.length) { + const terms = codes.map((code) => { + if (d.object_kind === "iri") { + const val = (d.values || []).find((x) => x.code === code); + return sparqlTerm(val ? val.iri : code); + } + return `"${code}"`; + }); + filterLines.push(`FILTER(${v} IN (${terms.join(", ")})) .`); + } + } + + return `${prefixHeader(registry)} +SELECT DISTINCT ?s ?value ?table_name ${vars.join(" ")} WHERE { + ${patterns.join("\n ")} + ${filterLines.join("\n ")} +}`; +} + +// Map a result's full IRI back to a human label for a given dimension. +export function labelForIri(registry, dim, fullIri) { + for (const v of dim.values || []) { + if (v.iri && expandCurie(registry, v.iri) === fullIri) return v.label || v.code; + } + return fullIri; +} \ No newline at end of file diff --git a/factsheet/frontend/src/components/comparison/tibTerms.js b/factsheet/frontend/src/components/comparison/tibTerms.js new file mode 100644 index 000000000..cd91c87a1 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/tibTerms.js @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Resolve an ontology IRI to { label, description } via the TIB Terminology +// Service. Extracted from quantitativeView.jsx so the registry-driven view can +// reuse the exact same resolution (terms → individuals → properties) and cache. +// OEO references all terms under its own base, so we normalise to the oeo IRI +// from the short form before querying. + +import axios from "axios"; + +const cache = {}; + +export async function resolveTerm(iri) { + if (!iri) return null; + if (cache[iri]) return cache[iri]; + + const shortForm = iri.split("/").pop().split(":").pop(); + const officialIri = `https://openenergyplatform.org/ontology/oeo/${shortForm}`; + const encoded = encodeURIComponent(officialIri); + const baseUrl = + import.meta.env.VITE_TSS_API_BASE?.replace(/\/$/, "") || + "https://api.terminology.tib.eu/api"; + const ontology = import.meta.env.VITE_TSS_DEFAULT_ONTOLOGY || "oeo"; + + const tryEndpoint = async (endpoint) => { + try { + const res = await axios.get( + `${baseUrl}/ontologies/${ontology}/${endpoint}?iri=${encoded}` + ); + const items = res.data?._embedded?.[endpoint]; + if (items && items.length > 0) { + const item = items[0]; + return { + iri, + label: item.label || shortForm, + description: + item.description && item.description.length > 0 + ? item.description.join(" ") + : "No official definition provided in the ontology.", + type: endpoint, + }; + } + } catch (e) { + /* fall through */ + } + return null; + }; + + let info = + (await tryEndpoint("terms")) || + (await tryEndpoint("individuals")) || + (await tryEndpoint("properties")); + if (!info) { + info = { + iri, + label: shortForm, + description: "Term not found in Terminology Service.", + type: "unknown", + }; + } + cache[iri] = info; + return info; +} + +// Resolve many IRIs; returns a map { iri: info }. +export async function resolveTerms(iris) { + const out = {}; + await Promise.all( + [...new Set(iris.filter(Boolean))].map(async (iri) => { + out[iri] = await resolveTerm(iri); + }) + ); + return out; +} \ No newline at end of file diff --git a/factsheet/frontend/src/components/comparison/useRegistry.js b/factsheet/frontend/src/components/comparison/useRegistry.js new file mode 100644 index 000000000..fb61d58c4 --- /dev/null +++ b/factsheet/frontend/src/components/comparison/useRegistry.js @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// P0 of the registry-driven refactor (see Obsidian "10 - Frontend Refactor Plan"). +// Fetches the Dimension Property Registry contract from GET /oekg/registry/ once +// per page load and caches it. The contract is the shared vocabulary the +// comparison view uses to build filters + dynamic SPARQL (no hardcoded terms). + +import { useEffect, useState } from "react"; +import axios from "axios"; +import conf from "../../conf.json"; + +let _cache = null; // module-scope: one fetch per page load +let _inflight = null; + +export async function fetchRegistry() { + if (_cache) return _cache; + if (!_inflight) { + _inflight = axios + .get(conf.dimensionRegistry) + .then((res) => { + _cache = res.data; + return _cache; + }) + .finally(() => { + _inflight = null; + }); + } + return _inflight; +} + +export default function useRegistry() { + const [registry, setRegistry] = useState(_cache); + const [error, setError] = useState(null); + + useEffect(() => { + let active = true; + if (!_cache) { + fetchRegistry() + .then((r) => active && setRegistry(r)) + .catch((e) => active && setError(e)); + } + return () => { + active = false; + }; + }, []); + + return { registry, loading: !registry && !error, error }; +} \ No newline at end of file diff --git a/factsheet/frontend/src/components/comparisonBoardMain.tsx b/factsheet/frontend/src/components/comparisonBoardMain.tsx index f81dbb15a..4c718282d 100644 --- a/factsheet/frontend/src/components/comparisonBoardMain.tsx +++ b/factsheet/frontend/src/components/comparisonBoardMain.tsx @@ -19,10 +19,19 @@ import BreadcrumbsNavGrid from "../styles/oep-theme/components/breadcrumbsNaviga // Import our new sub-components import QualitativeView from "./comparison/qualitativeView.jsx"; import QuantitativeView from "./comparison/quantitativeView.jsx"; +// PROTOTYPE (wayfinder WF-07): multi-source selection variants on the +// Registry (beta) tab, ?variant=A|B|C|0. Falls back to RegistryComparison in +// production builds; remove with the prototype. +import MultiSourcePrototype from "./comparison/prototype_multisource/MultiSourcePrototype.jsx"; +// PROTOTYPE (WF-07 reaction round 6): content for the previously dead +// "How it works?" button — when the prototype folds into the real view, +// keep the dialog and re-home it. +import HowItWorksDialog from "./comparison/prototype_multisource/HowItWorks.jsx"; const ComparisonBoardMain = ({ params }) => { const [scenarios, setScenarios] = useState([]); const [alignment, setAlignment] = useState("Qualitative"); + const [howOpen, setHowOpen] = useState(false); useEffect(() => { const fetchInitialData = async () => { @@ -52,7 +61,9 @@ const ComparisonBoardMain = ({ params }) => { > - + {/* PROTOTYPE (WF-07 reaction, item 1): the Registry workbench needs + the full viewport width — the lg2 container blocks it. */} + {/* TOP TOOLBAR */} theme.spacing(4) }}> @@ -72,9 +83,15 @@ const ComparisonBoardMain = ({ params }) => { variant="text" size="small" startIcon={} + onClick={() => setHowOpen(true)} > How it works? + setHowOpen(false)} + alignment={alignment} + /> @@ -93,6 +110,9 @@ const ComparisonBoardMain = ({ params }) => { Quantitative + + Registry (beta) + @@ -106,6 +126,7 @@ const ComparisonBoardMain = ({ params }) => { {alignment === "Quantitative" && ( )} + {alignment === "Registry" && } ) diff --git a/factsheet/frontend/src/conf.json b/factsheet/frontend/src/conf.json index fad09fc89..a3bf54443 100644 --- a/factsheet/frontend/src/conf.json +++ b/factsheet/frontend/src/conf.json @@ -1,5 +1,6 @@ { "toep": "/", "obdi": "/api/oevkg-query", - "oekgQueryFilter": "oekg/filter-by-criteria/" + "oekgQueryFilter": "oekg/filter-by-criteria/", + "dimensionRegistry": "/oekg/registry/" } diff --git a/oekg/registry/README.md b/oekg/registry/README.md new file mode 100644 index 000000000..48e07c038 --- /dev/null +++ b/oekg/registry/README.md @@ -0,0 +1,97 @@ +# Dimension Property Registry + +The **single source of truth** for the harmonized RDF vocabulary used by the +scenario comparison service. It maps each comparable _dimension_ (region, gas, +scenario type, technology, …) to the **predicate** used in generated triples, +the **object kind** (IRI vs literal), and — for controlled vocabularies — the +**value space** (`code → IRI`). + +## Why it exists + +- oemetadata `isAbout` only gives a **concept (a class)**; an RDF triple needs a + **predicate (an object property)**. The Terminology Service can't derive that + link yet, so it must live as data. +- The mapping generator (separate repo) currently **hardcodes** this + `concept → predicate` lookup. +- The comparison UI + (`factsheet/frontend/src/components/comparison/quantitativeView.jsx`) + **hardcodes** the same predicates on the query side. + +This file replaces _both_ hardcodings. Harmonization only works if every +dataset's mapping and the UI use the **same** predicates and value IRIs — that +is exactly what this registry guarantees. + +## Files + +| File | Purpose | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dimension_property_registry.yaml` | The registry itself (source of truth). Provenance-tagged: `[confirmed]` / `[verify]` / `[ts]`. | +| `loader.py` | Loads the YAML → the `registry.json` **contract dict** (`load_registry()`), inlines value spaces per dimension, + accessors (`predicate_for`, `expand`). The shared API for both consumers. | +| `validate_registry.py` | Structural validator + Phase-1 work-list report. No Django deps; CI-ready. | +| `resolve_terms.py` | Resolves labels → IRIs via the TIB Terminology Service (so term IRIs aren't hand-maintained). | + +## Consumers (one shared contract) + +The registry is an **input**, not a generator output. `loader.build_contract()` +produces one dict consumed by both: + +- **Frontend** ← `GET /oekg/registry/` + (`oekg/views.py: dimension_registry_view`) → builds dynamic SPARQL + filter + UI. +- **Mapping generator** (imported by OEP as a Python package) ← OEP calls + `generate(table_oemetadata: dict, registry: dict) -> obda_str` with this same + dict in-process; the generator never imports OEP internals. + +```bash +python oekg/registry/loader.py # print the registry.json contract +``` + +## Curated here vs resolved from the TS + +The registry is **only** the `dimension → predicate` map (+ +object_kind/datatype) — a modelling choice the Terminology Service cannot derive +(property-by-label search is unreliable). All **term IRIs** (the `value_spaces`) +are resolvable from the TS and should be treated as a re-runnable cache, not +authored truth: + +```bash +python oekg/registry/resolve_terms.py "battery electric vehicle" +python oekg/registry/resolve_terms.py --fill-iamc-tokens +``` + +Per-dataset `value → IRI` mappings ultimately belong in oemetadata +`valueReference`, filled by the resolver and human-confirmed where ambiguous +(codes like `CO2` return several candidates). See the Obsidian note _08 - +Terminology Service_. + +## Usage + +```bash +python oekg/registry/validate_registry.py +``` + +Exit code `0` = structurally valid (TODOs are warnings); `1` = structural error. + +## Consumers + +- **Mapping generator (separate repo):** + `property_url = registry.predicate_for(concept_or_dimension)`, then + `f'{subject_url} {property_url} <{object_iri}> .'`. Vendor or fetch this file. +- **Comparison UI:** should read predicates from here instead of hardcoding them + (Phase 2). + +## Conventions + +- Specific predicates should be declared `rdfs:subPropertyOf` the + `generic_super_property` (`obo:IAO_0000136`, "is about"), so a generic query + catches everything and typed queries still enable dimensional grouping. +- **Namespace hygiene (correctness gate 10):** one canonical IRI form for + `oeo:`/`oekg:` must be used for _every_ predicate and object. See the warning + at the top of the YAML — the existing `.obda` files mix + `http://openenergy-platform.org/…` and `https://openenergyplatform.org/…`. + +## Background docs + +- `scripts/oevkg/sem_mapping/ANNOTATION_AND_MAPPING_DESIGN.md` (§5) +- Obsidian vault: _04 - Annotation Contract_, _05 - Generator Correctness_, + _07 - Concept to Predicate_ diff --git a/oekg/registry/__init__.py b/oekg/registry/__init__.py new file mode 100644 index 000000000..85dae9538 --- /dev/null +++ b/oekg/registry/__init__.py @@ -0,0 +1,4 @@ +""" +SPDX-FileCopyrightText: 2026 Jonas Huber © Reiner Lemoine Institut +SPDX-License-Identifier: AGPL-3.0-or-later +""" # noqa: 501 diff --git a/oekg/registry/dimension_property_registry.yaml b/oekg/registry/dimension_property_registry.yaml new file mode 100644 index 000000000..71253da51 --- /dev/null +++ b/oekg/registry/dimension_property_registry.yaml @@ -0,0 +1,439 @@ +# ============================================================================= +# Dimension Property Registry +# ----------------------------------------------------------------------------- +# Single source of truth for the harmonized RDF vocabulary used by the scenario +# comparison service. It maps each comparable DIMENSION to: +# - the CONCEPT it annotates (the class an oemetadata `isAbout` resolves to), +# - the PREDICATE used in the generated triple (the object property), +# - whether the object is an IRI or a literal (+ datatype), +# - and, for controlled vocabularies, the value space (code -> IRI). +# +# Read by BOTH: +# * the mapping generator (separate repo) -> replaces its hardcoded +# `concept -> predicate` lookup; supplies `property_url`. +# * the comparison UI (factsheet/frontend/.../quantitativeView.jsx) -> today +# these predicates are hardcoded there; the UI should read them from here. +# +# RESOLUTION MODEL (what is curated here vs resolved from the TIB TS): +# * CURATED HERE -> the `dimensions` list = dimension -> predicate association. +# This is a MODELLING choice, not ontology data: the TIB Terminology Service +# cannot derive "which object property attaches a data point to this concept" +# (property-by-label search is unreliable). Small + stable -> fine to maintain. +# * RESOLVED FROM TS -> all term/concept IRIs (the `value_spaces` below). Do NOT +# hand-author these as truth. Use oekg/registry/resolve_terms.py against +# https://api.terminology.tib.eu (e.g. /api/search?q=