cubevis 0.5.23__py3-none-any.whl → 0.5.25__py3-none-any.whl

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.

Potentially problematic release.


This version of cubevis might be problematic. Click here for more details.

@@ -45,12 +45,13 @@ import asyncio
45
45
  import shutil
46
46
  import websockets
47
47
  from os.path import basename, abspath, exists, join
48
+ import numpy as np
48
49
  from uuid import uuid4
49
50
  from html import escape as html_escape
50
51
  from contextlib import asynccontextmanager
51
52
  from bokeh.models import Button, TextInput, Checkbox, Div, LinearAxis, CustomJS, Spacer, Span, HoverTool, DataRange1d, Step, InlineStyleSheet
52
53
  from bokeh.events import ModelEvent, MouseEnter
53
- from bokeh.models import TabPanel, Tabs
54
+ from bokeh.models import TabPanel, Tabs, Range1d
54
55
  from bokeh.plotting import ColumnDataSource, figure, show
55
56
  from bokeh.layouts import column, row, layout
56
57
  from bokeh.io import reset_output as reset_bokeh_output, output_notebook
@@ -278,6 +279,23 @@ class InteractiveCleanUI:
278
279
  imdetails['path']['residual'] = join( output_dir, self._clean['gclean_paths'][imid]['residualname'] )
279
280
  imdetails['path']['mask'] = join( output_dir, self._clean['gclean_paths'][imid]['maskname'] )
280
281
 
282
+ ###
283
+ ### There is one set of tclean controls for all images/outlier/etc. because
284
+ ### in the final version gclean will handle the iterations for all fields...
285
+ ###
286
+ cwidth = 64
287
+ cheight = 40
288
+ self._control['iteration'] = { }
289
+ self._control['iteration']['continue'] = TipButton( max_width=cwidth, max_height=cheight, name='continue',
290
+ icon=svg_icon(icon_name="iclean-continue", size=18),
291
+ tooltip=Tooltip( content=HTML( '''Stop after <b>one major cycle</b> or when any stopping criteria is met.''' ), position='left') )
292
+ self._control['iteration']['finish'] = TipButton( max_width=cwidth, max_height=cheight, name='finish',
293
+ icon=svg_icon(icon_name="iclean-finish", size=18),
294
+ tooltip=Tooltip( content=HTML( '''<b>Continue</b> until some stopping criteria is met.''' ), position='left') )
295
+ self._control['iteration']['stop'] = TipButton( button_type="danger", max_width=cwidth, max_height=cheight, name='stop',
296
+ icon=svg_icon(icon_name="iclean-stop", size=18),
297
+ tooltip=Tooltip( content=HTML( '''<p>Clicking a <font color="red">red</font> stop button will cause this tab to close and control will return to Python.<p>Clicking an <font color="orange">orange</font> stop button will cause <tt>tclean</tt> to stop after the current major cycle.''' ), position='left' ) )
298
+
281
299
  for idx, (imid, imdetails) in enumerate(self._clean_targets.items( )):
282
300
  imdetails['gui'] = { }
283
301
 
@@ -290,8 +308,13 @@ class InteractiveCleanUI:
290
308
  ###
291
309
  imdetails['gui']['cube'] = CubeMask( imdetails['path']['residual'], mask=imdetails['path']['mask'], abort=self._abort_handler,
292
310
  init_script=CustomJS( args=dict( initial_convergence_state=self._init_values["convergence_state"],
311
+ clean_ctrl=self._control['iteration'],
293
312
  name=imid ),
294
- code='''document._casa_convergence_data = initial_convergence_state''' )
313
+ code='''document._casa_convergence_data = initial_convergence_state
314
+ clean_ctrl.continue.disable_add_sub = this.disable_add_sub.values
315
+ clean_ctrl.finish.disable_add_sub = this.disable_add_sub.values
316
+ clean_ctrl.stop.disable_add_sub = this.disable_add_sub.values
317
+ this.disable_add_sub.values.message = "cannot modify mask during cleaning"''' )
295
318
  if idx == 0 else None )
