mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 13:28:36 +03:00
Polygon shapes abstracted to be types of checkboxes; abstracted rendering of panel into one component; importing shapes from separate endpoint
This commit is contained in:
@@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<TabPanel>
|
||||
<h2>{panelTitle}</h2>
|
||||
<p>{panelDescription}</p>
|
||||
<h2>{panels.narratives.label}</h2>
|
||||
<p>{panels.narratives.description}</p>
|
||||
{this.props.narratives.map((narr) => {
|
||||
return (
|
||||
<div className="panel-action action">
|
||||
@@ -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 (
|
||||
<TabPanel>
|
||||
@@ -133,8 +119,9 @@ 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}
|
||||
checkboxColor={this.props.categoriesCheckboxColor}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
@@ -143,13 +130,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 (
|
||||
<TabPanel>
|
||||
<FilterListPanel
|
||||
@@ -159,13 +139,33 @@ class Toolbar extends React.Component {
|
||||
language={this.props.language}
|
||||
coloringSet={this.props.coloringSet}
|
||||
filterColors={this.props.filterColors}
|
||||
title={panelTitle}
|
||||
description={panelDescription}
|
||||
title={panels.filters.label}
|
||||
description={panels.filters.description}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
||||
renderToolbarShapePanel() {
|
||||
const { panels } = this.props.toolbarCopy;
|
||||
|
||||
if (this.props.features.USE_SHAPES) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<ShapesListPanel
|
||||
shapes={this.props.shapes}
|
||||
activeShapes={this.props.activeShapes}
|
||||
onShapeFilter={this.props.methods.onShapeFilter}
|
||||
language={this.props.language}
|
||||
title={panels.shapes.label}
|
||||
description={panels.shapes.description}
|
||||
checkboxColor={this.props.shapesCheckboxColor}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderToolbarTab(_selected, label, iconKey) {
|
||||
const isActive = this.state._selected === _selected;
|
||||
const classes = isActive ? "toolbar-tab active" : "toolbar-tab";
|
||||
@@ -196,6 +196,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}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
@@ -228,31 +229,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 (
|
||||
<div className="toolbar">
|
||||
<div className="toolbar-header" onClick={this.props.methods.onTitle}>
|
||||
@@ -260,17 +247,32 @@ class Toolbar extends React.Component {
|
||||
</div>
|
||||
<div className="toolbar-tabs">
|
||||
{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}
|
||||
</div>
|
||||
<BottomActions
|
||||
@@ -311,10 +313,12 @@ function mapStateToProps(state) {
|
||||
filters: selectors.getFilters(state),
|
||||
categories: selectors.getCategories(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
shapes: selectors.getShapes(state),
|
||||
language: state.app.language,
|
||||
toolbarCopy: state.app.toolbar,
|
||||
activeFilters: selectors.getActiveFilters(state),
|
||||
activeCategories: selectors.getActiveCategories(state),
|
||||
activeShapes: selectors.getActiveShapes(state),
|
||||
viewFilters: state.app.associations.views,
|
||||
narrative: state.app.associations.narrative,
|
||||
sitesShowing: state.app.flags.isShowingSites,
|
||||
@@ -322,6 +326,9 @@ function mapStateToProps(state) {
|
||||
coloringSet: state.app.associations.coloringSet,
|
||||
maxNumOfColors: state.ui.coloring.maxNumOfColors,
|
||||
filterColors: state.ui.coloring.colors,
|
||||
eventRadius: state.ui.eventRadius,
|
||||
categoriesCheckboxColor: state.ui.style.categories.checkboxColor,
|
||||
shapesCheckboxColor: state.ui.style.shapes.checkboxColor,
|
||||
features: selectors.getFeatures(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
const Checkbox = ({ label, isActive, onClickCheckbox, color }) => {
|
||||
const styles = {
|
||||
background: isActive ? color : "none",
|
||||
border: `1px solid ${color}`,
|
||||
};
|
||||
|
||||
const Checkbox = ({ label, isActive, onClickCheckbox, color, styleProps }) => {
|
||||
return (
|
||||
<div className={isActive ? "item active" : "item"}>
|
||||
<span style={{ color: color }}>{label}</span>
|
||||
<button onClick={onClickCheckbox}>
|
||||
<div className="checkbox" style={styles} />
|
||||
<div className="border">
|
||||
<div className="checkbox" style={styleProps} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import marked from "marked";
|
||||
import Checkbox from "../atoms/Checkbox";
|
||||
import PanelTree from "./atoms/PanelTree";
|
||||
|
||||
const CategoriesListPanel = ({
|
||||
categories,
|
||||
@@ -9,29 +9,8 @@ const CategoriesListPanel = ({
|
||||
language,
|
||||
title,
|
||||
description,
|
||||
checkboxColor,
|
||||
}) => {
|
||||
function renderCategoryTree() {
|
||||
return (
|
||||
<div>
|
||||
{categories.map((cat) => {
|
||||
return (
|
||||
<li
|
||||
key={cat.title.replace(/ /g, "_")}
|
||||
className="filter-filter active"
|
||||
style={{ marginLeft: "20px" }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.title}
|
||||
isActive={activeCategories.includes(cat.title)}
|
||||
onClickCheckbox={() => onCategoryFilter(cat.title)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="react-innertabpanel">
|
||||
<h2>{title}</h2>
|
||||
@@ -40,7 +19,12 @@ const CategoriesListPanel = ({
|
||||
__html: marked(description),
|
||||
}}
|
||||
/>
|
||||
{renderCategoryTree()}
|
||||
<PanelTree
|
||||
data={categories}
|
||||
activeValues={activeCategories}
|
||||
onSelect={onCategoryFilter}
|
||||
defaultCheckboxColor={checkboxColor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
34
src/components/controls/ShapesListPanel.js
Normal file
34
src/components/controls/ShapesListPanel.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import marked from "marked";
|
||||
import PanelTree from "./atoms/PanelTree";
|
||||
import { mapStyleByShape } from "../../common/utilities";
|
||||
|
||||
const ShapesListPanel = ({
|
||||
shapes,
|
||||
activeShapes,
|
||||
onShapeFilter,
|
||||
language,
|
||||
title,
|
||||
description,
|
||||
checkboxColor,
|
||||
}) => {
|
||||
const styledShapes = mapStyleByShape(shapes, activeShapes);
|
||||
return (
|
||||
<div className="react-innertabpanel">
|
||||
<h2>{title}</h2>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked(description),
|
||||
}}
|
||||
/>
|
||||
<PanelTree
|
||||
data={styledShapes}
|
||||
activeValues={activeShapes}
|
||||
onSelect={onShapeFilter}
|
||||
defaultCheckboxColor={checkboxColor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShapesListPanel;
|
||||
32
src/components/controls/atoms/PanelTree.js
Normal file
32
src/components/controls/atoms/PanelTree.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import Checkbox from "../../atoms/Checkbox";
|
||||
|
||||
const PanelTree = ({ data, activeValues, onSelect, defaultCheckboxColor }) => {
|
||||
return (
|
||||
<div>
|
||||
{data.map((val) => {
|
||||
const isActive = activeValues.includes(val.title);
|
||||
const baseStyles = {
|
||||
background: isActive ? defaultCheckboxColor : "none",
|
||||
border: `1px solid ${defaultCheckboxColor}`,
|
||||
};
|
||||
return (
|
||||
<li
|
||||
key={val.title.replace(/ /g, "_")}
|
||||
className="filter-filter active"
|
||||
style={{ marginLeft: "20px" }}
|
||||
>
|
||||
<Checkbox
|
||||
label={val.title}
|
||||
isActive={isActive}
|
||||
onClickCheckbox={() => onSelect(val.title)}
|
||||
styleProps={val.styles ? val.styles : baseStyles}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PanelTree;
|
||||
@@ -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 (
|
||||
<Shapes
|
||||
<Regions
|
||||
svg={this.svgRef.current}
|
||||
shapes={this.props.domain.shapes}
|
||||
regions={this.props.domain.regions}
|
||||
projectPoint={this.projectPoint}
|
||||
styles={this.props.ui.shapes}
|
||||
styles={this.props.ui.regions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<line
|
||||
id={`${shape.name}_style`}
|
||||
id={`${region.name}_style`}
|
||||
markerStart="none"
|
||||
{...coords}
|
||||
style={shapeStyles}
|
||||
style={regionstyles}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!shapes || !shapes.length) return null;
|
||||
if (!regions || !regions.length) return null;
|
||||
|
||||
return (
|
||||
<Portal node={svg}>
|
||||
<g id="shapes-layer" className="narrative">
|
||||
{shapes.map(renderShape)}
|
||||
<g id="regions-layer" className="narrative">
|
||||
{regions.map(renderRegion)}
|
||||
</g>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapShapes;
|
||||
export default MapRegions;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})`}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isLatitude,
|
||||
isLongitude,
|
||||
} from "../../../common/utilities";
|
||||
import { AVAILABLE_SHAPES } from "../../../common/constants";
|
||||
|
||||
function renderDot(event, styles, props) {
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
@@ -158,17 +159,17 @@ const TimelineEvents = ({
|
||||
|
||||
let renderShape = isDot ? renderDot : renderBar;
|
||||
if (event.shape) {
|
||||
if (event.shape === "bar") {
|
||||
if (event.shape === AVAILABLE_SHAPES.BAR) {
|
||||
renderShape = renderBar;
|
||||
} else if (event.shape === "diamond") {
|
||||
} else if (event.shape === AVAILABLE_SHAPES.DIAMOND) {
|
||||
renderShape = renderDiamond;
|
||||
} else if (event.shape === "star") {
|
||||
} else if (event.shape === AVAILABLE_SHAPES.STAR) {
|
||||
renderShape = renderStar;
|
||||
} else if (event.shape === "triangle") {
|
||||
} else if (event.shape === AVAILABLE_SHAPES.TRIANGLE) {
|
||||
renderShape = renderTriangle;
|
||||
} else if (event.shape === "pentagon") {
|
||||
} else if (event.shape === AVAILABLE_SHAPES.PENTAGON) {
|
||||
renderShape = renderPentagon;
|
||||
} else if (event.shape === "square") {
|
||||
} else if (event.shape === AVAILABLE_SHAPES.SQUARE) {
|
||||
renderShape = renderSquare;
|
||||
} else {
|
||||
renderShape = renderDot;
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user