diff --git a/src/actions/index.js b/src/actions/index.js index 7c05dbe..9730c21 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -6,6 +6,7 @@ const EVENT_DATA_URL = urlFromEnv("EVENTS_EXT"); const ASSOCIATIONS_URL = urlFromEnv("ASSOCIATIONS_EXT"); const SOURCES_URL = urlFromEnv("SOURCES_EXT"); const SITES_URL = urlFromEnv("SITES_EXT"); +const REGIONS_URL = urlFromEnv("REGIONS_EXT"); const SHAPES_URL = urlFromEnv("SHAPES_EXT"); const domainMsg = (domainType) => @@ -79,6 +80,13 @@ export function fetchDomain() { .catch(() => handleError(domainMsg("sites"))); } + let regionsPromise = Promise.resolve([]); + if (features.USE_REGIONS) { + regionsPromise = fetch(REGIONS_URL) + .then((response) => response.json()) + .catch(() => handleError(domainMsg("regions"))); + } + let shapesPromise = Promise.resolve([]); if (features.USE_SHAPES) { shapesPromise = fetch(SHAPES_URL) @@ -91,6 +99,7 @@ export function fetchDomain() { associationsPromise, sourcesPromise, sitesPromise, + regionsPromise, shapesPromise, ]) .then((response) => { @@ -99,7 +108,8 @@ export function fetchDomain() { associations: response[1], sources: response[2], sites: response[3], - shapes: response[4], + regions: response[4], + shapes: response[5], notifications, }; if ( @@ -111,6 +121,7 @@ export function fetchDomain() { } dispatch(toggleFetchingDomain()); dispatch(setInitialCategories(result.associations)); + dispatch(setInitialShapes(result.shapes)); return result; }) .catch((err) => { @@ -205,6 +216,14 @@ export function toggleAssociations(association, value, shouldColor) { }; } +export const TOGGLE_SHAPES = "TOGGLE_SHAPES"; +export function toggleShapes(shape) { + return { + type: TOGGLE_SHAPES, + shape, + }; +} + export const SET_LOADING = "SET_LOADING"; export function setLoading() { return { @@ -227,6 +246,14 @@ export function setInitialCategories(values) { }; } +export const SET_INITIAL_SHAPES = "SET_INITIAL_SHAPES"; +export function setInitialShapes(values) { + return { + type: SET_INITIAL_SHAPES, + values, + }; +} + export const UPDATE_TIMERANGE = "UPDATE_TIMERANGE"; export function updateTimeRange(timerange) { return { diff --git a/src/common/constants.js b/src/common/constants.js index e295e4b..0ad7ec5 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -4,10 +4,31 @@ export const ASSOCIATION_MODES = { FILTER: "FILTER", }; -export const TIMELINE_ONLY = "TIMELINE_ONLY"; +export const SHAPE = "SHAPE"; export const DEFAULT_TAB_ICONS = { CATEGORY: "widgets", NARRATIVE: "timeline", FILTER: "filter_list", + SHAPE: "change_history", }; + +export const AVAILABLE_SHAPES = { + STAR: "STAR", + DIAMOND: "DIAMOND", + PENTAGON: "PENTAGON", + SQUARE: "SQUARE", + DOT: "DOT", + BAR: "BAR", + TRIANGLE: "TRIANGLE", +}; + +export const POLYGON_CLIP_PATH = { + STAR: + "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)", + DIAMOND: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)", + PENTAGON: "polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)", + TRIANGLE: "polygon(50% 0%, 0% 100%, 100% 100%)", +}; + +export const DEFAULT_CHECKBOX_COLOR = "#ffffff"; diff --git a/src/common/data/copy.json b/src/common/data/copy.json index 9e4fe21..4567108 100644 --- a/src/common/data/copy.json +++ b/src/common/data/copy.json @@ -145,7 +145,11 @@ "categories": "Categories", "categories_label": "Categories", "explore_by_category__title": "Explore events by category", - "explore_by_category__description": "‘Categories’ refer to the victims of a given incident.

If no categories are selected, all datapoints are displayed." + "explore_by_category__description": "‘Categories’ refer to the victims of a given incident.

If no categories are selected, all datapoints are displayed.", + "shapes": "Shapes", + "shapes_label": "Shapes", + "explore_by_shapes__title": "Explore events by shape breakdown", + "explore_by_shape__description": "Shapes map to a given type of event that appears on the timeline.

Select the shape marker to toggle this type of event on / off" }, "timeline": { "labels_title": "Testimonies", diff --git a/src/common/utilities.js b/src/common/utilities.js index 4ee3bdc..6aefd98 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -1,7 +1,7 @@ import moment from "moment"; import hash from "object-hash"; -import { ASSOCIATION_MODES } from "./constants"; +import { ASSOCIATION_MODES, POLYGON_CLIP_PATH } from "./constants"; let { DATE_FMT, TIME_FMT } = process.env; if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY"; @@ -148,7 +148,7 @@ export function getFilterAncestors(filter) { const accumulatedPath = splitFilter.slice(0, index + 1).join("/"); ancestors.push(accumulatedPath); }); - // // The last element here will be the leaf node aka the filter passed in + // The last element here will be the leaf node aka the filter passed in ancestors.pop(); return ancestors; } @@ -510,3 +510,23 @@ export function setD3Locale(d3) { d3.timeFormatDefaultLocale(languages[language]); } } + +export function mapStyleByShape(shapes, activeShapes) { + const styledShapes = shapes.map((s) => { + const { colour, shape, id } = s; + const style = { + checkboxStyles: { + background: activeShapes.includes(id) ? colour : "black", + border: "none", + clipPath: POLYGON_CLIP_PATH[shape], + }, + containerStyles: { + background: colour, + clipPath: POLYGON_CLIP_PATH[shape], + }, + }; + s.styles = style; + return s; + }); + return styledShapes; +} diff --git a/src/components/Layout.js b/src/components/Layout.js index 53ee5a9..cae6891 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -327,6 +327,7 @@ class Dashboard extends React.Component { actions.toggleAssociations("filters", filters), onCategoryFilter: (categories) => actions.toggleAssociations("categories", categories), + onShapeFilter: actions.toggleShapes, onSelectNarrative: this.setNarrative, }} /> diff --git a/src/components/Toolbar.js b/src/components/Toolbar.js index b7a80d9..30a9d51 100644 --- a/src/components/Toolbar.js +++ b/src/components/Toolbar.js @@ -7,6 +7,7 @@ import * as selectors from "../selectors"; import { Tabs, TabPanel } from "react-tabs"; import FilterListPanel from "./controls/FilterListPanel"; import CategoriesListPanel from "./controls/CategoriesListPanel"; +import ShapesListPanel from "./controls/ShapesListPanel"; import BottomActions from "./controls/BottomActions"; import copy from "../common/data/copy.json"; import { @@ -17,7 +18,6 @@ import { addToColoringSet, removeFromColoringSet, } from "../common/utilities.js"; -import { DEFAULT_TAB_ICONS } from "../common/constants"; class Toolbar extends React.Component { constructor(props) { @@ -85,17 +85,10 @@ class Toolbar extends React.Component { renderToolbarNarrativePanel() { const { panels } = this.props.toolbarCopy; - const panelTitle = panels.narratives.label - ? panels.narratives.label - : copy[this.props.language].toolbar.narratives; - const panelDescription = panels.narratives.description - ? panels.narratives.description - : copy[this.props.language].toolbar.explore_by_narrative__description; - return ( -

{panelTitle}

-

{panelDescription}

+

{panels.narratives.label}

+

{panels.narratives.description}

{this.props.narratives.map((narr) => { return (
@@ -118,13 +111,6 @@ class Toolbar extends React.Component { renderToolbarCategoriesPanel() { const { panels } = this.props.toolbarCopy; - const panelTitle = panels.categories.label - ? panels.categories.label - : copy[this.props.language].toolbar.categories; - const panelDescription = panels.categories.description - ? panels.categories.description - : copy[this.props.language].toolbar.explore_by_category__description; - if (this.props.features.USE_CATEGORIES) { return ( @@ -133,8 +119,8 @@ class Toolbar extends React.Component { activeCategories={this.props.activeCategories} onCategoryFilter={this.props.methods.onCategoryFilter} language={this.props.language} - title={panelTitle} - description={panelDescription} + title={panels.categories.label} + description={panels.categories.description} /> ); @@ -143,13 +129,6 @@ class Toolbar extends React.Component { renderToolbarFilterPanel() { const { panels } = this.props.toolbarCopy; - const panelTitle = panels.filters.label - ? panels.filters.label - : copy[this.props.language].toolbar.filters; - const panelDescription = panels.filters.description - ? panels.filters.description - : copy[this.props.language].toolbar.explore_by_filter__description; - return ( ); } + renderToolbarShapePanel() { + const { panels } = this.props.toolbarCopy; + + if (this.props.features.USE_SHAPES) { + return ( + + + + ); + } + } + renderToolbarTab(_selected, label, iconKey) { const isActive = this.state._selected === _selected; const classes = isActive ? "toolbar-tab active" : "toolbar-tab"; @@ -196,6 +194,7 @@ class Toolbar extends React.Component { : null} {features.USE_CATEGORIES ? this.renderToolbarCategoriesPanel() : null} {features.USE_ASSOCIATIONS ? this.renderToolbarFilterPanel() : null} + {features.USE_SHAPES ? this.renderToolbarShapePanel() : null}
); @@ -228,31 +227,17 @@ class Toolbar extends React.Component { const narrativesExist = narratives && narratives.length !== 0; let title = copy[this.props.language].toolbar.title; if (process.env.display_title) title = process.env.display_title; - const { panels } = toolbarCopy; - const narrativesLabel = copy[this.props.language].toolbar.narratives_label; - const filtersLabel = panels.filters.label - ? panels.filters.label - : copy[this.props.language].toolbar.filters_label; - const categoriesLabel = panels.categories.label - ? panels.categories.label - : copy[this.props.language].toolbar.categories_label; - - const filterIcon = panels.filters.icon - ? panels.filters.icon - : DEFAULT_TAB_ICONS.FILTER; - const categoriesIcon = panels.categories.icon - ? panels.categories.icon - : DEFAULT_TAB_ICONS.CATEGORY; const narrativesIdx = 0; const categoriesIdx = narrativesExist ? 1 : 0; const filtersIdx = - narrativesExist && features.CATEGORIES_AS_FILTERS + narrativesExist && features.USE_CATEGORIES ? 2 - : narrativesExist || features.CATEGORIES_AS_FILTERS + : narrativesExist || features.USE_CATEGORIES ? 1 : 0; + const shapesIdx = filtersIdx + 1; return (
@@ -260,17 +245,32 @@ class Toolbar extends React.Component {
{narrativesExist - ? this.renderToolbarTab(narrativesIdx, narrativesLabel, "timeline") + ? this.renderToolbarTab( + narrativesIdx, + panels.narratives.label, + panels.narratives.icon + ) : null} - {features.CATEGORIES_AS_FILTERS + {features.USE_CATEGORIES ? this.renderToolbarTab( categoriesIdx, - categoriesLabel, - categoriesIcon + panels.categories.label, + panels.categories.icon ) : null} {features.USE_ASSOCIATIONS - ? this.renderToolbarTab(filtersIdx, filtersLabel, filterIcon) + ? this.renderToolbarTab( + filtersIdx, + panels.filters.label, + panels.filters.icon + ) + : null} + {features.USE_SHAPES + ? this.renderToolbarTab( + shapesIdx, + panels.shapes.label, + panels.shapes.icon + ) : null}
{ - const styles = { - background: isActive ? color : "none", - border: `1px solid ${color}`, +const Checkbox = ({ label, isActive, onClickCheckbox, color, styleProps }) => { + const checkboxColor = color ? color : DEFAULT_CHECKBOX_COLOR; + const baseStyles = { + checkboxStyles: { + background: isActive ? checkboxColor : "none", + border: `1px solid ${checkboxColor}`, + }, }; - + const containerStyles = styleProps ? styleProps.containerStyles : {}; + const checkboxStyles = styleProps + ? styleProps.checkboxStyles + : baseStyles.checkboxStyles; return (
{label}
); diff --git a/src/components/controls/CategoriesListPanel.js b/src/components/controls/CategoriesListPanel.js index c86f6d7..47c85b7 100644 --- a/src/components/controls/CategoriesListPanel.js +++ b/src/components/controls/CategoriesListPanel.js @@ -1,6 +1,7 @@ import React from "react"; import marked from "marked"; -import Checkbox from "../atoms/Checkbox"; +import PanelTree from "./atoms/PanelTree"; +import { ASSOCIATION_MODES } from "../../common/constants"; const CategoriesListPanel = ({ categories, @@ -10,28 +11,6 @@ const CategoriesListPanel = ({ title, description, }) => { - function renderCategoryTree() { - return ( -
- {categories.map((cat) => { - return ( -
  • - onCategoryFilter(cat.title)} - /> -
  • - ); - })} -
    - ); - } - return (

    {title}

    @@ -40,7 +19,12 @@ const CategoriesListPanel = ({ __html: marked(description), }} /> - {renderCategoryTree()} +
    ); }; diff --git a/src/components/controls/ShapesListPanel.js b/src/components/controls/ShapesListPanel.js new file mode 100644 index 0000000..f72669c --- /dev/null +++ b/src/components/controls/ShapesListPanel.js @@ -0,0 +1,34 @@ +import React from "react"; +import marked from "marked"; +import PanelTree from "./atoms/PanelTree"; +import { mapStyleByShape } from "../../common/utilities"; +import { SHAPE } from "../../common/constants"; + +const ShapesListPanel = ({ + shapes, + activeShapes, + onShapeFilter, + language, + title, + description, +}) => { + const styledShapes = mapStyleByShape(shapes, activeShapes); + return ( +
    +

    {title}

    +

    + +

    + ); +}; + +export default ShapesListPanel; diff --git a/src/components/controls/atoms/PanelTree.js b/src/components/controls/atoms/PanelTree.js new file mode 100644 index 0000000..6da7098 --- /dev/null +++ b/src/components/controls/atoms/PanelTree.js @@ -0,0 +1,30 @@ +import React from "react"; +import Checkbox from "../../atoms/Checkbox"; +import { ASSOCIATION_MODES } from "../../../common/constants"; + +const PanelTree = ({ data, activeValues, onSelect, type }) => { + // If the parent panel is of type 'CATEGORY': filter on title. If panel is 'SHAPE': filter on id + const onSelectionType = type === ASSOCIATION_MODES.CATEGORY ? "title" : "id"; + return ( +
    + {data.map((val) => { + return ( +
  • + onSelect(val[onSelectionType])} + styleProps={val.styles} + /> +
  • + ); + })} +
    + ); +}; + +export default PanelTree; diff --git a/src/components/space/carto/Map.js b/src/components/space/carto/Map.js index a1fa588..13cd0cb 100644 --- a/src/components/space/carto/Map.js +++ b/src/components/space/carto/Map.js @@ -8,7 +8,7 @@ import { connect } from "react-redux"; import * as selectors from "../../../selectors"; import Sites from "./atoms/Sites"; -import Shapes from "./atoms/Shapes"; +import Regions from "./atoms/Regions"; import Events from "./atoms/Events"; import Clusters from "./atoms/Clusters"; import SelectedEvents from "./atoms/SelectedEvents"; @@ -324,13 +324,13 @@ class Map extends React.Component { ); } - renderShapes() { + renderRegions() { return ( - ); } @@ -481,7 +481,7 @@ class Map extends React.Component { {this.renderTiles()} {this.renderMarkers()} {isShowingSites ? this.renderSites() : null} - {this.renderShapes()} + {this.renderRegions()} {this.renderNarratives()} {this.renderEvents()} {this.renderClusters()} @@ -510,7 +510,7 @@ function mapStateToProps(state) { narratives: selectors.selectNarratives(state), categories: selectors.getCategories(state), sites: selectors.selectSites(state), - shapes: selectors.selectShapes(state), + regions: selectors.selectRegions(state), }, app: { views: state.app.associations.views, @@ -532,7 +532,7 @@ function mapStateToProps(state) { dom: state.ui.dom, narratives: state.ui.style.narratives, mapSelectedEvents: state.ui.style.selectedEvents, - shapes: state.ui.style.shapes, + regions: state.ui.style.regions, eventRadius: state.ui.eventRadius, radial: state.ui.style.clusters.radial, filterColors: state.ui.coloring.colors, diff --git a/src/components/space/carto/atoms/Shapes.js b/src/components/space/carto/atoms/Regions.js similarity index 50% rename from src/components/space/carto/atoms/Shapes.js rename to src/components/space/carto/atoms/Regions.js index ef0297f..2ec9aa9 100644 --- a/src/components/space/carto/atoms/Shapes.js +++ b/src/components/space/carto/atoms/Regions.js @@ -1,13 +1,13 @@ import React from "react"; import { Portal } from "react-portal"; -function MapShapes({ svg, shapes, projectPoint, styles }) { - function renderShape(shape) { +function MapRegions({ svg, regions, projectPoint, styles }) { + function renderRegion(region) { const lineCoords = []; - const points = shape.points.map(projectPoint); + const points = region.points.map(projectPoint); points.forEach((p1, idx) => { - if (idx < shape.points.length - 1) { + if (idx < region.points.length - 1) { const p2 = points[idx + 1]; lineCoords.push({ x1: p1.x, @@ -19,29 +19,29 @@ function MapShapes({ svg, shapes, projectPoint, styles }) { }); return lineCoords.map((coords) => { - const shapeStyles = - shape.name in styles ? styles[shape.name] : styles.default; + const regionstyles = + region.name in styles ? styles[region.name] : styles.default; return ( ); }); } - if (!shapes || !shapes.length) return null; + if (!regions || !regions.length) return null; return ( - - {shapes.map(renderShape)} + + {regions.map(renderRegion)} ); } -export default MapShapes; +export default MapRegions; diff --git a/src/components/time/atoms/DatetimePentagon.js b/src/components/time/atoms/DatetimePentagon.js index 233f54f..fff72bc 100644 --- a/src/components/time/atoms/DatetimePentagon.js +++ b/src/components/time/atoms/DatetimePentagon.js @@ -7,7 +7,7 @@ const DatetimePentagon = ({ x, y, r, transform, onSelect, styleProps }) => { onClick={onSelect} className="event" x={x} - y={y - r} + y={y} style={styleProps} points={`${x},${y + s} ${x + s},${y} ${x + s},${y - s} ${x - s},${ y - s diff --git a/src/components/time/atoms/DatetimeStar.js b/src/components/time/atoms/DatetimeStar.js index 60c590f..7056901 100644 --- a/src/components/time/atoms/DatetimeStar.js +++ b/src/components/time/atoms/DatetimeStar.js @@ -15,7 +15,7 @@ const DatetimeStar = ({ onClick={onSelect} className="event" x={x} - y={y - r} + y={y} style={styleProps} points={`${x + s},${y - s} ${x - r},${y} ${x + r},${y} ${x - s},${ y - s diff --git a/src/components/time/atoms/DatetimeTriangle.js b/src/components/time/atoms/DatetimeTriangle.js index b94c42c..d9b6075 100644 --- a/src/components/time/atoms/DatetimeTriangle.js +++ b/src/components/time/atoms/DatetimeTriangle.js @@ -7,7 +7,7 @@ const DatetimeTriangle = ({ x, y, r, transform, onSelect, styleProps }) => { onClick={onSelect} className="event" x={x} - y={y - r} + y={y} style={styleProps} points={`${x},${y + s} ${x + s},${y - s} ${x - s},${y - s}`} transform={`rotate(180, ${x}, ${y})`} diff --git a/src/components/time/atoms/Events.js b/src/components/time/atoms/Events.js index ca11291..7f5a0e0 100644 --- a/src/components/time/atoms/Events.js +++ b/src/components/time/atoms/Events.js @@ -14,6 +14,7 @@ import { isLatitude, isLongitude, } from "../../../common/utilities"; +import { AVAILABLE_SHAPES } from "../../../common/constants"; function renderDot(event, styles, props) { const colorPercentages = calculateColorPercentages( @@ -156,19 +157,21 @@ const TimelineEvents = ({ (isLatitude(event.latitude) && isLongitude(event.longitude)) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1); + const { shape: eventShape } = event; + let renderShape = isDot ? renderDot : renderBar; - if (event.shape) { - if (event.shape === "bar") { + if (eventShape.shape) { + if (eventShape.shape === AVAILABLE_SHAPES.BAR) { renderShape = renderBar; - } else if (event.shape === "diamond") { + } else if (eventShape.shape === AVAILABLE_SHAPES.DIAMOND) { renderShape = renderDiamond; - } else if (event.shape === "star") { + } else if (eventShape.shape === AVAILABLE_SHAPES.STAR) { renderShape = renderStar; - } else if (event.shape === "triangle") { + } else if (eventShape.shape === AVAILABLE_SHAPES.TRIANGLE) { renderShape = renderTriangle; - } else if (event.shape === "pentagon") { + } else if (eventShape.shape === AVAILABLE_SHAPES.PENTAGON) { renderShape = renderPentagon; - } else if (event.shape === "square") { + } else if (eventShape.shape === AVAILABLE_SHAPES.SQUARE) { renderShape = renderSquare; } else { renderShape = renderDot; diff --git a/src/components/time/atoms/Markers.js b/src/components/time/atoms/Markers.js index 00f3012..e0c06ff 100644 --- a/src/components/time/atoms/Markers.js +++ b/src/components/time/atoms/Markers.js @@ -5,6 +5,7 @@ import { isLatitude, isLongitude, } from "../../../common/utilities"; +import { AVAILABLE_SHAPES } from "../../../common/constants"; const TimelineMarkers = ({ styles, @@ -72,11 +73,11 @@ const TimelineMarkers = ({ function renderMarkerForEvent(y) { switch (event.shape) { case "circle": - case "diamond": - case "star": + case AVAILABLE_SHAPES.DIAMOND: + case AVAILABLE_SHAPES.STAR: acc.push(renderCircle(y)); break; - case "bar": + case AVAILABLE_SHAPES.BAR: acc.push(renderBar(y)); break; default: diff --git a/src/reducers/app.js b/src/reducers/app.js index d137850..829e8a8 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -8,6 +8,7 @@ import { UPDATE_COLORING_SET, CLEAR_FILTER, TOGGLE_ASSOCIATIONS, + TOGGLE_SHAPES, UPDATE_TIMERANGE, UPDATE_DIMENSIONS, UPDATE_NARRATIVE, @@ -26,6 +27,7 @@ import { SET_LOADING, SET_NOT_LOADING, SET_INITIAL_CATEGORIES, + SET_INITIAL_SHAPES, UPDATE_SEARCH_QUERY, } from "../actions"; @@ -153,6 +155,21 @@ function toggleAssociations(appState, action) { }; } +function toggleShapes(appState, action) { + let newShapes = [...appState.shapes]; + if (newShapes.includes(action.shape)) { + const idx = newShapes.indexOf(action.shape); + newShapes.splice(idx, 1); + } else { + newShapes.push(action.shape); + } + + return { + ...appState, + shapes: newShapes, + }; +} + function clearFilter(appState, action) { return { ...appState, @@ -256,6 +273,14 @@ function setInitialCategories(appState, action) { }; } +function setInitialShapes(appState, action) { + const shapeIds = action.values.map((sh) => sh.id); + return { + ...appState, + shapes: shapeIds, + }; +} + function updateSearchQuery(appState, action) { return { ...appState, @@ -275,6 +300,8 @@ function app(appState = initial.app, action) { return clearFilter(appState, action); case TOGGLE_ASSOCIATIONS: return toggleAssociations(appState, action); + case TOGGLE_SHAPES: + return toggleShapes(appState, action); case UPDATE_TIMERANGE: return updateTimeRange(appState, action); case UPDATE_DIMENSIONS: @@ -313,6 +340,8 @@ function app(appState = initial.app, action) { return setNotLoading(appState); case SET_INITIAL_CATEGORIES: return setInitialCategories(appState, action); + case SET_INITIAL_SHAPES: + return setInitialShapes(appState, action); case UPDATE_SEARCH_QUERY: return updateSearchQuery(appState, action); default: diff --git a/src/reducers/validate/regionSchema.js b/src/reducers/validate/regionSchema.js new file mode 100644 index 0000000..dad2d88 --- /dev/null +++ b/src/reducers/validate/regionSchema.js @@ -0,0 +1,8 @@ +import Joi from "joi"; + +const regionSchema = Joi.object().keys({ + name: Joi.string().required(), + items: Joi.array().required(), +}); + +export default regionSchema; diff --git a/src/reducers/validate/shapeSchema.js b/src/reducers/validate/shapeSchema.js index 637f773..c222b4f 100644 --- a/src/reducers/validate/shapeSchema.js +++ b/src/reducers/validate/shapeSchema.js @@ -1,8 +1,10 @@ import Joi from "joi"; const shapeSchema = Joi.object().keys({ - name: Joi.string().required(), - items: Joi.array().required(), + id: Joi.string().allow(""), + title: Joi.string().allow(""), + shape: Joi.string().allow(""), + colour: Joi.string().allow(""), }); export default shapeSchema; diff --git a/src/reducers/validate/validators.js b/src/reducers/validate/validators.js index c520e84..8299575 100644 --- a/src/reducers/validate/validators.js +++ b/src/reducers/validate/validators.js @@ -4,6 +4,7 @@ import createEventSchema from "./eventSchema"; import siteSchema from "./siteSchema"; import associationsSchema from "./associationsSchema"; import sourceSchema from "./sourceSchema"; +import regionSchema from "./regionSchema"; import shapeSchema from "./shapeSchema"; import { calcDatetime, capitalize } from "../../common/utilities"; @@ -53,6 +54,7 @@ export function validateDomain(domain, features) { sites: [], associations: [], sources: {}, + regions: [], shapes: [], notifications: domain ? domain.notifications : null, }; @@ -66,6 +68,7 @@ export function validateDomain(domain, features) { sites: [], associations: [], sources: [], + regions: [], shapes: [], }; @@ -114,14 +117,31 @@ export function validateDomain(domain, features) { validateArray(domain.sites, "sites", siteSchema); validateArray(domain.associations, "associations", associationsSchema); validateObject(domain.sources, "sources", sourceSchema); - validateObject(domain.shapes, "shapes", shapeSchema); + validateArray(domain.regions, "regions", regionSchema); + validateArray(domain.shapes, "shapes", shapeSchema); // NB: [lat, lon] array is best format for projecting into map - sanitizedDomain.shapes = sanitizedDomain.shapes.map((shape) => ({ - name: shape.name, - points: shape.items.map((coords) => coords.replace(/\s/g, "").split(",")), + sanitizedDomain.regions = sanitizedDomain.regions.map((region) => ({ + name: region.name, + points: region.items.map((coords) => coords.replace(/\s/g, "").split(",")), })); + sanitizedDomain.shapes = sanitizedDomain.shapes.reduce((acc, val) => { + if (!val.shape) { + discardedDomain.shapes.push({ + ...val, + error: makeError( + "events", + val.id, + "Invalid event shape. Please specify a shape for this type of event." + ), + }); + } else { + acc.push(val); + } + return acc; + }, []); + const duplicateAssociations = findDuplicateAssociations(domain.associations); // Duplicated associations if (duplicateAssociations.length > 0) { @@ -136,6 +156,7 @@ export function validateDomain(domain, features) { // append events with datetime and sort sanitizedDomain.events = sanitizedDomain.events.filter((event, idx) => { + let errorMsg = ""; event.id = idx; // event.associations comes in as a [association.ids...]; convert to actual association objects event.associations = event.associations.reduce((acc, id) => { @@ -145,19 +166,31 @@ export function validateDomain(domain, features) { if (foundAssociation) acc.push(foundAssociation); return acc; }, []); + + if (event.shape) { + const relatedShapeObj = sanitizedDomain.shapes.find( + (elem) => elem.id === event.shape + ); + if (!relatedShapeObj) + errorMsg = + "Failed to find related shape. Please verify shape type for event."; + else { + event.shape = relatedShapeObj; + } + } // if lat, long come in with commas, replace with decimal format event.latitude = event.latitude.replace(",", "."); event.longitude = event.longitude.replace(",", "."); event.datetime = calcDatetime(event.date, event.time); - if (!isValidDate(event.datetime)) { + if (!isValidDate(event.datetime)) + errorMsg = + "Invalid date. It's been dropped, as otherwise timemap won't work as expected."; + + if (errorMsg) { discardedDomain.events.push({ ...event, - error: makeError( - "events", - event.id, - "Invalid date. It's been dropped, as otherwise timemap won't work as expected." - ), + error: makeError("events", event.id, errorMsg), }); return false; } @@ -177,6 +210,5 @@ export function validateDomain(domain, features) { }); } }); - return sanitizedDomain; } diff --git a/src/scss/toolbar.scss b/src/scss/toolbar.scss index 5950bef..4546e5c 100644 --- a/src/scss/toolbar.scss +++ b/src/scss/toolbar.scss @@ -413,14 +413,26 @@ text-align: left; float: left; - .checkbox { - display: inline-block; - width: 12px; - height: 12px; - border: 1px solid $offwhite; - box-sizing: border-box; + .border { + width: 16px; + height: 16px; background: none; - float: left; + box-sizing: border-box; + position: relative; + + .checkbox { + display: inline-block; + width: 12px; + height: 12px; + border: 1px solid $offwhite; + box-sizing: border-box; + background: none; + float: left; + position: absolute; + top: 2px; + left: 2px; + background: none; + } } } diff --git a/src/selectors/index.js b/src/selectors/index.js index 6ad19d4..a5e3fe9 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -8,7 +8,7 @@ import { createFilterPathString, } from "../common/utilities"; import { isTimeRangedIn } from "./helpers"; -import { ASSOCIATION_MODES, TIMELINE_ONLY } from "../common/constants"; +import { ASSOCIATION_MODES, SHAPE } from "../common/constants"; // Input selectors export const getEvents = (state) => state.domain.events; @@ -24,6 +24,7 @@ export const getActiveNarrative = (state) => state.app.associations.narrative; export const getSelected = (state) => state.app.selected; export const getSites = (state) => state.domain.sites; export const getSources = (state) => state.domain.sources; +export const getRegions = (state) => state.domain.regions; export const getShapes = (state) => state.domain.shapes; export const getFilters = (state) => state.domain.associations.filter( @@ -32,6 +33,7 @@ export const getFilters = (state) => export const getNotifications = (state) => state.domain.notifications; export const getActiveFilters = (state) => state.app.associations.filters; export const getActiveCategories = (state) => state.app.associations.categories; +export const getActiveShapes = (state) => state.app.shapes; export const getTimeRange = (state) => state.app.timeline.range; export const getTimelineDimensions = (state) => state.app.timeline.dimensions; export const selectNarrative = (state) => state.app.associations.narrative; @@ -56,10 +58,10 @@ export const selectSources = createSelector( } ); -export const selectShapes = createSelector( - [getShapes, getFeatures], - (shapes, features) => { - if (features.USE_SHAPES) return shapes; +export const selectRegions = createSelector( + [getRegions, getFeatures], + (regions, features) => { + if (features.USE_REGIONS) return regions; return []; } ); @@ -71,8 +73,22 @@ export const selectShapes = createSelector( * 3. exist in an active category */ export const selectEvents = createSelector( - [getEvents, getActiveFilters, getActiveCategories, getTimeRange, getFeatures], - (events, activeFilters, activeCategories, timeRange, features) => { + [ + getEvents, + getActiveFilters, + getActiveCategories, + getActiveShapes, + getTimeRange, + getFeatures, + ], + ( + events, + activeFilters, + activeCategories, + activeShapes, + timeRange, + features + ) => { return events.reduce((acc, event) => { const isMatchingFilter = (event.associations && @@ -95,8 +111,14 @@ export const selectEvents = createSelector( isActiveTime = features.GRAPH_NONLOCATED ? (!event.latitude && !event.longitude) || isActiveTime : isActiveTime; - if (isActiveTime && isActiveCategory) { - if (event.type === TIMELINE_ONLY || isActiveFilter) { + const isActiveShape = + event.shape && activeShapes.includes(event.shape.id); + if (event.type === SHAPE) { + if (isActiveShape && isActiveCategory && isActiveTime) { + acc[event.id] = { ...event }; + } + } else { + if (isActiveFilter && isActiveCategory && isActiveTime) { acc[event.id] = { ...event }; } } diff --git a/src/store/initial.js b/src/store/initial.js index 40219f8..d0bd782 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -33,6 +33,8 @@ const initial = { associations: [], sources: {}, sites: [], + shapes: [], + regions: [], notifications: [], }, @@ -63,6 +65,7 @@ const initial = { sites: true, }, }, + shapes: [], isMobile: /Mobi/.test(navigator.userAgent), language: "en-US", cluster: { @@ -126,6 +129,12 @@ const initial = { title: copy[language].toolbar.explore_by_narrative__title, description: copy[language].toolbar.explore_by_narrative__description, }, + shapes: { + icon: DEFAULT_TAB_ICONS.SHAPE, + label: copy[language].toolbar.shapes_label, + title: copy[language].toolbar.explore_by_shape__title, + description: copy[language].toolbar.explore_by_shape__description, + }, }, }, loading: false, @@ -149,7 +158,7 @@ const initial = { strokeWidth: 3, }, }, - shapes: { + regions: { default: { stroke: "blue", strokeWidth: 3, @@ -182,7 +191,7 @@ const initial = { USE_ASSOCIATIONS: false, USE_SITES: false, USE_SOURCES: false, - USE_SHAPES: false, + USE_REGIONS: false, GRAPH_NONLOCATED: false, HIGHLIGHT_GROUPS: false, },