Repository: FarmBot/Farmbot-Web-App (ts)

Issue:
Improve grid line labels in farm designer
# Expected Behavior
The grid line labels (`100`, `200`, etc) should be legible at all zoom levels

# Actual Behavior
At 0.75 zoom and farther out the labels are too small to easily read.

# Suggested Solution
- [ ] Scale the labels inversely to zoom level so they always stay the same size to the user, despite zoom level
- [x] At zoom level 0.5 and farther out switch from labels every 100mm to every 200mm
- [x] At zoom level 0.2 and farther out only show labels every 500mm
- I think a good size for the text might be where the bounding box rendered on screen is 16 to 18px tall. This might take some tweaking to look good.
- There may be some legibility issues with the labels on the left if when they stretch into the darker brown or even gray areas, which may require tweaking. Ideas: adding a text-shadow, making the font bolder or darker in color.

Table-ize Settings
Restructure settings that have X/Y/Z options into easier to read and more compact tables. Applies to Axes, Motors, Stall Detection, and Error Handling sections.

![Table-ize Settings](https://user-images.githubusercontent.com/5524043/190531852-d7c28887-8b9e-47fa-8a01-5c45279931aa.jpg)


Pull request description:
Grid and settings updates
fixes #2379, fixes #1685

Patch:
diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss
index 579092d46..ea9efe103 100644
--- a/frontend/css/farm_designer/farm_designer_panels.scss
+++ b/frontend/css/farm_designer/farm_designer_panels.scss
@@ -1543,6 +1543,14 @@ li {
   padding: 0;
 }
 
+.low-pad {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+  &.z-param-input {
+    padding-left: 4.25rem;
+  }
+}
+
 .settings-panel {
   .panel-top {
     .fa-chevron-right,
@@ -1615,7 +1623,6 @@ li {
     }
   }
   .label-headings {
-    margin-right: 2rem;
     label {
       line-height: 1rem;
     }
@@ -1705,11 +1712,13 @@ li {
     margin-top: 0.5rem;
   }
   label {
-    margin: 0 !important;
-    line-height: 3rem;
+    margin: auto 0 !important;
+    line-height: 1;
+    font-size: 1.2rem !important;
   }
   .bp4-popover-wrapper {
     display: inline;
+    margin: auto;
     float: none;
   }
   .map-size-inputs {
@@ -1724,7 +1733,7 @@ li {
     }
   }
   .help-icon {
-    margin-left: 1rem;
+    margin-left: 0.5rem;
   }
   .designer-setting {
     &.disabled {
diff --git a/frontend/css/global.scss b/frontend/css/global.scss
index 61e7a88e3..3b316bbb7 100644
--- a/frontend/css/global.scss
+++ b/frontend/css/global.scss
@@ -517,7 +517,6 @@ a {
 .single-setting-row {
   .col-xs-5 {
     padding-left: 4.75rem;
-    padding-right: 3rem;
     &.centered-button-div {
       padding-left: 6rem;
     }
@@ -2742,10 +2741,12 @@ textarea:focus {
   }
 }
 
-.z-height-input,
-.z-param-input {
-  padding-left: 1rem;
-  padding-right: 3rem;
+.widget-body-tooltips {
+  display: flex;
+  padding-right: 0.5rem;
+  .bp4-popover-wrapper {
+    line-height: 3rem;
+  }
 }
 
 .z-param-label {
diff --git a/frontend/farm_designer/map/background/grid.tsx b/frontend/farm_designer/map/background/grid.tsx
index 2b6aacb23..c47f9e1cd 100644
--- a/frontend/farm_designer/map/background/grid.tsx
+++ b/frontend/farm_designer/map/background/grid.tsx
@@ -1,11 +1,8 @@
-/* eslint-disable indent */
-/* eslint-disable object-curly-spacing */
-/* eslint-disable max-len */
-import * as React from "react";
+import React from "react";
 import { GridProps } from "../interfaces";
 import { transformXY, transformForQuadrant } from "../util";
-import { Color } from "../../../ui/index";
-import { range } from "lodash";
+import { Color } from "../../../ui";
+import { range, round } from "lodash";
 
 export function Grid(props: GridProps) {
   const { mapTransformProps, zoomLvl } = props;
@@ -16,110 +13,112 @@ export function Grid(props: GridProps) {
   const arrowEnd = transformXY(25, 25, mapTransformProps);
   const xLabel = transformXY(15, -10, mapTransformProps);
   const yLabel = transformXY(-11, 18, mapTransformProps);
-  const minorStrokeWidth = zoomLvl <= 0.5 ? "0" : "1";
-  const majorStrokeWidth = zoomLvl <= 0.5 ? "3" : "2";
-  const superiorStrokeWidth = zoomLvl <= 0.5 ? "6" : "4";
+  const strokeWidth = zoomLvl <= 0.5
+    ? { minor: 0, major: 3, superior: 6 }
+    : { minor: 1, major: 2, superior: 4 };
 
-  // Start axis-values controls
-  // TODO: Create helper to regroup code and clean grid.tsx
-  // Text transform:scale value
-  const axisTransformValue = zoomLvl <= 1 ? 2 - zoomLvl : 1;
-  // Start and increment steps to visualize in grid
-  let axisStep;
-  if (zoomLvl <= 0.2) {
-    axisStep = 500;
-  } else if (zoomLvl <= 0.5) {
-    axisStep = 200;
-  } else {
-    axisStep = 100;
-  }
-  // End axis-values controls
+  /** Map grid axis label step size according to zoom level. */
+  const calcAxisStep = (zoomLvl: number) => {
+    if (zoomLvl <= 0.2) { return 500; }
+    if (zoomLvl <= 0.5) { return 200; }
+    return 100;
+  };
+
+  /** Distance from map grid extents to label. */
+  const calcLabelOffset = (zoomLvl: number) => {
+    if (zoomLvl <= 0.2) { return -100; }
+    if (zoomLvl >= 1) { return -10; }
+    return -20;
+  };
+
+  const inverseScale = `scale(${round(1 / zoomLvl, 2)})`;
+
+  const gridLabels = (axis: "x" | "y") => {
+    const axisStep = calcAxisStep(zoomLvl);
+    const vertical = axis == (xySwap ? "x" : "y");
+    const offset = calcLabelOffset(zoomLvl);
+    return range(axisStep, gridSize[axis], axisStep)
+      .map(position => {
+        const location = axis == "y"
+          ? transformXY(offset, position, mapTransformProps)
+          : transformXY(position, offset, mapTransformProps);
+        return (
+          <text key={`${axis}-label-${position}`}
+            id={`${axis}-label`}
+            x={location.qx}
+            y={location.qy}
+            style={{
+              transformOrigin: "center",
+              transformBox: "fill-box",
+              transform: [
+                inverseScale,
+                vertical ? "rotate(-90deg)" : "",
+              ].join(" "),
+            }}>
+            {position}
+          </text>
+        );
+      });
+  };
 
   return <g className="drop-area-background" onClick={props.onClick}
     onMouseDown={props.onMouseDown}>
     <defs>
       <pattern id="minor_grid"
-        width={10} height={10} patternUnits="userSpaceOnUse">
-        <path d="M10,0 L0,0 L0,10" strokeWidth={minorStrokeWidth}
-          fill="none" stroke="rgba(0, 0, 0, 0.15)" />
+        width={10} height={10} patternUnits={"userSpaceOnUse"}>
+        <path d={"M10,0 L0,0 L0,10"} strokeWidth={strokeWidth.minor}
+          fill={"none"} stroke={"rgba(0, 0, 0, 0.15)"} />
       </pattern>
 
       <pattern id={"major_grid"}
-        width={100} height={100} patternUnits="userSpaceOnUse">
-        <path d="M100,0 L0,0 0,100" strokeWidth={majorStrokeWidth}
-          fill="none" stroke="rgba(0, 0, 0, 0.3)" />
+        width={100} height={100} patternUnits={"userSpaceOnUse"}>
+        <path d={"M100,0 L0,0 0,100"} strokeWidth={strokeWidth.major}
+          fill={"none"} stroke={"rgba(0, 0, 0, 0.3)"} />
       </pattern>
 
-      <pattern id="superior_grid" width={1000} height={1000} patternUnits="userSpaceOnUse">
-        <path d="M1000,0 L0,0 0,1000" strokeWidth={superiorStrokeWidth}
-              fill="none" stroke="rgba(0, 0, 0, 0.4)" />
+      <pattern id="superior_grid" width={1000} height={1000}
+        patternUnits={"userSpaceOnUse"}>
+        <path d={"M1000,0 L0,0 0,1000"} strokeWidth={strokeWidth.superior}
+          fill={"none"} stroke={"rgba(0, 0, 0, 0.4)"} />
       </pattern>
 
       <marker id="arrow"
-        markerWidth={10} markerHeight={10} refX={0} refY={2} orient="auto"
-        markerUnits="strokeWidth">
-        <path d="M0,0 L0,4 L5,2 z" fill={Color.gridGray} />
+        markerWidth={10} markerHeight={10} refX={0} refY={2} orient={"auto"}
+        markerUnits={"strokeWidth"}>
+        <path d={"M0,0 L0,4 L5,2 z"} fill={Color.gridGray} />
       </marker>
     </defs>
 
     <g id="grid">
       <rect id="minor-grid"
-        width={gridSizeW} height={gridSizeH} fill="url(#minor_grid)" />
+        width={gridSizeW} height={gridSizeH} fill={"url(#minor_grid)"} />
       <rect id="major-grid" transform={transformForQuadrant(mapTransformProps)}
-        width={gridSizeW} height={gridSizeH} fill="url(#major_grid)" />
+        width={gridSizeW} height={gridSizeH} fill={"url(#major_grid)"} />
       <rect id="superior-grid" transform={transformForQuadrant(mapTransformProps)}
-            width={gridSizeW} height={gridSizeH} fill="url(#superior_grid)" />
-      <rect id="border" width={gridSizeW} height={gridSizeH} fill="none"
-        stroke="rgba(0,0,0,0.3)" strokeWidth={2} />
+        width={gridSizeW} height={gridSizeH} fill={"url(#superior_grid)"} />
+      <rect id="border" width={gridSizeW} height={gridSizeH} fill={"none"}
+        stroke={"rgba(0,0,0,0.3)"} strokeWidth={2} />
     </g>
 
-    <g id="origin-marker">
+    <g id="origin-marker" transform={inverseScale}>
       <circle cx={origin.qx} cy={origin.qy} r={4} fill={Color.gridGray} />
-      <g id="axis-labels" fontFamily="Arial" fontSize="10"
-        textAnchor="middle" dominantBaseline="central">
+      <g id={"axis-labels"} fontFamily={"Arial"} fontSize={14}
+        textAnchor={"middle"} dominantBaseline={"central"}>
         <text x={xLabel.qx} y={xLabel.qy}>X</text>
         <text x={yLabel.qx} y={yLabel.qy}>Y</text>
       </g>
       <g id="axis-arrows"
-        stroke={Color.gridGray} strokeWidth="3" markerEnd="url(#arrow)">
+        stroke={Color.gridGray} strokeWidth={3} markerEnd={"url(#arrow)"}>
         <line x1={origin.qx} y1={origin.qy} x2={arrowEnd.qx} y2={origin.qy} />
         <line x1={origin.qx} y1={origin.qy} x2={origin.qx} y2={arrowEnd.qy} />
       </g>
     </g>
 
-    <g id="axis-values" fontFamily="Arial" fontSize="10"
-      textAnchor="middle" dominantBaseline="central" fill="rgba(0, 0, 0, 0.3)">
-      {range(axisStep, gridSize.x, axisStep).map((i) => {
-        const location = transformXY(i, -10, mapTransformProps);
-        return (
-          <text key={"x-label-" + i}
-                fontSize="16"
-                fontWeight="bold"
-                className="x-label"
-                color="rgba(0, 0, 0, 0.4)"
-                x={location.qx}
-                y={location.qy}
-                style={{transformOrigin: "center", transformBox: "fill-box", transform: `scale(${axisTransformValue})`}}>
-            {i}
-          </text>
-        );
-
-      })}
-      {range(axisStep, gridSize.y, axisStep).map((i) => {
-        const location = transformXY(10, i - 10, mapTransformProps);
-        return (
-            <text key={"y-label-" + i}
-                  fontSize="16"
-                  fontWeight="bold"
-                  className="y-label"
-                  color="rgba(0, 0, 0, 0.4)"
-                  x={location.qx}
-                  y={location.qy}
-                  style={{transformOrigin: "center", transformBox: "fill-box", transform: `scale(${axisTransformValue})`, textAnchor: "start"}}>
-              {i}
-            </text>
-        );
-      })}
+    <g id="axis-values" fontFamily={"Arial"} fontSize={14} textAnchor={"middle"}
+      dominantBaseline={"central"} fill={"rgba(0, 0, 0, 0.3)"} fontWeight={"bold"}
+      color={"rgba(0, 0, 0, 0.4)"}>
+      {gridLabels("x")}
+      {gridLabels("y")}
     </g>
   </g>;
 }
diff --git a/frontend/settings/fbos_settings/z_height_inputs.tsx b/frontend/settings/fbos_settings/z_height_inputs.tsx
index 6ec1d12b2..ea2f931a3 100644
--- a/frontend/settings/fbos_settings/z_height_inputs.tsx
+++ b/frontend/settings/fbos_settings/z_height_inputs.tsx
@@ -15,7 +15,7 @@ export const SafeHeight = (props: ZHeightInputProps) =>
         </label>
         <Help text={ToolTips.SAFE_HEIGHT} />
       </Col>
-      <Col xs={4} className={"z-height-input"}>
+      <Col xs={4} className={"z-height-input low-pad"}>
         <BotConfigInputBox
           setting={"safe_height"}
           dispatch={props.dispatch}
@@ -33,7 +33,7 @@ export const SoilHeight = (props: ZHeightInputProps) =>
         </label>
         <Help text={ToolTips.FALLBACK_SOIL_HEIGHT} />
       </Col>
-      <Col xs={4} className={"z-height-input"}>
+      <Col xs={4} className={"z-height-input low-pad"}>
         <BotConfigInputBox
           setting={"soil_height"}
           dispatch={props.dispatch}
diff --git a/frontend/settings/hardware_settings/boolean_mcu_input_group.tsx b/frontend/settings/hardware_settings/boolean_mcu_input_group.tsx
index b5340de43..59e253fd1 100644
--- a/frontend/settings/hardware_settings/boolean_mcu_input_group.tsx
+++ b/frontend/settings/hardware_settings/boolean_mcu_input_group.tsx
@@ -25,9 +25,9 @@ export class BooleanMCUInputGroup
     const xParam = sourceFwConfig(x);
     const yParam = sourceFwConfig(y);
     const zParam = sourceFwConfig(z);
-    const width = 4;
+    const width = 3;
     return <div className={"mcu-inputs"}>
-      <Col xs={width} className={"centered-button-div"}>
+      <Col xs={width} className={"centered-button-div low-pad"}>
         <ToggleButton dispatch={dispatch}
           grayscale={grayscale?.x}
           disabled={this.props.disabled || disable?.x}
@@ -38,7 +38,7 @@ export class BooleanMCUInputGroup
           toggleAction={() =>
             dispatch(settingToggle(x, sourceFwConfig, displayAlert))} />
       </Col>
-      <Col xs={width} className={"centered-button-div"}>
+      <Col xs={width} className={"centered-button-div low-pad"}>
         <ToggleButton dispatch={dispatch}
           grayscale={grayscale?.y}
           disabled={this.props.disabled || disable?.y}
@@ -49,7 +49,7 @@ export class BooleanMCUInputGroup
           toggleAction={() =>
             dispatch(settingToggle(y, sourceFwConfig, displayAlert))} />
       </Col>
-      <Col xs={width} className={"centered-button-div"}>
+      <Col xs={width} className={"centered-button-div low-pad"}>
         <ToggleButton dispatch={dispatch}
           grayscale={grayscale?.z}
           disabled={this.props.disabled || disable?.z}
@@ -86,7 +86,7 @@ export class BooleanMCUInputGroup
       hidden={this.props.advanced && !(this.props.showAdvanced || this.anyModified)}
       className={this.props.advanced ? "advanced" : undefined}>
       <Row>
-        <Col xs={12} className={"widget-body-tooltips"}>
+        <Col xs={3} className={"widget-body-tooltips"}>
           <label>
             {t(this.props.label)}
             {this.props.caution &&
@@ -94,8 +94,8 @@ export class BooleanMCUInputGroup
           </label>
           <Help text={this.tooltip} />
         </Col>
+        <this.Toggles />
       </Row>
-      <Row><this.Toggles /></Row>
     </Highlight>;
   }
 }
diff --git a/frontend/settings/hardware_settings/calibration_row.tsx b/frontend/settings/hardware_settings/calibration_row.tsx
index 7254c3a8b..551920599 100644
--- a/frontend/settings/hardware_settings/calibration_row.tsx
+++ b/frontend/settings/hardware_settings/calibration_row.tsx
@@ -17,8 +17,8 @@ export class CalibrationRow extends React.Component<CalibrationRowProps> {
         .map(row => {
           const { axis } = row;
           const hardwareDisabled = type == "zero" ? false : row.disabled;
-          return <Col xs={4} key={axis}
-            className={"centered-button-div"}>
+          return <Col xs={3} key={axis}
+            className={"centered-button-div low-pad"}>
             <LockableButton
               disabled={arduinoBusy || hardwareDisabled || !botOnline}
               className={lockedClass(locked)}
@@ -34,14 +34,14 @@ export class CalibrationRow extends React.Component<CalibrationRowProps> {
   render() {
     return <Highlight settingName={this.props.title}>
       <Row>
-        <Col xs={12} className={"widget-body-tooltips"}>
+        <Col xs={3} className={"widget-body-tooltips"}>
           <label>
             {t(this.props.title)}
           </label>
           <Help text={t(this.props.toolTip)} />
         </Col>
+        <this.Axes />
       </Row>
-      <Row><this.Axes /></Row>
     </Highlight>;
   }
 }
diff --git a/frontend/settings/hardware_settings/motors.tsx b/frontend/settings/hardware_settings/motors.tsx
index ba61fe914..9fd3c95dd 100644
--- a/frontend/settings/hardware_settings/motors.tsx
+++ b/frontend/settings/hardware_settings/motors.tsx
@@ -82,7 +82,7 @@ export function Motors(props: MotorsProps) {
                 z: getDefault("movement_max_spd_z2") / scale.z
               })} />
           </Col>
-          <Col xs={4} className={"z-param-input"}>
+          <Col xs={4} className={"z-param-input low-pad"}>
             <McuInputBox {...commonProps}
               setting={"movement_max_spd_z2"}
               scale={scale.z} />
@@ -119,7 +119,7 @@ export function Motors(props: MotorsProps) {
                 z: getDefault("movement_min_spd_z2") / scale.z
               })} />
           </Col>
-          <Col xs={4} className={"z-param-input"}>
+          <Col xs={4} className={"z-param-input low-pad"}>
             <McuInputBox {...commonProps}
               setting={"movement_min_spd_z2"}
               scale={scale.z} />
@@ -146,7 +146,7 @@ export function Motors(props: MotorsProps) {
                 z: getDefault("movement_steps_acc_dec_z2") / scale.z
               })} />
           </Col>
-          <Col xs={4} className={"z-param-input"}>
+          <Col xs={4} className={"z-param-input low-pad"}>
             <McuInputBox {...commonProps}
               setting={"movement_steps_acc_dec_z2"}
               scale={scale.z} />
diff --git a/frontend/settings/hardware_settings/numeric_mcu_input_group.tsx b/frontend/settings/hardware_settings/numeric_mcu_input_group.tsx
index 9ba8879eb..07bb694fe 100644
--- a/frontend/settings/hardware_settings/numeric_mcu_input_group.tsx
+++ b/frontend/settings/hardware_settings/numeric_mcu_input_group.tsx
@@ -26,7 +26,7 @@ export class NumericMCUInputGroup
       disabled,
     };
     return <div className={"mcu-inputs"}>
-      <Col xs={4}>
+      <Col xs={3} className={"low-pad"}>
         <McuInputBox {...commonProps}
           setting={x}
           scale={xScale}
@@ -35,7 +35,7 @@ export class NumericMCUInputGroup
           warning={warning?.x}
           gray={gray?.x} />
       </Col>
-      <Col xs={4}>
+      <Col xs={3} className={"low-pad"}>
         <McuInputBox {...commonProps}
           setting={y}
           scale={yScale}
@@ -44,7 +44,7 @@ export class NumericMCUInputGroup
           warning={warning?.y}
           gray={gray?.y} />
       </Col>
-      <Col xs={4}>
+      <Col xs={3} className={"low-pad"}>
         <McuInputBox {...commonProps}
           setting={z}
           scale={zScale}
@@ -96,14 +96,14 @@ export class NumericMCUInputGroup
         && !this.error}
       className={this.props.advanced ? "advanced" : undefined}>
       <Row>
-        <Col xs={12} className={"widget-body-tooltips"}>
+        <Col xs={3} className={"widget-body-tooltips"}>
           <label>
             {t(label)}
           </label>
           <Help text={this.tooltip} />
         </Col>
+        <this.Inputs />
       </Row>
-      <Row><this.Inputs /></Row>
     </Highlight>;
   }
 }
diff --git a/frontend/settings/hardware_settings/single_setting_row.tsx b/frontend/settings/hardware_settings/single_setting_row.tsx
index 8f1adf6d2..7e6203b94 100644
--- a/frontend/settings/hardware_settings/single_setting_row.tsx
+++ b/frontend/settings/hardware_settings/single_setting_row.tsx
@@ -26,10 +26,10 @@ export const SingleSettingRow = (props: SingleSettingRowProps) => {
         <Help text={tooltip} position={Position.RIGHT} />
       </Col>
       {settingType === "button"
-        ? <Col xs={5} className={"centered-button-div"}>
+        ? <Col xs={5} className={"centered-button-div low-pad"}>
           {children}
         </Col>
-        : <Col xs={5}>{children}</Col>}
+        : <Col xs={5} className={"low-pad"}>{children}</Col>}
     </Row>
   </Highlight>;
 };
diff --git a/frontend/settings/hardware_settings/space_panel_header.tsx b/frontend/settings/hardware_settings/space_panel_header.tsx
index 1faf83d32..1a5fb378a 100644
--- a/frontend/settings/hardware_settings/space_panel_header.tsx
+++ b/frontend/settings/hardware_settings/space_panel_header.tsx
@@ -1,25 +1,25 @@
-import * as React from "react";
-import { Row, Col } from "../../ui/index";
+import React from "react";
+import { Row, Col } from "../../ui";
 import { t } from "../../i18next_wrapper";
 import { Highlight } from "../maybe_highlight";
 import { DeviceSetting } from "../../constants";
 
 export function SpacePanelHeader() {
-  const width = 4;
+  const width = 3;
   return <div className="label-headings">
     <Highlight settingName={DeviceSetting.axisHeadingLabels}>
       <Row>
-        <Col xs={width} className={"centered-button-div"}>
+        <Col xsOffset={3} xs={width} className={"centered-button-div low-pad"}>
           <label>
             {t("X AXIS")}
           </label>
         </Col>
-        <Col xs={width} className={"centered-button-div"}>
+        <Col xs={width} className={"centered-button-div low-pad"}>
           <label>
             {t("Y AXIS")}
           </label>
         </Col>
-        <Col xs={width} className={"centered-button-div"}>
+        <Col xs={width} className={"centered-button-div low-pad"}>
           <label>
             {t("Z AXIS")}
           </label>


Test patch:
diff --git a/frontend/farm_designer/map/background/__tests__/grid_test.tsx b/frontend/farm_designer/map/background/__tests__/grid_test.tsx
index 1ceacd422..0fdbed0ed 100644
--- a/frontend/farm_designer/map/background/__tests__/grid_test.tsx
+++ b/frontend/farm_designer/map/background/__tests__/grid_test.tsx
@@ -1,4 +1,4 @@
-import * as React from "react";
+import React from "react";
 import { Grid } from "../grid";
 import { shallow } from "enzyme";
 import { GridProps } from "../../interfaces";
@@ -6,15 +6,13 @@ import {
   fakeMapTransformProps,
 } from "../../../../__test_support__/map_transform_props";
 
-describe("<Grid/>", () => {
-  function fakeProps(): GridProps {
-    return {
-      mapTransformProps: fakeMapTransformProps(),
-      zoomLvl: 1,
-      onClick: jest.fn(),
-      onMouseDown: jest.fn(),
-    };
-  }
+describe("<Grid />", () => {
+  const fakeProps = (): GridProps => ({
+    mapTransformProps: fakeMapTransformProps(),
+    zoomLvl: 1,
+    onClick: jest.fn(),
+    onMouseDown: jest.fn(),
+  });
 
   it("renders grid", () => {
     const expectedGridShape = { width: 3000, height: 1500 };
@@ -39,77 +37,52 @@ describe("<Grid/>", () => {
       expect.objectContaining(expectedGridShape));
   });
 
-  it("render default patterns strokes above 0.5 zoom", () => {
-    const p = fakeProps();
-    p.zoomLvl = 0.6;
-    const wrapper = shallow(<Grid {...p} />);
-    const minorGrid = wrapper.find("#minor_grid>path");
-    const majorGrid = wrapper.find("#major_grid>path");
-    const superiorGrid = wrapper.find("#superior_grid>path");
-    expect(minorGrid.props()).toHaveProperty("strokeWidth", "1");
-    expect(majorGrid.props()).toHaveProperty("strokeWidth", "2");
-    expect(superiorGrid.props()).toHaveProperty("strokeWidth", "4");
-  });
-
-  it("change patterns strokes on 0.5 zoom and below", () => {
-    const p = fakeProps();
-    p.zoomLvl = 0.5;
-    const wrapper = shallow(<Grid {...p} />);
-    const minorGrid = wrapper.find("#minor_grid>path");
-    const majorGrid = wrapper.find("#major_grid>path");
-    const superiorGrid = wrapper.find("#superior_grid>path");
-    expect(minorGrid.props()).toHaveProperty("strokeWidth", "0");
-    expect(majorGrid.props()).toHaveProperty("strokeWidth", "3");
-    expect(superiorGrid.props()).toHaveProperty("strokeWidth", "6");
-  });
-
-  it("visualizes axis values every 100mm above 0.5 zoom", () => {
-    const p = fakeProps();
-    p.zoomLvl = 0.6;
-    const wrapper = shallow(<Grid {...p} />);
-    const xAxisValues = wrapper.find(".x-label").children();
-    const yAxisValues = wrapper.find(".y-label").children();
-    expect(xAxisValues).toHaveLength(29);
-    expect(yAxisValues).toHaveLength(14);
-  });
+  it.each<[number, number, number, number]>([
+    [0.6, 1, 2, 4],
+    [0.5, 0, 3, 6],
+  ])("render correct pattern strokes at zoom level: %s",
+    (zoomLvl, minor, major, superior) => {
+      const p = fakeProps();
+      p.zoomLvl = zoomLvl;
+      const wrapper = shallow(<Grid {...p} />);
+      const minorGrid = wrapper.find("#minor_grid>path");
+      const majorGrid = wrapper.find("#major_grid>path");
+      const superiorGrid = wrapper.find("#superior_grid>path");
+      expect(minorGrid.props()).toHaveProperty("strokeWidth", minor);
+      expect(majorGrid.props()).toHaveProperty("strokeWidth", major);
+      expect(superiorGrid.props()).toHaveProperty("strokeWidth", superior);
+    });
 
-  it("visualizes axis values every 200mm between 0.5 and 0.2 excluded zoom", () => {
+  it.each<[number, number, number]>([
+    [0.6, 29, 14],
+    [0.5, 14, 7],
+    [0.2, 5, 2],
+  ])("visualizes axis values at zoom level: %s", (zoomLvl, xCount, yCount) => {
     const p = fakeProps();
-    p.zoomLvl = 0.5;
+    p.zoomLvl = zoomLvl;
     const wrapper = shallow(<Grid {...p} />);
-    const xAxisValues = wrapper.find(".x-label").children();
-    const YAxisValues = wrapper.find(".y-label").children();
-    expect(xAxisValues).toHaveLength(14);
-    expect(YAxisValues).toHaveLength(7);
+    const xAxisValues = wrapper.find("#x-label").children();
+    const yAxisValues = wrapper.find("#y-label").children();
+    expect(xAxisValues).toHaveLength(xCount);
+    expect(yAxisValues).toHaveLength(yCount);
   });
 
-  it("visualizes axis values every 500mm on 0.2 zoom and below", () => {
-    const p = fakeProps();
-    p.zoomLvl = 0.2;
-    const wrapper = shallow(<Grid {...p} />);
-    const xAxisValues = wrapper.find(".x-label").children();
-    const yAxisValues = wrapper.find(".y-label").children();
-    expect(xAxisValues).toHaveLength(5);
-    expect(yAxisValues).toHaveLength(2);
-  });
-
-  it("use transform scale 1 for zoom above 1", () => {
-    const p = fakeProps();
-    p.zoomLvl = 1.1;
-    const wrapper = shallow(<Grid {...p} />);
-    const xTextNode = wrapper.find(".x-label").first();
-    const yTextNode = wrapper.find(".y-label").first();
-    expect(xTextNode.prop("style")).toHaveProperty("transform", "scale(1)");
-    expect(yTextNode.prop("style")).toHaveProperty("transform", "scale(1)");
-  });
-
-  it("use transform scale 1.5 for zoom on 0.5", () => {
-    const p = fakeProps();
-    p.zoomLvl = 0.5;
-    const wrapper = shallow(<Grid {...p} />);
-    const xTextNode = wrapper.find(".x-label").first();
-    const yTextNode = wrapper.find(".y-label").first();
-    expect(xTextNode.prop("style")).toHaveProperty("transform", "scale(1.5)");
-    expect(yTextNode.prop("style")).toHaveProperty("transform", "scale(1.5)");
-  });
+  it.each<[number, number, number, number, number, string, string]>([
+    [1.1, 100, -10, -10, 100, "scale(0.91) ", "scale(0.91) rotate(-90deg)"],
+    [0.5, 200, -20, -20, 200, "scale(2) ", "scale(2) rotate(-90deg)"],
+    [0.1, 500, -100, -100, 500, "scale(10) ", "scale(10) rotate(-90deg)"],
+  ])("has correct transform for zoom level: %s",
+    (zoomLvl, xx, xy, yx, yy, xTransform, yTransform) => {
+      const p = fakeProps();
+      p.zoomLvl = zoomLvl;
+      const wrapper = shallow(<Grid {...p} />);
+      const xTextNodeProps = wrapper.find("#x-label").first().props();
+      const yTextNodeProps = wrapper.find("#y-label").first().props();
+      expect(xTextNodeProps.style?.transform).toEqual(xTransform);
+      expect(yTextNodeProps.style?.transform).toEqual(yTransform);
+      expect(xTextNodeProps.x).toEqual(xx);
+      expect(xTextNodeProps.y).toEqual(xy);
+      expect(yTextNodeProps.x).toEqual(yx);
+      expect(yTextNodeProps.y).toEqual(yy);
+    });
 });