<NME> index.ts
<BEF> import { IEvent } from "../collector/event"
import { Edge as EdgeLabel, Message, NodeLabel } from "../collector/logger"
import TimeComposer from "../collector/timeComposer"
import TypedGraph from "../collector/typedgraph"
import "../object/extensions"
import { IObservableTree, IObserverTree, ObserverTree } from "../oct/oct"
import "../utils"
import Grapher from "./grapher"
import layoutf from "./layout"
import { In as RenderInput, graph$ } from "./render"
import { time } from "./time"
import {
  MarbleClick, MarbleHoover,
  SelectionGraphEdge, SelectionGraphNode, SelectionGraphNone,
  UIEvent,
} from "./uievent"
import * as Rx from "rxjs"
import { Subject } from "rxjs/Subject"
import { VNode } from "snabbdom/vnode"

export interface DataSource {
  dataObs: Rx.Observable<Message>
}

export type ViewState = {
  tick?: number
  flowSelection?: SelectionGraphNode | SelectionGraphEdge | SelectionGraphNone | MarbleClick
  hooverSelection?: SelectionGraphNone | MarbleHoover
  hoover?: string
  scheduler?: string
  timeRange?: { scheduler: string, min: number, max: number }
}

export type GraphNode = {
  name: string
  labels: NodeLabel[]
}
export type GraphEdge = {
  labels: EdgeLabel[]
}

export type Graphs = {
  _sequence: number,
  events: IEvent[],
  main: TypedGraph<IObservableTree | IObserverTree, {}>,
  subscriptions: TypedGraph<IObserverTree, {}>,
  time: TimeComposer
  isStopped: boolean
}

export function isIObserver(a: any): a is IObserverTree {
  return a && "observable" in a
}

function distance(a: IObservableTree | IObserverTree, b: IObservableTree | IObserverTree): number | undefined {
  if (isIObserver(a) && isIObserver(b)) {
    return a.observable.id === b.observable.id ? 0.21 : 1
  }
  return undefined
}

function blueprintObserver(tree: IObservableTree, sink?: IObserverTree): IObserverTree[] {
  let subs: IObserverTree[] = []
  let sub = new ObserverTree(tree.id + "-blueprint")
  subs.push(sub)
  sub.setObservable([tree])
  if (sink) {
    sub.setSink([sink])
  }

  if (tree.sources && tree.sources.length) {
    tree.sources.forEach(source => {
      subs.push(...blueprintObserver(source, sub))
    })
  }

  return subs
}

function addBlueprints(
  subscriptions: TypedGraph<IObserverTree, {}>,
  full: TypedGraph<IObserverTree | IObservableTree, {}>
): TypedGraph<IObserverTree, {}> {
  try {
    let endpoints = full.sinks().map(n => full.node(n)).filter(n => !isIObserver(n)) as Array<IObservableTree>
    let observers = endpoints.flatMap(observable => blueprintObserver(observable))
    let copy = subscriptions.filterNodes(_ => true)
    observers.forEach(s => copy.setNode(s.id, s))
    observers.forEach(s => s.sink && copy.setEdge(s.id, s.sink.id))
    return copy
  } catch (e) {
    console.warn("Failed adding blueprints :(", e)
    return subscriptions
  }
}

function getObservableId(input: IObservableTree | IObserverTree, node: string): string {
  if (isIObserver(input) && input.observable) {
    return input.observable.id
  } else if (input && input.id) {
    return input.id
  } else {
    console.warn("getObservableId for undefined node", node, input, new Error())
    return ""
  }
}

function getLayoutObservableId(graph: TypedGraph<IObservableTree | IObserverTree, any>, node: string): string {
  // TODO: this workaround is needed since layout hierarchies are computed 
  // for intermediate layout nodes too, but the layout engine should do this conversion itself
  if (node.indexOf("dummy-") === 0) {
    return getObservableId(graph.node(node.split("-")[1]), node)
  } else {
    return getObservableId(graph.node(node), node)
  }
}

