(25 \/ 60) * 200 = 83.33333333333334px<\/code><\/pre>\n\n\n\nGetting the Y coordinate for each data point is easier. If we are spacing the points equally across the graph, then we can divide the width of the chart (200px) by the number of values in our data array (5) to get 40px. That means the first value will have an X coordinate of 40px (to leave a margin for a left axis if we want one), and the last value will have an X coordinate of 200px.<\/p>\n\n\n\n
You just got mathed! 🤓<\/p>\n\n\n\n
For now, let\u2019s add inline styles to each of the divs in the list items. Our new HTML becomes this, where the inline styles contain the calculated positioning for each point.<\/p>\n\n\n\n
<figure class=\"css-chart\">\n\u00a0 <ul class=\"line-chart\">\n\u00a0 \u00a0 <li>\n\u00a0 \u00a0 \u00a0 <div class=\"data-point\" data-value=\"25\" style=\"bottom: 83.33333333333334px; left: 40px;\"><\/div>\n\u00a0 \u00a0 <\/li>\n\u00a0 \u00a0 <li>\n\u00a0 \u00a0 \u00a0 <div class=\"data-point\" data-value=\"60\" style=\"bottom: 200px; left: 80px;\"><\/div>\n\u00a0 \u00a0 <\/li>\n\u00a0 \u00a0 <li>\n\u00a0 \u00a0 \u00a0 <div class=\"data-point\" data-value=\"45\" style=\"bottom: 150px; left: 120px;\"><\/div>\n\u00a0 \u00a0 <\/li>\n\u00a0 \u00a0 <li>\n\u00a0 \u00a0 \u00a0 <div class=\"data-point\" data-value=\"50\" style=\"bottom: 166.66666666666669pxpx; left: 160px;\"><\/div>\n\u00a0 \u00a0 <\/li>\n\u00a0 \u00a0 <li>\n\u00a0 \u00a0 \u00a0 <div class=\"data-point\" data-value=\"40\" style=\"bottom: 133.33333333333331px; left: 200px;\"><\/div>\n\u00a0 \u00a0 <\/li>\n\u00a0 <\/ul>\n<\/figure><\/code><\/pre>\n\n\n\nWe have a chart!<\/figcaption><\/figure>\n\n\n\nHey, that looks a lot better! But even though you can see where this is going, you still can\u2019t really call this a line graph. No problem. We only need to use a little<\/em> more math to finish our game of connect-the-dots. Take a look at the picture of our rendered data points again. Can you see the triangles that connect them? If not, maybe this next picture will help:<\/p>\n\n\n\n <\/figure>\n\n\n\nWhy is that important? Shhh, the answer\u2019s coming up next.<\/p>\n\n\n
Rendering line segments<\/h3>\n\n\n See the triangles now? And they\u2019re not just any old triangles. They\u2019re the best kind of triangles (for our purposes anyway) because they are right<\/strong> triangles! When we calculated the Y coordinates of our data points earlier, we were also calculating the length of one leg of our right triangle (i.e. the \u201crun\u201d if you think of it as a stair step). If we calculate the difference in the X coordinate from one point to the next, that will tell us the length of another side of our right triangle (i.e. the \u201crise\u201d of a stair step). And with those two pieces of information, we can calculate the length of the magical hypotenuse which, as it turns out, is exactl<\/em>y what we need to draw to the screen in order to connect our dots and make a real line chart.<\/p>\n\n\n\nFor example, let\u2019s take the second and third points on the chart.<\/p>\n\n\n\n
<!-- ... -->\u00a0\n\u2028\n<li>\n\u00a0 <div class=\"data-point\" data-value=\"60\" style=\"bottom: 200px; left: 80px;\"><\/div>\n<\/li>\n<li>\n\u00a0 <div class=\"data-point\" data-value=\"45\" style=\"bottom: 150px; left: 120px;\"><\/div>\n<\/li>\n\u2028\n<!-- ... --><\/code><\/pre>\n\n\n\nThe second data point has a Y value of 200 and the third data point has a Y value of 150, so the opposite side of the triangle connecting them has a length of 200 minus 150, or 50. It has an adjacent side that is 40 pixels long (the amount of spacing we put between each of our points).<\/p>\n\n\n\n
That means the length of the hypotenuse is the square root of 50 squared plus 40 squared, or 64.03124237432849.<\/p>\n\n\n\nThe hypotenuse is what we need to draw our line graph<\/figcaption><\/figure>\n\n\n\nLet\u2019s create another div inside of each list item in the chart that will serve as the hypotenuse of a triangle drawn from that point. Then we’ll set an inline custom property on our new div that contains the length of that hypotenuse.<\/p>\n\n\n\n
<!-- ... -->\n\u2028\n<li>\n\u00a0 <div class=\"data-point\" data-value=\"60\"><\/div>\n\u00a0 <div class=\"line-segment\" style=\"--hypotenuse: 64.03124237432849;\"><\/div>\n<\/li>\n\u2028\n<!-- ... --><\/code><\/pre>\n\n\n\nWhile we\u2019re at it, our line segments are going to need to know their proper X and Y coordinates, so let\u2019s remove the inline styles from our .data-point<\/code> elements and add CSS custom properties to their parent (the <li><\/code> element) instead. Let\u2019s call these properties, creatively, --x<\/code> and --y<\/code>. Our data points don\u2019t need to know about the hypotenuse (the length of our line segment), so we can add a CSS custom property for the length of the hypotenuse directly to our .line-segment<\/code>. So now our HTML will look like this:<\/p>\n\n\n\n<!-- ... -->\n\u2028\n<li style=\"--y: 200px; --x: 80px\">\n\u00a0 <div class=\"data-point\" data-value=\"60\"><\/div>\n\u00a0 <div class=\"line-segment\" style=\"--hypotenuse: 64.03124237432849;\"><\/div>\n<\/li>\n\u2028\n<!-- ... --><\/code><\/pre>\n\n\n\nWe\u2019ll need to update our CSS to position the data points with those new custom properties and style up the new .line-segment<\/code> div we added to the markup:<\/p>\n\n\n\n.data-point {\n\u00a0 \/* Same as before *\/\n\u2028\n\u00a0 bottom: var(--y);\n\u00a0 left: var(--x);\n}\n\u2028\n.line-segment {\n\u00a0 background-color: blue;\n\u00a0 bottom: var(--y);\n\u00a0 height: 3px;\n\u00a0 left: var(--x);\n\u00a0 position: absolute;\n\u00a0 width: calc(var(--hypotenuse) * 1px);\n}<\/code><\/pre>\n\n\n\nWe have all of the parts now. They just don\u2019t fit together correctly yet.<\/figcaption><\/figure>\n\n\n\nWell, we have line segments now but this isn\u2019t at all what we want. To get a functional line chart, we need to apply a transformation. But first, let\u2019s fix a couple of things.<\/p>\n\n\n\n
First off, our line segments line up with the bottom of our data points, but we want the origin of the line segments to be the center of the data point circles. We can fix that with a quick CSS change to our .data-point<\/code> styles. We need to adjust their X and Y position to account for both the size of the data point and its border as well as the width of the line segment.<\/p>\n\n\n\n.data-point {\n\u00a0 \/* ... *\/\n\u2028\n\u00a0 \/* The data points have a radius of 8px and the line segment has a width of 3px,\u00a0\n\u00a0 \u00a0 so we split the difference to center the data points on the line segment origins *\/\n\u00a0 bottom: calc(var(--y) - 6.5px);\n\u00a0 left: calc(var(--x) - 9.5px);\n}<\/code><\/pre>\n\n\n\nSecondly, our line segments are being rendered on top of the data points instead of behind them. We can address that by putting the line segment first in our HTML:<\/p>\n\n\n\n
<!-- ... -->\n\u2028\n<li style=\"--y: 200px; --x: 80px\">\n\u00a0 \u00a0 <div class=\"line-segment\" style=\"--hypotenuse: 64.03124237432849;\"><\/div>\n\u00a0 \u00a0 <div class=\"data-point\" data-value=\"60\"><\/div>\n<\/li>\n\u2028\n<!-- ... --><\/code><\/pre>\n\n\nWe\u2019ve almost got it now. We just need to do one last bit of math. Specifically, we need to find the measure of the angle that faces the opposite side of our right triangle and then rotate our line segment by that same number of degrees.<\/p>\n\n\n\n
How do we do that? Trigonometry! You may recall the little mnemonic trick to remember how sine, cosine and tangent are calculated:<\/p>\n\n\n\n
SOH (Sine = Opposite over Hypotenuse<\/li> CAH (Cosine = Adjacent over Hypotenuse)<\/li> TOA (Tangent = Opposite over Adjacent)<\/li><\/ul>\n\n\n\nYou can use any of them because we know the length of all three sides of our right triangle. I picked sine, so that that leaves us with this equation:<\/p>\n\n\n\n
sin(x) = Opposite \/ Hypotenuse<\/code><\/pre>\n\n\n\nThe answer to that equation will tell us how to rotate each line segment to have it connect to the next data point. We can quickly do this in JavaScript using Math.asin(Opposite \/ Hypotenuse)<\/code>. It will give us the answer in radians though, so we\u2019ll need to multiply the result by (180 \/ Math.PI)<\/code>.<\/p>\n\n\n\nUsing the example of our second data point from earlier, we already worked out that the opposite side has a length of 50 and the hypotenuse has a length of 64.03124237432849, so we can re-write our equation like this:<\/p>\n\n\n\n
sin(x) = 50 \/ 64.03124237432849 = 51.34019174590991<\/code><\/pre>\n\n\n\nThat\u2019s the angle we\u2019re looking for! We need to solve that equation for each of our data points and then pass the value as a CSS custom property on our .line-segment<\/code> elements. That will give us HTML that looks like this:<\/p>\n\n\n\n<!-- ... -->\n\u2028\n<li style=\"--y: 200px; --x: 80px\">\n\u00a0 <div class=\"data-point\" data-value=\"60\"><\/div>\n\u00a0 <div class=\"line-segment\" style=\"--hypotenuse: 64.03124237432849; --angle: 51.34019174590991;\"><\/div>\n<\/li>\n\u2028\n<!-- ... --><\/code><\/pre>\n\n\n\nAnd here\u2019s where we can apply those properties in the CSS:<\/p>\n\n\n\n
.line-segment {\n\u00a0 \/* ... *\/\n\u00a0 transform: rotate(calc(var(--angle) * 1deg));\n\u00a0 width: calc(var(--hypotenuse) * 1px);\n}<\/code><\/pre>\n\n\n\nNow when we render that, we have our line segments!<\/p>\n\n\n\nWell, the line segments are definitely rotated. We need one more step.<\/figcaption><\/figure>\n\n\n\nWait, what? Our line segments are all over the place. What now? Oh, right. By default, transform: rotate()<\/code> rotates around the center of the transformed element. We want the rotation to occur from the bottom-left corner to angle away from our current data point to the next one. That means we need to set one more CSS property on our .line-segment<\/code> class.<\/p>\n\n\n\n.line-segment {\n \/* ... *\/\n transform: rotate(calc(var(--angle) * 1deg));\n transform-origin: left bottom;\n width: calc(var(--hypotenuse) * 1px);\n}<\/code><\/pre>\n\n\n\nAnd, now when we render it, we finally get the CSS-only line graph we\u2019ve been waiting for.<\/p>\n\n\n\nAt last! A line graph!<\/figcaption><\/figure>\n\n\n\nImportant note:<\/strong> When you calculate the value of the opposite side (the \u201crise\u201d), make sure it\u2019s calculated as the \u201cY position of the current data point\u201d minus the \u201cY position of the next data point.\u201d That will result in a negative value when the next data point is a larger value (higher up on the graph) than the current data point which will result in a negative rotation. That\u2019s how we ensure the line slopes upwards.<\/p>\n\n\nWhen to use this kind of chart<\/h3>\n\n\n This approach is great for a simple static site or for a dynamic site that uses server-side generated content. Of course, it can also be used on a site with client-side dynamically generated content, but then you are back to running JavaScript on the client. The CodePen at the top of this post shows an example of client-side dynamic generation of this line chart.<\/p>\n\n\n\n
The CSS calc()<\/code> function is highly useful, but it can\u2019t calculate sine, cosine, and tangent for us. That means you\u2019d have to either calculate your values by hand or write a quick function (client-side or server-side) to generate the needed values (X, Y, hypotenuse and angle) for our CSS custom properties.<\/p>\n\n\n\nI know some of you got through this and will feel like it\u2019s not vanilla CSS if it requires a script to calculate the values \u2014 and that\u2019s fair. The point is that all of the chart rendering is done in CSS. The data points and the lines that connect them are all done with HTML elements and CSS that works beautifully, even in a statically rendered environment with no JavaScript enabled. And perhaps more importantly, there\u2019s no need to download yet another bloated library just to render a simple line graph on your page.<\/p>\n\n\n
Potential improvements<\/h3>\n\n\n As with anything, there\u2019s always something we can do to take things to the next level. In this case, I think there are three areas where this approach could be improved.<\/p>\n\n\n
Responsiveness<\/h4>\n\n\n The approach I\u2019ve outlined uses a fixed size for the chart dimensions, which is exactly what we don\u2019t want in a responsive design. We can work around this limitation if we can run JavaScript on the client. Instead of hard-coding our chart size, we can set a CSS custom property (remember our --widget-size<\/code> property?), base all of the calculations on it, and update that property when the container or window either initially displays or resizes using some form of a container query or a window resize listener.<\/p>\n\n\n