Tehsurfer/plotvuer

View on GitHub
src/components/PlotVuer.vue

Summary

Maintainability
Test Coverage
<template>
  <div class="plotvuer_parent" :title="collapseName">
    <div class="ui-controls">
      <el-popover content="Zoom In" placement="left" 
        :appendToBody=false trigger="manual" popper-class="plot-popper" v-model="hoverVisabilities[0].value">
        <el-button icon="el-icon-plus" circle class="zoomIn icon-button" 
          @click="zoomIn()" :disabled="zoomInDisabled" size="mini" slot="reference" @mouseover.native="showToolitip(0)" @mouseout.native="hideToolitip(0)"></el-button>
      </el-popover>
      <el-popover content="Zoom Out" placement="left"
        :appendToBody=false trigger="manual" popper-class="plot-popper" v-model="hoverVisabilities[1].value">
        <el-button icon="el-icon-minus" circle class="zoomOut icon-button"
        @click="zoomOut()" :disabled="zoomOutDisabled"  size="mini" slot="reference" @mouseover.native="showToolitip(1)" @mouseout.native="hideToolitip(1)"></el-button>
      </el-popover>
      <el-popover content="Reset view" placement="left"
        :appendToBody=false trigger="manual" popper-class="plot-popper" v-model="hoverVisabilities[2].value">
        <el-button icon="el-icon-refresh-right" circle class="resetView icon-button"
          @click="resetView()" size="mini" slot="reference" @mouseover.native="showToolitip(2)" @mouseout.native="hideToolitip(2)"></el-button>
      </el-popover>

    </div>
  
    <div class="controls" ref="controls">
      <div class='title'>{{title}}</div>

      <div v-if="plotType !== 'plotly-only'">
        <span>
        <el-select
          class="channel-select"
          ref="selectBox"
          v-model="channelx"
          multiple
          filterable
          collapse-tags
          default-first-option
          :popper-append-to-body="false"
          :placeholder="ui.placeholderx"
        >
          <el-option v-for="item in allChannelsX" :key="item" :label="item" :value="item"></el-option>
        </el-select>
        </span>
        <span v-if="ui.showSecondSelector">
        <el-select
          class="channel-select"
          ref="selectBox2"
          v-model="channely"
          multiple
          filterable
          collapse-tags
          default-first-option
          :popper-append-to-body="false"
          :placeholder="ui.placeholdery"
        >
          <el-option v-for="item in allChannelsY" :key="item" :label="item" :value="item"></el-option>
        </el-select>
        </span>
        <span>
          <el-button class="view-heatmap-button" @click="ui.buttonPlot">{{ui.button}}</el-button>
        </span>
      </div>
    </div>
    
    <div ref="container" class="vue-plotly"/>
    
  </div>
</template>
<script>
/* eslint-disable no-alert, no-console */
import Plotly from './custom-plotly'
import Vue from "vue"
import { Select, Option, Collapse, CollapseItem, Button, Popover} from "element-ui"
import CsvManager from "./csv_manager"
import ReziseSensor from "css-element-queries/src/ResizeSensor"

Vue.use(Select)
Vue.use(Option)
Vue.use(Collapse)
Vue.use(CollapseItem)
Vue.use(Button)
Vue.use(Popover)

