feat: Add line chart and paste dialog selection (#2670)
Co-authored-by: dwelle <luzar.david@gmail.com> Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
parent
c1e2146d78
commit
022f349dc6
@ -1,23 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
import colors from "../colors";
|
||||||
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
|
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
|
||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { CODES, KEYS } from "../keys";
|
|
||||||
import { getShortcutKey } from "../utils";
|
|
||||||
import useIsMobile from "../is-mobile";
|
|
||||||
import { register } from "./register";
|
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { t } from "../i18n";
|
||||||
import { getCommonBounds } from "../element";
|
import useIsMobile from "../is-mobile";
|
||||||
import { getNewZoom } from "../scene/zoom";
|
import { CODES, KEYS } from "../keys";
|
||||||
|
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
import { getNewZoom } from "../scene/zoom";
|
||||||
import colors from "../colors";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
|
import { getShortcutKey } from "../utils";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@ -67,6 +66,7 @@ export const actionClearCanvas = register({
|
|||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize,
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
shouldAddWatermark: appState.shouldAddWatermark,
|
||||||
showStats: appState.showStats,
|
showStats: appState.showStats,
|
||||||
|
pasteDialog: appState.pasteDialog,
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
117
src/appState.ts
117
src/appState.ts
@ -1,12 +1,12 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
|
||||||
import { getDateTime } from "./utils";
|
|
||||||
import { t } from "./i18n";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
import { t } from "./i18n";
|
||||||
|
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
||||||
|
import { getDateTime } from "./utils";
|
||||||
|
|
||||||
export const getDefaultAppState = (): Omit<
|
export const getDefaultAppState = (): Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@ -14,64 +14,63 @@ export const getDefaultAppState = (): Omit<
|
|||||||
> => {
|
> => {
|
||||||
return {
|
return {
|
||||||
appearance: "light",
|
appearance: "light",
|
||||||
isLoading: false,
|
collaborators: new Map(),
|
||||||
errorMessage: null,
|
currentChartType: "bar",
|
||||||
|
currentItemBackgroundColor: "transparent",
|
||||||
|
currentItemEndArrowhead: "arrow",
|
||||||
|
currentItemFillStyle: "hachure",
|
||||||
|
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||||
|
currentItemLinearStrokeSharpness: "round",
|
||||||
|
currentItemOpacity: 100,
|
||||||
|
currentItemRoughness: 1,
|
||||||
|
currentItemStartArrowhead: null,
|
||||||
|
currentItemStrokeColor: oc.black,
|
||||||
|
currentItemStrokeSharpness: "sharp",
|
||||||
|
currentItemStrokeStyle: "solid",
|
||||||
|
currentItemStrokeWidth: 1,
|
||||||
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
|
cursorButton: "up",
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
resizingElement: null,
|
|
||||||
multiElement: null,
|
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
startBoundElement: null,
|
editingGroupId: null,
|
||||||
editingLinearElement: null,
|
editingLinearElement: null,
|
||||||
elementType: "selection",
|
|
||||||
elementLocked: false,
|
elementLocked: false,
|
||||||
|
elementType: "selection",
|
||||||
|
errorMessage: null,
|
||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
exportEmbedScene: false,
|
exportEmbedScene: false,
|
||||||
shouldAddWatermark: false,
|
fileHandle: null,
|
||||||
currentItemStrokeColor: oc.black,
|
gridSize: null,
|
||||||
currentItemBackgroundColor: "transparent",
|
height: window.innerHeight,
|
||||||
currentItemFillStyle: "hachure",
|
|
||||||
currentItemStrokeWidth: 1,
|
|
||||||
currentItemStrokeStyle: "solid",
|
|
||||||
currentItemRoughness: 1,
|
|
||||||
currentItemOpacity: 100,
|
|
||||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
|
||||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
|
||||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
|
||||||
currentItemStrokeSharpness: "sharp",
|
|
||||||
currentItemLinearStrokeSharpness: "round",
|
|
||||||
currentItemStartArrowhead: null,
|
|
||||||
currentItemEndArrowhead: "arrow",
|
|
||||||
viewBackgroundColor: oc.white,
|
|
||||||
scrollX: 0 as FlooredNumber,
|
|
||||||
scrollY: 0 as FlooredNumber,
|
|
||||||
cursorButton: "up",
|
|
||||||
scrolledOutside: false,
|
|
||||||
name: `${t("labels.untitled")}-${getDateTime()}`,
|
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
|
isLibraryOpen: false,
|
||||||
|
isLoading: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
selectionElement: null,
|
|
||||||
zoom: {
|
|
||||||
value: 1 as NormalizedZoomValue,
|
|
||||||
translation: { x: 0, y: 0 },
|
|
||||||
},
|
|
||||||
openMenu: null,
|
|
||||||
lastPointerDownWith: "mouse",
|
lastPointerDownWith: "mouse",
|
||||||
selectedElementIds: {},
|
multiElement: null,
|
||||||
|
name: `${t("labels.untitled")}-${getDateTime()}`,
|
||||||
|
openMenu: null,
|
||||||
|
pasteDialog: { shown: false, data: null },
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
|
resizingElement: null,
|
||||||
|
scrolledOutside: false,
|
||||||
|
scrollX: 0 as FlooredNumber,
|
||||||
|
scrollY: 0 as FlooredNumber,
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
selectionElement: null,
|
||||||
|
shouldAddWatermark: false,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showShortcutsDialog: false,
|
showShortcutsDialog: false,
|
||||||
suggestedBindings: [],
|
|
||||||
zenModeEnabled: false,
|
|
||||||
gridSize: null,
|
|
||||||
editingGroupId: null,
|
|
||||||
selectedGroupIds: {},
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight,
|
|
||||||
isLibraryOpen: false,
|
|
||||||
fileHandle: null,
|
|
||||||
collaborators: new Map(),
|
|
||||||
showStats: false,
|
showStats: false,
|
||||||
|
startBoundElement: null,
|
||||||
|
suggestedBindings: [],
|
||||||
|
viewBackgroundColor: oc.white,
|
||||||
|
width: window.innerWidth,
|
||||||
|
zenModeEnabled: false,
|
||||||
|
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,24 +90,25 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
|
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
|
||||||
) => config)({
|
) => config)({
|
||||||
appearance: { browser: true, export: false },
|
appearance: { browser: true, export: false },
|
||||||
|
collaborators: { browser: false, export: false },
|
||||||
|
currentChartType: { browser: true, export: false },
|
||||||
currentItemBackgroundColor: { browser: true, export: false },
|
currentItemBackgroundColor: { browser: true, export: false },
|
||||||
|
currentItemEndArrowhead: { browser: true, export: false },
|
||||||
currentItemFillStyle: { browser: true, export: false },
|
currentItemFillStyle: { browser: true, export: false },
|
||||||
currentItemFontFamily: { browser: true, export: false },
|
currentItemFontFamily: { browser: true, export: false },
|
||||||
currentItemFontSize: { browser: true, export: false },
|
currentItemFontSize: { browser: true, export: false },
|
||||||
|
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
||||||
currentItemOpacity: { browser: true, export: false },
|
currentItemOpacity: { browser: true, export: false },
|
||||||
currentItemRoughness: { browser: true, export: false },
|
currentItemRoughness: { browser: true, export: false },
|
||||||
|
currentItemStartArrowhead: { browser: true, export: false },
|
||||||
currentItemStrokeColor: { browser: true, export: false },
|
currentItemStrokeColor: { browser: true, export: false },
|
||||||
|
currentItemStrokeSharpness: { browser: true, export: false },
|
||||||
currentItemStrokeStyle: { browser: true, export: false },
|
currentItemStrokeStyle: { browser: true, export: false },
|
||||||
currentItemStrokeWidth: { browser: true, export: false },
|
currentItemStrokeWidth: { browser: true, export: false },
|
||||||
currentItemTextAlign: { browser: true, export: false },
|
currentItemTextAlign: { browser: true, export: false },
|
||||||
currentItemStrokeSharpness: { browser: true, export: false },
|
|
||||||
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
|
||||||
currentItemStartArrowhead: { browser: true, export: false },
|
|
||||||
currentItemEndArrowhead: { browser: true, export: false },
|
|
||||||
cursorButton: { browser: true, export: false },
|
cursorButton: { browser: true, export: false },
|
||||||
draggingElement: { browser: false, export: false },
|
draggingElement: { browser: false, export: false },
|
||||||
editingElement: { browser: false, export: false },
|
editingElement: { browser: false, export: false },
|
||||||
startBoundElement: { browser: false, export: false },
|
|
||||||
editingGroupId: { browser: true, export: false },
|
editingGroupId: { browser: true, export: false },
|
||||||
editingLinearElement: { browser: false, export: false },
|
editingLinearElement: { browser: false, export: false },
|
||||||
elementLocked: { browser: true, export: false },
|
elementLocked: { browser: true, export: false },
|
||||||
@ -116,6 +116,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
errorMessage: { browser: false, export: false },
|
errorMessage: { browser: false, export: false },
|
||||||
exportBackground: { browser: true, export: false },
|
exportBackground: { browser: true, export: false },
|
||||||
exportEmbedScene: { browser: true, export: false },
|
exportEmbedScene: { browser: true, export: false },
|
||||||
|
fileHandle: { browser: false, export: false },
|
||||||
gridSize: { browser: true, export: true },
|
gridSize: { browser: true, export: true },
|
||||||
height: { browser: false, export: false },
|
height: { browser: false, export: false },
|
||||||
isBindingEnabled: { browser: false, export: false },
|
isBindingEnabled: { browser: false, export: false },
|
||||||
@ -126,7 +127,10 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
lastPointerDownWith: { browser: true, export: false },
|
lastPointerDownWith: { browser: true, export: false },
|
||||||
multiElement: { browser: false, export: false },
|
multiElement: { browser: false, export: false },
|
||||||
name: { browser: true, export: false },
|
name: { browser: true, export: false },
|
||||||
|
offsetLeft: { browser: false, export: false },
|
||||||
|
offsetTop: { browser: false, export: false },
|
||||||
openMenu: { browser: true, export: false },
|
openMenu: { browser: true, export: false },
|
||||||
|
pasteDialog: { browser: false, export: false },
|
||||||
previousSelectedElementIds: { browser: true, export: false },
|
previousSelectedElementIds: { browser: true, export: false },
|
||||||
resizingElement: { browser: false, export: false },
|
resizingElement: { browser: false, export: false },
|
||||||
scrolledOutside: { browser: true, export: false },
|
scrolledOutside: { browser: true, export: false },
|
||||||
@ -138,16 +142,13 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
shouldAddWatermark: { browser: true, export: false },
|
shouldAddWatermark: { browser: true, export: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false },
|
||||||
showShortcutsDialog: { browser: false, export: false },
|
showShortcutsDialog: { browser: false, export: false },
|
||||||
|
showStats: { browser: true, export: false },
|
||||||
|
startBoundElement: { browser: false, export: false },
|
||||||
suggestedBindings: { browser: false, export: false },
|
suggestedBindings: { browser: false, export: false },
|
||||||
viewBackgroundColor: { browser: true, export: true },
|
viewBackgroundColor: { browser: true, export: true },
|
||||||
width: { browser: false, export: false },
|
width: { browser: false, export: false },
|
||||||
zenModeEnabled: { browser: true, export: false },
|
zenModeEnabled: { browser: true, export: false },
|
||||||
zoom: { browser: true, export: false },
|
zoom: { browser: true, export: false },
|
||||||
offsetTop: { browser: false, export: false },
|
|
||||||
offsetLeft: { browser: false, export: false },
|
|
||||||
fileHandle: { browser: false, export: false },
|
|
||||||
collaborators: { browser: false, export: false },
|
|
||||||
showStats: { browser: true, export: false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
||||||
|
431
src/charts.ts
431
src/charts.ts
@ -1,13 +1,21 @@
|
|||||||
import { EVENT_MAGIC, trackEvent } from "./analytics";
|
import { EVENT_MAGIC, trackEvent } from "./analytics";
|
||||||
import colors from "./colors";
|
import colors from "./colors";
|
||||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "./constants";
|
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, ENV } from "./constants";
|
||||||
import { newElement, newTextElement, newLinearElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
import { ExcalidrawElement } from "./element/types";
|
import { NonDeletedExcalidrawElement } from "./element/types";
|
||||||
import { randomId } from "./random";
|
import { randomId } from "./random";
|
||||||
|
|
||||||
|
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
const BAR_WIDTH = 32;
|
const BAR_WIDTH = 32;
|
||||||
const BAR_GAP = 12;
|
const BAR_GAP = 12;
|
||||||
const BAR_HEIGHT = 256;
|
const BAR_HEIGHT = 256;
|
||||||
|
const GRID_OPACITY = 50;
|
||||||
|
|
||||||
|
export const CHART_LABELS = {
|
||||||
|
bar: "labels.chartTypeBar",
|
||||||
|
line: "labels.chartTypeLine",
|
||||||
|
};
|
||||||
|
|
||||||
export interface Spreadsheet {
|
export interface Spreadsheet {
|
||||||
title: string | null;
|
title: string | null;
|
||||||
@ -139,114 +147,48 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
|||||||
return transposedResults;
|
return transposedResults;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
|
const bgColors = colors.elementBackground.slice(
|
||||||
export const renderSpreadsheet = (
|
2,
|
||||||
|
colors.elementBackground.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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: colors.elementStroke[0],
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
strokeStyle: "solid",
|
||||||
|
strokeWidth: 1,
|
||||||
|
verticalAlign: "middle",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const getChartDimentions = (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,
|
spreadsheet: Spreadsheet,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): ExcalidrawElement[] => {
|
groupId: string,
|
||||||
const values = spreadsheet.values;
|
backgroundColor: string,
|
||||||
const max = Math.max(...values);
|
): ChartElements => {
|
||||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
return (
|
||||||
const chartWidth = (BAR_WIDTH + BAR_GAP) * values.length + BAR_GAP;
|
|
||||||
const maxColors = colors.elementBackground.length;
|
|
||||||
const bgColors = colors.elementBackground.slice(2, maxColors);
|
|
||||||
|
|
||||||
// Put all the common properties here so when the whole chart is selected
|
|
||||||
// the properties dialog shows the correct selected values
|
|
||||||
const commonProps = {
|
|
||||||
backgroundColor: bgColors[Math.floor(Math.random() * bgColors.length)],
|
|
||||||
fillStyle: "hachure",
|
|
||||||
fontFamily: DEFAULT_FONT_FAMILY,
|
|
||||||
fontSize: DEFAULT_FONT_SIZE,
|
|
||||||
groupIds: [randomId()],
|
|
||||||
opacity: 100,
|
|
||||||
roughness: 1,
|
|
||||||
strokeColor: colors.elementStroke[0],
|
|
||||||
strokeSharpness: "sharp",
|
|
||||||
strokeStyle: "solid",
|
|
||||||
strokeWidth: 1,
|
|
||||||
verticalAlign: "middle",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const minYLabel = newTextElement({
|
|
||||||
...commonProps,
|
|
||||||
x: x - BAR_GAP,
|
|
||||||
y: y - BAR_GAP,
|
|
||||||
text: "0",
|
|
||||||
textAlign: "right",
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxYLabel = newTextElement({
|
|
||||||
...commonProps,
|
|
||||||
x: x - BAR_GAP,
|
|
||||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
|
||||||
text: max.toLocaleString(),
|
|
||||||
textAlign: "right",
|
|
||||||
});
|
|
||||||
|
|
||||||
const xAxisLine = newLinearElement({
|
|
||||||
type: "line",
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
width: chartWidth,
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[chartWidth, 0],
|
|
||||||
],
|
|
||||||
...commonProps,
|
|
||||||
});
|
|
||||||
|
|
||||||
const yAxisLine = newLinearElement({
|
|
||||||
type: "line",
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
height: chartHeight,
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[0, -chartHeight],
|
|
||||||
],
|
|
||||||
...commonProps,
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxValueLine = newLinearElement({
|
|
||||||
type: "line",
|
|
||||||
x,
|
|
||||||
y: y - BAR_HEIGHT - BAR_GAP,
|
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
...commonProps,
|
|
||||||
strokeStyle: "dotted",
|
|
||||||
width: chartWidth,
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[chartWidth, 0],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const bars = values.map((value, index) => {
|
|
||||||
const barHeight = (value / max) * BAR_HEIGHT;
|
|
||||||
return newElement({
|
|
||||||
...commonProps,
|
|
||||||
type: "rectangle",
|
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
|
|
||||||
y: y - barHeight - BAR_GAP,
|
|
||||||
width: BAR_WIDTH,
|
|
||||||
height: barHeight,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const xLabels =
|
|
||||||
spreadsheet.labels?.map((label, index) => {
|
spreadsheet.labels?.map((label, index) => {
|
||||||
return newTextElement({
|
return newTextElement({
|
||||||
|
groupIds: [groupId],
|
||||||
|
backgroundColor,
|
||||||
...commonProps,
|
...commonProps,
|
||||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||||
@ -257,29 +199,288 @@ export const renderSpreadsheet = (
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
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 } = getChartDimentions(spreadsheet);
|
||||||
|
const xLine = newLinearElement({
|
||||||
|
backgroundColor,
|
||||||
|
groupIds: [groupId],
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: null,
|
||||||
|
width: chartWidth,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[chartWidth, 0],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const yLine = newLinearElement({
|
||||||
|
backgroundColor,
|
||||||
|
groupIds: [groupId],
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: null,
|
||||||
|
height: chartHeight,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0, -chartHeight],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxLine = newLinearElement({
|
||||||
|
backgroundColor,
|
||||||
|
groupIds: [groupId],
|
||||||
|
...commonProps,
|
||||||
|
type: "line",
|
||||||
|
x,
|
||||||
|
y: y - BAR_HEIGHT - BAR_GAP,
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: null,
|
||||||
|
strokeStyle: "dotted",
|
||||||
|
width: chartWidth,
|
||||||
|
opacity: GRID_OPACITY,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[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 } = getChartDimentions(spreadsheet);
|
||||||
|
|
||||||
const title = spreadsheet.title
|
const title = spreadsheet.title
|
||||||
? newTextElement({
|
? newTextElement({
|
||||||
|
backgroundColor,
|
||||||
|
groupIds: [groupId],
|
||||||
...commonProps,
|
...commonProps,
|
||||||
text: spreadsheet.title,
|
text: spreadsheet.title,
|
||||||
x: x + chartWidth / 2,
|
x: x + chartWidth / 2,
|
||||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - maxYLabel.height,
|
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
trackEvent(EVENT_MAGIC, "chart", "bars", bars.length);
|
const debugRect = debug
|
||||||
|
? newElement({
|
||||||
|
backgroundColor,
|
||||||
|
groupIds: [groupId],
|
||||||
|
...commonProps,
|
||||||
|
type: "rectangle",
|
||||||
|
x,
|
||||||
|
y: y - chartHeight,
|
||||||
|
width: chartWidth,
|
||||||
|
height: chartHeight,
|
||||||
|
strokeColor: colors.elementStroke[0],
|
||||||
|
fillStyle: "solid",
|
||||||
|
opacity: 6,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
title,
|
...(debugRect ? [debugRect] : []),
|
||||||
...bars,
|
...(title ? [title] : []),
|
||||||
...xLabels,
|
...chartXLabels(spreadsheet, x, y, groupId, backgroundColor),
|
||||||
xAxisLine,
|
...chartYLabels(spreadsheet, x, y, groupId, backgroundColor),
|
||||||
yAxisLine,
|
...chartLines(spreadsheet, x, y, groupId, backgroundColor),
|
||||||
maxValueLine,
|
];
|
||||||
minYLabel,
|
};
|
||||||
maxYLabel,
|
|
||||||
].filter((element) => element !== null) as ExcalidrawElement[];
|
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,
|
||||||
|
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: null,
|
||||||
|
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,
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: null,
|
||||||
|
height: cy,
|
||||||
|
strokeStyle: "dotted",
|
||||||
|
opacity: GRID_OPACITY,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0, cy],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...chartBaseElements(
|
||||||
|
spreadsheet,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
groupId,
|
||||||
|
backgroundColor,
|
||||||
|
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||||
|
),
|
||||||
|
line,
|
||||||
|
...lines,
|
||||||
|
...dots,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderSpreadsheet = (
|
||||||
|
chartType: string,
|
||||||
|
spreadsheet: Spreadsheet,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): ChartElements => {
|
||||||
|
trackEvent(EVENT_MAGIC, "chart", chartType, spreadsheet.values.length);
|
||||||
|
if (chartType === "line") {
|
||||||
|
return chartTypeLine(spreadsheet, x, y);
|
||||||
|
}
|
||||||
|
return chartTypeBar(spreadsheet, x, y);
|
||||||
};
|
};
|
||||||
|
@ -1,181 +1,167 @@
|
|||||||
|
import { Point, simplify } from "points-on-curve";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import rough from "roughjs/bin/rough";
|
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { simplify, Point } from "points-on-curve";
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
import {
|
|
||||||
newElement,
|
|
||||||
newTextElement,
|
|
||||||
duplicateElement,
|
|
||||||
isInvisiblySmallElement,
|
|
||||||
isTextElement,
|
|
||||||
textWysiwyg,
|
|
||||||
getCommonBounds,
|
|
||||||
getCursorForResizingElement,
|
|
||||||
getPerfectElementSize,
|
|
||||||
getNormalizedDimensions,
|
|
||||||
newLinearElement,
|
|
||||||
transformElements,
|
|
||||||
getElementWithTransformHandleType,
|
|
||||||
getResizeOffsetXY,
|
|
||||||
getResizeArrowDirection,
|
|
||||||
getTransformHandleTypeFromCoords,
|
|
||||||
isNonDeletedElement,
|
|
||||||
updateTextElement,
|
|
||||||
dragSelectedElements,
|
|
||||||
getDragOffsetXY,
|
|
||||||
dragNewElement,
|
|
||||||
hitTest,
|
|
||||||
isHittingElementBoundingBoxWithoutHittingElement,
|
|
||||||
getNonDeletedElements,
|
|
||||||
} from "../element";
|
|
||||||
import {
|
|
||||||
getElementsWithinSelection,
|
|
||||||
isOverScrollBars,
|
|
||||||
getElementsAtPosition,
|
|
||||||
getElementContainingPosition,
|
|
||||||
getNormalizedZoom,
|
|
||||||
getSelectedElements,
|
|
||||||
isSomeElementSelected,
|
|
||||||
calculateScrollCenter,
|
|
||||||
} from "../scene";
|
|
||||||
import { loadFromBlob, exportCanvas } from "../data";
|
|
||||||
|
|
||||||
import { renderScene } from "../renderer";
|
|
||||||
import {
|
|
||||||
AppState,
|
|
||||||
GestureEvent,
|
|
||||||
Gesture,
|
|
||||||
ExcalidrawProps,
|
|
||||||
SceneData,
|
|
||||||
} from "../types";
|
|
||||||
import {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
NonDeleted,
|
|
||||||
ExcalidrawGenericElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
ExcalidrawBindableElement,
|
|
||||||
} from "../element/types";
|
|
||||||
|
|
||||||
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
|
||||||
|
|
||||||
import {
|
|
||||||
isWritableElement,
|
|
||||||
isInputLike,
|
|
||||||
isToolIcon,
|
|
||||||
debounce,
|
|
||||||
distance,
|
|
||||||
resetCursor,
|
|
||||||
viewportCoordsToSceneCoords,
|
|
||||||
sceneCoordsToViewportCoords,
|
|
||||||
setCursorForShape,
|
|
||||||
tupleToCoors,
|
|
||||||
ResolvablePromise,
|
|
||||||
resolvablePromise,
|
|
||||||
withBatchedUpdates,
|
|
||||||
} from "../utils";
|
|
||||||
import {
|
|
||||||
KEYS,
|
|
||||||
isArrowKey,
|
|
||||||
getResizeCenterPointKey,
|
|
||||||
getResizeWithSidesSameLengthKey,
|
|
||||||
getRotateWithDiscreteAngleKey,
|
|
||||||
CODES,
|
|
||||||
} from "../keys";
|
|
||||||
|
|
||||||
import { findShapeByKey } from "../shapes";
|
|
||||||
import { createHistory, SceneHistory } from "../history";
|
|
||||||
|
|
||||||
import ContextMenu from "./ContextMenu";
|
|
||||||
|
|
||||||
import { ActionManager } from "../actions/manager";
|
|
||||||
import "../actions";
|
import "../actions";
|
||||||
|
import { actionDeleteSelected, actionFinalize } from "../actions";
|
||||||
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||||
|
import { ActionManager } from "../actions/manager";
|
||||||
import { actions } from "../actions/register";
|
import { actions } from "../actions/register";
|
||||||
|
|
||||||
import { ActionResult } from "../actions/types";
|
import { ActionResult } from "../actions/types";
|
||||||
import { getDefaultAppState } from "../appState";
|
|
||||||
import { t, getLanguage } from "../i18n";
|
|
||||||
|
|
||||||
import {
|
|
||||||
copyToClipboard,
|
|
||||||
parseClipboard,
|
|
||||||
probablySupportsClipboardBlob,
|
|
||||||
probablySupportsClipboardWriteText,
|
|
||||||
} from "../clipboard";
|
|
||||||
import { normalizeScroll } from "../scene";
|
|
||||||
import { getCenter, getDistance } from "../gesture";
|
|
||||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CURSOR_TYPE,
|
|
||||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
|
||||||
ELEMENT_TRANSLATE_AMOUNT,
|
|
||||||
POINTER_BUTTON,
|
|
||||||
DRAGGING_THRESHOLD,
|
|
||||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
|
||||||
LINE_CONFIRM_THRESHOLD,
|
|
||||||
EVENT,
|
|
||||||
ENV,
|
|
||||||
CANVAS_ONLY_ACTIONS,
|
|
||||||
DEFAULT_VERTICAL_ALIGN,
|
|
||||||
GRID_SIZE,
|
|
||||||
MIME_TYPES,
|
|
||||||
TAP_TWICE_TIMEOUT,
|
|
||||||
TOUCH_CTX_MENU_TIMEOUT,
|
|
||||||
APP_NAME,
|
|
||||||
} from "../constants";
|
|
||||||
|
|
||||||
import LayerUI from "./LayerUI";
|
|
||||||
import { ScrollBars, SceneState } from "../scene/types";
|
|
||||||
import { mutateElement } from "../element/mutateElement";
|
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
||||||
import {
|
|
||||||
isLinearElement,
|
|
||||||
isLinearElementType,
|
|
||||||
isBindingElement,
|
|
||||||
isBindingElementType,
|
|
||||||
} from "../element/typeChecks";
|
|
||||||
import { actionFinalize, actionDeleteSelected } from "../actions";
|
|
||||||
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
|
||||||
import {
|
|
||||||
getSelectedGroupIds,
|
|
||||||
isSelectedViaGroup,
|
|
||||||
selectGroupsForSelectedElements,
|
|
||||||
isElementInGroup,
|
|
||||||
getSelectedGroupIdForElement,
|
|
||||||
getElementsInGroup,
|
|
||||||
editGroupForSelectedElement,
|
|
||||||
} from "../groups";
|
|
||||||
import { Library } from "../data/library";
|
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import {
|
|
||||||
getHoveredElementForBinding,
|
|
||||||
maybeBindLinearElement,
|
|
||||||
getEligibleElementsForBinding,
|
|
||||||
bindOrUnbindSelectedElements,
|
|
||||||
unbindLinearElements,
|
|
||||||
fixBindingsAfterDuplication,
|
|
||||||
fixBindingsAfterDeletion,
|
|
||||||
isLinearElementSimpleAndAlreadyBound,
|
|
||||||
isBindingEnabled,
|
|
||||||
updateBoundElements,
|
|
||||||
shouldEnableBindingForPointerEvent,
|
|
||||||
} from "../element/binding";
|
|
||||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
|
||||||
import { deepCopyElement } from "../element/newElement";
|
|
||||||
import { renderSpreadsheet } from "../charts";
|
|
||||||
import { isValidLibrary } from "../data/json";
|
|
||||||
import { getNewZoom } from "../scene/zoom";
|
|
||||||
import { restore } from "../data/restore";
|
|
||||||
import {
|
import {
|
||||||
EVENT_DIALOG,
|
EVENT_DIALOG,
|
||||||
EVENT_LIBRARY,
|
EVENT_LIBRARY,
|
||||||
EVENT_SHAPE,
|
EVENT_SHAPE,
|
||||||
trackEvent,
|
trackEvent,
|
||||||
} from "../analytics";
|
} from "../analytics";
|
||||||
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
parseClipboard,
|
||||||
|
probablySupportsClipboardBlob,
|
||||||
|
probablySupportsClipboardWriteText,
|
||||||
|
} from "../clipboard";
|
||||||
|
import {
|
||||||
|
APP_NAME,
|
||||||
|
CANVAS_ONLY_ACTIONS,
|
||||||
|
CURSOR_TYPE,
|
||||||
|
DEFAULT_VERTICAL_ALIGN,
|
||||||
|
DRAGGING_THRESHOLD,
|
||||||
|
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||||
|
ELEMENT_TRANSLATE_AMOUNT,
|
||||||
|
ENV,
|
||||||
|
EVENT,
|
||||||
|
GRID_SIZE,
|
||||||
|
LINE_CONFIRM_THRESHOLD,
|
||||||
|
MIME_TYPES,
|
||||||
|
POINTER_BUTTON,
|
||||||
|
TAP_TWICE_TIMEOUT,
|
||||||
|
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||||
|
TOUCH_CTX_MENU_TIMEOUT,
|
||||||
|
} from "../constants";
|
||||||
|
import { exportCanvas, loadFromBlob } from "../data";
|
||||||
|
import { isValidLibrary } from "../data/json";
|
||||||
|
import { Library } from "../data/library";
|
||||||
|
import { restore } from "../data/restore";
|
||||||
|
import {
|
||||||
|
dragNewElement,
|
||||||
|
dragSelectedElements,
|
||||||
|
duplicateElement,
|
||||||
|
getCommonBounds,
|
||||||
|
getCursorForResizingElement,
|
||||||
|
getDragOffsetXY,
|
||||||
|
getElementWithTransformHandleType,
|
||||||
|
getNonDeletedElements,
|
||||||
|
getNormalizedDimensions,
|
||||||
|
getPerfectElementSize,
|
||||||
|
getResizeArrowDirection,
|
||||||
|
getResizeOffsetXY,
|
||||||
|
getTransformHandleTypeFromCoords,
|
||||||
|
hitTest,
|
||||||
|
isHittingElementBoundingBoxWithoutHittingElement,
|
||||||
|
isInvisiblySmallElement,
|
||||||
|
isNonDeletedElement,
|
||||||
|
isTextElement,
|
||||||
|
newElement,
|
||||||
|
newLinearElement,
|
||||||
|
newTextElement,
|
||||||
|
textWysiwyg,
|
||||||
|
transformElements,
|
||||||
|
updateTextElement,
|
||||||
|
} from "../element";
|
||||||
|
import {
|
||||||
|
bindOrUnbindSelectedElements,
|
||||||
|
fixBindingsAfterDeletion,
|
||||||
|
fixBindingsAfterDuplication,
|
||||||
|
getEligibleElementsForBinding,
|
||||||
|
getHoveredElementForBinding,
|
||||||
|
isBindingEnabled,
|
||||||
|
isLinearElementSimpleAndAlreadyBound,
|
||||||
|
maybeBindLinearElement,
|
||||||
|
shouldEnableBindingForPointerEvent,
|
||||||
|
unbindLinearElements,
|
||||||
|
updateBoundElements,
|
||||||
|
} from "../element/binding";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { mutateElement } from "../element/mutateElement";
|
||||||
|
import { deepCopyElement } from "../element/newElement";
|
||||||
|
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||||
|
import {
|
||||||
|
isBindingElement,
|
||||||
|
isBindingElementType,
|
||||||
|
isLinearElement,
|
||||||
|
isLinearElementType,
|
||||||
|
} from "../element/typeChecks";
|
||||||
|
import {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawGenericElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "../element/types";
|
||||||
|
import { getCenter, getDistance } from "../gesture";
|
||||||
|
import {
|
||||||
|
editGroupForSelectedElement,
|
||||||
|
getElementsInGroup,
|
||||||
|
getSelectedGroupIdForElement,
|
||||||
|
getSelectedGroupIds,
|
||||||
|
isElementInGroup,
|
||||||
|
isSelectedViaGroup,
|
||||||
|
selectGroupsForSelectedElements,
|
||||||
|
} from "../groups";
|
||||||
|
import { createHistory, SceneHistory } from "../history";
|
||||||
|
import { getLanguage, t } from "../i18n";
|
||||||
|
import {
|
||||||
|
CODES,
|
||||||
|
getResizeCenterPointKey,
|
||||||
|
getResizeWithSidesSameLengthKey,
|
||||||
|
getRotateWithDiscreteAngleKey,
|
||||||
|
isArrowKey,
|
||||||
|
KEYS,
|
||||||
|
} from "../keys";
|
||||||
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||||
|
import { renderScene } from "../renderer";
|
||||||
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
|
import {
|
||||||
|
calculateScrollCenter,
|
||||||
|
getElementContainingPosition,
|
||||||
|
getElementsAtPosition,
|
||||||
|
getElementsWithinSelection,
|
||||||
|
getNormalizedZoom,
|
||||||
|
getSelectedElements,
|
||||||
|
isOverScrollBars,
|
||||||
|
isSomeElementSelected,
|
||||||
|
normalizeScroll,
|
||||||
|
} from "../scene";
|
||||||
|
import Scene from "../scene/Scene";
|
||||||
|
import { SceneState, ScrollBars } from "../scene/types";
|
||||||
|
import { getNewZoom } from "../scene/zoom";
|
||||||
|
import { findShapeByKey } from "../shapes";
|
||||||
|
import {
|
||||||
|
AppState,
|
||||||
|
ExcalidrawProps,
|
||||||
|
Gesture,
|
||||||
|
GestureEvent,
|
||||||
|
SceneData,
|
||||||
|
} from "../types";
|
||||||
|
import {
|
||||||
|
debounce,
|
||||||
|
distance,
|
||||||
|
isInputLike,
|
||||||
|
isToolIcon,
|
||||||
|
isWritableElement,
|
||||||
|
resetCursor,
|
||||||
|
ResolvablePromise,
|
||||||
|
resolvablePromise,
|
||||||
|
sceneCoordsToViewportCoords,
|
||||||
|
setCursorForShape,
|
||||||
|
tupleToCoors,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
withBatchedUpdates,
|
||||||
|
} from "../utils";
|
||||||
|
import ContextMenu from "./ContextMenu";
|
||||||
|
import LayerUI from "./LayerUI";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
|
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
@ -374,7 +360,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
elements={this.scene.getElements()}
|
elements={this.scene.getElements()}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
onLockToggle={this.toggleLock}
|
onLockToggle={this.toggleLock}
|
||||||
onInsertShape={(elements) =>
|
onInsertElements={(elements) =>
|
||||||
this.addElementsFromPasteOrLibrary(
|
this.addElementsFromPasteOrLibrary(
|
||||||
elements,
|
elements,
|
||||||
DEFAULT_PASTE_X,
|
DEFAULT_PASTE_X,
|
||||||
@ -1004,9 +990,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (data.errorMessage) {
|
if (data.errorMessage) {
|
||||||
this.setState({ errorMessage: data.errorMessage });
|
this.setState({ errorMessage: data.errorMessage });
|
||||||
} else if (data.spreadsheet) {
|
} else if (data.spreadsheet) {
|
||||||
this.addElementsFromPasteOrLibrary(
|
this.setState({
|
||||||
renderSpreadsheet(data.spreadsheet, cursorX, cursorY),
|
pasteDialog: {
|
||||||
);
|
data: data.spreadsheet,
|
||||||
|
shown: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else if (data.elements) {
|
} else if (data.elements) {
|
||||||
this.addElementsFromPasteOrLibrary(data.elements);
|
this.addElementsFromPasteOrLibrary(data.elements);
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Modal } from "./Modal";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { Island } from "./Island";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { back, close } from "./icons";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
import "./Dialog.scss";
|
import "./Dialog.scss";
|
||||||
|
import { back, close } from "./icons";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
import { Modal } from "./Modal";
|
||||||
|
|
||||||
const useRefState = <T,>() => {
|
const useRefState = <T,>() => {
|
||||||
const [refValue, setRefValue] = useState<T | null>(null);
|
const [refValue, setRefValue] = useState<T | null>(null);
|
||||||
@ -23,6 +22,7 @@ export const Dialog = (props: {
|
|||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
|
autofocus?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export const Dialog = (props: {
|
|||||||
|
|
||||||
const focusableElements = queryFocusableElements(islandNode);
|
const focusableElements = queryFocusableElements(islandNode);
|
||||||
|
|
||||||
if (focusableElements.length > 0) {
|
if (focusableElements.length > 0 && props.autofocus !== false) {
|
||||||
// If there's an element other than close, focus it.
|
// If there's an element other than close, focus it.
|
||||||
(focusableElements[1] || focusableElements[0]).focus();
|
(focusableElements[1] || focusableElements[0]).focus();
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ export const Dialog = (props: {
|
|||||||
islandNode.addEventListener("keydown", handleKeyDown);
|
islandNode.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [islandNode]);
|
}, [islandNode, props.autofocus]);
|
||||||
|
|
||||||
const queryFocusableElements = (node: HTMLElement) => {
|
const queryFocusableElements = (node: HTMLElement) => {
|
||||||
const focusableElements = node.querySelectorAll<HTMLElement>(
|
const focusableElements = node.querySelectorAll<HTMLElement>(
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
EVENT_LIBRARY,
|
EVENT_LIBRARY,
|
||||||
trackEvent,
|
trackEvent,
|
||||||
} from "../analytics";
|
} from "../analytics";
|
||||||
|
import { PasteChartDialog } from "./PasteChartDialog";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -60,7 +61,7 @@ interface LayerUIProps {
|
|||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onCollabButtonClick?: () => void;
|
onCollabButtonClick?: () => void;
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onInsertShape: (elements: LibraryItem) => void;
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||||
zenModeEnabled: boolean;
|
zenModeEnabled: boolean;
|
||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
lng: string;
|
lng: string;
|
||||||
@ -318,7 +319,7 @@ const LayerUI = ({
|
|||||||
elements,
|
elements,
|
||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onInsertShape,
|
onInsertElements,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
toggleZenMode,
|
toggleZenMode,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
@ -456,7 +457,7 @@ const LayerUI = ({
|
|||||||
<LibraryMenu
|
<LibraryMenu
|
||||||
pendingElements={getSelectedElements(elements, appState)}
|
pendingElements={getSelectedElements(elements, appState)}
|
||||||
onClickOutside={closeLibrary}
|
onClickOutside={closeLibrary}
|
||||||
onInsertShape={onInsertShape}
|
onInsertShape={onInsertElements}
|
||||||
onAddToLibrary={deselectItems}
|
onAddToLibrary={deselectItems}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
/>
|
/>
|
||||||
@ -592,21 +593,8 @@ const LayerUI = ({
|
|||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|
||||||
return isMobile ? (
|
const dialogs = (
|
||||||
<MobileMenu
|
<>
|
||||||
appState={appState}
|
|
||||||
elements={elements}
|
|
||||||
actionManager={actionManager}
|
|
||||||
libraryMenu={libraryMenu}
|
|
||||||
exportButton={renderExportDialog()}
|
|
||||||
setAppState={setAppState}
|
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
|
||||||
onLockToggle={onLockToggle}
|
|
||||||
canvas={canvas}
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="layer-ui__wrapper">
|
|
||||||
{appState.isLoading && <LoadingMessage />}
|
{appState.isLoading && <LoadingMessage />}
|
||||||
{appState.errorMessage && (
|
{appState.errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
@ -619,6 +607,40 @@ const LayerUI = ({
|
|||||||
onClose={() => setAppState({ showShortcutsDialog: false })}
|
onClose={() => setAppState({ showShortcutsDialog: false })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{appState.pasteDialog.shown && (
|
||||||
|
<PasteChartDialog
|
||||||
|
setAppState={setAppState}
|
||||||
|
appState={appState}
|
||||||
|
onInsertChart={onInsertElements}
|
||||||
|
onClose={() =>
|
||||||
|
setAppState({
|
||||||
|
pasteDialog: { shown: false, data: null },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return isMobile ? (
|
||||||
|
<>
|
||||||
|
{dialogs}
|
||||||
|
<MobileMenu
|
||||||
|
appState={appState}
|
||||||
|
elements={elements}
|
||||||
|
actionManager={actionManager}
|
||||||
|
libraryMenu={libraryMenu}
|
||||||
|
exportButton={renderExportDialog()}
|
||||||
|
setAppState={setAppState}
|
||||||
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
|
onLockToggle={onLockToggle}
|
||||||
|
canvas={canvas}
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="layer-ui__wrapper">
|
||||||
|
{dialogs}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
{renderBottomAppMenu()}
|
{renderBottomAppMenu()}
|
||||||
{
|
{
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { exportToSvg } from "../scene/export";
|
import oc from "open-color";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { close } from "../components/icons";
|
import { close } from "../components/icons";
|
||||||
|
import { MIME_TYPES } from "../constants";
|
||||||
import "./LibraryUnit.scss";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
|
import { exportToSvg } from "../scene/export";
|
||||||
import { LibraryItem } from "../types";
|
import { LibraryItem } from "../types";
|
||||||
import { MIME_TYPES } from "../constants";
|
import "./LibraryUnit.scss";
|
||||||
|
|
||||||
// fa-plus
|
// fa-plus
|
||||||
const PLUS_ICON = (
|
const PLUS_ICON = (
|
||||||
@ -38,7 +38,7 @@ export const LibraryUnit = ({
|
|||||||
}
|
}
|
||||||
const svg = exportToSvg(elementsToRender, {
|
const svg = exportToSvg(elementsToRender, {
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: "#fff",
|
viewBackgroundColor: oc.white,
|
||||||
shouldAddWatermark: false,
|
shouldAddWatermark: false,
|
||||||
});
|
});
|
||||||
for (const child of ref.current!.children) {
|
for (const child of ref.current!.children) {
|
||||||
|
@ -15,7 +15,6 @@ import { Section } from "./Section";
|
|||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||||
import { LockIcon } from "./LockIcon";
|
import { LockIcon } from "./LockIcon";
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import { EVENT_ACTION, trackEvent } from "../analytics";
|
import { EVENT_ACTION, trackEvent } from "../analytics";
|
||||||
@ -46,7 +45,6 @@ export const MobileMenu = ({
|
|||||||
isCollaborating,
|
isCollaborating,
|
||||||
}: MobileMenuProps) => (
|
}: MobileMenuProps) => (
|
||||||
<>
|
<>
|
||||||
{appState.isLoading && <LoadingMessage />}
|
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading) => (
|
{(heading) => (
|
||||||
|
46
src/components/PasteChartDialog.scss
Normal file
46
src/components/PasteChartDialog.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@import "../css/_variables";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.PasteChartDialog {
|
||||||
|
@media #{$media-query} {
|
||||||
|
.Island {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
@media #{$media-query} {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ChartPreview {
|
||||||
|
margin: 8px;
|
||||||
|
text-align: center;
|
||||||
|
width: 192px;
|
||||||
|
height: 128px;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 1px;
|
||||||
|
border: 1px solid $oc-gray-4;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
max-height: 120px;
|
||||||
|
max-width: 186px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid $oc-blue-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
src/components/PasteChartDialog.tsx
Normal file
121
src/components/PasteChartDialog.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import oc from "open-color";
|
||||||
|
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
|
||||||
|
import { ChartType } from "../element/types";
|
||||||
|
import { exportToSvg } from "../scene/export";
|
||||||
|
import { AppState, LibraryItem } from "../types";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import "./PasteChartDialog.scss";
|
||||||
|
|
||||||
|
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||||
|
|
||||||
|
const ChartPreviewBtn = (props: {
|
||||||
|
spreadsheet: Spreadsheet | null;
|
||||||
|
chartType: ChartType;
|
||||||
|
selected: boolean;
|
||||||
|
onClick: OnInsertChart;
|
||||||
|
}) => {
|
||||||
|
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [chartElements, setChartElements] = useState<ChartElements | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!props.spreadsheet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = renderSpreadsheet(
|
||||||
|
props.chartType,
|
||||||
|
props.spreadsheet,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
setChartElements(elements);
|
||||||
|
|
||||||
|
const svg = exportToSvg(elements, {
|
||||||
|
exportBackground: false,
|
||||||
|
viewBackgroundColor: oc.white,
|
||||||
|
shouldAddWatermark: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
|
if (props.selected) {
|
||||||
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
previewNode.removeChild(svg);
|
||||||
|
};
|
||||||
|
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="ChartPreview"
|
||||||
|
onClick={() => {
|
||||||
|
if (chartElements) {
|
||||||
|
props.onClick(props.chartType, chartElements);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={previewRef} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PasteChartDialog = ({
|
||||||
|
setAppState,
|
||||||
|
appState,
|
||||||
|
onClose,
|
||||||
|
onInsertChart,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
onClose: () => void;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
onInsertChart: (elements: LibraryItem) => void;
|
||||||
|
}) => {
|
||||||
|
const handleClose = React.useCallback(() => {
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||||
|
onInsertChart(elements);
|
||||||
|
setAppState({
|
||||||
|
currentChartType: chartType,
|
||||||
|
pasteDialog: {
|
||||||
|
shown: false,
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
maxWidth={500}
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
title={"Paste chart"}
|
||||||
|
className={"PasteChartDialog"}
|
||||||
|
autofocus={false}
|
||||||
|
>
|
||||||
|
<div className={"container"}>
|
||||||
|
<ChartPreviewBtn
|
||||||
|
chartType="bar"
|
||||||
|
spreadsheet={appState.pasteDialog.data}
|
||||||
|
selected={appState.currentChartType === "bar"}
|
||||||
|
onClick={handleChartClick}
|
||||||
|
/>
|
||||||
|
<ChartPreviewBtn
|
||||||
|
chartType="line"
|
||||||
|
spreadsheet={appState.pasteDialog.data}
|
||||||
|
selected={appState.currentChartType === "line"}
|
||||||
|
onClick={handleChartClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -85,7 +85,6 @@ export const Stats = (props: {
|
|||||||
<td>{t("stats.total")}</td>
|
<td>{t("stats.total")}</td>
|
||||||
<td>{nFormatter(storageSizes.total, 1)}</td>
|
<td>{nFormatter(storageSizes.total, 1)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{selectedElements.length === 1 && (
|
{selectedElements.length === 1 && (
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2}>{t("stats.element")}</th>
|
<th colSpan={2}>{t("stats.element")}</th>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { FONT_FAMILY } from "../constants";
|
import { FONT_FAMILY } from "../constants";
|
||||||
|
|
||||||
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||||
export type FontFamily = keyof typeof FONT_FAMILY;
|
export type FontFamily = keyof typeof FONT_FAMILY;
|
||||||
export type FontString = string & { _brand: "fontString" };
|
export type FontString = string & { _brand: "fontString" };
|
||||||
|
@ -90,7 +90,9 @@
|
|||||||
"centerVertically": "Center vertically",
|
"centerVertically": "Center vertically",
|
||||||
"centerHorizontally": "Center horizontally",
|
"centerHorizontally": "Center horizontally",
|
||||||
"distributeHorizontally": "Distribute horizontally",
|
"distributeHorizontally": "Distribute horizontally",
|
||||||
"distributeVertically": "Distribute vertically"
|
"distributeVertically": "Distribute vertically",
|
||||||
|
"chartTypeBar": "Bar",
|
||||||
|
"chartTypeLine": "Line"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
@ -222,6 +224,8 @@
|
|||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "Angle",
|
"angle": "Angle",
|
||||||
|
"charts": "Charts",
|
||||||
|
"current": "Current",
|
||||||
"element": "Element",
|
"element": "Element",
|
||||||
"elements": "Elements",
|
"elements": "Elements",
|
||||||
"height": "Height",
|
"height": "Height",
|
||||||
|
File diff suppressed because it is too large
Load Diff
12
src/types.ts
12
src/types.ts
@ -9,6 +9,7 @@ import {
|
|||||||
GroupId,
|
GroupId,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ChartType,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
@ -17,6 +18,7 @@ import { SuggestedBinding } from "./element/binding";
|
|||||||
import { ImportedDataState } from "./data/types";
|
import { ImportedDataState } from "./data/types";
|
||||||
import { ExcalidrawImperativeAPI } from "./components/App";
|
import { ExcalidrawImperativeAPI } from "./components/App";
|
||||||
import type { ResolvablePromise } from "./utils";
|
import type { ResolvablePromise } from "./utils";
|
||||||
|
import { Spreadsheet } from "./charts";
|
||||||
|
|
||||||
export type FlooredNumber = number & { _brand: "FlooredNumber" };
|
export type FlooredNumber = number & { _brand: "FlooredNumber" };
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
@ -97,6 +99,16 @@ export type AppState = {
|
|||||||
fileHandle: import("browser-nativefs").FileSystemHandle | null;
|
fileHandle: import("browser-nativefs").FileSystemHandle | null;
|
||||||
collaborators: Map<string, Collaborator>;
|
collaborators: Map<string, Collaborator>;
|
||||||
showStats: boolean;
|
showStats: boolean;
|
||||||
|
currentChartType: ChartType;
|
||||||
|
pasteDialog:
|
||||||
|
| {
|
||||||
|
shown: false;
|
||||||
|
data: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
shown: true;
|
||||||
|
data: Spreadsheet;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
|
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
|
||||||
|
Loading…
x
Reference in New Issue
Block a user