Skip to content

Commit a8aa001

Browse files
committed
drone strikes
1 parent 7cf6b90 commit a8aa001

4 files changed

Lines changed: 171 additions & 9 deletions

File tree

docs/data/drone_by_month.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"2022-02": 21,
3+
"2022-03": 45,
4+
"2022-04": 96,
5+
"2022-05": 118,
6+
"2022-06": 193,
7+
"2022-07": 271,
8+
"2022-08": 143,
9+
"2022-09": 187,
10+
"2022-10": 101,
11+
"2022-11": 71,
12+
"2022-12": 35,
13+
"2023-01": 48,
14+
"2023-02": 71,
15+
"2023-03": 118,
16+
"2023-04": 195,
17+
"2023-05": 248,
18+
"2023-06": 174,
19+
"2023-07": 178,
20+
"2023-08": 132,
21+
"2023-09": 337,
22+
"2023-10": 218,
23+
"2023-11": 198,
24+
"2023-12": 150,
25+
"2024-01": 166,
26+
"2024-02": 172,
27+
"2024-03": 140,
28+
"2024-04": 133,
29+
"2024-05": 121,
30+
"2024-06": 162,
31+
"2024-07": 263,
32+
"2024-08": 347,
33+
"2024-09": 555,
34+
"2024-10": 775,
35+
"2024-11": 766,
36+
"2024-12": 439,
37+
"2025-01": 540,
38+
"2025-02": 646,
39+
"2025-03": 241
40+
}

