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

Custom TimeShift periods and Latest periods in Time Dimension Filter #765

Merged
merged 4 commits into from
Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
58 changes: 30 additions & 28 deletions config-examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dataCubes:
formula: $time
kind: time
granularities: [ 'P1D', 'P1W', 'P1M', 'P3M', 'P1Y' ]
timeShiftDurations: [ 'P1W', 'P1M', 'P3M', 'P6M' ]
latestPeriodDurations: [ 'P1D', 'P1W', 'P1M', 'P3M', 'P1Y' ]

- name: geography_group
title: Geography
Expand Down Expand Up @@ -101,33 +103,33 @@ dataCubes:
- name: development_indicators_group
title: Development Indicators
dimensions:
- name: gdp_per_capita
title: GDP Per Capita
description: Gross domestic product at purchasing power parity (thousands).
formula: $gdp_per_capita.divide(1000).fallback(-1)
kind: number
- name: extreme_poverty
title: Extreme Poverty
description: Share of the population living in extreme poverty (% of population).
formula: $extreme_poverty.fallback(-1)
kind: number

- name: human_development_index
title: Human Development Index
description: |
Summary measure of average achievement in key dimensions of human development: a long and healthy life,
being knowledgeable and have a decent standard of living (scale 0-100).
formula: $human_development_index.multiply(100).fallback(-1)
kind: number

- name: stringency_index
title: Stringency Index
description: |
Government Response Stringency Index: composite measure based on 9 response indicators including
school closures, workplace closures, and travel bans,
rescaled to a value from 0 to 100 (100 = strictest response).
formula: $stringency_index.fallback(-1)
kind: number
- name: gdp_per_capita
title: GDP Per Capita
description: Gross domestic product at purchasing power parity (thousands).
formula: $gdp_per_capita.divide(1000).fallback(-1)
kind: number
- name: extreme_poverty
title: Extreme Poverty
description: Share of the population living in extreme poverty (% of population).
formula: $extreme_poverty.fallback(-1)
kind: number

- name: human_development_index
title: Human Development Index
description: |
Summary measure of average achievement in key dimensions of human development: a long and healthy life,
being knowledgeable and have a decent standard of living (scale 0-100).
formula: $human_development_index.multiply(100).fallback(-1)
kind: number

- name: stringency_index
title: Stringency Index
description: |
Government Response Stringency Index: composite measure based on 9 response indicators including
school closures, workplace closures, and travel bans,
rescaled to a value from 0 to 100 (100 = strictest response).
formula: $stringency_index.fallback(-1)
kind: number

measures:
- name: cases_group
Expand Down Expand Up @@ -211,7 +213,7 @@ dataCubes:
rule: fixed
time: 2010-02-01T08:00:00.000Z

