Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Statistics improvements (Location and loan duration filtering) #26

Merged
merged 3 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions src/finland/OpenSearchAnalyticsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import * as React from "react";
import { createContext, useEffect, useState } from "react";
import { KeyValuePair } from "./finlandUtils";
import {
KeyValuePair,
NameValuePair,
loanDurationOptions,
readable,
} from "./finlandUtils";
const termsEndpoint = "/admin/events/terms";
const histogramEndpoint = "/admin/events/histogram";
const facetsEndpoint = "/admin/events/facets";
const municipalityEndpoint = "/admin/municipalities";

type BucketItem = {
export type BucketItem = {
key: string;
doc_count: number;
};
Expand All @@ -28,12 +34,19 @@ type HistogramData = {

export type FacetData = Record<string, { buckets: BucketItem[] }>;

export type FilterToOptionsFunc = (
filterKey: string,
buckets: BucketItem[]
) => NameValuePair[];

export type OpenSearchAnalyticsContextType = {
facetData?: FacetData;
eventData?: TermBucketData;
fetchEventData?: (params: KeyValuePair[]) => void;
histogramData?: HistogramData;
fetchHistogramData?: (params: KeyValuePair[]) => void;
filterToOptions?: FilterToOptionsFunc;
labelizeFilterChip: (item: KeyValuePair) => string;
isReady?: boolean;
};

Expand All @@ -55,6 +68,9 @@ export function OpenSearchAnalyticsContextProvider({
const [isReady, setIsReady] = useState(false);
const [eventData, setEventData] = useState<TermBucketData>(null);
const [histogramData, setHistogramData] = useState<HistogramData>(null);
const [municipalityMapping, setMunicipalityMapping] = useState<
Record<string, string>
>({});

useEffect(() => {
async function fetchFacets() {
Expand All @@ -70,7 +86,22 @@ export function OpenSearchAnalyticsContextProvider({
console.error("Error while fetching facets for statistics", err);
}
}
async function fetchMunicipalityMappings() {
try {
const response = await fetch(`${municipalityEndpoint}`);
const data = await response.json();
if (data) {
setMunicipalityMapping(data);
}
} catch (err) {
console.error(
"Error while fetching municipality mapping for statistics",
err
);
}
}
fetchFacets();
fetchMunicipalityMappings();
}, [library]);

async function fetchEventData(selections?: KeyValuePair[]) {
Expand Down Expand Up @@ -105,6 +136,39 @@ export function OpenSearchAnalyticsContextProvider({
}
}

function filterToOptions(filterKey: string, buckets?: BucketItem[]) {
if (filterKey === "duration") {
return [...loanDurationOptions]; // Spread to trigger SelectSearch re-render
}
if (!buckets?.length) {
return [];
}
if (filterKey === "location") {
return buckets
.map((item) => ({
value: item.key,
name: municipalityMapping[item.key] || item.key,
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
return buckets
.map((item) => ({
value: item.key,
name: item.key,
}))
.sort((a, b) => a.name.localeCompare(b.name));
}

function labelizeFilterChip({ key, value }: KeyValuePair) {
if (key === "location") {
return `${readable(key)}: ${municipalityMapping[value] || value}`;
}
if (key === "duration") {
return `${readable(key)}: ${readable(value) || value}`;
}
return `${readable(key)}: ${value}`;
}

return (
<OpenSearchAnalyticsContext.Provider
value={{
Expand All @@ -114,6 +178,8 @@ export function OpenSearchAnalyticsContextProvider({
fetchEventData,
histogramData,
fetchHistogramData,
filterToOptions,
labelizeFilterChip,
}}
>
{children}
Expand Down
10 changes: 9 additions & 1 deletion src/finland/components/EventBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ export function EventBarChart() {
setTimeframeOffset,
} = useFilters();

const { facetData, eventData, fetchEventData } = useOpenSearchAnalytics();
const {
facetData,
eventData,
fetchEventData,
filterToOptions,
labelizeFilterChip,
} = useOpenSearchAnalytics();

useEffect(() => {
const selections: KeyValuePair[] = [...activeFilters];
Expand Down Expand Up @@ -67,12 +73,14 @@ export function EventBarChart() {
setStartDate={setStartDate}
endDate={endDate}
setEndDate={setEndDate}
filterToOptions={filterToOptions}
/>

<FilterChips
activeFilters={activeFilters}
toggleFilter={toggleFilter}
clearFilters={clearFilters}
labelize={labelizeFilterChip}
/>

<div className="chart-prefix">
Expand Down
6 changes: 4 additions & 2 deletions src/finland/components/FilterChips.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import * as React from "react";
import { KeyValuePair, readable } from "../finlandUtils";
import { KeyValuePair } from "../finlandUtils";

type FilterChipsProps = {
activeFilters: KeyValuePair[];
toggleFilter: (selection: KeyValuePair) => void;
clearFilters: () => void;
labelize: (item: KeyValuePair) => string;
};

export function FilterChips({
activeFilters,
toggleFilter,
clearFilters,
labelize,
}: FilterChipsProps) {
return (
<div className="chip-container">
Expand All @@ -22,7 +24,7 @@ export function FilterChips({
onClick={() => toggleFilter(item)}
isSingle
>
{readable(item.key)}: {item.value}
{labelize(item)}
</Chip>
))}
<Chip
Expand Down
10 changes: 4 additions & 6 deletions src/finland/components/FilterInputs.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { KeyValuePair, filterKeys, readable, EventKey } from "../finlandUtils";
import { FacetData } from "../OpenSearchAnalyticsContext";
import { FacetData, FilterToOptionsFunc } from "../OpenSearchAnalyticsContext";
import SelectSearch from "react-select-search";
import { fuzzySearch } from "react-select-search";

Expand All @@ -11,6 +11,7 @@ type FilterInputsProps = {
setStartDate: (newDate: string) => void;
endDate: string;
setEndDate: (newDate: string) => void;
filterToOptions: FilterToOptionsFunc;
};

export function FilterInputs({
Expand All @@ -20,15 +21,12 @@ export function FilterInputs({
setStartDate,
endDate,
setEndDate,
filterToOptions,
}: FilterInputsProps) {
return (
<div className="input-wrapper">
{filterKeys.map((key) => {
const options =
facetData[key]?.buckets.map((item) => ({
value: item.key,
name: item.key,
})) || [];
const options = filterToOptions(key, facetData[key]?.buckets);
const label = readable(key);
return (
<div key={key} className="flex-col">
Expand Down
35 changes: 31 additions & 4 deletions src/finland/components/Histogram.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import {
LineChart,
Line,
Expand Down Expand Up @@ -33,11 +33,17 @@ import { useOpenSearchAnalytics } from "../hooks/useOpenSearchAnalytics";
export function Histogram() {
const [interval, setInterval] = useState<Interval>("hour");
const [inactiveGroups, setInactiveGroups] = useState<string[]>([]);
const lastLegendClick: React.MutableRefObject<{
key: string | null;
time: number | null;
}> = useRef({ key: null, time: null });

const {
facetData,
histogramData,
fetchHistogramData,
filterToOptions,
labelizeFilterChip,
} = useOpenSearchAnalytics();

const {
Expand Down Expand Up @@ -105,6 +111,10 @@ export function Histogram() {
});
}, [histogramData]);

if (!timeData || !facetData) return null;

const dateRangeLength = daysBetween(startDate, endDate);

function toggleGroup(key: string) {
setInactiveGroups(
inactiveGroups.includes(key)
Expand All @@ -113,9 +123,24 @@ export function Histogram() {
);
}

if (!timeData || !facetData) return null;
function toggleGroupSolo(key: string) {
setInactiveGroups(
inactiveGroups.length <= 1 ? eventKeys.filter((item) => item !== key) : []
);
}

const dateRangeLength = daysBetween(startDate, endDate);
function handleLegendClick(key: string) {
const now = Date.now();
if (
lastLegendClick.current.key === key &&
now - lastLegendClick.current.time < 300
) {
toggleGroupSolo(key);
} else {
toggleGroup(key);
}
lastLegendClick.current = { key, time: now };
}

return (
<div>
Expand All @@ -126,12 +151,14 @@ export function Histogram() {
setStartDate={setStartDate}
endDate={endDate}
setEndDate={setEndDate}
filterToOptions={filterToOptions}
/>

<FilterChips
activeFilters={activeFilters}
toggleFilter={toggleFilter}
clearFilters={clearFilters}
labelize={labelizeFilterChip}
/>

<div className="chart-prefix">
Expand Down Expand Up @@ -173,7 +200,7 @@ export function Histogram() {
timeStampToLabel(unixTime as number, interval)
}
/>
<Legend onClick={(item) => toggleGroup(item.dataKey)} />
<Legend onClick={(item) => handleLegendClick(item.dataKey)} />
{eventKeys.map((item, idx) => (
<Line
isAnimationActive={!prefersReducedMotion()}
Expand Down
26 changes: 25 additions & 1 deletion src/finland/finlandUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,23 @@ export interface IEvent {
time: string;
contributors: string[] | null;
start: string;
duration: string | null;
date?: string;
}

export type EventKey = keyof IEvent;

export type KeyValuePair = { key: string; value: string };

export type NameValuePair = { name: string; value: string };

export const eventTypes = {
circulation_manager_check_out: "circulation_manager_check_out",
circulation_manager_check_in: "circulation_manager_check_in",
circulation_manager_fulfill: "circulation_manager_fulfill",
circulation_manager_hold_place: "circulation_manager_hold_place",
circulation_manager_hold_release: "circulation_manager_hold_release",
circulation_manager_new_patron: "circulation_manager_new_patron",
distributor_check_out: "distributor_check_out",
distributor_check_in: "distributor_check_in",
distributor_hold_place: "distributor_hold_place",
Expand All @@ -52,6 +56,7 @@ export const readableNames: Record<string, string> = {
[eventTypes.circulation_manager_fulfill]: "Toimitukset",
[eventTypes.circulation_manager_hold_place]: "Varaukset, pyyntö",
[eventTypes.circulation_manager_hold_release]: "Varaukset, nouto",
[eventTypes.circulation_manager_new_patron]: "Uudet asiakkaat",
[eventTypes.distributor_check_out]: "Lainaukset",
[eventTypes.distributor_check_in]: "Palautukset",
[eventTypes.distributor_hold_place]: "Varaukset, pyyntö",
Expand All @@ -71,16 +76,28 @@ export const readableNames: Record<string, string> = {
fiction: "Fiktio",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Kirjallisuus" is usually the term used for "Fiction" by the libraries.

In general, all terms in Finnish will have to be replaced with English as source and then passed to the localization.

I supposed this will be done at the same time as the localization is prepared.

genres: "Genre",
audience: "Kohderyhmä",
location: "Sijainti",
duration: "Laina-aika (palautukset)",
// Time intervals
hour: "Tunti",
day: "Päivä",
month: "Kuukausi",
// Loan duration options:
under_2h: "Alle 2h",
over_2h: "Yli 2h",
};

export const readable = (input: string): string =>
readableNames[input] || input;

export const filterKeys = ["publisher", "language", "audience", "genres"];
export const filterKeys = [
"publisher",
"language",
"audience",
"genres",
"location",
"duration",
];

export const intervals = ["hour", "day", "month"];
export type Interval = "hour" | "day" | "month";
Expand Down Expand Up @@ -123,6 +140,13 @@ export const timeframeOptions: Record<Timeframe, TimeframeOption> = {
},
};

const loanDurations = ["under_2h", "over_2h"];

export const loanDurationOptions = loanDurations.map((key) => ({
value: key,
name: readable(key),
}));

/*
* General helper functions
*/
Expand Down
Loading
Loading