-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat: Dynamic most recent date filter #1291
Changes from all commits
e23bdf0
323ce96
7b6c076
29b3d4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,14 @@ | ||
import { t } from "@lingui/macro"; | ||
import { Trans, t } from "@lingui/macro"; | ||
import { | ||
CircularProgress, | ||
FormControlLabel, | ||
FormGroup, | ||
Switch as MUISwitch, | ||
Theme, | ||
Typography, | ||
} from "@mui/material"; | ||
import { makeStyles } from "@mui/styles"; | ||
import { TimeLocaleObject, extent, timeFormat, timeParse } from "d3"; | ||
import { TimeLocaleObject } from "d3"; | ||
import get from "lodash/get"; | ||
import orderBy from "lodash/orderBy"; | ||
import React, { | ||
|
@@ -28,6 +30,7 @@ import { | |
Slider, | ||
Switch, | ||
} from "@/components/form"; | ||
import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; | ||
import SelectTree from "@/components/select-tree"; | ||
import useDisclosure from "@/components/use-disclosure"; | ||
import { | ||
|
@@ -42,7 +45,10 @@ import { | |
ControlTab, | ||
OnOffControlTab, | ||
} from "@/configurator/components/chart-controls/control-tab"; | ||
import { DatePickerField } from "@/configurator/components/field-date-picker"; | ||
import { | ||
DatePickerField, | ||
DatePickerTimeUnit, | ||
} from "@/configurator/components/field-date-picker"; | ||
import { | ||
getTimeIntervalFormattedSelectOptions, | ||
getTimeIntervalWithProps, | ||
|
@@ -70,9 +76,9 @@ import { | |
Component, | ||
Dimension, | ||
HierarchyValue, | ||
ObservationValue, | ||
TemporalDimension, | ||
} from "@/domain/data"; | ||
import { truthy } from "@/domain/types"; | ||
import { useTimeFormatLocale } from "@/formatters"; | ||
import { TimeUnit } from "@/graphql/query-hooks"; | ||
import { useLocale } from "@/locales/use-locale"; | ||
|
@@ -252,94 +258,132 @@ export const DataFilterSelect = ({ | |
); | ||
}; | ||
|
||
const formatDate = timeFormat("%Y-%m-%d"); | ||
const parseDate = timeParse("%Y-%m-%d"); | ||
/** We can pin some filters' values to max value dynamically, so that when a new | ||
* value is added to the dataset, it will be automatically used as default filter | ||
* value for published charts. | ||
*/ | ||
const VISUALIZE_MAX_VALUE = "VISUALIZE_MAX_VALUE"; | ||
|
||
/** Checks if a given filter value is supposed to be dynamiaclly pinned to max | ||
* value. | ||
*/ | ||
export const isDynamicMaxValue = ( | ||
value: ObservationValue | ||
): value is "VISUALIZE_MAX_VALUE" => { | ||
return value === VISUALIZE_MAX_VALUE; | ||
}; | ||
|
||
export const DataFilterSelectDay = ({ | ||
dimension, | ||
label, | ||
disabled, | ||
isOptional, | ||
controls, | ||
}: { | ||
type DataFilterTemporalProps = { | ||
dimension: TemporalDimension; | ||
label: React.ReactNode; | ||
timeUnit: DatePickerTimeUnit; | ||
disabled?: boolean; | ||
isOptional?: boolean; | ||
controls?: React.ReactNode; | ||
}) => { | ||
}; | ||
|
||
export const DataFilterTemporal = (props: DataFilterTemporalProps) => { | ||
const { dimension, timeUnit, disabled, isOptional, controls } = props; | ||
const { label: _label, values, timeFormat } = dimension; | ||
const formatLocale = useTimeFormatLocale(); | ||
const formatDate = formatLocale.format(timeFormat); | ||
const parseDate = formatLocale.parse(timeFormat); | ||
const fieldProps = useSingleFilterSelect({ | ||
cubeIri: dimension.cubeIri, | ||
dimensionIri: dimension.iri, | ||
}); | ||
const noneLabel = t({ | ||
id: "controls.dimensionvalue.none", | ||
message: `No Filter`, | ||
}); | ||
const optionalLabel = t({ | ||
id: "controls.select.optional", | ||
message: `optional`, | ||
}); | ||
const allOptions = useMemo(() => { | ||
return isOptional | ||
? [ | ||
{ | ||
value: FIELD_VALUE_NONE, | ||
label: noneLabel, | ||
isNoneValue: true, | ||
}, | ||
...dimension.values, | ||
] | ||
: dimension.values; | ||
}, [isOptional, dimension.values, noneLabel]); | ||
|
||
const allOptionsSet = useMemo(() => { | ||
return new Set( | ||
allOptions | ||
.filter((x) => x.value !== FIELD_VALUE_NONE) | ||
.map((x) => { | ||
try { | ||
return x.value as string; | ||
} catch (e) { | ||
console.warn(`Bad value ${x.value}`); | ||
return; | ||
} | ||
}) | ||
.filter(truthy) | ||
); | ||
}, [allOptions]); | ||
|
||
const isDisabled = useCallback( | ||
const usesMostRecentDate = isDynamicMaxValue(fieldProps.value); | ||
const label = isOptional ? ( | ||
<> | ||
{_label}{" "} | ||
<span style={{ marginLeft: "0.25rem" }}> | ||
(<Trans id="controls.select.optional">optional</Trans>) | ||
</span> | ||
</> | ||
) : ( | ||
_label | ||
); | ||
const { minDate, maxDate, optionValues } = React.useMemo(() => { | ||
if (values.length) { | ||
const options = values.map((d) => { | ||
return { | ||
label: `${d.value}`, | ||
value: `${d.value}`, | ||
}; | ||
}); | ||
|
||
return { | ||
minDate: parseDate(`${values[0].value}`) as Date, | ||
maxDate: parseDate(`${values[values.length - 1].value}`) as Date, | ||
optionValues: options.map((d) => d.value), | ||
}; | ||
} else { | ||
const date = new Date(); | ||
return { | ||
minDate: date, | ||
maxDate: date, | ||
optionValues: [], | ||
}; | ||
} | ||
}, [values, parseDate]); | ||
const isDateDisabled = React.useCallback( | ||
(date: Date) => { | ||
return !allOptionsSet.has(formatDate(date)); | ||
return !optionValues.includes(formatDate(date)); | ||
}, | ||
[allOptionsSet] | ||
[optionValues, formatDate] | ||
); | ||
|
||
const dateValue = useMemo(() => { | ||
const parsed = fieldProps.value ? parseDate(fieldProps.value) : undefined; | ||
return parsed || new Date(); | ||
}, [fieldProps.value]); | ||
|
||
const [minDate, maxDate] = useMemo(() => { | ||
const [min, max] = extent(Array.from(allOptionsSet)); | ||
if (!min || !max) { | ||
return []; | ||
} | ||
return [new Date(min), new Date(max)] as const; | ||
}, [allOptionsSet]); | ||
|
||
return ( | ||
<DatePickerField | ||
label={isOptional ? `${label} (${optionalLabel})` : label} | ||
disabled={disabled} | ||
controls={controls} | ||
name={`date-picker-${dimension.iri}`} | ||
label={ | ||
<Flex | ||
sx={{ | ||
width: "100%", | ||
justifyContent: "space-between", | ||
alignItems: "center", | ||
}} | ||
> | ||
<FieldLabel | ||
label={ | ||
<OpenMetadataPanelWrapper dim={dimension}> | ||
{label} | ||
</OpenMetadataPanelWrapper> | ||
} | ||
/> | ||
{/* FIXME: adapt to design */} | ||
<FormGroup> | ||
<FormControlLabel | ||
control={ | ||
<MUISwitch | ||
checked={usesMostRecentDate} | ||
onChange={() => | ||
fieldProps.onChange({ | ||
target: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Instead of relying on the state usesMOstRecentDate, I think it is preferrable to rely on the |
||
value: usesMostRecentDate | ||
? formatDate(maxDate) | ||
: VISUALIZE_MAX_VALUE, | ||
}, | ||
}) | ||
} | ||
/> | ||
} | ||
// FIXME: adapt to design, translate | ||
label={<Typography variant="caption">Use most recent</Typography>} | ||
/> | ||
</FormGroup> | ||
</Flex> | ||
} | ||
value={ | ||
usesMostRecentDate ? maxDate : (parseDate(fieldProps.value) as Date) | ||
} | ||
onChange={fieldProps.onChange} | ||
name={dimension.iri} | ||
value={dateValue} | ||
isDateDisabled={isDisabled} | ||
isDateDisabled={isDateDisabled} | ||
timeUnit={timeUnit} | ||
dateFormat={formatDate} | ||
minDate={minDate} | ||
maxDate={maxDate} | ||
disabled={disabled || usesMostRecentDate} | ||
controls={controls} | ||
/> | ||
); | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand this 😓 Can you give me an example, or add a test 😁 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a test, but just to explain: possible filters coming from the backend do not return
VISUALIZE_MAX_VALUE
, but rather an actual date (e.g. 2023), so if we didn't replace it back withVISUALIZE_MAX_VALUE
, we would potentially trigger an update that shouldn't happen (as in fact, the pinned date didn't change, but is still equal toVISUALIZE_MAX_VALUE
). Maybe it would make sense to already do this replacement on the backend side? 🤔