Skip to content

Data Visualization using Chart.js

Chart.js is an amazing JS library that helps with the visualization of data through different types of charts and graphs. In this tutorial, we’re going to be generating images of the charts/graphs from the Chart.js JS library and combining them into one image using Next.js and Cloudinary.

The final project can be viewed on Codesandbox.

You can find the full source code on my Github repository.

I should mention that working knowledge of Javascript is required. Knowledge of React, Node.js, and Next.js is recommended but not required. I, however, recommend going through the Next.js docs to get a grip on some of its features that we’ll be using e.g. API Routes

Let’s start by creating a new Next.js project. Run the following command in your terminal to create a basic Next project.


npx create-next-app data-visualization-with-chartjs

Code language: JavaScript (javascript)

If you’d like to use features such as Typescript and more, have a look at the docs. Change directory to your new project.


cd data-visualization-with-chartjs

Code language: JavaScript (javascript)

Open your project in your favorite code editor. Visual Studio Code is a great option, because of its amazing support for js and jsx. Let’s proceed and install dependencies.

Run the following command to install Chart.js, Cloudinary and Formidable.


npm install chart.js cloudinary formidable

Code language: CSS (css)

Formidable helps us to parse form data easily, cloudinary enables us to communicate with the Cloudinary APIs.

To communicate with cloudinary APIs, we’ll need some credentials and API keys. Let’s get those. If you’re not familiar, cloudinary provides APIs to developers that allow for the upload and storage of images and videos. On top of that, you can also transform and optimize media. You can get started with a free developer account immediately and upgrade to a paid plan when your needs grow. Head over to cloudinary and create an account if you do not have one. Proceed to log in and navigate to your dashboard. Here you’ll find the Cloud name, API Key, and API Secret.

Cloudinary Dashboard

In your code editor with your project open, create a new file named .env.local at the root of your project and paste the following code inside.


CLOUD_NAME=YOUR_CLOUD_NAME

API_KEY=YOUR_API_KEY

API_SECRET=YOUR_API_SECRET

Replace YOUR_CLOUD_NAME YOUR_API_KEY and YOUR_API_SECRET with the Cloud name, API Key, and API Secret values that we just got.

Here we’re making use of environment variables to store our API keys securely. Read about Next.js support for environment variables here

We’re now ready to start our project/application. Let’s work on the front-end first.

We’re going to have two pages: the home page, where we’ll display the individual charts, and the images page where we’ll display the combined images.

We can begin by setting up a few styles for buttons and links. Add the following to styles/globals.css


  

a:hover  {

text-decoration:  underline;

}

  

button {

padding:  20px  30px;

border:  none;

font-weight:  bold;

background-color:  #0044ff;

color:  #ffffff;

}

  

button:disabled  {

background-color:  #cfcfcf;

}

  

button:hover:not([disabled])  {

background-color:  #0036ca;

}

Code language: CSS (css)

Next, create a folder called components at the root of your project. Inside this folder, create a new file called Layout.js and paste the following code inside.


// components/Layout.js

  

import  Head  from  "next/head";

import  Link  from  "next/link";

  

const  LayoutComponent  =  (props)  =>  {

const  {  children  }  =  props;

  

return  (

<div>

<Head>

<title>Data Visualization With Chartjs and Cloudinary</title>

<meta

name="description"

content="Data Visualization With Chartjs and Cloudinary"

/>

<link rel="icon"  href="/favicon.ico"  />

</Head>

  

<nav>

<Link  href="/">

<a>Home</a>

</Link>

<Link  href="/images">

<a>Images</a>

</Link>

</nav>

<main>{children}</main>

<style jsx>{`

nav {

min-height: 100px;

background-color: #0044ff;

display: flex;

justify-content: center;

align-items: center;

}

nav a {

color: white;

margin: 0 10px;

font-weight: bold;

}

`}</style>

</div>

);

};

  

export  default  LayoutComponent;

Code language: JavaScript (javascript)

This is a component that we can reuse to wrap our pages so we have a consistent layout and we don’t have to write the same code multiple times.

