Ingesting config through Create React App

This commit is contained in:
Zac Ioannidis
2020-12-07 19:28:07 +00:00
parent 00d840a65b
commit 3a54cd7df5
18 changed files with 1401 additions and 458 deletions

View File

@@ -1,22 +1,23 @@
import moment from 'moment'
import hash from 'object-hash'
import moment from "moment";
import hash from "object-hash";
let { DATE_FMT, TIME_FMT } = process.env
if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY'
if (!TIME_FMT) TIME_FMT = 'HH:mm'
let { DATE_FMT, TIME_FMT } = process.env;
if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY";
if (!TIME_FMT) TIME_FMT = "HH:mm";
export const language = process.env.store.app.language || 'en-US'
console.log(process.env);
export const language = process.env.store.app.language || "en-US";
export function calcDatetime (date, time) {
if (!time) time = '00:00'
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`)
return dt.toDate()
export function calcDatetime(date, time) {
if (!time) time = "00:00";
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`);
return dt.toDate();
}
export function getCoordinatesForPercent (radius, percent) {
const x = radius * Math.cos(2 * Math.PI * percent)
const y = radius * Math.sin(2 * Math.PI * percent)
return [x, y]
export function getCoordinatesForPercent(radius, percent) {
const x = radius * Math.cos(2 * Math.PI * percent);
const y = radius * Math.sin(2 * Math.PI * percent);
return [x, y];
}
/**
@@ -26,32 +27,33 @@ export function getCoordinatesForPercent (radius, percent) {
*
* Return value:
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
export function zipColorsToPercentages (colors, percentages) {
if (colors.length < percentages.length) throw new Error('You must declare an appropriate number of filter colors')
export function zipColorsToPercentages(colors, percentages) {
if (colors.length < percentages.length)
throw new Error("You must declare an appropriate number of filter colors");
return percentages.reduce((map, percent, idx) => {
map[colors[idx]] = percent
return map
}, {})
map[colors[idx]] = percent;
return map;
}, {});
}
/**
* Get URI params to start with predefined set of
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
* @param {string} name: name of paramater to search
* @param {string} url: url passed as variable, defaults to window.location.href
*/
export function getParameterByName (name, url) {
if (!url) url = window.location.href
name = name.replace(/[[\]]/g, `\\$&`)
* Get URI params to start with predefined set of
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
* @param {string} name: name of paramater to search
* @param {string} url: url passed as variable, defaults to window.location.href
*/
export function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[[\]]/g, `\\$&`);
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
const results = regex.exec(url)
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
if (!results) return null
if (!results[2]) return ''
if (!results) return null;
if (!results[2]) return "";
return decodeURIComponent(results[2].replace(/\+/g, ' '))
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
/**
@@ -59,32 +61,35 @@ export function getParameterByName (name, url) {
* @param {array} arr1: array of numbers
* @param {array} arr2: array of numbers
*/
export function areEqual (arr1, arr2) {
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
return element === arr2[index]
}))
export function areEqual(arr1, arr2) {
return (
arr1.length === arr2.length &&
arr1.every((element, index) => {
return element === arr2[index];
})
);
}
/**
* Return whether the variable is neither null nor undefined
* @param {object} variable
*/
export function isNotNullNorUndefined (variable) {
return (typeof variable !== 'undefined' && variable !== null)
* Return whether the variable is neither null nor undefined
* @param {object} variable
*/
export function isNotNullNorUndefined(variable) {
return typeof variable !== "undefined" && variable !== null;
}
/*
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
*/
export function capitalize (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
*/
export function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function trimAndEllipse (string, stringNum) {
export function trimAndEllipse(string, stringNum) {
if (string.length > stringNum) {
return string.substring(0, 120) + '...'
return string.substring(0, 120) + "...";
}
return string
return string;
}
/**
@@ -94,71 +99,72 @@ export function trimAndEllipse (string, stringNum) {
* through every association's given path attribute to find its location.
*
* Returns the list of parents: ex. ['Chemical', 'Tear Gas', ...]
*/
export function getFilterParents (associations, filter) {
*/
export function getFilterParents(associations, filter) {
for (let a of associations) {
const { filter_paths: fp } = a
const { filter_paths: fp } = a;
if (a.id === filter) {
return fp.slice(0, fp.length - 1)
return fp.slice(0, fp.length - 1);
}
const filterIndex = fp.indexOf(filter)
if (filterIndex === 0) return []
if (filterIndex > 0) return fp.slice(0, filterIndex)
const filterIndex = fp.indexOf(filter);
if (filterIndex === 0) return [];
if (filterIndex > 0) return fp.slice(0, filterIndex);
}
throw new Error('Attempted to get parents of nonexistent filter')
throw new Error("Attempted to get parents of nonexistent filter");
}
/**
* Grabs the second to last element in the paths array for a given existing filter.
* This is the filter's most immediate ancestor.
*/
export function getImmediateFilterParent (associations, filter) {
const parents = getFilterParents(associations, filter)
if (parents.length === 0) return null
return parents[parents.length - 1]
*/
export function getImmediateFilterParent(associations, filter) {
const parents = getFilterParents(associations, filter);
if (parents.length === 0) return null;
return parents[parents.length - 1];
}
/**
* Grab a meta filter's siblings, by way of the the `filter_path` hierarcy.
*/
export function getMetaFilterSiblings (allFilters, filterParent, filterKey) {
const idxParent = allFilters.map(f => {
return f.filter_paths.reduceRight((acc, path, idx) => {
if (path === filterParent) return f.filter_paths[idx + 1]
return acc
}, null)
})
.filter(metaFilter => !!metaFilter && metaFilter !== filterKey)
return [ ...(new Set(idxParent)) ]
*/
export function getMetaFilterSiblings(allFilters, filterParent, filterKey) {
const idxParent = allFilters
.map((f) => {
return f.filter_paths.reduceRight((acc, path, idx) => {
if (path === filterParent) return f.filter_paths[idx + 1];
return acc;
}, null);
})
.filter((metaFilter) => !!metaFilter && metaFilter !== filterKey);
return [...new Set(idxParent)];
}
/**
* Grabs a given filter's siblings: the set of associations that share the same immediate filter parent.
*/
export function getFilterSiblings (allFilters, filterParent, filterKey) {
const isMetaFilter = !allFilters.map(filt => filt.id).includes(filterKey)
*/
export function getFilterSiblings(allFilters, filterParent, filterKey) {
const isMetaFilter = !allFilters.map((filt) => filt.id).includes(filterKey);
if (isMetaFilter) {
return getMetaFilterSiblings(allFilters, filterParent, filterKey)
return getMetaFilterSiblings(allFilters, filterParent, filterKey);
}
return allFilters.reduce((acc, val) => {
const valParent = getImmediateFilterParent(allFilters, val.id)
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id)
return acc
}, [])
const valParent = getImmediateFilterParent(allFilters, val.id);
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id);
return acc;
}, []);
}
export function getEventCategories (event, categories) {
const matchedCategories = []
export function getEventCategories(event, categories) {
const matchedCategories = [];
if (event.associations && event.associations.length > 0) {
event.associations.reduce((acc, val) => {
const foundCategory = categories.find(cat => cat.id === val)
if (foundCategory) acc.push(foundCategory)
return acc
}, matchedCategories)
const foundCategory = categories.find((cat) => cat.id === val);
if (foundCategory) acc.push(foundCategory);
return acc;
}, matchedCategories);
}
return matchedCategories
return matchedCategories;
}
/**
@@ -167,186 +173,201 @@ export function getEventCategories (event, categories) {
* source, call with two sets of parentheses:
* const src = insetSourceFrom(sources)(anEvent)
*/
export function insetSourceFrom (allSources) {
export function insetSourceFrom(allSources) {
return (event) => {
let sources
let sources;
if (!event.sources) {
sources = []
sources = [];
} else {
sources = event.sources.map(id => {
return allSources.hasOwnProperty(id) ? allSources[id] : null
})
sources = event.sources.map((id) => {
return allSources.hasOwnProperty(id) ? allSources[id] : null;
});
}
return {
...event,
sources
}
}
sources,
};
};
}
/**
* Debugging function: put in place of a mapStateToProps function to
* view that source modal by default
*/
export function injectSource (id) {
return state => {
export function injectSource(id) {
return (state) => {
return {
...state,
app: {
...state.app,
source: state.domain.sources[id]
}
}
}
source: state.domain.sources[id],
},
};
};
}
export function urlFromEnv (ext) {
export function urlFromEnv(ext) {
if (process.env[ext]) {
if (!Array.isArray(process.env[ext])) { return [`${process.env.SERVER_ROOT}${process.env[ext]}`] } else {
return process.env[ext].map(suffix => `${process.env.SERVER_ROOT}${suffix}`)
if (!Array.isArray(process.env[ext])) {
return [`${process.env.SERVER_ROOT}${process.env[ext]}`];
} else {
return process.env[ext].map(
(suffix) => `${process.env.SERVER_ROOT}${suffix}`
);
}
} else {
return null
return null;
}
}
export function toggleFlagAC (flag) {
export function toggleFlagAC(flag) {
return (appState) => ({
...appState,
flags: {
...appState.flags,
[flag]: !appState.flags[flag]
}
})
[flag]: !appState.flags[flag],
},
});
}
export function selectTypeFromPath (path) {
let type
export function selectTypeFromPath(path) {
let type;
switch (true) {
case /\.(png|jpg)$/.test(path):
type = 'Image'; break
type = "Image";
break;
case /\.(mp4)$/.test(path):
type = 'Video'; break
type = "Video";
break;
case /\.(md)$/.test(path):
type = 'Text'; break
type = "Text";
break;
default:
type = 'Unknown'; break
type = "Unknown";
break;
}
return { type, path }
return { type, path };
}
export function typeForPath (path) {
let type
path = path.trim()
export function typeForPath(path) {
let type;
path = path.trim();
switch (true) {
case /\.((png)|(jpg)|(jpeg))$/.test(path):
type = 'Image'; break
type = "Image";
break;
case /\.(mp4)$/.test(path):
type = 'Video'; break
type = "Video";
break;
case /\.(md)$/.test(path):
type = 'Text'; break
type = "Text";
break;
case /\.(pdf)$/.test(path):
type = 'Document'; break
type = "Document";
break;
default:
type = 'Unknown'; break
type = "Unknown";
break;
}
return type
return type;
}
export function selectTypeFromPathWithPoster (path, poster) {
return { type: typeForPath(path), path, poster }
export function selectTypeFromPathWithPoster(path, poster) {
return { type: typeForPath(path), path, poster };
}
export function isIdentical (obj1, obj2) {
return hash(obj1) === hash(obj2)
export function isIdentical(obj1, obj2) {
return hash(obj1) === hash(obj2);
}
export function calcOpacity (num) {
export function calcOpacity(num) {
/* Events have opacity 0.5 by default, and get added to according to how many
* other events there are in the same render. The idea here is that the
* overlaying of events builds up a 'heat map' of the event space, where
* darker areas represent more events with proportion */
const base = num >= 1 ? 0.9 : 0
return base + (Math.min(0.5, 0.08 * (num - 1)))
const base = num >= 1 ? 0.9 : 0;
return base + Math.min(0.5, 0.08 * (num - 1));
}
export function calcClusterOpacity (pointCount, totalPoints) {
export function calcClusterOpacity(pointCount, totalPoints) {
/* Clusters represent multiple events within a specific radius. The darker the cluster,
the larger the number of underlying events. We use a multiplication factor (50) here as well
to ensure that the larger clusters have an appropriately darker shading. */
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50)
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50);
}
export function calcClusterSize (pointCount, totalPoints) {
export function calcClusterSize(pointCount, totalPoints) {
/* The larger the cluster size, the higher the count of points that the cluster represents.
Just like with opacity, we use a multiplication factor to ensure that clusters with higher point
counts appear larger. */
const maxSize = totalPoints > 60 ? 40 : 20
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150)
const maxSize = totalPoints > 60 ? 40 : 20;
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150);
}
export function calculateTotalClusterPoints (clusters) {
export function calculateTotalClusterPoints(clusters) {
return clusters.reduce((total, cl) => {
if (cl && cl.properties && cl.properties.cluster) {
total += cl.properties.point_count
total += cl.properties.point_count;
}
return total
}, 0)
return total;
}, 0);
}
export function isLatitude (lat) {
return !!lat && isFinite(lat) && Math.abs(lat) <= 90
export function isLatitude(lat) {
return !!lat && isFinite(lat) && Math.abs(lat) <= 90;
}
export function isLongitude (lng) {
return !!lng && isFinite(lng) && Math.abs(lng) <= 180
export function isLongitude(lng) {
return !!lng && isFinite(lng) && Math.abs(lng) <= 180;
}
export function mapClustersToLocations (clusters, locations) {
export function mapClustersToLocations(clusters, locations) {
return clusters.reduce((acc, cl) => {
const foundLocation = locations.find(location => location.label === cl.properties.id)
if (foundLocation) acc.push(foundLocation)
return acc
}, [])
const foundLocation = locations.find(
(location) => location.label === cl.properties.id
);
if (foundLocation) acc.push(foundLocation);
return acc;
}, []);
}
/**
* Loops through a set of either locations or events
* and calculates the proportionate percentage of every given association in relation to the coloring set
*/
export function calculateColorPercentages (set, coloringSet) {
if (coloringSet.length === 0) return [1]
const associationMap = {}
*/
export function calculateColorPercentages(set, coloringSet) {
if (coloringSet.length === 0) return [1];
const associationMap = {};
for (const [idx, value] of coloringSet.entries()) {
for (let filter of value) {
associationMap[filter] = idx
associationMap[filter] = idx;
}
}
const associationCounts = new Array(coloringSet.length)
associationCounts.fill(0)
const associationCounts = new Array(coloringSet.length);
associationCounts.fill(0);
let totalAssociations = 0
let totalAssociations = 0;
set.forEach(item => {
let innerSet = 'events' in item ? item.events : item
set.forEach((item) => {
let innerSet = "events" in item ? item.events : item;
if (!Array.isArray(innerSet)) innerSet = [innerSet]
if (!Array.isArray(innerSet)) innerSet = [innerSet];
innerSet.forEach(val => {
val.associations.forEach(a => {
const idx = associationMap[a]
if (!idx && idx !== 0) return
associationCounts[idx] += 1
totalAssociations += 1
})
})
})
innerSet.forEach((val) => {
val.associations.forEach((a) => {
const idx = associationMap[a];
if (!idx && idx !== 0) return;
associationCounts[idx] += 1;
totalAssociations += 1;
});
});
});
if (totalAssociations === 0) return [1]
if (totalAssociations === 0) return [1];
return associationCounts.map(count => count / totalAssociations)
return associationCounts.map((count) => count / totalAssociations);
}
/**
@@ -354,73 +375,76 @@ export function calculateColorPercentages (set, coloringSet) {
*
* Example coloringSet = [['Chemical', 'Tear Gas'], ['Procedural', 'Destruction of property']]
*/
export function getFilterIdxFromColorSet (filter, coloringSet) {
let filterIdx = -1
export function getFilterIdxFromColorSet(filter, coloringSet) {
let filterIdx = -1;
coloringSet.map((set, idx) => {
const foundIdx = set.indexOf(filter)
if (foundIdx !== -1) filterIdx = idx
})
return filterIdx
const foundIdx = set.indexOf(filter);
if (foundIdx !== -1) filterIdx = idx;
});
return filterIdx;
}
export const dateMin = function () {
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
return a < b ? a : b
})
}
return a < b ? a : b;
});
};
export const dateMax = function () {
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
return a > b ? a : b
})
}
return a > b ? a : b;
});
};
/** Taken from
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
* **/
export function binarySearch (ar, el, compareFn) {
var m = 0
var n = ar.length - 1
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
* **/
export function binarySearch(ar, el, compareFn) {
var m = 0;
var n = ar.length - 1;
while (m <= n) {
var k = (n + m) >> 1
var cmp = compareFn(el, ar[k])
var k = (n + m) >> 1;
var cmp = compareFn(el, ar[k]);
if (cmp > 0) {
m = k + 1
m = k + 1;
} else if (cmp < 0) {
n = k - 1
n = k - 1;
} else {
return k
return k;
}
}
return -m - 1
return -m - 1;
}
export function makeNiceDate (datetime) {
if (datetime === null) return null
export function makeNiceDate(datetime) {
if (datetime === null) return null;
// see https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date
const dateTimeFormat = new Intl.DateTimeFormat(
language,
{ year: 'numeric', month: 'long', day: '2-digit' }
)
const dateTimeFormat = new Intl.DateTimeFormat(language, {
year: "numeric",
month: "long",
day: "2-digit",
});
const [
{ value: month },,
{ value: day },,
{ value: year }
] = dateTimeFormat.formatToParts(datetime)
{ value: month },
,
{ value: day },
,
{ value: year },
] = dateTimeFormat.formatToParts(datetime);
return `${day} ${month}, ${year}`
return `${day} ${month}, ${year}`;
}
/**
* Sets the default locale for d3 to format dates in each available language.
* @param {Object} d3 - An instance of D3
*/
export function setD3Locale (d3) {
export function setD3Locale(d3) {
const languages = {
'es-MX': require('./data/es-MX.json')
}
"es-MX": require("./data/es-MX.json"),
};
if (language !== 'es-US' && languages[language]) {
d3.timeFormatDefaultLocale(languages[language])
if (language !== "es-US" && languages[language]) {
d3.timeFormatDefaultLocale(languages[language]);
}
}

View File

@@ -1,69 +1,69 @@
import React from 'react'
import { connect } from 'react-redux'
import React from "react";
import { connect } from "react-redux";
import * as selectors from '../selectors'
import { getFilterIdxFromColorSet } from '../common/utilities'
import * as selectors from "../selectors";
import { getFilterIdxFromColorSet } from "../common/utilities";
// import Card from './Card.jsx'
import { Card } from '@forensic-architecture/design-system/react'
import copy from '../common/data/copy.json'
import { Card } from "@forensic-architecture/design-system/react";
import copy from "../common/data/copy.json";
class CardStack extends React.Component {
constructor () {
super()
this.refs = {}
this.refCardStack = React.createRef()
this.refCardStackContent = React.createRef()
constructor() {
super();
this.refs = {};
this.refCardStack = React.createRef();
this.refCardStackContent = React.createRef();
}
componentDidUpdate () {
const isNarrative = !!this.props.narrative
componentDidUpdate() {
const isNarrative = !!this.props.narrative;
if (isNarrative) {
this.scrollToCard()
this.scrollToCard();
}
}
scrollToCard () {
const duration = 500
const element = this.refCardStack.current
scrollToCard() {
const duration = 500;
const element = this.refCardStack.current;
const cardScroll = this.refs[this.props.narrative.current].current
.offsetTop
.offsetTop;
let start = element.scrollTop
let change = cardScroll - start
let currentTime = 0
const increment = 20
let start = element.scrollTop;
let change = cardScroll - start;
let currentTime = 0;
const increment = 20;
// t = current time
// b = start value
// c = change in value
// d = duration
Math.easeInOutQuad = function (t, b, c, d) {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t -= 1
return (-c / 2) * (t * (t - 2) - 1) + b
}
t /= d / 2;
if (t < 1) return (c / 2) * t * t + b;
t -= 1;
return (-c / 2) * (t * (t - 2) - 1) + b;
};
const animateScroll = function () {
currentTime += increment
const val = Math.easeInOutQuad(currentTime, start, change, duration)
element.scrollTop = val
if (currentTime < duration) setTimeout(animateScroll, increment)
}
animateScroll()
currentTime += increment;
const val = Math.easeInOutQuad(currentTime, start, change, duration);
element.scrollTop = val;
if (currentTime < duration) setTimeout(animateScroll, increment);
};
animateScroll();
}
renderCards (events, selections) {
renderCards(events, selections) {
// if no selections provided, select all
if (!selections) {
selections = events.map((e) => true)
selections = events.map((e) => true);
}
this.refs = []
this.refs = [];
return events.map((event, idx) => {
const thisRef = React.createRef()
this.refs[idx] = thisRef
const thisRef = React.createRef();
this.refs[idx] = thisRef;
return (
<Card
@@ -72,109 +72,109 @@ class CardStack extends React.Component {
event,
colors: this.props.colors,
coloringSet: this.props.coloringSet,
getFilterIdxFromColorSet
getFilterIdxFromColorSet,
})}
language={this.props.language}
isLoading={this.props.isLoading}
isSelected={selections[idx]}
/>
)
})
);
});
}
renderSelectedCards () {
const { selected } = this.props
renderSelectedCards() {
const { selected } = this.props;
if (selected.length > 0) {
return this.renderCards(selected)
return this.renderCards(selected);
}
return null
return null;
}
renderNarrativeCards () {
const { narrative } = this.props
const showing = narrative.steps
renderNarrativeCards() {
const { narrative } = this.props;
const showing = narrative.steps;
const selections = showing.map((_, idx) => idx === narrative.current)
const selections = showing.map((_, idx) => idx === narrative.current);
return this.renderCards(showing, selections)
return this.renderCards(showing, selections);
}
renderCardStackHeader () {
const headerLang = copy[this.props.language].cardstack.header
renderCardStackHeader() {
const headerLang = copy[this.props.language].cardstack.header;
return (
<div
id='card-stack-header'
className='card-stack-header'
id="card-stack-header"
className="card-stack-header"
onClick={() => this.props.onToggleCardstack()}
>
<button className='side-menu-burg is-active'>
<button className="side-menu-burg is-active">
<span />
</button>
<p className='header-copy top'>
<p className="header-copy top">
{`${this.props.selected.length} ${headerLang}`}
</p>
</div>
)
);
}
renderCardStackContent () {
renderCardStackContent() {
return (
<div id='card-stack-content' className='card-stack-content'>
<div id="card-stack-content" className="card-stack-content">
<ul>{this.renderSelectedCards()}</ul>
</div>
)
);
}
renderNarrativeContent () {
renderNarrativeContent() {
return (
<div
id='card-stack-content'
className='card-stack-content'
id="card-stack-content"
className="card-stack-content"
ref={this.refCardStackContent}
>
<ul>{this.renderNarrativeCards()}</ul>
</div>
)
);
}
render () {
const { isCardstack, selected, narrative, timelineDims } = this.props
render() {
const { isCardstack, selected, narrative, timelineDims } = this.props;
// TODO: make '237px', which is the narrative header, less hard-coded
const height = `calc(100% - 237px - ${timelineDims.height}px)`
const height = `calc(100% - 237px - ${timelineDims.height}px)`;
if (selected.length > 0) {
if (!narrative) {
return (
<div
id='card-stack'
id="card-stack"
className={`card-stack
${isCardstack ? '' : ' folded'}`}
${isCardstack ? "" : " folded"}`}
>
{this.renderCardStackHeader()}
{this.renderCardStackContent()}
</div>
)
);
} else {
return (
<div
id='card-stack'
id="card-stack"
ref={this.refCardStack}
className={`card-stack narrative-mode
${isCardstack ? '' : ' folded'}`}
${isCardstack ? "" : " folded"}`}
style={{ height }}
>
{this.renderNarrativeContent()}
</div>
)
);
}
}
return <div />
return <div />;
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
narrative: selectors.selectActiveNarrative(state),
selected: selectors.selectSelected(state),
@@ -185,8 +185,8 @@ function mapStateToProps (state) {
cardUI: state.ui.card,
colors: state.ui.coloring.colors,
coloringSet: state.app.associations.coloringSet,
features: state.features
}
features: state.features,
};
}
export default connect(mapStateToProps)(CardStack)
export default connect(mapStateToProps)(CardStack);

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import { Portal } from 'react-portal'
import colors from '../../../common/global.js'
import ColoredMarkers from './ColoredMarkers.jsx'
import React, { useState } from "react";
import { Portal } from "react-portal";
import colors from "../../../common/global.js";
import ColoredMarkers from "./ColoredMarkers.jsx";
import {
calcClusterOpacity,
calcClusterSize,
@@ -9,18 +9,30 @@ import {
isLongitude,
calculateColorPercentages,
zipColorsToPercentages,
calculateTotalClusterPoints } from '../../../common/utilities'
calculateTotalClusterPoints,
} from "../../../common/utilities";
const DefsClusters = () => (
<defs>
<radialGradient id='clusterGradient'>
<stop offset='10%' stop-color='red' />
<stop offset='90%' stop-color='transparent' />
<radialGradient id="clusterGradient">
<stop offset="10%" stop-color="red" />
<stop offset="90%" stop-color="transparent" />
</radialGradient>
</defs>
)
);
function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick, getClusterChildren, coloringSet, filterColors }) {
function Cluster({
cluster,
size,
projectPoint,
totalPoints,
styles,
renderHover,
onClick,
getClusterChildren,
coloringSet,
filterColors,
}) {
/**
{
geometry: {
@@ -35,22 +47,25 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
type: "Feature"
}
*/
const { cluster_id: clusterId } = cluster.properties
const { cluster_id: clusterId } = cluster.properties;
const individualChildren = getClusterChildren(clusterId)
const colorPercentages = calculateColorPercentages(individualChildren, coloringSet)
const individualChildren = getClusterChildren(clusterId);
const colorPercentages = calculateColorPercentages(
individualChildren,
coloringSet
);
const { coordinates } = cluster.geometry
const [longitude, latitude] = coordinates
if (!isLatitude(latitude) || !isLongitude(longitude)) return null
const { x, y } = projectPoint([latitude, longitude])
const [hovered, setHovered] = useState(false)
const { coordinates } = cluster.geometry;
const [longitude, latitude] = coordinates;
const { x, y } = projectPoint([latitude, longitude]);
const [hovered, setHovered] = useState(false);
if (!isLatitude(latitude) || !isLongitude(longitude)) return null;
return (
<g
className={'cluster-event'}
className={"cluster-event"}
transform={`translate(${x}, ${y})`}
onClick={e => onClick({ id: clusterId, latitude, longitude })}
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
@@ -58,16 +73,16 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
radius={size}
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
styles={{
...styles
...styles,
}}
className={'cluster-event-marker'}
className={"cluster-event-marker"}
/>
{hovered ? renderHover(cluster) : null}
</g>
)
);
}
function ClusterEvents ({
function ClusterEvents({
projectPoint,
onSelect,
getClusterChildren,
@@ -76,56 +91,66 @@ function ClusterEvents ({
svg,
clusters,
filterColors,
selected
selected,
}) {
const totalPoints = calculateTotalClusterPoints(clusters)
const totalPoints = calculateTotalClusterPoints(clusters);
const styles = {
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
stroke: colors.darkBackground,
strokeWidth: 0
}
strokeWidth: 0,
};
function renderHover (txt, circleSize) {
return <>
<text text-anchor='middle' y='3px' style={{ fontWeight: 'bold', fill: 'black', zIndex: 10000 }}>{txt}</text>
<circle
class='event-hover'
cx='0'
cy='0'
r={circleSize + 2}
stroke={colors.primaryHighlight}
fill-opacity='0.0'
/>
</>
function renderHover(txt, circleSize) {
return (
<>
<text
text-anchor="middle"
y="3px"
style={{ fontWeight: "bold", fill: "black", zIndex: 10000 }}
>
{txt}
</text>
<circle
class="event-hover"
cx="0"
cy="0"
r={circleSize + 2}
stroke={colors.primaryHighlight}
fill-opacity="0.0"
/>
</>
);
}
return (
<Portal node={svg}>
<g className='cluster-locations'>
<g className="cluster-locations">
{isRadial ? <DefsClusters /> : null}
{clusters.map(c => {
const pointCount = c.properties.point_count
const clusterSize = calcClusterSize(pointCount, totalPoints)
return <Cluster
onClick={onSelect}
getClusterChildren={getClusterChildren}
coloringSet={coloringSet}
cluster={c}
filterColors={filterColors}
size={clusterSize}
projectPoint={projectPoint}
totalPoints={totalPoints}
styles={{
...styles,
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
}}
renderHover={() => renderHover(pointCount, clusterSize)}
/>
{clusters.map((c) => {
const pointCount = c.properties.point_count;
const clusterSize = calcClusterSize(pointCount, totalPoints);
return (
<Cluster
onClick={onSelect}
getClusterChildren={getClusterChildren}
coloringSet={coloringSet}
cluster={c}
filterColors={filterColors}
size={clusterSize}
projectPoint={projectPoint}
totalPoints={totalPoints}
styles={{
...styles,
fillOpacity: calcClusterOpacity(pointCount, totalPoints),
}}
renderHover={() => renderHover(pointCount, clusterSize)}
/>
);
})}
</g>
</Portal>
)
);
}
export default ClusterEvents
export default ClusterEvents;

View File

@@ -1,35 +1,49 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store/index.js'
import App from './components/App.jsx'
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store/index.js";
import App from "./components/App.jsx";
console.log(process.env);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('explore-app')
)
document.getElementById("explore-app")
);
// Expressions from https://exceptionshub.com/how-to-detect-safari-chrome-ie-firefox-and-opera-browser.html
/* eslint-disable */
// Opera 8.0+
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0
const isOpera =
(!!window.opr && !!opr.addons) ||
!!window.opera ||
navigator.userAgent.indexOf(" OPR/") >= 0;
// Firefox 1.0+
const isFirefox = typeof InstallTrigger !== 'undefined'
const isFirefox = typeof InstallTrigger !== "undefined";
// Safari 3.0+ "[object HTMLElementConstructor]"
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === '[object SafariRemoteNotification]' })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification))
const isSafari =
/constructor/i.test(window.HTMLElement) ||
(function (p) {
return p.toString() === "[object SafariRemoteNotification]";
})(
!window["safari"] ||
(typeof safari !== "undefined" && safari.pushNotification)
);
// Internet Explorer 6-11
const isIE = /* @cc_on!@ */false || !!document.documentMode
const isIE = /* @cc_on!@ */ false || !!document.documentMode;
// Edge 20+
const isEdge = !isIE && !!window.StyleMedia
const isEdge = !isIE && !!window.StyleMedia;
// Chrome 1+
const isChrome = !!window.chrome && !!window.chrome.webstore
const isChrome = !!window.chrome && !!window.chrome.webstore;
// Blink engine detection
const isBlink = (isChrome || isOpera) && !!window.CSS
const isBlink = (isChrome || isOpera) && !!window.CSS;
if (isEdge || isIE) {
alert('Please view this website in Opera for best viewing. It is untested in your browser.')
alert(
"Please view this website in Opera for best viewing. It is untested in your browser."
);
}
/* eslint-enable */