Merge branch 'main' into fix-warnings

This commit is contained in:
Lachlan Kermode
2022-04-03 21:24:50 -04:00
committed by GitHub
10 changed files with 280 additions and 49 deletions

View File

@@ -38,8 +38,25 @@ module.exports = {
{ label: "Zoom to 1 month", duration: 31 * one_day },
{ label: "Zoom to 3 months", duration: 3 * 31 * one_day },
],
range: [new Date(Date.now() - 31 * (60 * 60 * 1000 * 24)), new Date()],
// rangeLimits: []
range: {
/**
* Initial date range shown on map load.
* Use [start, end] (strings in ISO 8601 format) for a fixed range.
* Use undefined for a dynamic initial range based on the browser time.
*/
initial: undefined,
/** The number of days to show when using a dynamic initial range */
initialDaysShown: 31,
limits: {
/** Required. The lower bound of the range that can be accessed on the map. (ISO 8601) */
lower: "2022-02-01T00:00:00.000Z",
/**
* The upper bound of the range that can be accessed on the map.
* Defaults to current browser time if undefined.
*/
upper: undefined,
},
},
},
intro: [
'<div style="display:flex; flex-direction: row; width: 100%; min-width: calc(100% - 20px); max-width: 25vw; margin-top: 20px; gap: 20px; justify-content: space-between;"><img style="max-width:35vw; width:50%;" src="https://bellingcat-embeds.ams3.cdn.digitaloceanspaces.com/ukraine-timemap/cover01-s.jpg" frameborder="0"></img><img style="max-width:35vw; width:50%;" src="https://bellingcat-embeds.ams3.cdn.digitaloceanspaces.com/ukraine-timemap/cover02-s.jpg" frameborder="0"></img></div>',

13
package-lock.json generated
View File

@@ -96,6 +96,7 @@
"devDependencies": {
"@babel/highlight": "^7.14.5",
"ava": "1.0.0-beta.8",
"jest-date-mock": "^1.0.8",
"mocha": "^5.2.0",
"node-sass": "^6.0",
"redux-devtools": "^3.4.0"
@@ -13365,6 +13366,12 @@
"node": ">=8"
}
},
"node_modules/jest-date-mock": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/jest-date-mock/-/jest-date-mock-1.0.8.tgz",
"integrity": "sha512-0Lyp+z9xvuNmLbK+5N6FOhSiBeux05Lp5bbveFBmYo40Aggl2wwxFoIrZ+rOWC8nDNcLeBoDd2miQdEDSf3iQw==",
"dev": true
},
"node_modules/jest-diff": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
@@ -36604,6 +36611,12 @@
}
}
},
"jest-date-mock": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/jest-date-mock/-/jest-date-mock-1.0.8.tgz",
"integrity": "sha512-0Lyp+z9xvuNmLbK+5N6FOhSiBeux05Lp5bbveFBmYo40Aggl2wwxFoIrZ+rOWC8nDNcLeBoDd2miQdEDSf3iQw==",
"dev": true
},
"jest-diff": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",

View File

