Skip to content

Commit

Permalink
857/chart style (#903)
Browse files Browse the repository at this point in the history
**Related Ticket:** #857 

### Description of Changes
Change new E&A Chart style


### Notes & Questions About Changes
Part of #857
The list of changes specified in Figma:
https://www.figma.com/file/9INQauBWhiRxvOWDGhRrxO/US-GHG-Center?type=design&node-id=1127-2201&mode=design&t=VNfErIVXtEgHmbZi-0
- [x] On the chart, average and standard deviation are shown by default.
- [x] Standard deviation is shown as a grey band around the average
line.
- [x] Median, min and max are optional, accessed through the “Chart
options” icon. (*Unless editor specified which analysis to show)
- [x] Min and max are shown as grey dotted lines.
- [x] The y-axis is shown on the gray area outside the analysis period.
- [x] The y-axis should always show the zero line
- [x] Always expand with yaxis. Get rid of 'collapse' button
- [x] Optional: The chart actions icons are:”Download chart”, “Chart
options” and “Collapse”. Collapse is a local action and collapses the
current chart only. - **only implemented "Chart Options" for now**

Question: What should we do for circles that mark the time unit? If I
remember correctly, we added it because of the requests from
stakeholders.

### Validation / Testing

To check the changes, run analysis [on this
page](https://deploy-preview-903--veda-ui.netlify.app/exploration?datasets=%5B%7B%22id%22%3A%22no2-monthly%22%2C%22settings%22%3A%7B%22isVisible%22%3Atrue%2C%22opacity%22%3A100%2C%22analysisMetrics%22%3A%5B%7B%22id%22%3A%22mean%22%2C%22label%22%3A%22Average%22%2C%22chartLabel%22%3A%22Avg%22%2C%22themeColor%22%3A%22infographicB%22%7D%2C%7B%22id%22%3A%22std%22%2C%22label%22%3A%22St+Deviation%22%2C%22chartLabel%22%3A%22STD%22%2C%22themeColor%22%3A%22infographicD%22%7D%5D%7D%7D%2C%7B%22id%22%3A%22no2-monthly-diff%22%2C%22settings%22%3A%7B%22isVisible%22%3Atrue%2C%22opacity%22%3A100%2C%22analysisMetrics%22%3A%5B%7B%22id%22%3A%22mean%22%2C%22label%22%3A%22Average%22%2C%22chartLabel%22%3A%22Avg%22%2C%22themeColor%22%3A%22infographicB%22%7D%2C%7B%22id%22%3A%22std%22%2C%22label%22%3A%22St+Deviation%22%2C%22chartLabel%22%3A%22STD%22%2C%22themeColor%22%3A%22infographicD%22%7D%5D%7D%7D%5D&date=2023-08-31T15%3A00%3A00.000Z&aois=%5B%22le%7B_Tga%7EbDayfqWpxoNt%60%7EvEl_rpDpuaoJjb%7CsB%22%2C%228cd277%22%2Cfalse%5D&dateRange=2022-08-09T15%3A00%3A00.000Z%7C2023-09-30T15%3A00%3A00.000Z)
  • Loading branch information
hanbyul-here authored Apr 2, 2024
2 parents 9412b06 + 0e3a458 commit 694a665
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 151 deletions.
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

0 comments on commit 694a665

Please sign in to comment.