Skip to content
This repository has been archived by the owner on Oct 20, 2023. It is now read-only.

Commit

Permalink
Feature/343 timeline hover bar to see beacon data (#60)
Browse files Browse the repository at this point in the history
* Timeline bars onHover and onClick labels via popover

* bar onHover/onClick state to ctrl labels, update route when clicking beacons

* click to show beacon list with router update when clicking listed beacon
  • Loading branch information
sharplessHQ authored Jan 12, 2023
1 parent 8093fac commit bf1e3d2
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 33 deletions.
11 changes: 11 additions & 0 deletions applications/client/src/store/campaign/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ export class TimelineStore extends ExtendedModel(() => ({
pair?.beaconId && selectedBeaconIds.includes(pair?.beaconId) ? sum + (pair?.commandCount ?? 0) : sum,
0
),
beaconCommands: bucket?.beaconCommandCountPair.reduce(
(commands: Array<Record<string, string | number>>, pair) => [
...commands,
{
beaconId: pair?.beaconId,
commandCount: pair?.commandCount,
},
],
[]
),
beaconNumbers: bucket?.beaconCommandCountPair.length,
})) ?? []
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type CommandContainerProps = ComponentProps<'div'> & {
hideCommentButton?: boolean;
showPath?: boolean;
expandedCommandIDs?: string[];
removeExplandedCommandID?: (commandId: string) => void;
removeExpandedCommandID?: (commandId: string) => void;
};

