@morscherlab/mint-sdk 1.0.0-rc.2 → 1.0.0-rc.5
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/dist/__tests__/components/AppTopBar.navigation.test.d.ts +1 -0
- package/dist/__tests__/components/DoseCalculatorVolumeField.test.d.ts +1 -0
- package/dist/__tests__/components/PlateMapEditorToolbarInternal.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.controls.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.navigation.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.shell.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStep.presentation.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStepEditor.state.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStepParameterField.test.d.ts +1 -0
- package/dist/__tests__/components/ReagentList.presentation.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.colors.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.drag.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.groups.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.selection.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelectorSampleRow.test.d.ts +1 -0
- package/dist/__tests__/components/ScheduleCalendar.test.d.ts +1 -0
- package/dist/__tests__/components/SettingsModal.schema.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.colors.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.conditions.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.geometry.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.interaction.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.legend.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.rendering.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.sampleDrop.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/classify.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/columns.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/compose.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/cooccurrence.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/fingerprint.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/integration.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/template.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/tokenize.test.d.ts +1 -0
- package/dist/__tests__/composables/useAutoGroupInputSources.test.d.ts +1 -0
- package/dist/__tests__/composables/useScheduleCalendarLayout.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsComponents.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsExports.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsParsing.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsTemplates.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsTheme.test.d.ts +1 -0
- package/dist/components/AppTopBar.navigation.d.ts +11 -0
- package/dist/components/BaseButton.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseInput.vue.d.ts +2 -2
- package/dist/components/BasePill.vue.d.ts +1 -1
- package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseSlider.vue.d.ts +2 -2
- package/dist/components/BaseTextarea.vue.d.ts +2 -2
- package/dist/components/BaseToggle.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
- package/dist/components/ColorSlider.vue.d.ts +2 -2
- package/dist/components/ConcentrationInput.vue.d.ts +2 -2
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +2 -2
- package/dist/components/DoseCalculatorVolumeField.vue.d.ts +15 -0
- package/dist/components/DropdownButton.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/FormulaInput.vue.d.ts +2 -2
- package/dist/components/IconButton.vue.d.ts +1 -1
- package/dist/components/LoadingSpinner.vue.d.ts +1 -1
- package/dist/components/MoleculeInput.vue.d.ts +2 -2
- package/dist/components/MultiSelect.vue.d.ts +1 -1
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/PlateMapEditor.vue.d.ts +6 -6
- package/dist/components/PluginWorkspaceView.controls.d.ts +28 -0
- package/dist/components/PluginWorkspaceView.navigation.d.ts +29 -0
- package/dist/components/PluginWorkspaceView.props.d.ts +151 -0
- package/dist/components/PluginWorkspaceView.shell.d.ts +19 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +46 -195
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStep.presentation.d.ts +4 -0
- package/dist/components/ProtocolStepEditor.state.d.ts +18 -0
- package/dist/components/ProtocolStepParameterField.vue.d.ts +12 -0
- package/dist/components/ReagentList.presentation.d.ts +16 -0
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SampleSelector.colors.d.ts +13 -0
- package/dist/components/SampleSelector.drag.d.ts +24 -0
- package/dist/components/SampleSelector.groups.d.ts +15 -0
- package/dist/components/SampleSelector.selection.d.ts +26 -0
- package/dist/components/SampleSelector.vue.d.ts +4 -1
- package/dist/components/SampleSelectorSampleRow.vue.d.ts +21 -0
- package/dist/components/SegmentedControl.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +2 -2
- package/dist/components/SequenceProgressBar.vue.d.ts +1 -1
- package/dist/components/SettingsModal.schema.d.ts +9 -0
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +2 -2
- package/dist/components/TimePicker.vue.d.ts +2 -2
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +2 -2
- package/dist/components/WellPlate.colors.d.ts +9 -0
- package/dist/components/WellPlate.conditions.d.ts +26 -0
- package/dist/components/WellPlate.geometry.d.ts +23 -0
- package/dist/components/WellPlate.interaction.d.ts +71 -0
- package/dist/components/WellPlate.legend.d.ts +2 -0
- package/dist/components/WellPlate.rendering.d.ts +24 -0
- package/dist/components/WellPlate.sampleDrop.d.ts +8 -0
- package/dist/components/WellPlate.vue.d.ts +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/internal/ActionItemInternal.vue.d.ts +1 -1
- package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +28 -0
- package/dist/{components-BhK-dW99.js → components-DtHA2bgp.js} +3754 -2991
- package/dist/components-DtHA2bgp.js.map +1 -0
- package/dist/composables/autoGroup/classKey.d.ts +4 -0
- package/dist/composables/autoGroup/classify.d.ts +28 -0
- package/dist/composables/autoGroup/colors.d.ts +2 -0
- package/dist/composables/autoGroup/columns.d.ts +10 -0
- package/dist/composables/autoGroup/compose.d.ts +8 -0
- package/dist/composables/autoGroup/cooccurrence.d.ts +2 -0
- package/dist/composables/autoGroup/csv-shim.d.ts +2 -0
- package/dist/composables/autoGroup/fingerprint.d.ts +3 -0
- package/dist/composables/autoGroup/index.d.ts +16 -0
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +38 -0
- package/dist/composables/autoGroup/template.d.ts +15 -0
- package/dist/composables/autoGroup/tokenize.d.ts +8 -0
- package/dist/composables/autoGroupConstants.d.ts +1 -0
- package/dist/composables/autoGroupGrouping.d.ts +3 -0
- package/dist/composables/controlComponentBindings.d.ts +7 -0
- package/dist/composables/controlSchemaAdapters.d.ts +20 -0
- package/dist/composables/controlSchemaDoseDesign.d.ts +11 -0
- package/dist/composables/controlSchemaFormFields.d.ts +3 -0
- package/dist/composables/controlSchemaLayout.d.ts +7 -0
- package/dist/composables/controlSchemaModel.d.ts +5 -0
- package/dist/composables/controlSchemaNormalize.d.ts +15 -0
- package/dist/composables/controlSchemaTypes.d.ts +305 -0
- package/dist/composables/controlSchemaUtils.d.ts +9 -0
- package/dist/composables/controlWorkspaceOptions.d.ts +2 -0
- package/dist/composables/formBuilderSchema.d.ts +18 -0
- package/dist/composables/index.js +3 -3
- package/dist/composables/pluginEndpointBuilder.d.ts +13 -0
- package/dist/composables/protocolTemplateCatalog.d.ts +26 -0
- package/dist/composables/useAutoGroup.d.ts +61 -74
- package/dist/composables/useAutoGroupInputSources.d.ts +32 -0
- package/dist/composables/useBioTemplateControls.d.ts +1 -1
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +1 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +1 -1
- package/dist/composables/useControlSchema.d.ts +8 -346
- package/dist/composables/useControlWorkspace.d.ts +5 -0
- package/dist/composables/useForm.d.ts +2 -33
- package/dist/composables/useFormBuilder.d.ts +2 -9
- package/dist/composables/useFormValidation.d.ts +34 -0
- package/dist/composables/usePluginClient.d.ts +1 -4
- package/dist/composables/useProtocolTemplates.d.ts +2 -24
- package/dist/composables/useScheduleCalendarLayout.d.ts +49 -0
- package/dist/{composables-Bg7CFuNz.js → composables-Dlg8jenH.js} +33 -31
- package/dist/composables-Dlg8jenH.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/install.js +2 -2
- package/dist/styles.css +547 -516
- package/dist/templates/adapters.d.ts +14 -47
- package/dist/templates/assayLookups.d.ts +3 -0
- package/dist/templates/assayMatrixAdapters.d.ts +6 -0
- package/dist/templates/assayMatrixBuilder.d.ts +2 -0
- package/dist/templates/assayNormalizers.d.ts +4 -0
- package/dist/templates/builderDefaults.d.ts +1 -0
- package/dist/templates/builderIdUtils.d.ts +4 -0
- package/dist/templates/builderPresetControls.d.ts +10 -0
- package/dist/templates/builderReadUtils.d.ts +8 -0
- package/dist/templates/builders.d.ts +26 -67
- package/dist/templates/calibrationCurveAdapters.d.ts +4 -0
- package/dist/templates/calibrationCurveBuilder.d.ts +2 -0
- package/dist/templates/calibrationNormalizers.d.ts +5 -0
- package/dist/templates/componentBindingCatalog.d.ts +25 -0
- package/dist/templates/componentBindingHelpers.d.ts +17 -0
- package/dist/templates/componentDoseResponseProps.d.ts +3 -0
- package/dist/templates/componentGenericProps.d.ts +8 -0
- package/dist/templates/componentPlateHelpers.d.ts +5 -0
- package/dist/templates/componentPlateMapProps.d.ts +3 -0
- package/dist/templates/componentPropsFactory.d.ts +3 -0
- package/dist/templates/componentQpcrPlateProps.d.ts +3 -0
- package/dist/templates/componentTargetResolvers.d.ts +5 -0
- package/dist/templates/componentTemplateProps.d.ts +3 -0
- package/dist/templates/controlSchemaClone.d.ts +4 -0
- package/dist/templates/controlSchemaConstants.d.ts +10 -0
- package/dist/templates/controlSchemaMerge.d.ts +3 -0
- package/dist/templates/controlSchemaTargets.d.ts +17 -0
- package/dist/templates/controlSchemaTypes.d.ts +4 -0
- package/dist/templates/controlSchemas.d.ts +4 -4
- package/dist/templates/defaultBioTemplateBuilder.d.ts +2 -0
- package/dist/templates/doseResponseAdapters.d.ts +4 -0
- package/dist/templates/doseResponseBuilder.d.ts +2 -0
- package/dist/templates/elisaAssayCollectionBuilder.d.ts +2 -0
- package/dist/templates/flowCytometryAssayCollectionBuilder.d.ts +2 -0
- package/dist/templates/flowCytometryPanelBuilder.d.ts +2 -0
- package/dist/templates/flowNormalizers.d.ts +8 -0
- package/dist/templates/flowPanelAdapters.d.ts +4 -0
- package/dist/templates/index.js +1 -1
- package/dist/templates/instrumentRunAdapterHelpers.d.ts +8 -0
- package/dist/templates/instrumentRunAdapters.d.ts +8 -0
- package/dist/templates/instrumentRunBuilder.d.ts +2 -0
- package/dist/templates/lcmsBatchCollectionBuilder.d.ts +2 -0
- package/dist/templates/plateGeometry.d.ts +4 -0
- package/dist/templates/plateMapAdapters.d.ts +3 -0
- package/dist/templates/plateMapBuilder.d.ts +2 -0
- package/dist/templates/presetControlSchemas.d.ts +534 -0
- package/dist/templates/protocolAdapters.d.ts +5 -0
- package/dist/templates/protocolNormalizers.d.ts +6 -0
- package/dist/templates/protocolStepsBuilder.d.ts +2 -0
- package/dist/templates/qpcrAdapters.d.ts +5 -0
- package/dist/templates/qpcrExpressionCollectionBuilder.d.ts +2 -0
- package/dist/templates/qpcrPlateBuilder.d.ts +2 -0
- package/dist/templates/reagentAdapters.d.ts +5 -0
- package/dist/templates/reagentListBuilder.d.ts +2 -0
- package/dist/templates/runNormalizers.d.ts +10 -0
- package/dist/templates/sampleNormalizers.d.ts +4 -0
- package/dist/templates/samplePrepAdapters.d.ts +4 -0
- package/dist/templates/samplePrepBuilder.d.ts +2 -0
- package/dist/templates/sampleSheetAdapters.d.ts +5 -0
- package/dist/templates/sampleSheetBuilder.d.ts +2 -0
- package/dist/templates/targetedMetabolomicsCollectionBuilder.d.ts +2 -0
- package/dist/templates/targetedMetabolomicsHelpers.d.ts +5 -0
- package/dist/templates/templateAdapterTypes.d.ts +48 -0
- package/dist/templates/templateControlSchemas.d.ts +400 -0
- package/dist/templates/templateCreateOptions.d.ts +165 -0
- package/dist/templates/templateEnvelopes.d.ts +9 -0
- package/dist/templates/templatePackCollectionBuilder.d.ts +2 -0
- package/dist/templates/templatePresetCollectionBuilder.d.ts +18 -0
- package/dist/templates/templateQpcrTypes.d.ts +42 -0
- package/dist/templates/templateValidators.d.ts +13 -0
- package/dist/templates/timeCourseAdapters.d.ts +5 -0
- package/dist/templates/timeCourseBuilder.d.ts +2 -0
- package/dist/templates/types.d.ts +5 -250
- package/dist/templates/wellPlateScreenCollectionBuilder.d.ts +2 -0
- package/dist/templates/westernBlotAssayCollectionBuilder.d.ts +2 -0
- package/dist/{templates-BorLR_7p.js → templates-DtdUvJ4c.js} +3565 -3411
- package/dist/templates-DtdUvJ4c.js.map +1 -0
- package/dist/types/auto-group.d.ts +79 -9
- package/dist/types/componentLabTypes.d.ts +161 -0
- package/dist/types/componentWorkflowTypes.d.ts +150 -0
- package/dist/types/components.d.ts +2 -311
- package/dist/{useProtocolTemplates-n6AJqSqv.js → useProtocolTemplates-Bm5vyH4_.js} +1220 -454
- package/dist/useProtocolTemplates-Bm5vyH4_.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.navigation.test.ts +70 -0
- package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +53 -0
- package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +54 -0
- package/src/__tests__/components/PluginWorkspaceView.controls.test.ts +156 -0
- package/src/__tests__/components/PluginWorkspaceView.navigation.test.ts +102 -0
- package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +41 -0
- package/src/__tests__/components/ProtocolStep.presentation.test.ts +31 -0
- package/src/__tests__/components/ProtocolStepEditor.state.test.ts +165 -0
- package/src/__tests__/components/ProtocolStepParameterField.test.ts +44 -0
- package/src/__tests__/components/ReagentList.presentation.test.ts +68 -0
- package/src/__tests__/components/SampleSelector.colors.test.ts +49 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +100 -0
- package/src/__tests__/components/SampleSelector.groups.test.ts +81 -0
- package/src/__tests__/components/SampleSelector.selection.test.ts +70 -0
- package/src/__tests__/components/SampleSelector.test.ts +32 -0
- package/src/__tests__/components/SampleSelectorSampleRow.test.ts +37 -0
- package/src/__tests__/components/ScheduleCalendar.test.ts +44 -0
- package/src/__tests__/components/SettingsModal.schema.test.ts +97 -0
- package/src/__tests__/components/WellPlate.colors.test.ts +28 -0
- package/src/__tests__/components/WellPlate.conditions.test.ts +68 -0
- package/src/__tests__/components/WellPlate.geometry.test.ts +54 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +171 -0
- package/src/__tests__/components/WellPlate.legend.test.ts +13 -0
- package/src/__tests__/components/WellPlate.rendering.test.ts +122 -0
- package/src/__tests__/components/WellPlate.sampleDrop.test.ts +70 -0
- package/src/__tests__/composables/autoGroup/classify.test.ts +107 -0
- package/src/__tests__/composables/autoGroup/columns.test.ts +135 -0
- package/src/__tests__/composables/autoGroup/compose.test.ts +227 -0
- package/src/__tests__/composables/autoGroup/cooccurrence.test.ts +91 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +50 -0
- package/src/__tests__/composables/autoGroup/integration.test.ts +79 -0
- package/src/__tests__/composables/autoGroup/template.test.ts +70 -0
- package/src/__tests__/composables/autoGroup/tokenize.test.ts +33 -0
- package/src/__tests__/composables/useAutoGroup.test.ts +129 -625
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +107 -0
- package/src/__tests__/composables/useControlSchema.test.ts +23 -0
- package/src/__tests__/composables/useScheduleCalendarLayout.test.ts +89 -0
- package/src/__tests__/docs/extractDocsComponents.test.ts +142 -0
- package/src/__tests__/docs/extractDocsExports.test.ts +77 -0
- package/src/__tests__/docs/extractDocsParsing.test.ts +69 -0
- package/src/__tests__/docs/extractDocsTemplates.test.ts +54 -0
- package/src/__tests__/docs/extractDocsTheme.test.ts +89 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -1
- package/src/__tests__/fixtures/auto-group/mixed-lc-ms-batch.txt +187 -0
- package/src/components/AppSidebar.vue +2 -6
- package/src/components/AppTopBar.navigation.ts +62 -0
- package/src/components/AppTopBar.vue +17 -44
- package/src/components/AutoGroupModal.story.vue +50 -0
- package/src/components/AutoGroupModal.vue +441 -158
- package/src/components/ControlWorkspaceView.vue +2 -6
- package/src/components/DoseCalculator.vue +13 -73
- package/src/components/DoseCalculatorVolumeField.vue +61 -0
- package/src/components/ExperimentTimeline.vue +6 -31
- package/src/components/FormBuilder.vue +2 -7
- package/src/components/PlateMapEditor.vue +32 -106
- package/src/components/PluginWorkspaceView.controls.ts +182 -0
- package/src/components/PluginWorkspaceView.navigation.ts +106 -0
- package/src/components/PluginWorkspaceView.props.ts +174 -0
- package/src/components/PluginWorkspaceView.shell.ts +66 -0
- package/src/components/PluginWorkspaceView.vue +85 -404
- package/src/components/ProtocolStep.presentation.ts +31 -0
- package/src/components/ProtocolStepEditor.state.ts +104 -0
- package/src/components/ProtocolStepEditor.vue +48 -179
- package/src/components/ProtocolStepParameterField.vue +134 -0
- package/src/components/ReagentList.presentation.ts +105 -0
- package/src/components/ReagentList.vue +16 -79
- package/src/components/SampleSelector.colors.ts +43 -0
- package/src/components/SampleSelector.drag.ts +164 -0
- package/src/components/SampleSelector.groups.ts +109 -0
- package/src/components/SampleSelector.selection.ts +103 -0
- package/src/components/SampleSelector.vue +82 -349
- package/src/components/SampleSelectorSampleRow.vue +64 -0
- package/src/components/ScheduleCalendar.vue +44 -199
- package/src/components/SettingsModal.schema.ts +71 -0
- package/src/components/SettingsModal.vue +16 -46
- package/src/components/WellPlate.colors.ts +56 -0
- package/src/components/WellPlate.conditions.ts +100 -0
- package/src/components/WellPlate.geometry.ts +91 -0
- package/src/components/WellPlate.interaction.ts +272 -0
- package/src/components/WellPlate.legend.ts +8 -0
- package/src/components/WellPlate.rendering.ts +105 -0
- package/src/components/WellPlate.sampleDrop.ts +73 -0
- package/src/components/WellPlate.vue +102 -550
- package/src/components/internal/PlateMapEditorToolbarInternal.vue +128 -0
- package/src/composables/autoGroup/classKey.ts +5 -0
- package/src/composables/autoGroup/classify.ts +205 -0
- package/src/composables/autoGroup/colors.ts +6 -0
- package/src/composables/autoGroup/columns.ts +226 -0
- package/src/composables/autoGroup/compose.ts +156 -0
- package/src/composables/autoGroup/cooccurrence.ts +46 -0
- package/src/composables/autoGroup/csv-shim.ts +44 -0
- package/src/composables/autoGroup/fingerprint.ts +49 -0
- package/src/composables/autoGroup/index.ts +20 -0
- package/src/composables/autoGroup/replicatePreGroup.ts +90 -0
- package/src/composables/autoGroup/template.ts +126 -0
- package/src/composables/autoGroup/tokenize.ts +41 -0
- package/src/composables/autoGroup/vocab.json +67 -0
- package/src/composables/autoGroupConstants.ts +4 -0
- package/src/composables/autoGroupGrouping.ts +148 -0
- package/src/composables/controlComponentBindings.ts +80 -0
- package/src/composables/controlSchemaAdapters.ts +196 -0
- package/src/composables/controlSchemaDoseDesign.ts +215 -0
- package/src/composables/controlSchemaFormFields.ts +61 -0
- package/src/composables/controlSchemaLayout.ts +59 -0
- package/src/composables/controlSchemaModel.ts +163 -0
- package/src/composables/controlSchemaNormalize.ts +101 -0
- package/src/composables/controlSchemaTypes.ts +364 -0
- package/src/composables/controlSchemaUtils.ts +36 -0
- package/src/composables/controlWorkspaceOptions.ts +21 -0
- package/src/composables/formBuilderSchema.ts +153 -0
- package/src/composables/pluginEndpointBuilder.ts +203 -0
- package/src/composables/protocolTemplateCatalog.ts +325 -0
- package/src/composables/useAutoGroup.ts +395 -549
- package/src/composables/useAutoGroupInputSources.ts +147 -0
- package/src/composables/useBioTemplateControls.ts +1 -1
- package/src/composables/useBioTemplatePresetWorkspace.ts +1 -1
- package/src/composables/useBioTemplateWorkspace.ts +1 -1
- package/src/composables/useControlSchema.ts +64 -1312
- package/src/composables/useControlWorkspace.ts +201 -0
- package/src/composables/useForm.ts +5 -187
- package/src/composables/useFormBuilder.ts +11 -153
- package/src/composables/useFormValidation.ts +154 -0
- package/src/composables/usePluginClient.ts +10 -193
- package/src/composables/useProtocolTemplates.ts +10 -328
- package/src/composables/useScheduleCalendarLayout.ts +287 -0
- package/src/styles/components/auto-group-modal.css +248 -310
- package/src/templates/adapters.ts +89 -930
- package/src/templates/assayLookups.ts +33 -0
- package/src/templates/assayMatrixAdapters.ts +78 -0
- package/src/templates/assayMatrixBuilder.ts +59 -0
- package/src/templates/assayNormalizers.ts +34 -0
- package/src/templates/builderDefaults.ts +11 -0
- package/src/templates/builderIdUtils.ts +20 -0
- package/src/templates/builderPresetControls.ts +165 -0
- package/src/templates/builderReadUtils.ts +57 -0
- package/src/templates/builders.ts +122 -2350
- package/src/templates/calibrationCurveAdapters.ts +59 -0
- package/src/templates/calibrationCurveBuilder.ts +99 -0
- package/src/templates/calibrationNormalizers.ts +60 -0
- package/src/templates/componentBindingCatalog.ts +90 -0
- package/src/templates/componentBindingHelpers.ts +93 -0
- package/src/templates/componentBindings.ts +12 -461
- package/src/templates/componentDoseResponseProps.ts +42 -0
- package/src/templates/componentGenericProps.ts +77 -0
- package/src/templates/componentPlateHelpers.ts +29 -0
- package/src/templates/componentPlateMapProps.ts +32 -0
- package/src/templates/componentPropsFactory.ts +21 -0
- package/src/templates/componentQpcrPlateProps.ts +28 -0
- package/src/templates/componentTargetResolvers.ts +69 -0
- package/src/templates/componentTemplateProps.ts +78 -0
- package/src/templates/controlSchemaClone.ts +32 -0
- package/src/templates/controlSchemaConstants.ts +11 -0
- package/src/templates/controlSchemaMerge.ts +40 -0
- package/src/templates/controlSchemaTargets.ts +87 -0
- package/src/templates/controlSchemaTypes.ts +20 -0
- package/src/templates/controlSchemas.ts +22 -704
- package/src/templates/defaultBioTemplateBuilder.ts +124 -0
- package/src/templates/doseResponseAdapters.ts +45 -0
- package/src/templates/doseResponseBuilder.ts +44 -0
- package/src/templates/elisaAssayCollectionBuilder.ts +62 -0
- package/src/templates/flowCytometryAssayCollectionBuilder.ts +41 -0
- package/src/templates/flowCytometryPanelBuilder.ts +53 -0
- package/src/templates/flowNormalizers.ts +58 -0
- package/src/templates/flowPanelAdapters.ts +58 -0
- package/src/templates/instrumentRunAdapterHelpers.ts +94 -0
- package/src/templates/instrumentRunAdapters.ts +163 -0
- package/src/templates/instrumentRunBuilder.ts +97 -0
- package/src/templates/lcmsBatchCollectionBuilder.ts +38 -0
- package/src/templates/plateGeometry.ts +62 -0
- package/src/templates/plateMapAdapters.ts +36 -0
- package/src/templates/plateMapBuilder.ts +43 -0
- package/src/templates/presetControlSchemas.ts +258 -0
- package/src/templates/protocolAdapters.ts +69 -0
- package/src/templates/protocolNormalizers.ts +37 -0
- package/src/templates/protocolStepsBuilder.ts +36 -0
- package/src/templates/qpcrAdapters.ts +104 -0
- package/src/templates/qpcrExpressionCollectionBuilder.ts +33 -0
- package/src/templates/qpcrPlateBuilder.ts +96 -0
- package/src/templates/reagentAdapters.ts +77 -0
- package/src/templates/reagentListBuilder.ts +30 -0
- package/src/templates/runNormalizers.ts +63 -0
- package/src/templates/sampleNormalizers.ts +58 -0
- package/src/templates/samplePrepAdapters.ts +63 -0
- package/src/templates/samplePrepBuilder.ts +51 -0
- package/src/templates/sampleSheetAdapters.ts +75 -0
- package/src/templates/sampleSheetBuilder.ts +23 -0
- package/src/templates/targetedMetabolomicsCollectionBuilder.ts +79 -0
- package/src/templates/targetedMetabolomicsHelpers.ts +102 -0
- package/src/templates/templateAdapterTypes.ts +58 -0
- package/src/templates/templateControlSchemas.ts +320 -0
- package/src/templates/templateCreateOptions.ts +208 -0
- package/src/templates/templateEnvelopes.ts +137 -0
- package/src/templates/templatePackCollectionBuilder.ts +23 -0
- package/src/templates/templatePresetCollectionBuilder.ts +139 -0
- package/src/templates/templateQpcrTypes.ts +48 -0
- package/src/templates/templateValidators.ts +414 -0
- package/src/templates/timeCourseAdapters.ts +73 -0
- package/src/templates/timeCourseBuilder.ts +64 -0
- package/src/templates/types.ts +79 -275
- package/src/templates/wellPlateScreenCollectionBuilder.ts +36 -0
- package/src/templates/westernBlotAssayCollectionBuilder.ts +68 -0
- package/src/types/auto-group.ts +107 -9
- package/src/types/componentLabTypes.ts +235 -0
- package/src/types/componentWorkflowTypes.ts +190 -0
- package/src/types/components.ts +74 -424
- package/dist/components-BhK-dW99.js.map +0 -1
- package/dist/composables-Bg7CFuNz.js.map +0 -1
- package/dist/templates-BorLR_7p.js.map +0 -1
- package/dist/useProtocolTemplates-n6AJqSqv.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as toBioTemplateComponentProps, F as getBioTemplateComponentProps$1, H as toBioTemplateComponentPropsById, Hn as useConcentrationUnits, Jt as getBioTemplatePresetInfo, L as toBioTemplateComponentBindings, P as getBioTemplateComponentBindings, Qt as getBioTemplatePackInfo, R as toBioTemplateComponentBindingsById, Rn as getFieldRegistryEntry, Sn as useControlWorkspace, U as toBioTemplateComponentSnippets, V as toBioTemplateComponentPropsByComponent$1, W as toBioTemplateComponentUsage, d as createBioTemplatePresetCollectionFromControls, dn as extractTemplateCollection, i as createBioTemplateControlToolkit, p as createBioTemplatePackCollection, sn as createTemplateCollection, u as createBioTemplatePresetCollection, un as ensureTemplateFromCollection, z as toBioTemplateComponentImports, zn as getTypeDefault } from "./templates-DtdUvJ4c.js";
|
|
2
2
|
import { g as useSettingsStore, t as useAuthStore } from "./auth-B7g4J4ZF.js";
|
|
3
3
|
import { computed, effectScope, getCurrentScope, inject, onMounted, onScopeDispose, onUnmounted, provide, reactive, readonly, ref, shallowRef, toRaw, toValue, watch } from "vue";
|
|
4
4
|
import axios from "axios";
|
|
@@ -144,7 +144,7 @@ function useTheme() {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
//#endregion
|
|
147
|
-
//#region src/composables/
|
|
147
|
+
//#region src/composables/useFormValidation.ts
|
|
148
148
|
var validators = {
|
|
149
149
|
required: (value, message = "This field is required") => {
|
|
150
150
|
if (value === null || value === void 0 || value === "") return message;
|
|
@@ -182,6 +182,70 @@ var validators = {
|
|
|
182
182
|
return null;
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
|
+
function validateFieldValue(value, fieldRules, formData) {
|
|
186
|
+
if (!fieldRules) return null;
|
|
187
|
+
if (fieldRules.required) {
|
|
188
|
+
const message = typeof fieldRules.required === "string" ? fieldRules.required : void 0;
|
|
189
|
+
const error = validators.required(value, message);
|
|
190
|
+
if (error) return error;
|
|
191
|
+
}
|
|
192
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
193
|
+
if (fieldRules.minLength !== void 0) {
|
|
194
|
+
const config = typeof fieldRules.minLength === "number" ? {
|
|
195
|
+
value: fieldRules.minLength,
|
|
196
|
+
message: void 0
|
|
197
|
+
} : fieldRules.minLength;
|
|
198
|
+
const error = validators.minLength(value, config.value, config.message);
|
|
199
|
+
if (error) return error;
|
|
200
|
+
}
|
|
201
|
+
if (fieldRules.maxLength !== void 0) {
|
|
202
|
+
const config = typeof fieldRules.maxLength === "number" ? {
|
|
203
|
+
value: fieldRules.maxLength,
|
|
204
|
+
message: void 0
|
|
205
|
+
} : fieldRules.maxLength;
|
|
206
|
+
const error = validators.maxLength(value, config.value, config.message);
|
|
207
|
+
if (error) return error;
|
|
208
|
+
}
|
|
209
|
+
if (fieldRules.min !== void 0) {
|
|
210
|
+
const config = typeof fieldRules.min === "number" ? {
|
|
211
|
+
value: fieldRules.min,
|
|
212
|
+
message: void 0
|
|
213
|
+
} : fieldRules.min;
|
|
214
|
+
const error = validators.min(value, config.value, config.message);
|
|
215
|
+
if (error) return error;
|
|
216
|
+
}
|
|
217
|
+
if (fieldRules.max !== void 0) {
|
|
218
|
+
const config = typeof fieldRules.max === "number" ? {
|
|
219
|
+
value: fieldRules.max,
|
|
220
|
+
message: void 0
|
|
221
|
+
} : fieldRules.max;
|
|
222
|
+
const error = validators.max(value, config.value, config.message);
|
|
223
|
+
if (error) return error;
|
|
224
|
+
}
|
|
225
|
+
if (fieldRules.pattern !== void 0) {
|
|
226
|
+
const config = fieldRules.pattern instanceof RegExp ? {
|
|
227
|
+
value: fieldRules.pattern,
|
|
228
|
+
message: void 0
|
|
229
|
+
} : fieldRules.pattern;
|
|
230
|
+
const error = validators.pattern(value, config.value, config.message);
|
|
231
|
+
if (error) return error;
|
|
232
|
+
}
|
|
233
|
+
if (fieldRules.email) {
|
|
234
|
+
const message = typeof fieldRules.email === "string" ? fieldRules.email : void 0;
|
|
235
|
+
const error = validators.email(value, message);
|
|
236
|
+
if (error) return error;
|
|
237
|
+
}
|
|
238
|
+
if (fieldRules.custom) {
|
|
239
|
+
const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom];
|
|
240
|
+
for (const rule of customRules) {
|
|
241
|
+
const error = rule(value, formData);
|
|
242
|
+
if (error) return error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/composables/useForm.ts
|
|
185
249
|
/**
|
|
186
250
|
* Form state management composable with validation.
|
|
187
251
|
*
|
|
@@ -228,97 +292,9 @@ function useForm(initialValues, rules = {}) {
|
|
|
228
292
|
function validateField(field) {
|
|
229
293
|
const value = data[field];
|
|
230
294
|
const fieldRules = rules[field];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
if (fieldRules.required) {
|
|
236
|
-
const message = typeof fieldRules.required === "string" ? fieldRules.required : void 0;
|
|
237
|
-
const error = validators.required(value, message);
|
|
238
|
-
if (error) {
|
|
239
|
-
errors[field] = error;
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (value === null || value === void 0 || value === "") {
|
|
244
|
-
errors[field] = null;
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
if (fieldRules.minLength !== void 0) {
|
|
248
|
-
const config = typeof fieldRules.minLength === "number" ? {
|
|
249
|
-
value: fieldRules.minLength,
|
|
250
|
-
message: void 0
|
|
251
|
-
} : fieldRules.minLength;
|
|
252
|
-
const error = validators.minLength(value, config.value, config.message);
|
|
253
|
-
if (error) {
|
|
254
|
-
errors[field] = error;
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (fieldRules.maxLength !== void 0) {
|
|
259
|
-
const config = typeof fieldRules.maxLength === "number" ? {
|
|
260
|
-
value: fieldRules.maxLength,
|
|
261
|
-
message: void 0
|
|
262
|
-
} : fieldRules.maxLength;
|
|
263
|
-
const error = validators.maxLength(value, config.value, config.message);
|
|
264
|
-
if (error) {
|
|
265
|
-
errors[field] = error;
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (fieldRules.min !== void 0) {
|
|
270
|
-
const config = typeof fieldRules.min === "number" ? {
|
|
271
|
-
value: fieldRules.min,
|
|
272
|
-
message: void 0
|
|
273
|
-
} : fieldRules.min;
|
|
274
|
-
const error = validators.min(value, config.value, config.message);
|
|
275
|
-
if (error) {
|
|
276
|
-
errors[field] = error;
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (fieldRules.max !== void 0) {
|
|
281
|
-
const config = typeof fieldRules.max === "number" ? {
|
|
282
|
-
value: fieldRules.max,
|
|
283
|
-
message: void 0
|
|
284
|
-
} : fieldRules.max;
|
|
285
|
-
const error = validators.max(value, config.value, config.message);
|
|
286
|
-
if (error) {
|
|
287
|
-
errors[field] = error;
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (fieldRules.pattern !== void 0) {
|
|
292
|
-
const config = fieldRules.pattern instanceof RegExp ? {
|
|
293
|
-
value: fieldRules.pattern,
|
|
294
|
-
message: void 0
|
|
295
|
-
} : fieldRules.pattern;
|
|
296
|
-
const error = validators.pattern(value, config.value, config.message);
|
|
297
|
-
if (error) {
|
|
298
|
-
errors[field] = error;
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (fieldRules.email) {
|
|
303
|
-
const message = typeof fieldRules.email === "string" ? fieldRules.email : void 0;
|
|
304
|
-
const error = validators.email(value, message);
|
|
305
|
-
if (error) {
|
|
306
|
-
errors[field] = error;
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (fieldRules.custom) {
|
|
311
|
-
const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom];
|
|
312
|
-
for (const rule of customRules) {
|
|
313
|
-
const error = rule(value, data);
|
|
314
|
-
if (error) {
|
|
315
|
-
errors[field] = error;
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
errors[field] = null;
|
|
321
|
-
return true;
|
|
295
|
+
const error = validateFieldValue(value, fieldRules, data);
|
|
296
|
+
errors[field] = error;
|
|
297
|
+
return error === null;
|
|
322
298
|
}
|
|
323
299
|
function validate() {
|
|
324
300
|
let isAllValid = true;
|
|
@@ -422,7 +398,7 @@ function isPlainRecord(value) {
|
|
|
422
398
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
423
399
|
}
|
|
424
400
|
//#endregion
|
|
425
|
-
//#region src/composables/
|
|
401
|
+
//#region src/composables/formBuilderSchema.ts
|
|
426
402
|
/**
|
|
427
403
|
* Evaluate a JSON-serializable field condition against the current form data.
|
|
428
404
|
*
|
|
@@ -510,6 +486,8 @@ function replaceRecord(target, source) {
|
|
|
510
486
|
function hasOwnKey(source, key) {
|
|
511
487
|
return Object.prototype.hasOwnProperty.call(source, key);
|
|
512
488
|
}
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/composables/useFormBuilder.ts
|
|
513
491
|
/**
|
|
514
492
|
* Drive a `FormSchema` as reactive form state.
|
|
515
493
|
*
|
|
@@ -2098,6 +2076,788 @@ function useWellPlateEditor(initialState, options = {}) {
|
|
|
2098
2076
|
};
|
|
2099
2077
|
}
|
|
2100
2078
|
//#endregion
|
|
2079
|
+
//#region src/composables/autoGroup/tokenize.ts
|
|
2080
|
+
var DELIMITER_CANDIDATES = [
|
|
2081
|
+
"_",
|
|
2082
|
+
"-",
|
|
2083
|
+
"."
|
|
2084
|
+
];
|
|
2085
|
+
/** Pick the delimiter whose split produces the most consistent field count across `lines`. */
|
|
2086
|
+
function pickPrimaryDelimiter(lines) {
|
|
2087
|
+
if (lines.length === 0) return "_";
|
|
2088
|
+
let best = "_";
|
|
2089
|
+
let bestScore = -1;
|
|
2090
|
+
for (const candidate of DELIMITER_CANDIDATES) {
|
|
2091
|
+
const counts = lines.map((l) => l.split(candidate).length);
|
|
2092
|
+
const frequency = /* @__PURE__ */ new Map();
|
|
2093
|
+
for (const c of counts) frequency.set(c, (frequency.get(c) ?? 0) + 1);
|
|
2094
|
+
let mode = 1;
|
|
2095
|
+
let modeFreq = 0;
|
|
2096
|
+
for (const [c, f] of frequency) if (f > modeFreq || f === modeFreq && c > mode) {
|
|
2097
|
+
mode = c;
|
|
2098
|
+
modeFreq = f;
|
|
2099
|
+
}
|
|
2100
|
+
const score = mode > 1 ? modeFreq / lines.length : 0;
|
|
2101
|
+
if (score > bestScore) {
|
|
2102
|
+
best = candidate;
|
|
2103
|
+
bestScore = score;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return best;
|
|
2107
|
+
}
|
|
2108
|
+
/** Split a single sample name on `delimiter`. Empty input returns `['']` so callers
|
|
2109
|
+
* always receive at least one token. */
|
|
2110
|
+
function splitMulti(name, delimiter) {
|
|
2111
|
+
return name.split(delimiter);
|
|
2112
|
+
}
|
|
2113
|
+
//#endregion
|
|
2114
|
+
//#region src/composables/autoGroup/classKey.ts
|
|
2115
|
+
function classKey(c) {
|
|
2116
|
+
return c.subKind ? `${c.kind}:${c.subKind}` : c.kind;
|
|
2117
|
+
}
|
|
2118
|
+
var vocab_default = {
|
|
2119
|
+
$schema: "./vocab.schema.json",
|
|
2120
|
+
_doc: {
|
|
2121
|
+
"purpose": "Sample-name classification vocabulary for the auto-group pipeline. Edit this file to teach the classifier new sample types or matrices without changing TypeScript. All matching is case-insensitive unless a pattern's regex flags say otherwise.",
|
|
2122
|
+
"patterns": "Regex source strings (no surrounding slashes). Compiled with the `i` flag at load time.",
|
|
2123
|
+
"standardAllow": "Tokens that should be classified as Standard even when they don't match `patterns.standard`. Case-sensitive — use the exact spelling that appears in your filenames.",
|
|
2124
|
+
"matrixVocab": "Lowercase token → canonical display name. Tokens are normalised to lowercase before lookup, so adding 'tissues' handles 'Tissues' / 'TISSUES' / 'tissues' uniformly. Multiple aliases map to one canonical (e.g. cell + cells → Cells).",
|
|
2125
|
+
"ionizationModes": "Lowercase polarity tokens (pos / positive / neg / negative). Suffixed to the subKind so POS and NEG acquisitions get independent schemas.",
|
|
2126
|
+
"tissueParentTokens": "Tokens that flag the immediately-following token as the organ subKind (so `tissues / kidney` becomes Biological/Tissues, tagging both positions).",
|
|
2127
|
+
"replicateStripPatterns": "Regex sources stripped from each sample name during the replicate pre-grouping pass. Samples whose stripped (base) names match are treated as replicates of one another and grouped together before tokenisation. Default set covers: trailing run-order numbers, _T<n>, _B<n>, and _Rep<n> markers."
|
|
2128
|
+
},
|
|
2129
|
+
patterns: {
|
|
2130
|
+
"blank": "^(blank|blk|solv?blank|matrix.?blank)$",
|
|
2131
|
+
"iqc": "^iqc$",
|
|
2132
|
+
"eqc": "^eqc$",
|
|
2133
|
+
"standard": "^(standard|std\\d*|cal(?:ibrator)?\\d*|.+standard)$",
|
|
2134
|
+
"spikeIn": "spike.?in",
|
|
2135
|
+
"stock": "^stock$"
|
|
2136
|
+
},
|
|
2137
|
+
standardAllow: [
|
|
2138
|
+
"ExtractionS",
|
|
2139
|
+
"IS",
|
|
2140
|
+
"internal.standard",
|
|
2141
|
+
"PQC",
|
|
2142
|
+
"pooledQC"
|
|
2143
|
+
],
|
|
2144
|
+
matrixVocab: {
|
|
2145
|
+
"plasma": "Plasma",
|
|
2146
|
+
"serum": "Serum",
|
|
2147
|
+
"tissue": "Tissue",
|
|
2148
|
+
"tissues": "Tissues",
|
|
2149
|
+
"kidney": "Kidney",
|
|
2150
|
+
"liver": "Liver",
|
|
2151
|
+
"tumor": "Tumor",
|
|
2152
|
+
"urine": "Urine",
|
|
2153
|
+
"brain": "Brain",
|
|
2154
|
+
"muscle": "Muscle",
|
|
2155
|
+
"adipose": "Adipose",
|
|
2156
|
+
"csf": "CSF",
|
|
2157
|
+
"feces": "Feces",
|
|
2158
|
+
"faeces": "Faeces",
|
|
2159
|
+
"cells": "Cells",
|
|
2160
|
+
"cell": "Cells",
|
|
2161
|
+
"media": "Media",
|
|
2162
|
+
"medium": "Medium",
|
|
2163
|
+
"extract": "Extract"
|
|
2164
|
+
},
|
|
2165
|
+
ionizationModes: {
|
|
2166
|
+
"pos": "POS",
|
|
2167
|
+
"positive": "POS",
|
|
2168
|
+
"neg": "NEG",
|
|
2169
|
+
"negative": "NEG"
|
|
2170
|
+
},
|
|
2171
|
+
tissueParentTokens: ["tissue", "tissues"],
|
|
2172
|
+
replicateStripPatterns: [
|
|
2173
|
+
"[_-]\\d{2,4}[A-Za-z]?$",
|
|
2174
|
+
"[_-]T\\d+(?=[_-]|$)",
|
|
2175
|
+
"[_-]B\\d+(?=[_-]|$)",
|
|
2176
|
+
"[_-](?:rep(?:licate)?)\\d+(?=[_-]|$)"
|
|
2177
|
+
]
|
|
2178
|
+
};
|
|
2179
|
+
//#endregion
|
|
2180
|
+
//#region src/composables/autoGroup/classify.ts
|
|
2181
|
+
function compileVocab(data) {
|
|
2182
|
+
return {
|
|
2183
|
+
blankPattern: new RegExp(data.patterns.blank, "i"),
|
|
2184
|
+
iqcPattern: new RegExp(data.patterns.iqc, "i"),
|
|
2185
|
+
eqcPattern: new RegExp(data.patterns.eqc, "i"),
|
|
2186
|
+
standardPattern: new RegExp(data.patterns.standard, "i"),
|
|
2187
|
+
spikeInPattern: new RegExp(data.patterns.spikeIn, "i"),
|
|
2188
|
+
stockPattern: new RegExp(data.patterns.stock, "i"),
|
|
2189
|
+
standardAllow: new Set(data.standardAllow),
|
|
2190
|
+
matrixVocab: new Map(Object.entries(data.matrixVocab)),
|
|
2191
|
+
ionizationModes: new Map(Object.entries(data.ionizationModes).map(([k, v]) => [k, v])),
|
|
2192
|
+
tissueParentTokens: new Set(data.tissueParentTokens)
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
var classVocab = compileVocab(vocab_default);
|
|
2196
|
+
function matrixCanonical(token, vocab) {
|
|
2197
|
+
return vocab.matrixVocab.get(token.toLowerCase());
|
|
2198
|
+
}
|
|
2199
|
+
function isTissueParentToken(token, vocab) {
|
|
2200
|
+
return vocab.tissueParentTokens.has(token.toLowerCase());
|
|
2201
|
+
}
|
|
2202
|
+
var DEFAULT_DISPOSITION = {
|
|
2203
|
+
biological: "group",
|
|
2204
|
+
iqc: "overlay",
|
|
2205
|
+
eqc: "overlay",
|
|
2206
|
+
standard: "overlay",
|
|
2207
|
+
"spike-in": "overlay",
|
|
2208
|
+
stock: "overlay",
|
|
2209
|
+
blank: "overlay",
|
|
2210
|
+
unknown: "group"
|
|
2211
|
+
};
|
|
2212
|
+
function classifyTokens(tokens, vocab) {
|
|
2213
|
+
for (let i = 0; i < tokens.length; i++) if (vocab.blankPattern.test(tokens[i])) return {
|
|
2214
|
+
kind: "blank",
|
|
2215
|
+
tagPositions: [i]
|
|
2216
|
+
};
|
|
2217
|
+
for (let i = 0; i < tokens.length; i++) if (vocab.iqcPattern.test(tokens[i])) return {
|
|
2218
|
+
kind: "iqc",
|
|
2219
|
+
tagPositions: [i]
|
|
2220
|
+
};
|
|
2221
|
+
for (let i = 0; i < tokens.length; i++) if (vocab.eqcPattern.test(tokens[i])) return {
|
|
2222
|
+
kind: "eqc",
|
|
2223
|
+
tagPositions: [i]
|
|
2224
|
+
};
|
|
2225
|
+
for (let i = 0; i < tokens.length; i++) if (vocab.standardPattern.test(tokens[i]) || vocab.standardAllow.has(tokens[i])) return {
|
|
2226
|
+
kind: "standard",
|
|
2227
|
+
tagPositions: [i]
|
|
2228
|
+
};
|
|
2229
|
+
for (let i = 0; i < tokens.length; i++) if (vocab.spikeInPattern.test(tokens[i])) return {
|
|
2230
|
+
kind: "spike-in",
|
|
2231
|
+
tagPositions: [i]
|
|
2232
|
+
};
|
|
2233
|
+
for (let i = 0; i < tokens.length; i++) if (vocab.stockPattern.test(tokens[i])) return {
|
|
2234
|
+
kind: "stock",
|
|
2235
|
+
tagPositions: [i]
|
|
2236
|
+
};
|
|
2237
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2238
|
+
const canonical = matrixCanonical(tokens[i], vocab);
|
|
2239
|
+
if (canonical) {
|
|
2240
|
+
const isTissueParent = isTissueParentToken(tokens[i], vocab);
|
|
2241
|
+
const tagPositions = [i];
|
|
2242
|
+
if (isTissueParent) {
|
|
2243
|
+
if (i + 1 < tokens.length) tagPositions.push(i + 1);
|
|
2244
|
+
return {
|
|
2245
|
+
kind: "biological",
|
|
2246
|
+
subKind: "Tissues",
|
|
2247
|
+
tagPositions
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
if (i > 0 && isTissueParentToken(tokens[i - 1], vocab)) {
|
|
2251
|
+
tagPositions.unshift(i - 1);
|
|
2252
|
+
return {
|
|
2253
|
+
kind: "biological",
|
|
2254
|
+
subKind: "Tissues",
|
|
2255
|
+
tagPositions
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
kind: "biological",
|
|
2260
|
+
subKind: canonical,
|
|
2261
|
+
tagPositions
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return {
|
|
2266
|
+
kind: "unknown",
|
|
2267
|
+
tagPositions: []
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* LC-MS batches run positive- and negative-ion-mode acquisitions separately,
|
|
2272
|
+
* and the two produce different feature tables that should never be grouped
|
|
2273
|
+
* together. When a POS / NEG / positive / negative token is present we suffix
|
|
2274
|
+
* it to the class label so the schema builder treats each mode as its own class.
|
|
2275
|
+
*/
|
|
2276
|
+
function ionizationModeFor(tokens, vocab) {
|
|
2277
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2278
|
+
const mode = vocab.ionizationModes.get(tokens[i].toLowerCase());
|
|
2279
|
+
if (mode) return {
|
|
2280
|
+
mode,
|
|
2281
|
+
position: i
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
function labelFor(kind, subKind) {
|
|
2286
|
+
const base = {
|
|
2287
|
+
biological: "Biological",
|
|
2288
|
+
iqc: "IQC",
|
|
2289
|
+
eqc: "EQC",
|
|
2290
|
+
standard: "Standard",
|
|
2291
|
+
"spike-in": "SpikeIn",
|
|
2292
|
+
stock: "Stock",
|
|
2293
|
+
blank: "Blank",
|
|
2294
|
+
unknown: "Unknown"
|
|
2295
|
+
};
|
|
2296
|
+
return subKind ? `${base[kind]} / ${subKind}` : base[kind];
|
|
2297
|
+
}
|
|
2298
|
+
/** Classify each tokenized sample into a SampleClass. */
|
|
2299
|
+
function detectClass(tokenizedSamples, options) {
|
|
2300
|
+
const vocab = {
|
|
2301
|
+
...classVocab,
|
|
2302
|
+
...options.vocab ?? {}
|
|
2303
|
+
};
|
|
2304
|
+
const hints = options.sampleTypeHints ?? [];
|
|
2305
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2306
|
+
for (let i = 0; i < tokenizedSamples.length; i++) {
|
|
2307
|
+
const hint = hints[i]?.toLowerCase();
|
|
2308
|
+
let kind;
|
|
2309
|
+
let subKind;
|
|
2310
|
+
let tagPositions;
|
|
2311
|
+
if (hint === "qc") {
|
|
2312
|
+
kind = "iqc";
|
|
2313
|
+
tagPositions = [];
|
|
2314
|
+
} else if (hint === "blank") {
|
|
2315
|
+
kind = "blank";
|
|
2316
|
+
tagPositions = [];
|
|
2317
|
+
} else {
|
|
2318
|
+
const r = classifyTokens(tokenizedSamples[i], vocab);
|
|
2319
|
+
kind = r.kind;
|
|
2320
|
+
subKind = r.subKind;
|
|
2321
|
+
tagPositions = r.tagPositions;
|
|
2322
|
+
}
|
|
2323
|
+
const mode = ionizationModeFor(tokenizedSamples[i], vocab);
|
|
2324
|
+
if (mode) {
|
|
2325
|
+
subKind = subKind ? `${subKind} / ${mode.mode}` : mode.mode;
|
|
2326
|
+
if (!tagPositions.includes(mode.position)) tagPositions.push(mode.position);
|
|
2327
|
+
}
|
|
2328
|
+
const key = classKey({
|
|
2329
|
+
kind,
|
|
2330
|
+
subKind
|
|
2331
|
+
});
|
|
2332
|
+
const bucket = buckets.get(key);
|
|
2333
|
+
if (bucket) {
|
|
2334
|
+
bucket.members.push(i);
|
|
2335
|
+
for (const p of tagPositions) if (!bucket.classTagPositions.includes(p)) bucket.classTagPositions.push(p);
|
|
2336
|
+
} else buckets.set(key, {
|
|
2337
|
+
kind,
|
|
2338
|
+
subKind,
|
|
2339
|
+
label: labelFor(kind, subKind),
|
|
2340
|
+
members: [i],
|
|
2341
|
+
classTagPositions: [...tagPositions],
|
|
2342
|
+
disposition: DEFAULT_DISPOSITION[kind]
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
return [...buckets.values()];
|
|
2346
|
+
}
|
|
2347
|
+
//#endregion
|
|
2348
|
+
//#region src/composables/autoGroup/columns.ts
|
|
2349
|
+
var NUMERIC_VALUE_UNIT = /^(\d+(?:dot\d+)?)([A-Za-z%/μ]+)$/;
|
|
2350
|
+
var RATIO = /^(\d+)to(\d+)$/;
|
|
2351
|
+
var RUN_ORDER_PATTERN = /^\d{2,4}[A-Z]?$/;
|
|
2352
|
+
var REPLICATE_TOKEN_PATTERN = /^(?:rep(?:licate)?|r)\d+$/i;
|
|
2353
|
+
var DEFAULT_FACTOR_CARDINALITY_MAX = 50;
|
|
2354
|
+
var LOW_CARDINALITY_NUMERIC_AS_FACTOR_MAX = 5;
|
|
2355
|
+
function parseNumeric(token) {
|
|
2356
|
+
const ratio = RATIO.exec(token);
|
|
2357
|
+
if (ratio) {
|
|
2358
|
+
const num = Number(ratio[1]);
|
|
2359
|
+
const den = Number(ratio[2]);
|
|
2360
|
+
if (den === 0) return null;
|
|
2361
|
+
return {
|
|
2362
|
+
value: num / den,
|
|
2363
|
+
unit: "ratio",
|
|
2364
|
+
raw: token
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
const m = NUMERIC_VALUE_UNIT.exec(token);
|
|
2368
|
+
if (!m) return null;
|
|
2369
|
+
const valueStr = m[1].replace(/dot/g, ".");
|
|
2370
|
+
const value = Number(valueStr);
|
|
2371
|
+
if (Number.isNaN(value)) return null;
|
|
2372
|
+
return {
|
|
2373
|
+
value,
|
|
2374
|
+
unit: m[2],
|
|
2375
|
+
raw: token
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
function inferRole(input) {
|
|
2379
|
+
const { values, position, totalPositions, classSize } = input;
|
|
2380
|
+
const cardinality = new Set(values).size;
|
|
2381
|
+
if (cardinality === 1) return "constant";
|
|
2382
|
+
if (position === totalPositions - 1) {
|
|
2383
|
+
if (values.every((v) => RUN_ORDER_PATTERN.test(v)) && cardinality >= classSize * .5) return "run-order";
|
|
2384
|
+
}
|
|
2385
|
+
if (values.every((v) => parseNumeric(v) !== null)) {
|
|
2386
|
+
if (cardinality <= LOW_CARDINALITY_NUMERIC_AS_FACTOR_MAX) return "factor";
|
|
2387
|
+
return "numeric";
|
|
2388
|
+
}
|
|
2389
|
+
if (values.every((v) => REPLICATE_TOKEN_PATTERN.test(v))) return "replicate";
|
|
2390
|
+
if (cardinality <= DEFAULT_FACTOR_CARDINALITY_MAX) return "factor";
|
|
2391
|
+
return "ignore";
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Auto-detect a friendlier column name from the column's values + role.
|
|
2395
|
+
* Returns null when nothing matches, so callers can fall back to "Token N".
|
|
2396
|
+
*
|
|
2397
|
+
* Heuristics are content-derived and deterministic. They cover the common
|
|
2398
|
+
* LC-MS / proteomics naming conventions; users can override anything via
|
|
2399
|
+
* the column popover (rename input).
|
|
2400
|
+
*/
|
|
2401
|
+
function inferColumnName(values, uniqueValues, role) {
|
|
2402
|
+
if (uniqueValues.length === 0) return null;
|
|
2403
|
+
if ((role === "numeric" || role === "factor") && values.every((v) => parseNumeric(v) !== null)) {
|
|
2404
|
+
const categoryFor = (u) => {
|
|
2405
|
+
const ul = u.toLowerCase();
|
|
2406
|
+
if ([
|
|
2407
|
+
"min",
|
|
2408
|
+
"sec",
|
|
2409
|
+
"s",
|
|
2410
|
+
"hr",
|
|
2411
|
+
"h",
|
|
2412
|
+
"d",
|
|
2413
|
+
"day",
|
|
2414
|
+
"days",
|
|
2415
|
+
"wk",
|
|
2416
|
+
"week",
|
|
2417
|
+
"hour",
|
|
2418
|
+
"hours"
|
|
2419
|
+
].includes(ul)) return "Timepoint";
|
|
2420
|
+
if ([
|
|
2421
|
+
"nm",
|
|
2422
|
+
"um",
|
|
2423
|
+
"mm",
|
|
2424
|
+
"m",
|
|
2425
|
+
"molar",
|
|
2426
|
+
"mol"
|
|
2427
|
+
].includes(ul)) return "Concentration";
|
|
2428
|
+
if ([
|
|
2429
|
+
"mg",
|
|
2430
|
+
"g",
|
|
2431
|
+
"kg",
|
|
2432
|
+
"ng",
|
|
2433
|
+
"ug",
|
|
2434
|
+
"pg"
|
|
2435
|
+
].includes(ul)) return "Mass";
|
|
2436
|
+
if ([
|
|
2437
|
+
"ml",
|
|
2438
|
+
"l",
|
|
2439
|
+
"ul",
|
|
2440
|
+
"nl",
|
|
2441
|
+
"pl"
|
|
2442
|
+
].includes(ul)) return "Volume";
|
|
2443
|
+
if (ul === "%") return "Percentage";
|
|
2444
|
+
if ([
|
|
2445
|
+
"c",
|
|
2446
|
+
"k",
|
|
2447
|
+
"celsius",
|
|
2448
|
+
"kelvin"
|
|
2449
|
+
].includes(ul)) return "Temperature";
|
|
2450
|
+
return null;
|
|
2451
|
+
};
|
|
2452
|
+
const tally = /* @__PURE__ */ new Map();
|
|
2453
|
+
for (const v of values) {
|
|
2454
|
+
const p = parseNumeric(v);
|
|
2455
|
+
const cat = p ? categoryFor(p.unit) : null;
|
|
2456
|
+
if (cat) tally.set(cat, (tally.get(cat) ?? 0) + 1);
|
|
2457
|
+
}
|
|
2458
|
+
let best = null;
|
|
2459
|
+
for (const [cat, count] of tally) if (!best || count > best.count) best = {
|
|
2460
|
+
cat,
|
|
2461
|
+
count
|
|
2462
|
+
};
|
|
2463
|
+
if (best && best.count >= values.length / 2) return best.cat;
|
|
2464
|
+
}
|
|
2465
|
+
if (values.every((v) => /^\d{6}$/.test(v) || /^\d{8}$/.test(v))) return "Date";
|
|
2466
|
+
if (role === "run-order") return "Run Order";
|
|
2467
|
+
if (values.every((v) => /^(pos|neg|positive|negative)$/i.test(v))) return "Polarity";
|
|
2468
|
+
if (values.every((v) => /^(plasma|serum|tissue|tissues|kidney|liver|tumor|urine|brain|muscle|adipose|csf|feces|faeces|cells?|media|medium|extract)$/i.test(v))) return "Matrix";
|
|
2469
|
+
if (values.every((v) => /^(exp|expt|study)\d+$/i.test(v))) return "Experiment";
|
|
2470
|
+
if (uniqueValues.length <= 4 && values.every((v) => /^[A-Z]{2,3}$/.test(v))) return "Username";
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
function buildClassSchema(tokenizedMembers, classKind, subKind, classTagPositions) {
|
|
2474
|
+
const totalPositions = tokenizedMembers[0]?.length ?? 0;
|
|
2475
|
+
const classSize = tokenizedMembers.length;
|
|
2476
|
+
const columns = [];
|
|
2477
|
+
const tagSet = new Set(classTagPositions);
|
|
2478
|
+
for (let pos = 0; pos < totalPositions; pos++) {
|
|
2479
|
+
const values = tokenizedMembers.map((t) => t[pos] ?? "");
|
|
2480
|
+
const uniqueValues = [...new Set(values)];
|
|
2481
|
+
let role;
|
|
2482
|
+
if (tagSet.has(pos) && uniqueValues.length === 1) role = "class-tag";
|
|
2483
|
+
else role = inferRole({
|
|
2484
|
+
values,
|
|
2485
|
+
position: pos,
|
|
2486
|
+
totalPositions,
|
|
2487
|
+
classSize
|
|
2488
|
+
});
|
|
2489
|
+
const numeric = role === "numeric" ? { byValue: Object.fromEntries(uniqueValues.map((v) => [v, parseNumeric(v)]).filter((e) => e[1] !== null)) } : void 0;
|
|
2490
|
+
const inferredName = inferColumnName(values, uniqueValues, role);
|
|
2491
|
+
columns.push({
|
|
2492
|
+
index: pos,
|
|
2493
|
+
name: inferredName ?? `Token ${pos + 1}`,
|
|
2494
|
+
sourceIndices: [pos],
|
|
2495
|
+
uniqueValues,
|
|
2496
|
+
cardinality: uniqueValues.length,
|
|
2497
|
+
role,
|
|
2498
|
+
numeric,
|
|
2499
|
+
type: pos === 0 ? "prefix" : "suffix"
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
let usernameAssigned = false;
|
|
2503
|
+
for (const c of columns) if (c.name === "Username") if (usernameAssigned) c.name = "Project";
|
|
2504
|
+
else usernameAssigned = true;
|
|
2505
|
+
return {
|
|
2506
|
+
classKind,
|
|
2507
|
+
subKind,
|
|
2508
|
+
columns,
|
|
2509
|
+
groupBy: columns.filter((c) => c.role === "factor").map((c) => c.index)
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
//#endregion
|
|
2513
|
+
//#region src/composables/autoGroup/cooccurrence.ts
|
|
2514
|
+
var THRESHOLD = .95;
|
|
2515
|
+
var MIN_CLASS_SIZE = 4;
|
|
2516
|
+
function functionalDependency(rows, srcIdx, dstIdx) {
|
|
2517
|
+
const map = /* @__PURE__ */ new Map();
|
|
2518
|
+
for (const row of rows) {
|
|
2519
|
+
const a = row[srcIdx] ?? "";
|
|
2520
|
+
const b = row[dstIdx] ?? "";
|
|
2521
|
+
const set = map.get(a);
|
|
2522
|
+
if (set) set.add(b);
|
|
2523
|
+
else map.set(a, new Set([b]));
|
|
2524
|
+
}
|
|
2525
|
+
if (map.size === 0) return 0;
|
|
2526
|
+
let functional = 0;
|
|
2527
|
+
for (const set of map.values()) if (set.size === 1) functional++;
|
|
2528
|
+
return functional / map.size;
|
|
2529
|
+
}
|
|
2530
|
+
function findMerges(schema, rows) {
|
|
2531
|
+
if (rows.length < MIN_CLASS_SIZE) return [];
|
|
2532
|
+
const out = [];
|
|
2533
|
+
for (let i = 0; i < schema.columns.length - 1; i++) {
|
|
2534
|
+
const a = schema.columns[i];
|
|
2535
|
+
const b = schema.columns[i + 1];
|
|
2536
|
+
if (a.role !== "factor" && b.role !== "factor") continue;
|
|
2537
|
+
const forward = functionalDependency(rows, a.index, b.index);
|
|
2538
|
+
const backward = functionalDependency(rows, b.index, a.index);
|
|
2539
|
+
if (forward < THRESHOLD || backward < THRESHOLD) continue;
|
|
2540
|
+
if (new Set(rows.map((r) => `${r[a.index] ?? ""}_${r[b.index] ?? ""}`)).size < 2) continue;
|
|
2541
|
+
out.push({
|
|
2542
|
+
classKind: schema.classKind,
|
|
2543
|
+
subKind: schema.subKind,
|
|
2544
|
+
columnIndices: [a.index, b.index],
|
|
2545
|
+
proposedName: `Column ${a.index + 1}–${b.index + 1}`,
|
|
2546
|
+
confidence: Math.min(forward, backward)
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
return out;
|
|
2550
|
+
}
|
|
2551
|
+
//#endregion
|
|
2552
|
+
//#region src/composables/autoGroup/colors.ts
|
|
2553
|
+
var DEFAULT_COLORS = [
|
|
2554
|
+
"#3B82F6",
|
|
2555
|
+
"#10B981",
|
|
2556
|
+
"#F59E0B",
|
|
2557
|
+
"#EF4444",
|
|
2558
|
+
"#8B5CF6",
|
|
2559
|
+
"#EC4899",
|
|
2560
|
+
"#06B6D4",
|
|
2561
|
+
"#84CC16",
|
|
2562
|
+
"#F97316",
|
|
2563
|
+
"#6366F1"
|
|
2564
|
+
];
|
|
2565
|
+
var QC_OVERLAY_COLOR = "#6B7280";
|
|
2566
|
+
//#endregion
|
|
2567
|
+
//#region src/composables/autoGroup/compose.ts
|
|
2568
|
+
/** Apply the operation pipeline to a single raw token value:
|
|
2569
|
+
* numeric-parse → alias → exclude → bin
|
|
2570
|
+
* Returns null if the row is excluded.
|
|
2571
|
+
*/
|
|
2572
|
+
function applyOps(col, raw) {
|
|
2573
|
+
let value = raw;
|
|
2574
|
+
if (col.ops?.alias && raw in col.ops.alias) value = col.ops.alias[raw];
|
|
2575
|
+
if (col.ops?.exclude?.includes(raw) || col.ops?.exclude?.includes(value)) return null;
|
|
2576
|
+
if (col.role === "numeric" && col.binning?.mode === "binned") {
|
|
2577
|
+
const parsed = col.numeric?.byValue[raw];
|
|
2578
|
+
if (parsed && col.binning.edges) {
|
|
2579
|
+
const bin = pickBin(parsed.value, col.binning);
|
|
2580
|
+
if (bin !== null) value = bin;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return value;
|
|
2584
|
+
}
|
|
2585
|
+
function pickBin(value, b) {
|
|
2586
|
+
if (!b.edges || b.edges.length === 0) return null;
|
|
2587
|
+
for (let i = 0; i < b.edges.length; i++) if (value < b.edges[i]) return b.labels?.[i] ?? `< ${b.edges[i]}`;
|
|
2588
|
+
const last = b.edges[b.edges.length - 1];
|
|
2589
|
+
return b.labels?.[b.edges.length] ?? `> ${last}`;
|
|
2590
|
+
}
|
|
2591
|
+
function composeGroups(input) {
|
|
2592
|
+
const experimentalGroups = [];
|
|
2593
|
+
const qcGroups = [];
|
|
2594
|
+
const excludedSamples = [];
|
|
2595
|
+
const metadata = [];
|
|
2596
|
+
let colorIdx = 0;
|
|
2597
|
+
for (const cls of input.classes) {
|
|
2598
|
+
const schema = input.schemas[classKey(cls)];
|
|
2599
|
+
if (!schema) continue;
|
|
2600
|
+
if (cls.disposition === "exclude") {
|
|
2601
|
+
for (const m of cls.members) excludedSamples.push(input.sampleNames[m]);
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
2605
|
+
const colByIdx = new Map(schema.columns.map((c) => [c.index, c]));
|
|
2606
|
+
for (const m of cls.members) {
|
|
2607
|
+
const tokens = input.tokenizedSamples[m];
|
|
2608
|
+
const rowName = input.sampleNames[m];
|
|
2609
|
+
let excluded = false;
|
|
2610
|
+
const fieldValues = {};
|
|
2611
|
+
for (const col of schema.columns) {
|
|
2612
|
+
const display = applyOps(col, col.sourceIndices.map((idx) => tokens[idx] ?? "").join("_"));
|
|
2613
|
+
if (display === null) {
|
|
2614
|
+
excluded = true;
|
|
2615
|
+
break;
|
|
2616
|
+
}
|
|
2617
|
+
fieldValues[col.displayName ?? col.name] = display;
|
|
2618
|
+
}
|
|
2619
|
+
if (excluded) {
|
|
2620
|
+
excludedSamples.push(rowName);
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
const keyParts = cls.disposition === "group" ? schema.groupBy.map((idx) => {
|
|
2624
|
+
const col = colByIdx.get(idx);
|
|
2625
|
+
return fieldValues[col.displayName ?? col.name];
|
|
2626
|
+
}) : [cls.label];
|
|
2627
|
+
const groupKey = cls.disposition === "group" && cls.kind !== "unknown" && keyParts.length > 0 ? `${cls.label} / ${keyParts.join(" / ")}` : keyParts.length > 0 ? keyParts.join(" / ") : cls.label;
|
|
2628
|
+
const bucket = groupMap.get(groupKey);
|
|
2629
|
+
if (bucket) bucket.samples.push(rowName);
|
|
2630
|
+
else groupMap.set(groupKey, {
|
|
2631
|
+
name: groupKey,
|
|
2632
|
+
samples: [rowName]
|
|
2633
|
+
});
|
|
2634
|
+
metadata.push({
|
|
2635
|
+
sampleName: rowName,
|
|
2636
|
+
fields: fieldValues,
|
|
2637
|
+
group: groupKey
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
for (const { name, samples } of groupMap.values()) {
|
|
2641
|
+
const g = {
|
|
2642
|
+
name,
|
|
2643
|
+
color: cls.disposition === "overlay" ? QC_OVERLAY_COLOR : DEFAULT_COLORS[colorIdx++ % DEFAULT_COLORS.length],
|
|
2644
|
+
samples
|
|
2645
|
+
};
|
|
2646
|
+
if (cls.disposition === "overlay") qcGroups.push(g);
|
|
2647
|
+
else experimentalGroups.push(g);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return {
|
|
2651
|
+
groups: [...experimentalGroups, ...qcGroups],
|
|
2652
|
+
experimentalGroups,
|
|
2653
|
+
qcGroups,
|
|
2654
|
+
metadata,
|
|
2655
|
+
excludedSamples,
|
|
2656
|
+
schemas: Object.values(input.schemas)
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
//#endregion
|
|
2660
|
+
//#region src/composables/autoGroup/fingerprint.ts
|
|
2661
|
+
function serializeFingerprint(schemas) {
|
|
2662
|
+
return {
|
|
2663
|
+
version: 1,
|
|
2664
|
+
classes: schemas.map((s) => ({
|
|
2665
|
+
kind: s.classKind,
|
|
2666
|
+
subKind: s.subKind,
|
|
2667
|
+
columns: s.columns.map((c) => ({
|
|
2668
|
+
name: c.displayName ?? c.name,
|
|
2669
|
+
role: c.role ?? "factor",
|
|
2670
|
+
sourceIndices: c.sourceIndices,
|
|
2671
|
+
binning: c.binning,
|
|
2672
|
+
ops: c.ops
|
|
2673
|
+
})),
|
|
2674
|
+
groupBy: s.groupBy
|
|
2675
|
+
}))
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
function restoreFingerprint(fp, current) {
|
|
2679
|
+
return fp.classes.map((snap) => {
|
|
2680
|
+
const target = current.find((c) => c.classKind === snap.kind && c.subKind === snap.subKind);
|
|
2681
|
+
if (!target) throw new Error(`Fingerprint class not present in current input: ${snap.kind}${snap.subKind ? "/" + snap.subKind : ""}`);
|
|
2682
|
+
if (target.columns.length !== snap.columns.length) throw new Error(`Saved schema expects ${snap.columns.length} columns, current data has ${target.columns.length} for class ${snap.kind}${snap.subKind ? "/" + snap.subKind : ""}. column count mismatch`);
|
|
2683
|
+
return {
|
|
2684
|
+
...target,
|
|
2685
|
+
groupBy: snap.groupBy,
|
|
2686
|
+
columns: target.columns.map((col, i) => ({
|
|
2687
|
+
...col,
|
|
2688
|
+
displayName: snap.columns[i].name,
|
|
2689
|
+
role: snap.columns[i].role,
|
|
2690
|
+
binning: snap.columns[i].binning,
|
|
2691
|
+
ops: snap.columns[i].ops
|
|
2692
|
+
}))
|
|
2693
|
+
};
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
//#endregion
|
|
2697
|
+
//#region src/composables/autoGroup/template.ts
|
|
2698
|
+
var DEFAULT_BLANK_HEADERS = [
|
|
2699
|
+
"group",
|
|
2700
|
+
"class",
|
|
2701
|
+
"replicate",
|
|
2702
|
+
"notes"
|
|
2703
|
+
];
|
|
2704
|
+
function rfc4180Quote(cell) {
|
|
2705
|
+
if (/[",\n]/.test(cell)) return `"${cell.replace(/"/g, "\"\"")}"`;
|
|
2706
|
+
return cell;
|
|
2707
|
+
}
|
|
2708
|
+
function joinRow(cells, sep) {
|
|
2709
|
+
return sep === "," ? cells.map(rfc4180Quote).join(",") : cells.join(" ");
|
|
2710
|
+
}
|
|
2711
|
+
function today() {
|
|
2712
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2713
|
+
}
|
|
2714
|
+
function composeTemplate(samples, schemas, classes, options) {
|
|
2715
|
+
const sep = options.format === "tsv" ? " " : ",";
|
|
2716
|
+
const ext = options.format === "tsv" ? "tsv" : "csv";
|
|
2717
|
+
const filename = `${options.experimentCode ? `${options.experimentCode}_metadata` : "sample_metadata"}_${today()}.${ext}`;
|
|
2718
|
+
if (options.mode === "blank" || !schemas || !classes) {
|
|
2719
|
+
const headers = ["sample_name", ...options.suggestedColumns ?? DEFAULT_BLANK_HEADERS];
|
|
2720
|
+
const rows = samples.map((s) => {
|
|
2721
|
+
return joinRow([s, ...Array(headers.length - 1).fill("")], sep);
|
|
2722
|
+
});
|
|
2723
|
+
return {
|
|
2724
|
+
content: [joinRow(headers, sep), ...rows].join("\n"),
|
|
2725
|
+
filename
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
const tokenized = options.tokenizedSamples ?? [];
|
|
2729
|
+
const allColumns = [];
|
|
2730
|
+
const sampleToClass = /* @__PURE__ */ new Map();
|
|
2731
|
+
for (const cls of classes) for (const m of cls.members) sampleToClass.set(m, cls);
|
|
2732
|
+
const schemaByKey = /* @__PURE__ */ new Map();
|
|
2733
|
+
for (const s of schemas) {
|
|
2734
|
+
const k = s.subKind ? `${s.classKind}:${s.subKind}` : s.classKind;
|
|
2735
|
+
schemaByKey.set(k, s);
|
|
2736
|
+
}
|
|
2737
|
+
const colByNameByKey = /* @__PURE__ */ new Map();
|
|
2738
|
+
const groupBySetByKey = /* @__PURE__ */ new Map();
|
|
2739
|
+
for (const s of schemas) {
|
|
2740
|
+
const k = s.subKind ? `${s.classKind}:${s.subKind}` : s.classKind;
|
|
2741
|
+
const m = /* @__PURE__ */ new Map();
|
|
2742
|
+
for (const c of s.columns) m.set(c.displayName ?? c.name, c);
|
|
2743
|
+
colByNameByKey.set(k, m);
|
|
2744
|
+
groupBySetByKey.set(k, new Set(s.groupBy));
|
|
2745
|
+
const seenColNames = /* @__PURE__ */ new Set();
|
|
2746
|
+
for (const c of s.columns) {
|
|
2747
|
+
const name = c.displayName ?? c.name;
|
|
2748
|
+
if (!seenColNames.has(name)) {
|
|
2749
|
+
seenColNames.add(name);
|
|
2750
|
+
if (!allColumns.some((x) => x.colName === name)) allColumns.push({
|
|
2751
|
+
schemaKey: k,
|
|
2752
|
+
colName: name
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
const headers = [
|
|
2758
|
+
"sample_name",
|
|
2759
|
+
"class",
|
|
2760
|
+
...allColumns.map((c) => c.colName),
|
|
2761
|
+
"group",
|
|
2762
|
+
"notes"
|
|
2763
|
+
];
|
|
2764
|
+
const rows = [];
|
|
2765
|
+
for (let i = 0; i < samples.length; i++) {
|
|
2766
|
+
const cls = sampleToClass.get(i);
|
|
2767
|
+
const classLabel = cls?.label ?? "";
|
|
2768
|
+
const tokens = tokenized[i] ?? [];
|
|
2769
|
+
const cells = [samples[i], classLabel];
|
|
2770
|
+
if (cls) {
|
|
2771
|
+
const k = classKey(cls);
|
|
2772
|
+
const groupBySet = groupBySetByKey.get(k);
|
|
2773
|
+
const colByName = colByNameByKey.get(k);
|
|
2774
|
+
const keyParts = [];
|
|
2775
|
+
for (const col of allColumns) if (col.schemaKey === k && colByName) {
|
|
2776
|
+
const schemaCol = colByName.get(col.colName);
|
|
2777
|
+
const raw = schemaCol?.sourceIndices.map((idx) => tokens[idx] ?? "").join("_") ?? "";
|
|
2778
|
+
cells.push(raw);
|
|
2779
|
+
if (schemaCol && groupBySet?.has(schemaCol.index)) keyParts.push(raw);
|
|
2780
|
+
} else cells.push("");
|
|
2781
|
+
cells.push(keyParts.join(" / "));
|
|
2782
|
+
} else {
|
|
2783
|
+
for (const _ of allColumns) cells.push("");
|
|
2784
|
+
cells.push("");
|
|
2785
|
+
}
|
|
2786
|
+
cells.push("");
|
|
2787
|
+
rows.push(joinRow(cells, sep));
|
|
2788
|
+
}
|
|
2789
|
+
return {
|
|
2790
|
+
content: [joinRow(headers, sep), ...rows].join("\n"),
|
|
2791
|
+
filename
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
//#endregion
|
|
2795
|
+
//#region src/composables/autoGroup/replicatePreGroup.ts
|
|
2796
|
+
/**
|
|
2797
|
+
* Replicate pre-grouping.
|
|
2798
|
+
*
|
|
2799
|
+
* Strip the replicate / injection-number markers from each sample's name, then
|
|
2800
|
+
* collapse samples that share a base name. Those samples ARE replicates of one
|
|
2801
|
+
* another by definition; the remaining unique base names are what the rest of
|
|
2802
|
+
* the pipeline (tokenize → classify → schema → compose) should operate on.
|
|
2803
|
+
*
|
|
2804
|
+
* This pre-pass replaces the older "treat every sample as independent, then
|
|
2805
|
+
* infer replicate columns" approach. Identifying replicate-equivalence by name
|
|
2806
|
+
* before tokenisation removes a class of fragile cardinality heuristics.
|
|
2807
|
+
*/
|
|
2808
|
+
var STRIP_PATTERNS = vocab_default.replicateStripPatterns.map((src) => new RegExp(src, "gi"));
|
|
2809
|
+
/**
|
|
2810
|
+
* Iteratively strip every replicate / injection-number marker from `name`.
|
|
2811
|
+
* Each pattern is applied repeatedly until it stops matching, so a sample like
|
|
2812
|
+
* `study_T1_Rep2_004` collapses to `study` in one call.
|
|
2813
|
+
*/
|
|
2814
|
+
function stripReplicateTokens(name) {
|
|
2815
|
+
let base = name;
|
|
2816
|
+
for (const re of STRIP_PATTERNS) {
|
|
2817
|
+
let prev;
|
|
2818
|
+
do {
|
|
2819
|
+
prev = base;
|
|
2820
|
+
base = base.replace(re, "");
|
|
2821
|
+
re.lastIndex = 0;
|
|
2822
|
+
} while (base !== prev);
|
|
2823
|
+
}
|
|
2824
|
+
return base;
|
|
2825
|
+
}
|
|
2826
|
+
function preGroupReplicates(samples) {
|
|
2827
|
+
const map = /* @__PURE__ */ new Map();
|
|
2828
|
+
const baseOrder = [];
|
|
2829
|
+
for (let i = 0; i < samples.length; i++) {
|
|
2830
|
+
const base = stripReplicateTokens(samples[i]);
|
|
2831
|
+
const bucket = map.get(base);
|
|
2832
|
+
if (bucket) bucket.push(i);
|
|
2833
|
+
else {
|
|
2834
|
+
map.set(base, [i]);
|
|
2835
|
+
baseOrder.push(base);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return {
|
|
2839
|
+
baseNames: baseOrder,
|
|
2840
|
+
membersByBase: baseOrder.map((b) => map.get(b)),
|
|
2841
|
+
originalSamples: samples
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Expand groups produced over base names back to groups of original sample
|
|
2846
|
+
* names. Each entry in `group.samples` is treated as a base name and replaced
|
|
2847
|
+
* by the list of original samples that collapsed onto it.
|
|
2848
|
+
*/
|
|
2849
|
+
function expandGroupsWithReplicates(groups, preGrouping) {
|
|
2850
|
+
const baseToMembers = /* @__PURE__ */ new Map();
|
|
2851
|
+
for (let i = 0; i < preGrouping.baseNames.length; i++) baseToMembers.set(preGrouping.baseNames[i], preGrouping.membersByBase[i]);
|
|
2852
|
+
return groups.map((g) => ({
|
|
2853
|
+
...g,
|
|
2854
|
+
samples: g.samples.flatMap((baseName) => {
|
|
2855
|
+
const indices = baseToMembers.get(baseName);
|
|
2856
|
+
return indices ? indices.map((i) => preGrouping.originalSamples[i]) : [baseName];
|
|
2857
|
+
})
|
|
2858
|
+
}));
|
|
2859
|
+
}
|
|
2860
|
+
//#endregion
|
|
2101
2861
|
//#region src/composables/experimentDesignData.ts
|
|
2102
2862
|
function isRecord(value) {
|
|
2103
2863
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -2200,119 +2960,17 @@ function extractSampleNamesFromDesignData(rawData, options = {}) {
|
|
|
2200
2960
|
return extractSampleOptionsFromDesignData(rawData, options).map((sample) => sample.value);
|
|
2201
2961
|
}
|
|
2202
2962
|
//#endregion
|
|
2203
|
-
//#region src/composables/
|
|
2204
|
-
var DEFAULT_COLORS = [
|
|
2205
|
-
"#3B82F6",
|
|
2206
|
-
"#10B981",
|
|
2207
|
-
"#F59E0B",
|
|
2208
|
-
"#EF4444",
|
|
2209
|
-
"#8B5CF6",
|
|
2210
|
-
"#EC4899",
|
|
2211
|
-
"#06B6D4",
|
|
2212
|
-
"#84CC16",
|
|
2213
|
-
"#F97316",
|
|
2214
|
-
"#6366F1"
|
|
2215
|
-
];
|
|
2216
|
-
var DELIMITER_CANDIDATES = [
|
|
2217
|
-
"_",
|
|
2218
|
-
"-",
|
|
2219
|
-
"."
|
|
2220
|
-
];
|
|
2221
|
-
function analyzeDelimiter(lines) {
|
|
2222
|
-
if (lines.length === 0) return {
|
|
2223
|
-
delimiter: "_",
|
|
2224
|
-
dominantFieldCount: 1,
|
|
2225
|
-
minFieldCount: 1,
|
|
2226
|
-
consistency: 0
|
|
2227
|
-
};
|
|
2228
|
-
let bestDelimiter = "_";
|
|
2229
|
-
let bestConsistency = -1;
|
|
2230
|
-
let bestFieldCount = 1;
|
|
2231
|
-
for (const candidate of DELIMITER_CANDIDATES) {
|
|
2232
|
-
const fieldCounts = lines.map((line) => line.split(candidate).length);
|
|
2233
|
-
const countFrequency = /* @__PURE__ */ new Map();
|
|
2234
|
-
for (const count of fieldCounts) countFrequency.set(count, (countFrequency.get(count) ?? 0) + 1);
|
|
2235
|
-
let modeCount = 1;
|
|
2236
|
-
let modeFrequency = 0;
|
|
2237
|
-
for (const [count, freq] of countFrequency) if (freq > modeFrequency || freq === modeFrequency && count > modeCount) {
|
|
2238
|
-
modeCount = count;
|
|
2239
|
-
modeFrequency = freq;
|
|
2240
|
-
}
|
|
2241
|
-
const rawConsistency = modeFrequency / lines.length;
|
|
2242
|
-
const consistency = modeCount > 1 ? rawConsistency : 0;
|
|
2243
|
-
if (consistency > bestConsistency || consistency === bestConsistency && DELIMITER_CANDIDATES.indexOf(candidate) < DELIMITER_CANDIDATES.indexOf(bestDelimiter)) {
|
|
2244
|
-
bestDelimiter = candidate;
|
|
2245
|
-
bestConsistency = consistency;
|
|
2246
|
-
bestFieldCount = modeCount;
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
const multiFieldCounts = lines.map((line) => line.split(bestDelimiter).length).filter((c) => c >= 2);
|
|
2250
|
-
const minFieldCount = multiFieldCounts.length > 0 ? Math.min(...multiFieldCounts) : 1;
|
|
2251
|
-
return {
|
|
2252
|
-
delimiter: bestDelimiter,
|
|
2253
|
-
dominantFieldCount: bestFieldCount,
|
|
2254
|
-
minFieldCount,
|
|
2255
|
-
consistency: bestConsistency
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
function detectOutliers(lines, delimiter, minFieldCount) {
|
|
2259
|
-
const outliers = [];
|
|
2260
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2261
|
-
const fieldCount = lines[i].split(delimiter).length;
|
|
2262
|
-
if (fieldCount < minFieldCount) outliers.push({
|
|
2263
|
-
sample: lines[i],
|
|
2264
|
-
index: i,
|
|
2265
|
-
fieldCount,
|
|
2266
|
-
action: "include"
|
|
2267
|
-
});
|
|
2268
|
-
}
|
|
2269
|
-
return outliers;
|
|
2270
|
-
}
|
|
2271
|
-
var QC_KEYWORDS = new Set([
|
|
2272
|
-
"eqc",
|
|
2273
|
-
"iqc",
|
|
2274
|
-
"qc",
|
|
2275
|
-
"blank",
|
|
2276
|
-
"std",
|
|
2277
|
-
"standard",
|
|
2278
|
-
"test"
|
|
2279
|
-
]);
|
|
2280
|
-
function classifyOutlierAction(sample, delimiter) {
|
|
2281
|
-
return sample.split(delimiter).some((seg) => QC_KEYWORDS.has(seg.toLowerCase())) ? "qc" : "include";
|
|
2282
|
-
}
|
|
2283
|
-
function isUsefulField(field, rowCount) {
|
|
2284
|
-
return field.cardinality > 1 && !(rowCount > 1 && field.cardinality === rowCount);
|
|
2285
|
-
}
|
|
2286
|
-
function extractColumns(samples, delimiter, minFieldCount) {
|
|
2287
|
-
if (samples.length === 0) return [];
|
|
2288
|
-
const suffixCount = minFieldCount - 1;
|
|
2289
|
-
const rows = samples.map((s) => {
|
|
2290
|
-
const parts = s.split(delimiter);
|
|
2291
|
-
const splitAt = parts.length - suffixCount;
|
|
2292
|
-
return [parts.slice(0, splitAt).join(delimiter), ...parts.slice(splitAt)];
|
|
2293
|
-
});
|
|
2294
|
-
const columnCount = minFieldCount;
|
|
2295
|
-
const columns = [];
|
|
2296
|
-
for (let col = 0; col < columnCount; col++) {
|
|
2297
|
-
const values = rows.map((row) => row[col]);
|
|
2298
|
-
const unique = [...new Set(values)];
|
|
2299
|
-
columns.push({
|
|
2300
|
-
index: col,
|
|
2301
|
-
name: col === 0 ? "Condition" : `Field ${col + 1}`,
|
|
2302
|
-
uniqueValues: unique,
|
|
2303
|
-
cardinality: unique.length,
|
|
2304
|
-
type: col === 0 ? "prefix" : "suffix"
|
|
2305
|
-
});
|
|
2306
|
-
}
|
|
2307
|
-
return columns;
|
|
2308
|
-
}
|
|
2963
|
+
//#region src/composables/autoGroup/csv-shim.ts
|
|
2309
2964
|
function parseCSVLine(line, delimiter = ",") {
|
|
2310
2965
|
const result = [];
|
|
2311
2966
|
let current = "";
|
|
2312
2967
|
let inQuotes = false;
|
|
2313
2968
|
for (let i = 0; i < line.length; i++) {
|
|
2314
2969
|
const char = line[i];
|
|
2315
|
-
if (char === "\"") inQuotes
|
|
2970
|
+
if (char === "\"") if (inQuotes && line[i + 1] === "\"") {
|
|
2971
|
+
current += "\"";
|
|
2972
|
+
i++;
|
|
2973
|
+
} else inQuotes = !inQuotes;
|
|
2316
2974
|
else if (char === delimiter && !inQuotes) {
|
|
2317
2975
|
result.push(current.trim());
|
|
2318
2976
|
current = "";
|
|
@@ -2332,8 +2990,8 @@ function parseCSV(text) {
|
|
|
2332
2990
|
const values = parseCSVLine(lines[i], csvDelimiter);
|
|
2333
2991
|
if (values.length !== headers.length) continue;
|
|
2334
2992
|
const row = {};
|
|
2335
|
-
headers.forEach((
|
|
2336
|
-
row[
|
|
2993
|
+
headers.forEach((h, idx) => {
|
|
2994
|
+
row[h] = values[idx];
|
|
2337
2995
|
});
|
|
2338
2996
|
rows.push(row);
|
|
2339
2997
|
}
|
|
@@ -2354,301 +3012,407 @@ function parseCSV(text) {
|
|
|
2354
3012
|
delimiter: csvDelimiter
|
|
2355
3013
|
};
|
|
2356
3014
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
const
|
|
2369
|
-
const
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
3015
|
+
//#endregion
|
|
3016
|
+
//#region src/composables/useAutoGroup.ts
|
|
3017
|
+
var deprecationWarned = false;
|
|
3018
|
+
var outlierDeprecationWarned = false;
|
|
3019
|
+
function warnDeprecated(api) {
|
|
3020
|
+
if (deprecationWarned) return;
|
|
3021
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
3022
|
+
console.warn(`[useAutoGroup] ${api} is deprecated; use the new class-first API (setClassDisposition / toggleGroupBy / setValueOps).`);
|
|
3023
|
+
deprecationWarned = true;
|
|
3024
|
+
}
|
|
3025
|
+
function useAutoGroup() {
|
|
3026
|
+
const inputMode = ref("paste");
|
|
3027
|
+
const rawText = ref("");
|
|
3028
|
+
const csvData = ref(null);
|
|
3029
|
+
const sampleTypeHints = ref([]);
|
|
3030
|
+
const classes = ref([]);
|
|
3031
|
+
const schemas = ref({});
|
|
3032
|
+
const activeClassKey = ref("");
|
|
3033
|
+
const samples = computed(() => {
|
|
3034
|
+
if (csvData.value) return csvData.value.rows.map((r) => r[csvData.value.sampleColumn] ?? "");
|
|
3035
|
+
return rawText.value.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3036
|
+
});
|
|
3037
|
+
const tokenized = ref([]);
|
|
3038
|
+
const delimiter = ref("_");
|
|
3039
|
+
const preGrouping = ref(null);
|
|
3040
|
+
function parseInput() {
|
|
3041
|
+
if ((inputMode.value === "csv" || inputMode.value === "experiment") && csvData.value) {
|
|
3042
|
+
parseInputFromCsv();
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
parseInputFromPaste();
|
|
2388
3046
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
3047
|
+
function parseInputFromCsv() {
|
|
3048
|
+
preGrouping.value = null;
|
|
3049
|
+
const csv = csvData.value;
|
|
3050
|
+
const nonSampleCols = csv.columns.filter((c) => c !== csv.sampleColumn && c !== "sample_type");
|
|
3051
|
+
if (nonSampleCols.length === 0) {
|
|
3052
|
+
tokenized.value = csv.rows.map(() => []);
|
|
3053
|
+
classes.value = [{
|
|
3054
|
+
kind: "unknown",
|
|
3055
|
+
label: "Unknown",
|
|
3056
|
+
members: csv.rows.map((_, i) => i),
|
|
3057
|
+
classTagPositions: [],
|
|
3058
|
+
disposition: "group"
|
|
3059
|
+
}];
|
|
3060
|
+
schemas.value = { unknown: {
|
|
3061
|
+
classKind: "unknown",
|
|
3062
|
+
columns: [],
|
|
3063
|
+
groupBy: []
|
|
3064
|
+
} };
|
|
3065
|
+
activeClassKey.value = "unknown";
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
const tokens = csv.rows.map((r) => nonSampleCols.map((col) => r[col] ?? ""));
|
|
3069
|
+
tokenized.value = tokens;
|
|
3070
|
+
const detected = detectClass(tokens, { sampleTypeHints: csv.rows.map((r) => {
|
|
3071
|
+
const t = String(r["sample_type"] ?? "").toLowerCase();
|
|
3072
|
+
if (t === "qc") return "qc";
|
|
3073
|
+
if (t === "blank") return "blank";
|
|
3074
|
+
}) });
|
|
3075
|
+
classes.value = detected;
|
|
3076
|
+
const newSchemas = {};
|
|
3077
|
+
for (const cls of detected) {
|
|
3078
|
+
const schema = buildClassSchema(cls.members.map((i) => tokens[i]), cls.kind, cls.subKind, cls.classTagPositions);
|
|
3079
|
+
schema.columns = schema.columns.map((c, i) => ({
|
|
3080
|
+
...c,
|
|
3081
|
+
name: nonSampleCols[i] ?? c.name,
|
|
3082
|
+
originalName: nonSampleCols[i]
|
|
3083
|
+
}));
|
|
3084
|
+
newSchemas[classKey(cls)] = schema;
|
|
3085
|
+
}
|
|
3086
|
+
schemas.value = newSchemas;
|
|
3087
|
+
if (detected.length > 0) activeClassKey.value = classKey(detected[0]);
|
|
2398
3088
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
3089
|
+
function parseInputFromPaste() {
|
|
3090
|
+
const lines = samples.value;
|
|
3091
|
+
if (lines.length === 0) {
|
|
3092
|
+
classes.value = [];
|
|
3093
|
+
schemas.value = {};
|
|
3094
|
+
preGrouping.value = null;
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
const pre = preGroupReplicates(lines);
|
|
3098
|
+
preGrouping.value = pre;
|
|
3099
|
+
const d = pickPrimaryDelimiter(pre.baseNames);
|
|
3100
|
+
delimiter.value = d;
|
|
3101
|
+
tokenized.value = pre.baseNames.map((l) => splitMulti(l, d));
|
|
3102
|
+
const hints = pre.membersByBase.map((members) => sampleTypeHints.value[members[0]]);
|
|
3103
|
+
const detected = detectClass(tokenized.value, { sampleTypeHints: hints });
|
|
3104
|
+
classes.value = detected;
|
|
3105
|
+
const newSchemas = {};
|
|
3106
|
+
for (const cls of detected) {
|
|
3107
|
+
const memberTokens = cls.members.map((i) => tokenized.value[i]);
|
|
3108
|
+
newSchemas[classKey(cls)] = buildClassSchema(memberTokens, cls.kind, cls.subKind, cls.classTagPositions);
|
|
3109
|
+
}
|
|
3110
|
+
schemas.value = newSchemas;
|
|
3111
|
+
if (detected.length > 0) activeClassKey.value = classKey(detected[0]);
|
|
3112
|
+
}
|
|
3113
|
+
const activeSchema = computed(() => schemas.value[activeClassKey.value] ?? null);
|
|
3114
|
+
const suggestions = computed(() => {
|
|
3115
|
+
const schema = activeSchema.value;
|
|
3116
|
+
if (!schema) return [];
|
|
3117
|
+
const activeClass = classes.value.find((c) => classKey(c) === activeClassKey.value);
|
|
3118
|
+
if (!activeClass) return [];
|
|
3119
|
+
return findMerges(schema, activeClass.members.map((i) => tokenized.value[i]));
|
|
3120
|
+
});
|
|
3121
|
+
const result = computed(() => {
|
|
3122
|
+
const pre = preGrouping.value;
|
|
3123
|
+
const composed = composeGroups({
|
|
3124
|
+
tokenizedSamples: tokenized.value,
|
|
3125
|
+
sampleNames: pre ? pre.baseNames : samples.value,
|
|
3126
|
+
schemas: schemas.value,
|
|
3127
|
+
classes: classes.value
|
|
2404
3128
|
});
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
3129
|
+
if (!pre) return composed;
|
|
3130
|
+
return {
|
|
3131
|
+
...composed,
|
|
3132
|
+
groups: expandGroupsWithReplicates(composed.groups, pre),
|
|
3133
|
+
experimentalGroups: composed.experimentalGroups ? expandGroupsWithReplicates(composed.experimentalGroups, pre) : void 0,
|
|
3134
|
+
qcGroups: composed.qcGroups ? expandGroupsWithReplicates(composed.qcGroups, pre) : void 0
|
|
3135
|
+
};
|
|
3136
|
+
});
|
|
3137
|
+
const fingerprint = computed(() => serializeFingerprint(result.value.schemas ?? []));
|
|
3138
|
+
const groups = computed(() => result.value.groups);
|
|
3139
|
+
const qcGroups = computed(() => result.value.qcGroups ?? []);
|
|
3140
|
+
const metadata = computed(() => result.value.metadata);
|
|
3141
|
+
const excludedSamples = computed(() => result.value.excludedSamples);
|
|
3142
|
+
function setClassDisposition(key, disposition) {
|
|
3143
|
+
const target = classes.value.find((c) => classKey(c) === key);
|
|
3144
|
+
if (!target || target.disposition === disposition) return;
|
|
3145
|
+
classes.value = classes.value.map((c) => classKey(c) === key ? {
|
|
3146
|
+
...c,
|
|
3147
|
+
disposition
|
|
3148
|
+
} : c);
|
|
3149
|
+
}
|
|
3150
|
+
function setClassDispositions(predicate, disposition) {
|
|
3151
|
+
let changed = false;
|
|
3152
|
+
const next = classes.value.map((c) => {
|
|
3153
|
+
if (predicate(c) && c.disposition !== disposition) {
|
|
3154
|
+
changed = true;
|
|
3155
|
+
return {
|
|
3156
|
+
...c,
|
|
3157
|
+
disposition
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
return c;
|
|
2409
3161
|
});
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
* their explicit `sample_type` field.
|
|
2424
|
-
*
|
|
2425
|
-
* Returns null if no samples with conditions are found.
|
|
2426
|
-
*/
|
|
2427
|
-
function extractSamplesFromDesignData(rawData) {
|
|
2428
|
-
const designData = unwrapExperimentDesignData(rawData);
|
|
2429
|
-
if (!designData) return null;
|
|
2430
|
-
const samples = designData.samples;
|
|
2431
|
-
if (!Array.isArray(samples) || samples.length === 0) return null;
|
|
2432
|
-
const allConditionKeys = [];
|
|
2433
|
-
const keySet = /* @__PURE__ */ new Set();
|
|
2434
|
-
const filteredSamples = [];
|
|
2435
|
-
for (const sample of samples) {
|
|
2436
|
-
const sampleType = String(sample.sample_type ?? "sample").toLowerCase();
|
|
2437
|
-
if (sampleType === "qc" || sampleType === "blank") continue;
|
|
2438
|
-
filteredSamples.push(sample);
|
|
2439
|
-
const conditions = sample.conditions;
|
|
2440
|
-
if (conditions && typeof conditions === "object") {
|
|
2441
|
-
for (const key of Object.keys(conditions)) if (!keySet.has(key)) {
|
|
2442
|
-
keySet.add(key);
|
|
2443
|
-
allConditionKeys.push(key);
|
|
3162
|
+
if (changed) classes.value = next;
|
|
3163
|
+
}
|
|
3164
|
+
function toggleGroupBy(key, idx) {
|
|
3165
|
+
const schema = schemas.value[key];
|
|
3166
|
+
if (!schema) return;
|
|
3167
|
+
const set = new Set(schema.groupBy);
|
|
3168
|
+
if (set.has(idx)) set.delete(idx);
|
|
3169
|
+
else set.add(idx);
|
|
3170
|
+
schemas.value = {
|
|
3171
|
+
...schemas.value,
|
|
3172
|
+
[key]: {
|
|
3173
|
+
...schema,
|
|
3174
|
+
groupBy: [...set].sort((a, b) => a - b)
|
|
2444
3175
|
}
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
function updateColumn(key, idx, patch) {
|
|
3179
|
+
const schema = schemas.value[key];
|
|
3180
|
+
if (!schema) return;
|
|
3181
|
+
const col = schema.columns.find((c) => c.index === idx);
|
|
3182
|
+
if (!col) return;
|
|
3183
|
+
let changed = false;
|
|
3184
|
+
for (const k of Object.keys(patch)) if (col[k] !== patch[k]) {
|
|
3185
|
+
changed = true;
|
|
3186
|
+
break;
|
|
2445
3187
|
}
|
|
3188
|
+
if (!changed) return;
|
|
3189
|
+
schemas.value = {
|
|
3190
|
+
...schemas.value,
|
|
3191
|
+
[key]: {
|
|
3192
|
+
...schema,
|
|
3193
|
+
columns: schema.columns.map((c) => c.index === idx ? {
|
|
3194
|
+
...c,
|
|
3195
|
+
...patch
|
|
3196
|
+
} : c)
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
2446
3199
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
columns: ["sample_name", ...allConditionKeys],
|
|
2450
|
-
rows: filteredSamples.map((sample) => {
|
|
2451
|
-
const conditions = sample.conditions ?? {};
|
|
2452
|
-
const row = { sample_name: String(sample.sample_name ?? "") };
|
|
2453
|
-
for (const key of allConditionKeys) row[key] = conditions[key] ?? "";
|
|
2454
|
-
return row;
|
|
2455
|
-
}),
|
|
2456
|
-
sampleColumn: "sample_name",
|
|
2457
|
-
delimiter: ","
|
|
2458
|
-
};
|
|
2459
|
-
}
|
|
2460
|
-
function computeGroupsFromCsv(csvData, columns, enabledFields) {
|
|
2461
|
-
const groupMap = /* @__PURE__ */ new Map();
|
|
2462
|
-
const metadata = [];
|
|
2463
|
-
const enabledCols = columns.filter((c) => enabledFields.has(c.index)).sort((a, b) => a.index - b.index);
|
|
2464
|
-
for (const row of csvData.rows) {
|
|
2465
|
-
const sampleName = row[csvData.sampleColumn];
|
|
2466
|
-
const groupKey = enabledCols.map((col) => row[col.originalName ?? col.name]).join(" / ");
|
|
2467
|
-
const group = groupMap.get(groupKey);
|
|
2468
|
-
if (group) group.push(sampleName);
|
|
2469
|
-
else groupMap.set(groupKey, [sampleName]);
|
|
2470
|
-
const fields = {};
|
|
2471
|
-
for (const col of columns) fields[col.name] = row[col.originalName ?? col.name];
|
|
2472
|
-
metadata.push({
|
|
2473
|
-
sampleName,
|
|
2474
|
-
fields,
|
|
2475
|
-
group: groupKey
|
|
2476
|
-
});
|
|
3200
|
+
function setRole(key, idx, role) {
|
|
3201
|
+
updateColumn(key, idx, { role });
|
|
2477
3202
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
3203
|
+
function setColumnName(key, idx, name) {
|
|
3204
|
+
updateColumn(key, idx, { displayName: name });
|
|
3205
|
+
}
|
|
3206
|
+
function setValueOps(key, idx, ops) {
|
|
3207
|
+
updateColumn(key, idx, { ops });
|
|
3208
|
+
}
|
|
3209
|
+
function setBinning(key, idx, binning) {
|
|
3210
|
+
updateColumn(key, idx, { binning });
|
|
3211
|
+
}
|
|
3212
|
+
function mergeColumns(key, indices) {
|
|
3213
|
+
const schema = schemas.value[key];
|
|
3214
|
+
if (!schema || indices.length < 2) return;
|
|
3215
|
+
const sorted = [...indices].sort((a, b) => a - b);
|
|
3216
|
+
const start = sorted[0];
|
|
3217
|
+
const merged = {
|
|
3218
|
+
index: start,
|
|
3219
|
+
name: `Column ${sorted.map((i) => i + 1).join("–")}`,
|
|
3220
|
+
sourceIndices: sorted.flatMap((i) => schema.columns.find((c) => c.index === i)?.sourceIndices ?? [i]),
|
|
3221
|
+
uniqueValues: [],
|
|
3222
|
+
cardinality: 0,
|
|
3223
|
+
role: "factor"
|
|
3224
|
+
};
|
|
3225
|
+
const dropped = new Set(sorted);
|
|
3226
|
+
schemas.value = {
|
|
3227
|
+
...schemas.value,
|
|
3228
|
+
[key]: {
|
|
3229
|
+
...schema,
|
|
3230
|
+
columns: [merged, ...schema.columns.filter((c) => !dropped.has(c.index))].sort((a, b) => a.index - b.index),
|
|
3231
|
+
groupBy: schema.groupBy.filter((i) => !dropped.has(i) || i === start)
|
|
3232
|
+
}
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
function loadFingerprint(fp) {
|
|
3236
|
+
const restored = restoreFingerprint(fp, Object.values(schemas.value));
|
|
3237
|
+
const next = {};
|
|
3238
|
+
for (const s of restored) next[classKey({
|
|
3239
|
+
kind: s.classKind,
|
|
3240
|
+
subKind: s.subKind
|
|
3241
|
+
})] = s;
|
|
3242
|
+
schemas.value = next;
|
|
3243
|
+
}
|
|
3244
|
+
const canDownloadTemplate = computed(() => samples.value.length > 0);
|
|
3245
|
+
function downloadTemplate(mode = "prefilled", format = "csv") {
|
|
3246
|
+
const { content, filename } = composeTemplate(samples.value, mode === "prefilled" ? Object.values(schemas.value) : null, mode === "prefilled" ? classes.value : null, {
|
|
3247
|
+
mode,
|
|
3248
|
+
format,
|
|
3249
|
+
tokenizedSamples: tokenized.value
|
|
3250
|
+
});
|
|
3251
|
+
const blob = new Blob([content], { type: format === "tsv" ? "text/tab-separated-values" : "text/csv" });
|
|
3252
|
+
const url = URL.createObjectURL(blob);
|
|
3253
|
+
const a = document.createElement("a");
|
|
3254
|
+
a.href = url;
|
|
3255
|
+
a.download = filename;
|
|
3256
|
+
document.body.appendChild(a);
|
|
3257
|
+
a.click();
|
|
3258
|
+
document.body.removeChild(a);
|
|
3259
|
+
URL.revokeObjectURL(url);
|
|
3260
|
+
}
|
|
3261
|
+
/** Test seam — exposes the template content/filename without triggering a browser download. */
|
|
3262
|
+
function composeTemplateForTest(opts) {
|
|
3263
|
+
return composeTemplate(samples.value, opts.mode === "prefilled" ? Object.values(schemas.value) : null, opts.mode === "prefilled" ? classes.value : null, {
|
|
3264
|
+
...opts,
|
|
3265
|
+
tokenizedSamples: tokenized.value
|
|
2485
3266
|
});
|
|
2486
|
-
colorIdx++;
|
|
2487
3267
|
}
|
|
2488
|
-
return {
|
|
2489
|
-
groups,
|
|
2490
|
-
metadata,
|
|
2491
|
-
excludedSamples: []
|
|
2492
|
-
};
|
|
2493
|
-
}
|
|
2494
|
-
/** Parses sample names or CSV data to propose group assignments with outlier detection and preview. */
|
|
2495
|
-
function useAutoGroup() {
|
|
2496
|
-
const inputMode = ref("paste");
|
|
2497
|
-
const rawText = ref("");
|
|
2498
|
-
const csvData = ref(null);
|
|
2499
|
-
const delimiter = ref("_");
|
|
2500
|
-
const dominantFieldCount = ref(1);
|
|
2501
|
-
const minFieldCount = ref(1);
|
|
2502
3268
|
const outliers = ref([]);
|
|
2503
|
-
const fields = ref([]);
|
|
2504
|
-
const fieldNames = ref({});
|
|
2505
|
-
const enabledFields = ref(/* @__PURE__ */ new Set());
|
|
2506
|
-
const isTabularMode = computed(() => (inputMode.value === "csv" || inputMode.value === "experiment") && csvData.value !== null);
|
|
2507
|
-
const samples = computed(() => {
|
|
2508
|
-
const data = csvData.value;
|
|
2509
|
-
if (isTabularMode.value && data) return data.rows.map((r) => r[data.sampleColumn]);
|
|
2510
|
-
return rawText.value.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2511
|
-
});
|
|
2512
3269
|
const hasOutliers = computed(() => outliers.value.length > 0);
|
|
2513
|
-
const conformingSamples = computed(() =>
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
const map = /* @__PURE__ */ new Map();
|
|
2519
|
-
for (const o of outliers.value) map.set(o.index, o.action);
|
|
2520
|
-
return map;
|
|
3270
|
+
const conformingSamples = computed(() => samples.value);
|
|
3271
|
+
const dominantFieldCount = computed(() => tokenized.value[0]?.length ?? 0);
|
|
3272
|
+
const minFieldCount = computed(() => {
|
|
3273
|
+
if (tokenized.value.length === 0) return 0;
|
|
3274
|
+
return Math.min(...tokenized.value.map((t) => t.length));
|
|
2521
3275
|
});
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
});
|
|
2528
|
-
const _computedResult = computed(() => {
|
|
2529
|
-
if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) return {
|
|
2530
|
-
groups: [],
|
|
2531
|
-
metadata: [],
|
|
2532
|
-
excludedSamples: []
|
|
2533
|
-
};
|
|
2534
|
-
if (isTabularMode.value && csvData.value) return computeGroupsFromCsv(csvData.value, effectiveColumns.value, enabledFields.value);
|
|
2535
|
-
return computeGroups(samples.value, effectiveColumns.value, enabledFields.value, outlierActions.value, delimiter.value, minFieldCount.value);
|
|
3276
|
+
const fields = computed(() => activeSchema.value?.columns ?? []);
|
|
3277
|
+
const fieldNames = computed(() => {
|
|
3278
|
+
const out = {};
|
|
3279
|
+
for (const c of fields.value) out[c.index] = c.displayName ?? c.name;
|
|
3280
|
+
return out;
|
|
2536
3281
|
});
|
|
2537
|
-
const
|
|
2538
|
-
const
|
|
2539
|
-
const excludedSamples = computed(() => _computedResult.value.excludedSamples);
|
|
3282
|
+
const enabledFields = computed(() => activeSchema.value ? new Set(activeSchema.value.groupBy) : /* @__PURE__ */ new Set());
|
|
3283
|
+
const effectiveColumns = fields;
|
|
2540
3284
|
const allSingletons = computed(() => groups.value.length > 1 && groups.value.every((g) => g.samples.length === 1));
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
const lines = samples.value;
|
|
2548
|
-
if (lines.length === 0) return;
|
|
2549
|
-
const analysis = analyzeDelimiter(lines);
|
|
2550
|
-
delimiter.value = analysis.delimiter;
|
|
2551
|
-
dominantFieldCount.value = analysis.dominantFieldCount;
|
|
2552
|
-
outliers.value = detectOutliers(lines, analysis.delimiter, analysis.dominantFieldCount);
|
|
2553
|
-
for (const outlier of outliers.value) outlier.action = classifyOutlierAction(outlier.sample, analysis.delimiter);
|
|
2554
|
-
const conforming = lines.filter((_, i) => !outliers.value.some((o) => o.index === i));
|
|
2555
|
-
const conformingFieldCounts = conforming.map((s) => s.split(analysis.delimiter).length);
|
|
2556
|
-
minFieldCount.value = conformingFieldCounts.length > 0 ? Math.min(...conformingFieldCounts) : analysis.dominantFieldCount;
|
|
2557
|
-
fields.value = extractColumns(conforming, analysis.delimiter, minFieldCount.value);
|
|
2558
|
-
fieldNames.value = {};
|
|
2559
|
-
const rowCount = conforming.length;
|
|
2560
|
-
enabledFields.value = new Set(fields.value.filter((f) => isUsefulField(f, rowCount)).map((f) => f.index));
|
|
2561
|
-
}
|
|
2562
|
-
function parseCsvInput() {
|
|
2563
|
-
if (!csvData.value) return;
|
|
2564
|
-
const csv = csvData.value;
|
|
2565
|
-
fields.value = csv.columns.filter((c) => c !== csv.sampleColumn).map((col, i) => {
|
|
2566
|
-
const values = csv.rows.map((r) => r[col]);
|
|
2567
|
-
const unique = [...new Set(values)];
|
|
2568
|
-
return {
|
|
2569
|
-
index: i,
|
|
2570
|
-
name: col,
|
|
2571
|
-
originalName: col,
|
|
2572
|
-
uniqueValues: unique,
|
|
2573
|
-
cardinality: unique.length
|
|
2574
|
-
};
|
|
2575
|
-
});
|
|
2576
|
-
outliers.value = [];
|
|
2577
|
-
delimiter.value = csv.delimiter;
|
|
2578
|
-
dominantFieldCount.value = csv.columns.length;
|
|
2579
|
-
fieldNames.value = Object.fromEntries(fields.value.map((f) => [f.index, f.name]));
|
|
2580
|
-
const rowCount = csv.rows.length;
|
|
2581
|
-
enabledFields.value = new Set(fields.value.filter((f) => isUsefulField(f, rowCount)).map((f) => f.index));
|
|
2582
|
-
}
|
|
2583
|
-
function setOutlierAction(index, action) {
|
|
2584
|
-
const outlier = outliers.value.find((o) => o.index === index);
|
|
2585
|
-
if (outlier) {
|
|
2586
|
-
outlier.action = action;
|
|
2587
|
-
outliers.value = [...outliers.value];
|
|
3285
|
+
function setOutlierAction(_, __) {
|
|
3286
|
+
if (!outlierDeprecationWarned) {
|
|
3287
|
+
if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
|
|
3288
|
+
console.warn("[useAutoGroup] setOutlierAction is deprecated AND inactive in the class-first redesign. Use setClassDisposition('iqc' | 'eqc' | …, 'group' | 'overlay' | 'exclude') instead.");
|
|
3289
|
+
outlierDeprecationWarned = true;
|
|
3290
|
+
}
|
|
2588
3291
|
}
|
|
2589
3292
|
}
|
|
2590
|
-
function setAllOutlierActions(
|
|
2591
|
-
|
|
2592
|
-
|
|
3293
|
+
function setAllOutlierActions(__) {
|
|
3294
|
+
if (!outlierDeprecationWarned) {
|
|
3295
|
+
if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
|
|
3296
|
+
console.warn("[useAutoGroup] setAllOutlierActions is deprecated AND inactive in the class-first redesign. Use setClassDisposition('iqc' | 'eqc' | …, 'group' | 'overlay' | 'exclude') instead.");
|
|
3297
|
+
outlierDeprecationWarned = true;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
2593
3300
|
}
|
|
2594
3301
|
function toggleField(index) {
|
|
2595
|
-
|
|
2596
|
-
if (
|
|
2597
|
-
else newSet.add(index);
|
|
2598
|
-
enabledFields.value = newSet;
|
|
3302
|
+
warnDeprecated("toggleField");
|
|
3303
|
+
if (activeClassKey.value) toggleGroupBy(activeClassKey.value, index);
|
|
2599
3304
|
}
|
|
2600
3305
|
function renameField(index, name) {
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
[index]: name
|
|
2604
|
-
};
|
|
3306
|
+
warnDeprecated("renameField");
|
|
3307
|
+
if (activeClassKey.value) setColumnName(activeClassKey.value, index, name);
|
|
2605
3308
|
}
|
|
2606
3309
|
function loadExperimentData(rawData) {
|
|
2607
3310
|
const parsed = extractSamplesFromDesignData(rawData);
|
|
2608
3311
|
if (!parsed) return false;
|
|
2609
3312
|
inputMode.value = "experiment";
|
|
2610
3313
|
csvData.value = parsed;
|
|
2611
|
-
|
|
3314
|
+
sampleTypeHints.value = parsed.sampleTypeHints;
|
|
3315
|
+
parseInput();
|
|
2612
3316
|
return true;
|
|
2613
3317
|
}
|
|
2614
3318
|
function reset() {
|
|
2615
3319
|
rawText.value = "";
|
|
2616
3320
|
csvData.value = null;
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
3321
|
+
classes.value = [];
|
|
3322
|
+
schemas.value = {};
|
|
3323
|
+
activeClassKey.value = "";
|
|
3324
|
+
tokenized.value = [];
|
|
2620
3325
|
outliers.value = [];
|
|
2621
|
-
|
|
2622
|
-
fieldNames.value = {};
|
|
2623
|
-
enabledFields.value = /* @__PURE__ */ new Set();
|
|
3326
|
+
sampleTypeHints.value = [];
|
|
2624
3327
|
}
|
|
2625
3328
|
return {
|
|
2626
3329
|
inputMode,
|
|
2627
3330
|
rawText,
|
|
2628
3331
|
csvData,
|
|
3332
|
+
sampleTypeHints,
|
|
3333
|
+
classes,
|
|
3334
|
+
schemas,
|
|
3335
|
+
activeClassKey,
|
|
2629
3336
|
delimiter,
|
|
2630
3337
|
dominantFieldCount,
|
|
2631
3338
|
minFieldCount,
|
|
2632
|
-
outliers,
|
|
2633
|
-
fields,
|
|
2634
|
-
fieldNames,
|
|
2635
|
-
enabledFields,
|
|
2636
3339
|
samples,
|
|
2637
|
-
|
|
2638
|
-
|
|
3340
|
+
tokenized,
|
|
3341
|
+
activeSchema,
|
|
3342
|
+
suggestions,
|
|
2639
3343
|
groups,
|
|
3344
|
+
qcGroups,
|
|
2640
3345
|
metadata,
|
|
2641
3346
|
excludedSamples,
|
|
2642
|
-
allSingletons,
|
|
2643
3347
|
result,
|
|
3348
|
+
fingerprint,
|
|
3349
|
+
fields,
|
|
3350
|
+
fieldNames,
|
|
3351
|
+
enabledFields,
|
|
2644
3352
|
effectiveColumns,
|
|
3353
|
+
hasOutliers,
|
|
3354
|
+
outliers,
|
|
3355
|
+
conformingSamples,
|
|
3356
|
+
allSingletons,
|
|
3357
|
+
canDownloadTemplate,
|
|
3358
|
+
setClassDisposition,
|
|
3359
|
+
setClassDispositions,
|
|
3360
|
+
toggleGroupBy,
|
|
3361
|
+
setRole,
|
|
3362
|
+
setColumnName,
|
|
3363
|
+
setValueOps,
|
|
3364
|
+
setBinning,
|
|
3365
|
+
mergeColumns,
|
|
3366
|
+
loadFingerprint,
|
|
2645
3367
|
parseInput,
|
|
3368
|
+
downloadTemplate,
|
|
3369
|
+
composeTemplateForTest,
|
|
2646
3370
|
loadExperimentData,
|
|
3371
|
+
reset,
|
|
2647
3372
|
setOutlierAction,
|
|
2648
3373
|
setAllOutlierActions,
|
|
2649
3374
|
toggleField,
|
|
2650
|
-
renameField
|
|
2651
|
-
|
|
3375
|
+
renameField
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
function extractSamplesFromDesignData(rawData) {
|
|
3379
|
+
const designData = unwrapExperimentDesignData(rawData);
|
|
3380
|
+
if (!designData) return null;
|
|
3381
|
+
const samples = designData.samples;
|
|
3382
|
+
if (!Array.isArray(samples) || samples.length === 0) return null;
|
|
3383
|
+
const allConditionKeys = [];
|
|
3384
|
+
const keySet = /* @__PURE__ */ new Set();
|
|
3385
|
+
const sampleTypeHints = [];
|
|
3386
|
+
for (const sample of samples) {
|
|
3387
|
+
const rawType = sample.sample_type;
|
|
3388
|
+
sampleTypeHints.push(typeof rawType === "string" ? rawType.toLowerCase() : void 0);
|
|
3389
|
+
const conditions = sample.conditions;
|
|
3390
|
+
if (conditions && typeof conditions === "object") {
|
|
3391
|
+
for (const key of Object.keys(conditions)) if (!keySet.has(key)) {
|
|
3392
|
+
keySet.add(key);
|
|
3393
|
+
allConditionKeys.push(key);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
if (allConditionKeys.length === 0) return null;
|
|
3398
|
+
return {
|
|
3399
|
+
columns: [
|
|
3400
|
+
"sample_name",
|
|
3401
|
+
...allConditionKeys,
|
|
3402
|
+
"sample_type"
|
|
3403
|
+
],
|
|
3404
|
+
rows: samples.map((s) => {
|
|
3405
|
+
const conditions = s.conditions ?? {};
|
|
3406
|
+
const row = {
|
|
3407
|
+
sample_name: String(s.sample_name ?? ""),
|
|
3408
|
+
sample_type: String(s.sample_type ?? "sample")
|
|
3409
|
+
};
|
|
3410
|
+
for (const key of allConditionKeys) row[key] = conditions[key] ?? "";
|
|
3411
|
+
return row;
|
|
3412
|
+
}),
|
|
3413
|
+
sampleColumn: "sample_name",
|
|
3414
|
+
delimiter: ",",
|
|
3415
|
+
sampleTypeHints
|
|
2652
3416
|
};
|
|
2653
3417
|
}
|
|
2654
3418
|
//#endregion
|
|
@@ -4196,8 +4960,8 @@ function useReagentSeries() {
|
|
|
4196
4960
|
};
|
|
4197
4961
|
}
|
|
4198
4962
|
//#endregion
|
|
4199
|
-
//#region src/composables/
|
|
4200
|
-
var
|
|
4963
|
+
//#region src/composables/protocolTemplateCatalog.ts
|
|
4964
|
+
var BUILT_IN_PROTOCOL_TEMPLATES = [
|
|
4201
4965
|
{
|
|
4202
4966
|
id: "builtin-incubation",
|
|
4203
4967
|
type: "incubation",
|
|
@@ -4537,6 +5301,8 @@ var BUILT_IN_TEMPLATES = [
|
|
|
4537
5301
|
}]
|
|
4538
5302
|
}
|
|
4539
5303
|
];
|
|
5304
|
+
//#endregion
|
|
5305
|
+
//#region src/composables/useProtocolTemplates.ts
|
|
4540
5306
|
var STORAGE_KEY = "mint-custom-protocol-templates";
|
|
4541
5307
|
function isStepTemplate(value) {
|
|
4542
5308
|
if (!value || typeof value !== "object") return false;
|
|
@@ -4562,13 +5328,13 @@ function saveCustomTemplatesToStorage(templates) {
|
|
|
4562
5328
|
function useProtocolTemplates() {
|
|
4563
5329
|
const customTemplates = ref(loadCustomTemplates());
|
|
4564
5330
|
const allTemplates = computed(() => {
|
|
4565
|
-
return [...
|
|
5331
|
+
return [...BUILT_IN_PROTOCOL_TEMPLATES, ...customTemplates.value];
|
|
4566
5332
|
});
|
|
4567
5333
|
function getTemplateByType(type) {
|
|
4568
|
-
return customTemplates.value.find((t) => t.type === type) ||
|
|
5334
|
+
return customTemplates.value.find((t) => t.type === type) || BUILT_IN_PROTOCOL_TEMPLATES.find((t) => t.type === type);
|
|
4569
5335
|
}
|
|
4570
5336
|
function getTemplateById(id) {
|
|
4571
|
-
return customTemplates.value.find((t) => t.id === id) ||
|
|
5337
|
+
return customTemplates.value.find((t) => t.id === id) || BUILT_IN_PROTOCOL_TEMPLATES.find((t) => t.id === id);
|
|
4572
5338
|
}
|
|
4573
5339
|
function saveCustomTemplate(template) {
|
|
4574
5340
|
const newTemplate = {
|
|
@@ -4643,7 +5409,7 @@ function useProtocolTemplates() {
|
|
|
4643
5409
|
return strValue;
|
|
4644
5410
|
}
|
|
4645
5411
|
return {
|
|
4646
|
-
builtInTemplates:
|
|
5412
|
+
builtInTemplates: BUILT_IN_PROTOCOL_TEMPLATES,
|
|
4647
5413
|
customTemplates,
|
|
4648
5414
|
allTemplates,
|
|
4649
5415
|
getTemplateByType,
|
|
@@ -4656,6 +5422,6 @@ function useProtocolTemplates() {
|
|
|
4656
5422
|
};
|
|
4657
5423
|
}
|
|
4658
5424
|
//#endregion
|
|
4659
|
-
export {
|
|
5425
|
+
export { resolveExperimentCode as $, parseCSV as A, useExperimentSelector as B, useExpansionSet as C, hslToHex as D, hexToHsl as E, classKey as F, EXPERIMENT_STATUS_LABELS as G, useDebouncedWatch as H, useWellPlateEditor as I, SORT_OPTIONS as J, EXPERIMENT_STATUS_OPTIONS as K, useDoseCalculator as L, extractSampleOptionsFromDesignData as M, unwrapExperimentDesignData as N, extractSamplesFromDesignData as O, DEFAULT_COLORS as P, getExperimentStatusVariant as Q, APP_EXPERIMENT_KEY as R, resolveCurrentExperimentId as S, deriveShade as T, useApi as U, useRequestSyncState as V, DATE_PRESET_OPTIONS as W, formatExperimentDate as X, datePresetToISO as Y, formatExperimentStatus as Z, useScheduleDrag as _, useReagentSeries as a, useToast as at, currentExperimentFromContext as b, useBioTemplatePresetWorkspace as c, useTextSearch as ct, getBioTemplateComponentProps as d, usePlatformContext as et, toBioTemplateComponentPropsByComponent as f, useExperimentSave as g, useTemplateCollection as h, generateDilutionSeries as i, useTheme as it, extractSampleNamesFromDesignData as j, useAutoGroup as k, useBioTemplatePackWorkspace as l, compareSortValues as lt, useBioTemplateControls as m, DEFAULT_PRESETS as n, evaluateCondition as nt, useGroupAssignment as o, candidateMatchesSearch as ot, useBioTemplateComponents as p, EXPERIMENT_STATUS_VARIANT_MAP as q, DEFAULT_UNITS as r, useForm as rt, useRackEditor as s, normalizeSearchQuery as st, useProtocolTemplates as t, useFormBuilder as tt, useBioTemplateWorkspace as u, useSortedItems as ut, useExperimentSamples as v, useSampleGroups as w, getInjectedPlatformContext as x, useExperimentData as y, useAppExperiment as z };
|
|
4660
5426
|
|
|
4661
|
-
//# sourceMappingURL=useProtocolTemplates-
|
|
5427
|
+
//# sourceMappingURL=useProtocolTemplates-Bm5vyH4_.js.map
|