From 17a81a7643de75e5462c33bb8dfd7b89f831560a Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Wed, 12 Dec 2018 16:38:15 +0000 Subject: [PATCH 01/20] Revert "Refactor Timeline component and timeline, mapping logic" --- src/js/map/map.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 41b1bb6..78c2e4e 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -125,16 +125,6 @@ Stop and start the development process in terminal after you have added your tok } function getSVGBoundaries() { - const mapNode = d3.select('.leaflet-map-pane').node(); - - // We'll get the transform of the leaflet container, - // which will let us offset the SVG by the same quantity - const transform = window - .getComputedStyle(mapNode) - .getPropertyValue('transform'); - - // However getComputedStyle returns an awkward string of the format - // matrix(0, 0, 1, 0, 0.56523, 123123), hence this awkwardness return { transformX: +transform.split(',')[4], transformY: +transform.split(',')[5].split(')')[0] @@ -147,8 +137,7 @@ Stop and start the development process in terminal after you have added your tok let WIDTH = boundingClient.width; let HEIGHT = boundingClient.height; - // Offset with leaflet map transform boundaries - const { transformX, transformY } = getSVGBoundaries(); + g.attr('transform', `translate(${-(topLeft.x - 100)},${-(topLeft.y - 100)})`); svg.attr('width', WIDTH) .attr('height', HEIGHT) @@ -161,9 +150,14 @@ Stop and start the development process in terminal after you have added your tok g.selectAll('.narrative') .attr('d', sequenceLine); + + const busLine = d3.line() + .x(d => lMap.latLngToLayerPoint(d).x) + .y(d => lMap.latLngToLayerPoint(d).y) + .curve(d3.curveMonotoneX); } - lMap.on("zoomend viewreset moveend", updateSVG); + lMap.on("zoom viewreset move", updateSVG); /** * Returns latitud / longitude From 58aaadc5d43299692adf3e7413cbb94a342a313e Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 13 Dec 2018 11:07:24 +0100 Subject: [PATCH 02/20] Hide toolbar on narrative mode --- src/components/Card.jsx | 4 +- src/components/Dashboard.jsx | 3 - src/components/NarrativeCard.js | 7 +- src/components/Timeline.jsx | 4 +- src/components/Toolbar.jsx | 3 +- src/components/Viewport.jsx | 6 +- src/js/map/map.js | 32 +++++-- src/scss/map.scss | 6 +- src/scss/narrativecard.scss | 8 +- src/scss/timeline.scss | 4 + src/scss/toolbar.scss | 154 +++----------------------------- 11 files changed, 71 insertions(+), 160 deletions(-) diff --git a/src/components/Card.jsx b/src/components/Card.jsx index b85a144..35def13 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -129,11 +129,11 @@ class Card extends React.Component { renderHeader() { return (
-
+
{this.renderTimestamp()} {this.renderLocation()}
- {/* {this.renderCategory()} */} + {this.renderCategory()}
{this.renderSummary()}
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 479be0a..c2873bd 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -109,9 +109,6 @@ class Dashboard extends React.Component { onSelect={this.handleSelect} actions={this.props.actions} /> - {this.renderClose()} -
{this.props.narrative.label}
+

{this.props.narrative.label}

{this.props.narrative.description}

-

{this.state.step + 1}/{steps.length}. {step.location}

+
+ location_on + {this.state.step + 1}/{steps.length}. {step.location} +
this.goToPrevKeyFrame()}>←
= this.props.narrative.steps.length - 1) ? 'disabled ' : ''} action`} onClick={() => this.goToNextKeyFrame()}>→
diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 273fc98..ff4d089 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -47,6 +47,7 @@ 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' : ''}`; + classes += this.props.app.narrative ? 'narrative-mode' : ''; const date0 = formatterWithYear(this.props.app.timerange[0]); const date1 = formatterWithYear(this.props.app.timerange[1]); @@ -80,7 +81,8 @@ function mapStateToProps(state) { timerange: selectors.getTimeRange(state), selected: state.app.selected, language: state.app.language, - zoomLevels: state.app.zoomLevels + zoomLevels: state.app.zoomLevels, + narrative: state.app.narrative }, dom: state.ui.dom, } diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 70fcabc..087dab8 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -175,7 +175,7 @@ class Toolbar extends React.Component { render() { return ( -
+
{this.renderToolbarTabs()} {this.renderToolbarPanels()}
@@ -193,6 +193,7 @@ function mapStateToProps(state) { categoryFilter: state.app.filters.categories, viewFilters: state.app.filters.views, features: state.app.features, + narrative: state.app.narrative, } } diff --git a/src/components/Viewport.jsx b/src/components/Viewport.jsx index 3bcc808..b712f5e 100644 --- a/src/components/Viewport.jsx +++ b/src/components/Viewport.jsx @@ -19,8 +19,9 @@ class Viewport extends React.Component { } render() { + const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper'; return ( -
+
) @@ -39,7 +40,8 @@ function mapStateToProps(state) { views: state.app.filters.views, selected: state.app.selected, highlighted: state.app.highlighted, - mapAnchor: state.app.mapAnchor + mapAnchor: state.app.mapAnchor, + narrative: state.app.narrative }, ui: { dom: state.ui.dom, diff --git a/src/js/map/map.js b/src/js/map/map.js index 78c2e4e..4a4c68d 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -17,6 +17,7 @@ export default function(newApp, ui, methods) { const app = { selected: [], highlighted: null, + narrative: null, views: Object.assign({}, newApp.views), } @@ -362,7 +363,6 @@ Stop and start the development process in terminal after you have added your tok .exit() .remove(); - let styleName narrativesDom .enter().append('path') .attr('class', 'narrative') @@ -384,7 +384,24 @@ Stop and start the development process in terminal after you have added your tok const styleProps = getNarrativeStyle(d[0].narrative); return styleProps.stroke; }) + .style('stroke-opacity', d => { + if (app.narrative === null) return 0; + if (!d[0] || app.narrative.id !== d[0].narrative) return 0.2; + return 1; + }) .style('fill', 'none'); + + narrativesDom + .style('stroke', d => { + if (!d[0]) return 'none'; + const styleProps = getNarrativeStyle(d[0].narrative); + return styleProps.stroke; + }) + .style('stroke-opacity', d => { + if (app.narrative === null) return 0; + if (!d[0] || app.narrative.id !== d[0].narrative) return 0.2; + return 1; + }) } /** @@ -393,22 +410,25 @@ Stop and start the development process in terminal after you have added your tok */ function update(newDomain, newApp) { updateSVG(); + const isNewDomain = (hash(domain) !== hash(newDomain)); + const isNewAppProps = (hash(app) !== hash(newApp)); - if (hash(domain) !== hash(newDomain)) { + if (isNewDomain) { domain.locations = newDomain.locations; domain.narratives = newDomain.narratives; domain.categories = newDomain.categories; domain.sites = newDomain.sites; - renderDomain(); } - if (hash(app) !== hash(newApp)) { + if (isNewAppProps) { app.selected = newApp.selected; app.highlighted = newApp.highlighted; + app.narrative = newApp.narrative; app.views = newApp.views; - - renderSelectedAndHighlight(); } + + if (isNewDomain || isNewAppProps) renderDomain(); + if (isNewAppProps) renderSelectedAndHighlight(); } /** diff --git a/src/scss/map.scss b/src/scss/map.scss index d2c51cf..fc4f2ea 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -27,8 +27,12 @@ z-index: $hidden; } &.show { - z-index: $map; + z-index: $map; } + &.narrative-mode { + left: 0; + } + .event { fill: $event_default; cursor: pointer; diff --git a/src/scss/narrativecard.scss b/src/scss/narrativecard.scss index 3f7f7a5..b8e5e11 100644 --- a/src/scss/narrativecard.scss +++ b/src/scss/narrativecard.scss @@ -4,9 +4,9 @@ NARRATIVE INFO .narrative-info { position: fixed; top: 10px; - left: 130px; + left: 10px; height: auto; - width: 270px; + width: 370px; box-sizing: border-box; padding: 15px; max-height: calc(100% - 250px); @@ -23,6 +23,10 @@ NARRATIVE INFO h3 { font-size: $large; + font-family: 'Merriweather', 'Georgia', serif; + letter-spacing: 0.1em; + text-transform: uppercase; + font-weight: 100; } p { diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index 6e8cddf..2205ce1 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -22,6 +22,10 @@ } } + &.narrative-mode { + left: 0; + } + .timeline-header { height: 0px; width: 100%; diff --git a/src/scss/toolbar.scss b/src/scss/toolbar.scss index 9459f1d..d6178f6 100644 --- a/src/scss/toolbar.scss +++ b/src/scss/toolbar.scss @@ -9,6 +9,10 @@ z-index: $header; background: $midgrey; + &.narrative-mode { + left: -110px; + } + .toolbar { position: relative; width: 110px; @@ -292,37 +296,6 @@ } } - .people-tab { - width: 50%; - font-family: 'Lato', Helvetica, sans-serif; - font-size: $normal; - text-transform: uppercase; - letter-spacing: 0.1em; - - svg { - transform: translate(-2px,0)scale(0.6); - &:hover { - transition: 0.2s ease; - stroke: $offwhite; - } - } - - &.react-tabs__tab--selected { - svg circle, - svg path { - stroke: $offwhite; - } - } - - svg circle, - svg path { - transition: 0.2s ease; - fill: none; - stroke: $midwhite; - stroke-width: 3; - } - } - .react-tabs__tab-list { height: 40px; overflow: hidden; @@ -362,6 +335,14 @@ height: 0; margin: 0; } + + .panel-header { + visibility: hidden; + + .caret { + transform: translate(8px, 5px)rotate(225deg); + } + } } input { @@ -491,7 +472,7 @@ } } - &:first-child { + /*&:first-child { button { background-image: url("/static/archive/img/scene01.jpg"); } } &:nth-child(2n) { @@ -503,114 +484,7 @@ &.back-to-map { button { background-image: url("/static/archive/img/map.jpg"); } - } - } -} - -.taggroup-wrapper { - margin-top: 30px; - z-index: 10; - border-bottom: none; - - &:last-child { - margin-bottom: 0; - border-bottom: 1px solid rgba(white, 0); - } - - &:hover { - transition: 0.1s ease; - } - - .collapsible-item { - width: calc(100% - 32px); - float: left; - } - - .taggroup-header { - width: 100%; - margin: 0; - font-size: $large; - - h2::first-letter { - margin-top: 0; - } - } - - .taggroup-content { - width: 100%; - display: inline-block; - padding-left: 10px; - box-sizing: border-box; - transition: 0.2s ease; - - .tagsubgroup-wrapper { - border: none; - border-bottom: 1px solid rgba(white, 0.25); - &:first-letter { - text-transform: uppercase; - } - - &:last-child { - border-bottom: 0; - } - - .tagsubgroup-header { - cursor: pointer; - } - - &.folded { - .tagsubgroup-content { - overflow: hidden; - padding: 0 10px; - transition: 0.2s ease; - height: 0; - border-top: 0; - } - } - - .item { - overflow: auto; - min-height: 32px; - height: auto; - - span { - height: auto; - } - } - } - - .tag-filter { - outline: none; - border: 0; - background: none; - color: $offwhite; - margin-left: 20px; - width: calc(100% - 20px); - box-sizing: border-box; - padding: 0; - font-size: $normal; - font-weight: 400; - text-align: left; - cursor: pointer; - border: 1px solid $black; - border-bottom: 1px solid rgba(white, 0.25); - &:first-letter { - text-transform: uppercase; - } - - &:last-child { - border-bottom: 1px solid rgba(white, 0); - } - } - } - - &.folded { - .filter-list-content { - padding: 0 10px; - border-top: 0; - transition: 0.2s ease; - height: 0; - } + }*/ } } From 6dee9fee2fa8f2fda2d7eda133c4db40cf33b742 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 13 Dec 2018 11:37:57 +0100 Subject: [PATCH 03/20] Select only updateNarrative as action to pass to Toolbar --- src/components/CardStack.jsx | 11 ----------- src/components/Dashboard.jsx | 31 ++++++++++++++++--------------- src/components/Toolbar.jsx | 2 +- src/reducers/app.js | 2 +- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/components/CardStack.jsx b/src/components/CardStack.jsx index bb47d71..4e7f8b0 100644 --- a/src/components/CardStack.jsx +++ b/src/components/CardStack.jsx @@ -36,17 +36,6 @@ class CardStack extends React.Component { return ''; } - renderLocation() { - let locationName = copy[this.props.language].cardstack.unknown_location; - if (this.props.selected.length > 0) { - if (isNotNullNorUndefined(this.props.selected[0].location)) { - locationName = this.props.selected[0].location; - } - return (

in:{` ${locationName}`}

) - } - return ''; - } - renderCardStackHeader() { const header_lang = copy[this.props.language].cardstack.header; diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index c2873bd..0f400db 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -76,23 +76,17 @@ class Dashboard extends React.Component { render() { return (
+ { this.props.actions.updateNarrative(narrative); }} + actions={this.props.actions} + /> this.getCategoryColor(category) }} /> - - this.props.actions.updateSelected([])} - getNarrativeLinks={event => this.getNarrativeLinks(event)} - getCategoryColor={category => this.getCategoryColor(category)} - /> this.getCategoryColor(category) }} /> + { this.props.actions.updateNarrative(narrative); }} + /> + this.props.actions.updateSelected([])} + getNarrativeLinks={event => this.getNarrativeLinks(event)} + getCategoryColor={category => this.getCategoryColor(category)} + /> this.props.actions.toggleInfoPopup()} /> - { - this.props.actions.updateNarrative(narrative); + this.props.onSelectNarrative(narrative); }); } diff --git a/src/reducers/app.js b/src/reducers/app.js index 7fe01be..5353466 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -43,7 +43,7 @@ function updateNarrative(appState, action) { // Add some margin to the datetime extent minDate = minDate - ((maxDate - minDate) / 20); maxDate = maxDate + ((maxDate - minDate) / 20); - + return appState; return Object.assign({}, appState, { narrative: action.narrative, filters: Object.assign({}, appState.filters, { From 6d06ab8ce400208db465d6c68dee37ff8ce6bc4d Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 13 Dec 2018 12:30:05 +0100 Subject: [PATCH 04/20] Align transform of svg --- src/js/map/map.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 4a4c68d..e7b3ae0 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -126,6 +126,16 @@ Stop and start the development process in terminal after you have added your tok } function getSVGBoundaries() { + const mapNode = d3.select('.leaflet-map-pane').node(); + + // We'll get the transform of the leaflet container, + // which will let us offset the SVG by the same quantity + const transform = window + .getComputedStyle(mapNode) + .getPropertyValue('transform'); + + // However getComputedStyle returns an awkward string of the format + // matrix(0, 0, 1, 0, 0.56523, 123123), hence this awkwardness return { transformX: +transform.split(',')[4], transformY: +transform.split(',')[5].split(')')[0] @@ -138,7 +148,8 @@ Stop and start the development process in terminal after you have added your tok let WIDTH = boundingClient.width; let HEIGHT = boundingClient.height; - g.attr('transform', `translate(${-(topLeft.x - 100)},${-(topLeft.y - 100)})`); + // Offset with leaflet map transform boundaries + const { transformX, transformY } = getSVGBoundaries(); svg.attr('width', WIDTH) .attr('height', HEIGHT) @@ -146,19 +157,18 @@ Stop and start the development process in terminal after you have added your tok g.selectAll('.location').attr('transform', (d) => { const newPoint = projectPoint([+d.latitude, +d.longitude]); - return `translate(${newPoint.x},${newPoint.y})`; + return `translate(${newPoint.x + transformX},${newPoint.y + transformY})`; }); + const sequenceLine = d3.line() + .x(d => getCoords(d).x + transformX) + .y(d => getCoords(d).y + transformY); + g.selectAll('.narrative') .attr('d', sequenceLine); - - const busLine = d3.line() - .x(d => lMap.latLngToLayerPoint(d).x) - .y(d => lMap.latLngToLayerPoint(d).y) - .curve(d3.curveMonotoneX); } - lMap.on("zoom viewreset move", updateSVG); + lMap.on("zoomend viewreset moveend", updateSVG); /** * Returns latitud / longitude From 3e9b621655f79b3f17432542b36521444d5ea7ba Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 13 Dec 2018 13:18:42 +0100 Subject: [PATCH 05/20] Clean up location transform in map --- src/components/Timeline.jsx | 46 +--- .../presentational/TimelineHeader.js | 15 ++ src/js/map/map.js | 7 +- src/js/timeline/timeline.js | 222 ++++++++++-------- 4 files changed, 161 insertions(+), 129 deletions(-) create mode 100644 src/components/presentational/TimelineHeader.js diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index ff4d089..e2139a5 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -4,6 +4,7 @@ import * as selectors from '../selectors'; import copy from '../js/data/copy.json'; import { formatterWithYear } from '../js/utilities'; +import TimelineHeader from './presentational/TimelineHeader'; import TimelineLogic from '../js/timeline/timeline.js'; class Timeline extends React.Component { @@ -15,18 +16,12 @@ class Timeline extends React.Component { } componentDidMount() { - const ui = { - dom: this.props.dom - } - - this.timeline = new TimelineLogic(this.props.app, ui, this.props.methods); + this.timeline = new TimelineLogic(this.props.app, this.props.ui, this.props.methods); this.timeline.update(this.props.domain, this.props.app); - this.timeline.render(this.props.domain); } componentWillReceiveProps(nextProps) { this.timeline.update(nextProps.domain, nextProps.app); - this.timeline.render(nextProps.domain); } onClickArrow() { @@ -35,35 +30,19 @@ class Timeline extends React.Component { }); } - renderLabels() { - const labels = copy[this.props.language].timeline.labels; - return this.props.categories.map((label) => { - const groupLen = this.props.categories.length - return (
{label}
); - }); - } - render() { - 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' : ''}`; - classes += this.props.app.narrative ? 'narrative-mode' : ''; - const date0 = formatterWithYear(this.props.app.timerange[0]); - const date1 = formatterWithYear(this.props.app.timerange[1]); return (
-
-
this.onClickArrow()}> -