We also need a component to display our graphs/charts from Chart.js. Create a file named ChartOnCanvas.js under the components/ folder and paste the following code.


import  {  Chart,  ChartConfiguration  }  from  "chart.js";

import  {

forwardRef,

useCallback,

useEffect,

useImperativeHandle,

useRef,

}  from  "react";

  

/**

*

* @param  {{ config: ChartConfiguration; width:number;height:number;}}  props

* @returns

*/

const  ChartOnCanvas  =  forwardRef(function  ChartOnCanvas(props,  ref)  {

const  {  config,  width  =  400,  height  =  400  }  =  props;

  

// Variable to store our chart reference

let  chart  =  useRef(null);

  

// Use the `useImperativeHandle` hook to bind our ref to the parent component's ref and expose a method called `toBase64Image`

useImperativeHandle(

ref,

()  =>  ({

toBase64Image()  {

return  chart?.current?.toBase64Image();

},

}),

[]

);

  

// Ref to the canvas element

const  canvasRef  =  useRef(null);

  

const  createChart  =  useCallback(()  =>  {

chart.current  =  new  Chart(canvasRef.current,  {

...config,

plugins:  [

...(config.plugins  ??  []),

{

id:  "custom_canvas_background_color",

beforeDraw:  (chart)  =>  {

const  ctx  =  chart.canvas.getContext("2d");

ctx.save();

ctx.globalCompositeOperation  =  "destination-over";

ctx.fillStyle  =  "white";

ctx.fillRect(0,  0,  chart.width,  chart.height);

ctx.restore();

},

},

],

});

  

return  ()  =>  {

chart?.current?.destroy();

};

},  []);

  

useEffect(()  =>  {

createChart();

},  [createChart]);

  

return  (

<div

ref={ref}

style={{

width,

height,

}}

>

<canvas ref={canvasRef}  width={width}  height={width}></canvas>

</div>

);

});

  

export  default  ChartOnCanvas;

Code language: JavaScript (javascript)

Let’s go over what this all means. Chart.js displays the data on an HTML canvas. We created a custom component so that we can easily have different canvas elements that manage their state. We also need to expose a few functions that we can call via a DOM reference. This is where forwardRef and useImperativeHandle come in. I’ll try to explain both. Sometimes when we create a custom react component, we may want to pass a ref attribute to another component that is below in the tree. For example when we wrap a native button element with a custom component and we want to access the native button from outside of our component. To access the native button, we pass a reference down the tree using forwardRef. Read more about it here. To add to this, we may also want to expose custom instance values/methods to the parent of our custom component. This is exactly what useImperativeHandle does. If you have some custom fields/methods in your custom component that you would like to access outside of that custom component, you expose them using the useImperativeHandle hook. Read more about it here.

Our ChartOnCanvas component takes in a few props. We need a width and height for our canvas size, and also a config object that we’ll pass to chart.js. The config object tells chart.js what data to display, how to display it, and what type of chart to display it on. Read about it here.

chart stores a reference to our chart using the useRef hook.

Inside useImperativeHandle we expose a method called toBase64Image. This method converts a chart to a base64 image string. We’ll call it from outside the component to get the base64 string on demand.

canvasRef stores a reference to our canvas element.

createChart is a memoized callback. We use a useCallback hook to store a memoized callback function. The function creates a new chart, passes it the config object we get from the props, and then assigns the chart to thechart variable. Notice how we use the plugins field to paint the background of the canvas white. Read about this here

The useEffect effect hook just calls createChart when the component is rendered. Read about useEffect.

For the HTML we just have a canvas element with a ref canvasRef.

Let’s move on to the home page. Replace the contents in pages/index.js with the following code.


// pages/index.js

import  Head  from  "next/head";

import  {  Chart,  registerables  }  from  "chart.js";

Chart.register(...(registerables  ??  []));

import  ChartOnCanvas  from  "../components/ChartOnCanvas";

import  {  useState,  useRef  }  from  "react";

import  {  useRouter  }  from  "next/dist/client/router";

import  LayoutComponent  from  "../components/Layout";

