@a3s-lab/office 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +470 -0
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/dist/0~1403.js +245 -0
- package/dist/0~152.js +94 -0
- package/dist/0~3539.js +1856 -0
- package/dist/0~3557.js +179 -0
- package/dist/0~3635.js +302 -0
- package/dist/0~4595.js +1162 -0
- package/dist/0~5050.js +308 -0
- package/dist/0~6552.js +787 -0
- package/dist/0~7048.js +2110 -0
- package/dist/0~7614.js +1037 -0
- package/dist/0~8155.js +299 -0
- package/dist/0~9445.js +283 -0
- package/dist/0~9512.js +743 -0
- package/dist/0~document-editor.js +10967 -0
- package/dist/0~markdown-editor.js +2260 -0
- package/dist/0~pdf-viewer.js +2156 -0
- package/dist/0~presentation-editor.js +8359 -0
- package/dist/0~rslib-runtime.js +27 -0
- package/dist/0~spreadsheet-editor.js +10388 -0
- package/dist/0~use-office-draft.js +46 -0
- package/dist/0~work-document-extensions.js +2470 -0
- package/dist/0~work-document-links.js +14 -0
- package/dist/0~work-document-model-codec.js +42 -0
- package/dist/0~work-document-page-color.js +11 -0
- package/dist/0~work-document-paragraph-formatting.js +425 -0
- package/dist/0~work-docx-bibliography.js +347 -0
- package/dist/0~work-docx-export.js +1151 -0
- package/dist/0~work-docx-import.js +3116 -0
- package/dist/0~work-docx-page-color.js +42 -0
- package/dist/0~work-office-diagnostics.js +1377 -0
- package/dist/0~work-pptx-export.js +1009 -0
- package/dist/0~work-pptx-import.js +1138 -0
- package/dist/0~work-presentation-charts.js +442 -0
- package/dist/0~work-presentation-transition.js +38 -0
- package/dist/0~work-presentation-visual-scale.js +38 -0
- package/dist/0~work-spreadsheet-formula-support.js +9 -0
- package/dist/1134.js +445 -0
- package/dist/4104.js +5786 -0
- package/dist/432.js +211 -0
- package/dist/4704.js +1364 -0
- package/dist/6489.js +182 -0
- package/dist/8715.js +3959 -0
- package/dist/8728.js +723 -0
- package/dist/9356.js +10 -0
- package/dist/core.d.ts +15 -0
- package/dist/core.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/internal/design-system/primitives/button/button.d.ts +9 -0
- package/dist/internal/design-system/primitives/button/icon-button.d.ts +7 -0
- package/dist/internal/design-system/primitives/dialog/dialog.d.ts +12 -0
- package/dist/internal/design-system/primitives/feedback/collection-state.d.ts +9 -0
- package/dist/internal/design-system/primitives/feedback/inline-notice.d.ts +10 -0
- package/dist/internal/design-system/primitives/feedback/state-view.d.ts +13 -0
- package/dist/internal/design-system/primitives/form/field.d.ts +19 -0
- package/dist/internal/design-system/primitives/index.d.ts +14 -0
- package/dist/internal/design-system/primitives/layout/split-handle.d.ts +17 -0
- package/dist/internal/design-system/primitives/navigation/segmented-control.d.ts +19 -0
- package/dist/internal/design-system/primitives/navigation/tab-navigation.d.ts +13 -0
- package/dist/internal/design-system/primitives/navigation/tabs.d.ts +21 -0
- package/dist/internal/design-system/primitives/overlay/dialog-focus-scope.d.ts +17 -0
- package/dist/internal/design-system/primitives/overlay/popover.d.ts +34 -0
- package/dist/internal/design-system/primitives/overlay/portal-root.d.ts +1 -0
- package/dist/internal/design-system/primitives/status/status-badge.d.ts +6 -0
- package/dist/internal/features/work/components/work-document-pages.d.ts +4 -0
- package/dist/internal/features/work/components/work-editor-agent-menus.d.ts +12 -0
- package/dist/internal/features/work/components/work-editor-loading-state.d.ts +3 -0
- package/dist/internal/features/work/editors/document-changes-panel.d.ts +9 -0
- package/dist/internal/features/work/editors/document-citation-source-form.d.ts +33 -0
- package/dist/internal/features/work/editors/document-citations-panel.d.ts +8 -0
- package/dist/internal/features/work/editors/document-columns-panel.d.ts +5 -0
- package/dist/internal/features/work/editors/document-comment-composer.d.ts +13 -0
- package/dist/internal/features/work/editors/document-comments-panel.d.ts +17 -0
- package/dist/internal/features/work/editors/document-editor-focus.d.ts +1 -0
- package/dist/internal/features/work/editors/document-editor-pagination.d.ts +3 -0
- package/dist/internal/features/work/editors/document-editor-support.d.ts +34 -0
- package/dist/internal/features/work/editors/document-editor.d.ts +20 -0
- package/dist/internal/features/work/editors/document-find-highlight.d.ts +8 -0
- package/dist/internal/features/work/editors/document-find-replace-panel.d.ts +10 -0
- package/dist/internal/features/work/editors/document-formatting-options.d.ts +113 -0
- package/dist/internal/features/work/editors/document-home-ribbon.d.ts +9 -0
- package/dist/internal/features/work/editors/document-layout-panel.d.ts +10 -0
- package/dist/internal/features/work/editors/document-list-gallery.d.ts +4 -0
- package/dist/internal/features/work/editors/document-navigation-panel.d.ts +9 -0
- package/dist/internal/features/work/editors/document-page-chrome-editor.d.ts +39 -0
- package/dist/internal/features/work/editors/document-page-chrome-panel.d.ts +5 -0
- package/dist/internal/features/work/editors/document-page-chrome-ribbon.d.ts +10 -0
- package/dist/internal/features/work/editors/document-page-navigation.d.ts +14 -0
- package/dist/internal/features/work/editors/document-page-stack.d.ts +8 -0
- package/dist/internal/features/work/editors/document-pagination-popover.d.ts +4 -0
- package/dist/internal/features/work/editors/document-paragraph-spacing-popover.d.ts +4 -0
- package/dist/internal/features/work/editors/document-picture-ribbon.d.ts +4 -0
- package/dist/internal/features/work/editors/document-ruler-tab-stops.d.ts +12 -0
- package/dist/internal/features/work/editors/document-ruler.d.ts +15 -0
- package/dist/internal/features/work/editors/document-selection-context-menu.d.ts +22 -0
- package/dist/internal/features/work/editors/document-selection-toolbar.d.ts +8 -0
- package/dist/internal/features/work/editors/document-status-bar.d.ts +19 -0
- package/dist/internal/features/work/editors/document-style-gallery.d.ts +4 -0
- package/dist/internal/features/work/editors/document-table-insert-popover.d.ts +4 -0
- package/dist/internal/features/work/editors/document-table-ribbon.d.ts +7 -0
- package/dist/internal/features/work/editors/document-task-pane.d.ts +11 -0
- package/dist/internal/features/work/editors/document-toolbar.d.ts +100 -0
- package/dist/internal/features/work/editors/document-vertical-ruler.d.ts +6 -0
- package/dist/internal/features/work/editors/markdown-editor-focus.d.ts +2 -0
- package/dist/internal/features/work/editors/markdown-editor.d.ts +16 -0
- package/dist/internal/features/work/editors/markdown-insert-dialog.d.ts +19 -0
- package/dist/internal/features/work/editors/markdown-selection-context-menu.d.ts +14 -0
- package/dist/internal/features/work/editors/markdown-source-commands.d.ts +41 -0
- package/dist/internal/features/work/editors/markdown-status.d.ts +7 -0
- package/dist/internal/features/work/editors/markdown-toolbar.d.ts +22 -0
- package/dist/internal/features/work/editors/markdown-workspace.d.ts +26 -0
- package/dist/internal/features/work/editors/office-checkbox.d.ts +9 -0
- package/dist/internal/features/work/editors/office-color-picker.d.ts +11 -0
- package/dist/internal/features/work/editors/office-controls.d.ts +7 -0
- package/dist/internal/features/work/editors/office-dialog.d.ts +35 -0
- package/dist/internal/features/work/editors/office-editor-extension.d.ts +86 -0
- package/dist/internal/features/work/editors/office-file-data.d.ts +1 -0
- package/dist/internal/features/work/editors/office-font-families.d.ts +187 -0
- package/dist/internal/features/work/editors/office-menu-keyboard.d.ts +2 -0
- package/dist/internal/features/work/editors/office-number-field.d.ts +21 -0
- package/dist/internal/features/work/editors/office-number-normalization.d.ts +9 -0
- package/dist/internal/features/work/editors/office-select.d.ts +19 -0
- package/dist/internal/features/work/editors/office-shortcuts.d.ts +1 -0
- package/dist/internal/features/work/editors/office-slider.d.ts +10 -0
- package/dist/internal/features/work/editors/office-table-dimensions.d.ts +4 -0
- package/dist/internal/features/work/editors/office-table-insert-popover.d.ts +7 -0
- package/dist/internal/features/work/editors/office-task-pane.d.ts +7 -0
- package/dist/internal/features/work/editors/office-text-field.d.ts +20 -0
- package/dist/internal/features/work/editors/office-tiptap-extensions.d.ts +2 -0
- package/dist/internal/features/work/editors/pdf-annotation-controller.d.ts +23 -0
- package/dist/internal/features/work/editors/pdf-editor-extensions.d.ts +36 -0
- package/dist/internal/features/work/editors/pdf-thumbnail-rail.d.ts +26 -0
- package/dist/internal/features/work/editors/pdf-toolbar.d.ts +23 -0
- package/dist/internal/features/work/editors/pdf-viewer-controller.d.ts +48 -0
- package/dist/internal/features/work/editors/pdf-viewer.d.ts +11 -0
- package/dist/internal/features/work/editors/presentation-arrangement-controller.d.ts +19 -0
- package/dist/internal/features/work/editors/presentation-chart-analysis-canvas.d.ts +21 -0
- package/dist/internal/features/work/editors/presentation-chart-axis-canvas.d.ts +33 -0
- package/dist/internal/features/work/editors/presentation-chart-axis-editor.d.ts +5 -0
- package/dist/internal/features/work/editors/presentation-chart-canvas.d.ts +5 -0
- package/dist/internal/features/work/editors/presentation-chart-cartesian-canvas.d.ts +3 -0
- package/dist/internal/features/work/editors/presentation-chart-data-label-editor.d.ts +6 -0
- package/dist/internal/features/work/editors/presentation-chart-data-labels.d.ts +26 -0
- package/dist/internal/features/work/editors/presentation-chart-layout-editor.d.ts +5 -0
- package/dist/internal/features/work/editors/presentation-chart-legend-canvas.d.ts +15 -0
- package/dist/internal/features/work/editors/presentation-chart-panel.d.ts +7 -0
- package/dist/internal/features/work/editors/presentation-chart-series-analysis-editor.d.ts +6 -0
- package/dist/internal/features/work/editors/presentation-chart-series-canvas.d.ts +15 -0
- package/dist/internal/features/work/editors/presentation-command-controller.d.ts +3 -0
- package/dist/internal/features/work/editors/presentation-command-types.d.ts +206 -0
- package/dist/internal/features/work/editors/presentation-comments-panel.d.ts +9 -0
- package/dist/internal/features/work/editors/presentation-context-menu.d.ts +11 -0
- package/dist/internal/features/work/editors/presentation-design-panel.d.ts +13 -0
- package/dist/internal/features/work/editors/presentation-editor-focus.d.ts +12 -0
- package/dist/internal/features/work/editors/presentation-editor-operations.d.ts +10 -0
- package/dist/internal/features/work/editors/presentation-editor-types.d.ts +22 -0
- package/dist/internal/features/work/editors/presentation-editor.d.ts +3 -0
- package/dist/internal/features/work/editors/presentation-keyboard-extension.d.ts +3 -0
- package/dist/internal/features/work/editors/presentation-player.d.ts +7 -0
- package/dist/internal/features/work/editors/presentation-presenter-view.d.ts +15 -0
- package/dist/internal/features/work/editors/presentation-selection.d.ts +21 -0
- package/dist/internal/features/work/editors/presentation-slide-canvas.d.ts +26 -0
- package/dist/internal/features/work/editors/presentation-slide-thumbnail.d.ts +18 -0
- package/dist/internal/features/work/editors/presentation-status-bar.d.ts +10 -0
- package/dist/internal/features/work/editors/presentation-text-editor.d.ts +22 -0
- package/dist/internal/features/work/editors/presentation-text-formatting.d.ts +3 -0
- package/dist/internal/features/work/editors/presentation-thumbnail-rail.d.ts +20 -0
- package/dist/internal/features/work/editors/presentation-toolbar.d.ts +19 -0
- package/dist/internal/features/work/editors/presentation-transition-panel.d.ts +9 -0
- package/dist/internal/features/work/editors/presentation-workspace.d.ts +38 -0
- package/dist/internal/features/work/editors/spreadsheet-calculation-model.d.ts +8 -0
- package/dist/internal/features/work/editors/spreadsheet-calculation-projection.d.ts +32 -0
- package/dist/internal/features/work/editors/spreadsheet-chart-axis-editor.d.ts +9 -0
- package/dist/internal/features/work/editors/spreadsheet-chart-draft.d.ts +17 -0
- package/dist/internal/features/work/editors/spreadsheet-chart-layout-editor.d.ts +13 -0
- package/dist/internal/features/work/editors/spreadsheet-chart-panel.d.ts +10 -0
- package/dist/internal/features/work/editors/spreadsheet-chart-series-style-editor.d.ts +9 -0
- package/dist/internal/features/work/editors/spreadsheet-clipboard-shortcuts.d.ts +9 -0
- package/dist/internal/features/work/editors/spreadsheet-command-controller.d.ts +121 -0
- package/dist/internal/features/work/editors/spreadsheet-conditional-comparison-fields.d.ts +7 -0
- package/dist/internal/features/work/editors/spreadsheet-conditional-format-model.d.ts +72 -0
- package/dist/internal/features/work/editors/spreadsheet-conditional-format-panel.d.ts +7 -0
- package/dist/internal/features/work/editors/spreadsheet-conditional-threshold-fields.d.ts +10 -0
- package/dist/internal/features/work/editors/spreadsheet-context-menu.d.ts +36 -0
- package/dist/internal/features/work/editors/spreadsheet-data-label-editor.d.ts +9 -0
- package/dist/internal/features/work/editors/spreadsheet-editor-ribbon.d.ts +46 -0
- package/dist/internal/features/work/editors/spreadsheet-editor-support.d.ts +31 -0
- package/dist/internal/features/work/editors/spreadsheet-editor.d.ts +16 -0
- package/dist/internal/features/work/editors/spreadsheet-error-bar-editor.d.ts +10 -0
- package/dist/internal/features/work/editors/spreadsheet-find-bar.d.ts +8 -0
- package/dist/internal/features/work/editors/spreadsheet-find.d.ts +9 -0
- package/dist/internal/features/work/editors/spreadsheet-formula-panel.d.ts +10 -0
- package/dist/internal/features/work/editors/spreadsheet-header-footer-fields.d.ts +5 -0
- package/dist/internal/features/work/editors/spreadsheet-keyboard-navigation.d.ts +18 -0
- package/dist/internal/features/work/editors/spreadsheet-number-format.d.ts +11 -0
- package/dist/internal/features/work/editors/spreadsheet-pivot-panel.d.ts +10 -0
- package/dist/internal/features/work/editors/spreadsheet-print-settings-panel.d.ts +6 -0
- package/dist/internal/features/work/editors/spreadsheet-protection-panel.d.ts +7 -0
- package/dist/internal/features/work/editors/spreadsheet-sheet-bar.d.ts +17 -0
- package/dist/internal/features/work/editors/spreadsheet-sheet-model.d.ts +13 -0
- package/dist/internal/features/work/editors/spreadsheet-trendline-editor.d.ts +8 -0
- package/dist/internal/features/work/editors/spreadsheet-workbook-panel.d.ts +16 -0
- package/dist/internal/features/work/editors/use-document-comments.d.ts +26 -0
- package/dist/internal/features/work/editors/use-document-insert-commands.d.ts +19 -0
- package/dist/internal/features/work/editors/use-document-layout-fonts.d.ts +2 -0
- package/dist/internal/features/work/editors/use-document-page-chrome.d.ts +33 -0
- package/dist/internal/features/work/editors/use-document-pagination.d.ts +46 -0
- package/dist/internal/features/work/editors/use-markdown-source-history.d.ts +16 -0
- package/dist/internal/features/work/editors/use-office-draft.d.ts +10 -0
- package/dist/internal/features/work/editors/use-office-editor-keyboard-shortcuts.d.ts +9 -0
- package/dist/internal/features/work/editors/use-office-editor-runtime.d.ts +6 -0
- package/dist/internal/features/work/editors/use-office-editor-wheel-zoom.d.ts +19 -0
- package/dist/internal/features/work/editors/use-office-history.d.ts +10 -0
- package/dist/internal/features/work/editors/use-presentation-clipboard.d.ts +22 -0
- package/dist/internal/features/work/editors/use-presentation-design-commands.d.ts +33 -0
- package/dist/internal/features/work/editors/use-presentation-element-commands.d.ts +29 -0
- package/dist/internal/features/work/editors/use-presentation-geometry.d.ts +17 -0
- package/dist/internal/features/work/editors/use-presentation-history.d.ts +12 -0
- package/dist/internal/features/work/editors/use-presentation-review-commands.d.ts +22 -0
- package/dist/internal/features/work/editors/use-presentation-selection.d.ts +11 -0
- package/dist/internal/features/work/editors/use-presentation-slide-commands.d.ts +18 -0
- package/dist/internal/features/work/editors/use-presentation-thumbnail-visibility.d.ts +4 -0
- package/dist/internal/features/work/editors/use-presentation-thumbnail-window.d.ts +17 -0
- package/dist/internal/features/work/editors/use-presentation-transform.d.ts +53 -0
- package/dist/internal/features/work/editors/use-spreadsheet-calculation.d.ts +17 -0
- package/dist/internal/features/work/editors/use-spreadsheet-workbook-sync.d.ts +10 -0
- package/dist/internal/features/work/editors/work-office-chrome.d.ts +61 -0
- package/dist/internal/features/work/editors/work-office-ribbon-overflow.d.ts +18 -0
- package/dist/internal/features/work/work-agent-proposal-apply.d.ts +8 -0
- package/dist/internal/features/work/work-agent-proposal.d.ts +55 -0
- package/dist/internal/features/work/work-agent-request.d.ts +6 -0
- package/dist/internal/features/work/work-chart-series-analysis-context.d.ts +3 -0
- package/dist/internal/features/work/work-document-caption-nodes.d.ts +13 -0
- package/dist/internal/features/work/work-document-captions.d.ts +14 -0
- package/dist/internal/features/work/work-document-changes.d.ts +35 -0
- package/dist/internal/features/work/work-document-character-formatting.d.ts +13 -0
- package/dist/internal/features/work/work-document-citation-nodes.d.ts +25 -0
- package/dist/internal/features/work/work-document-citations.d.ts +21 -0
- package/dist/internal/features/work/work-document-columns.d.ts +15 -0
- package/dist/internal/features/work/work-document-comments.d.ts +48 -0
- package/dist/internal/features/work/work-document-extensions.d.ts +11 -0
- package/dist/internal/features/work/work-document-field-node.d.ts +12 -0
- package/dist/internal/features/work/work-document-fields.d.ts +15 -0
- package/dist/internal/features/work/work-document-file-io.d.ts +4 -0
- package/dist/internal/features/work/work-document-fonts.d.ts +16 -0
- package/dist/internal/features/work/work-document-image-layout.d.ts +29 -0
- package/dist/internal/features/work/work-document-incremental-layout.d.ts +17 -0
- package/dist/internal/features/work/work-document-layout.d.ts +5 -0
- package/dist/internal/features/work/work-document-line-measurement.d.ts +24 -0
- package/dist/internal/features/work/work-document-links.d.ts +2 -0
- package/dist/internal/features/work/work-document-lists.d.ts +25 -0
- package/dist/internal/features/work/work-document-model-codec.d.ts +3 -0
- package/dist/internal/features/work/work-document-model.d.ts +12 -0
- package/dist/internal/features/work/work-document-note-nodes.d.ts +11 -0
- package/dist/internal/features/work/work-document-notes.d.ts +18 -0
- package/dist/internal/features/work/work-document-outline.d.ts +14 -0
- package/dist/internal/features/work/work-document-page-break.d.ts +2 -0
- package/dist/internal/features/work/work-document-page-chrome-commands.d.ts +15 -0
- package/dist/internal/features/work/work-document-page-chrome.d.ts +19 -0
- package/dist/internal/features/work/work-document-page-color.d.ts +3 -0
- package/dist/internal/features/work/work-document-pages.d.ts +19 -0
- package/dist/internal/features/work/work-document-pagination-decorations.d.ts +3 -0
- package/dist/internal/features/work/work-document-pagination-dom.d.ts +35 -0
- package/dist/internal/features/work/work-document-pagination-measurement.d.ts +9 -0
- package/dist/internal/features/work/work-document-pagination-types.d.ts +88 -0
- package/dist/internal/features/work/work-document-pagination.d.ts +17 -0
- package/dist/internal/features/work/work-document-paragraph-formatting.d.ts +61 -0
- package/dist/internal/features/work/work-document-search.d.ts +11 -0
- package/dist/internal/features/work/work-document-section-editor.d.ts +20 -0
- package/dist/internal/features/work/work-document-section-node.d.ts +13 -0
- package/dist/internal/features/work/work-document-section.d.ts +34 -0
- package/dist/internal/features/work/work-document-selection-menu.d.ts +54 -0
- package/dist/internal/features/work/work-document-tab-node.d.ts +6 -0
- package/dist/internal/features/work/work-document-tab-stops.d.ts +29 -0
- package/dist/internal/features/work/work-document-table-cell-formatting.d.ts +46 -0
- package/dist/internal/features/work/work-document-table-commands.d.ts +17 -0
- package/dist/internal/features/work/work-document-table-pagination.d.ts +5 -0
- package/dist/internal/features/work/work-document-table-row.d.ts +37 -0
- package/dist/internal/features/work/work-document-table-sizing.d.ts +27 -0
- package/dist/internal/features/work/work-document-text-layout.d.ts +14 -0
- package/dist/internal/features/work/work-docx-bibliography.d.ts +12 -0
- package/dist/internal/features/work/work-docx-caption-diagnostics.d.ts +6 -0
- package/dist/internal/features/work/work-docx-caption-export.d.ts +4 -0
- package/dist/internal/features/work/work-docx-caption-fields.d.ts +5 -0
- package/dist/internal/features/work/work-docx-caption-import.d.ts +20 -0
- package/dist/internal/features/work/work-docx-change-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-citation-diagnostics.d.ts +3 -0
- package/dist/internal/features/work/work-docx-citation-export.d.ts +3 -0
- package/dist/internal/features/work/work-docx-citation-import.d.ts +23 -0
- package/dist/internal/features/work/work-docx-column-export.d.ts +14 -0
- package/dist/internal/features/work/work-docx-column-import.d.ts +2 -0
- package/dist/internal/features/work/work-docx-comment-import.d.ts +14 -0
- package/dist/internal/features/work/work-docx-export-formatting.d.ts +30 -0
- package/dist/internal/features/work/work-docx-export-image.d.ts +2 -0
- package/dist/internal/features/work/work-docx-export.d.ts +2 -0
- package/dist/internal/features/work/work-docx-field-export.d.ts +2 -0
- package/dist/internal/features/work/work-docx-field-import.d.ts +16 -0
- package/dist/internal/features/work/work-docx-field-instructions.d.ts +8 -0
- package/dist/internal/features/work/work-docx-image-layout-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-import.d.ts +50 -0
- package/dist/internal/features/work/work-docx-list-import.d.ts +14 -0
- package/dist/internal/features/work/work-docx-note-diagnostics.d.ts +3 -0
- package/dist/internal/features/work/work-docx-note-import.d.ts +3 -0
- package/dist/internal/features/work/work-docx-page-chrome-diagnostics.d.ts +3 -0
- package/dist/internal/features/work/work-docx-page-chrome-import.d.ts +11 -0
- package/dist/internal/features/work/work-docx-page-color.d.ts +2 -0
- package/dist/internal/features/work/work-docx-paragraph-alignment-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-paragraph-direction-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-paragraph-indent-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-paragraph-pagination-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-paragraph-spacing-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-paragraph-styles.d.ts +18 -0
- package/dist/internal/features/work/work-docx-run-formatting-import.d.ts +19 -0
- package/dist/internal/features/work/work-docx-tab-stop-import.d.ts +13 -0
- package/dist/internal/features/work/work-docx-table-cell-export.d.ts +4 -0
- package/dist/internal/features/work/work-docx-table-cell-import.d.ts +16 -0
- package/dist/internal/features/work/work-docx-table-row-import.d.ts +13 -0
- package/dist/internal/features/work/work-docx-table-sizing-export.d.ts +8 -0
- package/dist/internal/features/work/work-docx-table-sizing-import.d.ts +12 -0
- package/dist/internal/features/work/work-docx-theme.d.ts +9 -0
- package/dist/internal/features/work/work-file-data.d.ts +1 -0
- package/dist/internal/features/work/work-file-download.d.ts +3 -0
- package/dist/internal/features/work/work-file-io.d.ts +8 -0
- package/dist/internal/features/work/work-markdown-extensions.d.ts +19 -0
- package/dist/internal/features/work/work-markdown-file-io.d.ts +3 -0
- package/dist/internal/features/work/work-markdown-selection-menu.d.ts +57 -0
- package/dist/internal/features/work/work-office-diagnostics.d.ts +9 -0
- package/dist/internal/features/work/work-ooxml-package.d.ts +28 -0
- package/dist/internal/features/work/work-pdf-export.d.ts +23 -0
- package/dist/internal/features/work/work-pptx-chart-axes.d.ts +3 -0
- package/dist/internal/features/work/work-pptx-chart-axis-diagnostics.d.ts +5 -0
- package/dist/internal/features/work/work-pptx-chart-data-labels.d.ts +12 -0
- package/dist/internal/features/work/work-pptx-chart-import.d.ts +11 -0
- package/dist/internal/features/work/work-pptx-chart-layout-style.d.ts +15 -0
- package/dist/internal/features/work/work-pptx-chart-series-analysis.d.ts +13 -0
- package/dist/internal/features/work/work-pptx-chart-xy.d.ts +3 -0
- package/dist/internal/features/work/work-pptx-comments.d.ts +17 -0
- package/dist/internal/features/work/work-pptx-export.d.ts +4 -0
- package/dist/internal/features/work/work-pptx-groups.d.ts +19 -0
- package/dist/internal/features/work/work-pptx-import.d.ts +6 -0
- package/dist/internal/features/work/work-pptx-layout-export.d.ts +13 -0
- package/dist/internal/features/work/work-pptx-layout-import.d.ts +30 -0
- package/dist/internal/features/work/work-pptx-style.d.ts +13 -0
- package/dist/internal/features/work/work-pptx-transition.d.ts +13 -0
- package/dist/internal/features/work/work-presentation-agent-context.d.ts +6 -0
- package/dist/internal/features/work/work-presentation-chart-axes.d.ts +14 -0
- package/dist/internal/features/work/work-presentation-charts.d.ts +37 -0
- package/dist/internal/features/work/work-presentation-clipboard.d.ts +26 -0
- package/dist/internal/features/work/work-presentation-file-io.d.ts +15 -0
- package/dist/internal/features/work/work-presentation-groups.d.ts +17 -0
- package/dist/internal/features/work/work-presentation-layouts.d.ts +13 -0
- package/dist/internal/features/work/work-presentation-transition.d.ts +4 -0
- package/dist/internal/features/work/work-presentation-visual-scale.d.ts +2 -0
- package/dist/internal/features/work/work-repository.d.ts +4 -0
- package/dist/internal/features/work/work-spreadsheet-agent-context.d.ts +15 -0
- package/dist/internal/features/work/work-spreadsheet-chart-axis.d.ts +42 -0
- package/dist/internal/features/work/work-spreadsheet-chart-layout.d.ts +29 -0
- package/dist/internal/features/work/work-spreadsheet-chart-legend-svg.d.ts +18 -0
- package/dist/internal/features/work/work-spreadsheet-chart-series-layout.d.ts +24 -0
- package/dist/internal/features/work/work-spreadsheet-chart-series-style.d.ts +29 -0
- package/dist/internal/features/work/work-spreadsheet-chart-svg-utils.d.ts +7 -0
- package/dist/internal/features/work/work-spreadsheet-chart-svg.d.ts +3 -0
- package/dist/internal/features/work/work-spreadsheet-chart-types.d.ts +147 -0
- package/dist/internal/features/work/work-spreadsheet-chart-validation.d.ts +2 -0
- package/dist/internal/features/work/work-spreadsheet-charts.d.ts +17 -0
- package/dist/internal/features/work/work-spreadsheet-combination-chart-svg.d.ts +9 -0
- package/dist/internal/features/work/work-spreadsheet-comments.d.ts +7 -0
- package/dist/internal/features/work/work-spreadsheet-conditional-canvas.d.ts +5 -0
- package/dist/internal/features/work/work-spreadsheet-conditional-colors.d.ts +9 -0
- package/dist/internal/features/work/work-spreadsheet-conditional-comparisons.d.ts +4 -0
- package/dist/internal/features/work/work-spreadsheet-conditional-data-bar.d.ts +8 -0
- package/dist/internal/features/work/work-spreadsheet-conditional-format.d.ts +10 -0
- package/dist/internal/features/work/work-spreadsheet-conditional-icons.d.ts +107 -0
- package/dist/internal/features/work/work-spreadsheet-conditional-values.d.ts +19 -0
- package/dist/internal/features/work/work-spreadsheet-data-label-svg.d.ts +27 -0
- package/dist/internal/features/work/work-spreadsheet-error-bar-svg.d.ts +5 -0
- package/dist/internal/features/work/work-spreadsheet-error-bars.d.ts +8 -0
- package/dist/internal/features/work/work-spreadsheet-formula-analysis.d.ts +30 -0
- package/dist/internal/features/work/work-spreadsheet-formula-support.d.ts +1 -0
- package/dist/internal/features/work/work-spreadsheet-formulas.d.ts +19 -0
- package/dist/internal/features/work/work-spreadsheet-header-footer.d.ts +14 -0
- package/dist/internal/features/work/work-spreadsheet-line-chart-svg.d.ts +10 -0
- package/dist/internal/features/work/work-spreadsheet-page-breaks.d.ts +4 -0
- package/dist/internal/features/work/work-spreadsheet-page-setup.d.ts +26 -0
- package/dist/internal/features/work/work-spreadsheet-pivot-engine.d.ts +21 -0
- package/dist/internal/features/work/work-spreadsheet-pivot-values.d.ts +7 -0
- package/dist/internal/features/work/work-spreadsheet-pivots.d.ts +27 -0
- package/dist/internal/features/work/work-spreadsheet-protection.d.ts +55 -0
- package/dist/internal/features/work/work-spreadsheet-ranges.d.ts +25 -0
- package/dist/internal/features/work/work-spreadsheet-trendline-svg.d.ts +9 -0
- package/dist/internal/features/work/work-spreadsheet-trendlines.d.ts +11 -0
- package/dist/internal/features/work/work-spreadsheet-xy-chart-axes-svg.d.ts +10 -0
- package/dist/internal/features/work/work-templates.d.ts +4 -0
- package/dist/internal/features/work/work-types.d.ts +513 -0
- package/dist/internal/features/work/work-xlsx-chart-axes.d.ts +4 -0
- package/dist/internal/features/work/work-xlsx-chart-diagnostics.d.ts +3 -0
- package/dist/internal/features/work/work-xlsx-chart-layout.d.ts +16 -0
- package/dist/internal/features/work/work-xlsx-chart-series-style.d.ts +6 -0
- package/dist/internal/features/work/work-xlsx-chart-values.d.ts +10 -0
- package/dist/internal/features/work/work-xlsx-charts.d.ts +32 -0
- package/dist/internal/features/work/work-xlsx-conditional-format-diagnostics.d.ts +6 -0
- package/dist/internal/features/work/work-xlsx-conditional-format-write.d.ts +11 -0
- package/dist/internal/features/work/work-xlsx-conditional-format.d.ts +29 -0
- package/dist/internal/features/work/work-xlsx-defined-names.d.ts +4 -0
- package/dist/internal/features/work/work-xlsx-drawing-geometry.d.ts +27 -0
- package/dist/internal/features/work/work-xlsx-formula-diagnostics.d.ts +3 -0
- package/dist/internal/features/work/work-xlsx-formulas.d.ts +40 -0
- package/dist/internal/features/work/work-xlsx-header-footer.d.ts +12 -0
- package/dist/internal/features/work/work-xlsx-images.d.ts +17 -0
- package/dist/internal/features/work/work-xlsx-interop.d.ts +39 -0
- package/dist/internal/features/work/work-xlsx-page-breaks.d.ts +7 -0
- package/dist/internal/features/work/work-xlsx-page-setup-diagnostics.d.ts +2 -0
- package/dist/internal/features/work/work-xlsx-page-setup.d.ts +5 -0
- package/dist/internal/features/work/work-xlsx-pivot-diagnostics.d.ts +3 -0
- package/dist/internal/features/work/work-xlsx-pivot-xml.d.ts +9 -0
- package/dist/internal/features/work/work-xlsx-pivots.d.ts +32 -0
- package/dist/internal/features/work/work-xlsx-protection.d.ts +24 -0
- package/dist/internal/features/workspace/components/workspace-context-menu.d.ts +29 -0
- package/dist/internal/features/workspace/components/workspace-text-control-selection.d.ts +7 -0
- package/dist/internal/kernel/office-kernel-client-types.d.ts +40 -0
- package/dist/internal/kernel/office-kernel-client.d.ts +4 -0
- package/dist/internal/kernel/office-kernel-fallback.d.ts +2 -0
- package/dist/internal/kernel/office-kernel-presentation-fallback.d.ts +2 -0
- package/dist/internal/kernel/office-kernel-protocol.d.ts +229 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-fallback-formula.d.ts +3 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-fallback-validation.d.ts +5 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-fallback-value.d.ts +6 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-fallback.d.ts +2 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-protocol.d.ts +112 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-session-fallback.d.ts +8 -0
- package/dist/internal/kernel/office-kernel-text-fallback.d.ts +10 -0
- package/dist/internal/kernel/office-kernel-version.d.ts +1 -0
- package/dist/internal/kernel/office-kernel.worker.d.ts +1 -0
- package/dist/internal/keyboard-shortcuts.d.ts +2 -0
- package/dist/internal/state/app-state.d.ts +8 -0
- package/dist/noto-naskh-arabic-regular.ttf +0 -0
- package/dist/noto-naskh-arabic.LICENSE.txt +94 -0
- package/dist/noto-sans-hans-regular.otf +0 -0
- package/dist/noto-sans-hans.LICENSE.txt +94 -0
- package/dist/noto-sans-hebrew-regular.ttf +0 -0
- package/dist/noto-sans-hebrew.LICENSE.txt +94 -0
- package/dist/noto-sans-regular.ttf +0 -0
- package/dist/noto-sans.LICENSE.txt +94 -0
- package/dist/office-kernel.wasm +0 -0
- package/dist/office-kernel.worker.js +14605 -0
- package/dist/office-surface.d.ts +10 -0
- package/dist/pdfium.LICENSE.txt +196 -0
- package/dist/pdfium.wasm +0 -0
- package/dist/pptxgen.LICENSE.txt +21 -0
- package/dist/pptxgen.bundle.js +11244 -0
- package/dist/react.d.ts +51 -0
- package/dist/react.js +1 -0
- package/dist/styles.css +17435 -0
- package/dist/styles.js +0 -0
- package/dist/vue.d.ts +282 -0
- package/dist/vue.js +231 -0
- package/dist/web-component.d.ts +98 -0
- package/dist/web-component.js +335 -0
- package/docs/browser-editor-architecture.md +947 -0
- package/package.json +165 -0
package/dist/8715.js
ADDED
|
@@ -0,0 +1,3959 @@
|
|
|
1
|
+
import jszip from "jszip";
|
|
2
|
+
import { createOfficeId as createWorkId } from "./1134.js";
|
|
3
|
+
function normalizeWorkSpreadsheetDoughnutHoleSize(value) {
|
|
4
|
+
const size = Number(value);
|
|
5
|
+
if (!Number.isFinite(size)) return 50;
|
|
6
|
+
return Math.min(90, Math.max(10, Math.round(size)));
|
|
7
|
+
}
|
|
8
|
+
function normalizeWorkSpreadsheetRadarStyle(value) {
|
|
9
|
+
return 'standard' === value || 'filled' === value || 'marker' === value ? value : 'marker';
|
|
10
|
+
}
|
|
11
|
+
function normalizeWorkSpreadsheetScatterStyle(value) {
|
|
12
|
+
return 'line' === value || 'lineMarker' === value || 'smooth' === value || 'smoothMarker' === value || 'marker' === value ? value : 'marker';
|
|
13
|
+
}
|
|
14
|
+
function normalizeWorkSpreadsheetBubbleScale(value) {
|
|
15
|
+
const scale = Number(value);
|
|
16
|
+
if (!Number.isFinite(scale)) return 100;
|
|
17
|
+
return Math.min(300, Math.max(0, Math.round(scale)));
|
|
18
|
+
}
|
|
19
|
+
function normalizeWorkSpreadsheetBubbleSizeRepresents(value) {
|
|
20
|
+
return 'width' === value || 'w' === value ? 'width' : 'area';
|
|
21
|
+
}
|
|
22
|
+
function workSpreadsheetChartUsesNumericXAxis(type) {
|
|
23
|
+
return 'scatter' === type || 'bubble' === type;
|
|
24
|
+
}
|
|
25
|
+
function workSpreadsheetChartSupportsAxes(type) {
|
|
26
|
+
return 'pie' !== type && 'doughnut' !== type;
|
|
27
|
+
}
|
|
28
|
+
function normalizeWorkSpreadsheetCombinationSeriesType(value) {
|
|
29
|
+
return 'line' === value || 'area' === value || 'column' === value ? value : 'column';
|
|
30
|
+
}
|
|
31
|
+
function normalizeWorkSpreadsheetChartAxisGroup(value) {
|
|
32
|
+
return 'secondary' === value ? 'secondary' : 'primary';
|
|
33
|
+
}
|
|
34
|
+
function workSpreadsheetCombinationSeriesTypeLabel(type) {
|
|
35
|
+
if ('line' === type) return '折线图';
|
|
36
|
+
if ('area' === type) return '面积图';
|
|
37
|
+
return '柱形图';
|
|
38
|
+
}
|
|
39
|
+
function workSpreadsheetChartSupportsTrendlines(type) {
|
|
40
|
+
return 'pie' !== type && 'doughnut' !== type && 'radar' !== type;
|
|
41
|
+
}
|
|
42
|
+
function workSpreadsheetChartSupportsErrorBars(type) {
|
|
43
|
+
return 'pie' !== type && 'doughnut' !== type && 'radar' !== type;
|
|
44
|
+
}
|
|
45
|
+
function normalizeWorkSpreadsheetErrorBars(source, chartType) {
|
|
46
|
+
const direction = workSpreadsheetChartUsesNumericXAxis(chartType) && 'x' === source.direction ? 'x' : 'y';
|
|
47
|
+
const barType = 'plus' === source.barType || 'minus' === source.barType ? source.barType : 'both';
|
|
48
|
+
const valueType = 'percentage' === source.valueType || 'standardDeviation' === source.valueType || 'standardError' === source.valueType || 'custom' === source.valueType || 'fixedValue' === source.valueType ? source.valueType : 'fixedValue';
|
|
49
|
+
const fallbackValue = 'percentage' === valueType ? 5 : 1;
|
|
50
|
+
const numericValue = Number(source.value);
|
|
51
|
+
const value = Number.isFinite(numericValue) ? Math.max(0, numericValue) : fallbackValue;
|
|
52
|
+
const plusValues = source.plusValues?.map(normalizedErrorBarAmount);
|
|
53
|
+
const minusValues = source.minusValues?.map(normalizedErrorBarAmount);
|
|
54
|
+
return {
|
|
55
|
+
direction,
|
|
56
|
+
barType,
|
|
57
|
+
valueType,
|
|
58
|
+
...'fixedValue' === valueType || 'percentage' === valueType || 'standardDeviation' === valueType ? {
|
|
59
|
+
value
|
|
60
|
+
} : {},
|
|
61
|
+
...false === source.showEndCaps ? {
|
|
62
|
+
showEndCaps: false
|
|
63
|
+
} : {},
|
|
64
|
+
...'custom' === valueType && 'minus' !== barType && plusValues?.length ? {
|
|
65
|
+
plusValues
|
|
66
|
+
} : {},
|
|
67
|
+
...'custom' === valueType && 'minus' !== barType && source.plusReference?.trim() ? {
|
|
68
|
+
plusReference: source.plusReference.trim().replace(/^=/, '')
|
|
69
|
+
} : {},
|
|
70
|
+
...'custom' === valueType && 'plus' !== barType && minusValues?.length ? {
|
|
71
|
+
minusValues
|
|
72
|
+
} : {},
|
|
73
|
+
...'custom' === valueType && 'plus' !== barType && source.minusReference?.trim() ? {
|
|
74
|
+
minusReference: source.minusReference.trim().replace(/^=/, '')
|
|
75
|
+
} : {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function workSpreadsheetErrorBarValueTypeLabel(type) {
|
|
79
|
+
if ('percentage' === type) return '百分比';
|
|
80
|
+
if ('standardDeviation' === type) return '标准差';
|
|
81
|
+
if ('standardError' === type) return '标准误差';
|
|
82
|
+
if ('custom' === type) return '自定义';
|
|
83
|
+
return '固定值';
|
|
84
|
+
}
|
|
85
|
+
function workSpreadsheetErrorBarTypeLabel(type) {
|
|
86
|
+
if ('plus' === type) return '正向';
|
|
87
|
+
if ('minus' === type) return '负向';
|
|
88
|
+
return '双向';
|
|
89
|
+
}
|
|
90
|
+
function normalizedErrorBarAmount(value) {
|
|
91
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
92
|
+
}
|
|
93
|
+
function normalizeWorkSpreadsheetDataLabelPosition(value) {
|
|
94
|
+
return 'center' === value || 'insideBase' === value || 'insideEnd' === value || 'outsideEnd' === value || 'left' === value || 'right' === value || 'above' === value || 'below' === value || 'bestFit' === value ? value : 'bestFit';
|
|
95
|
+
}
|
|
96
|
+
function normalizeWorkSpreadsheetDataLabels(source, chartType) {
|
|
97
|
+
const position = source.position ? normalizeWorkSpreadsheetDataLabelPosition(source.position) : void 0;
|
|
98
|
+
const separator = 'string' == typeof source.separator ? source.separator.slice(0, 64) : void 0;
|
|
99
|
+
return {
|
|
100
|
+
...true === source.showValue ? {
|
|
101
|
+
showValue: true
|
|
102
|
+
} : {},
|
|
103
|
+
...true === source.showCategoryName ? {
|
|
104
|
+
showCategoryName: true
|
|
105
|
+
} : {},
|
|
106
|
+
...true === source.showSeriesName ? {
|
|
107
|
+
showSeriesName: true
|
|
108
|
+
} : {},
|
|
109
|
+
...true === source.showPercentage && ('pie' === chartType || 'doughnut' === chartType) ? {
|
|
110
|
+
showPercentage: true
|
|
111
|
+
} : {},
|
|
112
|
+
...true === source.showBubbleSize && 'bubble' === chartType ? {
|
|
113
|
+
showBubbleSize: true
|
|
114
|
+
} : {},
|
|
115
|
+
...void 0 !== separator ? {
|
|
116
|
+
separator
|
|
117
|
+
} : {},
|
|
118
|
+
...position ? {
|
|
119
|
+
position
|
|
120
|
+
} : {}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function workSpreadsheetDataLabelPositionLabel(position) {
|
|
124
|
+
if ('center' === position) return '居中';
|
|
125
|
+
if ('insideBase' === position) return '内侧基部';
|
|
126
|
+
if ('insideEnd' === position) return '内侧末端';
|
|
127
|
+
if ('outsideEnd' === position) return '外侧末端';
|
|
128
|
+
if ('left' === position) return '左侧';
|
|
129
|
+
if ('right' === position) return '右侧';
|
|
130
|
+
if ('above' === position) return '上方';
|
|
131
|
+
if ('below' === position) return '下方';
|
|
132
|
+
return '最佳匹配';
|
|
133
|
+
}
|
|
134
|
+
function normalizeWorkSpreadsheetTrendlineType(value) {
|
|
135
|
+
return 'exponential' === value || 'logarithmic' === value || 'polynomial' === value || 'power' === value || 'movingAverage' === value || 'linear' === value ? value : 'linear';
|
|
136
|
+
}
|
|
137
|
+
function normalizeWorkSpreadsheetTrendline(trendline) {
|
|
138
|
+
const type = normalizeWorkSpreadsheetTrendlineType(trendline.type);
|
|
139
|
+
const name = trendline.name?.trim().slice(0, 255);
|
|
140
|
+
const order = normalizedInteger(trendline.order, 2, 6, 2);
|
|
141
|
+
const period = normalizedInteger(trendline.period, 2, 255, 2);
|
|
142
|
+
const forward = normalizedNonNegativeNumber(trendline.forward);
|
|
143
|
+
const backward = normalizedNonNegativeNumber(trendline.backward);
|
|
144
|
+
const intercept = Number.isFinite(trendline.intercept) ? Number(trendline.intercept) : void 0;
|
|
145
|
+
return {
|
|
146
|
+
type,
|
|
147
|
+
...name ? {
|
|
148
|
+
name
|
|
149
|
+
} : {},
|
|
150
|
+
...'polynomial' === type ? {
|
|
151
|
+
order
|
|
152
|
+
} : {},
|
|
153
|
+
...'movingAverage' === type ? {
|
|
154
|
+
period
|
|
155
|
+
} : {},
|
|
156
|
+
...forward > 0 ? {
|
|
157
|
+
forward
|
|
158
|
+
} : {},
|
|
159
|
+
...backward > 0 ? {
|
|
160
|
+
backward
|
|
161
|
+
} : {},
|
|
162
|
+
...void 0 !== intercept ? {
|
|
163
|
+
intercept
|
|
164
|
+
} : {},
|
|
165
|
+
...true === trendline.displayEquation ? {
|
|
166
|
+
displayEquation: true
|
|
167
|
+
} : {},
|
|
168
|
+
...true === trendline.displayRSquared ? {
|
|
169
|
+
displayRSquared: true
|
|
170
|
+
} : {}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function workSpreadsheetTrendlineTypeLabel(type) {
|
|
174
|
+
if ('exponential' === type) return '指数';
|
|
175
|
+
if ('logarithmic' === type) return '对数';
|
|
176
|
+
if ('polynomial' === type) return '多项式';
|
|
177
|
+
if ('power' === type) return '幂';
|
|
178
|
+
if ('movingAverage' === type) return '移动平均';
|
|
179
|
+
return '线性';
|
|
180
|
+
}
|
|
181
|
+
function normalizedInteger(value, minimum, maximum, fallback) {
|
|
182
|
+
const number = Number(value);
|
|
183
|
+
if (!Number.isFinite(number)) return fallback;
|
|
184
|
+
return Math.min(maximum, Math.max(minimum, Math.round(number)));
|
|
185
|
+
}
|
|
186
|
+
function normalizedNonNegativeNumber(value) {
|
|
187
|
+
const number = Number(value);
|
|
188
|
+
return Number.isFinite(number) ? Math.max(0, number) : 0;
|
|
189
|
+
}
|
|
190
|
+
function workSpreadsheetChartTypeLabel(type) {
|
|
191
|
+
if ('bar' === type) return '条形图';
|
|
192
|
+
if ('line' === type) return '折线图';
|
|
193
|
+
if ('pie' === type) return '饼图';
|
|
194
|
+
if ('doughnut' === type) return '圆环图';
|
|
195
|
+
if ('area' === type) return '面积图';
|
|
196
|
+
if ('radar' === type) return '雷达图';
|
|
197
|
+
if ('scatter' === type) return '散点图';
|
|
198
|
+
if ('bubble' === type) return '气泡图';
|
|
199
|
+
if ('combination' === type) return '组合图';
|
|
200
|
+
return '柱形图';
|
|
201
|
+
}
|
|
202
|
+
const CELL_REFERENCE = /\$?([A-Z]{1,3})\$?([1-9]\d*)/i;
|
|
203
|
+
const CELL_OR_RANGE = new RegExp(`^${CELL_REFERENCE.source}(?::${CELL_REFERENCE.source})?$`, 'i');
|
|
204
|
+
const COLUMN_RANGE = /^\$?([A-Z]{1,3}):\$?([A-Z]{1,3})$/i;
|
|
205
|
+
const ROW_RANGE = /^\$?([1-9]\d*):\$?([1-9]\d*)$/;
|
|
206
|
+
const DEFINED_NAME = /^[\p{L}_\\][\p{L}\p{N}_.\\]*$/u;
|
|
207
|
+
function isValidSpreadsheetDefinedName(value) {
|
|
208
|
+
const name = value.trim();
|
|
209
|
+
if (!name || name.length > 255 || !DEFINED_NAME.test(name) || /^_xlnm\./i.test(name)) return false;
|
|
210
|
+
if (/^[A-Z]{1,3}[1-9]\d*$/i.test(name) || /^R\d+C\d+$/i.test(name)) return false;
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
function normalizeSpreadsheetPrintArea(value) {
|
|
214
|
+
const parts = splitRangeList(value.trim().replace(/^=/, ''));
|
|
215
|
+
if (!parts.length) return null;
|
|
216
|
+
const normalized = parts.map((part)=>{
|
|
217
|
+
const reference = unqualifiedReference(part);
|
|
218
|
+
if (!CELL_OR_RANGE.test(reference) && !COLUMN_RANGE.test(reference) && !ROW_RANGE.test(reference)) return null;
|
|
219
|
+
return reference.replace(/[A-Z]+/gi, (column)=>column.toUpperCase());
|
|
220
|
+
});
|
|
221
|
+
return normalized.every((part)=>Boolean(part)) ? normalized.join(',') : null;
|
|
222
|
+
}
|
|
223
|
+
function normalizeSpreadsheetPrintTitleRows(value) {
|
|
224
|
+
const match = ROW_RANGE.exec(unqualifiedReference(value.trim().replace(/^=/, '')));
|
|
225
|
+
if (!match) return null;
|
|
226
|
+
const start = Number(match[1]);
|
|
227
|
+
const end = Number(match[2]);
|
|
228
|
+
return `$${Math.min(start, end)}:$${Math.max(start, end)}`;
|
|
229
|
+
}
|
|
230
|
+
function normalizeSpreadsheetPrintTitleColumns(value) {
|
|
231
|
+
const match = COLUMN_RANGE.exec(unqualifiedReference(value.trim().replace(/^=/, '')));
|
|
232
|
+
if (!match) return null;
|
|
233
|
+
const start = decodeColumn(match[1]);
|
|
234
|
+
const end = decodeColumn(match[2]);
|
|
235
|
+
return `$${encodeColumn(Math.min(start, end))}:$${encodeColumn(Math.max(start, end))}`;
|
|
236
|
+
}
|
|
237
|
+
function parseSpreadsheetPrintTitles(value) {
|
|
238
|
+
let rows;
|
|
239
|
+
let columns;
|
|
240
|
+
for (const part of splitRangeList(value.trim().replace(/^=/, ''))){
|
|
241
|
+
const rowReference = normalizeSpreadsheetPrintTitleRows(part);
|
|
242
|
+
if (rowReference) {
|
|
243
|
+
if (rows) return null;
|
|
244
|
+
const match = ROW_RANGE.exec(rowReference);
|
|
245
|
+
rows = [
|
|
246
|
+
Number(match[1]) - 1,
|
|
247
|
+
Number(match[2]) - 1
|
|
248
|
+
];
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const columnReference = normalizeSpreadsheetPrintTitleColumns(part);
|
|
252
|
+
if (!columnReference || columns) return null;
|
|
253
|
+
const match = COLUMN_RANGE.exec(columnReference);
|
|
254
|
+
columns = [
|
|
255
|
+
decodeColumn(match[1]),
|
|
256
|
+
decodeColumn(match[2])
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
return rows || columns ? {
|
|
260
|
+
rows,
|
|
261
|
+
columns
|
|
262
|
+
} : null;
|
|
263
|
+
}
|
|
264
|
+
function parseSpreadsheetCellRanges(value) {
|
|
265
|
+
const parts = splitRangeList(value.trim().replace(/^=/, ''));
|
|
266
|
+
if (!parts.length) return null;
|
|
267
|
+
const ranges = parts.map((part)=>{
|
|
268
|
+
const cells = CELL_OR_RANGE.exec(unqualifiedReference(part));
|
|
269
|
+
if (!cells) return null;
|
|
270
|
+
const first = decodeCell(cells[1], cells[2]);
|
|
271
|
+
const second = cells[3] && cells[4] ? decodeCell(cells[3], cells[4]) : first;
|
|
272
|
+
return {
|
|
273
|
+
row: [
|
|
274
|
+
Math.min(first.row, second.row),
|
|
275
|
+
Math.max(first.row, second.row)
|
|
276
|
+
],
|
|
277
|
+
column: [
|
|
278
|
+
Math.min(first.column, second.column),
|
|
279
|
+
Math.max(first.column, second.column)
|
|
280
|
+
]
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
return ranges.every((range)=>Boolean(range)) ? ranges : null;
|
|
284
|
+
}
|
|
285
|
+
function formatSpreadsheetCellRanges(ranges) {
|
|
286
|
+
return ranges.map((range)=>{
|
|
287
|
+
const start = encodeCell(range.row[0], range.column[0]);
|
|
288
|
+
const end = encodeCell(range.row[1], range.column[1]);
|
|
289
|
+
return start === end ? start : `${start}:${end}`;
|
|
290
|
+
}).join(',');
|
|
291
|
+
}
|
|
292
|
+
function qualifySpreadsheetRange(value, sheetName) {
|
|
293
|
+
const prefix = `'${sheetName.replaceAll("'", "''")}'!`;
|
|
294
|
+
return splitRangeList(value).map((part)=>part.includes('!') ? part.trim() : `${prefix}${part.trim()}`).join(',');
|
|
295
|
+
}
|
|
296
|
+
function stripSpreadsheetSheetQualifier(value, sheetName) {
|
|
297
|
+
const quotedPrefix = `'${sheetName.replaceAll("'", "''")}'!`;
|
|
298
|
+
const plainPrefix = `${sheetName}!`;
|
|
299
|
+
return splitRangeList(value.trim().replace(/^=/, '')).map((part)=>{
|
|
300
|
+
const reference = part.trim();
|
|
301
|
+
if (reference.toLowerCase().startsWith(quotedPrefix.toLowerCase())) return reference.slice(quotedPrefix.length);
|
|
302
|
+
if (reference.toLowerCase().startsWith(plainPrefix.toLowerCase())) return reference.slice(plainPrefix.length);
|
|
303
|
+
return reference;
|
|
304
|
+
}).join(',');
|
|
305
|
+
}
|
|
306
|
+
function splitRangeList(value) {
|
|
307
|
+
const parts = [];
|
|
308
|
+
let current = '';
|
|
309
|
+
let quoted = false;
|
|
310
|
+
for(let index = 0; index < value.length; index += 1){
|
|
311
|
+
const character = value[index];
|
|
312
|
+
if ("'" === character) {
|
|
313
|
+
if (quoted && "'" === value[index + 1]) {
|
|
314
|
+
current += "''";
|
|
315
|
+
index += 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
quoted = !quoted;
|
|
319
|
+
}
|
|
320
|
+
if (',' !== character || quoted) current += character;
|
|
321
|
+
else {
|
|
322
|
+
if (current.trim()) parts.push(current.trim());
|
|
323
|
+
current = '';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (current.trim()) parts.push(current.trim());
|
|
327
|
+
return parts;
|
|
328
|
+
}
|
|
329
|
+
function unqualifiedReference(value) {
|
|
330
|
+
const reference = value.trim().replace(/^=/, '');
|
|
331
|
+
const separator = reference.lastIndexOf('!');
|
|
332
|
+
return separator >= 0 ? reference.slice(separator + 1) : reference;
|
|
333
|
+
}
|
|
334
|
+
function decodeCell(column, row) {
|
|
335
|
+
return {
|
|
336
|
+
row: Math.max(0, Number(row) - 1),
|
|
337
|
+
column: decodeColumn(column)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function decodeColumn(value) {
|
|
341
|
+
let result = 0;
|
|
342
|
+
for (const character of value.toUpperCase())result = 26 * result + character.charCodeAt(0) - 64;
|
|
343
|
+
return Math.max(0, result - 1);
|
|
344
|
+
}
|
|
345
|
+
function encodeCell(row, column) {
|
|
346
|
+
return `${encodeColumn(column)}${Math.max(0, row) + 1}`;
|
|
347
|
+
}
|
|
348
|
+
function encodeColumn(column) {
|
|
349
|
+
let value = Math.max(0, column) + 1;
|
|
350
|
+
let label = '';
|
|
351
|
+
while(value > 0){
|
|
352
|
+
value -= 1;
|
|
353
|
+
label = String.fromCharCode(65 + value % 26) + label;
|
|
354
|
+
value = Math.floor(value / 26);
|
|
355
|
+
}
|
|
356
|
+
return label;
|
|
357
|
+
}
|
|
358
|
+
function spreadsheetPivotCellValue(cell) {
|
|
359
|
+
return cell?.v ?? cell?.m ?? null;
|
|
360
|
+
}
|
|
361
|
+
function normalizeSpreadsheetPivotFilterValue(value) {
|
|
362
|
+
if (null == value || '' === value) return null;
|
|
363
|
+
if (value instanceof Date) return value.toISOString();
|
|
364
|
+
if ('number' == typeof value) return Number.isFinite(value) ? value : String(value);
|
|
365
|
+
if ('boolean' == typeof value) return value;
|
|
366
|
+
return String(value);
|
|
367
|
+
}
|
|
368
|
+
function spreadsheetPivotFilterValueKey(value) {
|
|
369
|
+
if (null === value) return 'blank:';
|
|
370
|
+
return `${typeof value}:${String(value)}`;
|
|
371
|
+
}
|
|
372
|
+
function displaySpreadsheetPivotValue(value) {
|
|
373
|
+
if (null == value || '' === value) return '(空白)';
|
|
374
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
375
|
+
if ('boolean' == typeof value) return value ? 'TRUE' : 'FALSE';
|
|
376
|
+
return String(value);
|
|
377
|
+
}
|
|
378
|
+
function finiteSpreadsheetPivotNumber(value) {
|
|
379
|
+
if ('number' == typeof value) return Number.isFinite(value) ? value : null;
|
|
380
|
+
if (value instanceof Date) return value.getTime();
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
function spreadsheetPivotFilterItemsFromSource(sourceSheet, sourceBounds, fieldIndex) {
|
|
384
|
+
if (fieldIndex < 0 || fieldIndex > sourceBounds.endColumn - sourceBounds.startColumn) return [];
|
|
385
|
+
const items = new Map();
|
|
386
|
+
for(let row = sourceBounds.startRow + 1; row <= sourceBounds.endRow; row += 1){
|
|
387
|
+
const value = normalizeSpreadsheetPivotFilterValue(spreadsheetPivotCellValue(sourceSheet.data?.[row]?.[sourceBounds.startColumn + fieldIndex]));
|
|
388
|
+
const item = {
|
|
389
|
+
value,
|
|
390
|
+
label: displaySpreadsheetPivotValue(value)
|
|
391
|
+
};
|
|
392
|
+
items.set(spreadsheetPivotFilterValueKey(value), item);
|
|
393
|
+
}
|
|
394
|
+
return Array.from(items.values()).sort((left, right)=>left.label.localeCompare(right.label, 'zh-CN', {
|
|
395
|
+
numeric: true,
|
|
396
|
+
sensitivity: 'base'
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
function buildSpreadsheetPivotOutput(sourceSheet, sourceBounds, fields, pivot) {
|
|
400
|
+
const rowCombinations = new Map();
|
|
401
|
+
const columnCombinations = new Map();
|
|
402
|
+
const cells = new Map();
|
|
403
|
+
const rowTotals = new Map();
|
|
404
|
+
const columnTotals = new Map();
|
|
405
|
+
const totals = pivot.values.map(()=>accumulator());
|
|
406
|
+
const singleColumn = combination([]);
|
|
407
|
+
if (!pivot.columnFields.length) columnCombinations.set(singleColumn.key, singleColumn);
|
|
408
|
+
for(let row = sourceBounds.startRow + 1; row <= sourceBounds.endRow; row += 1){
|
|
409
|
+
const values = fields.map((field)=>spreadsheetPivotCellValue(sourceSheet.data?.[row]?.[sourceBounds.startColumn + field.index]));
|
|
410
|
+
if (values.every((value)=>null == value || '' === value)) continue;
|
|
411
|
+
if (!matchesPivotReportFilters(values, pivot)) continue;
|
|
412
|
+
const rowCombination = combination(pivot.rowFields.map((fieldIndex)=>values[fieldIndex]));
|
|
413
|
+
const columnCombination = pivot.columnFields.length ? combination(pivot.columnFields.map((fieldIndex)=>values[fieldIndex])) : singleColumn;
|
|
414
|
+
rowCombinations.set(rowCombination.key, rowCombination);
|
|
415
|
+
columnCombinations.set(columnCombination.key, columnCombination);
|
|
416
|
+
for (const [valueIndex, valueField] of pivot.values.entries()){
|
|
417
|
+
const value = values[valueField.fieldIndex];
|
|
418
|
+
updateAccumulator(mapAccumulator(cells, cellKey(rowCombination.key, columnCombination.key, valueIndex)), value);
|
|
419
|
+
updateAccumulator(mapAccumulator(rowTotals, cellKey(rowCombination.key, '', valueIndex)), value);
|
|
420
|
+
updateAccumulator(mapAccumulator(columnTotals, cellKey('', columnCombination.key, valueIndex)), value);
|
|
421
|
+
updateAccumulator(totals[valueIndex], value);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const sortedRows = Array.from(rowCombinations.values()).sort(compareCombinations);
|
|
425
|
+
const sortedColumns = Array.from(columnCombinations.values()).sort(compareCombinations);
|
|
426
|
+
if (!sortedRows.length) return [];
|
|
427
|
+
const headers = [
|
|
428
|
+
...pivot.rowFields.map((fieldIndex)=>fields[fieldIndex].name),
|
|
429
|
+
...sortedColumns.flatMap((column)=>pivot.values.map((value)=>columnValueCaption(column, pivot.columnFields, fields, value))),
|
|
430
|
+
...pivot.rowGrandTotals && pivot.columnFields.length ? pivot.values.map((value)=>`总计 · ${valueCaption(value, fields)}`) : []
|
|
431
|
+
];
|
|
432
|
+
const output = [
|
|
433
|
+
headers.map((value)=>pivotHeaderCell(value))
|
|
434
|
+
];
|
|
435
|
+
for (const row of sortedRows){
|
|
436
|
+
const result = row.values.map((value)=>pivotDimensionCell(value));
|
|
437
|
+
for (const column of sortedColumns)for (const [valueIndex, value] of pivot.values.entries())result.push(pivotValueCell(accumulatorValue(cells.get(cellKey(row.key, column.key, valueIndex)), value.aggregation), false));
|
|
438
|
+
if (pivot.rowGrandTotals && pivot.columnFields.length) for (const [valueIndex, value] of pivot.values.entries())result.push(pivotValueCell(accumulatorValue(rowTotals.get(cellKey(row.key, '', valueIndex)), value.aggregation), true));
|
|
439
|
+
output.push(result);
|
|
440
|
+
}
|
|
441
|
+
if (pivot.columnGrandTotals) {
|
|
442
|
+
const result = pivot.rowFields.map((_field, index)=>0 === index ? pivotTotalLabelCell() : pivotValueCell(null, true));
|
|
443
|
+
for (const column of sortedColumns)for (const [valueIndex, value] of pivot.values.entries())result.push(pivotValueCell(accumulatorValue(columnTotals.get(cellKey('', column.key, valueIndex)), value.aggregation), true));
|
|
444
|
+
if (pivot.rowGrandTotals && pivot.columnFields.length) for (const [valueIndex, value] of pivot.values.entries())result.push(pivotValueCell(accumulatorValue(totals[valueIndex], value.aggregation), true));
|
|
445
|
+
output.push(result);
|
|
446
|
+
}
|
|
447
|
+
const reportFilters = pivot.reportFilters ?? [];
|
|
448
|
+
if (!reportFilters.length) return output;
|
|
449
|
+
const width = output[0]?.length ?? 0;
|
|
450
|
+
const filterRows = reportFilters.map((filter)=>{
|
|
451
|
+
const row = Array(width).fill(null);
|
|
452
|
+
row[0] = pivotFilterLabelCell(fields[filter.fieldIndex].name);
|
|
453
|
+
row[1] = pivotFilterSelectionCell(void 0 === filter.selectedItem ? '(全部)' : displaySpreadsheetPivotValue(filter.selectedItem));
|
|
454
|
+
return row;
|
|
455
|
+
});
|
|
456
|
+
filterRows.push(Array(width).fill(null));
|
|
457
|
+
return [
|
|
458
|
+
...filterRows,
|
|
459
|
+
...output
|
|
460
|
+
];
|
|
461
|
+
}
|
|
462
|
+
function accumulator() {
|
|
463
|
+
return {
|
|
464
|
+
nonEmptyCount: 0,
|
|
465
|
+
numberCount: 0,
|
|
466
|
+
sum: 0,
|
|
467
|
+
product: 1,
|
|
468
|
+
mean: 0,
|
|
469
|
+
m2: 0
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function mapAccumulator(map, key) {
|
|
473
|
+
const existing = map.get(key);
|
|
474
|
+
if (existing) return existing;
|
|
475
|
+
const next = accumulator();
|
|
476
|
+
map.set(key, next);
|
|
477
|
+
return next;
|
|
478
|
+
}
|
|
479
|
+
function updateAccumulator(state, value) {
|
|
480
|
+
if (null != value && '' !== value) state.nonEmptyCount += 1;
|
|
481
|
+
const number = finiteSpreadsheetPivotNumber(value);
|
|
482
|
+
if (null === number) return;
|
|
483
|
+
state.numberCount += 1;
|
|
484
|
+
state.sum += number;
|
|
485
|
+
state.product *= number;
|
|
486
|
+
state.minimum = void 0 === state.minimum ? number : Math.min(state.minimum, number);
|
|
487
|
+
state.maximum = void 0 === state.maximum ? number : Math.max(state.maximum, number);
|
|
488
|
+
const delta = number - state.mean;
|
|
489
|
+
state.mean += delta / state.numberCount;
|
|
490
|
+
state.m2 += delta * (number - state.mean);
|
|
491
|
+
}
|
|
492
|
+
function accumulatorValue(state, aggregation) {
|
|
493
|
+
if (!state) return 'sum' === aggregation || 'count' === aggregation || 'counta' === aggregation ? 0 : null;
|
|
494
|
+
if ('count' === aggregation) return state.numberCount;
|
|
495
|
+
if ('counta' === aggregation) return state.nonEmptyCount;
|
|
496
|
+
if ('average' === aggregation) return state.numberCount ? state.sum / state.numberCount : null;
|
|
497
|
+
if ('max' === aggregation) return state.maximum ?? null;
|
|
498
|
+
if ('min' === aggregation) return state.minimum ?? null;
|
|
499
|
+
if ('product' === aggregation) return state.numberCount ? state.product : null;
|
|
500
|
+
if ('stdDev' === aggregation) return state.numberCount > 1 ? Math.sqrt(state.m2 / (state.numberCount - 1)) : null;
|
|
501
|
+
if ('stdDevP' === aggregation) return state.numberCount ? Math.sqrt(state.m2 / state.numberCount) : null;
|
|
502
|
+
if ('var' === aggregation) return state.numberCount > 1 ? state.m2 / (state.numberCount - 1) : null;
|
|
503
|
+
if ('varP' === aggregation) return state.numberCount ? state.m2 / state.numberCount : null;
|
|
504
|
+
return state.sum;
|
|
505
|
+
}
|
|
506
|
+
function combination(values) {
|
|
507
|
+
return {
|
|
508
|
+
key: JSON.stringify(values.map(normalizedPivotKeyValue)),
|
|
509
|
+
values
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function normalizedPivotKeyValue(value) {
|
|
513
|
+
if (value instanceof Date) return [
|
|
514
|
+
'date',
|
|
515
|
+
value.toISOString()
|
|
516
|
+
];
|
|
517
|
+
if ('number' == typeof value) return [
|
|
518
|
+
'number',
|
|
519
|
+
Number.isFinite(value) ? value : null
|
|
520
|
+
];
|
|
521
|
+
if ('boolean' == typeof value) return [
|
|
522
|
+
'boolean',
|
|
523
|
+
value
|
|
524
|
+
];
|
|
525
|
+
if (null == value || '' === value) return [
|
|
526
|
+
'blank'
|
|
527
|
+
];
|
|
528
|
+
return [
|
|
529
|
+
'string',
|
|
530
|
+
String(value)
|
|
531
|
+
];
|
|
532
|
+
}
|
|
533
|
+
function compareCombinations(left, right) {
|
|
534
|
+
const count = Math.max(left.values.length, right.values.length);
|
|
535
|
+
for(let index = 0; index < count; index += 1){
|
|
536
|
+
const difference = displaySpreadsheetPivotValue(left.values[index]).localeCompare(displaySpreadsheetPivotValue(right.values[index]), 'zh-CN', {
|
|
537
|
+
numeric: true,
|
|
538
|
+
sensitivity: 'base'
|
|
539
|
+
});
|
|
540
|
+
if (difference) return difference;
|
|
541
|
+
}
|
|
542
|
+
return 0;
|
|
543
|
+
}
|
|
544
|
+
function cellKey(rowKey, columnKey, valueIndex) {
|
|
545
|
+
return `${rowKey.length}:${rowKey}${columnKey.length}:${columnKey}${valueIndex}`;
|
|
546
|
+
}
|
|
547
|
+
function columnValueCaption(column, columnFields, fields, value) {
|
|
548
|
+
const dimensions = column.values.map((item, index)=>{
|
|
549
|
+
const label = displaySpreadsheetPivotValue(item);
|
|
550
|
+
return columnFields.length > 1 ? `${fields[columnFields[index]].name}: ${label}` : label;
|
|
551
|
+
});
|
|
552
|
+
return [
|
|
553
|
+
...dimensions,
|
|
554
|
+
valueCaption(value, fields)
|
|
555
|
+
].filter(Boolean).join(' · ');
|
|
556
|
+
}
|
|
557
|
+
function valueCaption(value, fields) {
|
|
558
|
+
return value.caption?.trim() || defaultPivotValueCaption(fields[value.fieldIndex]?.name ?? `字段 ${value.fieldIndex + 1}`, value.aggregation);
|
|
559
|
+
}
|
|
560
|
+
function defaultPivotValueCaption(fieldName, aggregation) {
|
|
561
|
+
return `${fieldName}(${spreadsheetPivotAggregationLabel(aggregation)})`;
|
|
562
|
+
}
|
|
563
|
+
function spreadsheetPivotAggregationLabel(aggregation) {
|
|
564
|
+
if ('count' === aggregation) return '计数';
|
|
565
|
+
if ('counta' === aggregation) return '非空计数';
|
|
566
|
+
if ('average' === aggregation) return '平均值';
|
|
567
|
+
if ('max' === aggregation) return '最大值';
|
|
568
|
+
if ('min' === aggregation) return '最小值';
|
|
569
|
+
if ('product' === aggregation) return '乘积';
|
|
570
|
+
if ('stdDev' === aggregation) return '样本标准差';
|
|
571
|
+
if ('stdDevP' === aggregation) return '总体标准差';
|
|
572
|
+
if ('var' === aggregation) return '样本方差';
|
|
573
|
+
if ('varP' === aggregation) return '总体方差';
|
|
574
|
+
return '求和';
|
|
575
|
+
}
|
|
576
|
+
function pivotHeaderCell(value) {
|
|
577
|
+
return {
|
|
578
|
+
v: value,
|
|
579
|
+
m: value,
|
|
580
|
+
bl: 1,
|
|
581
|
+
bg: '#e7f1ec',
|
|
582
|
+
fc: '#173d2b'
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function pivotDimensionCell(value) {
|
|
586
|
+
const display = displaySpreadsheetPivotValue(value);
|
|
587
|
+
return {
|
|
588
|
+
v: null == value || '' === value ? display : value,
|
|
589
|
+
m: display
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function pivotValueCell(value, total) {
|
|
593
|
+
if (null === value) return total ? {
|
|
594
|
+
v: '',
|
|
595
|
+
m: '',
|
|
596
|
+
bl: 1,
|
|
597
|
+
bg: '#f3f7f5'
|
|
598
|
+
} : null;
|
|
599
|
+
return {
|
|
600
|
+
v: value,
|
|
601
|
+
m: formatPivotNumber(value),
|
|
602
|
+
bl: total ? 1 : void 0,
|
|
603
|
+
bg: total ? '#f3f7f5' : void 0,
|
|
604
|
+
ht: 2
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function pivotTotalLabelCell() {
|
|
608
|
+
return {
|
|
609
|
+
v: '总计',
|
|
610
|
+
m: '总计',
|
|
611
|
+
bl: 1,
|
|
612
|
+
bg: '#f3f7f5'
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function pivotFilterLabelCell(value) {
|
|
616
|
+
return {
|
|
617
|
+
v: value,
|
|
618
|
+
m: value,
|
|
619
|
+
bl: 1,
|
|
620
|
+
bg: '#f3f7f5',
|
|
621
|
+
fc: '#173d2b'
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function pivotFilterSelectionCell(value) {
|
|
625
|
+
return {
|
|
626
|
+
v: value,
|
|
627
|
+
m: value,
|
|
628
|
+
bg: '#f3f7f5'
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function formatPivotNumber(value) {
|
|
632
|
+
return Number.isInteger(value) ? String(value) : String(Number(value.toPrecision(15)));
|
|
633
|
+
}
|
|
634
|
+
function matchesPivotReportFilters(values, pivot) {
|
|
635
|
+
return (pivot.reportFilters ?? []).every((filter)=>void 0 === filter.selectedItem || spreadsheetPivotFilterValueKey(normalizeSpreadsheetPivotFilterValue(values[filter.fieldIndex])) === spreadsheetPivotFilterValueKey(filter.selectedItem));
|
|
636
|
+
}
|
|
637
|
+
const MAXIMUM_SOURCE_CELLS = 100000;
|
|
638
|
+
const MAXIMUM_OUTPUT_CELLS = 20000;
|
|
639
|
+
const MAXIMUM_XLSX_ROW = 1048575;
|
|
640
|
+
const MAXIMUM_XLSX_COLUMN = 16383;
|
|
641
|
+
function spreadsheetPivotCount(content) {
|
|
642
|
+
return content.sheets.reduce((count, sheet)=>count + (sheet.pivotTables?.length ?? 0), 0);
|
|
643
|
+
}
|
|
644
|
+
function spreadsheetPivotFields(content, pivot) {
|
|
645
|
+
const sourceSheet = content.sheets.find((sheet)=>sheet.id === pivot.sourceSheetId);
|
|
646
|
+
const range = singleRange(pivot.sourceReference);
|
|
647
|
+
if (!sourceSheet || !range) return [];
|
|
648
|
+
const fields = [];
|
|
649
|
+
for(let column = range.startColumn; column <= range.endColumn; column += 1){
|
|
650
|
+
const header = spreadsheetPivotCellValue(sourceSheet.data?.[range.startRow]?.[column]);
|
|
651
|
+
if (null == header || '' === String(header).trim()) return [];
|
|
652
|
+
const name = displaySpreadsheetPivotValue(header).trim();
|
|
653
|
+
let numeric = false;
|
|
654
|
+
for(let row = range.startRow + 1; row <= range.endRow; row += 1){
|
|
655
|
+
const value = spreadsheetPivotCellValue(sourceSheet.data?.[row]?.[column]);
|
|
656
|
+
if (null != value && '' !== value) {
|
|
657
|
+
if (null !== finiteSpreadsheetPivotNumber(value)) {
|
|
658
|
+
numeric = true;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
fields.push({
|
|
664
|
+
index: column - range.startColumn,
|
|
665
|
+
name,
|
|
666
|
+
numeric
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return uniqueFieldNames(fields) ? fields : [];
|
|
670
|
+
}
|
|
671
|
+
function spreadsheetPivotFilterItems(content, pivot, fieldIndex) {
|
|
672
|
+
const sourceSheet = content.sheets.find((sheet)=>sheet.id === pivot.sourceSheetId);
|
|
673
|
+
const range = singleRange(pivot.sourceReference);
|
|
674
|
+
if (!sourceSheet || !range || fieldIndex < 0 || fieldIndex > range.endColumn - range.startColumn) return [];
|
|
675
|
+
return spreadsheetPivotFilterItemsFromSource(sourceSheet, range, fieldIndex);
|
|
676
|
+
}
|
|
677
|
+
function createSpreadsheetPivotFromSelection(content, sourceSheetId, selection) {
|
|
678
|
+
const sourceSheet = content.sheets.find((sheet)=>sheet.id === sourceSheetId);
|
|
679
|
+
const bounds = selectionBounds(selection);
|
|
680
|
+
if (!sourceSheet || !bounds || bounds.endRow <= bounds.startRow) return {
|
|
681
|
+
content,
|
|
682
|
+
error: '请选择包含标题行和至少一行数据的连续区域。'
|
|
683
|
+
};
|
|
684
|
+
if (spreadsheetPivotIntersects(sourceSheet, {
|
|
685
|
+
startRow: bounds.startRow,
|
|
686
|
+
endRow: bounds.endRow,
|
|
687
|
+
startColumn: bounds.startColumn,
|
|
688
|
+
endColumn: bounds.endColumn
|
|
689
|
+
}).length) return {
|
|
690
|
+
content,
|
|
691
|
+
error: '不能把现有数据透视表的输出区域作为新的透视表源。'
|
|
692
|
+
};
|
|
693
|
+
const sourceReference = formatSpreadsheetCellRanges([
|
|
694
|
+
{
|
|
695
|
+
row: [
|
|
696
|
+
bounds.startRow,
|
|
697
|
+
bounds.endRow
|
|
698
|
+
],
|
|
699
|
+
column: [
|
|
700
|
+
bounds.startColumn,
|
|
701
|
+
bounds.endColumn
|
|
702
|
+
]
|
|
703
|
+
}
|
|
704
|
+
]);
|
|
705
|
+
const draftPivot = {
|
|
706
|
+
id: createWorkId('pivot'),
|
|
707
|
+
name: nextPivotName(content),
|
|
708
|
+
sourceSheetId,
|
|
709
|
+
sourceReference,
|
|
710
|
+
anchor: 'A1',
|
|
711
|
+
rowFields: [],
|
|
712
|
+
columnFields: [],
|
|
713
|
+
reportFilters: [],
|
|
714
|
+
values: [],
|
|
715
|
+
rowGrandTotals: true,
|
|
716
|
+
columnGrandTotals: true,
|
|
717
|
+
styleName: 'PivotStyleLight16',
|
|
718
|
+
refreshOnLoad: true
|
|
719
|
+
};
|
|
720
|
+
const fields = spreadsheetPivotFields(content, draftPivot);
|
|
721
|
+
if (!fields.length) return {
|
|
722
|
+
content,
|
|
723
|
+
error: '透视表源区域的标题必须非空且不重复。'
|
|
724
|
+
};
|
|
725
|
+
const valueField = fields.find((field)=>field.numeric) ?? fields.at(-1);
|
|
726
|
+
const dimensions = fields.filter((field)=>field.index !== valueField.index);
|
|
727
|
+
const rowField = dimensions[0] ?? fields.find((field)=>field.index !== valueField.index);
|
|
728
|
+
if (!rowField) return {
|
|
729
|
+
content,
|
|
730
|
+
error: '透视表至少需要一个分类字段和一个值字段。'
|
|
731
|
+
};
|
|
732
|
+
const columnField = dimensions.find((field)=>field.index !== rowField.index);
|
|
733
|
+
draftPivot.rowFields = [
|
|
734
|
+
rowField.index
|
|
735
|
+
];
|
|
736
|
+
draftPivot.columnFields = columnField ? [
|
|
737
|
+
columnField.index
|
|
738
|
+
] : [];
|
|
739
|
+
draftPivot.values = [
|
|
740
|
+
{
|
|
741
|
+
fieldIndex: valueField.index,
|
|
742
|
+
aggregation: valueField.numeric ? 'sum' : 'counta',
|
|
743
|
+
caption: defaultPivotValueCaption(valueField.name, valueField.numeric ? 'sum' : 'counta')
|
|
744
|
+
}
|
|
745
|
+
];
|
|
746
|
+
const ownerSheetId = createWorkId('sheet');
|
|
747
|
+
const ownerSheet = {
|
|
748
|
+
id: ownerSheetId,
|
|
749
|
+
name: nextSheetName(content, '数据透视表'),
|
|
750
|
+
order: content.sheets.length,
|
|
751
|
+
status: 0,
|
|
752
|
+
row: 40,
|
|
753
|
+
column: 12,
|
|
754
|
+
data: Array.from({
|
|
755
|
+
length: 40
|
|
756
|
+
}, ()=>Array(12).fill(null)),
|
|
757
|
+
config: {},
|
|
758
|
+
pivotTables: [
|
|
759
|
+
draftPivot
|
|
760
|
+
]
|
|
761
|
+
};
|
|
762
|
+
const withReport = {
|
|
763
|
+
...content,
|
|
764
|
+
sheets: [
|
|
765
|
+
...content.sheets,
|
|
766
|
+
ownerSheet
|
|
767
|
+
]
|
|
768
|
+
};
|
|
769
|
+
const validation = spreadsheetPivotValidation(withReport, ownerSheetId, draftPivot);
|
|
770
|
+
if (!validation.valid) return {
|
|
771
|
+
content,
|
|
772
|
+
error: validation.message
|
|
773
|
+
};
|
|
774
|
+
const next = refreshSpreadsheetPivotTables(withReport);
|
|
775
|
+
return {
|
|
776
|
+
content: next,
|
|
777
|
+
ownerSheetId,
|
|
778
|
+
pivotId: draftPivot.id
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function refreshSpreadsheetPivotTables(content) {
|
|
782
|
+
if (!spreadsheetPivotCount(content)) return content;
|
|
783
|
+
const sheets = content.sheets.map((sheet)=>({
|
|
784
|
+
...sheet,
|
|
785
|
+
pivotTables: sheet.pivotTables?.map((pivot)=>({
|
|
786
|
+
...pivot,
|
|
787
|
+
rowFields: [
|
|
788
|
+
...pivot.rowFields
|
|
789
|
+
],
|
|
790
|
+
columnFields: [
|
|
791
|
+
...pivot.columnFields
|
|
792
|
+
],
|
|
793
|
+
reportFilters: pivot.reportFilters?.map((filter)=>({
|
|
794
|
+
...filter
|
|
795
|
+
})),
|
|
796
|
+
values: pivot.values.map((value)=>({
|
|
797
|
+
...value
|
|
798
|
+
}))
|
|
799
|
+
}))
|
|
800
|
+
}));
|
|
801
|
+
const next = {
|
|
802
|
+
...content,
|
|
803
|
+
sheets
|
|
804
|
+
};
|
|
805
|
+
for (const ownerSheet of sheets){
|
|
806
|
+
if (!ownerSheet.pivotTables?.length || !ownerSheet.id) continue;
|
|
807
|
+
let data = ownerSheet.data;
|
|
808
|
+
let dataChanged = false;
|
|
809
|
+
for (const pivot of ownerSheet.pivotTables){
|
|
810
|
+
const resolved = resolvePivot(next, ownerSheet.id, pivot);
|
|
811
|
+
if (resolved) {
|
|
812
|
+
if (!dataChanged) {
|
|
813
|
+
data = cloneCellMatrix(ownerSheet.data);
|
|
814
|
+
ownerSheet.data = data;
|
|
815
|
+
dataChanged = true;
|
|
816
|
+
}
|
|
817
|
+
clearRange(data, singleRange(pivot.outputReference ?? ''));
|
|
818
|
+
writePivotOutput(data, resolved.outputBounds, resolved.output);
|
|
819
|
+
pivot.outputReference = resolved.outputReference;
|
|
820
|
+
ownerSheet.row = Math.max(ownerSheet.row ?? 0, resolved.outputBounds.endRow + 1, data.length);
|
|
821
|
+
ownerSheet.column = Math.max(ownerSheet.column ?? 0, resolved.outputBounds.endColumn + 1);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return next;
|
|
826
|
+
}
|
|
827
|
+
function reconcileSpreadsheetPivots(content, changedSheets) {
|
|
828
|
+
const sourceById = new Map(content.sheets.flatMap((sheet)=>sheet.id ? [
|
|
829
|
+
[
|
|
830
|
+
sheet.id,
|
|
831
|
+
sheet
|
|
832
|
+
]
|
|
833
|
+
] : []));
|
|
834
|
+
const sheets = changedSheets.map((sheet)=>{
|
|
835
|
+
const source = sheet.id ? sourceById.get(sheet.id) : void 0;
|
|
836
|
+
return source?.pivotTables?.length ? {
|
|
837
|
+
...sheet,
|
|
838
|
+
pivotTables: source.pivotTables
|
|
839
|
+
} : sheet;
|
|
840
|
+
});
|
|
841
|
+
return refreshSpreadsheetPivotTables({
|
|
842
|
+
...content,
|
|
843
|
+
sheets
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function deleteSpreadsheetPivotTable(content, ownerSheetId, pivotId) {
|
|
847
|
+
const sheets = content.sheets.map((sheet)=>{
|
|
848
|
+
if (sheet.id !== ownerSheetId) return sheet;
|
|
849
|
+
const pivot = sheet.pivotTables?.find((candidate)=>candidate.id === pivotId);
|
|
850
|
+
if (!pivot) return sheet;
|
|
851
|
+
const data = cloneCellMatrix(sheet.data);
|
|
852
|
+
clearRange(data, singleRange(pivot.outputReference ?? ''));
|
|
853
|
+
const pivotTables = (sheet.pivotTables ?? []).filter((candidate)=>candidate.id !== pivotId);
|
|
854
|
+
return {
|
|
855
|
+
...sheet,
|
|
856
|
+
data,
|
|
857
|
+
pivotTables: pivotTables.length ? pivotTables : void 0
|
|
858
|
+
};
|
|
859
|
+
});
|
|
860
|
+
return {
|
|
861
|
+
...content,
|
|
862
|
+
sheets
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function spreadsheetPivotValidation(content, ownerSheetId, pivot) {
|
|
866
|
+
const failure = pivotFailure(content, ownerSheetId, pivot);
|
|
867
|
+
if (failure) return failure;
|
|
868
|
+
const resolved = resolvePivot(content, ownerSheetId, pivot);
|
|
869
|
+
return resolved ? {
|
|
870
|
+
valid: true,
|
|
871
|
+
outputReference: resolved.outputReference
|
|
872
|
+
} : {
|
|
873
|
+
valid: false,
|
|
874
|
+
code: 'pivot.invalid',
|
|
875
|
+
message: '无法根据当前字段和源数据生成透视表。'
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function spreadsheetPivotOutputContains(sheet, row, column) {
|
|
879
|
+
return (sheet.pivotTables ?? []).some((pivot)=>{
|
|
880
|
+
const bounds = singleRange(pivot.outputReference ?? '');
|
|
881
|
+
return Boolean(bounds && containsCell(bounds, row, column));
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
function spreadsheetPivotIntersects(sheet, range) {
|
|
885
|
+
return (sheet.pivotTables ?? []).filter((pivot)=>{
|
|
886
|
+
const bounds = singleRange(pivot.outputReference ?? '');
|
|
887
|
+
return Boolean(bounds && rangesOverlap(bounds, range));
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function resolvePivot(content, ownerSheetId, pivot) {
|
|
891
|
+
const failure = pivotFailure(content, ownerSheetId, pivot);
|
|
892
|
+
if (failure) return null;
|
|
893
|
+
const ownerSheet = content.sheets.find((sheet)=>sheet.id === ownerSheetId);
|
|
894
|
+
const sourceSheet = content.sheets.find((sheet)=>sheet.id === pivot.sourceSheetId);
|
|
895
|
+
const sourceBounds = singleRange(pivot.sourceReference);
|
|
896
|
+
const anchor = singleRange(pivot.anchor);
|
|
897
|
+
const fields = spreadsheetPivotFields(content, pivot);
|
|
898
|
+
const output = buildSpreadsheetPivotOutput(sourceSheet, sourceBounds, fields, pivot);
|
|
899
|
+
if (!output.length || !output[0]?.length) return null;
|
|
900
|
+
const outputBounds = {
|
|
901
|
+
startRow: anchor.startRow,
|
|
902
|
+
endRow: anchor.startRow + output.length - 1,
|
|
903
|
+
startColumn: anchor.startColumn,
|
|
904
|
+
endColumn: anchor.startColumn + output[0].length - 1
|
|
905
|
+
};
|
|
906
|
+
const outputReference = formatBounds(outputBounds);
|
|
907
|
+
if (output.length * output[0].length > MAXIMUM_OUTPUT_CELLS) return null;
|
|
908
|
+
if (outputBounds.endRow > MAXIMUM_XLSX_ROW || outputBounds.endColumn > MAXIMUM_XLSX_COLUMN) return null;
|
|
909
|
+
if (ownerSheet.id === sourceSheet.id && rangesOverlap(sourceBounds, outputBounds)) return null;
|
|
910
|
+
const oldOutput = singleRange(pivot.outputReference ?? '');
|
|
911
|
+
for (const other of ownerSheet.pivotTables ?? []){
|
|
912
|
+
if (other.id === pivot.id) continue;
|
|
913
|
+
const otherOutput = singleRange(other.outputReference ?? '');
|
|
914
|
+
if (otherOutput && rangesOverlap(otherOutput, outputBounds)) return null;
|
|
915
|
+
}
|
|
916
|
+
for(let row = outputBounds.startRow; row <= outputBounds.endRow; row += 1)for(let column = outputBounds.startColumn; column <= outputBounds.endColumn; column += 1)if (!(oldOutput && containsCell(oldOutput, row, column))) {
|
|
917
|
+
if (ownerSheet.data?.[row]?.[column]) return null;
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
ownerSheet,
|
|
921
|
+
sourceSheet,
|
|
922
|
+
sourceBounds,
|
|
923
|
+
outputBounds,
|
|
924
|
+
outputReference,
|
|
925
|
+
fields,
|
|
926
|
+
output
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function pivotFailure(content, ownerSheetId, pivot) {
|
|
930
|
+
const ownerSheet = content.sheets.find((sheet)=>sheet.id === ownerSheetId);
|
|
931
|
+
const sourceSheet = content.sheets.find((sheet)=>sheet.id === pivot.sourceSheetId);
|
|
932
|
+
if (!ownerSheet || !sourceSheet) return invalid('pivot.source-sheet-missing', '找不到透视表的源工作表或目标工作表。');
|
|
933
|
+
const sourceBounds = singleRange(pivot.sourceReference);
|
|
934
|
+
if (!sourceBounds || sourceBounds.endRow <= sourceBounds.startRow) return invalid('pivot.source-reference-invalid', '源区域必须是包含标题和数据的连续单元格范围。');
|
|
935
|
+
const sourceCellCount = (sourceBounds.endRow - sourceBounds.startRow + 1) * (sourceBounds.endColumn - sourceBounds.startColumn + 1);
|
|
936
|
+
if (sourceCellCount > MAXIMUM_SOURCE_CELLS) return invalid('pivot.source-too-large', `源区域不能超过 ${MAXIMUM_SOURCE_CELLS.toLocaleString()} 个单元格。`);
|
|
937
|
+
const anchor = singleRange(pivot.anchor);
|
|
938
|
+
if (!anchor || anchor.startRow !== anchor.endRow || anchor.startColumn !== anchor.endColumn) return invalid('pivot.anchor-invalid', '输出位置必须是一个 A1 单元格地址。');
|
|
939
|
+
const fields = spreadsheetPivotFields(content, pivot);
|
|
940
|
+
if (!fields.length) return invalid('pivot.headers-invalid', '源区域的标题必须非空且不重复。');
|
|
941
|
+
const reportFilters = pivot.reportFilters ?? [];
|
|
942
|
+
const reportFilterFields = reportFilters.map((filter)=>filter.fieldIndex);
|
|
943
|
+
if (new Set(reportFilterFields).size !== reportFilterFields.length || reportFilterFields.some((index)=>!fields[index]) || reportFilterFields.some((index)=>pivot.rowFields.includes(index) || pivot.columnFields.includes(index))) return invalid('pivot.filters-invalid', '报表筛选字段必须存在、互不重复,且不能同时作为行或列字段。');
|
|
944
|
+
for (const filter of reportFilters)if (void 0 !== filter.selectedItem && !spreadsheetPivotFilterItems(content, pivot, filter.fieldIndex).some((item)=>spreadsheetPivotFilterValueKey(item.value) === spreadsheetPivotFilterValueKey(filter.selectedItem))) return invalid('pivot.filter-item-invalid', '报表筛选器选择的项目已不在源数据中。');
|
|
945
|
+
const dimensionFields = [
|
|
946
|
+
...pivot.rowFields,
|
|
947
|
+
...pivot.columnFields,
|
|
948
|
+
...reportFilterFields
|
|
949
|
+
];
|
|
950
|
+
if (!pivot.rowFields.length) return invalid('pivot.rows-empty', '至少选择一个行字段。');
|
|
951
|
+
if (!pivot.values.length) return invalid('pivot.values-empty', '至少选择一个值字段。');
|
|
952
|
+
if (new Set(dimensionFields).size !== dimensionFields.length || dimensionFields.some((index)=>!fields[index]) || pivot.values.some((value)=>!fields[value.fieldIndex])) return invalid('pivot.fields-invalid', '透视表包含重复、缺失或超出源区域的字段。');
|
|
953
|
+
const output = buildSpreadsheetPivotOutput(sourceSheet, sourceBounds, fields, pivot);
|
|
954
|
+
if (!output.length || !output[0]?.length) return invalid('pivot.output-empty', '当前源数据不能生成透视表结果。');
|
|
955
|
+
if (output.length * output[0].length > MAXIMUM_OUTPUT_CELLS) return invalid('pivot.output-too-large', `透视表结果不能超过 ${MAXIMUM_OUTPUT_CELLS.toLocaleString()} 个单元格。`);
|
|
956
|
+
const outputBounds = {
|
|
957
|
+
startRow: anchor.startRow,
|
|
958
|
+
endRow: anchor.startRow + output.length - 1,
|
|
959
|
+
startColumn: anchor.startColumn,
|
|
960
|
+
endColumn: anchor.startColumn + output[0].length - 1
|
|
961
|
+
};
|
|
962
|
+
if (outputBounds.endRow > MAXIMUM_XLSX_ROW || outputBounds.endColumn > MAXIMUM_XLSX_COLUMN) return invalid('pivot.output-out-of-bounds', '透视表结果超出 XLSX 工作表边界。');
|
|
963
|
+
if (ownerSheet.id === sourceSheet.id && rangesOverlap(sourceBounds, outputBounds)) return invalid('pivot.output-overlaps-source', '透视表输出区域与源数据重叠,请改用其他位置或工作表。');
|
|
964
|
+
if (Object.values(ownerSheet.config?.merge ?? {}).some((merge)=>rangesOverlap(outputBounds, {
|
|
965
|
+
startRow: merge.r,
|
|
966
|
+
endRow: merge.r + merge.rs - 1,
|
|
967
|
+
startColumn: merge.c,
|
|
968
|
+
endColumn: merge.c + merge.cs - 1
|
|
969
|
+
}))) return invalid('pivot.output-overlaps-merge', '透视表输出区域与合并单元格重叠。');
|
|
970
|
+
const oldOutput = singleRange(pivot.outputReference ?? '');
|
|
971
|
+
for (const other of ownerSheet.pivotTables ?? []){
|
|
972
|
+
if (other.id === pivot.id) continue;
|
|
973
|
+
const otherOutput = singleRange(other.outputReference ?? '');
|
|
974
|
+
if (otherOutput && rangesOverlap(otherOutput, outputBounds)) return invalid('pivot.output-overlaps-pivot', '透视表输出区域与另一个透视表重叠。');
|
|
975
|
+
}
|
|
976
|
+
for(let row = outputBounds.startRow; row <= outputBounds.endRow; row += 1)for(let column = outputBounds.startColumn; column <= outputBounds.endColumn; column += 1)if (!(oldOutput && containsCell(oldOutput, row, column))) {
|
|
977
|
+
if (ownerSheet.data?.[row]?.[column]) return invalid('pivot.output-not-empty', '透视表输出区域包含现有内容,请选择空白位置。');
|
|
978
|
+
}
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
function uniqueFieldNames(fields) {
|
|
982
|
+
const names = fields.map((field)=>field.name.trim().toLocaleLowerCase());
|
|
983
|
+
return names.every(Boolean) && new Set(names).size === names.length;
|
|
984
|
+
}
|
|
985
|
+
function singleRange(reference) {
|
|
986
|
+
const ranges = parseSpreadsheetCellRanges(reference);
|
|
987
|
+
if (ranges?.length !== 1) return null;
|
|
988
|
+
return {
|
|
989
|
+
startRow: ranges[0].row[0],
|
|
990
|
+
endRow: ranges[0].row[1],
|
|
991
|
+
startColumn: ranges[0].column[0],
|
|
992
|
+
endColumn: ranges[0].column[1]
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function selectionBounds(selection) {
|
|
996
|
+
if (selection.row.length < 2 || selection.column.length < 2) return null;
|
|
997
|
+
return {
|
|
998
|
+
startRow: Math.max(0, Math.min(selection.row[0], selection.row[1])),
|
|
999
|
+
endRow: Math.max(0, Math.max(selection.row[0], selection.row[1])),
|
|
1000
|
+
startColumn: Math.max(0, Math.min(selection.column[0], selection.column[1])),
|
|
1001
|
+
endColumn: Math.max(0, Math.max(selection.column[0], selection.column[1]))
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function containsCell(bounds, row, column) {
|
|
1005
|
+
return row >= bounds.startRow && row <= bounds.endRow && column >= bounds.startColumn && column <= bounds.endColumn;
|
|
1006
|
+
}
|
|
1007
|
+
function rangesOverlap(left, right) {
|
|
1008
|
+
return left.startRow <= right.endRow && left.endRow >= right.startRow && left.startColumn <= right.endColumn && left.endColumn >= right.startColumn;
|
|
1009
|
+
}
|
|
1010
|
+
function formatBounds(bounds) {
|
|
1011
|
+
return formatSpreadsheetCellRanges([
|
|
1012
|
+
{
|
|
1013
|
+
row: [
|
|
1014
|
+
bounds.startRow,
|
|
1015
|
+
bounds.endRow
|
|
1016
|
+
],
|
|
1017
|
+
column: [
|
|
1018
|
+
bounds.startColumn,
|
|
1019
|
+
bounds.endColumn
|
|
1020
|
+
]
|
|
1021
|
+
}
|
|
1022
|
+
]);
|
|
1023
|
+
}
|
|
1024
|
+
function cloneCellMatrix(source) {
|
|
1025
|
+
return (source ?? []).map((row)=>[
|
|
1026
|
+
...row
|
|
1027
|
+
]);
|
|
1028
|
+
}
|
|
1029
|
+
function clearRange(data, bounds) {
|
|
1030
|
+
if (!bounds) return;
|
|
1031
|
+
for(let row = bounds.startRow; row <= bounds.endRow; row += 1)if (data[row]) for(let column = bounds.startColumn; column <= bounds.endColumn; column += 1)data[row][column] = null;
|
|
1032
|
+
}
|
|
1033
|
+
function writePivotOutput(data, bounds, output) {
|
|
1034
|
+
const width = output[0]?.length ?? 0;
|
|
1035
|
+
while(data.length <= bounds.endRow)data.push([]);
|
|
1036
|
+
for(let row = 0; row < output.length; row += 1){
|
|
1037
|
+
const target = data[bounds.startRow + row] ?? [];
|
|
1038
|
+
data[bounds.startRow + row] = target;
|
|
1039
|
+
while(target.length < bounds.startColumn + width)target.push(null);
|
|
1040
|
+
for(let column = 0; column < width; column += 1)target[bounds.startColumn + column] = output[row][column] ?? null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function nextSheetName(content, base) {
|
|
1044
|
+
const existing = new Set(content.sheets.map((sheet)=>sheet.name.toLocaleLowerCase()));
|
|
1045
|
+
if (!existing.has(base.toLocaleLowerCase())) return base;
|
|
1046
|
+
let index = 2;
|
|
1047
|
+
while(existing.has(`${base} ${index}`.toLocaleLowerCase()))index += 1;
|
|
1048
|
+
return `${base} ${index}`;
|
|
1049
|
+
}
|
|
1050
|
+
function nextPivotName(content) {
|
|
1051
|
+
const existing = new Set(content.sheets.flatMap((sheet)=>(sheet.pivotTables ?? []).map((pivot)=>pivot.name.toLocaleLowerCase())));
|
|
1052
|
+
let index = 1;
|
|
1053
|
+
while(existing.has(`pivottable${index}`))index += 1;
|
|
1054
|
+
return `PivotTable${index}`;
|
|
1055
|
+
}
|
|
1056
|
+
function invalid(code, message) {
|
|
1057
|
+
return {
|
|
1058
|
+
valid: false,
|
|
1059
|
+
code,
|
|
1060
|
+
message
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
class OoxmlPackage {
|
|
1064
|
+
zip;
|
|
1065
|
+
constructor(zip){
|
|
1066
|
+
this.zip = zip;
|
|
1067
|
+
}
|
|
1068
|
+
static async load(buffer) {
|
|
1069
|
+
return new OoxmlPackage(await jszip.loadAsync(buffer));
|
|
1070
|
+
}
|
|
1071
|
+
has(partPath) {
|
|
1072
|
+
return Boolean(this.zip.file(partPath));
|
|
1073
|
+
}
|
|
1074
|
+
paths(prefix) {
|
|
1075
|
+
return Object.keys(this.zip.files).filter((path)=>path.startsWith(prefix) && !this.zip.files[path]?.dir);
|
|
1076
|
+
}
|
|
1077
|
+
async text(partPath) {
|
|
1078
|
+
const entry = this.zip.file(partPath);
|
|
1079
|
+
if (!entry) throw new Error(`Office package part is missing: ${partPath}`);
|
|
1080
|
+
return entry.async('text');
|
|
1081
|
+
}
|
|
1082
|
+
async xml(partPath) {
|
|
1083
|
+
return parseXml(await this.text(partPath), partPath);
|
|
1084
|
+
}
|
|
1085
|
+
async bytes(partPath) {
|
|
1086
|
+
const entry = this.zip.file(partPath);
|
|
1087
|
+
if (!entry) throw new Error(`Office package part is missing: ${partPath}`);
|
|
1088
|
+
return entry.async('uint8array');
|
|
1089
|
+
}
|
|
1090
|
+
async relationships(sourcePart) {
|
|
1091
|
+
const partPath = relationshipsPartPath(sourcePart);
|
|
1092
|
+
if (!this.has(partPath)) return new Map();
|
|
1093
|
+
const document = await this.xml(partPath);
|
|
1094
|
+
return new Map(descendants(document, 'Relationship').map((element)=>{
|
|
1095
|
+
const relationship = {
|
|
1096
|
+
id: attribute(element, 'Id') ?? '',
|
|
1097
|
+
target: resolvePartTarget(sourcePart, attribute(element, 'Target') ?? ''),
|
|
1098
|
+
type: attribute(element, 'Type') ?? '',
|
|
1099
|
+
targetMode: attribute(element, 'TargetMode') ?? void 0
|
|
1100
|
+
};
|
|
1101
|
+
return [
|
|
1102
|
+
relationship.id,
|
|
1103
|
+
relationship
|
|
1104
|
+
];
|
|
1105
|
+
}));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function parseXml(source, label = 'Office XML') {
|
|
1109
|
+
const document = new DOMParser().parseFromString(source, 'application/xml');
|
|
1110
|
+
const error = descendants(document, 'parsererror')[0];
|
|
1111
|
+
if (error) throw new Error(`${label} is not valid XML: ${error.textContent?.trim() || 'parse error'}`);
|
|
1112
|
+
return document;
|
|
1113
|
+
}
|
|
1114
|
+
function attribute(element, name) {
|
|
1115
|
+
const direct = element.getAttribute(name);
|
|
1116
|
+
if (null !== direct) return direct;
|
|
1117
|
+
const localName = name.includes(':') ? name.slice(name.indexOf(':') + 1) : name;
|
|
1118
|
+
return Array.from(element.attributes).find((item)=>{
|
|
1119
|
+
const itemLocalName = item.localName.includes(':') ? item.localName.slice(item.localName.indexOf(':') + 1) : item.localName;
|
|
1120
|
+
return itemLocalName === localName && (!name.includes(':') || item.name === name);
|
|
1121
|
+
})?.value ?? null;
|
|
1122
|
+
}
|
|
1123
|
+
function xmlNamespacePrefix(element, namespace) {
|
|
1124
|
+
if (!namespace) return element.prefix;
|
|
1125
|
+
if ('function' == typeof element.lookupPrefix) {
|
|
1126
|
+
const prefix = element.lookupPrefix(namespace);
|
|
1127
|
+
if (prefix) return prefix;
|
|
1128
|
+
}
|
|
1129
|
+
let current = element;
|
|
1130
|
+
while(current){
|
|
1131
|
+
if (current.namespaceURI === namespace && current.prefix) return current.prefix;
|
|
1132
|
+
const declaration = Array.from(current.attributes).find((item)=>item.value === namespace && ('xmlns' === item.name || item.name.startsWith('xmlns:')));
|
|
1133
|
+
if (declaration?.name.startsWith('xmlns:')) return declaration.name.slice(6);
|
|
1134
|
+
current = current.parentElement;
|
|
1135
|
+
}
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
function directChildren(parent, localName) {
|
|
1139
|
+
return Array.from(parent.children).filter((element)=>!localName || element.localName === localName);
|
|
1140
|
+
}
|
|
1141
|
+
function directChild(parent, localName) {
|
|
1142
|
+
return directChildren(parent, localName)[0];
|
|
1143
|
+
}
|
|
1144
|
+
function descendants(parent, localName) {
|
|
1145
|
+
return Array.from(parent.querySelectorAll('*')).filter((element)=>element.localName === localName);
|
|
1146
|
+
}
|
|
1147
|
+
function firstDescendant(parent, localName) {
|
|
1148
|
+
if (!parent) return;
|
|
1149
|
+
return descendants(parent, localName)[0];
|
|
1150
|
+
}
|
|
1151
|
+
function childPath(parent, ...localNames) {
|
|
1152
|
+
let current = parent;
|
|
1153
|
+
for (const name of localNames){
|
|
1154
|
+
if (!current) return;
|
|
1155
|
+
current = directChild(current, name);
|
|
1156
|
+
}
|
|
1157
|
+
return current instanceof Element ? current : void 0;
|
|
1158
|
+
}
|
|
1159
|
+
function resolvePartTarget(sourcePart, target) {
|
|
1160
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target)) return target;
|
|
1161
|
+
const segments = target.startsWith('/') ? [] : sourcePart.split('/').slice(0, -1);
|
|
1162
|
+
for (const segment of target.replace(/^\/+/, '').split('/'))if (segment && '.' !== segment) if ('..' === segment) segments.pop();
|
|
1163
|
+
else segments.push(segment);
|
|
1164
|
+
return segments.join('/');
|
|
1165
|
+
}
|
|
1166
|
+
function contentTypeForPart(partPath) {
|
|
1167
|
+
const extension = partPath.split('.').pop()?.toLowerCase();
|
|
1168
|
+
const types = {
|
|
1169
|
+
apng: 'image/apng',
|
|
1170
|
+
bmp: 'image/bmp',
|
|
1171
|
+
emf: 'image/emf',
|
|
1172
|
+
gif: 'image/gif',
|
|
1173
|
+
jpeg: 'image/jpeg',
|
|
1174
|
+
jpg: 'image/jpeg',
|
|
1175
|
+
png: 'image/png',
|
|
1176
|
+
svg: 'image/svg+xml',
|
|
1177
|
+
tif: 'image/tiff',
|
|
1178
|
+
tiff: 'image/tiff',
|
|
1179
|
+
webp: 'image/webp',
|
|
1180
|
+
wmf: 'image/wmf'
|
|
1181
|
+
};
|
|
1182
|
+
return types[extension ?? ''] ?? 'application/octet-stream';
|
|
1183
|
+
}
|
|
1184
|
+
function bytesToDataUrl(bytes, contentType) {
|
|
1185
|
+
let binary = '';
|
|
1186
|
+
const chunkSize = 32768;
|
|
1187
|
+
for(let offset = 0; offset < bytes.length; offset += chunkSize)binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
|
|
1188
|
+
return `data:${contentType};base64,${btoa(binary)}`;
|
|
1189
|
+
}
|
|
1190
|
+
function relationshipsPartPath(sourcePart) {
|
|
1191
|
+
const separator = sourcePart.lastIndexOf('/');
|
|
1192
|
+
const directory = separator >= 0 ? sourcePart.slice(0, separator + 1) : '';
|
|
1193
|
+
const fileName = separator >= 0 ? sourcePart.slice(separator + 1) : sourcePart;
|
|
1194
|
+
return `${directory}_rels/${fileName}.rels`;
|
|
1195
|
+
}
|
|
1196
|
+
const WORK_SPREADSHEET_CHART_COLORS = [
|
|
1197
|
+
'#4f6bed',
|
|
1198
|
+
'#e66c37',
|
|
1199
|
+
'#8f95a3',
|
|
1200
|
+
'#f4b41a',
|
|
1201
|
+
'#36a2ae',
|
|
1202
|
+
'#6a9f4e'
|
|
1203
|
+
];
|
|
1204
|
+
function finiteChartNumber(value) {
|
|
1205
|
+
return Number.isFinite(value) ? value : 0;
|
|
1206
|
+
}
|
|
1207
|
+
function compactChartNumber(value) {
|
|
1208
|
+
const absolute = Math.abs(value);
|
|
1209
|
+
if (absolute >= 1000000) return `${roundChartNumber(value / 1000000)}M`;
|
|
1210
|
+
if (absolute >= 1000) return `${roundChartNumber(value / 1000)}K`;
|
|
1211
|
+
return String(roundChartNumber(value));
|
|
1212
|
+
}
|
|
1213
|
+
function truncateChartText(value, length) {
|
|
1214
|
+
const text = String(value);
|
|
1215
|
+
return text.length > length ? `${text.slice(0, length - 1)}…` : text;
|
|
1216
|
+
}
|
|
1217
|
+
function roundChartNumber(value) {
|
|
1218
|
+
return Math.round(100 * value) / 100;
|
|
1219
|
+
}
|
|
1220
|
+
function escapeChartXml(value) {
|
|
1221
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
1222
|
+
}
|
|
1223
|
+
function normalizeWorkSpreadsheetChartLegendPosition(value) {
|
|
1224
|
+
return 'left' === value || 'top' === value || 'bottom' === value || 'topRight' === value || 'right' === value ? value : 'right';
|
|
1225
|
+
}
|
|
1226
|
+
function normalizeWorkSpreadsheetChartLegendOverlay(value) {
|
|
1227
|
+
return true === value;
|
|
1228
|
+
}
|
|
1229
|
+
function workSpreadsheetChartSupportsGrouping(type) {
|
|
1230
|
+
return 'column' === type || 'bar' === type || 'line' === type || 'area' === type;
|
|
1231
|
+
}
|
|
1232
|
+
function workSpreadsheetChartSupportsBarSpacing(type) {
|
|
1233
|
+
return 'column' === type || 'bar' === type;
|
|
1234
|
+
}
|
|
1235
|
+
function workSpreadsheetChartSupportsSmoothLines(type) {
|
|
1236
|
+
return 'line' === type;
|
|
1237
|
+
}
|
|
1238
|
+
function workSpreadsheetChartDefaultGrouping(type) {
|
|
1239
|
+
return workSpreadsheetChartSupportsBarSpacing(type) ? 'clustered' : 'standard';
|
|
1240
|
+
}
|
|
1241
|
+
function normalizeWorkSpreadsheetChartGrouping(value, type) {
|
|
1242
|
+
const fallback = workSpreadsheetChartDefaultGrouping(type);
|
|
1243
|
+
if (!workSpreadsheetChartSupportsGrouping(type)) return fallback;
|
|
1244
|
+
if ('stacked' === value || 'percentStacked' === value) return value;
|
|
1245
|
+
if (workSpreadsheetChartSupportsBarSpacing(type)) return 'standard' === value || 'clustered' === value ? value : fallback;
|
|
1246
|
+
return 'standard' === value ? value : fallback;
|
|
1247
|
+
}
|
|
1248
|
+
function workSpreadsheetChartGroupingIsStacked(grouping) {
|
|
1249
|
+
return 'stacked' === grouping || 'percentStacked' === grouping;
|
|
1250
|
+
}
|
|
1251
|
+
function normalizeWorkSpreadsheetChartGapWidth(value) {
|
|
1252
|
+
return work_spreadsheet_chart_layout_normalizedInteger(value, 0, 500, 150);
|
|
1253
|
+
}
|
|
1254
|
+
function normalizeWorkSpreadsheetChartOverlap(value, grouping) {
|
|
1255
|
+
return work_spreadsheet_chart_layout_normalizedInteger(value, -100, 100, workSpreadsheetChartGroupingIsStacked(grouping) ? 100 : 0);
|
|
1256
|
+
}
|
|
1257
|
+
function normalizeWorkSpreadsheetChartSmoothLines(value) {
|
|
1258
|
+
return true === value;
|
|
1259
|
+
}
|
|
1260
|
+
function normalizeWorkSpreadsheetChartLayout(source) {
|
|
1261
|
+
const grouping = normalizeWorkSpreadsheetChartGrouping(source.grouping, source.type);
|
|
1262
|
+
return {
|
|
1263
|
+
legendPosition: normalizeWorkSpreadsheetChartLegendPosition(source.legendPosition),
|
|
1264
|
+
legendOverlay: normalizeWorkSpreadsheetChartLegendOverlay(source.legendOverlay),
|
|
1265
|
+
grouping: workSpreadsheetChartSupportsGrouping(source.type) ? grouping : void 0,
|
|
1266
|
+
gapWidth: workSpreadsheetChartSupportsBarSpacing(source.type) ? normalizeWorkSpreadsheetChartGapWidth(source.gapWidth) : void 0,
|
|
1267
|
+
overlap: workSpreadsheetChartSupportsBarSpacing(source.type) ? normalizeWorkSpreadsheetChartOverlap(source.overlap, grouping) : void 0,
|
|
1268
|
+
smoothLines: workSpreadsheetChartSupportsSmoothLines(source.type) ? normalizeWorkSpreadsheetChartSmoothLines(source.smoothLines) : void 0
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
function workSpreadsheetChartSupportsSeriesAnalysis(source) {
|
|
1272
|
+
if (!workSpreadsheetChartSupportsGrouping(source.type)) return true;
|
|
1273
|
+
return !workSpreadsheetChartGroupingIsStacked(normalizeWorkSpreadsheetChartGrouping(source.grouping, source.type));
|
|
1274
|
+
}
|
|
1275
|
+
function workSpreadsheetChartLegendPositionLabel(position) {
|
|
1276
|
+
if ('left' === position) return '左侧';
|
|
1277
|
+
if ('top' === position) return '顶部';
|
|
1278
|
+
if ('bottom' === position) return '底部';
|
|
1279
|
+
if ('topRight' === position) return '右上角';
|
|
1280
|
+
return '右侧';
|
|
1281
|
+
}
|
|
1282
|
+
function workSpreadsheetChartGroupingLabel(grouping) {
|
|
1283
|
+
if ('stacked' === grouping) return '堆积';
|
|
1284
|
+
if ('percentStacked' === grouping) return '百分比堆积';
|
|
1285
|
+
if ('standard' === grouping) return '标准';
|
|
1286
|
+
return '簇状';
|
|
1287
|
+
}
|
|
1288
|
+
function work_spreadsheet_chart_layout_normalizedInteger(value, minimum, maximum, fallback) {
|
|
1289
|
+
if (null == value || '' === value) return fallback;
|
|
1290
|
+
const number = Number(value);
|
|
1291
|
+
if (!Number.isFinite(number)) return fallback;
|
|
1292
|
+
return Math.min(maximum, Math.max(minimum, Math.round(number)));
|
|
1293
|
+
}
|
|
1294
|
+
const LINE_DASHES = new Set([
|
|
1295
|
+
'solid',
|
|
1296
|
+
'dash',
|
|
1297
|
+
'dot',
|
|
1298
|
+
'dashDot'
|
|
1299
|
+
]);
|
|
1300
|
+
const MARKER_SYMBOLS = new Set([
|
|
1301
|
+
'none',
|
|
1302
|
+
'circle',
|
|
1303
|
+
'square',
|
|
1304
|
+
'diamond',
|
|
1305
|
+
'triangle',
|
|
1306
|
+
'plus',
|
|
1307
|
+
'x',
|
|
1308
|
+
'star'
|
|
1309
|
+
]);
|
|
1310
|
+
function normalizeWorkSpreadsheetChartSeriesStyle(source) {
|
|
1311
|
+
if (!source || 'object' != typeof source) return;
|
|
1312
|
+
const fillColor = normalizeWorkSpreadsheetChartColor(source.fillColor);
|
|
1313
|
+
const fillTransparency = optionalBoundedNumber(source.fillTransparency, 0, 100, 0);
|
|
1314
|
+
const lineColor = normalizeWorkSpreadsheetChartColor(source.lineColor);
|
|
1315
|
+
const lineWidth = optionalBoundedNumber(source.lineWidth, 0.25, 20, 2.25);
|
|
1316
|
+
const lineDash = LINE_DASHES.has(source.lineDash) ? source.lineDash : void 0;
|
|
1317
|
+
const marker = normalizeWorkSpreadsheetChartMarkerStyle(source.marker);
|
|
1318
|
+
const normalized = {
|
|
1319
|
+
...fillColor ? {
|
|
1320
|
+
fillColor
|
|
1321
|
+
} : {},
|
|
1322
|
+
...void 0 !== fillTransparency ? {
|
|
1323
|
+
fillTransparency
|
|
1324
|
+
} : {},
|
|
1325
|
+
...lineColor ? {
|
|
1326
|
+
lineColor
|
|
1327
|
+
} : {},
|
|
1328
|
+
...void 0 !== lineWidth ? {
|
|
1329
|
+
lineWidth
|
|
1330
|
+
} : {},
|
|
1331
|
+
...lineDash ? {
|
|
1332
|
+
lineDash
|
|
1333
|
+
} : {},
|
|
1334
|
+
...marker ? {
|
|
1335
|
+
marker
|
|
1336
|
+
} : {}
|
|
1337
|
+
};
|
|
1338
|
+
return Object.keys(normalized).length ? normalized : void 0;
|
|
1339
|
+
}
|
|
1340
|
+
function normalizeWorkSpreadsheetChartColor(value) {
|
|
1341
|
+
if ('string' != typeof value) return;
|
|
1342
|
+
const text = value.trim().replace(/^#/, '');
|
|
1343
|
+
if (/^[0-9a-f]{3}$/i.test(text)) return `#${text.split('').map((character)=>character.repeat(2)).join('')}`.toUpperCase();
|
|
1344
|
+
return /^[0-9a-f]{6}$/i.test(text) ? `#${text.toUpperCase()}` : void 0;
|
|
1345
|
+
}
|
|
1346
|
+
function defaultWorkSpreadsheetChartSeriesStyle(seriesIndex) {
|
|
1347
|
+
const color = workSpreadsheetChartSeriesDefaultColor(seriesIndex);
|
|
1348
|
+
return {
|
|
1349
|
+
fillColor: color,
|
|
1350
|
+
fillTransparency: 0,
|
|
1351
|
+
lineColor: color,
|
|
1352
|
+
lineWidth: 2.5,
|
|
1353
|
+
lineDash: 'solid',
|
|
1354
|
+
marker: {
|
|
1355
|
+
symbol: 'circle',
|
|
1356
|
+
size: 5,
|
|
1357
|
+
fillColor: '#FFFFFF',
|
|
1358
|
+
lineColor: color
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
function workSpreadsheetChartSeriesDefaultColor(seriesIndex) {
|
|
1363
|
+
return WORK_SPREADSHEET_CHART_COLORS[seriesIndex % WORK_SPREADSHEET_CHART_COLORS.length].toUpperCase();
|
|
1364
|
+
}
|
|
1365
|
+
function spreadsheetChartSeriesFillStyle(series, seriesIndex, defaultOpacity = 1, defaultColor = workSpreadsheetChartSeriesDefaultColor(seriesIndex)) {
|
|
1366
|
+
const style = normalizeWorkSpreadsheetChartSeriesStyle(series.style);
|
|
1367
|
+
const color = style?.fillColor ?? defaultColor;
|
|
1368
|
+
const opacity = style?.fillTransparency === void 0 ? boundedNumber(defaultOpacity, 0, 1, 1) : 1 - style.fillTransparency / 100;
|
|
1369
|
+
return {
|
|
1370
|
+
color,
|
|
1371
|
+
opacity,
|
|
1372
|
+
attributes: `fill="${escapeChartXml(color)}"${opacity < 1 ? ` fill-opacity="${compactDecimal(opacity)}"` : ''}`
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function spreadsheetChartSeriesLineStyle(series, seriesIndex, defaultWidth, defaultColor = workSpreadsheetChartSeriesDefaultColor(seriesIndex)) {
|
|
1376
|
+
const style = normalizeWorkSpreadsheetChartSeriesStyle(series.style);
|
|
1377
|
+
const hasCustomLine = Boolean(style?.lineColor || style?.lineWidth !== void 0 || style?.lineDash);
|
|
1378
|
+
const color = style?.lineColor ?? defaultColor;
|
|
1379
|
+
const width = style?.lineWidth ?? (hasCustomLine && defaultWidth <= 0 ? 1 : defaultWidth);
|
|
1380
|
+
const dash = style?.lineDash ?? 'solid';
|
|
1381
|
+
const dashArray = spreadsheetChartLineDashArray(dash);
|
|
1382
|
+
return {
|
|
1383
|
+
color,
|
|
1384
|
+
width,
|
|
1385
|
+
dash,
|
|
1386
|
+
attributes: !hasCustomLine && defaultWidth <= 0 ? 'stroke="none"' : `stroke="${escapeChartXml(color)}" stroke-width="${compactDecimal(width)}"${dashArray ? ` stroke-dasharray="${dashArray}"` : ''}`
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function spreadsheetChartSeriesLegendColor(series, seriesIndex) {
|
|
1390
|
+
const style = normalizeWorkSpreadsheetChartSeriesStyle(series.style);
|
|
1391
|
+
return style?.fillColor ?? style?.lineColor ?? workSpreadsheetChartSeriesDefaultColor(seriesIndex);
|
|
1392
|
+
}
|
|
1393
|
+
function spreadsheetChartSeriesMarkerSvg(series, seriesIndex, x, y, options) {
|
|
1394
|
+
const style = normalizeWorkSpreadsheetChartSeriesStyle(series.style);
|
|
1395
|
+
const marker = style?.marker;
|
|
1396
|
+
const visible = marker ? 'none' !== marker.symbol : options.visible;
|
|
1397
|
+
if (!visible) return '';
|
|
1398
|
+
const symbol = marker?.symbol && 'none' !== marker.symbol ? marker.symbol : 'circle';
|
|
1399
|
+
const size = marker?.size ?? options.defaultSize ?? 5;
|
|
1400
|
+
const radius = Math.max(1, 0.6 * size);
|
|
1401
|
+
const line = spreadsheetChartSeriesLineStyle(series, seriesIndex, 2);
|
|
1402
|
+
const fillColor = marker?.fillColor ?? '#FFFFFF';
|
|
1403
|
+
const lineColor = marker?.lineColor ?? line.color;
|
|
1404
|
+
const common = `data-marker-symbol="${symbol}" data-marker-size="${compactDecimal(size)}"${options.attributes ? ` ${options.attributes}` : ''}`;
|
|
1405
|
+
const paint = `fill="${escapeChartXml(fillColor)}" stroke="${escapeChartXml(lineColor)}" stroke-width="2"`;
|
|
1406
|
+
const pointX = roundChartNumber(x);
|
|
1407
|
+
const pointY = roundChartNumber(y);
|
|
1408
|
+
if ('square' === symbol) {
|
|
1409
|
+
const side = 1.7 * radius;
|
|
1410
|
+
return `<rect ${common} x="${roundChartNumber(x - side / 2)}" y="${roundChartNumber(y - side / 2)}" width="${roundChartNumber(side)}" height="${roundChartNumber(side)}" rx="0.6" ${paint}/>`;
|
|
1411
|
+
}
|
|
1412
|
+
if ('diamond' === symbol) return `<path ${common} d="M ${pointX} ${roundChartNumber(y - radius)} L ${roundChartNumber(x + radius)} ${pointY} L ${pointX} ${roundChartNumber(y + radius)} L ${roundChartNumber(x - radius)} ${pointY} Z" ${paint}/>`;
|
|
1413
|
+
if ('triangle' === symbol) return `<path ${common} d="M ${pointX} ${roundChartNumber(y - radius)} L ${roundChartNumber(x + 0.9 * radius)} ${roundChartNumber(y + 0.75 * radius)} L ${roundChartNumber(x - 0.9 * radius)} ${roundChartNumber(y + 0.75 * radius)} Z" ${paint}/>`;
|
|
1414
|
+
if ('plus' === symbol || 'x' === symbol) {
|
|
1415
|
+
const paths = 'plus' === symbol ? `M ${roundChartNumber(x - radius)} ${pointY} L ${roundChartNumber(x + radius)} ${pointY} M ${pointX} ${roundChartNumber(y - radius)} L ${pointX} ${roundChartNumber(y + radius)}` : `M ${roundChartNumber(x - radius)} ${roundChartNumber(y - radius)} L ${roundChartNumber(x + radius)} ${roundChartNumber(y + radius)} M ${roundChartNumber(x + radius)} ${roundChartNumber(y - radius)} L ${roundChartNumber(x - radius)} ${roundChartNumber(y + radius)}`;
|
|
1416
|
+
return `<path ${common} d="${paths}" fill="none" stroke="${escapeChartXml(lineColor)}" stroke-width="2" stroke-linecap="round"/>`;
|
|
1417
|
+
}
|
|
1418
|
+
if ('star' === symbol) {
|
|
1419
|
+
const points = Array.from({
|
|
1420
|
+
length: 10
|
|
1421
|
+
}, (_, index)=>{
|
|
1422
|
+
const angle = -Math.PI / 2 + index * Math.PI / 5;
|
|
1423
|
+
const pointRadius = index % 2 ? 0.42 * radius : radius;
|
|
1424
|
+
return `${roundChartNumber(x + Math.cos(angle) * pointRadius)},${roundChartNumber(y + Math.sin(angle) * pointRadius)}`;
|
|
1425
|
+
}).join(' ');
|
|
1426
|
+
return `<polygon ${common} points="${points}" ${paint}/>`;
|
|
1427
|
+
}
|
|
1428
|
+
return `<circle ${common} cx="${pointX}" cy="${pointY}" r="${roundChartNumber(radius)}" ${paint}/>`;
|
|
1429
|
+
}
|
|
1430
|
+
function spreadsheetChartSeriesStyleContext(style) {
|
|
1431
|
+
const normalized = normalizeWorkSpreadsheetChartSeriesStyle(style);
|
|
1432
|
+
if (!normalized) return '';
|
|
1433
|
+
const descriptions = [];
|
|
1434
|
+
if (normalized.fillColor || void 0 !== normalized.fillTransparency) descriptions.push(`填充 ${normalized.fillColor ?? '默认颜色'}${void 0 === normalized.fillTransparency ? '' : `(透明度 ${compactDecimal(normalized.fillTransparency)}%)`}`);
|
|
1435
|
+
if (normalized.lineColor || void 0 !== normalized.lineWidth || normalized.lineDash) descriptions.push([
|
|
1436
|
+
`线条 ${normalized.lineColor ?? '默认颜色'}`,
|
|
1437
|
+
void 0 === normalized.lineWidth ? '' : `${compactDecimal(normalized.lineWidth)} 磅`,
|
|
1438
|
+
normalized.lineDash ? workSpreadsheetChartLineDashLabel(normalized.lineDash) : ''
|
|
1439
|
+
].filter(Boolean).join('、'));
|
|
1440
|
+
if (normalized.marker) {
|
|
1441
|
+
const marker = normalized.marker;
|
|
1442
|
+
descriptions.push([
|
|
1443
|
+
`数据标记 ${workSpreadsheetChartMarkerSymbolLabel(marker.symbol ?? 'circle')}`,
|
|
1444
|
+
void 0 === marker.size ? '' : `${compactDecimal(marker.size)} 磅`,
|
|
1445
|
+
marker.fillColor ? `填充 ${marker.fillColor}` : '',
|
|
1446
|
+
marker.lineColor ? `轮廓 ${marker.lineColor}` : ''
|
|
1447
|
+
].filter(Boolean).join('、'));
|
|
1448
|
+
}
|
|
1449
|
+
return descriptions.length ? `;系列外观:${descriptions.join(',')}` : '';
|
|
1450
|
+
}
|
|
1451
|
+
function workSpreadsheetChartLineDashLabel(dash) {
|
|
1452
|
+
if ('dash' === dash) return '虚线';
|
|
1453
|
+
if ('dot' === dash) return '点线';
|
|
1454
|
+
if ('dashDot' === dash) return '点划线';
|
|
1455
|
+
return '实线';
|
|
1456
|
+
}
|
|
1457
|
+
function workSpreadsheetChartMarkerSymbolLabel(symbol) {
|
|
1458
|
+
if ('none' === symbol) return '无';
|
|
1459
|
+
if ('square' === symbol) return '方形';
|
|
1460
|
+
if ('diamond' === symbol) return '菱形';
|
|
1461
|
+
if ('triangle' === symbol) return '三角形';
|
|
1462
|
+
if ('plus' === symbol) return '加号';
|
|
1463
|
+
if ('x' === symbol) return '叉号';
|
|
1464
|
+
if ('star' === symbol) return '星形';
|
|
1465
|
+
return '圆形';
|
|
1466
|
+
}
|
|
1467
|
+
function normalizeWorkSpreadsheetChartMarkerStyle(value) {
|
|
1468
|
+
if (!value || 'object' != typeof value) return;
|
|
1469
|
+
const source = value;
|
|
1470
|
+
const symbol = MARKER_SYMBOLS.has(source.symbol) ? source.symbol : void 0;
|
|
1471
|
+
const size = optionalBoundedNumber(source.size, 2, 72, 5);
|
|
1472
|
+
const fillColor = normalizeWorkSpreadsheetChartColor(source.fillColor);
|
|
1473
|
+
const lineColor = normalizeWorkSpreadsheetChartColor(source.lineColor);
|
|
1474
|
+
const marker = {
|
|
1475
|
+
...symbol ? {
|
|
1476
|
+
symbol
|
|
1477
|
+
} : {},
|
|
1478
|
+
...void 0 !== size ? {
|
|
1479
|
+
size
|
|
1480
|
+
} : {},
|
|
1481
|
+
...fillColor ? {
|
|
1482
|
+
fillColor
|
|
1483
|
+
} : {},
|
|
1484
|
+
...lineColor ? {
|
|
1485
|
+
lineColor
|
|
1486
|
+
} : {}
|
|
1487
|
+
};
|
|
1488
|
+
return Object.keys(marker).length ? marker : void 0;
|
|
1489
|
+
}
|
|
1490
|
+
function spreadsheetChartLineDashArray(dash) {
|
|
1491
|
+
if ('dash' === dash) return '8 4';
|
|
1492
|
+
if ('dot' === dash) return '2 3';
|
|
1493
|
+
if ('dashDot' === dash) return '8 4 2 4';
|
|
1494
|
+
return '';
|
|
1495
|
+
}
|
|
1496
|
+
function optionalBoundedNumber(value, minimum, maximum, fallback) {
|
|
1497
|
+
if (null == value || '' === value) return;
|
|
1498
|
+
const number = Number(value);
|
|
1499
|
+
return boundedNumber(number, minimum, maximum, fallback);
|
|
1500
|
+
}
|
|
1501
|
+
function boundedNumber(value, minimum, maximum, fallback) {
|
|
1502
|
+
return Math.min(maximum, Math.max(minimum, Number.isFinite(value) ? value : fallback));
|
|
1503
|
+
}
|
|
1504
|
+
function compactDecimal(value) {
|
|
1505
|
+
return String(Math.round(100 * value) / 100);
|
|
1506
|
+
}
|
|
1507
|
+
const VERTICAL_RESERVE = 118;
|
|
1508
|
+
const HORIZONTAL_RESERVE = 40;
|
|
1509
|
+
function spreadsheetChartLegendLayout(chart, sourcePlot, frame) {
|
|
1510
|
+
if (!chart.showLegend) return {
|
|
1511
|
+
plot: sourcePlot,
|
|
1512
|
+
svg: ''
|
|
1513
|
+
};
|
|
1514
|
+
const position = normalizeWorkSpreadsheetChartLegendPosition(chart.legendPosition);
|
|
1515
|
+
const overlay = normalizeWorkSpreadsheetChartLegendOverlay(chart.legendOverlay);
|
|
1516
|
+
const plot = reservedPlot(sourcePlot, position, overlay);
|
|
1517
|
+
const bounds = legendBounds(sourcePlot, plot, frame, position, overlay);
|
|
1518
|
+
const items = spreadsheetChartLegendItems(chart).slice(0, 8);
|
|
1519
|
+
const entries = 'top' === position || 'bottom' === position ? horizontalLegendEntries(items, bounds) : verticalLegendEntries(items, bounds);
|
|
1520
|
+
return {
|
|
1521
|
+
plot,
|
|
1522
|
+
svg: `<g data-chart-legend-position="${position}" data-chart-legend-overlay="${overlay}" aria-label="图例">${entries}</g>`
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
function reservedPlot(source, position, overlay) {
|
|
1526
|
+
if (overlay) return source;
|
|
1527
|
+
if ('left' === position) return {
|
|
1528
|
+
...source,
|
|
1529
|
+
x: source.x + VERTICAL_RESERVE,
|
|
1530
|
+
width: Math.max(40, source.width - VERTICAL_RESERVE)
|
|
1531
|
+
};
|
|
1532
|
+
if ('right' === position || 'topRight' === position) return {
|
|
1533
|
+
...source,
|
|
1534
|
+
width: Math.max(40, source.width - VERTICAL_RESERVE)
|
|
1535
|
+
};
|
|
1536
|
+
if ('top' === position) return {
|
|
1537
|
+
...source,
|
|
1538
|
+
y: source.y + HORIZONTAL_RESERVE,
|
|
1539
|
+
height: Math.max(40, source.height - HORIZONTAL_RESERVE)
|
|
1540
|
+
};
|
|
1541
|
+
return {
|
|
1542
|
+
...source,
|
|
1543
|
+
height: Math.max(40, source.height - HORIZONTAL_RESERVE)
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function legendBounds(source, plot, frame, position, overlay) {
|
|
1547
|
+
if ('left' === position) return {
|
|
1548
|
+
x: 12,
|
|
1549
|
+
y: frame.titleHeight + 18,
|
|
1550
|
+
width: VERTICAL_RESERVE - 18,
|
|
1551
|
+
height: frame.height - frame.titleHeight - 32
|
|
1552
|
+
};
|
|
1553
|
+
if ('right' === position || 'topRight' === position) return {
|
|
1554
|
+
x: overlay ? source.x + source.width - VERTICAL_RESERVE + 12 : plot.x + plot.width + 12,
|
|
1555
|
+
y: 'topRight' === position ? frame.titleHeight + 4 : frame.titleHeight + 18,
|
|
1556
|
+
width: VERTICAL_RESERVE - 18,
|
|
1557
|
+
height: frame.height - frame.titleHeight - 32
|
|
1558
|
+
};
|
|
1559
|
+
if ('top' === position) return {
|
|
1560
|
+
x: plot.x,
|
|
1561
|
+
y: frame.titleHeight + 4,
|
|
1562
|
+
width: plot.width,
|
|
1563
|
+
height: HORIZONTAL_RESERVE - 4
|
|
1564
|
+
};
|
|
1565
|
+
return {
|
|
1566
|
+
x: plot.x,
|
|
1567
|
+
y: frame.height - HORIZONTAL_RESERVE + 6,
|
|
1568
|
+
width: plot.width,
|
|
1569
|
+
height: HORIZONTAL_RESERVE - 8
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function spreadsheetChartLegendItems(chart) {
|
|
1573
|
+
if ('pie' === chart.type || 'doughnut' === chart.type) {
|
|
1574
|
+
const values = chart.series[0]?.values ?? [];
|
|
1575
|
+
const seriesColor = normalizeWorkSpreadsheetChartSeriesStyle(chart.series[0]?.style)?.fillColor;
|
|
1576
|
+
return chart.categories.map((category, index)=>({
|
|
1577
|
+
color: seriesColor ?? WORK_SPREADSHEET_CHART_COLORS[index % WORK_SPREADSHEET_CHART_COLORS.length],
|
|
1578
|
+
label: `${category}${void 0 === values[index] ? '' : ` ${compactChartNumber(values[index])}`}`
|
|
1579
|
+
}));
|
|
1580
|
+
}
|
|
1581
|
+
return chart.series.map((series, index)=>({
|
|
1582
|
+
color: spreadsheetChartSeriesLegendColor(series, index),
|
|
1583
|
+
label: series.name || `系列 ${index + 1}`
|
|
1584
|
+
}));
|
|
1585
|
+
}
|
|
1586
|
+
function verticalLegendEntries(items, bounds) {
|
|
1587
|
+
return items.map((item, index)=>legendEntrySvg(item, index, bounds.x, bounds.y + 21 * index, 16)).join('');
|
|
1588
|
+
}
|
|
1589
|
+
function horizontalLegendEntries(items, bounds) {
|
|
1590
|
+
if (!items.length) return '';
|
|
1591
|
+
const columns = Math.max(1, Math.min(items.length, Math.floor(bounds.width / 96)));
|
|
1592
|
+
const itemWidth = bounds.width / columns;
|
|
1593
|
+
return items.map((item, index)=>{
|
|
1594
|
+
const column = index % columns;
|
|
1595
|
+
const row = Math.floor(index / columns);
|
|
1596
|
+
return legendEntrySvg(item, index, bounds.x + column * itemWidth, bounds.y + 17 * row + 9, Math.max(8, Math.floor(itemWidth / 7)));
|
|
1597
|
+
}).join('');
|
|
1598
|
+
}
|
|
1599
|
+
function legendEntrySvg(item, index, x, baselineY, maximumCharacters) {
|
|
1600
|
+
return [
|
|
1601
|
+
`<rect data-chart-legend-entry="${index}" x="${roundChartNumber(x)}" y="${roundChartNumber(baselineY - 9)}" width="10" height="10" rx="2" fill="${item.color}"/>`,
|
|
1602
|
+
`<text x="${roundChartNumber(x + 16)}" y="${roundChartNumber(baselineY)}" fill="#536078" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10">${escapeChartXml(truncateChartText(item.label, maximumCharacters))}</text>`
|
|
1603
|
+
].join('');
|
|
1604
|
+
}
|
|
1605
|
+
function spreadsheetChartSeriesLayout(chart) {
|
|
1606
|
+
const grouping = normalizeWorkSpreadsheetChartGrouping(chart.grouping, chart.type);
|
|
1607
|
+
const stacked = workSpreadsheetChartGroupingIsStacked(grouping);
|
|
1608
|
+
const categoryCount = Math.max(1, chart.categories.length, ...chart.series.map((series)=>series.values.length));
|
|
1609
|
+
const result = chart.series.map(()=>[]);
|
|
1610
|
+
for(let categoryIndex = 0; categoryIndex < categoryCount; categoryIndex += 1){
|
|
1611
|
+
const rawValues = chart.series.map((series)=>finiteChartNumber(series.values[categoryIndex] ?? 0));
|
|
1612
|
+
const positiveTotal = rawValues.reduce((sum, value)=>sum + Math.max(0, value), 0);
|
|
1613
|
+
const negativeTotal = rawValues.reduce((sum, value)=>sum + Math.abs(Math.min(0, value)), 0);
|
|
1614
|
+
let positive = 0;
|
|
1615
|
+
let negative = 0;
|
|
1616
|
+
rawValues.forEach((rawValue, seriesIndex)=>{
|
|
1617
|
+
const value = 'percentStacked' === grouping ? rawValue >= 0 ? positiveTotal ? rawValue / positiveTotal : 0 : negativeTotal ? rawValue / negativeTotal : 0 : rawValue;
|
|
1618
|
+
const start = stacked ? value >= 0 ? positive : negative : 0;
|
|
1619
|
+
const end = start + value;
|
|
1620
|
+
if (stacked) if (value >= 0) positive = end;
|
|
1621
|
+
else negative = end;
|
|
1622
|
+
if (categoryIndex < (chart.series[seriesIndex]?.values.length ?? 0)) result[seriesIndex].push({
|
|
1623
|
+
categoryIndex,
|
|
1624
|
+
rawValue,
|
|
1625
|
+
start,
|
|
1626
|
+
end
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
const scaleValues = result.flatMap((series)=>series.flatMap((point)=>[
|
|
1631
|
+
point.start,
|
|
1632
|
+
point.end
|
|
1633
|
+
]));
|
|
1634
|
+
return {
|
|
1635
|
+
grouping,
|
|
1636
|
+
stacked,
|
|
1637
|
+
categoryCount,
|
|
1638
|
+
series: result,
|
|
1639
|
+
scaleValues
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function spreadsheetChartBarGeometry(groupSize, seriesCount, chart, maximumBarSize) {
|
|
1643
|
+
const grouping = normalizeWorkSpreadsheetChartGrouping(chart.grouping, chart.type);
|
|
1644
|
+
const gapWidth = normalizeWorkSpreadsheetChartGapWidth(chart.gapWidth);
|
|
1645
|
+
const overlap = normalizeWorkSpreadsheetChartOverlap(chart.overlap, grouping);
|
|
1646
|
+
const count = Math.max(1, seriesCount);
|
|
1647
|
+
const clusterSize = Math.max(1, 100 * groupSize / (100 + gapWidth));
|
|
1648
|
+
const stepRatio = 1 - overlap / 100;
|
|
1649
|
+
const idealBarSize = clusterSize / Math.max(1, 1 + (count - 1) * stepRatio);
|
|
1650
|
+
const barSize = Math.max(1, Math.min(maximumBarSize, idealBarSize));
|
|
1651
|
+
const step = barSize * stepRatio;
|
|
1652
|
+
const totalSize = barSize + step * (count - 1);
|
|
1653
|
+
const start = (groupSize - totalSize) / 2;
|
|
1654
|
+
return {
|
|
1655
|
+
barSize,
|
|
1656
|
+
renderedSize: Math.max(0.75, barSize - Math.min(1.5, 0.08 * barSize)),
|
|
1657
|
+
offset: (seriesIndex)=>start + seriesIndex * step
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
function spreadsheetDataLabelSvg(chart, seriesIndex, pointIndex, geometry) {
|
|
1661
|
+
const series = chart.series[seriesIndex];
|
|
1662
|
+
if (!series?.dataLabels) return '';
|
|
1663
|
+
const labels = normalizeWorkSpreadsheetDataLabels(series.dataLabels, chart.type);
|
|
1664
|
+
const text = spreadsheetDataLabelText(chart, seriesIndex, pointIndex);
|
|
1665
|
+
if (!text) return '';
|
|
1666
|
+
const position = normalizeWorkSpreadsheetDataLabelPosition(labels.position);
|
|
1667
|
+
const placed = placeDataLabel(geometry, position);
|
|
1668
|
+
const visualText = truncateChartText(text.replaceAll(/\r?\n/g, ' '), 32);
|
|
1669
|
+
return `<text data-data-label-series="${seriesIndex}:${pointIndex}" data-data-label-position="${position}" data-data-label-text="${escapeChartXml(text)}" x="${roundChartNumber(placed.x)}" y="${roundChartNumber(placed.y)}" fill="#27344d" stroke="#ffffff" stroke-width="3" stroke-linejoin="round" paint-order="stroke" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="9" font-weight="500" text-anchor="${placed.anchor}">${escapeChartXml(visualText)}</text>`;
|
|
1670
|
+
}
|
|
1671
|
+
function spreadsheetDataLabelText(chart, seriesIndex, pointIndex) {
|
|
1672
|
+
const series = chart.series[seriesIndex];
|
|
1673
|
+
if (!series?.dataLabels) return '';
|
|
1674
|
+
const labels = normalizeWorkSpreadsheetDataLabels(series.dataLabels, chart.type);
|
|
1675
|
+
const parts = [];
|
|
1676
|
+
if (labels.showSeriesName) parts.push(series.name || `系列 ${seriesIndex + 1}`);
|
|
1677
|
+
if (labels.showCategoryName) {
|
|
1678
|
+
const category = 'scatter' === chart.type || 'bubble' === chart.type ? compactChartNumber(series.xValues?.[pointIndex] ?? pointIndex + 1) : chart.categories[pointIndex] ?? String(pointIndex + 1);
|
|
1679
|
+
parts.push(category);
|
|
1680
|
+
}
|
|
1681
|
+
if (labels.showValue) parts.push(compactChartNumber(series.values[pointIndex]));
|
|
1682
|
+
if (labels.showPercentage) {
|
|
1683
|
+
const values = series.values.map((value)=>Math.max(0, finiteChartNumber(value)));
|
|
1684
|
+
const total = values.reduce((sum, value)=>sum + value, 0);
|
|
1685
|
+
if (total > 0) parts.push(`${compactChartNumber(Math.round(values[pointIndex] / total * 1000) / 10)}%`);
|
|
1686
|
+
}
|
|
1687
|
+
if (labels.showBubbleSize) parts.push(compactChartNumber(series.bubbleSizes?.[pointIndex] ?? 0));
|
|
1688
|
+
return parts.join(labels.separator ?? ', ');
|
|
1689
|
+
}
|
|
1690
|
+
function placeDataLabel(geometry, position) {
|
|
1691
|
+
if ('verticalBar' === geometry.kind) return placeVerticalBarLabel(geometry, position);
|
|
1692
|
+
if ('horizontalBar' === geometry.kind) return placeHorizontalBarLabel(geometry, position);
|
|
1693
|
+
if ('circular' === geometry.kind) return placeCircularLabel(geometry, position);
|
|
1694
|
+
if ('left' === position) return {
|
|
1695
|
+
x: geometry.x - 6,
|
|
1696
|
+
y: geometry.y + 3,
|
|
1697
|
+
anchor: 'end'
|
|
1698
|
+
};
|
|
1699
|
+
if ('right' === position) return {
|
|
1700
|
+
x: geometry.x + 6,
|
|
1701
|
+
y: geometry.y + 3,
|
|
1702
|
+
anchor: 'start'
|
|
1703
|
+
};
|
|
1704
|
+
if ('below' === position) return {
|
|
1705
|
+
x: geometry.x,
|
|
1706
|
+
y: geometry.y + 13,
|
|
1707
|
+
anchor: 'middle'
|
|
1708
|
+
};
|
|
1709
|
+
if ('center' === position || 'insideBase' === position || 'insideEnd' === position) return {
|
|
1710
|
+
x: geometry.x,
|
|
1711
|
+
y: geometry.y + 3,
|
|
1712
|
+
anchor: 'middle'
|
|
1713
|
+
};
|
|
1714
|
+
return {
|
|
1715
|
+
x: geometry.x,
|
|
1716
|
+
y: geometry.y - 7,
|
|
1717
|
+
anchor: 'middle'
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function placeVerticalBarLabel(geometry, position) {
|
|
1721
|
+
const positive = geometry.value >= 0;
|
|
1722
|
+
if ('left' === position) return {
|
|
1723
|
+
x: geometry.x - 5,
|
|
1724
|
+
y: geometry.valueY + 3,
|
|
1725
|
+
anchor: 'end'
|
|
1726
|
+
};
|
|
1727
|
+
if ('right' === position) return {
|
|
1728
|
+
x: geometry.x + 5,
|
|
1729
|
+
y: geometry.valueY + 3,
|
|
1730
|
+
anchor: 'start'
|
|
1731
|
+
};
|
|
1732
|
+
if ('center' === position) return {
|
|
1733
|
+
x: geometry.x,
|
|
1734
|
+
y: (geometry.valueY + geometry.baselineY) / 2 + 3,
|
|
1735
|
+
anchor: 'middle'
|
|
1736
|
+
};
|
|
1737
|
+
if ('insideBase' === position) return {
|
|
1738
|
+
x: geometry.x,
|
|
1739
|
+
y: geometry.baselineY + (positive ? -5 : 12),
|
|
1740
|
+
anchor: 'middle'
|
|
1741
|
+
};
|
|
1742
|
+
if ('insideEnd' === position) return {
|
|
1743
|
+
x: geometry.x,
|
|
1744
|
+
y: geometry.valueY + (positive ? 11 : -5),
|
|
1745
|
+
anchor: 'middle'
|
|
1746
|
+
};
|
|
1747
|
+
if ('below' === position) return {
|
|
1748
|
+
x: geometry.x,
|
|
1749
|
+
y: geometry.valueY + 13,
|
|
1750
|
+
anchor: 'middle'
|
|
1751
|
+
};
|
|
1752
|
+
return {
|
|
1753
|
+
x: geometry.x,
|
|
1754
|
+
y: geometry.valueY + (positive ? -6 : 13),
|
|
1755
|
+
anchor: 'middle'
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function placeHorizontalBarLabel(geometry, position) {
|
|
1759
|
+
const positive = geometry.value >= 0;
|
|
1760
|
+
if ('above' === position) return {
|
|
1761
|
+
x: geometry.valueX,
|
|
1762
|
+
y: geometry.y - 5,
|
|
1763
|
+
anchor: 'middle'
|
|
1764
|
+
};
|
|
1765
|
+
if ('below' === position) return {
|
|
1766
|
+
x: geometry.valueX,
|
|
1767
|
+
y: geometry.y + 12,
|
|
1768
|
+
anchor: 'middle'
|
|
1769
|
+
};
|
|
1770
|
+
if ('center' === position) return {
|
|
1771
|
+
x: (geometry.valueX + geometry.baselineX) / 2,
|
|
1772
|
+
y: geometry.y + 3,
|
|
1773
|
+
anchor: 'middle'
|
|
1774
|
+
};
|
|
1775
|
+
if ('insideBase' === position) return {
|
|
1776
|
+
x: geometry.baselineX + (positive ? 5 : -5),
|
|
1777
|
+
y: geometry.y + 3,
|
|
1778
|
+
anchor: positive ? 'start' : 'end'
|
|
1779
|
+
};
|
|
1780
|
+
if ('insideEnd' === position) return {
|
|
1781
|
+
x: geometry.valueX + (positive ? -5 : 5),
|
|
1782
|
+
y: geometry.y + 3,
|
|
1783
|
+
anchor: positive ? 'end' : 'start'
|
|
1784
|
+
};
|
|
1785
|
+
if ('left' === position) return {
|
|
1786
|
+
x: geometry.valueX - 6,
|
|
1787
|
+
y: geometry.y + 3,
|
|
1788
|
+
anchor: 'end'
|
|
1789
|
+
};
|
|
1790
|
+
if ('right' === position) return {
|
|
1791
|
+
x: geometry.valueX + 6,
|
|
1792
|
+
y: geometry.y + 3,
|
|
1793
|
+
anchor: 'start'
|
|
1794
|
+
};
|
|
1795
|
+
return {
|
|
1796
|
+
x: geometry.valueX + (positive ? 6 : -6),
|
|
1797
|
+
y: geometry.y + 3,
|
|
1798
|
+
anchor: positive ? 'start' : 'end'
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
function placeCircularLabel(geometry, position) {
|
|
1802
|
+
const thickness = geometry.outerRadius - geometry.innerRadius;
|
|
1803
|
+
const radius = 'outsideEnd' === position ? geometry.outerRadius + 14 : 'insideBase' === position ? geometry.innerRadius + 0.28 * thickness : 'insideEnd' === position ? geometry.innerRadius + 0.76 * thickness : geometry.innerRadius + 0.58 * thickness;
|
|
1804
|
+
const x = geometry.centerX + Math.cos(geometry.angle) * radius;
|
|
1805
|
+
const y = geometry.centerY + Math.sin(geometry.angle) * radius;
|
|
1806
|
+
if ('left' === position) return {
|
|
1807
|
+
x: x - 8,
|
|
1808
|
+
y: y + 3,
|
|
1809
|
+
anchor: 'end'
|
|
1810
|
+
};
|
|
1811
|
+
if ('right' === position) return {
|
|
1812
|
+
x: x + 8,
|
|
1813
|
+
y: y + 3,
|
|
1814
|
+
anchor: 'start'
|
|
1815
|
+
};
|
|
1816
|
+
if ('above' === position) return {
|
|
1817
|
+
x,
|
|
1818
|
+
y: y - 7,
|
|
1819
|
+
anchor: 'middle'
|
|
1820
|
+
};
|
|
1821
|
+
if ('below' === position) return {
|
|
1822
|
+
x,
|
|
1823
|
+
y: y + 13,
|
|
1824
|
+
anchor: 'middle'
|
|
1825
|
+
};
|
|
1826
|
+
const anchor = 'outsideEnd' === position ? Math.cos(geometry.angle) >= 0 ? 'start' : 'end' : 'middle';
|
|
1827
|
+
return {
|
|
1828
|
+
x,
|
|
1829
|
+
y: y + 3,
|
|
1830
|
+
anchor
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
function spreadsheetErrorBarAmounts(series, source, chartType) {
|
|
1834
|
+
const errorBars = normalizeWorkSpreadsheetErrorBars(source, chartType);
|
|
1835
|
+
const values = errorBarSourceValues(series, errorBars);
|
|
1836
|
+
const sharedAmount = sharedErrorAmount(values, errorBars);
|
|
1837
|
+
return values.map((value, index)=>{
|
|
1838
|
+
const amount = 'percentage' === errorBars.valueType ? Math.abs(value) * ((errorBars.value ?? 5) / 100) : 'custom' === errorBars.valueType ? 0 : sharedAmount;
|
|
1839
|
+
const minus = 'custom' === errorBars.valueType ? finiteNonNegative(errorBars.minusValues?.[index]) : amount;
|
|
1840
|
+
const plus = 'custom' === errorBars.valueType ? finiteNonNegative(errorBars.plusValues?.[index]) : amount;
|
|
1841
|
+
return {
|
|
1842
|
+
minus: 'plus' === errorBars.barType ? 0 : minus,
|
|
1843
|
+
plus: 'minus' === errorBars.barType ? 0 : plus
|
|
1844
|
+
};
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
function spreadsheetSeriesErrorBarBounds(series, chartType, direction) {
|
|
1848
|
+
const values = 'x' === direction ? numericXValues(series) : series.values;
|
|
1849
|
+
const matching = (series.errorBars ?? []).filter((source)=>normalizeWorkSpreadsheetErrorBars(source, chartType).direction === direction);
|
|
1850
|
+
if (!matching.length) return values.filter(Number.isFinite);
|
|
1851
|
+
return matching.flatMap((source)=>{
|
|
1852
|
+
const amounts = spreadsheetErrorBarAmounts(series, source, chartType);
|
|
1853
|
+
return values.flatMap((value, index)=>{
|
|
1854
|
+
if (!Number.isFinite(value)) return [];
|
|
1855
|
+
const amount = amounts[index] ?? {
|
|
1856
|
+
minus: 0,
|
|
1857
|
+
plus: 0
|
|
1858
|
+
};
|
|
1859
|
+
return [
|
|
1860
|
+
value - amount.minus,
|
|
1861
|
+
value,
|
|
1862
|
+
value + amount.plus
|
|
1863
|
+
];
|
|
1864
|
+
});
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
function errorBarSourceValues(series, errorBars) {
|
|
1868
|
+
return 'x' === errorBars.direction ? numericXValues(series) : series.values;
|
|
1869
|
+
}
|
|
1870
|
+
function numericXValues(series) {
|
|
1871
|
+
return series.values.map((_, index)=>series.xValues?.[index] ?? index + 1);
|
|
1872
|
+
}
|
|
1873
|
+
function sharedErrorAmount(values, errorBars) {
|
|
1874
|
+
if ('fixedValue' === errorBars.valueType) return errorBars.value ?? 1;
|
|
1875
|
+
if ('standardDeviation' === errorBars.valueType) return populationStandardDeviation(values) * (errorBars.value ?? 1);
|
|
1876
|
+
if ('standardError' === errorBars.valueType) {
|
|
1877
|
+
const usableCount = values.filter(Number.isFinite).length;
|
|
1878
|
+
return usableCount ? populationStandardDeviation(values) / Math.sqrt(usableCount) : 0;
|
|
1879
|
+
}
|
|
1880
|
+
return 0;
|
|
1881
|
+
}
|
|
1882
|
+
function populationStandardDeviation(values) {
|
|
1883
|
+
const usable = values.filter(Number.isFinite);
|
|
1884
|
+
if (!usable.length) return 0;
|
|
1885
|
+
const mean = usable.reduce((sum, value)=>sum + value, 0) / usable.length;
|
|
1886
|
+
return Math.sqrt(usable.reduce((sum, value)=>sum + (value - mean) ** 2, 0) / usable.length);
|
|
1887
|
+
}
|
|
1888
|
+
function finiteNonNegative(value) {
|
|
1889
|
+
return 'number' == typeof value && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
1890
|
+
}
|
|
1891
|
+
function spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, position) {
|
|
1892
|
+
const series = chart.series[seriesIndex];
|
|
1893
|
+
if (!series?.errorBars?.length) return '';
|
|
1894
|
+
const color = WORK_SPREADSHEET_CHART_COLORS[seriesIndex % WORK_SPREADSHEET_CHART_COLORS.length];
|
|
1895
|
+
return series.errorBars.map((source, errorBarIndex)=>{
|
|
1896
|
+
const errorBars = normalizeWorkSpreadsheetErrorBars(source, chart.type);
|
|
1897
|
+
const values = errorBarSourceValues(series, errorBars);
|
|
1898
|
+
const amounts = spreadsheetErrorBarAmounts(series, errorBars, chart.type);
|
|
1899
|
+
const points = values.map((value, pointIndex)=>{
|
|
1900
|
+
if (!Number.isFinite(value)) return '';
|
|
1901
|
+
const amount = amounts[pointIndex] ?? {
|
|
1902
|
+
minus: 0,
|
|
1903
|
+
plus: 0
|
|
1904
|
+
};
|
|
1905
|
+
const start = position(errorBars.direction, pointIndex, value - amount.minus);
|
|
1906
|
+
const end = position(errorBars.direction, pointIndex, value + amount.plus);
|
|
1907
|
+
const horizontal = Math.abs(end[0] - start[0]) >= Math.abs(end[1] - start[1]);
|
|
1908
|
+
const caps = false === errorBars.showEndCaps ? '' : `${amount.minus > 0 ? errorBarCap(start, horizontal) : ''}${amount.plus > 0 ? errorBarCap(end, horizontal) : ''}`;
|
|
1909
|
+
return `<g data-error-bar-point="${seriesIndex}:${errorBarIndex}:${pointIndex}" data-error-minus="${roundChartNumber(amount.minus)}" data-error-plus="${roundChartNumber(amount.plus)}"><path d="M ${roundChartNumber(start[0])} ${roundChartNumber(start[1])} L ${roundChartNumber(end[0])} ${roundChartNumber(end[1])}" fill="none" stroke="${color}" stroke-width="1.4"/>${caps}</g>`;
|
|
1910
|
+
}).join('');
|
|
1911
|
+
return `<g data-error-bars-series="${seriesIndex}:${errorBarIndex}" data-error-bars-direction="${errorBars.direction}" data-error-bars-type="${errorBars.barType}" data-error-bars-value-type="${errorBars.valueType}" data-error-bars-end-caps="${false !== errorBars.showEndCaps}" color="${color}">${points}</g>`;
|
|
1912
|
+
}).join('');
|
|
1913
|
+
}
|
|
1914
|
+
function errorBarCap(point, horizontal) {
|
|
1915
|
+
return horizontal ? `<path d="M ${roundChartNumber(point[0])} ${roundChartNumber(point[1] - 4)} L ${roundChartNumber(point[0])} ${roundChartNumber(point[1] + 4)}" fill="none" stroke="currentColor" stroke-width="1.4"/>` : `<path d="M ${roundChartNumber(point[0] - 4)} ${roundChartNumber(point[1])} L ${roundChartNumber(point[0] + 4)} ${roundChartNumber(point[1])}" fill="none" stroke="currentColor" stroke-width="1.4"/>`;
|
|
1916
|
+
}
|
|
1917
|
+
const DEFAULT_TICK_COUNT = 5;
|
|
1918
|
+
const MAX_RENDERED_TICKS = 64;
|
|
1919
|
+
function workSpreadsheetChartAxisIsValueAxis(chartType, position) {
|
|
1920
|
+
if (!workSpreadsheetChartSupportsAxes(chartType) || 'top' === position) return false;
|
|
1921
|
+
if (workSpreadsheetChartUsesNumericXAxis(chartType)) return 'bottom' === position || 'left' === position;
|
|
1922
|
+
if ('bar' === chartType) return 'bottom' === position;
|
|
1923
|
+
if ('combination' === chartType) return 'left' === position || 'right' === position;
|
|
1924
|
+
return 'left' === position;
|
|
1925
|
+
}
|
|
1926
|
+
function workSpreadsheetChartAxisIsCategoryAxis(chartType, position) {
|
|
1927
|
+
if (!workSpreadsheetChartSupportsAxes(chartType) || workSpreadsheetChartUsesNumericXAxis(chartType)) return false;
|
|
1928
|
+
if ('bar' === chartType) return 'left' === position;
|
|
1929
|
+
if ('combination' === chartType) return 'bottom' === position || 'top' === position;
|
|
1930
|
+
return 'bottom' === position;
|
|
1931
|
+
}
|
|
1932
|
+
function workSpreadsheetChartAxisShowsMajorGridlinesByDefault(chartType, position) {
|
|
1933
|
+
if (!workSpreadsheetChartAxisIsValueAxis(chartType, position)) return false;
|
|
1934
|
+
if (workSpreadsheetChartUsesNumericXAxis(chartType)) return 'left' === position;
|
|
1935
|
+
if ('bar' === chartType) return 'bottom' === position;
|
|
1936
|
+
return 'left' === position;
|
|
1937
|
+
}
|
|
1938
|
+
function workSpreadsheetChartAxisDefaultLabelPosition(chartType, position) {
|
|
1939
|
+
return 'combination' === chartType && 'top' === position ? 'none' : 'nextTo';
|
|
1940
|
+
}
|
|
1941
|
+
function workSpreadsheetChartAxisPositionLabel(position) {
|
|
1942
|
+
if ('left' === position) return '纵轴';
|
|
1943
|
+
if ('top' === position) return '次横轴';
|
|
1944
|
+
if ('right' === position) return '次纵轴';
|
|
1945
|
+
return '横轴';
|
|
1946
|
+
}
|
|
1947
|
+
function workSpreadsheetChartAxisLabelPositionLabel(position) {
|
|
1948
|
+
if ('high' === position) return '高位';
|
|
1949
|
+
if ('low' === position) return '低位';
|
|
1950
|
+
if ('none' === position) return '不显示';
|
|
1951
|
+
return '轴旁';
|
|
1952
|
+
}
|
|
1953
|
+
function workSpreadsheetChartAxisTickMarkLabel(tickMark) {
|
|
1954
|
+
if ('inside' === tickMark) return '向内';
|
|
1955
|
+
if ('outside' === tickMark) return '向外';
|
|
1956
|
+
if ('cross' === tickMark) return '交叉';
|
|
1957
|
+
return '无';
|
|
1958
|
+
}
|
|
1959
|
+
function normalizeWorkSpreadsheetChartAxes(source, chartType, hasSecondaryAxes = false) {
|
|
1960
|
+
if (!source || !workSpreadsheetChartSupportsAxes(chartType)) return;
|
|
1961
|
+
const bottom = normalizeWorkSpreadsheetChartAxis(source.bottom, chartType, 'bottom');
|
|
1962
|
+
const left = normalizeWorkSpreadsheetChartAxis(source.left, chartType, 'left');
|
|
1963
|
+
const top = 'combination' === chartType && hasSecondaryAxes ? normalizeWorkSpreadsheetChartAxis(source.top, chartType, 'top') : void 0;
|
|
1964
|
+
const right = 'combination' === chartType && hasSecondaryAxes ? normalizeWorkSpreadsheetChartAxis(source.right, chartType, 'right') : void 0;
|
|
1965
|
+
return bottom || left || top || right ? {
|
|
1966
|
+
...bottom ? {
|
|
1967
|
+
bottom
|
|
1968
|
+
} : {},
|
|
1969
|
+
...left ? {
|
|
1970
|
+
left
|
|
1971
|
+
} : {},
|
|
1972
|
+
...top ? {
|
|
1973
|
+
top
|
|
1974
|
+
} : {},
|
|
1975
|
+
...right ? {
|
|
1976
|
+
right
|
|
1977
|
+
} : {}
|
|
1978
|
+
} : void 0;
|
|
1979
|
+
}
|
|
1980
|
+
function normalizeWorkSpreadsheetChartAxis(source, chartType, position) {
|
|
1981
|
+
if (!source) return;
|
|
1982
|
+
const title = source.title?.trim();
|
|
1983
|
+
const titleReference = source.titleReference?.trim().replace(/^=/, '');
|
|
1984
|
+
const valueAxis = workSpreadsheetChartAxisIsValueAxis(chartType, position);
|
|
1985
|
+
const categoryAxis = workSpreadsheetChartAxisIsCategoryAxis(chartType, position);
|
|
1986
|
+
const minimum = valueAxis && Number.isFinite(source.minimum) ? Number(source.minimum) : void 0;
|
|
1987
|
+
const maximum = valueAxis && Number.isFinite(source.maximum) ? Number(source.maximum) : void 0;
|
|
1988
|
+
const validRange = void 0 === minimum || void 0 === maximum || minimum < maximum;
|
|
1989
|
+
const majorUnit = valueAxis && Number.isFinite(source.majorUnit) && Number(source.majorUnit) > 0 ? Number(source.majorUnit) : void 0;
|
|
1990
|
+
const showMajorGridlines = valueAxis && 'boolean' == typeof source.showMajorGridlines ? source.showMajorGridlines : void 0;
|
|
1991
|
+
const numberFormat = valueAxis ? source.numberFormat?.trim().slice(0, 255) : void 0;
|
|
1992
|
+
const numberFormatSourceLinked = valueAxis && 'boolean' == typeof source.numberFormatSourceLinked ? source.numberFormatSourceLinked : void 0;
|
|
1993
|
+
const labelPosition = normalizedAxisLabelPosition(source.labelPosition);
|
|
1994
|
+
const defaultLabelPosition = workSpreadsheetChartAxisDefaultLabelPosition(chartType, position);
|
|
1995
|
+
const majorTickMark = normalizedAxisTickMark(source.majorTickMark);
|
|
1996
|
+
const labelInterval = categoryAxis ? normalizedLabelInterval(source.labelInterval) : void 0;
|
|
1997
|
+
const normalized = {
|
|
1998
|
+
...title ? {
|
|
1999
|
+
title
|
|
2000
|
+
} : {},
|
|
2001
|
+
...titleReference ? {
|
|
2002
|
+
titleReference
|
|
2003
|
+
} : {},
|
|
2004
|
+
...true === source.reverseOrder ? {
|
|
2005
|
+
reverseOrder: true
|
|
2006
|
+
} : {},
|
|
2007
|
+
...labelPosition && labelPosition !== defaultLabelPosition ? {
|
|
2008
|
+
labelPosition
|
|
2009
|
+
} : {},
|
|
2010
|
+
...majorTickMark && 'none' !== majorTickMark ? {
|
|
2011
|
+
majorTickMark
|
|
2012
|
+
} : {},
|
|
2013
|
+
...void 0 !== labelInterval ? {
|
|
2014
|
+
labelInterval
|
|
2015
|
+
} : {},
|
|
2016
|
+
...validRange && void 0 !== minimum ? {
|
|
2017
|
+
minimum
|
|
2018
|
+
} : {},
|
|
2019
|
+
...validRange && void 0 !== maximum ? {
|
|
2020
|
+
maximum
|
|
2021
|
+
} : {},
|
|
2022
|
+
...void 0 !== majorUnit ? {
|
|
2023
|
+
majorUnit
|
|
2024
|
+
} : {},
|
|
2025
|
+
...void 0 !== showMajorGridlines ? {
|
|
2026
|
+
showMajorGridlines
|
|
2027
|
+
} : {},
|
|
2028
|
+
...numberFormat ? {
|
|
2029
|
+
numberFormat
|
|
2030
|
+
} : {},
|
|
2031
|
+
...void 0 !== numberFormatSourceLinked ? {
|
|
2032
|
+
numberFormatSourceLinked
|
|
2033
|
+
} : {}
|
|
2034
|
+
};
|
|
2035
|
+
return Object.keys(normalized).length ? normalized : void 0;
|
|
2036
|
+
}
|
|
2037
|
+
function normalizedAxisLabelPosition(value) {
|
|
2038
|
+
return 'nextTo' === value || 'high' === value || 'low' === value || 'none' === value ? value : void 0;
|
|
2039
|
+
}
|
|
2040
|
+
function normalizedAxisTickMark(value) {
|
|
2041
|
+
return 'none' === value || 'inside' === value || 'outside' === value || 'cross' === value ? value : void 0;
|
|
2042
|
+
}
|
|
2043
|
+
function normalizedLabelInterval(value) {
|
|
2044
|
+
const interval = Number(value);
|
|
2045
|
+
return Number.isInteger(interval) && interval >= 1 && interval <= 31999 ? interval : void 0;
|
|
2046
|
+
}
|
|
2047
|
+
function spreadsheetChartAxisDisplayAttributes(axis, chartType, position) {
|
|
2048
|
+
const labelPosition = axis?.labelPosition ?? workSpreadsheetChartAxisDefaultLabelPosition(chartType, position);
|
|
2049
|
+
const majorTickMark = axis?.majorTickMark ?? 'none';
|
|
2050
|
+
return [
|
|
2051
|
+
`data-axis-display="${position}"`,
|
|
2052
|
+
`data-axis-reverse-order="${axis?.reverseOrder === true}"`,
|
|
2053
|
+
`data-axis-label-position="${labelPosition}"`,
|
|
2054
|
+
`data-axis-major-tick-mark="${majorTickMark}"`,
|
|
2055
|
+
workSpreadsheetChartAxisIsCategoryAxis(chartType, position) ? `data-axis-label-interval="${axis?.labelInterval ?? 'auto'}"` : ''
|
|
2056
|
+
].filter(Boolean).join(' ');
|
|
2057
|
+
}
|
|
2058
|
+
function spreadsheetChartAxisValueRatio(value, scale, axis) {
|
|
2059
|
+
const ratio = Math.min(1, Math.max(0, (finiteChartNumber(value) - scale.minimum) / scale.span));
|
|
2060
|
+
return axis?.reverseOrder === true ? 1 - ratio : ratio;
|
|
2061
|
+
}
|
|
2062
|
+
function spreadsheetChartCategoryVisualIndex(index, count, axis) {
|
|
2063
|
+
return axis?.reverseOrder === true ? count - 1 - index : index;
|
|
2064
|
+
}
|
|
2065
|
+
function spreadsheetChartCategoryLabelVisible(index, count, axis) {
|
|
2066
|
+
const explicitInterval = normalizedLabelInterval(axis?.labelInterval);
|
|
2067
|
+
const interval = explicitInterval ?? Math.max(1, Math.ceil(count / 7));
|
|
2068
|
+
return index % interval === 0 || void 0 === explicitInterval && index === count - 1;
|
|
2069
|
+
}
|
|
2070
|
+
function spreadsheetChartAxisLabelLayout(axis, chartType, position, plot, tickX, tickY) {
|
|
2071
|
+
const labelPosition = axis?.labelPosition ?? workSpreadsheetChartAxisDefaultLabelPosition(chartType, position);
|
|
2072
|
+
if ('none' === labelPosition) return null;
|
|
2073
|
+
if ('bottom' === position) return {
|
|
2074
|
+
x: tickX,
|
|
2075
|
+
y: 'high' === labelPosition ? plot.y - 7 : plot.y + plot.height + 18,
|
|
2076
|
+
textAnchor: 'middle'
|
|
2077
|
+
};
|
|
2078
|
+
if ('top' === position) return {
|
|
2079
|
+
x: tickX,
|
|
2080
|
+
y: 'low' === labelPosition ? plot.y + plot.height + 18 : plot.y - 7,
|
|
2081
|
+
textAnchor: 'middle'
|
|
2082
|
+
};
|
|
2083
|
+
if ('left' === position) {
|
|
2084
|
+
const high = 'high' === labelPosition;
|
|
2085
|
+
return {
|
|
2086
|
+
x: high ? plot.x + plot.width + 8 : plot.x - 8,
|
|
2087
|
+
y: tickY + 4,
|
|
2088
|
+
textAnchor: high ? 'start' : 'end'
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
const low = 'low' === labelPosition;
|
|
2092
|
+
return {
|
|
2093
|
+
x: low ? plot.x - 8 : plot.x + plot.width + 8,
|
|
2094
|
+
y: tickY + 4,
|
|
2095
|
+
textAnchor: low ? 'end' : 'start'
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
function spreadsheetChartMajorTickSvg(axis, position, index, x, y) {
|
|
2099
|
+
const mark = axis?.majorTickMark ?? 'none';
|
|
2100
|
+
if ('none' === mark) return '';
|
|
2101
|
+
const inward = 'bottom' === position ? [
|
|
2102
|
+
0,
|
|
2103
|
+
-1
|
|
2104
|
+
] : 'top' === position ? [
|
|
2105
|
+
0,
|
|
2106
|
+
1
|
|
2107
|
+
] : 'left' === position ? [
|
|
2108
|
+
1,
|
|
2109
|
+
0
|
|
2110
|
+
] : [
|
|
2111
|
+
-1,
|
|
2112
|
+
0
|
|
2113
|
+
];
|
|
2114
|
+
const insideLength = 'outside' === mark ? 0 : 'cross' === mark ? 3 : 5;
|
|
2115
|
+
const outsideLength = 'inside' === mark ? 0 : 'cross' === mark ? 3 : 5;
|
|
2116
|
+
const x1 = x - inward[0] * outsideLength;
|
|
2117
|
+
const y1 = y - inward[1] * outsideLength;
|
|
2118
|
+
const x2 = x + inward[0] * insideLength;
|
|
2119
|
+
const y2 = y + inward[1] * insideLength;
|
|
2120
|
+
return `<line data-axis-major-tick="${position}:${index}" x1="${roundChartNumber(x1)}" y1="${roundChartNumber(y1)}" x2="${roundChartNumber(x2)}" y2="${roundChartNumber(y2)}" stroke="#7d8798" stroke-width="1"/>`;
|
|
2121
|
+
}
|
|
2122
|
+
function spreadsheetChartAxisScale(values, axis, options = {}) {
|
|
2123
|
+
const finiteValues = values.filter(Number.isFinite).map(finiteChartNumber);
|
|
2124
|
+
const sourceValues = finiteValues.length ? finiteValues : [
|
|
2125
|
+
0,
|
|
2126
|
+
1
|
|
2127
|
+
];
|
|
2128
|
+
let automaticMinimum = Math.min(...sourceValues);
|
|
2129
|
+
let automaticMaximum = Math.max(...sourceValues);
|
|
2130
|
+
if (options.includeZero) {
|
|
2131
|
+
automaticMinimum = Math.min(0, automaticMinimum);
|
|
2132
|
+
automaticMaximum = Math.max(0, automaticMaximum);
|
|
2133
|
+
}
|
|
2134
|
+
const sourceSpan = automaticMaximum - automaticMinimum;
|
|
2135
|
+
const paddingRatio = Math.max(0, options.paddingRatio ?? 0);
|
|
2136
|
+
const padding = sourceSpan > 0 ? sourceSpan * paddingRatio : Math.max(1, Math.abs(automaticMinimum) * Math.max(paddingRatio, 0.05));
|
|
2137
|
+
automaticMinimum -= padding;
|
|
2138
|
+
automaticMaximum += padding;
|
|
2139
|
+
let minimum = Number.isFinite(axis?.minimum) ? Number(axis?.minimum) : automaticMinimum;
|
|
2140
|
+
let maximum = Number.isFinite(axis?.maximum) ? Number(axis?.maximum) : automaticMaximum;
|
|
2141
|
+
const fallbackSpan = Math.max(Number.isFinite(axis?.majorUnit) && Number(axis?.majorUnit) > 0 ? Number(axis?.majorUnit) : 0, 0.1 * Math.abs(minimum), 0.1 * Math.abs(maximum), 1);
|
|
2142
|
+
if (!(minimum < maximum)) if (axis?.minimum !== void 0 && void 0 === axis.maximum) maximum = minimum + fallbackSpan;
|
|
2143
|
+
else if (axis?.maximum !== void 0 && void 0 === axis.minimum) minimum = maximum - fallbackSpan;
|
|
2144
|
+
else {
|
|
2145
|
+
minimum = automaticMinimum;
|
|
2146
|
+
maximum = automaticMaximum;
|
|
2147
|
+
}
|
|
2148
|
+
if (!(minimum < maximum)) maximum = minimum + fallbackSpan;
|
|
2149
|
+
const span = Math.max(Number.EPSILON, maximum - minimum);
|
|
2150
|
+
const majorUnit = Number.isFinite(axis?.majorUnit) && Number(axis?.majorUnit) > 0 ? Number(axis?.majorUnit) : void 0;
|
|
2151
|
+
return {
|
|
2152
|
+
minimum,
|
|
2153
|
+
maximum,
|
|
2154
|
+
span,
|
|
2155
|
+
...void 0 !== majorUnit ? {
|
|
2156
|
+
majorUnit
|
|
2157
|
+
} : {},
|
|
2158
|
+
ticks: axisTicks(minimum, maximum, majorUnit)
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function spreadsheetChartAxisGridlinesVisible(axis, chartType, position) {
|
|
2162
|
+
return axis?.showMajorGridlines ?? workSpreadsheetChartAxisShowsMajorGridlinesByDefault(chartType, position);
|
|
2163
|
+
}
|
|
2164
|
+
function spreadsheetChartAxisScaleAttributes(axis, scale, chartType, position) {
|
|
2165
|
+
return [
|
|
2166
|
+
`data-axis-scale="${position}"`,
|
|
2167
|
+
`data-axis-minimum="${roundChartNumber(scale.minimum)}"`,
|
|
2168
|
+
`data-axis-maximum="${roundChartNumber(scale.maximum)}"`,
|
|
2169
|
+
void 0 === scale.majorUnit ? '' : `data-axis-major-unit="${roundChartNumber(scale.majorUnit)}"`,
|
|
2170
|
+
`data-axis-gridlines="${spreadsheetChartAxisGridlinesVisible(axis, chartType, position)}"`,
|
|
2171
|
+
axis?.numberFormat ? `data-axis-number-format="${escapeChartXml(axis.numberFormat)}"` : '',
|
|
2172
|
+
axis?.numberFormatSourceLinked === void 0 ? '' : `data-axis-number-format-source-linked="${axis.numberFormatSourceLinked}"`,
|
|
2173
|
+
spreadsheetChartAxisDisplayAttributes(axis, chartType, position)
|
|
2174
|
+
].filter(Boolean).join(' ');
|
|
2175
|
+
}
|
|
2176
|
+
function formatSpreadsheetChartAxisNumber(value, formatCode) {
|
|
2177
|
+
const code = formatCode?.trim();
|
|
2178
|
+
if (!code || /^general$/i.test(code)) return compactChartNumber(value);
|
|
2179
|
+
const section = code.split(';')[value < 0 ? 1 : 0] || code.split(';')[0] || code;
|
|
2180
|
+
const percent = section.includes('%');
|
|
2181
|
+
const scientific = /e[+-]/i.test(section);
|
|
2182
|
+
const decimalPattern = /[0#]+\.([0#]+)/.exec(section);
|
|
2183
|
+
const decimals = Math.min(12, decimalPattern?.[1].length ?? 0);
|
|
2184
|
+
const scaled = percent ? 100 * value : value;
|
|
2185
|
+
let formatted;
|
|
2186
|
+
if (scientific) {
|
|
2187
|
+
const [mantissa, exponentText] = scaled.toExponential(decimals).split('e');
|
|
2188
|
+
const exponent = Number(exponentText);
|
|
2189
|
+
const exponentWidth = /e[+-](0+)/i.exec(section)?.[1].length ?? 1;
|
|
2190
|
+
formatted = `${mantissa}E${exponent >= 0 ? '+' : '-'}${String(Math.abs(exponent)).padStart(exponentWidth, '0')}`;
|
|
2191
|
+
} else formatted = new Intl.NumberFormat('en-US', {
|
|
2192
|
+
useGrouping: section.includes(','),
|
|
2193
|
+
minimumFractionDigits: decimals,
|
|
2194
|
+
maximumFractionDigits: decimals
|
|
2195
|
+
}).format(scaled);
|
|
2196
|
+
const currency = /[$¥€£]/.exec(section)?.[0] ?? '';
|
|
2197
|
+
return `${currency}${formatted}${percent ? '%' : ''}`;
|
|
2198
|
+
}
|
|
2199
|
+
function axisTicks(minimum, maximum, majorUnit) {
|
|
2200
|
+
if (void 0 === majorUnit) return Array.from({
|
|
2201
|
+
length: DEFAULT_TICK_COUNT
|
|
2202
|
+
}, (_, index)=>normalizedTick(minimum + index / (DEFAULT_TICK_COUNT - 1) * (maximum - minimum)));
|
|
2203
|
+
const epsilon = Math.max(Number.EPSILON, 1e-9 * Math.abs(majorUnit));
|
|
2204
|
+
const first = Math.ceil((minimum - epsilon) / majorUnit) * majorUnit;
|
|
2205
|
+
const count = Math.floor((maximum - first + epsilon) / majorUnit) + 1;
|
|
2206
|
+
if (count <= 0) return [
|
|
2207
|
+
normalizedTick(minimum),
|
|
2208
|
+
normalizedTick(maximum)
|
|
2209
|
+
];
|
|
2210
|
+
if (count > MAX_RENDERED_TICKS) {
|
|
2211
|
+
const step = Math.ceil(count / MAX_RENDERED_TICKS);
|
|
2212
|
+
return Array.from({
|
|
2213
|
+
length: Math.ceil(count / step)
|
|
2214
|
+
}, (_, index)=>normalizedTick(first + index * step * majorUnit)).filter((value)=>value <= maximum + epsilon);
|
|
2215
|
+
}
|
|
2216
|
+
return Array.from({
|
|
2217
|
+
length: count
|
|
2218
|
+
}, (_, index)=>normalizedTick(first + index * majorUnit));
|
|
2219
|
+
}
|
|
2220
|
+
function normalizedTick(value) {
|
|
2221
|
+
return Math.abs(value) < 1e-12 ? 0 : Math.round(1e12 * value) / 1e12;
|
|
2222
|
+
}
|
|
2223
|
+
function spreadsheetLineChartSvg(chart, plot, valuePosition, layout) {
|
|
2224
|
+
const pointX = (index)=>1 === layout.categoryCount ? plot.x + plot.width / 2 : plot.x + (spreadsheetChartCategoryVisualIndex(index, layout.categoryCount, chart.axes?.bottom) + 0.5) / layout.categoryCount * plot.width;
|
|
2225
|
+
const smooth = 'line' === chart.type && normalizeWorkSpreadsheetChartSmoothLines(chart.smoothLines);
|
|
2226
|
+
return layout.series.map((points, seriesIndex)=>{
|
|
2227
|
+
const positioned = points.map((point)=>({
|
|
2228
|
+
...point,
|
|
2229
|
+
x: pointX(point.categoryIndex),
|
|
2230
|
+
startY: valuePosition(point.start),
|
|
2231
|
+
endY: valuePosition(point.end)
|
|
2232
|
+
}));
|
|
2233
|
+
if (!positioned.length) return '';
|
|
2234
|
+
const path = chartLinePath(positioned.map((point)=>[
|
|
2235
|
+
point.x,
|
|
2236
|
+
point.endY
|
|
2237
|
+
]), smooth);
|
|
2238
|
+
const series = chart.series[seriesIndex];
|
|
2239
|
+
const fill = spreadsheetChartSeriesFillStyle(series, seriesIndex, 0.2);
|
|
2240
|
+
const line = spreadsheetChartSeriesLineStyle(series, seriesIndex, 2.5);
|
|
2241
|
+
const area = 'area' === chart.type ? `<path data-area-series="${seriesIndex}" data-area-baseline="${layout.stacked ? 'stacked' : 'zero'}" d="${stackedAreaPath(positioned)}" ${fill.attributes}/>` : '';
|
|
2242
|
+
const markers = positioned.map((point)=>spreadsheetChartSeriesMarkerSvg(series, seriesIndex, point.x, point.endY, {
|
|
2243
|
+
visible: true,
|
|
2244
|
+
defaultSize: 5,
|
|
2245
|
+
attributes: `data-chart-point="${seriesIndex}:${point.categoryIndex}" data-stack-start="${roundChartNumber(point.start)}" data-stack-end="${roundChartNumber(point.end)}"`
|
|
2246
|
+
})).join('');
|
|
2247
|
+
const dataLabels = positioned.map((point)=>spreadsheetDataLabelSvg(chart, seriesIndex, point.categoryIndex, {
|
|
2248
|
+
kind: 'point',
|
|
2249
|
+
x: point.x,
|
|
2250
|
+
y: point.endY
|
|
2251
|
+
})).join('');
|
|
2252
|
+
const errorBars = layout.stacked ? '' : spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, (_direction, pointIndex, value)=>[
|
|
2253
|
+
pointX(pointIndex),
|
|
2254
|
+
valuePosition(value)
|
|
2255
|
+
]);
|
|
2256
|
+
return `${area}${errorBars}<path data-line-series="${seriesIndex}" data-smooth="${smooth}" d="${path}" fill="none" ${line.attributes} stroke-linejoin="round" stroke-linecap="round"/>${markers}${dataLabels}`;
|
|
2257
|
+
}).join('');
|
|
2258
|
+
}
|
|
2259
|
+
function stackedAreaPath(points) {
|
|
2260
|
+
const upper = points.map((point, index)=>`${index ? 'L' : 'M'} ${roundChartNumber(point.x)} ${roundChartNumber(point.endY)}`);
|
|
2261
|
+
const lower = [
|
|
2262
|
+
...points
|
|
2263
|
+
].reverse().map((point)=>`L ${roundChartNumber(point.x)} ${roundChartNumber(point.startY)}`);
|
|
2264
|
+
return [
|
|
2265
|
+
...upper,
|
|
2266
|
+
...lower,
|
|
2267
|
+
'Z'
|
|
2268
|
+
].join(' ');
|
|
2269
|
+
}
|
|
2270
|
+
function chartLinePath(points, smooth) {
|
|
2271
|
+
if (!points.length) return '';
|
|
2272
|
+
if (!smooth || points.length < 3) return points.map(([x, y], index)=>`${index ? 'L' : 'M'} ${roundChartNumber(x)} ${roundChartNumber(y)}`).join(' ');
|
|
2273
|
+
const commands = [
|
|
2274
|
+
`M ${roundChartNumber(points[0][0])} ${roundChartNumber(points[0][1])}`
|
|
2275
|
+
];
|
|
2276
|
+
for(let index = 0; index < points.length - 1; index += 1){
|
|
2277
|
+
const previous = points[index - 1] ?? points[index];
|
|
2278
|
+
const current = points[index];
|
|
2279
|
+
const next = points[index + 1];
|
|
2280
|
+
const following = points[index + 2] ?? next;
|
|
2281
|
+
commands.push(`C ${roundChartNumber(current[0] + (next[0] - previous[0]) / 6)} ${roundChartNumber(current[1] + (next[1] - previous[1]) / 6)} ${roundChartNumber(next[0] - (following[0] - current[0]) / 6)} ${roundChartNumber(next[1] - (following[1] - current[1]) / 6)} ${roundChartNumber(next[0])} ${roundChartNumber(next[1])}`);
|
|
2282
|
+
}
|
|
2283
|
+
return commands.join(' ');
|
|
2284
|
+
}
|
|
2285
|
+
function fitSpreadsheetTrendline(xValues, yValues, source) {
|
|
2286
|
+
const trendline = normalizeWorkSpreadsheetTrendline(source);
|
|
2287
|
+
const observed = yValues.flatMap((y, index)=>{
|
|
2288
|
+
const x = xValues[index];
|
|
2289
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [
|
|
2290
|
+
{
|
|
2291
|
+
x,
|
|
2292
|
+
y
|
|
2293
|
+
}
|
|
2294
|
+
] : [];
|
|
2295
|
+
});
|
|
2296
|
+
if (observed.length < 2) return null;
|
|
2297
|
+
if ('movingAverage' === trendline.type) return movingAverageFit(observed, trendline.period ?? 2);
|
|
2298
|
+
const model = regressionModel(observed, trendline);
|
|
2299
|
+
if (!model) return null;
|
|
2300
|
+
const minimum = Math.min(...observed.map((point)=>point.x)) - (trendline.backward ?? 0);
|
|
2301
|
+
const maximum = Math.max(...observed.map((point)=>point.x)) + (trendline.forward ?? 0);
|
|
2302
|
+
const points = sampledPoints(minimum, maximum, model.predict);
|
|
2303
|
+
if (points.length < 2) return null;
|
|
2304
|
+
return {
|
|
2305
|
+
points,
|
|
2306
|
+
equation: model.equation,
|
|
2307
|
+
rSquared: coefficientOfDetermination(observed, model.predict)
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
function regressionModel(observed, trendline) {
|
|
2311
|
+
if ('linear' === trendline.type) return polynomialModel(observed, 1, trendline.intercept);
|
|
2312
|
+
if ('polynomial' === trendline.type) return polynomialModel(observed, Math.min(trendline.order ?? 2, observed.length - 1), trendline.intercept);
|
|
2313
|
+
if ('exponential' === trendline.type) {
|
|
2314
|
+
const usable = observed.filter((point)=>point.y > 0);
|
|
2315
|
+
const coefficients = linearCoefficients(usable.map((point)=>({
|
|
2316
|
+
x: point.x,
|
|
2317
|
+
y: Math.log(point.y)
|
|
2318
|
+
})));
|
|
2319
|
+
if (!coefficients) return null;
|
|
2320
|
+
const [intercept, slope] = coefficients;
|
|
2321
|
+
const factor = Math.exp(intercept);
|
|
2322
|
+
return {
|
|
2323
|
+
predict: (x)=>factor * Math.exp(slope * x),
|
|
2324
|
+
equation: `y = ${formatCoefficient(factor)}e^(${formatCoefficient(slope)}x)`
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
if ('logarithmic' === trendline.type) {
|
|
2328
|
+
const usable = observed.filter((point)=>point.x > 0);
|
|
2329
|
+
const coefficients = linearCoefficients(usable.map((point)=>({
|
|
2330
|
+
x: Math.log(point.x),
|
|
2331
|
+
y: point.y
|
|
2332
|
+
})));
|
|
2333
|
+
if (!coefficients) return null;
|
|
2334
|
+
const [intercept, slope] = coefficients;
|
|
2335
|
+
return {
|
|
2336
|
+
predict: (x)=>x > 0 ? intercept + slope * Math.log(x) : NaN,
|
|
2337
|
+
equation: `y = ${formatCoefficient(slope)}ln(x) ${signedConstant(intercept)}`
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
if ('power' === trendline.type) {
|
|
2341
|
+
const usable = observed.filter((point)=>point.x > 0 && point.y > 0);
|
|
2342
|
+
const coefficients = linearCoefficients(usable.map((point)=>({
|
|
2343
|
+
x: Math.log(point.x),
|
|
2344
|
+
y: Math.log(point.y)
|
|
2345
|
+
})));
|
|
2346
|
+
if (!coefficients) return null;
|
|
2347
|
+
const [intercept, exponent] = coefficients;
|
|
2348
|
+
const factor = Math.exp(intercept);
|
|
2349
|
+
return {
|
|
2350
|
+
predict: (x)=>x > 0 ? factor * x ** exponent : NaN,
|
|
2351
|
+
equation: `y = ${formatCoefficient(factor)}x^${formatCoefficient(exponent)}`
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
function polynomialModel(observed, order, fixedIntercept) {
|
|
2357
|
+
const startPower = void 0 === fixedIntercept ? 0 : 1;
|
|
2358
|
+
const coefficientCount = order - startPower + 1;
|
|
2359
|
+
if (coefficientCount < 1 || observed.length < coefficientCount) return null;
|
|
2360
|
+
const matrix = Array.from({
|
|
2361
|
+
length: coefficientCount
|
|
2362
|
+
}, (_, row)=>Array.from({
|
|
2363
|
+
length: coefficientCount
|
|
2364
|
+
}, (_, column)=>observed.reduce((sum, point)=>sum + point.x ** (row + column + 2 * startPower), 0)));
|
|
2365
|
+
const vector = Array.from({
|
|
2366
|
+
length: coefficientCount
|
|
2367
|
+
}, (_, row)=>observed.reduce((sum, point)=>sum + point.x ** (row + startPower) * (point.y - (fixedIntercept ?? 0)), 0));
|
|
2368
|
+
const solved = solveLinearSystem(matrix, vector);
|
|
2369
|
+
if (!solved) return null;
|
|
2370
|
+
const coefficients = void 0 === fixedIntercept ? solved : [
|
|
2371
|
+
fixedIntercept,
|
|
2372
|
+
...solved
|
|
2373
|
+
];
|
|
2374
|
+
const predict = (x)=>coefficients.reduce((sum, coefficient, power)=>sum + coefficient * x ** power, 0);
|
|
2375
|
+
return {
|
|
2376
|
+
predict,
|
|
2377
|
+
equation: polynomialEquation(coefficients)
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
function linearCoefficients(observed) {
|
|
2381
|
+
if (observed.length < 2) return null;
|
|
2382
|
+
const count = observed.length;
|
|
2383
|
+
const sumX = observed.reduce((sum, point)=>sum + point.x, 0);
|
|
2384
|
+
const sumY = observed.reduce((sum, point)=>sum + point.y, 0);
|
|
2385
|
+
const sumXX = observed.reduce((sum, point)=>sum + point.x * point.x, 0);
|
|
2386
|
+
const sumXY = observed.reduce((sum, point)=>sum + point.x * point.y, 0);
|
|
2387
|
+
const denominator = count * sumXX - sumX * sumX;
|
|
2388
|
+
if (Math.abs(denominator) < 1e-12) return null;
|
|
2389
|
+
const slope = (count * sumXY - sumX * sumY) / denominator;
|
|
2390
|
+
return [
|
|
2391
|
+
(sumY - slope * sumX) / count,
|
|
2392
|
+
slope
|
|
2393
|
+
];
|
|
2394
|
+
}
|
|
2395
|
+
function solveLinearSystem(matrix, vector) {
|
|
2396
|
+
const size = vector.length;
|
|
2397
|
+
const augmented = matrix.map((row, index)=>[
|
|
2398
|
+
...row,
|
|
2399
|
+
vector[index]
|
|
2400
|
+
]);
|
|
2401
|
+
for(let column = 0; column < size; column += 1){
|
|
2402
|
+
let pivot = column;
|
|
2403
|
+
for(let row = column + 1; row < size; row += 1)if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column])) pivot = row;
|
|
2404
|
+
if (Math.abs(augmented[pivot][column]) < 1e-12) return null;
|
|
2405
|
+
[augmented[column], augmented[pivot]] = [
|
|
2406
|
+
augmented[pivot],
|
|
2407
|
+
augmented[column]
|
|
2408
|
+
];
|
|
2409
|
+
const divisor = augmented[column][column];
|
|
2410
|
+
for(let index = column; index <= size; index += 1)augmented[column][index] /= divisor;
|
|
2411
|
+
for(let row = 0; row < size; row += 1){
|
|
2412
|
+
if (row === column) continue;
|
|
2413
|
+
const factor = augmented[row][column];
|
|
2414
|
+
for(let index = column; index <= size; index += 1)augmented[row][index] -= factor * augmented[column][index];
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
return augmented.map((row)=>row[size]);
|
|
2418
|
+
}
|
|
2419
|
+
function sampledPoints(minimum, maximum, predict) {
|
|
2420
|
+
const span = maximum - minimum;
|
|
2421
|
+
if (!Number.isFinite(span) || span <= 0) return [];
|
|
2422
|
+
return Array.from({
|
|
2423
|
+
length: 65
|
|
2424
|
+
}, (_, index)=>{
|
|
2425
|
+
const x = minimum + index / 64 * span;
|
|
2426
|
+
return {
|
|
2427
|
+
x,
|
|
2428
|
+
y: predict(x)
|
|
2429
|
+
};
|
|
2430
|
+
}).filter((point)=>Number.isFinite(point.y));
|
|
2431
|
+
}
|
|
2432
|
+
function movingAverageFit(observed, period) {
|
|
2433
|
+
if (observed.length < period) return null;
|
|
2434
|
+
const points = observed.slice(period - 1).map((point, index)=>({
|
|
2435
|
+
x: point.x,
|
|
2436
|
+
y: observed.slice(index, index + period).reduce((sum, item)=>sum + item.y, 0) / period
|
|
2437
|
+
}));
|
|
2438
|
+
return {
|
|
2439
|
+
points,
|
|
2440
|
+
equation: `Moving average (${period})`
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
function coefficientOfDetermination(observed, predict) {
|
|
2444
|
+
const usable = observed.map((point)=>({
|
|
2445
|
+
actual: point.y,
|
|
2446
|
+
predicted: predict(point.x)
|
|
2447
|
+
})).filter((point)=>Number.isFinite(point.predicted));
|
|
2448
|
+
if (usable.length < 2) return;
|
|
2449
|
+
const mean = usable.reduce((sum, point)=>sum + point.actual, 0) / usable.length;
|
|
2450
|
+
const total = usable.reduce((sum, point)=>sum + (point.actual - mean) ** 2, 0);
|
|
2451
|
+
const residual = usable.reduce((sum, point)=>sum + (point.actual - point.predicted) ** 2, 0);
|
|
2452
|
+
if (total < 1e-12) return residual < 1e-12 ? 1 : 0;
|
|
2453
|
+
return Math.max(0, Math.min(1, 1 - residual / total));
|
|
2454
|
+
}
|
|
2455
|
+
function polynomialEquation(coefficients) {
|
|
2456
|
+
const terms = coefficients.map((coefficient, power)=>{
|
|
2457
|
+
if (Math.abs(coefficient) < 1e-10) return '';
|
|
2458
|
+
const absolute = formatCoefficient(Math.abs(coefficient));
|
|
2459
|
+
const variable = 0 === power ? '' : 1 === power ? 'x' : `x^${power}`;
|
|
2460
|
+
return `${coefficient < 0 ? '-' : '+'}${absolute}${variable}`;
|
|
2461
|
+
}).filter(Boolean);
|
|
2462
|
+
if (!terms.length) return 'y = 0';
|
|
2463
|
+
return `y = ${terms.join(' ').replace(/^\+/, '').replaceAll('+', '+ ').replaceAll('-', '- ').trim()}`;
|
|
2464
|
+
}
|
|
2465
|
+
function formatCoefficient(value) {
|
|
2466
|
+
return String(roundChartNumber(value));
|
|
2467
|
+
}
|
|
2468
|
+
function signedConstant(value) {
|
|
2469
|
+
return `${value < 0 ? '-' : '+'} ${formatCoefficient(Math.abs(value))}`;
|
|
2470
|
+
}
|
|
2471
|
+
function spreadsheetSeriesTrendlineFits(series, xValues) {
|
|
2472
|
+
return (series.trendlines ?? []).flatMap((source, sourceIndex)=>{
|
|
2473
|
+
const fit = fitSpreadsheetTrendline(xValues, series.values, source);
|
|
2474
|
+
return fit ? [
|
|
2475
|
+
{
|
|
2476
|
+
sourceIndex,
|
|
2477
|
+
fit
|
|
2478
|
+
}
|
|
2479
|
+
] : [];
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
function spreadsheetSeriesTrendlinesSvg(series, seriesIndex, xValues, position) {
|
|
2483
|
+
const color = WORK_SPREADSHEET_CHART_COLORS[seriesIndex % WORK_SPREADSHEET_CHART_COLORS.length];
|
|
2484
|
+
return spreadsheetSeriesTrendlineFits(series, xValues).map(({ sourceIndex, fit })=>{
|
|
2485
|
+
const trendline = normalizeWorkSpreadsheetTrendline(series.trendlines?.[sourceIndex] ?? {
|
|
2486
|
+
type: 'linear'
|
|
2487
|
+
});
|
|
2488
|
+
const points = fit.points.map((point)=>position(point.x, point.y));
|
|
2489
|
+
if (points.length < 2) return '';
|
|
2490
|
+
const path = points.map(([x, y], index)=>`${index ? 'L' : 'M'} ${roundChartNumber(x)} ${roundChartNumber(y)}`).join(' ');
|
|
2491
|
+
const equation = trendline.displayEquation && fit.equation ? fit.equation : void 0;
|
|
2492
|
+
const rSquared = trendline.displayRSquared && void 0 !== fit.rSquared ? roundChartNumber(fit.rSquared) : void 0;
|
|
2493
|
+
const label = [
|
|
2494
|
+
equation,
|
|
2495
|
+
void 0 === rSquared ? '' : `R² = ${rSquared}`
|
|
2496
|
+
].filter(Boolean).join(' · ');
|
|
2497
|
+
const labelPoint = points.at(-1);
|
|
2498
|
+
return [
|
|
2499
|
+
`<g data-trendline-series="${seriesIndex}:${sourceIndex}" data-trendline-type="${trendline.type}"${trendline.name ? ` data-trendline-name="${escapeChartXml(trendline.name)}"` : ''}${equation ? ` data-trendline-equation="${escapeChartXml(equation)}"` : ''}${void 0 === rSquared ? '' : ` data-trendline-r-squared="${rSquared}"`}>`,
|
|
2500
|
+
`<path d="${path}" fill="none" stroke="${color}" stroke-width="2" stroke-dasharray="${sourceIndex % 2 ? '3 3' : '7 4'}" stroke-linecap="round"/>`,
|
|
2501
|
+
label ? `<text x="${roundChartNumber(labelPoint[0] - 4)}" y="${roundChartNumber(labelPoint[1] - 7 - 12 * sourceIndex)}" fill="${color}" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="9" text-anchor="end">${escapeChartXml(label)}</text>` : '',
|
|
2502
|
+
'</g>'
|
|
2503
|
+
].join('');
|
|
2504
|
+
}).join('');
|
|
2505
|
+
}
|
|
2506
|
+
function spreadsheetCombinationChartSvg(chart, plot) {
|
|
2507
|
+
const values = chart.series.flatMap((series)=>combinationSeriesValues(series));
|
|
2508
|
+
if (!values.length) return emptyCombinationChartSvg(plot);
|
|
2509
|
+
const hasSecondaryAxis = chart.series.some((series)=>'secondary' === normalizeWorkSpreadsheetChartAxisGroup(series.axisGroup));
|
|
2510
|
+
const primaryValues = chart.series.filter((series)=>'primary' === normalizeWorkSpreadsheetChartAxisGroup(series.axisGroup)).flatMap((series)=>combinationSeriesValues(series));
|
|
2511
|
+
const secondaryValues = chart.series.filter((series)=>'secondary' === normalizeWorkSpreadsheetChartAxisGroup(series.axisGroup)).flatMap((series)=>combinationSeriesValues(series));
|
|
2512
|
+
const primaryAxis = combinationAxis(primaryValues.length ? primaryValues : values, plot, chart.axes?.left);
|
|
2513
|
+
const secondaryAxis = combinationAxis(secondaryValues.length ? secondaryValues : values, plot, chart.axes?.right);
|
|
2514
|
+
const categoryCount = Math.max(1, chart.categories.length, ...chart.series.map((series)=>series.values.length));
|
|
2515
|
+
const pointX = (index, axis)=>1 === categoryCount ? plot.x + plot.width / 2 : plot.x + (spreadsheetChartCategoryVisualIndex(index, categoryCount, axis) + 0.5) / categoryCount * plot.width;
|
|
2516
|
+
const axisForSeries = (seriesIndex)=>'secondary' === normalizeWorkSpreadsheetChartAxisGroup(chart.series[seriesIndex]?.axisGroup) ? secondaryAxis : primaryAxis;
|
|
2517
|
+
const pointXForSeries = (seriesIndex, index)=>pointX(index, 'secondary' === normalizeWorkSpreadsheetChartAxisGroup(chart.series[seriesIndex]?.axisGroup) ? chart.axes?.top : chart.axes?.bottom);
|
|
2518
|
+
const areas = chart.series.map((series, seriesIndex)=>'area' === normalizeWorkSpreadsheetCombinationSeriesType(series.chartType) ? combinationAreaSeriesSvg(chart, seriesIndex, (index)=>pointXForSeries(seriesIndex, index), axisForSeries(seriesIndex)) : '').join('');
|
|
2519
|
+
const columns = combinationColumnSeriesSvg(chart, plot, categoryCount, axisForSeries, pointXForSeries);
|
|
2520
|
+
const lines = chart.series.map((series, seriesIndex)=>'line' === normalizeWorkSpreadsheetCombinationSeriesType(series.chartType) ? combinationLineSeriesSvg(chart, seriesIndex, (index)=>pointXForSeries(seriesIndex, index), axisForSeries(seriesIndex)) : '').join('');
|
|
2521
|
+
const trendlines = chart.series.map((series, seriesIndex)=>spreadsheetSeriesTrendlinesSvg(series, seriesIndex, combinationTrendlineXValues(series), (x, y)=>[
|
|
2522
|
+
pointXForSeries(seriesIndex, x - 1),
|
|
2523
|
+
axisForSeries(seriesIndex).position(y)
|
|
2524
|
+
])).join('');
|
|
2525
|
+
return `<g data-combination-chart="true">${combinationAxesSvg(chart, plot, primaryAxis, secondaryAxis, hasSecondaryAxis)}${areas}${columns}${lines}${trendlines}${combinationCategoryLabelsSvg(chart, plot, categoryCount)}</g>`;
|
|
2526
|
+
}
|
|
2527
|
+
function combinationTrendlineXValues(series) {
|
|
2528
|
+
return series.values.map((_, index)=>index + 1);
|
|
2529
|
+
}
|
|
2530
|
+
function combinationTrendlineValues(series) {
|
|
2531
|
+
return spreadsheetSeriesTrendlineFits(series, combinationTrendlineXValues(series)).flatMap((item)=>item.fit.points.map((point)=>point.y));
|
|
2532
|
+
}
|
|
2533
|
+
function combinationSeriesValues(series) {
|
|
2534
|
+
return [
|
|
2535
|
+
...series.values.map(finiteChartNumber),
|
|
2536
|
+
...spreadsheetSeriesErrorBarBounds(series, 'combination', 'y'),
|
|
2537
|
+
...combinationTrendlineValues(series)
|
|
2538
|
+
];
|
|
2539
|
+
}
|
|
2540
|
+
function combinationAxis(values, plot, axis) {
|
|
2541
|
+
const scale = spreadsheetChartAxisScale(values, axis, {
|
|
2542
|
+
includeZero: true
|
|
2543
|
+
});
|
|
2544
|
+
return {
|
|
2545
|
+
...scale,
|
|
2546
|
+
position: (value)=>plot.y + (1 - spreadsheetChartAxisValueRatio(value, scale, axis)) * plot.height
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function combinationAxesSvg(chart, plot, primary, secondary, hasSecondary) {
|
|
2550
|
+
const primaryAxis = chart.axes?.left;
|
|
2551
|
+
const secondaryAxis = chart.axes?.right;
|
|
2552
|
+
const primaryGridlines = spreadsheetChartAxisGridlinesVisible(primaryAxis, chart.type, 'left');
|
|
2553
|
+
const secondaryGridlines = spreadsheetChartAxisGridlinesVisible(secondaryAxis, chart.type, 'right');
|
|
2554
|
+
const grid = primary.ticks.map((value, index)=>{
|
|
2555
|
+
const y = primary.position(value);
|
|
2556
|
+
const label = spreadsheetChartAxisLabelLayout(primaryAxis, chart.type, 'left', plot, plot.x, y);
|
|
2557
|
+
return `${primaryGridlines ? `<line x1="${plot.x}" y1="${roundChartNumber(y)}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(y)}" stroke="#e8ebf0" stroke-width="1"/>` : ''}${spreadsheetChartMajorTickSvg(primaryAxis, 'left', index, plot.x, y)}${label ? `<text data-axis-tick="left" x="${roundChartNumber(label.x)}" y="${roundChartNumber(label.y)}" fill="#717b8f" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${label.textAnchor}">${escapeChartXml(formatSpreadsheetChartAxisNumber(value, primaryAxis?.numberFormat))}</text>` : ''}`;
|
|
2558
|
+
}).join('');
|
|
2559
|
+
const secondaryLabels = hasSecondary ? `<g data-secondary-axis="true" ${spreadsheetChartAxisScaleAttributes(secondaryAxis, secondary, chart.type, 'right')}>${secondary.ticks.map((value, index)=>{
|
|
2560
|
+
const y = secondary.position(value);
|
|
2561
|
+
const label = spreadsheetChartAxisLabelLayout(secondaryAxis, chart.type, 'right', plot, plot.x + plot.width, y);
|
|
2562
|
+
return `${secondaryGridlines ? `<line x1="${plot.x}" y1="${roundChartNumber(y)}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(y)}" stroke="#eef0f4" stroke-width="1" stroke-dasharray="2 2"/>` : ''}${spreadsheetChartMajorTickSvg(secondaryAxis, 'right', index, plot.x + plot.width, y)}${label ? `<text data-axis-tick="right" x="${roundChartNumber(label.x)}" y="${roundChartNumber(label.y)}" fill="#717b8f" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${label.textAnchor}">${escapeChartXml(formatSpreadsheetChartAxisNumber(value, secondaryAxis?.numberFormat))}</text>` : ''}`;
|
|
2563
|
+
}).join('')}<line x1="${roundChartNumber(plot.x + plot.width)}" y1="${plot.y}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#aeb6c4" stroke-width="1.2"/></g>` : '';
|
|
2564
|
+
return `<g ${spreadsheetChartAxisScaleAttributes(primaryAxis, primary, chart.type, 'left')}>${grid}<line x1="${plot.x}" y1="${plot.y}" x2="${plot.x}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#aeb6c4" stroke-width="1.2"/></g><g ${spreadsheetChartAxisDisplayAttributes(chart.axes?.bottom, chart.type, 'bottom')}><line x1="${plot.x}" y1="${roundChartNumber(plot.y + plot.height)}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#aeb6c4" stroke-width="1.2"/></g>${secondaryLabels}`;
|
|
2565
|
+
}
|
|
2566
|
+
function combinationAreaSeriesSvg(chart, seriesIndex, pointX, axis) {
|
|
2567
|
+
const series = chart.series[seriesIndex];
|
|
2568
|
+
const points = series.values.map((value, index)=>[
|
|
2569
|
+
pointX(index),
|
|
2570
|
+
axis.position(value)
|
|
2571
|
+
]);
|
|
2572
|
+
if (!points.length) return '';
|
|
2573
|
+
const fill = spreadsheetChartSeriesFillStyle(series, seriesIndex, 0.2);
|
|
2574
|
+
const lineStyle = spreadsheetChartSeriesLineStyle(series, seriesIndex, 2);
|
|
2575
|
+
const line = points.map(([x, y], index)=>`${index ? 'L' : 'M'} ${roundChartNumber(x)} ${roundChartNumber(y)}`).join(' ');
|
|
2576
|
+
const baseline = axis.position(0);
|
|
2577
|
+
const path = `${line} L ${roundChartNumber(points.at(-1)?.[0] ?? pointX(0))} ${roundChartNumber(baseline)} L ${roundChartNumber(points[0][0])} ${roundChartNumber(baseline)} Z`;
|
|
2578
|
+
const dataLabels = points.map(([x, y], pointIndex)=>spreadsheetDataLabelSvg(chart, seriesIndex, pointIndex, {
|
|
2579
|
+
kind: 'point',
|
|
2580
|
+
x,
|
|
2581
|
+
y
|
|
2582
|
+
})).join('');
|
|
2583
|
+
const errorBars = spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, (_direction, pointIndex, value)=>[
|
|
2584
|
+
pointX(pointIndex),
|
|
2585
|
+
axis.position(value)
|
|
2586
|
+
]);
|
|
2587
|
+
return `<g data-combination-series="${seriesIndex}" data-series-chart-type="area" data-axis-group="${normalizeWorkSpreadsheetChartAxisGroup(series.axisGroup)}"><path data-combination-area="${seriesIndex}" d="${path}" ${fill.attributes} ${lineStyle.attributes} stroke-linejoin="round"/>${errorBars}${dataLabels}</g>`;
|
|
2588
|
+
}
|
|
2589
|
+
function combinationColumnSeriesSvg(chart, plot, categoryCount, axisForSeries, pointXForSeries) {
|
|
2590
|
+
const columnSeries = chart.series.flatMap((series, seriesIndex)=>'column' === normalizeWorkSpreadsheetCombinationSeriesType(series.chartType) ? [
|
|
2591
|
+
{
|
|
2592
|
+
series,
|
|
2593
|
+
seriesIndex
|
|
2594
|
+
}
|
|
2595
|
+
] : []);
|
|
2596
|
+
if (!columnSeries.length) return '';
|
|
2597
|
+
const groupWidth = plot.width / categoryCount;
|
|
2598
|
+
const barWidth = Math.max(2, Math.min(36, 0.72 * groupWidth / columnSeries.length));
|
|
2599
|
+
return columnSeries.map(({ series, seriesIndex }, columnIndex)=>{
|
|
2600
|
+
const axis = axisForSeries(seriesIndex);
|
|
2601
|
+
const baseline = axis.position(0);
|
|
2602
|
+
const fill = spreadsheetChartSeriesFillStyle(series, seriesIndex);
|
|
2603
|
+
const line = spreadsheetChartSeriesLineStyle(series, seriesIndex, 0);
|
|
2604
|
+
const seriesX = (pointIndex)=>pointXForSeries(seriesIndex, pointIndex) - barWidth * columnSeries.length / 2 + columnIndex * barWidth;
|
|
2605
|
+
const renderedWidth = Math.max(1, barWidth - 1.5);
|
|
2606
|
+
const bars = series.values.map((value, index)=>{
|
|
2607
|
+
const y = axis.position(value);
|
|
2608
|
+
const x = seriesX(index);
|
|
2609
|
+
return `<rect data-combination-column="${seriesIndex}:${index}" x="${roundChartNumber(x)}" y="${roundChartNumber(Math.min(y, baseline))}" width="${roundChartNumber(renderedWidth)}" height="${roundChartNumber(Math.max(1, Math.abs(y - baseline)))}" rx="1.5" ${fill.attributes} ${line.attributes}/>${spreadsheetDataLabelSvg(chart, seriesIndex, index, {
|
|
2610
|
+
kind: 'verticalBar',
|
|
2611
|
+
x: x + renderedWidth / 2,
|
|
2612
|
+
valueY: y,
|
|
2613
|
+
baselineY: baseline,
|
|
2614
|
+
value
|
|
2615
|
+
})}`;
|
|
2616
|
+
}).join('');
|
|
2617
|
+
const errorBars = spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, (_direction, pointIndex, value)=>[
|
|
2618
|
+
seriesX(pointIndex) + renderedWidth / 2,
|
|
2619
|
+
axis.position(value)
|
|
2620
|
+
]);
|
|
2621
|
+
return `<g data-combination-series="${seriesIndex}" data-series-chart-type="column" data-axis-group="${normalizeWorkSpreadsheetChartAxisGroup(series.axisGroup)}">${bars}${errorBars}</g>`;
|
|
2622
|
+
}).join('');
|
|
2623
|
+
}
|
|
2624
|
+
function combinationLineSeriesSvg(chart, seriesIndex, pointX, axis) {
|
|
2625
|
+
const series = chart.series[seriesIndex];
|
|
2626
|
+
const points = series.values.map((value, index)=>[
|
|
2627
|
+
pointX(index),
|
|
2628
|
+
axis.position(value)
|
|
2629
|
+
]);
|
|
2630
|
+
if (!points.length) return '';
|
|
2631
|
+
const line = spreadsheetChartSeriesLineStyle(series, seriesIndex, 2.5);
|
|
2632
|
+
const path = points.map(([x, y], index)=>`${index ? 'L' : 'M'} ${roundChartNumber(x)} ${roundChartNumber(y)}`).join(' ');
|
|
2633
|
+
const markers = points.map(([x, y], pointIndex)=>spreadsheetChartSeriesMarkerSvg(series, seriesIndex, x, y, {
|
|
2634
|
+
visible: true,
|
|
2635
|
+
defaultSize: 5,
|
|
2636
|
+
attributes: `data-combination-marker="${seriesIndex}:${pointIndex}"`
|
|
2637
|
+
})).join('');
|
|
2638
|
+
const dataLabels = points.map(([x, y], pointIndex)=>spreadsheetDataLabelSvg(chart, seriesIndex, pointIndex, {
|
|
2639
|
+
kind: 'point',
|
|
2640
|
+
x,
|
|
2641
|
+
y
|
|
2642
|
+
})).join('');
|
|
2643
|
+
const errorBars = spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, (_direction, pointIndex, value)=>[
|
|
2644
|
+
pointX(pointIndex),
|
|
2645
|
+
axis.position(value)
|
|
2646
|
+
]);
|
|
2647
|
+
return `<g data-combination-series="${seriesIndex}" data-series-chart-type="line" data-axis-group="${normalizeWorkSpreadsheetChartAxisGroup(series.axisGroup)}">${errorBars}<path data-combination-line="${seriesIndex}" d="${path}" fill="none" ${line.attributes} stroke-linejoin="round" stroke-linecap="round"/>${markers}${dataLabels}</g>`;
|
|
2648
|
+
}
|
|
2649
|
+
function combinationCategoryLabelsSvg(chart, plot, categoryCount) {
|
|
2650
|
+
const labels = (position, axis)=>{
|
|
2651
|
+
const axisY = 'bottom' === position ? plot.y + plot.height : plot.y;
|
|
2652
|
+
return Array.from({
|
|
2653
|
+
length: categoryCount
|
|
2654
|
+
}, (_, index)=>{
|
|
2655
|
+
const visible = spreadsheetChartCategoryLabelVisible(index, categoryCount, axis);
|
|
2656
|
+
const label = chart.categories[index] ?? String(index + 1);
|
|
2657
|
+
const visualIndex = spreadsheetChartCategoryVisualIndex(index, categoryCount, axis);
|
|
2658
|
+
const x = plot.x + (visualIndex + 0.5) / categoryCount * plot.width;
|
|
2659
|
+
const labelLayout = spreadsheetChartAxisLabelLayout(axis, chart.type, position, plot, x, axisY);
|
|
2660
|
+
return `${spreadsheetChartMajorTickSvg(axis, position, index, x, axisY)}${visible && labelLayout ? `<text data-axis-category-label="${position}:${index}" x="${roundChartNumber(labelLayout.x)}" y="${roundChartNumber(labelLayout.y)}" fill="#536078" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${labelLayout.textAnchor}">${escapeChartXml(truncateChartText(label, 13))}</text>` : ''}`;
|
|
2661
|
+
}).join('');
|
|
2662
|
+
};
|
|
2663
|
+
const bottom = `<g ${spreadsheetChartAxisDisplayAttributes(chart.axes?.bottom, chart.type, 'bottom')}>${labels('bottom', chart.axes?.bottom)}</g>`;
|
|
2664
|
+
const topAxis = chart.axes?.top;
|
|
2665
|
+
const showTopAxis = (topAxis?.labelPosition ?? 'none') !== 'none' || (topAxis?.majorTickMark ?? 'none') !== 'none';
|
|
2666
|
+
const top = showTopAxis ? `<g ${spreadsheetChartAxisDisplayAttributes(topAxis, chart.type, 'top')}><line x1="${plot.x}" y1="${plot.y}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${plot.y}" stroke="#aeb6c4" stroke-width="1.2"/>${labels('top', topAxis)}</g>` : '';
|
|
2667
|
+
return `${bottom}${top}`;
|
|
2668
|
+
}
|
|
2669
|
+
function emptyCombinationChartSvg(plot) {
|
|
2670
|
+
return `<text x="${roundChartNumber(plot.x + plot.width / 2)}" y="${roundChartNumber(plot.y + plot.height / 2)}" fill="#8791a3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="13" text-anchor="middle">没有可绘制的数据</text>`;
|
|
2671
|
+
}
|
|
2672
|
+
function spreadsheetXyChartAxesSvg(chart, plot, xBounds, yBounds) {
|
|
2673
|
+
const xAxis = chart.axes?.bottom;
|
|
2674
|
+
const yAxis = chart.axes?.left;
|
|
2675
|
+
const xGridlines = spreadsheetChartAxisGridlinesVisible(xAxis, chart.type, 'bottom');
|
|
2676
|
+
const yGridlines = spreadsheetChartAxisGridlinesVisible(yAxis, chart.type, 'left');
|
|
2677
|
+
const xTicks = xBounds.ticks.map((value, index)=>{
|
|
2678
|
+
const x = plot.x + spreadsheetChartAxisValueRatio(value, xBounds, xAxis) * plot.width;
|
|
2679
|
+
const label = spreadsheetChartAxisLabelLayout(xAxis, chart.type, 'bottom', plot, x, plot.y + plot.height);
|
|
2680
|
+
return `${xGridlines ? `<line x1="${roundChartNumber(x)}" y1="${plot.y}" x2="${roundChartNumber(x)}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#eef0f4" stroke-width="1"/>` : ''}${spreadsheetChartMajorTickSvg(xAxis, 'bottom', index, x, plot.y + plot.height)}${label ? `<text data-axis-tick="bottom" x="${roundChartNumber(label.x)}" y="${roundChartNumber(label.y)}" fill="#717b8f" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${label.textAnchor}">${escapeChartXml(formatSpreadsheetChartAxisNumber(value, xAxis?.numberFormat))}</text>` : ''}`;
|
|
2681
|
+
}).join('');
|
|
2682
|
+
const yTicks = yBounds.ticks.map((value, index)=>{
|
|
2683
|
+
const y = plot.y + (1 - spreadsheetChartAxisValueRatio(value, yBounds, yAxis)) * plot.height;
|
|
2684
|
+
const label = spreadsheetChartAxisLabelLayout(yAxis, chart.type, 'left', plot, plot.x, y);
|
|
2685
|
+
return `${yGridlines ? `<line x1="${plot.x}" y1="${roundChartNumber(y)}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(y)}" stroke="#e8ebf0" stroke-width="1"/>` : ''}${spreadsheetChartMajorTickSvg(yAxis, 'left', index, plot.x, y)}${label ? `<text data-axis-tick="left" x="${roundChartNumber(label.x)}" y="${roundChartNumber(label.y)}" fill="#717b8f" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${label.textAnchor}">${escapeChartXml(formatSpreadsheetChartAxisNumber(value, yAxis?.numberFormat))}</text>` : ''}`;
|
|
2686
|
+
}).join('');
|
|
2687
|
+
return `<g ${spreadsheetChartAxisScaleAttributes(xAxis, xBounds, chart.type, 'bottom')}>${xTicks}<line x1="${plot.x}" y1="${roundChartNumber(plot.y + plot.height)}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#aeb6c4" stroke-width="1.2"/></g><g ${spreadsheetChartAxisScaleAttributes(yAxis, yBounds, chart.type, 'left')}>${yTicks}<line x1="${plot.x}" y1="${plot.y}" x2="${plot.x}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#aeb6c4" stroke-width="1.2"/></g>`;
|
|
2688
|
+
}
|
|
2689
|
+
const WIDTH = 640;
|
|
2690
|
+
const HEIGHT = 360;
|
|
2691
|
+
function spreadsheetChartSvgDataUrl(chart) {
|
|
2692
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(spreadsheetChartSvg(chart))}`;
|
|
2693
|
+
}
|
|
2694
|
+
function spreadsheetChartSvg(chart) {
|
|
2695
|
+
const title = chart.title?.trim();
|
|
2696
|
+
const titleHeight = title ? 42 : 16;
|
|
2697
|
+
const circular = 'pie' === chart.type || 'doughnut' === chart.type;
|
|
2698
|
+
const polar = 'radar' === chart.type;
|
|
2699
|
+
const xy = 'scatter' === chart.type || 'bubble' === chart.type;
|
|
2700
|
+
const hasSecondaryAxis = 'combination' === chart.type && chart.series.some((series)=>'secondary' === normalizeWorkSpreadsheetChartAxisGroup(series.axisGroup));
|
|
2701
|
+
const axes = normalizeWorkSpreadsheetChartAxes(chart.axes, chart.type, hasSecondaryAxis);
|
|
2702
|
+
const grouping = normalizeWorkSpreadsheetChartGrouping(chart.grouping, chart.type);
|
|
2703
|
+
const renderedChart = {
|
|
2704
|
+
...chart,
|
|
2705
|
+
axes,
|
|
2706
|
+
...workSpreadsheetChartSupportsGrouping(chart.type) ? {
|
|
2707
|
+
grouping
|
|
2708
|
+
} : {},
|
|
2709
|
+
...workSpreadsheetChartSupportsBarSpacing(chart.type) ? {
|
|
2710
|
+
gapWidth: normalizeWorkSpreadsheetChartGapWidth(chart.gapWidth),
|
|
2711
|
+
overlap: normalizeWorkSpreadsheetChartOverlap(chart.overlap, grouping)
|
|
2712
|
+
} : {},
|
|
2713
|
+
...workSpreadsheetChartSupportsSmoothLines(chart.type) ? {
|
|
2714
|
+
smoothLines: normalizeWorkSpreadsheetChartSmoothLines(chart.smoothLines)
|
|
2715
|
+
} : {}
|
|
2716
|
+
};
|
|
2717
|
+
const topAxisTitleHeight = axes?.top?.title ? 18 : 0;
|
|
2718
|
+
const secondaryAxisWidth = hasSecondaryAxis ? axes?.right?.title ? 58 : 44 : 0;
|
|
2719
|
+
const plotLeft = 'bar' === chart.type ? 88 : circular || polar ? 24 : xy ? 64 : 56;
|
|
2720
|
+
const basePlot = {
|
|
2721
|
+
x: plotLeft,
|
|
2722
|
+
y: titleHeight + 12 + topAxisTitleHeight,
|
|
2723
|
+
width: WIDTH - plotLeft - 18 - secondaryAxisWidth,
|
|
2724
|
+
height: HEIGHT - titleHeight - 54 - topAxisTitleHeight
|
|
2725
|
+
};
|
|
2726
|
+
const legend = spreadsheetChartLegendLayout(renderedChart, basePlot, {
|
|
2727
|
+
width: WIDTH,
|
|
2728
|
+
height: HEIGHT,
|
|
2729
|
+
titleHeight
|
|
2730
|
+
});
|
|
2731
|
+
const plot = legend.plot;
|
|
2732
|
+
const plotAttributes = spreadsheetChartPlotAttributes(renderedChart, plot);
|
|
2733
|
+
const plotSvg = 'pie' === chart.type ? pieChartSvg(renderedChart, plot) : 'doughnut' === chart.type ? doughnutChartSvg(renderedChart, plot) : 'radar' === chart.type ? radarChartSvg(renderedChart, plot) : xy ? xyChartSvg(renderedChart, plot) : 'combination' === chart.type ? spreadsheetCombinationChartSvg(renderedChart, plot) : cartesianChartSvg(renderedChart, plot);
|
|
2734
|
+
return [
|
|
2735
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WIDTH} ${HEIGHT}" preserveAspectRatio="none" data-chart-type="${escapeChartXml(String(chart.type))}">`,
|
|
2736
|
+
'<rect width="640" height="360" rx="8" fill="#ffffff"/>',
|
|
2737
|
+
'<rect x="0.75" y="0.75" width="638.5" height="358.5" rx="7.25" fill="none" stroke="#d9dee8" stroke-width="1.5"/>',
|
|
2738
|
+
title ? `<text x="${WIDTH / 2}" y="27" fill="#172033" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="17" font-weight="600" text-anchor="middle">${escapeChartXml(title)}</text>` : '',
|
|
2739
|
+
`<g ${plotAttributes}>${plotSvg}</g>`,
|
|
2740
|
+
axes ? chartAxisTitlesSvg(axes, plot) : '',
|
|
2741
|
+
legend.svg,
|
|
2742
|
+
'</svg>'
|
|
2743
|
+
].join('');
|
|
2744
|
+
}
|
|
2745
|
+
function spreadsheetChartPlotAttributes(chart, plot) {
|
|
2746
|
+
const attributes = [
|
|
2747
|
+
'data-plot-area="true"',
|
|
2748
|
+
`data-plot-x="${roundChartNumber(plot.x)}"`,
|
|
2749
|
+
`data-plot-y="${roundChartNumber(plot.y)}"`,
|
|
2750
|
+
`data-plot-width="${roundChartNumber(plot.width)}"`,
|
|
2751
|
+
`data-plot-height="${roundChartNumber(plot.height)}"`
|
|
2752
|
+
];
|
|
2753
|
+
if (workSpreadsheetChartSupportsGrouping(chart.type)) attributes.push(`data-chart-grouping="${normalizeWorkSpreadsheetChartGrouping(chart.grouping, chart.type)}"`);
|
|
2754
|
+
if (workSpreadsheetChartSupportsBarSpacing(chart.type)) {
|
|
2755
|
+
const grouping = normalizeWorkSpreadsheetChartGrouping(chart.grouping, chart.type);
|
|
2756
|
+
attributes.push(`data-chart-gap-width="${normalizeWorkSpreadsheetChartGapWidth(chart.gapWidth)}"`);
|
|
2757
|
+
attributes.push(`data-chart-overlap="${normalizeWorkSpreadsheetChartOverlap(chart.overlap, grouping)}"`);
|
|
2758
|
+
}
|
|
2759
|
+
if (workSpreadsheetChartSupportsSmoothLines(chart.type)) attributes.push(`data-chart-smooth-lines="${normalizeWorkSpreadsheetChartSmoothLines(chart.smoothLines)}"`);
|
|
2760
|
+
return attributes.join(' ');
|
|
2761
|
+
}
|
|
2762
|
+
function chartAxisTitlesSvg(axes, plot) {
|
|
2763
|
+
const bottom = chartAxisTitleText(axes.bottom?.title);
|
|
2764
|
+
const left = chartAxisTitleText(axes.left?.title);
|
|
2765
|
+
const top = chartAxisTitleText(axes.top?.title);
|
|
2766
|
+
const right = chartAxisTitleText(axes.right?.title);
|
|
2767
|
+
const font = 'font-family="-apple-system,BlinkMacSystemFont,\'Segoe UI\',sans-serif" font-size="11" font-weight="600"';
|
|
2768
|
+
const bottomY = Math.min(HEIGHT - 7, plot.y + plot.height + 35);
|
|
2769
|
+
const leftX = Math.max(13, plot.x - 43);
|
|
2770
|
+
const rightX = plot.x + plot.width + 45;
|
|
2771
|
+
return [
|
|
2772
|
+
bottom ? `<text data-axis-title="bottom" x="${roundChartNumber(plot.x + plot.width / 2)}" y="${roundChartNumber(bottomY)}" fill="#3f4a60" ${font} text-anchor="middle">${escapeChartXml(bottom)}</text>` : '',
|
|
2773
|
+
left ? `<text data-axis-title="left" x="${roundChartNumber(leftX)}" y="${roundChartNumber(plot.y + plot.height / 2)}" fill="#3f4a60" ${font} text-anchor="middle" transform="rotate(-90 ${roundChartNumber(leftX)} ${roundChartNumber(plot.y + plot.height / 2)})">${escapeChartXml(left)}</text>` : '',
|
|
2774
|
+
top ? `<text data-axis-title="top" x="${roundChartNumber(plot.x + plot.width / 2)}" y="${roundChartNumber(plot.y - 7)}" fill="#3f4a60" ${font} text-anchor="middle">${escapeChartXml(top)}</text>` : '',
|
|
2775
|
+
right ? `<text data-axis-title="right" x="${roundChartNumber(rightX)}" y="${roundChartNumber(plot.y + plot.height / 2)}" fill="#3f4a60" ${font} text-anchor="middle" transform="rotate(90 ${roundChartNumber(rightX)} ${roundChartNumber(plot.y + plot.height / 2)})">${escapeChartXml(right)}</text>` : ''
|
|
2776
|
+
].join('');
|
|
2777
|
+
}
|
|
2778
|
+
function chartAxisTitleText(title) {
|
|
2779
|
+
return title?.trim() ? truncateChartText(title.trim(), 54) : '';
|
|
2780
|
+
}
|
|
2781
|
+
function doughnutChartSvg(chart, plot) {
|
|
2782
|
+
const hasValues = chart.series.some((series)=>series.values.some((value)=>Math.max(0, finiteChartNumber(value)) > 0));
|
|
2783
|
+
if (!hasValues) return emptyChartSvg(plot, '没有可绘制的数据');
|
|
2784
|
+
const centerX = plot.x + plot.width / 2;
|
|
2785
|
+
const centerY = plot.y + plot.height / 2;
|
|
2786
|
+
const radius = Math.max(24, 0.4 * Math.min(plot.width, plot.height));
|
|
2787
|
+
const holeSize = normalizeWorkSpreadsheetDoughnutHoleSize(chart.doughnutHoleSize);
|
|
2788
|
+
const holeRadius = holeSize / 100 * radius;
|
|
2789
|
+
const ringWidth = (radius - holeRadius) / Math.max(1, chart.series.length);
|
|
2790
|
+
const ringGap = Math.min(1, 0.18 * ringWidth);
|
|
2791
|
+
const rings = chart.series.map((_, seriesIndex)=>{
|
|
2792
|
+
const outerRadius = radius - seriesIndex * ringWidth;
|
|
2793
|
+
const innerRadius = holeRadius + (chart.series.length - seriesIndex - 1) * ringWidth + (seriesIndex < chart.series.length - 1 ? ringGap : 0);
|
|
2794
|
+
return `<g data-doughnut-series="${seriesIndex}">${doughnutRingSvg(chart, seriesIndex, centerX, centerY, innerRadius, outerRadius)}</g>`;
|
|
2795
|
+
}).join('');
|
|
2796
|
+
return `<g data-hole-size="${holeSize}">${rings}</g>`;
|
|
2797
|
+
}
|
|
2798
|
+
function doughnutRingSvg(chart, seriesIndex, centerX, centerY, innerRadius, outerRadius) {
|
|
2799
|
+
const values = chart.series[seriesIndex]?.values ?? [];
|
|
2800
|
+
const usableValues = values.map((value)=>Math.max(0, finiteChartNumber(value)));
|
|
2801
|
+
const total = usableValues.reduce((sum, value)=>sum + value, 0);
|
|
2802
|
+
if (!total) return '';
|
|
2803
|
+
const positiveIndices = usableValues.flatMap((value, index)=>value > 0 ? [
|
|
2804
|
+
index
|
|
2805
|
+
] : []);
|
|
2806
|
+
if (1 === positiveIndices.length) {
|
|
2807
|
+
const index = positiveIndices[0];
|
|
2808
|
+
const middleRadius = (innerRadius + outerRadius) / 2;
|
|
2809
|
+
const fill = spreadsheetChartSeriesFillStyle(chart.series[seriesIndex], seriesIndex, 1, WORK_SPREADSHEET_CHART_COLORS[index % WORK_SPREADSHEET_CHART_COLORS.length]);
|
|
2810
|
+
return `<circle cx="${roundChartNumber(centerX)}" cy="${roundChartNumber(centerY)}" r="${roundChartNumber(middleRadius)}" fill="none" stroke="${fill.color}" stroke-opacity="${fill.opacity}" stroke-width="${roundChartNumber(outerRadius - innerRadius)}" data-chart-slice="${seriesIndex}:${index}"/>${spreadsheetDataLabelSvg(chart, seriesIndex, index, {
|
|
2811
|
+
kind: 'circular',
|
|
2812
|
+
centerX,
|
|
2813
|
+
centerY,
|
|
2814
|
+
angle: 0,
|
|
2815
|
+
innerRadius,
|
|
2816
|
+
outerRadius
|
|
2817
|
+
})}`;
|
|
2818
|
+
}
|
|
2819
|
+
let angle = -Math.PI / 2;
|
|
2820
|
+
return usableValues.map((value, index)=>{
|
|
2821
|
+
const nextAngle = angle + value / total * Math.PI * 2;
|
|
2822
|
+
const middleAngle = angle + (nextAngle - angle) / 2;
|
|
2823
|
+
if (!value) {
|
|
2824
|
+
angle = nextAngle;
|
|
2825
|
+
return '';
|
|
2826
|
+
}
|
|
2827
|
+
const largeArc = nextAngle - angle > Math.PI ? 1 : 0;
|
|
2828
|
+
const outerStartX = centerX + Math.cos(angle) * outerRadius;
|
|
2829
|
+
const outerStartY = centerY + Math.sin(angle) * outerRadius;
|
|
2830
|
+
const outerEndX = centerX + Math.cos(nextAngle) * outerRadius;
|
|
2831
|
+
const outerEndY = centerY + Math.sin(nextAngle) * outerRadius;
|
|
2832
|
+
const innerEndX = centerX + Math.cos(nextAngle) * innerRadius;
|
|
2833
|
+
const innerEndY = centerY + Math.sin(nextAngle) * innerRadius;
|
|
2834
|
+
const innerStartX = centerX + Math.cos(angle) * innerRadius;
|
|
2835
|
+
const innerStartY = centerY + Math.sin(angle) * innerRadius;
|
|
2836
|
+
const path = [
|
|
2837
|
+
`M ${roundChartNumber(outerStartX)} ${roundChartNumber(outerStartY)}`,
|
|
2838
|
+
`A ${roundChartNumber(outerRadius)} ${roundChartNumber(outerRadius)} 0 ${largeArc} 1 ${roundChartNumber(outerEndX)} ${roundChartNumber(outerEndY)}`,
|
|
2839
|
+
`L ${roundChartNumber(innerEndX)} ${roundChartNumber(innerEndY)}`,
|
|
2840
|
+
`A ${roundChartNumber(innerRadius)} ${roundChartNumber(innerRadius)} 0 ${largeArc} 0 ${roundChartNumber(innerStartX)} ${roundChartNumber(innerStartY)}`,
|
|
2841
|
+
'Z'
|
|
2842
|
+
].join(' ');
|
|
2843
|
+
angle = nextAngle;
|
|
2844
|
+
const fill = spreadsheetChartSeriesFillStyle(chart.series[seriesIndex], seriesIndex, 1, WORK_SPREADSHEET_CHART_COLORS[index % WORK_SPREADSHEET_CHART_COLORS.length]);
|
|
2845
|
+
const line = spreadsheetChartSeriesLineStyle(chart.series[seriesIndex], seriesIndex, 1, '#FFFFFF');
|
|
2846
|
+
return `<path data-chart-slice="${seriesIndex}:${index}" d="${path}" ${fill.attributes} ${line.attributes}/>${spreadsheetDataLabelSvg(chart, seriesIndex, index, {
|
|
2847
|
+
kind: 'circular',
|
|
2848
|
+
centerX,
|
|
2849
|
+
centerY,
|
|
2850
|
+
angle: middleAngle,
|
|
2851
|
+
innerRadius,
|
|
2852
|
+
outerRadius
|
|
2853
|
+
})}`;
|
|
2854
|
+
}).join('');
|
|
2855
|
+
}
|
|
2856
|
+
function radarChartSvg(chart, plot) {
|
|
2857
|
+
const categoryCount = Math.max(0, chart.categories.length, ...chart.series.map((series)=>series.values.length));
|
|
2858
|
+
if (categoryCount < 3) return emptyChartSvg(plot, '雷达图至少需要三个分类');
|
|
2859
|
+
const values = chart.series.flatMap((series)=>series.values).map(finiteChartNumber);
|
|
2860
|
+
if (!values.length) return emptyChartSvg(plot, '没有可绘制的数据');
|
|
2861
|
+
const axis = chart.axes?.left;
|
|
2862
|
+
const categoryAxis = chart.axes?.bottom;
|
|
2863
|
+
const scale = spreadsheetChartAxisScale(values, axis, {
|
|
2864
|
+
includeZero: true
|
|
2865
|
+
});
|
|
2866
|
+
const centerX = plot.x + plot.width / 2;
|
|
2867
|
+
const centerY = plot.y + plot.height / 2;
|
|
2868
|
+
const radius = Math.max(24, 0.36 * Math.min(plot.width, plot.height));
|
|
2869
|
+
const point = (index, ratio)=>{
|
|
2870
|
+
const visualIndex = spreadsheetChartCategoryVisualIndex(index, categoryCount, categoryAxis);
|
|
2871
|
+
const angle = -Math.PI / 2 + visualIndex / categoryCount * Math.PI * 2;
|
|
2872
|
+
return [
|
|
2873
|
+
centerX + Math.cos(angle) * radius * ratio,
|
|
2874
|
+
centerY + Math.sin(angle) * radius * ratio
|
|
2875
|
+
];
|
|
2876
|
+
};
|
|
2877
|
+
const polygon = (ratio)=>Array.from({
|
|
2878
|
+
length: categoryCount
|
|
2879
|
+
}, (_, index)=>point(index, ratio).map(roundChartNumber).join(',')).join(' ');
|
|
2880
|
+
const showGridlines = spreadsheetChartAxisGridlinesVisible(axis, chart.type, 'left');
|
|
2881
|
+
const grid = scale.ticks.filter((tick)=>tick > scale.minimum).map((tick, index)=>{
|
|
2882
|
+
const ratio = spreadsheetChartAxisValueRatio(tick, scale, axis);
|
|
2883
|
+
const labelVisible = (axis?.labelPosition ?? 'nextTo') !== 'none';
|
|
2884
|
+
const tickY = centerY - radius * ratio;
|
|
2885
|
+
return `${showGridlines ? `<polygon points="${polygon(ratio)}" fill="none" stroke="#dfe4ec" stroke-width="1"/>` : ''}${spreadsheetChartMajorTickSvg(axis, 'left', index, centerX, tickY)}${labelVisible ? `<text data-axis-tick="left" x="${roundChartNumber(centerX + 4)}" y="${roundChartNumber(tickY + 10)}" fill="#717b8f" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="9" text-anchor="start">${escapeChartXml(formatSpreadsheetChartAxisNumber(tick, axis?.numberFormat))}</text>` : ''}`;
|
|
2886
|
+
}).join('');
|
|
2887
|
+
const spokes = Array.from({
|
|
2888
|
+
length: categoryCount
|
|
2889
|
+
}, (_, index)=>{
|
|
2890
|
+
const [x, y] = point(index, 1);
|
|
2891
|
+
return `<line x1="${roundChartNumber(centerX)}" y1="${roundChartNumber(centerY)}" x2="${roundChartNumber(x)}" y2="${roundChartNumber(y)}" stroke="#e8ebf0" stroke-width="1"/>`;
|
|
2892
|
+
}).join('');
|
|
2893
|
+
const labels = Array.from({
|
|
2894
|
+
length: categoryCount
|
|
2895
|
+
}, (_, index)=>{
|
|
2896
|
+
const [x, y] = point(index, 1.14);
|
|
2897
|
+
const [tickX, tickY] = point(index, 1);
|
|
2898
|
+
const horizontal = x - centerX;
|
|
2899
|
+
const anchor = Math.abs(horizontal) < 8 ? 'middle' : horizontal > 0 ? 'start' : 'end';
|
|
2900
|
+
const visible = (categoryAxis?.labelPosition ?? 'nextTo') !== 'none' && spreadsheetChartCategoryLabelVisible(index, categoryCount, categoryAxis);
|
|
2901
|
+
return `${spreadsheetChartMajorTickSvg(categoryAxis, 'bottom', index, tickX, tickY)}${visible ? `<text data-axis-category-label="${index}" x="${roundChartNumber(x)}" y="${roundChartNumber(y + 4)}" fill="#536078" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${anchor}">${escapeChartXml(truncateChartText(chart.categories[index] ?? String(index + 1), 13))}</text>` : ''}`;
|
|
2902
|
+
}).join('');
|
|
2903
|
+
const style = normalizeWorkSpreadsheetRadarStyle(chart.radarStyle);
|
|
2904
|
+
const series = chart.series.map((item, seriesIndex)=>{
|
|
2905
|
+
const customStyle = normalizeWorkSpreadsheetChartSeriesStyle(item.style);
|
|
2906
|
+
const fill = spreadsheetChartSeriesFillStyle(item, seriesIndex, customStyle?.fillColor ? 1 : 'filled' === style ? 0.24 : 0);
|
|
2907
|
+
const line = spreadsheetChartSeriesLineStyle(item, seriesIndex, 2.2);
|
|
2908
|
+
const points = Array.from({
|
|
2909
|
+
length: categoryCount
|
|
2910
|
+
}, (_, index)=>{
|
|
2911
|
+
const value = finiteChartNumber(item.values[index] ?? 0);
|
|
2912
|
+
return point(index, spreadsheetChartAxisValueRatio(value, scale, axis));
|
|
2913
|
+
});
|
|
2914
|
+
const pointList = points.map(([x, y])=>`${roundChartNumber(x)},${roundChartNumber(y)}`).join(' ');
|
|
2915
|
+
const markers = points.map(([x, y])=>spreadsheetChartSeriesMarkerSvg(item, seriesIndex, x, y, {
|
|
2916
|
+
visible: 'marker' === style,
|
|
2917
|
+
defaultSize: 5
|
|
2918
|
+
})).join('');
|
|
2919
|
+
const dataLabels = points.map(([x, y], pointIndex)=>spreadsheetDataLabelSvg(chart, seriesIndex, pointIndex, {
|
|
2920
|
+
kind: 'point',
|
|
2921
|
+
x,
|
|
2922
|
+
y
|
|
2923
|
+
})).join('');
|
|
2924
|
+
return `<g data-radar-series="${seriesIndex}"><polygon points="${pointList}" ${fill.attributes} ${line.attributes} stroke-linejoin="round"/>${markers}${dataLabels}</g>`;
|
|
2925
|
+
}).join('');
|
|
2926
|
+
return `<g data-radar-style="${style}"><g ${spreadsheetChartAxisScaleAttributes(axis, scale, chart.type, 'left')}>${grid}</g>${spokes}${series}<g ${spreadsheetChartAxisDisplayAttributes(categoryAxis, chart.type, 'bottom')}>${labels}</g></g>`;
|
|
2927
|
+
}
|
|
2928
|
+
function pieChartSvg(chart, plot) {
|
|
2929
|
+
const values = chart.series[0]?.values ?? [];
|
|
2930
|
+
const usableValues = values.map((value)=>Math.max(0, finiteChartNumber(value)));
|
|
2931
|
+
const total = usableValues.reduce((sum, value)=>sum + value, 0);
|
|
2932
|
+
if (!total) return emptyChartSvg(plot, '没有可绘制的数据');
|
|
2933
|
+
const centerX = plot.x + plot.width / 2;
|
|
2934
|
+
const centerY = plot.y + plot.height / 2;
|
|
2935
|
+
const radius = Math.max(24, 0.38 * Math.min(plot.width, plot.height));
|
|
2936
|
+
let angle = -Math.PI / 2;
|
|
2937
|
+
return usableValues.map((value, index)=>{
|
|
2938
|
+
const nextAngle = angle + value / total * Math.PI * 2;
|
|
2939
|
+
const middleAngle = angle + (nextAngle - angle) / 2;
|
|
2940
|
+
const largeArc = nextAngle - angle > Math.PI ? 1 : 0;
|
|
2941
|
+
const startX = centerX + Math.cos(angle) * radius;
|
|
2942
|
+
const startY = centerY + Math.sin(angle) * radius;
|
|
2943
|
+
const endX = centerX + Math.cos(nextAngle) * radius;
|
|
2944
|
+
const endY = centerY + Math.sin(nextAngle) * radius;
|
|
2945
|
+
const series = chart.series[0];
|
|
2946
|
+
const fill = spreadsheetChartSeriesFillStyle(series, 0, 1, WORK_SPREADSHEET_CHART_COLORS[index % WORK_SPREADSHEET_CHART_COLORS.length]);
|
|
2947
|
+
const line = spreadsheetChartSeriesLineStyle(series, 0, 0, '#FFFFFF');
|
|
2948
|
+
const path = 1 === usableValues.length ? `<circle data-chart-slice="0:${index}" cx="${roundChartNumber(centerX)}" cy="${roundChartNumber(centerY)}" r="${roundChartNumber(radius)}" ${fill.attributes} ${line.attributes}/>` : `<path data-chart-slice="0:${index}" d="M ${roundChartNumber(centerX)} ${roundChartNumber(centerY)} L ${roundChartNumber(startX)} ${roundChartNumber(startY)} A ${roundChartNumber(radius)} ${roundChartNumber(radius)} 0 ${largeArc} 1 ${roundChartNumber(endX)} ${roundChartNumber(endY)} Z" ${fill.attributes} ${line.attributes}/>`;
|
|
2949
|
+
angle = nextAngle;
|
|
2950
|
+
return `${path}${spreadsheetDataLabelSvg(chart, 0, index, {
|
|
2951
|
+
kind: 'circular',
|
|
2952
|
+
centerX,
|
|
2953
|
+
centerY,
|
|
2954
|
+
angle: middleAngle,
|
|
2955
|
+
innerRadius: 0,
|
|
2956
|
+
outerRadius: radius
|
|
2957
|
+
})}`;
|
|
2958
|
+
}).join('');
|
|
2959
|
+
}
|
|
2960
|
+
function xyChartSvg(chart, plot) {
|
|
2961
|
+
const showNegativeBubbles = true === chart.showNegativeBubbles;
|
|
2962
|
+
const seriesPoints = chart.series.map((series)=>spreadsheetXySeriesPoints(series, 'bubble' === chart.type, showNegativeBubbles));
|
|
2963
|
+
const points = seriesPoints.flat();
|
|
2964
|
+
if (!points.length) return emptyChartSvg(plot, 'bubble' === chart.type ? '气泡图需要 X、Y 和非零气泡大小数据' : '散点图需要 X 和 Y 数值');
|
|
2965
|
+
const trendlinePoints = chart.series.flatMap((series)=>spreadsheetSeriesTrendlineFits(series, spreadsheetTrendlineXValues(series)).flatMap((item)=>item.fit.points));
|
|
2966
|
+
const scaledPoints = [
|
|
2967
|
+
...points,
|
|
2968
|
+
...trendlinePoints
|
|
2969
|
+
];
|
|
2970
|
+
const paddingRatio = 'bubble' === chart.type ? 0.18 : 0.06;
|
|
2971
|
+
const xBounds = spreadsheetChartAxisScale([
|
|
2972
|
+
...scaledPoints.map((point)=>point.x),
|
|
2973
|
+
...chart.series.flatMap((series)=>spreadsheetSeriesErrorBarBounds(series, chart.type, 'x'))
|
|
2974
|
+
], chart.axes?.bottom, {
|
|
2975
|
+
paddingRatio
|
|
2976
|
+
});
|
|
2977
|
+
const yBounds = spreadsheetChartAxisScale([
|
|
2978
|
+
...scaledPoints.map((point)=>point.y),
|
|
2979
|
+
...chart.series.flatMap((series)=>spreadsheetSeriesErrorBarBounds(series, chart.type, 'y'))
|
|
2980
|
+
], chart.axes?.left, {
|
|
2981
|
+
paddingRatio
|
|
2982
|
+
});
|
|
2983
|
+
const pointX = (value)=>plot.x + spreadsheetChartAxisValueRatio(value, xBounds, chart.axes?.bottom) * plot.width;
|
|
2984
|
+
const pointY = (value)=>plot.y + (1 - spreadsheetChartAxisValueRatio(value, yBounds, chart.axes?.left)) * plot.height;
|
|
2985
|
+
const axes = spreadsheetXyChartAxesSvg(chart, plot, xBounds, yBounds);
|
|
2986
|
+
const errorBars = chart.series.map((series, seriesIndex)=>spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, (direction, pointIndex, value)=>{
|
|
2987
|
+
const x = series.xValues?.[pointIndex] ?? pointIndex + 1;
|
|
2988
|
+
const y = series.values[pointIndex] ?? 0;
|
|
2989
|
+
return 'x' === direction ? [
|
|
2990
|
+
pointX(value),
|
|
2991
|
+
pointY(y)
|
|
2992
|
+
] : [
|
|
2993
|
+
pointX(x),
|
|
2994
|
+
pointY(value)
|
|
2995
|
+
];
|
|
2996
|
+
})).join('');
|
|
2997
|
+
const trendlines = chart.series.map((series, seriesIndex)=>spreadsheetSeriesTrendlinesSvg(series, seriesIndex, spreadsheetTrendlineXValues(series), (x, y)=>[
|
|
2998
|
+
pointX(x),
|
|
2999
|
+
pointY(y)
|
|
3000
|
+
])).join('');
|
|
3001
|
+
if ('bubble' === chart.type) return `${axes}${errorBars}${bubbleSeriesSvg(chart, seriesPoints, pointX, pointY)}${trendlines}`;
|
|
3002
|
+
return `${axes}${errorBars}${scatterSeriesSvg(chart, seriesPoints, pointX, pointY)}${trendlines}`;
|
|
3003
|
+
}
|
|
3004
|
+
function spreadsheetTrendlineXValues(series) {
|
|
3005
|
+
return series.values.map((_, index)=>series.xValues?.[index] ?? index + 1);
|
|
3006
|
+
}
|
|
3007
|
+
function spreadsheetXySeriesPoints(series, requireBubbleSize, showNegativeBubbles) {
|
|
3008
|
+
return series.values.flatMap((rawY, sourceIndex)=>{
|
|
3009
|
+
const rawX = series.xValues?.[sourceIndex] ?? sourceIndex + 1;
|
|
3010
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return [];
|
|
3011
|
+
if (!requireBubbleSize) return [
|
|
3012
|
+
{
|
|
3013
|
+
x: rawX,
|
|
3014
|
+
y: rawY,
|
|
3015
|
+
sourceIndex
|
|
3016
|
+
}
|
|
3017
|
+
];
|
|
3018
|
+
const bubbleSize = series.bubbleSizes?.[sourceIndex];
|
|
3019
|
+
if ('number' != typeof bubbleSize || !Number.isFinite(bubbleSize) || 0 === bubbleSize || !showNegativeBubbles && bubbleSize < 0) return [];
|
|
3020
|
+
return [
|
|
3021
|
+
{
|
|
3022
|
+
x: rawX,
|
|
3023
|
+
y: rawY,
|
|
3024
|
+
sourceIndex,
|
|
3025
|
+
bubbleSize
|
|
3026
|
+
}
|
|
3027
|
+
];
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
function scatterSeriesSvg(chart, seriesPoints, pointX, pointY) {
|
|
3031
|
+
const style = normalizeWorkSpreadsheetScatterStyle(chart.scatterStyle);
|
|
3032
|
+
const showLine = 'marker' !== style;
|
|
3033
|
+
const showMarkers = 'marker' === style || 'lineMarker' === style || 'smoothMarker' === style;
|
|
3034
|
+
const smooth = 'smooth' === style || 'smoothMarker' === style;
|
|
3035
|
+
const series = seriesPoints.map((points, seriesIndex)=>{
|
|
3036
|
+
const sourceSeries = chart.series[seriesIndex];
|
|
3037
|
+
const lineStyle = spreadsheetChartSeriesLineStyle(sourceSeries, seriesIndex, 2.3);
|
|
3038
|
+
const positioned = points.map((point)=>({
|
|
3039
|
+
...point,
|
|
3040
|
+
plotX: pointX(point.x),
|
|
3041
|
+
plotY: pointY(point.y)
|
|
3042
|
+
}));
|
|
3043
|
+
const line = showLine && positioned.length > 1 ? `<path data-scatter-line="${seriesIndex}" d="${scatterPath(positioned, smooth)}" fill="none" ${lineStyle.attributes} stroke-linejoin="round" stroke-linecap="round"/>` : '';
|
|
3044
|
+
const markers = positioned.map((point)=>spreadsheetChartSeriesMarkerSvg(sourceSeries, seriesIndex, point.plotX, point.plotY, {
|
|
3045
|
+
visible: showMarkers,
|
|
3046
|
+
defaultSize: 5.67,
|
|
3047
|
+
attributes: `data-scatter-marker="${seriesIndex}:${point.sourceIndex}" data-point-x="${roundChartNumber(point.x)}" data-point-y="${roundChartNumber(point.y)}"`
|
|
3048
|
+
})).join('');
|
|
3049
|
+
const dataLabels = positioned.map((point)=>spreadsheetDataLabelSvg(chart, seriesIndex, point.sourceIndex, {
|
|
3050
|
+
kind: 'point',
|
|
3051
|
+
x: point.plotX,
|
|
3052
|
+
y: point.plotY
|
|
3053
|
+
})).join('');
|
|
3054
|
+
return `<g data-scatter-series="${seriesIndex}">${line}${markers}${dataLabels}</g>`;
|
|
3055
|
+
}).join('');
|
|
3056
|
+
return `<g data-scatter-style="${style}">${series}</g>`;
|
|
3057
|
+
}
|
|
3058
|
+
function scatterPath(points, smooth) {
|
|
3059
|
+
if (!points.length) return '';
|
|
3060
|
+
if (!smooth || points.length < 3) return points.map((point, index)=>`${index ? 'L' : 'M'} ${roundChartNumber(point.plotX)} ${roundChartNumber(point.plotY)}`).join(' ');
|
|
3061
|
+
const commands = [
|
|
3062
|
+
`M ${roundChartNumber(points[0].plotX)} ${roundChartNumber(points[0].plotY)}`
|
|
3063
|
+
];
|
|
3064
|
+
for(let index = 0; index < points.length - 1; index += 1){
|
|
3065
|
+
const previous = points[index - 1] ?? points[index];
|
|
3066
|
+
const current = points[index];
|
|
3067
|
+
const next = points[index + 1];
|
|
3068
|
+
const following = points[index + 2] ?? next;
|
|
3069
|
+
const control1X = current.plotX + (next.plotX - previous.plotX) / 6;
|
|
3070
|
+
const control1Y = current.plotY + (next.plotY - previous.plotY) / 6;
|
|
3071
|
+
const control2X = next.plotX - (following.plotX - current.plotX) / 6;
|
|
3072
|
+
const control2Y = next.plotY - (following.plotY - current.plotY) / 6;
|
|
3073
|
+
commands.push(`C ${roundChartNumber(control1X)} ${roundChartNumber(control1Y)} ${roundChartNumber(control2X)} ${roundChartNumber(control2Y)} ${roundChartNumber(next.plotX)} ${roundChartNumber(next.plotY)}`);
|
|
3074
|
+
}
|
|
3075
|
+
return commands.join(' ');
|
|
3076
|
+
}
|
|
3077
|
+
function bubbleSeriesSvg(chart, seriesPoints, pointX, pointY) {
|
|
3078
|
+
const scale = normalizeWorkSpreadsheetBubbleScale(chart.bubbleScale);
|
|
3079
|
+
const sizeRepresents = normalizeWorkSpreadsheetBubbleSizeRepresents(chart.bubbleSizeRepresents);
|
|
3080
|
+
const maximumSize = Math.max(1, ...seriesPoints.flatMap((points)=>points.map((point)=>Math.abs(point.bubbleSize ?? 0))));
|
|
3081
|
+
const maximumRadius = Math.max(0, Math.min(54, scale / 100 * 18));
|
|
3082
|
+
const series = seriesPoints.map((points, seriesIndex)=>{
|
|
3083
|
+
const sourceSeries = chart.series[seriesIndex];
|
|
3084
|
+
const sourceStyle = normalizeWorkSpreadsheetChartSeriesStyle(sourceSeries.style);
|
|
3085
|
+
const fill = spreadsheetChartSeriesFillStyle(sourceSeries, seriesIndex, 0.34);
|
|
3086
|
+
const line = spreadsheetChartSeriesLineStyle(sourceSeries, seriesIndex, 1.8);
|
|
3087
|
+
const bubbles = points.map((point)=>{
|
|
3088
|
+
const size = point.bubbleSize ?? 0;
|
|
3089
|
+
const ratio = Math.abs(size) / maximumSize;
|
|
3090
|
+
const radius = maximumRadius * ('area' === sizeRepresents ? Math.sqrt(ratio) : ratio);
|
|
3091
|
+
const negative = size < 0;
|
|
3092
|
+
const bubbleOpacity = sourceStyle?.fillTransparency === void 0 ? negative ? 0.2 : 0.34 : fill.opacity;
|
|
3093
|
+
return `<circle data-bubble-point="${seriesIndex}:${point.sourceIndex}" data-point-x="${roundChartNumber(point.x)}" data-point-y="${roundChartNumber(point.y)}" data-bubble-size="${roundChartNumber(size)}" data-negative-bubble="${negative}" cx="${roundChartNumber(pointX(point.x))}" cy="${roundChartNumber(pointY(point.y))}" r="${roundChartNumber(radius)}" fill="${fill.color}" fill-opacity="${bubbleOpacity}" ${line.attributes}${negative && 'solid' === line.dash ? ' stroke-dasharray="3 2"' : ''}/>${spreadsheetDataLabelSvg(chart, seriesIndex, point.sourceIndex, {
|
|
3094
|
+
kind: 'point',
|
|
3095
|
+
x: pointX(point.x),
|
|
3096
|
+
y: pointY(point.y) - radius
|
|
3097
|
+
})}`;
|
|
3098
|
+
}).join('');
|
|
3099
|
+
return `<g data-bubble-series="${seriesIndex}">${bubbles}</g>`;
|
|
3100
|
+
}).join('');
|
|
3101
|
+
return `<g data-bubble-scale="${scale}" data-show-negative-bubbles="${true === chart.showNegativeBubbles}" data-bubble-size-represents="${sizeRepresents}">${series}</g>`;
|
|
3102
|
+
}
|
|
3103
|
+
function cartesianChartSvg(chart, plot) {
|
|
3104
|
+
const layout = spreadsheetChartSeriesLayout(chart);
|
|
3105
|
+
const trendlineValues = layout.stacked ? [] : chart.series.flatMap((series)=>spreadsheetSeriesTrendlineFits(series, categoryTrendlineXValues(series)).flatMap((item)=>item.fit.points.map((point)=>point.y)));
|
|
3106
|
+
const values = [
|
|
3107
|
+
...layout.scaleValues,
|
|
3108
|
+
...layout.stacked ? [] : chart.series.flatMap((series)=>spreadsheetSeriesErrorBarBounds(series, chart.type, 'y')),
|
|
3109
|
+
...trendlineValues
|
|
3110
|
+
];
|
|
3111
|
+
if (!values.length) return emptyChartSvg(plot, '没有可绘制的数据');
|
|
3112
|
+
if ('bar' === chart.type) return horizontalBarChartSvg(chart, plot, layout, values);
|
|
3113
|
+
const axis = chart.axes?.left;
|
|
3114
|
+
const scale = spreadsheetChartAxisScale(values, axis, {
|
|
3115
|
+
includeZero: true
|
|
3116
|
+
});
|
|
3117
|
+
const valuePosition = (value)=>plot.y + (1 - spreadsheetChartAxisValueRatio(value, scale, axis)) * plot.height;
|
|
3118
|
+
const baseline = valuePosition(0);
|
|
3119
|
+
const numberFormat = axis?.numberFormat ?? ('percentStacked' === layout.grouping ? '0%' : void 0);
|
|
3120
|
+
const showGridlines = spreadsheetChartAxisGridlinesVisible(axis, chart.type, 'left');
|
|
3121
|
+
const grid = scale.ticks.map((value, index)=>{
|
|
3122
|
+
const y = valuePosition(value);
|
|
3123
|
+
const label = spreadsheetChartAxisLabelLayout(axis, chart.type, 'left', plot, plot.x, y);
|
|
3124
|
+
return `${showGridlines ? `<line x1="${plot.x}" y1="${roundChartNumber(y)}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(y)}" stroke="#e8ebf0" stroke-width="1"/>` : ''}${spreadsheetChartMajorTickSvg(axis, 'left', index, plot.x, y)}${label ? `<text data-axis-tick="left" x="${roundChartNumber(label.x)}" y="${roundChartNumber(label.y)}" fill="#717b8f" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${label.textAnchor}">${escapeChartXml(formatSpreadsheetChartAxisNumber(value, numberFormat))}</text>` : ''}`;
|
|
3125
|
+
}).join('');
|
|
3126
|
+
const axes = [
|
|
3127
|
+
`<g ${spreadsheetChartAxisScaleAttributes(axis, scale, chart.type, 'left')}>${grid}<line x1="${plot.x}" y1="${plot.y}" x2="${plot.x}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#aeb6c4" stroke-width="1.2"/></g>`,
|
|
3128
|
+
`<g ${spreadsheetChartAxisDisplayAttributes(chart.axes?.bottom, chart.type, 'bottom')}><line x1="${plot.x}" y1="${roundChartNumber(baseline)}" x2="${roundChartNumber(plot.x + plot.width)}" y2="${roundChartNumber(baseline)}" stroke="#aeb6c4" stroke-width="1.2"/></g>`
|
|
3129
|
+
].join('');
|
|
3130
|
+
const trendlines = layout.stacked ? '' : cartesianTrendlinesSvg(chart, plot, valuePosition);
|
|
3131
|
+
if ('line' === chart.type || 'area' === chart.type) return `${axes}${spreadsheetLineChartSvg(chart, plot, valuePosition, layout)}${trendlines}${categoryLabelsSvg(chart, plot, baseline)}`;
|
|
3132
|
+
return `${axes}${barChartSvg(chart, plot, valuePosition, baseline, layout)}${trendlines}${categoryLabelsSvg(chart, plot, baseline)}`;
|
|
3133
|
+
}
|
|
3134
|
+
function cartesianTrendlinesSvg(chart, plot, valuePosition) {
|
|
3135
|
+
const categoryCount = Math.max(1, chart.categories.length, ...chart.series.map((series)=>series.values.length));
|
|
3136
|
+
const categoryAxis = chart.axes?.bottom;
|
|
3137
|
+
const categoryPosition = (value)=>1 === categoryCount ? plot.x + plot.width / 2 : plot.x + spreadsheetChartCategoryVisualIndex(value - 1, categoryCount, categoryAxis) / Math.max(1, categoryCount - 1) * plot.width;
|
|
3138
|
+
return chart.series.map((series, seriesIndex)=>spreadsheetSeriesTrendlinesSvg(series, seriesIndex, categoryTrendlineXValues(series), (x, y)=>[
|
|
3139
|
+
categoryPosition(x),
|
|
3140
|
+
valuePosition(y)
|
|
3141
|
+
])).join('');
|
|
3142
|
+
}
|
|
3143
|
+
function categoryTrendlineXValues(series) {
|
|
3144
|
+
return series.values.map((_, index)=>index + 1);
|
|
3145
|
+
}
|
|
3146
|
+
function barChartSvg(chart, plot, valuePosition, baseline, layout) {
|
|
3147
|
+
const categoryCount = layout.categoryCount;
|
|
3148
|
+
const seriesCount = Math.max(1, chart.series.length);
|
|
3149
|
+
const groupWidth = plot.width / categoryCount;
|
|
3150
|
+
const categoryAxis = chart.axes?.bottom;
|
|
3151
|
+
const geometry = spreadsheetChartBarGeometry(groupWidth, seriesCount, chart, 44);
|
|
3152
|
+
return layout.series.map((points, seriesIndex)=>{
|
|
3153
|
+
const seriesX = (pointIndex)=>plot.x + spreadsheetChartCategoryVisualIndex(pointIndex, categoryCount, categoryAxis) * groupWidth + geometry.offset(seriesIndex);
|
|
3154
|
+
const renderedWidth = geometry.renderedSize;
|
|
3155
|
+
const bars = points.map((point)=>{
|
|
3156
|
+
const startY = layout.stacked ? valuePosition(point.start) : baseline;
|
|
3157
|
+
const endY = valuePosition(point.end);
|
|
3158
|
+
const x = seriesX(point.categoryIndex);
|
|
3159
|
+
return `<rect data-chart-bar="${seriesIndex}:${point.categoryIndex}" data-stack-start="${roundChartNumber(point.start)}" data-stack-end="${roundChartNumber(point.end)}" x="${roundChartNumber(x)}" y="${roundChartNumber(Math.min(endY, startY))}" width="${roundChartNumber(renderedWidth)}" height="${roundChartNumber(Math.max(1, Math.abs(endY - startY)))}" rx="1.5" ${spreadsheetChartSeriesFillStyle(chart.series[seriesIndex], seriesIndex).attributes} ${spreadsheetChartSeriesLineStyle(chart.series[seriesIndex], seriesIndex, 0).attributes}/>${spreadsheetDataLabelSvg(chart, seriesIndex, point.categoryIndex, {
|
|
3160
|
+
kind: 'verticalBar',
|
|
3161
|
+
x: x + renderedWidth / 2,
|
|
3162
|
+
valueY: endY,
|
|
3163
|
+
baselineY: startY,
|
|
3164
|
+
value: point.rawValue
|
|
3165
|
+
})}`;
|
|
3166
|
+
}).join('');
|
|
3167
|
+
const errorBars = layout.stacked ? '' : spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, (_direction, pointIndex, value)=>[
|
|
3168
|
+
seriesX(pointIndex) + renderedWidth / 2,
|
|
3169
|
+
valuePosition(value)
|
|
3170
|
+
]);
|
|
3171
|
+
return `${bars}${errorBars}`;
|
|
3172
|
+
}).join('');
|
|
3173
|
+
}
|
|
3174
|
+
function horizontalBarChartSvg(chart, plot, layout, values) {
|
|
3175
|
+
const axis = chart.axes?.bottom;
|
|
3176
|
+
const scale = spreadsheetChartAxisScale(values, axis, {
|
|
3177
|
+
includeZero: true
|
|
3178
|
+
});
|
|
3179
|
+
const valuePosition = (value)=>plot.x + spreadsheetChartAxisValueRatio(value, scale, axis) * plot.width;
|
|
3180
|
+
const zero = valuePosition(0);
|
|
3181
|
+
const categoryCount = layout.categoryCount;
|
|
3182
|
+
const seriesCount = Math.max(1, chart.series.length);
|
|
3183
|
+
const groupHeight = plot.height / categoryCount;
|
|
3184
|
+
const geometry = spreadsheetChartBarGeometry(groupHeight, seriesCount, chart, 30);
|
|
3185
|
+
const categoryAxis = chart.axes?.left;
|
|
3186
|
+
const numberFormat = axis?.numberFormat ?? ('percentStacked' === layout.grouping ? '0%' : void 0);
|
|
3187
|
+
const showGridlines = spreadsheetChartAxisGridlinesVisible(axis, chart.type, 'bottom');
|
|
3188
|
+
const grid = scale.ticks.map((value, index)=>{
|
|
3189
|
+
const x = valuePosition(value);
|
|
3190
|
+
const label = spreadsheetChartAxisLabelLayout(axis, chart.type, 'bottom', plot, x, plot.y + plot.height);
|
|
3191
|
+
return `${showGridlines ? `<line x1="${roundChartNumber(x)}" y1="${plot.y}" x2="${roundChartNumber(x)}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#e8ebf0" stroke-width="1"/>` : ''}${spreadsheetChartMajorTickSvg(axis, 'bottom', index, x, plot.y + plot.height)}${label ? `<text data-axis-tick="bottom" x="${roundChartNumber(label.x)}" y="${roundChartNumber(label.y)}" fill="#717b8f" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${label.textAnchor}">${escapeChartXml(formatSpreadsheetChartAxisNumber(value, numberFormat))}</text>` : ''}`;
|
|
3192
|
+
}).join('');
|
|
3193
|
+
const labels = Array.from({
|
|
3194
|
+
length: categoryCount
|
|
3195
|
+
}, (_, index)=>{
|
|
3196
|
+
const visible = spreadsheetChartCategoryLabelVisible(index, categoryCount, categoryAxis);
|
|
3197
|
+
const label = chart.categories[index] ?? String(index + 1);
|
|
3198
|
+
const visualIndex = spreadsheetChartCategoryVisualIndex(index, categoryCount, categoryAxis);
|
|
3199
|
+
const y = plot.y + (visualIndex + 0.5) * groupHeight;
|
|
3200
|
+
const labelLayout = spreadsheetChartAxisLabelLayout(categoryAxis, chart.type, 'left', plot, zero, y);
|
|
3201
|
+
return `${spreadsheetChartMajorTickSvg(categoryAxis, 'left', index, zero, y)}${visible && labelLayout ? `<text data-axis-category-label="${index}" x="${roundChartNumber(labelLayout.x)}" y="${roundChartNumber(labelLayout.y)}" fill="#536078" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${labelLayout.textAnchor}">${escapeChartXml(truncateChartText(label, 11))}</text>` : ''}`;
|
|
3202
|
+
}).join('');
|
|
3203
|
+
const bars = layout.series.map((points, seriesIndex)=>{
|
|
3204
|
+
const seriesY = (pointIndex)=>plot.y + spreadsheetChartCategoryVisualIndex(pointIndex, categoryCount, categoryAxis) * groupHeight + geometry.offset(seriesIndex);
|
|
3205
|
+
const renderedHeight = geometry.renderedSize;
|
|
3206
|
+
const fill = spreadsheetChartSeriesFillStyle(chart.series[seriesIndex], seriesIndex);
|
|
3207
|
+
const line = spreadsheetChartSeriesLineStyle(chart.series[seriesIndex], seriesIndex, 0);
|
|
3208
|
+
const seriesBars = points.map((point)=>{
|
|
3209
|
+
const startX = layout.stacked ? valuePosition(point.start) : zero;
|
|
3210
|
+
const endX = valuePosition(point.end);
|
|
3211
|
+
const y = seriesY(point.categoryIndex);
|
|
3212
|
+
return `<rect data-chart-bar="${seriesIndex}:${point.categoryIndex}" data-stack-start="${roundChartNumber(point.start)}" data-stack-end="${roundChartNumber(point.end)}" x="${roundChartNumber(Math.min(endX, startX))}" y="${roundChartNumber(y)}" width="${roundChartNumber(Math.max(1, Math.abs(endX - startX)))}" height="${roundChartNumber(renderedHeight)}" rx="1.5" ${fill.attributes} ${line.attributes}/>${spreadsheetDataLabelSvg(chart, seriesIndex, point.categoryIndex, {
|
|
3213
|
+
kind: 'horizontalBar',
|
|
3214
|
+
y: y + renderedHeight / 2,
|
|
3215
|
+
valueX: endX,
|
|
3216
|
+
baselineX: startX,
|
|
3217
|
+
value: point.rawValue
|
|
3218
|
+
})}`;
|
|
3219
|
+
}).join('');
|
|
3220
|
+
const errorBars = layout.stacked ? '' : spreadsheetSeriesErrorBarsSvg(chart, seriesIndex, (_direction, pointIndex, value)=>[
|
|
3221
|
+
valuePosition(value),
|
|
3222
|
+
seriesY(pointIndex) + renderedHeight / 2
|
|
3223
|
+
]);
|
|
3224
|
+
return `${seriesBars}${errorBars}`;
|
|
3225
|
+
}).join('');
|
|
3226
|
+
const trendlines = layout.stacked ? '' : chart.series.map((series, seriesIndex)=>spreadsheetSeriesTrendlinesSvg(series, seriesIndex, categoryTrendlineXValues(series), (category, value)=>[
|
|
3227
|
+
valuePosition(value),
|
|
3228
|
+
plot.y + (spreadsheetChartCategoryVisualIndex(category - 1, categoryCount, categoryAxis) + 0.5) / categoryCount * plot.height
|
|
3229
|
+
])).join('');
|
|
3230
|
+
return `<g ${spreadsheetChartAxisScaleAttributes(axis, scale, chart.type, 'bottom')}>${grid}<line x1="${roundChartNumber(zero)}" y1="${plot.y}" x2="${roundChartNumber(zero)}" y2="${roundChartNumber(plot.y + plot.height)}" stroke="#aeb6c4" stroke-width="1.2"/></g><g ${spreadsheetChartAxisDisplayAttributes(categoryAxis, chart.type, 'left')}>${labels}</g>${bars}${trendlines}`;
|
|
3231
|
+
}
|
|
3232
|
+
function categoryLabelsSvg(chart, plot, axisY) {
|
|
3233
|
+
const categoryCount = Math.max(1, chart.categories.length, ...chart.series.map((series)=>series.values.length));
|
|
3234
|
+
const axis = chart.axes?.bottom;
|
|
3235
|
+
const labels = Array.from({
|
|
3236
|
+
length: categoryCount
|
|
3237
|
+
}, (_, index)=>{
|
|
3238
|
+
const visible = spreadsheetChartCategoryLabelVisible(index, categoryCount, axis);
|
|
3239
|
+
const label = chart.categories[index] ?? String(index + 1);
|
|
3240
|
+
const visualIndex = spreadsheetChartCategoryVisualIndex(index, categoryCount, axis);
|
|
3241
|
+
const x = plot.x + (visualIndex + 0.5) / categoryCount * plot.width;
|
|
3242
|
+
const labelLayout = spreadsheetChartAxisLabelLayout(axis, chart.type, 'bottom', plot, x, axisY);
|
|
3243
|
+
return `${spreadsheetChartMajorTickSvg(axis, 'bottom', index, x, axisY)}${visible && labelLayout ? `<text data-axis-category-label="${index}" x="${roundChartNumber(labelLayout.x)}" y="${roundChartNumber(labelLayout.y)}" fill="#536078" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="10" text-anchor="${labelLayout.textAnchor}">${escapeChartXml(truncateChartText(label, 13))}</text>` : ''}`;
|
|
3244
|
+
}).join('');
|
|
3245
|
+
return `<g ${spreadsheetChartAxisDisplayAttributes(axis, chart.type, 'bottom')}>${labels}</g>`;
|
|
3246
|
+
}
|
|
3247
|
+
function emptyChartSvg(plot, message) {
|
|
3248
|
+
return `<text x="${roundChartNumber(plot.x + plot.width / 2)}" y="${roundChartNumber(plot.y + plot.height / 2)}" fill="#8791a3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="13" text-anchor="middle">${escapeChartXml(message)}</text>`;
|
|
3249
|
+
}
|
|
3250
|
+
const XLSX_EMU_PER_PIXEL = 9525;
|
|
3251
|
+
const DEFAULT_COLUMN_WIDTH = 96;
|
|
3252
|
+
const DEFAULT_ROW_HEIGHT = 24;
|
|
3253
|
+
function readXlsxDrawingAnchor(anchor) {
|
|
3254
|
+
const extent = directChild(anchor, 'ext');
|
|
3255
|
+
const position = directChild(anchor, 'pos');
|
|
3256
|
+
return {
|
|
3257
|
+
from: readMarker(directChild(anchor, 'from')),
|
|
3258
|
+
to: readMarker(directChild(anchor, 'to')),
|
|
3259
|
+
leftEmu: numberAttribute(position, 'x'),
|
|
3260
|
+
topEmu: numberAttribute(position, 'y'),
|
|
3261
|
+
widthEmu: numberAttribute(extent, 'cx'),
|
|
3262
|
+
heightEmu: numberAttribute(extent, 'cy')
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function xlsxDrawingAnchorToBounds(anchor, config) {
|
|
3266
|
+
const left = anchor.from ? markerPosition(anchor.from.column, anchor.from.columnOffsetEmu, (index)=>spreadsheetColumnWidth(config, index)) : emuToPixels(anchor.leftEmu);
|
|
3267
|
+
const top = anchor.from ? markerPosition(anchor.from.row, anchor.from.rowOffsetEmu, (index)=>spreadsheetRowHeight(config, index)) : emuToPixels(anchor.topEmu);
|
|
3268
|
+
const right = anchor.to ? markerPosition(anchor.to.column, anchor.to.columnOffsetEmu, (index)=>spreadsheetColumnWidth(config, index)) : left + emuToPixels(anchor.widthEmu);
|
|
3269
|
+
const bottom = anchor.to ? markerPosition(anchor.to.row, anchor.to.rowOffsetEmu, (index)=>spreadsheetRowHeight(config, index)) : top + emuToPixels(anchor.heightEmu);
|
|
3270
|
+
if (![
|
|
3271
|
+
left,
|
|
3272
|
+
top,
|
|
3273
|
+
right,
|
|
3274
|
+
bottom
|
|
3275
|
+
].every(Number.isFinite)) return null;
|
|
3276
|
+
return {
|
|
3277
|
+
left: roundPosition(Math.max(0, left)),
|
|
3278
|
+
top: roundPosition(Math.max(0, top)),
|
|
3279
|
+
width: roundPosition(Math.max(1, right - left)),
|
|
3280
|
+
height: roundPosition(Math.max(1, bottom - top))
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
function xlsxTwoCellAnchorMarkers(bounds, sheet) {
|
|
3284
|
+
const fromColumn = axisMarker(bounds.left, (index)=>spreadsheetColumnWidth(sheet.config, index));
|
|
3285
|
+
const toColumn = axisMarker(bounds.left + bounds.width, (index)=>spreadsheetColumnWidth(sheet.config, index));
|
|
3286
|
+
const fromRow = axisMarker(bounds.top, (index)=>spreadsheetRowHeight(sheet.config, index));
|
|
3287
|
+
const toRow = axisMarker(bounds.top + bounds.height, (index)=>spreadsheetRowHeight(sheet.config, index));
|
|
3288
|
+
return `${markerXml('from', fromColumn, fromRow)}${markerXml('to', toColumn, toRow)}`;
|
|
3289
|
+
}
|
|
3290
|
+
function spreadsheetColumnWidth(config, index) {
|
|
3291
|
+
const key = String(index);
|
|
3292
|
+
if (Object.hasOwn(config?.colhidden ?? {}, key)) return 0;
|
|
3293
|
+
const width = config?.columnlen?.[key];
|
|
3294
|
+
return 'number' == typeof width && width > 0 ? width : DEFAULT_COLUMN_WIDTH;
|
|
3295
|
+
}
|
|
3296
|
+
function spreadsheetRowHeight(config, index) {
|
|
3297
|
+
const key = String(index);
|
|
3298
|
+
if (Object.hasOwn(config?.rowhidden ?? {}, key)) return 0;
|
|
3299
|
+
const height = config?.rowlen?.[key];
|
|
3300
|
+
return 'number' == typeof height && height > 0 ? height : DEFAULT_ROW_HEIGHT;
|
|
3301
|
+
}
|
|
3302
|
+
function readMarker(element) {
|
|
3303
|
+
if (!element) return;
|
|
3304
|
+
return {
|
|
3305
|
+
column: nonNegativeInteger(directChild(element, 'col')?.textContent),
|
|
3306
|
+
row: nonNegativeInteger(directChild(element, 'row')?.textContent),
|
|
3307
|
+
columnOffsetEmu: nonNegativeNumber(directChild(element, 'colOff')?.textContent),
|
|
3308
|
+
rowOffsetEmu: nonNegativeNumber(directChild(element, 'rowOff')?.textContent)
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
function numberAttribute(element, name) {
|
|
3312
|
+
if (!element) return;
|
|
3313
|
+
const value = Number(attribute(element, name));
|
|
3314
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
3315
|
+
}
|
|
3316
|
+
function nonNegativeInteger(value) {
|
|
3317
|
+
const parsed = Number(value);
|
|
3318
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
3319
|
+
}
|
|
3320
|
+
function nonNegativeNumber(value) {
|
|
3321
|
+
const parsed = Number(value);
|
|
3322
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
3323
|
+
}
|
|
3324
|
+
function markerPosition(index, offsetEmu, size) {
|
|
3325
|
+
let position = 0;
|
|
3326
|
+
for(let current = 0; current < index; current += 1)position += size(current);
|
|
3327
|
+
return position + emuToPixels(offsetEmu);
|
|
3328
|
+
}
|
|
3329
|
+
function emuToPixels(value) {
|
|
3330
|
+
return (value ?? 0) / XLSX_EMU_PER_PIXEL;
|
|
3331
|
+
}
|
|
3332
|
+
function axisMarker(position, size) {
|
|
3333
|
+
let remaining = Math.max(0, Number.isFinite(position) ? position : 0);
|
|
3334
|
+
let index = 0;
|
|
3335
|
+
while(index < 1048576){
|
|
3336
|
+
const currentSize = Math.max(0, size(index));
|
|
3337
|
+
if (currentSize > 0 && remaining < currentSize) break;
|
|
3338
|
+
if (currentSize > 0) remaining -= currentSize;
|
|
3339
|
+
index += 1;
|
|
3340
|
+
}
|
|
3341
|
+
return {
|
|
3342
|
+
index,
|
|
3343
|
+
offsetEmu: Math.round(remaining * XLSX_EMU_PER_PIXEL)
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
function markerXml(kind, column, row) {
|
|
3347
|
+
return [
|
|
3348
|
+
`<xdr:${kind}>`,
|
|
3349
|
+
`<xdr:col>${column.index}</xdr:col><xdr:colOff>${column.offsetEmu}</xdr:colOff>`,
|
|
3350
|
+
`<xdr:row>${row.index}</xdr:row><xdr:rowOff>${row.offsetEmu}</xdr:rowOff>`,
|
|
3351
|
+
`</xdr:${kind}>`
|
|
3352
|
+
].join('');
|
|
3353
|
+
}
|
|
3354
|
+
function roundPosition(value) {
|
|
3355
|
+
return Math.round(1000 * value) / 1000;
|
|
3356
|
+
}
|
|
3357
|
+
const CHART_PREVIEW_IMAGE_PREFIX = 'work-chart-preview-';
|
|
3358
|
+
function spreadsheetChartCount(content) {
|
|
3359
|
+
return content.sheets.reduce((count, sheet)=>count + (sheet.charts?.length ?? 0), 0);
|
|
3360
|
+
}
|
|
3361
|
+
function spreadsheetSheetsWithChartPreviews(content) {
|
|
3362
|
+
return content.sheets.map((sheet)=>{
|
|
3363
|
+
const chartImages = (sheet.charts ?? []).map((chart)=>chartPreviewImage(resolveSpreadsheetChart(content, sheet, chart)));
|
|
3364
|
+
if (!chartImages.length) return sheet;
|
|
3365
|
+
return {
|
|
3366
|
+
...sheet,
|
|
3367
|
+
images: [
|
|
3368
|
+
...sheet.images ?? [],
|
|
3369
|
+
...chartImages
|
|
3370
|
+
]
|
|
3371
|
+
};
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
function reconcileSpreadsheetChartPreviews(content, changedSheets) {
|
|
3375
|
+
const sourceById = new Map(content.sheets.flatMap((sheet)=>sheet.id ? [
|
|
3376
|
+
[
|
|
3377
|
+
sheet.id,
|
|
3378
|
+
sheet
|
|
3379
|
+
]
|
|
3380
|
+
] : []));
|
|
3381
|
+
const renamedSheets = new Map(changedSheets.flatMap((sheet)=>{
|
|
3382
|
+
const sourceName = sheet.id ? sourceById.get(sheet.id)?.name : void 0;
|
|
3383
|
+
return sourceName && sourceName !== sheet.name ? [
|
|
3384
|
+
[
|
|
3385
|
+
sourceName,
|
|
3386
|
+
sheet.name
|
|
3387
|
+
]
|
|
3388
|
+
] : [];
|
|
3389
|
+
}));
|
|
3390
|
+
const sheets = changedSheets.map((changedSheet)=>{
|
|
3391
|
+
const source = (changedSheet.id ? sourceById.get(changedSheet.id) : void 0) ?? changedSheet;
|
|
3392
|
+
const previewImages = new Map((changedSheet.images ?? []).filter((image)=>image.id.startsWith(CHART_PREVIEW_IMAGE_PREFIX)).map((image)=>[
|
|
3393
|
+
image.id,
|
|
3394
|
+
image
|
|
3395
|
+
]));
|
|
3396
|
+
const charts = (source.charts ?? []).flatMap((sourceChart)=>{
|
|
3397
|
+
const chart = renameSpreadsheetChartReferences(sourceChart, renamedSheets);
|
|
3398
|
+
const preview = previewImages.get(chartPreviewImageId(chart.id));
|
|
3399
|
+
if (!preview) return [];
|
|
3400
|
+
return [
|
|
3401
|
+
{
|
|
3402
|
+
...chart,
|
|
3403
|
+
left: finiteDimension(preview.left, chart.left),
|
|
3404
|
+
top: finiteDimension(preview.top, chart.top),
|
|
3405
|
+
width: finiteDimension(preview.width, chart.width, 1),
|
|
3406
|
+
height: finiteDimension(preview.height, chart.height, 1)
|
|
3407
|
+
}
|
|
3408
|
+
];
|
|
3409
|
+
});
|
|
3410
|
+
const images = (changedSheet.images ?? []).filter((image)=>!image.id.startsWith(CHART_PREVIEW_IMAGE_PREFIX));
|
|
3411
|
+
return {
|
|
3412
|
+
...changedSheet,
|
|
3413
|
+
images: images.length ? images : void 0,
|
|
3414
|
+
charts: charts.length ? charts : void 0,
|
|
3415
|
+
...changedSheet.formulaMetadata ?? source.formulaMetadata ? {
|
|
3416
|
+
formulaMetadata: changedSheet.formulaMetadata ?? source.formulaMetadata
|
|
3417
|
+
} : {}
|
|
3418
|
+
};
|
|
3419
|
+
});
|
|
3420
|
+
return {
|
|
3421
|
+
...content,
|
|
3422
|
+
sheets
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
function resolveSpreadsheetChart(content, ownerSheet, chart) {
|
|
3426
|
+
const titleValue = chart.titleReference ? spreadsheetReferenceValues(content, ownerSheet, chart.titleReference)?.[0] : void 0;
|
|
3427
|
+
const categories = chart.categoryReference ? spreadsheetReferenceValues(content, ownerSheet, chart.categoryReference)?.map(displayCellValue) : void 0;
|
|
3428
|
+
const series = chart.series.map((item)=>resolveSpreadsheetChartSeries(content, ownerSheet, item));
|
|
3429
|
+
const axes = resolveSpreadsheetChartAxes(content, ownerSheet, chart.axes);
|
|
3430
|
+
return {
|
|
3431
|
+
...chart,
|
|
3432
|
+
title: void 0 === titleValue ? chart.title : displayCellValue(titleValue),
|
|
3433
|
+
categories: categories ?? chart.categories,
|
|
3434
|
+
series,
|
|
3435
|
+
...axes ? {
|
|
3436
|
+
axes
|
|
3437
|
+
} : {}
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
function createSpreadsheetChartFromSelection(content, sheetId, selection) {
|
|
3441
|
+
const sheet = content.sheets.find((candidate)=>candidate.id === sheetId);
|
|
3442
|
+
if (!sheet) return null;
|
|
3443
|
+
const bounds = normalizedSelection(selection ?? sheet.luckysheet_select_save?.at(-1));
|
|
3444
|
+
if (!bounds) return null;
|
|
3445
|
+
const rowCount = bounds.endRow - bounds.startRow + 1;
|
|
3446
|
+
const columnCount = bounds.endColumn - bounds.startColumn + 1;
|
|
3447
|
+
const hasHeader = rowCount > 1 && selectionHasHeader(sheet, bounds);
|
|
3448
|
+
const dataStartRow = hasHeader ? bounds.startRow + 1 : bounds.startRow;
|
|
3449
|
+
const chartNumber = spreadsheetChartCount(content) + 1;
|
|
3450
|
+
const chartName = `图表 ${chartNumber}`;
|
|
3451
|
+
let categoryReference;
|
|
3452
|
+
let categories;
|
|
3453
|
+
let series;
|
|
3454
|
+
if (columnCount > 1) {
|
|
3455
|
+
categoryReference = cellRangeFormula(sheet.name, dataStartRow, bounds.startColumn, bounds.endRow, bounds.startColumn);
|
|
3456
|
+
categories = rangeValues(sheet, dataStartRow, bounds.startColumn, bounds.endRow, bounds.startColumn).map(displayCellValue);
|
|
3457
|
+
series = inclusiveNumbers(bounds.startColumn + 1, bounds.endColumn).map((column, index)=>({
|
|
3458
|
+
name: hasHeader ? displayCellValue(sheet.data?.[bounds.startRow]?.[column]?.v ?? `系列 ${index + 1}`) : `系列 ${index + 1}`,
|
|
3459
|
+
nameReference: hasHeader ? cellRangeFormula(sheet.name, bounds.startRow, column, bounds.startRow, column) : void 0,
|
|
3460
|
+
valuesReference: cellRangeFormula(sheet.name, dataStartRow, column, bounds.endRow, column),
|
|
3461
|
+
values: rangeValues(sheet, dataStartRow, column, bounds.endRow, column).map(numericCellValue)
|
|
3462
|
+
}));
|
|
3463
|
+
} else {
|
|
3464
|
+
categories = inclusiveNumbers(dataStartRow, bounds.endRow).map((row)=>String(row + 1));
|
|
3465
|
+
series = [
|
|
3466
|
+
{
|
|
3467
|
+
name: hasHeader ? displayCellValue(sheet.data?.[bounds.startRow]?.[bounds.startColumn]?.v ?? '系列 1') : '系列 1',
|
|
3468
|
+
nameReference: hasHeader ? cellRangeFormula(sheet.name, bounds.startRow, bounds.startColumn, bounds.startRow, bounds.startColumn) : void 0,
|
|
3469
|
+
valuesReference: cellRangeFormula(sheet.name, dataStartRow, bounds.startColumn, bounds.endRow, bounds.startColumn),
|
|
3470
|
+
values: rangeValues(sheet, dataStartRow, bounds.startColumn, bounds.endRow, bounds.startColumn).map(numericCellValue)
|
|
3471
|
+
}
|
|
3472
|
+
];
|
|
3473
|
+
}
|
|
3474
|
+
const left = axisPosition(bounds.endColumn + 1, (index)=>spreadsheetColumnWidth(sheet.config, index)) + 16;
|
|
3475
|
+
const top = axisPosition(bounds.startRow, (index)=>spreadsheetRowHeight(sheet.config, index));
|
|
3476
|
+
return {
|
|
3477
|
+
id: createWorkId('chart'),
|
|
3478
|
+
name: chartName,
|
|
3479
|
+
altText: `${chartName},来源于 ${sheet.name} 的当前选区`,
|
|
3480
|
+
type: 'column',
|
|
3481
|
+
title: chartName,
|
|
3482
|
+
categories,
|
|
3483
|
+
categoryReference,
|
|
3484
|
+
series,
|
|
3485
|
+
showLegend: series.length > 1,
|
|
3486
|
+
legendPosition: 'right',
|
|
3487
|
+
legendOverlay: false,
|
|
3488
|
+
grouping: 'clustered',
|
|
3489
|
+
gapWidth: 150,
|
|
3490
|
+
overlap: 0,
|
|
3491
|
+
left,
|
|
3492
|
+
top,
|
|
3493
|
+
width: 480,
|
|
3494
|
+
height: 288
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
function parseSpreadsheetChartReference(content, ownerSheet, reference) {
|
|
3498
|
+
const formula = reference.trim().replace(/^=/, '');
|
|
3499
|
+
const match = /^(?:(?:'((?:[^']|'')+)'|([^!]+))!)?(\$?[A-Z]{1,3}\$?[1-9]\d*)(?::(\$?[A-Z]{1,3}\$?[1-9]\d*))?$/i.exec(formula);
|
|
3500
|
+
if (!match) return null;
|
|
3501
|
+
const sheetName = (match[1]?.replaceAll("''", "'") ?? match[2])?.trim();
|
|
3502
|
+
if (sheetName?.includes('[') || sheetName?.includes(']')) return null;
|
|
3503
|
+
const sheet = sheetName ? content.sheets.find((candidate)=>candidate.name === sheetName) : ownerSheet;
|
|
3504
|
+
if (!sheet) return null;
|
|
3505
|
+
const start = decodeCellAddress(match[3]);
|
|
3506
|
+
const end = decodeCellAddress(match[4] ?? match[3]);
|
|
3507
|
+
if (!start || !end) return null;
|
|
3508
|
+
return {
|
|
3509
|
+
sheet,
|
|
3510
|
+
startRow: Math.min(start.row, end.row),
|
|
3511
|
+
endRow: Math.max(start.row, end.row),
|
|
3512
|
+
startColumn: Math.min(start.column, end.column),
|
|
3513
|
+
endColumn: Math.max(start.column, end.column)
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
function spreadsheetReferenceValues(content, ownerSheet, reference) {
|
|
3517
|
+
const range = parseSpreadsheetChartReference(content, ownerSheet, reference);
|
|
3518
|
+
if (!range) return;
|
|
3519
|
+
return rangeValues(range.sheet, range.startRow, range.startColumn, range.endRow, range.endColumn);
|
|
3520
|
+
}
|
|
3521
|
+
function quoteSpreadsheetSheetName(name) {
|
|
3522
|
+
return /^[A-Za-z_][A-Za-z0-9_.]*$/.test(name) ? name : `'${name.replaceAll("'", "''")}'`;
|
|
3523
|
+
}
|
|
3524
|
+
function renameSpreadsheetChartReferences(chart, renamedSheets) {
|
|
3525
|
+
if (!renamedSheets.size) return chart;
|
|
3526
|
+
return {
|
|
3527
|
+
...chart,
|
|
3528
|
+
titleReference: renameSpreadsheetReference(chart.titleReference, renamedSheets),
|
|
3529
|
+
axes: renameSpreadsheetChartAxesReferences(chart.axes, renamedSheets),
|
|
3530
|
+
categoryReference: renameSpreadsheetReference(chart.categoryReference, renamedSheets),
|
|
3531
|
+
series: chart.series.map((series)=>({
|
|
3532
|
+
...series,
|
|
3533
|
+
nameReference: renameSpreadsheetReference(series.nameReference, renamedSheets),
|
|
3534
|
+
valuesReference: renameSpreadsheetReference(series.valuesReference, renamedSheets),
|
|
3535
|
+
xValuesReference: renameSpreadsheetReference(series.xValuesReference, renamedSheets),
|
|
3536
|
+
bubbleSizesReference: renameSpreadsheetReference(series.bubbleSizesReference, renamedSheets),
|
|
3537
|
+
...series.errorBars ? {
|
|
3538
|
+
errorBars: series.errorBars.map((errorBars)=>({
|
|
3539
|
+
...errorBars,
|
|
3540
|
+
plusReference: renameSpreadsheetReference(errorBars.plusReference, renamedSheets),
|
|
3541
|
+
minusReference: renameSpreadsheetReference(errorBars.minusReference, renamedSheets)
|
|
3542
|
+
}))
|
|
3543
|
+
} : {}
|
|
3544
|
+
}))
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
function renameSpreadsheetChartAxesReferences(axes, renamedSheets) {
|
|
3548
|
+
if (!axes) return;
|
|
3549
|
+
const bottom = renameSpreadsheetChartAxisReference(axes.bottom, renamedSheets);
|
|
3550
|
+
const left = renameSpreadsheetChartAxisReference(axes.left, renamedSheets);
|
|
3551
|
+
const top = renameSpreadsheetChartAxisReference(axes.top, renamedSheets);
|
|
3552
|
+
const right = renameSpreadsheetChartAxisReference(axes.right, renamedSheets);
|
|
3553
|
+
return {
|
|
3554
|
+
...bottom ? {
|
|
3555
|
+
bottom
|
|
3556
|
+
} : {},
|
|
3557
|
+
...left ? {
|
|
3558
|
+
left
|
|
3559
|
+
} : {},
|
|
3560
|
+
...top ? {
|
|
3561
|
+
top
|
|
3562
|
+
} : {},
|
|
3563
|
+
...right ? {
|
|
3564
|
+
right
|
|
3565
|
+
} : {}
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
function renameSpreadsheetChartAxisReference(axis, renamedSheets) {
|
|
3569
|
+
return axis ? {
|
|
3570
|
+
...axis,
|
|
3571
|
+
titleReference: renameSpreadsheetReference(axis.titleReference, renamedSheets)
|
|
3572
|
+
} : void 0;
|
|
3573
|
+
}
|
|
3574
|
+
function renameSpreadsheetReference(reference, renamedSheets) {
|
|
3575
|
+
if (!reference) return;
|
|
3576
|
+
const formula = reference.trim().replace(/^=/, '');
|
|
3577
|
+
const match = /^(?:'((?:[^']|'')+)'|([^!]+))!(.+)$/.exec(formula);
|
|
3578
|
+
const sheetName = match ? match[1]?.replaceAll("''", "'") ?? match[2] : void 0;
|
|
3579
|
+
const renamed = sheetName ? renamedSheets.get(sheetName) : void 0;
|
|
3580
|
+
return renamed && match ? `${quoteSpreadsheetSheetName(renamed)}!${match[3]}` : formula;
|
|
3581
|
+
}
|
|
3582
|
+
function resolveSpreadsheetChartSeries(content, ownerSheet, series) {
|
|
3583
|
+
const nameValue = series.nameReference ? spreadsheetReferenceValues(content, ownerSheet, series.nameReference)?.[0] : void 0;
|
|
3584
|
+
const values = series.valuesReference ? spreadsheetReferenceValues(content, ownerSheet, series.valuesReference)?.map(numericCellValue) : void 0;
|
|
3585
|
+
const xValues = series.xValuesReference ? spreadsheetReferenceValues(content, ownerSheet, series.xValuesReference)?.map(numericCellValue) : void 0;
|
|
3586
|
+
const bubbleSizes = series.bubbleSizesReference ? spreadsheetReferenceValues(content, ownerSheet, series.bubbleSizesReference)?.map(numericCellValue) : void 0;
|
|
3587
|
+
const errorBars = series.errorBars?.map((source)=>({
|
|
3588
|
+
...source,
|
|
3589
|
+
plusValues: source.plusReference ? spreadsheetReferenceValues(content, ownerSheet, source.plusReference)?.map(numericCellValue) ?? source.plusValues : source.plusValues,
|
|
3590
|
+
minusValues: source.minusReference ? spreadsheetReferenceValues(content, ownerSheet, source.minusReference)?.map(numericCellValue) ?? source.minusValues : source.minusValues
|
|
3591
|
+
}));
|
|
3592
|
+
return {
|
|
3593
|
+
...series,
|
|
3594
|
+
name: void 0 === nameValue ? series.name : displayCellValue(nameValue),
|
|
3595
|
+
values: values ?? series.values,
|
|
3596
|
+
xValues: xValues ?? series.xValues,
|
|
3597
|
+
bubbleSizes: bubbleSizes ?? series.bubbleSizes,
|
|
3598
|
+
...errorBars ? {
|
|
3599
|
+
errorBars
|
|
3600
|
+
} : {}
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
function resolveSpreadsheetChartAxes(content, ownerSheet, axes) {
|
|
3604
|
+
if (!axes) return;
|
|
3605
|
+
const bottom = resolveSpreadsheetChartAxis(content, ownerSheet, axes.bottom);
|
|
3606
|
+
const left = resolveSpreadsheetChartAxis(content, ownerSheet, axes.left);
|
|
3607
|
+
const top = resolveSpreadsheetChartAxis(content, ownerSheet, axes.top);
|
|
3608
|
+
const right = resolveSpreadsheetChartAxis(content, ownerSheet, axes.right);
|
|
3609
|
+
return {
|
|
3610
|
+
...bottom ? {
|
|
3611
|
+
bottom
|
|
3612
|
+
} : {},
|
|
3613
|
+
...left ? {
|
|
3614
|
+
left
|
|
3615
|
+
} : {},
|
|
3616
|
+
...top ? {
|
|
3617
|
+
top
|
|
3618
|
+
} : {},
|
|
3619
|
+
...right ? {
|
|
3620
|
+
right
|
|
3621
|
+
} : {}
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
function resolveSpreadsheetChartAxis(content, ownerSheet, axis) {
|
|
3625
|
+
if (!axis) return;
|
|
3626
|
+
const titleValue = axis.titleReference ? spreadsheetReferenceValues(content, ownerSheet, axis.titleReference)?.[0] : void 0;
|
|
3627
|
+
return {
|
|
3628
|
+
...axis,
|
|
3629
|
+
title: void 0 === titleValue ? axis.title : displayCellValue(titleValue)
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
function chartPreviewImage(chart) {
|
|
3633
|
+
return {
|
|
3634
|
+
id: chartPreviewImageId(chart.id),
|
|
3635
|
+
name: chart.name,
|
|
3636
|
+
altText: chart.altText ?? chart.title ?? chart.name,
|
|
3637
|
+
contentType: 'image/svg+xml',
|
|
3638
|
+
src: spreadsheetChartSvgDataUrl(chart),
|
|
3639
|
+
left: chart.left,
|
|
3640
|
+
top: chart.top,
|
|
3641
|
+
width: chart.width,
|
|
3642
|
+
height: chart.height
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
function chartPreviewImageId(chartId) {
|
|
3646
|
+
return `${CHART_PREVIEW_IMAGE_PREFIX}${chartId}`;
|
|
3647
|
+
}
|
|
3648
|
+
function rangeValues(sheet, startRow, startColumn, endRow, endColumn) {
|
|
3649
|
+
const values = [];
|
|
3650
|
+
for(let row = startRow; row <= endRow; row += 1)for(let column = startColumn; column <= endColumn; column += 1){
|
|
3651
|
+
const cell = sheet.data?.[row]?.[column];
|
|
3652
|
+
values.push(cell?.v ?? cell?.m ?? '');
|
|
3653
|
+
}
|
|
3654
|
+
return values;
|
|
3655
|
+
}
|
|
3656
|
+
function selectionHasHeader(sheet, bounds) {
|
|
3657
|
+
const seriesStartColumn = bounds.endColumn > bounds.startColumn ? bounds.startColumn + 1 : bounds.startColumn;
|
|
3658
|
+
const firstRowValues = rangeValues(sheet, bounds.startRow, seriesStartColumn, bounds.startRow, bounds.endColumn);
|
|
3659
|
+
const bodyValues = rangeValues(sheet, bounds.startRow + 1, seriesStartColumn, bounds.endRow, bounds.endColumn);
|
|
3660
|
+
return firstRowValues.some((value)=>String(value ?? '').trim() && !isNumericValue(value)) && bodyValues.some(isNumericValue);
|
|
3661
|
+
}
|
|
3662
|
+
function normalizedSelection(selection) {
|
|
3663
|
+
if (!selection) return null;
|
|
3664
|
+
const startRow = Math.max(0, Math.min(selection.row[0], selection.row[1]));
|
|
3665
|
+
const endRow = Math.max(startRow, Math.max(selection.row[0], selection.row[1]));
|
|
3666
|
+
const startColumn = Math.max(0, Math.min(selection.column[0], selection.column[1]));
|
|
3667
|
+
const endColumn = Math.max(startColumn, Math.max(selection.column[0], selection.column[1]));
|
|
3668
|
+
if (![
|
|
3669
|
+
startRow,
|
|
3670
|
+
endRow,
|
|
3671
|
+
startColumn,
|
|
3672
|
+
endColumn
|
|
3673
|
+
].every(Number.isSafeInteger)) return null;
|
|
3674
|
+
return {
|
|
3675
|
+
startRow,
|
|
3676
|
+
endRow,
|
|
3677
|
+
startColumn,
|
|
3678
|
+
endColumn
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
function cellRangeFormula(sheetName, startRow, startColumn, endRow, endColumn) {
|
|
3682
|
+
const start = absoluteCellAddress(startRow, startColumn);
|
|
3683
|
+
const end = absoluteCellAddress(endRow, endColumn);
|
|
3684
|
+
return `${quoteSpreadsheetSheetName(sheetName)}!${start}${start === end ? '' : `:${end}`}`;
|
|
3685
|
+
}
|
|
3686
|
+
function absoluteCellAddress(row, column) {
|
|
3687
|
+
return `$${columnName(column)}$${row + 1}`;
|
|
3688
|
+
}
|
|
3689
|
+
function decodeCellAddress(address) {
|
|
3690
|
+
const match = /^\$?([A-Z]{1,3})\$?([1-9]\d*)$/i.exec(address);
|
|
3691
|
+
if (!match) return null;
|
|
3692
|
+
let column = 0;
|
|
3693
|
+
for (const character of match[1].toUpperCase())column = 26 * column + character.charCodeAt(0) - 64;
|
|
3694
|
+
const row = Number(match[2]);
|
|
3695
|
+
if (column < 1 || column > 16384 || row < 1 || row > 1048576) return null;
|
|
3696
|
+
return {
|
|
3697
|
+
row: row - 1,
|
|
3698
|
+
column: column - 1
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
function columnName(index) {
|
|
3702
|
+
let value = index + 1;
|
|
3703
|
+
let result = '';
|
|
3704
|
+
while(value > 0){
|
|
3705
|
+
value -= 1;
|
|
3706
|
+
result = String.fromCharCode(65 + value % 26) + result;
|
|
3707
|
+
value = Math.floor(value / 26);
|
|
3708
|
+
}
|
|
3709
|
+
return result;
|
|
3710
|
+
}
|
|
3711
|
+
function axisPosition(index, size) {
|
|
3712
|
+
let position = 0;
|
|
3713
|
+
for(let current = 0; current < index; current += 1)position += size(current);
|
|
3714
|
+
return position;
|
|
3715
|
+
}
|
|
3716
|
+
function inclusiveNumbers(start, end) {
|
|
3717
|
+
return Array.from({
|
|
3718
|
+
length: Math.max(0, end - start + 1)
|
|
3719
|
+
}, (_, index)=>start + index);
|
|
3720
|
+
}
|
|
3721
|
+
function displayCellValue(value) {
|
|
3722
|
+
if (value instanceof Date) return value.toLocaleDateString('zh-CN');
|
|
3723
|
+
if (null == value) return '';
|
|
3724
|
+
return String(value);
|
|
3725
|
+
}
|
|
3726
|
+
function numericCellValue(value) {
|
|
3727
|
+
if (value instanceof Date) return value.getTime();
|
|
3728
|
+
const number = 'number' == typeof value ? value : Number(value);
|
|
3729
|
+
return Number.isFinite(number) ? number : 0;
|
|
3730
|
+
}
|
|
3731
|
+
function isNumericValue(value) {
|
|
3732
|
+
if (value instanceof Date || 'number' == typeof value) return true;
|
|
3733
|
+
if ('string' != typeof value || !value.trim()) return false;
|
|
3734
|
+
return Number.isFinite(Number(value));
|
|
3735
|
+
}
|
|
3736
|
+
function finiteDimension(value, fallback, minimum = 0) {
|
|
3737
|
+
return Number.isFinite(value) ? Math.max(minimum, value) : fallback;
|
|
3738
|
+
}
|
|
3739
|
+
const DEFAULT_SPREADSHEET_CALCULATION_SETTINGS = {
|
|
3740
|
+
mode: 'automatic',
|
|
3741
|
+
fullCalculationOnLoad: false,
|
|
3742
|
+
forceFullCalculation: false,
|
|
3743
|
+
iterativeCalculation: false,
|
|
3744
|
+
maximumIterations: 100,
|
|
3745
|
+
maximumChange: 0.001,
|
|
3746
|
+
fullPrecision: true
|
|
3747
|
+
};
|
|
3748
|
+
const VOLATILE_FUNCTIONS = new Set([
|
|
3749
|
+
'CELL',
|
|
3750
|
+
'INFO',
|
|
3751
|
+
'INDIRECT',
|
|
3752
|
+
'NOW',
|
|
3753
|
+
'OFFSET',
|
|
3754
|
+
'RAND',
|
|
3755
|
+
'RANDBETWEEN',
|
|
3756
|
+
'TODAY'
|
|
3757
|
+
]);
|
|
3758
|
+
const FUTURE_FUNCTIONS = new Set([
|
|
3759
|
+
'ARRAYTOTEXT',
|
|
3760
|
+
'BYCOL',
|
|
3761
|
+
'BYROW',
|
|
3762
|
+
'CHOOSECOLS',
|
|
3763
|
+
'CHOOSEROWS',
|
|
3764
|
+
'DROP',
|
|
3765
|
+
'EXPAND',
|
|
3766
|
+
'FIELDVALUE',
|
|
3767
|
+
'FILTER',
|
|
3768
|
+
'HSTACK',
|
|
3769
|
+
'IMAGE',
|
|
3770
|
+
'ISOMITTED',
|
|
3771
|
+
'LAMBDA',
|
|
3772
|
+
'LET',
|
|
3773
|
+
'MAKEARRAY',
|
|
3774
|
+
'MAP',
|
|
3775
|
+
'RANDARRAY',
|
|
3776
|
+
'REDUCE',
|
|
3777
|
+
'SCAN',
|
|
3778
|
+
'SEQUENCE',
|
|
3779
|
+
'SORT',
|
|
3780
|
+
'SORTBY',
|
|
3781
|
+
'STOCKHISTORY',
|
|
3782
|
+
'TAKE',
|
|
3783
|
+
'TEXTAFTER',
|
|
3784
|
+
'TEXTBEFORE',
|
|
3785
|
+
'TEXTSPLIT',
|
|
3786
|
+
'TOCOL',
|
|
3787
|
+
'TOROW',
|
|
3788
|
+
'UNIQUE',
|
|
3789
|
+
'VALUETOTEXT',
|
|
3790
|
+
'VSTACK',
|
|
3791
|
+
'WRAPCOLS',
|
|
3792
|
+
'WRAPROWS',
|
|
3793
|
+
'XLOOKUP',
|
|
3794
|
+
'XMATCH'
|
|
3795
|
+
]);
|
|
3796
|
+
const FUTURE_FUNCTION_PREFIXES = new Map([
|
|
3797
|
+
[
|
|
3798
|
+
'FILTER',
|
|
3799
|
+
'_xlfn._xlws.'
|
|
3800
|
+
],
|
|
3801
|
+
[
|
|
3802
|
+
'SORT',
|
|
3803
|
+
'_xlfn._xlws.'
|
|
3804
|
+
],
|
|
3805
|
+
[
|
|
3806
|
+
'SORTBY',
|
|
3807
|
+
'_xlfn._xlws.'
|
|
3808
|
+
],
|
|
3809
|
+
[
|
|
3810
|
+
'UNIQUE',
|
|
3811
|
+
'_xlfn.'
|
|
3812
|
+
],
|
|
3813
|
+
...Array.from(FUTURE_FUNCTIONS).filter((name)=>![
|
|
3814
|
+
'FILTER',
|
|
3815
|
+
'SORT',
|
|
3816
|
+
'SORTBY',
|
|
3817
|
+
'UNIQUE'
|
|
3818
|
+
].includes(name)).map((name)=>[
|
|
3819
|
+
name,
|
|
3820
|
+
'_xlfn.'
|
|
3821
|
+
])
|
|
3822
|
+
]);
|
|
3823
|
+
function effectiveSpreadsheetCalculationSettings(settings) {
|
|
3824
|
+
if (!settings) return {
|
|
3825
|
+
...DEFAULT_SPREADSHEET_CALCULATION_SETTINGS
|
|
3826
|
+
};
|
|
3827
|
+
return {
|
|
3828
|
+
mode: settings.mode,
|
|
3829
|
+
fullCalculationOnLoad: Boolean(settings.fullCalculationOnLoad),
|
|
3830
|
+
forceFullCalculation: Boolean(settings.forceFullCalculation),
|
|
3831
|
+
iterativeCalculation: Boolean(settings.iterativeCalculation),
|
|
3832
|
+
maximumIterations: clampedInteger(settings.maximumIterations, 1, 10000, 100),
|
|
3833
|
+
maximumChange: positiveNumber(settings.maximumChange, 0.001),
|
|
3834
|
+
fullPrecision: false !== settings.fullPrecision
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
function editableSpreadsheetFormula(source) {
|
|
3838
|
+
return transformFormulaOutsideStrings(source, (segment)=>segment.replace(/_xlfn\._xlws\.(?=[A-Z][A-Z0-9_.]*\s*\()/gi, '').replace(/_xlfn\.(?=[A-Z][A-Z0-9_.]*\s*\()/gi, '').replace(/_xlws\.(?=[A-Z][A-Z0-9_.]*\s*\()/gi, ''));
|
|
3839
|
+
}
|
|
3840
|
+
function spreadsheetFormulaForXlsx(currentFormula, sourceFormula) {
|
|
3841
|
+
const current = currentFormula.trim().replace(/^=/, '');
|
|
3842
|
+
const source = sourceFormula?.trim().replace(/^=/, '');
|
|
3843
|
+
if (source && comparableFormula(source) === comparableFormula(current)) return source;
|
|
3844
|
+
return futureFunctionPrefixes(current, source);
|
|
3845
|
+
}
|
|
3846
|
+
function spreadsheetFormulaRangeForCell(sheet, row, column) {
|
|
3847
|
+
const address = spreadsheetCellAddress(row, column);
|
|
3848
|
+
return (sheet.formulaMetadata?.ranges ?? []).find((range)=>range.anchor.toUpperCase().replaceAll('$', '') === address);
|
|
3849
|
+
}
|
|
3850
|
+
function spreadsheetFormulaRangesForSelection(sheet, selection) {
|
|
3851
|
+
return (sheet.formulaMetadata?.ranges ?? []).filter((range)=>work_spreadsheet_formulas_rangesOverlap(selection, parseSpreadsheetFormulaRange(range.reference)));
|
|
3852
|
+
}
|
|
3853
|
+
function spreadsheetFormulaRangeConflict(sheet, range) {
|
|
3854
|
+
const bounds = parseSpreadsheetFormulaRange(range.reference);
|
|
3855
|
+
if (!bounds) return '引用范围无效';
|
|
3856
|
+
const anchor = parseSpreadsheetCellAddress(range.anchor);
|
|
3857
|
+
if (!anchor || !work_spreadsheet_formulas_containsCell(bounds, anchor.row, anchor.column)) return '锚点不在引用范围内';
|
|
3858
|
+
const anchorCell = sheet.data?.[anchor.row]?.[anchor.column];
|
|
3859
|
+
if (!anchorCell) return '锚点单元格不存在';
|
|
3860
|
+
const overlaps = (sheet.formulaMetadata?.ranges ?? []).filter((candidate)=>candidate !== range && work_spreadsheet_formulas_rangesOverlap(bounds, parseSpreadsheetFormulaRange(candidate.reference)));
|
|
3861
|
+
if (overlaps.length) return '与其他公式范围重叠';
|
|
3862
|
+
for (const [row, cells] of (sheet.data ?? []).entries())if (!(row < bounds.startRow) && !(row > bounds.endRow)) {
|
|
3863
|
+
for (const [column, cell] of cells.entries())if (!(column < bounds.startColumn) && !(column > bounds.endColumn)) {
|
|
3864
|
+
if (row !== anchor.row || column !== anchor.column) {
|
|
3865
|
+
if (cell?.f) return `${spreadsheetCellAddress(row, column)} 包含独立公式`;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
if (Object.values(sheet.config?.merge ?? {}).some((merge)=>work_spreadsheet_formulas_rangesOverlap(bounds, {
|
|
3870
|
+
startRow: merge.r,
|
|
3871
|
+
endRow: merge.r + merge.rs - 1,
|
|
3872
|
+
startColumn: merge.c,
|
|
3873
|
+
endColumn: merge.c + merge.cs - 1
|
|
3874
|
+
}))) return '范围内包含合并单元格';
|
|
3875
|
+
if ('data-table' !== range.type && !anchorCell.f) return '锚点公式已被删除';
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
function spreadsheetFormulaFunctions(formula) {
|
|
3879
|
+
const withoutStrings = formula.replace(/"(?:[^"]|"")*"/g, '""');
|
|
3880
|
+
const functions = new Set();
|
|
3881
|
+
for (const match of withoutStrings.matchAll(/(?:_xlfn\.)?(?:_xlws\.)?([A-Z][A-Z0-9_.]*)\s*\(/gi))if (match[1]) functions.add(match[1].toUpperCase());
|
|
3882
|
+
return Array.from(functions);
|
|
3883
|
+
}
|
|
3884
|
+
function formulaHasExternalReference(formula) {
|
|
3885
|
+
return /\[[^\]]+\][^!]*!/i.test(formula.replace(/"(?:[^"]|"")*"/g, '""'));
|
|
3886
|
+
}
|
|
3887
|
+
function formulaHasStructuredReference(formula) {
|
|
3888
|
+
const withoutExternalReferences = formula.replace(/"(?:[^"]|"")*"/g, '""').replace(/\[[^\]]+\][^!]*!/gi, '');
|
|
3889
|
+
return /\[(?:@|#)[^\]]+\]/i.test(withoutExternalReferences) || /\b[A-Z_\\][A-Z0-9_.]*\s*\[[^\]]+\]/i.test(withoutExternalReferences);
|
|
3890
|
+
}
|
|
3891
|
+
function volatileSpreadsheetFormulaFunctions(formula) {
|
|
3892
|
+
return spreadsheetFormulaFunctions(formula).filter((name)=>VOLATILE_FUNCTIONS.has(name));
|
|
3893
|
+
}
|
|
3894
|
+
function spreadsheetCellAddress(row, column) {
|
|
3895
|
+
let value = column + 1;
|
|
3896
|
+
let label = '';
|
|
3897
|
+
while(value > 0){
|
|
3898
|
+
value -= 1;
|
|
3899
|
+
label = String.fromCharCode(65 + value % 26) + label;
|
|
3900
|
+
value = Math.floor(value / 26);
|
|
3901
|
+
}
|
|
3902
|
+
return `${label}${row + 1}`;
|
|
3903
|
+
}
|
|
3904
|
+
function parseSpreadsheetFormulaRange(reference) {
|
|
3905
|
+
const normalized = reference.trim().replaceAll('$', '').split('!').at(-1) ?? '';
|
|
3906
|
+
const [startText, endText = startText] = normalized.split(':');
|
|
3907
|
+
const start = parseSpreadsheetCellAddress(startText);
|
|
3908
|
+
const end = parseSpreadsheetCellAddress(endText);
|
|
3909
|
+
if (!start || !end) return null;
|
|
3910
|
+
return {
|
|
3911
|
+
startRow: Math.min(start.row, end.row),
|
|
3912
|
+
endRow: Math.max(start.row, end.row),
|
|
3913
|
+
startColumn: Math.min(start.column, end.column),
|
|
3914
|
+
endColumn: Math.max(start.column, end.column)
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
function parseSpreadsheetCellAddress(address) {
|
|
3918
|
+
const match = /^([A-Z]{1,3})([1-9]\d*)$/i.exec(address.trim().replaceAll('$', ''));
|
|
3919
|
+
if (!match) return null;
|
|
3920
|
+
let column = 0;
|
|
3921
|
+
for (const character of match[1].toUpperCase())column = 26 * column + character.charCodeAt(0) - 64;
|
|
3922
|
+
const row = Number(match[2]) - 1;
|
|
3923
|
+
if (row > 1048575 || column > 16384) return null;
|
|
3924
|
+
return {
|
|
3925
|
+
row,
|
|
3926
|
+
column: column - 1
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
function work_spreadsheet_formulas_containsCell(bounds, row, column) {
|
|
3930
|
+
return row >= bounds.startRow && row <= bounds.endRow && column >= bounds.startColumn && column <= bounds.endColumn;
|
|
3931
|
+
}
|
|
3932
|
+
function work_spreadsheet_formulas_rangesOverlap(first, second) {
|
|
3933
|
+
if (!second) return false;
|
|
3934
|
+
return first.startRow <= second.endRow && first.endRow >= second.startRow && first.startColumn <= second.endColumn && first.endColumn >= second.startColumn;
|
|
3935
|
+
}
|
|
3936
|
+
function comparableFormula(formula) {
|
|
3937
|
+
return editableSpreadsheetFormula(formula).trim();
|
|
3938
|
+
}
|
|
3939
|
+
function futureFunctionPrefixes(formula, sourceFormula) {
|
|
3940
|
+
const sourcePrefixes = new Map();
|
|
3941
|
+
const sourceFunctions = (sourceFormula ?? '').replace(/"(?:[^"]|"")*"/g, '""');
|
|
3942
|
+
for (const match of sourceFunctions.matchAll(/(_xlfn\.(?:_xlws\.)?)([A-Z][A-Z0-9_.]*)\s*(?=\()/gi))if (match[1] && match[2]) sourcePrefixes.set(match[2].toUpperCase(), match[1]);
|
|
3943
|
+
return transformFormulaOutsideStrings(formula, (segment)=>segment.replace(/(?<![A-Z0-9_.])([A-Z][A-Z0-9_.]*)\s*(?=\()/gi, (match, functionName)=>{
|
|
3944
|
+
const normalized = functionName.toUpperCase();
|
|
3945
|
+
const prefix = sourcePrefixes.get(normalized) ?? FUTURE_FUNCTION_PREFIXES.get(normalized);
|
|
3946
|
+
return prefix ? `${prefix}${match}` : match;
|
|
3947
|
+
}));
|
|
3948
|
+
}
|
|
3949
|
+
function clampedInteger(value, minimum, maximum, fallback) {
|
|
3950
|
+
if (!Number.isFinite(value)) return fallback;
|
|
3951
|
+
return Math.min(maximum, Math.max(minimum, Math.round(value)));
|
|
3952
|
+
}
|
|
3953
|
+
function positiveNumber(value, fallback) {
|
|
3954
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
3955
|
+
}
|
|
3956
|
+
function transformFormulaOutsideStrings(formula, transform) {
|
|
3957
|
+
return formula.split(/("(?:[^"]|"")*")/).map((segment, index)=>index % 2 ? segment : transform(segment)).join('');
|
|
3958
|
+
}
|
|
3959
|
+
export { OoxmlPackage, attribute, bytesToDataUrl, childPath, contentTypeForPart, createSpreadsheetChartFromSelection, createSpreadsheetPivotFromSelection, defaultPivotValueCaption, defaultWorkSpreadsheetChartSeriesStyle, deleteSpreadsheetPivotTable, descendants, directChild, directChildren, editableSpreadsheetFormula, effectiveSpreadsheetCalculationSettings, errorBarSourceValues, firstDescendant, fitSpreadsheetTrendline, formatSpreadsheetCellRanges, formatSpreadsheetChartAxisNumber, formulaHasExternalReference, formulaHasStructuredReference, isValidSpreadsheetDefinedName, normalizeSpreadsheetPrintArea, normalizeSpreadsheetPrintTitleColumns, normalizeSpreadsheetPrintTitleRows, normalizeWorkSpreadsheetBubbleScale, normalizeWorkSpreadsheetBubbleSizeRepresents, normalizeWorkSpreadsheetChartAxes, normalizeWorkSpreadsheetChartAxisGroup, normalizeWorkSpreadsheetChartColor, normalizeWorkSpreadsheetChartGapWidth, normalizeWorkSpreadsheetChartGrouping, normalizeWorkSpreadsheetChartLayout, normalizeWorkSpreadsheetChartLegendOverlay, normalizeWorkSpreadsheetChartLegendPosition, normalizeWorkSpreadsheetChartOverlap, normalizeWorkSpreadsheetChartSeriesStyle, normalizeWorkSpreadsheetChartSmoothLines, normalizeWorkSpreadsheetCombinationSeriesType, normalizeWorkSpreadsheetDataLabelPosition, normalizeWorkSpreadsheetDataLabels, normalizeWorkSpreadsheetDoughnutHoleSize, normalizeWorkSpreadsheetErrorBars, normalizeWorkSpreadsheetRadarStyle, normalizeWorkSpreadsheetScatterStyle, normalizeWorkSpreadsheetTrendline, normalizeWorkSpreadsheetTrendlineType, parseSpreadsheetCellRanges, parseSpreadsheetChartReference, parseSpreadsheetPrintTitles, parseXml, qualifySpreadsheetRange, readXlsxDrawingAnchor, reconcileSpreadsheetChartPreviews, reconcileSpreadsheetPivots, refreshSpreadsheetPivotTables, resolveSpreadsheetChart, roundChartNumber, spreadsheetCellAddress, spreadsheetChartAxisGridlinesVisible, spreadsheetChartAxisLabelLayout, spreadsheetChartAxisScale, spreadsheetChartAxisValueRatio, spreadsheetChartBarGeometry, spreadsheetChartCategoryLabelVisible, spreadsheetChartCategoryVisualIndex, spreadsheetChartCount, spreadsheetChartSeriesFillStyle, spreadsheetChartSeriesLayout, spreadsheetChartSeriesLegendColor, spreadsheetChartSeriesLineStyle, spreadsheetChartSeriesStyleContext, spreadsheetErrorBarAmounts, spreadsheetFormulaForXlsx, spreadsheetFormulaFunctions, spreadsheetFormulaRangeConflict, spreadsheetFormulaRangeForCell, spreadsheetFormulaRangesForSelection, spreadsheetPivotAggregationLabel, spreadsheetPivotCount, spreadsheetPivotFields, spreadsheetPivotFilterItems, spreadsheetPivotFilterValueKey, spreadsheetPivotIntersects, spreadsheetPivotOutputContains, spreadsheetPivotValidation, spreadsheetSheetsWithChartPreviews, stripSpreadsheetSheetQualifier, volatileSpreadsheetFormulaFunctions, workSpreadsheetChartAxisDefaultLabelPosition, workSpreadsheetChartAxisIsCategoryAxis, workSpreadsheetChartAxisIsValueAxis, workSpreadsheetChartAxisLabelPositionLabel, workSpreadsheetChartAxisPositionLabel, workSpreadsheetChartAxisShowsMajorGridlinesByDefault, workSpreadsheetChartAxisTickMarkLabel, workSpreadsheetChartGroupingIsStacked, workSpreadsheetChartGroupingLabel, workSpreadsheetChartLegendPositionLabel, workSpreadsheetChartSupportsAxes, workSpreadsheetChartSupportsBarSpacing, workSpreadsheetChartSupportsErrorBars, workSpreadsheetChartSupportsGrouping, workSpreadsheetChartSupportsSeriesAnalysis, workSpreadsheetChartSupportsSmoothLines, workSpreadsheetChartSupportsTrendlines, workSpreadsheetChartTypeLabel, workSpreadsheetChartUsesNumericXAxis, workSpreadsheetCombinationSeriesTypeLabel, workSpreadsheetDataLabelPositionLabel, workSpreadsheetErrorBarTypeLabel, workSpreadsheetErrorBarValueTypeLabel, workSpreadsheetTrendlineTypeLabel, xlsxDrawingAnchorToBounds, xlsxTwoCellAnchorMarkers, xmlNamespacePrefix };
|