def __init__(self, client: MQTTClientWrapper, name="fp50", config=None): super().__init__(name, config=config) self.client = client self.topic_base = self.config["waterBath"][name]["topicBase"] self.topic = self.topic_base + "/setpoint" self.message_subject = Subject() self.client.subscribe(self.topic_base + "/power") self.client.subscribe(self.topic_base + "/temperature") self.client.subscribe(self.topic_base + "/setpoint") power_sub = Subject() temp_sub = Subject() setpoint_sub = Subject() self.client.message_callback_add(self.topic_base + "/power", lambda *args: power_sub.on_next(args)) self.client.message_callback_add(self.topic_base + "/temperature", lambda *args: temp_sub.on_next(args)) self.client.message_callback_add(self.topic_base + "/setpoint", lambda *args: setpoint_sub.on_next(args)) rx.combine_latest( power_sub, temp_sub, setpoint_sub ).pipe( operators.map(lambda x: FP50Message( float(x[0][2].payload), float(x[1][2].payload), float(x[2][2].payload) )) ).subscribe( self.message_subject )
def main(): # setup gui image = Image.open(IMAGE_FILE) image_width, image_height = image.size window_size = RectSize(width=image_width, height=image_height) window = initialize_window(window_size) canvas = create_canvas_on(window, window_size, canvas_place=Point(0, 0)) image_tk = ImageTk.PhotoImage(image) canvas.create_image(0, 0, image=image_tk, anchor="nw") def draw_rect(rectangle): canvas.delete("rect") draw_rect_on( canvas, rectangle, fill_color="orange", stipple="gray12", outline_color="orange", width=1.0, tag="rect", ) # state mouse_left_click = createMouseEventStream(window, "<Button-1>") mouse_left_drag = createMouseEventStream(window, "<B1-Motion>") rect_begin = mouse_left_click rect_end = rx.merge(mouse_left_drag, mouse_left_click) rect = rx.combine_latest(rect_begin, rect_end).pipe( ops.map(lambda tpl: rect_from_2points(*tpl))) rect.subscribe(draw_rect) window.mainloop()
def combine_latest( # *streams: rx.typing.Observable, s1: Optional[rx.typing.Observable[T1]] = None, s2: Optional[rx.typing.Observable[T2]] = None, s3: Optional[rx.typing.Observable[T3]] = None, ) -> Any: return rx.combine_latest(cast(rx.Observable, [s1, s2, s3]))
def __init__(self, context: BlenderContext) -> None: super().__init__(context) self._position = Subject() self._activeInputs = Subject() # noinspection PyTypeChecker self.position = self._position.pipe( ops.distinct_until_changed(), ops.map(lambda v: tuple( p * s for p, s in zip(v, context.window_size.tuple))), ops.map(Point.from_tuple), ops.share()) codes = { MouseButton.LEFT: bge.events.LEFTMOUSE, MouseButton.MIDDLE: bge.events.MIDDLEMOUSE, MouseButton.RIGHT: bge.events.RIGHTMOUSE } def pressed(e: SCA_InputEvent) -> bool: return KX_INPUT_ACTIVE in e.status or KX_INPUT_JUST_ACTIVATED in e.status def value_for(button: MouseButton) -> Observable: code = codes[button] return self._activeInputs.pipe( ops.start_with({}), ops.map(lambda i: code in i and pressed(i[code])), ops.map(lambda v: button if v else 0)) # noinspection PyTypeChecker self.buttons = rx.combine_latest( *[value_for(b) for b in MouseButton]).pipe( ops.map(lambda v: reduce(lambda a, b: a | b, v)), ops.distinct_until_changed(), ops.share())
def __init__(self, uri, config, masterGraph, mqtt, influx): self.uri = uri self.config = config self.masterGraph = masterGraph self.mqtt = mqtt self.influx = influx self.mqttTopic = self.topicFromConfig(self.config) statPath = '/subscribed_topic/' + self.mqttTopic.decode( 'ascii').replace('/', '|') scales.init(self, statPath) self._mqttStats = scales.collection(statPath + '/incoming', scales.IntStat('count'), scales.RecentFpsStat('fps')) rawBytes = self.subscribeMqtt() rawBytes = rx.operators.do_action(self.countIncomingMessage)(rawBytes) parsed = self.getParser()(rawBytes) g = self.config for conv in g.items(g.value(self.uri, ROOM['conversions'])): parsed = self.conversionStep(conv)(parsed) outputQuadsSets = rx.combine_latest(*[ self.makeQuads(parsed, plan) for plan in g.objects(self.uri, ROOM['graphStatements']) ]) outputQuadsSets.subscribe_(self.updateQuads)
def start(self, args: dict): from alleycat.ui.blender import UI self.context = UI().create_context() window = Frame(self.context, BorderLayout()) window.bounds = Bounds(160, 70, 280, 200) panel = Panel(self.context, HBoxLayout()) panel.set_color(StyleKeys.Background, RGBA(0.3, 0.3, 0.3, 0.8)) window.add(panel, padding=Insets(10, 10, 10, 10)) icon = Canvas(self.context, self.context.toolkit.images["cat.png"]) icon.minimum_size_override = Some(Dimension(64, 64)) panel.add(icon) label = Label(self.context, text_size=18) label.set_color(StyleKeys.Text, RGBA(1, 1, 1, 1)) panel.add(label) button1 = LabelButton(self.context, text_size=16, text="Button 1") button2 = LabelButton(self.context, text_size=16, text="Button 2") buttons = Panel(self.context, HBoxLayout(spacing=10, direction=BoxDirection.Reverse)) buttons.add(button2) buttons.add(button1) window.add(buttons, Border.Bottom, Insets(0, 10, 10, 10)) def handle_button(button: str): if len(button) > 0: label.text = f"{button} is pressed" panel.set_color(StyleKeys.Background, RGBA(1, 0, 0, 1)) else: label.text = "" panel.set_color(StyleKeys.Background, RGBA(0.1, 0.1, 0.1, 0.8)) button1_active = button1.observe("active").pipe( ops.map(lambda v: "Button 1" if v else "")) button2_active = button2.observe("active").pipe( ops.map(lambda v: "Button 2" if v else "")) button_active = rx.combine_latest(button1_active, button2_active).pipe( ops.map(lambda v: v[0] + v[1])) button_active.subscribe(handle_button, on_error=self.context.error_handler) window.draggable = True window.resizable = True
def __init__(self, client: MQTTClientWrapper, name="fp50", config=None): super().__init__(name, config=config) self.client = client self.topic_base = self.config["waterBath"][name]["topicBase"] self.topic = self.topic_base + "/setpoint" self.message_subject = Subject() self.client.subscribe(self.topic_base + "/crystallizer_temperature") self.client.subscribe(self.topic_base + "/setpoint") self.interval_scheduler = NewThreadScheduler() def update(x, scheduler=None): self.client.publish(self.topic_base + "/crystallizer_temperature", None) rx.interval(self.config["waterBath"][name]["interval"], self.interval_scheduler).subscribe(update) def convert(x): payloads = [xx[2].payload for xx in x] for p in payloads: if not p: # skipping conversion request return None return { # "power": float(payloads[0]), # "internal_temperature": float(payloads[1]), "crystallizer_temperature": float(payloads[0]), "setpoint": float(payloads[1]), } rx.combine_latest( from_callback( self.client.message_callback_add)(self.topic_base + "/crystallizer_temperature"), from_callback(self.client.message_callback_add)(self.topic_base + "/setpoint"), ).pipe(operators.map(convert), operators.filter(lambda x: x is not None), operators.debounce(0.6)).subscribe(self.message_subject)
def main(): dbus = DBus.new_tcp_connection('192.168.178.137') service_name = 'com.victronenergy.example' def always(_): return True dbus.publish_ve_property(service_name, '/ProductName', 'Example Product') dbus.publish_ve_property(service_name, '/YouCannotEditMe', 'try it') dbus.publish_ve_property(service_name, '/EnterNumberBetween0and10', 5, accept_change=lambda n: 0 <= float(n) <= 10) dbus.publish_ve_property(service_name, '/EnterSalutation', 'Hello', accept_change=always) dbus.publish_ve_property(service_name, '/EnterYourName', '', accept_change=always) dbus.publish_ve_property(service_name, '/Greetings', 'please enter a name and a salutation') def update_counter(i): dbus.publish_ve_property(service_name, '/Counter', i) rx.interval(timedelta(seconds=1)).subscribe(update_counter) def greeter(s_n: Tuple[VeProperty, VeProperty]): s, n = s_n salutation = s.text name = n.text greeting = f'{salutation} {name}' if salutation and name else 'please enter a name and a salutation' dbus.publish_ve_property(service_name, '/Greetings', greeting) salutation = dbus.observe_ve_property(service_name, '/EnterSalutation') name = dbus.observe_ve_property(service_name, '/EnterYourName') rx.combine_latest(salutation, name).subscribe(greeter) dbus.run_forever()
def combine_latest(source: Observable) -> Observable: """Merges the specified observable sequences into one observable sequence by creating a tuple whenever any of the observable sequences produces an element. Examples: >>> obs = combine_latest(source) Returns: An observable sequence containing the result of combining elements of the sources into a tuple. """ sources = (source,) + others return rx.combine_latest(*sources)
def combine_latest(source: Observable) -> Observable: """Merges the specified observable sequences into one observable sequence by creating a tuple whenever any of the observable sequences produces an element. Examples: >>> obs = combine_latest(source) Returns: An observable sequence containing the result of combining elements of the sources into a tuple. """ sources = (source, ) + others return rx.combine_latest(*sources)
def _export_geometry(geometry, geometry_description): export_years = combine_latest( *[_export_year( geometry=geometry, year_start=year_range[0], year_end=year_range[1], export_description='{}_{}_{}'.format(geometry_description, year_range[0].year, description), year_dir='/'.join([download_dir, description, geometry_description, str(year_range[0].year)]) ) for year_range in year_ranges] ).pipe( map(lambda progresses: _sum_dicts(progresses)), map(lambda p: {**p, 'geometry': geometry_description}) ) process_geometry = _process_geometry('/'.join([download_dir, description, geometry_description])) return concat( export_years, process_geometry )
def combine_latest(source: Observable) -> Observable: """Merges the specified observable sequences into one observable sequence by creating a tuple whenever any of the observable sequences produces an element. Examples: >>> obs = combine_latest(source) Returns: An observable sequence containing the result of combining elements of the sources into a tuple. """ sources: List[Observable] = [source] if isinstance(other, typing.Observable): sources += [other] else: sources += other return rx.combine_latest(sources)
def download_folder(folder): def aggregate_progress(progresses: list): total_files = len(progresses) total_bytes = sum([int(p['file']['size']) for p in progresses]) downloaded_files = len([ p for p in progresses if p['downloaded_bytes'] == p['total_bytes'] ]) downloaded_bytes = sum([p['downloaded_bytes'] for p in progresses]) return { 'downloaded_files': downloaded_files, 'downloaded_bytes': downloaded_bytes, 'total_files': total_files, 'total_bytes': total_bytes } return list_folder_recursively(credentials, folder).pipe( map(lambda files: filter_files(files)), flat_map(lambda files: combine_latest( *[download_file(f, get_file_destination(f)) for f in files])), map(aggregate_progress))
def download_folder(folder): def aggregate_progress(progresses: list): total_files = len(progresses) total_bytes = sum([int(p.file['size']) for p in progresses]) downloaded_files = len( [p for p in progresses if p.downloaded_bytes == p.total_bytes]) downloaded_bytes = sum([p.downloaded_bytes for p in progresses]) return progress( default_message= 'Downloaded {downloaded_files} of {total_files} files ({downloaded} of {total})', message_key='tasks.drive.download_folder', downloaded_files=downloaded_files, downloaded_bytes=downloaded_bytes, downloaded=format_bytes(downloaded_bytes), total_files=total_files, total_bytes=total_bytes, total=format_bytes(total_bytes)) return list_folder_recursively(credentials, folder).pipe( map(lambda files: filter_files(files)), flat_map(lambda files: combine_latest( *[download_file(f, get_file_destination(f)) for f in files]) if files else empty()), flat_map(aggregate_progress))
def audio_encoder(sources): # Parse configuration parser = create_arg_parser() parsed_argv = sources.argv.argv.pipe( ops.skip(1), argparse.parse(parser), ops.filter(lambda i: i.key == 'config'), ops.subscribe_on(aio_scheduler), ops.share(), ) # monitor and parse config file monitor_init = parsed_argv.pipe( ops.flat_map(lambda i: rx.from_([ inotify.AddWatch( id='config', path=i.value, flags=aionotify.Flags.MODIFY), inotify.Start(), ]))) config_update = sources.inotify.response.pipe( ops.debounce(5.0, scheduler=aio_scheduler), ops.map(lambda i: True), ops.start_with(True), ) read_request, read_response = rx.combine_latest( parsed_argv, config_update).pipe( ops.starmap( lambda config, _: file.Read(id='config', path=config.value)), file.read(sources.file.response), ) config = read_response.pipe( ops.filter(lambda i: i.id == "config"), ops.flat_map(lambda i: i.data), parse_config, ) # Transcode request handling encode_init = config.pipe( ops.map(lambda i: i.encode), ops.distinct_until_changed(), ops.map(lambda i: encoder.Configure(samplerate=i.samplerate, bitdepth=i.bitdepth)), ) encode_request = sources.httpd.route.pipe( ops.filter(lambda i: i.id == 'flac_transcode'), ops.flat_map(lambda i: i.request), ops.flat_map(lambda i: rx.just(i, encode_scheduler)), ops.map(lambda i: encoder.EncodeMp3( id=i.context, data=i.data, key=i.match_info['key'])), ) encoder_request = rx.merge(encode_init, encode_request) # store encoded file store_requests = sources.encoder.response.pipe( ops.observe_on(s3_scheduler), ops.map(lambda i: s3.UploadObject( key=i.key + '.flac', data=i.data, id=i.id, )), ) # acknowledge http request http_response = sources.s3.response.pipe( ops.map(lambda i: httpd.Response( data='ok'.encode('utf-8'), context=i.id, ))) # http server http_init = config.pipe( ops.take(1), ops.flat_map(lambda i: rx.from_([ httpd.Initialize(request_max_size=0), httpd.AddRoute( methods=['POST'], path='/api/transcode/v1/flac/{key:[a-zA-Z0-9-\._]*}', id='flac_transcode', ), httpd.StartServer(host=i.server.http.host, port=i.server.http.port ), ])), ) http = rx.merge(http_init, http_response) # s3 database s3_init = config.pipe( ops.take(1), ops.map(lambda i: s3.Configure( access_key=i.s3.access_key, secret_key=i.s3.secret_key, bucket=i.s3.bucket, endpoint_url=i.s3.endpoint_url, region_name=i.s3.region_name, )), ) # merge sink requests file_requests = read_request s3_requests = rx.merge(s3_init, store_requests) return Sink( encoder=encoder.Sink(request=encoder_request), s3=s3.Sink(request=s3_requests), file=file.Sink(request=file_requests), httpd=httpd.Sink(control=http), inotify=inotify.Sink(request=monitor_init), )
def _repeat_when_player_comes_online(self, obs): return rx.combine_latest( self.obs_teams, self._obs_player_came_online).pipe( ops.map(lambda t: t[0]) )
def main(sources): example1_completed = rx.combine_latest( sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example1_set_head_angle")), sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example1_set_lift_height")), ) example2_completed = rx.combine_latest( sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example2_drive_straight")), sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example2_turn_in_place")), ) example3_completed = rx.combine_latest( sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example3_set_lift_height")), sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example3_set_head_angle")), sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example3_drive_straight")), sources["Cozmo"].pipe( ops.filter( lambda i: i["id"] is "example3_display_oled_face_image")), ) example4_completed = rx.combine_latest( sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example4_set_lift_height")), sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example4_set_head_angle")), sources["Cozmo"].pipe( ops.filter(lambda i: i["id"] is "example4_drive_straight")), sources["Cozmo"].pipe( ops.filter( lambda i: i["id"] is "example4_display_oled_face_image")), ) example1_completed.subscribe(on_next=lambda i: print("example1_completed")) example2_completed.subscribe(on_next=lambda i: print("example2_completed")) example3_completed.subscribe(on_next=lambda i: print("example3_completed")) example4_completed.subscribe(on_next=lambda i: print("example4_completed")) rxcozmo = rx.merge( # example1_lift_head rx.of({ "id": "example1_set_head_angle", "name": "set_head_angle", "value": cozmo.robot.MAX_HEAD_ANGLE, }), rx.of({ "id": "example1_set_lift_height", "name": "set_lift_height", "value": 1.0, }), # example2_conflicting_actions example1_completed.pipe( ops.map( lambda i: { "id": "example2_drive_straight", "name": "drive_straight", "value": { "distance": distance_mm(50), "speed": speed_mmps(25), "should_play_anim": False, }, })), example1_completed.pipe( ops.map( lambda i: { "id": "example2_turn_in_place", "name": "turn_in_place", "value": { "angle": degrees(90), }, })), # example3_abort_one_action example2_completed.pipe( ops.map( lambda i: { "id": "example3_set_lift_height", "name": "set_lift_height", "value": 0, })), example2_completed.pipe( ops.map( lambda i: { "id": "example3_set_head_angle", "name": "set_head_angle", "value": { "angle": cozmo.robot.MIN_HEAD_ANGLE, "duration": 6.0, }, })), example2_completed.pipe( ops.map( lambda i: { "id": "example3_drive_straight", "name": "drive_straight", "value": { "distance": distance_mm(75), "speed": speed_mmps(25), "should_play_anim": False, }, })), example2_completed.pipe( ops.map( lambda i: { "id": "example3_display_oled_face_image", "name": "display_oled_face_image", "value": { "screen_data": face_image, "duration_ms": 30000.0, }, })), # abort actions example2_completed.pipe( ops.map(lambda i: { "type": "abort", "name": "set_lift_height" })), example2_completed.pipe( ops.delay(0.1), ops.map(lambda i: { "type": "abort", "name": "set_head_angle" }), ), example2_completed.pipe( ops.delay(2), ops.map(lambda i: { "type": "abort", "name": "display_oled_face_image" }), ), # example4_abort_all_actions example3_completed.pipe( ops.map( lambda i: { "id": "example4_set_lift_height", "name": "set_lift_height", "value": 0.0, })), example3_completed.pipe( ops.map( lambda i: { "id": "example4_set_head_angle", "name": "set_head_angle", "value": { "angle": cozmo.robot.MIN_HEAD_ANGLE, "duration": 6.0, }, })), example3_completed.pipe( ops.map( lambda i: { "id": "example4_drive_straight", "name": "drive_straight", "value": { "distance": distance_mm(75), "speed": speed_mmps(25), "should_play_anim": False, }, })), example3_completed.pipe( ops.map( lambda i: { "id": "example4_display_oled_face_image", "name": "display_oled_face_image", "value": { "screen_data": face_image, "duration_ms": 30000.0, }, })), # abort all actions example3_completed.pipe( ops.delay(2), ops.map(lambda i: { "type": "abort", "name": "set_lift_height" }), ), example3_completed.pipe( ops.delay(2), ops.map(lambda i: { "type": "abort", "name": "set_head_angle" }), ), example3_completed.pipe( ops.delay(2), ops.map(lambda i: { "type": "abort", "name": "drive_straight" }), ), example3_completed.pipe( ops.delay(2), ops.map(lambda i: { "type": "abort", "name": "display_oled_face_image" }), ), ) sinks = {"Cozmo": rxcozmo} return sinks
import rx from rx.operators import map, start_with from .streams import * myTradeStream = rx.combine_latest( rx.interval(0.01).pipe( map(lambda _: datetime.now().strftime('%Y-%m-%d %H:%M:%S')), ), PriceLastBuyStream.pipe(map(lambda res: 'last buy: ' + str(res)), start_with('waiting...')), PriceLastSellStream.pipe(map(lambda res: 'last sell: ' + str(res)), start_with('waiting...')), PriceMinAskStream.pipe(map(lambda res: 'min ask: ' + str(res)), start_with('waiting...')), PriceMaxBidStream.pipe(map(lambda res: 'max bid: ' + str(res)), start_with('waiting...')), )
# flat 하면 리스트를 푼다. # [[1,2], [3]] -> [1,2,3] # 그리고 나서 map 한다. x -> x # [1, 2, 3] -> [1, 2, 3] print('-- window_with_time') test_scheduler = TestScheduler() rx.interval(0.05, test_scheduler).pipe(ops.take_until( rx.timer(0.1)), ops.window_with_time(0.01)).subscribe( lambda observable: observable.pipe(ops.count()).subscribe(print_value)) test_scheduler.start() time.sleep(0.5) # 결과가 이게 맞나?? 단위가 뭐지? print('-- combine_latest') rx.combine_latest( rx.interval(0.1).pipe(ops.map(lambda x: 'a- {}'.format(x))), rx.interval(0.2).pipe(ops.map(lambda x: 'b- {}'.format(x)))).pipe( ops.take_until(rx.timer(0.5))).subscribe(print_value) time.sleep(1) # 누가 먼저 결과를 배출할지 예상 불가 print('-- zip') rx.zip( rx.interval(0.1).pipe(ops.map(lambda x: 'a- {}'.format(x))), rx.interval(0.2).pipe(ops.map(lambda x: 'b- {}'.format(x)))).pipe( ops.take_until(rx.timer(1))).subscribe(print_value) time.sleep(1.2) # zip 은 양쪽 모두 값이 있어야 된다
ops.map(lambda res: res.bids), ops.filter(bool), ops.map(lambda ds: sorted(ds, key=lambda x: float(x[0]))), ops.map(lambda ds: float(ds[-1][0])),) def buy(price, amount=AMOUNT): return bitbank.order(price, amount, 'buy', 'limit') def sell(price, amount=AMOUNT): return bitbank.order(price, amount, 'sell', 'limit') priceBuyMainStream = rx.combine_latest( PriceLastSellStream, PriceMaxBidStream).pipe( ops.map(lambda ps: (float(ps[0]) + float(ps[1])) / 2)) priceBuyLCStream = priceBuyMainStream.pipe( ops.map(lambda p: p + ALPHA)) priceSellMainStream = rx.combine_latest( PriceLastBuyStream, PriceMinAskStream).pipe( ops.map(lambda ps: (float(ps[0]) + float(ps[1])) / 2)) priceSellLCStream = priceSellMainStream.pipe( ops.map(lambda p: p - ALPHA)) initialOrderStream = canOrderStream.pipe( ops.filter(bool))
class Component(Drawable, StyleResolver, MouseEventHandler, EventDispatcher, ContextAware, ReactiveObject): visible: RP[bool] = rv.new_property() parent: RP[Maybe[Container]] = rv.from_value(Nothing) offset: RV[Point] = parent.as_view().map(lambda _, parent: parent.map( lambda p: rx.combine_latest(p.observe("offset"), p.observe("location")) .pipe(ops.map(lambda v: v[0] + v[1]))).or_else_call(lambda: rx.of( Point(0, 0)))).pipe(lambda _: (ops.exclusive(), )) _minimum_size: RP[Dimension] = rv.from_value(Dimension(0, 0)) _preferred_size: RP[Dimension] = rv.from_value(Dimension(0, 0)) minimum_size_override: RP[Maybe[Dimension]] = rv.from_value(Nothing) minimum_size: RV[Dimension] = rv.combine_latest( _minimum_size, minimum_size_override)(ops.pipe(ops.map(lambda v: v[1].value_or(v[0])), ops.distinct_until_changed())) preferred_size_override: RP[Maybe[Dimension]] = rv.from_value( Nothing).pipe(lambda o: (ops.combine_latest(o.observe("minimum_size")), ops.map(lambda t: t[0].map(lambda v: t[1].copy( width=max(v.width, t[1].width), height=max(v.height, t[1].height)))), ops.distinct_until_changed())) preferred_size: RV[Dimension] = rv.combine_latest( _preferred_size, preferred_size_override, minimum_size)(ops.pipe( ops.map(lambda v: (v[1].value_or(v[0]), v[2])), ops.map(lambda v: v[0].copy(width=max(v[0].width, v[1].width), height=max(v[0].height, v[1].height))), ops.distinct_until_changed())) bounds: RP[Bounds] = Bounded.bounds.pipe(lambda o: ( ops.combine_latest(o.observe("minimum_size")), ops.map(lambda v: v[0].copy(width=max(v[0].width, v[1].width), height=max(v[0].height, v[1].height))), ops.start_with(o.preferred_size))) def __init__(self, context: Context, visible: bool = True) -> None: if context is None: raise ValueError("Argument 'context' is required.") # noinspection PyTypeChecker self.visible = visible self._context = context self._valid = False self._ui = self.create_ui() assert self._ui is not None super().__init__() self.validate() self.ui \ .on_invalidate(self) \ .pipe(ops.take_until(self.on_dispose)) \ .subscribe(lambda _: self.invalidate(), on_error=self.error_handler) @property def context(self) -> Context: return self._context @property def ui(self) -> ComponentUI: return self._ui @property def look_and_feel(self) -> LookAndFeel: return self.context.look_and_feel def create_ui(self) -> ComponentUI: return self.context.look_and_feel.create_ui(self) def show(self) -> None: # noinspection PyTypeChecker self.visible = True def hide(self) -> None: # noinspection PyTypeChecker self.visible = False @property def valid(self) -> bool: return self._valid # noinspection PyTypeChecker def validate(self, force: bool = False) -> None: if self.visible and (not self.valid or force): self._minimum_size = self.ui.minimum_size(self) self._preferred_size = self.ui.preferred_size(self) self._valid = True self.parent.map(lambda p: p.request_layout()) def invalidate(self) -> None: self._valid = False self.parent.map(lambda p: p.invalidate()) def draw(self, g: Graphics) -> None: if self.visible: g.save() (dx, dy) = self.parent.map(lambda p: p.location).value_or(Point(0, 0)) (cx, cy, cw, ch) = self.ui.clip_bounds(self).tuple g.translate(dx, dy) g.rectangle(cx, cy, cw, ch) g.clip() try: self.draw_component(g) except BaseException as e: self.error_handler(e) g.restore() def draw_component(self, g: Graphics) -> None: self.ui.draw(g, self) def position_of(self, event: PositionalEvent) -> Point: if event is None: raise ValueError("Argument 'event' is required.") return event.position - self.offset @property def inputs(self) -> Mapping[str, Input]: return self.context.inputs @property def parent_dispatcher(self) -> Maybe[EventDispatcher]: # noinspection PyTypeChecker return self.parent def __repr__(self) -> Any: return str({"id": id(self), "type": type(self).__name__})
def chronometer(valid=None, offset=None, start=None, source=None, radio_group=None, plus_button=None, minus_button=None, selectors=None): """ Time/forecast exploration widget Creates a figure with glyphs for each point in time/forecast space along with +/- buttons and a radio group to navigate subsets of available forecasts >>> source = bokeh.models.ColumnDataSource({ ... "valid": [], ... "start": [], ... "offset": [] ... }) >>> figure, radio_group, plus, minus = chronometer( ... valid="valid", ... start="start", ... offset="offset", ... source=source) >>> isinstance(figure, bokeh.plotting.Figure) True The associated glyphs react to changes in the `source.selected.indices` and clicks from the radio group, plus and minus buttons >>> import datetime as dt >>> source.stream({ ... "valid": [dt.datetime(2018, 1, 1, 12)], ... "start": [dt.datetime(2018, 1, 1, 0)], ... "offset": [12] ... }) >>> source.selected.indices = [0] **Selectors** The radio group allows the user to choose between time/forecast selection algorithms, the user is also able to specify their own time/forecast space selection algorithm >>> selectors = { ... 0: select("key"), ... } The keys of the selectors dict maps radio group active index to functions that select points in the column data source **Widgets** A figure, a radio group and buttons for navigating backwards/forwards inside a selection are returned to allow the user to craft their own layout :param valid: key in source :param start: key in source :param offset: key in source :param source: ColumnDataSource :returns: figure, radio group, plus button and minus button bokeh widgets """ msg = ("please specify 'valid', 'start' and 'offset' " "keywords") assert valid is not None, msg assert start is not None, msg assert offset is not None, msg if radio_group is None: radio_group = bokeh.models.RadioGroup( labels=["Time", "Forecast", "Run"], inline=True, width=210, active=2) if selectors is None: selectors = { 0: select(valid), 1: select(offset), 2: select(start), } if plus_button is None: plus_button = bokeh.models.Button(label="+", width=50) if minus_button is None: minus_button = bokeh.models.Button(label="-", width=50) hover_tool = bokeh.models.HoverTool(tooltips=[ ('valid', '@' + valid + '{%F %T}'), ('start', '@' + start + '{%F %T}'), ('length', 'T+@' + offset) ], formatters={ valid: 'datetime', start: 'datetime' }) pan_tool = bokeh.models.PanTool(dimensions="width") tap_tool = bokeh.models.TapTool() figure = bokeh.plotting.figure( x_axis_type='datetime', plot_height=200, plot_width=350, tools=[tap_tool, hover_tool, pan_tool, "xwheel_zoom"], active_scroll="xwheel_zoom", toolbar_location=None) figure.toolbar.active_inspect = hover_tool figure.ygrid.grid_line_color = None figure.xaxis.axis_label = "Validity time" figure.xaxis.axis_label_text_font_size = "10px" figure.yaxis.axis_label = "Forecast length" figure.yaxis.axis_label_text_font_size = "10px" offsets = source.data[offset][:] if len(offsets) > 0: figure.yaxis.ticker = ticks(max(offsets)) renderer = figure.square(x=valid, y=offset, source=source, size=8, line_color=None, nonselection_alpha=1, nonselection_line_color=None) tap_tool.renderers = [renderer] second_source = bokeh.models.ColumnDataSource({ valid: [], offset: [], start: [] }) renderer = figure.square(x=valid, y=offset, size=8, source=second_source) renderer.selection_glyph = bokeh.models.Square(fill_alpha=1, fill_color="Red", line_color="Black") renderer.nonselection_glyph = bokeh.models.Square(fill_alpha=1, fill_color="White", line_color="Black") selected = rx.Stream() source.selected.on_change('indices', rx.callback(selected)) active = rx.Stream() radio_group.on_change("active", rx.callback(active)) changes = rx.combine_latest(active.map(lambda i: selectors[i]), selected).filter(all_not_none) def render(source, second_source): """Updates secondary source used to highlight selection""" def wrapper(event): selector, indices = event if len(indices) == 0: indices = [] data = {k: [] for k in source.data.keys()} else: index = indices[0] pts = selector(source, index) indices = [pts[0].tolist().index(index)] data = {k: np.asarray(v)[pts] for k, v in source.data.items()} second_source.data = data second_source.selected.indices = indices return wrapper changes.map(render(source, second_source)) plus = rx.Stream() plus_button.on_click(rx.click(plus)) plus = plus.map(+1) minus = rx.Stream() minus_button.on_click(rx.click(minus)) minus = minus.map(-1) steps = rx.Merge(plus, minus) steps.map(move(second_source)).map( sync(source, second_source, valid=valid, offset=offset)) if radio_group.active is not None: active.emit(radio_group.active) return figure, radio_group, plus_button, minus_button