Dynamically creating SVG in Div

What are you trying to do?

Use a CustomJS callback to create an svg item and assign it to the text of a Div model.

What have you tried that did NOT work as expected?

“Quasi” MRE below. You click the button, and the div updates with a dynamically generated svg inside it.

I’m using d3 to create the svg (simple d3 line plot) inside the callback and use my trust save_html_wJSResources function to have access to d3 library there.

What’s happening is seemingly the size of the svg does not scale with the size of the Div model, or something to that effect. For example I can hard set the Div model to be width/height of 1000, but if I set the svg’s width/height properties similarly, I don’t get what I expect (see my comments in the JS code).

I don’t really know if this is a d3 usage/svg usage error, or if it’s a product of sticking the svgs inner/outerHTML into the div model that causes this. Wondering if anyone can weigh in/diagnose.

Thanks!

from bokeh.plotting import figure, show, save
from bokeh.layouts import column, row
from bokeh.models import CustomJS, Button, Div
from jinja2 import Template
from bokeh.embed import components
from bokeh.resources import Resources

#this is only dumped here for MRE: its used to have access to d3 in the callback
def save_html_wJSResources(bk_obj,fname,resources_list_dict,html_title='Bokeh Plot',theme=None
    ,icon_url='https://aquainsight.sharepoint.com/sites/AquaInsight/_api/siteiconmanager/getsitelogo?type=%271%27&hash=637675014792340093'):
    '''function to save a bokeh figure/layout/widget but with additional JS resources imported at the top of the html
        resources_list_dict is a dict input of where to import additional JS libs/scripts so they can be utilized into CustomJS etc in bokeh work
        e.g. {'sources':['http://d3js.org/d3.v6.js'],'scripts':[var_storing_jsscript]}
        theme arg is to pass a yaml-based theme -->use getTheme() function to retrieve dictionary of custom themes, and pass the yaml key
        icon_url --> point to a url for an image for the icon url
        '''
    if theme is None:
        script, div = components(bk_obj)
        theme={'yaml':None,'css':''}
    else:
        script, div = components(bk_obj, theme=theme)
    print(icon_url)
    if icon_url is None:
        template = Template('''<!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="utf-8">
                <title>'''+html_title+'''</title>
                {{ resources }}
                {{ script }}
            </head>
            <body>
                <div>
                {{ div }}
                </div>
            </body>
        </html>
        ''')
    else:
        template = Template('''<!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="utf-8">
                <link rel="icon" href="'''+icon_url+'''">
                <title>'''+html_title+'''</title>
                {{ resources }}
                {{ script }}
            </head>
            <body>
                <div>
                {{ div }}
                </div>
            </body>
        </html>
        ''')

    
    resources = Resources().render()
    
    for r in resources_list_dict['sources']:
        resources = resources +'''\n<script type='text/javascript' src="'''+r+'''"></script>'''
    for r in resources_list_dict['scripts']:
        resources = resources +'''\n<script type='text/javascript'> '''+r+'''</script>'''  
    
    html = template.render(resources=resources,
                           script=script,
                           div=div)
    print('writing '+fname+'...')
    with open(fname, mode="w", encoding="utf-8") as f:
        f.write(html)
        


btn = Button(label='Sank')

#tried a bunch of sizing options here but don't think the problem is with the bokeh model div...
div = Div(text=''
          # ,styles={'width':'100%','height':'100%'}
          # ,sizing_mode="stretch_both"
          ,width=900,height=900
          )
div.resizable=True
# div.min_height=1000
# div.min_width = 1000

cb = CustomJS(args=dict(div=div)
              ,code='''
              
        //No matter what, the total extent of the plot sits within some predefined size.
        //If I adjust svg Width and svgHeight, the size of the plot (inside the div stays the same but the "zoom" changes)
        //e.g. change svgWidth and svgHeight to 200,150 and see what happens
        var svgWidth = 900, svgHeight = 600;
        div.width = svgWidth
        div.height = svgHeight
		var margin = {top: 10, right: 10, bottom: 30, left: 50};
		var width = svgWidth - margin.left - margin.right;
		var height = svgHeight - margin.top - margin.bottom;

		// Create SVG element
		var svg = d3.create('svg')
			.attr("width", svgWidth)
			.attr("height", svgHeight);

		// Create chart group to provide margin
		var g = svg.append("g")
		  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

		// Prepare data
		var data = [
		  {x:1, y:6},
		  {x:2, y:7},
		  {x:3, y:2},
		  {x:4, y:4},
		  {x:5, y:5}
		];

		// Set up scales
		var x = d3.scaleLinear()
			.domain([0, d3.max(data, function(d) { return +d.x; })])
			.range([0, width]);

		var y = d3.scaleLinear()
			.domain([0, d3.max(data, function(d) { return +d.y; })])
			.range([height, 0]);

		// Add X axis 
		g.append("g")
		  .attr("transform", "translate(0," + height + ")")
		  .call(d3.axisBottom(x));

		// Add Y axis
		g.append("g")
		  .call(d3.axisLeft(y));

		// Add line
		g.append("path")
		  .datum(data)
		  .attr("fill", "none")
		  .attr("stroke", "steelblue")
		  .attr("stroke-width", 1.5)
		  .attr("d", d3.line()
			.x(function(d) { return x(d.x) })
			.y(function(d) { return y(d.y) })
		  );
        console.log(svg) 

        div.text= '<svg>'+svg._groups[0][0].outerHTML+'</svg>'
        
        console.log(div)
              ''')   
              
btn.js_on_click(cb)

save_html_wJSResources(bk_obj=column([btn,div])
                                  , fname=r'test.html'
                                  , resources_list_dict={'sources':['https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.0/d3.min.js'
                                                                    ,'https://cdnjs.cloudflare.com/ajax/libs/d3-sankey/0.12.3/d3-sankey.min.js']
                                                         ,'scripts':[]}
                                  ,html_title='This thing'
                                  )

Figured it out → need to spec the size of the <svg> itself.

e.g. <svg width="900px" height ="900px">

1 Like