Generate Custom Art with React

Milecia

Art is one of the ways you can express yourself freely. As someone working in tech, it might be hard to find the time to take up painting or drawing when you're trying to constantly stay on top of all the new tools coming out. Luckily for us, there are some creative ways to combine tech and art to make expressive visuals through data visualization.

In this tutorial, we're going to make a React app to generate custom artwork with the d3.js and changing data. This will help us learn more about an advanced JavaScript topic, play with a new library, and make some art in the process.

Set up the React app

Let's start by creating a brand new React TypeScript app with the following command:

1$ npx create-react-app art-generator --template typescript

Since we used npx to create the app, we'll be working with npm commands throughout the rest of the project instead of yarn. If you do prefer yarn, you can create the app with yarn create-react-app art-generator --template typescript.

With all of the files and folders in place for the React app, we can install the packages we need.

1$ npm i d3 @types/d3 html-to-image

We're getting the d3 library installed so we can make the art based on some data we get from the user and we're using html-to-image to save that art to Cloudinary. If you don't have a Cloudinary account, you can sign up for a free one here. You'll need your cloud name and upload preset from your account settings in order to make the API call to upload the art images.

Now that everything is set up and we have the credentials we need to upload images, we can start working on a new component to get some user input to create the images.

Add the generated art component

The first thing we need to do is create a new folder in the src folder called components. Inside the components folder, add a new file called Art.tsx. This is where we will write all of the code for this art functionality. To make sure that this component is rendered, we're going to update the App.tsx file to import this component we are going to make in a bit.

So open your App.tsx file and edit it to look like this:

1// App.tsx
2
3import Art from "./components/Art";
4
5function App() {
6 return <Art />;
7}
8
9export default App;

It trims down a lot of the boilerplate code and imports to the exact thing we need. If you try running the app now, you'll get an error because we haven't made the Art component yet. That's what we are about to make.

The art component

Open the Art.tsx file and add the following imports.

1// Art.tsx
2
3import { useEffect, useRef, useState } from "react";
4import { toPng } from "html-to-image";
5import * as d3 from "d3";

This is all we're going to need to create the artwork from user input. Next, we need to define the type for the user input data since we're building a TypeScript app. Beneath the import statements, add this type declaration.

1// Art.tsx
2...
3
4type ConfigDataType = {
5 color: string,
6 svgHeight: number,
7 svgWidth: number,
8 xMultiplier: number,
9 yMultiplier: number,
10 othWidth: number,
11 data: number[],
12};

These are all of the values we'll get from the user to make their art and they'll be used in d3 as values for attributes. In order to use d3, we need to write a function outside of the actual component to set up what and how d3 will draw elements on the page.

Set up d3.js

Since we know the type of data we expect to be used in d3, let's go ahead and write a function to handle the d3 drawings below the type definition.

1// Art.tsx
2...
3
4function drawArt(configData: ConfigDataType) {
5 const svg = d3
6 .select("#art")
7 .append("svg")
8 .attr("width", configData.svgWidth)
9 .attr("height", configData.svgHeight)
10 .style("margin-left", 100);
11
12 svg
13 .selectAll("rect")
14 .data(configData.data)
15 .enter()
16 .append("rect")
17 .attr("x", (d, i) => i * configData.xMultiplier)
18 .attr("y", (d, i) => 300 - configData.yMultiplier * d)
19 .attr("width", configData.othWidth)
20 .attr("height", (d, i) => d * configData.yMultiplier)
21 .attr("fill", configData.color);
22}

This is a very simple d3 drawing based on the user input. Honestly, d3 has a pretty steep learning curve and I usually have the docs open the entire time I'm working. So let's walk through this code in a little more detail.

