Chart.Js – HoNoSoFt https://blog.honosoft.com Blog & Roll Fri, 11 Jan 2019 12:52:34 +0000 en-CA hourly 1 https://wordpress.org/?v=6.8.8 https://blog.honosoft.com/wp-content/uploads/2018/06/logo.png Chart.Js – HoNoSoFt https://blog.honosoft.com 32 32 Canvas Chart ➡️ Snapshot ➡️ Manipulate ➡️ Send it to your confluence page (or elsewhere) https://blog.honosoft.com/2019/01/11/canvas-chart-%e2%9e%a1%ef%b8%8f-snapshot-%e2%9e%a1%ef%b8%8f-manipulate-%e2%9e%a1%ef%b8%8f-send-it-to-your-confluence-page-or-elsewhere/?utm_source=rss&utm_medium=rss&utm_campaign=canvas-chart-%25e2%259e%25a1%25ef%25b8%258f-snapshot-%25e2%259e%25a1%25ef%25b8%258f-manipulate-%25e2%259e%25a1%25ef%25b8%258f-send-it-to-your-confluence-page-or-elsewhere https://blog.honosoft.com/2019/01/11/canvas-chart-%e2%9e%a1%ef%b8%8f-snapshot-%e2%9e%a1%ef%b8%8f-manipulate-%e2%9e%a1%ef%b8%8f-send-it-to-your-confluence-page-or-elsewhere/#respond Fri, 11 Jan 2019 12:39:37 +0000 https://blog.honosoft.com/?p=426 Continue Reading]]> Today, let’s create a connection between one of your app using ChartJs + Confluence and send or synchronize a chart. The flow with Confluence server used will be with the basic authentication. In the case you want to build a real plugin/extension to Confluence, please follow the proper guide about it. In this blog post, the basic authentication is simply a mean to an end for quick prototyping.

A complete example of a standalone project can be found on Github (https://github.com/Nordes/HoNoSoFt.PushChartToConfluence.Sample).

Introduction

Like I said, we will have an application serving some charts using the Canvas (HTML5) technology. We will then resize, take a snapshot and then transmit this file to our backend server. Then it will connect to confluence, look at the existing attachments and if it exists it will update using a comment or in the case where the file does not exists, we will be posting a new file. It is nice to update the file, but why not add the file (image) to the page if it is not already there. 🙂

Proposal using a chart

Note that we’re going to add the chart, but the same flow could be used to also update some comment/text. As long as you are familiar with html syntax, you should be able to do something good.

Pre-requisites before starting

  1. If you already have a confluence server or follow the official installation guide from Confluence, skip to #3 (Create a small application)
  2. For the docker users, let’s go towards the evaluation “just because”
    1. docker pull atlassian/confluence-server:latest
      1. About 700mo
    2. While it download, you can start by requesting a KEY
      1. https://my.atlassian.com/license/evaluation
      2. Select a Confluence server license and keep that page open until the docker image is ready.
    3. Start the docker image
      • Command: docker run -v $volume$:/var/atlassian/application-data/confluence --name="confluence" -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server
        • $volume$: C:/demo/your/confluence/home
      • Open site: http://localhost:8090/
    4. During installation, fetch the Server ID and input it in the evaluation page.
      1. Follow the instructions
        1. Select trial
        2. Get the Server ID
        3. Go on your atlassian page already open and paste it
        4. Generate your key
        5. Copy/paste your key in the form within the confluence page
        6. Click next (wait a little) and voilà
        7. Setup a user or two and then let’s start
  3. Create a small application with ChartJs (or use the one built in the previous post )

Back-End

In the back-end, we will first be receiving the image to at least test our local upload between front-end and back-end. After, we will start integrating the Confluence connexion.

Dotnet ImagesController: Receive Images (Part 1)

Within Dotnet Core, we can receive files or forms controls using the IFormCollection. I don’t think it is common to receive such thing, but when talking about receiving files, this comes handy. The client (JavaScript) will then create a collection and send it using a multipart/form-data. In there we can find many things, however what we will only using here are the files (((IFormCollection)MyFormCollection).Files).

Let’s create a controller named ImagesController we will receive the files as stream and then copy those files to a temporary folders. If you already played around with file streaming you can probably skip this part.

[Route("api/[controller]")]
[ApiController]
public ImagesController : ControllerBase {
    // .. some constructor stuff

    /// <summary>
    /// This will save the image in your local "temp" folder.
    /// </summary>
    [HttpPost]
    [ProducesResponseType((int)HttpStatusCode.Created)]
    public async Task<IActionResult> PostAsync(IFormCollection formCollection)
    {
        var files = formCollection.Files;
        long size = files.Sum(f => f.Length);
        List<string> fileList = new List<string>();

        foreach (var formFile in files)
        {
            // full path to file in temp location
            var filePath = Path.GetTempFileName();
            if (formFile.Length > 0)
            {
                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    // Save to file... We could remove the await, have a List<Task<..>> and then do a Task.WhenAll(myList)
                    await formFile.CopyToAsync(stream);
                    fileList.Add(filePath);
                }
            }
        }

        // Example of processed uploaded files returning details of the new file + original request.
        // You shouldn't rely on (or) trust the FileName property without validation.
        return Ok(new { count = files.Count, size, fileList });
    }
}

