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:
Logan Williams
2026-01-06 08:14:11 -05:00
committed by GitHub
parent 6fab980c46
commit a2c6621669
7 changed files with 121 additions and 13 deletions

View File

@@ -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}
/>
);
}

View File

@@ -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}

View File

@@ -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,
}}

View File

@@ -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,

View File

@@ -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,

View File

@@ -52,7 +52,7 @@ const initial = {
errors: {
source: false,
},
highlighted: null,
highlighted: [],
selected: [],
source: null,
associations: {

View File

@@ -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,