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

857/chart style #903

Merged
merged 7 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 0 additions & 2 deletions app/scripts/components/exploration/atoms/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,3 @@ export const timelineSizesAtom = atom((get) => {
};
});

// Whether or not the dataset rows are expanded.
export const isExpandedAtom = atom<boolean>(true);
143 changes: 102 additions & 41 deletions app/scripts/components/exploration/components/chart-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React, {
MutableRefObject,
forwardRef,
useEffect,
useState
useState,
useMemo
} from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import styled, {css} from 'styled-components';
import {
useFloating,
autoUpdate,
Expand All @@ -14,12 +15,11 @@ import {
shift
} from '@floating-ui/react';
import { bisector, ScaleTime, sort } from 'd3';
import { useAtomValue } from 'jotai';
import { format } from 'date-fns';
import { glsp, themeVal } from '@devseed-ui/theme-provider';

import { AnalysisTimeseriesEntry, TimeDensity } from '../types.d.ts';
import { isExpandedAtom } from '../atoms/timeline';
import { AnalysisTimeseriesEntry, TimeDensity, TimelineDatasetSuccess } from '../types.d.ts';
import { FADED_TEXT_COLOR, TEXT_TITLE_BG_COLOR, HEADER_COLUMN_WIDTH } from '../constants';
import { DataMetric } from './datasets/analysis-metrics';

import { getNumForChart } from '$components/common/chart/utils';
Expand Down Expand Up @@ -48,12 +48,16 @@ const MetricList = styled.ul`
list-style: none;
margin: 0 -${glsp()};
padding: 0;
padding-top: ${glsp(0.25)};
gap: ${glsp(0.25)};

> li {
padding: ${glsp(0, 1)};
}
`;
const MetricLi = styled.li`
display: flex;
justify-content: space-between;
`;

const MetricItem = styled.p<{ metricThemeColor: string }>`
display: flex;
Expand All @@ -71,46 +75,74 @@ const MetricItem = styled.p<{ metricThemeColor: string }>`
}
`;

type DivProps = JSX.IntrinsicElements['div'];
const fadedtext = css`
color: ${FADED_TEXT_COLOR};
`;

const TitleBox = styled.div`
background-color: ${TEXT_TITLE_BG_COLOR};
${fadedtext};
padding: ${glsp(0.5)};
font-size: 0.75rem;
`;

const ContentBox = styled.div`
padding: ${glsp(0.5)};
font-size: 0.75rem;
`;
const MetaBox = styled.div`
display: flex;
align-items: center;
gap: ${glsp(1)};
`;

const UnitBox = styled.div`
${fadedtext};
`;

type DivProps = JSX.IntrinsicElements['div'];
interface DatasetPopoverProps extends DivProps {
data: AnalysisTimeseriesEntry;
activeMetrics: DataMetric[];
timeDensity: TimeDensity;
dataset: TimelineDatasetSuccess;
}

function DatasetPopoverComponent(
props: DatasetPopoverProps,
ref: MutableRefObject<HTMLDivElement>
) {
const { data, activeMetrics, timeDensity, ...rest } = props;

const isExpanded = useAtomValue(isExpandedAtom);

const { data, dataset, activeMetrics, timeDensity, style, ...rest } = props;

// Check if there is no data to show
const hasData = activeMetrics.some(
(metric) => typeof data[metric.id] === 'number'
);

if (!isExpanded || !hasData) return null;
if (!hasData) return null;

return createPortal(
<div ref={ref} {...rest}>
<strong>{timeDensityFormat(data.date, timeDensity)}</strong>
<MetricList>
{activeMetrics.map((metric) => {
const dataPoint = data[metric.id];

return typeof dataPoint !== 'number' ? null : (
<li key={metric.id}>
<MetricItem metricThemeColor={metric.themeColor}>
<strong>{metric.chartLabel}:</strong>
{getNumForChart(dataPoint)}
</MetricItem>
</li>
);
})}
</MetricList>
<div ref={ref} style={{...style, padding: 0, gap: 0}} {...rest}>
<TitleBox>{dataset.data.name}</TitleBox>
<ContentBox>
<MetaBox style={{display: 'flex'}}>
<strong>{timeDensityFormat(data.date, timeDensity)}</strong>
<UnitBox>{dataset.data.info?.unit}</UnitBox>
</MetaBox>
<MetricList>
{activeMetrics.map((metric) => {
const dataPoint = data[metric.id];
return typeof dataPoint !== 'number' ? null : (
<MetricLi key={metric.id}>
<MetricItem metricThemeColor={metric.themeColor}>
{metric.chartLabel}
</MetricItem>
<strong>{getNumForChart(dataPoint)}</strong>
</MetricLi>
);
})}
</MetricList>
</ContentBox>
</div>,
document.body
);
Expand Down Expand Up @@ -140,6 +172,7 @@ function getClosestDataPoint(
data?: AnalysisTimeseriesEntry[],
positionDate?: Date
) {

if (!positionDate || !data) return;

const dataSorted = sort(data, (a, b) => a.date.getTime() - b.date.getTime());
Expand Down Expand Up @@ -178,7 +211,6 @@ interface InteractionDataPointOptions {
*/
export function getInteractionDataPoint(options: InteractionDataPointOptions) {
const { isHovering, xScaled, containerWidth, layerX, data } = options;

if (
!isHovering ||
!xScaled ||
Expand All @@ -197,13 +229,10 @@ export function getInteractionDataPoint(options: InteractionDataPointOptions) {
const closestDataPointPosition = closestDataPoint
? xScaled(closestDataPoint.date)
: Infinity;

const delta = Math.abs(layerX - closestDataPointPosition);


const inView =
closestDataPointPosition >= 0 &&
closestDataPointPosition <= containerWidth &&
delta <= 80;
closestDataPointPosition <= containerWidth;

return inView ? closestDataPoint : undefined;
}
Expand All @@ -212,7 +241,9 @@ interface PopoverHookOptions {
x?: number;
y?: number;
data?: AnalysisTimeseriesEntry;
dataset?: AnalysisTimeseriesEntry[];
enabled?: boolean;
xScaled?: ScaleTime<number, number>;
}

/**
Expand All @@ -222,7 +253,7 @@ interface PopoverHookOptions {
* @returns
*/
export function usePopover(options: PopoverHookOptions) {
const { x, y, data, enabled } = options;
const { x, y, data, xScaled, dataset, enabled } = options;

const inView = !!data;

Expand All @@ -233,16 +264,46 @@ export function usePopover(options: PopoverHookOptions) {
const [_isVisible, setVisible] = useState(inView);
const isVisible = enabled && _isVisible;

// Do not make tooltip to follow the cursor.
// Instead, show tooltip at the edge of the timeline
// even if the cursor is off from the data timeline.
const datasetMinX = useMemo(() => {
if (!xScaled || !dataset) return;
return (xScaled(dataset[dataset.length-1]?.date) + HEADER_COLUMN_WIDTH);
}, [xScaled, dataset]);

const datasetMaxX = useMemo(() => {
if (!xScaled || !dataset) return;
return (xScaled(dataset[0]?.date) + HEADER_COLUMN_WIDTH);
}, [xScaled, dataset]);

const finalClientX = useMemo(() => {
if (!datasetMinX || !datasetMaxX || !x) return;
return x < datasetMinX ? datasetMinX : x > datasetMaxX? datasetMaxX: x;
},[datasetMaxX, datasetMinX, x]);

// Determine which direction that popover needs to be displayed
const midpointX = useMemo(() => {
if (!xScaled || !dataset) return;
const start = xScaled(dataset[0]?.date) + HEADER_COLUMN_WIDTH;
const end = xScaled(dataset[dataset.length - 1]?.date) + HEADER_COLUMN_WIDTH;
return (start + end) / 2;
}, [xScaled, dataset]);

const popoverLeft = useMemo(() => {
if (finalClientX === undefined || midpointX === undefined) return true; // Default to true or decide based on your UI needs
return finalClientX < midpointX;
}, [finalClientX, midpointX]);

const floating = useFloating({
placement: 'left',
placement: popoverLeft ? 'left' : 'right',
open: isVisible,
onOpenChange: setVisible,
middleware: [offset(10), flip(), shift({ padding: 16 })],
whileElementsMounted: autoUpdate
});

const { refs, floatingStyles } = floating;

// Use a virtual element for the position reference.
// https://floating-ui.com/docs/virtual-elements
useEffect(() => {
Expand All @@ -256,17 +317,17 @@ export function usePopover(options: PopoverHookOptions) {
return {
width: 0,
height: 0,
x: x ?? 0,
x: finalClientX ?? 0,
y: y ?? 0,
top: y ?? 0,
left: x ?? 0,
right: x ?? 0,
left: finalClientX ?? 0,
right: finalClientX ?? 0,
bottom: y ?? 0
};
}
});
setVisible(true);
}, [refs, inView, x, y]);
}, [refs, inView, finalClientX, y]);

return {
refs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,34 @@ export interface DataMetric {
| 'infographicC'
| 'infographicD'
| 'infographicE';
style?: Record<string, string>
}

export const DATA_METRICS: DataMetric[] = [
{
id: 'min',
label: 'Min',
chartLabel: 'Min',
themeColor: 'infographicA'
themeColor: 'infographicA',
style: { "strokeDasharray": "2 2"}
},
{
id: 'mean',
label: 'Average',
chartLabel: 'Avg',
chartLabel: 'Average',
themeColor: 'infographicB'
},
{
id: 'max',
label: 'Max',
chartLabel: 'Max',
themeColor: 'infographicC'
themeColor: 'infographicC',
style: { "strokeDasharray": "2 2"}
},
{
id: 'std',
label: 'St Deviation',
chartLabel: 'STD',
chartLabel: 'St Deviation',
themeColor: 'infographicD'
},
{
Expand All @@ -49,6 +52,8 @@ export const DATA_METRICS: DataMetric[] = [
}
];

export const DEFAULT_DATA_METRICS: DataMetric[] = DATA_METRICS.filter(metric => metric.id ==='mean' || metric.id==='std');

const MetricList = styled(DropMenu)`
display: flex;
flex-flow: column;
Expand Down
Loading
Loading