Skip to content

Commit

Permalink
[feat] Sync filter with layer timeline
Browse files Browse the repository at this point in the history
Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
  • Loading branch information
igorDykhta committed Oct 28, 2024
1 parent 0b6f320 commit 1d7bfc7
Show file tree
Hide file tree
Showing 24 changed files with 533 additions and 147 deletions.
1 change: 1 addition & 0 deletions src/actions/src/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const ActionTypes = {
REMOVE_NOTIFICATION: `${ACTION_PREFIX}REMOVE_NOTIFICATION`,
SET_LOCALE: `${ACTION_PREFIX}SET_LOCALE`,
LAYER_FILTERED_ITEMS_CHANGE: `${ACTION_PREFIX}LAYER_FILTERED_ITEMS_CHANGE`,
SYNC_TIME_FILTER_WITH_LAYER_TIMELINE: `${ACTION_PREFIX}SYNC_TIME_FILTER_WITH_LAYER_TIMELINE`,
TOGGLE_PANEL_LIST_VIEW: `${ACTION_PREFIX}TOGGLE_PANEL_LIST_VIEW`,

// uiState > export image
Expand Down
19 changes: 19 additions & 0 deletions src/actions/src/vis-state-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,25 @@ export function layerFilteredItemsChange(
};
}

export type SyncTimeFilterWithLayerTimelineAction = {
idx: number;
enable: boolean;
};

export function syncTimeFilterWithLayerTimeline(
idx: SyncTimeFilterWithLayerTimelineAction['idx'],
enable: SyncTimeFilterWithLayerTimelineAction['enable']
): Merge<
SyncTimeFilterWithLayerTimelineAction,
{type: typeof ActionTypes.SYNC_TIME_FILTER_WITH_LAYER_TIMELINE}
> {
return {
type: ActionTypes.SYNC_TIME_FILTER_WITH_LAYER_TIMELINE,
idx,
enable
};
}

/**
* This declaration is needed to group actions in docs
*/
Expand Down
80 changes: 49 additions & 31 deletions src/components/src/bottom-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, {forwardRef, useMemo, useCallback} from 'react';
import styled, {withTheme} from 'styled-components';

import {FILTER_VIEW_TYPES} from '@kepler.gl/constants';
import {hasPortableWidth, isSideFilter} from '@kepler.gl/utils';
import {hasPortableWidth, isSideFilter, mergeFilterWithTimeline} from '@kepler.gl/utils';
import {media, breakPointValues} from '@kepler.gl/styles';
import {TimeRangeFilter} from '@kepler.gl/types';

