plumber icon indicating copy to clipboard operation
plumber copied to clipboard

Unable to parse the form part of my multi-content form.

Open priyapatel1 opened this issue 11 months ago • 0 comments

I'm building an app that allows you to upload a csv and turn it into a graph generated through ggplot. Vue frontend, R backend. I've been trying to parse the form portion of my app for a couple days and I'm not able to get it working. I am, however, able to view my csv data and turn it into a data frame.

Here is my Vue frontend.

<script setup>
import { ref } from 'vue'
import axios from 'axios'

const file = ref(null)
const graphType = ref('scatter')
const xAxis = ref('')
const yAxis = ref('')
const graphData = ref(null)
const loading = ref(false)
const error = ref(null)
const csvPreview = ref(null)
const customText = ref('')
const availableColumns = ref([])

const graphTypes = [
  { value: 'scatter', label: 'Scatter Plot' },
  { value: 'line', label: 'Line Graph' },
  { value: 'bar', label: 'Bar Chart' },
  { value: 'histogram', label: 'Histogram' }
]

const handleFileUpload = async (event) => {
  const uploadedFile = event.target.files[0]
  if (uploadedFile && uploadedFile.type === 'text/csv') {
    file.value = uploadedFile
    // Preview CSV data
    const reader = new FileReader()
    reader.onload = async (e) => {
      const text = e.target.result
      const lines = text.split('\n')
      const headers = lines[0].split(',')
      csvPreview.value = {
        headers,
        firstRow: lines[1].split(',')
      }

      // Get column names from the backend
      const formData = new FormData()
      formData.append('file', uploadedFile)
      formData.append('graphType', graphType.value)
      formData.append('xAxis', '')
      formData.append('yAxis', '')
      formData.append('customText', customText.value)

      try {
        const response = await axios.post('http://localhost:8000/api/generate-graph', formData, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })
        availableColumns.value = response.data.columns
      } catch (err) {
        console.error('Error getting column names:', err)
        error.value = 'Error getting column names: ' + (err.response?.data || err.message)
      }
    }
    reader.readAsText(uploadedFile)
  } else {
    error.value = 'Please upload a valid CSV file'
  }
}

const generateGraph = async () => {
  if (!file.value) {
    error.value = 'Please upload a CSV file first'
    return
  }

  if (!xAxis.value || !yAxis.value) {
    error.value = 'Please specify both X and Y axis columns'
    return
  }

  loading.value = true
  error.value = null


  // I think the issue is that there are two types of data being sent to the backend, one is the csv and the other is the form data
  // I need to send the csv as a file and the form data as a form data object
  const formData = new FormData()
  formData.append('file', file.value)
  formData.append('graphType', graphType.value)
  formData.append('xAxis', xAxis.value)
  formData.append('yAxis', yAxis.value)
  formData.append('customText', customText.value)

  // Debug logging
  console.log('Form data being sent:')
  for (let pair of formData.entries()) {
    console.log(pair[0] + ': ' + pair[1])
  }
  console.log('xAxis:', xAxis.value)
  console.log('yAxis:', yAxis.value)
  console.log('graphType:', graphType.value)
  console.log('customText:', customText.value)

  try {

    const response = await axios.post('http://localhost:8000/api/generate-graph', formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    })
    graphData.value = response.data
  } catch (err) {
    console.error('Error details:', err)
    error.value = 'Error generating graph: ' + (err.response?.data || err.message)
  } finally {
    loading.value = false
  }
}

const downloadGraph = async (format) => {
  if (!graphData.value) return

  try {
    const response = await axios.get(`http://localhost:8000/api/download`, {
      params: {
        format,
        path: graphData.value[`${format}_path`]
      },
      responseType: 'blob'
    })

    const url = window.URL.createObjectURL(new Blob([response.data]))
    const link = document.createElement('a')
    link.href = url
    link.setAttribute('download', `graph.${format}`)
    document.body.appendChild(link)
    link.click()
    link.remove()
  } catch (err) {
    error.value = 'Error downloading graph: ' + err.message
  }
}

</script>

<template>
  <div class="container">
    <h1>EasyGraph - Data Visualization Made Simple</h1>

    <div class="upload-section">
      <input type="file" accept=".csv" @change="handleFileUpload" class="file-input">
      <p v-if="error" class="error">{{ error }}</p>

      <div class="control-group">
        <label for="customText">Custom Text:</label>
        <input type="text" v-model="customText" id="customText" placeholder="Enter custom text">
      </div>

      <div v-if="csvPreview" class="csv-preview">
        <h3>CSV Preview</h3>
        <p>Available columns: {{ csvPreview.headers.join(', ') }}</p>
        <p>First row: {{ csvPreview.firstRow.join(', ') }}</p>
      </div>
    </div>

    <div class="controls" v-if="file">
      <div class="control-group">
        <label for="graphType">Graph Type:</label>
        <select v-model="graphType" id="graphType">
          <option v-for="type in graphTypes" :key="type.value" :value="type.value">
            {{ type.label }}
          </option>
        </select>
      </div>

      <div class="control-group">
        <label for="xAxis">X-Axis:</label>
        <select v-model="xAxis" id="xAxis" class="column-select">
          <option value="">Select a column</option>
          <option v-for="column in availableColumns" :key="column" :value="column">
            {{ column }}
          </option>
        </select>
      </div>

      <div class="control-group">
        <label for="yAxis">Y-Axis:</label>
        <select v-model="yAxis" id="yAxis" class="column-select">
          <option value="">Select a column</option>
          <option v-for="column in availableColumns" :key="column" :value="column">
            {{ column }}
          </option>
        </select>
      </div>

      <button @click="generateGraph" :disabled="loading" class="generate-btn">
        {{ loading ? 'Generating...' : 'Generate Graph' }}
      </button>
    </div>

    <div class="graph-container" v-if="graphData">
      <img :src="`http://localhost:8000/api/download?format=png&path=${graphData.png_path}`" alt="Generated Graph"
        class="graph-image">
      <div class="download-buttons">
        <button @click="downloadGraph('png')">Download PNG</button>
        <button @click="downloadGraph('pdf')">Download PDF</button>
      </div>
    </div>
  </div>
