Merge pull request #67 from forensic-architecture/topic/narrative-left

Topic/narrative left closes #63
This commit is contained in:
Lachlan Kermode
2019-01-04 15:05:04 +01:00
committed by GitHub
18 changed files with 577 additions and 463 deletions

View File

@@ -213,6 +213,20 @@ export function updateNarrative(narrative) {
}
}
export const INCREMENT_NARRATIVE_CURRENT = 'INCREMENT_NARRATIVE_CURRENT';
export function incrementNarrativeCurrent() {
return {
type: INCREMENT_NARRATIVE_CURRENT
}
}
export const DECREMENT_NARRATIVE_CURRENT = 'DECREMENT_NARRATIVE_CURRENT';
export function decrementNarrativeCurrent() {
return {
type: DECREMENT_NARRATIVE_CURRENT
}
}
export const RESET_ALLFILTERS = 'RESET_ALLFILTERS'
export function resetAllFilters() {
return {

View File

@@ -1,61 +1,56 @@
import copy from '../js/data/copy.json';
import copy from '../js/data/copy.json'
import {
isNotNullNorUndefined,
parseDate,
formatterWithYear
} from '../js/utilities';
import React from 'react';
} from '../js/utilities'
import React from 'react'
import Spinner from './presentational/Spinner';
import CardTimestamp from './presentational/CardTimestamp';
import CardLocation from './presentational/CardLocation';
import CardCaret from './presentational/CardCaret';
import CardTags from './presentational/CardTags';
import CardSummary from './presentational/CardSummary';
import CardSource from './presentational/CardSource';
import CardCategory from './presentational/CardCategory';
import CardNarrative from './presentational/CardNarrative';
import Spinner from './presentational/Spinner'
import CardTimestamp from './presentational/CardTimestamp'
import CardLocation from './presentational/CardLocation'
import CardCaret from './presentational/CardCaret'
import CardTags from './presentational/CardTags'
import CardSummary from './presentational/CardSummary'
import CardSource from './presentational/CardSource'
import CardCategory from './presentational/CardCategory'
import CardNarrative from './presentational/CardNarrative'
class Card extends React.Component {
constructor(props) {
super(props);
super(props)
this.state = {
isHighlighted: false
};
isOpen: false
}
}
toggle() {
this.setState({
isHighlighted: !this.state.isHighlighted
}, () => {
if (!this.state.isHighlighted) {
this.props.onHighlight(this.props.event);
} else {
this.props.onHighlight(null);
}
});
isOpen: !this.state.isOpen
})
}
makeTimelabel(timestamp) {
if (timestamp === null) return null;
const parsedTimestamp = parseDate(timestamp);
const timelabel = formatterWithYear(parsedTimestamp);
return timelabel;
if (timestamp === null) return null
const parsedTimestamp = parseDate(timestamp)
const timelabel = formatterWithYear(parsedTimestamp)
return timelabel
}
renderCategory() {
const categoryTitle = copy[this.props.language].cardstack.category;
const categoryLabel = this.props.event.category;
const color = this.props.getCategoryColor(this.props.event.category);
const categoryTitle = copy[this.props.language].cardstack.category
const categoryLabel = this.props.event.category
const color = this.props.getCategoryColor(this.props.event.category)
return (
<CardCategory
categoryTitle={categoryTitle}
categoryLabel={categoryLabel}
color={color}
/>
);
return null
// return (
// <CardCategory
// categoryTitle={categoryTitle}
// categoryLabel={categoryLabel}
// color={color}
// />
// )
}
renderSummary() {
@@ -63,7 +58,7 @@ class Card extends React.Component {
<CardSummary
language={this.props.language}
description={this.props.event.description}
isHighlighted={this.state.isHighlighted}
isOpen={this.state.isOpen}
/>
)
}
@@ -96,7 +91,7 @@ class Card extends React.Component {
const source_lang = copy[this.props.language].cardstack.sources
return (
<div className="card-col">
<div className='card-col'>
<h4>{source_lang}: </h4>
{this.props.event.sources.map(source => (
<CardSource
@@ -117,11 +112,11 @@ class Card extends React.Component {
language={this.props.language}
timestamp={this.props.event.timestamp}
/>
);
)
}
renderNarrative() {
const links = this.props.getNarrativeLinks(this.props.event);
const links = this.props.getNarrativeLinks(this.props.event)
if (links !== null) {
@@ -136,52 +131,48 @@ class Card extends React.Component {
}
}
renderHeader() {
renderMain() {
return (
<div className="card-collapsed">
<div className="card-row">
<div className='card-container'>
<div className='card-row details'>
{this.renderTimestamp()}
{this.renderLocation()}
</div>
{this.renderCategory()}
<br/>
{this.renderSummary()}
</div>
);
)
}
renderContent() {
if (this.state.isHighlighted) {
return (
<div className="card-bottomhalf">
{this.renderTags()}
{this.renderSources()}
{this.renderNarrative()}
</div>
)
} else {
return <div classname="card-bottomhalf"></div>
}
renderExtra() {
return (
<div className='card-bottomhalf'>
{this.renderTags()}
{this.renderSources()}
{this.renderNarrative()}
</div>
)
}
renderCaret() {
return (
<CardCaret
toggle={() => this.toggle()}
isHighlighted={this.state.isHighlighted}
isOpen={this.state.isOpen}
/>
)
}
render() {
const { isSelected } = this.props
return (
<li className='event-card'>
{this.renderHeader()}
{this.renderContent()}
{this.renderCaret()}
<li className={`event-card ${isSelected ? 'selected' : ''}`}>
{this.renderMain()}
{this.state.isOpen ? this.renderExtra() : null}
{isSelected ? this.renderCaret() : null}
</li>
);
)
}
}
export default Card;
export default Card

View File

@@ -1,44 +1,56 @@
import React from 'react';
import React from 'react'
import { connect } from 'react-redux'
import * as selectors from '../selectors'
import Card from './Card.jsx';
import copy from '../js/data/copy.json';
import Card from './Card.jsx'
import copy from '../js/data/copy.json'
import {
isNotNullNorUndefined
} from '../js/utilities.js';
} from '../js/utilities.js'
class CardStack extends React.Component {
renderCards(events, selections) {
// if no selections provided, select all
if (!selections)
selections = events.map(e => true)
constructor(props) {
super(props);
return events.map((event, idx) => (
<Card
event={event}
sourceError={this.props.sourceError}
language={this.props.language}
isLoading={this.props.isLoading}
isSelected={selections[idx]}
getNarrativeLinks={this.props.getNarrativeLinks}
getCategoryGroup={this.props.getCategoryGroup}
getCategoryColor={this.props.getCategoryColor}
getCategoryLabel={this.props.getCategoryLabel}
onViewSource={this.props.onViewSource}
onHighlight={this.props.onHighlight}
onSelect={this.props.onSelect}
/>
))
}
renderCards() {
if (this.props.selected.length > 0) {
return this.props.selected.map((event) => {
return (
<Card
event={event}
sourceError={this.props.sourceError}
language={this.props.language}
isLoading={this.props.isLoading}
getNarrativeLinks={this.props.getNarrativeLinks}
getCategoryGroup={this.props.getCategoryGroup}
getCategoryColor={this.props.getCategoryColor}
getCategoryLabel={this.props.getCategoryLabel}
onViewSource={this.props.onViewSource}
onHighlight={this.props.onHighlight}
onSelect={this.props.onSelect}
/>
);
});
renderSelectedCards() {
const { selected } = this.props
if (selected.length > 0) {
return this.renderCards(selected)
}
return '';
return null
}
renderNarrativeCards() {
const { narrative } = this.props
const showing = narrative.steps.slice(narrative.current)
const selections = showing
.map((_, idx) => (idx === 0))
return this.renderCards(showing, selections)
}
renderCardStackHeader() {
const header_lang = copy[this.props.language].cardstack.header;
const header_lang = copy[this.props.language].cardstack.header
return (
<div
@@ -58,27 +70,59 @@ class CardStack extends React.Component {
return (
<div id="card-stack-content" className="card-stack-content">
<ul>
{this.renderCards()}
{this.renderSelectedCards()}
</ul>
</div>
);
)
}
renderNarrativeContent() {
return (
<div id="card-stack-content" className="card-stack-content">
<ul>
{this.renderNarrativeCards()}
</ul>
</div>
)
}
render() {
if (this.props.selected.length > 0) {
return (
<div id="card-stack" className={`card-stack ${this.props.isCardstack ? '' : ' folded'}`}>
{this.renderCardStackHeader()}
{this.renderCardStackContent()}
</div>
);
const { isCardstack, selected, narrative } = this.props
if (selected.length > 0) {
if (!narrative) {
return (
<div
id="card-stack"
className={`card-stack
${isCardstack ? '' : ' folded'}`
}
>
{this.renderCardStackHeader()}
{this.renderCardStackContent()}
</div>
)
} else {
return (
<div
id="card-stack"
className={`card-stack narrative-mode
${isCardstack ? '' : ' folded'}`
}
>
{this.renderNarrativeContent()}
</div>
)
}
}
return <div/>;
return <div/>
}
}
function mapStateToProps(state) {
return {
narrative: selectors.selectActiveNarrative(state),
selected: selectors.selectSelected(state),
sourceError: state.app.errors.source,
language: state.app.language,

View File

@@ -1,32 +1,33 @@
import React from 'react';
import React from 'react'
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../actions';
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as actions from '../actions'
import SourceOverlay from './SourceOverlay.jsx';
import LoadingOverlay from './presentational/LoadingOverlay';
import Map from './Map.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 SourceOverlay from './SourceOverlay.jsx'
import LoadingOverlay from './presentational/LoadingOverlay'
import Map from './Map.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';
import { parseDate } from '../js/utilities'
import { injectNarrative } from '../js/utilities'
class Dashboard extends React.Component {
constructor(props) {
super(props);
super(props)
this.handleViewSource = this.handleViewSource.bind(this)
this.handleHighlight = this.handleHighlight.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleSelectNarrative = this.handleSelectNarrative.bind(this);
this.handleTagFilter = this.handleTagFilter.bind(this);
this.updateTimerange = this.updateTimerange.bind(this);
this.getCategoryColor = this.getCategoryColor.bind(this);
this.handleHighlight = this.handleHighlight.bind(this)
this.setNarrative = this.setNarrative.bind(this)
this.moveInNarrative = this.moveInNarrative.bind(this)
this.handleSelect = this.handleSelect.bind(this)
this.getCategoryColor = this.getCategoryColor.bind(this)
this.eventsById = {}
}
@@ -34,18 +35,18 @@ class Dashboard extends React.Component {
componentDidMount() {
if (!this.props.app.isMobile) {
this.props.actions.fetchDomain()
.then(domain => this.props.actions.updateDomain(domain));
.then(domain => this.props.actions.updateDomain(domain))
}
}
handleHighlight(highlighted) {
this.props.actions.updateHighlighted((highlighted) ? highlighted : null);
this.props.actions.updateHighlighted((highlighted) ? highlighted : null)
}
getEventById(eventId) {
if (this.eventsById[eventId]) return this.eventsById[eventId];
this.eventsById[eventId] = this.props.domain.events.find(ev => ev.id === eventId);
return this.eventsById[eventId];
if (this.eventsById[eventId]) return this.eventsById[eventId]
this.eventsById[eventId] = this.props.domain.events.find(ev => ev.id === eventId)
return this.eventsById[eventId]
}
handleViewSource(source) {
@@ -54,104 +55,116 @@ class Dashboard extends React.Component {
handleSelect(selected) {
if (selected) {
let eventsToSelect = selected.map(event => this.getEventById(event.id));
let eventsToSelect = selected.map(event => this.getEventById(event.id))
eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp))
this.props.actions.updateSelected(eventsToSelect)
}
}
handleSelectNarrative(narrative) {
this.props.actions.updateNarrative(narrative);
}
handleTagFilter(tag) {
this.props.actions.updateTagFilters(tag);
}
updateTimerange(timeRange) {
this.props.actions.updateTimeRange(timeRange);
}
getCategoryColor(category='other') {
return this.props.ui.style.categories[category] || this.props.ui.style.categories['other']
}
getNarrativeLinks(event) {
const narrative = this.props.domain.narratives.find(nv => nv.id === event.narrative);
if (narrative) return narrative.byId[event.id];
return null;
const narrative = this.props.domain.narratives.find(nv => nv.id === event.narrative)
if (narrative) return narrative.byId[event.id]
return null
}
setNarrative(narrative) {
// only handleSelect if narrative is not null
if (!!narrative)
this.handleSelect([ narrative.steps[0] ])
this.props.actions.updateNarrative(narrative)
}
moveInNarrative(amt) {
const { current } = this.props.app.narrativeState
const { narrative } = this.props.app
if (amt === 1) {
this.handleSelect([ narrative.steps[current + 1] ])
this.props.actions.incrementNarrativeCurrent()
}
if (amt === -1) {
this.handleSelect([ narrative.steps[current - 1] ])
this.props.actions.decrementNarrativeCurrent()
}
}
render() {
const { actions, app, domain, ui } = this.props
return (
<div>
<Toolbar
onFilter={this.handleTagFilter}
onSelectNarrative={this.handleSelectNarrative}
actions={this.props.actions}
isNarrative={!!app.narrative}
methods={{
onFilter: actions.updateTagFilters,
onSelectNarrative: this.setNarrative
}}
/>
<Map
mapId="map"
mapId='map'
methods={{
onSelect: this.handleSelect,
onSelectNarrative: this.handleSelectNarrative,
onSelectNarrative: this.setNarrative,
getCategoryColor: this.getCategoryColor,
}}
/>
<Timeline
methods={{
onSelect: this.handleSelect,
onUpdateTimerange: this.updateTimerange,
onUpdateTimerange: actions.updateTimeRange,
getCategoryColor: category => this.getCategoryColor(category)
}}
/>
{(this.props.app.narrative !== null)
? <NarrativeCard
onSelect={this.handleSelect}
onSelectNarrative={this.handleSelectNarrative}
/>
: ''
}
<NarrativeCard
methods={{
onNext: () => this.moveInNarrative(1),
onPrev: () => this.moveInNarrative(-1),
onSelectNarrative: this.setNarrative
}}
/>
<CardStack
onViewSource={this.handleViewSource}
onSelect={this.handleSelect}
onHighlight={this.handleHighlight}
onToggleCardstack={() => this.props.actions.updateSelected([])}
onToggleCardstack={() => actions.updateSelected([])}
getNarrativeLinks={event => this.getNarrativeLinks(event)}
getCategoryColor={category => this.getCategoryColor(category)}
/>
<InfoPopUp
ui={this.props.ui}
app={this.props.app}
toggle={() => this.props.actions.toggleInfoPopup()}
ui={ui}
app={app}
toggle={() => actions.toggleInfoPopup()}
/>
<Notification
isNotification={this.props.app.flags.isNotification}
notifications={this.props.domain.notifications}
onToggle={this.props.actions.markNotificationsRead}
isNotification={app.flags.isNotification}
notifications={domain.notifications}
onToggle={actions.markNotificationsRead}
/>
{this.props.app.source ? (
{app.source ? (
<SourceOverlay
source={this.props.app.source}
source={app.source}
onCancel={() => {
this.props.actions.updateSource(null)}
actions.updateSource(null)}
}
/>
) : null}
<LoadingOverlay
ui={this.props.app.flags.isFetchingDomain}
language={this.props.app.language}
ui={app.flags.isFetchingDomain}
language={app.language}
/>
</div>
);
)
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
};
}
}
function injectSource(id) {
@@ -167,4 +180,4 @@ function injectSource(id) {
export default connect(
state => state,
mapDispatchToProps,
)(Dashboard);
)(Dashboard)

View File

@@ -33,8 +33,10 @@ class MapEvents extends React.Component {
})
if (this.props.narrative) {
const { byId } = this.props.narrative
const eventsInNarrative = events.filter(e => byId.hasOwnProperty(e.id))
const { steps } = this.props.narrative
const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id)
const eventsInNarrative = events.filter(onlyIfInNarrative)
if (eventsInNarrative.length <= 0) {
styleProps = {
...styleProps,

View File

@@ -1,42 +1,42 @@
import React from 'react';
import { Portal } from 'react-portal';
import React from 'react'
import { Portal } from 'react-portal'
class MapNarratives extends React.Component {
projectPoint(location) {
const latLng = new L.LatLng(location[0], location[1]);
const latLng = new L.LatLng(location[0], location[1])
return {
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
};
}
}
getNarrativeStyle(narrativeId) {
const styleName = (narrativeId && narrativeId in this.props.narrativeProps)
? narrativeId
: 'default';
return this.props.narrativeProps[styleName];
: 'default'
return this.props.narrativeProps[styleName]
}
getStrokeWidth(narrative, step) {
if (!step) return 0;
return this.getNarrativeStyle(narrative.id).strokeWidth;
if (!step) return 0
return this.getNarrativeStyle(narrative.id).strokeWidth
}
getStrokeDashArray(narrative, step) {
if (!step) return 'none';
return (this.getNarrativeStyle(narrative.id).style === 'dotted') ? "2px 5px" : 'none';
if (!step) return 'none'
return (this.getNarrativeStyle(narrative.id).style === 'dotted') ? "2px 5px" : 'none'
}
getStroke(narrative, step) {
if (!step || this.props.narrative === null) return 'none';
return this.getNarrativeStyle(narrative.id).stroke;
if (!step || this.props.narrative === null) return 'none'
return this.getNarrativeStyle(narrative.id).stroke
}
getStrokeOpacity(narrative, step) {
if (this.props.narrative === null) return 0;
if (!step || narrative.id !== this.props.narrative.id) return 0.1;
return 1;
if (this.props.narrative === null) return 0
if (!step || narrative.id !== this.props.narrative.id) return 0.1
return 1
}
hasNoLocation(step) {
@@ -44,14 +44,14 @@ class MapNarratives extends React.Component {
}
renderNarrativeStep(allSteps, step, idx, n) {
const { x, y } = this.projectPoint([step.latitude, step.longitude]);
const step2 = allSteps[idx + 1];
const { x, y } = this.projectPoint([step.latitude, step.longitude])
const step2 = allSteps[idx + 1]
// don't draw if one of the steps has no location
if (this.hasNoLocation(step) || this.hasNoLocation(step2))
return null
const p2 = this.projectPoint([step2.latitude, step2.longitude]);
const p2 = this.projectPoint([step2.latitude, step2.longitude])
return (
<line
@@ -70,11 +70,11 @@ class MapNarratives extends React.Component {
}}
>
</line>
);
)
}
renderNarrative(n) {
const steps = n.steps.slice(0, n.steps.length - 1);
const steps = n.steps.slice(0, n.steps.length - 1)
return (
<g id={`narrative-${n.id.replace(/ /g,"_")}`} className="narrative">
@@ -84,14 +84,14 @@ class MapNarratives extends React.Component {
}
render() {
if (this.props.narrative === null) return (<div />);
if (this.props.narrative === null) return (<div />)
return (
<Portal node={this.props.svg}>
{this.props.narratives.map(n => this.renderNarrative(n))}
</Portal>
);
)
}
}
export default MapNarratives;
export default MapNarratives

View File

@@ -1,86 +1,65 @@
import React from 'react';
import React from 'react'
import { connect } from 'react-redux'
import { selectActiveNarrative } from '../selectors'
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 });
}
}
componentDidMount() {
const step = this.props.narrative.steps[this.state.step];
this.props.onSelect([step]);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.narrative === this.props.narrative && this.state.step !== prevState.step) {
const step = this.props.narrative.steps[this.state.step];
this.props.onSelect([step]);
} else if (prevProps.narrative !== this.props.narrative && this.props.narrative !== null) {
this.setState({
step: 0
}, () => {
const step = this.props.narrative.steps[this.state.step];
this.props.onSelect([step]);
});
}
}
renderClose() {
function NarrativeCard ({ narrative, methods }) {
const { onSelectNarrative, onNext, onPrev } = methods
function renderClose() {
return (
<button
className="side-menu-burg is-active"
onClick={() => { this.props.onSelectNarrative(null); }}
className='side-menu-burg is-active'
onClick={() => { onSelectNarrative(null) }}
>
<span></span>
</button>
)
}
render() {
if (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()}
<h3>{this.props.narrative.label}</h3>
<p>{this.props.narrative.description}</p>
<h6>
<i className="material-icons left">location_on</i>
{this.state.step + 1}/{steps.length}. {step.location}
</h6>
<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>
function _renderActions(current, steps) {
const prevExists = current !== 0
const nextExists = current < steps.length - 1
return (
<div className='actions'>
<div
className={`${prevExists ? '' : 'disabled'} action`}
onClick={prevExists ? onPrev : null}>&larr;
</div>
);
}
return (<div/>);
<div
className={`${nextExists ? '' : 'disabled'} action`}
onClick={nextExists ? onNext : null}>&rarr;
</div>
</div>
)
}
// no display if no narrative
if (!narrative) return null
const { steps, current } = narrative
if (steps[current]) {
const step = steps[current]
return (
<div className='narrative-info'>
{renderClose()}
<h3>{narrative.label}</h3>
<p>{narrative.description}</p>
<h6>
<i className='material-icons left'>location_on</i>
{current + 1}/{steps.length}. {step.location}
</h6>
{_renderActions(current, steps)}
</div>
)
} else {
return null
}
}
function mapStateToProps(state) {
return {
narrative: state.app.narrative
narrative: selectActiveNarrative(state)
}
}
export default connect(mapStateToProps)(NarrativeCard);
export default connect(mapStateToProps)(NarrativeCard)

View File

@@ -34,18 +34,20 @@ class Timeline extends React.Component {
}
render() {
const { isNarrative, app, ui } = this.props
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`;
classes += (this.props.app.narrative !== null) ? ' narrative-mode' : '';
classes += (app.narrative !== null) ? ' narrative-mode' : '';
return (
<div className={classes}>
<TimelineHeader
title={copy[this.props.app.language].timeline.info}
date0={formatterWithYear(this.props.app.timerange[0])}
date1={formatterWithYear(this.props.app.timerange[1])}
title={copy[app.language].timeline.info}
date0={formatterWithYear(app.timerange[0])}
date1={formatterWithYear(app.timerange[1])}
onClick={() => { this.onClickArrow(); }}
hideInfo={isNarrative}
/>
<div className="timeline-content">
<div id={this.props.ui.dom.timeline} className="timeline" />
<div id={ui.dom.timeline} className="timeline" />
</div>
</div>
);
@@ -54,6 +56,7 @@ class Timeline extends React.Component {
function mapStateToProps(state) {
return {
isNarrative: !!state.app.narrative,
domain: {
events: state.domain.events,
categories: selectors.selectCategories(state),

View File

@@ -5,27 +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 ToolbarBottomActions from './ToolbarBottomActions.jsx';
// import ToolbarBottomActions from './ToolbarBottomActions.jsx';
import copy from '../js/data/copy.json';
import { isNotNullNorUndefined, trimAndEllipse } from '../js/utilities.js';
import { trimAndEllipse } from '../js/utilities.js';
class Toolbar extends React.Component {
constructor(props) {
super(props);
this.state = {
tabNum: -1
};
super(props)
this.state = { _selected: -1 }
}
toggleTab(tabNum) {
this.setState({ tabNum: (this.state.tabNum === tabNum) ? -1 : tabNum });
selectTab(selected) {
const _selected = (this.state._selected === selected) ? -1 : selected
this.setState({ _selected });
}
renderClosePanel() {
return (
<div className="panel-header" onClick={() => this.toggleTab(-1)}>
<div className="panel-header" onClick={() => this.selectTab(-1)}>
<div className="caret"></div>
</div>
);
@@ -49,11 +46,8 @@ class Toolbar extends React.Component {
}
goToNarrative(narrative) {
this.setState({
tabNum: -1
}, () => {
this.props.onSelectNarrative(narrative);
});
this.selectTab(-1) // set all unselected within this component
this.props.methods.onSelectNarrative(narrative);
}
renderToolbarNarrativePanel() {
@@ -64,7 +58,7 @@ class Toolbar extends React.Component {
{this.props.narratives.map((narr) => {
return (
<div className="panel-action action">
<button style={{ backgroundColor: '#000' }} onClick={() => { this.goToNarrative(narr); }}>
<button style={{ backgroundColor: '#000' }} onClick={() => { this.goToNarrative(narr) }}>
<p>{narr.label}</p>
<p><small>{trimAndEllipse(narr.description, 120)}</small></p>
</button>
@@ -94,12 +88,12 @@ class Toolbar extends React.Component {
return '';
}
renderToolbarTab(tabNum, label) {
const isActive = (this.state.tabNum === tabNum);
renderToolbarTab(_selected, label) {
const isActive = (this.state._selected === _selected);
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
return (
<div className={classes} onClick={() => { this.toggleTab(tabNum); }}>
<div className={classes} onClick={() => { this.selectTab(_selected); }}>
<i className="material-icons">timeline</i>
<div className="tab-caption">{label}</div>
</div>
@@ -118,20 +112,17 @@ class Toolbar extends React.Component {
{this.renderToolbarTab(0, 'Focus stories')}
{this.renderToolbarTab(1, 'Explore freely')}
</div>
<ToolbarBottomActions
actions={this.props.actions}
/>
{/* <ToolbarBottomActions /> */}
</div>
)
}
renderToolbarPanels() {
let classes = (this.state.tabNum !== -1) ? 'toolbar-panels' : 'toolbar-panels folded';
let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded';
return (
<div className={classes}>
{this.renderClosePanel()}
<Tabs selectedIndex={this.state.tabNum}>
<Tabs selectedIndex={this.state._selected}>
{this.renderToolbarNarrativePanel()}
{this.renderToolbarTagPanel()}
</Tabs>
@@ -142,12 +133,12 @@ class Toolbar extends React.Component {
renderToolbarNavs() {
if (this.props.narratives) {
return this.props.narratives.map((nar, idx) => {
const isActive = (idx === this.state.tab);
const isActive = (idx === this.state._selected);
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
return (
<div className={classes} onClick={() => { this.toggleTab(idx); }}>
<div className={classes} onClick={() => { this.selectTab(idx); }}>
<div className="tab-caption">{nar.label}</div>
</div>
);
@@ -168,15 +159,14 @@ class Toolbar extends React.Component {
{this.renderToolbarTab(0, 'Narratives')}
{(isTags) ? this.renderToolbarTab(1, 'Explore by tag') : ''}
</div>
<ToolbarBottomActions
actions={this.props.actions}
/>
{/* <ToolbarBottomActions /> */}
</div>
)
}
render() {
const isNarrative = isNotNullNorUndefined(this.props.narrative);
const { isNarrative } = this.props
return (
<div id="toolbar-wrapper" className={`toolbar-wrapper ${(isNarrative) ? 'narrative-mode' : ''}`}>
{this.renderToolbarTabs()}

View File

@@ -1,11 +1,11 @@
import React from 'react';
const TimelineHeader = ({ title, date0, date1, onClick }) => (
<div className="timeline-header">
<div className="timeline-toggle" onClick={() => onClick()}>
<p><i className="arrow-down"></i></p>
const TimelineHeader = ({ title, date0, date1, onClick, hideInfo }) => (
<div className='timeline-header'>
<div className='timeline-toggle' onClick={() => onClick()}>
<p><i className='arrow-down'></i></p>
</div>
<div className="timeline-info">
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
<p>{title}</p>
<p>{date0} - {date1}</p>
</div>

View File

@@ -73,11 +73,41 @@ export function formatter(datetime) {
return d3.timeFormat("%d %b, %H:%M")(datetime);
}
export const parseTimestamp = ts => d3.timeParse("%Y-%m-%dT%H:%M:%S")(ts);
export function compareTimestamp (a, b) {
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp));
}
/**
* Inset the full source represenation from 'allSources' into an event. The
* function is 'curried' to allow easy use with maps. To use for a single
* source, call with two sets of parentheses:
* const src = insetSourceFrom(sources)(anEvent)
*/
export function insetSourceFrom(allSources) {
return (event) => {
let sources
if (!event.sources) {
sources = []
} else {
sources = event.sources.map(id => (
allSources.hasOwnProperty(id) ? allSources[id] : null
))
}
return {
...event,
sources
}
}
}
/**
* Debugging function: put in place of a mapStateToProps function to
* view that source modal by default
*/
function injectSource(id) {
export function injectSource(id) {
return state => ({
...state,
app: {
@@ -86,4 +116,3 @@ function injectSource(id) {
}
})
}

View File

@@ -1,6 +1,6 @@
import initial from '../store/initial.js';
import initial from '../store/initial.js'
import { parseDate } from '../js/utilities.js';
import { parseDate } from '../js/utilities.js'
import {
UPDATE_HIGHLIGHTED,
@@ -8,6 +8,8 @@ import {
UPDATE_TAGFILTERS,
UPDATE_TIMERANGE,
UPDATE_NARRATIVE,
INCREMENT_NARRATIVE_CURRENT,
DECREMENT_NARRATIVE_CURRENT,
UPDATE_SOURCE,
RESET_ALLFILTERS,
TOGGLE_LANGUAGE,
@@ -18,63 +20,69 @@ import {
TOGGLE_NOTIFICATIONS,
FETCH_ERROR,
FETCH_SOURCE_ERROR,
} from '../actions';
} from '../actions'
function updateHighlighted(appState, action) {
return Object.assign({}, appState, {
highlighted: action.highlighted
});
})
}
function updateSelected(appState, action) {
return Object.assign({}, appState, {
selected: action.selected
});
})
}
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 {
...appState,
narrative: action.narrative,
narrativeState: {
current: !!action.narrative ? 0 : null
}
}
}
return Object.assign({}, appState, {
narrative: action.narrative,
filters: Object.assign({}, appState.filters, {
timerange: [new Date(minDate), new Date(maxDate)]
}),
});
function incrementNarrativeCurrent(appState, action) {
return {
...appState,
narrativeState: {
current: appState.narrativeState.current += 1
}
}
}
function decrementNarrativeCurrent(appState, action) {
return {
...appState,
narrativeState: {
current: appState.narrativeState.current -= 1
}
}
}
function updateTagFilters(appState, action) {
const tagFilters = appState.filters.tags.slice(0);
const tagFilters = appState.filters.tags.slice(0)
const nextActiveState = action.tag.active
function traverseNode(node) {
const tagFilter = tagFilters.find(tF => tF.key === node.key);
node.active = nextActiveState;
if (!tagFilter) tagFilters.push(node);
const tagFilter = tagFilters.find(tF => tF.key === node.key)
node.active = nextActiveState
if (!tagFilter) tagFilters.push(node)
if (node && Object.keys(node.children).length > 0) {
Object.values(node.children).forEach((childNode) => { traverseNode(childNode); });
Object.values(node.children).forEach((childNode) => { traverseNode(childNode) })
}
}
traverseNode(action.tag);
traverseNode(action.tag)
return Object.assign({}, appState, {
filters: Object.assign({}, appState.filters, {
tags: tagFilters
})
});
})
}
function updateTimeRange(appState, action) { // XXX
@@ -82,7 +90,7 @@ function updateTimeRange(appState, action) { // XXX
filters: Object.assign({}, appState.filters, {
timerange: action.timerange
}),
});
})
}
function resetAllFilters(appState) { // XXX
@@ -96,26 +104,26 @@ function resetAllFilters(appState) { // XXX
],
}),
selected: [],
});
})
}
function toggleLanguage(appState, action) {
let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX';
let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX'
return Object.assign({}, appState, {
language: action.language || otherLanguage
});
})
}
function toggleMapView(appState, action) {
const isLayerInView = !appState.views[layer];
const newViews = {};
newViews[layer] = isLayerInView;
const views = Object.assign({}, appState.views, newViews);
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 updateSource(appState, action) {
@@ -138,7 +146,7 @@ function toggleFetchingDomain(appState, action) {
flags: Object.assign({}, appState.flags, {
isFetchingDomain: !appState.flags.isFetchingDomain
})
});
})
}
function toggleFetchingSources(appState, action) {
@@ -146,7 +154,7 @@ function toggleFetchingSources(appState, action) {
flags: Object.assign({}, appState.flags, {
isFetchingSources: !appState.flags.isFetchingSources
})
});
})
}
function toggleInfoPopup(appState, action) {
@@ -154,7 +162,7 @@ function toggleInfoPopup(appState, action) {
flags: Object.assign({}, appState.flags, {
isInfopopup: !appState.flags.isInfopopup
})
});
})
}
function toggleNotifications(appState, action) {
@@ -162,7 +170,7 @@ function toggleNotifications(appState, action) {
flags: Object.assign({}, appState.flags, {
isNotification: !appState.flags.isNotification
})
});
})
}
function fetchSourceError(appState, action) {
@@ -180,38 +188,42 @@ function fetchSourceError(appState, action) {
function app(appState = initial.app, action) {
switch (action.type) {
case UPDATE_HIGHLIGHTED:
return updateHighlighted(appState, action);
return updateHighlighted(appState, action)
case UPDATE_SELECTED:
return updateSelected(appState, action);
return updateSelected(appState, action)
case UPDATE_TAGFILTERS:
return updateTagFilters(appState, action);
return updateTagFilters(appState, action)
case UPDATE_TIMERANGE:
return updateTimeRange(appState, action);
return updateTimeRange(appState, action)
case UPDATE_NARRATIVE:
return updateNarrative(appState, action);
return updateNarrative(appState, action)
case INCREMENT_NARRATIVE_CURRENT:
return incrementNarrativeCurrent(appState, action)
case DECREMENT_NARRATIVE_CURRENT:
return decrementNarrativeCurrent(appState, action)
case UPDATE_SOURCE:
return updateSource(appState, action);
return updateSource(appState, action)
case RESET_ALLFILTERS:
return resetAllFilters(appState, action);
return resetAllFilters(appState, action)
case TOGGLE_LANGUAGE:
return toggleLanguage(appState, action);
return toggleLanguage(appState, action)
case TOGGLE_MAPVIEW:
return toggleMapView(appState, action);
return toggleMapView(appState, action)
case FETCH_ERROR:
return fetchError(appState, action);
return fetchError(appState, action)
case TOGGLE_FETCHING_DOMAIN:
return toggleFetchingDomain(appState, action);
return toggleFetchingDomain(appState, action)
case TOGGLE_FETCHING_SOURCES:
return toggleFetchingSources(appState, action);
return toggleFetchingSources(appState, action)
case TOGGLE_INFOPOPUP:
return toggleInfoPopup(appState, action);
return toggleInfoPopup(appState, action)
case TOGGLE_NOTIFICATIONS:
return toggleNotifications(appState, action);
return toggleNotifications(appState, action)
case FETCH_SOURCE_ERROR:
return fetchSourceError(appState, action);
return fetchSourceError(appState, action)
default:
return appState;
return appState
}
}
export default app;
export default app

View File

@@ -2,10 +2,10 @@
box-sizing: border-box;
margin: 1px 0 0 0;
padding: 15px;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 3px;
border: 1px solid $black;
// border-radius: 3px;
transition: 0.2 ease;
background: $offwhite;
background: $darkwhite;
color: $darkgrey;
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
font-size: $large;
@@ -39,10 +39,13 @@
.card-row, .card-col {
display: flex;
flex-direction: row;
border-bottom: 1px solid $lightwhite;
margin: 5px 0 10px 0;
padding-bottom: 10px;
&.details {
border-bottom: 1px solid $lightwhite;
}
.card-cell {
flex: 1;
}
@@ -120,6 +123,7 @@
height: 0;
overflow: hidden;
}
}
.card-toggle p {
@@ -197,6 +201,7 @@
.summary {
overflow: auto;
margin-top: 0;
border-bottom: none;
}
.tag {
@@ -204,4 +209,12 @@
margin: 0;
margin-right: 5px;
}
&.selected {
background: $offwhite;
}
.card-row {
border-color: darkgray;
}
}

View File

@@ -1,7 +1,9 @@
@import 'burger';
@import 'card';
$card-width: 500px;
$card-width: 370px;
$narrative-info-max-height: 170px;
$timeline-height: 170px;
.card-stack {
position: absolute;
@@ -9,11 +11,17 @@ $card-width: 500px;
right: 10px;
max-height: calc(100% - 208px);
height: auto;
overflow: auto;
overflow: hidden;
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
z-index: $header;
color: white;
-webkit-font-smoothing: antialiased;
&.narrative-mode {
right: auto;
left: 10px;
top: $narrative-info-max-height + 12px;
height: calc(100% - #{$narrative-info-max-height} - #{$timeline-height} - 12px);
}
&.full-height {
max-height: calc(100% - 20px);

View File

@@ -1,3 +1,5 @@
$narrative-info-width: 370px;
/*
NARRATIVE INFO
*/
@@ -5,8 +7,9 @@ NARRATIVE INFO
position: fixed;
top: 10px;
left: 10px;
height: auto;
width: 370px;
// height: auto;
height: 170px;
width: $narrative-info-width;
box-sizing: border-box;
padding: 15px;
max-height: calc(100% - 250px);

View File

@@ -1,4 +1,3 @@
.timeline-wrapper {
position: fixed;
box-sizing: border-box;
@@ -67,6 +66,10 @@
}
.timeline-info {
&.hidden {
display: none;
}
width: calc(#{$card-width} - 20px);
position: absolute;
margin-top: -70px;
margin-left: 10px;

View File

@@ -1,28 +1,32 @@
import { createSelector} from 'reselect'
import { parseTimestamp, compareTimestamp, insetSourceFrom } from '../js/utilities'
// 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 getNarratives = state => state.domain.narratives;
export const getSelected = state => state.app.selected;
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 getActiveNarrative = state => state.app.narrative
export const getActiveStep = state => state.app.narrativeState.current
export const getSelected = state => state.app.selected
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 getSources = state => {
if (process.env.features.USE_SOURCES) return state.domain.sources;
return [];
if (process.env.features.USE_SOURCES) return state.domain.sources
return []
}
export const getNotifications = state => state.domain.notifications;
export const getTagTree = state => state.domain.tags;
export const getTagsFilter = state => state.app.filters.tags;
export const getTimeRange = state => state.app.filters.timerange;
export const getNotifications = state => state.domain.notifications
export const getTagTree = state => state.domain.tags
export const getTagsFilter = state => state.app.filters.tags
export const getTimeRange = state => state.app.filters.timerange
/**
* Some handy helpers
*/
const parseTimestamp = ts => d3.timeParse("%Y-%m-%dT%H:%M:%S")(ts);
/**
* Given an event and all tags,
@@ -30,11 +34,11 @@ const parseTimestamp = ts => d3.timeParse("%Y-%m-%dT%H:%M:%S")(ts);
*/
function isTaggedIn(event, tagFilters) {
if (event.tags) {
const tagsInEvent = event.tags.split(",");
const tagsInEvent = event.tags.split(",")
const isTagged = tagsInEvent.some((tag) => {
return tagFilters.find(tF => (tF.key === tag && tF.active));
});
return isTagged;
return tagFilters.find(tF => (tF.key === tag && tF.active))
})
return isTagged
} else {
return false
}
@@ -48,7 +52,7 @@ function isNoTags(tagFilters) {
tagFilters.length === 0
|| !process.env.features.USE_TAGS
|| tagFilters.every(t => !t.active)
);
)
}
/**
@@ -59,7 +63,7 @@ function isTimeRangedIn(event, timeRange) {
return (
timeRange[0] < parseTimestamp(event.timestamp)
&& parseTimestamp(event.timestamp) < timeRange[1]
);
)
}
/**
@@ -71,64 +75,79 @@ export const selectEvents = createSelector(
(events, tagFilters, timeRange) => {
return events.reduce((acc, event) => {
const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters);
const isTimeRanged = isTimeRangedIn(event, timeRange);
const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters)
const isTimeRanged = isTimeRangedIn(event, timeRange)
if (isTimeRanged && isTagged) {
const eventClone = Object.assign({}, event);
acc[event.id] = eventClone;
const eventClone = Object.assign({}, event)
acc[event.id] = eventClone
}
return acc;
}, []);
});
return acc
}, [])
})
/**
* 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
*/
export const selectNarratives = createSelector(
[getEvents, getNarratives, getTagsFilter, getTimeRange],
(events, narrativeMetadata, tagFilters, timeRange) => {
[getEvents, getNarratives, getTagsFilter, getTimeRange, getSources],
(events, narrativesMeta, tagFilters, timeRange, sources) => {
const narratives = {};
events.forEach((evt) => {
const isTagged = isTaggedIn(evt, tagFilters) || isNoTags(tagFilters);
const isTimeRanged = isTimeRangedIn(evt, timeRange);
const isInNarrative = evt.narratives.length > 0;
const narratives = {}
const narrativeSkeleton = id => ({ id, steps: [] })
evt.narratives.map(narrative => {
if (!narratives[narrative]) {
narratives[narrative] = { id: narrative, steps: [], byId: {} };
}
/* populate narratives dict with events */
events.forEach(evt => {
const isTagged = isTaggedIn(evt, tagFilters) || isNoTags(tagFilters)
const isTimeRanged = isTimeRangedIn(evt, timeRange)
const isInNarrative = evt.narratives.length > 0
if (isInNarrative) {
narratives[narrative].steps.push(evt);
narratives[narrative].byId[evt.id] = { next: null, prev: null };
}
evt.narratives.forEach(narrative => {
// initialise
if (!narratives[narrative])
narratives[narrative] = narrativeSkeleton(narrative)
// add evt to steps
if (isInNarrative)
// NB: insetSourceFrom is a 'curried' function to allow with maps
narratives[narrative].steps.push(insetSourceFrom(sources)(evt))
})
});
})
Object.keys(narratives).forEach((key) => {
const steps = narratives[key].steps;
steps.sort((a, b) => {
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp));
});
/* sort steps by time */
Object.keys(narratives).forEach(key => {
const steps = narratives[key].steps
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;
});
steps.sort(compareTimestamp)
if (narrativeMetadata.find(n => n.id === key)) {
narratives[key] = Object.assign(narrativeMetadata.find(n => n.id === key), narratives[key]);
// 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 (narrativesMeta.find(n => n.id === key)) {
narratives[key] = {
...narrativesMeta.find(n => n.id === key),
...narratives[key]
}
}
});
})
return Object.values(narratives);
});
return Object.values(narratives)
})
/** Aggregate information about the narrative and the current step into
* a single object. If narrative is null, the whole object is null.
*/
export const selectActiveNarrative = createSelector(
[getActiveNarrative, getActiveStep],
(narrative, current) => !!narrative
? { ...narrative, current }
: null
)
/**
* Of all the filtered events, group them by location and return a list of
* locations with at least one event in it, based on the time range and tags
@@ -137,12 +156,12 @@ export const selectLocations = createSelector(
[selectEvents],
(events) => {
const selectedLocations = {};
const selectedLocations = {}
events.forEach(event => {
const location = event.location;
const location = event.location
if (selectedLocations[location]) {
selectedLocations[location].events.push(event);
selectedLocations[location].events.push(event)
} else {
selectedLocations[location] = {
label: location,
@@ -153,9 +172,11 @@ export const selectLocations = createSelector(
}
})
return Object.values(selectedLocations);
return Object.values(selectedLocations)
}
);
)
/**
* Of all the sources, select those that are relevant to the selected events.
@@ -167,21 +188,7 @@ export const selectSelected = createSelector(
return []
}
// NB: return source object if exists, otherwise null
const srcs = selected
.map(e => e.sources)
.map(_sources => {
if (!_sources) return [];
return _sources.map(id => (
sources.hasOwnProperty(id) ? sources[id] : null
))
}
)
return selected.map((s, idx) => ({
...s,
sources: srcs[idx]
}))
return selected.map(insetSourceFrom(sources))
}
)
@@ -191,7 +198,7 @@ export const selectSelected = createSelector(
export const selectCategories = createSelector(
[getCategories],
(categories) => categories
);
)
/**
@@ -201,23 +208,23 @@ export const selectCategories = createSelector(
export const selectTagList = createSelector(
[getTagTree],
(tags) => {
const tagList = [];
let depth = 0;
const tagList = []
let depth = 0
function traverseNode(node, depth) {
node.active = (!node.hasOwnProperty('active')) ? false : node.active;
node.depth = depth;
node.active = (!node.hasOwnProperty('active')) ? false : node.active
node.depth = depth
if (node.active) tagList.push(node)
if (Object.keys(node.children).length > 0) {
Object.values(node.children).forEach((childNode) => {
traverseNode(childNode, depth + 1);
});
traverseNode(childNode, depth + 1)
})
}
}
if (tags && tags !== undefined) {
if (tags.key && tags.children) traverseNode(tags, depth)
}
return tagList;
return tagList
}
)

View File

@@ -33,6 +33,9 @@ const initial = {
selected: [],
source: null,
narrative: null,
narrativeState: {
current: null
},
filters: {
timerange: [
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2013-02-23T12:00:00"),