const  HomePage  =  ()  =>  {

const  router  =  useRouter();

const  [loading,  setLoading]  =  useState(false);

const  chart1Ref  =  useRef();

const  chart2Ref  =  useRef();

const  chart3Ref  =  useRef();

const  chart4Ref  =  useRef();

const  generateImage  =  async  ()  =>  {

try  {

// Set loading to true

setLoading(true);

const  base64Urls  =  [

chart1Ref.current.toBase64Image(),

chart2Ref.current.toBase64Image(),

chart3Ref.current.toBase64Image(),

chart4Ref.current.toBase64Image(),

];

const  results  =  await  Promise.all(base64Urls.map((url)  =>  fetch(url)));

const  images  =  await  Promise.all(results.map((res)  =>  res.blob()));

const  formData  =  new  FormData();

for  (const  imageBlob  of  images)  {

formData.append("images",  imageBlob);

}


// Make a POST request to the `api/images/` endpoint

const  response  =  await  fetch("/api/images",  {

method:  "POST",

body:  formData,

});

const  data  =  await  response.json();

if  (!response.ok)  {

throw  data;

}

// Navigate to the images page

router.push("/images");

}  catch  (error)  {

console.error(error);

}  finally  {

setLoading(false);

}

};

  

return  (

<LayoutComponent>

<div className="actions">

<button

onClick={()  =>  {

generateImage();

}}

disabled={loading}

>

Generate Image

</button>

</div>

<div className="wrapper">

<ChartOnCanvas

ref={chart1Ref}

config={{

type:  "bar",

data:  {

labels:  ["Red",  "Blue",  "Yellow",  "Green",  "Purple",  "Orange"],

datasets:  [

{

label:  "# of Votes",

data:  [12,  19,  3,  5,  2,  3],

backgroundColor:  [

"rgba(255, 99, 132, 0.2)",

"rgba(54, 162, 235, 0.2)",

"rgba(255, 206, 86, 0.2)",

"rgba(75, 192, 192, 0.2)",

"rgba(153, 102, 255, 0.2)",

"rgba(255, 159, 64, 0.2)",

],

borderColor:  [

"rgba(255, 99, 132, 1)",

"rgba(54, 162, 235, 1)",

"rgba(255, 206, 86, 1)",

"rgba(75, 192, 192, 1)",

"rgba(153, 102, 255, 1)",

"rgba(255, 159, 64, 1)",

],

borderWidth:  1,

},

],

},

options:  {

plugins:  {

title:  {

display:  true,

text:  "Chart.js Bar Chart",

},

},

scales:  {

y:  {

beginAtZero:  true,

},

},

},

}}

></ChartOnCanvas>

<ChartOnCanvas

ref={chart2Ref}

config={{

type:  "line",

data:  {

labels:  ["Jan",  "Feb",  "Mar",  "Apr",  "May",  "Jun",  "Jul"],

datasets:  [

{

label:  "My First Dataset",

data:  [65,  59,  80,  81,  56,  55,  40],

fill:  false,

borderColor:  "rgb(75, 192, 192)",

tension:  0.1,

},

],

},

options:  {

plugins:  {

title:  {

display:  true,

text:  "Chart.js Line Chart",

},

},

},

}}

></ChartOnCanvas>

<ChartOnCanvas

ref={chart3Ref}

config={{

type:  "doughnut",

data:  {

labels:  ["Red",  "Blue",  "Yellow"],

datasets:  [

{

label:  "My First Dataset",

data:  [300,  50,  100],

backgroundColor:  [

"rgb(255, 99, 132)",

"rgb(54, 162, 235)",

"rgb(255, 205, 86)",

],

hoverOffset:  4,

},

],

},

options:  {

plugins:  {

title:  {

display:  true,

text:  "Chart.js Doughnut Chart",

},

},

scales:  {

y:  {

beginAtZero:  true,

},

},

},

}}

></ChartOnCanvas>

<ChartOnCanvas

ref={chart4Ref}

config={{

type:  "polarArea",

data:  {

labels:  ["Red",  "Green",  "Yellow",  "Grey",  "Blue"],

datasets:  [

{

label:  "My First Dataset",

data:  [11,  16,  7,  3,  14],

backgroundColor:  [

"rgb(255, 99, 132)",

"rgb(75, 192, 192)",

"rgb(255, 205, 86)",

"rgb(201, 203, 207)",

"rgb(54, 162, 235)",

],

},

],

},

options:  {

plugins:  {

title:  {

display:  true,

text:  "Chart.js Polar Area Chart",

},

},

scales:  {

y:  {

beginAtZero:  true,

},

},

},

}}

></ChartOnCanvas>

</div>

<style jsx>{`

div.actions {

width: 100vw;

display: flex;

justify-content: center;

align-items: center;

padding: 20px 0;

}

  

div.wrapper {

min-height: 100vh;

width: 100vw;

padding: 20px;

display: flex;

flex-flow: row wrap;

gap: 1rem;

align-content: flex-start;

justify-content: center;

}

  

div.wrapper > * {

flex: 0 0 400px;

border: 1px solid black;

}

`}</style>

</LayoutComponent>

);

};

  

