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

scaffold chart types #3022

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
50 changes: 50 additions & 0 deletions packages/gamut/src/BarChart/Bar/elements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { css } from '@codecademy/gamut-styles';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';

import { Box } from '../../Box';

export const minBarWidth = 8;

const baseStyles = {
alignItems: 'center',
height: '100%',
display: 'flex',
transitionDelay: '1.5s',
transition: 'width 0.5s',
position: 'absolute',
borderRadius: 'inherit',
borderColor: 'border-primary',
} as const;

export const Bar = styled(motion.div)(
css({
borderWidth: '1px',
borderStyle: 'solid',
...baseStyles,
})
);

export const ForegroundBar = styled(Box)(
css({
...baseStyles,
bg: 'feedback-warning',
borderLeftColor: 'transparent',
borderLeftStyle: 'solid',
borderLeftWidth: '1px',
borderRightStyle: 'solid',
borderRightWidth: '1px',
height: 'calc(100% - 2px)',
})
);

export const BarWrapper = styled(Box)(
css({
display: 'flex',
overflow: 'hidden',
position: 'relative',
alignItems: 'center',
height: { _: '8px', sm: '18px' },
borderRadius: { _: 'md', sm: 'xl' },
})
);
67 changes: 67 additions & 0 deletions packages/gamut/src/BarChart/Bar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';

import { Box } from '../../Box';
import { GridBoxProps } from '../../Box/props';
import { calculateBarWidth } from '../utils';
import { Bar, BarWrapper, ForegroundBar, minBarWidth } from './elements';

type BaseSkillsExperienceBarProps = {
startingValue?: number;
endingValue: number;
tickCount: number;
};

export const TotalBar: React.FC<BaseSkillsExperienceBarProps> = ({
startingValue,
endingValue,
tickCount,
}) => {
const animate = true;
const maxRange = 100;
const minRange = 0;

const showForegroundBar = Boolean(startingValue);

const barWidth = calculateBarWidth({
value: endingValue,
maxRange,
});

const foregroundBarWidth = calculateBarWidth({
value: startingValue ?? 0,
maxRange,
});

const initialBarWidth = `${Math.max(minBarWidth, foregroundBarWidth)}%`;

const endBarWidth = `${Math.max(minBarWidth, barWidth)}%`;

const animationProps = animate
? {
initial: { width: initialBarWidth },
animate: {
width: endBarWidth,
},
transition: { duration: 0.25, delay: 0.75 * maxRange },
}
: { width: endBarWidth };

return (
<BarWrapper>
{/* <Bar {...animationProps} /> */}
<Box
bg="paleBlue"
border={1}
borderRadius="xl"
height="100%"
width={endBarWidth}
/>
{showForegroundBar && (
<ForegroundBar
width={initialBarWidth}
data-testid="foreground-progress-bar"
/>
)}
</BarWrapper>
);
};
43 changes: 43 additions & 0 deletions packages/gamut/src/BarChart/ScaleChartHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ReactElement } from 'react';

import { GridBox } from '../Box';
import { GridBoxProps } from '../Box/props';
import { Column } from '../Layout';
import { Text } from '../Typography';
import { formatNumberUSCompact } from './utils';

export const getLabel = (
labelCount: number,
labelIndex: number,
max: number
) => {
const incrementalDecimal = 100 / (labelCount - 1) / 100;
return Math.floor(incrementalDecimal * labelIndex * max);
};

export const ScaleChartHeader: React.FC<{
min: number;
max: number;
labelCount: number;
}> = ({ labelCount, min, max }) => {
const gridColumns = labelCount;
const scaleLabels: ReactElement[] = [];
console.log(labelCount, min, max);
for (let i = min; i < labelCount; i++) {
scaleLabels.push(
<Column size={1} justifyItems="center" key={i}>
<Text data-testid="chart-header-label">
{formatNumberUSCompact(getLabel(labelCount, i, max))}
</Text>
</Column>
);
}
return (
<GridBox
gridTemplateColumns={{ _: `repeat(${gridColumns - 1}, 1fr)` }}
height="100%"
>
{scaleLabels}
</GridBox>
);
};
15 changes: 15 additions & 0 deletions packages/gamut/src/BarChart/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Box, GridBox } from '../Box';
import { TotalBar } from './Bar';
import { ScaleChartHeader } from './ScaleChartHeader';
import { BarChartProps } from './types';

export const BarChart: React.FC<BarChartProps> = () => {
return (
<Box width="100%" pl={0} mb={0}>
<ScaleChartHeader min={1} max={50} labelCount={8} />
<GridBox width="100%" pl={0} mb={0}>
<TotalBar startingValue={5} endingValue={45} tickCount={8} />
</GridBox>
</Box>
);
};
51 changes: 51 additions & 0 deletions packages/gamut/src/BarChart/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { GamutIconProps } from '@codecademy/gamut-icons';
import { HTMLProps } from 'react';

import { BoxProps } from '../Box';
import { ButtonProps } from '../Button';

type BarChartAriaLabel = {
'aria-label': string;
'aria-labelledby'?: never;
};

type BarChartAriaLabelledBy = {
'aria-label': never;
'aria-labelledby'?: string;
};

type BarChartLabel = BarChartAriaLabel | BarChartAriaLabelledBy;

