diff --git a/examples/plot_heatmap_golden_ratio/main.v b/examples/plot_heatmap_golden_ratio/main.v index e8329e3ef..c8e74eb79 100644 --- a/examples/plot_heatmap_golden_ratio/main.v +++ b/examples/plot_heatmap_golden_ratio/main.v @@ -3,38 +3,38 @@ module main import math import vsl.plot // import vsl.util -// phi = (1 + np.sqrt(5) )/2. # golden ratio -// xe = [0, 1, 1+(1/(phi**4)), 1+(1/(phi**3)), phi] -// ye = [0, 1/(phi**3), 1/phi**3+1/phi**4, 1/(phi**2), 1] -phi := (1 + math.sqrt(5)) / 2.0 -phi_pow_2 := math.pow(phi, 2.0) -phi_pow_3 := math.pow(phi, 3.0) -phi_pow_4 := math.pow(phi, 4.0) -xe := [0.0, 1.0, 1 + (1 / phi_pow_4), 1 + (1 / phi_pow_3), phi] -ye := [0.0, 1 / phi_pow_3, (1 / phi_pow_3) + (1 / phi_pow_4), 1 / phi_pow_2, 1] -z := [[13.0, 3, 3, 5], [13.0, 2, 1, 5], [13.0, 10, 11, 12], [13.0, 8, 8, 8]] +fn main() { + phi := (1 + math.sqrt(5)) / 2.0 + phi_pow_2 := math.pow(phi, 2.0) + phi_pow_3 := math.pow(phi, 3.0) + phi_pow_4 := math.pow(phi, 4.0) + xe := [0.0, 1.0, 1 + (1 / phi_pow_4), 1 + (1 / phi_pow_3), phi] + ye := [0.0, 1 / phi_pow_3, (1 / phi_pow_3) + (1 / phi_pow_4), 1 / phi_pow_2, 1] + z := [[13.0, 3, 3, 5], [13.0, 2, 1, 5], [13.0, 10, 11, 12], + [13.0, 8, 8, 8]] -// TODO: Draw Spiral -// a := 1.120529 -// b := 0.306349 + // TODO: Draw Spiral + // a := 1.120529 + // b := 0.306349 -// theta := util.lin_space(-math.pi/13, 4*math.pi, 1000) -// r := a*math.exp(-b*theta) -// x := r*math.cos(theta) -// y := r*math.sin(theta) + // theta := util.lin_space(-math.pi/13, 4*math.pi, 1000) + // r := a*math.exp(-b*theta) + // x := r*math.cos(theta) + // y := r*math.sin(theta) -mut plt := plot.Plot.new() + mut plt := plot.Plot.new() -plt.heatmap( - x: xe - y: ye - z: z -) -plt.layout( - title: 'Heatmap with Unequal Block Sizes' - width: 750 - height: 750 -) + plt.heatmap( + x: xe + y: ye + z: z + ) + plt.layout( + title: 'Heatmap with Unequal Block Sizes' + width: 750 + height: 750 + ) -plt.show()! + plt.show()! +} diff --git a/examples/plot_scatter/main.v b/examples/plot_scatter/main.v index 2027eb36f..95ed1423c 100644 --- a/examples/plot_scatter/main.v +++ b/examples/plot_scatter/main.v @@ -18,7 +18,7 @@ y := [ 1, 0, ] -x := util.arange(y.len).map(f64(it)) +x := util.arange(y.len) mut plt := plot.Plot.new() plt.scatter( diff --git a/examples/plot_scatter3d_1/main.v b/examples/plot_scatter3d_1/main.v index 1173d3771..21be058d4 100644 --- a/examples/plot_scatter3d_1/main.v +++ b/examples/plot_scatter3d_1/main.v @@ -19,7 +19,7 @@ fn main() { 1, 0, ] - x := util.arange(y.len).map(f64(it)) + x := util.arange(y.len) z := util.arange(y.len).map(util.arange(y.len).map(f64(it * it))) mut plt := plot.Plot.new() diff --git a/examples/plot_scatter_colorscale/main.v b/examples/plot_scatter_colorscale/main.v index 0cecfbc35..a975d3cd2 100644 --- a/examples/plot_scatter_colorscale/main.v +++ b/examples/plot_scatter_colorscale/main.v @@ -18,7 +18,7 @@ y := [ 1, 0, ] -x := util.arange(y.len).map(f64(it)) +x := util.arange(y.len) mut plt := plot.Plot.new() plt.scatter( diff --git a/examples/plot_scatter_with_annotations/main.v b/examples/plot_scatter_with_annotations/main.v new file mode 100644 index 000000000..37eca61ae --- /dev/null +++ b/examples/plot_scatter_with_annotations/main.v @@ -0,0 +1,43 @@ +module main + +import vsl.plot +import vsl.util + +fn main() { + y := [ + 0.0, + 1, + 3, + 1, + 0, + -1, + -3, + -1, + 0, + 1, + 3, + 1, + 0, + ] + x := util.arange(y.len) + mut plt := plot.Plot.new() + plt.scatter( + x: x + y: y + mode: 'lines+markers' + marker: plot.Marker{ + size: []f64{len: x.len, init: 10.0} + color: []string{len: x.len, init: '#FF0000'} + } + line: plot.Line{ + color: '#FF0000' + } + ) + plt.layout( + title: 'Scatter plot example' + annotations: [plot.Annotation{ + text: 'test annotation' + }] + ) + plt.show()! +} diff --git a/plot/annotation.v b/plot/annotation.v index 9b44f6044..42a9ecf70 100644 --- a/plot/annotation.v +++ b/plot/annotation.v @@ -3,10 +3,10 @@ module plot // Annotation handles all the information needed to annotate plots pub struct Annotation { pub mut: - x f64 - y f64 - text string [omitempty] - showarrow bool [omitempty] + x f64 [omitempty] + y f64 [omitempty] + text string [required] + showarrow bool arrowhead int [omitempty] arrowcolor string [omitempty] align string [omitempty] diff --git a/plot/layout.v b/plot/layout.v index f1bc46271..2d4ab5d44 100644 --- a/plot/layout.v +++ b/plot/layout.v @@ -4,10 +4,10 @@ module plot pub struct Layout { pub mut: title string - title_x f64 = 0.5 - autosize bool = true - width int = 550 - height int = 550 + title_x f64 + autosize bool + width int = 550 + height int = 550 xaxis Axis yaxis Axis annotations []Annotation diff --git a/plot/show.v b/plot/show.v index c6a3d51cb..831424830 100644 --- a/plot/show.v +++ b/plot/show.v @@ -2,118 +2,149 @@ module plot import json import net +import net.html import net.http import os import time -type TracesWithTypeValue = Trace | string - -struct PlotlyHandler { - plot Plot -mut: - server &http.Server [str: skip] = unsafe { nil } -} - -fn (mut handler PlotlyHandler) handle(req http.Request) http.Response { - mut r := http.Response{ - body: handler.plot.plotly() - header: req.header - } - r.set_status(.ok) - r.set_version(req.version) - go fn [mut handler] () { - time.sleep(300 * time.millisecond) - handler.server.close() - }() - return r +// PlotConfig is a configuration for the Plotly plot. +[params] +pub struct PlotConfig { + use_cdn bool } // show starts a web server and opens a browser window to display the plot. -pub fn (plot Plot) show() ! { +pub fn (p Plot) show(config PlotConfig) ! { $if test ? { println('Ignoring plot.show() because we are running in test mode') } $else { mut handler := PlotlyHandler{ - plot: plot + use_cdn: true + plot: p } listener := net.listen_tcp(net.AddrFamily.ip, ':0')! mut server := &http.Server{ accept_timeout: 1 * time.second listener: listener - port: 0 handler: handler } handler.server = server t := spawn server.listen_and_serve() - for server.status() != .running { - time.sleep(10 * time.millisecond) - } + server.wait_till_running()! os.open_uri('http://${server.addr}')! t.wait() } } -// TODO: This is a hack to allow the json encoder to work with sum types -fn encode[T](obj T) string { - strings_to_replace := [ - ',"[]f64"', - '"[]f64"', - ',"[]string"', - '"[]string"', - ] - mut obj_json := json.encode(obj) - for string_to_replace in strings_to_replace { - obj_json = obj_json.replace(string_to_replace, '') - } - return obj_json +// Plot is a plotly plot. +type TracesWithTypeValue = Trace | string + +// PlotlyScriptConfig is a configuration for the Plotly plot script. +[params] +pub struct PlotlyScriptConfig { + PlotConfig } -fn (plot Plot) plotly() string { - traces_with_type := plot.traces.map({ +// get_plotly_script returns the plot script as an html tag. +pub fn (p Plot) get_plotly_script(element_id string, config PlotlyScriptConfig) &html.Tag { + traces_with_type := p.traces.map({ 'type': TracesWithTypeValue(it.trace_type()) 'trace': TracesWithTypeValue(it) }) traces_with_type_json := encode(traces_with_type) - layout_json := encode(plot.layout) + layout_json := encode(p.layout) + + plot_script := &html.Tag{ + name: 'script' + attributes: { + 'type': 'module' + } + content: 'import "https://cdn.plot.ly/plotly-2.26.2.min.js"; + +function removeEmptyFieldsDeeply(obj) { + if (Array.isArray(obj)) { + return obj.map(removeEmptyFieldsDeeply); + } + if (typeof obj === "object") { + const newObj = Object.fromEntries( + Object.entries(obj) + .map(([key, value]) => [key, removeEmptyFieldsDeeply(value)]) + .filter(([_, value]) => !!value) + ); + return Object.keys(newObj).length > 0 ? newObj : undefined; + } + return obj; +} + +const layout = ${layout_json}; +const traces_with_type_json = ${traces_with_type_json}; +const data = [...traces_with_type_json] + .map(({ type, trace: { CommonTrace, _type, ...trace } }) => ({ type, ...CommonTrace, ...trace })); + +const payload = { + data: removeEmptyFieldsDeeply(data), + layout: removeEmptyFieldsDeeply(layout), +}; + +Plotly.newPlot("${element_id}", payload);' + } + + return plot_script +} + +fn (p Plot) get_html(element_id string, config PlotConfig) string { + title := if p.layout.title == '' { 'VSL Plot' } else { p.layout.title } + plot_script := p.get_plotly_script(element_id, use_cdn: config.use_cdn) return '
-0?[0]:[]);if(o.enter().append("g").classed(f.containerClassName,!0).style("cursor","pointer"),o.exit().each((function(){n.select(this).selectAll("g."+f.headerGroupClassName).each(a)})).remove(),0!==r.length){var l=o.selectAll("g."+f.headerGroupClassName).data(r,p);l.enter().append("g").classed(f.headerGroupClassName,!0);for(var u=s.ensureSingle(o,"g",f.dropdownButtonGroupClassName,(function(t){t.style("pointer-events","all")})),c=0;c 90&&i.log("Long binary search..."),h-1},e.sorterAsc=function(t,e){return t-e},e.sorterDes=function(t,e){return e-t},e.distinctVals=function(t){var r,n=t.slice();for(n.sort(e.sorterAsc),r=n.length-1;r>-1&&n[r]===o;r--);for(var i,a=n[r]-n[0]||1,s=a/(r||1)/1e4,l=[],u=0;u<=r;u++){var c=n[u],f=c-i;void 0===i?(l.push(c),i=c):f>s&&(a=Math.min(a,f),l.push(c),i=c)}return{vals:l,minDiff:a}},e.roundUp=function(t,e,r){for(var n,i=0,a=e.length-1,o=0,s=r?0:1,l=r?1:0,u=r?Math.ceil:Math.floor;i0&&(n=1),r&&n)return t.sort(e)}return n?t:t.reverse()},e.findIndexOfMin=function(t,e){e=e||a;for(var r,n=1/0,i=0;il?r.y-l:0;return Math.sqrt(u*u+f*f)}for(var p=h(u);p;){if((u+=p+r)>f)return;p=h(u)}for(p=h(f);p;){if(u>(f-=p+r))return;p=h(f)}return{min:u,max:f,len:f-u,total:c,isClosed:0===u&&f===c&&Math.abs(n.x-i.x)<.1&&Math.abs(n.y-i.y)<.1}},e.findPointOnPath=function(t,e,r,n){for(var i,a,o,s=(n=n||{}).pathLength||t.getTotalLength(),l=n.tolerance||.001,u=n.iterationLimit||30,c=t.getPointAtLength(0)[r]>t.getPointAtLength(s)[r]?-1:1,f=0,h=0,p=s;f0?p=i:h=i,f++}return a}},81697:function(t,e,r){"use strict";var n=r(92770),i=r(84267),a=r(25075),o=r(21081),s=r(22399).defaultLine,l=r(73627).isArrayOrTypedArray,u=a(s);function c(t,e){var r=t;return r[3]*=e,r}function f(t){if(n(t))return u;var e=a(t);return e.length?e:u}function h(t){return n(t)?t:1}t.exports={formatColor:function(t,e,r){var n,i,s,p,d,v=t.color,g=l(v),y=l(e),m=o.extractOpts(t),x=[];if(n=void 0!==m.colorscale?o.makeColorScaleFuncFromTrace(t):f,i=g?function(t,e){return void 0===t[e]?u:a(n(t[e]))}:f,s=y?function(t,e){return void 0===t[e]?1:h(t[e])}:h,g||y)for(var b=0;b
/i;e.BR_TAG_ALL=/
/gi;var _=/(^|[\s"'])style\s*=\s*("([^"]*);?"|'([^']*);?')/i,w=/(^|[\s"'])href\s*=\s*("([^"]*)"|'([^']*)')/i,T=/(^|[\s"'])target\s*=\s*("([^"\s]*)"|'([^'\s]*)')/i,k=/(^|[\s"'])popup\s*=\s*("([\w=,]*)"|'([\w=,]*)')/i;function A(t,e){if(!t)return null;var r=t.match(e),n=r&&(r[3]||r[4]);return n&&L(n)}var M=/(^|;)\s*color:/;e.plainText=function(t,e){for(var r=void 0!==(e=e||{}).len&&-1!==e.len?e.len:1/0,n=void 0!==e.allowedTags?e.allowedTags:["br"],i=t.split(m),a=[],o="",s=0,l=0;l
"+l;e.text=u}(t,o,r,u):"log"===c?function(t,e,r,n,a){var o=t.dtick,l=e.x,u=t.tickformat,c="string"==typeof o&&o.charAt(0);if("never"===a&&(a=""),n&&"L"!==c&&(o="L3",c="L"),u||"L"===c)e.text=bt(Math.pow(10,l),t,a,n);else if(i(o)||"D"===c&&s.mod(l+.01,1)<.1){var f=Math.round(l),h=Math.abs(f),p=t.exponentformat;"power"===p||mt(p)&&xt(f)?(e.text=0===f?1:1===f?"10":"10"+(f>1?"":P)+h+"",e.fontSize*=1.25):("e"===p||"E"===p)&&h>2?e.text="1"+p+(f>0?"+":P)+h:(e.text=bt(Math.pow(10,l),t,"","fakehover"),"D1"===o&&"y"===t._id.charAt(0)&&(e.dy-=e.fontSize/6))}else{if("D"!==c)throw"unrecognized dtick "+String(o);e.text=String(Math.round(Math.pow(10,s.mod(l,1)))),e.fontSize*=.75}if("D1"===t.dtick){var d=String(e.text).charAt(0);"0"!==d&&"1"!==d||("y"===t._id.charAt(0)?e.dx-=e.fontSize/4:(e.dy+=e.fontSize/2,e.dx+=(t.range[1]>t.range[0]?1:-1)*e.fontSize*(l<0?.5:.25)))}}(t,o,0,u,v):"category"===c?function(t,e){var r=t._categories[Math.round(e.x)];void 0===r&&(r=""),e.text=String(r)}(t,o):"multicategory"===c?function(t,e,r){var n=Math.round(e.x),i=t._categories[n]||[],a=void 0===i[1]?"":String(i[1]),o=void 0===i[0]?"":String(i[0]);r?e.text=o+" - "+a:(e.text=a,e.text2=o)}(t,o,r):Dt(t)?function(t,e,r,n,i){if("radians"!==t.thetaunit||r)e.text=bt(e.x,t,i,n);else{var a=e.x/180;if(0===a)e.text="0";else{var o=function(t){function e(t,e){return Math.abs(t-e)<=1e-6}var r=function(t){for(var r=1;!e(Math.round(t*r)/r,t);)r*=10;return r}(t),n=t*r,i=Math.abs(function t(r,n){return e(n,0)?r:t(n,r%n)}(n,r));return[Math.round(n/i),Math.round(r/i)]}(a);if(o[1]>=100)e.text=bt(s.deg2rad(e.x),t,i,n);else{var l=e.x<0;1===o[1]?1===o[0]?e.text="Ï€":e.text=o[0]+"Ï€":e.text=["",o[0],"","â„","",o[1],"","Ï€"].join(""),l&&(e.text=P+e.text)}}}}(t,o,r,u,v):function(t,e,r,n,i){"never"===i?i="":"all"===t.showexponent&&Math.abs(e.x/t.dtick)<1e-6&&(i="hide"),e.text=bt(e.x,t,i,n)}(t,o,0,u,v),n||(t.tickprefix&&!d(t.showtickprefix)&&(o.text=t.tickprefix+o.text),t.ticksuffix&&!d(t.showticksuffix)&&(o.text+=t.ticksuffix)),t.labelalias&&t.labelalias.hasOwnProperty(o.text)){var g=t.labelalias[o.text];"string"==typeof g&&(o.text=g)}if("boundaries"===t.tickson||t.showdividers){var y=function(e){var r=t.l2p(e);return r>=0&&r<=t._length?e:null};o.xbnd=[y(o.x-.5),y(o.x+t.dtick-.5)]}return o},q.hoverLabelText=function(t,e,r){r&&(t=s.extendFlat({},t,{hoverformat:r}));var n=Array.isArray(e)?e[0]:e,i=Array.isArray(e)?e[1]:void 0;if(void 0!==i&&i!==n)return q.hoverLabelText(t,n,r)+" - "+q.hoverLabelText(t,i,r);var a="log"===t.type&&n<=0,o=q.tickText(t,t.c2l(a?-n:n),"hover").text;return a?0===n?"0":P+o:o};var yt=["f","p","n","μ","m","","k","M","G","T"];function mt(t){return"SI"===t||"B"===t}function xt(t){return t>14||t<-15}function bt(t,e,r,n){var a=t<0,o=e._tickround,l=r||e.exponentformat||"B",u=e._tickexponent,c=q.getTickFormat(e),f=e.separatethousands;if(n){var h={exponentformat:l,minexponent:e.minexponent,dtick:"none"===e.showexponent?e.dtick:i(t)&&Math.abs(t)||1,range:"none"===e.showexponent?e.range.map(e.r2d):[0,t||1]};vt(h),o=(Number(h._tickround)||0)+4,u=h._tickexponent,e.hoverformat&&(c=e.hoverformat)}if(c)return e._numFormat(c)(t).replace(/-/g,P);var p,d=Math.pow(10,-o)/2;if("none"===l&&(u=0),(t=Math.abs(t))
")):x=h.textLabel;var L={x:h.traceCoordinate[0],y:h.traceCoordinate[1],z:h.traceCoordinate[2],data:_._input,fullData:_,curveNumber:_.index,pointNumber:T};d.appendArrayPointValue(L,_,T),t._module.eventData&&(L=_._module.eventData(L,h,_,{},T));var C={points:[L]};if(e.fullSceneLayout.hovermode){var P=[];d.loneHover({trace:_,x:(.5+.5*m[0]/m[3])*s,y:(.5-.5*m[1]/m[3])*l,xLabel:k.xLabel,yLabel:k.yLabel,zLabel:k.zLabel,text:x,name:c.name,color:d.castHoverOption(_,T,"bgcolor")||c.color,borderColor:d.castHoverOption(_,T,"bordercolor"),fontFamily:d.castHoverOption(_,T,"font.family"),fontSize:d.castHoverOption(_,T,"font.size"),fontColor:d.castHoverOption(_,T,"font.color"),nameLength:d.castHoverOption(_,T,"namelength"),textAlign:d.castHoverOption(_,T,"align"),hovertemplate:f.castOption(_,T,"hovertemplate"),hovertemplateLabels:f.extendFlat({},L,k),eventData:[L]},{container:n,gd:r,inOut_bbox:P}),L.bbox=P[0]}h.distance<5&&(h.buttons||w)?r.emit("plotly_click",C):r.emit("plotly_hover",C),this.oldEventData=C}else d.loneUnhover(n),this.oldEventData&&r.emit("plotly_unhover",this.oldEventData),this.oldEventData=void 0;e.drawAnnotations(e)},k.recoverContext=function(){var t=this;t.glplot.dispose();var e=function(){t.glplot.gl.isContextLost()?requestAnimationFrame(e):t.initializeGLPlot()?t.plot.apply(t,t.plotArgs):f.error("Catastrophic and unrecoverable WebGL error. Context lost.")};requestAnimationFrame(e)};var M=["xaxis","yaxis","zaxis"];function S(t,e,r){for(var n=t.fullSceneLayout,i=0;i<3;i++){var a=M[i],o=a.charAt(0),s=n[a],l=e[o],u=e[o+"calendar"],c=e["_"+o+"length"];if(f.isArrayOrTypedArray(l))for(var h,p=0;p<(c||l.length);p++)if(f.isArrayOrTypedArray(l[p]))for(var d=0;d
");b.text(T).attr("data-unformatted",T).call(f.convertToTspans,t),_=c.bBox(b.node())}b.attr("transform",a(-3,8-_.height)),x.insert("rect",".static-attribution").attr({x:-_.width-6,y:-_.height-3,width:_.width+6,height:_.height+3,fill:"rgba(255, 255, 255, 0.75)"});var k=1;_.width+6>w&&(k=w/(_.width+6));var A=[n.l+n.w*h.x[1],n.t+n.h*(1-h.y[0])];x.attr("transform",a(A[0],A[1])+o(k))}},e.updateFx=function(t){for(var e=t._fullLayout,r=e._subplots[p],n=0;n
")}(e,r,n,i):v.getValue(s.text,r),v.coerceString(m,o)}(C,n,i,T,M);w=function(t,e){var r=v.getValue(t.textposition,e);return v.coerceEnumerated(x,r)}(O,i);var z="stack"===g.mode||"relative"===g.mode,R=n[i],F=!z||R._outmost;if(D&&"none"!==w&&(!R.isBlank&&s!==u&&f!==p||"auto"!==w&&"inside"!==w)){var B=C.font,N=d.getBarColor(n[i],O),j=d.getInsideTextFont(O,i,B,N),U=d.getOutsideTextFont(O,i,B),V=r.datum();I?"log"===T.type&&V.s0<=0&&(s=T.range[0]