Skip to content

Material Dropdown Grouping

Prayas Lashkari edited this page Aug 16, 2026 · 2 revisions

Material Dropdown Grouping

How the Material filter dropdown fetches its options and organizes them into high-level categories.

See PR #24.

Overview

The Material dropdown is a multi-select rendered by FlatSelect in SampleFilters.tsx. Its options and counts are fetched live via SPARQL — not hardcoded. Each material type is grouped under one of four high-level buckets, with an Other catch-all:

  • Water
  • Biota
  • Solid Material
  • Air
  • Other

Data flow

Layer File Role
UI src/components/QueryEditor/SampleFilters.tsx Renders FlatSelect, maps material types → options
Render src/components/QueryEditor/FlatSelect/FlatSelect.tsx Grouped render with sticky section headers
Hook src/hooks/useDiscoveryQueries.ts (useMaterialTypes) Runs the query, maps bucketPrio → group
Query src/engine/templates/regions.ts (buildDiscoverMaterialTypesQuery) Builds the SPARQL
Client src/engine/sparqlClient.ts POSTs the query
Constants src/constants/materialTypes.ts MaterialType.group, MATERIAL_GROUP_BY_PRIO, fallback list

Endpoints (src/constants/endpoints.ts):

  • https://frink.apps.renci.org/sawgraph/sparql — default (no region filter)
  • https://frink.apps.renci.org/federation/sparql — when a region/state is selected

The query

Each material type is bucketed graph-side. A priority VALUES block binds every bucket a type matches; MIN(?prio) picks the winner. Unmatched types have no ?bucketPrio and fall to Other.

PREFIX rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX coso: <http://w3id.org/coso/v1/contaminoso#>

SELECT ?matType (SAMPLE(?_label) AS ?label)
       (COUNT(DISTINCT ?observation) AS ?num)
       (MIN(?prio) AS ?bucketPrio) WHERE {
  ?observation rdf:type coso:ContaminantObservation ;
               coso:analyzedSample ?sample .
  ?sample coso:sampleOfMaterialType ?matType .
  OPTIONAL {
    ?sample rdf:type ?bucketClass .
    VALUES (?bucketClass ?prio) {
      (coso:BiotaSample 1)
      (coso:SolidMaterialSample 2)
      (coso:WaterSample 3)
      (coso:AirSample 4)
    }
  }
  OPTIONAL { ?matType rdfs:label ?_label . }
  FILTER(STRSTARTS(STR(?matType), "http://w3id.org/"))
} GROUP BY ?matType
ORDER BY DESC(?num) ?label

The region-filtered branch adds a SamplePoint region pattern before the observation triples but keeps the same VALUES block, so grouping works with or without a region selected.

Why these four buckets

They are the direct children of coso:MaterialSample in the ontology, so they partition the space at the top tier:

sosa:Sample
└─ coso:MaterialSample        (catch-all root — matches all types, NOT a bucket)
   ├─ coso:AirSample
   ├─ coso:BiotaSample        → Animal* (Blood, Milk, Organ, Tissue), Plant*
   ├─ coso:SolidMaterialSample → Sediment, Soil, SolidWaste
   └─ coso:WaterSample         → BySource (Drinking/Ground/Surface/Waste), ByTreatment (Filtered/Raw/Treated)

MaterialSample itself is the catch-all root (matches all 151 types) and is intentionally not used as a bucket.

• cosoMaterialSample

Decisions & known quirks

Priority tiebreaker (Biota > Solid > Water > Air)

Data isn't perfectly clean — ~11 material types are sampled under more than one bucket (e.g. SEDIMENT, SOIL, MILK, BEEF, MANURE).

MIN(?bucketPrio) resolves these deterministically. A single global order can't satisfy every case — e.g. BEEF and MANURE are both Biota-vs-Solid ties but arguably want opposite buckets — so one or two assignments are debatable rather than wrong.

The MANURE case (region-dependent)

Bucketing follows whatever samples are in scope:

  • Unfiltered — MANURE's observation-samples include BiotaSample, so it groups under Biota.
  • Region filter applied — its in-scope samples are only SolidMaterialSample, so it groups under Solid Material.

Both are correct for their data slice. No override is hardcoded.

"Other" leakage (accepted)

Some types fall to Other because their observation-samples carry none of the four subclasses (only the MaterialSample root or a WQP media type): STORM WATER RUNOFF, PROCESS WATER, FEED, COMPOST, FRUIT, GRASS, etc. This is the honest graph-driven result. Pulling them in would mean extending the VALUES set (e.g. WQP SampleMedia) or adding a label-keyword fallback — deferred.

Single-level grouping only

The ontology nests deeper (Water › BySource/ByTreatment). FlatSelect stays flat with one tier of sticky headers; sub-sections can be added later if needed.

Fallback

If the query fails or returns nothing, the hook uses FALLBACK_MATERIAL_TYPES from src/constants/materialTypes.ts (ungrouped).

Clone this wiki locally