0

I've been trying to create this design using HTML, CSS and JS (particularly chart.io). I am wondering if there is a better way to do this without relying much on JS. The reason for this is that, this particular screenshot below is for a report. So I need to make sure that the design renders graciously with all browsers. And if a user decides to save it as PDF, the report should not have any problem rendering on different PDF viewers. I'm worried about having too much JS as I've had issues before where the designs break in iOS and Acrobat.

For starters, I'll use 12 o'clock as the top most part of the progress.

  • If the progress bar is exactly 12 o'clock, it has a blue gradient.
  • If it less than 12 oclock, it has a faint green gradient and not filling up the whole progress.
  • But if it goes beyond 12 o'clock, it's replaced by a purple gradient with the end having darker gradient and a curved end. The endings of the bar is also determined by how far it is from 12'oclock.

enter image description here

This is what I currently have at the moment.

    Chart.register(ChartDataLabels);

    const currentAge = 42;
    const biologicalAge = 44.37;
    const fullCircle = 440;
    const percent = Math.min(biologicalAge / currentAge, 1);
    const offset = fullCircle * (1 - percent);

    document.querySelector("circle[stroke-dashoffset='{{DASH_OFFSET}}']")
            .setAttribute("stroke-dashoffset", offset);

    const ctx = document.getElementById('ageChart').getContext('2d');
    const dataPoints = [40.44, 45.54, 44.37];

    const ageChart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: ['Jan 2024', 'Apr 2024', 'Jul 2024'],
        datasets: [{
          label: 'Biological Age',
          data: dataPoints,
          fill: false,
          tension: 0.4,
          borderColor: function(context) {
            const chart = context.chart;
            const {ctx, chartArea} = chart;
            if (!chartArea) return;
            const gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0);
            gradient.addColorStop(0, '#36d1dc');
            gradient.addColorStop(1, '#a646d7');
            return gradient;
          },
          borderWidth: 3,
          pointRadius: 0
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        layout: {
          padding: {
            left: 20,
            right: 20
          }
        },
        scales: {
          y: {
            suggestedMin: 35,
            suggestedMax: 50,
            ticks: {
              stepSize: 5
            },
            grid: {
              drawTicks: true,
              drawOnChartArea: true
            }
          },
          x: {
            grid: {
              drawTicks: false,
              drawOnChartArea: false,
              drawBorder: false,
              color: 'transparent',
              lineWidth: 0
            },
            border: {
              display: false
            },
            ticks: {
              padding: 8
            },
            offset: true
          }
        },
        plugins: {
          legend: {
            display: false
          },
          tooltip: {
            callbacks: {
              label: ctx => `Age: ${ctx.raw}`
            }
          },
          datalabels: {
            align: 'center',
            anchor: 'center',
            formatter: (value) => value.toFixed(2),
            backgroundColor: (context) => context.dataIndex === context.dataset.data.length - 1 ? '#000' : '#fff',
            borderRadius: 999,
            color: (context) => context.dataIndex === context.dataset.data.length - 1 ? '#fff' : '#000',
            font: {
              weight: 'bold'
            },
            padding: 6,
            borderWidth: 1,
            borderColor: '#000'
          }
        }
      },
      plugins: [ChartDataLabels]
    });
    body {
      font-family: sans-serif;
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: #fff;
      margin: 0;
      padding: 2rem;
      width: 700px;
    }

    .progress-container {
      position: relative;
      width: 160px;
      height: 160px;
      margin-bottom: 2rem;
    }

    .progress-container svg {
      transform: rotate(-90deg);
    }

    .progress-text {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      text-align: center;
    }

    .progress-text h2 {
      margin: 0;
      font-size: 2rem;
    }

    .chart-container {
      width: 440px;
      height: 150px;
    }
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Age Progress and Graph</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>

</head>
<body>
  <div class="progress-container">
    <svg width="160" height="160">
      <circle
        cx="80"
        cy="80"
        r="70"
        stroke="#eee"
        stroke-width="8"
        fill="none"
      ></circle>
      <circle
        cx="80"
        cy="80"
        r="70"
        stroke="url(#gradient)"
        stroke-width="8"
        fill="none"
        stroke-dasharray="440"
        stroke-dashoffset="{{DASH_OFFSET}}"
        stroke-linecap="round"
      ></circle>
      <defs>
        <linearGradient id="gradient" x1="1" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#a646d7" />
          <stop offset="100%" stop-color="#e84fd1" />
        </linearGradient>
      </defs>
    </svg>
    <div class="progress-text">
      <h2 style="font-weight:300;">44.37</h2>
      <small>Older ⤴</small>
    </div>
  </div>

  <div class="chart-container">
    <canvas id="ageChart"></canvas>
  </div>
