twibiral / obsidian-execute-code

Obsidian Plugin to execute code in a note.
MIT License
1.06k stars 67 forks source link

[FR] Implementation of interactive plotly charts (like the matplotlib implementation) #53

Open kkollsga opened 2 years ago

kkollsga commented 2 years ago

Hi, thanks again for a great obsidian plugin. Would love to be able to make plotly charts in obsidian to create an interactive data representation to spice up the notes.

Plotly is a js based charting library also supported in python: https://github.com/plotly/plotly.py

Plotly have been implemented into jupyterlab through an npm plugin: https://www.npmjs.com/package/jupyterlab-plotly

Similarly it would also be nice to implement ipython widgets to the output: https://github.com/jupyter-widgets/ipywidgets ipython widgets are interactive HTML widgets for Jupyter notebook that for instance is used by pandas to generate great looking interactive tables, among many other things.

If interactive outputs are implemented it would also be useful if the code block output is stored together with the notes. Currently the outputs gets cleared if you leave the note.

Thanks a lot =)

twibiral commented 1 year ago

Hi @kkollsga,

This would be great, but we don't know how to implement this yet. It's hard to open a communication channel between the generated plotly / HTML and the python console. Feel free to comment or create a PR if you have an idea.

kkollsga commented 1 year ago

@twibiral I actually made a python script that does what I need.

This is written in a python file:

import glob
class note:
    def __init__(self,note,path=r"C:\My-Notes"):
        self.note=note
        notefile = note+'.md'

        for i in range(5):
            result = glob.glob(path+'\\'+'*\\'*i+notefile)
            if len(result)>0:
                self.notepath = result[0]

    def output(self,text):
        with open( self.notepath, 'r+b') as f:
            offset = 0
            out_tag = []
            for line in f:
                if b'-----\n' == line or b'-----' == line:
                    out_tag.append(offset)
                offset += len(line)
            prefix = ""
            suffix = ""
            if len(out_tag)>1:
                f.seek(out_tag[1]+5)
                the_rest = f.read()
                print(the_rest)
                f.seek(out_tag[0])
            elif len(out_tag)==1:
                f.seek(out_tag[0]+5)
                the_rest = f.read()
                f.seek(out_tag[0])
                if the_rest != b'':
                    suffix = "\n"
            else:
                prefix = "\n\n"

            f.write(str(prefix).encode('utf-8')+b'-----\n\n'+str(text).encode('utf-8')+'\n\n-----'.encode('utf-8')+str(suffix).encode('utf-8'))
            if len(out_tag)>0:
                if the_rest != b'':
                    f.write(the_rest)
                f.truncate()

Then I need to add the following code in the obsidian execute code block:

import importlib.util, sys
spec = importlib.util.spec_from_file_location("obsidian_script", r"C:\obsidian_script.py") 
obs = importlib.util.module_from_spec(spec) 
sys.modules["obsidian_script"] = obs 
spec.loader.exec_module(obs)

# Note file name needs to be manually specified
page = obs.note('Python')

# Some random EU electricity generation data
test="`"+"``chart"+"""
type: bar
labels: [Ireland,Sweden,Slovenia,Netherlands,Malta,Slovakia,Austria,Poland,Portugal,Lithuania,Czechia,Hungary,Belgium,France,Italy,Romania,Germany,Cyprus,Latvia,Croatia,Bulgaria,Spain,Finland,Greece,Denmark,Luxembourg,Estonia,_,Norway,Iceland,__,Turkey,Serbia,Montenegro,North Macedonia,Albania,_,Kosovo,_,Moldova,Ukraine]
series:
  - title: Net change from 2010-2020 (%)
    data: [14.9,10.8,5.7,4.6,4.4,3.7,2.5,1.9,-1.0,-1.6,-4.1,-4.9,-5.4,-6.4,-6.6,-7.2,-8.3,-8.8,-10.3,-10.8,-11.8,-12.5,-13.8,-15.1,-24.4,-51.6,-55.7,0,24.8,12.1,0,44.1,-1.2,-16.5,-26.3,-30.1,0,34.9,0,-9.3,-21.1]
tension: 0.47
width: 100%
labelColors: true
fill: false
beginAtZero: false
"""+"``"+"`"

page.output(test)

I was later able to implement output to plotly through the plotly community plugin. Not sure if any of this could be implemented to the Execute code script though. But at least I enjoy using it =)

chlohal commented 1 year ago

Hi! This script looks great-- I'm not sure if it could be implemented, but just so you know, the note's file name could be attached automatically by using the magic command. You would need to do some post-editing of it, but that's probably easier than manual specification. I hope that helps! :)

kkollsga commented 1 year ago

Thanks for the idea :) Perhaps if it was possible to add a code snippet into a input field under settings, that would automatically run ahead of the code block each time, it wouldnt be necessary to copy code into the code block each time :)

twibiral commented 1 year ago

@kkollsga This is great! Maybe we can add this to an upcoming release.

PS: The feature to define a block that is executed before each block is already implemented since 0.15.0. Just take a look into the settings.

kkollsga commented 1 year ago

Based on your comments I have now finalized the code. If anyone wants to implement this feature.

Step 1, create a .py file and add the following code

class note:
    def __init__(self,notepath):
        self.notepath=notepath
        self.code_sep = "-----"

    def output(self,text):
        with open( self.notepath, 'r+b') as f:
            offset = 0
            out_tag = []
            for line in f:
                if self.code_sep+'\n' == line.decode('utf-8') or self.code_sep == line.decode('utf-8'):
                    out_tag.append(offset)
                offset += len(line)
            prefix = ""
            suffix = ""
            if len(out_tag)>1:
                f.seek(out_tag[1]+5)
                the_rest = f.read()
                f.seek(out_tag[0])
            elif len(out_tag)==1:
                f.seek(out_tag[0]+5)
                the_rest = f.read()
                f.seek(out_tag[0])
                if the_rest != b'':
                    suffix = "\n"
            else:
                prefix = "\n\n"

            f.write((prefix+self.code_sep+'\n'+text+'\n\n'+self.code_sep+suffix).encode('utf-8'))
            if len(out_tag)>0:
                if the_rest != b'':
                    f.write(the_rest)
                f.truncate()

Then add the following code in the "Inject Python code" input area in "Execute Code" settings, remember to update "<insert .py full path here>":

import importlib.util, sys
spec = importlib.util.spec_from_file_location("obsidian_script", "<insert .py full path here>") 
obs = importlib.util.module_from_spec(spec) 
sys.modules["obsidian_script"] = obs 
spec.loader.exec_module(obs)

vault = @vault
note = @note
notepath = vault[12:] + note[11:]
page = obs.note(notepath)

For a test write the followin in your obsidian note:

#run-python
test_string = "This is a test"
page.output(test_string)

This works great, I did hower find an issue if the notepath includes non english symbols. Not sure why though. When I run the code directly in python it finds the outfile without issue, but when running in execute code the script returns file not found. The solution is to just live with english letters in the filepath though. So no worries =)

chlohal commented 1 year ago

Having non-english characters is an issue we've seen before. #126 might help! :)