Feat/246 Add candle chart controls (#281)

* Add candle chart controls

* Improve dropdown menu styling

* Use latest pennant version

* Use translation helper

* Square off highlighted corners
This commit is contained in:
John Walley 2022-05-13 14:08:32 +01:00 committed by GitHub
parent 492eef0fd0
commit dca1af7391
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 463 additions and 9 deletions

View File

@ -1,11 +1,42 @@
import 'pennant/dist/style.css';
import { Chart, Interval } from 'pennant';
import {
Chart,
ChartType,
Interval,
Overlay,
Study,
chartTypeLabels,
intervalLabels,
overlayLabels,
studyLabels,
} from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useContext, useMemo } from 'react';
import { useContext, useMemo, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import {
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import { t } from '@vegaprotocol/react-helpers';
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
[ChartType.OHLC, IconNames.WATERFALL_CHART],
]);
export type CandlesChartContainerProps = {
marketId: string;
@ -18,11 +49,148 @@ export const CandlesChartContainer = ({
const { keypair } = useVegaWallet();
const theme = useContext(ThemeContext);
const [interval, setInterval] = useState<Interval>(Interval.I15M);
const [chartType, setChartType] = useState<ChartType>(ChartType.CANDLE);
const [overlays, setOverlays] = useState<Overlay[]>([]);
const [studies, setStudies] = useState<Study[]>([]);
const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, keypair?.pub);
}, [client, marketId, keypair]);
return (
<Chart dataSource={dataSource} interval={Interval.I15M} theme={theme} />
<div className="h-full flex flex-col">
<div className="px-8 flex flex-row flex-wrap gap-8">
<DropdownMenu>
<DropdownMenuTrigger asChild={true}>
<Button appendIconName="caret-down" variant="secondary">
{t('Interval')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
<DropdownMenuRadioItem
key={timeInterval}
inset
value={timeInterval}
>
<DropdownMenuItemIndicator>
<Icon name="tick" />
</DropdownMenuItemIndicator>
{intervalLabels[timeInterval]}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild={true}>
<Button appendIconName="caret-down" variant="secondary">
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={chartType}
onValueChange={(value) => {
setChartType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<DropdownMenuRadioItem key={type} inset value={type}>
<DropdownMenuItemIndicator>
<Icon name="tick" />
</DropdownMenuItemIndicator>
{chartTypeLabels[type]}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild={true}>
<Button appendIconName="caret-down" variant="secondary">
{t('Overlays')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.values(Overlay).map((overlay) => (
<DropdownMenuCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
inset
onCheckedChange={() => {
const newOverlays = [...overlays];
const index = overlays.findIndex((item) => item === overlay);
index !== -1
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
setOverlays(newOverlays);
}}
>
<DropdownMenuItemIndicator>
<Icon name="tick" />
</DropdownMenuItemIndicator>
{overlayLabels[overlay]}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild={true}>
<Button appendIconName="caret-down" variant="secondary">
{t('Studies')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.values(Study).map((study) => (
<DropdownMenuCheckboxItem
key={study}
inset
checked={studies.includes(study)}
onCheckedChange={() => {
const newStudies = [...studies];
const index = studies.findIndex((item) => item === study);
index !== -1
? newStudies.splice(index, 1)
: newStudies.push(study);
setStudies(newStudies);
}}
>
<DropdownMenuItemIndicator>
<Icon name="tick" />
</DropdownMenuItemIndicator>
{studyLabels[study]}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex-1">
<Chart
dataSource={dataSource}
options={{
chartType: chartType,
overlays: overlays,
studies: studies,
}}
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
setStudies(options.studies ?? []);
}}
/>
</div>
</div>
);
};

View File

@ -1,7 +1,6 @@
import type { ApolloClient } from '@apollo/client';
import { gql } from '@apollo/client';
import type { Candle, DataSource } from 'pennant';
import { Interval } from 'pennant';
import { addDecimal } from '@vegaprotocol/react-helpers';
import type { Chart, ChartVariables } from './__generated__/Chart';
@ -12,6 +11,7 @@ import type {
CandlesSubVariables,
} from './__generated__/CandlesSub';
import type { Subscription } from 'zen-observable-ts';
import { Interval } from '@vegaprotocol/types';
export const CANDLE_FRAGMENT = gql`
fragment CandleFields on Candle {

View File

@ -0,0 +1,93 @@
import { useState } from 'react';
import type { ComponentMeta } from '@storybook/react';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from './dropdown-menu';
import { Button } from '../button';
import { Icon } from '../icon';
export default {
title: 'DropdownMenu',
} as ComponentMeta<typeof DropdownMenu>;
export const CheckboxItems = () => {
const checkboxItems = [
{ label: 'Bollinger bands', state: useState(false) },
{ label: 'Envelope', state: useState(true) },
{ label: 'EMA', state: useState(false) },
{ label: 'Moving average', state: useState(false) },
{ label: 'Price monitoring bands', state: useState(false) },
];
return (
<div style={{ textAlign: 'center', padding: 50 }}>
<DropdownMenu>
<DropdownMenuTrigger>
<Button appendIconName="chevron-down">Options</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{checkboxItems.map(({ label, state: [checked, setChecked] }) => (
<DropdownMenuCheckboxItem
key={label}
inset
checked={checked}
onCheckedChange={setChecked}
>
<DropdownMenuItemIndicator>
<Icon name="tick" />
</DropdownMenuItemIndicator>
{label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
export const RadioItems = () => {
const files = ['README.md', 'index.js', 'page.css'];
const [file, setFile] = useState(files[1]);
return (
<div style={{ textAlign: 'center', padding: 50 }}>
<DropdownMenu>
<DropdownMenuTrigger>
<Button appendIconName="chevron-down">Open</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem inset onSelect={() => console.log('minimize')}>
Minimize window
</DropdownMenuItem>
<DropdownMenuItem inset onSelect={() => console.log('zoom')}>
Zoom
</DropdownMenuItem>
<DropdownMenuItem inset onSelect={() => console.log('smaller')}>
Smaller
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value={file} onValueChange={setFile}>
{files.map((file) => (
<DropdownMenuRadioItem key={file} inset value={file}>
{file}
<DropdownMenuItemIndicator>
<Icon name="tick" />
</DropdownMenuItemIndicator>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<p>Selected file: {file}</p>
</div>
);
};

View File

@ -0,0 +1,145 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import { forwardRef } from 'react';
const itemStyles = classNames([
'text-ui',
'text-black',
'dark:text-white',
'flex',
'items-center',
'justify-between',
'leading-1',
'cursor-default',
'select-none',
'whitespace-nowrap',
'h-[25px]',
'py-0',
'pr-8',
'color-black',
]);
const itemClass = classNames(itemStyles, [
'focus:bg-vega-yellow',
'dark:focus:bg-vega-yellow',
'focus:text-black',
'dark:focus:text-black',
'focus:outline-none',
]);
function getItemClasses(inset: boolean) {
return classNames(itemClass, inset ? 'pl-28' : 'pl-4', 'relative');
}
/**
* Contains all the parts of a dropdown menu.
*/
export const DropdownMenu = DropdownMenuPrimitive.Root;
/**
* The button that toggles the dropdown menu.
* By default, the {@link DropdownMenuContent} will position itself against the trigger.
*/
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
/**
* Used to group multiple {@link DropdownMenuRadioItem}s.
*/
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
/**
* The component that pops out when the dropdown menu is open.
*/
export const DropdownMenuContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentProps<typeof DropdownMenuPrimitive.Content>
>(({ className, ...contentProps }, forwardedRef) => (
<DropdownMenuPrimitive.Content
{...contentProps}
ref={forwardedRef}
className={classNames(
'inline-block box-border border-1 border-black bg-white dark:bg-black p-4',
className
)}
/>
));
/**
* The component that contains the dropdown menu items.
*/
export const DropdownMenuItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset = false, ...itemProps }, forwardedRef) => (
<DropdownMenuPrimitive.Item
{...itemProps}
ref={forwardedRef}
className={classNames(getItemClasses(inset), className)}
/>
));
/**
* An item that can be controlled and rendered like a checkbox.
*/
export const DropdownMenuCheckboxItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
inset?: boolean;
}
>(({ className, inset = false, ...checkboxItemProps }, forwardedRef) => (
<DropdownMenuPrimitive.CheckboxItem
{...checkboxItemProps}
ref={forwardedRef}
className={classNames(getItemClasses(inset), className)}
/>
));
/**
* An item that can be controlled and rendered like a radio.
*/
export const DropdownMenuRadioItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean;
}
>(({ className, inset = false, ...radioItemprops }, forwardedRef) => (
<DropdownMenuPrimitive.RadioItem
{...radioItemprops}
ref={forwardedRef}
className={classNames(getItemClasses(inset), className)}
/>
));
/**
* Renders when the parent {@link DropdownMenuCheckboxItem} or {@link DropdownMenuRadioItem} is checked.
* You can style this element directly, or you can use it as a wrapper to put an icon into, or both.
*/
export const DropdownMenuItemIndicator = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.ItemIndicator>,
React.ComponentProps<typeof DropdownMenuPrimitive.ItemIndicator>
>(({ className, ...itemIndicatorProps }, forwardedRef) => (
<DropdownMenuPrimitive.ItemIndicator
{...itemIndicatorProps}
ref={forwardedRef}
className={classNames(
'absolute inline-flex justify-center align-middle left-0 w-24',
className
)}
/>
));
/**
* Used to visually separate items in the dropdown menu.
*/
export const DropdownMenuSeparator = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentProps<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...separatorProps }, forwardedRef) => (
<DropdownMenuPrimitive.Separator
{...separatorProps}
ref={forwardedRef}
className={classNames('h-px my-1 mx-2.5 bg-black', className)}
/>
));

View File

@ -0,0 +1 @@
export * from './dropdown-menu';

View File

@ -5,6 +5,7 @@ export * from './callout';
export * from './card';
export * from './copy-with-tooltip';
export * from './dialog';
export * from './dropdown-menu';
export * from './etherscan-link';
export * from './form-group';
export * from './icon';

View File

@ -22,6 +22,7 @@
"@blueprintjs/select": "^3.16.6",
"@nrwl/next": "13.10.3",
"@radix-ui/react-dialog": "^0.1.5",
"@radix-ui/react-dropdown-menu": "^0.1.6",
"@radix-ui/react-tabs": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7",
"@sentry/nextjs": "^6.19.3",
@ -53,7 +54,7 @@
"immer": "^9.0.12",
"lodash": "^4.17.21",
"next": "^12.0.7",
"pennant": "0.4.7",
"pennant": "0.4.8",
"postcss": "^8.4.6",
"react": "17.0.2",
"react-copy-to-clipboard": "^5.0.4",

View File

@ -3307,6 +3307,20 @@
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-use-escape-keydown" "0.1.0"
"@radix-ui/react-dropdown-menu@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c"
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.1.0"
"@radix-ui/react-compose-refs" "0.1.0"
"@radix-ui/react-context" "0.1.1"
"@radix-ui/react-id" "0.1.5"
"@radix-ui/react-menu" "0.1.6"
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-use-controllable-state" "0.1.0"
"@radix-ui/react-focus-guards@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.0.tgz#ba3b6f902cba7826569f8edc21ff8223dece7def"
@ -3332,6 +3346,30 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "0.1.0"
"@radix-ui/react-menu@0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.1.6.tgz#7f9521a10f6a9cd819b33b33d5ed9538d79b2e75"
integrity sha512-ho3+bhpr3oAFkOBJ8VkUb1BcGoiZBB3OmcWPqa6i5RTUKrzNX/d6rauochu2xDlWjiRtpVuiAcsTVOeIC4FbYQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.1.0"
"@radix-ui/react-collection" "0.1.4"
"@radix-ui/react-compose-refs" "0.1.0"
"@radix-ui/react-context" "0.1.1"
"@radix-ui/react-dismissable-layer" "0.1.5"
"@radix-ui/react-focus-guards" "0.1.0"
"@radix-ui/react-focus-scope" "0.1.4"
"@radix-ui/react-id" "0.1.5"
"@radix-ui/react-popper" "0.1.4"
"@radix-ui/react-portal" "0.1.4"
"@radix-ui/react-presence" "0.1.2"
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-roving-focus" "0.1.5"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-use-direction" "0.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "^2.4.0"
"@radix-ui/react-popper@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029"
@ -3453,6 +3491,13 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-use-direction@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-direction/-/react-use-direction-0.1.0.tgz#97ac1d52e497c974389e7988f809238ed72e7df7"
integrity sha512-NajpY/An9TCPSfOVkgWIdXJV+VuWl67PxB6kOKYmtNAFHvObzIoh8o0n9sAuwSAyFCZVq211FEf9gvVDRhOyiA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-escape-keydown@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.1.0.tgz#dc80cb3753e9d1bd992adbad9a149fb6ea941874"
@ -16634,10 +16679,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
pennant@0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-0.4.7.tgz#b1541f57d563c8a14f026292f6f88c059938f31a"
integrity sha512-HKpQS9wUMdH7aqdiOTwWwXAd2GhxKT1RF//f9utYhTuWFK5dK44EvWiaOTol+ddpN30GStK4KxO8s2OkeYLvPw==
pennant@0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-0.4.8.tgz#a19a5aa862621f1ac868a44c1e62fc4db1250d95"
integrity sha512-/vk81reu4643Ol4C7sZ+SaJujpT8eHQmmETb/HmqzMR9pOY5rqMpOTzY5OMQO4SM8amvO40BkOaHt1HSnANcKA==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"