Guide on implementing a circular progress bar within a Vue component

I am currently developing a Vue component for my application that serves as a countdown timer, starting from X minutes and ending at 00:00.

Although I understand that animating with svg can achieve the desired effect, I lack the necessary expertise in using any svg libraries.

The animation I need for my progress component should move smoothly along a path based on time, with nodes being added or updated accordingly.

Here is my existing countdown component:

var app = new Vue({
  el: '#app',
  data: {
    date: moment(2 * 60 * 1000)
  },
  computed: {
    time: function(){
      return this.date.format('mm:ss');
    }
  },
  mounted: function(){
  var timer = setInterval(() => {
      this.date = moment(this.date.subtract(1, 'seconds'));
        
      if(this.date.diff(moment(0)) === 0){
        clearInterval(timer);
        
        alert('Done!');
      }
    }, 1000);
  }
});
<script src="https://momentjs.com/downloads/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>

<div id="app">{{ time }}</div>

Here is the svg code for the progress circle:

<svg x="0px" y="0px" viewBox="0 0 90 90">
    <style type="text/css">
        .st0{fill:#FFFFFF;}
        .st1{fill:none;stroke:#B5B5B5;stroke-miterlimit:10;}
        .st2{fill:none;stroke:#408EFF;stroke-linecap:round;stroke-miterlimit:10;}
        .st3{fill:#408EFF;}
    </style>
    <rect class="st0" width="90" height="90"/>
    <circle class="st1" cx="45" cy="45" r="40"/>
    <path class="st2" d="M45,5c22.1,0,40,17.9,40,40S67.1,85,45,85S5,67.1,5,45S22.9,5,45,5"/>
    <circle class="st3" cx="45" cy="5" r="3"/>
</svg>

How can I go about achieving the intended outcome?

I would greatly appreciate any assistance provided.

Answer №1

To create the arc, it is essential to have a good understanding of SVG shapes, especially the <path> element.

Here's an illustration:

Vue.component('progress-ring', {
  template: '#progress-ring',
  props: {
    value: {
      type: Number,
      default: 0,
    },
    min: {
      type: Number,
      default: 0,
    },
    max: {
      type: Number,
      default: 1,
    },
    text: {
      type: null,
      default: '',
    },
  },
  computed: {
    theta() {
      const frac = (this.value - this.min) / (this.max - this.min) || 0;
      return frac * 2 * Math.PI;
    },
    path() {
      const large = this.theta > Math.PI;
      return `M0,-46 A46,46,0,${large ? 1 : 0},1,${this.endX},${this.endY}`;
    },
    endX() {
      return Math.cos(this.theta - Math.PI * 0.5) * 46;
    },
    endY() {
      return Math.sin(this.theta - Math.PI * 0.5) * 46;
    },
  },
});

new Vue({
  el: '#app',
});
body {
  font-family: sans-serif;
}

.progress-ring {
  width: 100px;
  height: 100px;
}

.progress-ring-circle {
  stroke: rgba(0, 0, 0, 0.1);
  stroke-width: 1;
  fill: none;
}

.progress-ring-ring {
  stroke: #007fff;
  stroke-width: 2;
  fill: none;
}

.progress-ring-end {
  fill: #007fff;
}
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>

<div id="app">
  <progress-ring :min="0" :max="100" :value="40" text="12:34"></progress-ring>
</div>

<template id="progress-ring">
  <svg class="progress-ring" viewBox="-50,-50,100,100">
    <circle class="progress-ring-circle" r="46"/>
    <path class="progress-ring-ring" :d="path"/>
    <circle class="progress-ring-end" :cx="endX" :cy="endY" r="4"/>
    <text alignment-baseline="middle" text-anchor="middle">{{ text }}</text>
  </svg>
</template>

To animate it, you can simply manipulate the value prop using JavaScript, utilizing functions like setInterval or other techniques.

Answer №2

Another way to implement this is by defining the path in an array where each node represents a step in the path. Then, you can push each path node to the current progress path at regular intervals.

Here's a demonstration of this concept:

var app = new Vue({
  el: '#app',
  data: {
    date: moment(2 * 60 * 1000),
    pathRoute: ['M45 5', 'c22.1 0 40 17.9 40 40','S67.1 85 45 85','S5 67.1 5 45','S22.9 5 45 5'],
    pathProgess: [],
    stepIndex: 0
  },
  computed: {
    time: function(){
      return this.date.format('mm:ss');
    },
    computedProgress: function () {
      return this.pathProgess.join(' ')
    }
  },
  mounted: function(){
  var timer = setInterval(() => {
      this.date = moment(this.date.subtract(1, 'seconds'));
      this.$set(this.pathProgess, this.stepIndex, this.pathRoute[this.stepIndex])
      this.stepIndex++
      if(this.date.diff(moment(0)) === 0){
        clearInterval(timer);
      }
    }, 1000);
  }
});
.st0{fill:#FFFFFF;}
.st1{fill:none;stroke:#B5B5B5;stroke-miterlimit:10;}
.st2{fill:none;stroke:#408EFF;stroke-linecap:round;stroke-miterlimit:10;}
.st3{fill:#408EFF;}
<script src="https://momentjs.com/downloads/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>

<div id="app">
<p>{{computedProgress}}</p>
<svg x="0px" y="0px" viewBox="0 0 90 90">
    <rect class="st0" width="90" height="90"/>
    <circle class="st1" cx="45" cy="45" r="40"/>
    <text class="circle-chart-percent" x="20.91549431" y="40.5" font-size="8">{{time}}</text>
    <path class="st2" :d="computedProgress"/>
    <circle class="st3" cx="45" cy="5" r="3"/>
</svg>

</div>

Alternatively, you can refer to this answer on another question, which explains how to calculate the path in real-time.

var app = new Vue({
  el: '#app',
  data: {
    date: moment(2 * 60 * 1000),
    pathProgess: ''
  },
  computed: {
    time: function(){
      return this.date.format('mm:ss');
    }
  },
  mounted: function(){
    let maxValue = this.date.diff(moment(0), 'seconds') //total seconds
  var timer = setInterval(() => {
      this.date = moment(this.date.subtract(1, 'seconds'))
      let curValue = this.date.diff(moment(0), 'seconds') // current seconds
      this.pathProgess = this.describeArc(45, 45, 40, 0, (maxValue-curValue)*360/maxValue)
      if(this.date.diff(moment(0)) === 0){
        clearInterval(timer);
      }
    }, 1000);
  },
  methods: {
      polarToCartesian: function (centerX, centerY, radius, angleInDegrees) {
        var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;

        return {
          x: centerX + (radius * Math.cos(angleInRadians)),
          y: centerY + (radius * Math.sin(angleInRadians))
        };
      },
      describeArc: function (x, y, radius, startAngle, endAngle){

          var start = this.polarToCartesian(x, y, radius, endAngle);
          var end = this.polarToCartesian(x, y, radius, startAngle);

          var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";

          var d = [
              "M", start.x, start.y, 
              "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
          ].join(" ");

          return d;       
      }
  }
});
.st0{fill:#FFFFFF;}
.st1{fill:none;stroke:#B5B5B5;stroke-miterlimit:10;}
.st2{fill:none;stroke:#408EFF;stroke-linecap:round;stroke-miterlimit:10;}
.st3{fill:#408EFF;}
<script src="https://momentjs.com/downloads/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>

<div id="app">
<p>{{pathProgess}}</p>
<svg x="0px" y="0px" viewBox="0 0 90 90">
    <rect class="st0" width="90" height="90"/>
    <circle class="st1" cx="45" cy="45" r="40"/>
    <text class="circle-chart-percent"x="20.91549431"y="40.5"font-size="8">{{time}}</text>
    <path class="st2" :d="pathProgess"/>
    <circle class="st3" cx="45" cy="5" r="3"/>
</svg>

</div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Updating the table row by extracting data and populating it into a form

As part of my project, I am implementing a feature where I can input 'Actors' into a table using a Form. This functionality allows me to select a row in the table and retrieve the data of the chosen Actor back into the form for updating or deleti ...

Exploring the use of color in text embellishments

Is it possible to change the color of the underline on text when hovering, while keeping it different from the text color? I have successfully achieved this in Firefox using the property "-moz-text-decoration-color". However, this does not work in other m ...

Creating a dropdown feature for menu items in Vue when the number or width of items exceeds the menu bar's limits

I am working on a navigation bar that displays menu items as tabs. One issue I am encountering is when the number of menu items exceeds the space available, I need to move the excess items into a dropdown menu (showmore) using Vue. Here is an example of t ...

VueJS component fails to remain anchored at the bottom of the page while scrolling

I am currently using a <md-progress-bar> component in my VueJS application, and I am trying to make it stay fixed at the bottom of the screen when I scroll. I have attempted to use the styles position: fixed;, absolute, and relative, but none of them ...

Angular 6 - Consistently returning a value of -1

I'm facing an issue with updating a record in my service where the changes are not being reflected in the component's data. listData contains all the necessary information. All variables have relevant data stored in them. For example: 1, 1, my ...

There is only a singular model connected in the mongoose schema

After examining my connection, I received the following output: // Establish a connection with Mongo const promise = mongoose .connect(db, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true, useFindAndModify: fals ...

An error was encountered with Ajax and JSONP due to an unexpected token causing a SyntaxError

Currently attempting to retrieve information from the host at 000webhost The data is in JSONP format: { "categories": { "category": [ { "name": "Android", "parent": "Computer Science", "ID": "2323" }, { ...

Troubleshooting a Vue 2 component's prop receiving an incorrect value

I'm currently working on creating a menu that navigates between categories. When a category has a sub-category, it should return a boolean value indicating whether it has a sub-category or not. <template> <select><slot></slot ...

Guide on incorporating a URL link into an <a class=> tag

Working on a Wordpress website, I encountered an issue in the widget section where I can't get any URL code to work! Here is the current code snippet; [one_third] <a class="home-buttons" href="http://example.com/mortgage-calculator"><i clas ...

What is the optimal approach for managing script initialization on both desktop and mobile devices?

I have implemented a feature on my website that can detect whether the viewer is using a mobile device. Additionally, I have created a JavaScript script that adjusts settings based on whether the user is on a mobile device or not. However, I am wondering ...

Displaying Keystonejs list items on every page

Is there a more effective method to efficiently load the content of a Keystone's list on every page, rather than calling it individually in each view? Could this be achieved through middleware.js? The objective is to create a dropdown for the navigat ...

Bootstrap navbar failing to be responsive - collapses instantly

Currently, I am in the process of developing a MEAN stack application and using Bootstrap for styling purposes. While working on the project, I made some modifications by adding routing and ngIf statements. But now, I have encountered an issue where the na ...

Tips for updating the Vuetify 3 fontDisplayStyle

I've noticed that Vuetify 3 uses the Roboto font by default, but I would like to switch it to Poppins. Unfortunately, I'm having trouble finding clear instructions on how to make this change. I attempted to modify the font-family within the styl ...

Tips on sorting items in React

Hey there, I'm a beginner at react and currently working on my first ecommerce website. My main concern is about filtering products by size. I'm struggling with the logic behind it. Any help or guidance would be greatly appreciated. I also attem ...

Utilizing correct Django CSRF validation when making a fetch post request

I've been experimenting with JavaScript's fetch library to perform a form submission on my Django application. Despite my efforts, I keep running into CSRF validation issues. The Ajax documentation suggests specifying a header, which I have atte ...

Encountering a 500 error while attempting to deploy a Django Rest-Vue project on Heroku

After successfully deploying my first project to Heroku, I encountered an issue with consuming the API. The API endpoint is located at https://segmentacion-app.herokuapp.com/cluster/, and the frontend can be accessed at https://cluster-app-wedo.herokuapp.c ...

The most efficient method for manipulating the DOM in React for superior performance

When it comes to animating a DOM element with a number sequence going from 0 to N using a tween library in React, what is the most efficient and reliable approach? Would using setState be considered ideal, even when combined with the shouldComponentUpdate ...

Utilize CSS to incorporate special characters as list markers

Is there a way to use CSS to make the list marker in an HTML list display as "►"? ...

What steps should be taken to avoid an event from occurring when an error message is encountered?

I have a dropdown list of cars where an error message is displayed if any of them becomes inactive. When the error message is shown, clicking on the Route Car button should prevent any event from occurring, i.e., no modal popup should be displayed. How ca ...

Adjustment to website design when decreasing browser size

As I decrease the size of the browser window, I notice that the top and bottom footers begin to shrink, causing the content within them (such as the navigation bars) to overlap with the main text. Can anyone explain why this happens? I apologize if my qu ...