From 346fd2ddcf38cf604845cc49ffe38633e9a5cd09 Mon Sep 17 00:00:00 2001 From: esikkala <esko.ikkala@aalto.fi> Date: Tue, 20 Aug 2019 12:10:38 +0300 Subject: [PATCH] Add new facet type for integer values --- .../components/facet_bar/ActiveFilters.js | 11 +- src/client/components/facet_bar/ChipsArray.js | 7 +- src/client/components/facet_bar/FacetBar.js | 20 ++- src/client/components/facet_bar/FacetInfo.js | 8 ++ .../{DateSliderFacet.js => SliderFacet.js} | 136 +++++++++--------- src/client/epics/index.js | 6 + src/client/reducers/helpers.js | 21 ++- src/client/reducers/manuscriptsFacets.js | 21 ++- src/server/sparql/FacetConfigs.js | 7 + src/server/sparql/FacetValues.js | 8 +- src/server/sparql/Filters.js | 37 +++++ src/server/sparql/SparqlQueriesGeneral.js | 10 ++ 12 files changed, 218 insertions(+), 74 deletions(-) rename src/client/components/facet_bar/{DateSliderFacet.js => SliderFacet.js} (53%) diff --git a/src/client/components/facet_bar/ActiveFilters.js b/src/client/components/facet_bar/ActiveFilters.js index f5d9855c..6b42b43e 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 a706f7f8..026b7b9d 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 1b025d82..9afdcea1 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 e694b60b..ee9449cb 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 fcec52bf..b4642d21 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 8724b551..c3e199c9 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 01405719..c182a01d 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 8eda79cf..e1cdc66b 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 3c2b4452..173aceb5 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 df777c89..9993b85b 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 5ced5a4d..afa8be72 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 3f503030..9ea3cae1 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> + } +`; -- GitLab