Skip to main content

Circular Gauge

The Circular Gauge turns a score into a semicircular dial with a target line, so a number reads instantly as how full the gauge is and whether it has reached its goal. It is built with a standard Microsoft List and a small piece of column-formatting JSON that draws the arc with SVG. The dial fills to match the value, a dark tick marks the target on the arc, and the color shifts as the value meets, approaches, or falls short of the target - green when it is met, amber when it is close, red when it is below. A blank value shows an empty gauge labeled No data.
Author
SharePoint Maven
Built with
Features
JSON, Number Column, Person or Group Column, Single Line of Text Column
Category
Formatting

Want one like this?

Talk to Greg
  • Renders a number as a semicircular gauge that fills in proportion to the value
  • A dark tick on the arc marks the target, so meeting or missing the goal is obvious
  • Color shifts with the target – green when the value meets it, amber when it is close, red when it is below
  • The arc is drawn with SVG inside the JSON, so it stays crisp at any size and nothing is installed
  • The gauge value comes from the current column and the target comes from a Target column, so each row can carry its own goal
  • Applied as column-formatting JSON, so it travels with the view and changes nothing about the underlying data
  • KPI scores against a quarterly target
  • Sales or quota attainment
  • Service level or uptime against a goal
  • Survey, satisfaction, or NPS scores
  • Readiness or maturity assessments
  • Utilization against a planned target
  • Design Pattern: Metrics & Progress
  • Format Scope: Column
Display nameColumn typeInternal name
TitleSingle line of text[Title]
ScoreNumber[Score]
TargetNumber[Target]
OwnerPerson or Group[Owner]
  1. Build a list with these columns before you paste the JSON.
  2. Populate the list with data.
  3. Because the JSON is applied at the column level, the Score column can use any name you want.
  4. Score and Target must both be Number columns holding real numeric values, for example 65 and 80.
  5. If the gauge is blank, confirm the Target column exists, contains a number, and that its internal name matches the JSON reference.
  6. Keep the outer wrapper in the JSON unchanged – an earlier version stretched the root element to the row height and made the gauge oversized and misaligned.
  1. On the Score column, open the column header menu, choose Column settings, then Format this column.
  2. Switch to Advanced mode.
  3. Paste the JSON below and click Save.
{
  "$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
  "elmType": "div",
  "style": {
    "position": "relative",
    "width": "108px",
    "height": "62px",
    "min-width": "108px",
    "max-width": "108px",
    "min-height": "62px",
    "max-height": "62px",
    "margin": "0 auto",
    "display": "flex",
    "align-items": "center",
    "justify-content": "center",
    "box-sizing": "border-box"
  },
  "children": [
    {
      "elmType": "svg",
      "attributes": {
        "viewBox": "0 0 100 62",
        "preserveAspectRatio": "xMidYMid meet"
      },
      "style": {
        "width": "100px",
        "height": "60px",
        "display": "=if(@currentField == '', 'none', 'block')"
      },
      "children": [
        {
          "elmType": "path",
          "attributes": {
            "d": "M 8 50 A 42 42 0 0 1 92 50 L 83 50 A 33 33 0 0 0 17 50 Z"
          },
          "style": {
            "fill": "#e8eaed"
          }
        },
        {
          "elmType": "path",
          "attributes": {
            "d": "M 17 50 A 4.5 4.5 0 1 0 8 50 A 4.5 4.5 0 1 0 17 50 Z"
          },
          "style": {
            "fill": "#e8eaed"
          }
        },
        {
          "elmType": "path",
          "attributes": {
            "d": "M 92 50 A 4.5 4.5 0 1 0 83 50 A 4.5 4.5 0 1 0 92 50 Z"
          },
          "style": {
            "fill": "#e8eaed"
          }
        },
        {
          "elmType": "path",
          "attributes": {
            "d": "='M 8 50 A 42 42 0 0 1 ' + (50 + (42 * cos((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + ' ' + (50 + (42 * sin((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + ' L ' + (50 + (33 * cos((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + ' ' + (50 + (33 * sin((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + ' A 33 33 0 0 0 17 50 Z'"
          },
          "style": {
            "fill": "=if(@currentField >= [$Target], '#107c10', if(@currentField >= ([$Target] * 0.85), '#f7a600', '#d13438'))",
            "display": "=if(@currentField <= 0, 'none', 'block')"
          }
        },
        {
          "elmType": "path",
          "attributes": {
            "d": "M 17 50 A 4.5 4.5 0 1 0 8 50 A 4.5 4.5 0 1 0 17 50 Z"
          },
          "style": {
            "fill": "=if(@currentField >= [$Target], '#107c10', if(@currentField >= ([$Target] * 0.85), '#f7a600', '#d13438'))",
            "display": "=if(@currentField <= 0, 'none', 'block')"
          }
        },
        {
          "elmType": "path",
          "attributes": {
            "d": "='M ' + ((50 + (37.5 * cos((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + 4.5) + ' ' + (50 + (37.5 * sin((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + ' A 4.5 4.5 0 1 0 ' + ((50 + (37.5 * cos((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) - 4.5) + ' ' + (50 + (37.5 * sin((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + ' A 4.5 4.5 0 1 0 ' + ((50 + (37.5 * cos((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + 4.5) + ' ' + (50 + (37.5 * sin((3.14159265 + (3.14159265 * if(@currentField > 100, 100, if(@currentField < 0, 0, @currentField)) / 100))))) + ' Z'"
          },
          "style": {
            "fill": "=if(@currentField >= [$Target], '#107c10', if(@currentField >= ([$Target] * 0.85), '#f7a600', '#d13438'))",
            "display": "=if(@currentField <= 0, 'none', 'block')"
          }
        },
        {
          "elmType": "path",
          "attributes": {
            "d": "='M ' + (50 + (44 * cos((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100))))) + ' ' + (50 + (44 * sin((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100))))) + ' L ' + (50 + (31 * cos((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100)))) - (1.7 * sin((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100))))) + ' ' + (50 + (31 * sin((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100)))) + (1.7 * cos((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100))))) + ' L ' + (50 + (31 * cos((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100)))) + (1.7 * sin((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100))))) + ' ' + (50 + (31 * sin((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100)))) - (1.7 * cos((3.14159265 + (3.14159265 * if([$Target] > 100, 100, if([$Target] < 0, 0, [$Target])) / 100))))) + ' Z'"
          },
          "style": {
            "fill": "#201f1e",
            "display": "=if([$Target] == '', 'none', 'block')"
          }
        }
      ]
    },
    {
      "elmType": "div",
      "txtContent": "=if(@currentField == '', 'No data', @currentField)",
      "style": {
        "position": "absolute",
        "left": "0",
        "right": "0",
        "top": "26px",
        "text-align": "center",
        "font-size": "17px",
        "font-weight": "700",
        "line-height": "19px",
        "color": "#201f1e"
      }
    },
    {
      "elmType": "div",
      "txtContent": "=if(@currentField == '', '', if([$Target] == '', '', 'target ' + [$Target]))",
      "style": {
        "position": "absolute",
        "left": "0",
        "right": "0",
        "top": "46px",
        "text-align": "center",
        "font-size": "12px",
        "line-height": "13px",
        "font-weight": "500",
        "color": "#605e5c"
      }
    }
  ]
}
What is the Circular Gauge built with?

