From e15e50ca1f5fa810167d9437e257b3c7252c5015 Mon Sep 17 00:00:00 2001 From: AgustinVallejo Date: Fri, 4 Oct 2024 10:17:36 -0500 Subject: [PATCH] Creating a parent class for the histogram, see https://github.com/phetsims/quantum-measurement/issues/22 --- js/coins/view/CoinMeasurementHistogram.ts | 212 +++--------------- js/common/view/QuantumMeasurementHistogram.ts | 204 +++++++++++++++++ 2 files changed, 229 insertions(+), 187 deletions(-) create mode 100644 js/common/view/QuantumMeasurementHistogram.ts diff --git a/js/coins/view/CoinMeasurementHistogram.ts b/js/coins/view/CoinMeasurementHistogram.ts index e0c2138..97fdc37 100644 --- a/js/coins/view/CoinMeasurementHistogram.ts +++ b/js/coins/view/CoinMeasurementHistogram.ts @@ -8,213 +8,51 @@ * @author John Blanco, PhET Interactive Simulations */ -import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; -import DerivedStringProperty from '../../../../axon/js/DerivedStringProperty.js'; -import Dimension2 from '../../../../dot/js/Dimension2.js'; -import Range from '../../../../dot/js/Range.js'; -import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize.js'; -import StringUtils from '../../../../phetcommon/js/util/StringUtils.js'; -import NumberDisplay, { NumberDisplayOptions } from '../../../../scenery-phet/js/NumberDisplay.js'; +import { EmptySelfOptions } from '../../../../phet-core/js/optionize.js'; +import WithRequired from '../../../../phet-core/js/types/WithRequired.js'; import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; -import { AlignBox, AlignGroup, Color, HBox, Line, Node, NodeOptions, Rectangle, Text } from '../../../../scenery/js/imports.js'; +import { Color, NodeOptions, RichText } from '../../../../scenery/js/imports.js'; import { SystemType } from '../../common/model/SystemType.js'; import TwoStateSystemSet from '../../common/model/TwoStateSystemSet.js'; import QuantumMeasurementConstants from '../../common/QuantumMeasurementConstants.js'; +import QuantumMeasurementHistogram from '../../common/view/QuantumMeasurementHistogram.js'; import quantumMeasurement from '../../quantumMeasurement.js'; -import QuantumMeasurementStrings from '../../QuantumMeasurementStrings.js'; import { ClassicalCoinStates } from '../model/ClassicalCoinStates.js'; import { QuantumCoinStates } from '../model/QuantumCoinStates.js'; -import { MAX_COINS } from '../model/CoinsExperimentSceneModel.js'; -import NumberProperty from '../../../../axon/js/NumberProperty.js'; -import WithRequired from '../../../../phet-core/js/types/WithRequired.js'; -import Multilink from '../../../../axon/js/Multilink.js'; type SelfOptions = EmptySelfOptions; export type CoinMeasurementHistogramOptions = SelfOptions & WithRequired; -const HISTOGRAM_SIZE = new Dimension2( 200, 160 ); // size excluding labels at bottom, in screen coordinates -const AXIS_STROKE = Color.BLACK; -const AXIS_LINE_WIDTH = 2; const LABEL_FONT = new PhetFont( { size: 20, weight: 'bold' } ); -const NUMBER_DISPLAY_RANGE = new Range( 0, MAX_COINS ); -const NUMBER_DISPLAY_MAX_WIDTH = HISTOGRAM_SIZE.width / 2 * 0.85; -const HISTOGRAM_BAR_WIDTH = HISTOGRAM_SIZE.width / 6; -const NUMBER_DISPLAY_OPTIONS: NumberDisplayOptions = { - align: 'center', - xMargin: 0, - backgroundStroke: null, - textOptions: { - maxWidth: NUMBER_DISPLAY_MAX_WIDTH, - font: LABEL_FONT - } -}; -export default class CoinMeasurementHistogram extends Node { +export default class CoinMeasurementHistogram extends QuantumMeasurementHistogram { public constructor( coinSet: TwoStateSystemSet, systemType: SystemType, providedOptions: CoinMeasurementHistogramOptions ) { - // Create a Property that controls whether the values should be displayed. - const displayValuesProperty = DerivedProperty.valueEqualsConstant( - coinSet.measurementStateProperty, - 'revealed' - ); - - // Create the X and Y axes. - const xAxis = new Line( 0, 0, HISTOGRAM_SIZE.width, 0, { - stroke: AXIS_STROKE, - lineWidth: AXIS_LINE_WIDTH, - x: -HISTOGRAM_SIZE.width / 2, - centerY: 0 - } ); - - const yAxis = new Line( 0, 0, 0, HISTOGRAM_SIZE.height, { - stroke: AXIS_STROKE, - lineWidth: AXIS_LINE_WIDTH, - centerX: 0, - bottom: 0 - } ); - - - const numberOfCoinsStringProperty = new DerivedStringProperty( - [ coinSet.numberOfActiveSystemsProperty ], - numberOfCoins => StringUtils.fillIn( - QuantumMeasurementStrings.numberOfCoinsPatternStringProperty, - { number: numberOfCoins } - ) - ); - - const numberOfSystemsText = new Text( numberOfCoinsStringProperty, { - font: new PhetFont( 16 ), - centerX: 0, - centerY: yAxis.top * 1.2 - } ); - // Create the labels for the X axis. - const xAxisLeftLabel = new Text( - systemType === 'classical' ? - QuantumMeasurementConstants.CLASSICAL_UP_SYMBOL : - QuantumMeasurementConstants.SPIN_UP_ARROW_CHARACTER, - { - font: LABEL_FONT - } - ); - const xAxisRightLabel = new Text( - systemType === 'classical' ? - QuantumMeasurementConstants.CLASSICAL_DOWN_SYMBOL : - QuantumMeasurementConstants.SPIN_DOWN_ARROW_CHARACTER, - { - font: LABEL_FONT, - fill: Color.MAGENTA - } - ); - - // Create the number Properties for the left and right histogram bars. - const leftNumberProperty = new NumberProperty( 0, { - tandem: providedOptions.tandem.createTandem( 'leftNumberProperty' ) - } ); - const rightNumberProperty = new NumberProperty( 0, { - tandem: providedOptions.tandem.createTandem( 'rightNumberProperty' ) - } ); - - // Define a function to update the left and right number Properties. - const updateNumberProperties = () => { - - const leftTestValue = systemType === 'classical' ? 'heads' : 'up'; - const rightTestValue = systemType === 'classical' ? 'tails' : 'down'; - let leftTotal = 0; - let rightTotal = 0; - - if ( coinSet.measurementStateProperty.value === 'revealed' ) { - _.times( coinSet.numberOfActiveSystemsProperty.value, i => { - if ( coinSet.measuredValues[ i ] === leftTestValue ) { - leftTotal++; - } - else if ( coinSet.measuredValues[ i ] === rightTestValue ) { - rightTotal++; - } - } ); - } - leftNumberProperty.value = leftTotal; - rightNumberProperty.value = rightTotal; - }; - - Multilink.multilink( - [ coinSet.numberOfActiveSystemsProperty, coinSet.measurementStateProperty ], - updateNumberProperties - ); - - coinSet.measuredDataChangedEmitter.addListener( updateNumberProperties ); - - // Create the textual displays for the numbers. - const leftNumberDisplay = new NumberDisplay( leftNumberProperty, NUMBER_DISPLAY_RANGE, NUMBER_DISPLAY_OPTIONS ); - const rightNumberDisplay = new NumberDisplay( rightNumberProperty, NUMBER_DISPLAY_RANGE, NUMBER_DISPLAY_OPTIONS ); - - // Create the histogram bars for the right and left sides. - const maxBarHeight = yAxis.height - leftNumberDisplay.height; - const leftHistogramBar = new Rectangle( 0, 0, HISTOGRAM_BAR_WIDTH, maxBarHeight, { fill: Color.BLACK } ); - const rightHistogramBar = new Rectangle( 0, 0, HISTOGRAM_BAR_WIDTH, maxBarHeight, { fill: Color.MAGENTA } ); - - const leftAlignGroup = new AlignGroup( { matchVertical: false } ); - const rightAlignGroup = new AlignGroup( { matchVertical: false } ); - - const SPACING = HISTOGRAM_SIZE.width / 4; - - const numberDisplays = new HBox( { - children: [ - new AlignBox( leftNumberDisplay, { group: leftAlignGroup } ), - new AlignBox( rightNumberDisplay, { group: rightAlignGroup } ) - ], - centerX: 0, - spacing: SPACING, - top: yAxis.top, - visibleProperty: displayValuesProperty - } ); - const xAxisLabels = new HBox( { - children: [ - new AlignBox( xAxisLeftLabel, { group: leftAlignGroup } ), - new AlignBox( xAxisRightLabel, { group: rightAlignGroup } ) - ], - centerX: 0, - spacing: SPACING, - top: xAxis.centerY + 6 - } ); - const numberBars = new HBox( { - children: [ - new AlignBox( leftHistogramBar, { group: leftAlignGroup } ), - new AlignBox( rightHistogramBar, { group: rightAlignGroup } ) - ], - align: 'bottom', - centerX: 0, - spacing: SPACING, - bottom: xAxis.centerY, - stretch: false - } ); - - leftNumberProperty.link( leftNumber => { - const proportion = leftNumber / coinSet.numberOfActiveSystemsProperty.value; - leftHistogramBar.setRect( 0, 0, HISTOGRAM_BAR_WIDTH, proportion * maxBarHeight ); - numberBars.bottom = xAxis.centerY; - } ); - rightNumberProperty.link( rightNumber => { - const proportion = rightNumber / coinSet.numberOfActiveSystemsProperty.value; - rightHistogramBar.setRect( 0, 0, HISTOGRAM_BAR_WIDTH, proportion * maxBarHeight ); - numberBars.bottom = xAxis.centerY; // TODO: This is being called twice! https://github.com/phetsims/quantum-measurement/issues/22 - } ); - - const options = optionize()( { - children: [ - numberOfSystemsText, - numberBars, - yAxis, - xAxis, - xAxisLabels, - numberDisplays - ] - }, providedOptions ); + const xAxisLabels = [ + new RichText( + systemType === 'classical' ? + QuantumMeasurementConstants.CLASSICAL_UP_SYMBOL : + QuantumMeasurementConstants.SPIN_UP_ARROW_CHARACTER, + { + font: LABEL_FONT + } + ), + new RichText( + systemType === 'classical' ? + QuantumMeasurementConstants.CLASSICAL_DOWN_SYMBOL : + QuantumMeasurementConstants.SPIN_DOWN_ARROW_CHARACTER, + { + font: LABEL_FONT, + fill: Color.MAGENTA + } + ) + ]; - super( options ); + super( coinSet, systemType, xAxisLabels as [RichText, RichText], providedOptions ); } } diff --git a/js/common/view/QuantumMeasurementHistogram.ts b/js/common/view/QuantumMeasurementHistogram.ts new file mode 100644 index 0000000..444ccab --- /dev/null +++ b/js/common/view/QuantumMeasurementHistogram.ts @@ -0,0 +1,204 @@ +// Copyright 2024, University of Colorado Boulder +/** + * QuantumMeasurementHistogram displays a histogram with two bars, one for the quantity of each of the two possible + * outcomes for an experiment where multiple classical or quantum coins are flipped. + * + * @author John Blanco, PhET Interactive Simulations + */ + +import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; +import DerivedStringProperty from '../../../../axon/js/DerivedStringProperty.js'; +import Multilink from '../../../../axon/js/Multilink.js'; +import NumberProperty from '../../../../axon/js/NumberProperty.js'; +import Dimension2 from '../../../../dot/js/Dimension2.js'; +import Range from '../../../../dot/js/Range.js'; +import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize.js'; +import WithRequired from '../../../../phet-core/js/types/WithRequired.js'; +import StringUtils from '../../../../phetcommon/js/util/StringUtils.js'; +import NumberDisplay, { NumberDisplayOptions } from '../../../../scenery-phet/js/NumberDisplay.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import { AlignBox, AlignGroup, Color, HBox, Line, Node, NodeOptions, Rectangle, RichText, Text } from '../../../../scenery/js/imports.js'; +import { ClassicalCoinStates } from '../../coins/model/ClassicalCoinStates.js'; +import { MAX_COINS } from '../../coins/model/CoinsExperimentSceneModel.js'; +import { QuantumCoinStates } from '../../coins/model/QuantumCoinStates.js'; +import { SystemType } from '../../common/model/SystemType.js'; +import TwoStateSystemSet from '../../common/model/TwoStateSystemSet.js'; +import quantumMeasurement from '../../quantumMeasurement.js'; +import QuantumMeasurementStrings from '../../QuantumMeasurementStrings.js'; + +type SelfOptions = EmptySelfOptions; +export type QuantumMeasurementHistogramOptions = SelfOptions & WithRequired; + +const HISTOGRAM_SIZE = new Dimension2( 200, 160 ); // size excluding labels at bottom, in screen coordinates +const AXIS_STROKE = Color.BLACK; +const AXIS_LINE_WIDTH = 2; +const LABEL_FONT = new PhetFont( { size: 20, weight: 'bold' } ); +const NUMBER_DISPLAY_RANGE = new Range( 0, MAX_COINS ); +const NUMBER_DISPLAY_MAX_WIDTH = HISTOGRAM_SIZE.width / 2 * 0.85; +const HISTOGRAM_BAR_WIDTH = HISTOGRAM_SIZE.width / 6; +const NUMBER_DISPLAY_OPTIONS: NumberDisplayOptions = { + align: 'center', + xMargin: 0, + backgroundStroke: null, + textOptions: { + maxWidth: NUMBER_DISPLAY_MAX_WIDTH, + font: LABEL_FONT + } +}; + +export default class QuantumMeasurementHistogram extends Node { + + public constructor( coinSet: TwoStateSystemSet, + systemType: SystemType, + providedXAxisLabels: [ RichText, RichText ], + providedOptions: QuantumMeasurementHistogramOptions ) { + + // Create a Property that controls whether the values should be displayed. + const displayValuesProperty = DerivedProperty.valueEqualsConstant( + coinSet.measurementStateProperty, + 'revealed' + ); + + // Create the X and Y axes. + const xAxis = new Line( 0, 0, HISTOGRAM_SIZE.width, 0, { + stroke: AXIS_STROKE, + lineWidth: AXIS_LINE_WIDTH, + x: -HISTOGRAM_SIZE.width / 2, + centerY: 0 + } ); + + const yAxis = new Line( 0, 0, 0, HISTOGRAM_SIZE.height, { + stroke: AXIS_STROKE, + lineWidth: AXIS_LINE_WIDTH, + centerX: 0, + bottom: 0 + } ); + + + const numberOfCoinsStringProperty = new DerivedStringProperty( + [ coinSet.numberOfActiveSystemsProperty ], + numberOfCoins => StringUtils.fillIn( + QuantumMeasurementStrings.numberOfCoinsPatternStringProperty, + { number: numberOfCoins } + ) + ); + + const numberOfSystemsText = new Text( numberOfCoinsStringProperty, { + font: new PhetFont( 16 ), + centerX: 0, + centerY: yAxis.top * 1.2 + } ); + + // Create the labels for the X axis. + const xAxisLeftLabel = providedXAxisLabels[ 0 ]; + const xAxisRightLabel = providedXAxisLabels[ 1 ]; + + // Create the number Properties for the left and right histogram bars. + const leftNumberProperty = new NumberProperty( 0, { + tandem: providedOptions.tandem.createTandem( 'leftNumberProperty' ) + } ); + const rightNumberProperty = new NumberProperty( 0, { + tandem: providedOptions.tandem.createTandem( 'rightNumberProperty' ) + } ); + + // Define a function to update the left and right number Properties. + const updateNumberProperties = () => { + + const leftTestValue = systemType === 'classical' ? 'heads' : 'up'; + const rightTestValue = systemType === 'classical' ? 'tails' : 'down'; + let leftTotal = 0; + let rightTotal = 0; + + if ( coinSet.measurementStateProperty.value === 'revealed' ) { + _.times( coinSet.numberOfActiveSystemsProperty.value, i => { + if ( coinSet.measuredValues[ i ] === leftTestValue ) { + leftTotal++; + } + else if ( coinSet.measuredValues[ i ] === rightTestValue ) { + rightTotal++; + } + } ); + } + leftNumberProperty.value = leftTotal; + rightNumberProperty.value = rightTotal; + }; + + Multilink.multilink( + [ coinSet.numberOfActiveSystemsProperty, coinSet.measurementStateProperty ], + updateNumberProperties + ); + + coinSet.measuredDataChangedEmitter.addListener( updateNumberProperties ); + + // Create the textual displays for the numbers. + const leftNumberDisplay = new NumberDisplay( leftNumberProperty, NUMBER_DISPLAY_RANGE, NUMBER_DISPLAY_OPTIONS ); + const rightNumberDisplay = new NumberDisplay( rightNumberProperty, NUMBER_DISPLAY_RANGE, NUMBER_DISPLAY_OPTIONS ); + + // Create the histogram bars for the right and left sides. + const maxBarHeight = yAxis.height - leftNumberDisplay.height; + const leftHistogramBar = new Rectangle( 0, 0, HISTOGRAM_BAR_WIDTH, maxBarHeight, { fill: Color.BLACK } ); + const rightHistogramBar = new Rectangle( 0, 0, HISTOGRAM_BAR_WIDTH, maxBarHeight, { fill: Color.MAGENTA } ); + + const leftAlignGroup = new AlignGroup( { matchVertical: false } ); + const rightAlignGroup = new AlignGroup( { matchVertical: false } ); + + const SPACING = HISTOGRAM_SIZE.width / 4; + + const numberDisplays = new HBox( { + children: [ + new AlignBox( leftNumberDisplay, { group: leftAlignGroup } ), + new AlignBox( rightNumberDisplay, { group: rightAlignGroup } ) + ], + centerX: 0, + spacing: SPACING, + top: yAxis.top, + visibleProperty: displayValuesProperty + } ); + const xAxisLabels = new HBox( { + children: [ + new AlignBox( xAxisLeftLabel, { group: leftAlignGroup } ), + new AlignBox( xAxisRightLabel, { group: rightAlignGroup } ) + ], + centerX: 0, + spacing: SPACING, + top: xAxis.centerY + 6 + } ); + const numberBars = new HBox( { + children: [ + new AlignBox( leftHistogramBar, { group: leftAlignGroup } ), + new AlignBox( rightHistogramBar, { group: rightAlignGroup } ) + ], + align: 'bottom', + centerX: 0, + spacing: SPACING, + bottom: xAxis.centerY, + stretch: false + } ); + + leftNumberProperty.link( leftNumber => { + const proportion = leftNumber / coinSet.numberOfActiveSystemsProperty.value; + leftHistogramBar.setRect( 0, 0, HISTOGRAM_BAR_WIDTH, proportion * maxBarHeight ); + numberBars.bottom = xAxis.centerY; + } ); + rightNumberProperty.link( rightNumber => { + const proportion = rightNumber / coinSet.numberOfActiveSystemsProperty.value; + rightHistogramBar.setRect( 0, 0, HISTOGRAM_BAR_WIDTH, proportion * maxBarHeight ); + numberBars.bottom = xAxis.centerY; // TODO: This is being called twice! https://github.com/phetsims/quantum-measurement/issues/22 + } ); + + const options = optionize()( { + children: [ + numberOfSystemsText, + numberBars, + yAxis, + xAxis, + xAxisLabels, + numberDisplays + ] + }, providedOptions ); + + super( options ); + } +} + +quantumMeasurement.register( 'QuantumMeasurementHistogram', QuantumMeasurementHistogram ); \ No newline at end of file