296
319
 
297
320
  ###
@@ -343,57 +366,159 @@ class InteractiveCleanUI:
343
366
  </div>'''
344
367
 
345
368
  hover = HoverTool( tooltips=TOOLTIPS )
346
- imdetails['gui']['convergence'] = figure( sizing_mode=sizing_mode, y_axis_location="right",
347
- tools=[ hover ], toolbar_location=None, **kw )
348
369
 
349
- if orient == 'vertical':
350
- imdetails['gui']['convergence'].yaxis.axis_label='Iteration (cycle threshold dotted red)'
351
- imdetails['gui']['convergence'].xaxis.axis_label='Peak Residual'
352
- imdetails['gui']['convergence'].extra_x_ranges = { 'residual_range': DataRange1d( follow='end' ),
353
- 'flux_range': DataRange1d( follow='end' ) }
354
-
355
- imdetails['gui']['convergence'].step( 'values', 'iterations', source=imdetails['converge-data']['cyclethreshold'],
356
- line_color='red', x_range_name='residual_range', line_dash='dotted', line_width=2 )
357
- imdetails['gui']['convergence'].line( 'values', 'iterations', source=imdetails['converge-data']['residual'],
358
- line_color=self._converge_color['residual'], x_range_name='residual_range' )
359
- imdetails['gui']['convergence'].scatter( 'values', 'iterations', source=imdetails['converge-data']['residual'],
360
- color=self._converge_color['residual'], x_range_name='residual_range',size=10 )
361
- imdetails['gui']['convergence'].line( 'values', 'iterations', source=imdetails['converge-data']['flux'],
362
- line_color=self._converge_color['flux'], x_range_name='flux_range' )
363
- imdetails['gui']['convergence'].scatter( 'values', 'iterations', source=imdetails['converge-data']['flux'],
364
- color=self._converge_color['flux'], x_range_name='flux_range', size=10 )
365
-
366
- imdetails['gui']['convergence'].add_layout( LinearAxis( x_range_name='flux_range', axis_label='Total Flux',
367
- axis_line_color=self._converge_color['flux'],
368
- major_label_text_color=self._converge_color['flux'],
369
- axis_label_text_color=self._converge_color['flux'],
370
- major_tick_line_color=self._converge_color['flux'],
371
- minor_tick_line_color=self._converge_color['flux'] ), 'above')
370
+ ###
371
+ ### Data source that will be used for updating the convergence plot
372
+ ###
373
+ stokes = 0
374
+ convergence = imdetails['converge']['chan'][0][stokes]
375
+ imdetails['converge-data'] = { }
376
+ imdetails['converge-data']['flux'] = ColumnDataSource( data=dict( values=convergence['modelFlux'], iterations=convergence['iterations'],
377
+ cyclethreshold=convergence['cycleThresh'],
378
+ stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
379
+ type=['flux'] * len(convergence['iterations']) ) )
380
+ imdetails['converge-data']['residual'] = ColumnDataSource( data=dict( values=convergence['peakRes'], iterations=convergence['iterations'],
381
+ cyclethreshold=convergence['cycleThresh'],
382
+ stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
383
+ type=['residual'] * len(convergence['iterations'])) )
384
+ imdetails['converge-data']['cyclethreshold'] = ColumnDataSource( data=dict( values=convergence['cycleThresh'], iterations=convergence['iterations'] ) )
385
+
386
+ # Calculate explicit ranges for each dataset
387
+ flux_values = convergence['modelFlux']
388
+ residual_values = convergence['peakRes']
389
+ iterations = convergence['iterations']
390
+ cyclethresh_values = convergence['cycleThresh']
391
+
392
+ # Calculate ranges with padding
393
+ if len(flux_values) > 0:
394
+ flux_min, flux_max = np.min(flux_values), np.max(flux_values)
395
+ flux_padding = max((flux_max - flux_min) * 0.1, abs(flux_max * 0.05)) if flux_max != flux_min else abs(flux_max * 0.1)
396
+ else:
397
+ flux_min, flux_max, flux_padding = 0, 1, 0.1
372
398
 
399
+ if len(residual_values) > 0:
400
+ residual_min, residual_max = np.min(residual_values), np.max(residual_values)
401
+ residual_padding = max((residual_max - residual_min) * 0.1, abs(residual_max * 0.05)) if residual_max != residual_min else abs(residual_max * 0.1)
373
402
  else:
374
- imdetails['gui']['convergence'].xaxis.axis_label='Iteration (cycle threshold dotted red)'
375
- imdetails['gui']['convergence'].yaxis.axis_label='Peak Residual'
376
- imdetails['gui']['convergence'].extra_y_ranges = { 'residual_range': DataRange1d( follow='end' ),
377
- 'flux_range': DataRange1d( follow='end' ) }
378
-
379
- imdetails['gui']['convergence'].step( 'iterations', 'values', source=imdetails['converge-data']['cyclethreshold'],
380
- line_color='red', y_range_name='residual_range', line_dash='dotted', line_width=2 )
381
- imdetails['gui']['convergence'].line( 'iterations', 'values', source=imdetails['converge-data']['residual'],
382
- line_color=self._converge_color['residual'], y_range_name='residual_range' )
383
- imdetails['gui']['convergence'].scatter( 'iterations', 'values', source=imdetails['converge-data']['residual'],
384
- color=self._converge_color['residual'], y_range_name='residual_range',size=10 )
385
- imdetails['gui']['convergence'].line( 'iterations', 'values', source=imdetails['converge-data']['flux'],
386
- line_color=self._converge_color['flux'], y_range_name='flux_range' )
387
- imdetails['gui']['convergence'].scatter( 'iterations', 'values', source=imdetails['converge-data']['flux'],
388
- color=self._converge_color['flux'], y_range_name='flux_range', size=10 )
389
-
390
- imdetails['gui']['convergence'].add_layout( LinearAxis( y_range_name='flux_range', axis_label='Total Flux',
391
- axis_line_color=self._converge_color['flux'],
392
- major_label_text_color=self._converge_color['flux'],
393
- axis_label_text_color=self._converge_color['flux'],
394
- major_tick_line_color=self._converge_color['flux'],
395
- minor_tick_line_color=self._converge_color['flux'] ), 'right')
403
+ residual_min, residual_max, residual_padding = 0, 1, 0.1
404
+
405
+ # Create Range1d objects
406
+ flux_range = Range1d(start=flux_min - flux_padding, end=flux_max + flux_padding)
407
+ residual_range = Range1d(start=residual_min - residual_padding, end=residual_max + residual_padding)
408
+
409
+ # Ensure ranges are valid (non-zero span)
410
+ if flux_range.end - flux_range.start < 1e-10:
411
+ center = flux_range.start
412
+ if abs(center) < 1e-10: # If center is essentially 0
413
+ flux_range.start = -0.1
414
+ flux_range.end = 1.0
415
+ else:
416
+ span = max(abs(center * 0.2), 0.1)
417
+ flux_range.start = center - span
418
+ flux_range.end = center + span
419
+
420
+ if residual_range.end - residual_range.start < 1e-10:
421
+ center = residual_range.start
422
+ span = max(abs(center * 0.2), 0.1)
423
+ residual_range.start = center - span
424
+ residual_range.end = center + span
425
+
426
+ # ORIENTATION CONFIGURATION - this eliminates the conditional branches
427
+ config = {
428
+ 'vertical': {
429
+ 'iteration_axis': 'y',
430
+ 'data_axis': 'x',
431
+ 'main_axis_label': 'Iteration (cycle threshold dotted red)',
432
+ 'residual_axis_label': 'Peak Residual',
433
+ 'flux_axis_label': 'Total Flux',
434
+ 'residual_axis_pos': 'above',
435
+ 'flux_axis_pos': 'above',
436
+ 'extra_ranges_key': 'extra_x_ranges',
437
+ 'glyph_coords': ('values', 'iterations'), # (x, y)
438
+ 'range_name_param': 'x_range_name'
439
+ },
440
+ 'horizontal': {
441
+ 'iteration_axis': 'x',
442
+ 'data_axis': 'y',
443
+ 'main_axis_label': 'Iteration (cycle threshold dotted red)',
444
+ 'residual_axis_label': 'Peak Residual',
445
+ 'flux_axis_label': 'Total Flux',
446
+ 'residual_axis_pos': 'right',
447
+ 'flux_axis_pos': 'right',
448
+ 'extra_ranges_key': 'extra_y_ranges',
449
+ 'glyph_coords': ('iterations', 'values'), # (x, y)
450
+ 'range_name_param': 'y_range_name'
451
+ }
452
+ }
396
453
 
454
+ cfg = config[orient]
455
+
456
+ # Create figure with no default axes; to control the axes they must
457
+ # be explicitly set to None
458
+ imdetails['gui']['convergence'] = figure( sizing_mode=sizing_mode,
459
+ x_axis_location=None, y_axis_location=None,
460
+ tools=[ hover ], toolbar_location=None, **kw )
461
+
462
+ # Set up extra ranges
463
+ setattr(imdetails['gui']['convergence'], cfg['extra_ranges_key'], {
464
+ 'residual_range': residual_range,
465
+ 'flux_range': flux_range
466
+ })
467
+
468
+ # Store references for JavaScript updates
469
+ imdetails['converge-ranges'] = {
470
+ 'residual_range': residual_range,
471
+ 'flux_range': flux_range
472
+ }
473
+
474
+ # Create main iteration axis
475
+ main_axis = LinearAxis(axis_label=cfg['main_axis_label'])
476
+ main_axis_pos = 'below' if cfg['iteration_axis'] == 'x' else 'left'
477
+ imdetails['gui']['convergence'].add_layout(main_axis, main_axis_pos)
478
+
479
+ # Create glyphs using configuration
480
+ x_coord, y_coord = cfg['glyph_coords']
481
+ range_param = {cfg['range_name_param']: 'residual_range'}
482
+
483
+ imdetails['gui']['convergence'].step( x_coord, y_coord, source=imdetails['converge-data']['cyclethreshold'],
484
+ line_color='red', line_dash='dotted', line_width=2, **range_param )
485
+ imdetails['gui']['convergence'].line( x_coord, y_coord, source=imdetails['converge-data']['residual'],
486
+ line_color=self._converge_color['residual'], **range_param )
487
+ imdetails['gui']['convergence'].scatter( x_coord, y_coord, source=imdetails['converge-data']['residual'],
488
+ color=self._converge_color['residual'], size=10, **range_param )
489
+
490
+ # Flux glyphs
491
+ range_param = {cfg['range_name_param']: 'flux_range'}
492
+ imdetails['gui']['convergence'].line( x_coord, y_coord, source=imdetails['converge-data']['flux'],
493
+ line_color=self._converge_color['flux'], **range_param )
494
+ imdetails['gui']['convergence'].scatter( x_coord, y_coord, source=imdetails['converge-data']['flux'],
495
+ color=self._converge_color['flux'], size=10, **range_param )
496
+
497
+ # Create and style residual axis
498
+ residual_axis_param = {cfg['range_name_param'].replace('_name', '_name'): 'residual_range'}
499
+ residual_axis = LinearAxis(axis_label=cfg['residual_axis_label'], **residual_axis_param)
500
+ residual_axis.axis_line_color = self._converge_color['residual']
501
+ residual_axis.major_label_text_color = self._converge_color['residual']
502
+ residual_axis.axis_label_text_color = self._converge_color['residual']
503
+ residual_axis.major_tick_line_color = self._converge_color['residual']
504
+ residual_axis.minor_tick_line_color = self._converge_color['residual']
505
+ imdetails['gui']['convergence'].add_layout(residual_axis, cfg['residual_axis_pos'])
506
+
507
+ # Create and style flux axis
508
+ flux_axis_param = {cfg['range_name_param'].replace('_name', '_name'): 'flux_range'}
509
+ flux_axis = LinearAxis(axis_label=cfg['flux_axis_label'], **flux_axis_param)
510
+ flux_axis.axis_line_color = self._converge_color['flux']
511
+ flux_axis.major_label_text_color = self._converge_color['flux']
512
+ flux_axis.axis_label_text_color = self._converge_color['flux']
513
+ flux_axis.major_tick_line_color = self._converge_color['flux']
514
+ flux_axis.minor_tick_line_color = self._converge_color['flux']
515
+ imdetails['gui']['convergence'].add_layout(flux_axis, cfg['flux_axis_pos'])
516
+
517
+ # Store axis references for JavaScript access
518
+ imdetails['converge-axes'] = {
519
+ 'residual_axis': residual_axis,
520
+ 'flux_axis': flux_axis
521
+ }
397
522
 
398
523
  def _launch_gui( self ):
399
524
  '''create and show GUI