export  default  HomePage;

  

Code language: PHP (php)

At the top, we import the Chart library and also register all registerables so that we can be able to use them. Registerables are just all the chart components that we plan on using in our project. Read about this here

For our HomePage component, we have the router from Next.js and a loading state. We also have a useRef hook for each of the charts that we want to display. We’ll pass these refs to our ChartOnCanvas components. generateImage calls the toBase64Image() method on our charts references, converts these base64 strings to blobs then appends the blobs to formdata so that we can be able to upload them as multipart/form-data. If you’re wondering about the Promise.all() see this. We then post the form data to the /api/images endpoint which we’ll be creating later. When the post request to /api/images completes successfully, it navigates to the /images page, we’ll create that next. For the HTML, we just wrap the whole page in the Layout component that we created earlier. We then have a few ChartOnCanvas components. We pass some chart configuration to each of the ChartOnCanvas components and also pass a ref. The config objects that I have used in this tutorial are all from the chart.js examples. The button at the top triggers the generateImage function.

Let’s create the images page. Create a new file under pages/ called images.js and paste the following code inside.


import  {  useCallback,  useEffect,  useState  }  from  "react";

import  Image  from  "next/image";

import  Link  from  "next/link";

import  LayoutComponent  from  "../components/Layout";

  

const  ImagesPage  =  ()  =>  {

const  [loading,  setLoading]  =  useState(false);

const  [images,  setImages]  =  useState([]);

  

const  getImages  =  useCallback(async  ()  =>  {

try  {

setLoading(true);

const  response  =  await  fetch("/api/images",  {

method:  "GET",

});

  

const  data  =  await  response.json();

  

if  (!response.ok)  {

throw  data;

}

  

setImages(data.result.resources);

}  catch  (error)  {

console.log(error);

}  finally  {

setLoading(false);

}

},  []);

  

useEffect(()  =>  {

getImages();

},  [getImages]);

  

return  (

<LayoutComponent>

<div className="wrapper">

{loading  ?  (

<div className="loading">

<p>Loading...</p>

</div>

)  :  (

<div>

{images.length  >  0  ?  (

<div className="images">

{images.map((image,  index)  =>  (

<div key={`image-${index}`}  className="image-wrapper">

<div className="image">

<Image

src={image.secure_url}

alt={image.public_id}

width={image.width}

height={image.height}

layout="intrinsic"

></Image>

</div>

<div className="actions">

<Link  href={image.secure_url}  passHref>

<a target="_blank"  rel="noreferrer">

{image.public_id}

</a>

</Link>

</div>

</div>

))}

</div>

)  :  (

<div className="no-images">

<p>No Images Yet</p>

<Link  href="/"  passHref>

<button>Generate Some Images</button>

</Link>

</div>

)}

</div>

)}

</div>

<style jsx>{`

div.loading {

width: 100%;

height: calc(100vh - 100px);

display: flex;

justify-content: center;

align-items: center;

font-size: 2rem;

font-weight: bold;

}

  

div.no-images {

width: 100%;

height: calc(100vh - 100px);

display: flex;

flex-flow: column;

justify-content: center;

align-items: center;

}

  

div.no-images p {

font-size: 2rem;

font-weight: bold;

}

  

div.images {

width: 100%;

min-height: calc(100vh - 100px);

padding: 20px;

display: flex;

gap: 40px;

flex-flow: column nowrap;

justify-content: center;

align-items: center;

}

  

div.images div.image-wrapper {

display: flex;

flex-flow: column nowrap;

background-color: #f5f5f5;

-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);

-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);

box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);

}

  

div.images div.image-wrapper div.image {

flex: 1 0 auto;

background-color: #ffffff;

}

  

div.images div.image-wrapper div.actions {

min-height: 50px;

display: flex;

justify-content: center;

align-items: center;

padding: 10px 0;

}

`}</style>

</LayoutComponent>

);

};

  

