feat(editor): support radar chart and multiple series for other chart types (#10824)
This commit is contained in:
@@ -240,22 +240,21 @@ export const DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE = {
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// !!!MUST BE WITHOUT GRAY, TRANSPARENT AND BLACK!!!
|
// !!!MUST BE WITHOUT GRAY, TRANSPARENT AND BLACK!!!
|
||||||
export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) =>
|
export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) => [
|
||||||
[
|
// 2nd row
|
||||||
// 2nd row
|
COLOR_PALETTE.cyan[index],
|
||||||
COLOR_PALETTE.cyan[index],
|
COLOR_PALETTE.blue[index],
|
||||||
COLOR_PALETTE.blue[index],
|
COLOR_PALETTE.violet[index],
|
||||||
COLOR_PALETTE.violet[index],
|
COLOR_PALETTE.grape[index],
|
||||||
COLOR_PALETTE.grape[index],
|
COLOR_PALETTE.pink[index],
|
||||||
COLOR_PALETTE.pink[index],
|
|
||||||
|
|
||||||
// 3rd row
|
// 3rd row
|
||||||
COLOR_PALETTE.green[index],
|
COLOR_PALETTE.green[index],
|
||||||
COLOR_PALETTE.teal[index],
|
COLOR_PALETTE.teal[index],
|
||||||
COLOR_PALETTE.yellow[index],
|
COLOR_PALETTE.yellow[index],
|
||||||
COLOR_PALETTE.orange[index],
|
COLOR_PALETTE.orange[index],
|
||||||
COLOR_PALETTE.red[index],
|
COLOR_PALETTE.red[index],
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// other helpers
|
// other helpers
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
ValueOf,
|
ValueOf,
|
||||||
} from "@excalidraw/common/utility-types";
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line" | "radar";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
||||||
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
|
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
|
||||||
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
|
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ export const actionClearCanvas = register({
|
|||||||
gridStep: appState.gridStep,
|
gridStep: appState.gridStep,
|
||||||
gridModeEnabled: appState.gridModeEnabled,
|
gridModeEnabled: appState.gridModeEnabled,
|
||||||
stats: appState.stats,
|
stats: appState.stats,
|
||||||
pasteDialog: appState.pasteDialog,
|
|
||||||
activeTool:
|
activeTool:
|
||||||
appState.activeTool.type === "image"
|
appState.activeTool.type === "image"
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
showWelcomeScreen: false,
|
showWelcomeScreen: false,
|
||||||
theme: THEME.LIGHT,
|
theme: THEME.LIGHT,
|
||||||
collaborators: new Map(),
|
collaborators: new Map(),
|
||||||
currentChartType: "bar",
|
|
||||||
currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
|
currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||||
currentItemEndArrowhead: "arrow",
|
currentItemEndArrowhead: "arrow",
|
||||||
currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
|
currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
|
||||||
@@ -83,7 +82,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
openPopup: null,
|
openPopup: null,
|
||||||
openSidebar: null,
|
openSidebar: null,
|
||||||
openDialog: null,
|
openDialog: null,
|
||||||
pasteDialog: { shown: false, data: null },
|
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
scrolledOutside: false,
|
scrolledOutside: false,
|
||||||
@@ -150,7 +148,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
showWelcomeScreen: { browser: true, export: false, server: false },
|
showWelcomeScreen: { browser: true, export: false, server: false },
|
||||||
theme: { browser: true, export: false, server: false },
|
theme: { browser: true, export: false, server: false },
|
||||||
collaborators: { browser: false, export: false, server: false },
|
collaborators: { browser: false, export: false, server: false },
|
||||||
currentChartType: { browser: true, export: false, server: false },
|
|
||||||
currentItemBackgroundColor: { browser: true, export: false, server: false },
|
currentItemBackgroundColor: { browser: true, export: false, server: false },
|
||||||
currentItemEndArrowhead: { browser: true, export: false, server: false },
|
currentItemEndArrowhead: { browser: true, export: false, server: false },
|
||||||
currentItemFillStyle: { browser: true, export: false, server: false },
|
currentItemFillStyle: { browser: true, export: false, server: false },
|
||||||
@@ -212,7 +209,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
openPopup: { browser: false, export: false, server: false },
|
openPopup: { browser: false, export: false, server: false },
|
||||||
openSidebar: { browser: true, export: false, server: false },
|
openSidebar: { browser: true, export: false, server: false },
|
||||||
openDialog: { browser: false, export: false, server: false },
|
openDialog: { browser: false, export: false, server: false },
|
||||||
pasteDialog: { browser: false, export: false, server: false },
|
|
||||||
previousSelectedElementIds: { browser: true, export: false, server: false },
|
previousSelectedElementIds: { browser: true, export: false, server: false },
|
||||||
resizingElement: { browser: false, export: false, server: false },
|
resizingElement: { browser: false, export: false, server: false },
|
||||||
scrolledOutside: { browser: true, export: false, server: false },
|
scrolledOutside: { browser: true, export: false, server: false },
|
||||||
|
|||||||
+1063
-16
File diff suppressed because it is too large
Load Diff
@@ -1,481 +0,0 @@
|
|||||||
import { pointFrom } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import {
|
|
||||||
COLOR_PALETTE,
|
|
||||||
DEFAULT_CHART_COLOR_INDEX,
|
|
||||||
getAllColorsSpecificShade,
|
|
||||||
DEFAULT_FONT_FAMILY,
|
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
VERTICAL_ALIGN,
|
|
||||||
randomId,
|
|
||||||
isDevEnv,
|
|
||||||
FONT_SIZES,
|
|
||||||
} from "@excalidraw/common";
|
|
||||||
|
|
||||||
import {
|
|
||||||
newTextElement,
|
|
||||||
newLinearElement,
|
|
||||||
newElement,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
|
||||||
|
|
||||||
const BAR_WIDTH = 32;
|
|
||||||
const BAR_GAP = 12;
|
|
||||||
const BAR_HEIGHT = 256;
|
|
||||||
const GRID_OPACITY = 50;
|
|
||||||
|
|
||||||
export interface Spreadsheet {
|
|
||||||
title: string | null;
|
|
||||||
labels: string[] | null;
|
|
||||||
values: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
|
||||||
export const VALID_SPREADSHEET = "VALID_SPREADSHEET";
|
|
||||||
|
|
||||||
type ParseSpreadsheetResult =
|
|
||||||
| { type: typeof NOT_SPREADSHEET; reason: string }
|
|
||||||
| { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private exported for testing
|
|
||||||
*/
|
|
||||||
export const tryParseNumber = (s: string): number | null => {
|
|
||||||
const match = /^([-+]?)[$€£¥₩]?([-+]?)([\d.,]+)[%]?$/.exec(s);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parseFloat(`${(match[1] || match[2]) + match[3]}`.replace(/,/g, ""));
|
|
||||||
};
|
|
||||||
|
|
||||||
const isNumericColumn = (lines: string[][], columnIndex: number) =>
|
|
||||||
lines.slice(1).every((line) => tryParseNumber(line[columnIndex]) !== null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private exported for testing
|
|
||||||
*/
|
|
||||||
export const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
|
|
||||||
const numCols = cells[0].length;
|
|
||||||
|
|
||||||
if (numCols > 2) {
|
|
||||||
return { type: NOT_SPREADSHEET, reason: "More than 2 columns" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numCols === 1) {
|
|
||||||
if (!isNumericColumn(cells, 0)) {
|
|
||||||
return { type: NOT_SPREADSHEET, reason: "Value is not numeric" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasHeader = tryParseNumber(cells[0][0]) === null;
|
|
||||||
const values = (hasHeader ? cells.slice(1) : cells).map((line) =>
|
|
||||||
tryParseNumber(line[0]),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (values.length < 2) {
|
|
||||||
return { type: NOT_SPREADSHEET, reason: "Less than two rows" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: VALID_SPREADSHEET,
|
|
||||||
spreadsheet: {
|
|
||||||
title: hasHeader ? cells[0][0] : null,
|
|
||||||
labels: null,
|
|
||||||
values: values as number[],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelColumnNumeric = isNumericColumn(cells, 0);
|
|
||||||
const valueColumnNumeric = isNumericColumn(cells, 1);
|
|
||||||
|
|
||||||
if (!labelColumnNumeric && !valueColumnNumeric) {
|
|
||||||
return { type: NOT_SPREADSHEET, reason: "Value is not numeric" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const [labelColumnIndex, valueColumnIndex] = valueColumnNumeric
|
|
||||||
? [0, 1]
|
|
||||||
: [1, 0];
|
|
||||||
const hasHeader = tryParseNumber(cells[0][valueColumnIndex]) === null;
|
|
||||||
const rows = hasHeader ? cells.slice(1) : cells;
|
|
||||||
|
|
||||||
if (rows.length < 2) {
|
|
||||||
return { type: NOT_SPREADSHEET, reason: "Less than 2 rows" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: VALID_SPREADSHEET,
|
|
||||||
spreadsheet: {
|
|
||||||
title: hasHeader ? cells[0][valueColumnIndex] : null,
|
|
||||||
labels: rows.map((row) => row[labelColumnIndex]),
|
|
||||||
values: rows.map((row) => tryParseNumber(row[valueColumnIndex])!),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const transposeCells = (cells: string[][]) => {
|
|
||||||
const nextCells: string[][] = [];
|
|
||||||
for (let col = 0; col < cells[0].length; col++) {
|
|
||||||
const nextCellRow: string[] = [];
|
|
||||||
for (let row = 0; row < cells.length; row++) {
|
|
||||||
nextCellRow.push(cells[row][col]);
|
|
||||||
}
|
|
||||||
nextCells.push(nextCellRow);
|
|
||||||
}
|
|
||||||
return nextCells;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
|
||||||
// Copy/paste from excel, spreadsheets, tsv, csv.
|
|
||||||
// For now we only accept 2 columns with an optional header
|
|
||||||
|
|
||||||
// Check for tab separated values
|
|
||||||
let lines = text
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.trim().split("\t"));
|
|
||||||
|
|
||||||
// Check for comma separated files
|
|
||||||
if (lines.length && lines[0].length !== 2) {
|
|
||||||
lines = text
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.trim().split(","));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lines.length === 0) {
|
|
||||||
return { type: NOT_SPREADSHEET, reason: "No values" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const numColsFirstLine = lines[0].length;
|
|
||||||
const isSpreadsheet = lines.every((line) => line.length === numColsFirstLine);
|
|
||||||
|
|
||||||
if (!isSpreadsheet) {
|
|
||||||
return {
|
|
||||||
type: NOT_SPREADSHEET,
|
|
||||||
reason: "All rows don't have same number of columns",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = tryParseCells(lines);
|
|
||||||
if (result.type !== VALID_SPREADSHEET) {
|
|
||||||
const transposedResults = tryParseCells(transposeCells(lines));
|
|
||||||
if (transposedResults.type === VALID_SPREADSHEET) {
|
|
||||||
return transposedResults;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
|
|
||||||
|
|
||||||
// Put all the common properties here so when the whole chart is selected
|
|
||||||
// the properties dialog shows the correct selected values
|
|
||||||
const commonProps = {
|
|
||||||
fillStyle: "hachure",
|
|
||||||
fontFamily: DEFAULT_FONT_FAMILY,
|
|
||||||
fontSize: DEFAULT_FONT_SIZE,
|
|
||||||
opacity: 100,
|
|
||||||
roughness: 1,
|
|
||||||
strokeColor: COLOR_PALETTE.black,
|
|
||||||
roundness: null,
|
|
||||||
strokeStyle: "solid",
|
|
||||||
strokeWidth: 1,
|
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
|
||||||
locked: false,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const getChartDimensions = (spreadsheet: Spreadsheet) => {
|
|
||||||
const chartWidth =
|
|
||||||
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
|
|
||||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
|
||||||
return { chartWidth, chartHeight };
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartXLabels = (
|
|
||||||
spreadsheet: Spreadsheet,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
groupId: string,
|
|
||||||
backgroundColor: string,
|
|
||||||
): ChartElements => {
|
|
||||||
return (
|
|
||||||
spreadsheet.labels?.map((label, index) => {
|
|
||||||
return newTextElement({
|
|
||||||
groupIds: [groupId],
|
|
||||||
backgroundColor,
|
|
||||||
...commonProps,
|
|
||||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
|
||||||
y: y + BAR_GAP / 2,
|
|
||||||
width: BAR_WIDTH,
|
|
||||||
angle: 5.87 as Radians,
|
|
||||||
fontSize: FONT_SIZES.sm,
|
|
||||||
textAlign: "center",
|
|
||||||
verticalAlign: "top",
|
|
||||||
});
|
|
||||||
}) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartYLabels = (
|
|
||||||
spreadsheet: Spreadsheet,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
groupId: string,
|
|
||||||
backgroundColor: string,
|
|
||||||
): ChartElements => {
|
|
||||||
const minYLabel = newTextElement({
|
|
||||||
groupIds: [groupId],
|
|
||||||
backgroundColor,
|
|
||||||
...commonProps,
|
|
||||||
x: x - BAR_GAP,
|
|
||||||
y: y - BAR_GAP,
|
|
||||||
text: "0",
|
|
||||||
textAlign: "right",
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxYLabel = newTextElement({
|
|
||||||
groupIds: [groupId],
|
|
||||||
backgroundColor,
|
|
||||||
...commonProps,
|
|
||||||
x: x - BAR_GAP,
|
|
||||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
|
||||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
|
||||||
textAlign: "right",
|
|
||||||
});
|
|
||||||
|
|
||||||
return [minYLabel, maxYLabel];
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartLines = (
|
|
||||||
spreadsheet: Spreadsheet,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
groupId: string,
|
|
||||||
backgroundColor: string,
|
|
||||||
): ChartElements => {
|
|
||||||
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
|
|
||||||
const xLine = newLinearElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
type: "line",
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width: chartWidth,
|
|
||||||
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
|
||||||
});
|
|
||||||
|
|
||||||
const yLine = newLinearElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
type: "line",
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
height: chartHeight,
|
|
||||||
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxLine = newLinearElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
type: "line",
|
|
||||||
x,
|
|
||||||
y: y - BAR_HEIGHT - BAR_GAP,
|
|
||||||
strokeStyle: "dotted",
|
|
||||||
width: chartWidth,
|
|
||||||
opacity: GRID_OPACITY,
|
|
||||||
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
|
||||||
});
|
|
||||||
|
|
||||||
return [xLine, yLine, maxLine];
|
|
||||||
};
|
|
||||||
|
|
||||||
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
|
|
||||||
const chartBaseElements = (
|
|
||||||
spreadsheet: Spreadsheet,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
groupId: string,
|
|
||||||
backgroundColor: string,
|
|
||||||
debug?: boolean,
|
|
||||||
): ChartElements => {
|
|
||||||
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
|
|
||||||
|
|
||||||
const title = spreadsheet.title
|
|
||||||
? newTextElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
text: spreadsheet.title,
|
|
||||||
x: x + chartWidth / 2,
|
|
||||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
|
||||||
roundness: null,
|
|
||||||
textAlign: "center",
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const debugRect = debug
|
|
||||||
? newElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
type: "rectangle",
|
|
||||||
x,
|
|
||||||
y: y - chartHeight,
|
|
||||||
width: chartWidth,
|
|
||||||
height: chartHeight,
|
|
||||||
strokeColor: COLOR_PALETTE.black,
|
|
||||||
fillStyle: "solid",
|
|
||||||
opacity: 6,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return [
|
|
||||||
...(debugRect ? [debugRect] : []),
|
|
||||||
...(title ? [title] : []),
|
|
||||||
...chartXLabels(spreadsheet, x, y, groupId, backgroundColor),
|
|
||||||
...chartYLabels(spreadsheet, x, y, groupId, backgroundColor),
|
|
||||||
...chartLines(spreadsheet, x, y, groupId, backgroundColor),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartTypeBar = (
|
|
||||||
spreadsheet: Spreadsheet,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
): ChartElements => {
|
|
||||||
const max = Math.max(...spreadsheet.values);
|
|
||||||
const groupId = randomId();
|
|
||||||
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
|
|
||||||
|
|
||||||
const bars = spreadsheet.values.map((value, index) => {
|
|
||||||
const barHeight = (value / max) * BAR_HEIGHT;
|
|
||||||
return newElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
type: "rectangle",
|
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
|
|
||||||
y: y - barHeight - BAR_GAP,
|
|
||||||
width: BAR_WIDTH,
|
|
||||||
height: barHeight,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
...bars,
|
|
||||||
...chartBaseElements(
|
|
||||||
spreadsheet,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
groupId,
|
|
||||||
backgroundColor,
|
|
||||||
isDevEnv(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartTypeLine = (
|
|
||||||
spreadsheet: Spreadsheet,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
): ChartElements => {
|
|
||||||
const max = Math.max(...spreadsheet.values);
|
|
||||||
const groupId = randomId();
|
|
||||||
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
const points = [];
|
|
||||||
for (const value of spreadsheet.values) {
|
|
||||||
const cx = index * (BAR_WIDTH + BAR_GAP);
|
|
||||||
const cy = -(value / max) * BAR_HEIGHT;
|
|
||||||
points.push([cx, cy]);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxX = Math.max(...points.map((element) => element[0]));
|
|
||||||
const maxY = Math.max(...points.map((element) => element[1]));
|
|
||||||
const minX = Math.min(...points.map((element) => element[0]));
|
|
||||||
const minY = Math.min(...points.map((element) => element[1]));
|
|
||||||
|
|
||||||
const line = newLinearElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
type: "line",
|
|
||||||
x: x + BAR_GAP + BAR_WIDTH / 2,
|
|
||||||
y: y - BAR_GAP,
|
|
||||||
height: maxY - minY,
|
|
||||||
width: maxX - minX,
|
|
||||||
strokeWidth: 2,
|
|
||||||
points: points as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dots = spreadsheet.values.map((value, index) => {
|
|
||||||
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
|
|
||||||
const cy = -(value / max) * BAR_HEIGHT + BAR_GAP / 2;
|
|
||||||
return newElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
fillStyle: "solid",
|
|
||||||
strokeWidth: 2,
|
|
||||||
type: "ellipse",
|
|
||||||
x: x + cx + BAR_WIDTH / 2,
|
|
||||||
y: y + cy - BAR_GAP * 2,
|
|
||||||
width: BAR_GAP,
|
|
||||||
height: BAR_GAP,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const lines = spreadsheet.values.map((value, index) => {
|
|
||||||
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
|
|
||||||
const cy = (value / max) * BAR_HEIGHT + BAR_GAP / 2 + BAR_GAP;
|
|
||||||
return newLinearElement({
|
|
||||||
backgroundColor,
|
|
||||||
groupIds: [groupId],
|
|
||||||
...commonProps,
|
|
||||||
type: "line",
|
|
||||||
x: x + cx + BAR_WIDTH / 2 + BAR_GAP / 2,
|
|
||||||
y: y - cy,
|
|
||||||
height: cy,
|
|
||||||
strokeStyle: "dotted",
|
|
||||||
opacity: GRID_OPACITY,
|
|
||||||
points: [pointFrom(0, 0), pointFrom(0, cy)],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
...chartBaseElements(
|
|
||||||
spreadsheet,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
groupId,
|
|
||||||
backgroundColor,
|
|
||||||
isDevEnv(),
|
|
||||||
),
|
|
||||||
line,
|
|
||||||
...lines,
|
|
||||||
...dots,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renderSpreadsheet = (
|
|
||||||
chartType: string,
|
|
||||||
spreadsheet: Spreadsheet,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
): ChartElements => {
|
|
||||||
if (chartType === "line") {
|
|
||||||
return chartTypeLine(spreadsheet, x, y);
|
|
||||||
}
|
|
||||||
return chartTypeBar(spreadsheet, x, y);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { isDevEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { newElement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { commonProps } from "./charts.constants";
|
||||||
|
import {
|
||||||
|
chartBaseElements,
|
||||||
|
chartXLabels,
|
||||||
|
createSeriesLegend,
|
||||||
|
getBackgroundColor,
|
||||||
|
getCartesianChartLayout,
|
||||||
|
getChartDimensions,
|
||||||
|
getColorOffset,
|
||||||
|
getRotatedTextElementBottom,
|
||||||
|
getSeriesColors,
|
||||||
|
} from "./charts.helpers";
|
||||||
|
|
||||||
|
import type { ChartElements, Spreadsheet } from "./charts.types";
|
||||||
|
|
||||||
|
export const renderBarChart = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
colorSeed?: number,
|
||||||
|
): ChartElements => {
|
||||||
|
const series = spreadsheet.series;
|
||||||
|
const layout = getCartesianChartLayout("bar", series.length);
|
||||||
|
const max = Math.max(
|
||||||
|
1,
|
||||||
|
...series.flatMap((seriesData) =>
|
||||||
|
seriesData.values.map((value) => Math.max(0, value)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const colorOffset = getColorOffset(colorSeed);
|
||||||
|
const backgroundColor = getBackgroundColor(colorOffset);
|
||||||
|
const seriesColors = getSeriesColors(series.length, colorOffset);
|
||||||
|
const interBarGap =
|
||||||
|
series.length > 1
|
||||||
|
? Math.max(1, Math.floor(layout.gap / (series.length + 1)))
|
||||||
|
: 0;
|
||||||
|
const barWidth =
|
||||||
|
series.length > 1
|
||||||
|
? Math.max(
|
||||||
|
2,
|
||||||
|
(layout.slotWidth - interBarGap * (series.length - 1)) /
|
||||||
|
series.length,
|
||||||
|
)
|
||||||
|
: layout.slotWidth;
|
||||||
|
const clusterWidth =
|
||||||
|
series.length * barWidth + interBarGap * (series.length - 1);
|
||||||
|
const clusterOffset = (layout.slotWidth - clusterWidth) / 2;
|
||||||
|
|
||||||
|
const bars = series[0].values.flatMap((_, categoryIndex) =>
|
||||||
|
series.map((seriesData, seriesIndex) => {
|
||||||
|
const value = Math.max(0, seriesData.values[categoryIndex] ?? 0);
|
||||||
|
const barHeight = (value / max) * layout.chartHeight;
|
||||||
|
const barColor =
|
||||||
|
series.length > 1 ? seriesColors[seriesIndex] : backgroundColor;
|
||||||
|
return newElement({
|
||||||
|
backgroundColor: barColor,
|
||||||
|
...commonProps,
|
||||||
|
type: "rectangle",
|
||||||
|
fillStyle: series.length > 1 ? "solid" : commonProps.fillStyle,
|
||||||
|
strokeColor: series.length > 1 ? barColor : commonProps.strokeColor,
|
||||||
|
x:
|
||||||
|
x +
|
||||||
|
categoryIndex * (layout.slotWidth + layout.gap) +
|
||||||
|
layout.gap +
|
||||||
|
clusterOffset +
|
||||||
|
seriesIndex * (barWidth + interBarGap),
|
||||||
|
y: y - barHeight - layout.gap,
|
||||||
|
width: barWidth,
|
||||||
|
height: barHeight,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseElements = chartBaseElements(
|
||||||
|
spreadsheet,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
backgroundColor,
|
||||||
|
layout,
|
||||||
|
max,
|
||||||
|
isDevEnv(),
|
||||||
|
);
|
||||||
|
const xLabels = chartXLabels(spreadsheet, x, y, backgroundColor, layout);
|
||||||
|
const xLabelsBottomY = Math.max(
|
||||||
|
y + layout.gap / 2,
|
||||||
|
...xLabels.map((label) => getRotatedTextElementBottom(label)),
|
||||||
|
);
|
||||||
|
const { chartWidth } = getChartDimensions(spreadsheet, layout);
|
||||||
|
const seriesLegend = createSeriesLegend(
|
||||||
|
series,
|
||||||
|
seriesColors,
|
||||||
|
x + chartWidth / 2,
|
||||||
|
xLabelsBottomY,
|
||||||
|
y + layout.gap * 5,
|
||||||
|
backgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...baseElements, ...bars, ...seriesLegend];
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
COLOR_PALETTE,
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
|
export const CARTESIAN_BASE_SLOT_WIDTH = 44;
|
||||||
|
export const CARTESIAN_BAR_SLOT_EXTRA_PER_SERIES = 22;
|
||||||
|
export const CARTESIAN_BAR_SLOT_EXTRA_MAX = 66;
|
||||||
|
export const CARTESIAN_LINE_SLOT_WIDTH = 48;
|
||||||
|
export const CARTESIAN_GAP = 14;
|
||||||
|
export const CARTESIAN_BAR_HEIGHT = 304;
|
||||||
|
export const CARTESIAN_LINE_HEIGHT = 320;
|
||||||
|
export const CARTESIAN_LABEL_ROTATION = 5.87 as Radians;
|
||||||
|
export const CARTESIAN_LABEL_MIN_WIDTH = 28;
|
||||||
|
export const CARTESIAN_LABEL_SLOT_PADDING = 4;
|
||||||
|
export const CARTESIAN_LABEL_AXIS_CLEARANCE = 2;
|
||||||
|
export const CARTESIAN_LABEL_MAX_WIDTH_BUFFER = 10;
|
||||||
|
export const CARTESIAN_LABEL_ROTATED_WIDTH_BUFFER = 10;
|
||||||
|
export const CARTESIAN_LABEL_OVERFLOW_PREFERENCE_BUFFER = 8;
|
||||||
|
|
||||||
|
export const BAR_GAP = 12;
|
||||||
|
export const BAR_HEIGHT = 256;
|
||||||
|
export const GRID_OPACITY = 10;
|
||||||
|
|
||||||
|
export const RADAR_GRID_LEVELS = 4;
|
||||||
|
export const RADAR_LABEL_OFFSET = BAR_GAP * 2;
|
||||||
|
export const RADAR_PADDING = BAR_GAP * 2;
|
||||||
|
export const RADAR_SINGLE_SERIES_LOG_SCALE_THRESHOLD = 100;
|
||||||
|
export const RADAR_AXIS_LABEL_MAX_WIDTH = 140;
|
||||||
|
export const RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD = 0.35;
|
||||||
|
export const RADAR_AXIS_LABEL_CLEARANCE = BAR_GAP / 2;
|
||||||
|
export const RADAR_LEGEND_SWATCH_SIZE = 20;
|
||||||
|
export const RADAR_LEGEND_ITEM_GAP = BAR_GAP * 2;
|
||||||
|
export const RADAR_LEGEND_TEXT_GAP = BAR_GAP;
|
||||||
|
|
||||||
|
// Put all common chart element properties here so properties dialog
|
||||||
|
// shows stable values when selecting chart groups.
|
||||||
|
export const commonProps = {
|
||||||
|
fillStyle: "hachure",
|
||||||
|
fontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
opacity: 100,
|
||||||
|
roughness: 1,
|
||||||
|
strokeColor: COLOR_PALETTE.black,
|
||||||
|
roundness: null,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
strokeWidth: 1,
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
locked: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CartesianChartType = "bar" | "line";
|
||||||
|
|
||||||
|
export type CartesianChartLayout = {
|
||||||
|
slotWidth: number;
|
||||||
|
gap: number;
|
||||||
|
chartHeight: number;
|
||||||
|
xLabelMaxWidth: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1,865 @@
|
|||||||
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import {
|
||||||
|
COLOR_PALETTE,
|
||||||
|
DEFAULT_CHART_COLOR_INDEX,
|
||||||
|
FONT_FAMILY,
|
||||||
|
FONT_SIZES,
|
||||||
|
ROUNDNESS,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
getAllColorsSpecificShade,
|
||||||
|
getFontString,
|
||||||
|
getLineHeight,
|
||||||
|
ROUGHNESS,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getApproxMinLineWidth,
|
||||||
|
measureText,
|
||||||
|
newElement,
|
||||||
|
newLinearElement,
|
||||||
|
newTextElement,
|
||||||
|
wrapText,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ChartType,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BAR_GAP,
|
||||||
|
CARTESIAN_BAR_HEIGHT,
|
||||||
|
CARTESIAN_BASE_SLOT_WIDTH,
|
||||||
|
CARTESIAN_BAR_SLOT_EXTRA_MAX,
|
||||||
|
CARTESIAN_BAR_SLOT_EXTRA_PER_SERIES,
|
||||||
|
CARTESIAN_GAP,
|
||||||
|
CARTESIAN_LABEL_AXIS_CLEARANCE,
|
||||||
|
CARTESIAN_LABEL_MAX_WIDTH_BUFFER,
|
||||||
|
CARTESIAN_LABEL_MIN_WIDTH,
|
||||||
|
CARTESIAN_LABEL_OVERFLOW_PREFERENCE_BUFFER,
|
||||||
|
CARTESIAN_LABEL_ROTATED_WIDTH_BUFFER,
|
||||||
|
CARTESIAN_LABEL_ROTATION,
|
||||||
|
CARTESIAN_LABEL_SLOT_PADDING,
|
||||||
|
CARTESIAN_LINE_HEIGHT,
|
||||||
|
CARTESIAN_LINE_SLOT_WIDTH,
|
||||||
|
GRID_OPACITY,
|
||||||
|
RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD,
|
||||||
|
RADAR_AXIS_LABEL_CLEARANCE,
|
||||||
|
RADAR_AXIS_LABEL_MAX_WIDTH,
|
||||||
|
RADAR_LABEL_OFFSET,
|
||||||
|
RADAR_LEGEND_ITEM_GAP,
|
||||||
|
RADAR_LEGEND_SWATCH_SIZE,
|
||||||
|
RADAR_LEGEND_TEXT_GAP,
|
||||||
|
RADAR_PADDING,
|
||||||
|
RADAR_SINGLE_SERIES_LOG_SCALE_THRESHOLD,
|
||||||
|
BAR_HEIGHT,
|
||||||
|
commonProps,
|
||||||
|
type CartesianChartLayout,
|
||||||
|
type CartesianChartType,
|
||||||
|
} from "./charts.constants";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ChartElements,
|
||||||
|
Spreadsheet,
|
||||||
|
SpreadsheetSeries,
|
||||||
|
} from "./charts.types";
|
||||||
|
|
||||||
|
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
|
||||||
|
|
||||||
|
const getSpreadsheetDimensionCount = (spreadsheet: Spreadsheet) =>
|
||||||
|
spreadsheet.labels?.length ?? spreadsheet.series[0]?.values.length ?? 0;
|
||||||
|
|
||||||
|
export const isSpreadsheetValidForChartType = (
|
||||||
|
spreadsheet: Spreadsheet | null,
|
||||||
|
chartType: ChartType,
|
||||||
|
) => {
|
||||||
|
if (!spreadsheet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimensionCount = getSpreadsheetDimensionCount(spreadsheet);
|
||||||
|
if (dimensionCount < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chartType === "radar") {
|
||||||
|
return dimensionCount >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSeriesAwareSlotWidth = (
|
||||||
|
baseSlotWidth: number,
|
||||||
|
seriesCount: number,
|
||||||
|
) => {
|
||||||
|
const extraSlotWidth =
|
||||||
|
seriesCount <= 1
|
||||||
|
? 0
|
||||||
|
: Math.min(
|
||||||
|
CARTESIAN_BAR_SLOT_EXTRA_MAX,
|
||||||
|
(seriesCount - 1) * CARTESIAN_BAR_SLOT_EXTRA_PER_SERIES,
|
||||||
|
);
|
||||||
|
return baseSlotWidth + extraSlotWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCartesianChartLayout = (
|
||||||
|
chartType: CartesianChartType,
|
||||||
|
seriesCount: number,
|
||||||
|
): CartesianChartLayout => {
|
||||||
|
if (chartType === "line") {
|
||||||
|
const slotWidth = getSeriesAwareSlotWidth(
|
||||||
|
CARTESIAN_LINE_SLOT_WIDTH,
|
||||||
|
seriesCount,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
slotWidth,
|
||||||
|
gap: CARTESIAN_GAP,
|
||||||
|
chartHeight: CARTESIAN_LINE_HEIGHT,
|
||||||
|
xLabelMaxWidth:
|
||||||
|
slotWidth + CARTESIAN_GAP * 3 + CARTESIAN_LABEL_MAX_WIDTH_BUFFER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotWidth = getSeriesAwareSlotWidth(
|
||||||
|
CARTESIAN_BASE_SLOT_WIDTH,
|
||||||
|
seriesCount,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
slotWidth,
|
||||||
|
gap: CARTESIAN_GAP,
|
||||||
|
chartHeight: CARTESIAN_BAR_HEIGHT,
|
||||||
|
xLabelMaxWidth:
|
||||||
|
slotWidth + CARTESIAN_GAP * 3 + CARTESIAN_LABEL_MAX_WIDTH_BUFFER,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getChartDimensions = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
layout: CartesianChartLayout,
|
||||||
|
) => {
|
||||||
|
const chartWidth =
|
||||||
|
(layout.slotWidth + layout.gap) * spreadsheet.series[0].values.length +
|
||||||
|
layout.gap;
|
||||||
|
const chartHeight = layout.chartHeight + layout.gap * 2;
|
||||||
|
return { chartWidth, chartHeight };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRadarDimensions = () => {
|
||||||
|
const chartWidth = BAR_HEIGHT + RADAR_PADDING * 2;
|
||||||
|
const chartHeight = BAR_HEIGHT + RADAR_PADDING * 2;
|
||||||
|
return { chartWidth, chartHeight };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCircularDistance = (
|
||||||
|
firstIndex: number,
|
||||||
|
secondIndex: number,
|
||||||
|
paletteSize: number,
|
||||||
|
) => {
|
||||||
|
const absoluteDistance = Math.abs(firstIndex - secondIndex);
|
||||||
|
return Math.min(absoluteDistance, paletteSize - absoluteDistance);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSeriesColors = (
|
||||||
|
seriesCount: number,
|
||||||
|
colorOffset: number,
|
||||||
|
): readonly string[] => {
|
||||||
|
if (seriesCount <= 0 || bgColors.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const paletteSize = bgColors.length;
|
||||||
|
const startIndex = ((colorOffset % paletteSize) + paletteSize) % paletteSize;
|
||||||
|
const selectedIndices = [startIndex];
|
||||||
|
const maxUniqueColors = Math.min(seriesCount, paletteSize);
|
||||||
|
const availableIndices = new Set(
|
||||||
|
Array.from({ length: paletteSize }, (_, index) => index).filter(
|
||||||
|
(index) => index !== startIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
while (selectedIndices.length < maxUniqueColors) {
|
||||||
|
let bestIndex = -1;
|
||||||
|
let bestMinDistance = -1;
|
||||||
|
let bestAverageDistance = -1;
|
||||||
|
|
||||||
|
for (const candidateIndex of availableIndices) {
|
||||||
|
const distances = selectedIndices.map((selectedIndex) =>
|
||||||
|
getCircularDistance(candidateIndex, selectedIndex, paletteSize),
|
||||||
|
);
|
||||||
|
const minDistance = Math.min(...distances);
|
||||||
|
const averageDistance =
|
||||||
|
distances.reduce((total, distance) => total + distance, 0) /
|
||||||
|
distances.length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
minDistance > bestMinDistance ||
|
||||||
|
(minDistance === bestMinDistance &&
|
||||||
|
averageDistance > bestAverageDistance)
|
||||||
|
) {
|
||||||
|
bestIndex = candidateIndex;
|
||||||
|
bestMinDistance = minDistance;
|
||||||
|
bestAverageDistance = averageDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndices.push(bestIndex);
|
||||||
|
availableIndices.delete(bestIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
{ length: seriesCount },
|
||||||
|
(_, index) => bgColors[selectedIndices[index % selectedIndices.length]],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getColorOffset = (colorSeed?: number) => {
|
||||||
|
if (bgColors.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof colorSeed !== "number" || !Number.isFinite(colorSeed)) {
|
||||||
|
return Math.floor(Math.random() * bgColors.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seedText = colorSeed.toString();
|
||||||
|
let hash = 0;
|
||||||
|
for (let index = 0; index < seedText.length; index++) {
|
||||||
|
hash = (hash * 31 + seedText.charCodeAt(index)) | 0;
|
||||||
|
}
|
||||||
|
return Math.abs(hash) % bgColors.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBackgroundColor = (colorOffset: number) =>
|
||||||
|
bgColors[colorOffset];
|
||||||
|
|
||||||
|
export const getRadarValueScale = (
|
||||||
|
series: SpreadsheetSeries[],
|
||||||
|
_labelsLength: number,
|
||||||
|
) => {
|
||||||
|
const allValues = series.flatMap((s) =>
|
||||||
|
s.values.map((value) => Math.max(0, value)),
|
||||||
|
);
|
||||||
|
const positiveValues = allValues.filter((value) => value > 0);
|
||||||
|
const max = Math.max(1, ...allValues);
|
||||||
|
const minPositive =
|
||||||
|
positiveValues.length > 0 ? Math.min(...positiveValues) : 1;
|
||||||
|
const useLogScale =
|
||||||
|
series.length === 1 &&
|
||||||
|
minPositive > 0 &&
|
||||||
|
max / minPositive >= RADAR_SINGLE_SERIES_LOG_SCALE_THRESHOLD;
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderSteps: false,
|
||||||
|
normalize: (value: number, _axisIndex: number) => {
|
||||||
|
const safeValue = Math.max(0, value);
|
||||||
|
return useLogScale
|
||||||
|
? Math.log10(safeValue + 1) / Math.log10(max + 1)
|
||||||
|
: safeValue / max;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldWrapRadarText = (text: string) => /\s/.test(text.trim());
|
||||||
|
|
||||||
|
export const getRadarDisplayText = (
|
||||||
|
text: string,
|
||||||
|
fontString: ReturnType<typeof getFontString>,
|
||||||
|
maxWidth: number,
|
||||||
|
) => {
|
||||||
|
return shouldWrapRadarText(text)
|
||||||
|
? wrapText(text, fontString, maxWidth)
|
||||||
|
: text;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRadarAxisLabels = (
|
||||||
|
labels: readonly string[],
|
||||||
|
angles: readonly number[],
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
radius: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
): {
|
||||||
|
axisLabels: ChartElements;
|
||||||
|
axisLabelTopY: number;
|
||||||
|
axisLabelBottomY: number;
|
||||||
|
} => {
|
||||||
|
const fontFamily = FONT_FAMILY.Excalifont;
|
||||||
|
const fontSize = FONT_SIZES.sm;
|
||||||
|
const lineHeight = getLineHeight(fontFamily);
|
||||||
|
const fontString = getFontString({ fontFamily, fontSize });
|
||||||
|
const baseLabelWidth = Math.min(
|
||||||
|
RADAR_AXIS_LABEL_MAX_WIDTH,
|
||||||
|
radius * (labels.length > 8 ? 0.56 : 0.72),
|
||||||
|
);
|
||||||
|
const minLabelWidth = getApproxMinLineWidth(fontString, lineHeight);
|
||||||
|
|
||||||
|
const axisLabels = labels.map((label, index) => {
|
||||||
|
const angle = angles[index];
|
||||||
|
const longestWordWidth = Math.max(
|
||||||
|
0,
|
||||||
|
...label
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((word) => measureText(word, fontString, lineHeight).width),
|
||||||
|
);
|
||||||
|
const maxLabelWidth = Math.max(
|
||||||
|
minLabelWidth,
|
||||||
|
baseLabelWidth,
|
||||||
|
longestWordWidth,
|
||||||
|
);
|
||||||
|
const displayLabel = getRadarDisplayText(label, fontString, maxLabelWidth);
|
||||||
|
const metrics = measureText(displayLabel, fontString, lineHeight);
|
||||||
|
const cos = Math.cos(angle);
|
||||||
|
const sin = Math.sin(angle);
|
||||||
|
|
||||||
|
const textAlign: "left" | "center" | "right" =
|
||||||
|
cos > RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
|
||||||
|
? "left"
|
||||||
|
: cos < -RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
|
||||||
|
? "right"
|
||||||
|
: "center";
|
||||||
|
|
||||||
|
// Keep labels outside the radar ring by projecting text extents
|
||||||
|
// onto the axis direction.
|
||||||
|
const centerAlignedXExtent = textAlign === "center" ? metrics.width / 2 : 0;
|
||||||
|
const projectedExtent =
|
||||||
|
Math.abs(cos) * centerAlignedXExtent +
|
||||||
|
Math.abs(sin) * (metrics.height / 2);
|
||||||
|
const radialOffset =
|
||||||
|
RADAR_LABEL_OFFSET + projectedExtent + RADAR_AXIS_LABEL_CLEARANCE;
|
||||||
|
const anchorX = centerX + cos * (radius + radialOffset);
|
||||||
|
const anchorY = centerY + sin * (radius + radialOffset);
|
||||||
|
|
||||||
|
const yNudge =
|
||||||
|
sin > RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
|
||||||
|
? BAR_GAP / 3
|
||||||
|
: sin < -RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
|
||||||
|
? -BAR_GAP / 3
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return newTextElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
text: displayLabel,
|
||||||
|
originalText: label,
|
||||||
|
x: anchorX,
|
||||||
|
y: anchorY + yNudge,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
textAlign,
|
||||||
|
verticalAlign: "middle",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const axisLabelTopY = Math.min(...axisLabels.map((axisLabel) => axisLabel.y));
|
||||||
|
const axisLabelBottomY = Math.max(
|
||||||
|
...axisLabels.map((axisLabel) => axisLabel.y + axisLabel.height),
|
||||||
|
);
|
||||||
|
return { axisLabels, axisLabelTopY, axisLabelBottomY };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSeriesLegend = (
|
||||||
|
series: SpreadsheetSeries[],
|
||||||
|
seriesColors: readonly string[],
|
||||||
|
centerX: number,
|
||||||
|
minLegendTopY: number,
|
||||||
|
fallbackLegendY: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
): ChartElements => {
|
||||||
|
if (series.length <= 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontFamily = FONT_FAMILY["Lilita One"];
|
||||||
|
const fontSize = FONT_SIZES.lg;
|
||||||
|
const lineHeight = getLineHeight(fontFamily);
|
||||||
|
const fontString = getFontString({ fontFamily, fontSize });
|
||||||
|
const legendItems = series.map((seriesItem, index) => {
|
||||||
|
const label = seriesItem.title?.trim() || `Series ${index + 1}`;
|
||||||
|
const displayLabel = getRadarDisplayText(label, fontString, BAR_HEIGHT);
|
||||||
|
const metrics = measureText(displayLabel, fontString, lineHeight);
|
||||||
|
const itemWidth =
|
||||||
|
RADAR_LEGEND_SWATCH_SIZE + RADAR_LEGEND_TEXT_GAP + metrics.width;
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
displayLabel,
|
||||||
|
color: seriesColors[index],
|
||||||
|
width: itemWidth,
|
||||||
|
height: metrics.height,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const maxLegendHalfHeight = Math.max(
|
||||||
|
RADAR_LEGEND_SWATCH_SIZE / 2,
|
||||||
|
...legendItems.map((item) => item.height / 2),
|
||||||
|
);
|
||||||
|
const legendY = Math.max(
|
||||||
|
fallbackLegendY,
|
||||||
|
minLegendTopY + maxLegendHalfHeight + RADAR_LABEL_OFFSET,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pillPaddingX = RADAR_LEGEND_ITEM_GAP;
|
||||||
|
const pillPaddingY = RADAR_LEGEND_SWATCH_SIZE * 0.6;
|
||||||
|
const totalLegendWidth =
|
||||||
|
legendItems.reduce((total, item) => total + item.width, 0) +
|
||||||
|
RADAR_LEGEND_ITEM_GAP * Math.max(0, legendItems.length - 1);
|
||||||
|
const pillWidth = totalLegendWidth + pillPaddingX * 2;
|
||||||
|
const pillHeight = maxLegendHalfHeight * 2 + pillPaddingY * 2;
|
||||||
|
|
||||||
|
const legendElements: NonDeletedExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
// rounded pill background
|
||||||
|
legendElements.push(
|
||||||
|
newElement({
|
||||||
|
...commonProps,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
type: "rectangle",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeColor: COLOR_PALETTE.black,
|
||||||
|
x: centerX - pillWidth / 2,
|
||||||
|
y: legendY - pillHeight / 2,
|
||||||
|
width: pillWidth,
|
||||||
|
height: pillHeight,
|
||||||
|
roughness: ROUGHNESS.architect,
|
||||||
|
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursorX = centerX - totalLegendWidth / 2;
|
||||||
|
|
||||||
|
legendItems.forEach((item) => {
|
||||||
|
// solid filled swatch
|
||||||
|
legendElements.push(
|
||||||
|
newElement({
|
||||||
|
...commonProps,
|
||||||
|
backgroundColor: item.color,
|
||||||
|
type: "rectangle",
|
||||||
|
x: cursorX,
|
||||||
|
y: legendY - RADAR_LEGEND_SWATCH_SIZE / 2,
|
||||||
|
width: RADAR_LEGEND_SWATCH_SIZE,
|
||||||
|
height: RADAR_LEGEND_SWATCH_SIZE,
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeColor: item.color,
|
||||||
|
roughness: ROUGHNESS.architect,
|
||||||
|
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// label in default (black) color
|
||||||
|
legendElements.push(
|
||||||
|
newTextElement({
|
||||||
|
...commonProps,
|
||||||
|
text: item.displayLabel,
|
||||||
|
originalText: item.label,
|
||||||
|
autoResize: false,
|
||||||
|
x: cursorX + RADAR_LEGEND_SWATCH_SIZE + RADAR_LEGEND_TEXT_GAP,
|
||||||
|
y: legendY,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
textAlign: "left",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
cursorX += item.width + RADAR_LEGEND_ITEM_GAP;
|
||||||
|
});
|
||||||
|
|
||||||
|
return legendElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ellipsifyTextToWidth = (
|
||||||
|
text: string,
|
||||||
|
maxWidth: number,
|
||||||
|
fontString: ReturnType<typeof getFontString>,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
if (measureText(text, fontString, lineHeight).width <= maxWidth) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = text.length;
|
||||||
|
while (end > 1) {
|
||||||
|
const candidate = `${text.slice(0, end)}...`;
|
||||||
|
if (measureText(candidate, fontString, lineHeight).width <= maxWidth) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text[0] ? `${text[0]}...` : text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapOrEllipsifyTextToWidth = (
|
||||||
|
text: string,
|
||||||
|
maxWidth: number,
|
||||||
|
fontString: ReturnType<typeof getFontString>,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
if (measureText(text, fontString, lineHeight).width <= maxWidth) {
|
||||||
|
return { wrapped: false, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
const words = text.trim().split(/\s+/).filter(Boolean);
|
||||||
|
if (words.length > 1) {
|
||||||
|
const hasLongWord = words.some((word) => {
|
||||||
|
return measureText(word, fontString, lineHeight).width > maxWidth;
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!hasLongWord &&
|
||||||
|
maxWidth >= getApproxMinLineWidth(fontString, lineHeight)
|
||||||
|
) {
|
||||||
|
return { wrapped: true, text: wrapText(text, fontString, maxWidth) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapped: false,
|
||||||
|
text: ellipsifyTextToWidth(text, maxWidth, fontString, lineHeight),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRotatedBoundingBox = (
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
angle: number,
|
||||||
|
) => {
|
||||||
|
const cos = Math.abs(Math.cos(angle));
|
||||||
|
const sin = Math.abs(Math.sin(angle));
|
||||||
|
return {
|
||||||
|
width: width * cos + height * sin,
|
||||||
|
height: width * sin + height * cos,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartesianAxisLabelSpec = {
|
||||||
|
originalText: string;
|
||||||
|
text: string;
|
||||||
|
wrapped: boolean;
|
||||||
|
metrics: ReturnType<typeof measureText>;
|
||||||
|
rotatedWidth: number;
|
||||||
|
rotatedHeight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEllipsifiedLabel = (text: string) => text.includes("...");
|
||||||
|
|
||||||
|
const getCartesianAxisLabelSpec = (
|
||||||
|
label: string,
|
||||||
|
maxLabelWidth: number,
|
||||||
|
maxRotatedWidth: number,
|
||||||
|
fontString: ReturnType<typeof getFontString>,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
): CartesianAxisLabelSpec => {
|
||||||
|
const minWidth = Math.max(
|
||||||
|
CARTESIAN_LABEL_MIN_WIDTH,
|
||||||
|
Math.ceil(getApproxMinLineWidth(fontString, lineHeight)),
|
||||||
|
);
|
||||||
|
const maxWidth = Math.max(minWidth, Math.floor(maxLabelWidth));
|
||||||
|
const candidateWidths: number[] = [];
|
||||||
|
for (let width = maxWidth; width >= minWidth; width -= 4) {
|
||||||
|
candidateWidths.push(width);
|
||||||
|
}
|
||||||
|
if (candidateWidths[candidateWidths.length - 1] !== minWidth) {
|
||||||
|
candidateWidths.push(minWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRank = (spec: CartesianAxisLabelSpec) => {
|
||||||
|
const ellipsified = isEllipsifiedLabel(spec.text);
|
||||||
|
const visibleChars = spec.text
|
||||||
|
.replace(/\.\.\./g, "")
|
||||||
|
.replace(/\n/g, "").length;
|
||||||
|
const lineCount = spec.text.split("\n").length;
|
||||||
|
return {
|
||||||
|
ellipsified,
|
||||||
|
visibleChars,
|
||||||
|
lineCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldPrefer = (
|
||||||
|
candidate: CartesianAxisLabelSpec,
|
||||||
|
current: CartesianAxisLabelSpec,
|
||||||
|
) => {
|
||||||
|
const candidateRank = getRank(candidate);
|
||||||
|
const currentRank = getRank(current);
|
||||||
|
if (candidateRank.ellipsified !== currentRank.ellipsified) {
|
||||||
|
return !candidateRank.ellipsified;
|
||||||
|
}
|
||||||
|
if (candidateRank.visibleChars !== currentRank.visibleChars) {
|
||||||
|
return candidateRank.visibleChars > currentRank.visibleChars;
|
||||||
|
}
|
||||||
|
if (candidateRank.lineCount !== currentRank.lineCount) {
|
||||||
|
return candidateRank.lineCount < currentRank.lineCount;
|
||||||
|
}
|
||||||
|
return candidate.rotatedHeight < current.rotatedHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
let bestFit: CartesianAxisLabelSpec | null = null;
|
||||||
|
let bestOverflowAny: {
|
||||||
|
overflow: number;
|
||||||
|
spec: CartesianAxisLabelSpec;
|
||||||
|
} | null = null;
|
||||||
|
let bestOverflowNonEllipsified: {
|
||||||
|
overflow: number;
|
||||||
|
spec: CartesianAxisLabelSpec;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
for (const width of candidateWidths) {
|
||||||
|
const { wrapped, text } = wrapOrEllipsifyTextToWidth(
|
||||||
|
label,
|
||||||
|
width,
|
||||||
|
fontString,
|
||||||
|
lineHeight,
|
||||||
|
);
|
||||||
|
const metrics = measureText(text, fontString, lineHeight);
|
||||||
|
const rotated = getRotatedBoundingBox(
|
||||||
|
metrics.width,
|
||||||
|
metrics.height,
|
||||||
|
CARTESIAN_LABEL_ROTATION,
|
||||||
|
);
|
||||||
|
const spec = {
|
||||||
|
originalText: label,
|
||||||
|
text,
|
||||||
|
metrics,
|
||||||
|
rotatedWidth: rotated.width,
|
||||||
|
rotatedHeight: rotated.height,
|
||||||
|
wrapped,
|
||||||
|
};
|
||||||
|
const overflow = rotated.width - maxRotatedWidth;
|
||||||
|
if (overflow <= 0) {
|
||||||
|
if (!bestFit || shouldPrefer(spec, bestFit)) {
|
||||||
|
bestFit = spec;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!bestOverflowAny ||
|
||||||
|
overflow < bestOverflowAny.overflow ||
|
||||||
|
(overflow === bestOverflowAny.overflow &&
|
||||||
|
shouldPrefer(spec, bestOverflowAny.spec))
|
||||||
|
) {
|
||||||
|
bestOverflowAny = { overflow, spec };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!isEllipsifiedLabel(spec.text) &&
|
||||||
|
(!bestOverflowNonEllipsified ||
|
||||||
|
overflow < bestOverflowNonEllipsified.overflow ||
|
||||||
|
(overflow === bestOverflowNonEllipsified.overflow &&
|
||||||
|
shouldPrefer(spec, bestOverflowNonEllipsified.spec)))
|
||||||
|
) {
|
||||||
|
bestOverflowNonEllipsified = { overflow, spec };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestFit) {
|
||||||
|
return bestFit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
bestOverflowNonEllipsified &&
|
||||||
|
bestOverflowAny &&
|
||||||
|
bestOverflowNonEllipsified.overflow <=
|
||||||
|
bestOverflowAny.overflow + CARTESIAN_LABEL_OVERFLOW_PREFERENCE_BUFFER
|
||||||
|
) {
|
||||||
|
return bestOverflowNonEllipsified.spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestOverflowAny!.spec;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRotatedTextElementBottom = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
) => {
|
||||||
|
if (element.type !== "text") {
|
||||||
|
return element.y + element.height;
|
||||||
|
}
|
||||||
|
const rotated = getRotatedBoundingBox(
|
||||||
|
element.width,
|
||||||
|
element.height,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
return element.y + element.height / 2 + rotated.height / 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chartXLabels = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
layout: CartesianChartLayout,
|
||||||
|
): ChartElements => {
|
||||||
|
const fontFamily = commonProps.fontFamily;
|
||||||
|
const fontSize = FONT_SIZES.sm;
|
||||||
|
const lineHeight = getLineHeight(fontFamily);
|
||||||
|
const fontString = getFontString({ fontFamily, fontSize });
|
||||||
|
const maxRotatedWidth = Math.max(
|
||||||
|
1,
|
||||||
|
layout.slotWidth +
|
||||||
|
layout.gap -
|
||||||
|
CARTESIAN_LABEL_SLOT_PADDING * 2 +
|
||||||
|
CARTESIAN_LABEL_ROTATED_WIDTH_BUFFER,
|
||||||
|
);
|
||||||
|
const axisY = y;
|
||||||
|
|
||||||
|
return (
|
||||||
|
spreadsheet.labels?.map((label, index) => {
|
||||||
|
const labelSpec = getCartesianAxisLabelSpec(
|
||||||
|
label,
|
||||||
|
layout.xLabelMaxWidth,
|
||||||
|
maxRotatedWidth,
|
||||||
|
fontString,
|
||||||
|
lineHeight,
|
||||||
|
);
|
||||||
|
const centerX =
|
||||||
|
x +
|
||||||
|
index * (layout.slotWidth + layout.gap) +
|
||||||
|
layout.gap +
|
||||||
|
layout.slotWidth / 2;
|
||||||
|
const labelY =
|
||||||
|
axisY +
|
||||||
|
CARTESIAN_LABEL_AXIS_CLEARANCE +
|
||||||
|
(labelSpec.rotatedHeight - labelSpec.metrics.height) / 2;
|
||||||
|
|
||||||
|
return newTextElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
text: labelSpec.text,
|
||||||
|
originalText: labelSpec.wrapped ? label : labelSpec.text,
|
||||||
|
autoResize: !labelSpec.wrapped,
|
||||||
|
x: centerX,
|
||||||
|
y: labelY,
|
||||||
|
angle: CARTESIAN_LABEL_ROTATION,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
textAlign: "center",
|
||||||
|
verticalAlign: "top",
|
||||||
|
});
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartYLabels = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
layout: CartesianChartLayout,
|
||||||
|
maxValue = Math.max(...spreadsheet.series[0].values),
|
||||||
|
): ChartElements => {
|
||||||
|
const minYLabel = newTextElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
x: x - layout.gap,
|
||||||
|
y: y - layout.gap,
|
||||||
|
text: "0",
|
||||||
|
textAlign: "right",
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxYLabel = newTextElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
x: x - layout.gap,
|
||||||
|
y: y - layout.chartHeight - minYLabel.height / 2,
|
||||||
|
text: maxValue.toLocaleString(),
|
||||||
|
textAlign: "right",
|
||||||
|
});
|
||||||
|
|
||||||
|
return [minYLabel, maxYLabel];
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartLines = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
layout: CartesianChartLayout,
|
||||||
|
): ChartElements => {
|
||||||
|
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet, layout);
|
||||||
|
const xLine = newLinearElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: chartWidth,
|
||||||
|
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const yLine = newLinearElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
height: chartHeight,
|
||||||
|
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxLine = newLinearElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x,
|
||||||
|
y: y - layout.chartHeight - layout.gap,
|
||||||
|
strokeStyle: "dotted",
|
||||||
|
width: chartWidth,
|
||||||
|
opacity: GRID_OPACITY,
|
||||||
|
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return [xLine, yLine, maxLine];
|
||||||
|
};
|
||||||
|
|
||||||
|
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
|
||||||
|
export const chartBaseElements = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
layout: CartesianChartLayout,
|
||||||
|
maxValue = Math.max(...spreadsheet.series[0].values),
|
||||||
|
debug?: boolean,
|
||||||
|
): ChartElements => {
|
||||||
|
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet, layout);
|
||||||
|
|
||||||
|
const title = spreadsheet.title
|
||||||
|
? newTextElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
text: spreadsheet.title,
|
||||||
|
x: x + chartWidth / 2,
|
||||||
|
y: y - layout.chartHeight - layout.gap * 2 - DEFAULT_FONT_SIZE,
|
||||||
|
roundness: null,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: FONT_SIZES.xl,
|
||||||
|
fontFamily: FONT_FAMILY["Lilita One"],
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const debugRect = debug
|
||||||
|
? newElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
type: "rectangle",
|
||||||
|
x,
|
||||||
|
y: y - chartHeight,
|
||||||
|
width: chartWidth,
|
||||||
|
height: chartHeight,
|
||||||
|
strokeColor: COLOR_PALETTE.black,
|
||||||
|
fillStyle: "solid",
|
||||||
|
opacity: 6,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(debugRect ? [debugRect] : []),
|
||||||
|
...(title ? [title] : []),
|
||||||
|
...chartXLabels(spreadsheet, x, y, backgroundColor, layout),
|
||||||
|
...chartYLabels(spreadsheet, x, y, backgroundColor, layout, maxValue),
|
||||||
|
...chartLines(spreadsheet, x, y, backgroundColor, layout),
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { isDevEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { newElement, newLinearElement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { GRID_OPACITY, commonProps } from "./charts.constants";
|
||||||
|
import {
|
||||||
|
chartBaseElements,
|
||||||
|
chartXLabels,
|
||||||
|
createSeriesLegend,
|
||||||
|
getBackgroundColor,
|
||||||
|
getCartesianChartLayout,
|
||||||
|
getChartDimensions,
|
||||||
|
getColorOffset,
|
||||||
|
getRotatedTextElementBottom,
|
||||||
|
getSeriesColors,
|
||||||
|
} from "./charts.helpers";
|
||||||
|
|
||||||
|
import type { ChartElements, Spreadsheet } from "./charts.types";
|
||||||
|
|
||||||
|
export const renderLineChart = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
colorSeed?: number,
|
||||||
|
): ChartElements => {
|
||||||
|
const series = spreadsheet.series;
|
||||||
|
const layout = getCartesianChartLayout("line", series.length);
|
||||||
|
const max = Math.max(1, ...series.flatMap((seriesData) => seriesData.values));
|
||||||
|
const colorOffset = getColorOffset(colorSeed);
|
||||||
|
const backgroundColor = getBackgroundColor(colorOffset);
|
||||||
|
const seriesColors = getSeriesColors(series.length, colorOffset);
|
||||||
|
|
||||||
|
const lines = series.map((seriesData, seriesIndex) => {
|
||||||
|
const points = seriesData.values.map((value, valueIndex) =>
|
||||||
|
pointFrom<LocalPoint>(
|
||||||
|
valueIndex * (layout.slotWidth + layout.gap),
|
||||||
|
-(value / max) * layout.chartHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxX = Math.max(...points.map((point) => point[0]));
|
||||||
|
const maxY = Math.max(...points.map((point) => point[1]));
|
||||||
|
const minX = Math.min(...points.map((point) => point[0]));
|
||||||
|
const minY = Math.min(...points.map((point) => point[1]));
|
||||||
|
|
||||||
|
return newLinearElement({
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x: x + layout.gap + layout.slotWidth / 2,
|
||||||
|
y: y - layout.gap,
|
||||||
|
height: maxY - minY,
|
||||||
|
width: maxX - minX,
|
||||||
|
strokeColor: seriesColors[seriesIndex],
|
||||||
|
strokeWidth: 2,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dots = series.flatMap((seriesData, seriesIndex) =>
|
||||||
|
seriesData.values.map((value, valueIndex) => {
|
||||||
|
const cx = valueIndex * (layout.slotWidth + layout.gap) + layout.gap / 2;
|
||||||
|
const cy = -(value / max) * layout.chartHeight + layout.gap / 2;
|
||||||
|
return newElement({
|
||||||
|
backgroundColor: seriesColors[seriesIndex],
|
||||||
|
...commonProps,
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeColor: seriesColors[seriesIndex],
|
||||||
|
strokeWidth: 2,
|
||||||
|
type: "ellipse",
|
||||||
|
x: x + cx + layout.slotWidth / 2,
|
||||||
|
y: y + cy - layout.gap * 2,
|
||||||
|
width: layout.gap,
|
||||||
|
height: layout.gap,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const guideValues = series[0].values.map((_, valueIndex) =>
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
...series.map((seriesData) => seriesData.values[valueIndex] ?? 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const guides = guideValues.map((value, valueIndex) => {
|
||||||
|
const cx = valueIndex * (layout.slotWidth + layout.gap) + layout.gap / 2;
|
||||||
|
const cy = (value / max) * layout.chartHeight + layout.gap / 2 + layout.gap;
|
||||||
|
return newLinearElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x: x + cx + layout.slotWidth / 2 + layout.gap / 2,
|
||||||
|
y: y - cy,
|
||||||
|
height: cy,
|
||||||
|
strokeStyle: "dotted",
|
||||||
|
opacity: GRID_OPACITY,
|
||||||
|
points: [pointFrom(0, 0), pointFrom(0, cy)],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseElements = chartBaseElements(
|
||||||
|
spreadsheet,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
backgroundColor,
|
||||||
|
layout,
|
||||||
|
max,
|
||||||
|
isDevEnv(),
|
||||||
|
);
|
||||||
|
const xLabels = chartXLabels(spreadsheet, x, y, backgroundColor, layout);
|
||||||
|
const xLabelsBottomY = Math.max(
|
||||||
|
y + layout.gap / 2,
|
||||||
|
...xLabels.map((label) => getRotatedTextElementBottom(label)),
|
||||||
|
);
|
||||||
|
const { chartWidth } = getChartDimensions(spreadsheet, layout);
|
||||||
|
const seriesLegend = createSeriesLegend(
|
||||||
|
series,
|
||||||
|
seriesColors,
|
||||||
|
x + chartWidth / 2,
|
||||||
|
xLabelsBottomY,
|
||||||
|
y + layout.gap * 5,
|
||||||
|
backgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...baseElements, ...lines, ...guides, ...dots, ...seriesLegend];
|
||||||
|
};
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { type ParseSpreadsheetResult } from "./charts.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private exported for testing
|
||||||
|
*/
|
||||||
|
export const tryParseNumber = (s: string): number | null => {
|
||||||
|
const match =
|
||||||
|
/^([-+]?)[$\u20AC\u00A3\u00A5\u20A9]?([-+]?)([\d.,]+)[%]?$/.exec(s);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseFloat(`${(match[1] || match[2]) + match[3]}`.replace(/,/g, ""));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNumericColumn = (lines: string[][], columnIndex: number) =>
|
||||||
|
lines.slice(1).every((line) => tryParseNumber(line[columnIndex]) !== null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private exported for testing
|
||||||
|
*/
|
||||||
|
export const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
|
||||||
|
const numCols = cells[0].length;
|
||||||
|
|
||||||
|
if (numCols > 2) {
|
||||||
|
const hasHeader = cells[0].every((cell) => tryParseNumber(cell) === null);
|
||||||
|
const rows = hasHeader ? cells.slice(1) : cells;
|
||||||
|
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return { ok: false, reason: "No data rows" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidNumericColumn = rows.some((row) =>
|
||||||
|
row.slice(1).some((value) => tryParseNumber(value) === null),
|
||||||
|
);
|
||||||
|
if (invalidNumericColumn) {
|
||||||
|
return { ok: false, reason: "Value is not numeric" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// When there are more value columns than data rows, the data is in
|
||||||
|
// "wide" format — transpose so columns become labels (dimensions)
|
||||||
|
// and rows become series. This enables e.g. radar charts for wide data.
|
||||||
|
const numValueCols = numCols - 1;
|
||||||
|
if (numValueCols > rows.length) {
|
||||||
|
const labels = hasHeader ? cells[0].slice(1).map((h) => h.trim()) : null;
|
||||||
|
const series = rows.map((row) => ({
|
||||||
|
title: row[0]?.trim() || null,
|
||||||
|
values: row.slice(1).map((v) => tryParseNumber(v)!),
|
||||||
|
}));
|
||||||
|
const title =
|
||||||
|
series.length === 1
|
||||||
|
? series[0].title
|
||||||
|
: hasHeader
|
||||||
|
? cells[0][0].trim() || null
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: { title, labels, series },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = cells[0].slice(1).map((seriesTitle, index) => {
|
||||||
|
const valueColumnIndex = index + 1;
|
||||||
|
const fallbackTitle = `Series ${valueColumnIndex}`;
|
||||||
|
return {
|
||||||
|
title: hasHeader ? seriesTitle.trim() || fallbackTitle : fallbackTitle,
|
||||||
|
values: rows.map((row) => tryParseNumber(row[valueColumnIndex])!),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: hasHeader ? cells[0][0].trim() || null : null,
|
||||||
|
labels: rows.map((row) => row[0]),
|
||||||
|
series,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numCols === 1) {
|
||||||
|
if (!isNumericColumn(cells, 0)) {
|
||||||
|
return { ok: false, reason: "Value is not numeric" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasHeader = tryParseNumber(cells[0][0]) === null;
|
||||||
|
const title = hasHeader ? cells[0][0] : null;
|
||||||
|
const values = (hasHeader ? cells.slice(1) : cells).map((line) =>
|
||||||
|
tryParseNumber(line[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (values.length < 2) {
|
||||||
|
return { ok: false, reason: "Less than two rows" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
labels: null,
|
||||||
|
series: [{ title, values: values as number[] }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasHeader = tryParseNumber(cells[0][1]) === null;
|
||||||
|
const rows = hasHeader ? cells.slice(1) : cells;
|
||||||
|
|
||||||
|
if (rows.length < 2) {
|
||||||
|
return { ok: false, reason: "Less than 2 rows" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidNumericColumn = rows.some(
|
||||||
|
(row) => tryParseNumber(row[1]) === null,
|
||||||
|
);
|
||||||
|
if (invalidNumericColumn) {
|
||||||
|
return { ok: false, reason: "Value is not numeric" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = hasHeader ? cells[0][1] : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
labels: rows.map((row) => row[0]),
|
||||||
|
series: [{ title, values: rows.map((row) => tryParseNumber(row[1])!) }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
||||||
|
// Copy/paste from excel, spreadsheets, TSV, CSV, semicolon-separated.
|
||||||
|
const parseDelimitedLines = (delimiter: "\t" | "," | ";") =>
|
||||||
|
text
|
||||||
|
.replace(/\r\n?/g, "\n")
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.trim().length > 0)
|
||||||
|
.map((line) => line.split(delimiter).map((cell) => cell.trim()));
|
||||||
|
|
||||||
|
// Score each delimiter: prefer consistent column counts with the most columns.
|
||||||
|
// A delimiter that produces all single-column rows likely isn't the right one.
|
||||||
|
const candidates = (["\t", ",", ";"] as const).map((delimiter) => {
|
||||||
|
const parsed = parseDelimitedLines(delimiter);
|
||||||
|
const numCols = parsed[0]?.length ?? 0;
|
||||||
|
const isConsistent =
|
||||||
|
parsed.length > 0 && parsed.every((line) => line.length === numCols);
|
||||||
|
return { delimiter, parsed, numCols, isConsistent };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prefer: consistent + most columns. Among ties, tab > comma > semicolon
|
||||||
|
// (the array order already encodes this priority).
|
||||||
|
const best =
|
||||||
|
candidates.find((c) => c.isConsistent && c.numCols > 1) ??
|
||||||
|
candidates.find((c) => c.isConsistent) ??
|
||||||
|
candidates[0];
|
||||||
|
|
||||||
|
const lines = best.parsed;
|
||||||
|
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return { ok: false, reason: "No values" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const numColsFirstLine = lines[0].length;
|
||||||
|
const isSpreadsheet = lines.every((line) => line.length === numColsFirstLine);
|
||||||
|
|
||||||
|
if (!isSpreadsheet) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "All rows don't have same number of columns",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return tryParseCells(lines);
|
||||||
|
};
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FONT_FAMILY,
|
||||||
|
FONT_SIZES,
|
||||||
|
getFontString,
|
||||||
|
getLineHeight,
|
||||||
|
ROUGHNESS,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
measureText,
|
||||||
|
newLinearElement,
|
||||||
|
newTextElement,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BAR_GAP,
|
||||||
|
BAR_HEIGHT,
|
||||||
|
GRID_OPACITY,
|
||||||
|
RADAR_GRID_LEVELS,
|
||||||
|
RADAR_LABEL_OFFSET,
|
||||||
|
commonProps,
|
||||||
|
} from "./charts.constants";
|
||||||
|
import {
|
||||||
|
createRadarAxisLabels,
|
||||||
|
createSeriesLegend,
|
||||||
|
getBackgroundColor,
|
||||||
|
getColorOffset,
|
||||||
|
getRadarDimensions,
|
||||||
|
getRadarDisplayText,
|
||||||
|
getRadarValueScale,
|
||||||
|
getSeriesColors,
|
||||||
|
isSpreadsheetValidForChartType,
|
||||||
|
} from "./charts.helpers";
|
||||||
|
|
||||||
|
import type { ChartElements, Spreadsheet } from "./charts.types";
|
||||||
|
|
||||||
|
export const renderRadarChart = (
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
colorSeed?: number,
|
||||||
|
): ChartElements | null => {
|
||||||
|
if (!isSpreadsheetValidForChartType(spreadsheet, "radar")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels =
|
||||||
|
spreadsheet.labels ??
|
||||||
|
spreadsheet.series[0].values.map((_, index) => `Value ${index + 1}`);
|
||||||
|
|
||||||
|
const series = spreadsheet.series;
|
||||||
|
const { normalize, renderSteps } = getRadarValueScale(series, labels.length);
|
||||||
|
const colorOffset = getColorOffset(colorSeed);
|
||||||
|
const backgroundColor = getBackgroundColor(colorOffset);
|
||||||
|
const seriesColors = getSeriesColors(series.length, colorOffset);
|
||||||
|
const { chartWidth, chartHeight } = getRadarDimensions();
|
||||||
|
const centerX = x + chartWidth / 2;
|
||||||
|
const centerY = y - chartHeight / 2;
|
||||||
|
const radius = BAR_HEIGHT / 2;
|
||||||
|
const angles = labels.map(
|
||||||
|
(_, index) => -Math.PI / 2 + (Math.PI * 2 * index) / labels.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { axisLabels, axisLabelTopY, axisLabelBottomY } = createRadarAxisLabels(
|
||||||
|
labels,
|
||||||
|
angles,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
backgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const titleFontFamily = FONT_FAMILY["Lilita One"];
|
||||||
|
const titleFontSize = FONT_SIZES.xl;
|
||||||
|
const titleLineHeight = getLineHeight(titleFontFamily);
|
||||||
|
const titleFontString = getFontString({
|
||||||
|
fontFamily: titleFontFamily,
|
||||||
|
fontSize: titleFontSize,
|
||||||
|
});
|
||||||
|
const titleText = spreadsheet.title
|
||||||
|
? getRadarDisplayText(
|
||||||
|
spreadsheet.title,
|
||||||
|
titleFontString,
|
||||||
|
chartWidth + RADAR_LABEL_OFFSET * 2,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const titleTextMetrics = titleText
|
||||||
|
? measureText(titleText, titleFontString, titleLineHeight)
|
||||||
|
: null;
|
||||||
|
const title = titleText
|
||||||
|
? newTextElement({
|
||||||
|
backgroundColor,
|
||||||
|
...commonProps,
|
||||||
|
text: titleText,
|
||||||
|
originalText: spreadsheet.title ?? titleText,
|
||||||
|
x: x + chartWidth / 2,
|
||||||
|
y: axisLabelTopY - RADAR_LABEL_OFFSET - titleTextMetrics!.height / 2,
|
||||||
|
fontFamily: titleFontFamily,
|
||||||
|
fontSize: titleFontSize,
|
||||||
|
lineHeight: titleLineHeight,
|
||||||
|
textAlign: "center",
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const radarGridLines = renderSteps
|
||||||
|
? Array.from({ length: RADAR_GRID_LEVELS }, (_, levelIndex) => {
|
||||||
|
const levelRatio = (levelIndex + 1) / RADAR_GRID_LEVELS;
|
||||||
|
const levelRadius = radius * levelRatio;
|
||||||
|
const points = angles.map((angle) =>
|
||||||
|
pointFrom<LocalPoint>(
|
||||||
|
Math.cos(angle) * levelRadius,
|
||||||
|
Math.sin(angle) * levelRadius,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
points.push(pointFrom(points[0][0], points[0][1]));
|
||||||
|
|
||||||
|
return newLinearElement({
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x: centerX,
|
||||||
|
y: centerY,
|
||||||
|
width: levelRadius * 2,
|
||||||
|
height: levelRadius * 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: ROUGHNESS.architect,
|
||||||
|
opacity: GRID_OPACITY,
|
||||||
|
polygon: true,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const spokes = angles.map((angle) => {
|
||||||
|
const px = Math.cos(angle) * radius;
|
||||||
|
const py = Math.sin(angle) * radius;
|
||||||
|
return newLinearElement({
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x: centerX,
|
||||||
|
y: centerY,
|
||||||
|
width: Math.abs(px),
|
||||||
|
height: Math.abs(py),
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: ROUGHNESS.architect,
|
||||||
|
opacity: GRID_OPACITY,
|
||||||
|
points: [pointFrom(0, 0), pointFrom(px, py)],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const seriesPolygons = series.map((seriesData, index) => {
|
||||||
|
const points = angles.map((angle, axisIndex) => {
|
||||||
|
const value = seriesData.values[axisIndex] ?? 0;
|
||||||
|
const pointRadius = normalize(value, axisIndex) * radius;
|
||||||
|
return pointFrom<LocalPoint>(
|
||||||
|
Math.cos(angle) * pointRadius,
|
||||||
|
Math.sin(angle) * pointRadius,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
points.push(pointFrom(points[0][0], points[0][1]));
|
||||||
|
|
||||||
|
return newLinearElement({
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x: centerX,
|
||||||
|
y: centerY,
|
||||||
|
width: radius * 2,
|
||||||
|
height: radius * 2,
|
||||||
|
strokeColor: seriesColors[index],
|
||||||
|
strokeWidth: 2,
|
||||||
|
polygon: true,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const seriesLegend = createSeriesLegend(
|
||||||
|
series,
|
||||||
|
seriesColors,
|
||||||
|
centerX,
|
||||||
|
axisLabelBottomY,
|
||||||
|
y + BAR_GAP * 5,
|
||||||
|
backgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(title ? [title] : []),
|
||||||
|
...axisLabels,
|
||||||
|
...radarGridLines,
|
||||||
|
...spokes,
|
||||||
|
...seriesPolygons,
|
||||||
|
...seriesLegend,
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
|
export interface Spreadsheet {
|
||||||
|
title: string | null;
|
||||||
|
labels: string[] | null;
|
||||||
|
series: SpreadsheetSeries[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpreadsheetSeries {
|
||||||
|
title: string | null;
|
||||||
|
values: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParseSpreadsheetResult =
|
||||||
|
| { ok: false; reason: string }
|
||||||
|
| { ok: true; data: Spreadsheet };
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { ChartType } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { renderBarChart } from "./charts.bar";
|
||||||
|
import { renderLineChart } from "./charts.line";
|
||||||
|
import {
|
||||||
|
tryParseCells,
|
||||||
|
tryParseNumber,
|
||||||
|
tryParseSpreadsheet,
|
||||||
|
} from "./charts.parse";
|
||||||
|
import { renderRadarChart } from "./charts.radar";
|
||||||
|
|
||||||
|
import type { ChartElements, Spreadsheet } from "./charts.types";
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ParseSpreadsheetResult,
|
||||||
|
type Spreadsheet,
|
||||||
|
type SpreadsheetSeries,
|
||||||
|
type ChartElements,
|
||||||
|
} from "./charts.types";
|
||||||
|
|
||||||
|
export { isSpreadsheetValidForChartType } from "./charts.helpers";
|
||||||
|
export { tryParseCells, tryParseNumber, tryParseSpreadsheet };
|
||||||
|
|
||||||
|
export const renderSpreadsheet = (
|
||||||
|
chartType: ChartType,
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
colorSeed?: number,
|
||||||
|
): ChartElements | null => {
|
||||||
|
if (chartType === "line") {
|
||||||
|
return renderLineChart(spreadsheet, x, y, colorSeed);
|
||||||
|
}
|
||||||
|
if (chartType === "radar") {
|
||||||
|
return renderRadarChart(spreadsheet, x, y, colorSeed);
|
||||||
|
}
|
||||||
|
return renderBarChart(spreadsheet, x, y, colorSeed);
|
||||||
|
};
|
||||||
@@ -155,67 +155,4 @@ describe("parseClipboard()", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse spreadsheet from either text/plain and text/html", async () => {
|
|
||||||
let clipboardData;
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
await parseDataTransferEvent(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/plain": `a b
|
|
||||||
1 2
|
|
||||||
4 5
|
|
||||||
7 10`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(clipboardData.spreadsheet).toEqual({
|
|
||||||
title: "b",
|
|
||||||
labels: ["1", "4", "7"],
|
|
||||||
values: [2, 5, 10],
|
|
||||||
});
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
await parseDataTransferEvent(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `a b
|
|
||||||
1 2
|
|
||||||
4 5
|
|
||||||
7 10`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(clipboardData.spreadsheet).toEqual({
|
|
||||||
title: "b",
|
|
||||||
labels: ["1", "4", "7"],
|
|
||||||
values: [2, 5, 10],
|
|
||||||
});
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
await parseDataTransferEvent(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `<html>
|
|
||||||
<body>
|
|
||||||
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"a"}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"b"}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":10}">10</td></tr></tbody></table><!--EndFragment-->
|
|
||||||
</body>
|
|
||||||
</html>`,
|
|
||||||
"text/plain": `a b
|
|
||||||
1 2
|
|
||||||
4 5
|
|
||||||
7 10`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(clipboardData.spreadsheet).toEqual({
|
|
||||||
title: "b",
|
|
||||||
labels: ["1", "4", "7"],
|
|
||||||
values: [2, 5, 10],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,12 +33,8 @@ import {
|
|||||||
normalizeFile,
|
normalizeFile,
|
||||||
} from "./data/blob";
|
} from "./data/blob";
|
||||||
|
|
||||||
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
|
|
||||||
|
|
||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
|
|
||||||
import type { Spreadsheet } from "./charts";
|
|
||||||
|
|
||||||
import type { BinaryFiles } from "./types";
|
import type { BinaryFiles } from "./types";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
@@ -50,7 +46,6 @@ type ElementsClipboard = {
|
|||||||
export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
|
export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
|
||||||
|
|
||||||
export interface ClipboardData {
|
export interface ClipboardData {
|
||||||
spreadsheet?: Spreadsheet;
|
|
||||||
elements?: readonly ExcalidrawElement[];
|
elements?: readonly ExcalidrawElement[];
|
||||||
files?: BinaryFiles;
|
files?: BinaryFiles;
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -215,16 +210,6 @@ export const copyToClipboard = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePotentialSpreadsheet = (
|
|
||||||
text: string,
|
|
||||||
): { spreadsheet: Spreadsheet } | { errorMessage: string } | null => {
|
|
||||||
const result = tryParseSpreadsheet(text);
|
|
||||||
if (result.type === VALID_SPREADSHEET) {
|
|
||||||
return { spreadsheet: result.spreadsheet };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** internal, specific to parsing paste events. Do not reuse. */
|
/** internal, specific to parsing paste events. Do not reuse. */
|
||||||
function parseHTMLTree(el: ChildNode) {
|
function parseHTMLTree(el: ChildNode) {
|
||||||
let result: PastedMixedContent = [];
|
let result: PastedMixedContent = [];
|
||||||
@@ -551,19 +536,6 @@ export const parseClipboard = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
// if system clipboard contains spreadsheet, use it even though it's
|
|
||||||
// technically possible it's staler than in-app clipboard
|
|
||||||
const spreadsheetResult =
|
|
||||||
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
|
|
||||||
|
|
||||||
if (spreadsheetResult) {
|
|
||||||
return spreadsheetResult;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(parsedEventData.value);
|
const systemClipboardData = JSON.parse(parsedEventData.value);
|
||||||
const programmaticAPI =
|
const programmaticAPI =
|
||||||
|
|||||||
@@ -425,6 +425,8 @@ import { EraserTrail } from "../eraser";
|
|||||||
|
|
||||||
import { getShortcutKey } from "../shortcut";
|
import { getShortcutKey } from "../shortcut";
|
||||||
|
|
||||||
|
import { tryParseSpreadsheet } from "../charts";
|
||||||
|
|
||||||
import ConvertElementTypePopup, {
|
import ConvertElementTypePopup, {
|
||||||
getConversionTypeFromElements,
|
getConversionTypeFromElements,
|
||||||
convertElementTypePopupAtom,
|
convertElementTypePopupAtom,
|
||||||
@@ -3542,14 +3544,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------- Spreadsheet -------------------
|
// ------------------- Spreadsheet -------------------
|
||||||
if (data.spreadsheet && !isPlainPaste) {
|
|
||||||
this.setState({
|
if (!isPlainPaste && data.text) {
|
||||||
pasteDialog: {
|
const result = tryParseSpreadsheet(data.text);
|
||||||
data: data.spreadsheet,
|
if (result.ok) {
|
||||||
shown: true,
|
this.setState({
|
||||||
},
|
openDialog: {
|
||||||
});
|
name: "charts",
|
||||||
return;
|
data: result.data,
|
||||||
|
rawText: data.text,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------- Images or SVG code -------------------
|
// ------------------- Images or SVG code -------------------
|
||||||
|
|||||||
@@ -556,13 +556,13 @@ const LayerUI = ({
|
|||||||
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.openDialog?.name === "charts" && (
|
||||||
<PasteChartDialog
|
<PasteChartDialog
|
||||||
setAppState={setAppState}
|
data={appState.openDialog.data}
|
||||||
appState={appState}
|
rawText={appState.openDialog.rawText}
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
setAppState({
|
setAppState({
|
||||||
pasteDialog: { shown: false, data: null },
|
openDialog: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,40 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.PasteChartDialog {
|
.PasteChartDialog {
|
||||||
|
.PasteChartDialog__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PasteChartDialog__titleText {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PasteChartDialog__reshuffleBtn {
|
||||||
|
margin-left: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
transition: transform 120ms ease, background-color 120ms ease,
|
||||||
|
color 120ms ease;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-blue-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.94);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
.Island {
|
.Island {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -11,35 +45,61 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ChartPreview {
|
.ChartPreview {
|
||||||
margin: 8px;
|
width: 260px;
|
||||||
text-align: center;
|
min-height: 190px;
|
||||||
width: 192px;
|
border-radius: 8px;
|
||||||
height: 128px;
|
padding: 10px;
|
||||||
border-radius: 2px;
|
|
||||||
padding: 1px;
|
|
||||||
border: 1px solid $color-gray-4;
|
border: 1px solid $color-gray-4;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
div {
|
.ChartPreview__canvas {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ChartPreview__label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-primary-color);
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
max-height: 120px;
|
max-height: 144px;
|
||||||
max-width: 186px;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
padding: 0;
|
border-color: $color-blue-5;
|
||||||
border: 2px solid $color-blue-5;
|
}
|
||||||
|
&:active {
|
||||||
|
border-color: $color-blue-5;
|
||||||
|
box-shadow: 0 0 0 1px $color-blue-5;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: $color-blue-5;
|
||||||
|
box-shadow: 0 0 0 1px $color-blue-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,57 @@
|
|||||||
import React, { useLayoutEffect, useRef, useState } from "react";
|
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { newTextElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ChartType } from "@excalidraw/element/types";
|
import type { ChartType } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { renderSpreadsheet } from "../charts";
|
import { isSpreadsheetValidForChartType, renderSpreadsheet } from "../charts";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
|
|
||||||
|
import { useUIAppState } from "../context/ui-appState";
|
||||||
|
|
||||||
import { useApp } from "./App";
|
import { useApp } from "./App";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
|
||||||
import "./PasteChartDialog.scss";
|
import "./PasteChartDialog.scss";
|
||||||
|
|
||||||
|
import { bucketFillIcon } from "./icons";
|
||||||
|
|
||||||
import type { ChartElements, Spreadsheet } from "../charts";
|
import type { ChartElements, Spreadsheet } from "../charts";
|
||||||
import type { UIAppState } from "../types";
|
|
||||||
|
type OnPlainTextPaste = (rawText: string) => void;
|
||||||
|
|
||||||
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||||
|
|
||||||
|
const getChartTypeLabel = (chartType: ChartType) => {
|
||||||
|
switch (chartType) {
|
||||||
|
case "bar":
|
||||||
|
return t("labels.chartType_bar");
|
||||||
|
case "line":
|
||||||
|
return t("labels.chartType_line");
|
||||||
|
case "radar":
|
||||||
|
return t("labels.chartType_radar");
|
||||||
|
default:
|
||||||
|
return chartType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const ChartPreviewBtn = (props: {
|
const ChartPreviewBtn = (props: {
|
||||||
spreadsheet: Spreadsheet | null;
|
spreadsheet: Spreadsheet | null;
|
||||||
chartType: ChartType;
|
chartType: ChartType;
|
||||||
selected: boolean;
|
colorSeed: number;
|
||||||
onClick: OnInsertChart;
|
onClick: OnInsertChart;
|
||||||
}) => {
|
}) => {
|
||||||
const previewRef = useRef<HTMLDivElement | null>(null);
|
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [chartElements, setChartElements] = useState<ChartElements | null>(
|
const [chartElements, setChartElements] = useState<ChartElements | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const { theme } = useUIAppState();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!props.spreadsheet) {
|
if (!props.spreadsheet) {
|
||||||
|
setChartElements(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +60,13 @@ const ChartPreviewBtn = (props: {
|
|||||||
props.spreadsheet,
|
props.spreadsheet,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
props.colorSeed,
|
||||||
);
|
);
|
||||||
|
if (!elements) {
|
||||||
|
setChartElements(null);
|
||||||
|
previewRef.current?.replaceChildren();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setChartElements(elements);
|
setChartElements(elements);
|
||||||
let svg: SVGSVGElement;
|
let svg: SVGSVGElement;
|
||||||
const previewNode = previewRef.current!;
|
const previewNode = previewRef.current!;
|
||||||
@@ -49,6 +77,7 @@ const ChartPreviewBtn = (props: {
|
|||||||
{
|
{
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: "#fff",
|
viewBackgroundColor: "#fff",
|
||||||
|
exportWithDarkMode: theme === "dark",
|
||||||
},
|
},
|
||||||
null, // files
|
null, // files
|
||||||
{
|
{
|
||||||
@@ -58,42 +87,108 @@ const ChartPreviewBtn = (props: {
|
|||||||
svg.querySelector(".style-fonts")?.remove();
|
svg.querySelector(".style-fonts")?.remove();
|
||||||
previewNode.replaceChildren();
|
previewNode.replaceChildren();
|
||||||
previewNode.appendChild(svg);
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
if (props.selected) {
|
|
||||||
(previewNode.parentNode as HTMLDivElement).focus();
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
previewNode.replaceChildren();
|
previewNode.replaceChildren();
|
||||||
};
|
};
|
||||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
}, [props.spreadsheet, props.chartType, props.colorSeed, theme]);
|
||||||
|
|
||||||
|
const chartTypeLabel = getChartTypeLabel(props.chartType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ChartPreview"
|
className="ChartPreview"
|
||||||
|
aria-label={chartTypeLabel}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (chartElements) {
|
if (chartElements) {
|
||||||
props.onClick(props.chartType, chartElements);
|
props.onClick(props.chartType, chartElements);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={previewRef} />
|
<div className="ChartPreview__canvas" ref={previewRef} />
|
||||||
|
<div className="ChartPreview__label">{chartTypeLabel}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlainTextPreviewBtn = (props: {
|
||||||
|
rawText: string;
|
||||||
|
onClick: OnPlainTextPaste;
|
||||||
|
}) => {
|
||||||
|
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { theme } = useUIAppState();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!props.rawText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textElement = newTextElement({
|
||||||
|
text: props.rawText,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const svg = await exportToSvg(
|
||||||
|
[textElement],
|
||||||
|
{
|
||||||
|
exportBackground: false,
|
||||||
|
viewBackgroundColor: "#fff",
|
||||||
|
exportWithDarkMode: theme === "dark",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
skipInliningFonts: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
svg.querySelector(".style-fonts")?.remove();
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
previewNode.appendChild(svg);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
};
|
||||||
|
}, [props.rawText, theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ChartPreview"
|
||||||
|
aria-label={t("labels.chartType_plaintext")}
|
||||||
|
onClick={() => {
|
||||||
|
props.onClick(props.rawText);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="ChartPreview__canvas" ref={previewRef} />
|
||||||
|
<div className="ChartPreview__label">
|
||||||
|
{t("labels.chartType_plaintext")}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PasteChartDialog = ({
|
export const PasteChartDialog = ({
|
||||||
setAppState,
|
data,
|
||||||
appState,
|
rawText,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
data: Spreadsheet;
|
||||||
|
rawText: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
|
||||||
}) => {
|
}) => {
|
||||||
const { onInsertElements } = useApp();
|
const { onInsertElements, focusContainer } = useApp();
|
||||||
|
const [colorSeed, setColorSeed] = useState(Math.random());
|
||||||
|
|
||||||
|
const handleReshuffleColors = React.useCallback(() => {
|
||||||
|
setColorSeed(Math.random());
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
@@ -103,36 +198,72 @@ export const PasteChartDialog = ({
|
|||||||
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||||
onInsertElements(elements);
|
onInsertElements(elements);
|
||||||
trackEvent("paste", "chart", chartType);
|
trackEvent("paste", "chart", chartType);
|
||||||
setAppState({
|
onClose();
|
||||||
currentChartType: chartType,
|
focusContainer();
|
||||||
pasteDialog: {
|
};
|
||||||
shown: false,
|
|
||||||
data: null,
|
const handlePlainTextClick = (rawText: string) => {
|
||||||
},
|
const textElement = newTextElement({
|
||||||
|
text: rawText,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
});
|
});
|
||||||
|
onInsertElements([textElement]);
|
||||||
|
trackEvent("paste", "chart", "plaintext");
|
||||||
|
onClose();
|
||||||
|
focusContainer();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
size="small"
|
size="regular"
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
title={t("labels.pasteCharts")}
|
title={
|
||||||
|
<div className="PasteChartDialog__title">
|
||||||
|
<div className="PasteChartDialog__titleText">
|
||||||
|
{t("labels.pasteCharts")}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="PasteChartDialog__reshuffleBtn"
|
||||||
|
onClick={handleReshuffleColors}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
handleReshuffleColors();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bucketFillIcon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
className={"PasteChartDialog"}
|
className={"PasteChartDialog"}
|
||||||
autofocus={false}
|
autofocus={false}
|
||||||
>
|
>
|
||||||
<div className={"container"}>
|
<div className={"container"}>
|
||||||
<ChartPreviewBtn
|
{(["bar", "line", "radar"] as const).map((chartType) => {
|
||||||
chartType="bar"
|
if (!isSpreadsheetValidForChartType(data, chartType)) {
|
||||||
spreadsheet={appState.pasteDialog.data}
|
return null;
|
||||||
selected={appState.currentChartType === "bar"}
|
}
|
||||||
onClick={handleChartClick}
|
|
||||||
/>
|
return (
|
||||||
<ChartPreviewBtn
|
<ChartPreviewBtn
|
||||||
chartType="line"
|
key={chartType}
|
||||||
spreadsheet={appState.pasteDialog.data}
|
chartType={chartType}
|
||||||
selected={appState.currentChartType === "line"}
|
spreadsheet={data}
|
||||||
onClick={handleChartClick}
|
colorSeed={colorSeed}
|
||||||
/>
|
onClick={handleChartClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{rawText && (
|
||||||
|
<PlainTextPreviewBtn
|
||||||
|
rawText={rawText}
|
||||||
|
onClick={handlePlainTextClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -319,3 +319,9 @@ export { isElementLink } from "@excalidraw/element";
|
|||||||
export { setCustomTextMetricsProvider } from "@excalidraw/element";
|
export { setCustomTextMetricsProvider } from "@excalidraw/element";
|
||||||
|
|
||||||
export { CommandPalette } from "./components/CommandPalette/CommandPalette";
|
export { CommandPalette } from "./components/CommandPalette/CommandPalette";
|
||||||
|
|
||||||
|
export {
|
||||||
|
renderSpreadsheet,
|
||||||
|
tryParseSpreadsheet,
|
||||||
|
isSpreadsheetValidForChartType,
|
||||||
|
} from "./charts";
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"pasteAsPlaintext": "Paste as plaintext",
|
"pasteAsPlaintext": "Paste as plaintext",
|
||||||
"pasteCharts": "Paste charts",
|
"pasteCharts": "Paste charts",
|
||||||
|
"chartType_bar": "Bar chart",
|
||||||
|
"chartType_line": "Line chart",
|
||||||
|
"chartType_radar": "Radar chart",
|
||||||
|
"chartType_plaintext": "Plain text",
|
||||||
"selectAll": "Select all",
|
"selectAll": "Select all",
|
||||||
"multiSelect": "Add element to selection",
|
"multiSelect": "Add element to selection",
|
||||||
"moveCanvas": "Move canvas",
|
"moveCanvas": "Move canvas",
|
||||||
|
|||||||
@@ -2,19 +2,24 @@
|
|||||||
|
|
||||||
exports[`tryParseSpreadsheet > works for numbers with comma in them 1`] = `
|
exports[`tryParseSpreadsheet > works for numbers with comma in them 1`] = `
|
||||||
{
|
{
|
||||||
"spreadsheet": {
|
"data": {
|
||||||
"labels": [
|
"labels": [
|
||||||
"Week 1",
|
"Week 1",
|
||||||
"Week 2",
|
"Week 2",
|
||||||
"Week 3",
|
"Week 3",
|
||||||
],
|
],
|
||||||
"title": "Users",
|
"series": [
|
||||||
"values": [
|
{
|
||||||
814,
|
"title": "Users",
|
||||||
10301,
|
"values": [
|
||||||
4264,
|
814,
|
||||||
|
10301,
|
||||||
|
4264,
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
"title": "Users",
|
||||||
},
|
},
|
||||||
"type": "VALID_SPREADSHEET",
|
"ok": true,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -889,7 +889,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"top": 40,
|
"top": 40,
|
||||||
},
|
},
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -951,10 +950,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -1091,7 +1086,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -1150,10 +1144,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -1308,7 +1298,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -1367,10 +1356,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -1642,7 +1627,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -1701,10 +1685,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -1976,7 +1956,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -2035,10 +2014,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -2193,7 +2168,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -2252,10 +2226,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -2437,7 +2407,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -2496,10 +2465,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -2738,7 +2703,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -2797,10 +2761,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -3113,7 +3073,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "#a5d8ff",
|
"currentItemBackgroundColor": "#a5d8ff",
|
||||||
@@ -3172,10 +3131,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -3609,7 +3564,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -3668,10 +3622,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -3935,7 +3885,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -3994,10 +3943,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -4261,7 +4206,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -4320,10 +4264,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -5549,7 +5489,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"top": -7,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -5608,10 +5547,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -6769,7 +6704,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"top": -7,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -6828,10 +6762,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -7707,7 +7637,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"top": -9,
|
"top": -9,
|
||||||
},
|
},
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -7769,10 +7698,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -8710,7 +8635,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"top": -7,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -8769,10 +8693,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -9704,7 +9624,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"top": 90,
|
"top": 90,
|
||||||
},
|
},
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -9766,10 +9685,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -74,10 +73,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -444,7 +439,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -503,10 +497,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -863,7 +853,6 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -922,10 +911,6 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -1432,7 +1417,6 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -1491,10 +1475,6 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -1642,7 +1622,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -1701,10 +1680,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -2029,7 +2004,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -2088,10 +2062,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -2277,7 +2247,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -2336,10 +2305,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -2460,7 +2425,6 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -2519,10 +2483,6 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -2788,7 +2748,6 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
@@ -2847,10 +2806,6 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"openPopup": "elementStroke",
|
"openPopup": "elementStroke",
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -3046,7 +3001,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -3105,10 +3059,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -3290,7 +3240,6 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -3349,10 +3298,6 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -3529,7 +3474,6 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -3588,10 +3532,6 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -3790,7 +3730,6 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -3849,10 +3788,6 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -4107,7 +4042,6 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -4166,10 +4100,6 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -4546,7 +4476,6 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -4605,10 +4534,6 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -4832,7 +4757,6 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -4891,10 +4815,6 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -5111,7 +5031,6 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -5170,10 +5089,6 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -5322,7 +5237,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -5381,10 +5295,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -5525,7 +5435,6 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -5584,10 +5493,6 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -5921,7 +5826,6 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -5980,10 +5884,6 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -6221,7 +6121,6 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -6280,10 +6179,6 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -7012,7 +6907,6 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -7071,10 +6965,6 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -7349,7 +7239,6 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
@@ -7408,10 +7297,6 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -7631,7 +7516,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -7690,10 +7574,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -7869,7 +7749,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -7928,10 +7807,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -8112,7 +7987,6 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -8171,10 +8045,6 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -8295,7 +8165,6 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -8354,10 +8223,6 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -8478,7 +8343,6 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -8537,10 +8401,6 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -8661,7 +8521,6 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -8720,10 +8579,6 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -8896,7 +8751,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -8955,10 +8809,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -9129,7 +8979,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -9188,10 +9037,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -9324,7 +9169,6 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -9383,10 +9227,6 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -9559,7 +9399,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -9618,10 +9457,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -9742,7 +9577,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -9801,10 +9635,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -9975,7 +9805,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -10034,10 +9863,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -10158,7 +9983,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -10217,10 +10041,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -10353,7 +10173,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -10412,10 +10231,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -10536,7 +10351,6 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -10595,10 +10409,6 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -11070,7 +10880,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -11129,10 +10938,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -11353,7 +11158,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -11412,10 +11216,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -11479,7 +11279,6 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -11538,10 +11337,6 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -11682,7 +11477,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -11741,10 +11535,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -12004,7 +11794,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -12063,10 +11852,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -12436,7 +12221,6 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -12495,10 +12279,6 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -13079,7 +12859,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -13141,10 +12920,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -13208,7 +12983,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -13267,10 +13041,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -13842,7 +13612,6 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -13901,10 +13670,6 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -14184,7 +13949,6 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -14243,10 +14007,6 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -14451,7 +14211,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -14510,10 +14269,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -14577,7 +14332,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -14636,10 +14390,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -14944,7 +14694,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -15003,10 +14752,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -15070,7 +14815,6 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -15132,10 +14876,6 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
|
|||||||
@@ -10,4 +10,155 @@ Week 3${"\t"}4,264`,
|
|||||||
);
|
);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("parses multi-series CSV for radar charts", () => {
|
||||||
|
const result = tryParseSpreadsheet(
|
||||||
|
`Metric,Player A,Player B,Player C
|
||||||
|
Speed,80,60,75
|
||||||
|
Strength,65,85,70
|
||||||
|
Agility,90,70,88
|
||||||
|
Intelligence,70,88,92
|
||||||
|
Stamina,85,75,80`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: "Metric",
|
||||||
|
labels: ["Speed", "Strength", "Agility", "Intelligence", "Stamina"],
|
||||||
|
series: [
|
||||||
|
{ title: "Player A", values: [80, 65, 90, 70, 85] },
|
||||||
|
{ title: "Player B", values: [60, 85, 70, 88, 75] },
|
||||||
|
{ title: "Player C", values: [75, 70, 88, 92, 80] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses TSV with empty chart-name header cell", () => {
|
||||||
|
const result = tryParseSpreadsheet(
|
||||||
|
`\tDunk\tEgg
|
||||||
|
Physical Strength\t10\t2
|
||||||
|
Swordsmanship\t8\t1
|
||||||
|
Political Instinct\t3\t9`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: null,
|
||||||
|
labels: ["Physical Strength", "Swordsmanship", "Political Instinct"],
|
||||||
|
series: [
|
||||||
|
{ title: "Dunk", values: [10, 8, 3] },
|
||||||
|
{ title: "Egg", values: [2, 1, 9] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses 2-row multi-series TSV without transposing", () => {
|
||||||
|
const result = tryParseSpreadsheet(
|
||||||
|
`Physical Strength\t10\t2
|
||||||
|
Swordsmanship skill\t8\t1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: null,
|
||||||
|
labels: ["Physical Strength", "Swordsmanship skill"],
|
||||||
|
series: [
|
||||||
|
{ title: "Series 1", values: [10, 8] },
|
||||||
|
{ title: "Series 2", values: [2, 1] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses semicolon-separated values", () => {
|
||||||
|
const result = tryParseSpreadsheet(
|
||||||
|
`Metric;Player A;Player B
|
||||||
|
Speed;80;60
|
||||||
|
Strength;65;85
|
||||||
|
Agility;90;70`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: "Metric",
|
||||||
|
labels: ["Speed", "Strength", "Agility"],
|
||||||
|
series: [
|
||||||
|
{ title: "Player A", values: [80, 65, 90] },
|
||||||
|
{ title: "Player B", values: [60, 85, 70] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("transposes wide data (more value cols than rows) into series-per-row", () => {
|
||||||
|
const result = tryParseSpreadsheet(
|
||||||
|
`trait,Dunk,Egg,Daeron
|
||||||
|
Physical,10,2,7
|
||||||
|
Mental,10,2,7`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: "trait",
|
||||||
|
labels: ["Dunk", "Egg", "Daeron"],
|
||||||
|
series: [
|
||||||
|
{ title: "Physical", values: [10, 2, 7] },
|
||||||
|
{ title: "Mental", values: [10, 2, 7] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("transposes single data row with header into single series", () => {
|
||||||
|
const result = tryParseSpreadsheet(
|
||||||
|
`trait,Dunk,Egg,Daeron
|
||||||
|
Physical,10,2,7`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: "Physical",
|
||||||
|
labels: ["Dunk", "Egg", "Daeron"],
|
||||||
|
series: [{ title: "Physical", values: [10, 2, 7] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("transposes single data row without header into single series", () => {
|
||||||
|
const result = tryParseSpreadsheet(`Physical,10,2,7`);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: "Physical",
|
||||||
|
labels: null,
|
||||||
|
series: [{ title: "Physical", values: [10, 2, 7] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers tab over comma/semicolon when tabs produce multiple columns", () => {
|
||||||
|
const result = tryParseSpreadsheet(
|
||||||
|
`Label\tValue
|
||||||
|
A\t10
|
||||||
|
B\t20`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
title: "Value",
|
||||||
|
labels: ["A", "B"],
|
||||||
|
series: [{ title: "Value", values: [10, 20] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import type {
|
|||||||
GroupId,
|
GroupId,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ChartType,
|
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
FileId,
|
FileId,
|
||||||
Theme,
|
Theme,
|
||||||
@@ -383,7 +382,8 @@ export interface AppState {
|
|||||||
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
||||||
| { name: "commandPalette" }
|
| { name: "commandPalette" }
|
||||||
| { name: "settings" }
|
| { name: "settings" }
|
||||||
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
|
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] }
|
||||||
|
| { name: "charts"; data: Spreadsheet; rawText: string };
|
||||||
/**
|
/**
|
||||||
* Reflects user preference for whether the default sidebar should be docked.
|
* Reflects user preference for whether the default sidebar should be docked.
|
||||||
*
|
*
|
||||||
@@ -425,16 +425,6 @@ export interface AppState {
|
|||||||
/** bitmap. Use `STATS_PANELS` bit values */
|
/** bitmap. Use `STATS_PANELS` bit values */
|
||||||
panels: number;
|
panels: number;
|
||||||
};
|
};
|
||||||
currentChartType: ChartType;
|
|
||||||
pasteDialog:
|
|
||||||
| {
|
|
||||||
shown: false;
|
|
||||||
data: null;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
shown: true;
|
|
||||||
data: Spreadsheet;
|
|
||||||
};
|
|
||||||
showHyperlinkPopup: false | "info" | "editor";
|
showHyperlinkPopup: false | "info" | "editor";
|
||||||
selectedLinearElement: LinearElementEditor | null;
|
selectedLinearElement: LinearElementEditor | null;
|
||||||
snapLines: readonly SnapLine[];
|
snapLines: readonly SnapLine[];
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemArrowType": "round",
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
@@ -75,10 +74,6 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
|
|||||||
Reference in New Issue
Block a user