type BarChartStyles = {
textColor?: Pick<BoxProps, 'color'>; // text default
foregroundBarColor?: Pick<BoxProps, 'color'>; // text default
backgroundBarColors?: Pick<BoxProps, 'color'>; // primary default
};

type BarProps = {
yLabel: string;
// The foreground stacked bar
startingValue: number;
// The background bar
endingValue?: number;
// The actual type is in Gamut
icon?: React.ComponentType<GamutIconProps>;
// onClick
onClick?: ButtonProps['onClick'];
// href
href?: HTMLProps<HTMLAnchorElement>['href'];
};
export type BarChartProps = BarChartLabel & {
// goes in hook
animate?: boolean;
barValues: BarProps[];
// goes in hook
maxRange: number;
// goes in hook
minRange: number;
order: 'ascending' | 'descending';
sortBy: 'label' | 'value' | 'none';
string: 'XP';
styleConfig: BarChartStyles;
xScale: number;
};
80 changes: 80 additions & 0 deletions packages/gamut/src/BarChart/utils/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export const numDigits = (num: number) => {
return Math.max(Math.floor(Math.log10(Math.abs(num))), 0) + 1;
};

export const columnBaseSize = (experience = 3) => {
const digits = numDigits(experience);
return {
sm: digits > 4 ? 5 : 4,
md: digits > 4 ? 5 : 4,
lg: digits > 4 ? 4 : 5,
xl: digits > 4 ? 5 : 4,
};
};

export const calculatePercent = (value: number, total: number) => {
return (value / total) * 100;
};

export const calculateBarWidth = ({
value,
maxRange,
}: {
value: number;
maxRange: number;
}) => {
return Math.floor(calculatePercent(value, maxRange));
};

// Calculate tick spacing and nice minimum and maximum data points on the axis.

export const calculateTicksAndRange = (
maxTicks: number,
minPoint: number,
maxPoint: number
): [number, number, number] => {
const range = niceNum(maxPoint - minPoint, false);
const tickSpacing = niceNum(range / (maxTicks - 1), true);
const niceMin = Math.floor(minPoint / tickSpacing) * tickSpacing;
const niceMax = Math.ceil(maxPoint / tickSpacing) * tickSpacing;
const tickCount = range / tickSpacing;
return [tickCount, niceMin, niceMax];
};

/**
* Returns a "nice" number approximately equal to range
* Rounds the number if round = true
* Takes the ceiling if round = false.
* A nice number is a simple decimal number, for example if a number is 1234, a nice number would be 1000 or 2000.
*/
export const niceNum = (range: number, roundDown: boolean): number => {
const exponent = Math.floor(Math.log10(range));
const fraction = range / 10 ** exponent;

let niceFraction: number;

if (roundDown) {
if (fraction < 1.5) niceFraction = 1;
else if (fraction < 3) niceFraction = 2;
else if (fraction < 7) niceFraction = 5;
else niceFraction = 10;
} else if (fraction <= 1) niceFraction = 1;
else if (fraction <= 2) niceFraction = 2;
else if (fraction <= 5) niceFraction = 5;
else niceFraction = 10;

return niceFraction * 10 ** exponent;
};

export const getPercentDiff = (v1: number, v2: number) => {
return (Math.abs(v1 - v2) / ((v1 + v2) / 2)) * 100;
};

export const formatNumberUS = (num: number) =>
Intl.NumberFormat('en').format(num);

export const formatNumberUSCompact = (num: number) =>
Intl.NumberFormat('en', {
notation: 'compact',
compactDisplay: 'short',
}).format(num);
1 change: 1 addition & 0 deletions packages/gamut/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './Anchor';
export * from './Animation';
export * from './AppWrapper';
export * from './Badge';
export * from './BarChart';
export * from './BodyPortal';
export * from './Box';
export * from './Breadcrumbs';
Expand Down
71 changes: 71 additions & 0 deletions packages/styleguide/src/lib/Organisms/BarChart/BarChart.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Canvas, Controls, Meta } from '@storybook/blocks';

import { ComponentHeader } from '~styleguide/blocks';

import * as BarChartStories from './BarChart.stories';

export const parameters = {
subtitle: `Template component`,
design: {
type: 'figma',
url: 'https: //www.figma.com/file/XXX',
},
status: 'current',
source: {
repo: 'gamut',
githubLink:
'https: //github.com/Codecademy/gamut/blob/main/packages/gamut/src/Logo',
},
};

<Meta of={BarChartStories} />;

<ComponentHeader {...parameters} />

## Usage

Use BarChart to [what it should be used for]

### Best practices:

- [recommendation / best practice for implementation]
- [recommendation / best practice for implementation]

When NOT to use

- [use case]- for [describe the use case], use the [similar component] component.
- [use case]- for [describe the use case], use the [similar component] component

## Anatomy

[Insert image exported from Figma]

1. [Element name]

- [description including available options and ux writing if relevant]

## Variants

### [Variant 1 name]

Use the [variant 1 name] to [what it should be used for]

<Canvas of={BarChartStories.Default} />

## Playground

If you are using a story named 'Default', you can forgo the `of` prop.

<Canvas sourceState="shown" of={BarChartStories.Default} />

<Controls />

## Accessibility considerations

- [Accessibility guidance]

## UX writing

- [content]
- [guidance]
- [guidance]
Loading
Loading