@svgrid/enterprise 3.0.2 → 3.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 +94 -24
- package/README.md +36 -4
- package/dist/SvAdvancedFilter.svelte +1 -1
- package/dist/SvAlertsManager.svelte +2 -1
- package/dist/SvBoard.svelte +2 -1
- package/dist/SvExportMenu.svelte +4 -1
- package/dist/SvExportMenu.svelte.d.ts +0 -13
- package/dist/SvExpressionEditor.svelte +3 -3
- package/dist/SvFormulaBar.svelte +94 -20
- package/dist/SvFormulaBar.svelte.d.ts +7 -0
- package/dist/SvGridAlerts.svelte +49 -9
- package/dist/SvGridBoard.svelte +13 -8
- package/dist/SvGridBulkEditDrawer.svelte +26 -2
- package/dist/SvGridEditPanel.svelte +3 -0
- package/dist/SvGridScheduler.svelte +69 -40
- package/dist/SvGridSelectionBar.svelte +10 -3
- package/dist/SvPivotDesigner.svelte +198 -40
- package/dist/SvPivotDesigner.svelte.d.ts +27 -20
- package/dist/SvSheet.svelte +4043 -243
- package/dist/SvSheet.svelte.d.ts +76 -3
- package/dist/SvSheetChartSetup.svelte +151 -0
- package/dist/SvSheetChartSetup.svelte.d.ts +18 -0
- package/dist/SvSheetComment.svelte +216 -31
- package/dist/SvSheetComment.svelte.d.ts +11 -11
- package/dist/SvSheetConditionalFormat.svelte +28 -31
- package/dist/SvSheetDataValidation.svelte +66 -42
- package/dist/SvSheetEditRanges.svelte +217 -0
- package/dist/SvSheetEditRanges.svelte.d.ts +15 -0
- package/dist/SvSheetErrors.svelte +125 -0
- package/dist/SvSheetErrors.svelte.d.ts +20 -0
- package/dist/SvSheetEvaluate.svelte +133 -0
- package/dist/SvSheetEvaluate.svelte.d.ts +15 -0
- package/dist/SvSheetFilterMenu.svelte +137 -34
- package/dist/SvSheetFilterMenu.svelte.d.ts +5 -0
- package/dist/SvSheetFindReplace.svelte +19 -19
- package/dist/SvSheetFormatCells.svelte +125 -81
- package/dist/SvSheetGoalSeek.svelte +17 -16
- package/dist/SvSheetInsertFunction.svelte +15 -13
- package/dist/SvSheetIteration.svelte +81 -0
- package/dist/SvSheetIteration.svelte.d.ts +10 -0
- package/dist/SvSheetLink.svelte +99 -0
- package/dist/SvSheetLink.svelte.d.ts +17 -0
- package/dist/SvSheetListPicker.svelte +4 -2
- package/dist/SvSheetListPicker.svelte.d.ts +0 -6
- package/dist/SvSheetManageRules.svelte +20 -18
- package/dist/SvSheetNameManager.svelte +17 -15
- package/dist/SvSheetPageSetup.svelte +159 -0
- package/dist/SvSheetPageSetup.svelte.d.ts +13 -0
- package/dist/SvSheetPasteSpecial.svelte +22 -20
- package/dist/SvSheetPivot.svelte +220 -0
- package/dist/SvSheetPivot.svelte.d.ts +21 -0
- package/dist/SvSheetProtectSheet.svelte +81 -0
- package/dist/SvSheetProtectSheet.svelte.d.ts +11 -0
- package/dist/SvSheetRemoveDuplicates.svelte +13 -10
- package/dist/SvSheetRibbon.svelte +104 -51
- package/dist/SvSheetSizeDialog.svelte +4 -2
- package/dist/SvSheetSort.svelte +218 -0
- package/dist/SvSheetSort.svelte.d.ts +28 -0
- package/dist/SvSheetSparklines.svelte +135 -0
- package/dist/SvSheetSparklines.svelte.d.ts +14 -0
- package/dist/SvSheetTable.svelte +160 -0
- package/dist/SvSheetTable.svelte.d.ts +24 -0
- package/dist/SvSheetTabs.svelte +161 -31
- package/dist/SvSheetTabs.svelte.d.ts +22 -8
- package/dist/SvSheetTextToColumns.svelte +20 -20
- package/dist/SvSheetValidationAlert.svelte +8 -6
- package/dist/advanced-filter-enable.js +1 -1
- package/dist/alerts/alert-engine.d.ts +6 -0
- package/dist/alerts/alert-engine.js +21 -4
- package/dist/alerts/alert-formats.js +2 -0
- package/dist/alerts/alert-types.d.ts +1 -1
- package/dist/alerts.d.ts +1 -1
- package/dist/alerts.js +2 -2
- package/dist/board.js +2 -2
- package/dist/bulk-edit.d.ts +3 -1
- package/dist/bulk-edit.js +1 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +39827 -16429
- package/dist/designer/assets/GridMenus-8gTNssgV.js +8 -0
- package/dist/designer/assets/{SvChartMenu-D9hEeHar.js → SvChartMenu-BDoeqGz6.js} +1 -1
- package/dist/designer/assets/{SvDateTimePicker-DARvKUL2.js → SvDateTimePicker-DCPg7WGN.js} +1 -1
- package/dist/designer/assets/SvGridCellEditor-DMoWJjEI.js +4 -0
- package/dist/designer/assets/{SvGridChart-B8j1SSy3.js → SvGridChart-Cy1ATrWh.js} +2 -2
- package/dist/designer/assets/SvGridChartBuilder-eQ3mn0L9.js +1 -0
- package/dist/designer/assets/{SvGridChartPanel-pT6ITc2P.js → SvGridChartPanel-CATdVc9X.js} +3 -3
- package/dist/designer/assets/{SvGridChartView-bg281UA1.js → SvGridChartView-CmsZtCED.js} +1 -1
- package/dist/designer/assets/SvGridDropdown-Cjz0H-fI.js +1 -0
- package/dist/designer/assets/{SvTabs-UFCMTHXS.js → SvTabs-2mHC6oaz.js} +1 -1
- package/dist/designer/assets/cell-formatting-BbubJTVz.js +1 -0
- package/dist/designer/assets/{chart-cIPUsbM5.js → chart-Cn7MnNqA.js} +1 -1
- package/dist/designer/assets/{chart-panel-messages-DtTxpBUU.js → chart-panel-messages-Dz7PTIqT.js} +1 -1
- package/dist/designer/assets/{chart-summary-Czu_5epb.js → chart-summary-BD1qYB2R.js} +1 -1
- package/dist/designer/assets/{chart-trend-D5I5VEe8.js → chart-trend-B8xr5LuD.js} +1 -1
- package/dist/designer/assets/{dismissable-BO547MH9.js → dismissable-ChWRPkoa.js} +1 -1
- package/dist/designer/assets/{export-format-D-eP4b2l.js → export-format-DGckmgKr.js} +1 -1
- package/dist/designer/assets/{focus-trap-CAreAhlU.js → focus-trap-V2-6Fy9I.js} +1 -1
- package/dist/designer/assets/{index-DwU7CAMm.js → index-BSjv6Qec.js} +317 -315
- package/dist/designer/assets/index-_JVFxt02.css +1 -0
- package/dist/designer/assets/row-resize-BInFWCpr.js +1 -0
- package/dist/designer/assets/server-block-cache-CXJKkJpy.js +1 -0
- package/dist/designer/assets/src-DUQ4xE3x.js +58 -0
- package/dist/designer/assets/validate-C3B8QKsM.js +1 -0
- package/dist/designer/index.html +17 -16
- package/dist/export-pdf.d.ts +75 -0
- package/dist/export-pdf.js +216 -15
- package/dist/export.js +10 -2
- package/dist/gantt/SvGridGantt.svelte +2890 -0
- package/dist/gantt/SvGridGantt.svelte.d.ts +30 -0
- package/dist/gantt/gantt-config.d.ts +73 -0
- package/dist/gantt/gantt-config.js +1 -0
- package/dist/gantt/gantt-critical-path.d.ts +67 -0
- package/dist/gantt/gantt-critical-path.js +207 -0
- package/dist/gantt/gantt-model.d.ts +257 -0
- package/dist/gantt/gantt-model.js +651 -0
- package/dist/gantt/gantt-resources.d.ts +59 -0
- package/dist/gantt/gantt-resources.js +71 -0
- package/dist/gantt/gantt.d.ts +7 -0
- package/dist/gantt/gantt.js +36 -0
- package/dist/gantt/index.d.ts +15 -0
- package/dist/gantt/index.js +22 -0
- package/dist/gantt/timeline-arrows.d.ts +102 -0
- package/dist/gantt/timeline-arrows.js +181 -0
- package/dist/import.d.ts +2 -2
- package/dist/import.js +1 -1
- package/dist/index.d.ts +34 -7
- package/dist/index.js +44 -7
- package/dist/install.js +8 -1
- package/dist/license-core.d.ts +17 -1
- package/dist/license-core.js +16 -5
- package/dist/license.d.ts +18 -2
- package/dist/license.js +71 -6
- package/dist/node/studio.js +140 -8
- package/dist/pivot-enable.js +2 -2
- package/dist/scheduler-dependencies.d.ts +35 -0
- package/dist/scheduler-dependencies.js +36 -1
- package/dist/scheduler.js +2 -2
- package/dist/selection-bar.js +2 -2
- package/dist/server/SvGroupCell.svelte +213 -0
- package/dist/server/SvGroupCell.svelte.d.ts +62 -0
- package/dist/server/SvRowGroupPanel.svelte +322 -0
- package/dist/server/SvRowGroupPanel.svelte.d.ts +37 -0
- package/dist/server/enable.d.ts +17 -0
- package/dist/server/enable.js +49 -0
- package/dist/server/index.d.ts +22 -0
- package/dist/server/index.js +24 -0
- package/dist/server/messages.d.ts +45 -0
- package/dist/server/messages.js +35 -0
- package/dist/server/server-group-model.d.ts +104 -0
- package/dist/server/server-group-model.js +272 -0
- package/dist/server/server-pivot.d.ts +46 -0
- package/dist/server/server-pivot.js +57 -0
- package/dist/server/server-row-model.d.ts +547 -0
- package/dist/server/server-row-model.js +1551 -0
- package/dist/server/server-selection.d.ts +99 -0
- package/dist/server/server-selection.js +215 -0
- package/dist/server/svgrid-adapter.d.ts +155 -0
- package/dist/server/svgrid-adapter.js +202 -0
- package/dist/sheet/ast.d.ts +45 -1
- package/dist/sheet/ast.js +14 -0
- package/dist/sheet/auto-filter.d.ts +58 -4
- package/dist/sheet/auto-filter.js +159 -11
- package/dist/sheet/autocomplete.d.ts +9 -0
- package/dist/sheet/autocomplete.js +90 -4
- package/dist/sheet/cell-images.d.ts +39 -0
- package/dist/sheet/cell-images.js +90 -0
- package/dist/sheet/cell-types.d.ts +58 -0
- package/dist/sheet/cell-types.js +96 -0
- package/dist/sheet/coerce.d.ts +40 -0
- package/dist/sheet/coerce.js +170 -3
- package/dist/sheet/comments.d.ts +43 -9
- package/dist/sheet/comments.js +85 -27
- package/dist/sheet/conditional-formats.d.ts +29 -6
- package/dist/sheet/conditional-formats.js +37 -12
- package/dist/sheet/csv.d.ts +32 -0
- package/dist/sheet/csv.js +158 -0
- package/dist/sheet/culture.d.ts +87 -0
- package/dist/sheet/culture.js +247 -0
- package/dist/sheet/delta.d.ts +102 -0
- package/dist/sheet/delta.js +203 -0
- package/dist/sheet/deps.d.ts +45 -1
- package/dist/sheet/deps.js +77 -2
- package/dist/sheet/document.d.ts +106 -4
- package/dist/sheet/document.js +204 -3
- package/dist/sheet/engine.d.ts +67 -0
- package/dist/sheet/engine.js +19 -0
- package/dist/sheet/entry.d.ts +8 -4
- package/dist/sheet/entry.js +171 -21
- package/dist/sheet/error-check.d.ts +49 -0
- package/dist/sheet/error-check.js +83 -0
- package/dist/sheet/evaluate-steps.d.ts +48 -0
- package/dist/sheet/evaluate-steps.js +196 -0
- package/dist/sheet/evaluate.d.ts +67 -0
- package/dist/sheet/evaluate.js +725 -13
- package/dist/sheet/filter-files.d.ts +37 -0
- package/dist/sheet/filter-files.js +465 -0
- package/dist/sheet/format-store.d.ts +15 -0
- package/dist/sheet/format-store.js +11 -1
- package/dist/sheet/from-rows.d.ts +14 -0
- package/dist/sheet/from-rows.js +50 -0
- package/dist/sheet/function-catalog.d.ts +1 -1
- package/dist/sheet/function-catalog.js +296 -5
- package/dist/sheet/functions.js +363 -91
- package/dist/sheet/index.d.ts +35 -8
- package/dist/sheet/index.js +35 -8
- package/dist/sheet/links.d.ts +57 -0
- package/dist/sheet/links.js +174 -0
- package/dist/sheet/messages.d.ts +778 -0
- package/dist/sheet/messages.js +806 -0
- package/dist/sheet/number-format.d.ts +52 -5
- package/dist/sheet/number-format.js +465 -68
- package/dist/sheet/objects.d.ts +106 -0
- package/dist/sheet/objects.js +149 -0
- package/dist/sheet/ods-document.d.ts +44 -0
- package/dist/sheet/ods-document.js +1275 -0
- package/dist/sheet/open-file.d.ts +26 -0
- package/dist/sheet/open-file.js +56 -0
- package/dist/sheet/outline.d.ts +82 -0
- package/dist/sheet/outline.js +228 -0
- package/dist/sheet/packs/array.d.ts +16 -0
- package/dist/sheet/packs/array.js +172 -0
- package/dist/sheet/packs/database.d.ts +42 -0
- package/dist/sheet/packs/database.js +142 -0
- package/dist/sheet/packs/engineering.d.ts +2 -0
- package/dist/sheet/packs/engineering.js +296 -0
- package/dist/sheet/packs/financial.d.ts +5 -0
- package/dist/sheet/packs/financial.js +220 -0
- package/dist/sheet/packs/math-stats.d.ts +2 -0
- package/dist/sheet/packs/math-stats.js +372 -0
- package/dist/sheet/packs/special.d.ts +44 -0
- package/dist/sheet/packs/special.js +187 -0
- package/dist/sheet/packs/stats-dist.d.ts +2 -0
- package/dist/sheet/packs/stats-dist.js +785 -0
- package/dist/sheet/packs/text-date.d.ts +2 -0
- package/dist/sheet/packs/text-date.js +332 -0
- package/dist/sheet/packs/trig.d.ts +2 -0
- package/dist/sheet/packs/trig.js +73 -0
- package/dist/sheet/page-setup.d.ts +73 -0
- package/dist/sheet/page-setup.js +73 -0
- package/dist/sheet/parse.js +42 -10
- package/dist/sheet/paste-special.d.ts +22 -0
- package/dist/sheet/paste-special.js +14 -5
- package/dist/sheet/pivot-range.d.ts +160 -0
- package/dist/sheet/pivot-range.js +333 -0
- package/dist/sheet/presence.d.ts +73 -0
- package/dist/sheet/presence.js +79 -0
- package/dist/sheet/print.d.ts +104 -0
- package/dist/sheet/print.js +169 -0
- package/dist/sheet/protection.d.ts +55 -1
- package/dist/sheet/protection.js +55 -2
- package/dist/sheet/rects.d.ts +1 -1
- package/dist/sheet/refs.d.ts +48 -0
- package/dist/sheet/refs.js +155 -8
- package/dist/sheet/ribbon-icons.d.ts +1 -1
- package/dist/sheet/ribbon-icons.js +186 -0
- package/dist/sheet/ribbon.d.ts +1 -1
- package/dist/sheet/ribbon.js +324 -23
- package/dist/sheet/sheet-kinds.d.ts +74 -0
- package/dist/sheet/sheet-kinds.js +91 -0
- package/dist/sheet/shortcuts.d.ts +13 -3
- package/dist/sheet/shortcuts.js +63 -12
- package/dist/sheet/sort.d.ts +51 -0
- package/dist/sheet/sort.js +107 -0
- package/dist/sheet/sparklines.d.ts +111 -0
- package/dist/sheet/sparklines.js +204 -0
- package/dist/sheet/table-styles.d.ts +74 -0
- package/dist/sheet/table-styles.js +152 -0
- package/dist/sheet/tables.d.ts +19 -0
- package/dist/sheet/tables.js +31 -0
- package/dist/sheet/tokenize.d.ts +18 -1
- package/dist/sheet/tokenize.js +60 -9
- package/dist/sheet/validation.d.ts +16 -0
- package/dist/sheet/validation.js +37 -0
- package/dist/sheet/workbook.d.ts +148 -3
- package/dist/sheet/workbook.js +742 -31
- package/dist/sheet/xls-cfb.d.ts +32 -0
- package/dist/sheet/xls-cfb.js +290 -0
- package/dist/sheet/xls-document.d.ts +10 -0
- package/dist/sheet/xls-document.js +1243 -0
- package/dist/sheet/xls-formula.d.ts +40 -0
- package/dist/sheet/xls-formula.js +569 -0
- package/dist/sheet/xlsx-document.d.ts +108 -0
- package/dist/sheet/xlsx-document.js +1849 -0
- package/dist/sheet/xlsx-drawing.d.ts +99 -0
- package/dist/sheet/xlsx-drawing.js +420 -0
- package/dist/sheet-enable.js +6 -7
- package/dist/sheet-text.d.ts +7 -0
- package/dist/sheet-text.js +29 -0
- package/dist/sources/auth-supabase.js +2 -2
- package/dist/sources/realtime-supabase.js +2 -2
- package/dist/sources/rest.d.ts +6 -2
- package/dist/sources/rest.js +42 -7
- package/dist/sources/supabase.d.ts +10 -1
- package/dist/sources/supabase.js +73 -40
- package/dist/studio/emit-project.js +14 -1
- package/dist/studio/index.js +1 -1
- package/dist/studio/project.d.ts +11 -2
- package/dist/studio/project.js +5 -2
- package/dist/studio/ui-components.generated.js +107 -2
- package/dist/sveltekit/in-memory.d.ts +2 -2
- package/dist/sveltekit/in-memory.js +125 -4
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/dist/sveltekit/query-plan.d.ts +50 -4
- package/dist/sveltekit/query-plan.js +25 -3
- package/dist/sveltekit/sql-source.d.ts +6 -0
- package/dist/sveltekit/sql-source.js +34 -6
- package/dist/sveltekit/sql.d.ts +31 -0
- package/dist/sveltekit/sql.js +135 -25
- package/dist/sveltekit/transport.d.ts +10 -2
- package/dist/sveltekit/transport.js +30 -6
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watermark.js +35 -2
- package/dist/wc/GridMenus-By9vPuq0.js +1088 -0
- package/dist/wc/SvChartMenu-CQNrQGT2.js +308 -0
- package/dist/wc/SvDateRangeInput-DR6m7bSM.js +199 -0
- package/dist/wc/SvDateTimePicker-D-vRnaMo.js +771 -0
- package/dist/wc/SvField-B_kurx_P.js +149 -0
- package/dist/wc/SvGrid.helpers-BdVDtjUm.js +135 -0
- package/dist/wc/SvGridCellEditor-Bnf1Kr5C.js +560 -0
- package/dist/wc/SvGridChart-BqUJh2pG.js +3572 -0
- package/dist/wc/SvGridChartBuilder-CGycC5cn.js +988 -0
- package/dist/wc/SvGridChartPanel-CRnizsHD.js +1746 -0
- package/dist/wc/SvGridChartView-D1pa_xMy.js +47 -0
- package/dist/wc/SvGridDropdown-45WsObLL.js +259 -0
- package/dist/wc/SvModal-DrfOYYyX.js +189 -0
- package/dist/wc/cell-formatting-CBfgahv1.js +86 -0
- package/dist/wc/chart-6OXlybVT.js +4341 -0
- package/dist/wc/chart-export-CeSADGfi.js +135 -0
- package/dist/wc/chart-export-pdf-BGko7MER.js +104 -0
- package/dist/wc/chart-summary-2m3ia1-n.js +137 -0
- package/dist/wc/chart-trend-CQOQtTMl.js +50 -0
- package/dist/wc/chart-validate-CD6ptiwg.js +180 -0
- package/dist/wc/client-DQIzjDBG.js +3064 -0
- package/dist/wc/column-resize-DUPgynvc.js +101 -0
- package/dist/wc/date-format-DS-z_h9v.js +1130 -0
- package/dist/wc/dismissable-CQ6XFXsk.js +45 -0
- package/dist/wc/editor-contract-peBDxir_.js +22 -0
- package/dist/wc/editor-registry-DxGKqcvx.js +24 -0
- package/dist/wc/focus-trap-Dbb2tJxb.js +65 -0
- package/dist/wc/list-option-DrRyivWp.js +82 -0
- package/dist/wc/popover-WsGlvtQv.js +69 -0
- package/dist/wc/positioning-BiKLfu1E.js +112 -0
- package/dist/wc/react.d.ts +41 -0
- package/dist/wc/react.js +90 -0
- package/dist/wc/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/wc/row-drag-touch-CycRR65n.js +93 -0
- package/dist/wc/row-resize-limG_N1z.js +111 -0
- package/dist/wc/sv-sheet-element.d.ts +97 -0
- package/dist/wc/sv-sheet-element.js +44213 -0
- package/dist/wc/svelte-virtualizer.svelte-CXtQy2TQ.js +530 -0
- package/dist/wc/validate-BqDthkNr.js +88 -0
- package/dist/wc/vue.d.ts +31 -0
- package/dist/wc/vue.js +75 -0
- package/package.json +30 -5
- package/dist/designer/assets/GridMenus-BlsvL_aU.js +0 -8
- package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +0 -4
- package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +0 -1
- package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +0 -1
- package/dist/designer/assets/cell-formatting-wr5wm7US.js +0 -1
- package/dist/designer/assets/index-CMFgAoVV.css +0 -1
- package/dist/designer/assets/row-resize-DLktcqUK.js +0 -1
- package/dist/designer/assets/src-EdI3Xqds.js +0 -58
- package/dist/designer/assets/validate-h-aslRV9.js +0 -1
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// Pure, like every model in this package: the types above are erased at build
|
|
2
|
+
// time and nothing here imports a value. That is what lets the plugin-free
|
|
3
|
+
// `unit` vitest project run this file without compiling a Svelte component (see
|
|
4
|
+
// the projects split in vite.config.ts). The three date helpers and the major
|
|
5
|
+
// grouping below are the grid's, re-stated rather than imported, for that
|
|
6
|
+
// reason alone - they are a dozen lines and pinned by the tests in this folder.
|
|
7
|
+
const MS_DAY = 86400000;
|
|
8
|
+
/** A new Date at local midnight of the same calendar day. */
|
|
9
|
+
export function startOfDay(d) {
|
|
10
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
11
|
+
}
|
|
12
|
+
/** `n` days after `d` (negative to go back), preserving the time of day. */
|
|
13
|
+
export function addDays(d, n) {
|
|
14
|
+
const out = new Date(d.getTime());
|
|
15
|
+
out.setDate(out.getDate() + n);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
/** Coerce a Date | epoch-ms | parseable string to a Date, or null if invalid. */
|
|
19
|
+
function toDate(value) {
|
|
20
|
+
if (value == null)
|
|
21
|
+
return null;
|
|
22
|
+
const d = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
23
|
+
return isNaN(d.getTime()) ? null : d;
|
|
24
|
+
}
|
|
25
|
+
/** Group consecutive ticks that share a key into one coarser header cell. */
|
|
26
|
+
function groupMajors(ticks, keyOf, labelOf) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const t of ticks) {
|
|
29
|
+
const k = keyOf(t);
|
|
30
|
+
const last = out[out.length - 1];
|
|
31
|
+
if (last && last._k === k)
|
|
32
|
+
last.widthPct += t.widthPct;
|
|
33
|
+
else
|
|
34
|
+
out.push({ _k: k, label: labelOf(t), leftPct: t.leftPct, widthPct: t.widthPct });
|
|
35
|
+
}
|
|
36
|
+
return out.map(({ _k, ...m }) => m);
|
|
37
|
+
}
|
|
38
|
+
/** A bare `YYYY-MM-DD`, with no time part. */
|
|
39
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
40
|
+
const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
41
|
+
const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
42
|
+
const MO_LONG = [
|
|
43
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
44
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Loop ceiling for the day-walking helpers. A calendar with every weekday
|
|
48
|
+
* non-working would otherwise spin forever looking for a working day; ranges
|
|
49
|
+
* here are bounded in days, so this is unreachable for real input.
|
|
50
|
+
*/
|
|
51
|
+
const GUARD = 4000;
|
|
52
|
+
/**
|
|
53
|
+
* Parse a task date. A bare `YYYY-MM-DD` becomes LOCAL midnight of that day
|
|
54
|
+
* (see the header); anything else goes through the grid's `toDate`.
|
|
55
|
+
*/
|
|
56
|
+
export function parseDay(value) {
|
|
57
|
+
if (typeof value === 'string' && DATE_ONLY.test(value)) {
|
|
58
|
+
const [y, m, d] = value.split('-').map(Number);
|
|
59
|
+
return new Date(y, m - 1, d);
|
|
60
|
+
}
|
|
61
|
+
return toDate(value ?? null);
|
|
62
|
+
}
|
|
63
|
+
/** True when `value` is a date-only string, so its end is inclusive of that day. */
|
|
64
|
+
function isDateOnly(value) {
|
|
65
|
+
return typeof value === 'string' && DATE_ONLY.test(value);
|
|
66
|
+
}
|
|
67
|
+
/** Build a {@link WorkingCalendar}. Weekends (`[0, 6]`) are the default. */
|
|
68
|
+
export function makeCalendar(nonWorkingDays = [0, 6], holidays = []) {
|
|
69
|
+
const set = new Set();
|
|
70
|
+
for (const h of holidays) {
|
|
71
|
+
const d = parseDay(h);
|
|
72
|
+
if (d)
|
|
73
|
+
set.add(startOfDay(d).getTime());
|
|
74
|
+
}
|
|
75
|
+
return { nonWorkingDays: [...nonWorkingDays], holidays: set };
|
|
76
|
+
}
|
|
77
|
+
/** True when work happens on `d`'s calendar day. */
|
|
78
|
+
export function isWorkingDay(d, cal) {
|
|
79
|
+
if (cal.nonWorkingDays.includes(d.getDay()))
|
|
80
|
+
return false;
|
|
81
|
+
return !cal.holidays.has(startOfDay(d).getTime());
|
|
82
|
+
}
|
|
83
|
+
/** Whether `cal` leaves any working day at all - the guard against looping. */
|
|
84
|
+
function hasWorkingDay(cal) {
|
|
85
|
+
return cal.nonWorkingDays.length < 7;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Working days in `[start, end)`. A task from Monday to the following Monday
|
|
89
|
+
* with weekends off is 5. Returns 0 when the range is empty or inverted.
|
|
90
|
+
*/
|
|
91
|
+
export function workingDays(start, end, cal) {
|
|
92
|
+
let cur = startOfDay(start);
|
|
93
|
+
const stop = end.getTime();
|
|
94
|
+
let n = 0;
|
|
95
|
+
let guard = 0;
|
|
96
|
+
while (cur.getTime() < stop && guard++ < GUARD) {
|
|
97
|
+
if (isWorkingDay(cur, cal))
|
|
98
|
+
n++;
|
|
99
|
+
cur = addDays(cur, 1);
|
|
100
|
+
}
|
|
101
|
+
return n;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The exclusive end of a task that starts on `start` and lasts `days` WORKING
|
|
105
|
+
* days: the midnight after its last working day. A zero or negative length
|
|
106
|
+
* gives `start` back (a milestone).
|
|
107
|
+
*/
|
|
108
|
+
export function addWorkingDays(start, days, cal) {
|
|
109
|
+
const from = startOfDay(start);
|
|
110
|
+
if (!(days > 0) || !hasWorkingDay(cal))
|
|
111
|
+
return from;
|
|
112
|
+
let cur = from;
|
|
113
|
+
let left = Math.ceil(days);
|
|
114
|
+
let guard = 0;
|
|
115
|
+
while (left > 0 && guard++ < GUARD) {
|
|
116
|
+
if (isWorkingDay(cur, cal))
|
|
117
|
+
left--;
|
|
118
|
+
cur = addDays(cur, 1);
|
|
119
|
+
}
|
|
120
|
+
return cur;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The start of a task that lasts `days` WORKING days and ends (exclusively) at
|
|
124
|
+
* `end`: the inverse of {@link addWorkingDays}. Walks back from the day before
|
|
125
|
+
* `end` counting working days, so a five-day task ending Saturday 00:00
|
|
126
|
+
* started the Monday before. Zero days gives `end`'s own midnight.
|
|
127
|
+
*/
|
|
128
|
+
export function startForWorkingDays(end, days, cal) {
|
|
129
|
+
const to = startOfDay(end);
|
|
130
|
+
if (!(days > 0) || !hasWorkingDay(cal))
|
|
131
|
+
return to;
|
|
132
|
+
let cur = to;
|
|
133
|
+
let left = Math.ceil(days);
|
|
134
|
+
let guard = 0;
|
|
135
|
+
while (left > 0 && guard++ < GUARD) {
|
|
136
|
+
cur = addDays(cur, -1);
|
|
137
|
+
if (isWorkingDay(cur, cal))
|
|
138
|
+
left--;
|
|
139
|
+
}
|
|
140
|
+
return cur;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The span a task keeps when it is moved to `start`: its WORKING days laid
|
|
144
|
+
* out from there, so a Monday-to-Friday task dragged to Wednesday ends the
|
|
145
|
+
* Tuesday after, not the Sunday. A task with no working day in it (a
|
|
146
|
+
* milestone, or one parked on a weekend) keeps its calendar length instead,
|
|
147
|
+
* so nothing collapses to zero.
|
|
148
|
+
*/
|
|
149
|
+
export function moveWorkingSpan(start, original, cal) {
|
|
150
|
+
const calMs = original.end.getTime() - original.start.getTime();
|
|
151
|
+
const days = workingDays(original.start, original.end, cal);
|
|
152
|
+
if (days <= 0 || calMs <= 0)
|
|
153
|
+
return { start, end: new Date(start.getTime() + calMs) };
|
|
154
|
+
return { start, end: addWorkingDays(start, days, cal) };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The nearest working day at or after `d` (`dir` 1) or at or before it
|
|
158
|
+
* (`dir` -1), at local midnight. A calendar with no working day at all returns
|
|
159
|
+
* `d`'s own midnight rather than spinning.
|
|
160
|
+
*/
|
|
161
|
+
export function snapToWorkingDay(d, dir, cal) {
|
|
162
|
+
let cur = startOfDay(d);
|
|
163
|
+
if (!hasWorkingDay(cal))
|
|
164
|
+
return cur;
|
|
165
|
+
let guard = 0;
|
|
166
|
+
while (!isWorkingDay(cur, cal) && guard++ < GUARD)
|
|
167
|
+
cur = addDays(cur, dir);
|
|
168
|
+
return cur;
|
|
169
|
+
}
|
|
170
|
+
/** Clamp anything to a sane 0-100 percent; junk reads as 0. */
|
|
171
|
+
function pct(value) {
|
|
172
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
173
|
+
if (!Number.isFinite(n))
|
|
174
|
+
return 0;
|
|
175
|
+
return Math.max(0, Math.min(100, n));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolve `rows` into bars. A row with no readable start is SKIPPED - a
|
|
179
|
+
* backlog item with no date is not a bar, and dropping it is better than
|
|
180
|
+
* inventing a date for it.
|
|
181
|
+
*
|
|
182
|
+
* End wins over duration; duration counts working days from the start; with
|
|
183
|
+
* neither the task is a milestone (`end === start`). A date-only end string is
|
|
184
|
+
* inclusive of its day (see the file header).
|
|
185
|
+
*/
|
|
186
|
+
export function resolveTasks(rows, spec, cal) {
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
const rawStart = spec.getStart(row);
|
|
190
|
+
const start = parseDay(rawStart);
|
|
191
|
+
if (!start)
|
|
192
|
+
continue;
|
|
193
|
+
const from = startOfDay(start);
|
|
194
|
+
const rawEnd = spec.getEnd?.(row);
|
|
195
|
+
const parsedEnd = parseDay(rawEnd);
|
|
196
|
+
let end;
|
|
197
|
+
if (parsedEnd) {
|
|
198
|
+
// A date-only end names its last day; a timestamped one is the instant.
|
|
199
|
+
end = isDateOnly(rawEnd) ? addDays(startOfDay(parsedEnd), 1) : parsedEnd;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const dur = spec.getDuration?.(row);
|
|
203
|
+
const n = typeof dur === 'number' ? dur : Number(dur);
|
|
204
|
+
end = Number.isFinite(n) && n > 0 ? addWorkingDays(from, n, cal) : from;
|
|
205
|
+
}
|
|
206
|
+
// An end at or before the start is a milestone, not a backwards bar.
|
|
207
|
+
if (end.getTime() < from.getTime())
|
|
208
|
+
end = from;
|
|
209
|
+
const parent = spec.getParent?.(row);
|
|
210
|
+
const parentKey = parent == null || parent === '' ? null : String(parent);
|
|
211
|
+
out.push({
|
|
212
|
+
key: spec.getKey(row),
|
|
213
|
+
row,
|
|
214
|
+
title: spec.getTitle?.(row) ?? '',
|
|
215
|
+
start: from,
|
|
216
|
+
end,
|
|
217
|
+
progress: pct(spec.getProgress?.(row)),
|
|
218
|
+
milestone: spec.getMilestone?.(row) === true || end.getTime() === from.getTime(),
|
|
219
|
+
parentKey,
|
|
220
|
+
color: spec.getColor?.(row),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Order tasks as a work-breakdown tree: roots in input order, each parent's
|
|
227
|
+
* children directly under it in input order. Rows whose parent id matches no
|
|
228
|
+
* task become roots rather than vanishing - the same rule the grid's tree data
|
|
229
|
+
* uses for a missing parent, and the one that keeps a filtered-out phase from
|
|
230
|
+
* hiding its tasks.
|
|
231
|
+
*
|
|
232
|
+
* Descendants of a collapsed node are left out of the returned list but still
|
|
233
|
+
* feed that node's summary, which is why this walks the full task set rather
|
|
234
|
+
* than the visible rows.
|
|
235
|
+
*
|
|
236
|
+
* A cycle in the parent links (a -> b -> a) is broken at the first repeat and
|
|
237
|
+
* the rows involved are treated as roots, so bad data cannot hang the render.
|
|
238
|
+
*
|
|
239
|
+
* Summary progress is the duration-weighted mean of the LEAF descendants: a
|
|
240
|
+
* two-day task at 100% beside a six-day task at 0% is 25% done, not 50%. A
|
|
241
|
+
* milestone weighs nothing (it has no duration), so a parent holding only
|
|
242
|
+
* milestones falls back to their plain mean.
|
|
243
|
+
*/
|
|
244
|
+
export function ganttTree(tasks, collapsed = new Set()) {
|
|
245
|
+
const byKey = new Map();
|
|
246
|
+
for (const t of tasks)
|
|
247
|
+
if (!byKey.has(t.key))
|
|
248
|
+
byKey.set(t.key, t);
|
|
249
|
+
// Resolve each task's EFFECTIVE parent: null for a root, an unknown parent,
|
|
250
|
+
// a self-link, or a link that would close a cycle.
|
|
251
|
+
const parentOf = new Map();
|
|
252
|
+
for (const t of tasks) {
|
|
253
|
+
const p = t.parentKey;
|
|
254
|
+
if (p == null || p === t.key || !byKey.has(p)) {
|
|
255
|
+
parentOf.set(t.key, null);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// Walk up; if we come back to this task, the link is part of a cycle.
|
|
259
|
+
const seen = new Set([t.key]);
|
|
260
|
+
let cur = p;
|
|
261
|
+
let cyclic = false;
|
|
262
|
+
let guard = 0;
|
|
263
|
+
while (cur != null && guard++ < GUARD) {
|
|
264
|
+
if (seen.has(cur)) {
|
|
265
|
+
cyclic = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
seen.add(cur);
|
|
269
|
+
const next = byKey.get(cur)?.parentKey;
|
|
270
|
+
cur = next == null || !byKey.has(next) ? null : next;
|
|
271
|
+
}
|
|
272
|
+
parentOf.set(t.key, cyclic ? null : p);
|
|
273
|
+
}
|
|
274
|
+
const childrenOf = new Map();
|
|
275
|
+
const roots = [];
|
|
276
|
+
for (const t of tasks) {
|
|
277
|
+
const p = parentOf.get(t.key) ?? null;
|
|
278
|
+
if (p == null)
|
|
279
|
+
roots.push(t);
|
|
280
|
+
else
|
|
281
|
+
(childrenOf.get(p) ?? childrenOf.set(p, []).get(p)).push(t);
|
|
282
|
+
}
|
|
283
|
+
const rollOf = new Map();
|
|
284
|
+
function roll(t) {
|
|
285
|
+
const hit = rollOf.get(t.key);
|
|
286
|
+
if (hit)
|
|
287
|
+
return hit;
|
|
288
|
+
const kids = childrenOf.get(t.key) ?? [];
|
|
289
|
+
let acc;
|
|
290
|
+
if (!kids.length) {
|
|
291
|
+
const days = Math.max(0, (t.end.getTime() - t.start.getTime()) / MS_DAY);
|
|
292
|
+
acc = {
|
|
293
|
+
start: t.start,
|
|
294
|
+
end: t.end,
|
|
295
|
+
weighted: t.progress * days,
|
|
296
|
+
days,
|
|
297
|
+
leaves: 1,
|
|
298
|
+
pctSum: t.progress,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// The descendants alone. A phase row has a start of its own only
|
|
303
|
+
// because `startField` is required; folding it in anchored the summary
|
|
304
|
+
// to wherever the phase was first typed in, so when its tasks moved the
|
|
305
|
+
// bar stretched back to that date instead of following them.
|
|
306
|
+
let start = null;
|
|
307
|
+
let end = null;
|
|
308
|
+
let weighted = 0;
|
|
309
|
+
let days = 0;
|
|
310
|
+
let leaves = 0;
|
|
311
|
+
let pctSum = 0;
|
|
312
|
+
for (const k of kids) {
|
|
313
|
+
const r = roll(k);
|
|
314
|
+
if (!start || r.start.getTime() < start.getTime())
|
|
315
|
+
start = r.start;
|
|
316
|
+
if (!end || r.end.getTime() > end.getTime())
|
|
317
|
+
end = r.end;
|
|
318
|
+
weighted += r.weighted;
|
|
319
|
+
days += r.days;
|
|
320
|
+
leaves += r.leaves;
|
|
321
|
+
pctSum += r.pctSum;
|
|
322
|
+
}
|
|
323
|
+
acc = { start: start ?? t.start, end: end ?? t.end, weighted, days, leaves, pctSum };
|
|
324
|
+
}
|
|
325
|
+
rollOf.set(t.key, acc);
|
|
326
|
+
return acc;
|
|
327
|
+
}
|
|
328
|
+
const out = [];
|
|
329
|
+
const walk = (list, depth) => {
|
|
330
|
+
for (const t of list) {
|
|
331
|
+
const kids = childrenOf.get(t.key) ?? [];
|
|
332
|
+
const isCollapsed = kids.length > 0 && collapsed.has(t.key);
|
|
333
|
+
const node = {
|
|
334
|
+
task: t,
|
|
335
|
+
depth,
|
|
336
|
+
hasChildren: kids.length > 0,
|
|
337
|
+
collapsed: isCollapsed,
|
|
338
|
+
};
|
|
339
|
+
if (kids.length) {
|
|
340
|
+
const r = roll(t);
|
|
341
|
+
node.summary = {
|
|
342
|
+
start: r.start,
|
|
343
|
+
end: r.end,
|
|
344
|
+
// Weight by duration; with only zero-length descendants there is
|
|
345
|
+
// nothing to weight by, so average them plainly.
|
|
346
|
+
progress: r.days > 0 ? r.weighted / r.days : r.leaves > 0 ? r.pctSum / r.leaves : 0,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
out.push(node);
|
|
350
|
+
if (kids.length && !isCollapsed)
|
|
351
|
+
walk(kids, depth + 1);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
walk(roots, 0);
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
/** Each visible node's row index, for arrow geometry. */
|
|
358
|
+
export function nodeIndex(nodes) {
|
|
359
|
+
const m = new Map();
|
|
360
|
+
nodes.forEach((n, i) => m.set(n.task.key, i));
|
|
361
|
+
return m;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Map every task key to the key of the visible row that stands for it: itself
|
|
365
|
+
* when it is on screen, else its nearest visible ancestor. A dependency
|
|
366
|
+
* pointing into a collapsed phase draws to that phase's summary bar instead of
|
|
367
|
+
* vanishing. Keys with no visible ancestor are left out.
|
|
368
|
+
*/
|
|
369
|
+
export function visibleAnchor(nodes, tasks) {
|
|
370
|
+
const visible = new Set(nodes.map((n) => n.task.key));
|
|
371
|
+
const parentOf = new Map();
|
|
372
|
+
for (const t of tasks)
|
|
373
|
+
parentOf.set(t.key, t.parentKey);
|
|
374
|
+
const anchor = new Map();
|
|
375
|
+
for (const t of tasks) {
|
|
376
|
+
let cur = t.key;
|
|
377
|
+
let guard = 0;
|
|
378
|
+
while (cur != null && !visible.has(cur) && guard++ < GUARD) {
|
|
379
|
+
cur = parentOf.get(cur) ?? null;
|
|
380
|
+
}
|
|
381
|
+
if (cur != null && visible.has(cur))
|
|
382
|
+
anchor.set(t.key, cur);
|
|
383
|
+
}
|
|
384
|
+
return anchor;
|
|
385
|
+
}
|
|
386
|
+
// --- the project window -----------------------------------------------------
|
|
387
|
+
/**
|
|
388
|
+
* The date window the chart spans: the earliest start to the latest end, plus
|
|
389
|
+
* `paddingDays` of slack each side, clamped to `minDate` / `maxDate`.
|
|
390
|
+
*
|
|
391
|
+
* Never returns an empty window: with no tasks it centres on `today`, so an
|
|
392
|
+
* empty plan still renders an axis rather than dividing by zero.
|
|
393
|
+
*/
|
|
394
|
+
export function projectRange(tasks, opts) {
|
|
395
|
+
const pad = Math.max(0, opts.paddingDays);
|
|
396
|
+
let start = null;
|
|
397
|
+
let end = null;
|
|
398
|
+
for (const t of tasks) {
|
|
399
|
+
if (!start || t.start.getTime() < start.getTime())
|
|
400
|
+
start = t.start;
|
|
401
|
+
if (!end || t.end.getTime() > end.getTime())
|
|
402
|
+
end = t.end;
|
|
403
|
+
}
|
|
404
|
+
const today = startOfDay(opts.today);
|
|
405
|
+
let from = addDays(start ? startOfDay(start) : today, -pad);
|
|
406
|
+
let to = addDays(end ? startOfDay(end) : today, pad);
|
|
407
|
+
const min = parseDay(opts.minDate ?? null);
|
|
408
|
+
const max = parseDay(opts.maxDate ?? null);
|
|
409
|
+
if (min && from.getTime() < startOfDay(min).getTime())
|
|
410
|
+
from = startOfDay(min);
|
|
411
|
+
if (max && to.getTime() > startOfDay(max).getTime())
|
|
412
|
+
to = startOfDay(max);
|
|
413
|
+
// A clamp (or a zero-length plan with no padding) can collapse the window.
|
|
414
|
+
if (to.getTime() <= from.getTime())
|
|
415
|
+
to = addDays(from, 1);
|
|
416
|
+
return { start: from, end: to };
|
|
417
|
+
}
|
|
418
|
+
// --- the header axis --------------------------------------------------------
|
|
419
|
+
/**
|
|
420
|
+
* Target pixel width of ONE tick per preset. The chart's total width is
|
|
421
|
+
* `ticks.length * ganttTickWidth[zoom]`; individual ticks keep their true
|
|
422
|
+
* proportion of the window (February is narrower than March), so bars and
|
|
423
|
+
* header cells always line up.
|
|
424
|
+
*/
|
|
425
|
+
export const ganttTickWidth = {
|
|
426
|
+
day: 48,
|
|
427
|
+
week: 28,
|
|
428
|
+
month: 40,
|
|
429
|
+
quarter: 96,
|
|
430
|
+
year: 72,
|
|
431
|
+
};
|
|
432
|
+
/** Local midnight of the `weekStartsOn` day at or before `d`. */
|
|
433
|
+
function startOfWeekOn(d, weekStartsOn) {
|
|
434
|
+
const s = startOfDay(d);
|
|
435
|
+
const shift = (s.getDay() - weekStartsOn + 7) % 7;
|
|
436
|
+
return addDays(s, -shift);
|
|
437
|
+
}
|
|
438
|
+
/** The tick unit each preset uses. */
|
|
439
|
+
function tickUnit(zoom) {
|
|
440
|
+
if (zoom === 'day' || zoom === 'week')
|
|
441
|
+
return 'day';
|
|
442
|
+
if (zoom === 'month')
|
|
443
|
+
return 'week';
|
|
444
|
+
return 'month';
|
|
445
|
+
}
|
|
446
|
+
/** Snap `d` back to the start of the tick it falls in. */
|
|
447
|
+
function snapBack(d, zoom, weekStartsOn) {
|
|
448
|
+
const unit = tickUnit(zoom);
|
|
449
|
+
if (unit === 'day')
|
|
450
|
+
return startOfDay(d);
|
|
451
|
+
if (unit === 'week')
|
|
452
|
+
return startOfWeekOn(d, weekStartsOn);
|
|
453
|
+
return new Date(d.getFullYear(), d.getMonth(), 1);
|
|
454
|
+
}
|
|
455
|
+
/** The start of the tick after the one `d` falls in. */
|
|
456
|
+
function nextTick(d, zoom) {
|
|
457
|
+
const unit = tickUnit(zoom);
|
|
458
|
+
if (unit === 'day')
|
|
459
|
+
return addDays(d, 1);
|
|
460
|
+
if (unit === 'week')
|
|
461
|
+
return addDays(d, 7);
|
|
462
|
+
return new Date(d.getFullYear(), d.getMonth() + 1, 1);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Build the Gantt's two-row header over `[start, end)` for a zoom preset. The
|
|
466
|
+
* window is snapped OUT to whole tick boundaries so the ticks tile it exactly -
|
|
467
|
+
* a half-drawn first column is where a header and its bars start disagreeing.
|
|
468
|
+
*
|
|
469
|
+
* Returns the scheduler's {@link TimelineAxis} shape (percentages, not pixels)
|
|
470
|
+
* so the Gantt and the scheduler timeline share `timelineGeom` and the same
|
|
471
|
+
* header markup:
|
|
472
|
+
*
|
|
473
|
+
* ```
|
|
474
|
+
* day ticks: days majors: weeks ("14 - 20 Sep")
|
|
475
|
+
* week ticks: days majors: months ("September 2026") <- the default
|
|
476
|
+
* month ticks: weeks majors: months
|
|
477
|
+
* quarter ticks: months majors: quarters ("Q3 2026")
|
|
478
|
+
* year ticks: months majors: years ("2026")
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
export function ganttAxis(start, end, zoom, opts = {}) {
|
|
482
|
+
const weekStartsOn = opts.weekStartsOn ?? 0;
|
|
483
|
+
const today = opts.today ? startOfDay(opts.today) : null;
|
|
484
|
+
const from = snapBack(start, zoom, weekStartsOn);
|
|
485
|
+
// The end snaps OUT: an end already on a tick boundary stays put, anything
|
|
486
|
+
// inside a tick grows to that tick's far edge, so the last column is whole.
|
|
487
|
+
const endBack = snapBack(end, zoom, weekStartsOn);
|
|
488
|
+
let to = endBack.getTime() === end.getTime() ? endBack : nextTick(endBack, zoom);
|
|
489
|
+
if (to.getTime() <= from.getTime())
|
|
490
|
+
to = nextTick(from, zoom);
|
|
491
|
+
const total = Math.max(1, to.getTime() - from.getTime());
|
|
492
|
+
const base = from.getTime();
|
|
493
|
+
const mk = (s, e, label) => ({
|
|
494
|
+
start: s,
|
|
495
|
+
end: e,
|
|
496
|
+
leftPct: ((s.getTime() - base) / total) * 100,
|
|
497
|
+
widthPct: ((e.getTime() - s.getTime()) / total) * 100,
|
|
498
|
+
label,
|
|
499
|
+
today: today ? s.getTime() <= today.getTime() && today.getTime() < e.getTime() : false,
|
|
500
|
+
});
|
|
501
|
+
const ticks = [];
|
|
502
|
+
let cur = from;
|
|
503
|
+
let guard = 0;
|
|
504
|
+
while (cur.getTime() < to.getTime() && guard++ < GUARD) {
|
|
505
|
+
const next = nextTick(cur, zoom);
|
|
506
|
+
const clipped = next.getTime() > to.getTime() ? to : next;
|
|
507
|
+
ticks.push(mk(cur, clipped, tickLabel(cur, zoom)));
|
|
508
|
+
cur = next;
|
|
509
|
+
}
|
|
510
|
+
let majors;
|
|
511
|
+
if (zoom === 'day') {
|
|
512
|
+
majors = groupMajors(ticks, (t) => String(startOfWeekOn(t.start, weekStartsOn).getTime()), (t) => weekLabel(startOfWeekOn(t.start, weekStartsOn)));
|
|
513
|
+
}
|
|
514
|
+
else if (zoom === 'week' || zoom === 'month') {
|
|
515
|
+
majors = groupMajors(ticks, (t) => `${t.start.getFullYear()}-${t.start.getMonth()}`, (t) => `${MO_LONG[t.start.getMonth()]} ${t.start.getFullYear()}`);
|
|
516
|
+
}
|
|
517
|
+
else if (zoom === 'quarter') {
|
|
518
|
+
majors = groupMajors(ticks, (t) => `${t.start.getFullYear()}-${Math.floor(t.start.getMonth() / 3)}`, (t) => `Q${Math.floor(t.start.getMonth() / 3) + 1} ${t.start.getFullYear()}`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
majors = groupMajors(ticks, (t) => String(t.start.getFullYear()), (t) => String(t.start.getFullYear()));
|
|
522
|
+
}
|
|
523
|
+
return { start: from, end: to, totalMs: total, ticks, majors };
|
|
524
|
+
}
|
|
525
|
+
/** A tick's own caption - short enough for the width its preset gives it. */
|
|
526
|
+
function tickLabel(d, zoom) {
|
|
527
|
+
switch (zoom) {
|
|
528
|
+
case 'day':
|
|
529
|
+
return `${WD[d.getDay()]} ${d.getDate()}`;
|
|
530
|
+
case 'week':
|
|
531
|
+
return String(d.getDate());
|
|
532
|
+
case 'month':
|
|
533
|
+
return String(d.getDate());
|
|
534
|
+
default:
|
|
535
|
+
return MO[d.getMonth()];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/** "14 - 20 Sep", or "28 Sep - 4 Oct" across a month boundary. */
|
|
539
|
+
function weekLabel(weekStart) {
|
|
540
|
+
const last = addDays(weekStart, 6);
|
|
541
|
+
const a = weekStart.getDate();
|
|
542
|
+
const b = last.getDate();
|
|
543
|
+
if (weekStart.getMonth() === last.getMonth()) {
|
|
544
|
+
return `${a} - ${b} ${MO[weekStart.getMonth()]}`;
|
|
545
|
+
}
|
|
546
|
+
return `${a} ${MO[weekStart.getMonth()]} - ${b} ${MO[last.getMonth()]}`;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Build the scale over `[start, end)`.
|
|
550
|
+
*
|
|
551
|
+
* With no `collapsed` predicate this is the plain linear mapping - one segment,
|
|
552
|
+
* `totalPx` exactly as asked - which is what every chart without the Pro axis
|
|
553
|
+
* uses. Pass one and each run of days it answers true for folds to `gapPx`
|
|
554
|
+
* instead of its real width, so a year of weekends stops eating two sevenths of
|
|
555
|
+
* the chart. Within a gap the mapping stays proportional, so a task that does
|
|
556
|
+
* run over a folded weekend still draws across it rather than collapsing to a
|
|
557
|
+
* line.
|
|
558
|
+
*
|
|
559
|
+
* `gapPx` of 0 removes the folded days outright: Friday's finish and Monday's
|
|
560
|
+
* start land on the same pixel.
|
|
561
|
+
*/
|
|
562
|
+
export function ganttScale(start, end, totalPx, opts = {}) {
|
|
563
|
+
const startMs = start.getTime();
|
|
564
|
+
const totalMs = Math.max(1, end.getTime() - startMs);
|
|
565
|
+
const pxPerMs = totalPx / totalMs;
|
|
566
|
+
const collapsedDay = opts.collapsed;
|
|
567
|
+
let segments;
|
|
568
|
+
let widthPx = totalPx;
|
|
569
|
+
if (!collapsedDay) {
|
|
570
|
+
segments = [{ start, end, x: 0, px: totalPx, collapsed: false }];
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
const gapPx = Math.max(0, opts.gapPx ?? 12);
|
|
574
|
+
segments = [];
|
|
575
|
+
let x = 0;
|
|
576
|
+
let runStart = start;
|
|
577
|
+
let runCollapsed = collapsedDay(startOfDay(start));
|
|
578
|
+
const push = (from, to, isCollapsed) => {
|
|
579
|
+
if (to.getTime() <= from.getTime())
|
|
580
|
+
return;
|
|
581
|
+
const px = isCollapsed ? gapPx : (to.getTime() - from.getTime()) * pxPerMs;
|
|
582
|
+
segments.push({ start: from, end: to, x, px, collapsed: isCollapsed });
|
|
583
|
+
x += px;
|
|
584
|
+
};
|
|
585
|
+
// Walk whole days: the fold is a calendar decision, so a partial first or
|
|
586
|
+
// last day belongs to whatever its own day is.
|
|
587
|
+
let day = addDays(startOfDay(start), 1);
|
|
588
|
+
while (day.getTime() < end.getTime()) {
|
|
589
|
+
const isCollapsed = collapsedDay(day);
|
|
590
|
+
if (isCollapsed !== runCollapsed) {
|
|
591
|
+
push(runStart, day, runCollapsed);
|
|
592
|
+
runStart = day;
|
|
593
|
+
runCollapsed = isCollapsed;
|
|
594
|
+
}
|
|
595
|
+
day = addDays(day, 1);
|
|
596
|
+
}
|
|
597
|
+
push(runStart, end, runCollapsed);
|
|
598
|
+
if (segments.length === 0)
|
|
599
|
+
segments = [{ start, end, x: 0, px: totalPx, collapsed: false }];
|
|
600
|
+
const tail = segments[segments.length - 1];
|
|
601
|
+
widthPx = tail.x + tail.px;
|
|
602
|
+
}
|
|
603
|
+
/** The segment `ms` falls in, by binary search; the ends clamp. */
|
|
604
|
+
const segmentAt = (ms) => {
|
|
605
|
+
let lo = 0;
|
|
606
|
+
let hi = segments.length - 1;
|
|
607
|
+
while (lo < hi) {
|
|
608
|
+
const mid = (lo + hi + 1) >> 1;
|
|
609
|
+
if (segments[mid].start.getTime() <= ms)
|
|
610
|
+
lo = mid;
|
|
611
|
+
else
|
|
612
|
+
hi = mid - 1;
|
|
613
|
+
}
|
|
614
|
+
return segments[lo];
|
|
615
|
+
};
|
|
616
|
+
const xOf = (d) => {
|
|
617
|
+
const ms = d.getTime();
|
|
618
|
+
if (ms <= startMs)
|
|
619
|
+
return (ms - startMs) * pxPerMs;
|
|
620
|
+
const last = segments[segments.length - 1];
|
|
621
|
+
if (ms >= last.end.getTime()) {
|
|
622
|
+
return last.x + last.px + (ms - last.end.getTime()) * pxPerMs;
|
|
623
|
+
}
|
|
624
|
+
const seg = segmentAt(ms);
|
|
625
|
+
const span = Math.max(1, seg.end.getTime() - seg.start.getTime());
|
|
626
|
+
return seg.x + ((ms - seg.start.getTime()) / span) * seg.px;
|
|
627
|
+
};
|
|
628
|
+
const dateAt = (x) => {
|
|
629
|
+
if (x <= 0)
|
|
630
|
+
return new Date(startMs + x / pxPerMs);
|
|
631
|
+
const last = segments[segments.length - 1];
|
|
632
|
+
const endPx = last.x + last.px;
|
|
633
|
+
if (x >= endPx)
|
|
634
|
+
return new Date(last.end.getTime() + (x - endPx) / pxPerMs);
|
|
635
|
+
let lo = 0;
|
|
636
|
+
let hi = segments.length - 1;
|
|
637
|
+
while (lo < hi) {
|
|
638
|
+
const mid = (lo + hi + 1) >> 1;
|
|
639
|
+
if (segments[mid].x <= x)
|
|
640
|
+
lo = mid;
|
|
641
|
+
else
|
|
642
|
+
hi = mid - 1;
|
|
643
|
+
}
|
|
644
|
+
const seg = segments[lo];
|
|
645
|
+
const span = seg.end.getTime() - seg.start.getTime();
|
|
646
|
+
if (seg.px <= 0)
|
|
647
|
+
return new Date(seg.start.getTime());
|
|
648
|
+
return new Date(seg.start.getTime() + ((x - seg.x) / seg.px) * span);
|
|
649
|
+
};
|
|
650
|
+
return { totalPx: widthPx, segments, xOf, dateAt };
|
|
651
|
+
}
|