mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
all tags->filters
This commit is contained in:
@@ -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
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 person’s 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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
36
src/components/presentational/Card/Filters.js
Normal file
36
src/components/presentational/Card/Filters.js
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(''),
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tag,
|
||||
.filter,
|
||||
p.see-more {
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user