type EqualityInput = {
  _sequence: number,
  graphs: Graphs,
  viewState: ViewState,
  focusNodes: string[],
}

function listEqual<T>(a: T[], b: T[], comparator: (a: T, b: T) => boolean = (c, d) => c === d): boolean {
  return a === b || a.length === b.length && a.every((ia, index) => comparator(ia, b[index]))
}

function graphEqual<N1, E1, N2, E2>(a: TypedGraph<N1, E1>, b: TypedGraph<N2, E2>): boolean {
  return a.edgeCount() === b.edgeCount() && a.nodeCount() === b.nodeCount() &&
    listEqual(a.nodes(), b.nodes()) &&
    listEqual(a.edges(), b.edges(), (c, d) => c.v === d.v && c.w === d.w)
}

function flowSelectionEqual(
  a: SelectionGraphEdge | SelectionGraphNode | SelectionGraphNone | MarbleHoover | MarbleClick,
  b: SelectionGraphEdge | SelectionGraphNode | SelectionGraphNone | MarbleHoover | MarbleClick
) {
  if (typeof a === "undefined" && typeof b === "undefined") { return true }
  if (typeof a === "undefined" || typeof b === "undefined") { return false }
  return a.type === b.type && Object.keys(a).every(k => (a as any)[k] === (b as any)[k])
}

function viewStateEqual(a: ViewState, b: ViewState): boolean {
  return a.tick === b.tick && flowSelectionEqual(a.flowSelection, b.flowSelection)
}

function equalData(a: EqualityInput, b: EqualityInput): boolean {
  let r = viewStateEqual(a.viewState, b.viewState) &&
    graphEqual(a.graphs.main, b.graphs.main) && graphEqual(a.graphs.subscriptions, b.graphs.subscriptions)
  return r
}

/** Folding UIEvent's over ViewState's */
function reduce(pstate: ViewState, event: UIEvent): ViewState {
  switch (event.type) {
    case "tickSelection":
      let state = Object.assign({}, pstate)
      state.tick = event.tick
      return state

    case "marbleHooverEnd":
    case "marbleHoover":
      return Object.assign({}, pstate, { hooverSelection: event })

    case "selectionGraphEdge":
    case "selectionGraphNode":
    case "selectionGraphNone":
    case "marbleClick":
      return Object.assign({}, pstate, { flowSelection: event })

    case "diagramOperatorHoover":
      return Object.assign({}, pstate, { hoover: event.observable })

    case "scheduler":
      return Object.assign({}, pstate, {
        scheduler: event.id,
        timeRange: pstate.scheduler === event.id ? pstate.timeRange : undefined,
      })

    case "timeRange":
      return Object.assign({}, pstate, {
        timeRange: {
          max: event.max, min: event.min,
          scheduler: event.scheduler,
        },
      })

    default:
      return pstate
  }
}

    return {
      full: graphs.full,
      main: graphs.main.filterNodes((id, node: GraphNode) => {
        if (id) { return true } // TODO remove dummy
        let annotations = (node ? node.labels || [] : [])
          .filter(ann => ann.label.type === "observable")
        if (annotations.length === 0) {
          return true
        }
        return annotations
          .some(ann => !ann.groups.length || ann.groups.slice(-1).some(g => viewState.openGroups.indexOf(`${g}`) >= 0))
      // kick-off
      .startWith({})
      .scan<ViewState>(reduce, {
        flowSelection: undefined,
        tick: undefined,
      })
  }
        //   groups.length === 0 ||
        //   (groups.find(g => viewState.openGroups.indexOf(`${g}`) >= 0) && true)
      }).filterEdges((ids, edge) => {
        if (ids) { return true } // TODO remove dummy
        return edge && edge.labels
          .filter(ann => ann.edge.label.type === "observable link")
          .some(ann => !ann.groups || !ann.groups.length || ann.groups.slice(-1).some(g => viewState.openGroups.indexOf(`${g}`) >= 0))
      }),
      subscriptions: graphs.subscriptions,
    }
  }
}

