|
| 1 | +--- |
| 2 | +id: range-facet |
| 3 | +title: RangeFacet |
| 4 | +--- |
| 5 | + |
| 6 | +`RangeFacet` renders a histogram, slider, and optional default/custom ranges for a numeric year range. |
| 7 | + |
| 8 | +It reads buckets from the `results` aggregations and stores the selected range in the query `filters`. |
| 9 | + |
| 10 | +## Usage |
| 11 | + |
| 12 | +```jsx |
| 13 | +<RangeFacet |
| 14 | + title="Publication Year" |
| 15 | + agg={{ aggName: "years" }} |
| 16 | + rangeSeparator=".." |
| 17 | + // optional |
| 18 | + defaultRanges={[ |
| 19 | + { label: "Last 1 year", type: "years", value: 1 }, |
| 20 | + { label: "Last 5 years", type: "years", value: 5 }, |
| 21 | + { label: "Last 6 months", type: "months", value: 6 }, |
| 22 | + ]} |
| 23 | + enableCustomRange |
| 24 | + // customize custom-range labels and placeholders |
| 25 | + customDatesLabel="Pick dates" |
| 26 | + dateRangeToLabel="–" |
| 27 | + datePlaceholders={{ YYYY: "Year", MM: "Mo", DD: "Day" }} |
| 28 | +/> |
| 29 | +``` |
| 30 | + |
| 31 | +## Aggregation format |
| 32 | + |
| 33 | +The component expects the aggregation result under `results.aggregations[aggName].buckets` |
| 34 | +with a numeric `key` (or `key_as_string`) and `doc_count`: |
| 35 | + |
| 36 | +```json |
| 37 | +{ |
| 38 | + "years": { |
| 39 | + "buckets": [ |
| 40 | + { "key": 2021, "doc_count": 12 }, |
| 41 | + { "key": 2022, "doc_count": 7 } |
| 42 | + ] |
| 43 | + } |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +## Filters format |
| 48 | + |
| 49 | +The selected filter is stored as `[ "<aggName>", "<from><rangeSeparator><to>" ]`. |
| 50 | +`<from>` and `<to>` can be `YYYY` or full ISO dates (`YYYY-MM-DD`) when custom ranges |
| 51 | +include months/days. |
| 52 | + |
| 53 | +## Props |
| 54 | + |
| 55 | +* **title** `String` |
| 56 | + |
| 57 | + The title rendered in the card header. |
| 58 | + |
| 59 | +* **agg** `Object` |
| 60 | + |
| 61 | + An object that describes the aggregation to read from `results`: |
| 62 | + |
| 63 | + * **aggName** `String`: the aggregation name to look for in `results` |
| 64 | + |
| 65 | +* **rangeSeparator** `String` |
| 66 | + |
| 67 | + The separator used when building the filter value (for example `..` or `--`). |
| 68 | + |
| 69 | +* **defaultRanges** `Array` *optional* |
| 70 | + |
| 71 | + Default ranges rendered as checkboxes. Each entry should have: |
| 72 | + |
| 73 | + * **label** `String` |
| 74 | + * **type** `String`: `"years"` or `"months"` |
| 75 | + * **value** `Number`: number of years/months back |
| 76 | + |
| 77 | +* **enableCustomRange** `Boolean` *optional* |
| 78 | + |
| 79 | + When `true`, enables the custom date range checkbox with optional month/day inputs. |
| 80 | + |
| 81 | +* **customDatesLabel** `String` *optional* |
| 82 | + |
| 83 | + Label for the button that expands the custom date inputs (year/month/day). Default: `"Custom Dates"`. |
| 84 | + |
| 85 | +* **dateRangeToLabel** `String` *optional* |
| 86 | + |
| 87 | + Text shown between the "from" and "to" date inputs. Default: `"to"`. |
| 88 | + |
| 89 | +* **datePlaceholders** `Object` *optional* |
| 90 | + |
| 91 | + Placeholder text for the date input fields. Only keys you provide are used; others keep their defaults. Keys: `YYYY`, `MM`, `DD`. Example: `{ YYYY: "Year", MM: "Mo", DD: "Day" }`. Default placeholders when not set: `"YYYY"`, `"MM"`, `"DD"`. |
| 92 | + |
| 93 | +* **histogramHeight** `Number` *optional* |
| 94 | + |
| 95 | + Height in pixels for the histogram area. |
| 96 | + |
| 97 | +* **overridableId** `String` *optional* |
| 98 | + |
| 99 | + Optional string to define a specific overridable id. |
| 100 | + |
| 101 | +## Usage when overriding |
| 102 | + |
| 103 | +### Overriding full component |
| 104 | + |
| 105 | +Use this when you want to replace the whole `RangeFacet` logic and UI. |
| 106 | + |
| 107 | +Brief flow (matches the default behavior): |
| 108 | + |
| 109 | +1. Read buckets from `currentResultsState.data.aggregations[agg.aggName].buckets`. |
| 110 | +2. Compute `min/max` years from bucket keys. |
| 111 | +3. Compute the histogram data with aggregation keys and doc counts. |
| 112 | +4. Store the selected range in `currentQueryState.filters` as: |
| 113 | + `[agg.aggName, `${from}${rangeSeparator}${to}`]`. |
| 114 | +5. Render your own UI (histogram/slider/etc). |
| 115 | + |
| 116 | +Example with only histogram and custom filter facet: |
| 117 | + |
| 118 | +```jsx |
| 119 | +class MyRangeFacet extends React.Component { |
| 120 | + constructor(props) { |
| 121 | + super(props); |
| 122 | + const { min, max } = this.getMinMax(); |
| 123 | + this.state = { range: [min, max], activeFilter: null }; |
| 124 | + } |
| 125 | + |
| 126 | + applyRange = (from, to) => { |
| 127 | + // Apply the filter |
| 128 | + }; |
| 129 | + |
| 130 | + onBarClick = (year) => { |
| 131 | + this.applyRange(year, year); |
| 132 | + }; |
| 133 | + |
| 134 | + onClear = () => { |
| 135 | + // Clear the filter |
| 136 | + }; |
| 137 | + |
| 138 | + getBuckets = () => { |
| 139 | + const { currentResultsState, agg } = this.props; |
| 140 | + const resultsAggregations = currentResultsState?.data?.aggregations; |
| 141 | + return resultsAggregations?.[agg.aggName]?.buckets ?? []; |
| 142 | + }; |
| 143 | + |
| 144 | + render() { |
| 145 | + const { title, agg, rangeSeparator, currentQueryState, histogramHeight } = |
| 146 | + this.props; |
| 147 | + const { min, max } = this.getMinMax(); |
| 148 | + const hasActiveFilter; |
| 149 | + // Extract the buckets in aggregations |
| 150 | + const buckets = getBuckets(); |
| 151 | + // Active filter range: [from(year), to(year)] |
| 152 | + const { range } = this.state |
| 153 | + |
| 154 | + const containerCmp = ( |
| 155 | + <> |
| 156 | + <RangeHistogram |
| 157 | + data={ |
| 158 | + // Extract key and doc_count from agg into a map, then fill every year with the count. |
| 159 | + getHistogramData(buckets, min, max) |
| 160 | + } |
| 161 | + range={range} |
| 162 | + height={histogramHeight} |
| 163 | + onBarClick={this.onBarClick} |
| 164 | + /> |
| 165 | + <RangeCustomFilter |
| 166 | + min={min} |
| 167 | + max={max} |
| 168 | + value={range} |
| 169 | + rangeSeparator={rangeSeparator} |
| 170 | + activeMode={RANGE_MODES.CUSTOM} |
| 171 | + activeFilter={this.state.activeFilter} |
| 172 | + onApply={(r, s) => this.applyRange(r[0], r[1])} |
| 173 | + onClear={this.onClear} |
| 174 | + /> |
| 175 | + </> |
| 176 | + ); |
| 177 | + |
| 178 | + return ( |
| 179 | + <RangeFacetElement |
| 180 | + title={title} |
| 181 | + containerCmp={containerCmp} |
| 182 | + hasActiveFilter={hasActiveFilter} |
| 183 | + onClear={this.onClear} |
| 184 | + /> |
| 185 | + ); |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +const overriddenComponents = { |
| 190 | + RangeFacet: MyRangeFacet, |
| 191 | +}; |
| 192 | +``` |
| 193 | +
|
| 194 | +### Overriding the element |
| 195 | +
|
| 196 | +Wraps the default content and lets you customize the header or layout while keeping |
| 197 | +the built-in histogram/slider/custom filters. |
| 198 | +
|
| 199 | +```jsx |
| 200 | +const MyRangeFacetElement = ({ title, containerCmp, hasActiveFilter, onClear }) => { |
| 201 | + return ( |
| 202 | + <section> |
| 203 | + <header> |
| 204 | + <strong>{title}</strong> |
| 205 | + {hasActiveFilter && ( |
| 206 | + <button type="button" onClick={onClear}> |
| 207 | + Clear |
| 208 | + </button> |
| 209 | + )} |
| 210 | + </header> |
| 211 | + {containerCmp} |
| 212 | + </section> |
| 213 | + ); |
| 214 | +}; |
| 215 | + |
| 216 | +const overriddenComponents = { |
| 217 | + "RangeFacet.element": MyRangeFacetElement, |
| 218 | +}; |
| 219 | +``` |
| 220 | +
|
| 221 | +### Overriding the histogram |
| 222 | +
|
| 223 | +Replace the histogram visualization. Use `data` and `range` to show counts and |
| 224 | +highlight the selected years. Call `onBarClick(year)` to update the range. |
| 225 | +
|
| 226 | +```jsx |
| 227 | +const MyRangeHistogram = ({ data, range, onBarClick }) => { |
| 228 | + return ( |
| 229 | + <ul> |
| 230 | + {data.map((item) => { |
| 231 | + ... |
| 232 | + })} |
| 233 | + </ul> |
| 234 | + ); |
| 235 | +}; |
| 236 | + |
| 237 | +const overriddenComponents = { |
| 238 | + "RangeFacet.Histogram.element": MyRangeHistogram, |
| 239 | +}; |
| 240 | +``` |
| 241 | +
|
| 242 | +#### Tooltip override (histogram) |
| 243 | +
|
| 244 | +The histogram tooltip is its own overridable element. The `tooltip` value has the |
| 245 | +shape `{ year, count, x, y }` where `x/y` are viewport coordinates used to position |
| 246 | +the tooltip. |
| 247 | +
|
| 248 | +```jsx |
| 249 | +const MyHistogramTooltip = ({ tooltip }) => { |
| 250 | + if (!tooltip) return null; |
| 251 | + return ( |
| 252 | + <div> |
| 253 | + {tooltip.year}: {tooltip.count} |
| 254 | + </div> |
| 255 | + ); |
| 256 | +}; |
| 257 | + |
| 258 | +const overriddenComponents = { |
| 259 | + "RangeFacet.Histogram.Tooltip.element": MyHistogramTooltip, |
| 260 | +}; |
| 261 | +``` |
| 262 | +
|
| 263 | +### Overriding the slider |
| 264 | +
|
| 265 | +Replace the range slider. Call `onChange([min, max])` when the selected range changes. |
| 266 | +
|
| 267 | +```jsx |
| 268 | +const MyRangeSlider = ({ min, max, value, onChange }) => { |
| 269 | + ... |
| 270 | +}; |
| 271 | + |
| 272 | +const overriddenComponents = { |
| 273 | + "RangeFacet.Slider.element": MyRangeSlider, |
| 274 | +}; |
| 275 | +``` |
| 276 | +
|
| 277 | +### Overriding the default filters |
| 278 | +
|
| 279 | +Replace the default ranges list. Call `onToggle(range, checked)` to select or clear |
| 280 | +a range option. |
| 281 | +
|
| 282 | +```jsx |
| 283 | +const MyDefaultFilters = ({ ranges, activeLabel, onToggle }) => { |
| 284 | + ... |
| 285 | +}; |
| 286 | + |
| 287 | +const overriddenComponents = { |
| 288 | + "RangeFacet.DefaultFilters.element": MyDefaultFilters, |
| 289 | +}; |
| 290 | +``` |
| 291 | +
|
| 292 | +### Overriding the custom filter |
| 293 | +
|
| 294 | +Replace the custom range checkbox and inputs wrapper. Use `checked/expanded` to |
| 295 | +control layout and render `dateError` when validation fails. |
| 296 | +
|
| 297 | +```jsx |
| 298 | +const MyCustomFilter = ({ checked, expanded, dateError, activeFilter, children }) => { |
| 299 | + ... |
| 300 | +}; |
| 301 | + |
| 302 | +const overriddenComponents = { |
| 303 | + "RangeFacet.CustomFilter.element": MyCustomFilter, |
| 304 | +}; |
| 305 | +``` |
| 306 | +
|
| 307 | +### Overriding the date range inputs (for custom filter) |
| 308 | +
|
| 309 | +Replace the date input layout for the custom filter. Use `format` to decide which |
| 310 | +parts to render and `values` to populate them. |
| 311 | +
|
| 312 | +```jsx |
| 313 | +const MyDateRangeInputs = ({ format, values, disabled, children }) => { |
| 314 | + ... |
| 315 | +}; |
| 316 | + |
| 317 | +const overriddenComponents = { |
| 318 | + "RangeFacet.DateInputs.Layout": MyDateRangeInputs, |
| 319 | +}; |
| 320 | +``` |
| 321 | +
|
| 322 | +`format` controls the input structure: |
| 323 | +
|
| 324 | +- `YYYY` renders a single row with year-only inputs. |
| 325 | +- `YYYY-MM` and `YYYY-MM-DD` render stacked rows for from/to values. |
| 326 | +
|
| 327 | +### RangeFacet parameters |
| 328 | +
|
| 329 | +Component that wraps the histogram, slider, and optional filters. |
| 330 | +
|
| 331 | +* **title** `String` |
| 332 | +
|
| 333 | + The title to render. |
| 334 | +
|
| 335 | +* **containerCmp** `React component` |
| 336 | +
|
| 337 | + The rendered content (histogram, slider, and filters). |
| 338 | +
|
| 339 | +* **hasActiveFilter** `Boolean` |
| 340 | +
|
| 341 | + `true` if a range filter is active, `false` otherwise. |
| 342 | +
|
| 343 | +* **onClear** `Function` |
| 344 | +
|
| 345 | + Function to call to clear the active range filter. |
| 346 | +
|
| 347 | +## Callback methods used by overrides |
| 348 | +
|
| 349 | +* **onClear** `Function` |
| 350 | +
|
| 351 | + Clears the active range filter and resets to the full available range. |
| 352 | +
|
| 353 | +* **onBarClick** `Function` |
| 354 | +
|
| 355 | + Accepts a `year` and updates the range to that single year. |
| 356 | +
|
| 357 | +* **onChange** `Function` |
| 358 | +
|
| 359 | + Accepts `[from, to]` to update the selected range (used by slider overrides). |
| 360 | +
|
| 361 | +* **onToggle** `Function` |
| 362 | +
|
| 363 | + Accepts `(range, checked)` to select a default range or clear it. |
| 364 | +
|
| 365 | +## Utilities |
| 366 | +
|
| 367 | +Helpers from `src/lib/components/RangeFacet/utils.js` used by `RangeFacet` and its sub-components. |
| 368 | +
|
| 369 | +* **extractBuckets(resultsAggregations, aggName)**: read aggregation buckets safely. |
| 370 | +* **getKey(bucket)**: returns `key_as_string` when present, otherwise `key`. |
| 371 | +* **getHistogramData(buckets, min, max)**: build continuous year data with counts. |
| 372 | +* **resolveDefaultRange(range, min, max, rangeSeparator)**: compute the year range |
| 373 | + and optional ISO date range for a default option. |
| 374 | +* **normalizeFilterValue(filterValue, rangeSeparator, minYear, maxYear)**: sanitize |
| 375 | + and normalize a filter string like `YYYY..YYYY` or ISO dates. |
| 376 | +* **parseFilterYears(filterValue, rangeSeparator)**: parse years from a filter |
| 377 | + string (supports ISO dates). |
| 378 | +* **findDefaultLabel(defaultRanges, filterValue, min, max, rangeSeparator)**: |
| 379 | + match an active filter to a default range label. |
| 380 | +* **buildDateRange({ fromYear, fromMonth, fromDay, toYear, toMonth, toDay, rangeSeparator })**: |
| 381 | + build a `YYYY..YYYY` or ISO `YYYY-MM-DD..YYYY-MM-DD` string. |
| 382 | +* **RANGE_MODES**: enum with `DEFAULT` and `CUSTOM` used for optional default and custom filters. |
0 commit comments