Skip to content

SHACL to filters

app.shacl_to_filters

Project SHACL property shapes into filter-panel widgets.

FilterOption

Bases: BaseModel

One selectable value inside a multiselect filter widget.

FilterWidget

Bases: BaseModel

One filter-panel widget derived from SHACL (UI only; not SPARQL).

get_filters_from_shacl(g, lang='en')

Build filter-panel dimensions from the SHACL property shapes.

Parameters:

Name Type Description Default
g Graph

Merged ontology graph.

required
lang str

UI language for labels and literal options.

'en'

Returns:

Type Description
list[FilterWidget]

Widgets sorted by label, including the synthetic entity-type dimension.

Source code in src/backend/app/shacl_to_filters.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def get_filters_from_shacl(g: Graph, lang: str = "en") -> list[FilterWidget]:
    """Build filter-panel dimensions from the SHACL property shapes.

    Args:
        g: Merged ontology graph.
        lang: UI language for labels and literal options.

    Returns:
        Widgets sorted by label, including the synthetic entity-type dimension.
    """
    filters: list[FilterWidget] = []

    for prop_shape in map_property_shapes(g):
        path = g.value(prop_shape, SH.path)
        if path in _SKIP_PROPS:
            continue

        datatype = g.value(prop_shape, SH.datatype)
        if datatype == XSD.anyURI:
            continue

        target_class = g.value(prop_shape, SH["class"])
        widget = _infer_widget(datatype)
        options = None
        low = high = None
        if widget == "multiselect":
            options = _multiselect_options(g, prop_shape, path, target_class, lang)
        elif widget == "slider":
            low, high = _slider_bounds(g, prop_shape, path, datatype)
        elif widget == "datepicker":
            low, high = _datepicker_bounds(g, path)

        filters.append(
            FilterWidget(
                id=local_name(str(path)),
                path=str(path),
                label=get_label(g, prop_shape, SH.name, lang),
                description=_scheme_description(g, target_class, lang),
                type=widget,
                options=options,
                min=low,
                max=high,
            )
        )

    filters.append(_entity_type_dimension(g, lang))
    return sorted(filters, key=lambda x: x.label)

_infer_widget(datatype)

Map an XSD datatype to a filter widget kind.

Parameters:

Name Type Description Default
datatype Node | None

sh:datatype value, or None.

required

Returns:

Type Description
FilterWidgetType

Widget type; defaults to multiselect.

Source code in src/backend/app/shacl_to_filters.py
119
120
121
122
123
124
125
126
127
128
def _infer_widget(datatype: Node | None) -> FilterWidgetType:
    """Map an XSD datatype to a filter widget kind.

    Args:
        datatype: ``sh:datatype`` value, or ``None``.

    Returns:
        Widget type; defaults to ``multiselect``.
    """
    return FILTER_BY_DATATYPE.get(datatype, "multiselect")

_multiselect_options(g, prop_shape, path, target_class, lang)

Collect multiselect choices from class instances, sh:in, or observed values.

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
prop_shape Node

Property-shape subject.

required
path Node

Property path URIRef.

required
target_class Node | None

sh:class constraint, if any.

required
lang str

Preferred label language.

required

Returns:

Type Description
list[FilterOption]

Options sorted by label.

Source code in src/backend/app/shacl_to_filters.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def _multiselect_options(
    g: Graph,
    prop_shape: Node,
    path: Node,
    target_class: Node | None,
    lang: str,
) -> list[FilterOption]:
    """Collect multiselect choices from class instances, sh:in, or observed values.

    Args:
        g: Ontology graph.
        prop_shape: Property-shape subject.
        path: Property path URIRef.
        target_class: ``sh:class`` constraint, if any.
        lang: Preferred label language.

    Returns:
        Options sorted by label.
    """
    in_list = g.value(prop_shape, SH["in"])
    if target_class:
        options = [_iri_option(g, s, lang) for s in g.subjects(RDF.type, target_class)]
    elif in_list is not None:
        options = [_iri_option(g, member, lang) for member in Collection(g, in_list)]
    else:
        seen: dict[str, FilterOption] = {}
        for val in g.objects(None, path):
            if isinstance(val, URIRef):
                key = str(val)
                if key not in seen:
                    seen[key] = _iri_option(g, val, lang)
            elif isinstance(val, RDFLiteral) and (
                val.language == lang or val.language is None
            ):
                key = str(val)
                if key not in seen:
                    seen[key] = FilterOption(value=key, label=key)
        options = list(seen.values())
    return sorted(options, key=lambda x: x.label)

_iri_option(g, term, lang)

Build the option selecting concept term.

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
term URIRef

Concept IRI the option selects.

required
lang str

Preferred language for label and definition.

required

Returns:

Type Description
FilterOption

The option, with description unset when the concept defines none.

Source code in src/backend/app/shacl_to_filters.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def _iri_option(g: Graph, term: URIRef, lang: str) -> FilterOption:
    """Build the option selecting concept *term*.

    Args:
        g: Ontology graph.
        term: Concept IRI the option selects.
        lang: Preferred language for label and definition.

    Returns:
        The option, with ``description`` unset when the concept defines none.
    """
    return FilterOption(
        value=str(term),
        label=get_label(g, term, RDFS.label, lang),
        description=get_definition(g, term, lang),
    )

