diff --git a/public/main.js b/public/main.js
index 808cabce90662c7fa1e742119e1140fd8548901f..5f923ed4a999231679a1d12b1457bdcd31575c0b 100644
--- a/public/main.js
+++ b/public/main.js
@@ -5539,6 +5539,155 @@ var $author$project$BoundedInt$change = F2(
 			boundedInt,
 			{value: value});
 	});
+var $elm$core$String$fromFloat = _String_fromNumber;
+var $elm$svg$Svg$Attributes$height = _VirtualDom_attribute('height');
+var $elm$core$Basics$pow = _Basics_pow;
+var $elm$core$Basics$sqrt = _Basics_sqrt;
+var $elm$svg$Svg$trustedNode = _VirtualDom_nodeNS('http://www.w3.org/2000/svg');
+var $elm$svg$Svg$svg = $elm$svg$Svg$trustedNode('svg');
+var $author$project$Main$toDotPosition = F4(
+	function (size, gap, offset, id) {
+		var marginalIncrease = (size + gap) / 2;
+		return _Utils_Tuple2((id * marginalIncrease) + offset, (((id + 1) % 2) * marginalIncrease) + offset);
+	});
+var $author$project$Main$boolAsIntString = function (bool) {
+	return bool ? '1' : '0';
+};
+var $elm$core$String$concat = function (strings) {
+	return A2($elm$core$String$join, '', strings);
+};
+var $elm$svg$Svg$Attributes$fill = _VirtualDom_attribute('fill');
+var $elm$svg$Svg$Attributes$fillOpacity = _VirtualDom_attribute('fill-opacity');
+var $elm$core$List$intersperse = F2(
+	function (sep, xs) {
+		if (!xs.b) {
+			return _List_Nil;
+		} else {
+			var hd = xs.a;
+			var tl = xs.b;
+			var step = F2(
+				function (x, rest) {
+					return A2(
+						$elm$core$List$cons,
+						sep,
+						A2($elm$core$List$cons, x, rest));
+				});
+			var spersed = A3($elm$core$List$foldr, step, _List_Nil, tl);
+			return A2($elm$core$List$cons, hd, spersed);
+		}
+	});
+var $elm$core$Tuple$mapBoth = F3(
+	function (funcA, funcB, _v0) {
+		var x = _v0.a;
+		var y = _v0.b;
+		return _Utils_Tuple2(
+			funcA(x),
+			funcB(y));
+	});
+var $elm$core$Tuple$mapFirst = F2(
+	function (func, _v0) {
+		var x = _v0.a;
+		var y = _v0.b;
+		return _Utils_Tuple2(
+			func(x),
+			y);
+	});
+var $elm$core$Tuple$mapSecond = F2(
+	function (func, _v0) {
+		var x = _v0.a;
+		var y = _v0.b;
+		return _Utils_Tuple2(
+			x,
+			func(y));
+	});
+var $elm$svg$Svg$Attributes$points = _VirtualDom_attribute('points');
+var $elm$svg$Svg$polygon = $elm$svg$Svg$trustedNode('polygon');
+var $elm$core$Tuple$second = function (_v0) {
+	var y = _v0.b;
+	return y;
+};
+var $elm$svg$Svg$Attributes$stroke = _VirtualDom_attribute('stroke');
+var $elm$svg$Svg$Attributes$strokeWidth = _VirtualDom_attribute('stroke-width');
+var $author$project$Main$tupleToString = function (tuple) {
+	var second = $elm$core$String$fromFloat(tuple.b);
+	var first = $elm$core$String$fromFloat(tuple.a);
+	return first + (',' + second);
+};
+var $author$project$Main$viewDot = F4(
+	function (size, strokeWidth, isFilled, position) {
+		return A2(
+			$elm$svg$Svg$polygon,
+			_List_fromArray(
+				[
+					$elm$svg$Svg$Attributes$points(
+					$elm$core$String$concat(
+						A2(
+							$elm$core$List$intersperse,
+							' ',
+							A2(
+								$elm$core$List$map,
+								$author$project$Main$tupleToString,
+								A2(
+									$elm$core$List$map,
+									$elm$core$Tuple$mapSecond(
+										$elm$core$Basics$add(position.b)),
+									A2(
+										$elm$core$List$map,
+										$elm$core$Tuple$mapFirst(
+											$elm$core$Basics$add(position.a)),
+										A2(
+											$elm$core$List$map,
+											A2(
+												$elm$core$Tuple$mapBoth,
+												$elm$core$Basics$mul(size),
+												$elm$core$Basics$mul(size)),
+											_List_fromArray(
+												[
+													_Utils_Tuple2(0.5, 0.0),
+													_Utils_Tuple2(1.0, 0.5),
+													_Utils_Tuple2(0.5, 1.0),
+													_Utils_Tuple2(0.0, 0.5)
+												])))))))),
+					$elm$svg$Svg$Attributes$fill('var(--bulma-body-color)'),
+					$elm$svg$Svg$Attributes$fillOpacity(
+					$author$project$Main$boolAsIntString(isFilled)),
+					$elm$svg$Svg$Attributes$stroke('var(--bulma-body-color)'),
+					$elm$svg$Svg$Attributes$strokeWidth(
+					$elm$core$String$fromFloat(strokeWidth))
+				]),
+			_List_Nil);
+	});
+var $elm$svg$Svg$Attributes$width = _VirtualDom_attribute('width');
+var $author$project$Main$viewDots = F2(
+	function (totalDotCount, filledDotCount) {
+		var isFilledList = A2(
+			$elm$core$List$map,
+			$elm$core$Basics$gt(filledDotCount),
+			A2($elm$core$List$range, 0, totalDotCount - 1));
+		var dotSize = 12;
+		var dotOutline = 0.05 * dotSize;
+		var svgOutlineMargin = $elm$core$Basics$sqrt(
+			2 * A2($elm$core$Basics$pow, dotOutline, 2)) / 2;
+		var dotGap = 4;
+		var marginalIncrease = (dotSize + dotGap) / 2;
+		return A2(
+			$elm$svg$Svg$svg,
+			_List_fromArray(
+				[
+					$elm$svg$Svg$Attributes$width(
+					$elm$core$String$fromFloat((dotSize + (marginalIncrease * (totalDotCount - 1))) + (2 * svgOutlineMargin))),
+					$elm$svg$Svg$Attributes$height(
+					$elm$core$String$fromFloat((dotSize + marginalIncrease) + (2 * svgOutlineMargin)))
+				]),
+			A3(
+				$elm$core$List$map2,
+				A2($author$project$Main$viewDot, dotSize, dotOutline),
+				isFilledList,
+				A2(
+					$elm$core$List$map,
+					A3($author$project$Main$toDotPosition, dotSize, dotGap, svgOutlineMargin),
+					A2($elm$core$List$range, 0, totalDotCount - 1))));
+	});
 var $author$project$Main$viewSphere = function (sphere) {
 	return A2(
 		$elm$html$Html$div,
@@ -5574,8 +5723,7 @@ var $author$project$Main$viewSphere = function (sphere) {
 				_List_Nil,
 				_List_fromArray(
 					[
-						$elm$html$Html$text(
-						$elm$core$String$fromInt(sphere.dots.value))
+						A2($author$project$Main$viewDots, sphere.dots.max, sphere.dots.value)
 					])),
 				A2(
 				$elm$html$Html$button,
diff --git a/src/Main.elm b/src/Main.elm
index 7ccda3f44c4fc175e302400803870abc897f9e78..c6408aad85790f2fa5ad53c7f81ca6444754ddd2 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -4,6 +4,8 @@ import Browser
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Html.Events exposing (..)
+import Svg exposing (svg)
+import Svg.Attributes
 import Character exposing (Character)
 import Sphere exposing (Sphere)
 import BoundedInt exposing (BoundedInt)
@@ -114,7 +116,7 @@ viewSphere sphere =
             [ class "button is-small"
             , onClick ( ChangeSphere { sphere | dots = BoundedInt.change sphere.dots (sphere.dots.value - 1) } )
             ] [ text "-" ]
-        , span [] [ text ( String.fromInt sphere.dots.value ) ]
+        , span [] [ viewDots sphere.dots.max sphere.dots.value ]
         , button 
             [ class "button is-small"
             , onClick ( ChangeSphere { sphere | dots = BoundedInt.change sphere.dots (sphere.dots.value + 1) } )
@@ -171,6 +173,71 @@ modalValue modalType character =
     case modalType of
         Name -> character.name
 
+viewDots : Int -> Int -> Html Msg
+viewDots totalDotCount filledDotCount =
+    let
+        dotSize = 12
+        dotGap = 4
+        dotOutline = 0.05 * dotSize
+        marginalIncrease = ( dotSize + dotGap ) / 2
+        svgOutlineMargin = ( sqrt ( 2 * ( dotOutline ^ 2 ) ) ) / 2
+
+        isFilledList = List.range 0 ( totalDotCount - 1 ) |> List.map ( (>) filledDotCount )
+    in
+        svg 
+            [ Svg.Attributes.width  ( String.fromFloat ( dotSize + marginalIncrease * toFloat ( totalDotCount - 1 ) + 2 * svgOutlineMargin ) )
+            , Svg.Attributes.height ( String.fromFloat ( dotSize + marginalIncrease + 2 * svgOutlineMargin ) )
+            ]
+            ( List.range 0 ( totalDotCount - 1 )
+                |> List.map ( toDotPosition dotSize dotGap svgOutlineMargin )
+                |> List.map2 ( viewDot dotSize dotOutline ) isFilledList
+            )
+
+toDotPosition : Float -> Float -> Float -> Int -> ( Float, Float )
+toDotPosition size gap offset id =
+    let
+        marginalIncrease = ( size + gap ) / 2
+    in
+        ( toFloat id * marginalIncrease + offset
+        , toFloat ( remainderBy 2 ( id + 1 ) ) * marginalIncrease + offset
+        )
+
+viewDot : Float -> Float -> Bool -> ( Float, Float ) -> Html Msg
+viewDot size strokeWidth isFilled position =
+    Svg.polygon 
+        [ Svg.Attributes.points 
+            (   [ ( 0.5, 0.0 )
+                , ( 1.0, 0.5 )
+                , ( 0.5, 1.0 )
+                , ( 0.0, 0.5 )
+                ]
+                |> List.map ( Tuple.mapBoth   ( (*) size ) ( (*) size ) )
+                |> List.map ( Tuple.mapFirst  ( (+) ( Tuple.first  position ) ) )
+                |> List.map ( Tuple.mapSecond ( (+) ( Tuple.second position ) ) )
+                |> List.map tupleToString
+                |> List.intersperse " "
+                |> String.concat
+            )
+        , Svg.Attributes.fill "var(--bulma-body-color)"
+        , Svg.Attributes.fillOpacity ( boolAsIntString isFilled )
+        , Svg.Attributes.stroke "var(--bulma-body-color)"
+        , Svg.Attributes.strokeWidth ( String.fromFloat strokeWidth )
+        ] []
+
+tupleToString : ( Float, Float ) -> String
+tupleToString tuple =
+    let
+        first  = String.fromFloat ( Tuple.first  tuple )
+        second = String.fromFloat ( Tuple.second tuple )
+    in
+        first ++ "," ++ second
+
+boolAsIntString : Bool -> String
+boolAsIntString bool =
+    if bool
+    then "1"
+    else "0"
+
 subscriptions : Model -> Sub Msg
 subscriptions _ =
   Sub.none