parent
23eb08088e
commit
4e4802b19e
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Spreadsheet,
|
Spreadsheet,
|
||||||
sortSpreadsheet,
|
|
||||||
tryParseCells,
|
tryParseCells,
|
||||||
tryParseNumber,
|
tryParseNumber,
|
||||||
VALID_SPREADSHEET,
|
VALID_SPREADSHEET,
|
||||||
@ -119,29 +118,4 @@ describe("charts", () => {
|
|||||||
expect(values).toEqual([61, -60, 85, -67, 54, 95]);
|
expect(values).toEqual([61, -60, 85, -67, 54, 95]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sortSpreadsheet", () => {
|
|
||||||
it("sorts strictly numerical labels columns in ascending order", () => {
|
|
||||||
const spreadsheet = [
|
|
||||||
["x", "y"],
|
|
||||||
["1°", "1"],
|
|
||||||
["9°", "2"],
|
|
||||||
["3°", "3"],
|
|
||||||
["6°", "4"],
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = tryParseCells(spreadsheet);
|
|
||||||
|
|
||||||
expect(result.type).toBe(VALID_SPREADSHEET);
|
|
||||||
|
|
||||||
const { title, labels, values } = sortSpreadsheet(
|
|
||||||
(result as { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet })
|
|
||||||
.spreadsheet,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(title).toEqual("y");
|
|
||||||
expect(labels).toEqual(["1°", "3°", "6°", "9°"]);
|
|
||||||
expect(values).toEqual([1, 3, 4, 2]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -36,8 +36,11 @@ type ParseSpreadsheetResult =
|
|||||||
| { type: typeof NOT_SPREADSHEET; reason: string }
|
| { type: typeof NOT_SPREADSHEET; reason: string }
|
||||||
| { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet };
|
| { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private exported for testing
|
||||||
|
*/
|
||||||
export const tryParseNumber = (s: string): number | null => {
|
export const tryParseNumber = (s: string): number | null => {
|
||||||
const match = /^([-+]?)[$€£¥₩]?([-+]?)([\d.,]+)[%°]?$/.exec(s);
|
const match = /^([-+]?)[$€£¥₩]?([-+]?)([\d.,]+)[%]?$/.exec(s);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -162,32 +165,6 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sortSpreadsheet = (spreadsheet: Spreadsheet) => {
|
|
||||||
const rows = [] as { label: string; value: number }[];
|
|
||||||
if (spreadsheet.labels == null || spreadsheet.values == null) {
|
|
||||||
return spreadsheet;
|
|
||||||
}
|
|
||||||
if (spreadsheet.labels.every((val) => tryParseNumber(val))) {
|
|
||||||
for (let i = 0; i < spreadsheet.labels.length; i++) {
|
|
||||||
rows.push({
|
|
||||||
label: spreadsheet.labels[i],
|
|
||||||
value: spreadsheet.values[i],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
rows.sort((a, b) => {
|
|
||||||
const aParsed = tryParseNumber(a.label)!;
|
|
||||||
const bParsed = tryParseNumber(b.label)!;
|
|
||||||
return aParsed - bParsed;
|
|
||||||
});
|
|
||||||
const newSpreadsheet = {} as Spreadsheet;
|
|
||||||
newSpreadsheet.title = spreadsheet.title;
|
|
||||||
newSpreadsheet.labels = rows.flatMap((row) => row.label);
|
|
||||||
newSpreadsheet.values = rows.flatMap((row) => row.value);
|
|
||||||
return newSpreadsheet;
|
|
||||||
}
|
|
||||||
return spreadsheet;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
|
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
|
||||||
|
|
||||||
// Put all the common properties here so when the whole chart is selected
|
// Put all the common properties here so when the whole chart is selected
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React, { useLayoutEffect, useRef, useState } from "react";
|
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import {
|
import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
|
||||||
ChartElements,
|
|
||||||
renderSpreadsheet,
|
|
||||||
sortSpreadsheet,
|
|
||||||
Spreadsheet,
|
|
||||||
tryParseNumber,
|
|
||||||
} from "../charts";
|
|
||||||
import { ChartType } from "../element/types";
|
import { ChartType } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
@ -22,7 +16,6 @@ import {
|
|||||||
getContainerElement,
|
getContainerElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
|
||||||
|
|
||||||
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||||
|
|
||||||
@ -31,7 +24,6 @@ const ChartPreviewBtn = (props: {
|
|||||||
chartType: ChartType;
|
chartType: ChartType;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: OnInsertChart;
|
onClick: OnInsertChart;
|
||||||
sortChartLabels: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const previewRef = useRef<HTMLDivElement | null>(null);
|
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [chartElements, setChartElements] = useState<ChartElements | null>(
|
const [chartElements, setChartElements] = useState<ChartElements | null>(
|
||||||
@ -51,10 +43,12 @@ const ChartPreviewBtn = (props: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spreadsheet = props.sortChartLabels
|
elements = renderSpreadsheet(
|
||||||
? sortSpreadsheet(props.spreadsheet)
|
props.chartType,
|
||||||
: props.spreadsheet;
|
props.spreadsheet,
|
||||||
elements = renderSpreadsheet(props.chartType, spreadsheet, 0, 0);
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
elements.forEach(
|
elements.forEach(
|
||||||
(el) =>
|
(el) =>
|
||||||
isTextElement(el) &&
|
isTextElement(el) &&
|
||||||
@ -85,12 +79,7 @@ const ChartPreviewBtn = (props: {
|
|||||||
previewNode.replaceChildren();
|
previewNode.replaceChildren();
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
}, [
|
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||||
props.spreadsheet,
|
|
||||||
props.chartType,
|
|
||||||
props.selected,
|
|
||||||
props.sortChartLabels,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -133,10 +122,6 @@ export const PasteChartDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const showSortChartLabels = appState.pasteDialog.data?.labels?.every((val) =>
|
|
||||||
tryParseNumber(val),
|
|
||||||
);
|
|
||||||
const [sortChartLabels, setSortChartLabels] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -152,28 +137,14 @@ export const PasteChartDialog = ({
|
|||||||
spreadsheet={appState.pasteDialog.data}
|
spreadsheet={appState.pasteDialog.data}
|
||||||
selected={appState.currentChartType === "bar"}
|
selected={appState.currentChartType === "bar"}
|
||||||
onClick={handleChartClick}
|
onClick={handleChartClick}
|
||||||
sortChartLabels={(showSortChartLabels && sortChartLabels) ?? false}
|
|
||||||
/>
|
/>
|
||||||
<ChartPreviewBtn
|
<ChartPreviewBtn
|
||||||
chartType="line"
|
chartType="line"
|
||||||
spreadsheet={appState.pasteDialog.data}
|
spreadsheet={appState.pasteDialog.data}
|
||||||
selected={appState.currentChartType === "line"}
|
selected={appState.currentChartType === "line"}
|
||||||
onClick={handleChartClick}
|
onClick={handleChartClick}
|
||||||
sortChartLabels={(showSortChartLabels && sortChartLabels) ?? false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showSortChartLabels && (
|
|
||||||
<div className={"container"}>
|
|
||||||
<CheckboxItem
|
|
||||||
checked={sortChartLabels}
|
|
||||||
onChange={(checked: boolean) => {
|
|
||||||
setSortChartLabels(checked);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("labels.sortChartLabels")}
|
|
||||||
</CheckboxItem>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -46,15 +46,12 @@ class LocalFileManager extends FileManager {
|
|||||||
const saveDataStateToLocalStorage = (
|
const saveDataStateToLocalStorage = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
appStateOnly = false,
|
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!appStateOnly) {
|
localStorage.setItem(
|
||||||
localStorage.setItem(
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
JSON.stringify(clearElementsForLocalStorage(elements)),
|
||||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
||||||
@ -75,12 +72,8 @@ export class LocalData {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
onFilesSaved: () => void,
|
onFilesSaved: () => void,
|
||||||
appStateOnly = false,
|
|
||||||
) => {
|
) => {
|
||||||
saveDataStateToLocalStorage(elements, appState, appStateOnly);
|
saveDataStateToLocalStorage(elements, appState);
|
||||||
if (appStateOnly) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.fileStorage.saveFiles({
|
await this.fileStorage.saveFiles({
|
||||||
elements,
|
elements,
|
||||||
@ -104,14 +97,6 @@ export class LocalData {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Saves the AppState, only if saving is paused. */
|
|
||||||
static saveAppState = (appState: AppState) => {
|
|
||||||
// we need to make the `isSavePaused` check synchronously (undebounced)
|
|
||||||
if (this.isSavePaused()) {
|
|
||||||
this._save([], appState, {}, () => {}, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static flushSave = () => {
|
static flushSave = () => {
|
||||||
this._save.flush();
|
this._save.flush();
|
||||||
};
|
};
|
||||||
|
@ -234,7 +234,7 @@ const initializeScene = async (opts: {
|
|||||||
...restoreAppState(
|
...restoreAppState(
|
||||||
{
|
{
|
||||||
...scene?.appState,
|
...scene?.appState,
|
||||||
...localDataState?.appState,
|
theme: localDataState?.appState?.theme || scene?.appState?.theme,
|
||||||
},
|
},
|
||||||
excalidrawAPI.getAppState(),
|
excalidrawAPI.getAppState(),
|
||||||
),
|
),
|
||||||
@ -590,8 +590,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
LocalData.saveAppState(appState);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"pasteAsPlaintext": "Paste as plaintext",
|
"pasteAsPlaintext": "Paste as plaintext",
|
||||||
"pasteCharts": "Paste charts",
|
"pasteCharts": "Paste charts",
|
||||||
"sortChartLabels": "Sort numerical labels in ascending order",
|
|
||||||
"selectAll": "Select all",
|
"selectAll": "Select all",
|
||||||
"multiSelect": "Add element to selection",
|
"multiSelect": "Add element to selection",
|
||||||
"moveCanvas": "Move canvas",
|
"moveCanvas": "Move canvas",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user