First, this function selects an HTML element with the art id and appends an svg HTML element inside it. (We'll make the art element in just a bit when we get to the rendered part of the component.) Then we set the height and width attributes for the svg element based on the user input. After that, we add a little style to the svg to give it a margin on the left.

Now that the svg element is there, we can start adding the art inside of it. We start by preemptively selecting all of the rect elements that will be created in the svg. Then we get the data array the user entered and start appending rect elements with attributes for the x, y, width, height, and fill attributes set based on the user input.

With this function, d3 is ready to draw something for us.

Set up the component functions

We're almost ready to render something on the page when the app runs, but we have to set up a few things for the component to work correctly. Below the drawArt function, add the following code. It looks like a lot, but we'll explain what's going on.

1// Art.tsx
2...
3
4export default function Art() {
5 const d3ContainerRef = useRef();
6 const [configData, setConfigData] = useState<ConfigDataType>({
7 color: "black",
8 svgHeight: 300,
9 svgWidth: 700,
10 xMultiplier: 90,
11 yMultiplier: 15,
12 othWidth: 65,
13 data: [12, 5, 6, 6, 9, 10],
14 });
15
16 useEffect(() => {
17 drawArt(configData);
18 }, [configData]);
19
20 function updateArt(e: any) {
21 e.preventDefault();
22
23 const newConfigs = {
24 color: e.target.color?.value || configData.color,
25 svgHeight: e.target.svgHeight?.value || configData.svgHeight,
26 svgWidth: e.target.svgWidth?.value || configData.svgWidth,
27 xMultiplier: e.target.xMultiplier?.value || configData.xMultiplier,
28 yMultiplier: e.target.yMultiplier?.value || configData.yMultiplier,
29 othWidth: e.target.othWidth?.value || configData.othWidth,
30 data: e.target.data?.value || configData.data,
31 };
32
33 setConfigData(newConfigs);
34 }
35
36 async function submit(e: any) {
37 e.preventDefault();
38
39 if (d3ContainerRef.current === null) {
40 return;
41 }
42
43 // @ts-ignore
44 const dataUrl = await toPng(d3ContainerRef.current, { cacheBust: true });
45
46 const uploadApi = `https://api.cloudinary.com/v1_1/your_cloud_name/image/upload`;
47
48 const formData = new FormData();
49 formData.append("file", dataUrl);
50 formData.append("upload_preset", "your_upload_preset_value");
51
52 await fetch(uploadApi, {
53 method: "POST",
54 body: formData,
55 });
56 }
57}

We start by creating a ref for the art element we targeted in the drawArt function. Then we set the initial state of the user input so that something shows on the screen when the app starts up. Next, you can see that we call the drawArt function each time the configData is changed. This is how we keep appending elements to the svg.

Then we have a couple of helper functions. The updateArt function takes the values from the user input (we'll make the form for this shortly) and calls setConfigData to update the state, which triggers the drawArt function. This will only be called when a button is clicked to prevent unexpected re-renders.

The submit function is what will get called when a user decides that they want to save the image to Cloudinary. We start by keeping the page from refreshing, then we make sure the ref element isn't empty. Next, we capture the image as a PNG and make a variable for the Cloudinary upload API.

After that, we make a new FormData object to hold the values we need to upload an image to Cloudinary. Once the data is ready, we called the fetch method to submit a POST request to the API. That's all of the functions we need for the component so the only thing left is the return statement.

Add the rendered elements

We need a form to get the user input, a couple of buttons, and the ref element. Add this code below all of the component functions we just defined.

1// Art.tsx
2...
3
4// Art()
5...
6
7return (
8 <>
9 <form onSubmit={updateArt}>
10 <div>
11 <label htmlFor="color">Color</label>
12 <input type="text" name="color" onChange={(e) => e.target.value} />
13 </div>
14 <div>
15 <label htmlFor="svgHeight">SVG Height</label>
16 <input
17 type="number"
18 name="svgHeight"
19 onChange={(e) => e.target.value}
20 />
21 </div>
22 <div>
23 <label htmlFor="svgWidth">SVG Width</label>
24 <input
25 type="number"
26 name="svgWidth"
27 onChange={(e) => e.target.value}
28 />
29 </div>
30 <div>
31 <label htmlFor="xMultiplier">X Multiplier</label>
32 <input
33 type="number"
34 name="xMultiplier"
35 onChange={(e) => e.target.value}
36 />
37 </div>
38 <div>
39 <label htmlFor="yMultiplier">Y Multiplier</label>
40 <input
41 type="number"
42 name="yMultiplier"
43 onChange={(e) => e.target.value}
44 />
45 </div>
46 <div>
47 <label htmlFor="othWidth">Other Width</label>
48 <input
49 type="number"
50 name="othWidth"
51 onChange={(e) => e.target.value}
52 />
53 </div>
54 <div>
55 <label htmlFor="data">Some Numbers</label>
56 <input type="text" name="data" onChange={(e) => e.target.value} />
57 </div>
58 <button type="submit">See Art</button>
59 </form>
60 {/* @ts-ignore */}
61 <div id="art" ref={d3ContainerRef}></div>
62 <button type="submit" onClick={submit}>
63 Save picture
64 </button>
65 </>
66 );
67}

We have a form here with several input fields and a button that calls the updateArt method. Then we have the art element we select in the drawArt function. Finally, there's the button to upload the image whenever the user is ready.

Now you can run the app with npm start and you should see something similar to this.

That's it! Now you can play around with the values through the form or you can work on the code and make fancier data visualizations.

Finished code

You can check out the complete code in the art-generator folder of this repo or this CodeSandbox

Conclusion

Yes, there is beauty in a simple bar chart. When is the last time you were actually able to make a simple bar chart for an app? No fancy transitions, no labels, no real scaling, or showing anything meaningful? That's a special kind of freedom and it does give you a chance to think about other things you can do with your data.

Milecia

Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.