type In = Rx.Observable<({ layout: Layout, viewState: ViewState, graphs: Graphs })>
type Out = { svg: Rx.Observable<VNode>, clicks: Rx.Observable<string>, groupClicks: Rx.Observable<string> }
function graph$(inp: In): Out {
          ),
        }))
        .combineLatest(obs, (layoutData, fullData) => Object.assign(fullData, { layout: layoutData.layout }))
      )
      .share()

    let timeComponent = time(inp)
    let { svg, clicks, groupClicks, tickSelection, uievents } = graph$(inp)

    this.timeSlider = timeComponent.vnode
    this.DOM = svg
    this.clicks = clicks
    this.groupClicks = groupClicks
    this.tickSelection = tickSelection
    this.uiEventsOutput = uievents.merge(timeComponent.uievent)
  }

  public stream(uiEvents?: Rx.Observer<UIEvent>): Rx.Observable<{ dom: VNode, timeSlider: VNode }> {
    return new Rx.Observable<{ dom: VNode, timeSlider: VNode }>(subscriber => {
      let subscription = new Rx.Subscription()
      subscription.add(this.DOM
        .combineLatest(this.timeSlider, (d, t) => ({ dom: d, timeSlider: t }))
        .subscribe(subscriber))
      subscription.add(this.uiEventsOutput.subscribe(this.uiEventsInput))
      if (typeof uiEvents !== "undefined") {
        subscription.add(this.uiEventsInput.subscribe(uiEvents))
      }
      return subscription
    })
  }

  // tslint:disable-next-line:max-line-length
  private focusNodes(graphs: Graphs, selection: SelectionGraphEdge | SelectionGraphNode | SelectionGraphNone | MarbleClick | MarbleHoover, initialSelection: boolean = false): string[] {
    if (!selection) {
      if (graphs.subscriptions.nodeCount() > 0 && initialSelection) {
        // Initial selection is first subscribe happening
        return getFlow(graphs.subscriptions, undefined, graphs.subscriptions.nodes()[0])
          .map(_ => _.id)
      }
      return []
    }
    switch (selection.type) {
      case "selectionGraphEdge": return getFlow(graphs.subscriptions, undefined, selection.v, selection.w)
        .map(_ => _.id)
      case "selectionGraphNode":
        let esIn = graphs.subscriptions.inEdges(selection.node) || []
        let esOut = graphs.subscriptions.outEdges(selection.node) || []
        if (esIn.length === 1) {
          return getFlow(graphs.subscriptions, undefined, esIn[0].v, selection.node).map(_ => _.id)
        } else {
          return getFlow(graphs.subscriptions, undefined, selection.node, esOut[0] && esOut[0].w).map(_ => _.id)
        }
      case "marbleHoover":
  let groupClicks = new Rx.Subject<string>()

  // Determine shown subscription flow
  let flowPath: [string, GraphNode][] = []
  if (viewState.focusNodes.length) {
    let subIds = graph.node(viewState.focusNodes[0])
      .labels
    }
  }

  private filter(graphs: Graphs, viewState: ViewState): Graphs {
    let scope = window as any || {}
    scope.filterGraphs = graphs
    // let subs = addBlueprints(graphs.subscriptions, graphs.main)
        let es = graphs.subscriptions.outEdges(subscriptionId)
        let os = graphs.full.outEdges(observableId)
        if (subscriptionId && es) {
          let out = es.map(_ => _.w)[0]
          return { observableId: graphs.full.hasNode(out) ? out : `${graphs.subscriptions.node(out)}`, subscriptionId: out }
        } else if (observableId && os && os.length) {
          let obs = os[0].w || 'no w'
          return { observableId: obs, subscriptionId: graphs.subscriptions.nodes().find(n => `${graphs.subscriptions.node(n)}` === obs) || null }
        // subs
      }
        let es = graphs.subscriptions.inEdges(subscriptionId)
        let os = graphs.full.inEdges(observableId)
        if (subscriptionId && es && es.length) {
          let out = es.map(_ => _.v)[0]
          return { observableId: graphs.full.hasNode(out) ? out : `${graphs.subscriptions.node(out)}`, subscriptionId: out }
        } else if (observableId && os && os.length) {
          let obs = os[0].v || 'no v'
          return { observableId: obs, subscriptionId: graphs.subscriptions.nodes().find(n => `${graphs.subscriptions.node(n)}` === obs) || null }
    }
      }
    }

    let start: { subscriptionId?: string, observableId?: string, current?: boolean } = { observableId: viewState.focusNodes[0], subscriptionId: `${subId}`, current: true }
    let forward = [start]
    let next = start
    let i = 0
    while (next && i++ < 100) {
      next = findNext(true, next.subscriptionId, next.observableId)
      if (next) { forward.push(next) }
    }
    let backward = []
    next = start
    while (next && i++ < 100) {
      next = findNext(false, next.subscriptionId, next.observableId)
      if (next) { backward.unshift(next) }
    }

    console.log("full flow", backward.concat(forward))

    // tslint:disable-next-line:max-line-length
    let inn = collect(subId.toString(10), w => graphs.subscriptions.hasNode(w) ? graphs.subscriptions.inEdges(w).map(e => e.v) : [])
    let out = collect(subId.toString(10), v => graphs.subscriptions.hasNode(v) ? graphs.subscriptions.outEdges(v).map(e => e.w) : [])
    let path = inn.reverse().concat([`${subId}`]).concat(out).reverse()
    flowPath = path.map(v => [
      graphs.main.hasNode(v) ? v : `${graphs.subscriptions.node(v)}`,
      graphs.main.node(graphs.main.hasNode(v) ? v : `${graphs.subscriptions.node(v)}`)
    ] as [string, GraphNode])
    // tslint:ensable:max-line-length
  }

  let flowPathIds = flowPath.map(_ => _[0])
  console.log("flow", flowPath, flowPathIds)

  // Render edges
    let continuation = (hasPref && ups.find(hasPref)) ||
      (hasPref && ups.find(n => oneUpHasPref(n.id))) ||
      ups[0]
    return [node].concat(collectUp(graph, continuation, hasPref, remaining - 1))
  }
  return [node]
}
function collectDown(
  graph: TypedGraph<IObserverTree, {}>, node: IObserverTree,
  hasPref?: (o: IObserverTree) => boolean,
  remaining: number = 50
): IObserverTree[] {
  let outEdges = node && graph.outEdges(node.id)
  if (outEdges && outEdges.length === 1 && remaining > 0) {
    let downs = outEdges.map(e => graph.node(e.w))
    let oneDownHasPref = (n: string) => (graph.outEdges(n) || [])
      .map(e => graph.node(e.w))
      .some(onedown => hasPref(onedown))
    let continuation = (hasPref && downs.find(hasPref)) ||
      (hasPref && downs.find(n => oneDownHasPref(n.id))) ||
      downs[0]
    return [node].concat(collectDown(graph, continuation, hasPref, remaining - 1))
  }
  return [node]
}

  // Render nodes
  function circle(item: { id: string, x: number, y: number }): { svg: VNode[], html: VNode[] } {
    let node = graph.node(item.id)
    let labels = node.labels || []
    let methods = labels.map(nl => nl.label)
      .filter(label => label.type === "observable")
      .reverse()

    let text = item.id + " " + (methods.map((l: any) => `${l.method}(${l.args})`).join(", ") || node.name || item.id)
    return focussed1 && focussed2 ? [
      ...collectUp(graph, focussed1, hasPref).slice(1).reverse(),
      focussed1,
      focussed2,
      ...collectDown(graph, focussed2, hasPref).slice(1),
    ] : []
  } else {
    let focussed = graph.node(ids[0])
    return focussed ? [
      ...collectUp(graph, focussed, hasPref).slice(1).reverse(),
      focussed,
      ...collectDown(graph, focussed, hasPref).slice(1),
    ] : []
  }

}
  // Optionally show flow
  let diagram = [h("div")]
  if (flowPath.length) {
    diagram = renderMarbles(flowPath.map(_ => _[1]))
  }

  let app = h("app", [
  return vna.key.toString().localeCompare(vnb.key.toString())
}

function renderMarbles(nodes: GraphNode[]): VNode[] {
  let coordinator = new MarbleCoordinator()
  let all_events: EventLabel[] = nodes.flatMap(n => n && n.labels || []).map(l => l.label).flatMap(l => l.type === "event" ? [l] : [])
  coordinator.add(all_events)
    if (!node) {
      return [h("div", { attrs: { class: clazz } }, "Unknown node")]
    }
    let obs = node.labels.map(_ => _.label).reverse().find(_ => _.type === "observable") as ObservableLabel
    let events = node.labels.map(_ => _.label).filter(_ => _.type === "event").map((evl: EventLabel) => evl)

    let box = h("div", { attrs: { class: clazz } }, [
      h("div", [], obs ? `${obs.method}(${obs.args})` : node.name),
      h("div", [], "stackFrame locationText"),
    ].filter((_, idx) => idx === 0))


<MSG> Improve call-groups emits and filter groups again

<DFF> @@ -194,11 +194,11 @@ export default class Visualizer {
     return {
       full: graphs.full,
       main: graphs.main.filterNodes((id, node: GraphNode) => {
-        if (id) { return true } // TODO remove dummy
+        // if (id) { return true } // TODO remove dummy
         let annotations = (node ? node.labels || [] : [])
           .filter(ann => ann.label.type === "observable")
         if (annotations.length === 0) {
-          return true
+          return false
         }
         return annotations
           .some(ann => !ann.groups.length || ann.groups.slice(-1).some(g => viewState.openGroups.indexOf(`${g}`) >= 0))
@@ -212,16 +212,37 @@ export default class Visualizer {
         //   groups.length === 0 ||
         //   (groups.find(g => viewState.openGroups.indexOf(`${g}`) >= 0) && true)
       }).filterEdges((ids, edge) => {
-        if (ids) { return true } // TODO remove dummy
+        // if (ids) { return true } // TODO remove dummy
         return edge && edge.labels
           .filter(ann => ann.edge.label.type === "observable link")
-          .some(ann => !ann.groups || !ann.groups.length || ann.groups.slice(-1).some(g => viewState.openGroups.indexOf(`${g}`) >= 0))
+          .some(ann => (
+            !ann.group ||
+            viewState.openGroups.indexOf(`${ann.group}`) === -1
+          ) && (
+              !ann.groups ||
+              !ann.groups.length ||
+              ann.groups.slice(-1).some(g => viewState.openGroups.indexOf(`${g}`) >= 0)
+            )
+          )
       }),
       subscriptions: graphs.subscriptions,
     }
   }
 }
 
+function filterLabels(labels: NodeLabel[], viewState: ViewState): NodeLabel[] {
+  if (typeof labels === "undefined" || !labels.length) { return [] }
+  return labels
+    .filter(l => l.label.type === "observable")
+    .filter(l =>
+      viewState.openGroups.indexOf(`${l.group}`) === -1 && (
+        !l.groups ||
+        !l.groups.length ||
+        l.groups.slice(-1).some(g => viewState.openGroups.indexOf(`${g}`) >= 0)
+      )
+    )
+}
+
 type In = Rx.Observable<({ layout: Layout, viewState: ViewState, graphs: Graphs })>
 type Out = { svg: Rx.Observable<VNode>, clicks: Rx.Observable<string>, groupClicks: Rx.Observable<string> }
 function graph$(inp: In): Out {
@@ -281,7 +302,7 @@ function graph(layout: Layout, viewState: ViewState, graphs: Graphs): {
   let groupClicks = new Rx.Subject<string>()
 
   // Determine shown subscription flow
-  let flowPath: [string, GraphNode][] = []
+  let flowPath: { subscriptionId?: string, observableId?: string }[] = []
   if (viewState.focusNodes.length) {
     let subIds = graph.node(viewState.focusNodes[0])
       .labels
@@ -295,8 +316,9 @@ function graph(layout: Layout, viewState: ViewState, graphs: Graphs): {
         let es = graphs.subscriptions.outEdges(subscriptionId)
         let os = graphs.full.outEdges(observableId)
         if (subscriptionId && es) {
+          console.log("edges down", es, os)
           let out = es.map(_ => _.w)[0]
-          return { observableId: graphs.full.hasNode(out) ? out : `${graphs.subscriptions.node(out)}`, subscriptionId: out }
+          return { observableId: `${graphs.subscriptions.node(out)}`, subscriptionId: out }
         } else if (observableId && os && os.length) {
           let obs = os[0].w || 'no w'
           return { observableId: obs, subscriptionId: graphs.subscriptions.nodes().find(n => `${graphs.subscriptions.node(n)}` === obs) || null }
@@ -305,8 +327,9 @@ function graph(layout: Layout, viewState: ViewState, graphs: Graphs): {
         let es = graphs.subscriptions.inEdges(subscriptionId)
         let os = graphs.full.inEdges(observableId)
         if (subscriptionId && es && es.length) {
+          console.log("edges up", es, os)
           let out = es.map(_ => _.v)[0]
-          return { observableId: graphs.full.hasNode(out) ? out : `${graphs.subscriptions.node(out)}`, subscriptionId: out }
+          return { observableId: `${graphs.subscriptions.node(out)}`, subscriptionId: out }
         } else if (observableId && os && os.length) {
           let obs = os[0].v || 'no v'
           return { observableId: obs, subscriptionId: graphs.subscriptions.nodes().find(n => `${graphs.subscriptions.node(n)}` === obs) || null }
@@ -314,35 +337,28 @@ function graph(layout: Layout, viewState: ViewState, graphs: Graphs): {
       }
     }
 
-    let start: { subscriptionId?: string, observableId?: string, current?: boolean } = { observableId: viewState.focusNodes[0], subscriptionId: `${subId}`, current: true }
-    let forward = [start]
-    let next = start
-    let i = 0
-    while (next && i++ < 100) {
-      next = findNext(true, next.subscriptionId, next.observableId)
-      if (next) { forward.push(next) }
-    }
-    let backward = []
-    next = start
-    while (next && i++ < 100) {
-      next = findNext(false, next.subscriptionId, next.observableId)
-      if (next) { backward.unshift(next) }
-    }
+    if (typeof subId !== "undefined") {
+      let start: { subscriptionId?: string, observableId?: string, current?: boolean } = { observableId: viewState.focusNodes[0], subscriptionId: `${subId}`, current: true }
+      let forward = [start]
+      let next = start
+      let i = 0
+      while (next && i++ < 100) {
+        next = findNext(true, next.subscriptionId, next.observableId)
+        if (next) { forward.push(next) }
+      }
+      let backward = []
+      next = start
+      while (next && i++ < 100) {
+        next = findNext(false, next.subscriptionId, next.observableId)
+        if (next) { backward.unshift(next) }
+      }
 
-    console.log("full flow", backward.concat(forward))
-
-    // tslint:disable-next-line:max-line-length
-    let inn = collect(subId.toString(10), w => graphs.subscriptions.hasNode(w) ? graphs.subscriptions.inEdges(w).map(e => e.v) : [])
-    let out = collect(subId.toString(10), v => graphs.subscriptions.hasNode(v) ? graphs.subscriptions.outEdges(v).map(e => e.w) : [])
-    let path = inn.reverse().concat([`${subId}`]).concat(out).reverse()
-    flowPath = path.map(v => [
-      graphs.main.hasNode(v) ? v : `${graphs.subscriptions.node(v)}`,
-      graphs.main.node(graphs.main.hasNode(v) ? v : `${graphs.subscriptions.node(v)}`)
-    ] as [string, GraphNode])
-    // tslint:ensable:max-line-length
+      flowPath = backward.concat(forward).filter(v => typeof v.subscriptionId !== "undefined")
+      // tslint:ensable:max-line-length
+    }
   }
 
-  let flowPathIds = flowPath.map(_ => _[0])
+  let flowPathIds = flowPath.map(_ => _.observableId)
   console.log("flow", flowPath, flowPathIds)
 
   // Render edges
@@ -375,9 +391,10 @@ function graph(layout: Layout, viewState: ViewState, graphs: Graphs): {
   // Render nodes
   function circle(item: { id: string, x: number, y: number }): { svg: VNode[], html: VNode[] } {
     let node = graph.node(item.id)
-    let labels = node.labels || []
+    let labels = filterLabels(node.labels, viewState)
     let methods = labels.map(nl => nl.label)
       .filter(label => label.type === "observable")
+      .filter(label => "method" in label)
       .reverse()
 
     let text = item.id + " " + (methods.map((l: any) => `${l.method}(${l.args})`).join(", ") || node.name || item.id)
@@ -512,7 +529,7 @@ function graph(layout: Layout, viewState: ViewState, graphs: Graphs): {
   // Optionally show flow
   let diagram = [h("div")]
   if (flowPath.length) {
-    diagram = renderMarbles(flowPath.map(_ => _[1]))
+    diagram = renderMarbles(flowPath.filter(_ => graphs.main.hasNode(_.observableId)).map(_ => graphs.full.node(_.observableId)), viewState)
   }
 
   let app = h("app", [
@@ -578,7 +595,7 @@ function vnodeSort(vna: VNode, vnb: VNode): number {
   return vna.key.toString().localeCompare(vnb.key.toString())
 }
 
-function renderMarbles(nodes: GraphNode[]): VNode[] {
+function renderMarbles(nodes: GraphNode[], viewState: ViewState): VNode[] {
   let coordinator = new MarbleCoordinator()
   let all_events: EventLabel[] = nodes.flatMap(n => n && n.labels || []).map(l => l.label).flatMap(l => l.type === "event" ? [l] : [])
   coordinator.add(all_events)
@@ -594,11 +611,11 @@ function renderMarbles(nodes: GraphNode[]): VNode[] {
     if (!node) {
       return [h("div", { attrs: { class: clazz } }, "Unknown node")]
     }
-    let obs = node.labels.map(_ => _.label).reverse().find(_ => _.type === "observable") as ObservableLabel
+    let obs = filterLabels(node.labels, viewState).map(_ => _.label)[0] as ObservableLabel
     let events = node.labels.map(_ => _.label).filter(_ => _.type === "event").map((evl: EventLabel) => evl)
 
     let box = h("div", { attrs: { class: clazz } }, [
-      h("div", [], obs ? `${obs.method}(${obs.args})` : node.name),
+      h("div", [], obs && obs.method ? `${obs.method}(${obs.args})` : node.name),
       h("div", [], "stackFrame locationText"),
     ].filter((_, idx) => idx === 0))
 

{'addition_count': 55, 'commit_subject': 'Improve call-groups emits and filter groups again', 'deletion_count': 38, 'file_extension': '.ts', 'lang': 'ts', 'license': 'mit', 'repo_name': 'hermanbanken/RxFiddle'}