export  default  ImagesPage;

Code language: JavaScript (javascript)

This is just a simple react component. We have a loading state and an images state to store all uploaded images. getImages makes a GET request to the /api/images/ route and gets all images that have been uploaded then updates the images state. For the HTML we just display the images in a flex layout container.

That’s about it for the front end. Moving on to the backend.

Create a new folder at the root of your project and name it lib. Inside this folder, create a new file called cloudinary.js and paste the following code inside.


// Import the v2 api and rename it to cloudinary

import  {  v2  as  cloudinary,  TransformationOptions  }  from  "cloudinary";

  

const  CLOUDINARY_FOLDER_NAME  =  "visualization-with-chartjs/";

  

// Initialize the SDK with cloud_name, api_key, and api_secret

cloudinary.config({

cloud_name:  process.env.CLOUD_NAME,

api_key:  process.env.API_KEY,

api_secret:  process.env.API_SECRET,

});

  

/**

* Gets a resource from cloudinary using its public id

*

* @param  {string}  publicId The public id of the image

*/

export  const  handleGetCloudinaryResource  =  (publicId)  =>  {

return  cloudinary.api.resource(publicId,  {

resource_type:  "image",

type:  "upload",

});

};

  

/**

* Get cloudinary uploads

* @returns  {Promise}

*/

export  const  handleGetCloudinaryUploads  =  ()  =>  {

return  cloudinary.api.resources({

type:  "upload",

prefix:  CLOUDINARY_FOLDER_NAME,

resource_type:  "image",

});

};

  

/**

* Uploads an image to cloudinary and returns the upload result

*

* @param  {{path: string; transformation?:TransformationOptions;publicId?: string; folder?: boolean; }}  resource

*/

export  const  handleCloudinaryUpload  =  (resource)  =>  {

return  cloudinary.uploader.upload(resource.path,  {

// Folder to store the image in

folder:  resource.folder  ?  CLOUDINARY_FOLDER_NAME  :  null,

// Public id of image.

public_id:  resource.publicId,

// Type of resource

resource_type:  "auto",

// Transformation to apply to the video

transformation:  resource.transformation,

});

};

  

/**

* Deletes resources from cloudinary. Takes in an array of public ids

* @param  {string[]}  ids

*/

export  const  handleCloudinaryDelete  =  (ids)  =>  {

return  cloudinary.api.delete_resources(ids,  {

resource_type:  "image",

});

};

  

Code language: JavaScript (javascript)

At the top, we import the cloudinary v2 api and rename it to cloudinary. This is just for readability and you can leave it as is. We then define a folder name where we’ll store all our images and proceed to initialize the cloudinary SDK by calling the config method. We pass the environment variables that we defined earlier.

handleGetCloudinaryResource calls the api.resource method on the SDK. It gets a specific resource from cloudinary using its public id. Read about this here

handleGetCloudinaryUploads calls the api.resources method to get all uploaded resources(images/videos) in the specified folder. Read more about this here

handleCloudinaryUpload uploads a resource to cloudinary by calling the uploader.upload method. We pass to it the path to the file we want to upload and also a few options. Read about the options that you can pass here here.

handleCloudinaryDelete takes in an array of public IDs and passes them to the api.delete_resources method. This method deletes the resources with the given public IDs from cloudinary.

