Emojigraph: Conventional Commits Npm package in React with Chart.js and simple-git
Published: 2023-12-13 15:47:09


![React Conventional Commits Graph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wz79wmpo40e93w2728th.png)

### What am I looking at?

It's a package that graphs your conventional commits into a Emoji Chart based on the number of types of commits. I built this package while working on a side project to see where I was spending my time. What's get measured get's managed don't you know. You can install the package here https://www.npmjs.com/package/react-conventional-commits-graph however it may have bugs so please open an issue since it's my first package. Now let's understand some prerequisites...

Oh and if you want to see the graph live on a page here it is

https://app.stripeappkit.com/api/commits

### Understanding Conventional Commits

Conventional commits are a specification for adding human and machine-readable meaning to commit messages. They follow a structured format to make the history of a repository more accessible and easier to navigate. Here's a brief overview of their key elements and importance:

1. **Structured Format**: Conventional commits require a specific format for commit messages, typically starting with a type (e.g., `feat`, `fix`), optionally followed by a scope, and a description. For example: `feat(database): add new indexing feature`.

2. **Types of Commits**: Common types include `feat` (new feature), `fix` (bug fix), `docs` (documentation changes), `style` (style changes that do not affect meaning), `refactor` (code changes that neither fix a bug nor add a feature), `test` (adding missing tests), and `chore` (maintenance tasks).

3. **Scope**: This is an optional part that describes the part of the codebase affected by the changes (e.g., `feat(auth)`).

4. **Description**: A concise description of the changes made.

5. **Importance**:

   - **Improved Readability**: Makes it easier to understand the purpose of each commit at a glance.
   - **Automated Tooling**: Enables automated tools to process commit messages and generate changelogs, version bumps, and other outputs based on the type of commits.
   - **Team Collaboration**: Helps team members quickly understand the nature of changes, improving collaboration and review processes.
   - **Project Management**: Assists in tracking the progress of features, fixes, and other types of changes in a project.

In summary, conventional commits provide a standardized way to write commit messages, enhancing readability, supporting tool automation, and improving collaborative development workflows.

### Building the NPM Package

We are going to be using mircobundle to package up the npm package for distribution as a react component.

#### make sure to globally install `microbundle` `npm i -g microbundle`

Steps

1. create a folder with project name mine was called commit-graph
2. Copy this into a new package.json file in the folder

   ```json
    {
     "name": "package-name",
     "description": "package description",
     "version": "1.2.8",
     "type": "module",
     "source": "src/index.js",
     "main": "dist/index.esm.cjs",
     "scripts": {
       "build": "microbundle --jsx 'React.createElement' --jsxImportSource react --globals react/jsx-runtime=jsx --format es,cjs",
       "dev": "microbundle watch --jsxFragment React.Fragment"
     },
     "dependencies": {
       "chart.js": "^4.4.0",
       "react-chartjs-2": "^5.2.0",
       "simple-git": "^3.21.0"
     },
     "peerDependencies": {
       "react": "^17.0.2",
       "react-dom": "^17.0.2"
     },
     "devDependencies": {
       "@babel/cli": "^7.23.4",
       "@babel/core": "^7.23.5",
       "@babel/preset-env": "^7.23.5",
       "@babel/preset-react": "^7.23.3",
       "babel-loader": "^9.1.3",
       "microbundle": "^0.15.1",
       "webpack-bundle-analyzer": "^4.10.1",
       "webpack-cli": "^5.1.4",
       "webpack-dev-server": "^4.15.1"
     },
     "bin": {
       "extract-commits": "server/processData.js"
     }
   }
   ```
3. Add a server folder at the root and a file called `processData.js` with the following code

   ```
   #!/usr/bin/env node
   import simpleGit from 'simple-git';
   import fs from "fs"
   
   const localPath = "./";
   
   // Check if the local repository exists
   if (fs.existsSync(localPath)) {
       // If it exists, use the existing repository
       processData(localPath);
   } else {
       console.error('Local repository does not exist.');
   }
   
   function processData(path) {
       simpleGit(path).log()
           .then(log => {
               const messages = log.all.map(commit => commit.message);
   
               // Write commit messages to a JSON file
               fs.writeFileSync('./public/commitMessages.json', JSON.stringify(messages, null, 2));
               console.log('Commit messages have been written to commitMessages.json');
           })
           .catch(err => console.error('Error in analyzing repository:', err));
   } 
   ```
4. lastly create a `src` folder with a components folder and `index.js` file in the root of `src` and a `CommitChart.js` file in the root of `components`

`index.js`

```
export { default as CommitChart } from './components/CommitChart';
```

`CommitChart.js`

