mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(tools): count and summarize every match in the area tools
aircraft_in_area and vessels_in_area return at most 200 rows, so a client asked which operators dominate an area, or the average altitude there, could only estimate from a truncated sample. Both tools now take an optional group_by (aircraft: operator, origin_country, type_code; ships: type, destination) and stats (airborne altitude and speed for aircraft, speed for ships). Each is computed over every match before rows are capped. Breakdowns list the ten largest groups plus other and unknown counts, and the summary names the three largest. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
591f299d11
commit
2110d06e36
10 files changed
+299
-5
No files matched your search
@@ -22,6 +22,75 @@ export function capRows(rows, limit = DEFAULT_LIMIT) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Most groups a breakdown lists before counting the rest as other. */
|
||||
export const MAX_GROUPS = 10;
|
||||
|
||||
/**
|
||||
* Count rows by one text field over every row, not just those returned.
|
||||
* Values group case-insensitively and keep their first spelling; rows
|
||||
* without a value are counted as unknown.
|
||||
*/
|
||||
export function breakdown(rows, field) {
|
||||
const groups = new Map();
|
||||
let unknown = 0;
|
||||
for (const row of rows) {
|
||||
const value = typeof row[field] === 'string' ? row[field].trim() : '';
|
||||
if (!value) {
|
||||
unknown += 1;
|
||||
continue;
|
||||
}
|
||||
const key = value.toLowerCase();
|
||||
const group = groups.get(key);
|
||||
if (group) group.count += 1;
|
||||
else groups.set(key, { value, count: 1 });
|
||||
}
|
||||
const sorted = [...groups.values()].sort(
|
||||
(a, b) => b.count - a.count || a.value.localeCompare(b.value),
|
||||
);
|
||||
const listed = sorted.slice(0, MAX_GROUPS);
|
||||
return {
|
||||
field,
|
||||
groups: listed,
|
||||
other_count: rows.length - unknown - sum(listed.map((g) => g.count)),
|
||||
unknown_count: unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/** Minimum, median, mean and maximum of one numeric field, or null. */
|
||||
export function numericSummary(rows, field) {
|
||||
const values = rows
|
||||
.map((row) => row[field])
|
||||
.filter(Number.isFinite)
|
||||
.sort((a, b) => a - b);
|
||||
if (!values.length) return null;
|
||||
const middle = values.length >> 1;
|
||||
const median =
|
||||
values.length % 2
|
||||
? values[middle]
|
||||
: (values[middle - 1] + values[middle]) / 2;
|
||||
const round1 = (value) => Number(value.toFixed(1));
|
||||
return {
|
||||
count: values.length,
|
||||
min: values[0],
|
||||
median: round1(median),
|
||||
mean: round1(sum(values) / values.length),
|
||||
max: values.at(-1),
|
||||
};
|
||||
}
|
||||
|
||||
/** "; most by operator: Delta (12), United (9), American (7)" or "". */
|
||||
export function breakdownPhrase(result, label) {
|
||||
if (!result.groups.length) return '';
|
||||
const top = result.groups
|
||||
.slice(0, 3)
|
||||
.map((group) => `${group.value} (${group.count})`);
|
||||
return `; most by ${label}: ${top.join(', ')}`;
|
||||
}
|
||||
|
||||
function sum(values) {
|
||||
return values.reduce((total, value) => total + value, 0);
|
||||
}
|
||||
|
||||
/** Format an epoch millisecond time as ISO 8601, or null. */
|
||||
export function isoTime(ms) {
|
||||
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
||||
|
||||
Reference in new issue
Block a user