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:
Lipis 2020-12-27 18:26:30 +02:00 committed by GitHub
parent c1e2146d78
commit 022f349dc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1122 additions and 393 deletions

View File

@ -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,
}; };

View File

@ -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">(

View File

@ -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);
}; };

View File

@ -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) {

View File

@ -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>(

View File

@ -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()}
{ {

View File

@ -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) {

View File

@ -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) => (

View 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;
}
}
}
}

View 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>
);
};

View File

@ -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>

View File

@ -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" };

View File

@ -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

View File

@ -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" };