Skip to content

Commit

Permalink
feat(protocol-designer): create view to browse final liquid state (#2451
Browse files Browse the repository at this point in the history
)

Build the ability to drill down into a labware from the final deck state in order to inspect the
resulting liquid state of your protocol.

Closes #2335
  • Loading branch information
b-cooper authored Oct 12, 2018
1 parent 7555e26 commit 5a436c3
Show file tree
Hide file tree
Showing 24 changed files with 345 additions and 123 deletions.
5 changes: 1 addition & 4 deletions components/src/deck/LabwareLabels.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
font-size: var(--fs-tiny);
text-anchor: middle;
dominant-baseline: middle;
fill: var(--c-white);
}

.tiny_labels {
/* For 384 plate */
font-size: var(--fs-micro);
}

.tiny_labels:nth-child(odd) {
fill: gray;
}
22 changes: 6 additions & 16 deletions components/src/deck/LabwareLabels.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ import {getWellDefsForSVG, wellIsRect} from '@opentrons/shared-data'
import {wellNameSplit} from '../utils'
import styles from './LabwareLabels.css'

type Props = {
labwareType: string,
}

// TODO: Ian 2018-08-14 change these offsets to negative numbers to place outside of Labware
const ROW_OFFSET = 4
const COLUMN_OFFSET = 4
type Props = {labwareType: string}
const ROW_OFFSET = -4
const COLUMN_OFFSET = -4

