-
Cycling infrastructure
-
Copenhagen is renowned as one of the world's most bicycle-friendly cities, with extensive cycling infrastructure
- serving hundreds of thousands of daily cyclists. The city's bicycle parking facilities are strategically located
- throughout the urban area to support this sustainable transportation network.
-
Explore the map below to discover bicycle parking locations across Copenhagen, including capacity information
- and geographic distribution patterns.
+
+
Where the bikes go
+
4,390 municipal bicycle racks, each with its capacity, street address and city district. Capacity
+ is the number the rack was built for, so the map reads as supply rather than as points.
+
"""^^rdf:XMLLiteral .
# Object block
@@ -28,5 +27,6 @@
# View block with MapMode
<#select-children-view> a ldh:View ;
+ dct:title "All bicycle parkings" ;
spin:query ldh:SelectChildren ;
ac:mode ac:MapMode .
diff --git a/demo/copenhagen/charging-stations.ttl b/demo/copenhagen/charging-stations.ttl
index 7d0201a..c93ae17 100644
--- a/demo/copenhagen/charging-stations.ttl
+++ b/demo/copenhagen/charging-stations.ttl
@@ -8,7 +8,18 @@
# Main container document
<> a dh:Container ;
dct:title "Charging stations" ;
- rdf:_1 <#select-children> .
+ rdf:_1 <#charging-intro> ;
+ rdf:_2 <#select-children> .
+
+# Intro XHTML
+<#charging-intro> a ldh:XHTML ;
+ rdf:value """
-
Public libraries
-
Copenhagen's public library system serves as a cornerstone of community life, providing free access to
- knowledge, culture, and digital resources. From the iconic central library to neighborhood branches,
- these facilities support learning, creativity, and social connection across all city districts.
-
Use the interactive map below to locate libraries throughout Copenhagen and discover their unique
- characteristics and services.
+
+
The branch network
+
Nineteen public libraries, each with its address, postal district, contact address and its page on
+ the city's library site. Open a branch to follow the link out.
+
"""^^rdf:XMLLiteral .
# Object block
@@ -28,5 +27,6 @@
# View block with MapMode
<#select-children-view> a ldh:View ;
+ dct:title "All libraries" ;
spin:query ldh:SelectChildren ;
ac:mode ac:MapMode .
diff --git a/demo/copenhagen/parking-facilities.ttl b/demo/copenhagen/parking-facilities.ttl
index 7d5272f..08fad2d 100644
--- a/demo/copenhagen/parking-facilities.ttl
+++ b/demo/copenhagen/parking-facilities.ttl
@@ -9,8 +9,9 @@
# Main container document
<> a dh:Container ;
dct:title "Parking facilities" ;
- rdf:_1 <#largest-parkings-block> ;
- rdf:_2 <#select-children> .
+ rdf:_1 <#parking-intro> ;
+ rdf:_2 <#largest-parkings-block> ;
+ rdf:_3 <#select-children> .
# Chart block (wrapped in ldh:Object per ContentMode contract)
<#largest-parkings-block> a ldh:Object ;
@@ -43,11 +44,22 @@ WHERE
ORDER BY DESC(?spaces)
LIMIT 100""" .
+# Intro XHTML
+<#parking-intro> a ldh:XHTML ;
+ rdf:value """
- """^^rdf:XMLLiteral .
<#city-intro> a ldh:XHTML ;
rdf:value """
-
City infrastructure overview
-
Explore Copenhagen's public facilities, infrastructure, and services through interactive maps and data visualizations.
- This dataset includes libraries, schools, parking facilities, sports centers, playgrounds, and more.
+
+
What the city publishes
+
Nine facility types from Copenhagen's open data, imported from CSV and given coordinates, so every
+ one of them lands on a map. The chart below counts them by type; the containers hold the records.
+
"""^^rdf:XMLLiteral .
<#facilities-overview-block> a ldh:Object ;
diff --git a/demo/copenhagen/schools.ttl b/demo/copenhagen/schools.ttl
index 11b36f0..63cadf3 100644
--- a/demo/copenhagen/schools.ttl
+++ b/demo/copenhagen/schools.ttl
@@ -8,7 +8,18 @@
# Main container document
<> a dh:Container ;
dct:title "Schools" ;
- rdf:_1 <#select-children> .
+ rdf:_1 <#schools-intro> ;
+ rdf:_2 <#select-children> .
+
+# Intro XHTML
+<#schools-intro> a ldh:XHTML ;
+ rdf:value """
+
+
The school map
+
57 schools with their district, postal address and the city's page for each. District is the unit
+ the municipality plans by, so it is what groups them.
+
+
"""^^rdf:XMLLiteral .
# Object block
<#select-children> a ldh:Object ;
@@ -16,5 +27,6 @@
# View block with MapMode
<#select-children-view> a ldh:View ;
+ dct:title "All schools" ;
spin:query ldh:SelectChildren ;
ac:mode ac:MapMode .
diff --git a/demo/copenhagen/sports-centers.ttl b/demo/copenhagen/sports-centers.ttl
index be87d7d..9457fb5 100644
--- a/demo/copenhagen/sports-centers.ttl
+++ b/demo/copenhagen/sports-centers.ttl
@@ -8,7 +8,18 @@
# Main container document
<> a dh:Container ;
dct:title "Sport centers" ;
- rdf:_1 <#select-children> .
+ rdf:_1 <#sports-intro> ;
+ rdf:_2 <#select-children> .
+
+# Intro XHTML
+<#sports-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Sport and recreation
+
31 sports centres with their address, contact address and own page. The map is the index; the link
+ is where the opening times live.
+
+
"""^^rdf:XMLLiteral .
# Object block
<#select-children> a ldh:Object ;
@@ -16,5 +27,6 @@
# View block with MapMode
<#select-children-view> a ldh:View ;
+ dct:title "All sports centers" ;
spin:query ldh:SelectChildren ;
ac:mode ac:MapMode .
diff --git a/demo/northwind-traders/.ldhignore b/demo/northwind-traders/.ldhignore
index 8453acf..f562874 100644
--- a/demo/northwind-traders/.ldhignore
+++ b/demo/northwind-traders/.ldhignore
@@ -3,3 +3,4 @@ Makefile
imports.csv
root.ttl
screenshot.gif
+unesco-mappings.ttl
diff --git a/demo/northwind-traders/admin/model/ns.ttl b/demo/northwind-traders/admin/model/ns.ttl
index c46c722..0dd03b6 100644
--- a/demo/northwind-traders/admin/model/ns.ttl
+++ b/demo/northwind-traders/admin/model/ns.ttl
@@ -1,5 +1,6 @@
@prefix : <#> .
@prefix ldh:
.
+@prefix ac: .
@prefix rdfs: .
@prefix owl: .
@prefix sp: .
@@ -20,7 +21,7 @@ schema:Order a owl:Class ;
rdfs:isDefinedBy : .
schema:City a owl:Class ;
- rdfs:label "City" ;
+ rdfs:label "Territory" ;
rdfs:isDefinedBy : .
schema:PostalAddress a owl:Class ;
@@ -28,11 +29,11 @@ schema:PostalAddress a owl:Class ;
rdfs:isDefinedBy : .
schema:ProductGroup a owl:Class ;
- rdfs:label "Product group" ;
+ rdfs:label "Category" ;
rdfs:isDefinedBy : .
schema:Corporation a owl:Class ;
- rdfs:label "Corporation" ;
+ rdfs:label "Company" ;
rdfs:isDefinedBy : .
schema:ContactPoint a owl:Class ;
@@ -40,7 +41,33 @@ schema:ContactPoint a owl:Class ;
rdfs:isDefinedBy : .
schema:ParcelDelivery a owl:Class ;
- rdfs:label "Parcel delivery" ;
+ rdfs:label "Delivery" ;
+ rdfs:isDefinedBy : .
+
+schema:DeliveryEvent a owl:Class ;
+ rdfs:label "Shipment" ;
+ rdfs:isDefinedBy : .
+
+schema:Offer a owl:Class ;
+ rdfs:label "Offer" ;
+ rdfs:isDefinedBy : .
+
+schema:PriceSpecification a owl:Class ;
+ rdfs:label "Price specification" ;
+ rdfs:isDefinedBy : .
+
+schema:DeliveryChargeSpecification a owl:Class ;
+ rdfs:label "Delivery charge" ;
+ rdfs:subClassOf schema:PriceSpecification ;
+ rdfs:isDefinedBy : .
+
+schema:UnitPriceSpecification a owl:Class ;
+ rdfs:label "Unit price" ;
+ rdfs:subClassOf schema:PriceSpecification ;
+ rdfs:isDefinedBy : .
+
+schema:DeliveryMethod a owl:Class ;
+ rdfs:label "Delivery method" ;
rdfs:isDefinedBy : .
schema:OrderItem a owl:Class ;
@@ -52,19 +79,359 @@ schema:Product a owl:Class ;
rdfs:isDefinedBy : .
schema:Place a owl:Class ;
- rdfs:label "Place" ;
+ rdfs:label "Region" ;
+ rdfs:isDefinedBy : .
+
+# constructors and constraints
+
+schema:Product spin:constructor :ProductConstructor ;
+ spin:constraint :MissingName .
+
+:ProductConstructor a ldh:Constructor ;
+ rdfs:label "Product constructor" ;
+ dct:title "Product constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this schema:name [ a xsd:string ] ;
+ schema:identifier [ a xsd:string ] ;
+ schema:category [ a schema:ProductGroup ] ;
+ schema:provider [ a schema:Corporation ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:City spin:constructor :CityConstructor .
+
+:CityConstructor a ldh:Constructor ;
+ rdfs:label "Territory constructor" ;
+ dct:title "Territory constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX geo:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this dct:title [ a xsd:string ] ;
+ schema:name [ a xsd:string ] ;
+ schema:identifier [ a xsd:string ] ;
+ geo:lat [ a xsd:string ] ;
+ geo:long [ a xsd:string ] ;
+ schema:containedInPlace [ a schema:Place ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:Person spin:constructor :PersonConstructor .
+
+:PersonConstructor a ldh:Constructor ;
+ rdfs:label "Person constructor" ;
+ dct:title "Person constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:givenName [ a xsd:string ] ;
+ schema:familyName [ a xsd:string ] ;
+ schema:honorificPrefix [ a xsd:string ] ;
+ schema:jobTitle [ a xsd:string ] ;
+ schema:birthDate [ a xsd:date ] ;
+ schema:telephone [ a xsd:string ] ;
+ schema:description [ a xsd:string ] ;
+ schema:address [ a schema:PostalAddress ] ;
+ schema:sponsor [ a schema:Person ] ;
+ schema:areaServed [ a schema:City ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:Corporation spin:constructor :CorporationConstructor .
+
+:CorporationConstructor a ldh:Constructor ;
+ rdfs:label "Company constructor" ;
+ dct:title "Company constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:legalName [ a xsd:string ] ;
+ schema:identifier [ a xsd:string ] ;
+ schema:telephone [ a xsd:string ] ;
+ schema:contactPoint [ a schema:ContactPoint ] ;
+ schema:employee [ a schema:Person ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:ContactPoint spin:constructor :ContactPointConstructor .
+
+:ContactPointConstructor a ldh:Constructor ;
+ rdfs:label "Contact point constructor" ;
+ dct:title "Contact point constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ schema:telephone [ a xsd:string ] ;
+ schema:faxNumber [ a xsd:string ] ;
+ schema:url [ a xsd:string ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:PostalAddress spin:constructor :PostalAddressConstructor .
+
+:PostalAddressConstructor a ldh:Constructor ;
+ rdfs:label "Postal address constructor" ;
+ dct:title "Postal address constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ schema:streetAddress [ a xsd:string ] ;
+ schema:addressLocality [ a xsd:string ] ;
+ schema:addressRegion [ a xsd:string ] ;
+ schema:postalCode [ a xsd:string ] ;
+ schema:addressCountry [ a xsd:string ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:ProductGroup spin:constructor :ProductGroupConstructor .
+
+:ProductGroupConstructor a ldh:Constructor ;
+ rdfs:label "Category constructor" ;
+ dct:title "Category constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:name [ a xsd:string ] ;
+ schema:description [ a xsd:string ] ;
+ schema:identifier [ a xsd:string ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:Place spin:constructor :PlaceConstructor .
+
+:PlaceConstructor a ldh:Constructor ;
+ rdfs:label "Region constructor" ;
+ dct:title "Region constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:name [ a xsd:string ] ;
+ schema:identifier [ a xsd:string ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:Order spin:constructor :OrderConstructor .
+
+:OrderConstructor a ldh:Constructor ;
+ rdfs:label "Order constructor" ;
+ dct:title "Order constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:identifier [ a xsd:string ] ;
+ schema:orderDate [ a xsd:date ] ;
+ schema:customer [ a schema:Corporation ] ;
+ schema:broker [ a schema:Person ] ;
+ schema:orderedItem [ a schema:OrderItem ] ;
+ schema:orderDelivery [ a schema:ParcelDelivery ] ;
+ schema:acceptedOffer [ a schema:Offer ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:OrderItem spin:constructor :OrderItemConstructor .
+
+:OrderItemConstructor a ldh:Constructor ;
+ rdfs:label "Order item constructor" ;
+ dct:title "Order item constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ schema:orderedItem [ a schema:Product ] ;
+ schema:orderQuantity [ a xsd:integer ] ;
+ schema:price [ a xsd:float ] ;
+ schema:priceCurrency [ a xsd:string ] ;
+ schema:discount [ a xsd:float ] ;
+ schema:priceSpecification [ a schema:UnitPriceSpecification ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:ParcelDelivery spin:constructor :ParcelDeliveryConstructor .
+
+:ParcelDeliveryConstructor a ldh:Constructor ;
+ rdfs:label "Delivery constructor" ;
+ dct:title "Delivery constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ schema:partOfOrder [ a schema:Order ] ;
+ schema:provider [ a schema:Corporation ] ;
+ schema:deliveryAddress [ a schema:PostalAddress ] ;
+ schema:expectedArrivalUntil [ a xsd:date ] ;
+ schema:deliveryStatus [ a schema:DeliveryEvent ] ;
+ schema:price [ a xsd:float ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:DeliveryEvent spin:constructor :DeliveryEventConstructor .
+
+:DeliveryEventConstructor a ldh:Constructor ;
+ rdfs:label "Shipment constructor" ;
+ dct:title "Shipment constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:startDate [ a xsd:date ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:Offer spin:constructor :OfferConstructor .
+
+:OfferConstructor a ldh:Constructor ;
+ rdfs:label "Offer constructor" ;
+ dct:title "Offer constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:itemOffered [ a schema:Product ] ;
+ schema:price [ a xsd:float ] ;
+ schema:priceCurrency [ a xsd:string ] ;
+ schema:priceSpecification [ a schema:PriceSpecification ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:PriceSpecification spin:constructor :PriceSpecificationConstructor .
+
+:PriceSpecificationConstructor a ldh:Constructor ;
+ rdfs:label "Price specification constructor" ;
+ dct:title "Price specification constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:price [ a xsd:float ] ;
+ schema:priceCurrency [ a xsd:string ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:UnitPriceSpecification spin:constructor :UnitPriceSpecificationConstructor .
+
+:UnitPriceSpecificationConstructor a ldh:Constructor ;
+ rdfs:label "Unit price constructor" ;
+ dct:title "Unit price constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:price [ a xsd:float ] ;
+ schema:priceCurrency [ a xsd:string ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+schema:DeliveryChargeSpecification spin:constructor :DeliveryChargeSpecificationConstructor .
+
+:DeliveryChargeSpecificationConstructor a ldh:Constructor ;
+ rdfs:label "Delivery charge constructor" ;
+ dct:title "Delivery charge constructor" ;
+ sp:text """
+ PREFIX schema:
+ PREFIX dct:
+ PREFIX xsd:
+
+ CONSTRUCT {
+ $this
+ dct:title [ a xsd:string ] ;
+ schema:price [ a xsd:float ] ;
+ schema:priceCurrency [ a xsd:string ] .
+ }
+ WHERE {}""" ;
+ rdfs:isDefinedBy : .
+
+:MissingName a ldh:MissingPropertyValue ;
+ rdfs:label "Missing schema:name" ;
+ sp:arg1 schema:name ;
rdfs:isDefinedBy : .
# properties
schema:broker a owl:ObjectProperty ;
- rdfs:label "Broker";
+ rdfs:label "Sales rep";
rdfs:domain schema:Order ;
rdfs:range schema:Person ;
rdfs:isDefinedBy : .
schema:areaServed a owl:ObjectProperty ;
- rdfs:label "Area served";
+ rdfs:label "Territory";
rdfs:domain schema:Person ;
rdfs:range schema:City ;
rdfs:isDefinedBy : .
@@ -94,11 +461,58 @@ schema:customer a owl:ObjectProperty ;
rdfs:isDefinedBy : .
schema:orderDelivery a owl:ObjectProperty ;
- rdfs:label "Order delivery" ;
+ rdfs:label "Delivery" ;
rdfs:domain schema:Order ;
rdfs:range schema:ParcelDelivery ;
rdfs:isDefinedBy : .
+schema:acceptedOffer a owl:ObjectProperty ;
+ rdfs:label "Accepted offer" ;
+ rdfs:domain schema:Order ;
+ rdfs:range schema:Offer ;
+ rdfs:isDefinedBy : .
+
+schema:offers a owl:ObjectProperty ;
+ rdfs:label "Offer" ;
+ rdfs:domain schema:Product ;
+ rdfs:range schema:Offer ;
+ rdfs:isDefinedBy : .
+
+schema:priceType a owl:ObjectProperty ;
+ rdfs:label "Price type" ;
+ rdfs:domain schema:UnitPriceSpecification ;
+ rdfs:isDefinedBy : .
+
+schema:itemOffered a owl:ObjectProperty ;
+ rdfs:label "Product" ;
+ rdfs:domain schema:Offer ;
+ rdfs:range schema:Product ;
+ rdfs:isDefinedBy : .
+
+schema:priceSpecification a owl:ObjectProperty ;
+ rdfs:label "Price specification" ;
+ rdfs:domain schema:Offer ;
+ rdfs:range schema:PriceSpecification ;
+ rdfs:isDefinedBy : .
+
+schema:appliesToDeliveryMethod a owl:ObjectProperty ;
+ rdfs:label "Delivery method" ;
+ rdfs:domain schema:DeliveryChargeSpecification ;
+ rdfs:range schema:DeliveryMethod ;
+ rdfs:isDefinedBy : .
+
+schema:deliveryStatus a owl:ObjectProperty ;
+ rdfs:label "Shipment" ;
+ rdfs:domain schema:ParcelDelivery ;
+ rdfs:range schema:DeliveryEvent ;
+ rdfs:isDefinedBy : .
+
+schema:partOfOrder a owl:ObjectProperty ;
+ rdfs:label "Order" ;
+ rdfs:domain schema:ParcelDelivery ;
+ rdfs:range schema:Order ;
+ rdfs:isDefinedBy : .
+
schema:deliveryAddress a owl:ObjectProperty ;
rdfs:label "Delivery address" ;
rdfs:domain schema:ParcelDelivery ;
@@ -117,20 +531,24 @@ schema:category a owl:ObjectProperty ;
rdfs:isDefinedBy : .
schema:containedInPlace a owl:ObjectProperty ;
- rdfs:label "Contained in place" ;
+ rdfs:label "Region" ;
rdfs:domain schema:City ;
rdfs:range schema:Place ;
rdfs:isDefinedBy : .
schema:sponsor a owl:ObjectProperty ;
- rdfs:label "Sponsor" ;
+ rdfs:label "Reports to" ;
rdfs:domain schema:Person ;
rdfs:range schema:Person ;
rdfs:isDefinedBy : .
schema:orderedItem a owl:ObjectProperty ;
rdfs:label "Ordered item" ;
- rdfs:range schema:Product ;
+ rdfs:isDefinedBy : .
+
+schema:orderStatus a owl:ObjectProperty ;
+ rdfs:label "Order status" ;
+ rdfs:domain schema:Order ;
rdfs:isDefinedBy : .
schema:name a owl:DatatypeProperty ;
@@ -146,7 +564,7 @@ schema:description a owl:DatatypeProperty ;
rdfs:isDefinedBy : .
schema:legalName a owl:DatatypeProperty ;
- rdfs:label "Legal name" ;
+ rdfs:label "Company name" ;
rdfs:domain schema:Corporation ;
rdfs:isDefinedBy : .
@@ -209,26 +627,32 @@ schema:birthDate a owl:DatatypeProperty ;
rdfs:isDefinedBy : .
schema:orderQuantity a owl:DatatypeProperty ;
- rdfs:label "Order quantity" ;
- rdfs:domain schema:OrderItem ;
- rdfs:isDefinedBy : .
-
-schema:discount a owl:DatatypeProperty ;
- rdfs:label "Discount" ;
+ rdfs:label "Quantity" ;
rdfs:domain schema:OrderItem ;
rdfs:isDefinedBy : .
+# left without an rdfs:domain on purpose: schema:price is carried both by the order
+# lines (as schema:Offer) and by the freight charge (as schema:DeliveryChargeSpecification)
schema:price a owl:DatatypeProperty ;
rdfs:label "Price" ;
rdfs:isDefinedBy : .
+schema:priceCurrency a owl:DatatypeProperty ;
+ rdfs:label "Currency" ;
+ rdfs:isDefinedBy : .
+
+schema:startDate a owl:DatatypeProperty ;
+ rdfs:label "Shipped" ;
+ rdfs:domain schema:DeliveryEvent ;
+ rdfs:isDefinedBy : .
+
schema:orderDate a owl:DatatypeProperty ;
rdfs:label "Order date" ;
rdfs:domain schema:Order ;
rdfs:isDefinedBy : .
schema:expectedArrivalUntil a owl:DatatypeProperty ;
- rdfs:label "Expected arrival until" ;
+ rdfs:label "Required by" ;
rdfs:domain schema:ParcelDelivery ;
rdfs:isDefinedBy : .
@@ -263,6 +687,8 @@ schema:broker ldh:inverseView :OrdersHandledByEmployee .
:OrdersHandledByEmployee a ldh:View ;
dct:title "Orders handled by this employee" ;
spin:query :SelectOrdersHandledByEmployee ;
+ ac:mode ac:TableMode ;
+ ldh:showWhenEmpty false ;
rdfs:isDefinedBy : .
:SelectOrdersHandledByEmployee a sp:Select ;
@@ -273,9 +699,12 @@ PREFIX schema:
SELECT DISTINCT ?order
WHERE
{ GRAPH ?graph
- { ?order schema:broker $about }
+ { ?order schema:broker $about ;
+ schema:orderDate ?orderDate ;
+ schema:orderStatus ?status
+ }
}
-ORDER BY DESC(?order)
+ORDER BY DESC(?orderDate)
""" ;
rdfs:isDefinedBy : .
@@ -288,6 +717,8 @@ schema:customer ldh:inverseView :OrdersFromCustomer .
:OrdersFromCustomer a ldh:View ;
dct:title "Orders from this customer" ;
spin:query :SelectOrdersFromCustomer ;
+ ac:mode ac:TableMode ;
+ ldh:showWhenEmpty false ;
rdfs:isDefinedBy : .
:SelectOrdersFromCustomer a sp:Select ;
@@ -298,9 +729,12 @@ PREFIX schema:
SELECT DISTINCT ?order
WHERE
{ GRAPH ?graph
- { ?order schema:customer $about }
+ { ?order schema:customer $about ;
+ schema:orderDate ?orderDate ;
+ schema:orderStatus ?status
+ }
}
-ORDER BY DESC(?order)
+ORDER BY DESC(?orderDate)
""" ;
rdfs:isDefinedBy : .
@@ -313,6 +747,8 @@ schema:provider ldh:inverseView :ProductsFromSupplier .
:ProductsFromSupplier a ldh:View ;
dct:title "Products supplied by this supplier" ;
spin:query :SelectProductsFromSupplier ;
+ ac:mode ac:TableMode ;
+ ldh:showWhenEmpty false ;
rdfs:isDefinedBy : .
:SelectProductsFromSupplier a sp:Select ;
@@ -323,12 +759,45 @@ PREFIX schema:
SELECT DISTINCT ?product
WHERE
{ GRAPH ?graph
- { ?product schema:provider $about }
+ { ?product a schema:Product ;
+ schema:provider $about ;
+ schema:category ?category ;
+ schema:offers/schema:price ?price
+ }
}
ORDER BY ?product
""" ;
rdfs:isDefinedBy : .
+# orders shipped by this shipper
+
+schema:provider ldh:inverseView :OrdersShippedByShipper .
+
+:OrdersShippedByShipper a ldh:View ;
+ dct:title "Orders shipped by this shipper" ;
+ spin:query :SelectOrdersShippedByShipper ;
+ ac:mode ac:TableMode ;
+ ldh:showWhenEmpty false ;
+ rdfs:isDefinedBy : .
+
+:SelectOrdersShippedByShipper a sp:Select ;
+ rdfs:label "Select orders shipped by shipper" ;
+ sp:text """
+PREFIX schema:
+
+SELECT DISTINCT ?order
+WHERE
+ { GRAPH ?graph
+ { ?order schema:orderDelivery ?delivery ;
+ schema:orderDate ?orderDate ;
+ schema:orderStatus ?status .
+ ?delivery schema:provider $about
+ }
+ }
+ORDER BY DESC(?orderDate)
+""" ;
+ rdfs:isDefinedBy : .
+
# containedInPlace
# cities in this region
@@ -338,6 +807,8 @@ schema:containedInPlace ldh:inverseView :CitiesInRegion .
:CitiesInRegion a ldh:View ;
dct:title "Cities in this region" ;
spin:query :SelectCitiesInRegion ;
+ ac:mode ac:MapMode ;
+ ldh:showWhenEmpty false ;
rdfs:isDefinedBy : .
:SelectCitiesInRegion a sp:Select ;
@@ -354,8 +825,120 @@ ORDER BY ?city
""" ;
rdfs:isDefinedBy : .
+# category
+
+# products in this category
+
+schema:category ldh:inverseView :ProductsInCategory .
+
+:ProductsInCategory a ldh:View ;
+ dct:title "Products in this category" ;
+ spin:query :SelectProductsInCategory ;
+ ac:mode ac:TableMode ;
+ ldh:showWhenEmpty false ;
+ rdfs:isDefinedBy : .
+
+:SelectProductsInCategory a sp:Select ;
+ rdfs:label "Select products in category" ;
+ sp:text """
+PREFIX schema:
+
+SELECT DISTINCT ?product
+WHERE
+ { GRAPH ?graph
+ { ?product a schema:Product ;
+ schema:category $about }
+ }
+ORDER BY ?product
+""" ;
+ rdfs:isDefinedBy : .
+
+# areaServed
+
+# employees serving this territory
+
+schema:areaServed ldh:inverseView :EmployeesServingTerritory .
+
+:EmployeesServingTerritory a ldh:View ;
+ dct:title "Employees serving this territory" ;
+ spin:query :SelectEmployeesServingTerritory ;
+ ac:mode ac:GridMode ;
+ ldh:showWhenEmpty false ;
+ rdfs:isDefinedBy : .
+
+:SelectEmployeesServingTerritory a sp:Select ;
+ rdfs:label "Select employees serving territory" ;
+ sp:text """
+PREFIX schema:
+
+SELECT DISTINCT ?employee
+WHERE
+ { GRAPH ?graph
+ { ?employee a schema:Person ;
+ schema:areaServed $about }
+ }
+ORDER BY ?employee
+""" ;
+ rdfs:isDefinedBy : .
+
+# sponsor
+
+# employees reporting to this employee
+
+schema:sponsor ldh:inverseView :DirectReports .
+
+:DirectReports a ldh:View ;
+ dct:title "Direct reports" ;
+ spin:query :SelectDirectReports ;
+ ac:mode ac:GridMode ;
+ ldh:showWhenEmpty false ;
+ rdfs:isDefinedBy : .
+
+:SelectDirectReports a sp:Select ;
+ rdfs:label "Select direct reports" ;
+ sp:text """
+PREFIX schema:
+
+SELECT DISTINCT ?employee
+WHERE
+ { GRAPH ?graph
+ { ?employee a schema:Person ;
+ schema:sponsor $about }
+ }
+ORDER BY ?employee
+""" ;
+ rdfs:isDefinedBy : .
+
# orderedItem
+# line items of this order (forward view — the master-detail "detail gallery")
+
+schema:orderedItem ldh:view :OrderLineItems .
+
+:OrderLineItems a ldh:View ;
+ dct:title "Line items" ;
+ spin:query :SelectOrderLineItems ;
+ ac:mode ac:TableMode ;
+ rdfs:isDefinedBy : .
+
+:SelectOrderLineItems a sp:Select ;
+ rdfs:label "Select line items of order" ;
+ sp:text """
+PREFIX schema:
+
+SELECT DISTINCT ?item
+WHERE
+ { GRAPH ?graph
+ { $about schema:orderedItem ?item .
+ ?item schema:orderedItem ?product ;
+ schema:orderQuantity ?quantity ;
+ schema:price ?lineTotal
+ }
+ }
+ORDER BY ?item
+""" ;
+ rdfs:isDefinedBy : .
+
# orders containing this product
schema:orderedItem ldh:inverseView :OrdersContainingProduct .
@@ -363,6 +946,8 @@ schema:orderedItem ldh:inverseView :OrdersContainingProduct .
:OrdersContainingProduct a ldh:View ;
dct:title "Orders containing this product" ;
spin:query :SelectOrdersContainingProduct ;
+ ac:mode ac:TableMode ;
+ ldh:showWhenEmpty false ;
rdfs:isDefinedBy : .
:SelectOrdersContainingProduct a sp:Select ;
@@ -373,10 +958,12 @@ PREFIX schema:
SELECT DISTINCT ?order
WHERE
{ GRAPH ?graph
- { ?order schema:orderedItem ?orderItem .
+ { ?order schema:orderedItem ?orderItem ;
+ schema:orderDate ?orderDate ;
+ schema:orderStatus ?status .
?orderItem schema:orderedItem $about
}
}
-ORDER BY DESC(?order)
+ORDER BY DESC(?orderDate)
""" ;
rdfs:isDefinedBy : .
diff --git a/demo/northwind-traders/categories.ttl b/demo/northwind-traders/categories.ttl
index 04e071d..d1f82df 100644
--- a/demo/northwind-traders/categories.ttl
+++ b/demo/northwind-traders/categories.ttl
@@ -10,21 +10,35 @@
<> a dh:Container ;
dct:title "Categories" ;
rdf:_1 <#category-intro> ;
- rdf:_2 <#category-revenue-block> ;
- rdf:_3 <#category-distribution> ;
- rdf:_4 <#products-per-category-block> ;
- rdf:_5 <#select-categories> .
+ rdf:_2 <#select-categories> ;
+ rdf:_3 <#revenue-intro> ;
+ rdf:_4 <#category-revenue-block> ;
+ rdf:_5 <#category-distribution> ;
+ rdf:_6 <#products-per-category-block> .
# Intro XHTML
<#category-intro> a ldh:XHTML ;
rdf:value """
-
Category analysis
-
Product categories represent different market segments with distinct characteristics and performance.
- Analyzing category-level metrics helps identify portfolio strengths, diversification opportunities, and inventory priorities.
+
+
Product categories
+
The segments the catalogue is organised into — beverages, condiments, produce and the rest.
+ Open one to see the products it holds.
+
+
"""^^rdf:XMLLiteral .
+
+ # Revenue intro
+ <#revenue-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Where the money is
+
Each category is a market segment with its own performance. Revenue first, then the spread of
+ products behind it.
+
"""^^rdf:XMLLiteral .
# Category revenue chart (wrapped in ldh:Object per ContentMode contract)
<#category-revenue-block> a ldh:Object ;
+ dct:title "Revenue by category" ;
rdf:value <#category-revenue> .
<#category-revenue> a ldh:ResultSetChart ;
@@ -43,9 +57,7 @@ WHERE {
GRAPH ?orderGraph {
?order schema:orderedItem ?orderItem .
?orderItem schema:orderedItem ?product ;
- schema:orderQuantity ?quantity ;
- schema:price ?price .
- BIND(?quantity * ?price AS ?sale)
+ schema:price ?sale .
}
GRAPH ?productGraph {
?product schema:category ?category .
@@ -60,13 +72,16 @@ ORDER BY DESC(?revenue)""" .
# Category distribution intro
<#category-distribution> a ldh:XHTML ;
rdf:value """
-
Product distribution
-
Understanding how products are distributed across categories reveals portfolio balance and potential gaps.
- Categories with more products may indicate core competencies or simply broader product lines.
+
+
Portfolio balance
+
Revenue and range don't always line up. A category can earn well on few lines, or spread wide
+ and earn little — the gap between the two charts is where the portfolio questions are.
+
"""^^rdf:XMLLiteral .
# Products per category chart (wrapped in ldh:Object per ContentMode contract)
<#products-per-category-block> a ldh:Object ;
+ dct:title "Products per category" ;
rdf:value <#products-per-category> .
<#products-per-category> a ldh:ResultSetChart ;
@@ -95,10 +110,12 @@ ORDER BY DESC(?productCount)""" .
# Object block (references the view)
<#select-categories> a ldh:Object ;
+ dct:title "All categories" ;
rdf:value <#select-categories-view> .
# View block (references the query, uses GridMode)
<#select-categories-view> a ldh:View ;
+ dct:title "All categories" ;
spin:query <#select-categories-query> ;
ac:mode .
diff --git a/demo/northwind-traders/categories/unesco-mappings.ttl b/demo/northwind-traders/categories/unesco-mappings.ttl
new file mode 100644
index 0000000..a6a5d5c
--- /dev/null
+++ b/demo/northwind-traders/categories/unesco-mappings.ttl
@@ -0,0 +1,41 @@
+# Mappings between Northwind product categories and UNESCO Thesaurus concepts.
+# Each category doubles as a skos:Concept, which makes the SKOS mapping relations
+# applicable and lets the SKOS package render the categories as concepts.
+# Import this file with an RDF import (see the documentation) - it is not
+# installed by update-folder.sh (.ldhignore).
+
+@prefix skos: .
+
+# 1 Beverages -> Beverages
+ a skos:Concept ;
+ skos:closeMatch .
+
+# 2 Condiments -> Food (no finer concept in the thesaurus)
+ a skos:Concept ;
+ skos:broadMatch .
+
+# 3 Confections -> Sugar
+ a skos:Concept ;
+ skos:relatedMatch .
+
+# 4 Dairy Products -> Dairy products
+ a skos:Concept ;
+ skos:closeMatch .
+
+# 5 Grains/Cereals -> Cereals
+ a skos:Concept ;
+ skos:closeMatch .
+
+# 6 Meat/Poultry -> Meat, Poultry
+ a skos:Concept ;
+ skos:closeMatch ,
+ .
+
+# 7 Produce -> Fruit, Vegetables
+ a skos:Concept ;
+ skos:closeMatch ,
+ .
+
+# 8 Seafood -> Fishery products
+ a skos:Concept ;
+ skos:closeMatch .
diff --git a/demo/northwind-traders/customers.ttl b/demo/northwind-traders/customers.ttl
index 27e421f..4aaf439 100644
--- a/demo/northwind-traders/customers.ttl
+++ b/demo/northwind-traders/customers.ttl
@@ -10,22 +10,25 @@
<> a dh:Container ;
dct:title "Customers" ;
rdf:_1 <#customers-intro> ;
- rdf:_2 <#top-customers-block> ;
- rdf:_3 <#geographic-distribution> ;
- rdf:_4 <#customers-by-country-block> ;
- rdf:_5 <#customer-insights> ;
- rdf:_6 <#select-customers> .
+ rdf:_2 <#select-customers> ;
+ rdf:_3 <#customer-insights> ;
+ rdf:_4 <#top-customers-block> ;
+ rdf:_5 <#geographic-distribution> ;
+ rdf:_6 <#customers-by-country-block> .
# Intro XHTML
<#customers-intro> a ldh:XHTML ;
rdf:value """
-
Customer insights
-
Understand customer value, geographic distribution, and purchasing patterns. Identifying top customers
- and market concentration helps prioritize account management and target growth strategies.
+
+
The account book
+
Every company Northwind sells to, with its contact details and address. Open an account to see
+ its full order history.
+
"""^^rdf:XMLLiteral .
# Top customers chart (wrapped in ldh:Object per ContentMode contract)
<#top-customers-block> a ldh:Object ;
+ dct:title "Top 10 customers by revenue" ;
rdf:value <#top-customers> .
<#top-customers> a ldh:ResultSetChart ;
@@ -44,9 +47,7 @@ WHERE {
GRAPH ?orderGraph {
?order schema:customer ?customer ;
schema:orderedItem ?orderItem .
- ?orderItem schema:orderQuantity ?quantity ;
- schema:price ?price .
- BIND(?quantity * ?price AS ?sale)
+ ?orderItem schema:price ?sale .
}
GRAPH ?customerGraph {
?customer schema:legalName ?companyName .
@@ -59,13 +60,16 @@ LIMIT 10""" .
# Geographic distribution intro
<#geographic-distribution> a ldh:XHTML ;
rdf:value """
-
Geographic distribution
-
Our customer base spans multiple countries, with varying levels of market penetration.
- Understanding geographic concentration helps guide expansion efforts and resource allocation.
+
+
Geographic distribution
+
The customer base spans many countries at very different depths. Where the accounts cluster is
+ where the account managers should be.
+
"""^^rdf:XMLLiteral .
# Customers by country chart (wrapped in ldh:Object per ContentMode contract)
<#customers-by-country-block> a ldh:Object ;
+ dct:title "Customers by country" ;
rdf:value <#customers-by-country> .
<#customers-by-country> a ldh:ResultSetChart ;
@@ -94,17 +98,20 @@ ORDER BY DESC(?customerCount)""" .
# Customer insights summary
<#customer-insights> a ldh:XHTML ;
rdf:value """
-
Customer portfolio
-
Browse individual customer records below to view detailed order history, contact information,
- and relationship details. Each customer profile provides insights into purchasing behavior and account status.
+
+
Account value
+
Not every account is worth the same. Revenue concentrates in a handful of them.
+
"""^^rdf:XMLLiteral .
# Object block (references the view)
<#select-customers> a ldh:Object ;
+ dct:title "All customers" ;
rdf:value <#select-customers-view> .
# View block (references the query, uses TableMode)
<#select-customers-view> a ldh:View ;
+ dct:title "All customers" ;
spin:query <#select-customers-query> ;
ac:mode .
diff --git a/demo/northwind-traders/customers/customers.rq b/demo/northwind-traders/customers/customers.rq
index 20c4dc1..5f17799 100644
--- a/demo/northwind-traders/customers/customers.rq
+++ b/demo/northwind-traders/customers/customers.rq
@@ -52,13 +52,17 @@ WHERE
<#contactTitle> ?contactTitle ;
<#address> ?address ;
<#city> ?city ;
- <#postalCode> ?postalCode ;
<#country> ?country ;
<#phone> ?phone .
OPTIONAL {
?customer_row <#region> ?region
}
+ # Ireland had no postcodes until Eircode in 2015, so HUNGO in Cork has none;
+ # requiring one here dropped the customer and left its 19 orders pointing at nothing
+ OPTIONAL {
+ ?customer_row <#postalCode> ?postalCode
+ }
OPTIONAL {
?customer_row <#fax> ?fax
}
diff --git a/demo/northwind-traders/employees.ttl b/demo/northwind-traders/employees.ttl
index ec672cc..a9b091f 100644
--- a/demo/northwind-traders/employees.ttl
+++ b/demo/northwind-traders/employees.ttl
@@ -10,19 +10,33 @@
<> a dh:Container ;
dct:title "Employees" ;
rdf:_1 <#employee-intro> ;
- rdf:_2 <#sales-by-employee-block> ;
- rdf:_3 <#select-employees> .
+ rdf:_2 <#select-employees> ;
+ rdf:_3 <#performance-intro> ;
+ rdf:_4 <#sales-by-employee-block> .
# Intro XHTML
<#employee-intro> a ldh:XHTML ;
rdf:value """
-
Employee performance
-
Track employee sales performance and order processing metrics. Understanding individual
- contributions helps recognize top performers, identify coaching opportunities, and optimize territory assignments.
+
+
The team
+
Everyone on the Northwind payroll, who they report to and the territories they cover. Open a
+ record to see the orders they booked.
+
+
"""^^rdf:XMLLiteral .
+
+ # Performance intro
+ <#performance-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Sales performance
+
Revenue booked per sales rep — the basis for recognising top performers and rebalancing
+ territory assignments.
+
"""^^rdf:XMLLiteral .
# Sales by employee chart (wrapped in ldh:Object per ContentMode contract)
<#sales-by-employee-block> a ldh:Object ;
+ dct:title "Sales by employee" ;
rdf:value <#sales-by-employee> .
<#sales-by-employee> a ldh:ResultSetChart ;
@@ -41,9 +55,7 @@ WHERE {
GRAPH ?orderGraph {
?order schema:broker ?employee ;
schema:orderedItem ?orderItem .
- ?orderItem schema:orderQuantity ?quantity ;
- schema:price ?price .
- BIND(?quantity * ?price AS ?sale)
+ ?orderItem schema:price ?sale .
}
GRAPH ?employeeGraph {
?employee schema:givenName ?givenName ;
@@ -56,10 +68,12 @@ ORDER BY DESC(?totalSales)""" .
# Object block (references the view)
<#select-employees> a ldh:Object ;
+ dct:title "All employees" ;
rdf:value <#select-employees-view> .
# View block (references the query, uses GridMode)
<#select-employees-view> a ldh:View ;
+ dct:title "All employees" ;
spin:query <#select-employees-query> ;
ac:mode .
diff --git a/demo/northwind-traders/orders.ttl b/demo/northwind-traders/orders.ttl
index e9f32e5..f74c54e 100644
--- a/demo/northwind-traders/orders.ttl
+++ b/demo/northwind-traders/orders.ttl
@@ -10,19 +10,37 @@
<> a dh:Container ;
dct:title "Orders" ;
rdf:_1 <#orders-intro> ;
- rdf:_2 <#orders-over-time-block> ;
- rdf:_3 <#geographic-intro> ;
- rdf:_4 <#orders-by-country-block> ;
- rdf:_5 <#order-trends-summary> ;
- rdf:_6 <#sales-by-region-per-year-block> ;
- rdf:_7 <#select-orders> .
+ rdf:_2 <#select-orders> ;
+ rdf:_3 <#analytics-intro> ;
+ rdf:_4 <#orders-over-time-block> ;
+ rdf:_5 <#fulfilment-intro> ;
+ rdf:_6 <#days-to-ship-block> ;
+ rdf:_7 <#on-time-block> ;
+ rdf:_8 <#carrier-intro> ;
+ rdf:_9 <#late-by-carrier-block> ;
+ rdf:_10 <#revenue-by-carrier-block> .
+
+ # Intro XHTML
+ <#orders-intro> a ldh:XHTML ;
+ rdf:value """
+
+
The order book
+
Every order links a customer, the sales rep who booked it, the shipper that delivered it and the
+ products it contains. Orders are listed newest first with their status — sort any column or
+ filter with the facets. Open one for its full master–detail view: line items with their
+ quantities and line totals, the catalogue price each was struck against, the delivery
+ and its address, and the people and companies involved.
+
+
"""^^rdf:XMLLiteral .
# Object block (references the view)
<#select-orders> a ldh:Object ;
+ dct:title "All orders" ;
rdf:value <#select-orders-view> .
# View block (references the query, uses TableMode)
<#select-orders-view> a ldh:View ;
+ dct:title "All orders" ;
spin:query <#select-orders-query> ;
ac:mode .
@@ -38,21 +56,24 @@
schema:identifier ?orderID ;
schema:orderDate ?orderDate ;
schema:customer ?customer ;
- schema:broker ?employee
+ schema:broker ?employee ;
+ schema:orderStatus ?status
}
}
- ORDER BY ?orderID""" .
+ ORDER BY DESC(?orderDate)""" .
- # Intro XHTML
- <#orders-intro> a ldh:XHTML ;
+ # Analytics intro
+ <#analytics-intro> a ldh:XHTML ;
rdf:value """
-
Order analytics
-
Track order volume, geographic distribution, and temporal trends. Understanding order patterns
- helps optimize inventory, predict demand, and identify growth opportunities across different markets.
+
+
Order analytics
+
Beyond the individual records, how has order volume developed over time?
+
"""^^rdf:XMLLiteral .
# Orders over time chart (wrapped in ldh:Object per ContentMode contract)
<#orders-over-time-block> a ldh:Object ;
+ dct:title "Orders per month" ;
rdf:value <#orders-over-time> .
<#orders-over-time> a ldh:ResultSetChart ;
@@ -77,86 +98,195 @@ WHERE {
GROUP BY ?month
ORDER BY ?month""" .
- # Geographic intro
- <#geographic-intro> a ldh:XHTML ;
+ # Fulfilment intro
+ <#fulfilment-intro> a ldh:XHTML ;
rdf:value """
-
Geographic distribution
-
Orders span multiple countries and regions. Analyzing geographic patterns reveals market penetration
- and helps identify expansion opportunities or regional challenges.
+
+
From order to doorstep
+
Every order records three dates: the day it was placed, the day the customer needed it
+ by, and — once the parcel actually leaves the warehouse — the day it was dispatched.
+ Two questions fall out of those. How quickly does Northwind get an order out of the
+ door, and how often does it beat the date it promised? Orders still waiting to ship
+ are left out of the second chart, since they have no outcome yet.
+
"""^^rdf:XMLLiteral .
- # Orders by country chart (wrapped in ldh:Object per ContentMode contract)
- <#orders-by-country-block> a ldh:Object ;
- rdf:value <#orders-by-country> .
+ # Days from order to dispatch
+ <#days-to-ship-block> a ldh:Object ;
+ dct:title "Days to ship" ;
+ rdf:value <#days-to-ship> .
- <#orders-by-country> a ldh:ResultSetChart ;
- dct:title "Orders by country" ;
- spin:query <#orders-by-country-query> ;
- ldh:chartType ;
- ldh:categoryVarName "country" ;
- ldh:seriesVarName "orderCount" .
+ <#days-to-ship> a ldh:ResultSetChart ;
+ dct:title "Days to ship" ;
+ spin:query <#days-to-ship-query> ;
+ ldh:chartType ;
+ ldh:categoryVarName "month" ;
+ ldh:seriesVarName "daysToShip" .
- <#orders-by-country-query> a sp:Select ;
- dct:title "Orders by country" ;
+ <#days-to-ship-query> a sp:Select ;
+ dct:title "Days to ship by month" ;
sp:text """PREFIX schema:
+PREFIX xsd:
-SELECT ?country (COUNT(DISTINCT ?order) AS ?orderCount)
+SELECT ?month (ROUND(AVG(?days) * 10) / 10 AS ?daysToShip)
WHERE {
GRAPH ?orderGraph {
- ?order schema:orderDelivery ?delivery .
- ?delivery schema:deliveryAddress ?address .
- ?address schema:addressCountry ?country .
+ ?order a schema:Order ;
+ schema:orderDate ?orderDate ;
+ schema:orderDelivery ?delivery .
+ # the dispatch is a DeliveryEvent leg of the parcel's journey, so orders that
+ # never shipped simply have no event and drop out of the average
+ ?delivery schema:deliveryStatus ?shipment .
+ ?shipment schema:startDate ?shippedDate .
+ BIND(SUBSTR(STR(?orderDate), 1, 7) AS ?month)
+ BIND((?shippedDate - ?orderDate) / xsd:dayTimeDuration("P1D") AS ?days)
+ }
+ # The order book stops part-way through its final month, which would render as a
+ # cliff rather than a trend. Drop that month — derived from the data, not pinned
+ # to a date, so it stays correct if the dataset grows.
+ {
+ SELECT (SUBSTR(STR(MAX(?anyDate)), 1, 7) AS ?partialMonth)
+ WHERE {
+ GRAPH ?g { ?anyOrder a schema:Order ; schema:orderDate ?anyDate }
+ }
+ }
+ FILTER (?month < ?partialMonth)
+}
+GROUP BY ?month
+ORDER BY ?month""" .
+
+ # On-time shipping rate
+ <#on-time-block> a ldh:Object ;
+ dct:title "Shipped on time" ;
+ rdf:value <#on-time> .
+
+ <#on-time> a ldh:ResultSetChart ;
+ dct:title "Shipped on time" ;
+ spin:query <#on-time-query> ;
+ ldh:chartType ;
+ ldh:categoryVarName "month" ;
+ ldh:seriesVarName "onTimePercent" .
+
+ <#on-time-query> a sp:Select ;
+ dct:title "On-time rate by month" ;
+ sp:text """PREFIX schema:
+
+SELECT ?month (ROUND(SUM(IF(?status = schema:OrderDelivered, 1, 0)) * 1000 / COUNT(?order)) / 10 AS ?onTimePercent)
+WHERE {
+ GRAPH ?orderGraph {
+ ?order a schema:Order ;
+ schema:orderDate ?orderDate ;
+ schema:orderStatus ?status .
+ BIND(SUBSTR(STR(?orderDate), 1, 7) AS ?month)
+ }
+ # an order still awaiting dispatch has no outcome yet - counting it as a miss would
+ # drag the rate down for the months at the end of the book
+ FILTER (?status != schema:OrderProcessing)
+ # The order book stops part-way through its final month, which would render as a
+ # cliff rather than a trend. Drop that month — derived from the data, not pinned
+ # to a date, so it stays correct if the dataset grows.
+ {
+ SELECT (SUBSTR(STR(MAX(?anyDate)), 1, 7) AS ?partialMonth)
+ WHERE {
+ GRAPH ?g { ?anyOrder a schema:Order ; schema:orderDate ?anyDate }
+ }
}
+ FILTER (?month < ?partialMonth)
}
-GROUP BY ?country
-ORDER BY DESC(?orderCount)
-LIMIT 10""" .
-
- # Chart block (wrapped in ldh:Object per ContentMode contract)
- <#sales-by-region-per-year-block> a ldh:Object ;
- rdf:value <#sales-by-region-per-year> .
-
- <#sales-by-region-per-year> a ldh:ResultSetChart ;
- dct:title "Sales by region per year" ;
- spin:query <#sales-by-regions-by-year-query> ;
- ldh:chartType ;
- ldh:categoryVarName "year" ;
- ldh:seriesVarName "regionName" ;
- ldh:seriesVarName "totalSales" .
-
- # Order trends summary
- <#order-trends-summary> a ldh:XHTML ;
+GROUP BY ?month
+ORDER BY ?month""" .
+
+ # Carrier intro
+ <#carrier-intro> a ldh:XHTML ;
rdf:value """
-
Regional performance
-
The table below shows sales performance broken down by region and year, enabling comparison
- of regional growth patterns and seasonal variations.
+
+
Who carries the freight
+
Three carriers deliver every order, which makes them worth comparing on two
+ axes. How often does a carrier miss the date the customer was promised, and
+ how much revenue does each one move month to month? The first is a question
+ about reliability, the second about how the shipping mix has shifted.
+
"""^^rdf:XMLLiteral .
- # Chart SELECT query
- <#sales-by-regions-by-year-query> a sp:Select ;
- dct:title "Sales by region per year" ;
- sp:text """PREFIX schema:
-PREFIX xsd:
-
-SELECT DISTINCT ?year ?regionName (SUM(?sale) AS ?totalSales)
-WHERE
- { GRAPH ?orderGraph
- { ?order schema:orderDate ?orderDate ;
- schema:broker ?employee ;
- schema:orderedItem ?orderItem
- BIND(year(xsd:dateTime(?orderDate)) AS ?year)
- ?orderItem schema:orderedItem ?product ;
- schema:orderQuantity ?quantity ;
- schema:price ?price
- BIND(( ?quantity * ?price ) AS ?sale)
- }
- GRAPH ?employeeGraph
- { ?employee schema:areaServed ?territory }
- GRAPH ?territoryGraph
- { ?territory schema:containedInPlace ?region }
- GRAPH ?regionGraph
- { ?region schema:name ?regionName }
- }
-GROUP BY ?year ?regionName
-ORDER BY DESC(?year) ?regionName
-LIMIT 100""" .
+ # Late shipments by carrier
+ <#late-by-carrier-block> a ldh:Object ;
+ dct:title "Late shipments by carrier" ;
+ rdf:value <#late-by-carrier> .
+
+ <#late-by-carrier> a ldh:ResultSetChart ;
+ dct:title "Late shipments by carrier" ;
+ spin:query <#late-by-carrier-query> ;
+ ldh:chartType ;
+ ldh:categoryVarName "carrier" ;
+ ldh:seriesVarName "latePercent" .
+
+ <#late-by-carrier-query> a sp:Select ;
+ dct:title "Late shipments by carrier" ;
+ sp:text """PREFIX schema:
+
+SELECT ?carrier (ROUND(SUM(IF(?status = schema:OrderProblem, 1, 0)) * 1000 / COUNT(?order)) / 10 AS ?latePercent)
+WHERE {
+ GRAPH ?orderGraph {
+ ?order a schema:Order ;
+ schema:orderStatus ?status ;
+ schema:orderDelivery ?delivery .
+ ?delivery schema:provider ?shipper .
+ }
+ GRAPH ?shipperGraph {
+ ?shipper schema:legalName ?carrier .
+ }
+ # not yet dispatched is not the same as delivered late
+ FILTER (?status != schema:OrderProcessing)
+}
+GROUP BY ?carrier
+ORDER BY DESC(?latePercent)""" .
+
+ # Revenue by carrier - the one multi-series chart in the app. Each ldh:seriesVarName
+ # names its own measure column (wide format); the query pivots with conditional
+ # aggregation. Three carriers is a small fixed set, so naming them here is tolerable -
+ # a fourth would silently not appear.
+ <#revenue-by-carrier-block> a ldh:Object ;
+ dct:title "Revenue by carrier" ;
+ rdf:value <#revenue-by-carrier> .
+
+ <#revenue-by-carrier> a ldh:ResultSetChart ;
+ dct:title "Revenue by carrier" ;
+ spin:query <#revenue-by-carrier-query> ;
+ ldh:chartType ;
+ ldh:categoryVarName "month" ;
+ ldh:seriesVarName "speedyExpress" ;
+ ldh:seriesVarName "unitedPackage" ;
+ ldh:seriesVarName "federalShipping" .
+
+ <#revenue-by-carrier-query> a sp:Select ;
+ dct:title "Revenue by carrier" ;
+ sp:text """PREFIX schema:
+
+SELECT ?month
+ (ROUND(SUM(IF(?carrier = "Speedy Express", ?lineTotal, 0))) AS ?speedyExpress)
+ (ROUND(SUM(IF(?carrier = "United Package", ?lineTotal, 0))) AS ?unitedPackage)
+ (ROUND(SUM(IF(?carrier = "Federal Shipping", ?lineTotal, 0))) AS ?federalShipping)
+WHERE {
+ GRAPH ?orderGraph {
+ ?order a schema:Order ;
+ schema:orderDate ?orderDate ;
+ schema:orderDelivery ?delivery ;
+ schema:orderedItem ?orderItem .
+ ?orderItem schema:price ?lineTotal .
+ ?delivery schema:provider ?shipper .
+ }
+ GRAPH ?shipperGraph {
+ ?shipper schema:legalName ?carrier .
+ }
+ BIND(SUBSTR(STR(?orderDate), 1, 7) AS ?month)
+ # Drop the final, incomplete month - derived from the data rather than pinned to a date.
+ {
+ SELECT (SUBSTR(STR(MAX(?anyDate)), 1, 7) AS ?partialMonth)
+ WHERE {
+ GRAPH ?g { ?anyOrder a schema:Order ; schema:orderDate ?anyDate }
+ }
+ }
+ FILTER (?month < ?partialMonth)
+}
+GROUP BY ?month
+ORDER BY ?month""" .
diff --git a/demo/northwind-traders/orders/order_details.rq b/demo/northwind-traders/orders/order_details.rq
index 30771e9..4533114 100644
--- a/demo/northwind-traders/orders/order_details.rq
+++ b/demo/northwind-traders/orders/order_details.rq
@@ -7,13 +7,32 @@ CONSTRUCT
{
GRAPH ?graph
{
+ ?graph foaf:topic ?orderItem, ?listPrice .
+
?order schema:orderedItem ?orderItem .
- ?orderItem a schema:OrderItem ;
+ # schema:OrderItem carries no price in schema.org - its only properties are
+ # orderedItem, orderQuantity, orderItemNumber, orderItemStatus and orderDelivery.
+ # Prices belong to schema:Offer, so the line is both: the item that was ordered
+ # and the offer that was accepted for it. schema:Offer's own schema:price is the
+ # price of the offer as a whole, i.e. what this line came to.
+ ?orderItem a schema:OrderItem, schema:Offer ;
schema:orderedItem ?product ;
+ schema:itemOffered ?product ;
schema:orderQuantity ?quantity ;
- schema:discount ?discount ;
- schema:price ?price .
+ schema:price ?lineTotal ;
+ schema:priceCurrency "USD" ;
+ schema:priceSpecification ?listPrice .
+
+ # Northwind's per-line discount rate has no home in schema.org - schema:discount
+ # is an Order-level property. Nothing is lost by dropping it: the undiscounted
+ # unit price below, the quantity above and the line total pin the rate exactly,
+ # and unlike a stored rate they cannot disagree with each other.
+ ?listPrice a schema:UnitPriceSpecification ;
+ dct:title "List price" ;
+ schema:priceType schema:ListPrice ;
+ schema:price ?price ;
+ schema:priceCurrency "USD" .
}
}
WHERE
@@ -27,8 +46,10 @@ WHERE
BIND(uri(concat(str($base), "orders/", encode_for_uri(?orderID), "/")) AS ?graph)
BIND(uri(concat(str(?graph), "#this")) AS ?order)
BIND(uri(concat(str(?graph), "#", STRUUID())) AS ?orderItem)
+ BIND(uri(concat(str(?orderItem), "-list-price")) AS ?listPrice)
BIND(uri(concat(str($base), "products/", encode_for_uri(?productID), "/#this")) AS ?product)
BIND (STRDT(?unitPrice, xsd:float) AS ?price)
BIND (STRDT(?quantityString, xsd:integer) AS ?quantity)
BIND (STRDT(?discountString, xsd:float) AS ?discount)
+ BIND (STRDT(str(round(?quantity * ?price * (1 - ?discount) * 100) / 100), xsd:float) AS ?lineTotal)
}
diff --git a/demo/northwind-traders/orders/orders.rq b/demo/northwind-traders/orders/orders.rq
index 98fbab5..9db16ff 100644
--- a/demo/northwind-traders/orders/orders.rq
+++ b/demo/northwind-traders/orders/orders.rq
@@ -9,7 +9,8 @@ CONSTRUCT
{
?graph dct:title ?orderID ;
foaf:primaryTopic ?order ;
- foaf:topic ?orderDelivery, ?deliveryAddress .
+ foaf:topic ?orderDelivery, ?deliveryAddress, ?shipment,
+ ?shippingOffer, ?freightCharge .
?order a schema:Order ;
schema:identifier ?orderID ;
@@ -17,22 +18,48 @@ CONSTRUCT
schema:customer ?customer ;
schema:broker ?employee ;
schema:orderDate ?orderDate ;
+ schema:orderStatus ?status ;
+ schema:acceptedOffer ?shippingOffer ;
schema:orderDelivery ?orderDelivery .
?orderDelivery a schema:ParcelDelivery ;
foaf:page ?graph ;
+ schema:partOfOrder ?order ;
schema:expectedArrivalUntil ?requiredDate ;
- # ?shippedDate ;
+ schema:deliveryStatus ?shipment ;
schema:deliveryAddress ?deliveryAddress ;
schema:provider ?shipper .
+ # schema:ParcelDelivery has no price property either. A delivery charge is a
+ # schema:DeliveryChargeSpecification, and the only way into a PriceSpecification
+ # is schema:priceSpecification from an Offer - so the freight the customer
+ # accepted is spelled out as exactly that.
+ ?shippingOffer a schema:Offer ;
+ foaf:page ?graph ;
+ dct:title "Shipping" ;
+ schema:priceSpecification ?freightCharge .
+
+ ?freightCharge a schema:DeliveryChargeSpecification ;
+ foaf:page ?graph ;
+ dct:title "Freight" ;
+ schema:price ?freight ;
+ schema:priceCurrency "USD" ;
+ schema:appliesToDeliveryMethod schema:ParcelService .
+
+ # schema:ParcelDelivery has no date of its own for "when did this leave the warehouse".
+ # The vocabulary models the journey as a series of schema:DeliveryEvent legs hanging
+ # off schema:deliveryStatus; Northwind knows exactly one leg, the dispatch.
+ ?shipment a schema:DeliveryEvent ;
+ foaf:page ?graph ;
+ dct:title ?shipmentTitle ;
+ schema:startDate ?shippedDate .
+
?deliveryAddress a schema:PostalAddress ;
foaf:page ?graph ;
schema:addressCountry ?shipCountry ;
schema:addressLocality ?shipCity ;
schema:postalCode ?shipPostalCode ;
schema:streetAddress ?shipAddress ;
- schema:location ?deliveryLocation ;
schema:addressRegion ?shipRegion .
}
}
@@ -48,26 +75,42 @@ WHERE
<#shipName> ?shipName ;
<#shipAddress> ?shipAddress ;
<#shipCity> ?shipCity ;
- <#shipPostalCode> ?shipPostalCode ;
<#shipCountry> ?shipCountry .
+ # ?shipment is bound inside the OPTIONAL so that the 21 orders that were never
+ # dispatched get no DeliveryEvent at all, rather than an empty one. It is built from
+ # $base (a constant, substituted before execution) and a re-matched ?orderID rather
+ # than from ?graph: an OPTIONAL group is evaluated standalone, so variables BINDed in
+ # the enclosing group are not in scope inside it.
OPTIONAL {
- ?order_row <#shippedDate> ?shippedDateString
+ ?order_row <#orderID> ?orderID ;
+ <#shippedDate> ?shippedDateString .
BIND(strdt(?shippedDateString, xsd:date) AS ?shippedDate)
+ BIND(uri(concat(str($base), "orders/", encode_for_uri(?orderID), "/#shipment")) AS ?shipment)
+ BIND(concat("Shipped ", ?shippedDateString) AS ?shipmentTitle)
}
OPTIONAL {
?order_row <#shipRegion> ?shipRegion
}
+ # 19 orders carry no postal code; requiring one here dropped them from the import
+ OPTIONAL {
+ ?order_row <#shipPostalCode> ?shipPostalCode
+ }
BIND(uri(concat(str($base), "orders/")) AS ?container)
BIND(uri(concat(str(?container), encode_for_uri(?orderID), "/")) AS ?graph)
BIND(uri(concat(str(?graph), "#this")) AS ?order)
BIND(uri(concat(str(?graph), "#delivery")) AS ?orderDelivery)
BIND(uri(concat(str(?graph), "#delivery-address")) AS ?deliveryAddress)
+ BIND(uri(concat(str(?graph), "#shipping")) AS ?shippingOffer)
+ BIND(uri(concat(str(?graph), "#freight")) AS ?freightCharge)
BIND(uri(concat(str($base), "employees/", encode_for_uri(?employeeID), "/#this")) AS ?employee)
BIND(uri(concat(str($base), "customers/", encode_for_uri(?customerID), "/#this")) AS ?customer)
BIND(uri(concat(str($base), "shippers/", encode_for_uri(?shipVia), "/#this")) AS ?shipper)
BIND(strdt(?orderDateString, xsd:date) AS ?orderDate)
BIND(strdt(?requiredDateString, xsd:date) AS ?requiredDate)
BIND(strdt(?freightString, xsd:float) AS ?freight)
+ BIND(IF(BOUND(?shippedDate),
+ IF(?shippedDate > ?requiredDate, schema:OrderProblem, schema:OrderDelivered),
+ schema:OrderProcessing) AS ?status)
}
diff --git a/demo/northwind-traders/products.ttl b/demo/northwind-traders/products.ttl
index 84ba577..d9e13e2 100644
--- a/demo/northwind-traders/products.ttl
+++ b/demo/northwind-traders/products.ttl
@@ -10,24 +10,35 @@
<> a dh:Container ;
dct:title "Products" ;
rdf:_1 <#products-intro> ;
- rdf:_2 <#top-selling-products-block> ;
- rdf:_3 <#category-analysis-intro> ;
- rdf:_4 <#revenue-by-category-block> ;
- rdf:_5 <#products-by-supplier-block> ;
- rdf:_6 <#supplier-context> ;
- rdf:_7 <#select-products> .
+ rdf:_2 <#select-products> ;
+ rdf:_3 <#performance-intro> ;
+ rdf:_4 <#top-selling-products-block> ;
+ rdf:_5 <#supplier-context> ;
+ rdf:_6 <#products-by-supplier-block> .
# Intro XHTML
<#products-intro> a ldh:XHTML ;
rdf:value """
-
Product analytics
-
Analyze product performance, category distribution, and supplier relationships.
- The Northwind catalog includes products across multiple categories from various suppliers,
- each with unique pricing and inventory characteristics.
+
+
The catalogue
+
Every product Northwind sells, with its category, supplier and unit price. Open one to see the
+ orders it appears in and the supplier behind it.
+
+
"""^^rdf:XMLLiteral .
+
+ # Performance intro
+ <#performance-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Product performance
+
Which lines actually carry the revenue? Category-level breakdowns live under
+ Categories .
+
"""^^rdf:XMLLiteral .
# Chart block (wrapped in ldh:Object per ContentMode contract)
<#top-selling-products-block> a ldh:Object ;
+ dct:title "Top selling products" ;
rdf:value <#top-selling-products> .
<#top-selling-products> a ldh:ResultSetChart ;
@@ -47,9 +58,7 @@ WHERE
{ GRAPH ?orderGraph
{ ?order schema:orderedItem ?orderItem .
?orderItem schema:orderedItem ?product ;
- schema:orderQuantity ?quantity ;
- schema:price ?price
- BIND (?quantity * ?price AS ?sale)
+ schema:price ?sale
}
GRAPH ?productGraph
{ ?product schema:name ?productName }
@@ -58,50 +67,9 @@ GROUP BY ?product ?productName
ORDER BY DESC(?totalSales)
LIMIT 5""" .
- # Category analysis intro
- <#category-analysis-intro> a ldh:XHTML ;
- rdf:value """
-
Category performance
-
Understanding how products and revenue are distributed across categories helps identify
- market strengths and opportunities. The following charts break down revenue and product count by category.
-
"""^^rdf:XMLLiteral .
-
- # Revenue by category chart (wrapped in ldh:Object per ContentMode contract)
- <#revenue-by-category-block> a ldh:Object ;
- rdf:value <#revenue-by-category> .
-
- <#revenue-by-category> a ldh:ResultSetChart ;
- dct:title "Revenue by category" ;
- spin:query <#category-revenue-query> ;
- ldh:chartType ;
- ldh:categoryVarName "categoryName" ;
- ldh:seriesVarName "revenue" .
-
- <#category-revenue-query> a sp:Select ;
- dct:title "Category revenue" ;
- sp:text """PREFIX schema:
-
-SELECT ?categoryName (SUM(?sale) AS ?revenue)
-WHERE {
- GRAPH ?orderGraph {
- ?order schema:orderedItem ?orderItem .
- ?orderItem schema:orderedItem ?product ;
- schema:orderQuantity ?quantity ;
- schema:price ?price .
- BIND(?quantity * ?price AS ?sale)
- }
- GRAPH ?productGraph {
- ?product schema:category ?category .
- }
- GRAPH ?categoryGraph {
- ?category schema:name ?categoryName .
- }
-}
-GROUP BY ?category ?categoryName
-ORDER BY DESC(?revenue)""" .
-
# Products by supplier chart (wrapped in ldh:Object per ContentMode contract)
<#products-by-supplier-block> a ldh:Object ;
+ dct:title "Products by supplier" ;
rdf:value <#products-by-supplier> .
<#products-by-supplier> a ldh:ResultSetChart ;
@@ -122,7 +90,7 @@ WHERE {
schema:provider ?supplier .
}
GRAPH ?supplierGraph {
- ?supplier schema:name ?supplierName .
+ ?supplier schema:legalName ?supplierName .
}
}
GROUP BY ?supplier ?supplierName
@@ -132,17 +100,21 @@ LIMIT 10""" .
# Supplier context
<#supplier-context> a ldh:XHTML ;
rdf:value """
-
Supplier relationships
-
Northwind Traders works with suppliers worldwide to source products. The distribution of products
- across suppliers shows which partnerships are most productive and may indicate supply chain dependencies.
+
+
Supplier relationships
+
Northwind sources from suppliers worldwide. How the catalogue spreads across them shows which
+ partnerships carry the range — and where the supply chain concentrates.
+
"""^^rdf:XMLLiteral .
# Object block (references the view)
<#select-products> a ldh:Object ;
+ dct:title "All products" ;
rdf:value <#select-products-view> .
# View block (references the query, uses TableMode)
<#select-products-view> a ldh:View ;
+ dct:title "All products" ;
spin:query <#select-products-query> ;
ac:mode .
diff --git a/demo/northwind-traders/products/products.rq b/demo/northwind-traders/products/products.rq
index 1f482db..fa0000a 100644
--- a/demo/northwind-traders/products/products.rq
+++ b/demo/northwind-traders/products/products.rq
@@ -8,7 +8,8 @@ CONSTRUCT
GRAPH ?graph
{
?graph dct:title ?productName ;
- foaf:primaryTopic ?product .
+ foaf:primaryTopic ?product ;
+ foaf:topic ?offer .
?product a schema:Product ;
schema:identifier ?productID ;
@@ -17,11 +18,21 @@ CONSTRUCT
schema:provider ?supplier ;
schema:category ?category ;
schema:description ?quantityPerUnit ;
- schema:price ?unitPrice .
+ schema:offers ?offer .
# ?unitsInStock ;
# ?unitsOnOrder ;
# ?reorderLevel ;
# ?discontinued - schema:Discontinued
+
+ # schema:Product has no price of its own - schema:price belongs to schema:Offer,
+ # which a product reaches through schema:offers. This is the catalogue price;
+ # what a given order line was actually billed at lives on that line's own offer.
+ ?offer a schema:Offer ;
+ foaf:page ?graph ;
+ dct:title "List price" ;
+ schema:itemOffered ?product ;
+ schema:price ?unitPrice ;
+ schema:priceCurrency "USD" .
}
}
WHERE
@@ -40,6 +51,7 @@ WHERE
BIND (uri(concat(str($base), "products/")) AS ?container)
BIND(uri(concat(str(?container), encode_for_uri(?productID), "/")) AS ?graph)
BIND(uri(concat(str(?graph), "#this")) AS ?product)
+ BIND(uri(concat(str(?graph), "#offer")) AS ?offer)
BIND(uri(concat(str($base), "suppliers/", encode_for_uri(?supplierID), "/#this")) AS ?supplier)
BIND(uri(concat(str($base), "categories/", encode_for_uri(?categoryID), "/#this")) AS ?category)
BIND (STRDT(?unitPriceString, xsd:float) AS ?unitPrice)
diff --git a/demo/northwind-traders/regions.ttl b/demo/northwind-traders/regions.ttl
index f9f8000..dbc22b8 100644
--- a/demo/northwind-traders/regions.ttl
+++ b/demo/northwind-traders/regions.ttl
@@ -8,14 +8,27 @@
# Main container document
<> a dh:Container ;
dct:title "Regions" ;
- rdf:_1 <#select-regions> .
+ rdf:_1 <#regions-intro> ;
+ rdf:_2 <#select-regions> .
+
+ # Intro XHTML
+ <#regions-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Sales regions
+
The top level of the sales geography. Each region contains the
+ territories that reps are assigned to.
+
+
"""^^rdf:XMLLiteral .
# Object block (references the view)
<#select-regions> a ldh:Object ;
+ dct:title "All regions" ;
rdf:value <#select-regions-view> .
# View block (references the query, no mode specified - uses default)
<#select-regions-view> a ldh:View ;
+ dct:title "All regions" ;
spin:query <#select-regions-query> .
# SELECT query
diff --git a/demo/northwind-traders/root.ttl b/demo/northwind-traders/root.ttl
index 4bb26a9..6a041eb 100644
--- a/demo/northwind-traders/root.ttl
+++ b/demo/northwind-traders/root.ttl
@@ -12,35 +12,28 @@
<> a def:Root ;
dct:title "Northwind Traders" ;
dct:description "Knowledge Graph representation of the Northwind Traders sample database" ;
- rdf:_1 <#page-header> ;
- rdf:_2 <#overview-intro> ;
- rdf:_3 <#sales-trend-block> ;
- rdf:_4 <#revenue-by-country-block> ;
- rdf:_5 <#top-selling-products> ;
- rdf:_6 <#top-manager-header> ;
- rdf:_7 <#top-manager> ;
- rdf:_8 <#navigation-prompt> ;
- rdf:_9 <#select-children> .
-
-<#page-header> a ldh:XHTML ;
- rdf:value """
-
-
Features include CSV data imports , SPARQL-based charts , faceted search and related results (parallax navigation) as well as
- rich documents composed of structured content backed by RDF Knowledge Graph data.
-
This application was created on LinkedDataHub using the low code paradigm: no programming was required, only scripts that
- invoke LinkedDataHub's CLI commands.
-
"""^^rdf:XMLLiteral .
+ rdf:_1 <#overview-intro> ;
+ rdf:_2 <#sales-trend-block> ;
+ rdf:_3 <#margin-intro> ;
+ rdf:_4 <#discount-rate-block> ;
+ rdf:_5 <#revenue-by-country-block> ;
+ rdf:_6 <#top-selling-products> ;
+ rdf:_7 <#top-manager-header> ;
+ rdf:_8 <#top-manager> ;
+ rdf:_9 <#navigation-prompt> ;
+ rdf:_10 <#select-children> .
<#overview-intro> a ldh:XHTML ;
rdf:value """
-
Executive dashboard
-
Explore key business metrics including sales trends, geographic distribution, and product performance. All visualizations are generated from live SPARQL queries over the RDF Knowledge Graph.
+
+
Executive dashboard
+
Sales performance at a glance — how revenue is trending, what the discounting costs,
+ where the markets are and which products carry the mix.
+
"""^^rdf:XMLLiteral .
<#sales-trend-block> a ldh:Object ;
+ dct:title "Monthly sales trend" ;
rdf:value <#sales-trend> .
<#sales-trend> a ldh:ResultSetChart ;
@@ -60,16 +53,83 @@ WHERE {
?order a schema:Order ;
schema:orderDate ?orderDate ;
schema:orderedItem ?orderItem .
+ ?orderItem schema:price ?sale .
+ BIND(SUBSTR(STR(?orderDate), 1, 7) AS ?month)
+ }
+ # The order book stops part-way through its final month, which would render as a
+ # cliff rather than a trend. Drop that month — derived from the data, not pinned
+ # to a date, so it stays correct if the dataset grows.
+ {
+ SELECT (SUBSTR(STR(MAX(?anyDate)), 1, 7) AS ?partialMonth)
+ WHERE {
+ GRAPH ?g { ?anyOrder a schema:Order ; schema:orderDate ?anyDate }
+ }
+ }
+ FILTER (?month < ?partialMonth)
+}
+GROUP BY ?month
+ORDER BY ?month""" .
+
+<#margin-intro> a ldh:XHTML ;
+ rdf:value """
+
+
What the deals cost
+
Revenue is what was billed, not what was listed. Every order line keeps the catalogue
+ price it started from alongside the price it actually went out at, so the gap between
+ them is what the sales rep gave away to win the deal. Measured against gross, that gap
+ swings between roughly 4% and 11% month to month — a commercial lever in its own
+ right, and one that never shows up in a revenue chart.
+
+
"""^^rdf:XMLLiteral .
+
+<#discount-rate-block> a ldh:Object ;
+ dct:title "Discount rate" ;
+ rdf:value <#discount-rate> .
+
+<#discount-rate> a ldh:ResultSetChart ;
+ dct:title "Discount rate" ;
+ spin:query <#discount-by-month-query> ;
+ ldh:chartType ;
+ ldh:categoryVarName "month" ;
+ ldh:seriesVarName "discountPercent" .
+
+<#discount-by-month-query> a sp:Select ;
+ dct:title "Discount rate by month" ;
+ sp:text """PREFIX schema:
+
+SELECT ?month (ROUND(SUM(?discount) * 1000 / SUM(?gross)) / 10 AS ?discountPercent)
+WHERE {
+ GRAPH ?orderGraph {
+ ?order a schema:Order ;
+ schema:orderDate ?orderDate ;
+ schema:orderedItem ?orderItem .
+ # the line's own price is net of discount; the catalogue price it was struck
+ # against hangs off it as a ListPrice specification
?orderItem schema:orderQuantity ?quantity ;
- schema:price ?price .
- BIND(?quantity * ?price AS ?sale)
+ schema:price ?net ;
+ schema:priceSpecification ?listPrice .
+ ?listPrice schema:priceType schema:ListPrice ;
+ schema:price ?unitPrice .
BIND(SUBSTR(STR(?orderDate), 1, 7) AS ?month)
+ BIND(?quantity * ?unitPrice AS ?gross)
+ BIND(?gross - ?net AS ?discount)
+ }
+ # The order book stops part-way through its final month, which would render as a
+ # cliff rather than a trend. Drop that month — derived from the data, not pinned
+ # to a date, so it stays correct if the dataset grows.
+ {
+ SELECT (SUBSTR(STR(MAX(?anyDate)), 1, 7) AS ?partialMonth)
+ WHERE {
+ GRAPH ?g { ?anyOrder a schema:Order ; schema:orderDate ?anyDate }
+ }
}
+ FILTER (?month < ?partialMonth)
}
GROUP BY ?month
ORDER BY ?month""" .
<#revenue-by-country-block> a ldh:Object ;
+ dct:title "Revenue by country" ;
rdf:value <#revenue-by-country> .
<#revenue-by-country> a ldh:ResultSetChart ;
@@ -88,11 +148,9 @@ WHERE {
GRAPH ?orderGraph {
?order schema:orderedItem ?orderItem ;
schema:orderDelivery ?delivery .
- ?orderItem schema:orderQuantity ?quantity ;
- schema:price ?price .
+ ?orderItem schema:price ?sale .
?delivery schema:deliveryAddress ?address .
?address schema:addressCountry ?country .
- BIND(?quantity * ?price AS ?sale)
}
}
GROUP BY ?country
@@ -100,22 +158,29 @@ ORDER BY DESC(?revenue)
LIMIT 10""" .
<#top-selling-products> a ldh:Object ;
+ dct:title "Top selling products" ;
rdf:value .
<#top-manager-header> a ldh:XHTML ;
rdf:value """
-
Top manager
-
Meet the top manager of Northwind Traders.
+
+
Sales leadership
+
The Vice President of Sales, with the territories, reports and order history attached to the role.
+
"""^^rdf:XMLLiteral .
<#top-manager> a ldh:Object ;
+ dct:title "Vice President, Sales" ;
rdf:value .
<#navigation-prompt> a ldh:XHTML ;
rdf:value """
-
Explore more
-
Dive deeper into detailed analytics by exploring the containers below. Each section provides comprehensive insights into products, orders, customers, employees, and more.
+
+
Browse the business
+
Open Orders for the order book and its master–detail line items, or go straight to products, customers, employees and the reference data behind them.
+
"""^^rdf:XMLLiteral .
<#select-children> a ldh:Object ;
+ dct:title "Sections" ;
rdf:value ldh:ChildrenView .
diff --git a/demo/northwind-traders/shippers.ttl b/demo/northwind-traders/shippers.ttl
index 2a5b1b9..2964f53 100644
--- a/demo/northwind-traders/shippers.ttl
+++ b/demo/northwind-traders/shippers.ttl
@@ -8,14 +8,26 @@
# Main container document
<> a dh:Container ;
dct:title "Shippers" ;
- rdf:_1 <#select-shippers> .
+ rdf:_1 <#shippers-intro> ;
+ rdf:_2 <#select-shippers> .
+
+ # Intro XHTML
+ <#shippers-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Getting it there
+
The carriers that deliver Northwind's orders. Open one to see every delivery it handled.
+
+
"""^^rdf:XMLLiteral .
# Object block (references the view)
<#select-shippers> a ldh:Object ;
+ dct:title "All shippers" ;
rdf:value <#select-shippers-view> .
# View block (references the query, no mode specified - uses default)
<#select-shippers-view> a ldh:View ;
+ dct:title "All shippers" ;
spin:query <#select-shippers-query> .
# SELECT query
diff --git a/demo/northwind-traders/suppliers.ttl b/demo/northwind-traders/suppliers.ttl
index 2f5b755..791e42f 100644
--- a/demo/northwind-traders/suppliers.ttl
+++ b/demo/northwind-traders/suppliers.ttl
@@ -9,14 +9,27 @@
# Main container document
<> a dh:Container ;
dct:title "Suppliers" ;
- rdf:_1 <#select-suppliers> .
+ rdf:_1 <#suppliers-intro> ;
+ rdf:_2 <#select-suppliers> .
+
+ # Intro XHTML
+ <#suppliers-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Where the goods come from
+
The companies Northwind buys from. Open a supplier to see everything it supplies to the
+ catalogue .
+
+
"""^^rdf:XMLLiteral .
# Object block (references the view)
<#select-suppliers> a ldh:Object ;
+ dct:title "All suppliers" ;
rdf:value <#select-suppliers-view> .
# View block (references the query, uses TableMode)
<#select-suppliers-view> a ldh:View ;
+ dct:title "All suppliers" ;
spin:query <#select-suppliers-query> ;
ac:mode .
diff --git a/demo/northwind-traders/territories.ttl b/demo/northwind-traders/territories.ttl
index cb1575f..9c4d5ce 100644
--- a/demo/northwind-traders/territories.ttl
+++ b/demo/northwind-traders/territories.ttl
@@ -9,14 +9,27 @@
# Main container document
<> a dh:Container ;
dct:title "Territories" ;
- rdf:_1 <#select-territories> .
+ rdf:_1 <#territories-intro> ;
+ rdf:_2 <#select-territories> .
+
+ # Intro XHTML
+ <#territories-intro> a ldh:XHTML ;
+ rdf:value """
+
+
Sales territories
+
The territories reps cover, plotted on the map and grouped into
+ regions .
+
+
"""^^rdf:XMLLiteral .
# Object block (references the view)
<#select-territories> a ldh:Object ;
+ dct:title "All territories" ;
rdf:value <#select-territories-view> .
# View block (references the query, uses MapMode)
<#select-territories-view> a ldh:View ;
+ dct:title "All territories" ;
spin:query <#select-territories-query> ;
ac:mode .
diff --git a/demo/unesco-thesaurus/collections.ttl b/demo/unesco-thesaurus/collections.ttl
index 417cbac..5f089b5 100644
--- a/demo/unesco-thesaurus/collections.ttl
+++ b/demo/unesco-thesaurus/collections.ttl
@@ -17,6 +17,7 @@
# View block (references the query)
<#select-collections-view> a ldh:View ;
+ dct:title "All collections" ;
spin:query <#select-collections-query> .
# SELECT query
diff --git a/demo/unesco-thesaurus/concept-schemes.ttl b/demo/unesco-thesaurus/concept-schemes.ttl
index 29b7bb2..2381640 100644
--- a/demo/unesco-thesaurus/concept-schemes.ttl
+++ b/demo/unesco-thesaurus/concept-schemes.ttl
@@ -17,6 +17,7 @@
# View block (references the query)
<#select-concept-schemes-view> a ldh:View ;
+ dct:title "All concept schemes" ;
spin:query <#select-concept-schemes-query> .
# SELECT query
diff --git a/demo/unesco-thesaurus/concepts.ttl b/demo/unesco-thesaurus/concepts.ttl
index 3e8655f..c16c954 100644
--- a/demo/unesco-thesaurus/concepts.ttl
+++ b/demo/unesco-thesaurus/concepts.ttl
@@ -15,15 +15,32 @@
# Intro XHTML
<#concepts-intro> a ldh:XHTML ;
rdf:value """
-
SKOS concepts
-
Concepts are the fundamental building blocks of the UNESCO Thesaurus vocabulary. Each concept represents
- a distinct unit of thought or terminology, enriched with multilingual labels, definitions, and semantic relationships.
-
SKOS (Simple Knowledge Organization System) relationships connect concepts through:
-
- Broader/Narrower - Hierarchical relationships forming taxonomic structures
- Related - Associative relationships between concepts in the same domain
- Exact/Close/Related Matches - Mappings to concepts in external vocabularies
-
+
+
The concepts
+
Every concept carries its labels in each language the thesaurus publishes, and its place in the
+ hierarchy. Open one to get the concept tree beside it — that column is the taxonomy editor package's,
+ not the platform's.
+
+
SKOS connects concepts three ways:
+
+
+ Hierarchical
+ Associative
+ Matches
+
+
+
Broader/Narrower relationships form the taxonomic backbone: every concept links up to more general concepts and down to more specific ones, and parallax
+ navigation follows those links in either direction.
+
+
+
Related relationships associate concepts across the hierarchy — concepts in the same domain that are neither broader nor narrower than each other.
+
+
+
Exact, close, broad and related matches map concepts to external vocabularies. The Northwind Traders demo maps its product categories here: its
+ Beverages category is a skos:closeMatch of this thesaurus's Beverages concept, Condiments a
+ skos:broadMatch of Food , and Confections a skos:relatedMatch of Sugar .
+
+
Browse the concepts below and use parallax navigation to explore their rich semantic network.
"""^^rdf:XMLLiteral .
@@ -33,6 +50,7 @@
# View block (references the query)
<#select-concepts-view> a ldh:View ;
+ dct:title "All concepts" ;
spin:query <#select-concepts-query> .
# SELECT query
diff --git a/demo/unesco-thesaurus/install.sh b/demo/unesco-thesaurus/install.sh
index 3e5510b..858f06e 100755
--- a/demo/unesco-thesaurus/install.sh
+++ b/demo/unesco-thesaurus/install.sh
@@ -45,9 +45,9 @@ fi
./update-folder.sh "$base" "$cert_pem_file" "$cert_password" "$pwd" "$pwd" "$proxy"
-printf "\n### Installing SKOS package\n\n"
+printf "\n### Installing taxonomy editor package\n\n"
-install-package.sh -b "$base" -f "$cert_pem_file" -p "$cert_password" --proxy "$proxy" --package "https://packages.linkeddatahub.com/skos/#this"
+install-package.sh -b "$base" -f "$cert_pem_file" -p "$cert_password" --proxy "$proxy" --package "https://packages.linkeddatahub.com/editor/taxonomy/#this"
printf "\n### Importing SKOS vocabulary\n\n"
diff --git a/demo/unesco-thesaurus/layout.xsl b/demo/unesco-thesaurus/layout.xsl
deleted file mode 100644
index bea9ee2..0000000
--- a/demo/unesco-thesaurus/layout.xsl
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-]>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/demo/unesco-thesaurus/root.ttl b/demo/unesco-thesaurus/root.ttl
index 410541e..b372e67 100644
--- a/demo/unesco-thesaurus/root.ttl
+++ b/demo/unesco-thesaurus/root.ttl
@@ -21,35 +21,41 @@
<#page-header> a ldh:XHTML ;
rdf:value """
-