Skip to content
Open
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
154 changes: 119 additions & 35 deletions src/components/Chart/Axis.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,79 @@
import React, { useMemo } from "react";
import { format } from "d3-format";
import type { ScaleLinear } from "d3-scale";
import { ScaleLinear, ScaleTime } from "d3-scale";

const formatter = format(",");

type Dimensions = {
boundedWidth: string;
boundedHeight: string;
boundedWidth: number;
boundedHeight: number;
};

type AxisSharedProps = {
dimensions: Dimensions;
scale: ScaleLinear<any, any>;
gridLines: boolean;
numberOfTicks: number;
tickFormat?: (value: any, index: number, ticks: any[]) => string;
label?: string;
labelColor?: string;
gridLineStrokeDasharray?: string;
gridLineStrokeDashoffset?: number;
tickLabelRotation?: string;
};

type AxisHorizontalProps = AxisSharedProps;
type AxisHorizontalProps = AxisSharedProps & {
scale: ScaleTime<any, any>;
tickValues?: Date[];
};

type AxisVerticalProps = AxisSharedProps;
type AxisVerticalProps = AxisSharedProps & {
scale: ScaleLinear<any, any>;
position?: "left" | "right";
};

// An ok default value: numberOfTicks = dimensions.boundedWidth / 80
const AxisHorizontal = ({
dimensions,
scale,
gridLines,
numberOfTicks,
tickFormat,
label,
labelColor,
gridLineStrokeDasharray,
gridLineStrokeDashoffset,
tickLabelRotation,
tickValues,
...props
}: AxisHorizontalProps) => {
const ticks = useMemo(() => {
return scale.ticks(numberOfTicks).map((value) => ({
const rawTicks = tickValues || scale.ticks(numberOfTicks);
const uniqueTicksMap = new Map();
rawTicks.forEach(value => {
const key = value instanceof Date ? value.toDateString() : (value as any).toString();
if (!uniqueTicksMap.has(key)) {
uniqueTicksMap.set(key, value);
} else if (value instanceof Date && uniqueTicksMap.get(key).getTime() !== value.getTime()) {
// Handle cases where toDateString() is the same but time is different
uniqueTicksMap.set(key, value);
}
});
return Array.from(uniqueTicksMap.values()).map((value) => ({
value,
xOffset: scale(value),
}));
}, [scale, numberOfTicks]);
}, [scale, numberOfTicks, tickValues]);

return (
<g
transform={`translate(0, ${dimensions.boundedHeight})`}
role="presentation"
{...props}
>
{ticks.map(({ value, xOffset }) => {
{ticks.map(({ value, xOffset }, i) => {
return (
<g
key={value}
key={value.toString()}
transform={`translate(${xOffset}, 0)`}
role="presentation"
>
Expand All @@ -53,25 +82,39 @@ const AxisHorizontal = ({
role="presentation"
style={{
pointerEvents: "none",
stroke: "#E3E3E3",
stroke: gridLines ? labelColor : "#E3E3E3",
strokeDasharray: gridLineStrokeDasharray,
strokeDashoffset: gridLineStrokeDashoffset,
}}
/>

<text
key={value}
key={value.toString()}
aria-hidden="true"
role="presentation"
style={{
textAnchor: "middle",
transform: "translateY(24px)",
textAnchor: tickLabelRotation ? "end" : "middle",
transform: tickLabelRotation ? "translateY(24px) rotate(-45deg)" : "translateY(24px)",
userSelect: "none",
fill: labelColor,
}}
>
{formatter(value)}
{tickFormat ? tickFormat(value, i, ticks) : formatter(value)}
</text>
</g>
);
})}
{label && (
<text
style={{
textAnchor: "middle",
transform: `translate(${dimensions.boundedWidth / 2}px, 40px)`,
fill: labelColor,
}}
>
{label}
</text>
)}
</g>
);
};
Expand All @@ -82,27 +125,46 @@ const AxisVertical = ({
scale,
gridLines,
numberOfTicks,
position = "right",
tickFormat,
label,
labelColor,
gridLineStrokeDasharray,
gridLineStrokeDashoffset,
...props
}: AxisVerticalProps) => {
const [x1, x2] = gridLines === true ? [-dimensions.boundedWidth, 0] : [0, 4];
const [x1, x2] =
gridLines === true
? position === "right"
? [-dimensions.boundedWidth, 0]
: [dimensions.boundedWidth, 0]
: position === "right"
? [0, 4]
: [0, -4];

const ticks = useMemo(() => {
return scale.ticks(numberOfTicks).map((value) => ({
const rawTicks = scale.ticks(numberOfTicks);
const uniqueTicks = Array.from(new Set(rawTicks)).map(Number);
return uniqueTicks.map((value) => ({
value,
yOffset: scale(value),
}));
}, [scale, numberOfTicks]);

const transform =
position === "right"
? `translate(${dimensions.boundedWidth}, 0)`
: `translate(0, 0)`;
const textAnchor = position === "right" ? "start" : "end";
const textTransform =
position === "right" ? "translateX(8px)" : "translateX(-8px)";

return (
<g
transform={`translate(${dimensions.boundedWidth}, 0)`}
role="presentation"
{...props}
>
<g transform={transform} role="presentation" {...props}>
{ticks.map(({ value, yOffset }, i) => {
return (
<g
key={value}
key={value.toString()}
transform={`translate(0, ${yOffset})`}
role="presentation"
>
Expand All @@ -112,26 +174,42 @@ const AxisVertical = ({
role="presentation"
style={{
pointerEvents: "none",
stroke: "#E3E3E3",
stroke: gridLines ? labelColor : "#E3E3E3",
strokeDasharray: gridLineStrokeDasharray,
strokeDashoffset: gridLineStrokeDashoffset,
}}
/>

<text
key={value}
key={value.toString()}
aria-hidden="true"
role="presentation"
style={{
textAnchor: "start",
transform: "translateX(8px)",
textAnchor,
transform: textTransform,
userSelect: "none",
fill: labelColor,
}}
dy="0.32em"
>
{`${formatter(value)}${i === ticks.length - 1 ? " WPM" : ""}`}
{tickFormat ? tickFormat(value, i, ticks) : formatter(value)}
</text>
</g>
);
})}
{label && (
<text
style={{
textAnchor: "middle",
transform: `translate(${position === "right" ? "45px" : "-45px"}, ${
dimensions.boundedHeight / 2
}px) rotate(-90deg)`,
fill: labelColor,
}}
>
{label}
</text>
)}
</g>
);
};
Expand All @@ -144,13 +222,19 @@ const axisComponentsByDimension = {
type AxisProps = {
dimension: "x" | "y";
dimensions: Dimensions;
} & AxisSharedProps;

const Axis = ({ dimension, dimensions, ...props }: AxisProps) => {
const AxisByDimension = axisComponentsByDimension[dimension];
if (!AxisByDimension) return null;

return <AxisByDimension dimensions={dimensions} {...props} />;
} & AxisSharedProps & {
scale: ScaleLinear<any, any> | ScaleTime<any, any>;
position?: "left" | "right";
tickValues?: any[];
};

const Axis = ({ dimension, dimensions, scale, ...props }: AxisProps) => {
if (dimension === "x") {
return <AxisHorizontal dimensions={dimensions} scale={scale as ScaleTime<any, any>} {...props} />;
} else if (dimension === "y") {
return <AxisVertical dimensions={dimensions} scale={scale as ScaleLinear<any, any>} {...props} />;
}
return null;
};

export default Axis;
4 changes: 3 additions & 1 deletion src/components/Chart/Rule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ type Props = {
x2: string | number;
y2: string | number;
stroke: string;
strokeDasharray?: string;
};

const Rule = ({ x1, y1, x2, y2, stroke }: Props) => (
const Rule = ({ x1, y1, x2, y2, stroke, strokeDasharray }: Props) => (
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
fill="none"
stroke={stroke}
strokeDasharray={strokeDasharray}
role="presentation"
/>
);
Expand Down
6 changes: 6 additions & 0 deletions src/components/FinishedSpeedChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ export default function FinishedSpeedChart({ data }: Props) {
scale={yScale}
numberOfTicks={4}
gridLines={true}
tickFormat={(value, index, ticks) => {
if (index === ticks.length - 1) {
return `${format(",")(value)} WPM`;
}
return format(",")(value);
}}
/>
)}
<g
Expand Down
2 changes: 2 additions & 0 deletions src/components/PseudoContentButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Props = {
children: React.ReactNode;
className: string;
dataClipboardTarget?: string;
dataClipboardText?: string;
style?: any;
onClick?: () => void;
};
Expand Down Expand Up @@ -80,6 +81,7 @@ const PseudoContentButton = (props: Props) => {
<button
className={props.className + (clicked ? " fade-out-up" : "")}
data-clipboard-target={props.dataClipboardTarget}
data-clipboard-text={props.dataClipboardText}
onClick={animatedPseudoContent}
style={props.style || {}}
>
Expand Down
Loading