@@ -103,6 +103,7 @@
"devDependencies": {
"@babel/highlight": "^7.14.5",
"ava": "1.0.0-beta.8",
"jest-date-mock": "^1.0.8",
"mocha": "^5.2.0",
"node-sass": "^6.0",
"redux-devtools": "^3.4.0"

View File

@@ -85,7 +85,6 @@ class Dashboard extends React.Component {
matchedEvents.push(events[idx]);
}
// check events before
let ptr = idx - 1;
while (
@@ -98,7 +97,6 @@ class Dashboard extends React.Component {
ptr -= 1;
}
// check events after
ptr = idx + 1;
while (
@@ -279,7 +277,7 @@ class Dashboard extends React.Component {
}
render() {
const { actions, app, domain, features } = this.props;
const { actions, app, domain, timeline, features } = this.props;
const dateHeight = 80;
const padding = 2;
const checkMobile = isMobileOnly || window.innerWidth < 600;
@@ -290,14 +288,14 @@ class Dashboard extends React.Component {
width: checkMobile
? "100vw"
: window.innerWidth > 768
? "60vw"
: "calc(100vw - var(--toolbar-width))",
? "60vw"
: "calc(100vw - var(--toolbar-width))",
maxWidth: checkMobile ? "100vw" : 600,
maxHeight: checkMobile
? "100vh"
: window.innerHeight > 768
? `calc(100vh - ${app.timeline.dimensions.height}px - ${dateHeight}px)`
: "100vh",
? `calc(100vh - ${timeline.dimensions.height}px - ${dateHeight}px)`
: "100vh",
left: checkMobile ? padding : "var(--toolbar-width)",
top: 0,
overflowY: "scroll",
@@ -344,7 +342,7 @@ class Dashboard extends React.Component {
/>
)}
<CardStack
timelineDims={app.timeline.dimensions}
timelineDims={timeline.dimensions}
onViewSource={this.handleViewSource}
onSelect={
app.associations.narrative ? this.selectNarrativeStep : () => null
@@ -357,9 +355,9 @@ class Dashboard extends React.Component {
narrative={
app.associations.narrative
? {
...app.associations.narrative,
current: this.props.narrativeIdx,
}
...app.associations.narrative,
current: this.props.narrativeIdx,
}
: null
}
methods={{
@@ -427,6 +425,9 @@ function mapDispatchToProps(dispatch) {
export default connect(
(state) => ({
...state,
timeline: {
dimensions: selectors.selectDimensions(state),
},
narrativeIdx: selectors.selectNarrativeIdx(state),
narratives: selectors.selectNarratives(state),
selected: selectors.selectSelected(state),

View File

@@ -34,7 +34,7 @@ class Timeline extends React.Component {
dims: props.dimensions,
scaleX: null,
scaleY: null,
timerange: [null, null], // two datetimes
timerange: [null, null], // two Dates
dragPos0: null,
transitionDuration: 300,
};
@@ -47,7 +47,7 @@ class Timeline extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps) {
if (hash(nextProps) !== hash(this.props)) {
this.setState({
timerange: nextProps.app.timeline.range,
timerange: nextProps.timeline.range,
scaleX: this.makeScaleX(),
});
}
@@ -219,7 +219,7 @@ class Timeline extends React.Component {
this.state.scaleX.domain()[0],
extent / 2
);
const { rangeLimits } = this.props.app.timeline;
const { rangeLimits } = this.props.timeline;
let newDomain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2);
let newDomainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2);
@@ -278,7 +278,7 @@ class Timeline extends React.Component {
const dragNow = this.state.scaleX.invert(d3.event.x).getTime();
const timeShift = (drag0 - dragNow) / 1000;
const { range, rangeLimits } = this.props.app.timeline;
const { range, rangeLimits } = this.props.timeline;
let newDomain0 = d3.timeSecond.offset(range[0], timeShift);
let newDomainF = d3.timeSecond.offset(range[1], timeShift);
@@ -361,7 +361,7 @@ class Timeline extends React.Component {
}
render() {
const { isNarrative, app, domain } = this.props;
const { isNarrative, app, timeline, domain } = this.props;
let classes = `timeline-wrapper ${this.state.isFolded ? " folded" : ""}`;
classes += app.narrative !== null ? " narrative-mode" : "";
@@ -405,7 +405,7 @@ class Timeline extends React.Component {
<svg ref={this.svgRef} width={dims.width} style={contentHeight}>
<Clip dims={dims} />
<Axis
ticks={app.timeline.dimensions.ticks}
ticks={timeline.dimensions.ticks}
dims={dims}
extent={this.getTimeScaleExtent()}
transitionDuration={this.state.transitionDuration}
@@ -432,7 +432,7 @@ class Timeline extends React.Component {
.default_categories_label
}
/>
{app.timeline.dimensions.ticks === 1 && (
{timeline.dimensions.ticks === 1 && (
<Handles
dims={dims}
onMoveTime={(dir) => {
@@ -442,7 +442,7 @@ class Timeline extends React.Component {
)}
<ZoomControls
extent={this.getTimeScaleExtent()}
zoomLevels={this.props.app.timeline.zoomLevels}
zoomLevels={timeline.zoomLevels}
dims={dims}
onApplyZoom={this.onApplyZoom}
/>
@@ -504,10 +504,16 @@ function mapStateToProps(state) {
app: {
selected: state.app.selected,
language: state.app.language,
timeline: state.app.timeline,
narrative: state.app.associations.narrative,
coloringSet: state.app.associations.coloringSet,
},
timeline: {
zoomLevels: state.app.timeline.zoomLevels,
dimensions: selectors.selectDimensions(state),
ticks: state.app.timeline.ticks,
range: selectors.selectTimeRange(state),
rangeLimits: selectors.selectTimeRangeLimits(state),
},
ui: {
dom: state.ui.dom,
styles: state.ui.style.selectedEvents,

View File

@@ -0,0 +1,16 @@
import { updateTimeRange } from "../../actions";
import initial from "../../store/initial.js";
import reduce from "../app";
describe("app reducer", () => {
it("can update the selected time range", () => {
const result = reduce(
initial.app,
updateTimeRange(["2022-01-01T00:00:00.000Z", "2022-03-01T00:30:00.000Z"])
);
expect(result.timeline.range.current).toEqual([
"2022-01-01T00:00:00.000Z",
"2022-03-01T00:30:00.000Z",
]);
});
});

View File

@@ -1,6 +1,7 @@
import initial from "../store/initial.js";
import { ASSOCIATION_MODES } from "../common/constants";
import { toggleFlagAC } from "../common/utilities";
import * as selectors from "../selectors";
import {
UPDATE_HIGHLIGHTED,
@@ -68,17 +69,14 @@ function updateColoringSet(appState, action) {
}
function updateNarrative(appState, action) {
let minTime = appState.timeline.range[0];
let maxTime = appState.timeline.range[1];
let [minTime, maxTime] = selectors.selectTimeRange(appState);
const cornerBound0 = [180, 180];
const cornerBound1 = [-180, -180];
// Compute narrative time range and map bounds
if (action.narrative) {
// Forced to comment out min and max time changes, not sure why?
minTime = appState.timeline.rangeLimits[0];
maxTime = appState.timeline.rangeLimits[1];
[minTime, maxTime] = selectors.selectTimeRangeLimits(appState);
// Find max and mins coordinates of narrative events
action.narrative.steps.forEach((step) => {
@@ -119,6 +117,7 @@ function updateNarrative(appState, action) {
minTime = minTime - Math.abs((maxTime - minTime) / 10);
maxTime = maxTime + Math.abs((maxTime - minTime) / 10);
}
return {
...appState,
associations: {
@@ -131,7 +130,10 @@ function updateNarrative(appState, action) {
},
timeline: {
...appState.timeline,
range: [minTime, maxTime],
range: {
...appState.timeline.range,
current: [minTime, maxTime],
},
},
};
}
@@ -200,7 +202,13 @@ function updateTimeRange(appState, action) {
...appState,
timeline: {
...appState.timeline,
range: action.timerange,
range: {
...appState.timeline.range,
current: [
new Date(action.timerange[0]).toISOString(),
new Date(action.timerange[1]).toISOString(),
],
},
},
};
}

View File

@@ -0,0 +1,142 @@
import initial from "../../store/initial";
import { advanceTo, clear } from "jest-date-mock";
import * as selectors from "../";
describe("timeline selectors", () => {
beforeAll(() => {
advanceTo(new Date("2022-02-01T00:00:00.000Z"));
});
afterAll(() => {
clear();
});
const state = (range) => ({
...initial,
app: {
...initial.app,
timeline: {
...initial.app.timeline,
range,
},
},
});
describe("selectTimeRange", () => {
it("returns the currently selected time range", () => {
expect(
selectors.selectTimeRange(
state({
initial: ["2020-03-03T00:00:00.000Z", "2024-01-04T00:00:00.000Z"],
current: ["2021-03-03T00:00:00.000Z", "2023-01-04T00:00:00.000Z"],
initialDaysShown: 31,
limits: {
lower: "2022-02-01T00:00:00.000Z",
upper: undefined,
},
})
)
).toEqual([
new Date("2021-03-03T00:00:00.000Z"),
new Date("2023-01-04T00:00:00.000Z"),
]);
});
it("falls back to a fixed default time range when no current range is set", () => {
expect(
selectors.selectTimeRange(
state({
current: undefined,
initial: ["2020-03-03T00:00:00.000Z", "2024-01-04T00:00:00.000Z"],
initialDaysShown: 31,
limits: {
lower: "2022-02-01T00:00:00.000Z",
upper: undefined,
},
})
)
).toEqual([
new Date("2020-03-03T00:00:00.000Z"),
new Date("2024-01-04T00:00:00.000Z"),
]);
});
it("falls back to a dynamic default time range when no fixed default range or current range is set", () => {
expect(
selectors.selectTimeRange(
state({
current: undefined,
initial: undefined,
initialDaysShown: 31,
limits: {
lower: "2022-02-01T00:00:00.000Z",
upper: undefined,
},
})
)
).toEqual([
new Date("2022-01-01T00:00:00.000Z"),
new Date("2022-02-01T00:00:00.000Z"),
]);
});
it("falls back to a dynamic default if an invalid default range is passed in", () => {
expect(
selectors.selectTimeRange(
state({
current: undefined,
initial: "some garbage data",
initialDaysShown: 31,
limits: {
lower: "2022-02-01T00:00:00.000Z",
upper: undefined,
},
})
)
).toEqual([
new Date("2022-01-01T00:00:00.000Z"),
new Date("2022-02-01T00:00:00.000Z"),
]);
});
});
describe("selectTimeRangeLimits", () => {
it("returns fixed time range limits", () => {
expect(
selectors.selectTimeRangeLimits(
state({
current: undefined,
initial: undefined,
initialDaysShown: 31,
limits: {
lower: "2021-02-01T00:00:00.000Z",
upper: "2023-03-03T00:00:00.000Z",
},
})
)
).toEqual([
new Date("2021-02-01T00:00:00.000Z"),
new Date("2023-03-03T00:00:00.000Z"),
]);
});
it("returns limits from a given lower bound to the current date, when no upper bound is passed in", () => {
expect(
selectors.selectTimeRangeLimits(
state({
current: undefined,
initial: undefined,
initialDaysShown: 31,
limits: {
lower: "2021-02-01T00:00:00.000Z",
upper: undefined,
},
})
)
).toEqual([
new Date("2021-02-01T00:00:00.000Z"),
new Date("2022-02-01T00:00:00.000Z"),
]);
});
});
});

View File

@@ -34,8 +34,6 @@ 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;
export const getFeatures = (state) => state.features;
export const getEventRadius = (state) => state.ui.eventRadius;
@@ -67,6 +65,47 @@ export const selectRegions = createSelector(
}
);
const getTimeRange = (state) => state.app.timeline.range.current;
const getInitialTimeRange = (state) => state.app.timeline.range.initial;
const getInitialDaysShown = (state) =>
state.app.timeline.range.initialDaysShown;
export const selectTimeRange = createSelector(
[getTimeRange, getInitialTimeRange, getInitialDaysShown],
(range, initialRange, initialDaysShown) => {
let start, end;
if (Array.isArray(range) && range.length === 2) {
[start, end] = range;
} else if (Array.isArray(initialRange) && initialRange.length === 2) {
[start, end] = initialRange;
} else {
end = new Date();
start = new Date(end.getTime() - initialDaysShown * 24 * 60 * 60 * 1000);
}
return [new Date(start), new Date(end)];
}
);
const getTimeRangeLimits = (state) => state.app.timeline.range.limits;
export const selectTimeRangeLimits = createSelector(
getTimeRangeLimits,
(limits) => {
return [new Date(limits.lower), new Date(limits.upper || Date.now())];
}
);
const getTimelineDimensions = (state) => state.app.timeline.dimensions;
export const selectDimensions = createSelector(
getTimelineDimensions,
(dimensions) => {
return {
...dimensions,
trackHeight: dimensions.contentHeight - 50, // height of time labels
};
}
);
/**
* Of all available events, selects those that
* 1. fall in time range
@@ -79,7 +118,7 @@ export const selectEvents = createSelector(
getActiveFilters,
getActiveCategories,
getActiveShapes,
getTimeRange,
selectTimeRange,
getFeatures,
],
(
@@ -353,13 +392,3 @@ export const selectSelected = createSelector(
return selected.map(insetSourceFrom(sources));
}
);
export const selectDimensions = createSelector(
[getTimelineDimensions],
(dimensions) => {
return {
...dimensions,
trackHeight: dimensions.contentHeight - 50, // height of time labels
};
}
);

View File

@@ -6,7 +6,7 @@ import { language } from "../common/utilities";
import { DEFAULT_TAB_ICONS } from "../common/constants";
const isSmallLaptop = window.innerHeight < 800;
const mapIniital = {
const mapInitial = {
anchor: [31.356397, 34.784818],
startZoom: 11,
minZoom: 2,
@@ -83,8 +83,9 @@ const initial = {
contentHeight: isSmallLaptop ? 160 : 200,
width_controls: 100,
},
range: [new Date(2001, 2, 23, 12), new Date(2021, 2, 23, 12)],
rangeLimits: [new Date(1, 1, 1, 1), new Date()],
range: {
current: null,
},
zoomLevels: copy[language].timeline.zoomLevels || [
{ label: "20 years", duration: 10512000 },
{ label: "2 years", duration: 1051200 },
@@ -210,13 +211,10 @@ if (process.env.store) {
appStore = initial;
}
// NB: config.js dates get implicitly converted to strings in mergeDeepLeft
appStore.app.timeline.range[0] = new Date(appStore.app.timeline.range[0]);
appStore.app.timeline.range[1] = new Date(appStore.app.timeline.range[1]);
appStore.app.flags.isIntropopup = !!appStore.app.intro;
if ("map" in appStore.app) {
appStore.app.map = mergeDeepLeft(appStore.app.map, mapIniital);
appStore.app.map = mergeDeepLeft(appStore.app.map, mapInitial);
}
if ("space3d" in appStore.app) {