{"id":27852,"date":"2022-03-23T23:00:16","date_gmt":"2022-03-23T23:00:16","guid":{"rendered":"http:\/\/Making-a-Picture-Heatmap-in-Redwood"},"modified":"2025-03-02T07:39:52","modified_gmt":"2025-03-02T15:39:52","slug":"making-a-picture-heatmap-in-redwood","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/","title":{"rendered":"Making a Picture Heatmap"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Sometimes we want to focus on specific parts of an image to figure out what\u2019s going on. This might involve looking at different quadrants of an image to see what the special features are. That can be done with machine learning or user input.<\/p>\n<p>In this post, we\u2019re going to look at different locations on multiple images to take a guess at where certain information or image details should be displayed. We\u2019ll save snapshots of the heatmap and upload them to Cloudinary and save a reference to them in a Postgres database with a Redwood app.<\/p>\n<h2>Some initial setup<\/h2>\n<p>A couple of things you\u2019ll need to have in place are:<\/p>\n<ul>\n<li>\n<a href=\"https:\/\/cloudinary.com\/users\/register\/free\">a Cloudinary account<\/a>\n<\/li>\n<li>\n<a href=\"https:\/\/www.postgresql.org\/download\/\">a local instance of Postgres<\/a>\n<\/li>\n<\/ul>\n<p>This covers all of the things we need outside of our Redwood app. Now we can open a terminal and create a new app.<\/p>\n<h2>Creating the Redwood app<\/h2>\n<p>In the terminal, run this command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn create redwood-app --typescript image-heatmap\n<\/code><\/span><\/pre>\n<p><em>Note: If you don\u2019t want to work with TypeScript, feel free to leave the <code>--typescript<\/code> flag out of the command.<\/em><\/p>\n<p>This will generate a lot of code for you. The main two folders we\u2019ll be working in are <code>web<\/code> and <code>api<\/code>. The <code>web<\/code> folder is where we\u2019ll write all of the React code for the front-end. The <code>api<\/code> directory is where we\u2019ll define our database schema and write a back-end using GraphQL.<\/p>\n<p>Let\u2019s start working in the back-end.<\/p>\n<h2>Building the database schema<\/h2>\n<p>Redwood uses <a href=\"https:\/\/www.prisma.io\/\">Prisma<\/a> as the ORM to handle database operations. In the <code>api &gt; db<\/code> folder, you\u2019ll see a <code>schema.prisma<\/code> file. This is where we\u2019ll define the database connection and the models that make up our tables and relations.<\/p>\n<p>The first thing we can do is change the <code>provider<\/code> value to <code>postgresql<\/code> from <code>sqlite<\/code>.<\/p>\n<p>Then you\u2019ll notice that the <code>url<\/code> value is coming from the <code>DATABASE_URL<\/code> while is an environment variable. So you\u2019ll need to open the <code>.env<\/code> file and uncomment this line and update it with your local connection string. That may look similar to this:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">DATABASE_URL=postgres:<span class=\"hljs-comment\">\/\/postgres:admin@localhost:5432\/heatmap<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now you can delete the example model that\u2019s in the <code>schema.prisma<\/code> and replace it with our real models below:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">model Image {\n  id      Int     @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  name    <span class=\"hljs-built_in\">String<\/span>\n  heatmap Heatmap&#91;]\n}\n\nmodel Heatmap {\n  id            <span class=\"hljs-built_in\">String<\/span> @id @<span class=\"hljs-keyword\">default<\/span>(uuid())\n  originalImage Image @relation(fields: &#91;imageId], <span class=\"hljs-attr\">references<\/span>: &#91;id])\n  imageId       Int\n  heatmapImage  <span class=\"hljs-built_in\">String<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This means that an image can have multiple heatmaps associated with it, but each heatmap can only be associated with one image. It\u2019ll help us keep track of which images we\u2019re working with when we look at reports using this data.<\/p>\n<p>Now that we have the models ready, we can run a migration on our database with the following command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood prisma migrate dev\n<\/code><\/span><\/pre>\n<p>This creates a new database and a couple of tables in our local Postgres instance. Now there\u2019s a really cool thing we can do to add images we want to work with.<\/p>\n<h2>Scaffolding with Redwood<\/h2>\n<p>Since we have our models in place, the Redwood CLI has a command that will generate all of the CRUD for us on both the front-end and back-end. In your terminal, run:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood generate scaffold image\n<\/code><\/span><\/pre>\n<p>This will create a lot of new files in different locations for us, but we\u2019ll have a fully functional front-end and back-end! This is a good time to start the app and take a look at what we have so far.<\/p>\n<p>In your terminal, run this command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood dev\n<\/code><\/span><\/pre>\n<p>This will open your browser to a page like this.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1638979018\/e-603fc55d218a650069f5228b\/mbtho227zreverg6ajvz.png\" alt=\"Redwood running in the browser with links to scaffold-generated pages\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1132\"\/><\/p>\n<p>If you go to the <code>\/images\/new<\/code> page, you\u2019ll see where you can add the name of the image to our database.<\/p>\n<p>Since we have everything set up for our images, let\u2019s get things ready to handle the heatmaps.<\/p>\n<h2>Adding more to the back-end<\/h2>\n<p>The front-end for the heatmap input will be pretty different from how we\u2019re handling the image data, so we\u2019ll build the back-end separately. We can still take advantage of the Redwood CLI to generate the back-end CRUD. Run the following command in your terminal:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood generate sdl --crud heatmap\n<\/code><\/span><\/pre>\n<p>Now if you take a look in <code>api &gt; src &gt; graphql<\/code>, you\u2019ll find all of the types for the heatmap queries and mutations. Then if you go over to <code>api &gt; src &gt; services<\/code>, you\u2019ll see the folder that has several files for the heatmap resolvers. There are a couple of test-related files and then the main file with the resolvers.<\/p>\n<p>If you take a look inside <code>heatmap.ts<\/code>, you\u2019ll see all of the resolvers to handle getting data and updating data. We don\u2019t need to make any changes here. All of our work will be on the front-end now.<\/p>\n<h2>Adding a new package to the front-end<\/h2>\n<p>Before we write the code, let\u2019s install a package we\u2019ll need to take the heatmap image straight from the browser. In the terminal, go to the <code>web<\/code> directory and run the following command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn add html-to-image\n<\/code><\/span><\/pre>\n<p>This is a really nice package that converts HTML elements into images. Now let\u2019s make the page for our heatmaps.<\/p>\n<h2>Adding the page to make heatmaps<\/h2>\n<p>In the terminal, go to the root of the project again and run this command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood generate page heatmap\n<\/code><\/span><\/pre>\n<p>This will create a new directory in <code>web &gt; src &gt; pages<\/code> called <code>HeatmapPage<\/code>. This also updates the <code>Routes.tsx<\/code> to include this new route in the app. Let\u2019s take a look in <code>web &gt; src &gt; pages &gt; HeatmapPage<\/code>.<\/p>\n<p>There are three files: one containing a Storybook story, one with a Jest test ready to run, and the page component itself. Open <code>HeatmapPage.tsx<\/code>. This is where we\u2019ll write all of the remaining code.<\/p>\n<h3>Updating the imports<\/h3>\n<p>You can delete all of the existing imports in this file and replace them with the following:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { useQuery, useMutation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useRef, useState } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { toPng } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'html-to-image'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>These are the hooks and methods we\u2019ll work with. Now we have a couple of GraphQL queries to make.<\/p>\n<h3>Setting up the GraphQL queries<\/h3>\n<p>Let\u2019s add these queries below the imports. Add the following code:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> GET_IMAGES = gql<span class=\"hljs-string\">`\n  query {\n    images {\n      id\n      name\n      url\n    }\n  }\n`<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> CREATE_HEATMAP_MUTATION = gql<span class=\"hljs-string\">`\n  mutation CreateHeatmapMutation($input: CreateHeatmapInput!) {\n    createHeatmap(input: $input) {\n      id\n    }\n  }\n`<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The first query will return the id, name, and URL of all the images we have saved in the database. That way we\u2019ll be able to decide what to apply the heatmap to.<\/p>\n<p>The second query is actually the mutation we need to call in order to make a new heatmap record in the database. It only returns the id because we don\u2019t need to do anything after the record is created.<\/p>\n<h3>Setting states in the component<\/h3>\n<p>Now we can start building the inner workings of this <code>HeatmapPage<\/code> component. We\u2019ll add some states and other things we need to make this component function like we want. Inside the <code>HeatmapPage<\/code> component, add the following code:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> &#91;createHeatmap] = useMutation(CREATE_HEATMAP_MUTATION)\n<span class=\"hljs-keyword\">const<\/span> { data, loading } = useQuery(GET_IMAGES)\n\n<span class=\"hljs-keyword\">const<\/span> heatmapRef = useRef(<span class=\"hljs-literal\">null<\/span>)\n<span class=\"hljs-keyword\">const<\/span> &#91;image, setImage] = useState({<span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-attr\">url<\/span>: <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${cloudName}<\/span>\/image\/upload\/v1606580778\/3dogs.jpg`<\/span>})\n<span class=\"hljs-keyword\">const<\/span> &#91;bottom, setBottom] = useState&lt;string&gt;(<span class=\"hljs-string\">'0'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> &#91;left, setLeft] = useState&lt;string&gt;(<span class=\"hljs-string\">'0'<\/span>)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The first two lines give us the method for creating new heatmap images and all of the images from our database, including the loading state. Then we set a ref to get the HTML elements that will make up the heatmap.<\/p>\n<p>Lastly, we set a few state variables. The <code>cloudName<\/code> value the <code>url<\/code> should be the cloud name you see in <a href=\"https:\/\/cloudinary.com\/console\/\">your Cloudinary console<\/a>.<\/p>\n<p>With the initial variables in place, let\u2019s add a quick check to see if the data is still loading. Below the state variables, add the following code:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">if<\/span> (loading) {\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>Loading...<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>If the data is still being loaded, we don\u2019t execute any more code. We just return this little loading element. The reason we do this is to prevent the app from crashing when there isn\u2019t data available when it expects.<\/p>\n<h3>The create\/upload function<\/h3>\n<p>Now that we have a good data check in place, let\u2019s start writing the functions we\u2019ll need to upload the heatmap images to Cloudinary and create new records in the local Postgres database. Below the loading check, add the following code:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> uploadHeatmap = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">if<\/span> (heatmapRef.current === <span class=\"hljs-literal\">null<\/span>) {\n    <span class=\"hljs-keyword\">return<\/span>\n  }\n\n  <span class=\"hljs-keyword\">const<\/span> dataUrl = <span class=\"hljs-keyword\">await<\/span> toPng(heatmapRef.current, { <span class=\"hljs-attr\">cacheBust<\/span>: <span class=\"hljs-literal\">true<\/span> })\n\n  <span class=\"hljs-keyword\">const<\/span> uploadApi = <span class=\"hljs-string\">`https:\/\/api.cloudinary.com\/v1_1\/<span class=\"hljs-subst\">${cloudName}<\/span>\/image\/upload`<\/span>\n\n  <span class=\"hljs-keyword\">const<\/span> formData = <span class=\"hljs-keyword\">new<\/span> FormData()\n  formData.append(<span class=\"hljs-string\">'file'<\/span>, dataUrl)\n  formData.append(<span class=\"hljs-string\">'upload_preset'<\/span>, <span class=\"hljs-string\">'lqe6bakr'<\/span>)\n\n  <span class=\"hljs-keyword\">const<\/span> cloudinaryRes = <span class=\"hljs-keyword\">await<\/span> fetch(uploadApi, {\n    <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'POST'<\/span>,\n    <span class=\"hljs-attr\">body<\/span>: formData,\n  })\n\n  <span class=\"hljs-keyword\">const<\/span> input = {\n    <span class=\"hljs-attr\">heatmapImage<\/span>: cloudinaryRes.url,\n    <span class=\"hljs-attr\">imageId<\/span>: image.id,\n  }\n\n  createHeatmap({\n    <span class=\"hljs-attr\">variables<\/span>: { input },\n  })\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>There are a few things going on here. We do a quick check to make sure the heatmap ref isn\u2019t null so that we know we can capture the image. Then we convert that ref to an image using the <code>toPng<\/code> method of that library we added earlier.<\/p>\n<p>Next, we set the URL for the Cloudinary API we need to upload any heatmap image. Remember that you can get the <code>cloudName<\/code> value from your Cloudinary console. Then we create a <code>FormData<\/code> object to hold the heatmap image and our Cloudinary upload preset.<\/p>\n<p>Then we make the call to the API, upload the image, and get the response back. That\u2019s where we get the URL to the heatmap image that gets passed into the <code>createHeatmap<\/code> variables.<\/p>\n<h3>A little helper function<\/h3>\n<p>We\u2019ll be manually placing a heatmap over the images we select through button clicks. That\u2019s why we have the <code>bottom<\/code> and <code>left<\/code> states. These control the location of the heatmap on a given image. So we need a little <code>switch<\/code> statement to update those states correctly.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> setPosition = <span class=\"hljs-function\">(<span class=\"hljs-params\">position<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">switch<\/span>(position) {\n    <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'top-left'<\/span>:\n      setBottom(<span class=\"hljs-string\">'0'<\/span>)\n      setLeft(<span class=\"hljs-string\">'0'<\/span>)\n      <span class=\"hljs-keyword\">break<\/span>\n    <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'top-right'<\/span>:\n      setBottom(<span class=\"hljs-string\">'0'<\/span>)\n      setLeft(<span class=\"hljs-string\">'80%'<\/span>)\n      <span class=\"hljs-keyword\">break<\/span>\n    <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'bottom-left'<\/span>:\n      setBottom(<span class=\"hljs-string\">'-150px'<\/span>)\n      setLeft(<span class=\"hljs-string\">'0'<\/span>)\n      <span class=\"hljs-keyword\">break<\/span>\n    <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'bottom-right'<\/span>:\n      setBottom(<span class=\"hljs-string\">'-150px'<\/span>)\n      setLeft(<span class=\"hljs-string\">'80%'<\/span>)\n      <span class=\"hljs-keyword\">break<\/span>\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>These values are going to be used in CSS, that\u2019s why they might look a bit all over the place. All that\u2019s left now is writing the HTML elements for this component.<\/p>\n<h3>A simple user interface<\/h3>\n<p>There are going to be multiple buttons and a dropdown to help users target, along with an element to display the picture and update where the heatmap is shown.<\/p>\n<p>Add the following code right below that helper function we made:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">return<\/span> (\n  <span class=\"xml\"><span class=\"hljs-tag\">&lt;&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span>&gt;<\/span>HeatmapPage<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">select<\/span> <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{(e)<\/span> =&gt;<\/span> {\n      const {id, url} = JSON.parse(e.target.value)\n      setImage({id: id, url: url})}\n      }\n    &gt;\n      {data.images.map(image =&gt; (\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span>\n          <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{image.id}<\/span>\n          <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{<\/span>`{\"<span class=\"hljs-attr\">id<\/span>\"<span class=\"hljs-attr\">:<\/span> ${<span class=\"hljs-attr\">image.id<\/span>}, \"<span class=\"hljs-attr\">url<\/span>\"<span class=\"hljs-attr\">:<\/span> \"${<span class=\"hljs-attr\">image.url<\/span>}\"}`}\n        &gt;<\/span>\n          {image.name}\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n        ))\n      }\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">select<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{uploadHeatmap}<\/span>&gt;<\/span>Upload Heatmap to Cloudinary<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">display:<\/span> '<span class=\"hljs-attr\">flex<\/span>', <span class=\"hljs-attr\">flexDirection:<\/span> '<span class=\"hljs-attr\">row<\/span>'}}&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('top-left')}&gt;Top-Left<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('top-right')}&gt;Top-Right<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('bottom-left')}&gt;Bottom-Left<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('bottom-right')}&gt;Bottom-Right<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>\n      <span class=\"hljs-attr\">ref<\/span>=<span class=\"hljs-string\">{heatmapRef}<\/span>\n      <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n        <span class=\"hljs-attr\">backgroundImage:<\/span> `<span class=\"hljs-attr\">url<\/span>(${<span class=\"hljs-attr\">image.url<\/span>})`,\n        <span class=\"hljs-attr\">backgroundRepeat:<\/span> '<span class=\"hljs-attr\">no-repeat<\/span>',\n        <span class=\"hljs-attr\">backgroundSize:<\/span> '<span class=\"hljs-attr\">cover<\/span>',\n        <span class=\"hljs-attr\">height:<\/span> <span class=\"hljs-attr\">300<\/span>,\n        <span class=\"hljs-attr\">position:<\/span> '<span class=\"hljs-attr\">absolute<\/span>',\n        <span class=\"hljs-attr\">width:<\/span> '<span class=\"hljs-attr\">100<\/span>%'\n      }}\n    &gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>\n        <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n          <span class=\"hljs-attr\">background:<\/span> '<span class=\"hljs-attr\">radial-gradient<\/span>(<span class=\"hljs-attr\">rgba<\/span>(<span class=\"hljs-attr\">0<\/span>, <span class=\"hljs-attr\">255<\/span>, <span class=\"hljs-attr\">25<\/span>, <span class=\"hljs-attr\">0.5<\/span>), <span class=\"hljs-attr\">rgba<\/span>(<span class=\"hljs-attr\">255<\/span>, <span class=\"hljs-attr\">0<\/span>, <span class=\"hljs-attr\">25<\/span>, <span class=\"hljs-attr\">0.5<\/span>))',\n          <span class=\"hljs-attr\">height:<\/span> <span class=\"hljs-attr\">150<\/span>,\n          <span class=\"hljs-attr\">position:<\/span> '<span class=\"hljs-attr\">relative<\/span>',\n          <span class=\"hljs-attr\">bottom:<\/span> <span class=\"hljs-attr\">bottom<\/span>,\n          <span class=\"hljs-attr\">left:<\/span> <span class=\"hljs-attr\">left<\/span>,\n          <span class=\"hljs-attr\">width:<\/span> <span class=\"hljs-attr\">250<\/span>,\n          <span class=\"hljs-attr\">zIndex:<\/span> <span class=\"hljs-attr\">10<\/span>\n        }}\n      &gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\n)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Let\u2019s walk through what\u2019s happening in this component. The first thing to note is the <code>&lt;select&gt;<\/code> element. This has an <code>onChange<\/code> function that gets the image id and URL from the selected element and updates the corresponding state.<\/p>\n<p>Inside the <code>&lt;select&gt;<\/code> element, we\u2019re using the image data fetched from GraphQL to create the options we can choose from in the dropdown. Right after that, we have a button that triggers the upload to Cloudinary and creates the database record for the heatmap.<\/p>\n<p>Then we have a set of buttons that update the location of the heatmap by updating that state. Finally, we have the image with the heatmap overlayed.<\/p>\n<p>You\u2019ll see where the <code>heatmapRef<\/code> is being used so that we can capture the image. These two <code>&lt;div&gt;<\/code> elements just have some CSS properties that update based on the selected image option and heatmap location a user picks.<\/p>\n<p>Your final code should look like this:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { useQuery, useMutation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useRef, useState } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { toPng } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'html-to-image'<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> GET_IMAGES = gql<span class=\"hljs-string\">`\n  query {\n    images {\n      id\n      name\n      url\n    }\n  }\n`<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> CREATE_HEATMAP_MUTATION = gql<span class=\"hljs-string\">`\n  mutation CreateHeatmapMutation($input: CreateHeatmapInput!) {\n    createHeatmap(input: $input) {\n      id\n    }\n  }\n`<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> HeatmapPage = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> &#91;createHeatmap] = useMutation(CREATE_HEATMAP_MUTATION)\n  <span class=\"hljs-keyword\">const<\/span> { data, loading } = useQuery(GET_IMAGES)\n\n  <span class=\"hljs-keyword\">const<\/span> heatmapRef = useRef(<span class=\"hljs-literal\">null<\/span>)\n  <span class=\"hljs-keyword\">const<\/span> &#91;image, setImage] = useState({<span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-attr\">url<\/span>: <span class=\"hljs-string\">'https:\/\/res.cloudinary.com\/milecia\/image\/upload\/v1606580778\/3dogs.jpg'<\/span>})\n  <span class=\"hljs-keyword\">const<\/span> &#91;bottom, setBottom] = useState&lt;string&gt;(<span class=\"hljs-string\">'0'<\/span>)\n  <span class=\"hljs-keyword\">const<\/span> &#91;left, setLeft] = useState&lt;string&gt;(<span class=\"hljs-string\">'0'<\/span>)\n\n  <span class=\"hljs-keyword\">if<\/span> (loading) {\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>Loading...<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  }\n\n  <span class=\"hljs-keyword\">const<\/span> uploadHeatmap = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">if<\/span> (heatmapRef.current === <span class=\"hljs-literal\">null<\/span>) {\n      <span class=\"hljs-keyword\">return<\/span>\n    }\n\n    <span class=\"hljs-keyword\">const<\/span> dataUrl = <span class=\"hljs-keyword\">await<\/span> toPng(heatmapRef.current, { <span class=\"hljs-attr\">cacheBust<\/span>: <span class=\"hljs-literal\">true<\/span> })\n\n    <span class=\"hljs-keyword\">const<\/span> uploadApi = <span class=\"hljs-string\">`https:\/\/api.cloudinary.com\/v1_1\/milecia\/image\/upload`<\/span>\n\n    <span class=\"hljs-keyword\">const<\/span> formData = <span class=\"hljs-keyword\">new<\/span> FormData()\n    formData.append(<span class=\"hljs-string\">'file'<\/span>, dataUrl)\n    formData.append(<span class=\"hljs-string\">'upload_preset'<\/span>, <span class=\"hljs-string\">'cwt1qiwn'<\/span>)\n\n    <span class=\"hljs-keyword\">const<\/span> cloudinaryRes = <span class=\"hljs-keyword\">await<\/span> fetch(uploadApi, {\n      <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'POST'<\/span>,\n      <span class=\"hljs-attr\">body<\/span>: formData,\n    })\n\n    <span class=\"hljs-keyword\">const<\/span> input = {\n      <span class=\"hljs-attr\">heatmapImage<\/span>: cloudinaryRes.url,\n      <span class=\"hljs-attr\">imageId<\/span>: image.id,\n    }\n\n    createHeatmap({\n      <span class=\"hljs-attr\">variables<\/span>: { input },\n    })\n  }\n\n  <span class=\"hljs-keyword\">const<\/span> setPosition = <span class=\"hljs-function\">(<span class=\"hljs-params\">position<\/span>) =&gt;<\/span> {\n    <span class=\"hljs-keyword\">switch<\/span>(position) {\n      <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'top-left'<\/span>:\n        setBottom(<span class=\"hljs-string\">'0'<\/span>)\n        setLeft(<span class=\"hljs-string\">'0'<\/span>)\n        <span class=\"hljs-keyword\">break<\/span>\n      <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'top-right'<\/span>:\n        setBottom(<span class=\"hljs-string\">'0'<\/span>)\n        setLeft(<span class=\"hljs-string\">'80%'<\/span>)\n        <span class=\"hljs-keyword\">break<\/span>\n      <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'bottom-left'<\/span>:\n        setBottom(<span class=\"hljs-string\">'-150px'<\/span>)\n        setLeft(<span class=\"hljs-string\">'0'<\/span>)\n        <span class=\"hljs-keyword\">break<\/span>\n      <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">'bottom-right'<\/span>:\n        setBottom(<span class=\"hljs-string\">'-150px'<\/span>)\n        setLeft(<span class=\"hljs-string\">'80%'<\/span>)\n        <span class=\"hljs-keyword\">break<\/span>\n    }\n  }\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span>&gt;<\/span>HeatmapPage<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">select<\/span> <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{(e)<\/span> =&gt;<\/span> {\n        const {id, url} = JSON.parse(e.target.value)\n        setImage({id: id, url: url})}\n        }\n      &gt;\n        {data.images.map(image =&gt; (\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span>\n            <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{image.id}<\/span>\n            <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{<\/span>`{\"<span class=\"hljs-attr\">id<\/span>\"<span class=\"hljs-attr\">:<\/span> ${<span class=\"hljs-attr\">image.id<\/span>}, \"<span class=\"hljs-attr\">url<\/span>\"<span class=\"hljs-attr\">:<\/span> \"${<span class=\"hljs-attr\">image.url<\/span>}\"}`}\n          &gt;<\/span>\n            {image.name}\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n          ))\n        }\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">select<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{uploadHeatmap}<\/span>&gt;<\/span>Upload Heatmap to Cloudinary<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">display:<\/span> '<span class=\"hljs-attr\">flex<\/span>', <span class=\"hljs-attr\">flexDirection:<\/span> '<span class=\"hljs-attr\">row<\/span>'}}&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('top-left')}&gt;Top-Left<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('top-right')}&gt;Top-Right<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('bottom-left')}&gt;Bottom-Left<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> setPosition('bottom-right')}&gt;Bottom-Right<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>\n        <span class=\"hljs-attr\">ref<\/span>=<span class=\"hljs-string\">{heatmapRef}<\/span>\n        <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n          <span class=\"hljs-attr\">backgroundImage:<\/span> `<span class=\"hljs-attr\">url<\/span>(${<span class=\"hljs-attr\">image.url<\/span>})`,\n          <span class=\"hljs-attr\">backgroundRepeat:<\/span> '<span class=\"hljs-attr\">no-repeat<\/span>',\n          <span class=\"hljs-attr\">backgroundSize:<\/span> '<span class=\"hljs-attr\">cover<\/span>',\n          <span class=\"hljs-attr\">height:<\/span> <span class=\"hljs-attr\">300<\/span>,\n          <span class=\"hljs-attr\">position:<\/span> '<span class=\"hljs-attr\">absolute<\/span>',\n          <span class=\"hljs-attr\">width:<\/span> '<span class=\"hljs-attr\">100<\/span>%'\n        }}\n      &gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>\n          <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n            <span class=\"hljs-attr\">background:<\/span> '<span class=\"hljs-attr\">radial-gradient<\/span>(<span class=\"hljs-attr\">rgba<\/span>(<span class=\"hljs-attr\">0<\/span>, <span class=\"hljs-attr\">255<\/span>, <span class=\"hljs-attr\">25<\/span>, <span class=\"hljs-attr\">0.5<\/span>), <span class=\"hljs-attr\">rgba<\/span>(<span class=\"hljs-attr\">255<\/span>, <span class=\"hljs-attr\">0<\/span>, <span class=\"hljs-attr\">25<\/span>, <span class=\"hljs-attr\">0.5<\/span>))',\n            <span class=\"hljs-attr\">height:<\/span> <span class=\"hljs-attr\">150<\/span>,\n            <span class=\"hljs-attr\">position:<\/span> '<span class=\"hljs-attr\">relative<\/span>',\n            <span class=\"hljs-attr\">bottom:<\/span> <span class=\"hljs-attr\">bottom<\/span>,\n            <span class=\"hljs-attr\">left:<\/span> <span class=\"hljs-attr\">left<\/span>,\n            <span class=\"hljs-attr\">width:<\/span> <span class=\"hljs-attr\">250<\/span>,\n            <span class=\"hljs-attr\">zIndex:<\/span> <span class=\"hljs-attr\">10<\/span>\n          }}\n        &gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\n  )\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> HeatmapPage\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>That\u2019s all of the code! Now open your terminal and run:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood dev\n<\/code><\/span><\/pre>\n<p>You should see something similar to this.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1638979100\/e-603fc55d218a650069f5228b\/a8v3drejh1gajs87txca.png\" alt=\"image of the user interface\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1141\"\/><\/p>\n<p>There\u2019s a chance you might want to add new images to select from. This is where you can go to the <code>images\/new<\/code> page and add new URLs to other images.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1638979115\/e-603fc55d218a650069f5228b\/dariylp9jsoqucnm8zru.png\" alt=\"the image addition page\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1046\"\/><\/p>\n<h2>Finished code<\/h2>\n<p>If you want to check out the complete project, you can find all of the code in <a href=\"https:\/\/github.com\/flippedcoder\/blog-examples\/tree\/main\/image-heatmap\">the <code>image-heatmap<\/code> directory of this repo<\/a>.<\/p>\n<p>Or you can check out the front-end in <a href=\"https:\/\/codesandbox.io\/s\/zen-mcclintock-hgc7y\">this Code Sandbox<\/a>.<\/p>\n<p>&lt;CodeSandBox\ntitle=\u201czen-mcclintock-hgc7y\u201d\nid=\u201czen-mcclintock-hgc7y\u201d\n\/&gt;<\/p>\n<h2>Conclusion<\/h2>\n<p>There are several ways you could take advantage of a dynamically moving heatmap. Maybe you\u2019ll end up building some machine learning model to create heatmaps for you so you can take a look at where people interact with the page the most. Or maybe you\u2019ll make something that lets users see where they have the most activity in apps.s<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":27853,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[134,370,177,246,371],"class_list":["post-27852","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-guest-post","tag-image","tag-javascript","tag-react","tag-under-review"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Making a Picture Heatmap<\/title>\n<meta name=\"description\" content=\"Sometimes you need to see how things are spread across an image using a heatmap that dynamically moves. In this post, we&#039;ll set up a heatmap that moves based on user input and we&#039;ll save the images to Cloudinary for later review.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Making a Picture Heatmap\" \/>\n<meta property=\"og:description\" content=\"Sometimes you need to see how things are spread across an image using a heatmap that dynamically moves. In this post, we&#039;ll set up a heatmap that moves based on user input and we&#039;ll save the images to Cloudinary for later review.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-23T23:00:16+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-03-02T15:39:52+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32-jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"3490\" \/>\n\t<meta property=\"og:image:height\" content=\"2792\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Making a Picture Heatmap\",\"datePublished\":\"2022-03-23T23:00:16+00:00\",\"dateModified\":\"2025-03-02T15:39:52+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\"},\"wordCount\":4,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA\",\"keywords\":[\"Guest Post\",\"Image\",\"Javascript\",\"React\",\"Under Review\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2022\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\",\"name\":\"Making a Picture Heatmap\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA\",\"datePublished\":\"2022-03-23T23:00:16+00:00\",\"dateModified\":\"2025-03-02T15:39:52+00:00\",\"description\":\"Sometimes you need to see how things are spread across an image using a heatmap that dynamically moves. In this post, we'll set up a heatmap that moves based on user input and we'll save the images to Cloudinary for later review.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA\",\"width\":3490,\"height\":2792},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Making a Picture Heatmap\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Making a Picture Heatmap","description":"Sometimes you need to see how things are spread across an image using a heatmap that dynamically moves. In this post, we'll set up a heatmap that moves based on user input and we'll save the images to Cloudinary for later review.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/","og_locale":"en_US","og_type":"article","og_title":"Making a Picture Heatmap","og_description":"Sometimes you need to see how things are spread across an image using a heatmap that dynamically moves. In this post, we'll set up a heatmap that moves based on user input and we'll save the images to Cloudinary for later review.","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/","og_site_name":"Cloudinary Blog","article_published_time":"2022-03-23T23:00:16+00:00","article_modified_time":"2025-03-02T15:39:52+00:00","og_image":[{"width":3490,"height":2792,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32-jpg?_i=AA","type":"image\/jpeg"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/"},"author":{"name":"","@id":""},"headline":"Making a Picture Heatmap","datePublished":"2022-03-23T23:00:16+00:00","dateModified":"2025-03-02T15:39:52+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/"},"wordCount":4,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA","keywords":["Guest Post","Image","Javascript","React","Under Review"],"inLanguage":"en-US","copyrightYear":"2022","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/","name":"Making a Picture Heatmap","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA","datePublished":"2022-03-23T23:00:16+00:00","dateModified":"2025-03-02T15:39:52+00:00","description":"Sometimes you need to see how things are spread across an image using a heatmap that dynamically moves. In this post, we'll set up a heatmap that moves based on user input and we'll save the images to Cloudinary for later review.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA","width":3490,"height":2792},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Making a Picture Heatmap"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":""}]}},"parsely":{"version":"1.1.0","canonical_url":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/","smart_links":{"inbound":0,"outbound":0},"traffic_boost_suggestions_count":0,"meta":{"@context":"https:\/\/schema.org","@type":"NewsArticle","headline":"Making a Picture Heatmap","url":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/","mainEntityOfPage":{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA&w=150&h=150&crop=1","image":{"@type":"ImageObject","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA"},"articleSection":"Uncategorized","author":[],"creator":[],"publisher":{"@type":"Organization","name":"Cloudinary Blog","logo":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA"},"keywords":["guest post","image","javascript","react","under review"],"dateCreated":"2022-03-23T23:00:16Z","datePublished":"2022-03-23T23:00:16Z","dateModified":"2025-03-02T15:39:52Z"},"rendered":"<meta name=\"parsely-title\" content=\"Making a Picture Heatmap\" \/>\n<meta name=\"parsely-link\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-picture-heatmap-in-redwood\/\" \/>\n<meta name=\"parsely-type\" content=\"post\" \/>\n<meta name=\"parsely-image-url\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA&w=150&amp;h=150&amp;crop=1\" \/>\n<meta name=\"parsely-pub-date\" content=\"2022-03-23T23:00:16Z\" \/>\n<meta name=\"parsely-section\" content=\"Uncategorized\" \/>\n<meta name=\"parsely-tags\" content=\"guest post,image,javascript,react,under review\" \/>","tracker_url":"https:\/\/cdn.parsely.com\/keys\/cloudinary.com\/p.js"},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926100\/Web_Assets\/blog\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32\/49767320bd679479e7e176574122ab8ffd8fa7a8-3490x2792-1_27853a4d32.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27852","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=27852"}],"version-history":[{"count":1,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27852\/revisions"}],"predecessor-version":[{"id":37072,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27852\/revisions\/37072"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/27853"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=27852"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=27852"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=27852"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}