jupyter-widgets / ipywidgets

Interactive Widgets for the Jupyter Notebook
https://ipywidgets.readthedocs.io
BSD 3-Clause "New" or "Revised" License
3.13k stars 948 forks source link

The view is updated twice #766

Open rshkarin opened 8 years ago

rshkarin commented 8 years ago

Hello,

I have created a custom widget, but when I set a new value for a model, it updates a view, and then updates it again on a response from the backend. Is it possible to update the view only once - on the response from the backend? Or somehow get a callback on response from backend?

SylvainCorlay commented 8 years ago

@rshkarin is it possible that you are seeing the updates of two different views?

rshkarin commented 8 years ago

Actually not really, I was using widget-cookiecutter to create a widget base. I just show a canvas which user can edit, when I change a slider position to the next image, the widget frontend set the edited data to the model and do touch. The backend takes these edited data, set it to the data array at an old slider index, and return the new image by the new slider index.

Here is the code example, all code for editing was removed:

Backend code:

import base64
import re
import ipywidgets as widgets
import numpy as np
from PIL import Image
import cStringIO
from io import BytesIO
from traitlets import Unicode, CUnicode, Bytes, Dict, Int, observe, default

@widgets.register('hello.Hello')
class HelloWorld(widgets.DOMWidget):
    """"""
    _view_name = Unicode('HelloView').tag(sync=True)
    _model_name = Unicode('HelloModel').tag(sync=True)
    _view_module = Unicode('data-labeler-widget').tag(sync=True)
    _model_module = Unicode('data-labeler-widget').tag(sync=True)

    # Define the custom state properties to sync with the front-end
    data = None
    label = None
    alpha_color = 0.5
    data_is_2d = True

    format = Unicode('png').tag(sync=True)
    width = CUnicode().tag(sync=True)
    height = CUnicode().tag(sync=True)
    materials = Dict().tag(sync=True)
    brush_color = CUnicode().tag(sync=True)
    num_slices = Int().tag(sync=True)
    current_state = Dict().tag(sync=True)

    def __init__(self, data, label, *args, **kwargs):
        self.data = data
        self.label = label
        self.data_is_2d = True if len(self.data.shape) < 3 else False

        if self.data_is_2d:
            self.num_slices, (self.height, self.width) = 0, self.data.shape
        else:
            self.num_slices, self.height, self.width = self.data.shape

        super(HelloWorld, self).__init__(**kwargs)

    @default('current_state')
    def current_state_default(self):
        if self.data_is_2d:
            return {'data': base64.b64encode(self.array2bytes(self.data)), \
                      'label': base64.b64encode(self.array2bytes(\
                                        self.label2png(self.label))), \
                      'curr_slice_idx': 0, \
                      'prev_slice_idx': 0 }
        else:
            return {'data': base64.b64encode(self.array2bytes(self.data[0])), \
                      'label': base64.b64encode(self.array2bytes(\
                                        self.label2png(self.label[0]))), \
                      'curr_slice_idx': 0, \
                      'prev_slice_idx': 0 }

    @observe('current_state')
    def current_state_changed(self, change):
        curr_state = change['new']

        if self.data_is_2d:
            self._label = \
                    self.png2label(self.bytes2array(base64.b64decode(curr_state['label'])))
            self.current_state = {'data': base64.b64encode(\
                                                self.array2bytes(self.data)), \
                                  'label': base64.b64encode(\
                                             self.array2bytes(self.label2png(self.label))), \
                                  'curr_slice_idx': \
                                            curr_state['curr_slice_idx'], \
                                  'prev_slice_idx': \
                                            curr_state['prev_slice_idx'] }
        else:
            self._label[curr_state['prev_slice_idx']] = \
                        self.png2label(self.bytes2array(base64.b64decode(curr_state['label'])))

            self.current_state = {'data': base64.b64encode(self.array2bytes(\
                                                self.data[curr_state['curr_slice_idx']])), \
                                  'label': base64.b64encode(self.array2bytes(\
                                                self.label2png(\
                                                    self.label[curr_state['curr_slice_idx']]))), \
                                  'curr_slice_idx': \
                                            curr_state['curr_slice_idx'], \
                                  'prev_slice_idx': \
                                            curr_state['prev_slice_idx'] }
.....

Frondend code:

var widgets = require('jupyter-js-widgets');
var _ = require('underscore');
var Konva = require('konva');
require('jquery');
require('jquery-ui');