defaultDuration: P1M
defaultDuration: P1D
defaultSortMeasure: count
defaultSelectedMeasures: [ "count" ]
introspection: no-autofill
Expand Down
5 changes: 3 additions & 2 deletions cypress/integration/relative-time-filter-menu.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ context("Relative Time Filter Menu", () => {
const filterMenuOkButton = () => timeFilter().find(".button.primary");
const latestSelector = () => timeFilter().find(".button-group:contains('Latest')");
const latestPreset = (preset) => latestSelector().find(`.group-member:contains(${preset})`);
const latestDayPreset = () => latestPreset("D").first();
const currentSelector = () => timeFilter().find(".button-group:contains('Current')");
const currentPreset = (preset) => currentSelector().find(`.group-member:contains(${preset})`);
const previousSelector = () => timeFilter().find(".button-group:contains('Previous')");
Expand Down Expand Up @@ -43,7 +44,7 @@ context("Relative Time Filter Menu", () => {
});

it("should mark selected preset", () => {
latestPreset("1D").should("have.class", "selected");
latestDayPreset().should("have.class", "selected");
});

it("should mark selected time shift", () => {
Expand All @@ -60,7 +61,7 @@ context("Relative Time Filter Menu", () => {

it("should disable Ok button after reverting preset", () => {
currentPreset("D").click();
latestPreset("1D").click();
latestDayPreset().click();

filterMenuOkButton().should("be.disabled");
});
Expand Down
14 changes: 7 additions & 7 deletions src/client/components/filter-menu/filter-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ export interface FilterMenuProps {
inside?: Element;
}

export const FilterMenu: React.SFC<FilterMenuProps> = (props: FilterMenuProps) => {
if (!props.dimension) return null;
switch (props.dimension.kind) {
export const FilterMenu: React.SFC<FilterMenuProps> = ({ dimension, ...props }: FilterMenuProps) => {
if (!dimension) return null;
switch (dimension.kind) {
case "time":
return <TimeFilterMenu {...props} />;
return <TimeFilterMenu dimension={dimension} {...props} />;
case "boolean":
return <BooleanFilterMenu {...props} />;
return <BooleanFilterMenu dimension={dimension}{...props} />;
case "number":
return <NumberFilterMenu {...props} />;
return <NumberFilterMenu dimension={dimension} {...props} />;
default:
return <StringFilterMenu {...props} />;
return <StringFilterMenu dimension={dimension}{...props} />;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import * as React from "react";
import { Clicker } from "../../../../common/models/clicker/clicker";
import { getMaxTime } from "../../../../common/models/data-cube/data-cube";
import { DateRange } from "../../../../common/models/date-range/date-range";
import { Dimension } from "../../../../common/models/dimension/dimension";
import { TimeDimension } from "../../../../common/models/dimension/dimension";
import { Essence } from "../../../../common/models/essence/essence";
import { FixedTimeFilterClause } from "../../../../common/models/filter-clause/filter-clause";
import { Filter } from "../../../../common/models/filter/filter";
Expand All @@ -37,7 +37,7 @@ export interface FixedTimeTabProps {
essence: Essence;
timekeeper: Timekeeper;
locale: Locale;
dimension: Dimension;
dimension: TimeDimension;
onClose: Fn;
clicker: Clicker;
}
Expand Down Expand Up @@ -154,6 +154,7 @@ export class FixedTimeTab extends React.Component<FixedTimeTabProps, FixedTimeTa
/>
<div className="cont">
<TimeShiftSelector
dimension={dimension}
shift={shift}
time={this.createDateRange()}
onShiftChange={this.setTimeShift}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Duration } from "chronoshift";
import * as React from "react";
import { Clicker } from "../../../../common/models/clicker/clicker";
import { isTimeAttribute } from "../../../../common/models/data-cube/data-cube";
import { Dimension } from "../../../../common/models/dimension/dimension";
import { TimeDimension } from "../../../../common/models/dimension/dimension";
import { Essence } from "../../../../common/models/essence/essence";
import { RelativeTimeFilterClause, TimeFilterPeriod } from "../../../../common/models/filter-clause/filter-clause";
import { Filter } from "../../../../common/models/filter/filter";
Expand All @@ -30,14 +30,15 @@ import { formatTimeRange } from "../../../../common/utils/time/time";
import { STRINGS } from "../../../config/constants";
import { ButtonGroup } from "../../button-group/button-group";
import { Button } from "../../button/button";
import { Preset } from "../../input-with-presets/input-with-presets";
import { StringInputWithPresets } from "../../input-with-presets/string-input-with-presets";
import { getTimeFilterPresets, LATEST_PRESETS, TimeFilterPreset } from "./presets";
import { getTimeFilterPresets, normalizeDurationName } from "./presets";
import { TimeShiftSelector } from "./time-shift-selector";

export interface PresetTimeTabProps {
essence: Essence;
timekeeper: Timekeeper;
dimension: Dimension;
dimension: TimeDimension;
clicker: Clicker;
onClose: Fn;
}
Expand All @@ -48,7 +49,7 @@ export interface PresetTimeTabState {
timeShift: string;
}

function initialState(essence: Essence, dimension: Dimension): PresetTimeTabState {
function initialState(essence: Essence, dimension: TimeDimension): PresetTimeTabState {
const filterClause = essence.filter.getClauseForDimension(dimension);
const timeShift = essence.timeShift.toJS();
if (filterClause instanceof RelativeTimeFilterClause) {
Expand Down Expand Up @@ -130,9 +131,13 @@ export class PresetTimeTab extends React.Component<PresetTimeTabProps, PresetTim
}

private renderLatestPresets() {
const { dimension } = this.props;
const { filterDuration, filterPeriod } = this.state;
const presets = LATEST_PRESETS.map(({ name, duration }: TimeFilterPreset) => {
return { name, identity: duration };
const presets: Array<Preset<string>> = dimension.latestPeriodDurations.map(duration => {
return {
name: normalizeDurationName(duration.toJS()),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm wondering if there's a reason to keep Duration objects in Dimension if we only convert it back to string every time. Same with TimeShifts on Dimension

Copy link
Contributor

Choose a reason for hiding this comment

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

Have you got any suggestion, where we could put it instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was wondering if we could store Duration and TimeShift as strings only. But I think that keeping rich objects in our model is better. It is easier to debug and monitor if we see concrete domain objects instead of serialised strings. Just like dates - no one like to debug code and look at ISO date strings :)

identity: duration.toJS()
};
});

const latestPeriod = filterPeriod === TimeFilterPeriod.LATEST;
Expand All @@ -145,7 +150,7 @@ export class PresetTimeTab extends React.Component<PresetTimeTabProps, PresetTim
placeholder={STRINGS.durationsExamples} />;
}

private renderButtonGroup(title: string, period: TimeFilterPeriod) {
private renderButtonGroup(title: string, period: TimeFilterPeriod.CURRENT | TimeFilterPeriod.PREVIOUS) {
const { filterDuration, filterPeriod } = this.state;
const activePeriod = period === filterPeriod;
const presets = getTimeFilterPresets(period);
Expand Down Expand Up @@ -189,6 +194,7 @@ export class PresetTimeTab extends React.Component<PresetTimeTabProps, PresetTim
{this.renderButtonGroup(STRINGS.previous, TimeFilterPeriod.PREVIOUS)}
<div className="preview preview--with-spacing">{previewText}</div>
<TimeShiftSelector
dimension={dimension}
shift={timeShift}
time={previewFilter}
timezone={essence.timezone}
Expand Down
31 changes: 7 additions & 24 deletions src/client/components/filter-menu/time-filter-menu/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import { $, Expression } from "plywood";
import { TimeFilterPeriod } from "../../../../common/models/filter-clause/filter-clause";
import { TimeShift } from "../../../../common/models/time-shift/time-shift";
import { MAX_TIME_REF_NAME, NOW_REF_NAME } from "../../../../common/models/time/time";

const $MAX_TIME = $(MAX_TIME_REF_NAME);
Expand All @@ -27,14 +26,6 @@ export interface TimeFilterPreset {
duration: string;
}

export const LATEST_PRESETS: TimeFilterPreset[] = [
{ name: "1H", duration: "PT1H" },
{ name: "6H", duration: "PT6H" },
{ name: "1D", duration: "P1D" },
{ name: "7D", duration: "P7D" },
{ name: "30D", duration: "P30D" }
];

export const CURRENT_PRESETS: TimeFilterPreset[] = [
{ name: "D", duration: "P1D" },
{ name: "W", duration: "P1W" },
Expand All @@ -51,11 +42,6 @@ export const PREVIOUS_PRESETS: TimeFilterPreset[] = [
{ name: "Y", duration: "P1Y" }
];

export interface ShiftPreset {
label: string;
shift: TimeShift;
}

export const DEFAULT_TIME_SHIFT_DURATIONS = [
"P1D", "P1W", "P1M", "P3M"
];
Expand All @@ -64,13 +50,12 @@ export const DEFAULT_LATEST_PERIOD_DURATIONS = [
"PT1H", "PT6H", "P1D", "P7D", "P30D"
];

export const COMPARISON_PRESETS: ShiftPreset[] = [
{ label: "Off", shift: TimeShift.empty() },
{ label: "D", shift: TimeShift.fromJS("P1D") },
{ label: "W", shift: TimeShift.fromJS("P1W") },
{ label: "M", shift: TimeShift.fromJS("P1M") },
{ label: "Q", shift: TimeShift.fromJS("P3M") }
];
export function normalizeDurationName(duration: string): string {
let normalized = duration.slice(1);
if (normalized.startsWith("T")) normalized = normalized.slice(1);
if (normalized.startsWith("1")) normalized = normalized.slice(1);
return normalized;
}

export function constructFilter(period: TimeFilterPeriod, duration: string): Expression {
switch (period) {
Expand All @@ -85,12 +70,10 @@ export function constructFilter(period: TimeFilterPeriod, duration: string): Exp
}
}

export function getTimeFilterPresets(period: TimeFilterPeriod): TimeFilterPreset[] {
export function getTimeFilterPresets(period: TimeFilterPeriod.CURRENT | TimeFilterPeriod.PREVIOUS): TimeFilterPreset[] {
switch (period) {
case TimeFilterPeriod.PREVIOUS:
return PREVIOUS_PRESETS;
case TimeFilterPeriod.LATEST:
return LATEST_PRESETS;
case TimeFilterPeriod.CURRENT:
return CURRENT_PRESETS;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import * as React from "react";
import { Clicker } from "../../../../common/models/clicker/clicker";
import { Dimension } from "../../../../common/models/dimension/dimension";
import { TimeDimension } from "../../../../common/models/dimension/dimension";
import { Essence } from "../../../../common/models/essence/essence";
import { RelativeTimeFilterClause } from "../../../../common/models/filter-clause/filter-clause";
import { Locale } from "../../../../common/models/locale/locale";
Expand Down Expand Up @@ -61,7 +61,7 @@ export interface TimeFilterMenuProps {
timekeeper: Timekeeper;
essence: Essence;
locale: Locale;
dimension: Dimension;
dimension: TimeDimension;
onClose: Fn;
containerStage?: Stage;
openOn: Element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import { Duration, Timezone } from "chronoshift";
import * as React from "react";
import { DateRange } from "../../../../common/models/date-range/date-range";
import { TimeDimension } from "../../../../common/models/dimension/dimension";
import { isValidTimeShift } from "../../../../common/models/time-shift/time-shift";
import { Unary } from "../../../../common/utils/functional/functional";
import { formatTimeRange } from "../../../../common/utils/time/time";
import { STRINGS } from "../../../config/constants";
import { Preset } from "../../input-with-presets/input-with-presets";
import { StringInputWithPresets } from "../../input-with-presets/string-input-with-presets";
import { COMPARISON_PRESETS } from "./presets";
import { normalizeDurationName } from "./presets";

function safeDurationFromJS(duration: string): Duration | null {
try {
Expand All @@ -33,7 +35,11 @@ function safeDurationFromJS(duration: string): Duration | null {
}
}

function timeShiftPreviewForRange({ shift, time, timezone }: Pick<TimeShiftSelectorProps, "shift" | "time" | "timezone">): string {
function timeShiftPreviewForRange({
shift,
time,
timezone
}: Pick<TimeShiftSelectorProps, "shift" | "time" | "timezone">): string {
if (time === null || !time.start || !time.end) return null;
const duration: Duration = safeDurationFromJS(shift);
if (duration === null) return null;
Expand All @@ -43,28 +49,34 @@ function timeShiftPreviewForRange({ shift, time, timezone }: Pick<TimeShiftSelec

export interface TimeShiftSelectorProps {
shift: string;
dimension: TimeDimension;
time: DateRange;
timezone: Timezone;
onShiftChange: Unary<string, void>;
}

const presets = COMPARISON_PRESETS.map(({ shift, label }) => ({
name: label,
identity: shift.toJS()
}));
function presets(dimension: TimeDimension): Array<Preset<string>> {
return [
{ name: "Off", identity: null },
...dimension.timeShiftDurations.map(shift => ({
name: normalizeDurationName(shift.toJS()),
identity: shift.toJS()
}))
];
}

export const TimeShiftSelector: React.SFC<TimeShiftSelectorProps> = props => {
const { onShiftChange, shift: selectedTimeShift } = props;
const { onShiftChange, dimension, shift: selectedTimeShift } = props;
const timeShiftPreview = timeShiftPreviewForRange(props);

return <React.Fragment>
<StringInputWithPresets
title={STRINGS.timeShift}
presets={presets}
presets={presets(dimension)}
selected={selectedTimeShift}
onChange={onShiftChange}
errorMessage={isValidTimeShift(selectedTimeShift) ? null : STRINGS.invalidDurationFormat}
placeholder={STRINGS.timeShiftExamples} />
placeholder={STRINGS.timeShiftExamples}/>
{timeShiftPreview ? <div className="preview">{timeShiftPreview}</div> : null}
</React.Fragment>;
};
Loading