```
import { useEffect, useRef, useState } from "react";
import { Bar } from "react-chartjs-2";
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
} from "chart.js";

const emojiMap = {
  bugs: "🐛",
  features: "✨",
  chores: "🧹", // Example emoji for chores
  fixes: "🔧", // Example emoji for fixes
  // others: "📦", // Example emoji for others
  docs: "📝",
  refactor: "♻️",
};

const emojiPlugin = {
  id: "emojiPlugin",
  afterDraw: (chart) => {
    const ctx = chart.ctx;

    chart.data.datasets.forEach((dataset, datasetIndex) => {
      const meta = chart.getDatasetMeta(datasetIndex);
      meta.data.forEach((bar, index) => {
        const emoji = emojiMap[chart.data.labels[index]];
        const { x, y, base, width, height } = bar;
        const dataValue = dataset.data[index]; // The number of emojis we want to show

        // Calculate the number of rows and columns of emojis based on the data value
        const numRows = Math.ceil(Math.sqrt(dataValue));
        const numCols = Math.ceil(dataValue / numRows);

        // Calculate the size of each emoji to fit the bar's dimensions
        const emojiWidth = width / numCols;
        const emojiHeight = height / numRows;
        const emojiSize = Math.min(emojiWidth, emojiHeight); // Use the smallest to fit both dimensions

        ctx.font = `${emojiSize}px Arial`;

        // Draw the emojis in a grid pattern
        // for (let row = 0; row < numRows; row++) {
        //   for (let col = 0; col < numCols; col++) {
        //     if (row * numCols + col < dataValue) { // Ensure we don't draw more emojis than the data value
        //       const emojiX = x - width / 2 + col * emojiWidth;
        //       const emojiY = base - (numRows - row) * emojiHeight; // Start from the bottom of the bar
        //       ctx.fillText(emoji, emojiX, emojiY);
        //     }
        //   }
        // }
        // ... rest of your plugin code

        // Draw the emojis in a grid pattern
        for (let row = 0; row < numRows; row++) {
          for (let col = 0; col < numCols; col++) {
            if (row * numCols + col < dataValue) {
              // Ensure we don't draw more emojis than the data value
              const emojiX = x - width / 2 + col * emojiWidth;
              const emojiY = base - emojiHeight - row * emojiHeight; // Adjusted to start from the bottom
              // Save the context and rotate the canvas around the emoji's center
              ctx.save();
              ctx.translate(emojiX + emojiSize / 2, emojiY + emojiSize / 2);
              ctx.rotate(Math.PI); // Rotate 180 degrees
              ctx.fillText(emoji, -emojiSize / 2, -emojiSize / 2);
              ctx.restore(); // Restore the context to the original state for the next emoji
            }
          }
        }

        // ... rest of your plugin code
      });
    });
  },
};

// Register the necessary components for Chart.js
ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
);
ChartJS.register(emojiPlugin); // Register the custom plugin

const CommitChart = () => {
  const [commitData, setCommitData] = useState({
    labels: [],
    datasets: [],
  });

  const chartRef = useRef(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("/commitMessages.json");
        if (!response.ok) {
          throw new Error("Network response was not ok");
        }
        const commitMessages = await response.json();

        // Process your commit messages to get the data for the chart
        // Assuming you process the data to the format Chart.js expects
        // Example: { labels: ['Label1', 'Label2'], datasets: [{ data: [1, 2] }] }
        const processedData = processData(commitMessages);

        // If the chart already exists, update it
        if (chartRef.current) {
          chartRef.current.data = processedData;
          chartRef.current.update();
        } else {
          setCommitData(processedData); // Set data for initial chart rendering
        }
      } catch (error) {
        console.error("Failed to fetch commit messages:", error);
      }
    };

    fetchData();
  }, []); // Empty dependency array means this effect runs once after the first render

  const options = {
    scales: {
      x: {
        beginAtZero: true,
      },
      y: {
        // Additional options for the Y-axis can be set here
      },
    },
    maintainAspectRatio: true,
    responsive: true,
    plugins: {
      // ... other plugin configurations
      emojiPlugin: {}, // This is just to enable the plugin
    },
  };

  return (
    <div>
      <h2>Commit Chart</h2>
      <Bar ref={chartRef} data={commitData} options={options} />
    </div>
  );
};

export default CommitChart;

// Helper function to process the raw data
function processData(commitMessages) {
  const counts = {
    bugs: 0,
    features: 0,
    chores: 0,
    fixes: 0,
    // others: 0,
    docs: 0,
    refactor:0,
  };

  commitMessages.forEach((msg) => {
    if (msg.includes("bug:") || msg.includes("🐛")) {
      counts.bugs++;
    } else if (msg.includes("feat:") || msg.includes("✨")) {
      counts.features++;
    } else if (msg.includes("chore:")) {
      counts.chores++;
    } else if (msg.includes("fix:")) {
      counts.fixes++;
    } else if (msg.includes("docs:")) {
      counts.docs++;
    } else if (msg.includes("refactor:")) {
      counts.refactor++;
    }
    //  else {
    // counts.others++; // Count all other commits as 'others'
    // }
  });

  return {
    labels: Object.keys(counts),
    datasets: [
      {
        label: "Number of Commits",
        data: Object.values(counts),
        backgroundColor: [
          "rgba(255, 99, 132, 0.2)", // color for bugs
          "rgba(54, 162, 235, 0.2)", // color for features
          "rgba(255, 206, 86, 0.2)", // color for chores
          "rgba(75, 192, 192, 0.2)", // color for fixes
          "rgba(153, 102, 255, 0.2)", // color for others
        ],
        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)",
        ],
        borderWidth: 1,
      },
    ],
  };
}
```
4. Finally install yalc globaly `npm i -g yalc`

With yalc you can run `yalc publish` and it will create a local link that you can add to your project like running `npm i pakcage-name` open up your React App, and for now I think it works best in a next JS app running React 17 will be fixing this for all versions. Naviagte there and run `yalc add `your-package-name` and now you can import the component and add the script for extract-commits to your package.json

5. The flow works like this

add script


```

  "scripts": {
    "extract-commits": "extract-commits"
  }

```

run `npm run extract-commits` should see a message that it a file was added to public folder

Got to a page an import the component

`import {CommitGraph} from "your-package-name"`

use it


```

  return (

   <CommitGraph/>

  )

```


That's it 😃

### Conclusion

In conclusion, the use of this NPM package could help you know where your time is spent.

the approach of integrating an emoji-based commit chart offers an engaging and visually intuitive way to track changes and progress. This feature not only adds an element of fun but also enhances the clarity and comprehensibility of project tracking.

I would say if you have something you built internally publish it to NPM, a great way to advertise your other work.

Kindly,

Anders from Wispy Company