</template>

Here is my R backend: app.R

library(plumber)
pr <- plumber::plumb("server/plumber.R")
pr$run(port = 8000, host = "0.0.0.0") 

plumber.R

#* @filter cors
function(req, res) {
  res$setHeader("Access-Control-Allow-Origin", "*")
  res$setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
  res$setHeader("Access-Control-Allow-Headers", "Content-Type")
  plumber::forward()
  print(req)
  print(req$postBody)
}

#* @post /api/generate-graph
#* @param file:file
#* @param graphType:string
#* @param xAxis:string
#* @param yAxis:string
#* @param customText:string
function(req, file, graphType, xAxis, yAxis, customText) {
  library(ggplot2)
  library(readr)
  
  # Debug prints
  print("Request contents:")
  print("POST body:")
  print(req$postBody)
  print("Parsed parameters:")
  print(paste("graphType:", graphType))
  print(paste("xAxis:", xAxis))
  print(paste("yAxis:", yAxis))
  print(paste("customText:", customText))
  
  # Remove Byte Order Mark (BOM) if present and fix newlines
  clean_string <- gsub("^\ufeff", "", file)
  clean_string <- gsub("\r\n", "\n", clean_string)
  
  # Read into a data frame
  df <- read_csv(I(clean_string))
  print(df)
  
  # Return all received parameters to verify
  list(
    data = df,
    columns = colnames(df),
    receivedParams = list(
      graphType = if(is.null(graphType)) "" else graphType,
      xAxis = if(is.null(xAxis)) "" else xAxis,
      yAxis = if(is.null(yAxis)) "" else yAxis,
      customText = if(is.null(customText)) "" else customText
    )
  )
  
#   # Create the plot based on graph type
#   p <- switch(graphType,
#     "scatter" = ggplot(data, aes_string(x = xAxis, y = yAxis)) + geom_point() + theme_minimal(),
#     "line" = ggplot(data, aes_string(x = xAxis, y = yAxis)) + geom_line() + theme_minimal(),
#     "bar" = ggplot(data, aes_string(x = xAxis, y = yAxis)) + geom_bar(stat = "identity") + theme_minimal(),
#     "histogram" = ggplot(data, aes_string(x = xAxis)) + geom_histogram() + theme_minimal()
#   )
  
#   # Save the plot in both PNG and PDF formats
#   png_path <- tempfile(fileext = ".png")
#   ggsave(png_path, p, width = 10, height = 6)
#   pdf_path <- tempfile(fileext = ".pdf")
#   ggsave(pdf_path, p, width = 10, height = 6)
  
#   # Return the paths to the generated files
#   list(png_path = png_path, pdf_path = pdf_path)
}

#* @get /api/download
#* @param format:string
#* @param path:string

# function(format, path) {
#   readBin(path, "raw", n = file.info(path)$size)
# } 

Here is the output of the code as it stands right now:

(base) priyapatel@Priyas-Air easygraph % Rscript server/app.R
Running plumber API at http://0.0.0.0:8000
Running swagger Docs at http://127.0.0.1:8000/__docs__/
<environment: 0x10f0d1a48>
[1] "------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"file\"; filename=\"test.csv\"\nContent-Type: text/csv\n\nx,y\n1,10\n2,3\n3,5\n4,7\n5,9\n6,5\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"graphType\"\n\nscatter\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"xAxis\"\n\nx\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"yAxis\"\n\ny\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"customText\"\n\n\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE--"
[1] "Request contents:"
[1] "POST body:"
[1] "------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"file\"; filename=\"test.csv\"\nContent-Type: text/csv\n\nx,y\n1,10\n2,3\n3,5\n4,7\n5,9\n6,5\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"graphType\"\n\nscatter\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"xAxis\"\n\nx\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"yAxis\"\n\ny\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE\nContent-Disposition: form-data; name=\"customText\"\n\n\n------WebKitFormBoundaryYm0qDy9DRDsfUGyE--"
[1] "Parsed parameters:"
[1] "graphType: "
[1] "xAxis: "
[1] "yAxis: "
[1] "customText: "
Rows: 6 Columns: 2
── Column specification ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): x, y

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 6 × 2
      x     y
  <dbl> <dbl>
1     1    10
2     2     3
3     3     5
4     4     7
5     5     9
6     6     5

What am I doing wrong?

priyapatel1 avatar May 15 '25 01:05 priyapatel1