Skip to content

Instantly share code, notes, and snippets.

@samselikoff
Last active May 2, 2024 06:02
Show Gist options
  • Save samselikoff/4aff333f7c8538bb44f1806931c39be5 to your computer and use it in GitHub Desktop.
Save samselikoff/4aff333f7c8538bb44f1806931c39be5 to your computer and use it in GitHub Desktop.
Diff from "Building an Animated Line Chart with d3, React and Framer Motion" https://www.youtube.com/watch?v=kPbRDn5Fg0Y
import * as d3 from "d3";
import {
eachMonthOfInterval,
endOfMonth,
format,
isSameMonth,
parseISO,
startOfMonth,
} from "date-fns";
import useMeasure from "react-use-measure";
import { motion } from "framer-motion";
import estimatedMax from "~/utils/estimated-max";
export default function Chart({ entries }) {
let [ref, bounds] = useMeasure();
if (
!entries
.flatMap((entry) => entry.sets)
.some((set) => set.reps > 0 && set.tracked)
) {
return (
<div className="flex h-full w-full items-center justify-center">
<p className="text-sm italic text-gray-400">
Add a tracked set to see a chart!
</p>
</div>
);
}
let data = [...entries]
.sort((a, b) => (a.date > b.date ? 1 : -1))
.map((entry) => {
let setWithHighestEstimatedMax = entry.sets
.filter((set) => set.reps > 0 && set.tracked)
.sort((a, b) => estimatedMax(b) - estimatedMax(a))[0];
return {
date: parseISO(entry.date),
estimatedMax: setWithHighestEstimatedMax
? estimatedMax(setWithHighestEstimatedMax)
: null,
};
})
.filter((s) => s.estimatedMax);
return (
<div className="relative h-full w-full" ref={ref}>
{bounds.width > 0 && (
<ChartInner data={data} width={bounds.width} height={bounds.height} />
)}
</div>
);
}
function ChartInner({ data, width, height }) {
let margin = {
top: 10,
right: 10,
bottom: 20,
left: 24,
};
let startDay = startOfMonth(data.at(0).date);
let endDay = endOfMonth(data.at(-1).date);
let months = eachMonthOfInterval({ start: startDay, end: endDay });
let xScale = d3
.scaleTime()
.domain([startDay, endDay])
.range([margin.left, width - margin.right]);
let yScale = d3
.scaleLinear()
.domain(d3.extent(data.map((d) => d.estimatedMax)))
.range([height - margin.bottom, margin.top]);
let line = d3
.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.estimatedMax));
let d = line(data);
return (
<>
<svg className="" viewBox={`0 0 ${width} ${height}`}>
{/* X axis */}
{months.map((month, i) => (
<g
key={month}
className="text-gray-400"
transform={`translate(${xScale(month)},0)`}
>
{i % 2 === 1 && (
<rect
width={xScale(endOfMonth(month)) - xScale(month)}
height={height - margin.bottom}
fill="currentColor"
className="text-gray-100"
/>
)}
<text
x={(xScale(endOfMonth(month)) - xScale(month)) / 2}
y={height - 5}
textAnchor="middle"
fill="currentColor"
className="text-[10px]"
>
{format(month, "MMM")}
</text>
</g>
))}
{/* Y axis */}
{yScale.ticks(5).map((max) => (
<g
transform={`translate(0,${yScale(max)})`}
className="text-gray-400"
key={max}
>
<line
x1={margin.left}
x2={width - margin.right}
stroke="currentColor"
strokeDasharray="1,3"
/>
<text
alignmentBaseline="middle"
className="text-[10px]"
fill="currentColor"
>
{max}
</text>
</g>
))}
{/* Line */}
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.5, type: "spring" }}
d={d}
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
{/* Circles */}
{data.map((d, i) => (
<motion.circle
key={d.date}
r="5"
cx={xScale(d.date)}
cy={yScale(d.estimatedMax)}
fill="currentColor"
strokeWidth={2}
stroke={
months.findIndex((m) => isSameMonth(m, d.date)) % 2 === 1
? "#f5f5f4"
: "white"
}
/>
))}
</svg>
</>
);
}
@RZB1414
Copy link

RZB1414 commented Feb 14, 2024

Hello, great video on YouTube. One question, what is this utils file? ~/utils/estimated-max , I'm still studying React and haven't been able to figure out what it is. Thanks.

@samselikoff
Copy link
Author

Oh should have in-lined that – here it is, you can just replace the import with this:

export default function estimatedMax(set) {
  if (+set.reps <= 0) {
    return 0;
  }

  // Jim Wendler formula
  return +set.weight * +set.reps * 0.0333 + +set.weight;
}

It's specific to the project, it's not a React thing btw :) Lemme know if that makes sense!

@djose9
Copy link

djose9 commented Apr 8, 2024

i tried replacing it and this is the error im getting

TypeError
Cannot assign to read only property 'message' of object 'SyntaxError: /src/index.js: Only one default export allowed per module. (21:0)

19 | }
20 |

21 | export default function Chart({ entries }) {
| ^
22 | let [ref, bounds] = useMeasure();
23 |
24 | if ('

@djose9
Copy link

djose9 commented Apr 8, 2024

this is the original error before replacing it :

ModuleNotFoundError
Could not find module in path: '~/utils/estimated-max' relative to '/src/index.js'
▶ 3 stack frames were collapsed.
$csb$eval
/src/index.js:12
9 | } from "date-fns";
10 | import useMeasure from "react-use-measure";
11 | import { motion } from "framer-motion";

12 | import estimatedMax from "~/utils/estimated-max";
13 |
14 | export default function Chart({ entries }) {
15 | let [ref, bounds] = useMeasure();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment