Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bokeh SVG plot unsupported #480

Closed
LorenzoPeve opened this issue Aug 3, 2022 · 11 comments
Closed

Bokeh SVG plot unsupported #480

LorenzoPeve opened this issue Aug 3, 2022 · 11 comments
Assignees

Comments

@LorenzoPeve
Copy link

LorenzoPeve commented Aug 3, 2022

Hello,

I am creating .svg figures using Bokeh Python library. The figures render good in the browser but when I do
`pdf.image(filepath)' I get an error saying ValueError: unsupported color specification rgb(255,255,255)

I tried searching for a solution but didn't go far. I am not sure what the root cause of the issue is. Any pointers?

Full error details here

File "c:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\tests\test_inclinometers.py", line 431, in <module>
    test_RST_plotting_vector()
  File "c:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\tests\test_inclinometers.py", line 396, in test_RST_plotting_vector
    rst_plt.plots_to_pdf(plots, filespath)
  File "c:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\hagtl\instrumentation\inclinometers\rst_plotting.py", line 376, in plots_to_pdf
    pdf.image(filename) #x = 1, y = 0.25, w = 7
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\fpdf.py", line 300, in wrapper
    return fn(self, *args, **kwargs)
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\fpdf.py", line 3313, in image
    return self._vector_image(img, x, y, w, h, link, title, alt_text)
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\fpdf.py", line 3386, in _vector_image
    svg = SVGObject(img.getvalue())
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 826, in __init__
    self.convert_graphics(svg_tree)
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 876, in convert_graphics
    self.build_group(root_tag, base_group)
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 1069, in build_group
    pdf_group.add_item(self.build_path(child))
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 1086, in build_path
    apply_styles(pdf_path, path)
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 295, in apply_styles
    attr, value = converter(svg_element.attrib[svg_attr])
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 222, in <lambda>
    "fill": lambda colorstr: ("fill_color", optional(colorstr, svgcolor)),
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 215, in optional
    return inheritable(value, converter)
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 207, in inheritable
    return converter(value)
  File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 166, in svgcolor
    raise ValueError(f"unsupported color specification {colorstr}")
ValueError: unsupported color specification rgb(255,255,255)
(base) PS C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library>
@Lucas-C
Copy link
Member

Lucas-C commented Aug 4, 2022

Hi @LorenzoPeve!

Thank you for reporting this.

Could you provide the SVG file you tried to embed in the PDF, please?
Without it, it will be very difficult for us to reproduce the problem and help you.

@gmischler
Copy link
Collaborator

It looks like the fpdf SVG parser currently only supports HTML color names ("palegoldenrod", "mediumorchid", "lightseagreen", etc.) and hex strings (#aa22ee).

However, your example uses the CSS color format rgb(222,012,123).

If that is actually a requirement in SVG (most likely, given that it is both produced and accepted by other software), then it should be fairly straightforward to implement.

@Lucas-C Lucas-C self-assigned this Aug 4, 2022
@Lucas-C
Copy link
Member

Lucas-C commented Aug 4, 2022

I'm working on implementing this in #481

@Lucas-C
Copy link
Member

Lucas-C commented Aug 4, 2022

This has been fixed on the mater branch of this repo, and can be tested with:

pip install git+https://github.com/PyFPDF/fpdf2.git@master

This fix will be part of the next release.

@LorenzoPeve
Copy link
Author

This is great! Thank you so much for the promptness, as I was getting close to a deadline.
I'll go ahead and get the development version until the next release.

@LorenzoPeve
Copy link
Author

Hello, I tried out the development version, and I am getting the following error. I just want to know if there is anything I can do on my end or if my SVG files don't work for some reason. I can make adjustments on my end to change the figures.

I have a reproducible example of my SVG. This is the code I am using, and I created dummy data.
Thanks again.

Error:

Traceback (most recent call last):
File "c:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\tests\test_inclinometers.py", line 439, in
test_RST_plotting_vector()
File "c:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\tests\test_inclinometers.py", line 396, in test_RST_plotting_vector
rst_plt.plots_to_pdf(plots, filespath)
File "c:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\hagtl\instrumentation\inclinometers\rst_plotting.py", line 395, in plots_to_pdf
pdf.image(filename, h = 10, w = 7)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\fpdf.py", line 317, in wrapper
return fn(self, *args, **kwargs)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\fpdf.py", line 3348, in image
return self._vector_image(img, x, y, w, h, link, title, alt_text)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\fpdf.py", line 3423, in _vector_image
svg = SVGObject(img.getvalue())
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 898, in init
self.convert_graphics(svg_tree)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 948, in convert_graphics
self.build_group(root_tag, base_group)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 1142, in build_group
pdf_group.add_item(self.build_path(child))
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 1159, in build_path
apply_styles(pdf_path, path)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 325, in apply_styles
attr, value = converter(svg_element.attrib[svg_attr])
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 270, in
optional(
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 240, in optional
return inheritable(value, converter)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 232, in inheritable
return converter(value)
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 271, in
dasharray, lambda da: [float(item) for item in NUMBER_SPLIT.split(da)]
File "C:\Users\lpeve\Documents\LP_Repo\ha-geotechnical-library\venv\lib\site-packages\fpdf\svg.py", line 271, in
dasharray, lambda da: [float(item) for item in NUMBER_SPLIT.split(da)]
ValueError: could not convert string to float: ''

import pandas as pd
import numpy as np
from fpdf import FPDF
import bokeh
from bokeh.plotting import figure, show
from bokeh.palettes import plasma, viridis
from bokeh.models import tickers, ranges
from bokeh.models.annotations import Legend
from datetime import datetime

def _get_rst_template(
        title: str, y_end: int, width: int = 350, height: int = 750, 
        x_range: list = [-1,1]) -> bokeh.plotting.Figure:
    
    """Helper function that returns a bokeh.plotting.Figure which serves as a 
    template canvas for later plotting inclinometer data. Sets the desired
    aesthetics (gridlines, axis labels, tick locations, etc) and configuration 
    for later plotting.
    
    Args:
        title (str): Plot's title.
        y_end (int): Y-axis endpoint.
        width (int): Plot's width. Defaults to 350.
        height (int): Plot's height. Defaults to 750.
        x_range (list): X-axis limits. Defaults to [-1, 1].
    """ 
    p = figure(
            title=title, x_axis_label='Profile Change (in)', 
            y_axis_label='Depth (ft)', outline_line_color = 'black',
            width = width, height = height)

    # Plot
    p.frame_width = width
    p.frame_height = height
    p.border_fill_color = None
    p.outline_line_color = 'black'
    
    # Axes
    p.axis.axis_label_text_font = 'Arial'
    p.axis.axis_label_text_font_size = '14px'
    p.axis.axis_label_text_font_style = 'normal'
    p.axis.axis_label_text_color = 'black'
    p.axis.major_tick_in = 6
    p.axis.major_tick_out = 6
    p.axis.minor_tick_in = 3
    p.axis.minor_tick_out = 0

    # Ranges & Ticks
    p.x_range = ranges.Range1d(start = x_range[0], end = x_range[-1])
    p.y_range = ranges.Range1d(start = y_end+2, end = -2)
    ticks = list(range(0,y_end+1,5))
    m_tikcs = np.arange(2.5,y_end+1,5)
    p.yaxis.ticker = tickers.FixedTicker(ticks = ticks, minor_ticks =m_tikcs)
    
    # GridLines
    p.ygrid.minor_grid_line_color = '#CBCBCB'
    p.ygrid.minor_grid_line_dash = 'dashed'
    p.ygrid.grid_line_color = '#CBCBCB'
    
    # Legend
    p.add_layout(Legend())
    p.legend.border_line_color = "black"
    p.legend.border_line_alpha = 1
    p.legend.click_policy="hide"

    # Title
    p.title_location = "above"
    p.title.align = "center"
    p.title.text_color = "black"
    p.title.text_font_style = 'bold'
    p.title.text_font_size = "20px"
    
    return p

def plot_rst_vector(
        df: pd.DataFrame, width: int = 350, height: int = 750, 
        x_range: list = [-1,1], pallete = 'plasma', toolbar: bool = True, 
        show_markers: bool = True) -> list[bokeh.plotting.Figure]:
    
    """Returns a list of displacement versus depth graphs. This function is
    supposed to take a subset of all the dates for which data is available.
    The graph will still plot but the legend will become illegible.
    
    Function iterates through all the inclinometers in a displacement
    dataframe, and creates one graph for each inclinometer.
    
    Args:
        df (pd.DataFrame): DataFrame with displacements where index is of type
            'DatetimeIndex' and columns correspond to each of the depth
            intervals for all inclinometers in the project. This should be a
            subset of the entire displacement dataframe. Dates on index must
            be unique.
        width (int): Plot's width. Defaults to 350.
        height (int): Plot's height. Defaults to 750.
        x_range (list): X-axis limits. Defaults to [-1, 1].
        pallete (str): Color pallete. One of "plasma" or "viridis".
        toolbar (bool): Whether or not to show the toolbar. Defaults to True.

    Returns:
        list[bokeh.plotting.Figure]: List of displacement versus depth graphs.

    Raises:
        ValueError: If dataframe has duplicate values in its index. 

    """ 

    # Input Checks
    if df.index.has_duplicates:
        raise ValueError ('Displacement DataFrame has duplicate values in ' + 
                        'index')

    # Initialize container
    plots = []

    # Separate Inclinometers
    f = lambda x: x.split('_')[0]
    grp = df.groupby(f, axis = 1, sort = False)

    # Set Pallete. NOTE: using offset we avoid getting the light yellow colors
    # at the end of the pallete which are hard to see.
    offset = 6
    if pallete == 'plasma':
        colors = plasma(len(df)+offset)[:-offset][::-1]
    else:
        colors = viridis(len(df)+offset)[:-offset][::-1]

    # Iterate through each Groupby.group
    for inclinometer, disp_columns in grp.groups.items():

        # Get Max Depth
        y_max = int((len(disp_columns) - 1) * 5)
   
        # Create y array
        y = list(range(0,y_max+1, 5))

        # Get Canvas
        p= _get_rst_template(inclinometer,  y_max , width= width, 
            height = height, x_range = x_range )
  
        # Iterate through each data row
        for date, color in zip(df.index, colors):            
            xArr = df.loc[date, disp_columns].values
            p.line(x = xArr, y = y, color = color, line_width = 1.5, 
                legend_label = str(date))

            # If True add scatter points
            if show_markers:
                p.scatter(x=xArr, y=y, color=color, size=7, marker='triangle', 
                    line_width=1.5, legend_label = str(date))

        # Toolbar
        if not toolbar:
            p.toolbar_location = None

        p.add_layout(p.legend[0], 'right')
        p.output_backend = "svg"
        plots.append(p)

    return plots


df = pd.DataFrame( [[0.25,0.25,0.25,0.25,0.25],
                    [0.5,0.5,0.5,0.5,0.5],
                    [0.75,0.75,0.75,0.75,0.75]],
    columns = ['DT85-1_0', 'DT85-1_5',  'DT85-1_10' , 'DT85-1_15' , 'DT85-1_20'],
    index = [datetime(2022, 3, 31), datetime(2022, 5, 31), datetime(2022, 7, 22)])

p = plot_rst_vector(df)
show(p[0])

def plots_to_pdf(plots: list) -> None:
    
    """Creates a PDF from a list of plots where each plot takes a page.

    Args:
        plots(list): List of Bokeh Figures (bokeh.plotting.Figure)
        filepath (str): Output path with filename and extension.
    
    """
    pdf = FPDF('P','in', format = "letter" )
    pdf.l_margin = 0
    pdf.r_margin = 0
    pdf.t_margin = 0
    pdf.b_margin = 0

    for i, plot in enumerate(plots):        
        pdf.add_page()
        filename = 'plot' + str(i) + '.svg'
        export_svg(plot, filename=filename)
        pdf.image(filename, x =1 , y = 1, h = 10, w = 7)
        os.remove(filename)
    pdf.output()
    return

plots_to_pdf(p)

@gmischler
Copy link
Collaborator

@LorenzoPeve, as requested before, could you please upload a minimal SVG file demonstrating the error?

It's rather inconvenient having to install several large third-party packages just to test something on our side, and I can't always tell what the problem is just by looking at the traceback.

@LorenzoPeve
Copy link
Author

Here is a sample SVG file. Let me know if there is anything else I should provide.

bokeh_plot (1)

@gmischler
Copy link
Collaborator

Here is a sample SVG file. Let me know if there is anything else I should provide.

Ah thanks.

Looks like we need to take into account that the path attribute 'stroke-dasharray' can be an empty string. Maybe we should generally expect that with all optional attributes, and essentially treat the empty ones as if they weren't there.

@Lucas-C
Copy link
Member

Lucas-C commented Aug 5, 2022

I fixed this in 34ddf55

fpdf2 can now renders the SVG image provided by @LorenzoPeve

@Lucas-C
Copy link
Member

Lucas-C commented Aug 16, 2022

This was released in v2.5.6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants