diff --git a/src/ui/Sparkline/Sparkline.spec.jsx b/src/ui/Sparkline/Sparkline.spec.tsx similarity index 91% rename from src/ui/Sparkline/Sparkline.spec.jsx rename to src/ui/Sparkline/Sparkline.spec.tsx index 614d9ca110..ec9a4d7acd 100644 --- a/src/ui/Sparkline/Sparkline.spec.jsx +++ b/src/ui/Sparkline/Sparkline.spec.tsx @@ -3,7 +3,11 @@ import { render, screen } from '@testing-library/react' import Sparkline from '.' describe('Sparkline', () => { - function setup(props) { + function setup(props: { + datum: any[] + description: string + dataTemplate: (d: number | null | undefined) => string + }) { render() } @@ -31,12 +35,14 @@ describe('Sparkline', () => { it("renders the correct number of tr's", () => { expect(screen.queryAllByRole('cell').length).toBe(8) }) + it('lines have an normal state', () => { expect(screen.queryAllByRole('cell')[0]).toHaveAttribute( 'data-mode', 'normal' ) }) + it('lines have an empty state', () => { expect(screen.queryAllByRole('cell')[4]).toHaveAttribute( 'data-mode', diff --git a/src/ui/Sparkline/Sparkline.stories.jsx b/src/ui/Sparkline/Sparkline.stories.jsx deleted file mode 100644 index 132892f44b..0000000000 --- a/src/ui/Sparkline/Sparkline.stories.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import Sparkline from './Sparkline' - -const Template = (args) => ( -
- {/* Sparkline conforms to the width and height of it's parent. */} - -
-) -const ManyTemplate = (args) => { - const range = 200 - const largeDataSetWithReusedData = Array(50) - .fill() - .map(() => Math.random() * range) - return ( - <> - {Array(50) - .fill() - .map((_, i) => { - return ( -
- -
- ) - })} - - ) -} - -const range = 200 -const createTestData = Array(20) - .fill() - .map(() => Math.random() * range - range / 2) - -export const NormalSparkline = Template.bind({}) -NormalSparkline.args = { - datum: createTestData, - description: 'storybook sparkline', - dataTemplate: (d) => `Foo ${d}%`, -} - -const createTestDataWMissing = Array(30) - .fill() - .map(() => (Math.random() > 0.4 ? Math.random() * range - range / 2 : null)) - -export const SparklineWithMissingData = Template.bind({}) -SparklineWithMissingData.args = { - datum: createTestDataWMissing, - description: 'storybook sparkline', - dataTemplate: (d) => `Foo ${d}%`, -} - -const createTestDataWMissingBeginning = Array(7) - .fill() - .map((_, i) => (i > 2 ? Math.random() * range - range / 2 : null)) - -export const SparklineWithMissingDataBeginning = Template.bind({}) -SparklineWithMissingDataBeginning.args = { - datum: createTestDataWMissingBeginning, - description: 'storybook sparkline', - dataTemplate: (d) => `Foo ${d}%`, -} - -const createTestDataWMissingEnding = Array(20) - .fill() - .map((_, i) => (i < 18 ? Math.random() * range - range / 2 : null)) - -export const SparklineWithMissingDataEnding = Template.bind({}) -SparklineWithMissingDataEnding.args = { - datum: createTestDataWMissingEnding, - description: 'storybook sparkline', - dataTemplate: (d) => `Foo ${d}%`, -} - -const createTestDataComplex = Array(10) - .fill() - .map((_) => ({ value: Math.random() * range - range / 2, foo: 'bar' })) - -export const SparklineWithComplexData = Template.bind({}) -SparklineWithComplexData.args = { - datum: createTestDataComplex, - select: (d) => d?.value, - description: 'storybook sparkline', - dataTemplate: (d) => `Foo ${d}%`, -} - -export const SparklineCustomLineWidth = Template.bind({}) -SparklineCustomLineWidth.args = { - datum: createTestData, - description: 'storybook sparkline', - dataTemplate: (d) => `Foo ${d}%`, - lineSize: 2, -} - -export const ManySparklines = ManyTemplate.bind({}) -ManySparklines.args = { - description: 'storybook sparkline', - dataTemplate: (d) => `${d}%`, -} - -export default { - title: 'Components/Sparkline', - component: Sparkline, - parameters: {}, -} diff --git a/src/ui/Sparkline/Sparkline.stories.tsx b/src/ui/Sparkline/Sparkline.stories.tsx new file mode 100644 index 0000000000..b0148f4e53 --- /dev/null +++ b/src/ui/Sparkline/Sparkline.stories.tsx @@ -0,0 +1,135 @@ +import { Meta, StoryObj } from '@storybook/react' + +import Sparkline, { SparklineProps } from './Sparkline' + +export default { + title: 'Components/Sparkline', + component: Sparkline, +} as Meta + +const renderTemplate = (args: SparklineProps) => ( +
+ {/* Sparkline conforms to the width and height of it's parent. */} + +
+) + +const renderManyTemplate = (args: SparklineProps) => { + const range = 200 + const largeDataSetWithReusedData = Array(50) + .fill(0) + .map(() => Math.random() * range) + return ( + <> + {Array(50) + .fill(0) + .map((_, i) => { + return ( +
+ +
+ ) + })} + + ) +} + +type Story = StoryObj +const range = 200 +const createTestData = Array(20) + .fill(0) + .map(() => Math.random() * range - range / 2) + +export const NormalSparkline: Story = { + args: { + datum: createTestData, + description: 'storybook sparkline', + dataTemplate: (d) => `Foo ${d}%`, + }, + render: (args) => { + return renderTemplate(args) + }, +} + +const createTestDataWithMissingValues = Array(30) + .fill(0) + .map(() => (Math.random() > 0.4 ? Math.random() * range - range / 2 : null)) + +export const SparklineWithMissingData: Story = { + args: { + datum: createTestDataWithMissingValues, + description: 'storybook sparkline', + dataTemplate: (d) => `Foo ${d}%`, + }, + render: (args) => { + return renderTemplate(args) + }, +} + +const createTestDataWithMissingValuesBeginning = Array(7) + .fill(0) + .map((_, i) => (i > 2 ? Math.random() * range - range / 2 : null)) + +export const SparklineWithMissingDataBeginning: Story = { + args: { + datum: createTestDataWithMissingValuesBeginning, + description: 'storybook sparkline', + dataTemplate: (d) => `Foo ${d}%`, + }, + render: (args) => { + return renderTemplate(args) + }, +} + +const createTestDataWithMissingValuesEnding = Array(20) + .fill(0) + .map((_, i) => (i < 18 ? Math.random() * range - range / 2 : null)) + +export const SparklineWithMissingDataEnding: Story = { + args: { + datum: createTestDataWithMissingValuesEnding, + description: 'storybook sparkline', + dataTemplate: (d) => `Foo ${d}%`, + }, + render: (args) => { + return renderTemplate(args) + }, +} + +const createTestDataComplex = Array(10) + .fill(0) + .map((_) => ({ value: Math.random() * range - range / 2, foo: 'bar' })) + +export const SparklineWithComplexData: Story = { + args: { + datum: createTestDataComplex, + select: (d) => d?.value, + description: 'storybook sparkline', + dataTemplate: (d) => `Foo ${d}%`, + }, + render: (args) => { + return renderTemplate(args) + }, +} + +export const SparklineCustomLineWidth: Story = { + args: { + datum: createTestData, + description: 'storybook sparkline', + dataTemplate: (d) => `Foo ${d}%`, + lineSize: 2, + }, + render: (args) => { + return renderTemplate(args) + }, +} + +export const ManySparklines: Story = { + args: { + description: 'storybook sparkline', + dataTemplate: (d) => `${d}%`, + }, + render: (args) => { + return renderManyTemplate(args) + }, +} diff --git a/src/ui/Sparkline/Sparkline.jsx b/src/ui/Sparkline/Sparkline.tsx similarity index 56% rename from src/ui/Sparkline/Sparkline.jsx rename to src/ui/Sparkline/Sparkline.tsx index d3ef2abeff..ca5c6039f3 100644 --- a/src/ui/Sparkline/Sparkline.jsx +++ b/src/ui/Sparkline/Sparkline.tsx @@ -2,24 +2,39 @@ import { extent } from 'd3-array' import { scaleLinear } from 'd3-scale' import isFinite from 'lodash/isFinite' import uniqueId from 'lodash/uniqueId' -import PropTypes from 'prop-types' import { useMemo } from 'react' import './sparkline.css' const HORIZONTAL_PADDING = 10 const FALLBACK_LINE_POS = 0.5 // Value between 0-1 +type NumberOrNullOrUndefined = number | null | undefined -const Sparkline = ({ +interface SparklineData { + value: NumberOrNullOrUndefined + start: NumberOrNullOrUndefined + end: NumberOrNullOrUndefined + mode: 'empty' | 'normal' +} + +export interface SparklineProps { + datum: any[] + description: string + dataTemplate: (value: NumberOrNullOrUndefined) => string + select?: (data: any) => NumberOrNullOrUndefined + lineSize?: number +} + +const Sparkline: React.FC = ({ datum, description, dataTemplate, select = (data) => data, lineSize = 1, }) => { - const data = useMemo( + const data: SparklineData[] = useMemo( () => - datum.reduce((prev, curr, index) => { + datum.reduce((prev, curr, index) => { const nextEntry = datum[index + 1] const previousPoint = prev[prev.length - 1] @@ -27,7 +42,7 @@ const Sparkline = ({ ...prev, { /* - Save the data points original selected value + Save the data point's original selected value */ value: select(curr), /* @@ -37,10 +52,10 @@ const Sparkline = ({ */ start: select(curr) ? select(curr) : previousPoint?.end, /* - End is the next entire's value. + End is the next entry's value. Used to draw a line from point a to b */ - end: select(nextEntry), + end: nextEntry ? select(nextEntry) : nextEntry, /* Sets the rendering mode of the line. */ @@ -50,25 +65,47 @@ const Sparkline = ({ }, []), [datum, select] ) - const [lowerDomain, upperDomain] = extent(data.map(({ value }) => value)) - const yPadding = upperDomain / HORIZONTAL_PADDING - const yScale = scaleLinear() - .domain([lowerDomain - yPadding, upperDomain + yPadding]) - .range([0, 1]) + + let yPadding + let yScale: (num: number) => number = (num) => num + const numericData = data + .map(({ value }) => value) + .filter((val) => typeof val === 'number') as number[] + const [lowerDomain, upperDomain] = extent(numericData) + + if (upperDomain && lowerDomain) { + yPadding = upperDomain / HORIZONTAL_PADDING + yScale = scaleLinear() + .domain([lowerDomain - yPadding, upperDomain + yPadding]) + .range([0, 1]) + } + + interface TableCustomCSSProperties extends React.CSSProperties { + '--line-width': string + '--start': string + '--size': string + } const tableCssProperties = { '--line-width': `${lineSize}px`, } return ( - +
{data.map(({ start, end, mode, value }) => { // Inline styles are not performant but because this is memoized it should be ok. const properties = { - '--start': start ? yScale(start).toFixed(2) : FALLBACK_LINE_POS, - '--size': end ? yScale(end).toFixed(2) : FALLBACK_LINE_POS, + '--start': start + ? yScale(start).toFixed(2) + : FALLBACK_LINE_POS.toString(), + '--size': end + ? yScale(end).toFixed(2) + : FALLBACK_LINE_POS.toString(), } return (
{description}
{dataTemplate(value)} @@ -90,12 +127,4 @@ const Sparkline = ({ ) } -Sparkline.propTypes = { - datum: PropTypes.array, - select: PropTypes.func, - description: PropTypes.string.isRequired, - dataTemplate: PropTypes.func.isRequired, - lineSize: PropTypes.number, -} - export default Sparkline