// Custom Model. Custom widgets models must at least provide default values
// for model attributes, including `_model_name`, `_view_name`, `_model_module`
// and `_view_module` when different from the base class.
//
// When serialiazing entire widget state for embedding, only values different from the
// defaults will be specified.
var HelloModel = widgets.DOMWidgetModel.extend({
    defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {
        _model_name : 'HelloModel',
        _view_name : 'HelloView',
        _model_module : 'data-labeler-widget',
        _view_module : 'data-labeler-widget',
        format: 'png',
        width: '',
        height: '',
        materials: '',
        brush_color: '',
        num_slices: 0,
        current_state: {}
    })
});

var HelloView = widgets.DOMWidgetView.extend({
    stage: null,
    canvas: null,
    drawingContext: null,
    drawingLayer: null,
    mouseContext: null,
    mouseLayer: null,
    slider: null,
    sliceLabel: null,
    render: function() {
      console.log('RENDER WIDGET!');
      var that = this;

      var cont = document.createElement("div");
      cont.setAttribute('id', 'konva-container');
      cont.setAttribute('class', 'konva-container-class');
      that.el.appendChild(cont);

      that.el.appendChild(document.createElement("p"));

      that.slider = document.createElement("input");
      that.slider.id = 'index-slicer';
      that.slider.type = 'range';
      that.slider.step = 1;
      that.slider.style = 'width: 500px !important';
      that.slider.min = 0;
      that.slider.max = that.model.get('num_slices') - 1;
      that.slider.value = that.model.get('current_state')['curr_slice_idx'];
      that.el.appendChild(that.slider);

      that.sliceLabel = document.createElement("span");
      that.sliceLabel.setAttribute('id', 'sliceLabel');
      that.sliceLabel.innerHTML = that.slider.value.toString();
      that.el.appendChild(that.sliceLabel);

      var win = document.defaultView;

      var width = 500;
      var height = 500;
      var isDragging = false;
      var isDragButtonPressed = false;
      var isMouseOnCanvas = false;
      var DRAG_CODE = 16; //shift
      var SAVE_CODE = 83;  //s
      var ERASE_CODE = 69; //e
      var isErasing = false;
      var ZOOM_IN_CODE = 87; //w
      var ZOOM_OUT_CODE = 81; //q
      var BRUSH_UPSIZE_CODE = 221; //]
      var BRUSH_DOWNSIZE_CODE = 219; //[
      var brushSizeMax = 80;
      var brushSizeMin = 2;
      var globalZoomLevel = 1;
      var globalZoomLevelPrev = globalZoomLevel;
      var globalZoomLevelMax = 4;
      var current_slice_idx = 0;
      var prev_slice_idx = current_slice_idx;

      that.slider.onchange = function () {
        console.log('that.slider.onchange = ' + this.value.toString());
        that.sliceLabel.innerHTML = this.value;

        prev_slice_idx = current_slice_idx;
        current_slice_idx = parseInt(this.value, 10);

        console.log('Synchronize data by index!');
        var changedLabelData = that.drawingLayer.toDataURL({mimeType: 'image/png', x: 0, y: 0, width: that.canvas.width, height: that.canvas.height, quality: 1.0});
        that.model.set('current_state', { 'label': changedLabelData.replace(/^data:image\/(png|jpg);base64,/, ''),
                                          'curr_slice_idx': current_slice_idx,
                                          'prev_slice_idx': prev_slice_idx,
                                          'data': that.model.get('current_state')['data'] });
        that.touch();
      }

      that.listenTo(that.model, 'change:current_state', function (sender, value) {
        console.log('CHANGE:CURR_STATE = ' + value.toString());
      });

      ..........

      that.update();
    },
    update: function () {
      console.log('UPDATE MODEL');
      var that = this;

      .........

      function drawDataLayer() {
          console.log('start drawDataLayer');
          var imageObj = new Image();
          console.log(that.model.get('current_state'));
          var imageSource = 'data:image/' + that.model.get('format') + ';base64,' + that.model.get('current_state')['data'];

          var sampleLayer = new Konva.Layer();
          sampleLayer.id = imageLayerId;

          imageObj.onload = function() {

            var sample = new Konva.Image({
              x: 0,
              y: 0,
              image: imageObj,
              width: that.model.get('width'),
              height: that.model.get('height')
            });

            // add the shape to the layer
            sampleLayer.add(sample);

            // add the layer to the stage
            var layers = that.stage.getLayers().toArray();

            for(var i = 0; i < layers.length; i++) {
                if (layers[i].id == imageLayerId) {
                    var layer = layers[i];
                    layer.remove();
                    layer.destroy();
                }
            }

            that.stage.add(sampleLayer);
            sampleLayer.moveToBottom();

            console.log('end drawDataLayer');

            drawLabelLayer();
          };
          imageObj.src = imageSource;
      }

      /////////////////////////////////////////

      function drawLabelLayer() {
          console.log('start drawLabelLayer');
          var imageLabelSource = 'data:image/' + that.model.get('format') + ';base64,' + that.model.get('current_state')['label'];

          that.drawingLayer = new Konva.Layer();
          that.drawingLayer.id = labelLayerId;

          var layers = that.stage.getLayers().toArray();
          for(var i = 0; i < layers.length; i++) {
              if (layers[i].id == labelLayerId) {
                  var layer = layers[i];
                  layer.remove();
                  layer.destroy();
              }
          }

          that.stage.add(that.drawingLayer);

          var canvas = document.createElement('canvas');
          canvas.width = that.model.get('width');
          canvas.height = that.model.get('height');

          var image = new Konva.Image({
              image: canvas,
              x : 0,
              y : 0
          });

          that.drawingLayer.add(image);
          that.stage.draw();

          that.canvas = canvas;
          that.drawingContext = that.canvas.getContext('2d');

          that.drawingContext.strokeStyle = that.model.get('brush_color');
          that.drawingContext.lineJoin = "round";
          that.drawingContext.lineWidth = 10;

          var img = new Image();
          img.onload = function() {
                that.drawingContext.drawImage(this, 0, 0, that.canvas.width, that.canvas.height);
                that.stage.draw();

                console.log('end drawLabelLayer');

                drawMouseLayer();
          }
          img.src = imageLabelSource;
      }

      /////////////////////////////////////////

      function drawMouseLayer() {
          console.log('start drawMouseLayer');
          that.mouseLayer = new Konva.Layer();
          that.mouseLayer.id = mouseLayerId;

          var layers = that.stage.getLayers().toArray();

          for(var i = 0; i < layers.length; i++) {
              if (layers[i].id == mouseLayerId) {
                  var layer = layers[i];
                  layer.remove();
                  layer.destroy();
              }
          }

          that.stage.add(that.mouseLayer);

          var mouseCanvas = document.createElement('canvas');
          mouseCanvas.width = that.model.get('width');
          mouseCanvas.height = that.model.get('height');

          var imageMouse = new Konva.Image({
              image: mouseCanvas,
              x : 0,
              y : 0
          });

         that.mouseLayer.add(imageMouse);

         that.mouseContext = mouseCanvas.getContext('2d');
         that.mouseContext.lineWidth = 1;
         that.mouseContext.strokeStyle = '#000000';
         that.mouseContext.globalCompositeOperation = 'copy';
         console.log('end drawMouseLayer');
     }

     drawDataLayer();
     that.stage.draw();

      return HelloView.__super__.update.apply(this);
    }
});

