diff --git a/src/ActiveCell.test.tsx b/src/ActiveCell.test.tsx index a424d704..9eb6fac6 100644 --- a/src/ActiveCell.test.tsx +++ b/src/ActiveCell.test.tsx @@ -17,14 +17,10 @@ const DISPATCH_MOCK = jest.fn(); const STATE_WITH_ACTIVE: Types.StoreState = { ...INITIAL_STATE, active: Point.ORIGIN, - rowDimensions: { - 0: { + dimensions: { + "0:0": { height: 42, top: 42, - }, - }, - columnDimensions: { - 0: { width: 42, left: 42, }, diff --git a/src/ActiveCell.tsx b/src/ActiveCell.tsx index d077c143..dc47e0b2 100644 --- a/src/ActiveCell.tsx +++ b/src/ActiveCell.tsx @@ -36,9 +36,7 @@ const ActiveCell: React.FC = (props) => { state.active ? Matrix.get(state.active, state.model.data) : undefined ); const dimensions = useSelector((state) => - active - ? getCellDimensions(active, state.rowDimensions, state.columnDimensions) - : undefined + active ? getCellDimensions(active, state.dimensions) : undefined ); const hidden = React.useMemo( () => !active || !dimensions, diff --git a/src/Copied.tsx b/src/Copied.tsx index b553c671..4b0000cd 100644 --- a/src/Copied.tsx +++ b/src/Copied.tsx @@ -6,9 +6,7 @@ import useSelector from "./use-selector"; const Copied: React.FC = () => { const range = useSelector((state) => state.copied); const dimensions = useSelector( - (state) => - range && - getRangeDimensions(state.rowDimensions, state.columnDimensions, range) + (state) => range && getRangeDimensions(state.dimensions, range) ); const hidden = range === null; diff --git a/src/Selected.tsx b/src/Selected.tsx index 3e1a2df1..ec466aad 100644 --- a/src/Selected.tsx +++ b/src/Selected.tsx @@ -8,12 +8,7 @@ const Selected: React.FC = () => { const dimensions = useSelector( (state) => selected && - getSelectedDimensions( - state.rowDimensions, - state.columnDimensions, - state.model.data, - state.selected - ) + getSelectedDimensions(state.dimensions, state.model.data, state.selected) ); const dragging = useSelector((state) => state.dragging); const hidden = useSelector( diff --git a/src/reducer.test.ts b/src/reducer.test.ts index 1963e08e..b7bef3c8 100644 --- a/src/reducer.test.ts +++ b/src/reducer.test.ts @@ -181,14 +181,10 @@ describe("reducer", () => { Actions.setCellDimensions(Point.ORIGIN, EXAMPLE_DIMENSIONS), { ...INITIAL_STATE, - rowDimensions: { - [Point.ORIGIN.row]: { + dimensions: { + [`${Point.ORIGIN.row}:${Point.ORIGIN.column}`]: { height: EXAMPLE_DIMENSIONS.height, top: EXAMPLE_DIMENSIONS.top, - }, - }, - columnDimensions: { - [Point.ORIGIN.column]: { width: EXAMPLE_DIMENSIONS.width, left: EXAMPLE_DIMENSIONS.left, }, diff --git a/src/reducer.ts b/src/reducer.ts index 2811d687..a652aac1 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -17,8 +17,7 @@ import { Model, updateCellValue, createFormulaParser } from "./engine"; export const INITIAL_STATE: Types.StoreState = { active: null, mode: "view", - rowDimensions: {}, - columnDimensions: {}, + dimensions: {}, lastChanged: null, hasPasted: false, cut: false, @@ -136,27 +135,26 @@ export default function reducer( } case Actions.SET_CELL_DIMENSIONS: { const { point, dimensions } = action.payload; - const prevRowDimensions = state.rowDimensions[point.row]; - const prevColumnDimensions = state.columnDimensions[point.column]; + const prevDimensions = state.dimensions[`${point.row}:${point.column}`]; if ( - prevRowDimensions && - prevColumnDimensions && - prevRowDimensions.top === dimensions.top && - prevRowDimensions.height === dimensions.height && - prevColumnDimensions.left === dimensions.left && - prevColumnDimensions.width === dimensions.width + prevDimensions && + prevDimensions.top === dimensions.top && + prevDimensions.height === dimensions.height && + prevDimensions.left === dimensions.left && + prevDimensions.width === dimensions.width ) { return state; } return { ...state, - rowDimensions: { - ...state.rowDimensions, - [point.row]: { top: dimensions.top, height: dimensions.height }, - }, - columnDimensions: { - ...state.columnDimensions, - [point.column]: { left: dimensions.left, width: dimensions.width }, + dimensions: { + ...state.dimensions, + [`${point.row}:${point.column}`]: { + top: dimensions.top, + height: dimensions.height, + left: dimensions.left, + width: dimensions.width, + }, }, }; } diff --git a/src/stories/CustomCell.tsx b/src/stories/CustomCell.tsx index 7d5af808..358ea2be 100644 --- a/src/stories/CustomCell.tsx +++ b/src/stories/CustomCell.tsx @@ -1,15 +1,14 @@ -/** - * Example custom cell component - */ - -import * as React from "react"; +import React, { + createRef, + useCallback, + useEffect, + useState, + MouseEvent, +} from "react"; import classnames from "classnames"; import { CellComponent } from ".."; -const HEIGHT = 30; -const WIDTH = 96; - -const CustomCell: CellComponent = ({ +export const CustomCell: CellComponent = ({ column, row, setCellDimensions, @@ -23,28 +22,88 @@ const CustomCell: CellComponent = ({ DataViewer, setCellData, }) => { - const rootRef = React.createRef(); + const rootRef = createRef(); + const [dimension, setDimension] = useState({ + width: 0, + height: 0, + top: 0, + left: 0, + }); - React.useEffect(() => { - setCellDimensions( - { row, column }, - { - height: HEIGHT, - width: WIDTH, - left: WIDTH * (column + 1), - top: HEIGHT * (row + 1), - } - ); - }, [setCellDimensions, column, row]); + const getColumnRangeSplit = (str: string) => { + if (!str) return {}; + const regex = /([A-Za-z]+)(\d+):([A-Za-z]+)(\d+)/; + const matches = str.match(regex); + if (matches) { + const [, startColumn, startRow, endColumn, endRow] = matches; + return { startColumn, startRow, endColumn, endRow }; + } else { + console.log("No match found."); + return {}; + } + }; + const getColumnRange = (columnRange: string) => { + if (!columnRange) { + return { colspan: undefined, rowspan: undefined }; + } + const { startColumn, startRow, endColumn, endRow } = + getColumnRangeSplit(columnRange); + const colspan = + (endColumn?.charCodeAt(0) ?? 0) - (startColumn?.charCodeAt(0) ?? 0); + const rowspan = parseInt(endRow ?? "") - parseInt(startRow ?? ""); + + return { + colspan: colspan === 0 ? undefined : colspan, + rowspan: rowspan === 0 ? undefined : rowspan, + }; + }; + + useEffect(() => { + const observer = new ResizeObserver((entries) => { + console.log("resize", { + width: entries[0].target.getBoundingClientRect().width, + height: entries[0].target.getBoundingClientRect().height, + top: entries[0].target.getBoundingClientRect().top - 16, + left: entries[0].target.getBoundingClientRect().left - 16, + }); + setDimension({ + width: entries[0].target.getBoundingClientRect().width, + height: entries[0].target.getBoundingClientRect().height, + top: entries[0].target.getBoundingClientRect().top - 16, + left: entries[0].target.getBoundingClientRect().left - 16, + }); + }); + if (rootRef.current) { + observer.observe(rootRef.current); + } + return () => { + rootRef.current && observer.unobserve(rootRef.current); + }; + }, []); + + useEffect(() => { + if (rootRef.current) { + setDimension({ + width: rootRef.current.getBoundingClientRect().width, + height: rootRef.current.getBoundingClientRect().height, + top: rootRef.current.getBoundingClientRect().top - 16, + left: rootRef.current.getBoundingClientRect().left - 16, + }); + } + }, []); + + useEffect(() => { + setCellDimensions({ row, column }, dimension); + }, [setCellDimensions, dimension, column, row]); - React.useEffect(() => { + useEffect(() => { if (rootRef.current && active && mode === "view") { rootRef.current.focus(); } }, [rootRef, active, mode]); - const handleMouseDown = React.useCallback( - (event) => { + const handleMouseDown = useCallback( + (event: MouseEvent) => { if (mode === "view") { if (event.shiftKey) { select({ row, column }); @@ -54,10 +113,10 @@ const CustomCell: CellComponent = ({ activate({ row, column }); } }, - [select, activate, column, mode, row] + [select, activate, column, mode, row, dimension] ); - const handleMouseOver = React.useCallback(() => { + const handleMouseOver = useCallback(() => { if (dragging) { select({ row, column }); } @@ -81,6 +140,8 @@ const CustomCell: CellComponent = ({ tabIndex={0} onMouseOver={handleMouseOver} onMouseDown={handleMouseDown} + colSpan={getColumnRange(data?.mergeRange ?? "").colspan} + rowSpan={getColumnRange(data?.mergeRange ?? "").rowspan} > { + const rootRef = createRef(); + const [dimension, setDimension] = useState({ + width: 0, + height: 0, + top: 0, + left: 0, + }); + + const getColumnRangeSplit = (str: string) => { + if (!str) return {}; + const regex = /([A-Za-z]+)(\d+):([A-Za-z]+)(\d+)/; + const matches = str.match(regex); + if (matches) { + const [, startColumn, startRow, endColumn, endRow] = matches; + return { startColumn, startRow, endColumn, endRow }; + } else { + console.log("No match found."); + return {}; + } + }; + const getColumnRange = (columnRange: string) => { + if (!columnRange) { + return { colspan: undefined, rowspan: undefined }; + } + const { startColumn, startRow, endColumn, endRow } = + getColumnRangeSplit(columnRange); + const colspan = + (endColumn?.charCodeAt(0) ?? 0) - (startColumn?.charCodeAt(0) ?? 0); + const rowspan = parseInt(endRow ?? "") - parseInt(startRow ?? ""); + + return { + colspan: colspan === 0 ? undefined : colspan + 1, + rowspan: rowspan === 0 ? undefined : rowspan + 1, + }; + }; + + useEffect(() => { + const observer = new ResizeObserver((entries) => { + console.log("resize", { + width: entries[0].target.getBoundingClientRect().width, + height: entries[0].target.getBoundingClientRect().height, + top: entries[0].target.getBoundingClientRect().top - 16, + left: entries[0].target.getBoundingClientRect().left - 16, + }); + setDimension({ + width: entries[0].target.getBoundingClientRect().width, + height: entries[0].target.getBoundingClientRect().height, + top: entries[0].target.getBoundingClientRect().top - 16, + left: entries[0].target.getBoundingClientRect().left - 16, + }); + }); + if (rootRef.current) { + observer.observe(rootRef.current); + } + return () => { + rootRef.current && observer.unobserve(rootRef.current); + }; + }, []); + + useEffect(() => { + if (rootRef.current) { + setDimension({ + width: rootRef.current.getBoundingClientRect().width, + height: rootRef.current.getBoundingClientRect().height, + top: rootRef.current.getBoundingClientRect().top - 16, + left: rootRef.current.getBoundingClientRect().left - 16, + }); + } + }, []); + + useEffect(() => { + setCellDimensions({ row, column }, dimension); + }, [setCellDimensions, dimension, column, row]); + + useEffect(() => { + if (rootRef.current && active && mode === "view") { + rootRef.current.focus(); + } + }, [rootRef, active, mode]); + + const handleMouseDown = useCallback( + (event: MouseEvent) => { + if (mode === "view") { + if (event.shiftKey) { + select({ row, column }); + return; + } + + activate({ row, column }); + } + }, + [select, activate, column, mode, row, dimension] + ); + + const handleMouseOver = useCallback(() => { + if (dragging) { + select({ row, column }); + } + }, [dragging, select, column, row]); + + if (data && data.DataViewer) { + ({ DataViewer, ...data } = data); + } + + return ( + + + + ); +}; + +export default CustomMergeCell; diff --git a/src/stories/Spreadsheet.stories.tsx b/src/stories/Spreadsheet.stories.tsx index fc17249f..1f869c63 100644 --- a/src/stories/Spreadsheet.stories.tsx +++ b/src/stories/Spreadsheet.stories.tsx @@ -14,6 +14,7 @@ import { import * as Matrix from "../matrix"; import { AsyncCellDataEditor, AsyncCellDataViewer } from "./AsyncCellData"; import CustomCell from "./CustomCell"; +import CustomMergeCell from "./CustomMergeCell"; import { RangeEdit, RangeView } from "./RangeDataComponents"; import { SelectEdit, SelectView } from "./SelectDataComponents"; import { CustomCornerIndicator } from "./CustomCornerIndicator"; @@ -24,6 +25,351 @@ type NumberCell = CellBase; const INITIAL_ROWS = 6; const INITIAL_COLUMNS = 4; const EMPTY_DATA = createEmptyMatrix(INITIAL_ROWS, INITIAL_COLUMNS); +const TEST_MERGE_DATA = [ + [ + { + address: "A1", + value: null, + isMerged: false, + style: {}, + }, + { + address: "B1", + value: null, + isMerged: false, + style: {}, + }, + { + address: "C1", + value: "Merge C1:E1", + isMerged: true, + mergeRange: "C1:E1", + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "D1", + value: "Merge C1:E1", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "E1", + value: "Merge C1:E1", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + ], + [ + { + address: "A2", + value: "Merge A2:A4", + isMerged: true, + mergeRange: "A2:A4", + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "B2", + value: "Merge B2:C2", + isMerged: true, + mergeRange: "B2:C2", + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "C2", + value: "Merge B2:C2", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + ], + [ + { + address: "A3", + value: "Merge A2:A4", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "B3", + value: null, + isMerged: false, + style: {}, + }, + { + address: "C3", + value: null, + isMerged: false, + style: {}, + }, + { + address: "D3", + value: "Merge D1:G1", + isMerged: true, + mergeRange: "D3:G3", + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "E3", + value: "Merge D1:G1", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "F3", + value: "Merge D1:G1", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "G3", + value: "Merge D1:G1", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + ], + [ + { + address: "A4", + value: "Merge A2:A4", + isMerged: true, + style: { + font: { + size: 12, + color: { + theme: 1, + }, + name: "Calibri", + family: 2, + scheme: "minor", + }, + border: {}, + fill: { + type: "pattern", + pattern: "none", + }, + alignment: { + horizontal: "center", + }, + }, + }, + { + address: "B4", + value: null, + isMerged: false, + style: {}, + }, + { + address: "C4", + value: "Merge C4:D4", + isMerged: true, + mergeRange: "C4:D4", + style: {}, + }, + { + address: "D4", + value: "Merge C4:D4", + isMerged: true, + style: {}, + }, + ], +]; const meta: Meta> = { title: "Spreadsheet", @@ -185,6 +531,13 @@ export const WithCustomCell: StoryObj = { }, }; +export const WithCustomMergeCell: StoryObj = { + args: { + data: TEST_MERGE_DATA, + Cell: CustomMergeCell, + }, +}; + export const RangeCell: StoryObj = { args: { data: Matrix.set( diff --git a/src/types.ts b/src/types.ts index fa47a46a..8e56c529 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,10 @@ export type CellBase = { className?: string; /** The value of the cell */ value: Value; + /** Check the cell is merged */ + isMerged?: boolean; + /** The mergeRange of the cell */ + mergeRange?: string; /** Custom component to render when the cell is edited, if not defined would default to the component defined for the Spreadsheet */ DataEditor?: DataEditorComponent>; /** Custom component to render when the cell is viewed, if not defined would default to the component defined for the Spreadsheet */ @@ -53,11 +57,7 @@ export type StoreState = { cut: boolean; active: Point | null; mode: Mode; - rowDimensions: Record | undefined>; - columnDimensions: Record< - number, - Pick | undefined - >; + dimensions: Record; dragging: boolean; lastChanged: Point | null; lastCommit: null | CellChange[]; diff --git a/src/util.test.ts b/src/util.test.ts index 47fb6416..102b3ae1 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -33,22 +33,28 @@ const EXAMPLE_CELL_DIMENSIONS: Types.Dimensions = { const EXAMPLE_STATE: Types.StoreState = { active: null, mode: "view", - rowDimensions: { - 0: { + dimensions: { + "0:0": { height: EXAMPLE_CELL_DIMENSIONS.height, top: EXAMPLE_CELL_DIMENSIONS.top, + width: EXAMPLE_CELL_DIMENSIONS.width, + left: EXAMPLE_CELL_DIMENSIONS.left, }, - 1: { + "0:1": { height: EXAMPLE_CELL_DIMENSIONS.height, - top: EXAMPLE_CELL_DIMENSIONS.top + EXAMPLE_CELL_DIMENSIONS.height, + top: EXAMPLE_CELL_DIMENSIONS.top, + width: EXAMPLE_CELL_DIMENSIONS.width, + left: EXAMPLE_CELL_DIMENSIONS.left + EXAMPLE_CELL_DIMENSIONS.width, }, - }, - columnDimensions: { - 0: { + "1:0": { + height: EXAMPLE_CELL_DIMENSIONS.height, + top: EXAMPLE_CELL_DIMENSIONS.top + EXAMPLE_CELL_DIMENSIONS.height, width: EXAMPLE_CELL_DIMENSIONS.width, left: EXAMPLE_CELL_DIMENSIONS.left, }, - 1: { + "1:1": { + height: EXAMPLE_CELL_DIMENSIONS.height, + top: EXAMPLE_CELL_DIMENSIONS.top + EXAMPLE_CELL_DIMENSIONS.height, width: EXAMPLE_CELL_DIMENSIONS.width, left: EXAMPLE_CELL_DIMENSIONS.left + EXAMPLE_CELL_DIMENSIONS.width, }, @@ -153,13 +159,9 @@ describe("getCellDimensions()", () => { ], ] as const; test.each(cases)("%s", (name, point, expected) => { - expect( - util.getCellDimensions( - point, - EXAMPLE_STATE.rowDimensions, - EXAMPLE_STATE.columnDimensions - ) - ).toEqual(expected); + expect(util.getCellDimensions(point, EXAMPLE_STATE.dimensions)).toEqual( + expected + ); }); }); @@ -214,13 +216,9 @@ describe("getRangeDimensions()", () => { ], ]; test.each(cases)("%s", (name, range, expected) => { - expect( - util.getRangeDimensions( - EXAMPLE_STATE.rowDimensions, - EXAMPLE_STATE.columnDimensions, - range - ) - ).toEqual(expected); + expect(util.getRangeDimensions(EXAMPLE_STATE.dimensions, range)).toEqual( + expected + ); }); }); @@ -232,8 +230,7 @@ describe("getSelectedDimensions()", () => { "point range", new RangeSelection(new PointRange(Point.ORIGIN, Point.ORIGIN)), util.getRangeDimensions( - EXAMPLE_STATE.rowDimensions, - EXAMPLE_STATE.columnDimensions, + EXAMPLE_STATE.dimensions, new PointRange(Point.ORIGIN, Point.ORIGIN) ), ], @@ -242,8 +239,7 @@ describe("getSelectedDimensions()", () => { test.each(cases)("%s", (name, selection, expected) => { expect( util.getSelectedDimensions( - EXAMPLE_STATE.rowDimensions, - EXAMPLE_STATE.columnDimensions, + EXAMPLE_STATE.dimensions, EXAMPLE_STATE.model.data, selection ) diff --git a/src/util.ts b/src/util.ts index 7c08bf2d..f69dd4a7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -76,37 +76,21 @@ export function readTextFromClipboard(event: ClipboardEvent): string { /** Get the dimensions of cell at point from state */ export function getCellDimensions( point: Point.Point, - rowDimensions: Types.StoreState["rowDimensions"] | undefined, - columnDimensions: Types.StoreState["columnDimensions"] | undefined + dimensions: Types.StoreState["dimensions"] | undefined ): Types.Dimensions | undefined { - const cellRowDimensions = rowDimensions && rowDimensions[point.row]; - const cellColumnDimensions = - columnDimensions && columnDimensions[point.column]; - return ( - cellRowDimensions && - cellColumnDimensions && { - ...cellRowDimensions, - ...cellColumnDimensions, - } - ); + const cellDimensions = + dimensions && dimensions?.[`${point.row}:${point.column}`]; + + return cellDimensions; } /** Get the dimensions of a range of cells */ export function getRangeDimensions( - rowDimensions: Types.StoreState["rowDimensions"], - columnDimensions: Types.StoreState["columnDimensions"], + dimensions: Types.StoreState["dimensions"], range: PointRange ): Types.Dimensions | undefined { - const startDimensions = getCellDimensions( - range.start, - rowDimensions, - columnDimensions - ); - const endDimensions = getCellDimensions( - range.end, - rowDimensions, - columnDimensions - ); + const startDimensions = getCellDimensions(range.start, dimensions); + const endDimensions = getCellDimensions(range.end, dimensions); return ( startDimensions && endDimensions && { @@ -120,15 +104,12 @@ export function getRangeDimensions( /** Get the dimensions of selected */ export function getSelectedDimensions( - rowDimensions: Types.StoreState["rowDimensions"], - columnDimensions: Types.StoreState["columnDimensions"], + dimensions: Types.StoreState["dimensions"], data: Matrix.Matrix, selected: Selection ): Types.Dimensions | undefined { const range = selected.toRange(data); - return range - ? getRangeDimensions(rowDimensions, columnDimensions, range) - : undefined; + return range ? getRangeDimensions(dimensions, range) : undefined; } /** Get given data as CSV */