mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-08 03:18:36 +03:00
Co-authored-by: Juan Camilo González <j.gonzalezj@uniandes.edu.co> Co-authored-by: msramalho <19508417+msramalho@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,7 @@ module.exports = {
|
||||
MAPBOX_TOKEN:
|
||||
"pk.eyJ1IjoiYmVsbGluZ2NhdC1tYXBib3giLCJhIjoiY2tleW0wbWliMDA1cTJ5bzdkbTRraHgwZSJ9.GJQkjPzj8554VhR5SPsfJg",
|
||||
// MEDIA_EXT: "/api/media",
|
||||
DATE_FMT: "MM/DD/YYYY",
|
||||
DATE_FMT: "M/D/YYYY",
|
||||
TIME_FMT: "HH:mm",
|
||||
|
||||
store: {
|
||||
|
||||
900
package-lock.json
generated
900
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,8 @@
|
||||
"camelcase": "^6.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.3.0",
|
||||
"css-loader": "4.3.0",
|
||||
"d3": "^5.7.0",
|
||||
"d3": "^7.4.2",
|
||||
"dayjs": "^1.11.0",
|
||||
"dotenv": "8.2.0",
|
||||
"dotenv-expand": "5.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
@@ -59,7 +60,6 @@
|
||||
"lint-staged": "^10.5.3",
|
||||
"marked": "^0.7.0",
|
||||
"mini-css-extract-plugin": "0.11.3",
|
||||
"moment": "^2.26.0",
|
||||
"object-hash": "^1.3.0",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.4",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import moment from "moment";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import dayjs from "dayjs";
|
||||
import hash from "object-hash";
|
||||
import { timeFormatDefaultLocale } from "d3";
|
||||
|
||||
import { ASSOCIATION_MODES, POLYGON_CLIP_PATH } from "./constants";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
let { DATE_FMT, TIME_FMT } = process.env;
|
||||
if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY";
|
||||
if (!TIME_FMT) TIME_FMT = "HH:mm";
|
||||
@@ -16,7 +20,7 @@ export function getPathLeaf(path) {
|
||||
|
||||
export function calcDatetime(date, time) {
|
||||
if (!time) time = "00:00";
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`);
|
||||
const dt = dayjs(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`);
|
||||
return dt.toDate();
|
||||
}
|
||||
|
||||
@@ -499,15 +503,14 @@ export function makeNiceDate(datetime) {
|
||||
|
||||
/**
|
||||
* Sets the default locale for d3 to format dates in each available language.
|
||||
* @param {Object} d3 - An instance of D3
|
||||
*/
|
||||
export function setD3Locale(d3) {
|
||||
export function setD3Locale() {
|
||||
const languages = {
|
||||
"es-MX": require("./data/es-MX.json"),
|
||||
};
|
||||
|
||||
if (language !== "es-US" && languages[language]) {
|
||||
d3.timeFormatDefaultLocale(languages[language]);
|
||||
timeFormatDefaultLocale(languages[language]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import * as d3 from "d3";
|
||||
import { axisBottom, timeFormat, select } from "d3";
|
||||
import { setD3Locale } from "../../common/utilities";
|
||||
|
||||
const TEXT_HEIGHT = 15;
|
||||
setD3Locale(d3);
|
||||
setD3Locale();
|
||||
class TimelineAxis extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -33,30 +33,28 @@ class TimelineAxis extends React.Component {
|
||||
|
||||
const { marginTop, contentHeight } = this.props.dims;
|
||||
if (this.props.scaleX) {
|
||||
this.x0 = d3
|
||||
.axisBottom(this.props.scaleX)
|
||||
this.x0 = axisBottom(this.props.scaleX)
|
||||
.ticks(this.props.ticks)
|
||||
.tickPadding(0)
|
||||
.tickSize(contentHeight - TEXT_HEIGHT - marginTop)
|
||||
.tickFormat(d3.timeFormat(fstFmt));
|
||||
.tickFormat(timeFormat(fstFmt));
|
||||
|
||||
this.x1 = d3
|
||||
.axisBottom(this.props.scaleX)
|
||||
this.x1 = axisBottom(this.props.scaleX)
|
||||
.ticks(this.props.ticks)
|
||||
.tickPadding(marginTop)
|
||||
.tickSize(0)
|
||||
.tickFormat(d3.timeFormat(sndFmt));
|
||||
.tickFormat(timeFormat(sndFmt));
|
||||
|
||||
if (!this.state.isInitialized) this.setState({ isInitialized: true });
|
||||
}
|
||||
|
||||
if (this.state.isInitialized) {
|
||||
d3.select(this.xAxis0Ref.current)
|
||||
select(this.xAxis0Ref.current)
|
||||
.transition()
|
||||
.duration(this.props.transitionDuration)
|
||||
.call(this.x0);
|
||||
|
||||
d3.select(this.xAxis1Ref.current)
|
||||
select(this.xAxis1Ref.current)
|
||||
.transition()
|
||||
.duration(this.props.transitionDuration)
|
||||
.call(this.x1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import * as d3 from "d3";
|
||||
import { drag as d3Drag, select } from "d3";
|
||||
|
||||
class TimelineCategories extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -12,13 +12,12 @@ class TimelineCategories extends React.Component {
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.state.isInitialized) {
|
||||
const drag = d3
|
||||
.drag()
|
||||
const drag = d3Drag()
|
||||
.on("start", this.props.onDragStart)
|
||||
.on("drag", this.props.onDrag)
|
||||
.on("end", this.props.onDragEnd);
|
||||
|
||||
d3.select(this.grabRef.current).call(drag);
|
||||
select(this.grabRef.current).call(drag);
|
||||
|
||||
this.setState({ isInitialized: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import * as d3 from "d3";
|
||||
import { scaleTime, timeMinute, timeSecond } from "d3";
|
||||
import hash from "object-hash";
|
||||
|
||||
import { setLoading, setNotLoading, updateTicks } from "../../actions";
|
||||
@@ -26,6 +26,9 @@ class Timeline extends React.Component {
|
||||
this.getY = this.getY.bind(this);
|
||||
this.onApplyZoom = this.onApplyZoom.bind(this);
|
||||
this.onSelect = this.onSelect.bind(this);
|
||||
this.onDragStart = this.onDragStart.bind(this);
|
||||
this.onDrag = this.onDrag.bind(this);
|
||||
this.onDragEnd = this.onDragEnd.bind(this);
|
||||
this.svgRef = React.createRef();
|
||||
this.state = {
|
||||
isFolded:
|
||||
@@ -90,8 +93,7 @@ class Timeline extends React.Component {
|
||||
}
|
||||
|
||||
makeScaleX() {
|
||||
return d3
|
||||
.scaleTime()
|
||||
return scaleTime()
|
||||
.domain(this.state.timerange)
|
||||
.range([
|
||||
this.state.dims.marginLeft,
|
||||
@@ -170,19 +172,19 @@ class Timeline extends React.Component {
|
||||
*/
|
||||
onMoveTime(direction) {
|
||||
const extent = this.getTimeScaleExtent();
|
||||
const newCentralTime = d3.timeMinute.offset(
|
||||
const newCentralTime = timeMinute.offset(
|
||||
this.state.scaleX.domain()[0],
|
||||
extent
|
||||
);
|
||||
|
||||
// if forward
|
||||
let domain0 = newCentralTime;
|
||||
let domainF = d3.timeMinute.offset(newCentralTime, extent);
|
||||
let domainF = timeMinute.offset(newCentralTime, extent);
|
||||
|
||||
// if backwards
|
||||
if (direction === "backwards") {
|
||||
domain0 = d3.timeMinute.offset(newCentralTime, -(2 * extent));
|
||||
domainF = d3.timeMinute.offset(newCentralTime, -extent);
|
||||
domain0 = timeMinute.offset(newCentralTime, -(2 * extent));
|
||||
domainF = timeMinute.offset(newCentralTime, -extent);
|
||||
}
|
||||
|
||||
this.props.methods.onUpdateTimerange([domain0, domainF]);
|
||||
@@ -192,8 +194,8 @@ class Timeline extends React.Component {
|
||||
onCenterTime(newCentralTime) {
|
||||
const extent = this.getTimeScaleExtent();
|
||||
|
||||
const domain0 = d3.timeMinute.offset(newCentralTime, -extent / 2);
|
||||
const domainF = d3.timeMinute.offset(newCentralTime, +extent / 2);
|
||||
const domain0 = timeMinute.offset(newCentralTime, -extent / 2);
|
||||
const domainF = timeMinute.offset(newCentralTime, +extent / 2);
|
||||
|
||||
this.setState({ timerange: [domain0, domainF] }, () => {
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
||||
@@ -215,14 +217,14 @@ class Timeline extends React.Component {
|
||||
*/
|
||||
onApplyZoom(zoom) {
|
||||
const extent = this.getTimeScaleExtent();
|
||||
const newCentralTime = d3.timeMinute.offset(
|
||||
const newCentralTime = timeMinute.offset(
|
||||
this.state.scaleX.domain()[0],
|
||||
extent / 2
|
||||
);
|
||||
const { rangeLimits } = this.props.timeline;
|
||||
|
||||
let newDomain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2);
|
||||
let newDomainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2);
|
||||
let newDomain0 = timeMinute.offset(newCentralTime, -zoom.duration / 2);
|
||||
let newDomainF = timeMinute.offset(newCentralTime, zoom.duration / 2);
|
||||
|
||||
if (rangeLimits) {
|
||||
// If the store contains absolute time limits,
|
||||
@@ -232,11 +234,11 @@ class Timeline extends React.Component {
|
||||
|
||||
if (newDomain0 < minDate) {
|
||||
newDomain0 = minDate;
|
||||
newDomainF = d3.timeMinute.offset(newDomain0, zoom.duration);
|
||||
newDomainF = timeMinute.offset(newDomain0, zoom.duration);
|
||||
}
|
||||
if (newDomainF > maxDate) {
|
||||
newDomainF = maxDate;
|
||||
newDomain0 = d3.timeMinute.offset(newDomainF, -zoom.duration);
|
||||
newDomain0 = timeMinute.offset(newDomainF, -zoom.duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,11 +260,11 @@ class Timeline extends React.Component {
|
||||
/*
|
||||
* Setup drag behavior
|
||||
*/
|
||||
onDragStart() {
|
||||
d3.event.sourceEvent.stopPropagation();
|
||||
onDragStart(event) {
|
||||
event.sourceEvent.stopPropagation();
|
||||
this.setState(
|
||||
{
|
||||
dragPos0: d3.event.x,
|
||||
dragPos0: event.x,
|
||||
},
|
||||
() => {
|
||||
this.toggleTransition(false);
|
||||
@@ -273,14 +275,14 @@ class Timeline extends React.Component {
|
||||
/*
|
||||
* Drag and update
|
||||
*/
|
||||
onDrag() {
|
||||
onDrag(event) {
|
||||
const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime();
|
||||
const dragNow = this.state.scaleX.invert(d3.event.x).getTime();
|
||||
const dragNow = this.state.scaleX.invert(event.x).getTime();
|
||||
const timeShift = (drag0 - dragNow) / 1000;
|
||||
|
||||
const { range, rangeLimits } = this.props.timeline;
|
||||
let newDomain0 = d3.timeSecond.offset(range[0], timeShift);
|
||||
let newDomainF = d3.timeSecond.offset(range[1], timeShift);
|
||||
const { range, rangeLimits } = this.props.app.timeline;
|
||||
let newDomain0 = timeSecond.offset(range[0], timeShift);
|
||||
let newDomainF = timeSecond.offset(range[1], timeShift);
|
||||
|
||||
if (rangeLimits) {
|
||||
// If the store contains absolute time limits,
|
||||
@@ -352,8 +354,8 @@ class Timeline extends React.Component {
|
||||
const timeframe = Math.floor(
|
||||
this.props.features.ZOOM_TO_TIMEFRAME_ON_TIMELINE_CLICK / 2
|
||||
);
|
||||
const start = d3.timeMinute.offset(event.datetime, -timeframe);
|
||||
const end = d3.timeMinute.offset(event.datetime, timeframe);
|
||||
const start = timeMinute.offset(event.datetime, -timeframe);
|
||||
const end = timeMinute.offset(event.datetime, timeframe);
|
||||
this.props.actions.updateTicks(1);
|
||||
this.props.methods.onUpdateTimerange([start, end]);
|
||||
}
|
||||
@@ -416,15 +418,9 @@ class Timeline extends React.Component {
|
||||
getCategoryY={(category) =>
|
||||
this.getY({ category, project: null })
|
||||
}
|
||||
onDragStart={() => {
|
||||
this.onDragStart();
|
||||
}}
|
||||
onDrag={() => {
|
||||
this.onDrag();
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
this.onDragEnd();
|
||||
}}
|
||||
onDragStart={this.onDragStart}
|
||||
onDrag={this.onDrag}
|
||||
onDragEnd={this.onDragEnd}
|
||||
categories={categories}
|
||||
features={this.props.features}
|
||||
fallbackLabel={
|
||||
|
||||
Reference in New Issue
Block a user