Skip to content

facets: add year range facet#289

Merged
zzacharo merged 1 commit intoinveniosoftware:masterfrom
zubeydecivelek:date-facet
Feb 11, 2026
Merged

facets: add year range facet#289
zzacharo merged 1 commit intoinveniosoftware:masterfrom
zubeydecivelek:date-facet

Conversation

@zubeydecivelek
Copy link
Contributor

@zubeydecivelek zubeydecivelek commented Jan 21, 2026

Description

RFC: https://github.com/inveniosoftware/rfcs/blob/1f7ec435f144226e2463c38ffe899869a944eb15/rfcs/rdm-0100-facets.md#date-facets
Codimd for UI and dependency comparison: https://codimd.web.cern.ch/O2LVmzZAQduLmBNJVRUiVg
Similar implementation on Open Data: cernopendata/cernopendata-portal#171

New RangeFacet component implemented to support year/range filtering.
RangeFacet container includes:

  • Histogram (Implemented with @visx/scale, @visx/shape)
  • RangeSlider for selecting min/max (Almost same with opendata implementation with small changes, thanks to @dianarand)
    Optional filters:
  • RangeQuickFilters for common presets (Last 5 years/Last 6 month)
  • RangeCustomFilter for manual input (YYYY-MM-DD to YYYY-MM-DD)

RangeFacet usage example:

<RangeFacet
  title="Year"
  agg={{ aggName: "years" }}
  rangeSeparator=".."
  // optional
  defaultRanges={[
    { label: "Last 1 year", type: "years", value: 1 },
    { label: "Last 5 years", type: "years", value: 5 },
    { label: "Last 6 months", type: "months", value: 6 },
  ]}
  // optional 
  enableCustomRange 
/>
Screenshot 2026-01-20 at 10 45 17 Screenshot 2026-01-20 at 10 46 04 Screenshot 2026-01-20 at 10 46 25 Screenshot 2026-01-20 at 10 46 47 Screenshot 2026-01-20 at 10 51 38 Screenshot 2026-01-20 at 10 52 13

Checklist

Ticks in all boxes and 🟢 on all GitHub actions status checks are required to merge:

Frontend

Reminder

By using GitHub, you have already agreed to the GitHub’s Terms of Service including that:

  1. You license your contribution under the same terms as the current repository’s license.
  2. You agree that you have the right to license your contribution under the current repository’s license.

@zubeydecivelek zubeydecivelek force-pushed the date-facet branch 2 times, most recently from 1572dd1 to 8a9c9c9 Compare January 21, 2026 16:53
Copy link
Member

@zzacharo zzacharo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general to follow some consistent pattern with the rest of the codebase here, you can have a look in BuggetAggregration component and see how we separate the Element from wrapper component.