module.exports = {
    HelloModel : HelloModel,
    HelloView : HelloView
};

Log from web console:

"that.slider.onchange = 10" index.js:204
"Synchronize data by index!" index.js:210
"CHANGE:CURR_STATE = [object Object]" index.js:229
"UPDATE MODEL" index.js:495
"start drawDataLayer" index.js:502
Object { label: "iVBORw0KGgoAAAANSUhE.....", curr_slice_idx: 10, prev_slice_idx: 0, data: "iVBORw0KGgoAAAANSUh...."} index.js:504
"b layers = 3" index.js:526
"a layers = 2" index.js:536
"end drawDataLayer" index.js:541
"start drawLabelLayer" index.js:549
"end drawLabelLayer" index.js:591
"start drawMouseLayer" index.js:599
"end drawMouseLayer" index.js:631
"CHANGE:CURR_STATE = [object Object]" index.js:229
"UPDATE MODEL" index.js:495
"start drawDataLayer" index.js:502
Object { curr_slice_idx: 10, prev_slice_idx: 0, data: "iVBORw0KGgoAAAA........", label: "iVBORw0KGgoAAAANSUh...." } index.js:504
"b layers = 3" index.js:526
"a layers = 2" index.js:536
"end drawDataLayer" index.js:541
"start drawLabelLayer" index.js:549
"end drawLabelLayer" index.js:591
"start drawMouseLayer" index.js:599
"end drawMouseLayer" index.js:631
jasongrout commented 7 years ago

We recently fixed a long-standing bug which caused multiple updates to accidentally happen - basically the logic in the kernel for seeing if an update came from the frontend, or if needed to be propagated to the frontend, was flawed, leading to unnecessary propagations to the frontend of an update the frontend had just sent.

Is this widget code up somewhere so I can install it to check it? Or can you check if it still has this issue with the latest 7.0 beta? (The code will need to be updated to work with ipywidgets 7.)