Skip to content

SHACL → filters

app.shacl_to_filters

SHACL → filter panel widgets (multiselect, slider, datepicker, toggle).

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
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 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
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 property in get_shacl_property(g):
        path = g.value(property, SH.path)
        if path in _SKIP_PROPS:
            continue

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

        target_class = g.value(property, SH["class"])
        sh_in_list = list(g.objects(property, SH["in"]))
        path_str = str(path)
        local_name = path_str.rsplit("#", maxsplit=1)[-1].rsplit("/", maxsplit=1)[-1]

        widget = _infer_widget(datatype)
        options = None
        min_v = max_v = None
        if widget == "multiselect":
            options = _multiselect_options(g, path, target_class, sh_in_list, lang)
        elif widget == "slider":
            bounds = _slider_bounds(g, property, path, datatype)
            min_v, max_v = bounds["min"], bounds["max"]
        elif widget == "datepicker":
            bounds = _datepicker_bounds(g, path)
            min_v, max_v = bounds["min"], bounds["max"]

        filters.append(
            FilterWidget(
                id=local_name,
                path=path_str,
                label=get_shacl_label(g, property, SH.name, lang),
                type=widget,
                order=0,
                options=options,
                min=min_v,
                max=max_v,
            )
        )

    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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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``.
    """
    if datatype in {XSD.integer, XSD.float, XSD.gYear}:
        return "slider"
    if datatype == XSD.date:
        return "datepicker"
    if datatype == XSD.boolean:
        return "toggle"
    return "multiselect"

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

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

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
path Node

Property path URIRef.

required
target_class Node | None

sh:class constraint, if any.

required
sh_in_list list[Node]

Objects of sh:in, 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
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def _multiselect_options(
    g: Graph,
    path: Node,
    target_class: Node | None,
    sh_in_list: list[Node],
    lang: str,
) -> list[FilterOption]:
    """Collect multiselect choices from class instances, sh:in, or observed values.

    Args:
        g: Ontology graph.
        path: Property path URIRef.
        target_class: ``sh:class`` constraint, if any.
        sh_in_list: Objects of ``sh:in``, if any.
        lang: Preferred label language.

    Returns:
        Options sorted by label.
    """
    options: list[FilterOption] = []
    if target_class:
        for s in g.subjects(RDF.type, target_class):
            options.append(
                FilterOption(value=str(s), label=get_shacl_label(g, s, RDFS.label, lang))
            )
    elif sh_in_list:
        for member in Collection(g, sh_in_list[0]):
            options.append(
                FilterOption(
                    value=str(member),
                    label=get_shacl_label(g, member, RDFS.label, lang),
                )
            )
    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] = FilterOption(
                        value=key,
                        label=get_shacl_label(g, val, RDFS.label, 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)

_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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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, property, path, datatype)

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

Parameters:

Name Type Description Default
g Graph

Ontology graph.

required
property Node

Property-shape subject.

required
path Node

Property path.

required
datatype Node | None

XSD datatype (affects gYear defaults).

required

Returns:

Type Description
dict[str, float | int]

Dict with min and max keys.

Source code in src/backend/app/shacl_to_filters.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def _slider_bounds(
    g: Graph, property: Node, path: Node, datatype: Node | None
) -> dict[str, float | int]:
    """Compute min/max for a slider from SHACL bounds or observed values.

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

    Returns:
        Dict with ``min`` and ``max`` keys.
    """
    vals = _numeric_values(g, path)
    if datatype == XSD.gYear:
        return {"min": min(vals) if vals else 1900, "max": max(vals) if vals else 2026}
    return {
        "min": int(g.value(property, SH.minInclusive) or (min(vals) if vals else 0)),
        "max": int(g.value(property, SH.maxInclusive) or (max(vals) if vals 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
dict[str, str]

Dict with min and max date strings.

Source code in src/backend/app/shacl_to_filters.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def _datepicker_bounds(g: Graph, path: Node) -> dict[str, str]:
    """Compute min/max ISO date strings from observed values.

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

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

_entity_type_dimension(g, lang)

Build the entity-type multiselect for the four Compass 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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def _entity_type_dimension(g: Graph, lang: str) -> FilterWidget:
    """Build the entity-type multiselect for the four Compass pin classes.

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

    Returns:
        Synthetic ``entityType`` widget over ``rdf:type``.
    """
    type_classes = [
        COMPASS.InternationalForum,
        COMPASS.Network,
        COMPASS.PartnerOrganization,
        COMPASS.Project,
    ]
    return FilterWidget(
        id="entityType",
        path=str(RDF.type),
        label="Entity Type" if lang == "en" else "Eintragsart",
        type="multiselect",
        order=0,
        options=[
            FilterOption(value=str(cls), label=get_shacl_label(g, cls, RDFS.label, lang))
            for cls in type_classes
        ],
    )