浏览代码

Add stats for nerds (#2453)

Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
Lipis 4 年之前
父节点
当前提交
dd993adc5c

+ 1 - 0
src/actions/actionCanvas.tsx

@@ -64,6 +64,7 @@ export const actionClearCanvas = register({
         exportEmbedScene: appState.exportEmbedScene,
         gridSize: appState.gridSize,
         shouldAddWatermark: appState.shouldAddWatermark,
+        showStats: appState.showStats,
       },
       commitToHistory: true,
     };

+ 2 - 2
src/actions/actionProperties.tsx

@@ -8,7 +8,7 @@ import {
 import {
   getCommonAttributeOfSelectedElements,
   isSomeElementSelected,
-  getTargetElement,
+  getTargetElements,
   canChangeSharpness,
 } from "../scene";
 import { ButtonSelect } from "../components/ButtonSelect";
@@ -561,7 +561,7 @@ export const actionChangeTextAlign = register({
 export const actionChangeSharpness = register({
   name: "changeSharpness",
   perform: (elements, appState, value) => {
-    const targetElements = getTargetElement(
+    const targetElements = getTargetElements(
       getNonDeletedElements(elements),
       appState,
     );

+ 2 - 0
src/appState.ts

@@ -71,6 +71,7 @@ export const getDefaultAppState = (): Omit<
     isLibraryOpen: false,
     fileHandle: null,
     collaborators: new Map(),
+    showStats: false,
   };
 };
 
@@ -146,6 +147,7 @@ const APP_STATE_STORAGE_CONF = (<
   offsetLeft: { browser: false, export: false },
   fileHandle: { browser: false, export: false },
   collaborators: { browser: false, export: false },
+  showStats: { browser: true, export: false },
 });
 
 const _clearAppStateForStorage = <ExportType extends "export" | "browser">(

+ 2 - 2
src/components/Actions.tsx

@@ -7,7 +7,7 @@ import {
   hasStroke,
   canChangeSharpness,
   hasText,
-  getTargetElement,
+  getTargetElements,
 } from "../scene";
 import { t } from "../i18n";
 import { SHAPES } from "../shapes";
@@ -29,7 +29,7 @@ export const SelectedShapeActions = ({
   renderAction: ActionManager["renderAction"];
   elementType: ExcalidrawElement["type"];
 }) => {
-  const targetElements = getTargetElement(
+  const targetElements = getTargetElements(
     getNonDeletedElements(elements),
     appState,
   );

+ 21 - 0
src/components/App.tsx

@@ -175,6 +175,7 @@ import {
   EVENT_SHAPE,
   trackEvent,
 } from "../analytics";
+import { Stats } from "./Stats";
 
 const { history } = createHistory();
 
@@ -377,6 +378,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           lng={getLanguage().lng}
           isCollaborating={this.props.isCollaborating || false}
         />
+        {this.state.showStats && (
+          <Stats
+            appState={this.state}
+            elements={this.scene.getElements()}
+            onClose={this.toggleStats}
+          />
+        )}
         <main>
           <canvas
             id="canvas"
@@ -1133,6 +1141,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     });
   };
 
+  toggleStats = () => {
+    if (!this.state.showStats) {
+      trackEvent(EVENT_DIALOG, "stats");
+    }
+    this.setState({
+      showStats: !this.state.showStats,
+    });
+  };
+
   setScrollToCenter = (remoteElements: readonly ExcalidrawElement[]) => {
     this.setState({
       ...calculateScrollCenter(
@@ -3564,6 +3581,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
             label: t("labels.toggleGridMode"),
             action: this.toggleGridMode,
           },
+          {
+            label: t("labels.toggleStats"),
+            action: this.toggleStats,
+          },
         ],
         top: clientY,
         left: clientX,

+ 37 - 0
src/components/Stats.scss

@@ -0,0 +1,37 @@
+@import "../css/_variables";
+
+.Stats {
+  position: fixed;
+  top: 64px;
+  right: 12px;
+  font-size: 12px;
+  z-index: 999;
+  h3 {
+    margin: 0 24px 8px 0;
+  }
+
+  .close {
+    float: right;
+    height: 16px;
+    width: 16px;
+    cursor: pointer;
+    svg {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  table {
+    width: 100%;
+    th {
+      border-bottom: 1px solid var(--input-border-color);
+      padding: 4px;
+    }
+    tr {
+      td:nth-child(2) {
+        min-width: 48px;
+        text-align: right;
+      }
+    }
+  }
+}

+ 159 - 0
src/components/Stats.tsx

@@ -0,0 +1,159 @@
+import React, { useEffect, useState } from "react";
+import { getCommonBounds } from "../element/bounds";
+import { NonDeletedExcalidrawElement } from "../element/types";
+import {
+  getElementsStorageSize,
+  getTotalStorageSize,
+} from "../excalidraw-app/data/localStorage";
+import { t } from "../i18n";
+import { getTargetElements } from "../scene";
+import { AppState } from "../types";
+import { debounce, nFormatter } from "../utils";
+import { close } from "./icons";
+import { Island } from "./Island";
+import "./Stats.scss";
+
+type StorageSizes = { scene: number; total: number };
+
+const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
+  cb({
+    scene: getElementsStorageSize(),
+    total: getTotalStorageSize(),
+  });
+}, 500);
+
+export const Stats = (props: {
+  appState: AppState;
+  elements: readonly NonDeletedExcalidrawElement[];
+  onClose: () => void;
+}) => {
+  const [storageSizes, setStorageSizes] = useState<StorageSizes>({
+    scene: 0,
+    total: 0,
+  });
+
+  useEffect(() => {
+    getStorageSizes((sizes) => {
+      setStorageSizes(sizes);
+    });
+  });
+
+  useEffect(() => () => getStorageSizes.cancel(), []);
+
+  const boundingBox = getCommonBounds(props.elements);
+  const selectedElements = getTargetElements(props.elements, props.appState);
+  const selectedBoundingBox = getCommonBounds(selectedElements);
+
+  return (
+    <div className="Stats">
+      <Island padding={2}>
+        <div className="close" onClick={props.onClose}>
+          {close}
+        </div>
+        <h3>{t("stats.title")}</h3>
+        <table>
+          <tbody>
+            <tr>
+              <th colSpan={2}>{t("stats.scene")}</th>
+            </tr>
+            <tr>
+              <td>{t("stats.elements")}</td>
+              <td>{props.elements.length}</td>
+            </tr>
+            <tr>
+              <td>{t("stats.width")}</td>
+              <td>{Math.round(boundingBox[2]) - Math.round(boundingBox[0])}</td>
+            </tr>
+            <tr>
+              <td>{t("stats.height")}</td>
+              <td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
+            </tr>
+            <tr>
+              <th colSpan={2}>{t("stats.storage")}</th>
+            </tr>
+            <tr>
+              <td>{t("stats.scene")}</td>
+              <td>{nFormatter(storageSizes.scene, 1)}</td>
+            </tr>
+            <tr>
+              <td>{t("stats.total")}</td>
+              <td>{nFormatter(storageSizes.total, 1)}</td>
+            </tr>
+
+            {selectedElements.length === 1 && (
+              <tr>
+                <th colSpan={2}>{t("stats.element")}</th>
+              </tr>
+            )}
+
+            {selectedElements.length > 1 && (
+              <>
+                <tr>
+                  <th colSpan={2}>{t("stats.selected")}</th>
+                </tr>
+                <tr>
+                  <td>{t("stats.elements")}</td>
+                  <td>{selectedElements.length}</td>
+                </tr>
+              </>
+            )}
+            {selectedElements.length > 0 && (
+              <>
+                <tr>
+                  <td>{"x"}</td>
+                  <td>
+                    {Math.round(
+                      selectedElements.length === 1
+                        ? selectedElements[0].x
+                        : selectedBoundingBox[0],
+                    )}
+                  </td>
+                </tr>
+                <tr>
+                  <td>{"y"}</td>
+                  <td>
+                    {Math.round(
+                      selectedElements.length === 1
+                        ? selectedElements[0].y
+                        : selectedBoundingBox[1],
+                    )}
+                  </td>
+                </tr>
+                <tr>
+                  <td>{t("stats.width")}</td>
+                  <td>
+                    {Math.round(
+                      selectedElements.length === 1
+                        ? selectedElements[0].width
+                        : selectedBoundingBox[2] - selectedBoundingBox[0],
+                    )}
+                  </td>
+                </tr>
+                <tr>
+                  <td>{t("stats.height")}</td>
+                  <td>
+                    {Math.round(
+                      selectedElements.length === 1
+                        ? selectedElements[0].height
+                        : selectedBoundingBox[3] - selectedBoundingBox[1],
+                    )}
+                  </td>
+                </tr>
+              </>
+            )}
+            {selectedElements.length === 1 && (
+              <tr>
+                <td>{t("stats.angle")}</td>
+                <td>
+                  {`${Math.round(
+                    (selectedElements[0].angle * 180) / Math.PI,
+                  )}°`}
+                </td>
+              </tr>
+            )}
+          </tbody>
+        </table>
+      </Island>
+    </div>
+  );
+};

+ 7 - 3
src/excalidraw-app/data/localStorage.ts

@@ -98,16 +98,20 @@ export const importFromLocalStorage = () => {
   return { elements, appState };
 };
 
+export const getElementsStorageSize = () => {
+  const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
+  const elementsSize = elements ? JSON.stringify(elements).length : 0;
+  return elementsSize;
+};
+
 export const getTotalStorageSize = () => {
   const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
   const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
-  const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
   const library = localStorage.getItem(APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
 
   const appStateSize = appState ? JSON.stringify(appState).length : 0;
   const collabSize = collab ? JSON.stringify(collab).length : 0;
-  const elementsSize = elements ? JSON.stringify(elements).length : 0;
   const librarySize = library ? JSON.stringify(library).length : 0;
 
-  return appStateSize + collabSize + elementsSize + librarySize;
+  return appStateSize + collabSize + librarySize + getElementsStorageSize();
 };

+ 13 - 0
src/locales/en.json

@@ -71,6 +71,7 @@
     "ungroup": "Ungroup selection",
     "collaborators": "Collaborators",
     "toggleGridMode": "Toggle grid mode",
+    "toggleStats": "Toggle stats for nerds",
     "addToLibrary": "Add to library",
     "removeFromLibrary": "Remove from library",
     "libraryLoadingMessage": "Loading library...",
@@ -213,5 +214,17 @@
   "charts": {
     "noNumericColumn": "You pasted a spreadsheet without a numeric column.",
     "tooManyColumns": "You pasted a spreadsheet with more than two columns."
+  },
+  "stats": {
+    "angle": "Angle",
+    "element": "Element",
+    "elements": "Elements",
+    "height": "Height",
+    "scene": "Scene",
+    "selected": "Selected",
+    "storage": "Storage",
+    "title": "Stats for nerds",
+    "total": "Total",
+    "width": "Width"
   }
 }

+ 1 - 1
src/scene/index.ts

@@ -4,7 +4,7 @@ export {
   getElementsWithinSelection,
   getCommonAttributeOfSelectedElements,
   getSelectedElements,
-  getTargetElement,
+  getTargetElements,
 } from "./selection";
 export { normalizeScroll, calculateScrollCenter } from "./scroll";
 export {

+ 6 - 10
src/scene/selection.ts

@@ -33,9 +33,8 @@ export const getElementsWithinSelection = (
 export const isSomeElementSelected = (
   elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
-): boolean => {
-  return elements.some((element) => appState.selectedElementIds[element.id]);
-};
+): boolean =>
+  elements.some((element) => appState.selectedElementIds[element.id]);
 
 /**
  * Returns common attribute (picked by `getAttribute` callback) of selected
@@ -59,15 +58,12 @@ export const getCommonAttributeOfSelectedElements = <T>(
 export const getSelectedElements = (
   elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
-) => {
-  return elements.filter((element) => appState.selectedElementIds[element.id]);
-};
+) => elements.filter((element) => appState.selectedElementIds[element.id]);
 
-export const getTargetElement = (
+export const getTargetElements = (
   elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
-) => {
-  return appState.editingElement
+) =>
+  appState.editingElement
     ? [appState.editingElement]
     : getSelectedElements(elements, appState);
-};

+ 67 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -66,6 +66,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -525,6 +526,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -966,6 +968,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -1735,6 +1738,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -1935,6 +1939,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -2379,6 +2384,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -2620,6 +2626,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -2778,6 +2785,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -3243,6 +3251,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -3545,6 +3554,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -3742,6 +3752,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -3975,6 +3986,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -4219,6 +4231,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -4613,6 +4626,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -4877,6 +4891,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -5195,6 +5210,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -5372,6 +5388,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -5527,6 +5544,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -5978,6 +5996,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -6280,6 +6299,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -8256,6 +8276,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -8610,6 +8631,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -8854,6 +8876,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -9099,6 +9122,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -9400,6 +9424,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -9555,6 +9580,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -9710,6 +9736,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -9865,6 +9892,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -10046,6 +10074,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -10227,6 +10256,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -10408,6 +10438,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -10589,6 +10620,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -10744,6 +10776,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -10899,6 +10932,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -11080,6 +11114,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -11235,6 +11270,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -11427,6 +11463,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -12127,6 +12164,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -12367,6 +12405,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": true,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -12458,6 +12497,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -12551,6 +12591,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -12706,6 +12747,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -13005,6 +13047,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -13304,6 +13347,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -13457,6 +13501,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -13646,6 +13691,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -13892,6 +13938,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -14201,6 +14248,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -15031,6 +15079,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -15330,6 +15379,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -15633,6 +15683,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -16002,6 +16053,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -16166,6 +16218,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -16472,6 +16525,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -16705,6 +16759,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -16953,6 +17008,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -17261,6 +17317,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -17354,6 +17411,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -17520,6 +17578,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -18319,6 +18378,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -18412,6 +18472,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -19181,6 +19242,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -19575,6 +19637,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -19815,6 +19878,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": true,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -19908,6 +19972,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -20389,6 +20454,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",
@@ -20480,6 +20546,7 @@ Object {
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
   "showShortcutsDialog": false,
+  "showStats": false,
   "startBoundElement": null,
   "suggestedBindings": Array [],
   "viewBackgroundColor": "#ffffff",

+ 19 - 15
src/tests/regressionTests.test.tsx

@@ -1,23 +1,23 @@
-import { reseed } from "../random";
+import { queryByText } from "@testing-library/react";
 import React from "react";
 import ReactDOM from "react-dom";
+import { copiedStyles } from "../actions/actionStyles";
+import { ExcalidrawElement } from "../element/types";
+import { setLanguage, t } from "../i18n";
+import { CODES, KEYS } from "../keys";
+import Excalidraw from "../packages/excalidraw/index";
+import { reseed } from "../random";
 import * as Renderer from "../renderer/renderScene";
+import { setDateTimeForTests } from "../utils";
+import { API } from "./helpers/api";
+import { Keyboard, Pointer, UI } from "./helpers/ui";
 import {
-  waitFor,
-  render,
-  screen,
   fireEvent,
   GlobalTestState,
+  render,
+  screen,
+  waitFor,
 } from "./test-utils";
-import Excalidraw from "../packages/excalidraw/index";
-import { setLanguage } from "../i18n";
-import { setDateTimeForTests } from "../utils";
-import { ExcalidrawElement } from "../element/types";
-import { queryByText } from "@testing-library/react";
-import { copiedStyles } from "../actions/actionStyles";
-import { UI, Pointer, Keyboard } from "./helpers/ui";
-import { API } from "./helpers/api";
-import { CODES, KEYS } from "../keys";
 
 const { h } = window;
 
@@ -633,10 +633,14 @@ describe("regression tests", () => {
     });
     const contextMenu = document.querySelector(".context-menu");
     const options = contextMenu?.querySelectorAll(".context-menu-option");
-    const expectedOptions = ["Select all", "Toggle grid mode"];
+    const expectedOptions = [
+      t("labels.selectAll"),
+      t("labels.toggleGridMode"),
+      t("labels.toggleStats"),
+    ];
 
     expect(contextMenu).not.toBeNull();
-    expect(options?.length).toBe(2);
+    expect(options?.length).toBe(3);
     expect(options?.item(0).textContent).toBe(expectedOptions[0]);
   });
 

+ 1 - 0
src/types.ts

@@ -95,6 +95,7 @@ export type AppState = {
   isLibraryOpen: boolean;
   fileHandle: import("browser-nativefs").FileSystemHandle | null;
   collaborators: Map<string, Collaborator>;
+  showStats: boolean;
 };
 
 export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };

+ 23 - 0
src/utils.ts

@@ -133,6 +133,9 @@ export const debounce = <T extends any[]>(
       fn(...lastArgs);
     }
   };
+  ret.cancel = () => {
+    clearTimeout(handle);
+  };
   return ret;
 };
 
@@ -336,3 +339,23 @@ export const withBatchedUpdates = <
   ((event) => {
     unstable_batchedUpdates(func as TFunction, event);
   }) as TFunction;
+
+//https://stackoverflow.com/a/9462382/8418
+export const nFormatter = (num: number, digits: number): string => {
+  const si = [
+    { value: 1, symbol: "b" },
+    { value: 1e3, symbol: "k" },
+    { value: 1e6, symbol: "M" },
+    { value: 1e9, symbol: "G" },
+  ];
+  const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
+  let index;
+  for (index = si.length - 1; index > 0; index--) {
+    if (num >= si[index].value) {
+      break;
+    }
+  }
+  return (
+    (num / si[index].value).toFixed(digits).replace(rx, "$1") + si[index].symbol
+  );
+};