export const CommandContainer = observer<CommandContainerProps>(
Expand All @@ -36,7 +36,7 @@ export const CommandContainer = observer<CommandContainerProps>(
hideCommentButton = false,
showPath = false,
expandedCommandIDs = [],
removeExplandedCommandID,
removeExpandedCommandID,
...props
}) => {
const store = useStore();
Expand Down Expand Up @@ -68,7 +68,7 @@ export const CommandContainer = observer<CommandContainerProps>(
},
});
}
removeExplandedCommandID?.(state.commandId);
removeExpandedCommandID?.(state.commandId);
}
},
localCommand: undefined as undefined | CommandModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const Commands = observer<CommandsProps>(({ sort, showPath = true }) => {
expandedCommandIDs: store.router.params.activeItemId
? observable.array([store.router.params.activeItemId])
: observable.array<string>([]),
removeExplandedCommandID(commandId: string) {
removeExpandedCommandID(commandId: string) {
this.expandedCommandIDs.remove(commandId);
},
scrollToCommand(commandId: string, commandIds: string[], behavior: ScrollBehavior = 'smooth') {
Expand Down Expand Up @@ -125,7 +125,7 @@ export const Commands = observer<CommandsProps>(({ sort, showPath = true }) => {
data-command-id={commandId}
showPath={showPath}
expandedCommandIDs={state.expandedCommandIDs}
removeExplandedCommandID={state.removeExplandedCommandID}
removeExpandedCommandID={state.removeExpandedCommandID}
/>
))
)}
Expand Down
122 changes: 122 additions & 0 deletions applications/client/src/views/Campaign/Timeline/BarLabels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { css } from '@emotion/react';
import { dateFormat, dateTimeFormat, Flex } from '@redeye/client/components';
import { routes, useStore } from '@redeye/client/store';
import { CampaignViews, Tabs } from '@redeye/client/types';
import type { UUID } from '@redeye/client/types';
import { Txt, FlexSplitter, Tokens } from '@redeye/ui-styles';
import { observer } from 'mobx-react-lite';
import type { ComponentProps } from 'react';
import type { IBar } from './TimelineChart';

type BarLabelsProps = ComponentProps<'div'> & {
bar: IBar;
dateFormatter: string | undefined;
};

export const BarLabelDate = observer<BarLabelsProps>(({ bar, dateFormatter }) => {
const store = useStore();
const dateStart = store.settings.momentTz(bar?.start).format(dateFormatter);
const dateEnd = store.settings.momentTz(bar?.end).format(dateFormatter);
const sameDate = dateStart.split(' ')[0] === dateEnd.split(' ')[0];

return sameDate && dateFormatter === dateFormat ? (
<Txt block bold small>
{dateStart}
</Txt>
) : sameDate && dateFormatter === dateTimeFormat ? (
<Txt block bold small>{`${dateStart} - ${dateEnd.split(' ')[1]}`}</Txt>
) : (
<Txt block bold small>{`${dateStart} - ${dateEnd}`}</Txt>
);
});

export const BarLabelOnHover = observer<BarLabelsProps>(({ bar, dateFormatter }) => (
<div css={barPopoverStyles}>
<BarLabelDate bar={bar} dateFormatter={dateFormatter} />
<FlexSplitter />
<Flex css={{ 'padding-top': '0.2rem' }}>
<Txt muted small css={marginStyles(1)}>
Beacons
</Txt>
<FlexSplitter />
<Txt small>{bar?.beaconNumbers}</Txt>
</Flex>
<Flex>
<Txt muted small css={marginStyles(1)}>
Total commands
</Txt>
<FlexSplitter />
<Txt small>{bar?.beaconCount}</Txt>
</Flex>
<Flex>
<Txt muted small css={marginStyles(1)}>
Active Beacon commands
</Txt>
<FlexSplitter />
<Txt small>{bar?.activeBeaconCount}</Txt>
</Flex>
</div>
));

export const BarLabelBeaconList = observer<BarLabelsProps>(({ bar, dateFormatter }) => {
const store = useStore();
const routeToBeacon = (beaconId: string) => {
store.router.updateRoute({
path: routes[CampaignViews.EXPLORE],
params: {
view: CampaignViews.EXPLORE,
currentItem: 'beacon',
currentItemId: beaconId as UUID,
tab: Tabs.COMMANDS,
activeItem: undefined,
activeItemId: undefined,
},
});
};
return (
<div css={barPopoverStyles}>
<BarLabelDate bar={bar} dateFormatter={dateFormatter} />
<FlexSplitter />
<Flex css={{ padding: '0.2rem 0' }}>
<Txt small bold>
Beacons
</Txt>
<FlexSplitter />
<Txt small bold>
Commands
</Txt>
</Flex>
{bar.beaconCommands.map((beaconCommand) => (
<Flex
key={beaconCommand.beaconId}
css={barPopoverRowStyles}
onClick={() => routeToBeacon(beaconCommand.beaconId as string)}
>
<Txt small css={marginStyles(0.5)}>
{store.graphqlStore.beacons.get(beaconCommand.beaconId as string)?.displayName}
</Txt>
<Txt muted small css={marginStyles(4)}>
{store.graphqlStore.beacons.get(beaconCommand.beaconId as string)?.meta[0].maybeCurrent?.username}
</Txt>
<FlexSplitter />
<Txt small>{beaconCommand.commandCount}</Txt>
</Flex>
))}
</div>
);
});

const barPopoverStyles = css`
padding: 0.4rem;
`;

const marginStyles = (num: number) => css`
margin-right: ${num}rem;
`;

const barPopoverRowStyles = css`
&:hover {
cursor: pointer;
background: ${Tokens.Components.MinimalButtonBackgroundColorHover};
}
`;
120 changes: 92 additions & 28 deletions applications/client/src/views/Campaign/Timeline/Bars.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Popover2, Popover2InteractionKind } from '@blueprintjs/popover2';
import { css } from '@emotion/react';
import { createState, durationFormatter } from '@redeye/client/components';
import { Tokens } from '@redeye/ui-styles';
import { max, scaleLinear } from 'd3';
import { observer } from 'mobx-react-lite';
import type { ComponentProps } from 'react';
import { Fragment } from 'react';
import { animated } from 'react-spring';
import { BarLabelOnHover, BarLabelBeaconList } from './BarLabels';
import { TIMELINE_BG_COLOR } from './timeline-static-vars';
import type { IBar, IDimensions, TimeScale } from './TimelineChart';

Expand All @@ -17,9 +19,15 @@ type BarsProps = ComponentProps<'div'> & {
scrubberTime: Date | null;
};

export const Bars = observer<BarsProps>(({ xScale, bars, start, dimensions, scrubberTime }) => {
export const Bars = observer<BarsProps>(({ xScale, bars, start, end, dimensions, scrubberTime }) => {
const yMax = max(bars.map((bar) => bar.beaconCount)) ?? 0;
const yScale = scaleLinear([0, yMax], [0, dimensions.height]);
const state = createState({
isHover: true as boolean,
toggleIsHover() {
this.isHover = !this.isHover;
},
});

return (
<g>
Expand All @@ -28,32 +36,78 @@ export const Bars = observer<BarsProps>(({ xScale, bars, start, dimensions, scru
const width = xScale(bar.end) - x;

return (
<Fragment key={`${start.valueOf()}-${bar.start.valueOf()}`}>
{/* Dead & Future Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.beaconCount)}
height={yScale(bar.beaconCount)}
css={[baseBarStyles, scrubberTime && bar.end <= scrubberTime ? deadBarStyles : futureBarStyles]}
/>
{/* Active Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.activeBeaconCount)}
height={yScale(bar.activeBeaconCount)}
css={[baseBarStyles, aliveBarStyles]}
/>
{/* Selected Beacon Bar */}
<animated.rect
x={x}
width={width}
y={dimensions.height - yScale(bar.selectedBeaconCount)}
height={yScale(bar.selectedBeaconCount)}
css={[baseBarStyles, selectedBarStyles]}
/>
</Fragment>
<Popover2
key={`${start.valueOf()}-${bar.start.valueOf()}`}
interactionKind={Popover2InteractionKind.HOVER}
content={
bar.beaconCount ? (
state.isHover ? (
<BarLabelOnHover bar={bar} dateFormatter={durationFormatter(start, end)} />
) : (
<BarLabelBeaconList bar={bar} dateFormatter={durationFormatter(start, end)} />
)
) : undefined
}
placement="bottom"
modifiers={{
arrow: { enabled: false },
offset: {
enabled: true,
options: {
offset: [0, 20],
},
},
}}
renderTarget={({ isOpen, ref, ...targetProps }) => (
<g
ref={ref}
{...targetProps}
onMouseDown={(e) => {
e.preventDefault();
state.toggleIsHover();
}}
>
{/* Interaction Beacon Bar for color */}
{bar.beaconCount && (
<rect
x={x}
width={width}
y={0}
height={dimensions.height}
css={[baseBarStyles, interactionBarStyles(isOpen)]}
/>
)}
{/* Dead & Future Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.beaconCount)}
height={yScale(bar.beaconCount)}
css={[baseBarStyles, scrubberTime && bar.end <= scrubberTime ? deadBarStyles : futureBarStyles]}
/>
{/* Active Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.activeBeaconCount)}
height={yScale(bar.activeBeaconCount)}
css={[baseBarStyles, aliveBarStyles]}
/>
{/* Selected Beacon Bar */}
<animated.rect
x={x}
width={width}
y={dimensions.height - yScale(bar.selectedBeaconCount)}
height={yScale(bar.selectedBeaconCount)}
css={[baseBarStyles, selectedBarStyles]}
/>
{/* Interaction Beacon Bar for Functionality */}
{!!bar.beaconCount && (
<rect x={x} width={width} y={0} height={dimensions.height} css={[interactionBarFnStyles]} />
)}
</g>
)}
/>
);
})}
</g>
Expand Down Expand Up @@ -81,3 +135,13 @@ const aliveBarStyles = css`
const selectedBarStyles = css`
fill: ${Tokens.CoreTokens.BeaconSelected};
`;

const interactionBarStyles = (hover: boolean) => css`
fill: ${hover ? Tokens.CoreTokens.BeaconInteracted : 'transparent'};
opacity: 0.3;
`;

const interactionBarFnStyles = css`
fill: transparent;
cursor: pointer;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface IBar {
beaconCount: number;
activeBeaconCount: number;
selectedBeaconCount: number;
beaconNumbers: number;
beaconCommands: Array<Record<string, string | number>>;
}

export interface IDimensions {
Expand Down
1 change: 1 addition & 0 deletions packages/client/ui-styles/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const CoreTokens = {
BeaconAlive: BpTokens.Colors.Gray2,
BeaconFuture: BpTokens.Colors.Black,
BeaconSelected: BpTokens.Colors.White,
BeaconInteracted: BpTokens.Colors.Black,

FontWeightBold: '700',
FontWeightNormal: '400',
Expand Down

0 comments on commit bf1e3d2

Please sign in to comment.