diff --git a/src/client/components/facet_bar/ActiveFilters.js b/src/client/components/facet_bar/ActiveFilters.js index f5d9855c77e5cd24a95056b111d6d1f8a8b4e04a..6b42b43ec9325196adb9417a24afb0d9528a73cc 100644 --- a/src/client/components/facet_bar/ActiveFilters.js +++ b/src/client/components/facet_bar/ActiveFilters.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ChipsArray from './ChipsArray'; const ActiveFilters = props => { - const { uriFilters, textFilters, timespanFilters, facets, someFacetIsFetching } = props; + const { uriFilters, textFilters, timespanFilters, integerFilters, facets, someFacetIsFetching } = props; const facetValues = []; Object.keys(uriFilters).map(activeFacetID => { Object.values(uriFilters[activeFacetID]).forEach(value => { @@ -31,6 +31,14 @@ const ActiveFilters = props => { value: timespanFilters[facetID] }); }); + Object.keys(integerFilters).map(facetID => { + facetValues.push({ + facetID: facetID, + facetLabel: facets[facetID].label, + filterType: 'integerFilter', + value: integerFilters[facetID] + }); + }); return ( <ChipsArray data={facetValues} @@ -49,6 +57,7 @@ ActiveFilters.propTypes = { spatialFilters: PropTypes.object.isRequired, textFilters: PropTypes.object.isRequired, timespanFilters: PropTypes.object.isRequired, + integerFilters: PropTypes.object.isRequired, updateFacetOption: PropTypes.func.isRequired, someFacetIsFetching: PropTypes.bool.isRequired, fetchFacet: PropTypes.func.isRequired diff --git a/src/client/components/facet_bar/ChipsArray.js b/src/client/components/facet_bar/ChipsArray.js index a706f7f8febeb63d392adb94917df63001323562..026b7b9d4cba072157bdfab080ba976fd273eea5 100644 --- a/src/client/components/facet_bar/ChipsArray.js +++ b/src/client/components/facet_bar/ChipsArray.js @@ -36,7 +36,7 @@ class ChipsArray extends React.Component { value: null }); break; - case 'timespanFilter': { + case 'timespanFilter': this.props.updateFacetOption({ facetClass: this.props.facetClass, facetID: item.facetID, @@ -47,7 +47,6 @@ class ChipsArray extends React.Component { facetClass: this.props.facetClass, facetID: item.facetID, }); - } } } }; @@ -89,6 +88,10 @@ class ChipsArray extends React.Component { valueLabel = `${this.ISOStringToYear(item.value.start)} to ${this.ISOStringToYear(item.value.end)}`; } + if (item.filterType === 'integerFilter') { + key = item.facetID; + valueLabel = `${item.value.start} to ${item.value.end}`; + } return ( <Tooltip key={key} title={`${item.facetLabel}: ${valueLabel}`}> <Chip diff --git a/src/client/components/facet_bar/FacetBar.js b/src/client/components/facet_bar/FacetBar.js index 1b025d82a68d704b821f41aaecf630f3a1a93487..9afdcea1bcd87c5b60aeec9818f0c23a3ab5433f 100644 --- a/src/client/components/facet_bar/FacetBar.js +++ b/src/client/components/facet_bar/FacetBar.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; import HierarchicalFacet from './HierarchicalFacet'; import TextFacet from './TextFacet'; -import DateSliderFacet from './DateSliderFacet'; +import SliderFacet from './SliderFacet'; import Paper from '@material-ui/core/Paper'; import FacetHeader from './FacetHeader'; import FacetInfo from './FacetInfo'; @@ -108,7 +108,7 @@ class FacetBar extends React.Component { break; case 'timespanFilter': facetComponent = ( - <DateSliderFacet + <SliderFacet facetID={facetID} facet={facet} facetClass={this.props.facetClass} @@ -117,6 +117,22 @@ class FacetBar extends React.Component { fetchFacet={this.props.fetchFacet} someFacetIsFetching={someFacetIsFetching} updateFacetOption={this.props.updateFacetOption} + dataType='ISOString' + /> + ); + break; + case 'integerFilter': + facetComponent = ( + <SliderFacet + facetID={facetID} + facet={facet} + facetClass={this.props.facetClass} + resultClass={this.props.resultClass} + facetUpdateID={facetUpdateID} + fetchFacet={this.props.fetchFacet} + someFacetIsFetching={someFacetIsFetching} + updateFacetOption={this.props.updateFacetOption} + dataType='integer' /> ); break; diff --git a/src/client/components/facet_bar/FacetInfo.js b/src/client/components/facet_bar/FacetInfo.js index e694b60bf16f1404d6555a14da50e01c65ed1e8e..ee9449cb0f3bbf99d9efee14f9bf9636f0279d76 100644 --- a/src/client/components/facet_bar/FacetInfo.js +++ b/src/client/components/facet_bar/FacetInfo.js @@ -40,10 +40,12 @@ class FacetInfo extends React.Component { let spatialFilters = {}; let textFilters = {}; let timespanFilters = {}; + let integerFilters = {}; let activeUriFilters = false; let activeSpatialFilters = false; let activeTextFilters = false; let activeTimespanFilters = false; + let activeIntegerFilters = false; for (const [key, value] of Object.entries(facets)) { if (has(value, 'uriFilter') && value.uriFilter !== null) { activeUriFilters = true; @@ -61,6 +63,10 @@ class FacetInfo extends React.Component { activeTimespanFilters = true; timespanFilters[key] = value.timespanFilter; } + if (has(value, 'integerFilter') && value.integerFilter !== null) { + activeIntegerFilters = true; + integerFilters[key] = value.integerFilter; + } } return ( <div className={classes.root}> @@ -78,6 +84,7 @@ class FacetInfo extends React.Component { || activeSpatialFilters || activeTextFilters || activeTimespanFilters + || activeIntegerFilters ) && <React.Fragment> <Typography variant="h6">Active filters:</Typography> @@ -89,6 +96,7 @@ class FacetInfo extends React.Component { spatialFilters={spatialFilters} textFilters={textFilters} timespanFilters={timespanFilters} + integerFilters={integerFilters} updateFacetOption={this.props.updateFacetOption} someFacetIsFetching={someFacetIsFetching} fetchFacet={this.props.fetchFacet} diff --git a/src/client/components/facet_bar/DateSliderFacet.js b/src/client/components/facet_bar/SliderFacet.js similarity index 53% rename from src/client/components/facet_bar/DateSliderFacet.js rename to src/client/components/facet_bar/SliderFacet.js index fcec52bf5282a1da23ade1dd123f7daeeae4ab40..b4642d211d22a59217092a7a04abecac438c4ef6 100644 --- a/src/client/components/facet_bar/DateSliderFacet.js +++ b/src/client/components/facet_bar/SliderFacet.js @@ -28,7 +28,7 @@ const styles = theme => ({ }, }); -class DateSliderFacet extends Component { +class SliderFacet extends Component { componentDidMount = () => { this.props.fetchFacet({ @@ -38,8 +38,11 @@ class DateSliderFacet extends Component { } handleSliderOnChange = values => { - values[0] = this.YearToISOString({ year: values[0], start: true }); - values[1] = this.YearToISOString({ year: values[1], start: false }); + console.log(values) + if (this.props.dataType === 'ISOString') { + values[0] = this.YearToISOString({ year: values[0], start: true }); + values[1] = this.YearToISOString({ year: values[1], start: false }); + } this.props.updateFacetOption({ facetClass: this.props.facetClass, facetID: this.props.facetID, @@ -83,79 +86,81 @@ class DateSliderFacet extends Component { render() { const { classes, someFacetIsFetching } = this.props; const { isFetching, min, max } = this.props.facet; + let domain = null; if (isFetching || min == null || max == null) { return( <div className={classes.spinnerContainer}> <CircularProgress style={{ color: purple[500] }} thickness={5} /> </div> ); - } else { + } else if (this.props.dataType === 'ISOString') { const minYear = this.ISOStringToYear(min); const maxYear = this.ISOStringToYear(max); - const domain = [ minYear, maxYear ]; // use as default values - - // https://github.com/sghall/react-compound-slider - return ( - <div className={classes.root}> - <Slider - mode={1} - step={1} - domain={domain} - disabled={someFacetIsFetching} - reversed={false} - rootStyle={sliderRootStyle} - onChange={this.handleSliderOnChange} - values={domain} - > - <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> - <Handles> - {({ handles, activeHandleID, getHandleProps }) => ( - <div className="slider-handles"> - {handles.map(handle => ( - <Handle - key={handle.id} - handle={handle} - domain={domain} - isActive={handle.id === activeHandleID} - getHandleProps={getHandleProps} - /> - ))} - </div> - )} - </Handles> - <Tracks left={false} right={false}> - {({ tracks, getTrackProps }) => ( - <div className="slider-tracks"> - {tracks.map(({ id, source, target }) => ( - <Track - key={id} - source={source} - target={target} - getTrackProps={getTrackProps} - /> - ))} - </div> - )} - </Tracks> - <Ticks count={10}> - {({ ticks }) => ( - <div className="slider-ticks"> - {ticks.map(tick => ( - <Tick key={tick.id} tick={tick} count={ticks.length} /> - ))} - </div> - )} - </Ticks> - </Slider> - </div> - ); + domain = [ minYear, maxYear ]; // use as default values + } else if (this.props.dataType === 'integer') { + domain = [ min, max ]; + domain = [ 0, 10000 ]; } - - + // Slider documentation: https://github.com/sghall/react-compound-slider + return ( + <div className={classes.root}> + <Slider + mode={1} + step={1} + domain={domain} + disabled={someFacetIsFetching} + reversed={false} + rootStyle={sliderRootStyle} + onChange={this.handleSliderOnChange} + values={domain} + > + <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> + <Handles> + {({ handles, activeHandleID, getHandleProps }) => ( + <div className="slider-handles"> + {handles.map(handle => ( + <Handle + key={handle.id} + handle={handle} + domain={domain} + isActive={handle.id === activeHandleID} + getHandleProps={getHandleProps} + /> + ))} + </div> + )} + </Handles> + <Tracks left={false} right={false}> + {({ tracks, getTrackProps }) => ( + <div className="slider-tracks"> + {tracks.map(({ id, source, target }) => ( + <Track + key={id} + source={source} + target={target} + getTrackProps={getTrackProps} + /> + ))} + </div> + )} + </Tracks> + <Ticks count={10}> + {({ ticks }) => ( + <div className="slider-ticks"> + {ticks.map(tick => ( + <Tick key={tick.id} tick={tick} count={ticks.length} /> + ))} + </div> + )} + </Ticks> + </Slider> + </div> + ); } } -DateSliderFacet.propTypes = { + +SliderFacet.propTypes = { classes: PropTypes.object.isRequired, facetID: PropTypes.string.isRequired, facet: PropTypes.object.isRequired, @@ -167,6 +172,7 @@ DateSliderFacet.propTypes = { facetUpdateID: PropTypes.number, updatedFilter: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), updatedFacet: PropTypes.string, + dataType: PropTypes.string.isRequired }; -export default withStyles(styles)(DateSliderFacet); +export default withStyles(styles)(SliderFacet); diff --git a/src/client/epics/index.js b/src/client/epics/index.js index 8724b5518adb268864260a23ffbd0d71aa7df064..c3e199c9c4cb1aa0e49df873b6263579b32d725f 100644 --- a/src/client/epics/index.js +++ b/src/client/epics/index.js @@ -288,6 +288,12 @@ export const stateToUrl = ({ priority: value.priority, values: value.timespanFilter }; + } else if (has(value, 'integerFilter') && value.integerFilter !== null) { + constraints[key] = { + filterType: value.filterType, + priority: value.priority, + values: value.integerFilter + }; } } if (Object.keys(constraints).length > 0) { diff --git a/src/client/reducers/helpers.js b/src/client/reducers/helpers.js index 01405719c867db4f4b7ab14e0f2302e774ce0534..c182a01de8580961be9c8b27374a342066a5448a 100644 --- a/src/client/reducers/helpers.js +++ b/src/client/reducers/helpers.js @@ -72,7 +72,8 @@ export const updateFacetOption = (state, action) => { 'uriFilter', 'spatialFilter', 'textFilter', - 'timespanFilter' + 'timespanFilter', + 'integerFilter' ]; if (filterTypes.includes(action.option)) { return updateFacetFilter(state, action); @@ -136,6 +137,21 @@ const updateFacetFilter = (state, action) => { } }; } + } else if (oldFacet.filterType === 'integerFilter') { + if (value == null) { + newFacet = { + ...state.facets[facetID], + integerFilter: null + }; + } else { + newFacet = { + ...state.facets[facetID], + integerFilter: { + start: value[0], + end: value[1] + } + }; + } } return { ...state, @@ -202,7 +218,8 @@ export const fetchFacetFailed = (state, action) => { }; export const updateFacetValues = (state, action) => { - if (state.facets[action.id].type === 'timespan') { + if (state.facets[action.id].type === 'timespan' + || state.facets[action.id].type === 'integer' ) { return { ...state, facets: { diff --git a/src/client/reducers/manuscriptsFacets.js b/src/client/reducers/manuscriptsFacets.js index 8eda79cf50769c15f528dfb32a4ec7586f5f742a..e1cdc66be3af807c4389740b12b831f7657303d0 100644 --- a/src/client/reducers/manuscriptsFacets.js +++ b/src/client/reducers/manuscriptsFacets.js @@ -129,6 +129,25 @@ export const INITIAL_STATE = { uriFilter: null, priority: 8 }, + // width: { + // id: 'width', + // label: 'Width', + // // predicate: defined in backend + // distinctValueCount: 0, + // values: [], + // flatValues: [], + // sortBy: 'instanceCount', + // sortDirection: 'desc', + // sortButton: true, + // spatialFilterButton: false, + // isFetching: false, + // searchField: true, + // containerClass: 'three', + // type: 'integer', + // filterType: 'integerFilter', + // integerFilter: null, + // priority: 9 + // }, collection: { id: 'collection', label: 'Collection', @@ -181,7 +200,7 @@ export const INITIAL_STATE = { containerClass: 'three', filterType: 'uriFilter', uriFilter: null, - priority: 9 + priority: 10 }, } }; diff --git a/src/server/sparql/FacetConfigs.js b/src/server/sparql/FacetConfigs.js index 3c2b44520946781aa405d053ae9b9da599132185..173aceb5b88e8c3128e96b9ae30ace59b480bb3d 100644 --- a/src/server/sparql/FacetConfigs.js +++ b/src/server/sparql/FacetConfigs.js @@ -56,6 +56,13 @@ export const facetConfigs = { predicate: 'crm:P45_consists_of', type: 'list', }, + width: { + id: 'width', + facetValueFilter: '', + labelPath: 'crm:P43_has_dimension/crm:P90_has_value', + predicate: 'crm:P43_has_dimension/crm:P90_has_value', + type: 'integer', + }, collection: { id: 'collection', facetValueFilter: '', diff --git a/src/server/sparql/FacetValues.js b/src/server/sparql/FacetValues.js index df777c892a57316457542a9ea4ba444883e3e1d3..9993b85be7aec5d39ea09b3bfbdefa24710dc451 100644 --- a/src/server/sparql/FacetValues.js +++ b/src/server/sparql/FacetValues.js @@ -2,7 +2,8 @@ import { runSelectQuery } from './SparqlApi'; import { endpoint, facetValuesQuery, - facetValuesQueryTimespan + facetValuesQueryTimespan, + facetValuesRange } from './SparqlQueriesGeneral'; import { prefixes } from './SparqlQueriesPrefixes'; import { facetConfigs } from './FacetConfigs'; @@ -44,6 +45,10 @@ export const getFacet = async ({ q = facetValuesQueryTimespan; mapper = mapTimespanFacet; break; + case 'integer': + q = facetValuesRange; + mapper = mapTimespanFacet; + break; default: q = facetValuesQuery; mapper = mapFacet; @@ -102,6 +107,7 @@ export const getFacet = async ({ q = q.replace('<START_PROPERTY>', facetConfig.startProperty); q = q.replace('<END_PROPERTY>', facetConfig.endProperty); } + // console.log(prefixes + q) const response = await runSelectQuery(prefixes + q, endpoint, mapper, resultFormat); return({ facetClass: facetClass, diff --git a/src/server/sparql/Filters.js b/src/server/sparql/Filters.js index 5ced5a4d4ee07f32a7e0c567ea7a0cd40b0ecd34..afa8be7296a70c7e979418ce2b87048690539a90 100644 --- a/src/server/sparql/Filters.js +++ b/src/server/sparql/Filters.js @@ -87,6 +87,15 @@ export const generateConstraintsBlock = ({ inverse: inverse }); break; + case 'integerFilter': + filterStr += generateIntegerFilter({ + facetClass: facetClass, + facetID: c.id, + filterTarget: filterTarget, + values: c.values, + inverse: inverse + }); + break; } }); return filterStr; @@ -175,6 +184,34 @@ const generateTimespanFilter = ({ } }; +const generateIntegerFilter = ({ + facetClass, + facetID, + filterTarget, + values, + inverse +}) => { + const facetConfig = facetConfigs[facetClass][facetID]; + const { start, end } = values; + const selectionStart = start; + const selectionEnd = end; + const filterStr = ` + ?${filterTarget} ${facetConfig.predicate} ?value . + FILTER( + ?value >= ${selectionStart} && ?value <= ${selectionEnd} + ) + `; + if (inverse) { + return ` + FILTER NOT EXISTS { + ${filterStr} + } + `; + } else { + return filterStr; + } +}; + const generateUriFilter = ({ facetClass, facetID, diff --git a/src/server/sparql/SparqlQueriesGeneral.js b/src/server/sparql/SparqlQueriesGeneral.js index 3f503030abcc39d262febf33fb0e9e622d3ba232..9ea3cae1b07d300892a37f4f78f28a441b61729c 100644 --- a/src/server/sparql/SparqlQueriesGeneral.js +++ b/src/server/sparql/SparqlQueriesGeneral.js @@ -131,3 +131,13 @@ export const facetValuesQueryTimespan = ` } } `; + +export const facetValuesRange = ` + # ignore selections from other facets + SELECT (MIN(?value) AS ?min) (MAX(?value) AS ?max) { + ?instance <PREDICATE> ?value . + VALUES ?facetClass { <FACET_CLASS> } + ?instance a ?facetClass . + <FACET_VALUE_FILTER> + } +`;