@@ -485,24 +610,6 @@ class InteractiveCleanUI:
485
610
  #print("%s: %s" % ( btn, self._clean_ids[btn] ) )
486
611
  self._pipe['control'].register( self._clean_ids[btn], clean_handler )
487
612
 
488
-
489
- ###
490
- ### There is one set of tclean controls for all images/outlier/etc. because
491
- ### in the final version gclean will handle the iterations for all fields...
492
- ###
493
- cwidth = 64
494
- cheight = 40
495
- self._control['iteration'] = { }
496
- self._control['iteration']['continue'] = TipButton( max_width=cwidth, max_height=cheight, name='continue',
497
- icon=svg_icon(icon_name="iclean-continue", size=18),
498
- tooltip=Tooltip( content=HTML( '''Stop after <b>one major cycle</b> or when any stopping criteria is met.''' ), position='left') )
499
- self._control['iteration']['finish'] = TipButton( max_width=cwidth, max_height=cheight, name='finish',
500
- icon=svg_icon(icon_name="iclean-finish", size=18),
501
- tooltip=Tooltip( content=HTML( '''<b>Continue</b> until some stopping criteria is met.''' ), position='left') )
502
- self._control['iteration']['stop'] = TipButton( button_type="danger", max_width=cwidth, max_height=cheight, name='stop',
503
- icon=svg_icon(icon_name="iclean-stop", size=18),
504
- tooltip=Tooltip( content=HTML( '''<p>Clicking a <font color="red">red</font> stop button will cause this tab to close and control will return to Python.<p>Clicking an <font color="orange">orange</font> stop button will cause <tt>tclean</tt> to stop after the current major cycle.''' ), position='left' ) )
505
-
506
613
  ###
