diff --git a/package.json b/package.json index 3d753f1..9482b53 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "private": true, "scripts": { "dev": "webpack-dev-server --content-base static --mode development", + "dev:wsl": "npm run dev -- --host 0.0.0.0", "build": "NODE_ENV=production webpack --mode production", "test": "ava --verbose", "test-watch": "ava --watch", diff --git a/src/actions/index.js b/src/actions/index.js index 66ba175..718277a 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -4,7 +4,6 @@ import { urlFromEnv } from '../common/utilities' // TODO: relegate these URLs entirely to environment variables // const CONFIG_URL = urlFromEnv('CONFIG_EXT') const EVENT_DATA_URL = urlFromEnv('EVENTS_EXT') -// const CATEGORY_URL = urlFromEnv('CATEGORIES_EXT') const ASSOCIATIONS_URL = urlFromEnv('ASSOCIATIONS_EXT') const SOURCES_URL = urlFromEnv('SOURCES_EXT') const SITES_URL = urlFromEnv('SITES_EXT') @@ -181,12 +180,13 @@ export function clearFilter (filter) { } } -export const TOGGLE_FILTER = 'TOGGLE_FILTER' -export function toggleFilter (filter, value) { +export const TOGGLE_ASSOCIATIONS = 'TOGGLE_ASSOCIATIONS' +export function toggleAssociations (association, value, shouldColor) { return { - type: TOGGLE_FILTER, - filter, - value + type: TOGGLE_ASSOCIATIONS, + association, + value, + shouldColor } } @@ -252,6 +252,14 @@ export function updateSource (source) { } } +export const UPDATE_COLORING_SET = 'UPDATE_COLORING_SET' +export function updateColoringSet (coloringSet) { + return { + type: UPDATE_COLORING_SET, + coloringSet + } +} + // UI export const TOGGLE_SITES = 'TOGGLE_SITES' diff --git a/src/common/utilities.js b/src/common/utilities.js index d5c26fc..0936894 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -11,6 +11,28 @@ export function calcDatetime (date, time) { 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] +} + +/** + * This function takes the array of percentages: [0.5, 0.5, ...] + * and maps it by index to the set of colors ['#fff', '#000', ...] + * If there aren't enough colors in the set, it raises an error for the user + * + * 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') + + return percentages.reduce((map, percent, idx) => { + 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 @@ -63,6 +85,48 @@ export function trimAndEllipse (string, stringNum) { return string } +/** + * From the set of associations, grab a given filter's set of parents, + * ie. all the elements in the path array before the idx where the filter is located. + * If we can't find the filter by the ID, we know its a meta filter, so we look + * 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) { + for (let a of associations) { + const { filter_paths: fp } = a + if (a.id === filter) { + return fp.slice(0, fp.length - 1) + } + 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') +} + +/** + * 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] +} + +/** + * Grabs a given filter's siblings: the set of associations that share the same immediate filter parent. +*/ +export function getFilterSiblings (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 + }, []) +} + export function getEventCategories (event, categories) { const matchedCategories = [] if (event.associations && event.associations.length > 0) { @@ -180,7 +244,7 @@ export function calcOpacity (num) { * 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.6 : 0 + const base = num >= 1 ? 0.9 : 0 return base + (Math.min(0.5, 0.08 * (num - 1))) } @@ -188,14 +252,16 @@ 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) + const base = 0.5 + return base + Math.min(0.95, 0.08 + (pointCount / totalPoints) * 50) } 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. */ - return Math.min(50, 10 + (pointCount / totalPoints) * 150) + const maxSize = totalPoints > 60 ? 40 : 20 + return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150) } export function isLatitude (lat) { @@ -214,6 +280,59 @@ export function mapClustersToLocations (clusters, locations) { }, []) } +/** + * 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 = {} + + for (const [idx, value] of coloringSet.entries()) { + for (let filter of value) { + associationMap[filter] = idx + } + } + + const associationCounts = new Array(coloringSet.length) + associationCounts.fill(0) + + let totalAssociations = 0 + + set.forEach(item => { + let innerSet = 'events' in item ? item.events : item + + 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 + }) + }) + }) + + if (totalAssociations === 0) return [1] + + return associationCounts.map(count => count / totalAssociations) +} + +/** + * Gets the idx of a given filter in relation to its position in the coloring set + * + * Example coloringSet = [['Chemical', 'Tear Gas'], ['Procedural', 'Destruction of property']] + */ +export function getFilterIdxFromColorSet (filter, coloringSet) { + let filterIdx = -1 + coloringSet.map((set, idx) => { + 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 diff --git a/src/components/Layout.js b/src/components/Layout.js index 3581ae8..7c587fa 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -264,8 +264,8 @@ class Dashboard extends React.Component { isNarrative={!!app.associations.narrative} methods={{ onTitle: actions.toggleCover, - onSelectFilter: filter => actions.toggleFilter('filters', filter), - onCategoryFilter: category => actions.toggleFilter('categories', category), + onSelectFilter: filters => actions.toggleAssociations('filters', filters), + onCategoryFilter: categories => actions.toggleAssociations('categories', categories), onSelectNarrative: this.setNarrative }} /> diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 8149ff3..8eb2f0d 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -29,6 +29,7 @@ class Map extends React.Component { this.projectPoint = this.projectPoint.bind(this) this.onClusterSelect = this.onClusterSelect.bind(this) this.loadClusterData = this.loadClusterData.bind(this) + this.getClusterChildren = this.getClusterChildren.bind(this) this.svgRef = React.createRef() this.map = null this.superclusterIndex = null @@ -171,6 +172,18 @@ class Map extends React.Component { } } + getClusterChildren (clusterId) { + if (this.superclusterIndex) { + try { + const children = this.superclusterIndex.getLeaves(clusterId, Infinity, 0) + return mapClustersToLocations(children, this.props.domain.locations) + } catch (err) { + return [] + } + } + return [] + } + alignLayers () { const mapNode = document.querySelector('.leaflet-map-pane') if (mapNode === null) return { transformX: 0, transformY: 0 } @@ -281,6 +294,11 @@ class Map extends React.Component { } renderEvents () { + /* + Uncomment below to filter out the locations already present in a cluster. + Leaving these lines commented out renders all the locations on the map, regardless of whether or not they are clustered + */ + const individualClusters = this.state.clusters.filter(cl => !cl.properties.cluster) const filteredLocations = mapClustersToLocations(individualClusters, this.props.domain.locations) return ( @@ -288,6 +306,7 @@ class Map extends React.Component { svg={this.svgRef.current} events={this.props.domain.events} locations={filteredLocations} + // locations={this.props.domain.locations} styleLocation={this.styleLocation} categories={this.props.domain.categories} projectPoint={this.projectPoint} @@ -296,6 +315,9 @@ class Map extends React.Component { onSelect={this.props.methods.onSelect} getCategoryColor={this.props.methods.getCategoryColor} eventRadius={this.props.ui.eventRadius} + coloringSet={this.props.app.coloringSet} + filterColors={this.props.ui.filterColors} + features={this.props.features} /> ) } @@ -310,6 +332,9 @@ class Map extends React.Component { clusters={allClusters} isRadial={this.props.ui.radial} onSelect={this.onClusterSelect} + coloringSet={this.props.app.coloringSet} + getClusterChildren={this.getClusterChildren} + filterColors={this.props.ui.filterColors} /> ) } @@ -384,6 +409,7 @@ function mapStateToProps (state) { language: state.app.language, loading: state.app.loading, narrative: state.app.associations.narrative, + coloringSet: state.app.associations.coloringSet, flags: { isShowingSites: state.app.flags.isShowingSites, isFetchingDomain: state.app.flags.isFetchingDomain @@ -396,7 +422,8 @@ function mapStateToProps (state) { mapSelectedEvents: state.ui.style.selectedEvents, shapes: state.ui.style.shapes, eventRadius: state.ui.eventRadius, - radial: state.ui.style.clusters.radial + radial: state.ui.style.clusters.radial, + filterColors: state.ui.coloring.colors }, features: selectors.getFeatures(state) } diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index f01056f..2b7753d 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -80,12 +80,10 @@ class Timeline extends React.Component { if (features.GRAPH_NONLOCATED && features.GRAPH_NONLOCATED.categories) { categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.id)) } - const catHeight = trackHeight / (categories.length) - const shiftUp = trackHeight / (categories.length) / 3 - const marginShift = marginTop === 0 ? 0 : marginTop - const manualAdjustment = trackHeight <= 60 ? (trackHeight <= 30 ? -8 : -5) : 0 + const extraPadding = 0 + const catHeight = categories.length > 2 ? trackHeight / categories.length : trackHeight / (categories.length + 1) const catsYpos = categories.map((g, i) => { - return ((i + 1) * catHeight) - shiftUp + marginShift + manualAdjustment + return ((i + 1) * catHeight) + marginTop + (extraPadding / 2) }) const catMap = categories.map(c => c.id) @@ -341,7 +339,7 @@ class Timeline extends React.Component { onDragStart={() => { this.onDragStart() }} onDrag={() => { this.onDrag() }} onDragEnd={() => { this.onDragEnd() }} - categories={this.props.app.activeCategories} + categories={categories.map(c => c.id)} features={this.props.features} /> this.getDatetimeX(ev.datetime)} getEventY={this.getY} - categories={this.props.domain.categories} + categories={categories} transitionDuration={this.state.transitionDuration} styles={this.props.ui.styles} features={this.props.features} @@ -368,7 +366,7 @@ class Timeline extends React.Component { @@ -403,20 +403,25 @@ function mapStateToProps (state) { domain: { events: selectors.selectStackedEvents(state), projects: selectors.selectProjects(state), - categories: selectors.getCategories(state), + categories: (state => { + const allcats = selectors.getCategories(state) + const active = selectors.getActiveCategories(state) + return allcats.filter(c => active.includes(c.id)) + })(state), narratives: state.domain.narratives }, app: { - activeCategories: selectors.getActiveCategories(state), selected: state.app.selected, language: state.app.language, timeline: state.app.timeline, - narrative: state.app.associations.narrative + narrative: state.app.associations.narrative, + coloringSet: state.app.associations.coloringSet }, ui: { dom: state.ui.dom, styles: state.ui.style.selectedEvents, - eventRadius: state.ui.eventRadius + eventRadius: state.ui.eventRadius, + filterColors: state.ui.coloring.colors }, features: selectors.getFeatures(state) } diff --git a/src/components/TimelineAxis.jsx b/src/components/TimelineAxis.jsx index a93ebd3..64c9aef 100644 --- a/src/components/TimelineAxis.jsx +++ b/src/components/TimelineAxis.jsx @@ -1,6 +1,8 @@ import React from 'react' import * as d3 from 'd3' +const TEXT_HEIGHT = 15 + class TimelineAxis extends React.Component { constructor () { super() @@ -27,18 +29,19 @@ class TimelineAxis extends React.Component { fstFmt = '%H:%M' } + let { marginTop, contentHeight } = this.props.dims if (this.props.scaleX) { this.x0 = d3.axisBottom(this.props.scaleX) .ticks(10) .tickPadding(0) - .tickSize(this.props.dims.trackHeight) + .tickSize(contentHeight - TEXT_HEIGHT - marginTop) .tickFormat(d3.timeFormat(fstFmt)) this.x1 = d3.axisBottom(this.props.scaleX) .ticks(10) - .tickPadding(this.props.dims.marginTop) + .tickPadding(marginTop) .tickSize(0) .tickFormat(d3.timeFormat(sndFmt)) @@ -59,18 +62,17 @@ class TimelineAxis extends React.Component { } render () { - const PADDING = 20 return ( diff --git a/src/components/Toolbar/FilterListPanel.js b/src/components/Toolbar/FilterListPanel.js index c3447f1..867d7cf 100644 --- a/src/components/Toolbar/FilterListPanel.js +++ b/src/components/Toolbar/FilterListPanel.js @@ -1,6 +1,8 @@ import React from 'react' import Checkbox from '../presentational/Checkbox' import copy from '../../common/data/copy.json' +import { getFilterIdxFromColorSet } from '../../common/utilities' +import { colors } from '../../common/global' /** recursively get an array of node keys to toggle */ function childrenToToggle (filter, activeFilters, parentOn) { @@ -38,22 +40,33 @@ function FilterListPanel ({ filters, activeFilters, onSelectFilter, - language + language, + coloringSet, + filterColors }) { function createNodeComponent (filter, depth) { const [key, children] = filter const matchingKeys = childrenToToggle(filter, activeFilters, activeFilters.includes(key)) + const idxFromColorSet = getFilterIdxFromColorSet(key, coloringSet) + + const assignedColor = idxFromColorSet !== -1 && activeFilters.includes(key) ? filterColors[idxFromColorSet] : colors.white + + const styles = ({ + color: assignedColor, + marginLeft: `${depth * 20}px` + }) return (
  • onSelectFilter(matchingKeys)} + onClickCheckbox={() => onSelectFilter(key, matchingKeys)} + backgroundColor={assignedColor} /> {Object.keys(children).length > 0 ? Object.entries(children).map(filter => createNodeComponent(filter, depth + 1)) diff --git a/src/components/Toolbar/Layout.js b/src/components/Toolbar/Layout.js index 7221b6f..9ac5806 100644 --- a/src/components/Toolbar/Layout.js +++ b/src/components/Toolbar/Layout.js @@ -9,11 +9,12 @@ import FilterListPanel from './FilterListPanel' import CategoriesListPanel from './CategoriesListPanel' import BottomActions from './BottomActions' import copy from '../../common/data/copy.json' -import { trimAndEllipse } from '../../common/utilities.js' +import { trimAndEllipse, getImmediateFilterParent, getFilterSiblings } from '../../common/utilities.js' class Toolbar extends React.Component { constructor (props) { super(props) + this.onSelectFilter = this.onSelectFilter.bind(this) this.state = { _selected: -1 } } @@ -22,6 +23,49 @@ class Toolbar extends React.Component { this.setState({ _selected }) } + onSelectFilter (key, matchingKeys) { + const { filters, activeFilters, coloringSet, maxNumOfColors } = this.props + + const parent = getImmediateFilterParent(filters, key) + const isTurningOff = activeFilters.includes(key) + + if (!isTurningOff) { + const flattenedColoringSet = coloringSet.flatMap(f => f) + const newColoringSet = matchingKeys.filter(k => flattenedColoringSet.indexOf(k) === -1) + const updatedColoringSet = [...coloringSet, newColoringSet] + + if (updatedColoringSet.length <= maxNumOfColors) { + this.props.actions.updateColoringSet(updatedColoringSet) + } + } else { + const newColoringSets = coloringSet.map(set => ( + set.filter(s => { + return !matchingKeys.includes(s) + }) + )) + this.props.actions.updateColoringSet(newColoringSets.filter(item => item.length !== 0)) + } + + if (parent) { + const parentOn = activeFilters.includes(parent) + if (parentOn) { + const siblings = getFilterSiblings(filters, parent, key) + let siblingsOff = true + for (let sibling of siblings) { + if (activeFilters.includes(sibling)) { + siblingsOff = false + break + } + } + + if (siblingsOff && isTurningOff) { + matchingKeys.push(parent) + } + } + } + this.props.methods.onSelectFilter(matchingKeys) + } + renderClosePanel () { return (
    this.selectTab(-1)}> @@ -75,8 +119,10 @@ class Toolbar extends React.Component { ) @@ -190,6 +236,9 @@ function mapStateToProps (state) { narrative: state.app.associations.narrative, sitesShowing: state.app.flags.isShowingSites, infoShowing: state.app.flags.isInfopopup, + coloringSet: state.app.associations.coloringSet, + maxNumOfColors: state.ui.coloring.maxNumOfColors, + filterColors: state.ui.coloring.colors, features: selectors.getFeatures(state) } } diff --git a/src/components/presentational/Checkbox.js b/src/components/presentational/Checkbox.js index 976551c..932da86 100644 --- a/src/components/presentational/Checkbox.js +++ b/src/components/presentational/Checkbox.js @@ -1,10 +1,17 @@ import React from 'react' -export default ({ label, isActive, onClickCheckbox }) => ( -
    - onClickCheckbox()}>{label} - -
    -) +export default ({ label, isActive, onClickCheckbox, backgroundColor }) => { + const styles = ({ + background: isActive ? backgroundColor : 'none', + border: `1px solid ${backgroundColor}` + }) + + return ( +
    + onClickCheckbox()}>{label} + +
    + ) +} diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index b999541..e3aad3a 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -1,7 +1,14 @@ import React, { useState } from 'react' import { Portal } from 'react-portal' import colors from '../../../common/global.js' -import { calcClusterOpacity, calcClusterSize, isLatitude, isLongitude } from '../../../common/utilities' +import ColoredMarkers from './ColoredMarkers.jsx' +import { + calcClusterOpacity, + calcClusterSize, + isLatitude, + isLongitude, + calculateColorPercentages, + zipColorsToPercentages } from '../../../common/utilities' const DefsClusters = () => ( @@ -12,7 +19,7 @@ const DefsClusters = () => ( ) -function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick }) { +function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick, getClusterChildren, coloringSet, filterColors }) { /** { geometry: { @@ -28,6 +35,10 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove } */ const { cluster_id: clusterId } = cluster.properties + + const individualChildren = getClusterChildren(clusterId) + const colorPercentages = calculateColorPercentages(individualChildren, coloringSet) + const { coordinates } = cluster.geometry const [longitude, latitude] = coordinates if (!isLatitude(latitude) || !isLongitude(longitude)) return null @@ -42,20 +53,15 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} > - {hovered ? renderHover(cluster) : null} - ) } @@ -63,9 +69,12 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove function ClusterEvents ({ projectPoint, onSelect, + getClusterChildren, + coloringSet, isRadial, svg, - clusters + clusters, + filterColors }) { const totalPoints = clusters.reduce((total, cl) => { if (cl && cl.properties) { @@ -80,8 +89,18 @@ function ClusterEvents ({ strokeWidth: 0 } - function renderHover (txt) { - return {txt} + function renderHover (txt, circleSize) { + return <> + {txt} + + } return ( @@ -93,7 +112,10 @@ function ClusterEvents ({ const clusterSize = calcClusterSize(pointCount, totalPoints) return <> - - {renderHover(pointCount)} - } + renderHover={() => renderHover(pointCount, clusterSize)} /> })} diff --git a/src/components/presentational/Map/ColoredMarkers.jsx b/src/components/presentational/Map/ColoredMarkers.jsx new file mode 100644 index 0000000..949ce24 --- /dev/null +++ b/src/components/presentational/Map/ColoredMarkers.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import { getCoordinatesForPercent } from '../../../common/utilities' + +function ColoredMarkers ({ radius, colorPercentMap, styles, className }) { + let cumulativeAngleSweep = 0 + const colors = Object.keys(colorPercentMap) + + return ( + + {colors.map((color, idx) => { + const colorPercent = colorPercentMap[color] + + const [startX, startY] = getCoordinatesForPercent(radius, cumulativeAngleSweep) + + cumulativeAngleSweep += colorPercent + + const [endX, endY] = getCoordinatesForPercent(radius, cumulativeAngleSweep) + // if the slices are less than 2, take the long arc + const largeArcFlag = (colors.length === 1) || colorPercent > 0.5 ? 1 : 0 + + // create an array and join it just for code readability + const arc = [ + `M ${startX} ${startY}`, // Move + `A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc + `L 0 0 `, // Line + `L ${startX} ${startY} Z` // Line + ].join(' ') + + const extraStyles = ({ + ...styles, + fill: color + }) + + return ( + + ) + })} + + ) +} + +export default ColoredMarkers diff --git a/src/components/presentational/Map/Events.jsx b/src/components/presentational/Map/Events.jsx index 588d4c0..4949bcf 100644 --- a/src/components/presentational/Map/Events.jsx +++ b/src/components/presentational/Map/Events.jsx @@ -1,7 +1,8 @@ import React from 'react' import { Portal } from 'react-portal' import colors from '../../../common/global.js' -import { calcOpacity } from '../../../common/utilities' +import ColoredMarkers from './ColoredMarkers.jsx' +import { calcOpacity, getCoordinatesForPercent, calculateColorPercentages, zipColorsToPercentages } from '../../../common/utilities' function MapEvents ({ getCategoryColor, @@ -13,14 +14,11 @@ function MapEvents ({ onSelect, svg, locations, - eventRadius + eventRadius, + coloringSet, + filterColors, + features }) { - 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] - } - function handleEventSelect (e, location) { const events = e.shiftKey ? selected.concat(location.events) : location.events onSelect(events) @@ -41,6 +39,27 @@ function MapEvents ({ ) } + function renderLocationSlicesByAssociation (location) { + const colorPercentages = calculateColorPercentages([location], coloringSet) + + let styles = ({ + stroke: colors.darkBackground, + strokeWidth: 0, + fillOpacity: narrative ? 1 : calcOpacity(location.events.length) + }) + + return ( + + ) + } + function renderLocationSlicesByCategory (location) { const locCategory = location.events.length > 0 ? location.events[0].category : 'default' const customStyles = styleLocation ? styleLocation(location) : null @@ -142,7 +161,8 @@ function MapEvents ({ transform={`translate(${x}, ${y})`} onClick={(e) => handleEventSelect(e, location)} > - {renderLocationSlicesByCategory(location)} + {features.COLOR_BY_ASSOCIATION ? renderLocationSlicesByAssociation(location) : null} + {features.COLOR_BY_CATEGORY ? renderLocationSlicesByCategory(location) : null} {extraRender ? extraRender() : null} {isSelected ? null : renderBorder()} diff --git a/src/components/presentational/Timeline/Events.js b/src/components/presentational/Timeline/Events.js index d8796d4..194c23e 100644 --- a/src/components/presentational/Timeline/Events.js +++ b/src/components/presentational/Timeline/Events.js @@ -1,21 +1,29 @@ import React from 'react' -import DatetimeDot from './DatetimeDot' import DatetimeBar from './DatetimeBar' import DatetimeSquare from './DatetimeSquare' import DatetimeStar from './DatetimeStar' import Project from './Project' -import { calcOpacity, getEventCategories } from '../../../common/utilities' +import ColoredMarkers from '../Map/ColoredMarkers.jsx' +import { calcOpacity, getEventCategories, zipColorsToPercentages, calculateColorPercentages } from '../../../common/utilities' function renderDot (event, styles, props) { - return + const colorPercentages = calculateColorPercentages([event], props.coloringSet) + return ( + + + + ) } function renderBar (event, styles, props) { @@ -72,7 +80,9 @@ const TimelineEvents = ({ features, setLoading, setNotLoading, - eventRadius + eventRadius, + filterColors, + coloringSet }) => { const narIds = narrative ? narrative.steps.map(s => s.id) : [] @@ -121,7 +131,9 @@ const TimelineEvents = ({ onSelect: () => onSelect(event), dims, highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.filters[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [], - features + features, + filterColors, + coloringSet }) } diff --git a/src/reducers/app.js b/src/reducers/app.js index d87f029..e56275d 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -5,8 +5,9 @@ import { toggleFlagAC } from '../common/utilities' import { UPDATE_HIGHLIGHTED, UPDATE_SELECTED, + UPDATE_COLORING_SET, CLEAR_FILTER, - TOGGLE_FILTER, + TOGGLE_ASSOCIATIONS, UPDATE_TIMERANGE, UPDATE_DIMENSIONS, UPDATE_NARRATIVE, @@ -39,6 +40,16 @@ function updateSelected (appState, action) { }) } +function updateColoringSet (appState, action) { + return { + ...appState, + associations: { + ...appState.associations, + coloringSet: action.coloringSet + } + } +} + function updateNarrative (appState, action) { let minTime = appState.timeline.range[0] let maxTime = appState.timeline.range[1] @@ -111,11 +122,11 @@ function updateNarrativeStepIdx (appState, action) { } } -function toggleFilter (appState, action) { +function toggleAssociations (appState, action) { if (!(action.value instanceof Array)) { action.value = [action.value] } - const { filter: associationType } = action + const { association: associationType } = action let newAssociations = appState.associations[associationType].slice(0) action.value.forEach(vl => { @@ -249,10 +260,12 @@ function app (appState = initial.app, action) { return updateHighlighted(appState, action) case UPDATE_SELECTED: return updateSelected(appState, action) + case UPDATE_COLORING_SET: + return updateColoringSet(appState, action) case CLEAR_FILTER: return clearFilter(appState, action) - case TOGGLE_FILTER: - return toggleFilter(appState, action) + case TOGGLE_ASSOCIATIONS: + return toggleAssociations(appState, action) case UPDATE_TIMERANGE: return updateTimeRange(appState, action) case UPDATE_DIMENSIONS: diff --git a/src/scss/map.scss b/src/scss/map.scss index bc33fc4..ab0f04f 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -152,7 +152,7 @@ } .leaflet-tile { - filter: brightness(110%) invert(100%) grayscale(800%) + filter: brightness(110%) invert(100%) grayscale(800%) contrast(80%); } /* diff --git a/src/scss/toolbar.scss b/src/scss/toolbar.scss index e75b3bc..615c04e 100644 --- a/src/scss/toolbar.scss +++ b/src/scss/toolbar.scss @@ -416,7 +416,6 @@ float: left; font-size: $normal; font-family: Helvetica, 'Georgia', 'serif'; - color: $midwhite; overflow: hidden; } diff --git a/src/store/initial.js b/src/store/initial.js index 61f6ea8..7658550 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -1,5 +1,5 @@ import { mergeDeepLeft } from 'ramda' -import global from '../common/global' +import global, { colors } from '../common/global' const initial = { /* @@ -35,6 +35,7 @@ const initial = { selected: [], source: null, associations: { + coloringSet: [], filters: [], narrative: null, categories: [], @@ -63,8 +64,8 @@ const initial = { dimensions: { height: 250, width: 0, - marginLeft: 100, - marginTop: 15, + marginLeft: 70, + marginTop: 10, // the padding used for the day/month labels inside the timeline marginBottom: 60, contentHeight: 200, width_controls: 100 @@ -131,6 +132,10 @@ const initial = { radial: false } }, + coloring: { + maxNumOfColors: 4, + colors: Object.values(colors) + }, dom: { timeline: 'timeline', timeslider: 'timeslider',