-->

dev javascript, chartjs

Chart.js is a popular JavaScript library for creating beautiful charts with JavaScript. In this post, we will have some fun with it by visualising Star Wars data.

Installation

There are various methods to install the library.

When it’s used in a project, I’d recommend using NPM. It’s simple, and it doesn’t involve any external CDN dependencies. You can install it by running the following command:

npm i chart.js

Another way to use Chart.js is using a CDN. For example, you can use Cloudflare CDN:

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.8.0/chart.min.js" integrity="sha512-sW/w8s4RWTdFFSduOTGtk4isV1+190E/GghVffMA9XczdJ2MDzSzLEubKAs5h0wzgSJOQTRYyaz73L3d6RtJSg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

In the sample project, I set the responsive option to false so that the data is more visible. Otherwise, it uses the entire window, making it hard to see the whole chart.

Data Source

In this little project, our data will be coming from Star Wars API

Webpage showing landing page of Star Wars API

It’s a free-to-use API that returns Star Wars-related data which can be fun to use in little projects such as this one.

Source Code

I will not include all the source code in this post as it becomes too lengthy. You can access the complete code here: Source code of the project.

Animated GIF showing various chart types implemented in the sample application

Getting Started

Bar Chart

Let’s get started with a bar chart that shows the number of characters and species shown in each film:

Bar chart showing the number of characters and species in Star Wars movies

  • In this example, we use dynamic data instead of a fixed array