docs/js/statistics.js

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ document.addEventListener('DOMContentLoaded', async () => {
1313
acled: 'data/acled_by_oblast.json',
1414
aid: 'data/aid_by_country.json',
1515
timeline: 'data/timeline_events.json',
16-
gdp: 'data/gdp_by_country.json'
16+
gdp: 'data/gdp_by_country.json',
17+
drone: 'data/drone_by_month.json',
1718
};
1819

1920
try {
20-
const [acledRaw, aidRaw, timelineRaw, gdpRaw] = await Promise.all([
21+
const [acledRaw, aidRaw, timelineRaw, gdpRaw, droneRaw] = await Promise.all([
2122
d3.json(DATA_PATHS.acled),
2223
d3.json(DATA_PATHS.aid),
2324
d3.json(DATA_PATHS.timeline),
24-
d3.json(DATA_PATHS.gdp)
25+
d3.json(DATA_PATHS.gdp),
26+
d3.json(DATA_PATHS.drone),
2527
]);
2628

2729
// 1. Data Structure Normalization
@@ -43,6 +45,7 @@ document.addEventListener('DOMContentLoaded', async () => {
4345
renderAidDonut(countriesArray);
4446
renderRankingTable(countriesArray, gdpRaw);
4547
renderDeathTimeSeries(acledRaw);
48+
renderDroneTimeSeries(droneRaw);
4649

4750

4851
} catch (err) {
@@ -896,6 +899,84 @@ function renderDeathTimeSeries(acledRaw) {
896899
.attr("fill", "var(--red)");
897900
}
898901

902+
function renderDroneTimeSeries(droneRaw) {
903+
const container = d3.select("#ts-drone-strikes");
904+
container.selectAll("*").remove();
905+
906+
const parseTime = d3.timeParse("%Y-%m");
907+
const chartData = Object.entries(droneRaw)
908+
.map(([month, count]) => ({ date: parseTime(month), value: +count }))
909+
.filter(d => d.date !== null)
910+
.sort((a, b) => a.date - b.date);
911+
912+
if (chartData.length === 0) return;
913+
914+
const margin = { top: 20, right: 30, bottom: 50, left: 55 };
915+
const width = container.node().getBoundingClientRect().width - margin.left - margin.right;
916+
const height = 260 - margin.top - margin.bottom;
917+
918+
const svg = container.append("svg")
919+
.attr("width", "100%")
920+
.attr("height", height + margin.top + margin.bottom)
921+
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
922+
.append("g")
923+
.attr("transform", `translate(${margin.left},${margin.top})`);
924+
925+
const x = d3.scaleTime().domain(d3.extent(chartData, d => d.date)).range([0, width]);
926+
const y = d3.scaleLinear().domain([0, d3.max(chartData, d => d.value) * 1.1]).range([height, 0]);
927+
928+
svg.append("g")
929+
.attr("class", "axis grid")
930+
.attr("transform", `translate(0,${height})`)
931+
.call(d3.axisBottom(x).ticks(6).tickFormat(d3.timeFormat("%Y/%m")).tickSize(-height));
932+
933+
svg.append("g")
934+
.attr("class", "axis grid")
935+
.call(d3.axisLeft(y).ticks(5).tickSize(-width));
936+
937+
// Area fill
938+
svg.append("path")
939+
.datum(chartData)
940+
.attr("fill", "rgba(52, 152, 219, 0.15)")
941+
.attr("d", d3.area().x(d => x(d.date)).y0(height).y1(d => y(d.value)).curve(d3.curveMonotoneX));
942+
943+
svg.append("path")
944+
.datum(chartData)
945+
.attr("fill", "none")
946+
.attr("stroke", "#3498db")
947+
.attr("stroke-width", 2.5)
948+
.attr("d", d3.line().x(d => x(d.date)).y(d => y(d.value)).curve(d3.curveMonotoneX));
949+
950+
let tooltip = d3.select(".chart-tooltip");
951+
if (tooltip.empty()) {
952+
tooltip = d3.select("body").append("div").attr("class", "chart-tooltip").style("opacity", 0);
953+
}
954+
955+
const mouseLine = svg.append("line")
956+
.attr("y1", 0).attr("y2", height)
957+
.attr("stroke", "#aaa").attr("stroke-width", 1).attr("stroke-dasharray", "4")
958+
.style("opacity", 0);
959+
960+
svg.append("rect")
961+
.attr("width", width).attr("height", height)
962+
.attr("fill", "none").attr("pointer-events", "all")
963+
.on("mouseout", () => { tooltip.style("opacity", 0); mouseLine.style("opacity", 0); })
964+
.on("mousemove", function (event) {
965+
const bisect = d3.bisector(d => d.date).left;
966+
const date = x.invert(d3.pointer(event)[0]);
967+
const i = bisect(chartData, date, 1);
968+
const d0 = chartData[i - 1], d1 = chartData[i];
969+
const d = d1 && (date - d0.date > d1.date - date) ? d1 : d0;
970+
if (!d) return;
971+
mouseLine.attr("x1", x(d.date)).attr("x2", x(d.date)).style("opacity", 1);
972+
tooltip.style("opacity", 1)
973+
.html(`<span class="tooltip-date">${d3.timeFormat("%Y/%m")(d.date)}</span>
974+
<div><span style="color:#3498db">●</span> Strikes: <strong>${d.value.toLocaleString()}</strong></div>`)
975+
.style("left", (event.pageX + 15) + "px")
976+
.style("top", (event.pageY - 28) + "px");
977+
});
978+
}
979+
899980

900981
function renderCombinedAidDashboard(countriesArray) {
901982
const totalGlobalEur = d3.sum(countriesArray, d => d.total_eur || 0);

docs/statistics.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ <h3>AID ALLOCATION ANALYSIS</h3>
9797
</div>
9898

9999

100+
<!-- Drone Strikes Time Series -->
101+
<div class="chart-card" style="margin-top: 24px;">
102+
<h3>AIR / DRONE STRIKES OVER TIME</h3>
103+
<div class="chart-guide-container">
104+
<p class="chart-guide"><span class="info-icon"></span> Monthly count of Air/drone strike events (ACLED sub_event_type)</p>
105+
</div>
106+
<div id="ts-drone-strikes" style="height: 260px;"></div>
107+
</div>
108+
100109
<!-- Rest of the data -->
101110
<div class="bottom-charts-row" style="margin-top: 24px;">
102111
<div class="chart-card">

preprocess.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
2323
DATA_DIR = os.path.join(BASE_DIR, "data")
24-
OUT_DIR = os.path.join(BASE_DIR, "website", "data")
24+
OUT_DIR = os.path.join(BASE_DIR, "docs", "data")
2525
os.makedirs(OUT_DIR, exist_ok=True)
2626

2727
# ---------------------------------------------------------------------------
@@ -133,7 +133,36 @@ def process_acled():
133133

134134

135135
# ---------------------------------------------------------------------------
136-
# 3. Ukraine Support Tracker -> aid_by_country.json
136+
# 3. ACLED -> drone_by_month.json
137+
# ---------------------------------------------------------------------------
138+
139+
def process_drone_strikes():
140+
print(" Processing drone strikes ...")
141+
monthly = defaultdict(int)
142+
143+
with open(ACLED_FILE, encoding="utf-8-sig", newline="") as f:
144+
reader = csv.DictReader(f)
145+
for row in reader:
146+
if row["sub_event_type"].strip() != "Air/drone strike":
147+
continue
148+
if row["country"].strip() not in ("Ukraine", "Russia"):
149+
continue
150+
date_str = row["event_date"].strip()
151+
try:
152+
month = datetime.strptime(date_str, "%Y-%m-%d").strftime("%Y-%m")
153+
except ValueError:
154+
month = date_str[:7]
155+
monthly[month] += 1
156+
157+
out = dict(sorted(monthly.items()))
158+
out_path = os.path.join(OUT_DIR, "drone_by_month.json")
159+
with open(out_path, "w", encoding="utf-8") as f:
160+
json.dump(out, f, ensure_ascii=False, indent=2)
161+
print(f" [ok] drone_by_month.json ({sum(out.values())} events across {len(out)} months)")
162+
163+
164+
# ---------------------------------------------------------------------------
165+
# 4. Ukraine Support Tracker -> aid_by_country.json
137166
# ---------------------------------------------------------------------------
138167

139168
ASSISTANCE_FILE = os.path.join(DATA_DIR, "assistance_main_data.xlsx")
@@ -287,11 +316,14 @@ def write_timeline():
287316
print("\n=== Step 2: Process ACLED data ===")
288317
process_acled()
289318

290-
print("\n=== Step 3: Process aid data ===")
319+
print("\n=== Step 3: Process drone strikes ===")
320+
process_drone_strikes()
321+
322+
print("\n=== Step 4: Process aid data ===")
291323
process_assistance()
292324

293-
print("\n=== Step 4: Write timeline events ===")
325+
print("\n=== Step 5: Write timeline events ===")
294326
write_timeline()
295327

296-
print("\nDone! Files in website/data/")
297-
print("Start the site: cd website && python -m http.server 8000")
328+
print("\nDone! Files in docs/data/")
329+
print("Start the site: cd docs && python -m http.server 8000")

0 commit comments

Comments
 (0)