This is named Part 1 since we will come back in that code in order to add some code to send to confluence.

Front-End

Do the snapshot

From JavaScript, we have two methods that are quite handy to take snapshot of a Canvas. The first one is “toBlob(…)” and the second is “toDataUrl(…)“. While “toBlob” is not supported everywhere, I find it more useful for the demo, otherwise feel free to use “toDataUrl” for all browsers supports except Edge and then convert that base64 into a binary data (image/png). In case you go towards the “toDataUrl” method, don’t forget that you will have to transform the data once it’s server side for something readable (binary) as an image/png.

You will see in the next sub-section how to start from a ChartJs chart which you’d like to push to your back-end having a resolution of 1200px wide. Don’t forget that if you use an adaptive screen UX, it will not be displayed as 1200px until you do a snapshot for a fraction of a second.

How does the resizing work?

As you already know, a ChartJs canvas does not resize automatically at your wishes and if you want a snapshot, you are required to hack your way. Next, we are going to have the following flow:

Interesting part in the JS, I will skip the trivial step of generating a chart since you’re I suspect that you’re already able to do so.

// Some code (I used VueJs, but other language should look alike)

snapshot: function () {
  // Resize to desired size, bigger it is, more heavy will be the blob file.
  this.$refs.chartContainer.style.width = '1200px'
  this.chart.resize()
  var ctx = this

  // Do a snapshot
  this.$refs.chart.toBlob(function (blob) {
    // Resize back to original size
    ctx.$refs.chartContainer.style.width = ""
    ctx.chart.resize()

    // Prepare the form post
    // The file name used will be the chart title, but depending on your case, you
    // might want to have something more precise based on parameter (hash, encoding, something). 
    var filename = `${ctx.chartConfig.options.title.text}.png`
    var data = new FormData()
    data.append('file', blob, filename)

    const config = {
        headers: { 'content-type': 'multipart/form-data' }
    }

    // Post the file to your backend
    ctx.$http.post('/api/images', data, config)
    // Later in the article it should become as
    // ctx.$http.post('/api/images/confluence/{desiredPageId}', data, config)
  }, "image/png", 0.95);
}

// Some code

As you can see, I resize to 1200px => snapshot (toBlob) => resize back to original => send the file as multipart/form-data to the api.

The file should normally be created within your temp folder and the exact location will come back through the API. It is also part of the current data contract. Now that you have that, you can consider yourself ready for the next step, which is to transfer that buffered binary data directly to confluence using their API’s.

Back-end Part 2 – Use confluence API’s

Now that we know we can receive file and save it, we now simply need to use the Confluence API’s from Atlassian. The related documentation for that is:

API’s that we’re going to use for this demo are:

  • [GET] api/content/{pageId}/child/attachment?filename={formFile.FileName}&expand=version
    • Search/Retrieve the existing attachment. In case it does not exists, it will sends back an empty array.
  • [POST] api/content/{pageId}/child/attachment/{attachmentData.Id}/data
    • Update in case of existing attachment
  • [POST] api/content/{pageId}/child/attachment
    • Create the attachment resource if it was not already existing
  • [GET] api/content/{pageId}?expand=version,body.storage
    • Retrieve the details on the current page, especially the body specified in readable/editable way (storage). The version is also mandatory when you want to update the page.
  • [PUT] api/content/{pageId}
    • Api used in order to update the confluence page.

