skalinichev / uplot-wrappers

React, Vue.js and Svelte wrappers for uPlot that allow you to work with charts declaratively inside your favorite framework
MIT License
94 stars 13 forks source link

What is the best way to access the data via context menu? #35

Closed jduncanRadBlue closed 3 months ago

jduncanRadBlue commented 3 months ago

I have a simple graph with data plotted. When a user right clicks on data, I display a context menu with a few options, one of them being a simple "Get Info" option where I'd like to display a dialog containing more detailed data.

I'm having a hard time getting to the data within the graph. So far I have tried using the uplot cursor.idxs to get the location of the click from this example: https://jsfiddle.net/w07t8q4g/

const [xIdx, yIdx] = timelineContainer.value.cursor.idxs

but I'm getting errors:

TypeError: Cannot read properties of undefined (reading 'idxs')

I am not sure how to accomplish this. Can anyone help? Thanks so much!

<template>
<div @contextmenu.prevent="showContextMenu($event)">
  <ContextMenu
      v-model="showMenu"
      :x="menuX"
      :y="menuY"
      :actions="contextMenuActions"
      @action-clicked="handleMenuAction"
    />
<v-navigation-drawer
      v-model="timeline"
      location="bottom"
      width="100%"
      style="transition: none;"
      disable-resize-watcher

    >
          <v-toolbar
            density="compact"
          >

         <v-toolbar-title>Timeline</v-toolbar-title>

        <v-spacer></v-spacer>

        <v-btn icon @click="closeTimeline">
          <v-icon>mdi-close</v-icon>
        </v-btn>
      </v-toolbar>

      <v-main class="pa-0">
        <v-container fluid >
              <v-card >
                <div id="uplot-Container" style="min-height: 350px; max-height: 400px;">
                <!-- <UplotVue ref="timelineContainer" :options="options" :data="plotData" :key="graphKey" /> -->
                <UplotVue v-show="displayAIS" ref="timelineContainer" :options="options" :data="plotData" :key="graphKey" />
                <div v-show="!displayAIS" >
                  <div class="myCol">
                      <span id="mySpan">
                        <v-icon >mdi-information-outline</v-icon>
                        <h4 >Data not displayed.  Select Data in Layers Manager to display Data.</h4>
                      </span>
                  </div>
                </div>
              </div>
              </v-card>
        </v-container>
      </v-main>
        </v-navigation-drawer>
</div>
  </template>

<script setup>
import {onMounted, onUpdated, ref, watch} from 'vue'
import "uplot/dist/uPlot.min.css";

import UplotVue from 'uplot-vue';
import ContextMenu from '@/components/ContextMenu.vue';
import {useFishState} from "@/store/fishState"
const stateStore = useStateStore()
import { useLayerState } from '@/store/lmState';
const layerState = useLayerState();
import {storeToRefs} from 'pinia'

const {timeline} = storeToRefs(stateStore)
const graphKey = ref(0)
const timelineContainer = ref(null)
const height = ref(null)
const width = ref(null)

const showMenu = ref(false)
const menuX = ref(0)
const menuY = ref(0)

const contextMenuActions = ref([
  { label: 'Get Info', action: 'getInfo', icon: 'mdi-information-box-outline', disabled: false },
  { label: 'Sync', action: 'sync', icon: 'mdi-autorenew', disabled: false },
]);

const plotData = stateStore.plotData

const showContextMenu = (event) => {
  //the context menu adds pixels to the x and y to show it offset, so get the correct pixel that was actually right clicked to find feature
  event.preventDefault()
  console.log(event)

  //this is the pixel location for the menu based on where the user right clicked and what gets passed to the contextMenu component for placement on screen.
  menuX.value = event.clientX
  menuY.value = event.clientY
  showMenu.value = true
}