export default {
  name: "PlotVuer",
  props:{
    title:{
      type: String,
      default: '',
    },
    url:{
      type: String,
      default: '',
    },
    dataInput:{
      type: Array,
      default: () => []
    },
    plotType:{
      type: String,
      default: 'heatmap'
    },
    yAxisFilter: {
      type: Array,
      default: () => []
    },
    xAxisFilter: {
      type: Array,
      default: () => []
    },
    helpMode: {
      type: Boolean,
      default: false
    }
    
  },
  data: function() {
    return {
      allChannelsX: [],
      allChannelsY: [],
      data: [{ x: [], y: [], type: "scatter" }],
      layout: {
        paper_bgcolor: "rgba(0,0,0,0)",
        plot_bgcolor: "rgba(0,0,0,0)",
        margin: {
          t: 5,
          l: 55,
          r: 55,
          b: 90,
          pad: 4
        },
        dragmode: 'pan' 
      },
      options: {
        type: Object
      },
      hoverVisabilities: [{value: false}, {value: false}, {value: false}, {value: false},{value: false}],
      inHelp: false,
      zoomLevel: 0,
      zoomInDisabled: false,
      zoomOutDisabled: true,
      watchShallow: false,
      csv: new CsvManager(),
      channelx: "Select a channel",
      channely: '',
      collapseName: "Options",
      buttonLabels: ["Plot As Heatmap", "Export as CSV"],
      selected: [],
      internalLayout: {
        ...this.layout,
        datarevision: 1
      }
    };
  },
  computed: {
    ui: function(){
      var ui = {}
      if(this.plotType === "heatmap"){
        ui = {
          'button': 'View Heatmap',
          'buttonPlot': this.heatmapPlotWithFilters,
          'placeholderx': 'Select gene', 
          'placeholdery': 'Select cell/sample',
          'showFirstSelector': true,
          'showSecondSelector': true
        }
      } else {
        ui = {
          'button': 'View Plot',
          'buttonPlot': this.timeseriesSelectedChannelsPlot,
          'placeholderx': 'Select channel',
          'placeholdery': 'Select gene',
          'showFirstSelector': true,
          'showSecondSelector': false,
        }
      }
      return ui
    },
  },
  methods: {
    loadData: function(data) {
      // Send data to plotly directly if 'plotly-only' is specified
      if (this.plotType === 'plotly-only') {
          Plotly.newPlot(this.$refs.container, this.dataInput, this.layout, this.getOptions())
          return 
      } else { // Else we treat the data as if it was in csv format
        this.csv.loadData(data).then(() => {
          this.findTypeThenPlot()
        });
      }
    },
    // loadURL: Takes a url pointing to a csv file as input and plots it
    loadURL: function(url) {
      this.csv.loadFile(url).then(() => {
        this.findTypeThenPlot()
      });
    },
    // findTypeThenPlot: Check the inputs (props) to Plotvuer and plot accordingly
    findTypeThenPlot: function(){
      this.allChannelsX = this.csv.getHeadersExceptForFirst();
      this.allChannelsY = this.csv.getColoumnByIndex(0)
      // Check if plot type is provided
      if (this.plotType === 'heatmap') {
        // Check if filters are definded
        if (this.yAxisFilter.length > 1){
          this.channelx = this.yAxisFilter
          this.channely = this.xAxisFilter
          this.heatmapPlotWithFilters()
        }
        // Plot all data if no filters are defined
        else{
          this.heatmapPlotAll()
        }
      } else { // If no plot type is defined, we attempt to deduce it with 'getDataType'
        this.data[0].x = this.csv.getColoumnByIndex(0)
        this.data[0].y = this.csv.getColoumnByIndex(1)
        this.data[0].type = this.csv.getDataType()
        this.plot_channel(this.csv.getHeaderByIndex(1))
        Plotly.newPlot(this.$refs.container, this.data, this.layout, this.getOptions())
      }
    },
    // plot_channel: plot a singular channel from a given coloumn name
    plot_channel: function(channel = false) {
      if (channel){
        this.data[0].x = this.csv.getColoumnByIndex(0)
        this.data[0].y = this.csv.getColoumnByName(channel)
        this.data[0].type = this.csv.getDataType() 
      }
    },
    // timeseriesSelectedChannelsPlot: Plot all selected channels
    timeseriesSelectedChannelsPlot: function(){
      this.data = []
      for(let i in this.channelx){
        this.data.push([])
        this.data[i].x = this.csv.getColoumnByIndex(0)
        this.data[i].y = this.csv.getColoumnByName(this.channelx[i])
        this.data[i].type = this.csv.getDataType()
      }
      Plotly.react(this.$refs.container, this.data, this.layout, this.getOptions())
    },
    // heatmapPlotWithFilters: Plot a heatmap with given x and y filters
    heatmapPlotWithFilters: function (){
      var data = this.csv.getByAxes(this.channelx, this.channely)
      var tdata = [
          {
            z: data,
            x: this.channelx,
            y: this.channely,
            type: "heatmap"
          }
        ];
        Plotly.react(this.$refs.container, tdata, this.layout, this.getOptions())
      
    },
    // heatmapPlotAll: plot all data avialable for heatmap 
    heatmapPlotAll: function (){
      var tdata = [
          {
            z: this.csv.getDataValuesOnly(),
            x: this.csv.getHeadersExceptForFirst(),
            y: this.csv.getColoumnByIndex(0)  ,
            type: "heatmap"
          }
        ];
        Plotly.react(this.$refs.container, tdata, this.layout, this.getOptions())
    },
    // handleResize: listener to resize plotly canvas and redraw
    handleResize: function() {
      new ReziseSensor(this.$el, () => {
        // this.layout.title =
        //   "Width now:" + this.$el.clientWidth + " Height now: " + (this.$el.parentElement.clientHeight - this.$refs.selectBox.$el.clientHeight)
        Plotly.relayout(this.$refs.container, {
          width: this.$el.clientWidth,
          height: this.$el.parentElement.clientHeight - this.$refs.controls.clientHeight
        });
      });
    },
    // zoomIn: Findd and clickd the plolty modebar 'zoom in' 
    zoomIn: function(){
      this.zoomLevel++
      this.$el.querySelector('a[data-attr="zoom"][data-val="in"]').click()
      this.setDisabledButtons(this.zoomLevel)
    },
    // zoomOut: Findd and clickd the plolty modebar 'zoom out' 
    zoomOut: function(){
      this.zoomLevel--
      this.$el.querySelector('a[data-attr="zoom"][data-val="out"]').click()
      this.setDisabledButtons(this.zoomLevel)
    },
    // setDisabledButtons(zoomLevel): Disable buttons once they hit max zoom
    setDisabledButtons(zoomLevel){
      if (zoomLevel === 8){
        this.zoomInDisabled = true
      } else {
        this.zoomInDisabled = false
      }
      if (zoomLevel === 0){
        this.zoomOutDisabled = true
      } else {
        this.zoomOutDisabled = false
      }
      return
    },
    resetView: function(){
      this.zoomLevel = 0
      this.$el.querySelector('a[data-attr="zoom"][data-val="reset"]').click()
      this.setDisabledButtons(this.zoomLevel)
    },
    setHelpMode: function(helpMode){
      if (helpMode){
        this.inHelp = true
        this.hoverVisabilities.forEach( (item) =>{
          item.value = true
        });
      } else {
        this.inHelp = false
        this.hoverVisabilities.forEach( (item) =>{
          item.value = false
        });
      }
    },
    showToolitip: function(tooltipNumber){
      if (!this.inHelp){
        this.hoverVisabilities[tooltipNumber].value = true;
        this.tooltipWait = setTimeout( ()=>{this.hoverVisabilities[tooltipNumber].value = true}, 1000)
      }
    },
    hideToolitip: function(tooltipNumber){
      if (!this.inHelp){
        this.hoverVisabilities[tooltipNumber].value = false
        clearInterval(this.tooltipWait)
      }
    },
    plot() {
      return Plotly.plot(this.$refs.container, this.data, this.layout, this.getOptions())
    },
    getOptions() {
      let el = this.$refs.container
      let opts = this.options

      // if width/height is not specified for toImageButton, default to el.clientWidth/clientHeight
      if (!opts) opts = {}
      if (!opts.toImageButtonOptions) opts.toImageButtonOptions = {}
      if (!opts.toImageButtonOptions.width) opts.toImageButtonOptions.width = el.clientWidth
      if (!opts.toImageButtonOptions.height) opts.toImageButtonOptions.height = el.clientHeight
      opts.scrollZoom = true
      return opts
    },
    newPlot() {
      return Plotly.newPlot(this.$refs.container, this.data, this.layout, this.getOptions())
    },
    react() {
      return Plotly.react(this.$refs.container, this.data, this.layout, this.getOptions())
    },
  },
  mounted() {
    if(this.url != ''){
      this.loadURL(this.url)
      this.react()
    } else if (this.dataInput.length != 0) {
      this.loadData(this.dataInput)
    }
    this.handleResize()
    this.$watch('data', () => {
      this.internalLayout.datarevision++
      this.react()
    }, { deep: !this.watchShallow })

    this.$watch('options', this.react, { deep: !this.watchShallow })
    this.$watch('layout', this.relayout, { deep: !this.watchShallow })
    
  },
  watch: {
    dataInput: function() {    
      this.loadData(this.dataInput)    
    },
    helpMode: function(val){
      this.setHelpMode(val)
    }
  },
  destroyed() {},
  beforeDestroy() {
    this.__generalListeners.forEach(obj => this.$refs.container.removeAllListeners(obj.fullName))
    Plotly.purge(this.$refs.container)
  }
};
</script>
<style scoped src="element-ui/lib/theme-chalk/index.css"></style>
<style scoped>
.controls{
  padding-left: 55px;
  padding-top: 5px;
  align-items: left;
  text-align: left;
}
.ui-controls{
  position: absolute;
  height: 100%;
  width: 40px;
  right: 0px;
  pointer-events: none;
  z-index: 5
}