-
-
-

{info_lang}

-

{date0} - {date1}

-
-
+ { this.onClickArrow(); }} + />
-
+
); @@ -81,10 +60,11 @@ function mapStateToProps(state) { timerange: selectors.getTimeRange(state), selected: state.app.selected, language: state.app.language, - zoomLevels: state.app.zoomLevels, - narrative: state.app.narrative + zoomLevels: state.app.zoomLevels }, - dom: state.ui.dom, + ui: { + dom: state.ui.dom, + } } } diff --git a/src/components/presentational/TimelineHeader.js b/src/components/presentational/TimelineHeader.js new file mode 100644 index 0000000..d75a605 --- /dev/null +++ b/src/components/presentational/TimelineHeader.js @@ -0,0 +1,15 @@ +import React from 'react'; + +const TimelineHeader = ({ title, date0, date1, onClick }) => ( +
+
onClick()}> +

+
+
+

{title}

+

{date0} - {date1}

+
+
+); + +export default TimelineHeader; diff --git a/src/js/map/map.js b/src/js/map/map.js index e7b3ae0..aedeb3e 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -157,7 +157,7 @@ Stop and start the development process in terminal after you have added your tok g.selectAll('.location').attr('transform', (d) => { const newPoint = projectPoint([+d.latitude, +d.longitude]); - return `translate(${newPoint.x + transformX},${newPoint.y + transformY})`; + return `translate(${newPoint.x},${newPoint.y})`; }); const sequenceLine = d3.line() @@ -279,9 +279,8 @@ Stop and start the development process in terminal after you have added your tok .enter().append('g') .attr('class', 'location') .attr('transform', (d) => { - d.LatLng = new L.LatLng(+d.latitude, +d.longitude); - return `translate(${lMap.latLngToLayerPoint(d.LatLng).x}, - ${lMap.latLngToLayerPoint(d.LatLng).y})`; + const newPoint = projectPoint([+d.latitude, +d.longitude]); + return `translate(${newPoint.x},${newPoint.y})`; }) .on('click', (location) => { methods.onSelect(location.events); diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 8a009e9..3598c94 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -9,54 +9,49 @@ import { parseDate, formatterWithYear } from '../utilities'; +import hash from 'object-hash'; import esLocale from '../data/es-MX.json'; import copy from '../data/copy.json'; -export default function(app, ui, methods) { +export default function(newApp, ui, methods) { d3.timeFormatDefaultLocale(esLocale); - const zoomLevels = app.zoomLevels; - let events = []; - let categories = []; - let selected = []; - let timerange = app.timerange; + const domain = { + events: [], + categories: [], + } + const app = { + selected: [], + highlighted: null, + zoomLevels: newApp.zoomLevels, + timerange: newApp.timerange, + language: newApp.language + } + + // Dimension of the client + const WIDTH_CONTROLS = 100; + const HEIGHT = 140; + const boundingClient = d3.select(`#${ui.dom.timeline}`).node().getBoundingClientRect(); + let WIDTH = boundingClient.width - WIDTH_CONTROLS; + + // Highlight events with a larger white ring marker + const markerRadius = 15; + + // NB: is it possible to do this with SCSS? + // A: Maybe, although we are using it programmatically here for now + const margin = { left: 120 }; // Drag behavior let dragPos0; let transitionDuration = 500; - // Dimension of the client - const WIDTH_CONTROLS = 100; - const boundingClient = d3.select(`#${ui.dom.timeline}`).node().getBoundingClientRect(); - let WIDTH = boundingClient.width - WIDTH_CONTROLS; - const HEIGHT = 140; - const markerRadius = 15; - // margin - // NB: is it possible to do this with SCSS? - // A: Maybe, although we are using it programmatically here for now - const mg = { - l: 120 - }; - /** * Create scales */ const scale = {}; - - scale.x = d3.scaleTime() - .domain(timerange) - .range([mg.l, WIDTH]); - - // calculate group step between categories - const groupStep = (106 - 30) / categories.length; - const groupYs = new Array(categories.length); - groupYs.map((g, i) => { - return 30 + i * groupStep; - }); - + scale.x = d3.scaleTime(); scale.y = d3.scaleOrdinal() - .domain(categories) - .range(groupYs); + /** * Initilize SVG elements and groups @@ -76,10 +71,10 @@ export default function(app, ui, methods) { .attr('width', WIDTH_CONTROLS) .attr('height', HEIGHT); + /* * Axis group elements */ - dom.axis = {}; dom.axis.x0 = dom.svg.append('g') @@ -105,11 +100,14 @@ export default function(app, ui, methods) { dom.axis.label1 = dom.svg.append('text') .attr('class', 'timelabelF timeLabel'); + /* * Plottable elements */ dom.dataset = dom.svg.append('g'); dom.events = dom.dataset.append('g'); + dom.markers = dom.svg.append('g'); + /* * Time Controls @@ -125,10 +123,11 @@ export default function(app, ui, methods) { dom.zooms = dom.controls.append('g'); dom.zooms.selectAll('.zoom-level-button') - .data(zoomLevels) + .data(app.zoomLevels) .enter().append('text') .attr('class', 'zoom-level-button'); + /* * Initialize axis function and element group */ @@ -152,37 +151,8 @@ export default function(app, ui, methods) { d3.axisLeft(scale.y) .tickValues([]); - /* - * Setup drag behavior - */ - const drag = - d3.drag() - .on('start', () => { - d3.event.sourceEvent.stopPropagation(); - dragPos0 = d3.event.x; - toggleTransition(false); - }) - .on('drag', () => { - const drag0 = scale.x.invert(dragPos0).getTime(); - const dragNow = scale.x.invert(d3.event.x).getTime(); - const timeShift = (drag0 - dragNow) / 1000; + updateAxis(); - const newDomain0 = d3.timeSecond.offset(timerange[0], timeShift); - const newDomainF = d3.timeSecond.offset(timerange[1], timeShift); - - scale.x.domain([newDomain0, newDomainF]) - render(); - }) - .on('end', () => { - toggleTransition(true); - methods.onUpdateTimerange(scale.x.domain()); - }); - - /* - * SVG groups for marker - */ - - dom.markers = dom.svg.append('g'); /** * Adapt dimensions when resizing @@ -192,6 +162,7 @@ export default function(app, ui, methods) { .getBoundingClientRect().width; } + /** * Resize timeline one window resice */ @@ -201,8 +172,8 @@ export default function(app, ui, methods) { WIDTH = getCurrentWidth() - WIDTH_CONTROLS; dom.svg.attr('width', WIDTH); - scale.x.range([mg.l, WIDTH]); - axis.y.tickSize(WIDTH - mg.l); + scale.x.range([margin.left, WIDTH]); + axis.y.tickSize(WIDTH - margin.left); dom.axis.y.attr('transform', `translate(${WIDTH}, 0)`) render(null); } @@ -210,6 +181,7 @@ export default function(app, ui, methods) { } addResizeListener(); + /** * Return which color event circle should be based on incident type * @param {object} eventPoint data object @@ -218,6 +190,7 @@ export default function(app, ui, methods) { return methods.getCategoryColor(eventPoint.category); } + /** * Given an event, get all the filtered events that happen simultaneously * @param {object} eventPoint: regular eventPoint data @@ -225,10 +198,11 @@ export default function(app, ui, methods) { function getAllEventsAtOnce(eventPoint) { const timestamp = eventPoint.timestamp; const category = eventPoint.category; - return events + return domain.events .filter(event => (event.timestamp === timestamp && category === event.category)) } + /* * Get y height of eventPoint, considering the ordinal Y scale * @param {object} eventPoint: regular eventPoint data @@ -238,6 +212,7 @@ export default function(app, ui, methods) { return scale.y(yGroup); } + /* * Get x position of eventPoint, considering the time scale * @param {object} eventPoint: regular eventPoint data @@ -250,6 +225,7 @@ export default function(app, ui, methods) { return (scale.x.domain()[1].getTime() - scale.x.domain()[0].getTime()) / 60000; } + /* * Given a number of minutes, calculate the width based on current scale.x * @param {number} minutes: number of minutes @@ -259,8 +235,12 @@ export default function(app, ui, methods) { return (minutes * WIDTH) / allMins; } + + /* + * TODO: Highlight zoom level based on time range selected + */ function highlightZoomLevel(zoom) { - zoomLevels.forEach((level) => { + app.zoomLevels.forEach((level) => { if (level.label === zoom.label) { level.active = true; } else { @@ -272,6 +252,7 @@ export default function(app, ui, methods) { .classed('active', level => level.active); } + /** * Apply zoom level to timeline * @param {object} zoom: zoom level from zoomLevels @@ -289,6 +270,7 @@ export default function(app, ui, methods) { methods.onUpdateTimerange(scale.x.domain()); } + /** * Shift time range by moving forward or backwards * @param {String} direction: 'forward' / 'backwards' @@ -316,6 +298,33 @@ export default function(app, ui, methods) { transitionDuration = (isTransition) ? 500 : 0; } + + /* + * Setup drag behavior + */ + const drag = d3.drag() + .on('start', () => { + d3.event.sourceEvent.stopPropagation(); + dragPos0 = d3.event.x; + toggleTransition(false); + }) + .on('drag', () => { + const drag0 = scale.x.invert(dragPos0).getTime(); + const dragNow = scale.x.invert(d3.event.x).getTime(); + const timeShift = (drag0 - dragNow) / 1000; + + const newDomain0 = d3.timeSecond.offset(app.timerange[0], timeShift); + const newDomainF = d3.timeSecond.offset(app.timerange[1], timeShift); + + scale.x.domain([newDomain0, newDomainF]); + render(); + }) + .on('end', () => { + toggleTransition(true); + methods.onUpdateTimerange(scale.x.domain()); + }); + + /** * Highlight event circle on hover */ @@ -325,6 +334,7 @@ export default function(app, ui, methods) { .classed('mouseover', true); } + /** * Unhighlight event when mouse out */ @@ -334,11 +344,12 @@ export default function(app, ui, methods) { .classed('mouseover', false); } + /** * It automatically sets brush timeline to a domain set by the params */ function updateTimeRange() { - scale.x.domain(timerange); + scale.x.domain(app.timerange); axis.x0.scale(scale.x); axis.x1.scale(scale.x); } @@ -351,23 +362,24 @@ export default function(app, ui, methods) { dom.axis.label0 .attr('x', 5) .attr('y', 15) - .text(formatterWithYear(timerange[0])); + .text(formatterWithYear(app.timerange[0])); dom.axis.label1 .attr('x', WIDTH - 5) .attr('y', 15) - .text(formatterWithYear(timerange[1])) + .text(formatterWithYear(app.timerange[1])) .style('text-anchor', 'end'); } + /** - * Makes a circular rinig mark in one particular location at a time + * Makes a circular ring mark in all selected events * @param {object} eventPoint: object with eventPoint data (time, loc, tags) */ function renderHighlight() { const markers = dom.markers .selectAll('circle') - .data(selected); + .data(app.selected); markers .enter() @@ -382,13 +394,14 @@ export default function(app, ui, methods) { markers.exit().remove(); } + /** * Return event circles of different groups */ function renderEvents() { const eventsDom = dom.events .selectAll('.event') - .data(events, d => d.id); + .data(domain.events, d => d.id); eventsDom .exit() @@ -417,6 +430,7 @@ export default function(app, ui, methods) { .attr('r', 5); } + /** * Render axis on timeline and viewbox boundaries */ @@ -437,7 +451,7 @@ export default function(app, ui, methods) { .duration(transitionDuration) .call(axis.x1); - axis.y.tickSize(WIDTH - mg.l); + axis.y.tickSize(WIDTH - margin.left); dom.axis.y .call(axis.y) @@ -453,12 +467,13 @@ export default function(app, ui, methods) { .attr('x', scale.x.range()[1] - 5); } + /** * Render left and right time shifting controls */ function renderTimeControls() { const zoomLabels = copy[app.language].timeline.zooms; - zoomLevels.forEach((level, i) => { + app.zoomLevels.forEach((level, i) => { level.label = zoomLabels[i]; }); @@ -495,49 +510,72 @@ export default function(app, ui, methods) { .on('click', zoom => applyZoom(zoom)); } + /** * Updates data displayed by this timeline, but only render if necessary * @param {Object} domain: Redux state domain subtree * @param {Object} app: Redux state app subtree */ - function updateAxis(domain) { - const categories = domain.categories - const groupStep = (106 - 30) / categories.length; - let groupYs = Array.apply(null, Array(categories.length)); + function updateAxis() { + scale.x = d3.scaleTime() + .domain(app.timerange) + .range([margin.left, WIDTH]); + + const groupStep = (106 - 30) / domain.categories.length; + let groupYs = Array.apply(null, Array(domain.categories.length)); groupYs = groupYs.map((g, i) => { return 30 + i * groupStep; }); scale.y = d3.scaleOrdinal() - .domain(categories) + .domain(domain.categories) .range(groupYs); axis.y = d3.axisLeft(scale.y) - .tickValues(categories.map(c => c.category)); + .tickValues(domain.categories.map(c => c.category)); } - function update(domain, app) { - updateAxis(domain); - renderAxis(); - events = domain.events; - timerange = app.timerange; - selected = app.selected.slice(0); - updateTimeRange(); + /** + * Updates displayable data on the timeline: events, selected and + * potentially adjusts time range + * @param {Object} newDomain: object of arrays of events and categories + * @param {Object} newApp: object of time range and selected events + */ + function update(newDomain, newApp) { + if (hash(domain) !== hash(newDomain)) { + domain.categories = newDomain.categories; + domain.events = newDomain.events; + updateAxis(); + renderContext(); + } + if (hash(app) !== hash(newApp)) { + app.timerange = newApp.timerange; + app.selected = newApp.selected.slice(0); + updateTimeRange(); + renderTimeLabels(); + renderEventsAndHighlight(); + } } - function render() { + function renderContext() { renderAxis(); renderTimeControls(); renderTimeLabels(); + } + function renderEventsAndHighlight() { renderEvents(); renderHighlight(); } + function render() { + renderContext(); + renderEventsAndHighlight(); + } + return { update, - render, }; } From 0bc4ddc54e33c82f02c03402721059cc79423465 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 14 Dec 2018 09:44:38 +0100 Subject: [PATCH 06/20] Remove unnecessary renders in map / timeline and style narrative mode --- src/components/NarrativeCard.js | 3 ++- src/components/Timeline.jsx | 12 ++++++++---- src/components/Toolbar.jsx | 4 +++- src/components/Viewport.jsx | 6 +++++- src/js/map/map.js | 3 ++- src/js/timeline/timeline.js | 34 +++++++++++++++++++-------------- src/reducers/app.js | 3 ++- src/store/initial.js | 14 ++++++++++---- 8 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/components/NarrativeCard.js b/src/components/NarrativeCard.js index fb7e4ae..2d16550 100644 --- a/src/components/NarrativeCard.js +++ b/src/components/NarrativeCard.js @@ -26,6 +26,7 @@ class NarrativeCard extends React.Component { componentDidUpdate() { if (this.props.narrative !== null) { const step = this.props.narrative.steps[this.state.step]; + console.log(step) this.props.onSelect([step]); } } @@ -34,7 +35,7 @@ class NarrativeCard extends React.Component { return ( diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index e2139a5..ae42fb2 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; import * as selectors from '../selectors'; +import hash from 'object-hash'; import copy from '../js/data/copy.json'; -import { formatterWithYear } from '../js/utilities'; +import { formatterWithYear, isNotNullNorUndefined } from '../js/utilities'; import TimelineHeader from './presentational/TimelineHeader'; import TimelineLogic from '../js/timeline/timeline.js'; @@ -21,7 +22,9 @@ class Timeline extends React.Component { } componentWillReceiveProps(nextProps) { - this.timeline.update(nextProps.domain, nextProps.app); + if (hash(nextProps) !== hash(this.props)) { + this.timeline.update(nextProps.domain, nextProps.app); + } } onClickArrow() { @@ -32,7 +35,7 @@ class Timeline extends React.Component { render() { let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`; - + classes += (this.props.app.narrative !== null) ? ' narrative-mode' : ''; return (
+
{this.renderToolbarTabs()} {this.renderToolbarPanels()}
diff --git a/src/components/Viewport.jsx b/src/components/Viewport.jsx index b712f5e..49f772d 100644 --- a/src/components/Viewport.jsx +++ b/src/components/Viewport.jsx @@ -1,6 +1,8 @@ import React from 'react' import { connect } from 'react-redux' import * as selectors from '../selectors' +import hash from 'object-hash'; + import Map from '../js/map/map.js' import { areEqual } from '../js/utilities.js' @@ -15,7 +17,9 @@ class Viewport extends React.Component { } componentWillReceiveProps(nextProps) { - this.map.update(nextProps.domain, nextProps.app) + if (hash(nextProps) !== hash(this.props)) { + this.map.update(nextProps.domain, nextProps.app) + } } render() { diff --git a/src/js/map/map.js b/src/js/map/map.js index aedeb3e..019a70d 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -430,10 +430,11 @@ Stop and start the development process in terminal after you have added your tok } if (isNewAppProps) { + app.views = newApp.views; app.selected = newApp.selected; app.highlighted = newApp.highlighted; + app.mapAnchor = newApp.mapAnchor; app.narrative = newApp.narrative; - app.views = newApp.views; } if (isNewDomain || isNewAppProps) renderDomain(); diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 3598c94..98d44ac 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -19,13 +19,13 @@ export default function(newApp, ui, methods) { const domain = { events: [], categories: [], + narratives: [] } const app = { - selected: [], - highlighted: null, - zoomLevels: newApp.zoomLevels, timerange: newApp.timerange, - language: newApp.language + selected: [], + language: newApp.language, + zoomLevels: newApp.zoomLevels } // Dimension of the client @@ -517,6 +517,8 @@ export default function(newApp, ui, methods) { * @param {Object} app: Redux state app subtree */ function updateAxis() { + updateTimeRange(); + scale.x = d3.scaleTime() .domain(app.timerange) .range([margin.left, WIDTH]); @@ -544,35 +546,39 @@ export default function(newApp, ui, methods) { * @param {Object} newApp: object of time range and selected events */ function update(newDomain, newApp) { - if (hash(domain) !== hash(newDomain)) { + const isNewDomain = (hash(domain) !== hash(newDomain)); + const isNewAppProps = (hash(app) !== hash(newApp)); + + if (isNewDomain) { domain.categories = newDomain.categories; domain.events = newDomain.events; - updateAxis(); - renderContext(); + domain.narratives = newDomain.narratives; } - if (hash(app) !== hash(newApp)) { + + if (isNewAppProps) { app.timerange = newApp.timerange; app.selected = newApp.selected.slice(0); - updateTimeRange(); - renderTimeLabels(); - renderEventsAndHighlight(); } + + if (isNewDomain || isNewAppProps) renderContent(); + if (isNewAppProps) renderContext(); } function renderContext() { - renderAxis(); renderTimeControls(); renderTimeLabels(); } - function renderEventsAndHighlight() { + function renderContent() { + updateAxis(); + renderAxis(); renderEvents(); renderHighlight(); } function render() { renderContext(); - renderEventsAndHighlight(); + renderContent(); } return { diff --git a/src/reducers/app.js b/src/reducers/app.js index 5353466..4fc0dc5 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -32,6 +32,7 @@ function updateSelected(appState, action) { } function updateNarrative(appState, action) { + console.log('this happens') if (action.narrative === null) { return Object.assign({}, appState, { narrative: action.narrative, @@ -43,7 +44,7 @@ function updateNarrative(appState, action) { // Add some margin to the datetime extent minDate = minDate - ((maxDate - minDate) / 20); maxDate = maxDate + ((maxDate - minDate) / 20); - return appState; + return Object.assign({}, appState, { narrative: action.narrative, filters: Object.assign({}, appState.filters, { diff --git a/src/store/initial.js b/src/store/initial.js index 03d8fef..11c6e60 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -115,11 +115,17 @@ const initial = { narratives: { default: { - style: 'solid', // ['dotted', 'solid'] - opacity: 0.5, // range between 0 and 1 - stroke: 'transparent', // Any hex or rgb code - strokeWidth: 2 + style: 'dotted', // ['dotted', 'solid'] + opacity: 0.9, // range between 0 and 1 + stroke: 'red', // Any hex or rgb code + strokeWidth: 3 }, + narrative_1: { + style: 'solid', // ['dotted', 'solid'] + opacity: 0.4, // range between 0 and 1 + stroke: '#f18f01', // Any hex or rgb code + strokeWidth: 3 + } } }, dom: { From 4423e792ec47bdb948b4bc4214a78929cc8ee67a Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 14 Dec 2018 17:00:31 +0100 Subject: [PATCH 07/20] Make selectors compute narratives as arrays --- src/components/NarrativeCard.js | 1 - src/reducers/schema/eventSchema.js | 2 +- src/selectors/index.js | 19 +++++++++++-------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/NarrativeCard.js b/src/components/NarrativeCard.js index 2d16550..3a41abb 100644 --- a/src/components/NarrativeCard.js +++ b/src/components/NarrativeCard.js @@ -26,7 +26,6 @@ class NarrativeCard extends React.Component { componentDidUpdate() { if (this.props.narrative !== null) { const step = this.props.narrative.steps[this.state.step]; - console.log(step) this.props.onSelect([step]); } } diff --git a/src/reducers/schema/eventSchema.js b/src/reducers/schema/eventSchema.js index b497841..614eba2 100644 --- a/src/reducers/schema/eventSchema.js +++ b/src/reducers/schema/eventSchema.js @@ -11,7 +11,7 @@ const eventSchema = Joi.object().keys({ longitude: Joi.string().allow('').required(), type: Joi.string().allow(''), category: Joi.string().required(), - narrative: Joi.string().allow(''), + narratives: Joi.array(), sources: Joi.array(), tags: Joi.string().allow(''), comments: Joi.string().allow(''), diff --git a/src/selectors/index.js b/src/selectors/index.js index 32a4e49..30d65a9 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -95,16 +95,18 @@ export const selectNarratives = createSelector( events.forEach((evt) => { const isTagged = isTaggedIn(evt, tagFilters) || isNoTags(tagFilters); const isTimeRanged = isTimeRangedIn(evt, timeRange); - const isInNarrative = evt.narrative; + const isInNarrative = evt.narratives.length > 0; - if (!narratives[evt.narrative]) { - narratives[evt.narrative] = { id: evt.narrative, steps: [], byId: {} }; - } + evt.narratives.map(narrative => { + if (!narratives[narrative]) { + narratives[narrative] = { id: narrative, steps: [], byId: {} }; + } - if (/*isTimeRanged && isTagged && */isInNarrative) { - narratives[evt.narrative].steps.push(evt); - narratives[evt.narrative].byId[evt.id] = { next: null, prev: null }; - } + if (isInNarrative) { + narratives[narrative].steps.push(evt); + narratives[narrative].byId[evt.id] = { next: null, prev: null }; + } + }) }); Object.keys(narratives).forEach((key) => { @@ -161,6 +163,7 @@ export const selectLocations = createSelector( export const selectSelected = createSelector( [getSelected, getSources], (selected, sources) => { + console.log(selected, sources) if (selected.length === 0) { return [] } From 40ac0eb12048308100bbc4f453be4419d5fe4324 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 14 Dec 2018 17:21:36 +0100 Subject: [PATCH 08/20] Only rerender NarrativeCard if step has changed --- src/components/Dashboard.jsx | 11 +++++++---- src/components/NarrativeCard.js | 11 ++++++++--- src/reducers/app.js | 1 - src/selectors/index.js | 1 - 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 0f400db..286be0d 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -94,10 +94,13 @@ class Dashboard extends React.Component { getCategoryColor: category => this.getCategoryColor(category) }} /> - { this.props.actions.updateNarrative(narrative); }} - /> + {(this.props.app.narrative !== null) + ? { this.props.actions.updateNarrative(narrative); }} + /> + : '' + } { - console.log(selected, sources) if (selected.length === 0) { return [] } From 7b25efda957fce6e8a2fc7354a799786d4d5df92 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 14 Dec 2018 17:28:28 +0100 Subject: [PATCH 09/20] Apply correct narrative styles in map --- src/js/map/map.js | 4 ++-- src/store/initial.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 019a70d..3ae5bed 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -395,7 +395,7 @@ Stop and start the development process in terminal after you have added your tok }) .style('stroke-opacity', d => { if (app.narrative === null) return 0; - if (!d[0] || app.narrative.id !== d[0].narrative) return 0.2; + if (!d[0] || !d[0].narratives.find(n => n === app.narrative.id)) return 0.2; return 1; }) .style('fill', 'none'); @@ -408,7 +408,7 @@ Stop and start the development process in terminal after you have added your tok }) .style('stroke-opacity', d => { if (app.narrative === null) return 0; - if (!d[0] || app.narrative.id !== d[0].narrative) return 0.2; + if (!d[0] || !d[0].narratives.find(n => n === app.narrative.id)) return 0.2; return 1; }) } diff --git a/src/store/initial.js b/src/store/initial.js index 11c6e60..3121edc 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -115,7 +115,7 @@ const initial = { narratives: { default: { - style: 'dotted', // ['dotted', 'solid'] + style: 'solid', // ['dotted', 'solid'] opacity: 0.9, // range between 0 and 1 stroke: 'red', // Any hex or rgb code strokeWidth: 3 From 288aff5c15343aa1c2919e3dc8c4b74826e1fe5f Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Sun, 16 Dec 2018 09:00:22 +0100 Subject: [PATCH 10/20] Style narrative based the one selected --- src/components/Toolbar.jsx | 5 +++-- src/js/map/map.js | 41 +++++++++++++++++++++---------------- src/js/utilities.js | 7 +++++++ src/scss/map.scss | 6 +++++- src/scss/narrativecard.scss | 7 +++++++ src/scss/toolbar.scss | 11 +++++++--- 6 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 36c8672..f2606fd 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -7,7 +7,7 @@ import Search from './Search.jsx'; import TagListPanel from './TagListPanel.jsx'; import ToolbarBottomActions from './ToolbarBottomActions.jsx'; import copy from '../js/data/copy.json'; -import { isNotNullNorUndefined } from '../js/utilities.js'; +import { isNotNullNorUndefined, trimAndEllipse } from '../js/utilities.js'; class Toolbar extends React.Component { @@ -66,7 +66,7 @@ class Toolbar extends React.Component {
) @@ -100,6 +100,7 @@ class Toolbar extends React.Component { return (
{ this.toggleTab(tabNum); }}> + timeline
{label}
); diff --git a/src/js/map/map.js b/src/js/map/map.js index 3ae5bed..ab0eeef 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -165,7 +165,7 @@ Stop and start the development process in terminal after you have added your tok .y(d => getCoords(d).y + transformY); g.selectAll('.narrative') - .attr('d', sequenceLine); + .attr('d', d => sequenceLine(d.steps)); } lMap.on("zoomend viewreset moveend", updateSVG); @@ -365,8 +365,8 @@ Stop and start the development process in terminal after you have added your tok } function renderNarratives() { - const narrativesDom = g.selectAll('.narrative') - .data(domain.narratives.map(d => d.steps)) + const narrativesDom = svg.selectAll('.narrative') + .data(domain.narratives) narrativesDom .exit() @@ -375,40 +375,45 @@ Stop and start the development process in terminal after you have added your tok narrativesDom .enter().append('path') .attr('class', 'narrative') - .attr('d', sequenceLine) + .attr('d', d => sequenceLine(d.steps)) .style('stroke-width', d => { - 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); + if (!d) return 0; + const styleProps = getNarrativeStyle(d.id); return styleProps.strokeWidth; }) .style('stroke-dasharray', d => { - if (!d[0]) return 'none'; - const styleProps = getNarrativeStyle(d[0].narrative); + if (!d) return 'none'; + const styleProps = getNarrativeStyle(d.id); return (styleProps.style === 'dotted') ? "2px 5px" : 'none'; }) .style('stroke', d => { - if (!d[0]) return 'none'; - const styleProps = getNarrativeStyle(d[0].narrative); + if (!d || app.narrative === null) return 'none'; + if (d.id !== app.narrative.id) return '#232323'; + const styleProps = getNarrativeStyle(d.id); return styleProps.stroke; }) .style('stroke-opacity', d => { if (app.narrative === null) return 0; - if (!d[0] || !d[0].narratives.find(n => n === app.narrative.id)) return 0.2; + if (!d || d.id !== app.narrative.id) return 0.2; return 1; }) - .style('fill', 'none'); + .style('fill', 'none') + .style('cursor', 'pointer') + .on('click', d => { + console.log(d) + }); narrativesDom + .attr('d', d => sequenceLine(d.steps)) .style('stroke', d => { - if (!d[0]) return 'none'; - const styleProps = getNarrativeStyle(d[0].narrative); + if (!d || app.narrative === null) return 'none'; + if (d.id !== app.narrative.id) return '#232323'; + const styleProps = getNarrativeStyle(d.id); return styleProps.stroke; }) .style('stroke-opacity', d => { if (app.narrative === null) return 0; - if (!d[0] || !d[0].narratives.find(n => n === app.narrative.id)) return 0.2; + if (!d || d.id !== app.narrative.id) return 0.2; return 1; }) } @@ -446,8 +451,8 @@ Stop and start the development process in terminal after you have added your tok */ function renderDomain () { renderSites(); - renderNarratives(); renderEvents(); + renderNarratives(); } function renderSelectedAndHighlight () { renderSelected(); diff --git a/src/js/utilities.js b/src/js/utilities.js index c51596c..e9692e2 100644 --- a/src/js/utilities.js +++ b/src/js/utilities.js @@ -44,6 +44,13 @@ export function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } +export function trimAndEllipse(string, stringNum) { + if (string.length > stringNum) { + return string.substring(0, 120) + '...' + } + return string; +} + /** * Return a Date object given a datetime string of the format: "2016-09-10T07:00:00" * @param {string} datetime diff --git a/src/scss/map.scss b/src/scss/map.scss index fc4f2ea..421d44f 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -32,13 +32,17 @@ &.narrative-mode { left: 0; } - + .event { fill: $event_default; cursor: pointer; opacity: 0.45; } + .narrative { + cursor: pointer; + } + .link { stroke: $midgrey; fill: none; diff --git a/src/scss/narrativecard.scss b/src/scss/narrativecard.scss index b8e5e11..ace0435 100644 --- a/src/scss/narrativecard.scss +++ b/src/scss/narrativecard.scss @@ -29,6 +29,13 @@ NARRATIVE INFO font-weight: 100; } + h6 { + margin: 10px 0; + i { + font-size: $normal; + } + } + p { font-family: 'Lato', 'Helvetica', sans-serif; font-size: $normal; diff --git a/src/scss/toolbar.scss b/src/scss/toolbar.scss index d6178f6..445c927 100644 --- a/src/scss/toolbar.scss +++ b/src/scss/toolbar.scss @@ -168,6 +168,7 @@ display: flex; align-items: center; justify-content: center; + flex-direction: column; height: 60px; width: 110px; padding: 5px 0 5px 0; @@ -454,7 +455,7 @@ height: 140px; line-height: 140px; width: 100%; - padding: 0; + padding: 10px; border: 1px solid $offwhite; background-size: 100%; color: $offwhite; @@ -470,6 +471,10 @@ transition: 0.2s ease; letter-spacing: 0.15em; } + + p { + text-transform: none; + } } /*&:first-child { @@ -515,10 +520,10 @@ height: 60px; padding: 0; - .tab-caption { + /*.tab-caption { transition: 0.2s ease; opacity: 0; - } + }*/ &:hover { .tab-caption { From 4e5cd8a7fb583c24f2fe479eff1b3878c8d490d7 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Sun, 16 Dec 2018 09:04:52 +0100 Subject: [PATCH 11/20] Change display order, put narrative under events --- src/js/map/map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index ab0eeef..2a5ca10 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -451,8 +451,8 @@ Stop and start the development process in terminal after you have added your tok */ function renderDomain () { renderSites(); + renderNarratives(); renderEvents(); - renderNarratives(); } function renderSelectedAndHighlight () { renderSelected(); From 31fd3ac5071a19c6fa68081ffe87459d97119767 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Sun, 16 Dec 2018 11:51:49 +0100 Subject: [PATCH 12/20] Refine update time on drag, and clip hidden events --- src/js/map/map.js | 2 +- src/js/timeline/timeline.js | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 2a5ca10..5de5fbe 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -451,7 +451,7 @@ Stop and start the development process in terminal after you have added your tok */ function renderDomain () { renderSites(); - renderNarratives(); + renderNarratives(); renderEvents(); } function renderSelectedAndHighlight () { diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 98d44ac..042ea21 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -49,7 +49,10 @@ export default function(newApp, ui, methods) { * Create scales */ const scale = {}; - scale.x = d3.scaleTime(); + scale.x = d3.scaleTime() + .domain(app.timerange) + .range([margin.left, WIDTH]); + scale.y = d3.scaleOrdinal() @@ -64,6 +67,14 @@ export default function(newApp, ui, methods) { .attr('width', WIDTH) .attr('height', HEIGHT); + dom.clip = dom.svg.append("svg:clipPath") + .attr("id", "clip") + .append("svg:rect") + .attr("x", margin.left) + .attr("y", 0) + .attr("width", WIDTH - margin.left) + .attr("height", HEIGHT - 25); + dom.controls = d3.select(`#${ui.dom.timeline}`) .append('svg') @@ -104,9 +115,10 @@ export default function(newApp, ui, methods) { /* * Plottable elements */ - dom.dataset = dom.svg.append('g'); - dom.events = dom.dataset.append('g'); - dom.markers = dom.svg.append('g'); + + dom.body = dom.svg.append("g").attr("clip-path", "url(#clip)"); + dom.events = dom.body.append('g'); + dom.markers = dom.body.append('g'); /* @@ -321,6 +333,7 @@ export default function(newApp, ui, methods) { }) .on('end', () => { toggleTransition(true); + app.timerange = scale.x.domain(); methods.onUpdateTimerange(scale.x.domain()); }); @@ -517,12 +530,6 @@ export default function(newApp, ui, methods) { * @param {Object} app: Redux state app subtree */ function updateAxis() { - updateTimeRange(); - - scale.x = d3.scaleTime() - .domain(app.timerange) - .range([margin.left, WIDTH]); - const groupStep = (106 - 30) / domain.categories.length; let groupYs = Array.apply(null, Array(domain.categories.length)); groupYs = groupYs.map((g, i) => { From bcc70a57f68d3b6327751fcdcbb17fad5e5dd4a4 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Sun, 16 Dec 2018 12:09:14 +0100 Subject: [PATCH 13/20] Update narrative paths on move, zoom --- src/js/map/map.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 5de5fbe..2d56977 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -160,11 +160,7 @@ Stop and start the development process in terminal after you have added your tok return `translate(${newPoint.x},${newPoint.y})`; }); - const sequenceLine = d3.line() - .x(d => getCoords(d).x + transformX) - .y(d => getCoords(d).y + transformY); - - g.selectAll('.narrative') + svg.selectAll('.narrative') .attr('d', d => sequenceLine(d.steps)); } @@ -237,9 +233,7 @@ Stop and start the development process in terminal after you have added your tok function getLocationEventsDistribution(location) { const eventCount = {}; const categories = domain.categories; - // categories.sort((a, b) => { - // return (+a.slice(-2) > +b.slice(-2)); - // }); + categories.forEach(cat => { eventCount[cat.category] = 0 }); From 470ef656174fdcfc131754051afff471e555a3b2 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Sun, 16 Dec 2018 13:01:04 +0100 Subject: [PATCH 14/20] Add arrowheads to narrative lines --- src/components/Toolbar.jsx | 4 ++-- src/js/data/copy.json | 4 +++- src/js/map/map.js | 30 ++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index f2606fd..75826c5 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -59,8 +59,8 @@ class Toolbar extends React.Component { renderToolbarNarrativePanel() { return ( -

Focus stories

-

Here are some highlighted stories

+

{copy[this.props.language].toolbar.narrative_panel_title}

+

{copy[this.props.language].toolbar.narrative_summary}

{this.props.narratives.map((narr) => { return (
diff --git a/src/js/data/copy.json b/src/js/data/copy.json index 2feca2b..21702e0 100644 --- a/src/js/data/copy.json +++ b/src/js/data/copy.json @@ -103,7 +103,9 @@ "title": "Directory of tags", "placeholder": "Search" } - } + }, + "narrative_panel_title": "Focus narratives", + "narrative_summary": "Here you can follow some curated stories we have found in the data." }, "timeline": { "zooms": [ diff --git a/src/js/map/map.js b/src/js/map/map.js index 2d56977..827810d 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -98,10 +98,24 @@ Stop and start the development process in terminal after you have added your tok .attr('viewBox', '0 0 6 6') .attr('refX', 3) .attr('refY', 3) - .attr('markerWidth', 14) - .attr('markerHeight', 14) + .attr('markerWidth', 6) + .attr('markerHeight', 6) .attr('orient', 'auto') .append('path') + .style('fill', 'red') + .attr('d', 'M0,3v-3l6,3l-6,3z'); + + svg.insert('defs', 'g') + .append('marker-off') + .attr('id', 'arrow-off') + .attr('viewBox', '0 0 6 6') + .attr('refX', 3) + .attr('refY', 3) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .style('fill', 'black') .attr('d', 'M0,3v-3l6,3l-6,3z'); map.on('zoomstart', () => { @@ -366,6 +380,12 @@ Stop and start the development process in terminal after you have added your tok .exit() .remove(); + const getMarker = (d) => { + if (!d || app.narrative === null) return 'none'; + if (d.id !== app.narrative.id) return 'url(#arrow-off)'; + return 'url(#arrow)'; + } + narrativesDom .enter().append('path') .attr('class', 'narrative') @@ -393,12 +413,18 @@ Stop and start the development process in terminal after you have added your tok }) .style('fill', 'none') .style('cursor', 'pointer') + .attr('marker-start', d => getMarker(d)) + .attr('marker-end', d => getMarker(d)) + .attr('mid-marker', d => getMarker(d)) .on('click', d => { console.log(d) }); narrativesDom .attr('d', d => sequenceLine(d.steps)) + .attr('marker-start', d => getMarker(d)) + .attr('marker-end', d => getMarker(d)) + .attr('mid-marker', d => getMarker(d)) .style('stroke', d => { if (!d || app.narrative === null) return 'none'; if (d.id !== app.narrative.id) return '#232323'; From 028429a975351b8fc15a3a61d6cda5ff0ac43517 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Mon, 17 Dec 2018 08:20:37 +0100 Subject: [PATCH 15/20] Style arrow markers with current narrative style --- src/js/map/map.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/map/map.js b/src/js/map/map.js index 827810d..dc89045 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -380,6 +380,11 @@ Stop and start the development process in terminal after you have added your tok .exit() .remove(); + if (app.narrative !== null) { + d3.selectAll('#arrow path') + .style('fill', getNarrativeStyle(app.narrative.id).stroke); + } + const getMarker = (d) => { if (!d || app.narrative === null) return 'none'; if (d.id !== app.narrative.id) return 'url(#arrow-off)'; From ce18bfb5ea0e7147a9b72e187aa7f4aeb0389acb Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 17 Dec 2018 11:51:06 +0000 Subject: [PATCH 16/20] temp fix to render domain error visible --- src/actions/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/actions/index.js b/src/actions/index.js index 1264aa2..24d2a6d 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -97,7 +97,6 @@ export function fetchDomain () { sourcesPromise ]) .then(response => { - dispatch(toggleFetchingDomain()) const result = { events: response[0], categories: response[1], @@ -107,11 +106,16 @@ export function fetchDomain () { sources: response[5], notifications } + if (Object.values(result).some(resp => resp.hasOwnProperty('error'))) { + throw new Error('Some URLs returned negative. If you are in development, check the server is running') + } return result }) .catch(err => { dispatch(fetchError(err.message)) dispatch(toggleFetchingDomain()) + // TODO: handle this appropriately in React hierarchy + alert(err.message) }) }; } From 8476b11409020210aec2217fc1ea134bcdb432bb Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Mon, 17 Dec 2018 16:07:30 +0100 Subject: [PATCH 17/20] Make non-selected narrative of their own color --- src/js/map/map.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index dc89045..8c40edc 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -57,7 +57,7 @@ export default function(newApp, ui, methods) { const map = L.map(id) .setView(center, zoom) .setMinZoom(10) - .setMaxZoom(18) + .setMaxZoom(19) .setMaxBounds(maxBoundaries) // NB: configure tile endpoint @@ -383,6 +383,9 @@ Stop and start the development process in terminal after you have added your tok if (app.narrative !== null) { d3.selectAll('#arrow path') .style('fill', getNarrativeStyle(app.narrative.id).stroke); + + d3.selectAll('.location-event-marker') + .style('fill-opacity', '0.1 !important') } const getMarker = (d) => { @@ -407,7 +410,6 @@ Stop and start the development process in terminal after you have added your tok }) .style('stroke', d => { if (!d || app.narrative === null) return 'none'; - if (d.id !== app.narrative.id) return '#232323'; const styleProps = getNarrativeStyle(d.id); return styleProps.stroke; }) @@ -432,7 +434,6 @@ Stop and start the development process in terminal after you have added your tok .attr('mid-marker', d => getMarker(d)) .style('stroke', d => { if (!d || app.narrative === null) return 'none'; - if (d.id !== app.narrative.id) return '#232323'; const styleProps = getNarrativeStyle(d.id); return styleProps.stroke; }) From f343135eedc5140c010de33f177aad58683673b5 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Tue, 18 Dec 2018 13:20:15 +0100 Subject: [PATCH 18/20] Render narratives as sequence of single lines --- src/js/map/map.js | 162 ++++++++++++++++++++++++++-------------------- src/scss/map.scss | 2 +- 2 files changed, 94 insertions(+), 70 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 8c40edc..b702f2e 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -106,7 +106,7 @@ Stop and start the development process in terminal after you have added your tok .attr('d', 'M0,3v-3l6,3l-6,3z'); svg.insert('defs', 'g') - .append('marker-off') + .append('marker') .attr('id', 'arrow-off') .attr('viewBox', '0 0 6 6') .attr('refX', 3) @@ -116,6 +116,7 @@ Stop and start the development process in terminal after you have added your tok .attr('orient', 'auto') .append('path') .style('fill', 'black') + .style('fill-opacity', 0.2) .attr('d', 'M0,3v-3l6,3l-6,3z'); map.on('zoomstart', () => { @@ -175,7 +176,7 @@ Stop and start the development process in terminal after you have added your tok }); svg.selectAll('.narrative') - .attr('d', d => sequenceLine(d.steps)); + .each((g, i, nodes) => { return updateNarrativeSteps(g, i, nodes); }); } lMap.on("zoomend viewreset moveend", updateSVG); @@ -315,6 +316,9 @@ Stop and start the development process in terminal after you have added your tok .transition() .duration(500) .attr('r', d => (d) ? Math.sqrt(16 * d) + 3 : 0); + + eventsDom.selectAll('.location-event-marker') + .style('fill-opacity', '0.1 !important'); } // NB: is this a function to be removed for future features? @@ -352,18 +356,11 @@ Stop and start the development process in terminal after you have added your tok } } - /*const sequenceLine = d3.line() - .x(d => getCoords(d).x) - .y(d => getCoords(d).y)*/ - - const sequenceLine = d3.line() - .x(d => getCoords(d).x + getSVGBoundaries().transformX) - .y(d => getCoords(d).y + getSVGBoundaries().transformY); - /** - * Clears existing narrative layer - * Renders all narrativ as paths - * Adds eventlayer to map - */ + /** + * Clears existing narrative layer + * Renders all narrativ as paths + * Adds eventlayer to map + */ function getNarrativeStyle(narrativeId) { const styleName = narrativeId && narrativeId in narrativeProps @@ -372,6 +369,12 @@ Stop and start the development process in terminal after you have added your tok return narrativeProps[styleName]; } + function getMarker (d) { + if (!d || app.narrative === null) return 'none'; + if (d.id === app.narrative.id) return 'url(#arrow)'; + return 'url(#arrow-off)'; + } + function renderNarratives() { const narrativesDom = svg.selectAll('.narrative') .data(domain.narratives) @@ -383,67 +386,88 @@ Stop and start the development process in terminal after you have added your tok if (app.narrative !== null) { d3.selectAll('#arrow path') .style('fill', getNarrativeStyle(app.narrative.id).stroke); - - d3.selectAll('.location-event-marker') - .style('fill-opacity', '0.1 !important') } - const getMarker = (d) => { - if (!d || app.narrative === null) return 'none'; - if (d.id !== app.narrative.id) return 'url(#arrow-off)'; - return 'url(#arrow)'; - } - - narrativesDom - .enter().append('path') + const narrativesEnter = narrativesDom + .enter().append('g') + .attr('id', d => 'narrative-' + d.id) .attr('class', 'narrative') - .attr('d', d => sequenceLine(d.steps)) - .style('stroke-width', d => { - if (!d) return 0; - const styleProps = getNarrativeStyle(d.id); - return styleProps.strokeWidth; - }) - .style('stroke-dasharray', d => { - if (!d) return 'none'; - const styleProps = getNarrativeStyle(d.id); - return (styleProps.style === 'dotted') ? "2px 5px" : 'none'; - }) - .style('stroke', d => { - if (!d || app.narrative === null) return 'none'; - const styleProps = getNarrativeStyle(d.id); - return styleProps.stroke; - }) - .style('stroke-opacity', d => { - if (app.narrative === null) return 0; - if (!d || d.id !== app.narrative.id) return 0.2; - return 1; - }) - .style('fill', 'none') - .style('cursor', 'pointer') - .attr('marker-start', d => getMarker(d)) - .attr('marker-end', d => getMarker(d)) - .attr('mid-marker', d => getMarker(d)) - .on('click', d => { - console.log(d) - }); - narrativesDom - .attr('d', d => sequenceLine(d.steps)) - .attr('marker-start', d => getMarker(d)) - .attr('marker-end', d => getMarker(d)) - .attr('mid-marker', d => getMarker(d)) - .style('stroke', d => { - if (!d || app.narrative === null) return 'none'; - const styleProps = getNarrativeStyle(d.id); - return styleProps.stroke; - }) - .style('stroke-opacity', d => { - if (app.narrative === null) return 0; - if (!d || d.id !== app.narrative.id) return 0.2; - return 1; - }) + narrativesDom.selectAll('.narrative') + .each((g, i, nodes) => { return updateNarrativeSteps(g, i, nodes); }); } + function updateNarrativeSteps(g, i, nodes) { + const n = d3.select(nodes[i]).data()[0]; + const allsteps = n.steps.slice(); + allsteps.push(n.steps[n.steps.length - 1]); + + const steps = d3.select(nodes[i]).selectAll('.narrative-step') + .data(n.steps) + + steps.enter().append('line') + .attr('class', 'narrative-step') + .attr('x1', d => getCoords(d).x + getSVGBoundaries().transformX) + .attr('x2', (d, j) => { return getCoords(allsteps[j + 1]).x + getSVGBoundaries().transformX; }) + .attr('y1', d => getCoords(d).y + getSVGBoundaries().transformY) + .attr('y2', (d, j) => { return getCoords(allsteps[j + 1]).y + getSVGBoundaries().transformY; }) + .style('stroke-width', d => { + if (!d) return 0; + const styleProps = getNarrativeStyle(n.id); + return styleProps.strokeWidth; + }) + .style('stroke-dasharray', d => { + if (!d) return 'none'; + const styleProps = getNarrativeStyle(n.id); + return (styleProps.style === 'dotted') ? "2px 5px" : 'none'; + }) + .style('stroke', d => { + if (!d || app.narrative === null) return 'none'; + const styleProps = getNarrativeStyle(n.id); + return styleProps.stroke; + }) + .style('stroke-opacity', d => { + if (app.narrative === null) return 0; + if (!d || d.id !== app.narrative.id) return 0.2; + return 1; + }) + .attr('marker-start', (d, j) => !j ? getMarker(n) : 'none') + .attr('marker-end', getMarker(n)) + .attr('mid-marker', getMarker(n)) + + steps + .attr('x1', d => getCoords(d).x + getSVGBoundaries().transformX) + .attr('x2', (d, j) => { return getCoords(allsteps[j + 1]).x + getSVGBoundaries().transformX; }) + .attr('y1', d => getCoords(d).y + getSVGBoundaries().transformY) + .attr('y2', (d, j) => { return getCoords(allsteps[j + 1]).y + getSVGBoundaries().transformY; }) + .style('stroke-width', d => { + if (!d) return 0; + const styleProps = getNarrativeStyle(n.id); + return styleProps.strokeWidth; + }) + .style('stroke-dasharray', d => { + if (!d) return 'none'; + const styleProps = getNarrativeStyle(n.id); + return (styleProps.style === 'dotted') ? "2px 5px" : 'none'; + }) + .style('stroke', d => { + if (!d || app.narrative === null) return 'none'; + const styleProps = getNarrativeStyle(n.id); + return styleProps.stroke; + }) + .style('stroke-opacity', d => { + if (app.narrative === null) return 0; + if (!d || n.id !== app.narrative.id) return 0.2; + return 1; + }) + .attr('marker-start', (d, j) => !j ? getMarker(n) : 'none') + .attr('marker-end', getMarker(n)) + .attr('mid-marker', getMarker(n)) + + steps + .exit() + .remove(); + } /** * Updates displayable data on the map: events, coevents and paths * @param {Object} domain: object of arrays of events, coevs, attacks, paths, sites diff --git a/src/scss/map.scss b/src/scss/map.scss index 421d44f..4a761f0 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -155,7 +155,7 @@ fill: $event_default; stroke-width: 0; transition: 0.2s ease; - fill-opacity: 0.8; + /*fill-opacity: 0.8;*/ cursor: pointer; &:hover { From 196abd27b026bbe0405296842e1787b9efd91c50 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Tue, 18 Dec 2018 13:38:59 +0100 Subject: [PATCH 19/20] Allow switching narratives by clicking on line --- src/components/Dashboard.jsx | 11 ++++++++--- src/js/map/map.js | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 286be0d..a462491 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -22,7 +22,7 @@ class Dashboard extends React.Component { this.handleHighlight = this.handleHighlight.bind(this); this.handleSelect = this.handleSelect.bind(this); - // this.handleToggle = this.handleToggle.bind(this); + this.handleSelectNarrative = this.handleSelectNarrative.bind(this); this.handleTagFilter = this.handleTagFilter.bind(this); this.updateTimerange = this.updateTimerange.bind(this); @@ -55,6 +55,10 @@ class Dashboard extends React.Component { } } + handleSelectNarrative(narrative) { + this.props.actions.updateNarrative(narrative); + } + handleTagFilter(tag) { this.props.actions.updateTagFilters(tag); } @@ -78,12 +82,13 @@ class Dashboard extends React.Component {
{ this.props.actions.updateNarrative(narrative); }} + onSelectNarrative={this.handleSelectNarrative} actions={this.props.actions} /> this.getCategoryColor(category) }} /> @@ -97,7 +102,7 @@ class Dashboard extends React.Component { {(this.props.app.narrative !== null) ? { this.props.actions.updateNarrative(narrative); }} + onSelectNarrative={this.handleSelectNarrative} /> : '' } diff --git a/src/js/map/map.js b/src/js/map/map.js index b702f2e..05739ce 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -377,7 +377,7 @@ Stop and start the development process in terminal after you have added your tok function renderNarratives() { const narrativesDom = svg.selectAll('.narrative') - .data(domain.narratives) + .data((app.narrative !== null) ? domain.narratives : []) narrativesDom .exit() @@ -434,6 +434,7 @@ Stop and start the development process in terminal after you have added your tok .attr('marker-start', (d, j) => !j ? getMarker(n) : 'none') .attr('marker-end', getMarker(n)) .attr('mid-marker', getMarker(n)) + .on('click', () => methods.onSelectNarrative(n) ) steps .attr('x1', d => getCoords(d).x + getSVGBoundaries().transformX) From 9e16271dbbf2fc7397367507110065f8a3cde50f Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Tue, 18 Dec 2018 15:37:39 +0100 Subject: [PATCH 20/20] Reset narrative to first event when switching between narratives --- src/components/NarrativeCard.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/NarrativeCard.js b/src/components/NarrativeCard.js index 080ae85..67a6bbc 100644 --- a/src/components/NarrativeCard.js +++ b/src/components/NarrativeCard.js @@ -32,6 +32,13 @@ class NarrativeCard extends React.Component { 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]); + }); } }