Merge bellingcat/main into bellingcat/translation (#51)

* Fix flashing on zoom

* fix(map): prevent crash when applying filter during zoom

* implements csv+json downloads

* format

* npm run lint:fix

* fix(map): disable focus outline when dragging

* Feature/reduce bundle size (#234) (#36)

Co-authored-by: Juan Camilo González <j.gonzalezj@uniandes.edu.co>
Co-authored-by: msramalho <19508417+msramalho@users.noreply.github.com>

* fix bug introduced in moment drop PR

* reinstate old structure (#38)

* closes #46

* addressing issues raised in #46

* new satellite style #47

* minor text update

* temp(hot fix to satellite toggle) needs further fix #46

* cleanup

* chore: remove cached .DS_Store file (#45)

* fix: refresh visible markers after panning map (#40)

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Co-authored-by: Miguel Sozinho Ramalho <19508417+msramalho@users.noreply.github.com>
Co-authored-by: Felix Spöttel <1682504+fspoettel@users.noreply.github.com>
Co-authored-by: Lachlan Kermode <lachiekermode@gmail.com>
Co-authored-by: Juan Camilo González <j.gonzalezj@uniandes.edu.co>
Co-authored-by: Zachary Lester <zachary.greg.lester@gmail.com>
This commit is contained in:
wattroll
2022-04-20 09:58:03 +03:00
committed by GitHub
parent aecbabf3d3
commit 9e37541a32
13 changed files with 608 additions and 445 deletions

View File

@@ -13,7 +13,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: {
@@ -141,6 +141,7 @@ module.exports = {
tiles: {
current: "bellingcat-mapbox/cl0qnou2y003m15s8ieuyhgsy",
default: "bellingcat-mapbox/cl0qnou2y003m15s8ieuyhgsy",
satellite: "bellingcat-mapbox/cl1win2vp003914pdhateva6p"
},
},
features: {

900
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -171,7 +171,7 @@
"formats": {
"csv": {
"label": "CSV",
"description": "CSV file where sources and filters are concatenated into a single column due to data structure limitations."
"description": "CSV file where sources and filters are concatenated into a single column each due to data structure limitations."
},
"json": {
"label": "JSON",

View File

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

Binary file not shown.

View File

@@ -1,11 +1,13 @@
import React from "react";
import copy from "../../common/data/copy.json";
import dayjs from "dayjs";
import { parse } from "json2csv";
import { downloadAsFile } from "../../common/utilities";
export class DownloadButton extends React.Component {
onDownload(format, domain) {
let filename = `ukr-civharm-${this.datetimeToDateString(new Date())}`;
console.log();
let filename = `ukr-civharm-${dayjs().format("YYYY-MM-DD")}`;
if (format === "csv") {
let outputData = this.getCsvData(domain);
downloadAsFile(`${filename}.csv`, outputData);
@@ -19,7 +21,7 @@ export class DownloadButton extends React.Component {
const exportEvents = events.map((e) => {
return {
id: e.civId,
date: this.datetimeToDateString(e.datetime),
date: e.date,
latitude: e.latitude,
longitude: e.longitude,
location: e.location,
@@ -37,7 +39,7 @@ export class DownloadButton extends React.Component {
const exportEvents = events.map((e) => {
return {
id: e.civId,
date: this.datetimeToDateString(e.datetime),
date: e.date,
latitude: e.latitude,
longitude: e.longitude,
location: e.location,
@@ -60,12 +62,6 @@ export class DownloadButton extends React.Component {
});
return JSON.stringify(exportEvents);
}
datetimeToDateString(datetime) {
try {
return datetime.toISOString().split("T")[0];
} catch (_) {}
return "";
}
render() {
const { language, domain, format } = this.props;
const textByFormat = copy[language].toolbar.download.panel.formats[format];

View File

@@ -168,6 +168,7 @@ class Map extends React.Component {
map.on("moveend", () => {
this.alignLayers();
this.updateClusters();
});
map.on("zoomend viewreset", () => {
@@ -547,7 +548,10 @@ class Map extends React.Component {
/>
{this.props.features.USE_SATELLITE_OVERLAY_TOGGLE && (
<SatelliteOverlayToggle
isUsingSatellite={this.props.ui.tiles === "satellite"}
isUsingSatellite={
this.props.ui.tiles ===
"bellingcat-mapbox/cl1win2vp003914pdhateva6p"
}
switchToSatellite={this.props.actions.useSatelliteTilesOverlay}
reset={this.props.actions.resetTilesOverlay}
/>

View File

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

View File

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

View File

@@ -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:
@@ -47,7 +50,7 @@ class Timeline extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps) {
if (hash(nextProps) !== hash(this.props)) {
this.setState({
timerange: nextProps.timeline.range,
timerange: nextProps.app.timeline.range,
scaleX: this.makeScaleX(),
});
}
@@ -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;
const { rangeLimits } = this.props.app.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]);
}
@@ -361,7 +363,8 @@ class Timeline extends React.Component {
}
render() {
const { isNarrative, app, timeline, domain } = this.props;
const { isNarrative, app, domain } = this.props;
const { timeline } = app;
let classes = `timeline-wrapper ${this.state.isFolded ? " folded" : ""}`;
classes += app.narrative !== null ? " narrative-mode" : "";
@@ -416,15 +419,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={
@@ -506,13 +503,13 @@ function mapStateToProps(state) {
language: state.app.language,
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),
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,

View File

@@ -9,7 +9,7 @@ function ui(uiState = initial.ui, action) {
...uiState,
tiles: {
...uiState.tiles,
current: "satellite",
current: uiState.tiles.satellite,
},
};
case RESET_TILES_OVERLAY:

View File

@@ -160,6 +160,7 @@ const initial = {
tiles: {
current: "openstreetmap", // ['openstreetmap', 'streets', 'satellite']
default: "openstreetmap", // ['openstreetmap', 'streets', 'satellite']
satellite: "satellite",
},
style: {
categories: {