{label}
-
- {isOptional ? (
-
({optionalLabel})
- ) : null}
+ {isOptional ?
({optionalLabel}) : null}
{isFetching ? (
) : null}
@@ -721,13 +698,13 @@ const FieldLabel = ({
};
export const ChartFieldField = ({
- label,
+ label = "",
field,
options,
optional,
disabled,
}: {
- label: string;
+ label?: string;
field: string;
options: Option[];
optional?: boolean;
@@ -776,7 +753,7 @@ export const ChartOptionRadioField = ({
label: string;
field: string | null;
path: string;
- value: string;
+ value: string | number;
defaultChecked?: boolean;
disabled?: boolean;
}) => {
diff --git a/app/configurator/components/ui-helpers.ts b/app/configurator/components/ui-helpers.ts
index bb435e5dd..875327df0 100644
--- a/app/configurator/components/ui-helpers.ts
+++ b/app/configurator/components/ui-helpers.ts
@@ -188,8 +188,8 @@ export const getIconName = (name: string): IconName => {
return "tableColumnTimeHidden";
case "time":
return "time";
- case "play":
- return "play";
+ case "animation":
+ return "animation";
default:
return "table";
diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx
index 53c874d50..3dd641080 100644
--- a/app/configurator/config-form.tsx
+++ b/app/configurator/config-form.tsx
@@ -297,7 +297,7 @@ export const useChartOptionRadioField = ({
}: {
field: string | null;
path: string;
- value: string;
+ value: string | number;
}): FieldProps => {
const locale = useLocale();
const [state, dispatch] = useConfiguratorState();
diff --git a/app/configurator/config-types.ts b/app/configurator/config-types.ts
index e29fed2d5..e041816f0 100644
--- a/app/configurator/config-types.ts
+++ b/app/configurator/config-types.ts
@@ -103,14 +103,6 @@ export type InteractiveFiltersTimeRange = t.TypeOf<
typeof InteractiveFiltersTimeRange
>;
-const InteractiveFiltersTimeSlider = t.type({
- // FIXME: add range
- componentIri: t.string,
-});
-export type InteractiveFiltersTimeSlider = t.TypeOf<
- typeof InteractiveFiltersTimeSlider
->;
-
const InteractiveFiltersDataConfig = t.type({
active: t.boolean,
componentIris: t.array(t.string),
@@ -123,7 +115,6 @@ const InteractiveFiltersConfig = t.union([
t.type({
legend: InteractiveFiltersLegend,
timeRange: InteractiveFiltersTimeRange,
- timeSlider: InteractiveFiltersTimeSlider,
dataFilters: InteractiveFiltersDataConfig,
}),
t.undefined,
@@ -167,6 +158,19 @@ const GenericSegmentField = t.intersection([
]);
export type GenericSegmentField = t.TypeOf
;
+const AnimationType = t.union([t.literal("continuous"), t.literal("stepped")]);
+export type AnimationType = t.TypeOf;
+
+const AnimationField = t.intersection([
+ GenericField,
+ t.type({
+ showPlayButton: t.boolean,
+ duration: t.number,
+ type: AnimationType,
+ }),
+]);
+export type AnimationField = t.TypeOf;
+
const SortingField = t.partial({
sorting: t.type({
sortingType: SortingType,
@@ -191,6 +195,7 @@ const ColumnFields = t.intersection([
}),
t.partial({
segment: ColumnSegmentField,
+ animation: AnimationField,
}),
]);
const ColumnConfig = t.type(
@@ -278,6 +283,7 @@ const ScatterPlotFields = t.intersection([
}),
t.partial({
segment: ScatterPlotSegmentField,
+ animation: AnimationField,
}),
]);
const ScatterPlotConfig = t.type(
@@ -296,11 +302,14 @@ export type ScatterPlotConfig = t.TypeOf;
const PieSegmentField = t.intersection([GenericSegmentField, SortingField]);
export type PieSegmentField = t.TypeOf;
-const PieFields = t.type({
- y: GenericField,
- // FIXME: "segment" should be "x" for consistency
- segment: PieSegmentField,
-});
+const PieFields = t.intersection([
+ t.type({
+ y: GenericField,
+ // FIXME: "segment" should be "x" for consistency
+ segment: PieSegmentField,
+ }),
+ t.partial({ animation: AnimationField }),
+]);
const PieConfig = t.type(
{
version: t.string,
@@ -625,6 +634,16 @@ export const isSegmentInConfig = (
return !isTableConfig(chartConfig) && !isMapConfig(chartConfig);
};
+export const isAnimationInConfig = (
+ chartConfig: ChartConfig
+): chartConfig is ColumnConfig | ScatterPlotConfig | PieConfig => {
+ return (
+ chartConfig.chartType === "column" ||
+ chartConfig.chartType === "scatterplot" ||
+ chartConfig.chartType === "pie"
+ );
+};
+
export const isColorFieldInConfig = (
chartConfig: ChartConfig
): chartConfig is MapConfig => {
@@ -695,6 +714,7 @@ type ColumnAdjusters = BaseAdjusters & {
| PieSegmentField
| TableFields
>;
+ animation: FieldAdjuster;
};
};
@@ -739,6 +759,7 @@ type ScatterPlotAdjusters = BaseAdjusters & {
| PieSegmentField
| TableFields
>;
+ animation: FieldAdjuster;
};
};
@@ -753,6 +774,7 @@ type PieAdjusters = BaseAdjusters & {
| ScatterPlotSegmentField
| TableFields
>;
+ animation: FieldAdjuster;
};
};
diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx
index 7b402a4a8..3d99e7a0b 100644
--- a/app/configurator/configurator-state.tsx
+++ b/app/configurator/configurator-state.tsx
@@ -39,17 +39,18 @@ import {
ConfiguratorStateConfiguringChart,
ConfiguratorStateSelectingDataSet,
DataSource,
+ decodeConfiguratorState,
+ Filters,
FilterValue,
FilterValueMultiValues,
- Filters,
GenericField,
GenericFields,
ImputationType,
InteractiveFiltersConfig,
+ isAnimationInConfig,
MapConfig,
MapFields,
NumericalColorField,
- decodeConfiguratorState,
isAreaConfig,
isColorFieldInConfig,
isColumnConfig,
@@ -200,9 +201,6 @@ export type ConfiguratorStateAction =
type: "INTERACTIVE_FILTER_CHANGED";
value: InteractiveFiltersConfig;
}
- | {
- type: "INTERACTIVE_FILTER_TIME_SLIDER_RESET";
- }
| {
type: "CHART_CONFIG_REPLACED";
value: {
@@ -812,15 +810,21 @@ export const handleChartFieldChanged = (
const component = [...dimensions, ...measures].find(
(dim) => dim.iri === componentIri
);
- const selectedValues = actionSelectedValues
- ? actionSelectedValues
- : component?.values || [];
+ const selectedValues = actionSelectedValues ?? component?.values ?? [];
+
// The field was not defined before
if (!f) {
// FIXME?
- // optionalFields = ['segment', 'areaLayer', 'symbolLayer'],
+ // optionalFields = ['animation', 'segment', 'areaLayer', 'symbolLayer'],
// should be reflected in chart encodings
- if (field === "segment") {
+ if (field === "animation" && isAnimationInConfig(draft.chartConfig)) {
+ draft.chartConfig.fields.animation = {
+ componentIri,
+ showPlayButton: true,
+ duration: 30,
+ type: "continuous",
+ };
+ } else if (field === "segment") {
// FIXME: This should be more chart specific
// (no "stacked" for scatterplots for instance)
if (isSegmentInConfig(draft.chartConfig)) {
@@ -868,7 +872,15 @@ export const handleChartFieldChanged = (
}
} else {
// The field is being updated
- if (
+ if (field === "animation" && isAnimationInConfig(draft.chartConfig)) {
+ draft.chartConfig.fields.animation = {
+ componentIri,
+ showPlayButton:
+ draft.chartConfig.fields.animation?.showPlayButton ?? true,
+ duration: draft.chartConfig.fields.animation?.duration ?? 30,
+ type: draft.chartConfig.fields.animation?.type ?? "continuous",
+ };
+ } else if (
field === "segment" &&
"segment" in draft.chartConfig.fields &&
draft.chartConfig.fields.segment &&
@@ -1124,18 +1136,6 @@ export const handleInteractiveFilterChanged = (
return draft;
};
-export const handleInteractiveFilterTimeSliderReset = (
- draft: ConfiguratorState
-) => {
- if (draft.state === "CONFIGURING_CHART") {
- if (draft.chartConfig.interactiveFiltersConfig) {
- draft.chartConfig.interactiveFiltersConfig.timeSlider.componentIri = "";
- }
- }
-
- return draft;
-};
-
const reducer: Reducer = (
draft,
action
@@ -1274,9 +1274,6 @@ const reducer: Reducer = (
case "INTERACTIVE_FILTER_CHANGED":
return handleInteractiveFilterChanged(draft, action);
- case "INTERACTIVE_FILTER_TIME_SLIDER_RESET":
- return handleInteractiveFilterTimeSliderReset(draft);
-
case "CHART_CONFIG_REPLACED":
if (draft.state === "CONFIGURING_CHART") {
draft.chartConfig = deriveFiltersFromFields(
diff --git a/app/configurator/interactive-filters/helpers.ts b/app/configurator/interactive-filters/helpers.ts
index 8b9d2e2b2..bf91948f8 100644
--- a/app/configurator/interactive-filters/helpers.ts
+++ b/app/configurator/interactive-filters/helpers.ts
@@ -1,44 +1,9 @@
-import { getFieldComponentIri, getFieldComponentIris } from "@/charts";
+import { getFieldComponentIris } from "@/charts";
import { isTemporalDimension } from "@/domain/data";
-import {
- DimensionMetadataFragment,
- TemporalDimension,
- TimeUnit,
-} from "@/graphql/query-hooks";
+import { TimeUnit } from "@/graphql/query-hooks";
import { DataCubeMetadata } from "@/graphql/types";
-import {
- ChartConfig,
- ConfiguratorStateConfiguringChart,
-} from "../config-types";
-
-export const getTimeSliderFilterDimensions = ({
- chartConfig,
- dataCubeByIri,
-}: {
- chartConfig: ChartConfig;
- dataCubeByIri: {
- dimensions: DimensionMetadataFragment[];
- measures: DimensionMetadataFragment[];
- };
-}): TemporalDimension[] => {
- if (dataCubeByIri) {
- const allComponents = [
- ...dataCubeByIri.dimensions,
- ...dataCubeByIri.measures,
- ];
- const xComponentIri = getFieldComponentIri(chartConfig.fields, "x");
- const xComponent = allComponents.find((d) => d.iri === xComponentIri);
-
- return allComponents.filter(
- (d) =>
- isTemporalDimension(d) &&
- (isTemporalDimension(xComponent) ? d.iri !== xComponent.iri : true)
- ) as TemporalDimension[];
- }
-
- return [];
-};
+import { ConfiguratorStateConfiguringChart } from "../config-types";
export const getDataFilterDimensions = (
chartConfig: ConfiguratorStateConfiguringChart["chartConfig"],
diff --git a/app/configurator/interactive-filters/interactive-filters-config-options.tsx b/app/configurator/interactive-filters/interactive-filters-config-options.tsx
index 4688eb4ff..645822845 100644
--- a/app/configurator/interactive-filters/interactive-filters-config-options.tsx
+++ b/app/configurator/interactive-filters/interactive-filters-config-options.tsx
@@ -1,11 +1,10 @@
import { t, Trans } from "@lingui/macro";
import { Box } from "@mui/material";
import { extent } from "d3";
-import get from "lodash/get";
-import { useEffect, useMemo, useRef } from "react";
+import { useEffect, useRef } from "react";
import { getFieldComponentIri } from "@/charts";
-import { Checkbox, Select } from "@/components/form";
+import { Checkbox } from "@/components/form";
import { Loading } from "@/components/hint";
import {
ControlSection,
@@ -15,19 +14,12 @@ import {
import { parseDate } from "@/configurator/components/ui-helpers";
import { ConfiguratorStateConfiguringChart } from "@/configurator/config-types";
import { EditorBrush } from "@/configurator/interactive-filters/editor-time-brush";
-import {
- useInteractiveTimeRangeFiltersToggle,
- useInteractiveTimeSliderFiltersSelect,
-} from "@/configurator/interactive-filters/interactive-filters-config-state";
+import { useInteractiveTimeRangeFiltersToggle } from "@/configurator/interactive-filters/interactive-filters-config-state";
import { InteractiveFilterType } from "@/configurator/interactive-filters/interactive-filters-configurator";
import { useFormatFullDateAuto } from "@/formatters";
-import { TemporalDimension, useComponentsQuery } from "@/graphql/query-hooks";
+import { useComponentsQuery } from "@/graphql/query-hooks";
import { useLocale } from "@/locales/use-locale";
-import { FIELD_VALUE_NONE } from "../constants";
-
-import { getTimeSliderFilterDimensions } from "./helpers";
-
export const InteractiveFiltersOptions = ({
state,
}: {
@@ -70,19 +62,6 @@ export const InteractiveFiltersOptions = ({
);
- } else if (activeField === "timeSlider") {
- return (
-
-
-
- Time slider
-
-
-
-
-
-
- );
}
}
@@ -190,79 +169,3 @@ const InteractiveTimeRangeFilterOptions = ({
return ;
}
};
-
-const InteractiveTimeSliderFilterOptions = ({
- state: { chartConfig, dataSet, dataSource },
-}: {
- state: ConfiguratorStateConfiguringChart;
-}) => {
- const locale = useLocale();
- const [{ data }] = useComponentsQuery({
- variables: {
- iri: dataSet,
- sourceType: dataSource.type,
- sourceUrl: dataSource.url,
- locale,
- },
- });
-
- const value =
- get(chartConfig, "interactiveFiltersConfig.timeSlider.componentIri") ||
- FIELD_VALUE_NONE;
-
- if (data?.dataCubeByIri) {
- const timeSliderDimensions = getTimeSliderFilterDimensions({
- chartConfig,
- dataCubeByIri: data.dataCubeByIri,
- });
-
- return (
-
- );
- } else {
- return ;
- }
-};
-
-const InteractiveTimeSliderFilterOptionsSelect = ({
- dimensions,
- value,
-}: {
- dimensions: TemporalDimension[];
- value: string;
-}) => {
- const fieldProps = useInteractiveTimeSliderFiltersSelect();
- const options = useMemo(() => {
- return [
- {
- label: t({
- id: "controls.none",
- message: "None",
- }),
- value: FIELD_VALUE_NONE,
- isNoneValue: true,
- },
- ...dimensions.map((d) => ({
- label: d.label,
- value: d.iri,
- })),
- ];
- }, [dimensions]);
-
- return (
-
- );
-};
diff --git a/app/configurator/interactive-filters/interactive-filters-config-state.tsx b/app/configurator/interactive-filters/interactive-filters-config-state.tsx
index 348fb2bce..f3ff49b16 100644
--- a/app/configurator/interactive-filters/interactive-filters-config-state.tsx
+++ b/app/configurator/interactive-filters/interactive-filters-config-state.tsx
@@ -1,4 +1,3 @@
-import { SelectChangeEvent } from "@mui/material";
import produce from "immer";
import get from "lodash/get";
import { ChangeEvent, useCallback } from "react";
@@ -11,8 +10,6 @@ import {
import { DimensionMetadataFragment } from "@/graphql/query-hooks";
import useEvent from "@/utils/use-event";
-import { FIELD_VALUE_NONE } from "../constants";
-
export const useInteractiveFiltersToggle = (target: "legend") => {
const [state, dispatch] = useConfiguratorState(isConfiguring);
const onChange = useEvent((e: ChangeEvent) => {
@@ -115,41 +112,6 @@ export const updateInteractiveTimeRangeFilter = produce(
}
);
-export const useInteractiveTimeSliderFiltersSelect = () => {
- const [state, dispatch] = useConfiguratorState(isConfiguring);
- const { chartConfig } = state;
-
- const onChange = useCallback<(e: SelectChangeEvent) => void>(
- (e) => {
- const value = (
- e.target.value === FIELD_VALUE_NONE ? "" : e.target.value
- ) as string;
-
- const newConfig = produce(
- chartConfig.interactiveFiltersConfig,
- (draft) => {
- if (draft?.timeSlider) {
- draft.timeSlider.componentIri = value;
- }
-
- return draft;
- }
- );
-
- dispatch({
- type: "INTERACTIVE_FILTER_CHANGED",
- value: newConfig,
- });
- },
- [chartConfig, dispatch]
- );
-
- return {
- name: "timeSlider",
- onChange,
- };
-};
-
/**
* Toggles all data filters
*/
diff --git a/app/configurator/interactive-filters/interactive-filters-configurator.tsx b/app/configurator/interactive-filters/interactive-filters-configurator.tsx
index efe477eaa..f707c6bfd 100644
--- a/app/configurator/interactive-filters/interactive-filters-configurator.tsx
+++ b/app/configurator/interactive-filters/interactive-filters-configurator.tsx
@@ -17,29 +17,15 @@ import {
useConfiguratorState,
} from "@/configurator/configurator-state";
import { isTemporalDimension } from "@/domain/data";
-import { flag } from "@/flags/flag";
import { useComponentsQuery } from "@/graphql/query-hooks";
import { useLocale } from "@/locales/use-locale";
-import { getTimeSliderFilterDimensions } from "./helpers";
-
-const ENABLE_TIME_SLIDER = typeof window !== "undefined" && flag("timeslider");
-
-export type InteractiveFilterType =
- | "legend"
- | "timeRange"
- | "timeSlider"
- | "dataFilters";
+export type InteractiveFilterType = "legend" | "timeRange" | "dataFilters";
export const isInteractiveFilterType = (
field: string | undefined
): field is InteractiveFilterType => {
- return (
- field === "legend" ||
- field === "timeRange" ||
- field === "timeSlider" ||
- field === "dataFilters"
- );
+ return field === "legend" || field === "timeRange" || field === "dataFilters";
};
export const InteractiveFiltersConfigurator = ({
@@ -65,21 +51,13 @@ export const InteractiveFiltersConfigurator = ({
const xComponentIri = getFieldComponentIri(fields, "x");
const xComponent = allComponents.find((d) => d.iri === xComponentIri);
- const timeSliderDimensions = getTimeSliderFilterDimensions({
- chartConfig,
- dataCubeByIri: data.dataCubeByIri,
- });
-
const canFilterTimeRange =
isTemporalDimension(xComponent) &&
chartConfigOptionsUISpec[chartType].interactiveFilters.includes(
"timeRange"
);
- const canFilterTimeSlider =
- ENABLE_TIME_SLIDER && timeSliderDimensions.length > 0;
-
- if (!canFilterTimeRange && !canFilterTimeSlider) {
+ if (!canFilterTimeRange) {
return null;
}
@@ -107,18 +85,6 @@ export const InteractiveFiltersConfigurator = ({
label={xComponent!.label}
/>
)}
- {/* Time slider */}
- {canFilterTimeSlider && (
-
- Time slider
-
- }
- />
- )}
);
@@ -165,9 +131,7 @@ const InteractiveFilterTabField = ({
const checked = state.activeField === value;
const active = !!get(
state,
- `chartConfig.interactiveFiltersConfig["${value}"].${
- value === "timeSlider" ? "componentIri" : "active"
- }`
+ `chartConfig.interactiveFiltersConfig["${value}"].active`
);
return (
diff --git a/app/configurator/interactive-filters/time-slider.tsx b/app/configurator/interactive-filters/time-slider.tsx
index cefdc2274..2d7e366dc 100644
--- a/app/configurator/interactive-filters/time-slider.tsx
+++ b/app/configurator/interactive-filters/time-slider.tsx
@@ -7,6 +7,7 @@ import { useInteractiveFilters } from "@/charts/shared/use-interactive-filters";
import { TableChartState } from "@/charts/table/table-state";
import { Slider as GenericSlider } from "@/components/form";
import { parseDate } from "@/configurator/components/ui-helpers";
+import { AnimationType } from "@/configurator/config-types";
import { useTimeFormatUnit } from "@/formatters";
import { DimensionMetadataFragment, TimeUnit } from "@/graphql/query-hooks";
import { TemporalDimension } from "@/graphql/resolver-types";
@@ -33,9 +34,16 @@ const useTimeline = () => {
export const TimeSlider = ({
componentIri,
dimensions,
+ showPlayButton,
+ animationDuration,
+ animationType,
}: {
componentIri?: string;
dimensions: DimensionMetadataFragment[];
+ showPlayButton: boolean;
+ /** Animation duration in seconds. */
+ animationDuration: number;
+ animationType: AnimationType;
}) => {
const component = React.useMemo(() => {
return dimensions.find((d) => d.iri === componentIri) as
@@ -77,17 +85,22 @@ export const TimeSlider = ({
]);
const timeline = React.useMemo(() => {
- return new Timeline(timelineData, formatDate);
- }, [timelineData, formatDate]);
+ return new Timeline({
+ type: animationType,
+ msDuration: animationDuration * 1000,
+ msValues: timelineData,
+ formatMsValue: formatDate,
+ });
+ }, [animationType, animationDuration, timelineData, formatDate]);
return (
-
+
);
};
-const Root = () => {
+const Root = ({ showPlayButton }: { showPlayButton: boolean }) => {
const timeline = useTimeline();
const chartState = useChartState();
@@ -101,8 +114,14 @@ const Root = () => {
mr: `${chartState.bounds.margins.right}px`,
}}
>
-
-
+ {showPlayButton && }
+
{timeline.formattedExtent[0]}
diff --git a/app/docs/annotations.docs.tsx b/app/docs/annotations.docs.tsx
index 71c906cb7..7f6e7f70f 100644
--- a/app/docs/annotations.docs.tsx
+++ b/app/docs/annotations.docs.tsx
@@ -14,10 +14,10 @@ import { InteractiveFiltersProvider } from "@/charts/shared/use-interactive-filt
import Flex from "@/components/flex";
import { ColumnConfig } from "@/configurator";
import {
+ dimensions,
fields,
margins,
measures,
- dimensions,
observations,
} from "@/docs/fixtures";
@@ -39,7 +39,7 @@ ${(
dimensions={dimensions}
chartConfig={
{
- fields: fields,
+ fields,
interactiveFiltersConfig: {
legend: { active: false, componentIri: "" },
timeRange: {
@@ -194,9 +194,6 @@ ${(
componentIri: "",
presets: { type: "range", from: "", to: "" },
},
- timeSlider: {
- componentIri: "",
- },
dataFilters: { active: false, componentIris: [] },
},
} as unknown as ColumnConfig
@@ -240,9 +237,6 @@ ${(
componentIri: "",
presets: { type: "range", from: "", to: "" },
},
- timeSlider: {
- componentIri: "",
- },
dataFilters: { active: false, componentIris: [] },
},
} as unknown as ColumnConfig
diff --git a/app/docs/columns.docs.tsx b/app/docs/columns.docs.tsx
index 638f7f07c..2bbbf0281 100644
--- a/app/docs/columns.docs.tsx
+++ b/app/docs/columns.docs.tsx
@@ -1,5 +1,4 @@
import { markdown, ReactSpecimen } from "catalog";
-import * as React from "react";
import { Columns, ErrorWhiskers } from "@/charts/column/columns-simple";
import { ColumnChart } from "@/charts/column/columns-state";
@@ -26,8 +25,9 @@ ${(
dimensions={columnDimensions}
chartConfig={
{
+ chartType: "column",
+ filters: {},
fields: columnFields,
-
interactiveFiltersConfig: {
legend: {
active: false,
@@ -46,9 +46,6 @@ ${(
active: false,
componentIri: "http://fake-iri",
},
- timeSlider: {
- componentIri: "",
- },
},
} as unknown as ColumnConfig
}
diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts
index a69d2aba6..973258a6c 100644
--- a/app/docs/fixtures.ts
+++ b/app/docs/fixtures.ts
@@ -42,9 +42,6 @@ export const states: ConfiguratorState[] = [
filters: {},
interactiveFiltersConfig: {
legend: { active: false, componentIri: "" },
- timeSlider: {
- componentIri: "",
- },
timeRange: {
active: false,
componentIri: "",
diff --git a/app/docs/lines.docs.tsx b/app/docs/lines.docs.tsx
index a5f258850..f2a5ab912 100644
--- a/app/docs/lines.docs.tsx
+++ b/app/docs/lines.docs.tsx
@@ -1,5 +1,4 @@
import { markdown, ReactSpecimen } from "catalog";
-import * as React from "react";
import { Lines } from "@/charts/line/lines";
import { LineChart } from "@/charts/line/lines-state";
@@ -27,9 +26,6 @@ const interactiveFiltersConfig: InteractiveFiltersConfig = {
componentIri: "",
presets: { type: "range", from: "", to: "" },
},
- timeSlider: {
- componentIri: "",
- },
dataFilters: {
active: false,
componentIris: [],
diff --git a/app/docs/scatterplot.docs.tsx b/app/docs/scatterplot.docs.tsx
index 19af56cc7..935d6c6ce 100644
--- a/app/docs/scatterplot.docs.tsx
+++ b/app/docs/scatterplot.docs.tsx
@@ -1,5 +1,4 @@
import { markdown, ReactSpecimen } from "catalog";
-import * as React from "react";
import { Scatterplot } from "@/charts/scatterplot/scatterplot-simple";
import { ScatterplotChart } from "@/charts/scatterplot/scatterplot-state";
@@ -30,9 +29,6 @@ const interactiveFiltersConfig: InteractiveFiltersConfig = {
componentIri: "",
presets: { type: "range", from: "", to: "" },
},
- timeSlider: {
- componentIri: "",
- },
dataFilters: {
active: false,
componentIris: [],
diff --git a/app/homepage/examples.tsx b/app/homepage/examples.tsx
index 3a936a41d..dcfae30f7 100644
--- a/app/homepage/examples.tsx
+++ b/app/homepage/examples.tsx
@@ -112,9 +112,6 @@ export const Examples = ({
},
componentIri: "",
},
- timeSlider: {
- componentIri: "",
- },
legend: {
active: false,
componentIri: "",
@@ -197,9 +194,6 @@ export const Examples = ({
},
componentIri: "",
},
- timeSlider: {
- componentIri: "",
- },
legend: {
active: true,
componentIri: "",
diff --git a/app/icons/components/IcAnimation.tsx b/app/icons/components/IcAnimation.tsx
new file mode 100644
index 000000000..4280d640f
--- /dev/null
+++ b/app/icons/components/IcAnimation.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+
+function SvgIcAnimation(props: React.SVGProps) {
+ return (
+
+ );
+}
+
+export default SvgIcAnimation;
diff --git a/app/icons/components/index.tsx b/app/icons/components/index.tsx
index 94efeaf55..1f7c10bc3 100644
--- a/app/icons/components/index.tsx
+++ b/app/icons/components/index.tsx
@@ -1,4 +1,5 @@
import { default as Add } from "@/icons/components/IcAdd";
+import { default as Animation } from "@/icons/components/IcAnimation";
import { default as AnnotationArea } from "@/icons/components/IcAnnotationArea";
import { default as App } from "@/icons/components/IcApp";
import { default as ArrowBottom } from "@/icons/components/IcArrowBottom";
@@ -136,6 +137,7 @@ import { default as ZoomIn } from "@/icons/components/IcZoomIn";
export const Icons = {
add: Add,
+ animation: Animation,
annotationArea: AnnotationArea,
app: App,
arrowBottom: ArrowBottom,
diff --git a/app/icons/svg/ic_animation.svg b/app/icons/svg/ic_animation.svg
new file mode 100644
index 000000000..82aac72f1
--- /dev/null
+++ b/app/icons/svg/ic_animation.svg
@@ -0,0 +1,8 @@
+
diff --git a/app/locales/de/messages.po b/app/locales/de/messages.po
index 670ad98fc..4906bbfab 100644
--- a/app/locales/de/messages.po
+++ b/app/locales/de/messages.po
@@ -179,6 +179,10 @@ msgstr "Standardeinstellung"
msgid "controls.abbreviations"
msgstr "AbkĂĽrzungen verwenden"
+#: app/configurator/components/field-i18n.ts
+msgid "controls.animation"
+msgstr "Animation"
+
#: app/configurator/components/chart-annotator.tsx
msgid "controls.annotator.add-description-warning"
msgstr "FĂĽgen Sie eine Beschreibung hinzu"
@@ -234,7 +238,7 @@ msgstr "Farbe"
#: app/configurator/components/chart-controls/control-tab.tsx
msgid "controls.color.add"
-msgstr "Hinzufügen …"
+msgstr "Hinzufügen…"
#: app/configurator/components/chart-options-selector.tsx
msgid "controls.color.opacity"
@@ -375,11 +379,6 @@ msgstr "-"
msgid "controls.imputation.type.zeros"
msgstr "Nullen"
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
-#: app/configurator/interactive-filters/interactive-filters-configurator.tsx
-msgid "controls.interactive.filters.timeSlider"
-msgstr "Zeitschieber"
-
#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.interactiveFilters.time.noTimeDimension"
msgstr "Keine Zeitdimension verfĂĽgbar!"
@@ -435,7 +434,6 @@ msgstr "ZurĂĽck zur Vorschau"
#: app/configurator/components/field.tsx
#: app/configurator/components/field.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.none"
msgstr "Keine"
@@ -459,6 +457,30 @@ msgstr "Suche zurĂĽcksetzen"
msgid "controls.section.additional-information"
msgstr "Zusätzliche Informationen"
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.duration"
+msgstr "Animation Dauer"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.settings"
+msgstr "Animation Einstellungen"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.show-play-button"
+msgstr "Schaltfläche Abspielen anzeigen"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type"
+msgstr "Animation Typ"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.continuous"
+msgstr "Kontinuierlich"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.stepped"
+msgstr "Abgestuft"
+
#: app/configurator/components/chart-configurator.tsx
msgid "controls.section.chart.options"
msgstr "Diagramm-Einstellungen"
@@ -580,7 +602,7 @@ msgid "controls.select.columnStyle.textStyle"
msgstr "Schriftstil"
#: app/configurator/components/chart-options-selector.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
+#: app/configurator/components/chart-options-selector.tsx
msgid "controls.select.dimension"
msgstr "Dimension auswählen"
diff --git a/app/locales/en/messages.po b/app/locales/en/messages.po
index 6482c2164..0312b5206 100644
--- a/app/locales/en/messages.po
+++ b/app/locales/en/messages.po
@@ -179,6 +179,10 @@ msgstr "Default Settings"
msgid "controls.abbreviations"
msgstr "Use abbreviations"
+#: app/configurator/components/field-i18n.ts
+msgid "controls.animation"
+msgstr "Animation"
+
#: app/configurator/components/chart-annotator.tsx
msgid "controls.annotator.add-description-warning"
msgstr "Please add a description"
@@ -234,7 +238,7 @@ msgstr "Color"
#: app/configurator/components/chart-controls/control-tab.tsx
msgid "controls.color.add"
-msgstr "Add ..."
+msgstr "Add…"
#: app/configurator/components/chart-options-selector.tsx
msgid "controls.color.opacity"
@@ -375,11 +379,6 @@ msgstr "-"
msgid "controls.imputation.type.zeros"
msgstr "Zeros"
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
-#: app/configurator/interactive-filters/interactive-filters-configurator.tsx
-msgid "controls.interactive.filters.timeSlider"
-msgstr "Time slider"
-
#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.interactiveFilters.time.noTimeDimension"
msgstr "There is no time dimension!"
@@ -435,7 +434,6 @@ msgstr "Back to preview"
#: app/configurator/components/field.tsx
#: app/configurator/components/field.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.none"
msgstr "None"
@@ -459,6 +457,30 @@ msgstr "Clear search field"
msgid "controls.section.additional-information"
msgstr "Additional information"
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.duration"
+msgstr "Animation Duration"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.settings"
+msgstr "Animation Settings"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.show-play-button"
+msgstr "Show Play button"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type"
+msgstr "Animation Type"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.continuous"
+msgstr "Continuous"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.stepped"
+msgstr "Stepped"
+
#: app/configurator/components/chart-configurator.tsx
msgid "controls.section.chart.options"
msgstr "Chart Options"
@@ -580,7 +602,7 @@ msgid "controls.select.columnStyle.textStyle"
msgstr "Text Style"
#: app/configurator/components/chart-options-selector.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
+#: app/configurator/components/chart-options-selector.tsx
msgid "controls.select.dimension"
msgstr "Select a dimension"
diff --git a/app/locales/fr/messages.po b/app/locales/fr/messages.po
index d755e4a20..f842c32d0 100644
--- a/app/locales/fr/messages.po
+++ b/app/locales/fr/messages.po
@@ -179,6 +179,10 @@ msgstr "Paramètres d'origine"
msgid "controls.abbreviations"
msgstr "Utiliser des abréviations"
+#: app/configurator/components/field-i18n.ts
+msgid "controls.animation"
+msgstr "Animation"
+
#: app/configurator/components/chart-annotator.tsx
msgid "controls.annotator.add-description-warning"
msgstr "Ajoutez une description au graphique"
@@ -375,11 +379,6 @@ msgstr "-"
msgid "controls.imputation.type.zeros"
msgstr "ZĂ©ros"
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
-#: app/configurator/interactive-filters/interactive-filters-configurator.tsx
-msgid "controls.interactive.filters.timeSlider"
-msgstr "Curseur temporel"
-
#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.interactiveFilters.time.noTimeDimension"
msgstr "Il n'y a pas de dimension temporelle!"
@@ -435,7 +434,6 @@ msgstr "Retour à l'aperçu"
#: app/configurator/components/field.tsx
#: app/configurator/components/field.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.none"
msgstr "Aucun"
@@ -459,6 +457,30 @@ msgstr "Effacer la recherche"
msgid "controls.section.additional-information"
msgstr "Informations supplémentaires"
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.duration"
+msgstr "Durée de l'animation"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.settings"
+msgstr "Paramètres d'animation"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.show-play-button"
+msgstr "Bouton Afficher la lecture"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type"
+msgstr "Type d'animation"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.continuous"
+msgstr "En continu"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.stepped"
+msgstr "En escalier"
+
#: app/configurator/components/chart-configurator.tsx
msgid "controls.section.chart.options"
msgstr "Paramètres graphiques"
@@ -580,7 +602,7 @@ msgid "controls.select.columnStyle.textStyle"
msgstr "Style du texte"
#: app/configurator/components/chart-options-selector.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
+#: app/configurator/components/chart-options-selector.tsx
msgid "controls.select.dimension"
msgstr "SĂ©lectionner une dimension"
diff --git a/app/locales/it/messages.po b/app/locales/it/messages.po
index 23b3b4188..b62d0921d 100644
--- a/app/locales/it/messages.po
+++ b/app/locales/it/messages.po
@@ -179,6 +179,10 @@ msgstr "Impostazioni predefinite"
msgid "controls.abbreviations"
msgstr "Usa abbreviazioni"
+#: app/configurator/components/field-i18n.ts
+msgid "controls.animation"
+msgstr "Animazione"
+
#: app/configurator/components/chart-annotator.tsx
msgid "controls.annotator.add-description-warning"
msgstr "Aggiungi una descrizione"
@@ -234,7 +238,7 @@ msgstr "Colore"
#: app/configurator/components/chart-controls/control-tab.tsx
msgid "controls.color.add"
-msgstr "Aggiungi ..."
+msgstr "Aggiungi…"
#: app/configurator/components/chart-options-selector.tsx
msgid "controls.color.opacity"
@@ -375,11 +379,6 @@ msgstr "-"
msgid "controls.imputation.type.zeros"
msgstr "Zeri"
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
-#: app/configurator/interactive-filters/interactive-filters-configurator.tsx
-msgid "controls.interactive.filters.timeSlider"
-msgstr "Cursore del tempo"
-
#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.interactiveFilters.time.noTimeDimension"
msgstr "Nessuna dimensione temporale disponibile!"
@@ -435,7 +434,6 @@ msgstr "Torna all'anteprima"
#: app/configurator/components/field.tsx
#: app/configurator/components/field.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
msgid "controls.none"
msgstr "Nessuno"
@@ -459,6 +457,30 @@ msgstr "Cancella la ricerca"
msgid "controls.section.additional-information"
msgstr "Informazioni aggiuntive"
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.duration"
+msgstr "Durata dell'animazione"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.settings"
+msgstr "Impostazioni dell'animazione"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.show-play-button"
+msgstr "Mostra il pulsante di riproduzione"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type"
+msgstr "Tipo di animazione"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.continuous"
+msgstr "Continuo"
+
+#: app/configurator/components/chart-options-selector.tsx
+msgid "controls.section.animation.type.stepped"
+msgstr "A gradini"
+
#: app/configurator/components/chart-configurator.tsx
msgid "controls.section.chart.options"
msgstr "Opzioni del grafico"
@@ -580,7 +602,7 @@ msgid "controls.select.columnStyle.textStyle"
msgstr "Stile del testo"
#: app/configurator/components/chart-options-selector.tsx
-#: app/configurator/interactive-filters/interactive-filters-config-options.tsx
+#: app/configurator/components/chart-options-selector.tsx
msgid "controls.select.dimension"
msgstr "Seleziona una dimensione"
diff --git a/app/test/__fixtures/config/dev/chartConfig-column-covid19.json b/app/test/__fixtures/config/dev/chartConfig-column-covid19.json
index d03c57cd2..a40d4a74a 100644
--- a/app/test/__fixtures/config/dev/chartConfig-column-covid19.json
+++ b/app/test/__fixtures/config/dev/chartConfig-column-covid19.json
@@ -25,9 +25,6 @@
"to": ""
}
},
- "timeSlider": {
- "componentIri": ""
- },
"dataFilters": {
"active": false,
"componentIris": []
diff --git a/app/test/__fixtures/config/int/bathing-water-quality-hierarchie.json b/app/test/__fixtures/config/int/bathing-water-quality-hierarchie.json
index b4b119daa..e1f398a8a 100644
--- a/app/test/__fixtures/config/int/bathing-water-quality-hierarchie.json
+++ b/app/test/__fixtures/config/int/bathing-water-quality-hierarchie.json
@@ -43,7 +43,7 @@
"value": "E.coli"
}
},
- "version": "1.3.0",
+ "version": "1.4.0",
"chartType": "column",
"interactiveFiltersConfig": {
"legend": {
@@ -59,9 +59,6 @@
},
"componentIri": ""
},
- "timeSlider": {
- "componentIri": ""
- },
"dataFilters": {
"active": true,
"componentIris": [
diff --git a/app/test/__fixtures/config/prod/line-1.json b/app/test/__fixtures/config/prod/line-1.json
index 3626ee28c..203101c45 100644
--- a/app/test/__fixtures/config/prod/line-1.json
+++ b/app/test/__fixtures/config/prod/line-1.json
@@ -11,7 +11,7 @@
"url": "https://lindas.admin.ch/query"
},
"chartConfig": {
- "version": "1.3.0",
+ "version": "1.4.0",
"fields": {
"x": {
"componentIri": "http://environment.ld.admin.ch/foen/px/0703010000_105/dimension/0"
diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts
index 55385c4a6..e98f6db25 100644
--- a/app/utils/chart-config/versioning.ts
+++ b/app/utils/chart-config/versioning.ts
@@ -1,6 +1,6 @@
import produce from "immer";
-export const CHART_CONFIG_VERSION = "1.3.0";
+export const CHART_CONFIG_VERSION = "1.4.0";
type Migration = {
description: string;
@@ -473,6 +473,66 @@ const migrations: Migration[] = [
});
}
+ return newConfig;
+ },
+ },
+ {
+ description: `ALL
+ fields {
+ + animation {
+ componentIri
+ }
+ }`,
+ from: "1.3.0",
+ to: "1.4.0",
+ up: (config: any) => {
+ let newConfig = { ...config, version: "1.4.0" };
+
+ const { interactiveFiltersConfig } = newConfig;
+
+ if (interactiveFiltersConfig) {
+ const { legend, timeRange, timeSlider, dataFilters } =
+ interactiveFiltersConfig;
+
+ newConfig = produce(newConfig, (draft: any) => {
+ draft.interactiveFiltersConfig = { legend, timeRange, dataFilters };
+
+ if (
+ ["column", "pie", "scatterplot"].includes(draft.chartType) &&
+ timeSlider?.componentIri
+ ) {
+ draft.fields.animation = {
+ ...timeSlider,
+ showPlayButton: true,
+ duration: 30,
+ type: "continuous",
+ };
+ }
+ });
+ }
+
+ return newConfig;
+ },
+ down: (config: any) => {
+ let newConfig = { ...config, version: "1.3.0" };
+
+ const { fields } = config;
+ const { animation } = fields;
+
+ if (animation) {
+ newConfig = produce(newConfig, (draft: any) => {
+ delete draft.fields.animation;
+
+ if (["column", "pie", "scatterplot"].includes(draft.chartType)) {
+ draft.interactiveFiltersConfig.timeSlider = {
+ componentIri: animation.componentIri,
+ };
+ } else {
+ draft.interactiveFiltersConfig.timeSlider = { componentIri: "" };
+ }
+ });
+ }
+
return newConfig;
},
},
diff --git a/app/utils/observables.spec.ts b/app/utils/observables.spec.ts
index a236f021b..1550afd23 100644
--- a/app/utils/observables.spec.ts
+++ b/app/utils/observables.spec.ts
@@ -4,7 +4,12 @@ describe("Timeline", () => {
const values = [1, 100, 200, 500, 750, 1000];
const [min, max] = [values[0], values[values.length - 1]];
const formatValue = (d: number) => `${d}ms`;
- const timeline = new Timeline(values, formatValue);
+ const timeline = new Timeline({
+ type: "continuous",
+ msDuration: 1000,
+ msValues: values,
+ formatMsValue: formatValue,
+ });
beforeEach(() => timeline.setProgress(0));
diff --git a/app/utils/observables.ts b/app/utils/observables.ts
index df5dfa28e..162a8bc7b 100644
--- a/app/utils/observables.ts
+++ b/app/utils/observables.ts
@@ -1,5 +1,7 @@
import { ascending, bisect, scaleLinear } from "d3";
+import { AnimationType } from "@/configurator/config-types";
+
abstract class Observable {
private observers: Function[];
@@ -27,12 +29,15 @@ abstract class Observable {
/** Observable timeline which encloses animation state and logic. */
export class Timeline extends Observable {
- /** Relative timeline progress (0-1). */
- public progress = 0;
+ /** */
+ private type: AnimationType;
// Animation state.
public playing = false;
- private duration = 10000;
+ /** Animation progress (0-1). */
+ private animationProgress = 0;
+ /** Duration of the animation in miliseconds. */
+ private animationDuration: number;
private requestAnimationFrameId: number | undefined;
/** Timestamp set when animation beings. */
private t: number | undefined;
@@ -42,6 +47,9 @@ export class Timeline extends Observable {
private msValues: number[];
/** Based on current progress. */
private msValue: number;
+ /** Min and max values of msValues. */
+ private minMsValue: number;
+ private maxMsValue: number;
/** msValueScale(min value) = 0, msValueScale(max value) = 1 */
private msValueScale = scaleLinear();
/** msValues converted to relative values (0-1). */
@@ -50,15 +58,29 @@ export class Timeline extends Observable {
private formatMsValue: (d: number) => string;
private formattedMsExtent: [min: string, max: string];
- constructor(msValues: number[], formatMsValue: (d: number) => string) {
+ constructor({
+ type,
+ msDuration,
+ msValues,
+ formatMsValue,
+ }: {
+ type: AnimationType;
+ msDuration: number;
+ msValues: number[];
+ formatMsValue: (d: number) => string;
+ }) {
super();
+ this.type = type;
+ this.animationDuration = msDuration;
this.msValues = msValues.sort(ascending);
const [min, max] = [
this.msValues[0],
this.msValues[this.msValues.length - 1],
];
this.msValue = min;
+ this.minMsValue = min;
+ this.maxMsValue = max;
this.msValueScale = this.msValueScale.range([min, max]);
this.msRelativeValues = this.msValues.map(this.msValueScale.invert);
this.formatMsValue = formatMsValue;
@@ -71,8 +93,8 @@ export class Timeline extends Observable {
public start = () => {
if (!this.playing) {
- if (this.progress === 1) {
- this.setProgress(0);
+ if (this.animationProgress === 1) {
+ this.setProgress(0, true);
}
this.playing = true;
@@ -84,16 +106,16 @@ export class Timeline extends Observable {
private animate = (t: number) => {
if (this.t === undefined) {
- this.t = t - this.progress * this.duration;
+ this.t = t - this.animationProgress * this.animationDuration;
}
- if (t - this.t > this.duration) {
+ if (t - this.t > this.animationDuration) {
this.stop(false);
this.setProgress(1);
} else {
- const progress = (t - this.t) / this.duration;
+ const progress = (t - this.t) / this.animationDuration;
+ this.setProgress(progress, true);
- this.setProgress(progress);
this.requestAnimationFrameId = requestAnimationFrame(
this.animate.bind(this)
);
@@ -110,21 +132,52 @@ export class Timeline extends Observable {
this.requestAnimationFrameId = undefined;
}
- if (notify) this.notify();
+ if (notify) {
+ this.notify();
+ }
}
};
- public setProgress = (progress: number) => {
- this.progress = progress;
- this.setValue(progress);
+ /** Sets the animation progress and timeline value.
+ *
+ * For stepped type during the animation, the progress is calculated based on the
+ * artificial division (equal time segments to jump between values) and does not
+ * correspond to the actual progress of the slider.
+ */
+ public setProgress = (progress: number, triggeredByAnimation = false) => {
+ let value: number;
+
+ switch (this.type) {
+ case "continuous":
+ this.animationProgress = progress;
+ value = progress;
+
+ break;
+ case "stepped":
+ let i: number;
+
+ if (triggeredByAnimation) {
+ i = Math.floor(progress * this.msValues.length);
+ } else {
+ const msValue = Math.round(this.msValueScale(progress));
+ i = bisect(this.msValues, msValue) - 1;
+ }
+
+ const msRelativeValue = this.msRelativeValues[i];
+ this.animationProgress = i / (this.msValues.length - 1);
+ value = msRelativeValue;
+
+ break;
+ }
+
+ this.setValue(value);
this.notify();
};
private setValue = (progress: number) => {
const ms = Math.round(this.msValueScale(progress));
- const i = bisect(this.msValues, ms);
-
- this.msValue = this.msValues[i - 1];
+ const i = bisect(this.msValues, ms) - 1;
+ this.msValue = this.msValues[i];
};
get domain() {
@@ -142,4 +195,16 @@ export class Timeline extends Observable {
get formattedExtent() {
return this.formattedMsExtent;
}
+
+ /** Timeline progress (0-1) (mapped to track background color of Slider). */
+ get progress() {
+ switch (this.type) {
+ case "continuous":
+ return this.animationProgress;
+ case "stepped":
+ return (
+ (this.msValue - this.minMsValue) / (this.maxMsValue - this.minMsValue)
+ );
+ }
+ }
}