export default function LabwareLabels (props: Props) {
const {labwareType} = props
// TODO: Ian 2018-06-27 Labels are not aligned nicely, but in new designs they're
// supposed to be moved outside of the Plate anyway
const allWells = getWellDefsForSVG(labwareType)

if (!allWells) {
Expand All @@ -34,15 +28,12 @@ export default function LabwareLabels (props: Props) {

const rowLabels = rowLetters.map(letter => {
const relativeWell = allWells[letter + '1']
const rectOffset = wellIsRect(relativeWell)
? relativeWell.length / 2
: 0
const rectOffset = wellIsRect(relativeWell) ? relativeWell.length / 2 : 0
return (
<text key={letter}
x={ROW_OFFSET}
y={relativeWell.y - rectOffset}
className={cx(styles.plate_label, {[styles.tiny_labels]: rowLetters.length > 8})}
>
className={cx(styles.plate_label, {[styles.tiny_labels]: rowLetters.length > 8})}>
{letter}
</text>
)
Expand All @@ -57,8 +48,7 @@ export default function LabwareLabels (props: Props) {
<text key={number}
x={relativeWell.x + rectOffset}
y={COLUMN_OFFSET}
className={cx(styles.plate_label, {[styles.tiny_labels]: colNumbers.length > 12})}
>
className={cx(styles.plate_label, {[styles.tiny_labels]: colNumbers.length > 12})}>
{number}
</text>
)
Expand Down
11 changes: 11 additions & 0 deletions components/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// @flow
import startCase from 'lodash/startCase'
import {
swatchColors,
MIXED_WELL_COLOR,
} from '@opentrons/components'

export const humanizeLabwareType = startCase

Expand All @@ -21,3 +25,10 @@ export const wellNameSplit = (wellName: string): [string, string] => {

return [letters, numbers]
}

// TODO Ian 2018-07-20: make sure '__air__' or other pseudo-ingredients don't get in here
export const ingredIdsToColor = (groupIds: Array<string>): ?string => {
if (groupIds.length === 0) return null
if (groupIds.length === 1) return swatchColors(Number(groupIds[0]))
return MIXED_WELL_COLOR
}
5 changes: 1 addition & 4 deletions protocol-designer/src/components/IngredientSelectionModal.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @flow
import * as React from 'react'

import SingleLabware from './SingleLabware'
import styles from './IngredientSelectionModal.css'

import SelectablePlate from '../containers/SelectablePlate'
Expand All @@ -18,9 +17,7 @@ export default function IngredientSelectionModal (props: Props) {
<IngredientPropertiesForm />
<LabwareNameEditForm />

<SingleLabware>
<SelectablePlate showLabels selectable />
</SingleLabware>
<SelectablePlate selectable />

<WellSelectionInstructions />
</div>
Expand Down
37 changes: 16 additions & 21 deletions protocol-designer/src/components/SelectablePlate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ import {
getIsTiprack,
} from '@opentrons/shared-data'
import {
swatchColors,
Labware,
Well,
Tip,
LabwareOutline,
LabwareLabels,
MIXED_WELL_COLOR,
ingredIdsToColor,
type Channels,
} from '@opentrons/components'

import {WELL_LABEL_OFFSET} from '../constants'
import SingleLabware from '../components/SingleLabware'
import SelectionRect from '../components/SelectionRect.js'
import type {ContentsByWell} from '../labware-ingred/types'
import type {RectEvent} from '../collision-types'
Expand All @@ -43,19 +44,6 @@ export type Props = {
pipetteChannels?: ?Channels,
}

// TODO Ian 2018-07-20: make sure '__air__' or other pseudo-ingredients don't get in here
function getFillColor (groupIds: Array<string>): ?string {
if (groupIds.length === 0) {
return null
}

if (groupIds.length === 1) {
return swatchColors(Number(groupIds[0]))
}

return MIXED_WELL_COLOR
}

// TODO: BC 2018-10-08 for disconnect hover and select in the IngredSelectionModal from
// redux, use SelectableLabware or similar component there. Also, where we are using this
// component in LabwareOnDeck, with no hover or select capabilities, pull out implicit highlighting
Expand Down Expand Up @@ -102,7 +90,7 @@ export default function SelectablePlate (props: Props) {
wellName={wellName}
highlighted={well.highlighted}
selected={well.selected}
fillColor={getFillColor(well.groupIds)}
fillColor={ingredIdsToColor(well.groupIds)}
svgOffset={{x: 1, y: -3}}
wellDef={allWellDefsByName[wellName]} />
)
Expand All @@ -122,15 +110,22 @@ export default function SelectablePlate (props: Props) {
selected: well.selected,
error: well.error,
maxVolume: well.maxVolume,
fillColor: getFillColor(well.groupIds),
fillColor: ingredIdsToColor(well.groupIds),
}
}

// FIXME: SelectionRect is somehow off by one in the x axis, hence the magic number
return (
<SelectionRect svg {...{onSelectionMove, onSelectionDone}}>
<Labware labwareType={containerType} getWellProps={getWellProps} getTipProps={getTipProps} />
<LabwareLabels labwareType={containerType} />
</SelectionRect>
<SingleLabware showLabels>
<SelectionRect
svg
originXOffset={WELL_LABEL_OFFSET - 1}
originYOffset={WELL_LABEL_OFFSET}
{...{onSelectionMove, onSelectionDone}}>
<Labware labwareType={containerType} getWellProps={getWellProps} getTipProps={getTipProps} />
<LabwareLabels labwareType={containerType} />
</SelectionRect>
</SingleLabware>
)
}
}
7 changes: 5 additions & 2 deletions protocol-designer/src/components/SelectionRect.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type Props = {
onSelectionDone?: (e: MouseEvent, GenericRect) => mixed,
svg?: boolean, // set true if this is an embedded SVG
children?: React.Node,
originXOffset?: number,
originYOffset?: number,
}

type State = {
Expand All @@ -31,6 +33,7 @@ class SelectionRect extends React.Component<Props, State> {
const top = Math.min(yStart, yDynamic)
const width = Math.abs(xDynamic - xStart)
const height = Math.abs(yDynamic - yStart)
const {originXOffset = 0, originYOffset = 0} = this.props

if (this.props.svg) {
// calculate ratio btw clientRect bounding box vs svg parent viewBox
Expand All @@ -47,8 +50,8 @@ class SelectionRect extends React.Component<Props, State> {
const yScale = viewBox.height / clientRect.height

return <rect
x={(left - clientRect.left) * xScale}
y={(top - clientRect.top) * yScale}
x={((left - clientRect.left) * xScale) - originXOffset}
y={((top - clientRect.top) * yScale) - originYOffset}
width={width * xScale}
height={height * yScale}
className={styles.selection_rect}
Expand Down
13 changes: 10 additions & 3 deletions protocol-designer/src/components/SingleLabware.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import styles from './SingleLabware.css'
type Props = {
className?: string,
children?: React.Node,
showLabels?: boolean,
}
export const LABEL_OFFSET = 8

/** Simply wraps SVG components like Plate/SelectablePlate with correct dimensions */
export default function SingleLabware (props: Props) {
const {children, className, showLabels = false} = props
const minX = showLabels ? -LABEL_OFFSET : 0
const minY = showLabels ? -LABEL_OFFSET : 0
const width = showLabels ? SLOT_WIDTH_MM + LABEL_OFFSET : SLOT_WIDTH_MM
const height = showLabels ? SLOT_HEIGHT_MM + LABEL_OFFSET : SLOT_HEIGHT_MM
return (
<div className={cx(styles.single_labware, props.className)}>
<svg viewBox={`0 0 ${SLOT_WIDTH_MM} ${SLOT_HEIGHT_MM}`}>
{props.children}
<div className={cx(styles.single_labware, className)}>
<svg viewBox={`${minX} ${minY} ${width} ${height}`}>
{children}
</svg>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class WellSelectionModal extends React.Component<Props, State> {
</OutlineButton>
</div>

<SingleLabwareWrapper>
<SingleLabwareWrapper showLabels>
<SelectableLabware
highlightedWells={this.state.highlightedWells}
selectedWells={this.state.selectedWells}
Expand Down Expand Up @@ -121,12 +121,13 @@ function mapStateToProps (state: BaseState, ownProps: OP): SP {
// TODO: Ian 2018-07-31 replace with util function, "findIndexOrNull"?
const orderedSteps = steplistSelectors.orderedSteps(state)
const timelineIdx = orderedSteps.findIndex(id => id === stepId)
const allWellContentsForStep = allWellContentsForSteps[timelineIdx]
const formData = steplistSelectors.getUnsavedForm(state)

return {
initialSelectedWells: formData ? formData[ownProps.name] : [],
pipette: pipetteId ? pipetteSelectors.equippedPipettes(state)[pipetteId] : null,
wellContents: labware ? allWellContentsForSteps[timelineIdx][labware.id] : {},
wellContents: labware && allWellContentsForStep ? allWellContentsForStep[labware.id] : {},
containerType: labware ? labware.type : 'missing labware',
}
}
Expand Down
92 changes: 92 additions & 0 deletions protocol-designer/src/components/labware/BrowseLabwareModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// @flow
import * as React from 'react'
import cx from 'classnames'
import map from 'lodash/map'
import {connect} from 'react-redux'

import {getWellDefsForSVG} from '@opentrons/shared-data'

import {
Modal,
Well,
LabwareOutline,
LabwareLabels,
ingredIdsToColor,
} from '@opentrons/components'
import type {BaseState, ThunkDispatch} from '../../types'
import i18n from '../../localization'

import * as wellContentsSelectors from '../../top-selectors/well-contents'
import {selectors} from '../../labware-ingred/reducers'
import * as labwareIngredsActions from '../../labware-ingred/actions'
import type {ContentsByWell} from '../../labware-ingred/types'

import SingleLabwareWrapper from '../SingleLabware'

import modalStyles from '../modals/modal.css'
import styles from './labware.css'

type SP = {
wellContents: ContentsByWell,
labwareType: string,
}
type DP = {
drillUp: () => mixed,
}

type Props = SP & DP

class BrowseLabwareModal extends React.Component<Props> {
handleClose = () => {
this.props.drillUp()
}

render () {
const allWellDefsByName = getWellDefsForSVG(this.props.labwareType)

return (
<Modal
className={modalStyles.modal}
contentsClassName={cx(modalStyles.modal_contents, modalStyles.transparent_content)}
onCloseClick={this.handleClose}>
<SingleLabwareWrapper showLabels>
<g>
<LabwareOutline />
{map(this.props.wellContents, (well, wellName) => (
<Well
selectable
key={wellName}
wellName={wellName}
highlighted={well.highlighted}
selected={well.selected}
fillColor={ingredIdsToColor(well.groupIds)}
svgOffset={{x: 1, y: -3}}
wellDef={allWellDefsByName[wellName]} />
))}
</g>
<LabwareLabels labwareType={this.props.labwareType} inner={false} />
</SingleLabwareWrapper>
<div className={styles.modal_instructions}>{i18n.t('modal.browse_labware.instructions')}</div>
</Modal>
)
}
}

function mapStateToProps (state: BaseState): SP {
const labwareId = selectors.getDrillDownLabwareId(state)
const allLabware = selectors.getLabware(state)
const labware = labwareId && allLabware ? allLabware[labwareId] : null
const allWellContents = wellContentsSelectors.lastValidWellContents(state)
const wellContents = labwareId && allWellContents ? allWellContents[labwareId] : {}

return {
wellContents,
labwareType: labware ? labware.type : 'missing labware',
}
}

function mapDispatchToProps (dispatch: ThunkDispatch<*>): DP {
return {drillUp: () => dispatch(labwareIngredsActions.drillUpFromLabware())}
}

export default connect(mapStateToProps, mapDispatchToProps)(BrowseLabwareModal)
24 changes: 24 additions & 0 deletions protocol-designer/src/components/labware/BrowseLabwareOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @flow
import React from 'react'
import cx from 'classnames'
import styles from './labware.css'

import ClickableText from './ClickableText'

type Props = {
drillDown: () => mixed,
drillUp: () => mixed,
}

function BrowseLabwareOverlay (props: Props) {
return (
<g className={cx(styles.slot_overlay, styles.appear_on_mouseover)}>
<rect className={styles.overlay_panel} />
<ClickableText
onClick={props.drillDown}
iconName='water' y='40%' text='View Liquids' />
</g>
)
}

export default BrowseLabwareOverlay
Loading

0 comments on commit 5a436c3

Please sign in to comment.