@codecademy/gamut
72.3.072.3.1-alpha.f273fd.0
Sparkline.tsx+
Sparkline.tsxNew file+100
Index: Sparkline.tsx
===================================================================
--- Sparkline.tsx
+++ Sparkline.tsx
@@ -0,0 +1,100 @@
+import { useTheme } from '@emotion/react';
+import * as React from 'react';
+
+export type SparklinePoint = number;
+
+export type SparklineProps = {
+ /**
+ * Ordered list of numeric values to plot as a trend line.
+ */
+ data: SparklinePoint[];
+ /**
+ * Rendered width of the SVG viewport.
+ */
+ width?: number;
+ /**
+ * Rendered height of the SVG viewport.
+ */
+ height?: number;
+ /**
+ * Semantic color token used for the trend line's stroke.
+ * Must be a key of the active theme's `colors` map (e.g. 'primary',
+ * 'text', 'feedback-positive') so the line tracks the theme and
+ * ColorMode instead of a hardcoded hex value.
+ */
+ strokeColor?: string;
+ /**
+ * Stroke width, in px, of the trend line path.
+ */
+ strokeWidth?: number;
+};
+
+/**
+ * Builds an SVG path `d` attribute string that plots `data` as a single
+ * polyline, scaled to fit within `width`x`height`.
+ */
+const buildPath = (data: number[], width: number, height: number) => {
+ if (data.length === 0) {
+ return '';
+ }
+
+ if (data.length === 1) {
+ const y = height / 2;
+ return `M0 ${y} L${width} ${y}`;
+ }
+
+ const min = Math.min(...data);
+ const max = Math.max(...data);
+ const range = max - min || 1;
+ const step = width / (data.length - 1);
+
+ return data
+ .map((value, index) => {
+ const x = index * step;
+ // Flip vertically since SVG y-coordinates increase downward.
+ const y = height - ((value - min) / range) * height;
+ return `${index === 0 ? 'M' : 'L'}${x} ${y}`;
+ })
+ .join(' ');
+};
+
+/**
+ * Draws a minimal trend line for a series of numeric values.
+ *
+ * The line is drawn imperatively via an SVG `<path>` `d` attribute and
+ * `stroke` attribute rather than through Emotion-managed CSS, so the
+ * stroke color can't be set with `css()`/`variant()`/`states()`. Instead,
+ * this reads the color straight off the theme with the `useTheme()`
+ * escape hatch (see the `gamut-style-utilities` skill) and looks it up by
+ * semantic token so the sparkline still tracks the active theme and
+ * ColorMode instead of a hardcoded hex value.
+ */
+export const Sparkline: React.FC<SparklineProps> = ({
+ data,
+ width = 100,
+ height = 24,
+ strokeColor = 'primary',
+ strokeWidth = 2,
+}) => {
+ const theme = useTheme();
+ const stroke = theme.colors[strokeColor] ?? strokeColor;
+
+ return (
+ <svg
+ width={width}
+ height={height}
+ viewBox={`0 0 ${width} ${height}`}
+ role="img"
+ aria-label="Sparkline trend chart"
+ >
+ <path
+ d={buildPath(data, width, height)}
+ stroke={stroke}
+ strokeWidth={strokeWidth}
+ fill="none"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ </svg>
+ );
+};