It is built with a standard Microsoft List (SharePoint list) and a small piece of column-formatting JSON that draws the arc using SVG. There is no custom development, no SPFx solution, and no third-party tools. It is the kind of clean, maintainable formatting Greg Zelfond builds for teams that want a dashboard-style gauge without ongoing development overhead.

What columns does it need?

It needs a Number column for the value, which the gauge reads as the current column, and a second Number column named Target for the goal line. Because the JSON refers to the target by its internal name, that column must be named Target, while the value column can be named anything since the gauge reads the current column.

Does this design use any custom development or third-party tools?

No. It uses only out-of-the-box SharePoint column formatting and inline SVG, which Microsoft supports natively. That keeps it stable and easy to maintain, and nothing breaks when SharePoint is updated. Out-of-the-box is the only way Greg builds, so you can own and extend the design yourself for years.

If LookBook 365 is code-free and out-of-the-box, why does this example include JSON?

Because SharePoint formatting JSON is not custom code – it is a native configuration feature built into lists and libraries. It is declarative: it only describes how an existing column looks, and cannot run scripts, reach external services, or change your data. Nothing is deployed and nothing breaks when Microsoft updates SharePoint, and you can edit or remove it anytime. That is why LookBook 365 treats it as out-of-the-box and low risk.

Can the target be the same for every row instead of a column?

Yes. If every item shares one goal, you can replace the Target column reference in the JSON with a fixed number, such as 80, and the tick will sit at the same place on every gauge. Using a Target column simply lets each row carry its own goal, which is handy on a KPI list.

Can Greg build a gauge like this for our lists?

Yes – this is exactly the kind of work Greg Zelfond does. As an independent SharePoint consultant and Microsoft MVP, he designs out-of-the-box list and library formatting like this so your team can read and maintain it without a developer. Reach out through the contact page to talk about your dashboards.