Create another file inside the lib/ folder and name it parse-form.js. Paste the following code inside.


// lib/parse-form.js

  

import  {  IncomingForm  }  from  "formidable";

  

/**

* Parses the incoming form data.

*

* @param  {NextApiRequest}  req The incoming request object

*/

export  const  parseForm  =  (req)  =>  {

return  new  Promise((resolve,  reject)  =>  {

const  form  =  new  IncomingForm({ keepExtensions:  true, multiples:  true  });

  

form.parse(req,  (error,  fields,  files)  =>  {

if  (error)  {

return  reject(error);

}

  

return  resolve({  fields,  files  });

});

});

};

Code language: JavaScript (javascript)

parseForm takes in a request object and uses Formidable to parse the formdata from the request. Read the formidable docs to learn more about all the options.

And now for the final piece of the puzzle, we need an API route. Let’s create an API route to handle the endpoint /api/images endpoint.

Create a new file called images.js under pages/api and paste the following code inside.


// pages/api/images.js

  

import  {  NextApiRequest,  NextApiResponse  }  from  "next";

import  {

handleCloudinaryDelete,

handleCloudinaryUpload,

handleGetCloudinaryUploads,

}  from  "../../lib/cloudinary";

import  {  parseForm  }  from  "../../lib/parse-form";

  

// Custom config for our API route

export  const  config  =  {

api:  {

bodyParser:  false,

},

};

  

/**

* The handler function for the API route. Takes in an incoming request and outgoing response.

*

* @param  {NextApiRequest}  req The incoming request object

* @param  {NextApiResponse}  res The outgoing response object

*/

const  ImagesRoute  =  async  (req,  res)  =>  {

switch  (req.method)  {

case  "GET":  {

try  {

const  result  =  await  handleGetRequest();

  

return  res.status(200).json({ message:  "Success",  result  });

}  catch  (error)  {

return  res.status(400).json({ message:  "Error",  error  });

}

}

  

case  "POST":  {

try  {

const  result  =  await  handlePostRequest(req);

  

return  res.status(201).json({ message:  "Success",  result  });

}  catch  (error)  {

console.error(error);

return  res.status(400).json({ message:  "Error",  error  });

}

}

  

default:  {

return  res.status(405).json({ message:  "Method not allowed"  });

}

}

};

  

const  handleGetRequest  =  async  ()  =>  {

const  uploads  =  await  handleGetCloudinaryUploads();

  

return  uploads;

};

  

/**

* Handles the POST request to the API route.

*

* @param  {NextApiRequest}  req The incoming request object

*/