507
614
  ### The single SHARED help button will be supplied by the first CubeMask...
508
615
  ###
@@ -534,23 +641,6 @@ class InteractiveCleanUI:
534
641
 
535
642
  self._clean['converge']['pipe'].register( self._clean['converge']['id'], convergence_handler )
536
643
 
537
- ###
538
- ### Data source that will be used for updating the convergence plot
539
- ###
540
- stokes = 0
541
- convergence = imdetails['converge']['chan'][0][stokes]
542
- imdetails['converge-data'] = { }
543
- imdetails['converge-data']['flux'] = ColumnDataSource( data=dict( values=convergence['modelFlux'], iterations=convergence['iterations'],
544
- cyclethreshold=convergence['cycleThresh'],
545
- stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
546
- type=['flux'] * len(convergence['iterations']) ) )
547
- imdetails['converge-data']['residual'] = ColumnDataSource( data=dict( values=convergence['peakRes'], iterations=convergence['iterations'],
548
- cyclethreshold=convergence['cycleThresh'],
549
- stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
550
- type=['residual'] * len(convergence['iterations'])) )
551
- imdetails['converge-data']['cyclethreshold'] = ColumnDataSource( data=dict( values=convergence['cycleThresh'], iterations=convergence['iterations'] ) )
552
-
553
-
554
644
  ###