function handleMenuAction(action){
  console.log(action)
  console.log('x: '+menuX.value + ' ' +'y: '+menuY.value)
  if (action === 'getInfo') {
    //Get the feature's data at the location of the click
    const xVal = timelineContainer.value.data[0][menuX.value]
    const yVal = timelineContainer.value.data[1][menuY.value]
    const xPos = timelineContainer.value.valToPos(xVal, 'x');
    const yPos = timelineContainer.value.valToPos(yVal, 'y');
    let popup = document.createElement("div");
    popup.classList.add("popup");
    popup.textContent = yVal;
    popup.style.left = xPos + "px";
    popup.style.top = yPos + "px";

    timelineContainer.value.over.appendChild(popup);
  }
}

const options = ref({
  Title: "AIS Data",
    drawOrder: ["series", "axes"],

    axes: [
      {
        side:0,
        stroke: "white",
        ticks: {
          stroke: "#808080"
        },
        grid: {
          stroke: "rgba(236, 236, 236, 0.5)"
        }
      },
      {
        stroke: "white",
        ticks: {
          stroke: "#808080"
        },
        grid: {
          stroke: "rgba(236, 236, 236, 0.5)"
        },

      }
    ],
    series: [
      {
        label: 'Timstamp'
      },
      {
        label: 'CM',
        stroke: "red",
        width: 4
        //fill: "rgba(255,0,0,0.1)",
        // value: fmtVal,

      },
      {
        label: "NC",
        stroke: "green",
        width: 4
        // fill: "rgba(0,255,0,0.1)",

      },
      {
        label: "OE",
        stroke: "blue",
        width: 4
        //fill: "rgba(0,0,255,0.1)",
        //value: fmtVal,
      },
      {
        label: "SB",
        stroke: "orange",
        width: 4
        //fill: "rgba(0,0,255,0.1)",
        //value: fmtVal,
      },
      {
        label: "ST",
        stroke: "yellow",
        width: 4
        //fill: "rgba(0,0,255,0.1)",
        //value: fmtVal,
      },
    ],
    scales: { x: { time: true } },
  },)

const closeTimeline = () => {
    fishState.setTimeline(false)
}

const {displayAIS} = storeToRefs(layerState);
layerState.$subscribe((mutation, state)=>{
})

onUpdated(() => {
  graphKey.value++
  const element = document.getElementById('uplot-Container')
  const positionInfo = element.getBoundingClientRect()
  const height = positionInfo.height
  const width = positionInfo.width

  options.value = {
    ...options.value,
    height: height - 50,
    width: width - 30
  }
})

onMounted(() => {
 console.log(plotData.value)
})
skalinichev commented 3 months ago

Hi

Sorry I have never used this functionality myself before. Can't really help you much here. Having said that, you still can ask the same question at https://github.com/leeoniya/uPlot/issues Hopefully there are more people that can help you with it!

leeoniya commented 3 months ago

https://github.com/leeoniya/uPlot/issues/989#issuecomment-2299298210

jduncanRadBlue commented 3 months ago

Thanks @leeoniya I have been looking at that and trying to figure out how to access the cursor and location, data in the vue component.
@skalinichev How do I access the data in the chart based on cursor location? I have the @create on the component saving a ref to the chart but the cursor.idxs are always null. Any ideas?

See this code from uPlot:

u.over.addEventListener('click', e => {
          const [xIdx, yIdx] = u.cursor.idxs;

          const xVal = u.data[0][xIdx];
          const yVal = u.data[1][yIdx];

          const xPos = u.valToPos(xVal, 'x');
          const yPos = u.valToPos(yVal, 'y');

          let popup = document.createElement("div");
          popup.classList.add("popup");
          popup.textContent = yVal;
          popup.style.left = xPos + "px";
          popup.style.top = yPos + "px";

          u.over.appendChild(popup);
        });
      }

Here is my code and screen shot of the chart.value being logged to the console. the cursor.idxs is an array of 6 null values

<UplotVue ref="timelineContainer" :options="options" :data="plotData" @click="handleMenuAction" :key="graphKey" @create="onCreate"/>

function handleMenuAction(){
  console.log(chart.value)
}

Screenshot 2024-08-25 at 10 48 47 AM