Update the ImagesController to forward to Confluence

There’s maybe more code than required in the controller. The proper approach would be to use a IDataProvider injected (for UT) and then implement the provider using the IHttpClientFactory. That way, all would be testable and also it would also put the logic where it should be. However, let’s put all for now in one place and please adapt for your needs.

// some code in the controller

        /// <summary>
        /// Receive 1 or more images to be sent to Confluence server.
        /// </summary>
        /// <param name="formCollection">The form data (only files are being consumed)</param>
        /// <param name="pageId">The confluence page Id</param>
        /// <remarks>
        /// More details can be found at https://developer.atlassian.com/server/confluence/confluence-rest-api-examples/
        /// </remarks>
        [HttpPost("confluence/{pageId}")]
        [ProducesResponseType((int)HttpStatusCode.Created)]
        public async Task<IActionResult> PostToConfluence(IFormCollection formCollection, int pageId)
        {
            var files = formCollection.Files;
            long size = files.Sum(f => f.Length);
            List<string> fileList = new List<string>();
            var forwardAttachmentTasks = new List<Task<FileTransferResult>>();

            foreach (var formFile in files)
            {
                if (formFile.Length > 0)
                {
                    forwardAttachmentTasks.Add(ForwardFileToConfluence(pageId, formFile));
                }
            }

            // In case we had multiple tasks at the same time.
            await Task.WhenAll(forwardAttachmentTasks.ToArray()).ConfigureAwait(false);
            await UpdatePage(pageId, forwardAttachmentTasks).ConfigureAwait(false);

            return StatusCode((int)HttpStatusCode.InternalServerError, new { count = files.Count, size, fileList });
        }

        private async Task UpdatePage(int pageId, List<Task<FileTransferResult>> forwardTasks)
        {
            if (forwardTasks.Any())
            {
                // Update the page
                var pageContentResult = await _confluenceHttpClient.GetAsync($"content/{pageId}?expand=version,body.storage");
                var pageContentData = JsonConvert.DeserializeObject<Models.Confluence.Content.ContentStorage>(await pageContentResult.Content.ReadAsStringAsync());

                string newContent = string.Empty;
                foreach (var sentAttachmentTask in forwardTasks)
                {
                    var sentAttachment = await sentAttachmentTask;
                    if (sentAttachment != null && sentAttachment.Results.Any())
                    {
                        // Add the file if not present on the page.
                        var fileName = sentAttachment.Results.First().Title;
                        if (pageContentData.Body.Storage.Value.IndexOf($"<ri:attachment ri:filename=\"{fileName}\" />") == -1)
                        {
                            newContent += $"<h2>You've just pushed: {fileName}</h2><p><ac:image><ri:attachment ri:filename=\"{fileName}\" /></ac:image></p>";
                        }
                        // Else: Nothing to do, it's already on the page somewhere.
                    }
                }

                // Update object (camelCase mandatory, so use a proper serializer in real life scenario)
                var updateQuery = new
                {
                    id = pageContentData.Id,
                    title = pageContentData.Title,
                    status = pageContentData.Status,
                    type = pageContentData.Type,
                    version = new { number = pageContentData.Version.Number + 1 },
                    body = new
                    {
                        storage = new
                        {
                            value = pageContentData.Body.Storage.Value + newContent,
                            representation = "storage"
                        }
                    }
                };

                var result = await _confluenceHttpClient.PutAsJsonAsync($"content/{pageId}", updateQuery);
            }
        }

        private async Task<FileTransferResult> ForwardFileToConfluence(int pageId, IFormFile formFile)
        {
            // Start getting if attachment exists
            var getIfAttachmentExists = _confluenceHttpClient.GetAsync($"content/{pageId}/child/attachment?filename={formFile.FileName}&expand=version").ConfigureAwait(false);
            // While previous request goes on, let's get the file.
            byte[] data;
            using (var br = new BinaryReader(formFile.OpenReadStream()))
            {
                data = br.ReadBytes((int)formFile.OpenReadStream().Length);
            }

            ByteArrayContent bytes = new ByteArrayContent(data);
            MultipartFormDataContent multipartContent = new MultipartFormDataContent();
            multipartContent.Add(bytes, "file", formFile.FileName);

            var attachmentRequestData = await getIfAttachmentExists;
            if (attachmentRequestData.IsSuccessStatusCode && attachmentRequestData.StatusCode == HttpStatusCode.OK)
            {
                // Page exists and no errors...
                var attachmentRequestContent = await attachmentRequestData.Content.ReadAsStringAsync().ConfigureAwait(false);
                var attachmentData = JsonConvert.DeserializeObject<FileSearch>(attachmentRequestContent);

                HttpResponseMessage putAttachmentResponse;
                // Update existing data.
                if (attachmentData.Size == 1)
                {
                    multipartContent.Add(new StringContent($"Automatic update/upload from TestApplication ;)."), "comment");
                    putAttachmentResponse = await _confluenceHttpClient.PostAsync(
                        $"content/{pageId}/child/attachment/{attachmentData.Results.First().Id}/data",
                        multipartContent);

                    // Result is 1 "item"
                    var content = await putAttachmentResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
                    var result = JsonConvert.DeserializeObject<Models.Confluence.Result>(content);

                    return new FileTransferResult() { Results = new Models.Confluence.Result[] { result }, Size = 1 };
                }
                else
                {
                    // Create the attachment
                    multipartContent.Add(new StringContent($"Automatic upload from TestApplication ;)."), "comment");
                    putAttachmentResponse = await _confluenceHttpClient.PostAsync(
                        $"content/{pageId}/child/attachment",
                        multipartContent);

                    // Result is a list of item.
                    var content = await putAttachmentResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
                    return JsonConvert.DeserializeObject<FileTransferResult>(content);
                }
            }

            return default(FileTransferResult);
        }

