Skip to content

Commit

Permalink
(WIP) feat: Add TimeSlider + filtering of data based on it
Browse files Browse the repository at this point in the history
  • Loading branch information
bprusinowski committed Oct 31, 2022
1 parent 6ad5123 commit 2ebea50
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 15 deletions.
6 changes: 6 additions & 0 deletions app/charts/column/chart-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
FilterValueSingle,
InteractiveFiltersConfig,
} from "@/configurator";
import { TimeSlider } from "@/configurator/interactive-filters/time-slider";
import { Observation } from "@/domain/data";
import {
DimensionMetadataFragment,
Expand Down Expand Up @@ -162,6 +163,11 @@ export const ChartColumns = memo(
</ChartSvg>
<Tooltip type="single" />
</ChartContainer>
{interactiveFiltersConfig?.timeSlider.componentIri && (
<TimeSlider
componentIri={interactiveFiltersConfig.timeSlider.componentIri}
/>
)}
</ColumnChart>
)}
</>
Expand Down
61 changes: 46 additions & 15 deletions app/charts/shared/chart-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,31 +116,62 @@ export const useDataAfterInteractiveFilters = ({
getX?: (d: Observation) => Date;
getSegment?: (d: Observation) => string;
}) => {
const [interactiveFilters] = useInteractiveFilters();
const { from, to } = interactiveFilters.timeRange;
const { categories } = interactiveFilters;
const activeInteractiveFilters = Object.keys(categories);
const [IFState] = useInteractiveFilters();

// time range
const fromTime = IFState.timeRange.from?.getTime();
const toTime = IFState.timeRange.to?.getTime();

// time slider
const getTime = useTemporalVariable(
interactiveFiltersConfig?.timeSlider.componentIri || ""
);
const timeSliderValue = IFState.timeSlider.value;

// legend
const legendItems = Object.keys(IFState.categories);

const allFilters = useMemo(() => {
const timeRangeFilter: ValuePredicate | null =
getX && from && to && interactiveFiltersConfig?.timeRange.active
? (d: Observation) =>
getX(d).getTime() >= from.getTime() &&
getX(d).getTime() <= to.getTime()
const timeRangeFilter =
getX && fromTime && toTime && interactiveFiltersConfig?.timeRange.active
? (d: Observation) => {
const time = getX(d).getTime();
return time >= fromTime && time <= toTime;
}
: null;
const legendFilter: ValuePredicate | null =
const timeSliderFilter =
interactiveFiltersConfig?.timeSlider.componentIri && timeSliderValue
? (d: Observation) => {
return getTime(d).getTime() === timeSliderValue.getTime();
}
: null;
const legendFilter =
interactiveFiltersConfig?.legend.active && getSegment
? (d: Observation) => !activeInteractiveFilters.includes(getSegment(d))
? (d: Observation) => {
return !legendItems.includes(getSegment(d));
}
: null;
return overEvery([timeRangeFilter, legendFilter].filter(truthy));

return overEvery(
(
[
timeRangeFilter,
timeSliderFilter,
legendFilter,
] as (ValuePredicate | null)[]
).filter(truthy)
);
}, [
activeInteractiveFilters,
from,
legendItems,
getSegment,
getX,
to,
getTime,
fromTime,
toTime,
interactiveFiltersConfig?.legend.active,
interactiveFiltersConfig?.timeRange.active,
interactiveFiltersConfig?.timeSlider.componentIri,
timeSliderValue,
]);

const preparedData = useMemo(() => {
Expand Down
6 changes: 6 additions & 0 deletions app/charts/shared/use-interactive-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type InteractiveFiltersStateAction =
type: "SET_TIME_RANGE_FILTER";
value: [Date, Date];
}
| {
type: "SET_TIME_SLIDER_FILTER";
value: Date;
}
| {
type: "RESET_DATA_FILTER";
}
Expand Down Expand Up @@ -67,6 +71,8 @@ const InteractiveFiltersStateReducer = (
case "SET_TIME_RANGE_FILTER":
draft.timeRange = { from: action.value[0], to: action.value[1] };
return draft;
case "SET_TIME_SLIDER_FILTER":
draft.timeSlider = { value: action.value };
case "RESET_DATA_FILTER":
draft.dataFilters = {};
return draft;
Expand Down
61 changes: 61 additions & 0 deletions app/configurator/interactive-filters/time-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { scaleLinear } from "d3";
import React, { ChangeEvent } from "react";

import { ChartState, useChartState } from "@/charts/shared/use-chart-state";
import { useInteractiveFilters } from "@/charts/shared/use-interactive-filters";
import { TableChartState } from "@/charts/table/table-state";
import { Slider } from "@/components/form";
import { parseDate } from "@/configurator/components/ui-helpers";

export const TimeSlider = ({ componentIri }: { componentIri?: string }) => {
const [_, dispatch] = useInteractiveFilters();
const [t, setT] = React.useState(0);
const chartState = useChartState() as NonNullable<
Exclude<ChartState, TableChartState>
>;
const onChange = React.useMemo(() => {
// FIXME: enable interactive filters for maps!
if (componentIri && chartState.chartType !== "map") {
const rawRange = [
...new Set(
chartState.allData.map((d) => d[componentIri]).filter(Boolean)
),
].sort() as string[];
const range = rawRange.map(parseDate);
const [min, max] = [range[0], range[range.length - 1]];
const scale = scaleLinear().range([min.getTime(), max.getTime()]);
const reversedRange = range.reverse();

return (e: ChangeEvent<HTMLInputElement>) => {
const t = +e.target.value;
setT(t);

const tDate = new Date(scale(t));
const IFDate = reversedRange.find((d) => d <= tDate) as Date;
dispatch({ type: "SET_TIME_SLIDER_FILTER", value: IFDate });
};
} else {
return (e: ChangeEvent<HTMLInputElement>) => {
const t = +e.target.value;
setT(t);
};
}
// @ts-ignore - allData is not there yet for maps
}, [chartState.chartType, chartState.allData, componentIri, dispatch]);

React.useEffect(() => {
onChange({ target: { value: "0" } } as ChangeEvent<HTMLInputElement>);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<Slider
name="time-slider"
min={0}
max={1}
step={0.0001}
value={t}
onChange={onChange}
/>
);
};

0 comments on commit 2ebea50

Please sign in to comment.