555
645
  ### help page for cube interactions
556
646
  ###
@@ -585,8 +675,12 @@ class InteractiveCleanUI:
585
675
  imdetails['gui']['image']['src'] = imdetails['gui']['cube'].js_obj( )
586
676
  imdetails['gui']['image']['fig'] = imdetails['gui']['cube'].image( grid=False, height_policy='max', width_policy='max',
587
677
  channelcb=CustomJS( args=dict( img_state={ 'src': imdetails['gui']['image']['src'],
588
- 'flux': imdetails['converge-data']['flux'],
589
- 'residual': imdetails['converge-data']['residual'],
678
+ 'flux': { 'source': imdetails['converge-data']['flux'],
679
+ 'axis': imdetails['converge-axes']['flux_axis'],
680
+ 'range': imdetails['converge-ranges']['flux_range'] },
681
+ 'residual': { 'source': imdetails['converge-data']['residual'],
682
+ 'axis': imdetails['converge-axes']['residual_axis'],
683
+ 'range': imdetails['converge-ranges']['residual_range'] },
590
684
  'cyclethreshold': imdetails['converge-data']['cyclethreshold'] },
591
685
  imid=imid,
592
686
  ctrl={ 'converge': self._clean['converge'] },
@@ -695,6 +789,7 @@ class InteractiveCleanUI:
695
789
  margin=(-1, 0, -10, 0), button_type='light',
696
790
  stylesheets=[ InlineStyleSheet( css='''.bk-btn { border: 0px solid #ccc; padding: 0 var(--padding-vertical) var(--padding-horizontal); margin-top: 3px; }''' ) ] )
697
791
 
792
+
698
793
  self._control['iteration']['cb'] = CustomJS( args=dict( images_state={ k: { 'status': v['gui']['stopcode'],
699
794
  'automask': v['gui']['params']['automask'],
700
795
  'iteration': v['gui']['params']['iteration'],
@@ -702,9 +797,13 @@ class InteractiveCleanUI:
702
797
  'src': v['gui']['cube'].js_obj( ),
703
798
  'spectrum': v['gui']['spectrum'],
704
799
  'src': v['gui']['image']['src'],
705
- 'flux': v['converge-data']['flux'],
800
+ 'flux': { 'source': v['converge-data']['flux'],
801
+ 'axis': v['converge-axes']['flux_axis'],
802
+ 'range': v['converge-ranges']['flux_range'] },
706
803
  'cyclethreshold': v['converge-data']['cyclethreshold'],
707
- 'residual': v['converge-data']['residual'],
804
+ 'residual': { 'source': v['converge-data']['residual'],
805
+ 'axis': v['converge-axes']['residual_axis'],
806
+ 'range': v['converge-ranges']['residual_range'] },
708
807
  'navi': { 'slider': v['gui']['slider'],
709
808
  'goto': v['gui']['goto'],
710
809
  ## it doesn't seem like pixel tracking must be disabled
@@ -1216,7 +1315,52 @@ class InteractiveCleanUI:
1216
1315
  ### -- The "Insert here ..." code seems to be called when when the stokes plane is changed --
1217
1316
  ### -- but there have been no tclean iterations yet... --
1218
1317
  ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1219
- 'update-converge': '''function update_convergence_single( target, data ) {
1318
+ 'update-converge': '''function updateAxisRange(source, range_obj, axis_obj, axis_name) {
1319
+ const data = source.data;
1320
+ const values = data['values'];
1321
+
1322
+ if (values.length === 0) return;
1323
+
1324
+ // Calculate data bounds
1325
+ const min_val = Math.min(...values);
1326
+ const max_val = Math.max(...values);
1327
+
1328
+ // Add padding (10% of range, with minimum padding)
1329
+ const span = max_val - min_val;
1330
+ const padding = max_val != min_val ? Math.max(span * 0.1, Math.abs(max_val * 0.02)) : Math.abs(max_val * 0.1);
1331
+
1332
+ // Update range
1333
+ range_obj.start = min_val - padding;
1334
+ range_obj.end = max_val + padding;
1335
+
1336
+ // Ensure ranges are valid (non-zero span)
1337
+ if ( range_obj.end - range_obj.start < 1e-10 ) {
1338
+ const center = range_obj.start
1339
+ if ( Math.abs(center) < 1e-10 ) {
1340
+ // If center is essentially 0
1341
+ range_obj.start = -0.1
1342
+ range_obj.end = 1.0
1343
+ } else {
1344
+ const span = Math.max(Math.abs(center * 0.2), 0.1)
1345
+ range_obj.start = center - span
1346
+ range_obj.end = center + span
1347
+ }
1348
+ }
1349
+
1350
+ // Optional: Update tick density
1351
+ if (axis_obj && axis_obj.ticker) {
1352
+ const new_span = range_obj.end - range_obj.start;
1353
+ if (new_span < 10) {
1354
+ axis_obj.ticker.desired_num_ticks = 8;
1355
+ } else if (new_span < 100) {
1356
+ axis_obj.ticker.desired_num_ticks = 10;
1357
+ } else {
1358
+ axis_obj.ticker.desired_num_ticks = 6;
1359
+ }
1360
+ }
1361
+ }
1362
+
1363
+ function update_convergence_single( target, data ) {
1220
1364
  const pos = target.src.cur_chan
1221
1365
  const imdata = data.get(pos[1]).get(pos[0])
1222
1366
  // chan----------------^^^^^^ ^^^^^^----stokes
@@ -1226,9 +1370,11 @@ class InteractiveCleanUI:
1226
1370
  const modelFlux = imdata.modelFlux
1227
1371
  const stopCode = imdata.stopCode
1228
1372
  const stopDesc = imdata.stopCode.map( code => stopdescmap.has(code) ? stopdescmap.get(code): "" )
1229
- target.residual.data = { iterations, cyclethreshold, stopDesc, values: peakRes, type: Array(iterations.length).fill('residual') }
1230
- target.flux.data = { iterations, cyclethreshold, stopDesc, values: modelFlux, type: Array(iterations.length).fill('flux') }
1373
+ target.residual.source.data = { iterations, cyclethreshold, stopDesc, values: peakRes, type: Array(iterations.length).fill('residual') }
1374
+ target.flux.source.data = { iterations, cyclethreshold, stopDesc, values: modelFlux, type: Array(iterations.length).fill('flux') }
1231
1375
  target.cyclethreshold.data = { iterations, values: cyclethreshold }
1376
+ updateAxisRange( target.flux.source, target.flux.range, target.flux.axis, 'Flux' )
1377
+ updateAxisRange( target.residual.source, target.residual.range, target.residual.axis, 'Residual' )
1232
1378
  }
1233
1379
 
1234
1380
  function update_convergence( recurse=false ) {
@@ -1312,6 +1458,7 @@ class InteractiveCleanUI:
1312
1458
  )
1313
1459
  }
1314
1460
  )
1461
+ clean_ctrl.continue.disable_add_sub.disabled = true
1315
1462
  clean_ctrl.continue.disabled = true
1316
1463
  clean_ctrl.finish.disabled = true
1317
1464
  clean_ctrl.stop.disabled = with_stop
@@ -1336,6 +1483,7 @@ class InteractiveCleanUI:
1336
1483
 
1337
1484
  clean_ctrl.stop.disabled = false
1338
1485
  if ( ! only_stop ) {
1486
+ clean_ctrl.continue.disable_add_sub.disabled = false
1339
1487
  clean_ctrl.continue.disabled = false
1340
1488
  clean_ctrl.finish.disabled = false
1341
1489
  }