all tags->filters

This commit is contained in:
Lachlan Kermode
2020-06-08 15:50:39 +02:00
parent 33bbb5d0aa
commit 16358f5ab9
22 changed files with 202 additions and 177 deletions

View File

@@ -2,7 +2,7 @@
**NOTE: WIP. These settings are currently slightly out of date.**
In order to make timemap interesting, you need to configure it to read events. When loaded in a browser, timemap queries HTTP endpoint, expecting from them well-defined JSON objects. There are certain endpoints, such as `events`, that are required, while others , such as `tags`, are optional; when provided, they enhance a timemap instance with additional features and capabilities related to the additional data.
In order to make timemap interesting, you need to configure it to read events. When loaded in a browser, timemap queries HTTP endpoint, expecting from them well-defined JSON objects. There are certain endpoints, such as `events`, that are required, while others , such as `filters`, are optional; when provided, they enhance a timemap instance with additional features and capabilities related to the additional data.
The URLs for these endpoints, as well as other configurable settings in your timemap instance, are read from the `config.js` that you created in step 3 of the setup above. The example contains sensible defaults. This section covers each option in more detail:
@@ -14,11 +14,11 @@ The URLs for these endpoints, as well as other configurable settings in your tim
| EVENT_DESC_ROOT | Endpoint for additional metadata for each individual event, concatenated to SERVER_ROOT | String | Yes |
| CATEGORY_EXT | Endpoint for categories, concatenated with SERVER_ROOT | String | Yes |
| NARRATIVE_EXT | Endpoint for narratives, concatenated with SERVER_ROOT | String | No |
| TAG_TREE_EXT | Endpoint for tags, concatenated with SERVER_ROOT | String | Yes |
| FILTER_TREE_EXT | Endpoint for filters, concatenated with SERVER_ROOT | String | Yes |
| SITES_EXT | Endpoint for sites, concatenated with SERVER_ROOT | String | Yes |
| MAP_ANCHOR | Geographic coordinates for original map anchor | Array of numbers | No |
| MAPBOX_TOKEN | Access token for Mapbox satellite imagery | String | No |
| features.USE_TAGS | Enable / Disable tags | boolean | No |
| features.USE_FILTERS | Enable / Disable filters | boolean | No |
| features.USE_SEARCH | Enable / Disable search | boolean | No |
| features.USE_SITES | Enable / Disable sites | boolean | No |
@@ -47,7 +47,7 @@ a `config.js` file in timemap's root folder (explained in the next section).
"lat":"17.810358",
"long":"-18.2251664",
"source":"",
"tags": "",
"filters": "",
"category": ""
}
]
@@ -69,35 +69,35 @@ a `config.js` file in timemap's root folder (explained in the next section).
#### Optional endpoints
3. **Tags**: `events` can be tagged by multiple `tags`. These will further characterize the event, and allow to select or deselect based on them. Tags are or can be distributed in a tree-like hierarchy, and each node on the tree can be a tag, including those who are not leafs.
3. **Filters**: `events` can be filterged by multiple `filters`. These will further characterize the event, and allow to select or deselect based on them. Filters are or can be distributed in a tree-like hierarchy, and each node on the tree can be a filter, including those who are not leafs.
```json
{
"key":"tags",
"key":"filters",
"children": {
"tag0": {
"key": "tag0 ",
"filter0": {
"key": "filter0 ",
"children": {
"tag00": {
"key": "tag00",
"filter00": {
"key": "filter00",
"children": {
"tag001": {
"key": "tag001",
"filter001": {
"key": "filter001",
"children": {}
}
}
},
"tag01": {
"key": "tag01",
"filter01": {
"key": "filter01",
"children": {}
}
}
},
"tag1": {
"key": "tag1",
"filter1": {
"key": "filter1",
"children": {
"tag10": {
"key": "tag10",
"filter10": {
"key": "filter10",
"children": {}
}
}
@@ -106,7 +106,7 @@ a `config.js` file in timemap's root folder (explained in the next section).
}
```
4. **Sites**: sites are labels on the map, aiming to highlight particularly relevant locations that should not be a function of time or tags.
4. **Sites**: sites are labels on the map, aiming to highlight particularly relevant locations that should not be a function of time or filters.
```json
[

View File

@@ -4,7 +4,7 @@ import { urlFromEnv } from '../common/utilities'
// TODO: relegate these URLs entirely to environment variables
const EVENT_DATA_URL = urlFromEnv('EVENT_EXT')
const CATEGORY_URL = urlFromEnv('CATEGORY_EXT')
const TAGS_URL = urlFromEnv('TAGS_EXT')
const FILTERS_URL = urlFromEnv('FILTERS_EXT')
const SOURCES_URL = urlFromEnv('SOURCES_EXT')
const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT')
const SITES_URL = urlFromEnv('SITES_EXT')
@@ -49,14 +49,14 @@ export function fetchDomain () {
.catch(() => handleError(domainMsg('sites')))
}
let tagsPromise = Promise.resolve([])
let filtersPromise = Promise.resolve([])
if (features.USE_FILTERS) {
if (!TAGS_URL) {
tagsPromise = Promise.resolve(handleError('USE_TAGS is true, but you have not provided a TAGS_EXT'))
if (!FILTERS_URL) {
filtersPromise = Promise.resolve(handleError('USE_FILTERS is true, but you have not provided a FILTERS_EXT'))
} else {
tagsPromise = fetch(TAGS_URL)
filtersPromise = fetch(FILTERS_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('tags')))
.catch(() => handleError(domainMsg('filters')))
}
}
@@ -83,7 +83,7 @@ export function fetchDomain () {
catPromise,
narPromise,
sitesPromise,
tagsPromise,
filtersPromise,
sourcesPromise,
shapesPromise
])
@@ -93,7 +93,7 @@ export function fetchDomain () {
categories: response[1],
narratives: response[2],
sites: response[3],
tags: response[4],
filters: response[4],
sources: response[5],
shapes: response[6],
notifications

View File

@@ -19,9 +19,9 @@
"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.",
"filters": "Filters",
"explore_by_filter__title": "Explore by filter or category",
"explore_by_filter__description": "Selecting filters 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",
@@ -80,7 +80,7 @@
"legend": {
"view2d": {
"paragraphs": [
"Selecting a series of tags, you will be able to explore events on the map of Iguala and on the timeline.",
"Selecting a series of filters, you will be able to explore events on the map of Iguala and on the timeline.",
"Each event is colored according the person that gave category of the event."
],
"colors": [
@@ -114,17 +114,17 @@
"overview": "Selecting the name of a person will show the events only according to a persons category or category. The number in the parentheses show how many events are contained in each category, e.g. (34)."
},
"search": {
"title": "Directory of tags",
"title": "Directory of filters",
"placeholder": "Search"
}
},
"narratives_label": "Narratives",
"narrative_summary": "Follow a path through the data, from one key event to the next.",
"categories": "Categories",
"tags": "Filters",
"tags_label": "Filters",
"explore_by_tag__title": "Explore by filter",
"explore_by_tag__description": "Selecting a filter will show you only those events that are annotated with the filter. If you select nothing, as well as everything, all data will be displayed.",
"filters": "Filters",
"filters_label": "Filters",
"explore_by_filter__title": "Explore by filter",
"explore_by_filter__description": "Selecting a filter will show you only those events that are annotated with the filter. If you select nothing, as well as everything, all data will be displayed.",
"explore_by_category__title": "Explore events by category",
"explore_by_category__description": ""
@@ -158,8 +158,8 @@
"location": "Localization",
"incident_type": "Type of action",
"description": "Summary",
"tags": "Tags",
"notags": "No known tags for this event.",
"filters": "Filters",
"nofilters": "No known filters for this event.",
"sources": "Sources",
"unknown_source": "The information for this source could not be retrieved.",
"category": "Category",

View File

@@ -4,7 +4,7 @@ import React from 'react'
import CardTime from './presentational/Card/Time'
import CardLocation from './presentational/Card/Location'
import CardCaret from './presentational/Card/Caret'
import CardTags from './presentational/Card/Tags'
import CardFilters from './presentational/Card/Filters'
import CardSummary from './presentational/Card/Summary'
import CardSource from './presentational/Card/Source'
import CardNarrative from './presentational/Card/Narrative'
@@ -38,13 +38,13 @@ class Card extends React.Component {
)
}
renderTags () {
if (!this.props.tags || (this.props.tags && this.props.tags.length === 0)) {
renderFilters () {
if (!this.props.filters || (this.props.filters && this.props.filters.length === 0)) {
return null
}
return (
<CardTags
tags={this.props.tags || []}
<CardFilters
filters={this.props.filters || []}
language={this.props.language}
/>
)
@@ -137,7 +137,7 @@ class Card extends React.Component {
renderExtra () {
return (
<div className='card-bottomhalf'>
{this.renderTags()}
{this.renderFilters()}
{this.renderSources()}
{this.renderNarrative()}
</div>

View File

@@ -104,7 +104,7 @@ class Dashboard extends React.Component {
setNarrative (narrative) {
// only handleSelect if narrative is not null
if (narrative) {
this.props.actions.clearFilter('tags')
this.props.actions.clearFilter('filters')
this.props.actions.clearFilter('categories')
this.handleSelect([ narrative.steps[0] ])
}
@@ -152,7 +152,7 @@ class Dashboard extends React.Component {
isNarrative={!!app.narrative}
methods={{
onTitle: actions.toggleCover,
onTagFilter: tag => actions.toggleFilter('tags', tag),
onSelectFilter: filter => actions.toggleFilter('filters', filter),
onCategoryFilter: category => actions.toggleFilter('categories', category),
onSelectNarrative: this.setNarrative
}}

View File

@@ -205,6 +205,7 @@ class Map extends React.Component {
return (
<Events
svg={this.svgRef.current}
events={this.props.domain.events}
locations={this.props.domain.locations}
styleLocation={this.styleLocation}
categories={this.props.domain.categories}

View File

@@ -14,7 +14,7 @@ export default ({
{categories.map(cat => {
return (<li
key={cat.category.replace(/ /g, '_')}
className={'tag-filter active'}
className={'filter-filter active'}
style={{ marginLeft: '20px' }}
>
<Checkbox

View File

@@ -10,10 +10,10 @@ function allAssociatedKeys (node) {
return childKeys
}
function TagListPanel ({
tags,
activeTags,
onTagFilter,
function FilterListPanel ({
filters,
activeFilters,
onSelectFilter,
language
}) {
function createNodeComponent (node, depth) {
@@ -22,21 +22,21 @@ function TagListPanel ({
return (
<li
key={node.key.replace(/ /g, '_')}
className={'tag-filter'}
className={'filter-filter'}
style={{ marginLeft: `${depth * 20}px` }}
>
{/* <svg width='10' height='10'> */}
{/* <g className='tag-inline'> */}
{/* <g className='filter-inline'> */}
{/* <path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform='rotate(270)' /> */}
{/* </g> */}
{/* </svg> */}
<Checkbox
label={node.key}
isActive={activeTags.includes(node.key)}
onClickCheckbox={() => onTagFilter(matchingKeys)}
isActive={activeFilters.includes(node.key)}
onClickCheckbox={() => onSelectFilter(matchingKeys)}
/>
{children.length > 0
? children.map(tag => createNodeComponent(tag, depth + 1))
? children.map(filter => createNodeComponent(filter, depth + 1))
: null}
</li>
)
@@ -45,18 +45,18 @@ function TagListPanel ({
function renderTree (children) {
return (
<div>
{Object.values(children).map(tag => createNodeComponent(tag, 1))}
{Object.values(children).map(filter => createNodeComponent(filter, 1))}
</div>
)
}
return (
<div className='react-innertabpanel'>
<h2>{copy[language].toolbar.tags}</h2>
<p>{copy[language].toolbar.explore_by_tag__description}</p>
{renderTree(tags.children)}
<h2>{copy[language].toolbar.filters}</h2>
<p>{copy[language].toolbar.explore_by_filter__description}</p>
{renderTree(filters.children)}
</div>
)
}
export default TagListPanel
export default FilterListPanel

View File

@@ -6,7 +6,7 @@ import * as selectors from '../../selectors'
import { Tabs, TabPanel } from 'react-tabs'
import Search from './Search'
import TagListPanel from './TagListPanel'
import FilterListPanel from './FilterListPanel'
import CategoriesListPanel from './CategoriesListPanel'
import BottomActions from './BottomActions'
import copy from '../../common/data/copy.json'
@@ -37,9 +37,9 @@ class Toolbar extends React.Component {
<TabPanel>
<Search
language={this.props.language}
tags={this.props.tags}
filters={this.props.filters}
categories={this.props.categories}
tagFilters={this.props.tagFilters}
filterFilters={this.props.filterFilters}
categoryFilters={this.props.categoryFilters}
filter={this.props.filter}
/>
@@ -73,7 +73,7 @@ class Toolbar extends React.Component {
}
renderToolbarCategoriesPanel () {
if (this.props.features.CATEGORIES_AS_TAGS) {
if (this.props.features.CATEGORIES_AS_FILTERS) {
return (
<TabPanel>
<CategoriesListPanel
@@ -90,10 +90,10 @@ class Toolbar extends React.Component {
renderToolbarFilterPanel () {
return (
<TabPanel>
<TagListPanel
tags={this.props.tags}
activeTags={this.props.activeTags}
onTagFilter={this.props.methods.onTagFilter}
<FilterListPanel
filters={this.props.filters}
activeFilters={this.props.activeFilters}
onSelectFilter={this.props.methods.onSelectFilter}
language={this.props.language}
/>
</TabPanel>
@@ -120,7 +120,7 @@ class Toolbar extends React.Component {
{this.renderClosePanel()}
<Tabs selectedIndex={this.state._selected}>
{features.USE_NARRATIVES ? this.renderToolbarNarrativePanel() : null}
{features.CATEGORIES_AS_TAGS ? this.renderToolbarCategoriesPanel() : null}
{features.CATEGORIES_AS_FILTERS ? this.renderToolbarCategoriesPanel() : null}
{features.USE_FILTERS ? this.renderToolbarFilterPanel() : null}
</Tabs>
</div>
@@ -149,7 +149,7 @@ class Toolbar extends React.Component {
let title = copy[this.props.language].toolbar.title
if (process.env.display_title) title = process.env.display_title
const narrativesLabel = copy[this.props.language].toolbar.narratives_label
const tagsLabel = copy[this.props.language].toolbar.tags_label
const filtersLabel = copy[this.props.language].toolbar.filters_label
const categoriesLabel = 'Categories' // TODO:
return (
@@ -157,8 +157,8 @@ class Toolbar extends React.Component {
<div className='toolbar-header'onClick={this.props.methods.onTitle}><p>{title}</p></div>
<div className='toolbar-tabs'>
{features.USE_NARRATIVES ? this.renderToolbarTab(0, narrativesLabel, 'timeline') : null}
{features.CATEGORIES_AS_TAGS ? this.renderToolbarTab(1, categoriesLabel, 'widgets') : null}
{features.USE_FILTERS ? this.renderToolbarTab(2, tagsLabel, 'filter_list') : null}
{features.CATEGORIES_AS_FILTERS ? this.renderToolbarTab(1, categoriesLabel, 'widgets') : null}
{features.USE_FILTERS ? this.renderToolbarTab(features.CATEGORIES_AS_FILTERS ? 2 : 1, filtersLabel, 'filter_list') : null}
</div>
<BottomActions
info={{
@@ -192,11 +192,11 @@ class Toolbar extends React.Component {
function mapStateToProps (state) {
return {
tags: selectors.getTagTree(state),
filters: selectors.getFilterTree(state),
categories: selectors.getCategories(state),
narratives: selectors.selectNarratives(state),
language: state.app.language,
activeTags: selectors.getActiveTags(state),
activeFilters: selectors.getActiveFilters(state),
activeCategories: selectors.getActiveCategories(state),
viewFilters: state.app.filters.views,
narrative: state.app.narrative,

View File

@@ -1,7 +1,7 @@
/* global fetch */
import React from 'react'
import copy from '../../common/data/copy.json'
import TagFilter from './TagFilter'
import SelectFilter from './SelectFilter'
export default class Search extends React.Component {
constructor (props) {
@@ -21,7 +21,7 @@ export default class Search extends React.Component {
.then(response => response.json())
.then(json => {
this.setState({
searchResults: json.tags
searchResults: json.filters
})
})
}
@@ -32,16 +32,16 @@ export default class Search extends React.Component {
renderSearchResults () {
return (
this.state.searchResults.map(tag => {
this.state.searchResults.map(filter => {
return (
<TagFilter
<SelectFilter
isShowTree
tags={this.props.tags}
filters={this.props.filters}
categories={this.props.categories}
tagFilters={this.props.tagFilters}
filterFilters={this.props.filterFilters}
categoryFilters={this.props.categoryFilters}
filter={this.props.filter}
tag={tag}
filter={filter}
isCategory={this.props.isCategory}
/>
)

View File

@@ -1,22 +1,22 @@
import React from 'react'
import Checkbox from '../presentational/Checkbox'
function TagFilter (props) {
function SelectFilter (props) {
function isActive () {
if (props.isCategory) {
return props.categoryFilters.includes(props.tag.id)
return props.categoryFilters.includes(props.filter.id)
}
return props.tagFilters.includes(props.tag.id)
return props.filterFilters.includes(props.filter.id)
}
function onClickTag () {
function onClickFilter () {
if (isActive()) {
props.filter({
tags: props.tagFilters.filter(element => element !== props.tag.id)
filters: props.filterFilters.filter(element => element !== props.filter.id)
})
} else {
props.filter({
tags: props.tagFilters.concat(props.tag.id)
filters: props.filterFilters.concat(props.filter.id)
})
}
}
@@ -24,44 +24,44 @@ function TagFilter (props) {
function onClickCategory () {
if (isActive()) {
props.filter({
categories: props.categoryFilters.filter(element => element !== props.tag.id)
categories: props.categoryFilters.filter(element => element !== props.filter.id)
})
} else {
props.filter({
categories: props.categoryFilters.concat(props.tag.id)
categories: props.categoryFilters.concat(props.filter.id)
})
}
}
function renderTag () {
const tag = props.tag
let classes = (isActive()) ? 'tag-filter active' : 'tag-filter'
let label = `${tag.name} ( ${tag.mentions} )`
function renderFilter () {
const filter = props.filter
let classes = (isActive()) ? 'filter-filter active' : 'filter-filter'
let label = `${filter.name} ( ${filter.mentions} )`
if (props.isShowTree) {
label = `${tag.group} > ${tag.subgroup} > ${tag.name} ( ${tag.mentions} )`
label = `${filter.group} > ${filter.subgroup} > ${filter.name} ( ${filter.mentions} )`
}
return (
<li
key={props.tag.id}
key={props.filter.id}
className={classes}
>
<Checkbox
isActive={isActive()}
label={label}
onClickCheckbox={() => onClickTag()}
onClickCheckbox={() => onClickFilter()}
/>
</li>
)
}
function renderCategory () {
const category = props.categories[props.tag.id]
let classes = (isActive()) ? 'tag-filter active' : 'tag-filter'
const category = props.categories[props.filter.id]
let classes = (isActive()) ? 'filter-filter active' : 'filter-filter'
if (category) {
return (
<li
key={props.tag.id}
key={props.filter.id}
className={classes}
>
<Checkbox
@@ -76,7 +76,7 @@ function TagFilter (props) {
}
if (props.isCategory) return (renderCategory())
return (renderTag())
return (renderFilter())
}
export default TagFilter
export default SelectFilter

View File

@@ -0,0 +1,36 @@
import React from 'react'
import copy from '../../../common/data/copy.json'
const CardFilters = ({ filters, language }) => {
const filtersLang = copy[language].cardstack.filters
const noFiltersLang = copy[language].cardstack.nofilters
if (filters.length > 0) {
return (
<div className='card-row card-cell filters'>
<h4>{filtersLang}:</h4>
<p>
{filters.map((filter, idx) => {
return (
<span className='filter'>
<small>{filter.name}</small>
{(idx < filters.length - 1)
? ','
: ''}
</span>
)
})}
</p>
</div>
)
}
return (
<div className='card-row card-cell filters'>
<h4>{filtersLang}</h4>
<p><small>{noFiltersLang}</small></p>
</div>
)
}
export default CardFilters

View File

@@ -1,36 +0,0 @@
import React from 'react'
import copy from '../../../common/data/copy.json'
const CardTags = ({ tags, language }) => {
const tagsLang = copy[language].cardstack.tags
const noTagsLang = copy[language].cardstack.notags
if (tags.length > 0) {
return (
<div className='card-row card-cell tags'>
<h4>{tagsLang}:</h4>
<p>
{tags.map((tag, idx) => {
return (
<span className='tag'>
<small>{tag.name}</small>
{(idx < tags.length - 1)
? ','
: ''}
</span>
)
})}
</p>
</div>
)
}
return (
<div className='card-row card-cell tags'>
<h4>{tagsLang}</h4>
<p><small>{noTagsLang}</small></p>
</div>
)
}
export default CardTags

View File

@@ -3,7 +3,17 @@ import { Portal } from 'react-portal'
import colors from '../../../common/global.js'
import { calcOpacity } from '../../../common/utilities'
function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation, selected, narrative, onSelect, svg, locations }) {
function MapEvents ({
getCategoryColor,
categories,
projectPoint,
styleLocation,
selected,
narrative,
onSelect,
svg,
locations
}) {
function getCoordinatesForPercent (radius, percent) {
const x = radius * Math.cos(2 * Math.PI * percent)
const y = radius * Math.sin(2 * Math.PI * percent)
@@ -38,7 +48,7 @@ function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation,
...extraStyles
})
const colorSlices = location.events.map(e => getCategoryColor(e.category))
const colorSlices = location.events.map(e => e.colour ? e.colour : getCategoryColor(e.category))
let cumulativeAngleSweep = 0

View File

@@ -109,7 +109,7 @@ const TimelineEvents = ({
y: eventY,
onSelect: () => onSelect(event),
dims,
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.tags[features.HIGHLIGHT_GROUPS.tagIndexIndicatingGroup]) : [],
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.filters[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [],
features
})
}

View File

@@ -124,12 +124,12 @@ function toggleFilter (appState, action) {
action.value = [action.value]
}
let newTags = appState.filters[action.filter].slice(0)
let newFilters = appState.filters[action.filter].slice(0)
action.value.forEach(vl => {
if (newTags.includes(vl)) {
newTags = newTags.filter(s => s !== vl)
if (newFilters.includes(vl)) {
newFilters = newFilters.filter(s => s !== vl)
} else {
newTags.push(vl)
newFilters.push(vl)
}
})
@@ -137,7 +137,7 @@ function toggleFilter (appState, action) {
...appState,
filters: {
...appState.filters,
[action.filter]: newTags
[action.filter]: newFilters
}
}
}

View File

@@ -14,6 +14,7 @@ const eventSchema = Joi.object().keys({
category_full: Joi.string().allow(''),
narratives: Joi.array(),
sources: Joi.array(),
filters: Joi.array().allow(''),
tags: Joi.array().allow(''),
comments: Joi.string().allow(''),
time_display: Joi.string().allow(''),

View File

@@ -25,7 +25,7 @@ const isLeaf = node => (Object.keys(node.children).length === 0)
const isDuplicate = (node, set) => { return (set.has(node.key)) }
/*
* Traverse a tag tree and check its duplicates
* Traverse a filter tree and check its duplicates
*/
function validateTree (node, parent, set, duplicates) {
if (!Array.isArray(node) || !node.length) {
@@ -36,7 +36,7 @@ function validateTree (node, parent, set, duplicates) {
if (isDuplicate(node, set)) {
duplicates.push({
id: node.key,
error: makeError('Tags', node.key, 'tag was found more than once in hierarchy. Ignoring duplicate.')
error: makeError('Filters', node.key, 'filter was found more than once in hierarchy. Ignoring duplicate.')
})
delete parent.children[node.key]
} else {
@@ -60,9 +60,13 @@ export function validateDomain (domain) {
sites: [],
narratives: [],
sources: {},
tags: {},
filters: {},
shapes: [],
notifications: domain.notifications
notifications: domain ? domain.notifications : null
}
if (domain === undefined) {
return sanitizedDomain
}
const discardedDomain = {
@@ -89,6 +93,12 @@ export function validateDomain (domain) {
function validateArray (items, domainKey, schema) {
items.forEach(item => {
// NB: backwards compatibility with 'tags' for 'filters'
if (domainKey === 'events') {
if (!item.filters && !!item.tags) {
item.filters = item.tags
}
}
validateArrayItem(item, domainKey, schema)
})
}
@@ -138,20 +148,20 @@ export function validateDomain (domain) {
}
})
// Validate uniqueness of tags
const tagSet = new Set([])
const duplicateTags = []
validateTree(domain.tags, {}, tagSet, duplicateTags)
// Validate uniqueness of filters
const filterSet = new Set([])
const duplicateFilters = []
validateTree(domain.filters, {}, filterSet, duplicateFilters)
// Duplicated tags
if (duplicateTags.length > 0) {
// Duplicated filters
if (duplicateFilters.length > 0) {
sanitizedDomain.notifications.push({
message: `Tags are required to be unique. Ignoring duplicates for now.`,
items: duplicateTags,
message: `Filters are required to be unique. Ignoring duplicates for now.`,
items: duplicateFilters,
type: 'error'
})
}
sanitizedDomain.tags = domain.tags
sanitizedDomain.filters = domain.filters
// append events with datetime and sort
sanitizedDomain.events.forEach(event => {

View File

@@ -154,7 +154,7 @@
}
}
.tags {
.filters {
width: 100%;
margin: 5px 0;
text-align: left;
@@ -206,7 +206,7 @@
white-space: pre-line;
}
.tag {
.filter {
display: inline-block;
margin: 0;
margin-right: 5px;

View File

@@ -34,7 +34,7 @@
}
}
.tag,
.filter,
p.see-more {
cursor: pointer;

View File

@@ -14,8 +14,8 @@ export const getSites = state => state.domain.sites
export const getSources = state => state.domain.sources
export const getShapes = state => state.domain.shapes
export const getNotifications = state => state.domain.notifications
export const getTagTree = state => state.domain.tags
export const getActiveTags = state => state.app.filters.tags
export const getFilterTree = state => state.domain.filters
export const getActiveFilters = state => state.app.filters.filters
export const getActiveCategories = state => state.app.filters.categories
export const getTimeRange = state => state.app.timeline.range
export const getTimelineDimensions = state => state.app.timeline.dimensions
@@ -42,20 +42,24 @@ export const selectShapes = createSelector([getShapes, getFeatures], (shapes, fe
/**
* Of all available events, selects those that
* 1. fall in time range
* 2. exist in an active tag
* 2. exist in an active filter
* 3. exist in an active category
*/
export const selectEvents = createSelector(
[getEvents, getActiveTags, getActiveCategories, getTimeRange, getFeatures],
(events, activeTags, activeCategories, timeRange, features) => {
[getEvents, getActiveFilters, getActiveCategories, getTimeRange, getFeatures],
(events, activeFilters, activeCategories, timeRange, features) => {
return events.reduce((acc, event) => {
const isMatchingTag = (event.tags && event.tags.map(tag => activeTags.includes(tag)).some(s => s)) || activeTags.length === 0
const isActiveTag = isMatchingTag || activeTags.length === 0
const isMatchingFilter = (event.filters &&
event.filters.map(filter =>
activeFilters.includes(filter))
.some(s => s)
) || activeFilters.length === 0
const isActiveFilter = isMatchingFilter || activeFilters.length === 0
const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0
let isActiveTime = isTimeRangedIn(event, timeRange)
isActiveTime = features.GRAPH_NONLOCATED ? ((!event.latitude && !event.longitude) || isActiveTime) : isActiveTime
if (isActiveTime && isActiveTag && isActiveCategory) {
if (isActiveTime && isActiveFilter && isActiveCategory) {
acc[event.id] = { ...event }
}
@@ -65,7 +69,7 @@ export const selectEvents = createSelector(
/**
* Of all available events, selects those that fall within the time range,
* and if TAGS are being used, select them if their tags are enabled
* and if filters are being used, select them if their filters are enabled
*/
export const selectNarratives = createSelector(
[getEvents, getNarratives, getSources, getFeatures],
@@ -161,12 +165,12 @@ export const selectEventsWithProjects = createSelector(
}
const projSize = 2 * sizes.eventDotR
const projectIdx = features.GRAPH_NONLOCATED.projectIdx || 0
const getProject = ev => ev.tags[projectIdx]
const getProject = ev => ev.filters[projectIdx]
const projects = {}
// get all projects
events = events.reduce((acc, event) => {
const project = event.tags.length >= 1 && !event.latitude && !event.longitude ? getProject(event) : null
const project = event.filters.length >= 1 && !event.latitude && !event.longitude ? getProject(event) : null
// add project if it doesn't exist
if (project !== null) {

View File

@@ -16,7 +16,7 @@ const initial = {
categories: [],
sources: {},
sites: [],
tags: {},
filters: {},
notifications: []
},
@@ -40,7 +40,7 @@ const initial = {
current: null
},
filters: {
tags: [],
filters: [],
categories: [],
views: {
events: true,
@@ -131,9 +131,8 @@ const initial = {
},
features: {
CATEGORIES_AS_TAGS: true,
USE_COVER: false,
USE_TAGS: false,
USE_FILTERS: false,
USE_SEARCH: false,
USE_SITES: false,
USE_SOURCES: false,