// ...
data: {
labels: data.map(d => d.title),
datasets: [
// ...
  • Chart.js doesn’t have built-in support to generate random colours automatically. So I used a function to return random colour values and called it for each member in the data array.
  • The data we display on the Y-axis is specified in the data.datasets property. I only added two datasets in this example, but you can add as many as you like.

Line chart

The structure is very similar. The main change is to set the type property to the line.

  • In this example, I showed how to pass in object arrays rather than primitives as data.
// ...
data: data.map(d => ({ x: d.name, y: d.height })),
// ...

This way, I was able to specify the x and y-axis values in one statement.

  • One note about responsiveness and how it handles data. The people endpoint returns 82 people, and in the image below, you can see all their heights:

Line chart showing the height of the characters in Star Wars movies

If you disable responsiveness and draw the chart on a smaller canvas, it hides some labels and shows what it can that is still readable. I think this is a smart approach. There is no way to try to put 82 labels in a small space when none of them can be read.

Line chart showing it's responsive and it will remove some labels when there is not enough space

Also, if you hover over the data points on the chart, it shows the label and the value (by which we can learn Yarael Poof is the tallest person with 264 cm. and Yoda is the shortest with 66 cm.)

Pie and Doughnut Charts

These charts are good at visualising a percentage compared to the whole. Unfortunately, Star Wars API didn’t have box office data to show, so I gathered box office values from another site and displayed them in pie and doughnut charts:

Pie chart:

Pie chart showing Star Wars box Office values in USD

Doughnut chart:

Doughnut chart showing Star Wars box Office values in USD

type: 'doughnut'

Both chart types are very similar. The only difference is the middle is empty in the doughnut chart. So the only difference code-wise was to change the type.

Mixed Charts

You can display different types in the same chart. The chart type for this type of chart needs to be a bar. It’s not very intuitive, but it’s how it works.

In the following example, I show the number of planets as a line chart, and the number of vehicles and starships as bar charts, all in the same visual:

Mixed chart showing the number of planets as a line chart, number of vehicles and starships as a bar chart

Other Charts

In addition to the basic charts, Chart.js supports many other charts such as bubble charts, radar charts, scatter charts etc. I’d recommend checking the resources section for documentation and samples to see them in action.

Conclusion

This post covered how to install and use the Chart.Js library, which can be very useful in creating beautiful charts. We used Star Wars API to visualise Star Wars trivia to make the project even more fun!

I hope you found this post valuable and fun. Please let me know what you think in the comments below.

Resources

dev javascript

Any developer who wrote any JavaScript code must have used the console.log() method at some point to log informational messages or for print debugging. However, while that method is quite useful, it is not the only one in our arsenal. So let’s look at some useful methods that are not very commonly known to most developers.

time() / timeLog() / timeEnd()

If you have some long-running tasks and want to get some insights into how long a task takes, these methods are handy. You can start a timer by calling:

console.time();

and end the timer by calling

console.timeEnd();

You can also pass a label to these methods to make the logs more readable. Otherwise, the times are logged under the “default” label.

timeLog() method can be used anywhere between time and timeEnd to log the timer’s current value.

The example below uses all the features discussed above:

console.time();
for (let i = 0; i < 10000000; i++) {
  if (i === 5000000) {
    console.timeLog();
  }
}
console.timeEnd();

It loops 10 million times and logs the time halfway through (console.timeLog()) and at the end (console.timeEnd())

and the output looks like this:

Output of timeLog function showing the time it took for a loop

table()

This neat little feature can be useful when displaying tabular data. In this example, we are going to get the first ten results from people endpoint on Star Wars API and display them in a table:

fetch ("https://swapi.py4e.com/api/people/")
  .then(data => data.text())
  .then(response => { 
    console.table(JSON.parse(response).results);
  });

Let’s check out the results:

Output of table function call showing Star Wars displayed in a table format

And all it takes is five lines of code to produce this!

warn() / error()

console.log() is fine for logging informational statements, but to make the warning and errors more readable, you can use warn() and error() methods. When checking the console, they stand out among all the other log lines.

For example, the following code:

for (let i = 1; i <= 10; i++) {
  console.log(i);
  if (i === 5) {
    console.warn("warning: half-way through");
  }
}
console.error("error: all gone now");

produces this output:

Output of warn and error functions among information lines in the console

debug()

I chose to cover debug in a separate section even though it works very similarly to log(), warn() and error() methods. By default, the log level in browsers is not set to display debug messages.

Default debug levels dropdown opened showing all levels selected except Verbose

So if we run the previous example with log() call replaced by debug() call, the output looks very simplified:

for (let i = 1; i <= 10; i++) {
  console.debug(i);
  if (i === 5) {
    console.warn("warning: half-way through");
  }
}
console.error("error: all gone now");

Output with debugging:

Output showing only warn and error outputs when Verbose is de-selected

If we want to see all the statements, we need to explicitly set the logging level to verbose and get the same results as before.

Debug levels dropdown expanded and showing all levels selected

group() / groupEnd() / groupCollapsed()

group() and groupEnd() let us create indented log entries within a group label.

For example, the following code

console.group("Group 1");
console.log("entry 1");
console.log("entry 2");
console.log("entry 3");
console.groupEnd("Group 1");

console.group("Group 2");
console.log("entry 1");
console.log("entry 2");
console.log("entry 3");
console.groupEnd("Group 2");

produces this result:

group and groupEnd function output

We can also create log entries in a collapsed state so that they can only be viewed when we explicitly expand the log group:

console.group("Group 1");
console.log("entry 1");
console.log("entry 2");
console.log("entry 3");
console.groupCollapsed("Hidden stuff until you expand");
console.log("Hidden entry 1");
console.log("Hidden entry 2");
console.groupEnd("Group 1");

By default, the output looks like this:

group, groupCollapsed and groupEnd function call output. Some lines are shown as collapsed.

To see the contents of the collapsed group, we need to expand it explicitly:

Collapsed lines are expanded to show the originally hidden lines

Conclusion

This post looked into useful Console API methods that are not commonly known. To ensure compatibility, we didn’t look into non-standard methods such as timeStamp() and profile(). Instead, I’d recommend visiting the resources below and looking into all the methods. Some of them are not covered here but might be valuable to you.

Resources

dev dotnet

Command-Line Interfaces (CLI) are invaluable tools for a developer. We use them daily to interact with AWS, Docker, GitHub, dotnet etc. We can develop scripts based on CLI commands to carry out complex tasks. In this post, we are going to develop a CLI for ourselves. Let’s get started!

Getting Started

We are going to use two things:

  • dotnet tool command
  • a very handy NuGet package called CliFx

dotnet tool

The simplest way to describe a dotnet tool is a console application distributed as a NuGet package.

Usually, when you go to a NuGet source site such as Nuget.org, you deal with class libraries. You download the class library and consume it in your application.

Similarly, you can publish your console application as a dotnet tool in NuGet package format. This allows installing applications by simply using dotnet CLI, such as:

dotnet tool install --global --add-source {PACKAGE PATH} {PACKAGE NAME}

To achieve that, all we have to do is create a new console application and modify the csproj file by adding the following lines:

<PackAsTool>true</PackAsTool>
<ToolCommandName>{ COMMAND NAME }</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>

Now let’s have a walkthrough and see it in action:

  1. Create a console application using dotnet CLI:
dotnet new console
  1. Edit the csproj file. In this example, I’m going to use JetBrains Rider IDE to edit, but you can use any IDE/text editor you want:

Rider IDE showing Edit menu expanded and Edit .csproj file selected

  1. Add the following lines inside the PropertyGroup element so that it looks something like this:

IDE showing .csproj file edited and new XML lines added

  1. Run the following command to create the NuGet package:
dotnet pack

Finder window showing NuGet package created as output of dotnet pack command

  1. Install it globally on your computer by running the following command:
dotnet tool install --global --add-source ./nupkg develop-a-cli-with-csharp

Please note the last argument is the name of the root namespace, not the name of the CLI we are creating.

  1. Now you can test the tool simply by running the name of the tool in the terminal:
mycli

and the output should look like this:

Terminal window showing output of mycli command

Great! We have our tool installed nicely on the computer. We can run it anywhere in the terminal (regardless of the path we are in). But there is more to a CLI than simply executing a console application. The most important of a CLI is to have commands and subcommands. For example, when we use the dotnet CLI, we enter the following command:

dotnet tool install --global --add-source ./nupkg develop-a-cli-with-csharp

In this example,

  • dotnet is the name of the CLI
  • tool is the command
  • install is the subcommand
  • The rest are arguments passed to the subcommand

We don’t have any mechanism to understand commands, subcommands and arguments. This is where CliFx comes in.

CliFx

CliFx is a simple to use NuGet package that adds the full capabilities of a CLI to our console application.

  1. Let’s start with installing the package:
dotnet add package CliFx

You should be able to see the package after running the command above:

Rider IDE showing CliFx package added to the project

  1. Replace the Main method with the following code:
using CliFx;

public static class Program
{
    public static async Task<int> Main() =>
        await new CliApplicationBuilder()
            .AddCommandsFromThisAssembly()
            .SetExecutableName("mycli")
            .SetTitle("My CLI")
            .SetDescription("A useful CLI tool to demo")
            .Build()
            .RunAsync();
}
  1. Now, let’s create our commands by creating two new classes: HelloCommand and WorldCommand. They should look like the below:
using CliFx;

public static class Program
{
    public static async Task<int> Main() =>
        await new CliApplicationBuilder()
            .AddCommandsFromThisAssembly()
            .SetExecutableName("mycli")
            .SetTitle("My CLI")
            .SetDescription("A useful CLI tool to demo")
            .Build()
            .RunAsync();
}
[Command("hello world")]
public class WorldCommand : ICommand
{
    public ValueTask ExecuteAsync(IConsole console)
    {
        console.Output.WriteLine("Hello, World!");
        return default;
    }
}
  1. Now run the application in the terminal without any parameters. You should get a nice help output:

Terminal window showing the output of mycli command. Only hello command is shown.

  1. Test the command and subcommand by running the following commands:
dotnet run -- hello
dotnet run -- hello world

The output should look like this:

Terminal window showing application output showing hello and hello world commands executed

Notice that by running the “hello” command, we are executing the ExecuteAsync method in HelloCommand class. WorldCommand is a subcommand of the hello command, so we can execute a different method by running “hello world”.

At this point, our installed tool is not affected by these changes. So we have to pack and update our tool now by running the following commands:

dotnet pack
dotnet tool update --global --add-source ./nupkg develop-a-cli-with-csharp

You can confirm the tool is updated by looking for output like this:

Terminal window showing the output of dotnet tool update command

  1. Finally, open another terminal window and type the CLI name
mycli

and you should see the new help output listing the available commands in the CLI:

Terminal window showing the output of mycli command running as a CLI

Conclusion

CLIs are handy tools for developers. In this post, we looked into creating a CLI capable of creating commands and subcommands. It can also be installed as a dotnet tool and distributed as a NuGet package.

Resources