@zubeydecivelek zubeydecivelek force-pushed the date-facet branch 2 times, most recently from c7aec02 to 6d7266d Compare January 22, 2026 14:21
@zubeydecivelek zubeydecivelek linked an issue Jan 22, 2026 that may be closed by this pull request
const getPartConfig = (part) => {
switch (part) {
case "YYYY":
return { placeholder: "YYYY", maxLength: 4, width: "8.5ch" };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does 8.5ch translate to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8.5ch is about 8.5 characters wide so enough space for YYYY

};

DateRangeInputs.defaultProps = {
overridableId: "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about providing the empty string as a default... wouldn't it be better not to have the prop at all or to make it required? Otherwise we can cause some wrongly built overridableIDs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the same for all the overridable classes in the repo, see. Should we keep like that or change?

Comment on lines 122 to 127
return this.setError(`Year must be between ${min} and ${max}`);
}
// Month bounds
if (from.m < 1 || from.m > 12 || to.m < 1 || to.m > 12) {
return this.setError("Month must be between 1 and 12");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it be better to use a date handling library to validate it? ping @zzacharo for opinion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found these libraries if we want to use :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have quite some experience with luxon, and we use it in InvenioRDM.

dateError={dateError}
>
<div className="range-custom-filter">
{!expanded ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what does expanded do, could you describe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for displaying the full date after clicking the Custom Dates button
Screenshot 2026-01-23 at 11 53 20

Screenshot 2026-01-23 at 11 53 29

if (checked) {
this.setState({ activeLabel: null });
onClear();
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is return needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes otherwise we can't uncheck it. Or below code can be in else block

const areLabelsClose = minVal !== maxVal && diff < 20;
const areLabelsOverlapping = minVal !== maxVal && diff < 10;

const labelPlacement = (percentage) => percentage + ((-3 * percentage) / 50 + 3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a comment in the code explaining the math? why we have -3 for example, for a person first time reading, it is hard to figure out

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the comment, it would be great if you can check :)

<div className="slider-track" />
<div
className="slider-range"
style={{ left: `${minPercent}%`, width: `${diff}%` }}
Copy link
Contributor

@kpsherva kpsherva Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove all inline styling, it is not a best practice (see in RDM docs). Sometimes it will cause errors if an instance has restritive CSP

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zubeydecivelek can you explain here why do we need dynamic styling? Maybe with @kpsherva we can find a better way to handle this :)

Copy link
Contributor Author

@zubeydecivelek zubeydecivelek Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selected range and label positions depend on the slider values and it's recalculates on every change.

Slider component is not written from scratch, it is based on the existing opendata implementation with minimal changes.

style={{ left: `${minPercent}%`, width: `${diff}%` }}
/>

{isDegenerate ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the variable name is quite interesting... what does it mean ? :)

Copy link
Contributor Author

@zubeydecivelek zubeydecivelek Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it means range collapses to a single point, in case the aggregation has only one value. I changed the variable name to isSingleValue

@zubeydecivelek zubeydecivelek force-pushed the date-facet branch 2 times, most recently from 1c1e4fc to 0dab5ad Compare January 27, 2026 14:07
Copy link
Contributor

@ntarocco ntarocco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Massive amount of work. From a quick review, it looks good to me. However I did not test it.

Do I ask too much if you could update the doc and example apps in this repo too? They will be really helpful for users testing this library out (you can even test it live with Zenodo/CDS Videos).

const { value, min, max } = this.props;
const { checked } = this.state;

if (!checked) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this condition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be able watch the filter even if it's not checked but it's not needed I'm removing it thanks :)

Comment on lines 122 to 127
return this.setError(`Year must be between ${min} and ${max}`);
}
// Month bounds
if (from.m < 1 || from.m > 12 || to.m < 1 || to.m > 12) {
return this.setError("Month must be between 1 and 12");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have quite some experience with luxon, and we use it in InvenioRDM.

@zubeydecivelek
Copy link
Contributor Author

@ntarocco thank you for reviewing!

Do I ask too much if you could update the doc and example apps in this repo too? They will be really helpful for users testing this library out (you can even test it live with Zenodo/CDS Videos).

Example app update is ready (for opensearch and cern-videos demos), I'll push the code after the facet :) and I'll update the doc thanks!

@zubeydecivelek
Copy link
Contributor Author

zubeydecivelek commented Feb 2, 2026

Clear button removed, to override it in invenio-search-ui. See here
Image
Screenshot 2026-01-29 at 12 04 14

Should we still keep it in react-searchkit?

...currentQueryState,
filters: [...filters, [agg.aggName, filterValue]],
});
}, 500);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should we have the delay parameter configurable as well or would it be an overkill?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense, but I’d prefer to keep it fixed for now. It’s internal performance detail rather than a user facing behavior. We can make it parameter if it's needed in the future.

Copy link

@JakobMiesner JakobMiesner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partial review together with @palkerecsenyi

return `${from.toISODate()}${rangeSeparator}${to.toISODate()}`;
}

export const RANGE_MODES = Object.freeze({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit and probably out of scope: I think the active mode and RANGE_MODES could be avoided by only passing the current value and updateValue from RangeFacet to the custom and default filters.
On render they can check is props.value == myExpectedValue (e.g. [2025, 2025] for "last 1 Year" filter) and be checked in that case and unchecked otherwise. And when clicked they do props.updateValue(myExpectedValue)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Yes it can be removed, but in this case default filter and custom filter will be checked. It might be better because user will see the exact filter. What do you think? @zzacharo @kpsherva
Screenshot 2026-02-04 at 16 56 55

onClick={() => this.setState({ expanded: true })}
className="searchkit-daterange-custom-toggle"
>
Custom Dates
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have i18n support in this library?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zzacharo Should we add invenio-i18n to react-searchkit?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot because it is a python module... in priciple the user facing labels we expose them as props and we translate the values when we call the component. See for example here the requests searchbar

Copy link
Contributor Author

@zubeydecivelek zubeydecivelek Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added them as props, so we can use i18n in invenio-search-ui. See the commit:42ff73e
@zzacharo @palkerecsenyi

Since we're not enabling the custom filters, do you think we still need to add the label customization in invenio-search-ui or should we enable everything as default?

@zubeydecivelek zubeydecivelek force-pushed the date-facet branch 3 times, most recently from d251dbc to c4d2160 Compare February 11, 2026 15:12
@zzacharo zzacharo merged commit a181ac4 into inveniosoftware:master Feb 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Group records by year add date range facet component

8 participants