</body>

I would greatly appreciate if you could provide some recommendations or if you could help me with this. Thank you in advance.

4
  • How is the PDF being created? Does the output from your code example result in a correct PDF? Is there a problem with a particular device/browser? Commented Apr 11 at 2:18
  • The report is initially generated in HTML but users have a way to print preview and save as PDF. Usually, this is where the issue starts. Commented Apr 11 at 20:01
  • What is the issue here? Is it how to draw the gauge/meter and graph the right/required way or is it the issue of printing? In relation to the later, you current solution draw an SVG and a canvas element -- are these not printed correctly? If they are not, it is difficult to help because the issue (printing problem) is not reproducible. Another question would be: Is the real issue that the styling of the page messes up the print. In that case, we need more info (CSS code and/or screen dump). Commented Apr 12 at 10:55
  • You are asking if there is a better way, not using JS. Would it be an option to generate the SVG on the back-end? Commented Apr 12 at 10:57

2 Answers 2

1

You’ll be pleased to hear that a circular gauge requires neither Javascript nor SVG to implement.

The solution is simple: conic-gradient.

enter image description here

body {
  font-family: sans-serif;
  margin: 1em;
  display: flex;
  flex-wrap: wrap;
  gap: 1em;
}

.circular-gauge {
  position: relative;
  width: 150px;
  height: 150px;
  margin-bottom: 1em;

  > :first-child {
    width: 100%;
    height: 100%;
    background: #eee;
    border-radius: 99em;
  }

  > :first-child::after {
    content: '';
    position: absolute;
    left: 7%;
    top: 7%;
    width: 86%;
    height: 86%;
    border-radius: 99em;
    background: white;
  }

  > :last-child {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
  }

  > :last-child h2 {
    margin: 0;
    font-size: 2em;
    font-weight:300;
  }
}