@media only screen and (max-width: 48em) {
  .ui-controls {
    position: absolute;
    height: 100%;
    width: 40px;
    right: 0px;
    top: 160px;
    pointer-events: none;
    z-index: 5
  }
}


.title{
  width: 572px;
  height: 17px;
  font-family: Helvetica;
  font-size: 14px;
  font-weight: 500;
  font-stretch: normal;
  font-style: normal;
  line-height: normal;
  letter-spacing: normal;
  color: #606266;
  padding-top: 8px;
  padding-bottom: 8px;
}
.options {
  position: absolute;
  z-index: 11000;
  height: calc(100% - 20px);
  text-align: right;
  overflow: auto;
  padding-top: 8px;
  padding-bottom: 8px;
}

.channel-select {
  min-width: 220px;
  margin: 8px;
  margin-left: 0px;
  margin-right: 16px;
  border-radius: 4px;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12);

}
.view-heatmap-button{
  border-radius: 4px;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12);
  border: solid 1px #d8dce6;
  background-color: #8300bf;
  margin: 8px;
  margin-left: 0px;
  margin-right: 16px;

  font-size: 14px;
  font-weight: 500;
  font-stretch: normal;
  font-style: normal;
  line-height: 1;
  letter-spacing: normal;
  color: white;
}
.input-div {
  display: flex;
  justify-content: space-between;
}

.zoomIn{
  top:51px;
  right:20px;
  position: absolute;
  pointer-events: auto;
}
.zoomOut{
  top:90px;
  right:20px;
  position: absolute;
    pointer-events: auto;
}
.resetView {
  top:129px;
  right:20px;
  position: absolute;
    pointer-events: auto;
}
.icon-button {
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);
  border: solid 1px #ffffff;
  background-color: #ffffff;
}
.freeSpin {
  bottom:79px;
  right:50%;
  position: absolute;
}
>>> .plot-popper {
  padding:9px 10px;
  min-width:150px;
  font-size:12px;
  color: #fff;
  background-color: #8300bf;  
}
>>> .plot-popper .popper__arrow::after{
  border-left-color: #8300bf !important;
}

>>> .el-select__tags-text {
  max-width: 90px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-block;
  vertical-align: middle;
}
>>> .modebar-container{
  display: none;
}
</style>