mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 13:28:36 +03:00
Supporting "highlighting" events via URL parameters (#85)
* Synchronize map state to URL * Add support for highlighting specific events based on URL parameters
This commit is contained in:
@@ -469,6 +469,7 @@ class Map extends Component {
|
||||
categories={this.props.domain.categories}
|
||||
projectPoint={this.projectPoint}
|
||||
selected={this.props.app.selected}
|
||||
highlighted={this.props.app.highlighted}
|
||||
narrative={this.props.app.narrative}
|
||||
onSelect={this.props.methods.onSelect}
|
||||
getCategoryColor={this.props.methods.getCategoryColor}
|
||||
@@ -495,6 +496,7 @@ class Map extends Component {
|
||||
coloringSet={this.props.app.coloringSet}
|
||||
getClusterChildren={this.getClusterChildren}
|
||||
filterColors={this.props.ui.filterColors}
|
||||
highlighted={this.props.app.highlighted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
calculateTotalClusterPoints,
|
||||
} from "../../../../common/utilities";
|
||||
|
||||
const HIGHLIGHT_COLOR = "#E31A1B";
|
||||
|
||||
const DefsClusters = () => (
|
||||
<defs>
|
||||
<radialGradient id="clusterGradient">
|
||||
@@ -32,6 +34,7 @@ function Cluster({
|
||||
getClusterChildren,
|
||||
coloringSet,
|
||||
filterColors,
|
||||
highlighted,
|
||||
}) {
|
||||
/**
|
||||
{
|
||||
@@ -50,10 +53,48 @@ function Cluster({
|
||||
const { cluster_id: clusterId } = cluster.properties;
|
||||
|
||||
const individualChildren = getClusterChildren(clusterId);
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
individualChildren,
|
||||
coloringSet
|
||||
);
|
||||
|
||||
// Calculate percentage of highlighted events in this cluster
|
||||
const allEvents = individualChildren.flatMap((loc) => loc.events);
|
||||
const totalEvents = allEvents.length;
|
||||
const highlightedEvents =
|
||||
highlighted && highlighted.length > 0
|
||||
? allEvents.filter((event) => highlighted.includes(event.civId))
|
||||
: [];
|
||||
const highlightedCount = highlightedEvents.length;
|
||||
const highlightedPercent = totalEvents > 0 ? highlightedCount / totalEvents : 0;
|
||||
|
||||
let colorPercentMap;
|
||||
if (highlightedPercent === 1) {
|
||||
// All events are highlighted
|
||||
colorPercentMap = { [HIGHLIGHT_COLOR]: 1 };
|
||||
} else if (highlightedPercent > 0) {
|
||||
// Mix of highlighted and non-highlighted events
|
||||
const nonHighlightedChildren = individualChildren.map((loc) => ({
|
||||
...loc,
|
||||
events: loc.events.filter(
|
||||
(event) => !highlighted || !highlighted.includes(event.civId)
|
||||
),
|
||||
})).filter((loc) => loc.events.length > 0);
|
||||
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
nonHighlightedChildren,
|
||||
coloringSet
|
||||
);
|
||||
// Scale down the category percentages and add highlight percentage
|
||||
const scaledPercentages = colorPercentages.map(
|
||||
(p) => p * (1 - highlightedPercent)
|
||||
);
|
||||
colorPercentMap = zipColorsToPercentages(filterColors, scaledPercentages);
|
||||
colorPercentMap[HIGHLIGHT_COLOR] = highlightedPercent;
|
||||
} else {
|
||||
// No highlighted events
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
individualChildren,
|
||||
coloringSet
|
||||
);
|
||||
colorPercentMap = zipColorsToPercentages(filterColors, colorPercentages);
|
||||
}
|
||||
|
||||
const { coordinates } = cluster.geometry;
|
||||
const [longitude, latitude] = coordinates;
|
||||
@@ -72,10 +113,7 @@ function Cluster({
|
||||
>
|
||||
<ColoredMarkers
|
||||
radius={size}
|
||||
colorPercentMap={zipColorsToPercentages(
|
||||
filterColors,
|
||||
colorPercentages
|
||||
)}
|
||||
colorPercentMap={colorPercentMap}
|
||||
styles={{
|
||||
...styles,
|
||||
}}
|
||||
@@ -97,6 +135,7 @@ function ClusterEvents({
|
||||
clusters,
|
||||
filterColors,
|
||||
selected,
|
||||
highlighted,
|
||||
}) {
|
||||
const totalPoints = calculateTotalClusterPoints(clusters);
|
||||
|
||||
@@ -144,6 +183,7 @@ function ClusterEvents({
|
||||
coloringSet={coloringSet}
|
||||
cluster={c}
|
||||
filterColors={filterColors}
|
||||
highlighted={highlighted}
|
||||
size={clusterSize}
|
||||
projectPoint={projectPoint}
|
||||
totalPoints={totalPoints}
|
||||
|
||||
@@ -8,12 +8,15 @@ import {
|
||||
zipColorsToPercentages,
|
||||
} from "../../../../common/utilities";
|
||||
|
||||
const HIGHLIGHT_COLOR = "#E31A1B";
|
||||
|
||||
function MapEvents({
|
||||
getCategoryColor,
|
||||
categories,
|
||||
projectPoint,
|
||||
styleLocation,
|
||||
selected,
|
||||
highlighted,
|
||||
narrative,
|
||||
onSelect,
|
||||
svg,
|
||||
@@ -46,18 +49,53 @@ function MapEvents({
|
||||
}
|
||||
|
||||
function renderLocationSlicesByAssociation(location) {
|
||||
const colorPercentages = calculateColorPercentages([location], coloringSet);
|
||||
|
||||
const styles = {
|
||||
stroke: colors.darkBackground,
|
||||
strokeWidth: 0,
|
||||
fillOpacity: narrative ? 1 : calcOpacity(location.events.length),
|
||||
};
|
||||
|
||||
// Calculate percentage of highlighted events
|
||||
const totalEvents = location.events.length;
|
||||
const highlightedEvents =
|
||||
highlighted && highlighted.length > 0
|
||||
? location.events.filter((event) => highlighted.includes(event.civId))
|
||||
: [];
|
||||
const highlightedCount = highlightedEvents.length;
|
||||
const highlightedPercent = highlightedCount / totalEvents;
|
||||
|
||||
// Get non-highlighted events for category color calculation
|
||||
const nonHighlightedEvents = location.events.filter(
|
||||
(event) => !highlighted || !highlighted.includes(event.civId)
|
||||
);
|
||||
|
||||
let colorPercentMap;
|
||||
if (highlightedPercent === 1) {
|
||||
// All events are highlighted
|
||||
colorPercentMap = { [HIGHLIGHT_COLOR]: 1 };
|
||||
} else if (highlightedPercent > 0) {
|
||||
// Mix of highlighted and non-highlighted events
|
||||
const nonHighlightedLocation = { ...location, events: nonHighlightedEvents };
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
[nonHighlightedLocation],
|
||||
coloringSet
|
||||
);
|
||||
// Scale down the category percentages and add highlight percentage
|
||||
const scaledPercentages = colorPercentages.map(
|
||||
(p) => p * (1 - highlightedPercent)
|
||||
);
|
||||
colorPercentMap = zipColorsToPercentages(filterColors, scaledPercentages);
|
||||
colorPercentMap[HIGHLIGHT_COLOR] = highlightedPercent;
|
||||
} else {
|
||||
// No highlighted events
|
||||
const colorPercentages = calculateColorPercentages([location], coloringSet);
|
||||
colorPercentMap = zipColorsToPercentages(filterColors, colorPercentages);
|
||||
}
|
||||
|
||||
return (
|
||||
<ColoredMarkers
|
||||
radius={eventRadius}
|
||||
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
|
||||
colorPercentMap={colorPercentMap}
|
||||
styles={{
|
||||
...styles,
|
||||
}}
|
||||
|
||||
@@ -464,6 +464,7 @@ class Timeline extends Component {
|
||||
eventRadius={this.props.ui.eventRadius}
|
||||
filterColors={this.props.ui.filterColors}
|
||||
coloringSet={this.props.app.coloringSet}
|
||||
highlighted={this.props.app.highlighted}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -510,6 +511,7 @@ function mapStateToProps(state) {
|
||||
},
|
||||
app: {
|
||||
selected: state.app.selected,
|
||||
highlighted: state.app.highlighted,
|
||||
language: state.app.language,
|
||||
narrative: state.app.associations.narrative,
|
||||
coloringSet: state.app.associations.coloringSet,
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "../../../common/utilities";
|
||||
import { AVAILABLE_SHAPES } from "../../../common/constants";
|
||||
|
||||
const HIGHLIGHT_COLOR = "#E31A1B";
|
||||
|
||||
function renderDot(event, styles, props) {
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
[event],
|
||||
@@ -144,6 +146,7 @@ const TimelineEvents = ({
|
||||
eventRadius,
|
||||
filterColors,
|
||||
coloringSet,
|
||||
highlighted,
|
||||
}) => {
|
||||
const narIds = narrative ? narrative.steps.map((s) => s.id) : [];
|
||||
|
||||
@@ -178,13 +181,23 @@ const TimelineEvents = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this event is highlighted
|
||||
const isHighlighted =
|
||||
highlighted &&
|
||||
highlighted.length > 0 &&
|
||||
highlighted.includes(event.civId);
|
||||
|
||||
// if an event has multiple categories, it should be rendered on each of
|
||||
// those timelines: so we create as many event 'shadows' as there are
|
||||
// categories
|
||||
const evShadows = getEventCategories(event, categories).map((cat) => {
|
||||
const y = getY({ ...event, category: cat });
|
||||
|
||||
const colour = event.colour ? event.colour : getCategoryColor(cat.title);
|
||||
const colour = isHighlighted
|
||||
? HIGHLIGHT_COLOR
|
||||
: event.colour
|
||||
? event.colour
|
||||
: getCategoryColor(cat.title);
|
||||
|
||||
const styles = {
|
||||
fill: colour,
|
||||
|
||||
@@ -52,7 +52,7 @@ const initial = {
|
||||
errors: {
|
||||
source: false,
|
||||
},
|
||||
highlighted: null,
|
||||
highlighted: [],
|
||||
selected: [],
|
||||
source: null,
|
||||
associations: {
|
||||
|
||||
@@ -70,6 +70,19 @@ export const SCHEMA = Object.freeze({
|
||||
}
|
||||
},
|
||||
},
|
||||
hid: {
|
||||
key: "hid",
|
||||
trigger: null, // Read-only from URL, no action triggers update
|
||||
type: SCHEMA_TYPES.STRING_ARRAY,
|
||||
dehydrate() {
|
||||
return []; // Never update URL from state
|
||||
},
|
||||
rehydrate(nextState, { hid }) {
|
||||
if (hid?.length) {
|
||||
nextState.app.highlighted = hid;
|
||||
}
|
||||
},
|
||||
},
|
||||
range: {
|
||||
key: "range",
|
||||
trigger: UPDATE_TIMERANGE,
|
||||
|
||||
Reference in New Issue
Block a user