.cg1 > :first-child {
  background: conic-gradient(#8aebc0 0turn, #72d2a5 0.7turn, #eee 0.7turn);
}

.cg2 > :first-child {
  background: conic-gradient(#a4b8f9 0turn, #6b62f1 1turn);
}

.cg3 > :first-child {
  background: conic-gradient(#c546d9 0turn, #832b99 1turn);
  transform: rotate(1.3turn);
}
<div class="circular-gauge cg1">
  <div></div>
  <div>
    <h2>0.7</h2>
  </div>
</div>

<div class="circular-gauge cg2">
  <div></div>
  <div>
    <h2>1.0</h2>
  </div>
</div>

<div class="circular-gauge cg3">
  <div></div>
  <div>
    <h2>1.3</h2>
  </div>
</div>

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. This is what I was actually looking for!
0

You can get many inspiration from codepen. Here's one pure css example: HTML

<div class="set-size charts-container">
  <div class="pie-wrapper progress-30">
    <span class="label">30<span class="smaller">%</span></span>
    <div class="pie">
      <div class="left-side half-circle"></div>
      <div class="right-side half-circle"></div>
    </div>
  </div>

  <div class="pie-wrapper progress-60">
    <span class="label">60<span class="smaller">%</span></span>
    <div class="pie">
      <div class="left-side half-circle"></div>
      <div class="right-side half-circle"></div>
    </div>
  </div>

  <div class="pie-wrapper progress-90">
    <span class="label">90<span class="smaller">%</span></span>
    <div class="pie">
      <div class="left-side half-circle"></div>
      <div class="right-side half-circle"></div>
    </div>
  </div>

  <div class="pie-wrapper progress-45 style-2">
    <span class="label">45<span class="smaller">%</span></span>
    <div class="pie">
      <div class="left-side half-circle"></div>
      <div class="right-side half-circle"></div>
    </div>
    <div class="shadow"></div>
  </div>

  <div class="pie-wrapper progress-75 style-2">
    <span class="label">75<span class="smaller">%</span></span>
    <div class="pie">
      <div class="left-side half-circle"></div>
      <div class="right-side half-circle"></div>
    </div>
    <div class="shadow"></div>
  </div>

  <div class="pie-wrapper progress-95 style-2">
    <span class="label">95<span class="smaller">%</span></span>
    <div class="pie">
      <div class="left-side half-circle"></div>
      <div class="right-side half-circle"></div>
    </div>
    <div class="shadow"></div>
  </div>
  
  <div class="pie-wrapper pie-wrapper--solid progress-65">
    <span class="label">65<span class="smaller">%</span></span>
  </div>
  
  <div class="pie-wrapper pie-wrapper--solid progress-25">
    <span class="label">25<span class="smaller">%</span></span>
  </div>
  
  <div class="pie-wrapper pie-wrapper--solid progress-88">
    <span class="label">88<span class="smaller">%</span></span>
  </div>
</div>

CSS:

@import url(https://fonts.googleapis.com/css?family=Lato:700);

// -- vars
$bg-color: #34495e;
$default-size: 1em;
$label-font-size: $default-size / 4;
$label-font-size-redo: $default-size * 4;

// -- mixins
@mixin size($width, $height) {
  height: $height;
  width: $width;
}

@mixin draw-progress($progress, $color) {
  .pie {
    .half-circle {
      border-color: $color;
    }

    .left-side {
      transform: rotate($progress * 3.6deg);
    }

    @if $progress <= 50 {
      .right-side {
        display: none;
      }
    } @else {
      clip: rect(auto, auto, auto, auto);

      .right-side {
        transform: rotate(180deg);
      }
    }
  }
}

@mixin draw-progress--solid($progress, $color, $bg-color) {
  background: linear-gradient(to right, $color 50%, $bg-color 50%);
  
  &:before {
    @if $progress <= 50 {
      background: $bg-color;
      transform: rotate((100 - (50 - $progress)) / 100 * 360deg * -1);
    } @else {
      background: $color;
      transform: rotate((100 - $progress) / 100 * 360deg);
    }
  }
}

// -- selectors
*,
*:before,
*:after {
  box-sizing: border-box;
}

html,
body {
  background: #ecf0f1;
  color: #444;
  font-family: 'Lato', Tahoma, Geneva, sans-serif;
  font-size: 16px;
  padding: 10px;
}

.set-size {
  font-size: 10em;
}

.charts-container {
  &:after {
    clear: both;
    content: '';
    display: table;
  }
}

.pie-wrapper {
  @include size($default-size, $default-size);
  float: left;
  margin: 15px;
  position: relative;
  
  &:nth-child(3n + 1) {
    clear: both;
  }

  .pie {
    @include size(100%, 100%);
    clip: rect(0, $default-size, $default-size, $default-size / 2);
    left: 0;
    position: absolute;
    top: 0;

    .half-circle {
      @include size(100%, 100%);
      border: ($default-size / 10) solid #3498db;
      border-radius: 50%;
      clip: rect(0, $default-size / 2, $default-size, 0);
      left: 0;
      position: absolute;
      top: 0;
    }
  }

  .label {
    background: $bg-color;
    border-radius: 50%;
    bottom: $label-font-size-redo / 10;
    color: #ecf0f1;
    cursor: default;
    display: block;
    font-size: $label-font-size;
    left: $label-font-size-redo / 10;
    line-height: $label-font-size-redo * .70;
    position: absolute;
    right: $label-font-size-redo / 10;
    text-align: center;
    top: $label-font-size-redo / 10;

    .smaller {
      color: #bdc3c7;
      font-size: .45em;
      padding-bottom: 20px;
      vertical-align: super;
    }
  }

  .shadow {
    @include size(100%, 100%);
    border: $default-size / 10 solid #bdc3c7;
    border-radius: 50%;
  }

  &.style-2 {
    .label {
      background: none;
      color: #7f8c8d;

      .smaller {
        color: #bdc3c7;
      }
    }
  }

  &.progress-30 {
    @include draw-progress(30, #3498db);
  }

  &.progress-60 {
    @include draw-progress(60, #9b59b6);
  }

  &.progress-90 {
    @include draw-progress(90, #e67e22);
  }

  &.progress-45 {
    @include draw-progress(45, #1abc9c);
  }

  &.progress-75 {
    @include draw-progress(75, #8e44ad);
  }

  &.progress-95 {
    @include draw-progress(95, #e74c3c);
  }
}

.pie-wrapper--solid {
  border-radius: 50%;
  overflow: hidden;

  &:before {
    border-radius: 0 100% 100% 0 / 50%;
    content: '';
    display: block;
    height: 100%;
    margin-left: 50%;
    transform-origin: left;
  }
  
  .label {
    background: transparent;
  }
  
  &.progress-65 {
    @include draw-progress--solid(65, #e67e22, $bg-color);
  }
  
  &.progress-25 {
    @include draw-progress--solid(25, #9b59b6, $bg-color);
  }
  
  &.progress-88 {
    @include draw-progress--solid(88, #3498db, $bg-color);
  }
}

https://codepen.io/jo-asakura/pen/NWWrWj

It is possible to achieve exact design as in shared image with some hard work, but it will be robust at the end if you did good.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.