const  handlePostRequest  =  async  (req)  =>  {

// Get the form data using the parseForm function

const  data  =  await  parseForm(req);

  

// Get images from the parsed form data

const  images  =  data.files.images;

  

// Cloudinary image upload results

const  imagesUploadResults  =  [];

  

// Final Image upload result

let  finalImageUploadResult;

  

// Loop over the images

for  (const  image  of  images)  {

// Check if it's the last image

if  (imagesUploadResults.length  ===  images.length  -  1)  {

// Get the already uploaded image results

const  otherImages  =  imagesUploadResults.slice(0,  images.length  -  1);

  

// Upload the last image and add the already uploaded images as layers

const  imageUploadResult  =  await  handleCloudinaryUpload({

path:  image.path,

folder:  true,

transformation:  [

...otherImages.map((image,  index,  arr)  =>  {

const  itemsBeforeCurrent  =  arr.slice(0,  index);

  

const  rowsBeforeCurrent  =  itemsBeforeCurrent.reduce(

(accumulator,  currentValue,  currentIndex,  array)  =>  {

if  (currentIndex  %  2  ===  0)  {

const  row  =  [{},  ...array].slice(

currentIndex,

currentIndex  +  2

);

  

if  (row.length  ==  2)  accumulator.push(row);

}

  

return  accumulator;

},

[]

);

  

return  {

overlay:  image.public_id.replace(/\//g,  ":"),

width:  400,

height:  400,

crop:  "scale",

gravity:  "north_west",

x:  index  %  2  ===  0  ?  400  :  0,

y:  rowsBeforeCurrent.length  *  400,

};

}),

],

});

  

finalImageUploadResult  =  imageUploadResult;

}  else  {

// Upload the image to cloudinary

const  imageUploadResult  =  await  handleCloudinaryUpload({

path:  image.path,

folder:  false,

});

  

imagesUploadResults.push(imageUploadResult);

}

}

  

// Delete the uploaded images that we no longer need

await  handleCloudinaryDelete(

imagesUploadResults.map((result)  =>  result.public_id)

);

  

return  finalImageUploadResult;

};

  

export  default  ImagesRoute;

  

Code language: JavaScript (javascript)

Read about Next.js API routes here

At the top, we export a custom config object. This custom config tells Next.js not to use the default body-parser middleware since we want to handle the form data ourselves.

ImagesRoute is the handler/controller for our route. It takes in a request object and a response object. We then export it as a default export.

We use a switch statement to only handle GET and POST requests. handleGetRequest gets all uploads by calling the handleGetCloudinaryUploads function that we created earlier.

handlePostRequest gets the incoming form data using the parseForm function that we defined. At this point, we must understand the next steps. Here’s what we’re doing. When the front-end makes a POST request to the /api/images endpoint, we receive an array of images. We’ll loop over the images and upload all images except the last one in the array. The last image will serve as the base for the combined image that we want to achieve. For our use case, we want an image with two columns and an infinite number of rows. The last image that I mentioned will be placed at the top-left, then we’ll map the already uploaded images as layers relative to this last image. Refer to the following table.

| Column 1 | Column 2 |

| –––– | –––– |

| 0 | 1 |

| 2 | 3 |

| 4 | 5 |

Here’s how we achieve this. When uploading the last image, we pass a transformation option. Read more about this here. In the transformation object, we pass an object which will serve as an overlay to the image we’re uploading. Read about how to add overlays to images here. The object has the following schema


{

overlay:  string;

width:  number;

height:  number;

crop:  string;

gravity:  string;

x:  number;

y:  number;

}

Code language: CSS (css)

The most important option here is the gravity option which we set to north_west. This tells cloudinary to use the top left of the image as the origin(0,0). That means that the x and y coordinates start at 0 at the top left. The other important options are the x and y options. Since we set our images to be 400×400, we know their sizes. So we know how to place the images. See below

| Column 1 | Column 2 |

| ––––– | ———— |

| x,y(0,0) | x,y(400,0) |

| x,y(0,400) | x,y(400,400) |

| x,y(0,800) | x,y(400,800) |

It’s easy to determine the x coordinate since we only have two columns. We just need to check the index of the image to see if it’s even or odd. For the y co-ordinate, however, it’s a bit tricky. There are several ways you could do this. For this tutorial let’s try something a bit unorthodox.


// ...

const  rowsBeforeCurrent  =  itemsBeforeCurrent.reduce(

(accumulator,  currentValue,  currentIndex,  array)  =>  {

if  (currentIndex  %  2  ===  0)  {

const  row  =  [{},  ...array].slice(

currentIndex,

currentIndex  +  2

);

  

if  (row.length  ==  2)  accumulator.push(row);

}

  

return  accumulator;

},

[]

);

// ...

Code language: PHP (php)

Don’t paste this anywhere, we’ve already written it. In the piece of code above, we take an array of all images and place them in groups of two, each group representing a row. We then count the number of rows and then multiply that with 400, our image height.

And that’s it for our application.

One more thing before we run our application. We need to add the cloudinary domain name to our next.config.js. This is because we’re using the Image component from Next.js to show images. As part of the optimization measures, we need to add the domain names for any external images that we show. Read about this here. Create a file called next.config.js at the root of your project if it doesn’t exist and add the following


module.exports  =  {

// ... other options

images:  {

domains:  ["res.cloudinary.com"],

},

};

Code language: JavaScript (javascript)

You can now run your project using


npm run dev

That’s it. You can find the full code on my Github

Back to top

Featured Post