// some code in the controller

It’s a lot of code. By reading it, it should be really easy to understand. During the file upload to Atlassian Confluence, we add a comment (updated or created) and the version gets updated. That way you could show the changes over time.

What else you could do?

  • Transform the backend in order to have a provider injected
  • Create UT
  • Create IT
  • Change the token used in order to use the official Atlassian flow for plugins
  • Automate everything back-end by creating a scheduled job (not tested, but should be feasible)
    1. Selenium
    2. Image docker + chromium
    3. Execute the javascript using chrome Webdriver
    4. Send the image using a job.
  • Send a SnapShot (not necessarily from Charts) while building your backend app in a pipeline.

Conclusion

Thank you for reading and I hope you have learned something today, or at least enjoyed this article.

]]>
https://blog.honosoft.com/2019/01/11/canvas-chart-%e2%9e%a1%ef%b8%8f-snapshot-%e2%9e%a1%ef%b8%8f-manipulate-%e2%9e%a1%ef%b8%8f-send-it-to-your-confluence-page-or-elsewhere/feed/ 0
ChartJs with SonarQube API’s – Make your own reports https://blog.honosoft.com/2018/11/22/chartjs-with-sonarqube-apis-make-your-own-reports/?utm_source=rss&utm_medium=rss&utm_campaign=chartjs-with-sonarqube-apis-make-your-own-reports https://blog.honosoft.com/2018/11/22/chartjs-with-sonarqube-apis-make-your-own-reports/#respond Thu, 22 Nov 2018 14:30:28 +0000 https://blog.honosoft.com/?p=390 Continue Reading]]> Recently asked to do reports every two weeks to all the teams about their good/bad behaviors and at the same time look if the guidelines were somewhat respected, I decided to build a dashboard using ChartJs and a connection to SonarQube via a local proxy built in Dotnet Core. That report is not built in order to hit the team or anything, it’s more in order to know our current situation and what we should improve. We can also relate the data from Jira (using the api’s) in order to know if we’re improving our bug ratio.

This article will only do a quick start using the template I’ve created recently in Dotnet Core. I won’t put too much details around the code since it’s quite straight forward.

Pre-requisites

  • Dotnet core 2.1 (at the time of writing)
  • NodeJs + NPM (>10 at the time of writing)
  • Visual Studio Code OR Visual Studio Community/Professional
  • A browser (Chrome, or anything else)

Install a quick start from a Dotnet Core template

