Merge pull request #39 from forensic-architecture/topic/guided-narratives

Topic/guided narratives
This commit is contained in:
Lachlan Kermode
2018-12-10 13:29:39 +00:00
committed by GitHub
33 changed files with 655 additions and 412 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
build/
node_modules/
config.js
dev.config.js

View File

@@ -4,6 +4,7 @@ module.exports = {
EVENT_EXT: '/api/example/export_events/rows',
CATEGORY_EXT: '/api/example/export_categories/rows',
SOURCES_EXT: '/api/example/export_events/ids',
NARRATIVE_EXT: '/api/example/export_narratives/rows',
TAGS_EXT: '/api/example/export_tags/tree',
SITES_EXT: '/api/example/export_sites/rows',
MAP_ANCHOR: [31.356397, 34.784818],

View File

@@ -8,12 +8,13 @@ function urlFromEnv(ext) {
}
// TODO: relegate these URLs entirely to environment variables
const EVENT_DATA_URL = urlFromEnv('EVENT_EXT')
const CATEGORY_URL = urlFromEnv('CATEGORY_EXT')
const TAG_URL = urlFromEnv('TAGS_EXT')
const SOURCES_URL = urlFromEnv('SOURCES_EXT')
const SITES_URL = urlFromEnv('SITES_EXT')
const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}`
const EVENT_DATA_URL = urlFromEnv('EVENT_EXT');
const CATEGORY_URL = urlFromEnv('CATEGORY_EXT');
const TAG_URL = urlFromEnv('TAGS_EXT');
const SOURCES_URL = urlFromEnv('SOURCES_EXT');
const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT');
const SITES_URL = urlFromEnv('SITES_EXT');
const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}`;
/*
* Create an error notification object
@@ -52,6 +53,10 @@ export function fetchDomain () {
.then(response => response.json())
.catch(handleError('categories'))
const narPromise = fetch(NARRATIVE_URL)
.then(response => response.json())
.catch(handleError('narratives'))
let sitesPromise = Promise.resolve([])
if (process.env.features.USE_SITES) {
sitesPromise = fetch(SITES_URL)
@@ -66,14 +71,16 @@ export function fetchDomain () {
.catch(handleError('tags'))
}
return Promise.all([ eventPromise, catPromise, sitesPromise, tagsPromise])
return Promise.all([eventPromise, catPromise, narPromise,
sitesPromise, tagsPromise])
.then(response => {
dispatch(toggleFetchingDomain())
const result = {
events: response[0],
categories: response[1],
sites: response[2],
tags: response[3],
narratives: response[2],
sites: response[3],
tags: response[4],
notifications
}
return result
@@ -102,6 +109,7 @@ export function updateDomain(domain) {
categories: domain.categories,
tags: domain.tags,
sites: domain.sites,
narratives: domain.narratives,
notifications: domain.notifications
}
}
@@ -156,6 +164,14 @@ export function updateTagFilters(tag) {
}
}
export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE';
export function updateNarrative(narrative) {
return {
type: UPDATE_NARRATIVE,
narrative
}
}
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE';
export function updateTimeRange(timerange) {
return {
@@ -209,6 +225,14 @@ export function toggleInfoPopup() {
}
}
export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW';
export function toggleMapView(layer) {
return {
type: TOGGLE_MAPVIEW,
layer
}
}
export const TOGGLE_NOTIFICATIONS = 'TOGGLE_NOTIFICATIONS'
export function toggleNotifications() {
return {

View File

@@ -1,5 +1,9 @@
import copy from '../js/data/copy.json';
import {isNotNullNorUndefined} from '../js/data/utilities';
import {
isNotNullNorUndefined,
parseDate,
formatterWithYear
} from '../js/utilities';
import React from 'react';
import Spinner from './presentational/Spinner';
@@ -35,8 +39,8 @@ class Card extends React.Component {
makeTimelabel(timestamp) {
if (timestamp === null) return null;
const parsedTimestamp = this.props.tools.parser(timestamp);
const timelabel = this.props.tools.formatterWithYear(parsedTimestamp);
const parsedTimestamp = parseDate(timestamp);
const timelabel = formatterWithYear(parsedTimestamp);
return timelabel;
}

View File

@@ -6,7 +6,7 @@ import Card from './Card.jsx';
import copy from '../js/data/copy.json';
import {
isNotNullNorUndefined
} from '../js/data/utilities.js';
} from '../js/utilities.js';
class CardStack extends React.Component {
@@ -21,7 +21,7 @@ class CardStack extends React.Component {
<Card
event={event}
language={this.props.language}
tools={this.props.tools}
isLoading={this.props.isLoading}
getNarrativeLinks={this.props.getNarrativeLinks}
getCategoryGroup={this.props.getCategoryGroup}
getCategoryColor={this.props.getCategoryColor}
@@ -90,9 +90,8 @@ function mapStateToProps(state) {
return {
selected: state.app.selected,
language: state.app.language,
tools: state.ui.tools,
isCardstack: state.ui.flags.isCardstack,
isFetchingSources: state.ui.flags.isFetchingSources
isCardstack: state.app.flags.isCardstack,
isLoading: state.app.flags.isFetchingEvents
}
}

View File

@@ -9,10 +9,13 @@ import LoadingOverlay from './presentational/LoadingOverlay';
import Viewport from './Viewport.jsx';
import Toolbar from './Toolbar.jsx';
import CardStack from './CardStack.jsx';
import NarrativeCard from './NarrativeCard.js';
import InfoPopUp from './InfoPopup.jsx';
import Timeline from './Timeline.jsx';
import Notification from './Notification.jsx';
import { parseDate } from '../js/utilities';
class Dashboard extends React.Component {
constructor(props) {
super(props);
@@ -23,7 +26,7 @@ class Dashboard extends React.Component {
this.handleTagFilter = this.handleTagFilter.bind(this);
this.updateTimerange = this.updateTimerange.bind(this);
this.eventsById = {};
this.eventsById = {}
}
componentDidMount() {
@@ -46,9 +49,7 @@ class Dashboard extends React.Component {
handleSelect(selected) {
if (selected) {
let eventsToSelect = selected.map(event => this.getEventById(event.id));
const p = this.props.ui.tools.parser;
eventsToSelect = eventsToSelect.sort((a, b) => p(a.timestamp) - p(b.timestamp))
eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp))
this.props.actions.fetchSelected(eventsToSelect)
}
@@ -67,7 +68,7 @@ class Dashboard extends React.Component {
}
getNarrativeLinks(event) {
const narrative = this.props.domain.narratives.find(nv => nv.key === event.narrative);
const narrative = this.props.domain.narratives.find(nv => nv.id === event.narrative);
if (narrative) return narrative.byId[event.id];
return null;
}
@@ -104,13 +105,17 @@ class Dashboard extends React.Component {
app={this.props.app}
toggle={() => this.props.actions.toggleInfoPopup()}
/>
<NarrativeCard
onSelect={this.handleSelect}
actions={this.props.actions}
/>
<Notification
isNotification={this.props.ui.flags.isNotification}
isNotification={this.props.app.flags.isNotification}
notifications={this.props.domain.notifications}
onToggle={this.props.actions.markNotificationsRead}
/>
<LoadingOverlay
ui={this.props.ui.flags.isFetchingDomain}
ui={this.props.app.flags.isFetchingDomain}
language={this.props.app.language}
/>
</div>

View File

@@ -27,7 +27,7 @@ export default class InfoPopUp extends React.Component{
renderView2DLegend() {
return (
<div className={`infopopup ${(this.props.ui.flags.isInfopopup) ? '' : 'hidden'}`}>
<div className={`infopopup ${(this.props.app.flags.isInfopopup) ? '' : 'hidden'}`}>
<button onClick={() => this.props.toggle()} className="side-menu-burg over-white is-active"><span /></button>
{this.renderView2DCopy()}
<div className="legend">

View File

@@ -0,0 +1,71 @@
import React from 'react';
import { connect } from 'react-redux'
class NarrativeCard extends React.Component {
constructor() {
super();
this.state = {
step: 0
}
}
goToPrevKeyFrame() {
if (this.state.step > 0) {
this.setState({ step: this.state.step - 1 });
}
}
goToNextKeyFrame() {
if (this.state.step < this.props.narrative.steps.length - 1) {
this.setState({ step: this.state.step + 1 });
}
}
componentDidUpdate() {
if (this.props.narrative !== null) {
const step = this.props.narrative.steps[this.state.step];
this.props.onSelect([step]);
}
}
renderClose() {
return (
<button
className="side-menu-burg is-active"
onClick={() => { this.props.actions.updateNarrative(null); }}
>
<span></span>
</button>
)
}
render() {
if (this.props.narrative !== null && this.props.narrative.steps[this.state.step]) {
const steps = this.props.narrative.steps;
const step = steps[this.state.step];
return (
<div className="narrative-info">
{this.renderClose()}
<h6>{this.props.narrative.label}</h6>
<p>{this.props.narrative.description}</p>
<h3>{this.state.step + 1}/{steps.length}. {step.location}</h3>
<div className="actions">
<div className={`${(!this.state.step) ? 'disabled ' : ''} action`} onClick={() => this.goToPrevKeyFrame()}>&larr;</div>
<div className={`${(this.state.step >= this.props.narrative.steps.length - 1) ? 'disabled ' : ''} action`} onClick={() => this.goToNextKeyFrame()}>&rarr;</div>
</div>
</div>
);
}
return (<div/>);
}
}
function mapStateToProps(state) {
return {
narrative: state.app.narrative
}
}
export default connect(mapStateToProps)(NarrativeCard);

View File

@@ -28,12 +28,12 @@ export default class Notification extends React.Component{
}
renderNotificationContent(notification) {
const { type, message, items } = notification;
let { type, message, items } = notification;
return (
<div>
<div className={`message ${type}`}>
{`${message}`}
{message}
</div>
<div className={`details ${this.state.isExtended}`}>
{(items !== null) ? this.renderItems(items) : ''}
@@ -48,7 +48,6 @@ export default class Notification extends React.Component{
return (
<div className={`notification-wrapper`}>
{this.props.notifications.map((notification) => {
return (
<div className='notification' onClick={() => this.toggleDetails() }>
<button

View File

@@ -13,11 +13,11 @@ class TagListPanel extends React.Component {
}
componentDidMount() {
this.computeTree(this.props.tags.children[this.props.tagType]);
this.computeTree(this.props.tags);//.children[this.props.tagType]);
}
componentWillReceiveProps(nextProps) {
this.computeTree(nextProps.tags.children[nextProps.tagType]);
this.computeTree(nextProps.tags);//.children[nextProps.tagType]);
}
onClickCheckbox(tag) {
@@ -65,8 +65,11 @@ class TagListPanel extends React.Component {
}
render() {
return (
<div className="react-innertabpanel">
<h2>Explore data by tag</h2>
<p>Explore freely all the data by selecting tags.</p>
{this.renderTree()}
</div>
);

View File

@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import * as selectors from '../selectors';
import copy from '../js/data/copy.json';
import { formatterWithYear } from '../js/utilities';
import TimelineLogic from '../js/timeline/timeline.js';
class Timeline extends React.Component {
@@ -15,7 +16,6 @@ class Timeline extends React.Component {
componentDidMount() {
const ui = {
tools: this.props.tools,
dom: this.props.dom
}
@@ -47,8 +47,8 @@ class Timeline extends React.Component {
const labels_title_lang = copy[this.props.app.language].timeline.labels_title;
const info_lang = copy[this.props.app.language].timeline.info;
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`;
const date0 = this.props.tools.formatterWithYear(this.props.app.timerange[0]);
const date1 = this.props.tools.formatterWithYear(this.props.app.timerange[1]);
const date0 = formatterWithYear(this.props.app.timerange[0]);
const date1 = formatterWithYear(this.props.app.timerange[1]);
return (
<div className={classes}>
@@ -82,9 +82,8 @@ function mapStateToProps(state) {
language: state.app.language,
zoomLevels: state.app.zoomLevels
},
tools: state.ui.tools,
dom: state.ui.dom,
}
}
export default connect(mapStateToProps)(Timeline);
export default connect(mapStateToProps)(Timeline);

View File

@@ -5,116 +5,24 @@ import * as selectors from '../selectors'
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import Search from './Search.jsx';
import TagListPanel from './TagListPanel.jsx';
import Icon from './Icon.jsx';
import ToolbarBottomActions from './ToolbarBottomActions.jsx';
import copy from '../js/data/copy.json';
// NB: i think this entire component can actually be part of a future feature...
class Toolbar extends React.Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = {
tab: -1
};
}
this.state = {
tabNum: -1
};
}
toggleTab(tabIndex) {
if ( this.state.tab === tabIndex ) {
this.setState({ tab: -1 });
} else {
this.setState({ tab: tabIndex });
}
}
toggleTab(tabNum) {
this.setState({ tabNum: (this.state.tabNum === tabNum) ? -1 : tabNum });
}
resetAllFilters() {
this.props.actions.resetAllFilters();
}
toggleInfoPopup() {
this.props.actions.toggleInfoPopup();
}
toggleLanguage() {
this.props.actions.toggleLanguage();
}
toggleMapViews(layer) {
const isLayerInView = !this.props.viewFilters[layer];
const newViews = {};
newViews[layer] = isLayerInView;
const views = Object.assign({}, this.props.viewFilters, newViews);
this.props.actions.updateFilters({ views });
}
renderMapActions() {
const isViewLayer = this.props.viewFilters;
const routeClass = (isViewLayer.routes) ? 'action-button active disabled' : 'action-button disabled'
const sitesClass = (isViewLayer.sites) ? 'action-button active disabled' : 'action-button disabled';
const coeventsClass = (isViewLayer.coevents) ? 'action-button active disabled' : 'action-button disabled';
return (
<div className="bottom-action-block">
<button
className={routeClass}
onClick={() => this.toggleMapViews('routes')}
>
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<path d="M0.806,13.646h7.619c2.762,0,3-0.238,3-3v-0.414c0-2.762,0.301-3,3.246-3h14.523"/>
<polyline points="16.671,9.228 19.103,7.233 16.671,5.237 "/>
</svg>
</button>
<button
className={sitesClass}
onClick={() => this.toggleMapViews('sites')}
>
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<path d="M24.615,6.793H5.385c-2.761,0-3,0.239-3,3v0.414
c0,2.762,0.239,3,3,3h7.621l1.996,2.432l1.996-2.432h7.618c2.762,0,3-0.238,3-3V9.793C27.615,7.032,27.377,6.793,24.615,6.793z"/>
</svg>
</button>
<button
className={coeventsClass}
onClick={() => this.toggleMapViews('coevents')}
>
<svg className="coevents" x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<polygon stroke-linejoin="round" stroke-miterlimit="10" points="19.178,20 10.823,20 10.473,14.081
10,13.396 10,6.084 20,6.084 20,13.396 19.445,14.021 "/>
<rect className="no-fill" x="11.4" y="7.867" width="7.2" height="3.35"/>
<line stroke-linejoin="round" stroke-miterlimit="10" x1="12.125" y1="1" x2="12.125" y2="5.35"/>
<rect x="11.4" y="4.271" width="1.496" height="1.079"/>
<rect x="17.104" y="4.271" width="1.496" height="1.079"/>
</svg>
</button>
</div>
);
}
renderBottomActions() {
return (
<div className="bottom-actions">
{this.renderMapActions()}
<div className="bottom-action-block">
<button className="action-button tiny default" onClick={() => { /*this.toggleLanguage()*/}}>
{(this.props.language === 'es-MX') ? 'ES' : 'EN' }
</button>
<button className="action-button info tiny default" onClick={() => {/*this.toggleInfoPopup()*/}}>
i
</button>
<button className="action-button tiny" onClick={() => this.resetAllFilters()}>
<svg className="reset" x="0px" y="0px" width="25px" height="25px" viewBox="7.5 7.5 25 25" enableBackground="new 7.5 7.5 25 25">
<path stroke-width="2" stroke-miterlimit="10" d="M28.822,16.386c1.354,3.219,0.898,7.064-1.5,9.924
c-3.419,4.073-9.49,4.604-13.562,1.186c-4.073-3.417-4.604-9.49-1.187-13.562c1.987-2.368,4.874-3.54,7.74-3.433" />
<polygon points="26.137,12.748 27.621,19.464 28.9,16.741 31.898,16.503" />
</svg>
</button>
</div>
</div>
);
}
renderPanelHeader() {
renderClosePanel() {
return (
<div className="panel-header" onClick={() => this.toggleTab(-1)}>
<div className="caret"></div>
@@ -122,107 +30,118 @@ class Toolbar extends React.Component {
);
}
renderToolbarTab(tabNum, key) {
const isActive = (tabNum === this.state.tab);
//let caption_lang = copy[this.props.language].toolbar.tabs[tabNum];
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
return (
<div className={classes} onClick={() => { this.toggleTab(tabNum); }}>
{/*<Icon iconType={key} />*/}
<div className="tab-caption">{key}</div>
</div>
);
}
renderToolbarTagRoot() {
if (this.props.features.USE_TAGS &&
this.props.tags.children) {
const roots = Object.values(this.props.tags.children);
return roots.map((root, idx) => {
return this.renderToolbarTab(idx, root.key);
})
}
return '';
}
renderToolbarTabs() {
const title = copy[this.props.language].toolbar.title;
return (
<div className="toolbar">
<div className="toolbar-header"><p>{title}</p></div>
<div className="toolbar-tabs">
{/*this.renderToolbarTab(0, 'search')*/}
{this.renderToolbarTagRoot()}
</div>
{/* {this.renderBottomActions()} */}
</div>
)
}
renderTagListPanel(tagType) {
const panels_lang = copy[this.props.language].toolbar.panels;
const title = (panels_lang[tagType]) ? panels_lang[tagType].title : tagType;
const overview = (panels_lang[tagType]) ? panels_lang[tagType].overview : '';
return (
<TagListPanel
tags={this.props.tags}
categories={this.props.categories}
tagFilters={this.props.tagFilters}
categoryFilters={this.props.categoryFilters}
filter={this.props.onFilter}
title={title}
overview={overview}
language={this.props.language}
tagType={tagType}
/>
);
}
renderSearch() {
if (this.props.features.USE_SEARCH) {
return (
<TabPanel>
<Search
language={this.props.language}
tags={this.props.tags}
categories={this.props.categories}
tagFilters={this.props.tagFilters}
categoryFilters={this.props.categoryFilters}
filter={this.props.onFilter}
/>
<Search
language={this.props.language}
tags={this.props.tags}
categories={this.props.categories}
tagFilters={this.props.tagFilters}
categoryFilters={this.props.categoryFilters}
filter={this.props.filter}
/>
</TabPanel>
)
}
}
renderToolbarTagList() {
goToNarrative(narrative) {
this.setState({
tabNum: -1
}, () => {
this.props.actions.updateNarrative(narrative);
});
}
renderToolbarNarrativePanel() {
return (
<TabPanel>
<h2>Focus stories</h2>
<p>Here are some highlighted stories</p>
{this.props.narratives.map((narr) => {
return (
<div className="panel-action action">
<button style={{ backgroundColor: '#000' }} onClick={() => { this.goToNarrative(narr); }}>
<p>{narr.label}</p>
<p><small>{narr.description}</small></p>
</button>
</div>
)
})}
</TabPanel>
);
}
renderToolbarTagPanel() {
if (this.props.features.USE_TAGS &&
this.props.tags.children) {
const roots = Object.values(this.props.tags.children);
return roots.map((root, idx) => {
return (
<TabPanel>
{this.renderTagListPanel(root.key)}
</TabPanel>
)
})
return (
<TabPanel>
<TagListPanel
tags={this.props.tags}
categories={this.props.categories}
tagFilters={this.props.tagFilters}
categoryFilters={this.props.categoryFilters}
filter={this.props.filter}
language={this.props.language}
/>
</TabPanel>
)
}
return '';
}
render() {
let classes = (this.state.tab !== -1) ? 'toolbar-panels' : 'toolbar-panels folded';
renderToolbarTab(tabNum, label) {
const isActive = (this.state.tabNum === tabNum);
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
return (
<div className={classes} onClick={() => { this.toggleTab(tabNum); }}>
<div className="tab-caption">{label}</div>
</div>
);
}
renderToolbarTabs() {
const title = copy[this.props.language].toolbar.title;
const isTags = this.props.tags && (this.props.tags.children > 0);
return (
<div className="toolbar">
<div className="toolbar-header"><p>{title}</p></div>
<div className="toolbar-tabs">
{/*this.renderToolbarTab(0, 'search')*/}
{this.renderToolbarTab(0, 'Narratives')}
{(isTags) ? this.renderToolbarTab(1, 'Explore by tag') : ''}
</div>
<ToolbarBottomActions
actions={this.props.actions}
/>
</div>
)
}
renderToolbarPanels() {
let classes = (this.state.tabNum !== -1) ? 'toolbar-panels' : 'toolbar-panels folded';
return (
<div className={classes}>
{this.renderClosePanel()}
<Tabs selectedIndex={this.state.tabNum}>
{this.renderToolbarNarrativePanel()}
{this.renderToolbarTagPanel()}
</Tabs>
</div>
)
}
render() {
return (
<div id="toolbar-wrapper" className="toolbar-wrapper">
{this.renderToolbarTabs()}
<div className={classes}>
{this.renderPanelHeader()}
<Tabs selectedIndex={this.state.tab}>
{this.renderToolbarTagList()}
</Tabs>
</div>
{this.renderToolbarPanels()}
</div>
);
}
@@ -232,11 +151,12 @@ function mapStateToProps(state) {
return {
tags: selectors.getTagTree(state),
categories: selectors.selectCategories(state),
narratives: selectors.selectNarratives(state),
language: state.app.language,
tagFilters: selectors.selectTagList(state),
categoryFilter: state.app.filters.categories,
viewFilters: state.app.filters.views,
features: state.app.features
features: state.app.features,
}
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import SitesIcon from './presentational/Icons/SitesIcon.js';
import RefreshIcon from './presentational/Icons/RefreshIcon.js';
import CoeventIcon from './presentational/Icons/CoeventIcon.js';
import RouteIcon from './presentational/Icons/RouteIcon.js';
class ToolbarBottomActions extends React.Component {
resetAllFilters() {
this.props.actions.resetAllFilters();
}
toggleInfoPopup() {
this.props.actions.toggleInfoPopup();
}
toggleLanguage() {
this.props.actions.toggleLanguage();
}
toggleMapViews(layer) {
this.props.actions.toggleMapView(layer);
}
renderMapActions() {
return (
<div className="bottom-action-block">
<RouteIcon
onClick={(view) => this.toggleMapViews(view)}
isEnabled={this.props.viewFilters.routes}
/>
<SitesIcon
onClick={(view) => this.toggleMapViews(view)}
isEnabled={this.props.viewFilters.sites}
/>
<CoeventIcon
onClick={(view) => this.toggleMapViews(view)}
isEnabled={this.props.viewFilters.coevents}
/>
</div>
);
}
render() {
return (
<div className="bottom-actions">
{/*}{this.renderMapActions()}
<div className="bottom-action-block">
<button className="action-button tiny default" onClick={() => { this.toggleLanguage()}}>
{(this.props.language === 'es-MX') ? 'ES' : 'EN' }
</button>
<button className="action-button info tiny default" onClick={() => { this.toggleInfoPopup()}}>
i
</button>
<button className="action-button tiny" onClick={() => this.resetAllFilters()}>
<RefreshIcon />
</button>
</div>*/}
</div>
);
}
}
export default ToolbarBottomActions;

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { connect } from 'react-redux'
import * as selectors from '../selectors'
import Map from '../js/map/map.js'
import { areEqual } from '../js/data/utilities.js'
import { areEqual } from '../js/utilities.js'
class Viewport extends React.Component {
constructor(props) {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import copy from '../../js/data/copy.json';
import {isNotNullNorUndefined} from '../../js/data/utilities';
import { isNotNullNorUndefined } from '../../js/utilities';
const CardLocation = ({ language, location }) => {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import copy from '../../js/data/copy.json';
import {isNotNullNorUndefined} from '../../js/data/utilities';
import { isNotNullNorUndefined } from '../../js/utilities';
const CardTimestamp = ({ makeTimelabel, language, timestamp }) => {

View File

@@ -0,0 +1,24 @@
import React from 'react';
const CoeventIcon = ({ isEnabled, toggleMapViews }) => {
const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled';
return (
<button
className={sitesClass}
onClick={() => toggleMapViews('coevents')}
>
<svg className="coevents" x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<polygon stroke-linejoin="round" stroke-miterlimit="10" points="19.178,20 10.823,20 10.473,14.081
10,13.396 10,6.084 20,6.084 20,13.396 19.445,14.021 "/>
<rect className="no-fill" x="11.4" y="7.867" width="7.2" height="3.35"/>
<line stroke-linejoin="round" stroke-miterlimit="10" x1="12.125" y1="1" x2="12.125" y2="5.35"/>
<rect x="11.4" y="4.271" width="1.496" height="1.079"/>
<rect x="17.104" y="4.271" width="1.496" height="1.079"/>
</svg>
</button>
);
}
export default CoeventIcon;

View File

@@ -0,0 +1,14 @@
import React from 'react';
const RefreshIcon = ({ }) => {
return (
<svg className="reset" x="0px" y="0px" width="25px" height="25px" viewBox="7.5 7.5 25 25" enableBackground="new 7.5 7.5 25 25">
<path stroke-width="2" stroke-miterlimit="10" d="M28.822,16.386c1.354,3.219,0.898,7.064-1.5,9.924
c-3.419,4.073-9.49,4.604-13.562,1.186c-4.073-3.417-4.604-9.49-1.187-13.562c1.987-2.368,4.874-3.54,7.74-3.433" />
<polygon points="26.137,12.748 27.621,19.464 28.9,16.741 31.898,16.503" />
</svg>
);
}
export default RefreshIcon;

View File

@@ -0,0 +1,20 @@
import React from 'react';
const RouteIcon = ({ isEnabled, toggleMapViews }) => {
const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled';
return (
<button
className={sitesClass}
onClick={() => toggleMapViews('routes')}
>
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<path d="M0.806,13.646h7.619c2.762,0,3-0.238,3-3v-0.414c0-2.762,0.301-3,3.246-3h14.523"/>
<polyline points="16.671,9.228 19.103,7.233 16.671,5.237 "/>
</svg>
</button>
);
}
export default RouteIcon;

View File

@@ -0,0 +1,19 @@
import React from 'react';
const SitesIcon = ({ isEnabled, toggleMapViews }) => {
const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled';
return (
<button
className={sitesClass}
onClick={() => toggleMapViews('sites')}
>
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<path d="M24.615,6.793H5.385c-2.761,0-3,0.239-3,3v0.414c0,2.762,0.239,3,3,3h7.621l1.996,2.432l1.996-2.432h7.618c2.762,0,3-0.238,3-3V9.793C27.615,7.032,27.377,6.793,24.615,6.793z"/>
</svg>
</button>
);
}
export default SitesIcon;

View File

@@ -1,7 +1,7 @@
import {
areEqual,
isNotNullNorUndefined
} from '../data/utilities';
} from '../utilities';
import hash from 'object-hash';
import 'leaflet-polylinedecorator';
@@ -227,8 +227,8 @@ Stop and start the development process in terminal after you have added your tok
// categories.sort((a, b) => {
// return (+a.slice(-2) > +b.slice(-2));
// });
categories.forEach(group => {
eventCount[group] = 0
categories.forEach(cat => {
eventCount[cat.category] = 0
});
location.events.forEach((event) => {;
@@ -239,9 +239,9 @@ Stop and start the development process in terminal after you have added your tok
const events = [];
while (i < categories.length) {
let _eventsCount = eventCount[categories[i]];
let _eventsCount = eventCount[categories[i].category];
for (let j = i + 1; j < categories.length; j++) {
_eventsCount += eventCount[categories[j]];
_eventsCount += eventCount[categories[j].category];
}
events.push(_eventsCount);
i++;
@@ -291,7 +291,7 @@ Stop and start the development process in terminal after you have added your tok
eventsDom
.enter().append('circle')
.attr('class', 'location-event-marker')
.style('fill', (d, i) => getCategoryColor(domain.categories[i]))
.style('fill', (d, i) => getCategoryColor(domain.categories[i].category))
.transition()
.duration(500)
.attr('r', d => (d) ? Math.sqrt(16 * d) + 3 : 0);
@@ -342,6 +342,13 @@ Stop and start the development process in terminal after you have added your tok
* Adds eventlayer to map
*/
function getNarrativeStyle(narrativeId) {
const styleName = narrativeId && narrativeId in narrativeProps
? narrativeId
: 'default';
return narrativeProps[styleName];
}
function renderNarratives() {
const narrativesDom = g.selectAll('.narrative')
.data(domain.narratives.map(d => d.steps))
@@ -356,20 +363,21 @@ Stop and start the development process in terminal after you have added your tok
.attr('class', 'narrative')
.attr('d', sequenceLine)
.style('stroke-width', d => {
styleName = d[0].narrative && d[0].narrative in narrativeProps
? d[0].narrative
: 'default'
const n = d[0].narrative;
return (n) ? narrativeProps[styleName].strokeWidth : 3;
if (!d[0]) return 0;
// Note: [0] is a non-elegant way to get the narrative id out of the first
// event in the narrative sequence
const styleProps = getNarrativeStyle(d[0].narrative);
return styleProps.strokeWidth;
})
.style('stroke-dasharray', d => {
const n = d[0].narrative;
if (narrativeProps[styleName].style === 'dotted') return "2px 5px";
return 'none';
if (!d[0]) return 'none';
const styleProps = getNarrativeStyle(d[0].narrative);
return (styleProps.style === 'dotted') ? "2px 5px" : 'none';
})
.style('stroke', d => {
const n = d[0].narrative;
return (n) ? narrativeProps[styleName].stroke : '#fff';
if (!d[0]) return 'none';
const styleProps = getNarrativeStyle(d[0].narrative);
return styleProps.stroke;
})
.style('fill', 'none');
}

View File

@@ -5,15 +5,16 @@
TODO: is it possible to express this idiomatically as React?
*/
import {
areEqual
} from '../data/utilities';
areEqual,
parseDate,
formatterWithYear
} from '../utilities';
import esLocale from '../data/es-MX.json';
import copy from '../data/copy.json';
export default function(app, ui, methods) {
d3.timeFormatDefaultLocale(esLocale);
const formatterWithYear = ui.tools.formatterWithYear;
const parser = ui.tools.parser;
const zoomLevels = app.zoomLevels;
let events = [];
let categories = [];
@@ -25,7 +26,7 @@ export default function(app, ui, methods) {
let transitionDuration = 500;
// Dimension of the client
const WIDTH_CONTROLS = 180;
const WIDTH_CONTROLS = 100;
const boundingClient = d3.select(`#${ui.dom.timeline}`).node().getBoundingClientRect();
let WIDTH = boundingClient.width - WIDTH_CONTROLS;
const HEIGHT = 140;
@@ -121,16 +122,6 @@ export default function(app, ui, methods) {
dom.backwards.append('circle');
dom.backwards.append('path');
dom.playGroup = dom.controls.append('g');
dom.playGroup.append('circle');
dom.play = dom.playGroup.append('g');
dom.play.append('path');
dom.pause = dom.playGroup.append('g').style('opacity', 0);
dom.pause.append('rect');
dom.pause.append('rect');
dom.zooms = dom.controls.append('g');
dom.zooms.selectAll('.zoom-level-button')
@@ -219,28 +210,6 @@ export default function(app, ui, methods) {
}
addResizeListener();
/**
* PLAY FUNCTIONALITY
*/
function stopBrushTransition() {
clearInterval(window.playInterval);
isPlaying = false;
dom.play.style('opacity', 1);
dom.pause.style('opacity', 0);
}
/**
* START PLAY SERIES OF TRANSITIONS
*/
function playBrushTransition() {
isPlaying = true;
dom.play.style('opacity', 0);
dom.pause.style('opacity', 1);
window.playInterval = setInterval(() => {
moveTime('forward');
}, playDuration);
}
/**
* Return which color event circle should be based on incident type
* @param {object} eventPoint data object
@@ -274,7 +243,7 @@ export default function(app, ui, methods) {
* @param {object} eventPoint: regular eventPoint data
*/
function getEventX(eventPoint) {
return scale.x(parser(eventPoint.timestamp));
return scale.x(parseDate(eventPoint.timestamp));
}
function getTimeScaleExtent() {
@@ -510,20 +479,6 @@ export default function(app, ui, methods) {
.attr('d', d3.symbol().type(d3.symbolTriangle).size(80))
.attr('transform', `translate(${scale.x.range()[1] - 20}, 62)rotate(90)`);
// These controls on separate svg
dom.playGroup.select('circle')
.attr('transform', 'translate(135, 60)rotate(90)')
.attr('r', 25);
dom.play.select('path')
.attr('d', d3.symbol().type(d3.symbolTriangle).size(260))
.attr('transform', 'translate(135, 60)rotate(90)');
dom.pause.selectAll('rect')
.attr('transform', (d, i) => `translate(${125 + (i * 15)}, 47)`)
.attr('height', 25)
.attr('width', 5);
dom.zooms.selectAll('text')
.text(d => d.label)
.attr('x', 60)
@@ -536,11 +491,6 @@ export default function(app, ui, methods) {
dom.backwards
.on('click', () => moveTime('backwards'));
dom.playGroup
.on('click', () => {
return (isPlaying) ? stopBrushTransition() : playBrushTransition();
});
dom.zooms.selectAll('text')
.on('click', zoom => applyZoom(zoom));
}
@@ -564,7 +514,7 @@ export default function(app, ui, methods) {
axis.y =
d3.axisLeft(scale.y)
.tickValues(categories);
.tickValues(categories.map(c => c.category));
}
function update(domain, app) {

View File

@@ -35,3 +35,25 @@ export function areEqual(arr1, arr2) {
export function isNotNullNorUndefined(variable) {
return (typeof variable !== 'undefined' && variable !== null);
}
/**
* Return a Date object given a datetime string of the format: "2016-09-10T07:00:00"
* @param {string} datetime
*/
export function parseDate(datetime) {
return new Date(datetime.slice(0, 4),
datetime.slice(5, 7) - 1,
datetime.slice(8, 10),
datetime.slice(11, 13),
datetime.slice(14, 16),
datetime.slice(17, 19)
);
}
export function formatterWithYear(datetime) {
return d3.timeFormat("%d %b %Y, %H:%M")(datetime);
}
export function formatter(datetime) {
return d3.timeFormat("%d %b, %H:%M")(datetime);
}

View File

@@ -1,12 +1,20 @@
import initial from '../store/initial.js';
import { parseDate } from '../js/utilities.js';
import {
UPDATE_HIGHLIGHTED,
UPDATE_SELECTED,
UPDATE_TAGFILTERS,
UPDATE_TIMERANGE,
UPDATE_NARRATIVE,
RESET_ALLFILTERS,
TOGGLE_LANGUAGE,
TOGGLE_MAPVIEW,
TOGGLE_FETCHING_DOMAIN,
TOGGLE_FETCHING_SOURCES,
TOGGLE_INFOPOPUP,
TOGGLE_NOTIFICATIONS,
FETCH_ERROR,
} from '../actions';
@@ -22,6 +30,28 @@ function updateSelected(appState, action) {
});
}
function updateNarrative(appState, action) {
if (action.narrative === null) {
return Object.assign({}, appState, {
narrative: action.narrative,
});
} else {
const dates = action.narrative.steps.map(n => parseDate(n.timestamp).getTime())
let minDate = Math.min(...dates);
let maxDate = Math.max(...dates);
// Add some margin to the datetime extent
minDate = minDate - ((maxDate - minDate) / 20);
maxDate = maxDate + ((maxDate - minDate) / 20);
return Object.assign({}, appState, {
narrative: action.narrative,
filters: Object.assign({}, appState.filters, {
timerange: [new Date(minDate), new Date(maxDate)]
}),
});
}
}
function updateTagFilters(appState, action) {
const tagFilters = appState.filters.tags.slice(0);
const nextActiveState = action.tag.active
@@ -74,6 +104,18 @@ function toggleLanguage(appState, action) {
});
}
function toggleMapView(appState, action) {
const isLayerInView = !appState.views[layer];
const newViews = {};
newViews[layer] = isLayerInView;
const views = Object.assign({}, appState.views, newViews);
return Object.assign({}, appState, {
filters: Object.assign({}, appState.filters, {
views
})
});
}
function fetchError(state, action) {
return {
...state,
@@ -82,6 +124,39 @@ function fetchError(state, action) {
}
}
function toggleFetchingDomain(appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isFetchingDomain: !appState.flags.isFetchingDomain
})
});
}
function toggleFetchingSources(appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isFetchingSources: !appState.flags.isFetchingSources
})
});
}
function toggleInfoPopup(appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isInfopopup: !appState.flags.isInfopopup
})
});
}
function toggleNotifications(appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isNotification: !appState.flags.isNotification
})
});
}
function app(appState = initial.app, action) {
switch (action.type) {
@@ -93,12 +168,24 @@ function app(appState = initial.app, action) {
return updateTagFilters(appState, action);
case UPDATE_TIMERANGE:
return updateTimeRange(appState, action);
case UPDATE_NARRATIVE:
return updateNarrative(appState, action);
case RESET_ALLFILTERS:
return resetAllFilters(appState, action);
case TOGGLE_LANGUAGE:
return toggleLanguage(appState, action);
case TOGGLE_MAPVIEW:
return toggleMapView(appState, action);
case FETCH_ERROR:
return fetchError(appState, action);
case TOGGLE_FETCHING_DOMAIN:
return toggleFetchingDomain(appState, action);
case TOGGLE_FETCHING_SOURCES:
return toggleFetchingSources(appState, action);
case TOGGLE_INFOPOPUP:
return toggleInfoPopup(appState, action);
case TOGGLE_NOTIFICATIONS:
return toggleNotifications(appState, action);
default:
return appState;
}

View File

@@ -0,0 +1,9 @@
import Joi from 'joi';
const narrativeSchema = Joi.object().keys({
id: Joi.string().required(),
description: Joi.string().allow('').required(),
label: Joi.string().required()
});
export default narrativeSchema;

View File

@@ -1,55 +1,9 @@
import initial from '../store/initial.js';
import {
TOGGLE_FETCHING_DOMAIN,
TOGGLE_FETCHING_SOURCES,
TOGGLE_VIEW,
TOGGLE_TIMELINE,
TOGGLE_INFOPOPUP,
TOGGLE_NOTIFICATIONS
} from '../actions'
function toggleFetchingDomain(uiState, action) {
return {
...uiState,
flags: {
...uiState.flags,
isFetchingDomain: !uiState.flags.isFetchingDomain
}
}
}
function toggleFetchingSources(uiState, action) {
return {
...uiState,
flags: {
...uiState.flags,
isFetchingSources: !uiState.flags.isFetchingSources
}
}
}
function toggleInfoPopup(uiState, action) {
return {
...uiState,
flags: {
...uiState.flags,
isInfopopup: !uiState.flags.isInfopopup
}
}
}
import {} from '../actions'
function ui(uiState = initial.ui, action) {
switch (action.type) {
case TOGGLE_FETCHING_DOMAIN:
return toggleFetchingDomain(uiState, action)
case TOGGLE_FETCHING_SOURCES:
return toggleFetchingSources(uiState, action)
case TOGGLE_INFOPOPUP:
return toggleInfoPopup(uiState, action)
default:
return uiState
}
return uiState;
}
export default ui;

View File

@@ -3,6 +3,7 @@ import Joi from 'joi';
import eventSchema from '../schema/eventSchema.js';
import categorySchema from '../schema/categorySchema.js';
import siteSchema from '../schema/siteSchema.js';
import narrativeSchema from '../schema/narrativeSchema.js';
import { capitalize } from './helpers.js';
@@ -57,6 +58,7 @@ export function validateDomain (domain) {
events: [],
categories: [],
sites: [],
narratives: [],
notifications: domain.notifications,
tags: {}
}
@@ -64,7 +66,8 @@ export function validateDomain (domain) {
const discardedDomain = {
events: [],
categories: [],
sites: []
sites: [],
narratives: [],
}
function validateItem(item, domainClass, schema) {
@@ -89,6 +92,10 @@ export function validateDomain (domain) {
domain.sites.forEach(site => {
validateItem(site, 'sites', siteSchema);
});
domain.narratives.forEach(narrative => {
validateItem(narrative, 'narratives', narrativeSchema);
});
// Message the number of failed items in domain
Object.keys(discardedDomain).forEach(disc => {

View File

@@ -6,6 +6,7 @@
@import 'loading';
@import 'header';
@import 'cardstack';
@import 'narrativecard';
@import 'map';
@import 'timeline';
@import 'tag-filters';

View File

@@ -0,0 +1,63 @@
/*
NARRATIVE INFO
*/
.narrative-info {
position: fixed;
top: 10px;
left: 130px;
height: auto;
width: 270px;
box-sizing: border-box;
padding: 15px;
max-height: calc(100% - 250px);
overflow: auto;
box-shadow: 0 19px 38px rgba($black, 0.3), 0 15px 12px rgba($black, 0.22);
background: $black;
border: 1px solid $midgrey;
color: $offwhite;
font-family: 'Merriweather', 'Georgia', serif;
h3, h6 {
text-align: center;
}
h3 {
font-size: $large;
}
p {
font-family: 'Lato', 'Helvetica', sans-serif;
font-size: $normal;
line-height: 1.4em;
}
.actions {
width: 100%;
.action {
width: calc(50% - 5px);
height: 40px;
box-sizing: border-box;
line-height: 40px;
font-family: 'Lato', 'Helvetica', sans-serif;
text-align: center;
display: inline-block;
&:not(.disabled) {
&:hover {
cursor: pointer;
transition: 0.2s ease;
color: $yellow;
}
}
&.disabled {
color: $midgrey;
cursor: normal;
}
&:first-child {
margin-right: 10px;
}
}
}
}

View File

@@ -173,7 +173,7 @@
.axisBoundaries {
stroke: $offwhite;
stroke-width: 1;
stroke-dasharray: 1px 4px;
stroke-dasharray: 1px 2px;
}
.event {

View File

@@ -161,10 +161,12 @@
}
.toolbar-tab {
display: inline-block;
display: flex;
align-items: center;
justify-content: center;
height: 60px;
width: 110px;
padding: 10px 0 5px 0;
padding: 5px 0 5px 0;
font-weight: 400;
text-overflow: ellipsis;
overflow: hidden;

View File

@@ -3,12 +3,13 @@ import {
} from 'reselect'
// Input selectors
export const getEvents = state => state.domain.events
export const getLocations = state => state.domain.locations
export const getCategories = state => state.domain.categories
export const getEvents = state => state.domain.events;
export const getLocations = state => state.domain.locations;
export const getCategories = state => state.domain.categories;
export const getNarratives = state => state.domain.narratives;
export const getSites = (state) => {
if (process.env.features.USE_SITES) return state.domain.sites
return []
if (process.env.features.USE_SITES) return state.domain.sites;
return [];
}
export const getNotifications = state => state.domain.notifications;
export const getTagTree = state => state.domain.tags;
@@ -84,8 +85,8 @@ export const selectEvents = createSelector(
* and if TAGS are being used, select them if their tags are enabled
*/
export const selectNarratives = createSelector(
[getEvents, getTagsFilter, getTimeRange],
(events, tagFilters, timeRange) => {
[getEvents, getNarratives, getTagsFilter, getTimeRange],
(events, narrativeMetadata, tagFilters, timeRange) => {
const narratives = {};
events.forEach((evt) => {
@@ -93,10 +94,11 @@ export const selectNarratives = createSelector(
const isTimeRanged = isTimeRangedIn(evt, timeRange);
const isInNarrative = evt.narrative;
if (isTimeRanged && isTagged && isInNarrative) {
if (!narratives[evt.narrative]) {
narratives[evt.narrative] = { key: evt.narrative, steps: [], byId: {} };
}
if (!narratives[evt.narrative]) {
narratives[evt.narrative] = { id: evt.narrative, steps: [], byId: {} };
}
if (/*isTimeRanged && isTagged && */isInNarrative) {
narratives[evt.narrative].steps.push(evt);
narratives[evt.narrative].byId[evt.id] = { next: null, prev: null };
}
@@ -104,13 +106,19 @@ export const selectNarratives = createSelector(
Object.keys(narratives).forEach((key) => {
const steps = narratives[key].steps;
steps.sort((a, b) => {
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp));
});
steps.forEach((step, i) => {
narratives[key].byId[step.id].next = (i < steps.length - 2) ? steps[i + 1] : null;
narratives[key].byId[step.id].prev = (i > 0) ? steps[i - 1] : null;
});
if (narrativeMetadata.find(n => n.id === key)) {
narratives[key] = Object.assign(narrativeMetadata.find(n => n.id === key), narratives[key]);
}
});
return Object.values(narratives);
@@ -150,26 +158,9 @@ export const selectLocations = createSelector(
*/
export const selectCategories = createSelector(
[getCategories],
(categories) => {
return categories.map(v => v.category);
}
(categories) => categories
);
/**
* Return categories by group
*/
export const selectCategoryGroups = createSelector(
[selectCategories],
(categories) => {
const groups = {};
categories.forEach((cat) => {
if (cat.group && !groups[cat.group]) {
groups[cat.group] = cat.group_label;
}
});
return Object.keys(groups).concat(['other']);
}
);
/**
* Given a tree of tags, return those tags as a list

View File

@@ -28,6 +28,7 @@ const initial = {
error: null,
highlighted: null,
selected: [],
narrative: null,
filters: {
timerange: [
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2013-02-23T12:00:00"),
@@ -84,37 +85,30 @@ const initial = {
features: {
USE_TAGS: process.env.features.USE_TAGS,
USE_SEARCH: process.env.features.USE_SEARCH
},
flags: {
isFetchingDomain: false,
isFetchingSources: false,
isCardstack: true,
isInfopopup: false,
isNotification: true
}
},
/*
* The 'ui' subtree of this state refers the state of the cosmetic
* elements of the application, such as color palettes of groups or how some
* of the UI tools are enabled or disabled dynamically by the user
* elements of the application, such as color palettes of categories
* as well as dom elements to attach SVG
*/
ui: {
style: {
colors: {
WHITE: "#efefef",
YELLOW: "#ffd800",
MIDGREY: "rgb(44, 44, 44)",
DARKGREY: "#232323",
PINK: "#F28B50",//rgb(232, 9, 90)",
ORANGE: "#F25835",//rgb(232, 9, 90)",
RED: "rgb(233, 0, 19)",
BLUE: "#F2DE79",//"rgb(48, 103 , 217)",
GREEN: "#4FF2F2",//"rgb(0, 158, 86)",
},
palette: d3.schemeCategory10,
categories: {
default: 'red',
// Add here other categories to differentiate by color, like:
alpha: '#00ff00',
beta: '#ff0000',
other: 'yellow'
alpha: '#c73e1d',
beta: '#f40000',
other: '#f3de2c'
},
narratives: {
@@ -127,7 +121,7 @@ const initial = {
narrative_1: {
style: 'solid', // ['dotted', 'solid']
opacity: 0.4, // range between 0 and 1
stroke: 'red', // Any hex or rgb code
stroke: '#f18f01', // Any hex or rgb code
strokeWidth: 2
}
}
@@ -137,18 +131,6 @@ const initial = {
timeslider: "timeslider",
map: "map"
},
flags: {
isFetchingDomain: false,
isFetchingSources: false,
isCardstack: true,
isInfopopup: false
},
tools: {
formatter: d3.timeFormat("%d %b, %H:%M"),
formatterWithYear: d3.timeFormat("%d %b %Y, %H:%M"),
parser: d3.timeParse("%Y-%m-%dT%H:%M:%S")
}
}
};