_scheme_description(g, target_class, lang)

Return the definition of the concept scheme a dimension's options sit in.

The scheme is found through skos:inScheme of the class instances, not by naming convention. Its definition is used over the shape's sh:description because the vocabulary carries it in every language.

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
target_class Node | None

sh:class constraint, or None.

required
lang str

Preferred definition language.

required

Returns:

Type Description
str | None

The definition, or None when no instance sits in a scheme that

str | None

defines one (e.g. relations to entity classes).

Source code in src/backend/app/shacl_to_filters.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def _scheme_description(g: Graph, target_class: Node | None, lang: str) -> str | None:
    """Return the definition of the concept scheme a dimension's options sit in.

    The scheme is found through ``skos:inScheme`` of the class instances, not by
    naming convention. Its definition is used over the shape's ``sh:description``
    because the vocabulary carries it in every language.

    Args:
        g: Ontology graph.
        target_class: ``sh:class`` constraint, or ``None``.
        lang: Preferred definition language.

    Returns:
        The definition, or ``None`` when no instance sits in a scheme that
        defines one (e.g. relations to entity classes).
    """
    if target_class is None:
        return None
    for concept in g.subjects(RDF.type, target_class):
        for scheme in g.objects(concept, SKOS.inScheme):
            if isinstance(scheme, URIRef):
                definition = get_definition(g, scheme, lang)
                if definition:
                    return definition
    return None

_numeric_values(g, path)

Collect numeric objects of path that parse as floats.

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
path Node

Property path.

required

Returns:

Type Description
list[float]

Successfully parsed numeric values (may be empty).

Source code in src/backend/app/shacl_to_filters.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def _numeric_values(g: Graph, path: Node) -> list[float]:
    """Collect numeric objects of *path* that parse as floats.

    Args:
        g: Ontology graph.
        path: Property path.

    Returns:
        Successfully parsed numeric values (may be empty).
    """
    values = []
    for value in g.objects(None, path):
        try:
            values.append(float(value))
        except (TypeError, ValueError):
            continue
    return values

_slider_bounds(g, prop_shape, path, datatype)

Compute min/max for a slider from SHACL bounds or observed values.

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
prop_shape Node

Property-shape subject.

required
path Node

Property path.

required
datatype Node | None

XSD datatype (affects gYear defaults).

required

Returns:

Type Description
tuple[float | int, float | int]

(min, max).

Source code in src/backend/app/shacl_to_filters.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def _slider_bounds(
    g: Graph, prop_shape: Node, path: Node, datatype: Node | None
) -> tuple[float | int, float | int]:
    """Compute min/max for a slider from SHACL bounds or observed values.

    Args:
        g: Ontology graph.
        prop_shape: Property-shape subject.
        path: Property path.
        datatype: XSD datatype (affects gYear defaults).

    Returns:
        ``(min, max)``.
    """
    values = _numeric_values(g, path)
    if datatype == XSD.gYear:
        return (min(values) if values else 1900, max(values) if values else 2026)
    return (
        int(g.value(prop_shape, SH.minInclusive) or (min(values) if values else 0)),
        int(g.value(prop_shape, SH.maxInclusive) or (max(values) if values else 1000)),
    )

_datepicker_bounds(g, path)

Compute min/max ISO date strings from observed values.

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
path Node

Property path.

required

Returns:

Type Description
tuple[str, str]

(min, max) date strings.

Source code in src/backend/app/shacl_to_filters.py
259
260
261
262
263
264
265
266
267
268
269
270
def _datepicker_bounds(g: Graph, path: Node) -> tuple[str, str]:
    """Compute min/max ISO date strings from observed values.

    Args:
        g: Ontology graph.
        path: Property path.

    Returns:
        ``(min, max)`` date strings.
    """
    dates = sorted(str(v) for v in g.objects(None, path) if str(v))
    return (dates[0], dates[-1]) if dates else ("2000-01-01", "2026-12-31")

_entity_type_dimension(g, lang)

Build the entity-type multiselect over the filterable pin classes.

Parameters:

Name Type Description Default
g Graph

Ontology graph (for class labels).

required
lang str

UI language.

required

Returns:

Type Description
FilterWidget

Synthetic entityType widget over rdf:type.

Source code in src/backend/app/shacl_to_filters.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def _entity_type_dimension(g: Graph, lang: str) -> FilterWidget:
    """Build the entity-type multiselect over the filterable pin classes.

    Args:
        g: Ontology graph (for class labels).
        lang: UI language.

    Returns:
        Synthetic ``entityType`` widget over ``rdf:type``.
    """
    type_classes = [COMPASS[name] for name in sorted(FILTERABLE_PIN_CLASSES)]
    return FilterWidget(
        id=ENTITY_TYPE_ID,
        path=str(RDF.type),
        label="Entity Type" if lang == "en" else "Eintragsart",
        type="multiselect",
        options=[
            FilterOption(value=str(cls), label=get_label(g, cls, RDFS.label, lang))
            for cls in type_classes
        ],
    )