As mentioned before, you will be using a quick start I created. I use it quite often, so there’s improvement in it happening regularly. So I suggest you to look for updates once in a while. It gives a front-end that could run without backend, and a backend in dotnet core 2.1 ready to serve API’s (including swagger).

> dotnet new -i HoNoSoFt.DotNet.Web.Spa.ProjectTemplates

Once installed, let’s create our template (vuejs-picnic-table):

> mkdir Sonar.Reports
> cd Sonar.Reports
> dotnet new vuejs-picnic-table

And to be sure it works, then let’s run at least in development mode.

> npm install
> dotnet run --environment="Development"

The installation of the NPM packages are needed most likely the first time due to the nature of the project. This could be automated, but would make the project takes a lot of time before even being able to start editing any files. (Personal choice here)

The result should look like the following screen (https://localhost:5001/) OR the demo page available here.

You might get an error the time Webpack generate the files. In that case, simply hit refresh (F5)

Once the application is running, keep it running while you edit your files (next step). You will simply have to hit F5 to refresh the changes.

Clean or add route to the template

Delete all the un-necessary things or simply add a new page. We’ll go here with the latter.

  • Create the file: ./ClientApp/pages/sonar.vue
    • <template>
       <div>
         <page-title title="Sonar" />
       </div>
      </template>
      
      <script>
      export default {
        
      }
      </script>
      
  • Add the new route for the page in ./ClientApp/router/routes.js
    • Add a import in the import section: import Sonar from 'pages/sonar'
    • Add the route after about:  { name: 'Sonar', path: '/:lang?/sonar', component: Sonar, display: 'Sonar', i18n: 'route.sonar', icon: 'icon-home', meta: { order: 6 } },
  • Add the translation/I18n for the new menu in ./ClientApp/_i18n/lang/en.js
    • In the routes group under about, add: sonar: 'Sonar'
  • Save all this and hit F5 in the browser

Congratulation! You’ve created your first page in VueJs. Now it is the time to fill the data and before that, we need to create a service provider.

Access to Sonar Qube API’s from C#

Create your provider in C#, because if you’re not on the same server as your SonarQube, you might get some issue by only using the front-end. Here, we will be creating a simple proxy without any parameters. For those of you who didn’t know, the API is available at your SonarQube URL like the following: https://mySonarQube.url.com/api/.

Generate an API key for your user

  • Go in your account (top right icon)
  • Go under security
  • Generate a new token (The name does not matters, but let’s name it: Sonar.Reports)
  • Copy and paste the key somewhere you won’t loose. Otherwise you will have to re-generate the API token.

Open your project in VS Code or Visual studio

If you’re lucky enough, you will have a Properties folder, otherwise you will have to create it and add the file launchSettings.json

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:21535",
      "sslPort": 44307
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/values",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Sonar.Reports": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/values",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Update your ./Startup.cs and add a new Service configuration:

public void ConfigureServices(IServiceCollection services){
  // ...
  services.AddHttpClient();
  // ...
}

Create the file ./Controllers/SonarController.cs and add the following code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace Sonar.Reports.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SonarController : ControllerBase
    {
        private HttpClient _sonarHttpClient;

        public SonarController(IHttpClientFactory httpClientFactory){
          _sonarHttpClient = httpClientFactory.CreateClient("sonar");
          _sonarHttpClient.BaseAddress = new Uri("https://sonar.yourSonarQube.com/api/");
          var basicToken = Convert.ToBase64String(Encoding.UTF8.GetBytes("f6d6fecfdcd7836612a0fef4a3f307b7725f8387:"));
          _sonarHttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", basicToken);
        }

        /// <summary>
        /// GET Sonar data
        /// </summary>
        /// <returns>Returns an enumerable of value</returns>
        [ProducesResponseType((int)System.Net.HttpStatusCode.OK)]
        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var result = await _sonarHttpClient.GetAsync("measures/search_history?component=yourComponentName&metrics=bugs%2Ccode_smells%2Cvulnerabilities%2Creliability_rating%2Csecurity_rating%2Csqale_rating&ps=1000");
            
            // Not perfect but it will do for the sample.
            return Content(await result.Content.ReadAsStringAsync());
        }
    }
}

