Allow filtering by category, intersecting with tags

This commit is contained in:
Franc Camps-Febrer
2019-01-16 11:16:05 -05:00
parent 470daf27e7
commit 4251ed0e94
7 changed files with 121 additions and 24 deletions

View File

@@ -189,7 +189,7 @@ export function updateDistrict(district) {
}
}
export const UPDATE_TAGFILTERS = 'UPDATE_TIMEFILTERS'
export const UPDATE_TAGFILTERS = 'UPDATE_TAGFILTERS'
export function updateTagFilters(tag) {
return {
type: UPDATE_TAGFILTERS,
@@ -197,6 +197,14 @@ export function updateTagFilters(tag) {
}
}
export const UPDATE_CATEGORYFILTERS = 'UPDATE_CATEGORYFILTERS'
export function updateCategoryFilters(category) {
return {
type: UPDATE_CATEGORYFILTERS,
category
}
}
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE';
export function updateTimeRange(timerange) {
return {

View File

@@ -98,7 +98,8 @@ class Dashboard extends React.Component {
<Toolbar
isNarrative={!!app.narrative}
methods={{
onFilter: actions.updateTagFilters,
onTagFilter: actions.updateTagFilters,
onCategoryFilter: actions.updateCategoryFilters,
onSelectNarrative: this.setNarrative
}}
/>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import Checkbox from './presentational/Checkbox';
import copy from '../js/data/copy.json';
class TagListPanel extends React.Component {
@@ -20,9 +21,10 @@ class TagListPanel extends React.Component {
this.computeTree(nextProps.tags);//.children[nextProps.tagType]);
}
onClickCheckbox(tag) {
tag.active = !tag.active
this.props.onFilter(tag);
onClickCheckbox(obj, type) {
obj.active = !obj.active
if (type === 'category') this.props.onCategoryFilter(obj);
if (type === 'tag') this.props.onTagFilter(obj);
}
createNodeComponent (node, depth) {
@@ -35,7 +37,7 @@ class TagListPanel extends React.Component {
<Checkbox
label={node.key}
isActive={node.active}
onClickCheckbox={() => this.onClickCheckbox(node)}
onClickCheckbox={() => this.onClickCheckbox(node, 'tag')}
/>
</li>
);
@@ -61,15 +63,42 @@ class TagListPanel extends React.Component {
}
renderTree() {
return this.state.treeComponents.map(c => c);
return (
<div>
<h2>{copy[this.props.language].toolbar.tags}</h2>
{this.state.treeComponents.map(c => c)}
</div>
)
}
renderCategoryTree() {
return (
<div>
<h2>{copy[this.props.language].toolbar.categories}</h2>
{this.props.categories.map(cat => {
return (<li
key={cat.category.replace(/ /g,"_")}
className={'tag-filter active'}
style={{ marginLeft: '20px' }}
>
<Checkbox
label={cat.category}
isActive={cat.active}
onClickCheckbox={() => this.onClickCheckbox(cat, 'category')}
/>
</li>)
})
}
</div>
)
}
render() {
return (
<div className="react-innertabpanel">
<h2>Explore data by tag</h2>
<p>Explore freely all the data by selecting tags.</p>
<h2>{copy[this.props.language].toolbar.explore_by_tag__title}</h2>
<p>{copy[this.props.language].toolbar.explore_by_tag__description}</p>
{this.renderCategoryTree()}
{this.renderTree()}
</div>
);

View File

@@ -81,7 +81,8 @@ class Toolbar extends React.Component {
categories={this.props.categories}
tagFilters={this.props.tagFilters}
categoryFilters={this.props.categoryFilters}
onFilter={this.props.methods.onFilter}
onTagFilter={this.props.methods.onTagFilter}
onCategoryFilter={this.props.methods.onCategoryFilter}
language={this.props.language}
/>
</TabPanel>
@@ -185,11 +186,11 @@ class Toolbar extends React.Component {
function mapStateToProps(state) {
return {
tags: selectors.getTagTree(state),
categories: selectors.selectCategories(state),
categories: selectors.getCategories(state),
narratives: selectors.selectNarratives(state),
language: state.app.language,
tagFilters: selectors.selectTagList(state),
categoryFilter: state.app.filters.categories,
categoryFilters: selectors.selectCategories(state),
viewFilters: state.app.filters.views,
features: state.app.features,
narrative: state.app.narrative,

View File

@@ -18,6 +18,10 @@
},
"toolbar": {
"title": "TITLE",
"categories": "Categories",
"tags": "Tags",
"explore_by_tag__title": "Explore by tag or category",
"explore_by_tag__description": "Selecting tags or categories, you'll see only those events that are tagged accordingly. If you select nothing, as well as everything, all data will be displayed.",
"panels": {
"mentions": {
"title": "Personas",
@@ -105,7 +109,11 @@
}
},
"narrative_panel_title": "Focus narratives",
"narrative_summary": "Here you can follow some curated stories we have found in the data."
"narrative_summary": "Here you can follow some curated stories we have found in the data.",
"categories": "Categories",
"tags": "Tags",
"explore_by_tag__title": "Explore by tag or category",
"explore_by_tag__description": "Selecting tags or categories, you'll see only those events that are tagged accordingly. If you select nothing, as well as everything, all data will be displayed."
},
"timeline": {
"zooms": [

View File

@@ -1,11 +1,10 @@
import initial from '../store/initial.js'
import { parseDate } from '../js/utilities.js'
import {
UPDATE_HIGHLIGHTED,
UPDATE_SELECTED,
UPDATE_TAGFILTERS,
UPDATE_CATEGORYFILTERS,
UPDATE_TIMERANGE,
UPDATE_NARRATIVE,
INCREMENT_NARRATIVE_CURRENT,
@@ -136,6 +135,25 @@ function updateTagFilters(appState, action) {
})
}
function updateCategoryFilters(appState, action) {
const categoryFilters = appState.filters.categories.slice(0)
const catFilter = categoryFilters.find(cF => cF.category === action.category.category);
if (!catFilter) {
categoryFilters.push(action.category)
} else {
catFilter.active = (!!action.category.active);
}
return Object.assign({}, appState, {
filters: Object.assign({}, appState.filters, {
categories: categoryFilters
})
})
}
function updateTimeRange(appState, action) { // XXX
return Object.assign({}, appState, {
filters: Object.assign({}, appState.filters, {
@@ -254,6 +272,8 @@ function app(appState = initial.app, action) {
return updateSelected(appState, action)
case UPDATE_TAGFILTERS:
return updateTagFilters(appState, action)
case UPDATE_CATEGORYFILTERS:
return updateCategoryFilters(appState, action)
case UPDATE_TIMERANGE:
return updateTimeRange(appState, action)
case UPDATE_NARRATIVE:

View File

@@ -20,6 +20,7 @@ export const getSources = state => {
export const getNotifications = state => state.domain.notifications
export const getTagTree = state => state.domain.tags
export const getTagsFilter = state => state.app.filters.tags
export const getCategoriesFilter = state => state.app.filters.categories
export const getTimeRange = state => state.app.filters.timerange
@@ -42,6 +43,19 @@ function isTaggedIn(event, tagFilters) {
}
}
/**
* Given an event and all categories,
* returns true/false if event has a category that is active
*/
function isTaggedInWithCategory(event, categories) {
if (event.category) {
if (categories.find(c => (c.category === event.category && c.active))) return true
return false;
} else {
return false
}
}
/*
* Returns true if no tags are selected
*/
@@ -53,6 +67,17 @@ function isNoTags(tagFilters) {
)
}
/*
* Returns true if no categories are selected
*/
function isNoCategories(categories) {
return (
categories.length === 0
|| !process.env.features.CATEGORIES_AS_TAGS
|| categories.every(c => !c.active)
)
}
/**
* Given an event and a time range,
* returns true/false if the event falls within timeRange
@@ -69,14 +94,15 @@ function isTimeRangedIn(event, timeRange) {
* and if TAGS are being used, select them if their tags are enabled
*/
export const selectEvents = createSelector(
[getEvents, getTagsFilter, getTimeRange],
(events, tagFilters, timeRange) => {
[getEvents, getTagsFilter, getCategoriesFilter, getTimeRange],
(events, tagFilters, categories, timeRange) => {
return events.reduce((acc, event) => {
const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters)
const isTaggedWithCategory = isTaggedInWithCategory(event, categories) || isNoCategories(categories)
const isTimeRanged = isTimeRangedIn(event, timeRange)
if (isTimeRanged && isTagged) {
if (isTimeRanged && isTagged && isTaggedWithCategory) {
const eventClone = Object.assign({}, event)
acc[event.id] = eventClone
}
@@ -90,16 +116,14 @@ export const selectEvents = createSelector(
* and if TAGS are being used, select them if their tags are enabled
*/
export const selectNarratives = createSelector(
[getEvents, getNarratives, getTagsFilter, getTimeRange, getSources],
(events, narrativesMeta, tagFilters, timeRange, sources) => {
[getEvents, getNarratives, getSources],
(events, narrativesMeta, sources) => {
const narratives = {}
const narrativeSkeleton = id => ({ id, steps: [] })
/* populate narratives dict with events */
events.forEach(evt => {
const isTagged = isTaggedIn(evt, tagFilters) || isNoTags(tagFilters)
const isTimeRanged = isTimeRangedIn(evt, timeRange)
const isInNarrative = evt.narratives.length > 0
evt.narratives.forEach(narrative => {
@@ -229,7 +253,13 @@ export const selectSelected = createSelector(
*/
export const selectCategories = createSelector(
[getCategories],
(categories) => categories
(categories) => {
categories.map(cat => {
cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active
});
console.log(categories)
return categories;
}
)