Expand Down Expand Up @@ -84,12 +84,10 @@ export default function BottomWidgetFactory(
const isOpen = Boolean(activeSidePanel);

const enlargedFilterIdx = useMemo(() => filters.findIndex(f => !isSideFilter(f)), [filters]);

const isMobile = hasPortableWidth(breakPointValues);

const animatedFilterIdx = useMemo(() => filters.findIndex(f => f.isAnimating), [filters]);
const animatedFilter = animatedFilterIdx > -1 ? filters[animatedFilterIdx] : null;

const isMobile = useMemo(() => hasPortableWidth(breakPointValues), []);
const isLegendPinned =
uiState.mapControls?.mapLegend?.show && uiState.mapControls?.mapLegend?.active;
const spaceForLegendWidth = isLegendPinned
Expand All @@ -98,8 +96,10 @@ export default function BottomWidgetFactory(
theme.bottomWidgetPaddingRight
: 0;

const enlargedFilterWidth =
(isOpen && !isMobile ? containerW - sidePanelWidth : containerW) - spaceForLegendWidth;
const enlargedFilterWidth = useMemo(
() => (!isMobile && isOpen ? containerW - sidePanelWidth : containerW) - spaceForLegendWidth,
[isMobile, isOpen, containerW, sidePanelWidth, spaceForLegendWidth]
);

// show playback control if layers contain trip layer & at least one trip layer is visible
const animatableLayer = useMemo(
Expand All @@ -108,8 +108,11 @@ export default function BottomWidgetFactory(
[layers]
);

const readyToAnimation =
Array.isArray(animationConfig.domain) && Number.isFinite(animationConfig.currentTime);
const readyToAnimation = useMemo(
() => Array.isArray(animationConfig.domain) && Number.isFinite(animationConfig.currentTime),
[animationConfig.domain, animationConfig.currentTime]
);

// if animation control is showing, hide time display in time slider
const showFloatingTimeDisplay = !animatableLayer.length;
const showAnimationControl =
Expand All @@ -118,7 +121,19 @@ export default function BottomWidgetFactory(

// if filter is not animating, pass in enlarged filter here because
// animation controller needs to call reset on it
const filter = (animatedFilter as TimeRangeFilter) || filters[enlargedFilterIdx];
const filter = useMemo(
() => (animatedFilter as TimeRangeFilter) || filters[enlargedFilterIdx],
[animatedFilter, filters, enlargedFilterIdx]
);

// we merge filter and timeline if filter is synced
const {filter: enhancedFilter, animationConfig: enhancedAnimationConfig} = useMemo(
() =>
filter?.syncedWithLayerTimeline
? mergeFilterWithTimeline(filter, animationConfig)
: {filter, animationConfig},
[filter, animationConfig]
);

const onClose = useCallback(
() => visStateActions.setFilterView(enlargedFilterIdx, FILTER_VIEW_TYPES.side),
Expand All @@ -144,27 +159,29 @@ export default function BottomWidgetFactory(
hasPadding={showAnimationControl || showTimeWidget}
ref={rootRef}
>
<LayerAnimationController
animationConfig={animationConfig}
setLayerAnimationTime={visStateActions.setLayerAnimationTime}
>
{(isAnimating, start, pause, resetAnimation, timeline, setTimelineValue) =>
showAnimationControl ? (
<LayerAnimationControl
updateAnimationSpeed={visStateActions.updateLayerAnimationSpeed}
toggleAnimation={visStateActions.toggleLayerAnimation}
isAnimatable={!animatedFilter}
isAnimating={isAnimating}
resetAnimation={resetAnimation}
setTimelineValue={setTimelineValue}
timeline={timeline}
/>
) : null
}
</LayerAnimationController>
{filter ? (
{!filter?.syncedWithLayerTimeline ? (
<LayerAnimationController
animationConfig={enhancedAnimationConfig}
setLayerAnimationTime={visStateActions.setLayerAnimationTime}
>
{(isAnimating, start, pause, resetAnimation, timeline, setTimelineValue) =>
showAnimationControl ? (
<LayerAnimationControl
updateAnimationSpeed={visStateActions.updateLayerAnimationSpeed}
toggleAnimation={visStateActions.toggleLayerAnimation}
isAnimatable={!animatedFilter}
isAnimating={isAnimating}
resetAnimation={resetAnimation}
setTimelineValue={setTimelineValue}
timeline={timeline}
/>
) : null
}
</LayerAnimationController>
) : null}
{enhancedFilter ? (
<FilterAnimationController
filter={filter}
filter={enhancedFilter}
filterIdx={animatedFilterIdx > -1 ? animatedFilterIdx : enlargedFilterIdx}
setFilterAnimationTime={visStateActions.setFilterAnimationTime}
>
Expand All @@ -173,7 +190,7 @@ export default function BottomWidgetFactory(
<TimeWidget
// TimeWidget uses React.memo, here we pass width
// even though it doesnt use it, to force rerender
filter={filters[enlargedFilterIdx] as TimeRangeFilter}
filter={enhancedFilter as TimeRangeFilter}
index={enlargedFilterIdx}
datasets={datasets}
readOnly={readOnly}
Expand All @@ -185,8 +202,9 @@ export default function BottomWidgetFactory(
updateAnimationSpeed={visStateActions.updateFilterAnimationSpeed}
resetAnimation={resetAnimation}
isAnimatable={!animationConfig || !animationConfig.isAnimating}
timeline={timeline}
animationConfig={animationConfig}
onClose={onClose}
timeline={timeline}
onToggleMinify={onToggleMinify}
/>
) : null
Expand Down
1 change: 1 addition & 0 deletions src/components/src/common/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export {default as WarningSign} from './warning-sign';
export {default as DrawPolygon} from './draw-polygon';
export {default as Polygon} from './polygon';
export {default as Rectangle} from './rectangle';
export {default as TimelineMarker} from './timeline-marker';
export {default as OrderByList} from './order-by-list';
export {default as OrderByDataset} from './order-by-dataset';
export {default as Messages} from './messages';
Expand Down
39 changes: 39 additions & 0 deletions src/components/src/common/icons/timeline-marker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import React, {Component} from 'react';
import PropTypes, {any} from 'prop-types';
import Base from './base';

export default class TimelineMarker extends Component {
static propTypes = {
/** Set the height of the icon, ex. '16px' */
height: PropTypes.string,
// not expected?
width: PropTypes.string,
style: any
};

static defaultProps = {
height: '16px',
predefinedClassName: 'data-ex-icons-timeline-marker',
viewBox: '0 0 5 12'
};

render() {
return (
<Base {...this.props}>
<svg
width="5"
height="12"
viewBox="0 0 5 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="5" height="9" fill="#C4C4C4" />
<path d="M2.5 11.5L0 9H5L2.5 11.5Z" fill="#C4C4C4" />
</svg>
</Base>
);
}
}
37 changes: 37 additions & 0 deletions src/components/src/common/range-slider-subline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import React from 'react';
import {TimelineMarker} from '../common/icons';

const LINE_STYLE = {
height: '4px',
width: '100%',
backgroundColor: '#5558DB'
};

const TIMELINE_MARKER_LEFT_STYLE = {position: 'absolute', top: '0', left: '-2px'};
const TIMELINE_MARKER_RIGHT_STYLE = {position: 'absolute', top: '0', right: '-2px'};

function RangeSliderSublineFactory() {
const RangeSliderSubline = ({line}) => {
return (
<div
style={{
marginLeft: `${line[0]}%`,
paddingTop: '14px',
width: `${line[1] - line[0]}%`,
position: 'relative'
}}
>
<TimelineMarker height="12px" width="5px" style={TIMELINE_MARKER_LEFT_STYLE} />
<div style={LINE_STYLE} />
<TimelineMarker height="12px" width="5px" style={TIMELINE_MARKER_RIGHT_STYLE} />
</div>
);
};

return RangeSliderSubline;
}

export default RangeSliderSublineFactory;
13 changes: 10 additions & 3 deletions src/components/src/common/range-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import styled from 'styled-components';
import RangePlotFactory from './range-plot';
import Slider from './slider/slider';
import {Input} from './styled-components';

import RangeSliderSublineFactory from '../common/range-slider-subline';
import {observeDimensions, unobserveDimensions, roundValToStep, clamp} from '@kepler.gl/utils';
import {LineChart, Filter, Bins} from '@kepler.gl/types';
import {Datasets} from '@kepler.gl/table';
Expand Down Expand Up @@ -60,6 +60,8 @@ interface RangeSliderProps {
step?: number;
sliderHandleWidth?: number;
xAxis?: ElementType;
sublines?: [number, number][];

timezone?: string | null;
timeFormat?: string;
playbackControlWidth?: number;
Expand All @@ -77,10 +79,11 @@ interface RangeSliderProps {
invertTrendColor?: boolean;
}

RangeSliderFactory.deps = [RangePlotFactory];
RangeSliderFactory.deps = [RangePlotFactory, RangeSliderSublineFactory];

export default function RangeSliderFactory(
RangePlot: ReturnType<typeof RangePlotFactory>
RangePlot: ReturnType<typeof RangePlotFactory>,
RangeSliderSubline: ReturnType<typeof RangeSliderSublineFactory>
): ComponentType<RangeSliderProps> {
class RangeSlider extends Component<RangeSliderProps> {
static defaultProps = {
Expand Down Expand Up @@ -227,6 +230,7 @@ export default function RangeSliderFactory(
timeFormat,
playbackControlWidth,
setFilterPlot,
sublines,
animationWindow,
filter,
datasets
Expand Down Expand Up @@ -266,6 +270,9 @@ export default function RangeSliderFactory(
setFilterPlot={setFilterPlot}
/>
) : null}
{sublines?.length
? sublines.map((line, index) => <RangeSliderSubline key={index} line={line} />)
: null}
<SliderWrapper
className="kg-range-slider__slider"
isRanged={isRanged}
Expand Down
3 changes: 2 additions & 1 deletion src/components/src/common/time-range-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import RangeSliderFactory from './range-slider';
import TimeSliderMarkerFactory from './time-slider-marker';
import PlaybackControlsFactory from './animation-control/playback-controls';
import TimeRangeSliderTimeTitleFactory from './time-range-slider-time-title';
import {LineChart, Timeline} from '@kepler.gl/types';
import {LineChart, Timeline, AnimationConfig} from '@kepler.gl/types';
import {ActionHandler, setFilterPlot} from '@kepler.gl/actions';
import AnimationControlFactory from './animation-control/animation-control';

Expand Down Expand Up @@ -45,6 +45,7 @@ type TimeRangeSliderProps = {
onChange: (v: number[]) => void;
timeline: Timeline;
invertTrendColor?: boolean;
animationConfig?: AnimationConfig;
};

const StyledSliderContainer = styled.div<StyledSliderContainerProps>`
Expand Down
Loading

0 comments on commit 1d7bfc7

Please sign in to comment.