Where:

  • www.yourSonarQube.com is your address of your Sonar Qube
  • The token you’ve created earlier replaces f6d6fecfdcd7836612a0fef4a3f307b7725f8387 (Don’t forget to keep the “:”, it is mandatory in a basic authentication, where it’s usually user:password in Base 64)
  • yourComponentName is replaced by your real component name in Sonar Qube (To get it, open sonar qube, go in your project you want to test, and then take the value from the URL. It’s always there.)

Now, restart your application (Ctrl+C and then re-type dotnet run, if you’re in console mode).

You can test the endpoint by going to http://localhost:5001/swagger/ and use the swagger UI in order to test it. If you have errors, you will see it in your console (or output in Visual Studio).

Add the ChartJS to your project

Go in your root folder where your project is located (csproj) and then type the following command in order to install Chart.Js:

> npm install chart.js --save

Now that you have installed this new dependency, you can add the Chart.Js within your VueJs application.

Edit your ./ClientApp/pages/sonar.vue and add a default Chart.Js available through all their demo on their site. Here, I will be using the line API

<template>
  <div>
    <page-title title="Sonar" />

    <canvas ref="chart" id="canvas"></canvas>
  </div>
</template>

<script>
// Import ChartJS
import Chart from 'chart.js'

// Colors
var chartColors = [
  'rgb(255, 99, 132)',
  'rgb(255, 159, 64)',
  'rgb(255, 205, 86)',
  'rgb(75, 192, 192)',
  'rgb(54, 162, 235)',
  'rgb(153, 102, 255)',
  'rgb(201, 203, 207)'
]

export default {
  data () {
    return {
      chart: null,
      chartConfig: {
        type: 'line',
        data: {
          labels: [],
          datasets: []
        },
        options: {
          responsive: true,
          title: {
            display: true,
            text: 'Enjoy your Chart.Js with Sonar'
          },
          tooltips: {
            mode: 'index',
            intersect: false,
          },
          hover: {
            mode: 'nearest',
            intersect: true
          },
          scales: {
            xAxes: [{
              display: true,
              type: 'time',
              scaleLabel: {
                display: true,
                labelString: 'Date'
              }
            }],
            yAxes: [{
              display: true,
              scaleLabel: {
                display: true,
                labelString: 'Value'
              }
            }]
          }
        }
      }
    }
  },

  mounted () {
    this.chart = new Chart(this.$refs.chart, this.chartConfig)
    this._loadSonarData()
  },

  methods: {
    _loadSonarData: async function () {
      let response = await this.$http.get(`./api/sonar`)
      var sonarData = response.data

      // We now have measures + data
      var measures = sonarData.measures
      let allLabels = []
      for (var m = 0; m < measures.length; m++) {
        let currentColor = chartColors[m % chartColors.length]
        var data = {
          label: measures[m].metric,
          backgroundColor: currentColor,
          borderColor: currentColor,
          tension: 0, // If you don't want bezier curves
          data: [],
          fill: false,
        }

        for (var h = 0; h < measures[m].history.length; h++) {
          let date = new Date(measures[m].history[h].date)
          if (allLabels.indexOf(date) < 0) {
            allLabels.push(date)
          }

          data.data.push(measures[m].history[h].value)
        }
        
        this.chartConfig.data.datasets.push(data)
      }

      this.chartConfig.data.labels = allLabels
      this.chart.update()
    }
  }
}
</script>

Results

If you’ve followed and didn’t make any big mistakes, the result should looks somewhat like the following

Next step?

Basically, yes, there’s a next step. Since you have been able to call the Sonar Qube Api’s, you now know that you can also configure all the requests to it. For example, you could say “I want data for the last 30 days” or in my case, for the last 3 sprints and then draw vertical lines (using a plugin) in order to show exactly where are the sprint and how it went (For example: rush at the end or not).

You could also add a dropdown at the top, or different menu or customization for other projects/stats you’d like to have.

Since you’re using the canvas, don’t forget you could also make a download or extract image using JavaScript. In a way, you could auto-generate a Powerpoint or word document, or other since you have all the data required to do so.

Conclusion

I hope you’ve enjoyed this post.

]]>
https://blog.honosoft.com/2018/11/22/chartjs-with-sonarqube-apis-make-your-own-reports/feed/ 0