diff --git a/docker/serviceConfigs/ontop/mapping.obda b/docker/serviceConfigs/ontop/mapping.obda index 77ab37ad4..809daba90 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,260 @@ 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 | %' + ]] 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/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..e07d45116 100644 --- a/factsheet/frontend/src/components/comparisonBoardMain.tsx +++ b/factsheet/frontend/src/components/comparisonBoardMain.tsx @@ -19,6 +19,7 @@ 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"; +import RegistryComparison from "./comparison/RegistryComparison.jsx"; const ComparisonBoardMain = ({ params }) => { const [scenarios, setScenarios] = useState([]); @@ -93,6 +94,9 @@ const ComparisonBoardMain = ({ params }) => { Quantitative + + Registry (beta) + @@ -106,6 +110,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..af0058525 --- /dev/null +++ b/oekg/registry/dimension_property_registry.yaml @@ -0,0 +1,295 @@ +# ============================================================================= +# 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=