Skip to content

Commit

Permalink
New version of vueplotly lib, able to handle data/layout/config gener…
Browse files Browse the repository at this point in the history
…ated by no-code editor (#68)
  • Loading branch information
protozoo authored Oct 23, 2023
1 parent 36aee5e commit 54e3879
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 151 deletions.
347 changes: 197 additions & 150 deletions assets/js/vueplotly.js
Original file line number Diff line number Diff line change
@@ -1,167 +1,214 @@
const eventsName = [
"AfterExport",
"AfterPlot",
"Animated",
"AnimatingFrame",
"AnimationInterrupted",
"AutoSize",
"BeforeExport",
"ButtonClicked",
"Click",
"ClickAnnotation",
"Deselect",
"DoubleClick",
"Framework",
"Hover",
"LegendClick",
"LegendDoubleClick",
"Relayout",
"Restyle",
"Redraw",
"Selected",
"Selecting",
"SliderChange",
"SliderEnd",
"SliderStart",
"Transitioning",
"TransitionInterrupted",
"Unhover"
];

const events = eventsName
.map(evt => evt.toLocaleLowerCase())
.map(eventName => ({
completeName: "plotly_" + eventName,
handler: context => (...args) => {
context.$emit.apply(context, [eventName, ...args]);
/*
* This function is used to replace the base64 encoded plotly data with a JS string
* that contains references to the model removing the $_{...} syntax
* It is expected to be loaded and run by the production app before the main Vue instance is created
*/
(function processPlotlyData() {
// Search the DOM for "plotly" elements
let plotlyInstances = document.querySelectorAll("plotly");
// For each plotly element...
plotlyInstances.forEach((plotlyInstance) => {
// List of attributes that could be base-64 encoded
let attributeNames = ['data', 'layout', 'config'];
// Iterate over all attributes
attributeNames.forEach((attributeName) => {
// Check if there's a bound attribute (i.e. :data, :layout, :config
// Only attempt to parse encoded one (data, layout, config) if bound not found
let literalAttribute = plotlyInstance.getAttribute(attributeName); // Expected to be a valid js object (binding)
let boundAttribute = plotlyInstance.getAttribute(":"+attributeName); // Expected to be a valid js object (binding)
// Both versions of attribute can't coexist
if( literalAttribute != null && boundAttribute != null ){
throw new Error("Both bound and literal attribute found for " + attributeName + ". Only one is allowed.");
}
// We don't need to do anything if there's a bound attribute (starting with ":", as it is assumed to be a valid JS object)
if( boundAttribute != null ){
return;
}
// Literal version is only expected to exist as base64-encoded json.
// (and just for testing purposes, it could be a non-encoded valid JS object)
if( literalAttribute != null ){
// Attempt to decode it and convert it to valid JS string from JSON
try{
let decodeString = atob(literalAttribute); // decode from base-64
let decodedJsString = jsonToJsString(decodeString); // convert to a JS string, suitable for vue
plotlyInstance.setAttribute(":"+attributeName, decodedJsString); // Replace the original base64 data with the JS string
plotlyInstance.removeAttribute(attributeName); // Remove the original base64 attribute
}catch(e){
// If there's an error, check if it starts with "{" and ends with "}", or "[" and "]"
// If so, it's a valid JS object as a string, so we can use it as is, but adding the ":" prefix so that it gets evaluated
if( (literalAttribute.startsWith("{") && literalAttribute.endsWith("}")) ||
(literalAttribute.startsWith("[") && literalAttribute.endsWith("]")) ){
plotlyInstance.setAttribute(":"+attributeName, literalAttribute); // Replace the original base64 data with the JS string
plotlyInstance.removeAttribute(attributeName); // Remove the original base64 attribute
}else{
throw new Error("Invalid literal attribute for " + attributeName + ". Expected a base64-encoded JSON string, or a valid JS object.");
}
}
}
});
});
function jsonToJsString(jsonString) {
// Parse the input string to a JSON object
const jsonObj = JSON.parse(jsonString);
// Helper function to recursively traverse and convert the object to a JS string
// When it finds a string that starts with $_{ and ends with }, it replaces it with a reference to the contained property
// i.e.: {a: '$_{b.c}'} => {a: b.c}
function traverse(obj) {
if (Array.isArray(obj)) {
return '[' + obj.map(item => traverse(item)).join(', ') + ']';
} else if (typeof obj === 'object') {
return '{' + Object.keys(obj).map(key => {
let value = obj[key];
if (typeof value === 'string' && value.startsWith('$_{') && value.endsWith('}')) {
value = value.slice(3, -1);
} else {
value = JSON.stringify(value);
}
return `${key}:${value}`;
}).join(', ') + '}';
} else {
return JSON.stringify(obj);
}
}
}));

const plotlyFunctions = ["restyle", "relayout", "update", "addTraces",
"deleteTraces", "moveTraces", "extendTraces",
"prependTraces", "purge"];

function cached(fn) {
const cache = Object.create(null);
return function cachedFn(str) {
const hit = cache[str];
return hit || (cache[str] = fn(str));
};
// Convert the JSON object to a JS string
const jsString = traverse(jsonObj);
// Return the JS string
return jsString;
}
})();
const eventsName = ["AfterExport", "AfterPlot", "Animated", "AnimatingFrame", "AnimationInterrupted", "AutoSize", "BeforeExport", "ButtonClicked", "Click", "ClickAnnotation", "Deselect", "DoubleClick", "Framework", "Hover", "LegendClick", "LegendDoubleClick", "Relayout", "Restyle", "Redraw", "Selected", "Selecting", "SliderChange", "SliderEnd", "SliderStart", "Transitioning", "TransitionInterrupted", "Unhover"]
, events = eventsName.map((e=>e.toLocaleLowerCase())).map((e=>({
completeName: "plotly_" + e,
handler: t=>(...i)=>{
t.$emit.apply(t, [e, ...i])
}
})))
, plotlyFunctions = ["restyle", "relayout", "update", "addTraces", "deleteTraces", "moveTraces", "extendTraces", "prependTraces", "purge"];
function cached(e) {
const t = Object.create(null);
return function(i) {
return t[i] || (t[i] = e(i))
}
}

const regex = /-(\w)/g;

const methods = plotlyFunctions.reduce((all, functionName) => {
all[functionName] = function(...args) {
return Plotly[functionName].apply(Plotly, [this.$el, ...args]);
};
return all;
}, {});

const camelize = cached(str => str.replace(regex, (_, c) => (c ? c.toUpperCase() : "")));

const directives = {};
if (typeof window !== "undefined") {
directives.resize = Vueresize;
const regex = /-(\w)/g
, methods = plotlyFunctions.reduce(((e,t)=>(e[t] = function(...e) {
return Plotly[t].apply(Plotly, [this.$el, ...e])
}

Vue.component('plotly', {
template: `<div :id="id" v-resize:debounce.100="onResize" ></div>`,
inheritAttrs: false,
directives,
,
e)), {})
, camelize = cached((e=>e.replace(regex, ((e,t)=>t ? t.toUpperCase() : ""))))
, directives = {};
"undefined" != typeof window && (directives.resize = Vueresize),
Vue.component("plotly", {
template: '<div :id="id" v-resize:debounce.100="onResize" ></div>',
inheritAttrs: !1,
directives: directives,
props: {
data: {
type: Array
},
layout: {
type: Object
},
config: {
type: Object
},
id: {
type: String,
required: false,
default: null
}
data: {
type: Array
},
layout: {
type: Object
},
config: {
type: Object
},
id: {
type: String,
required: !1,
default: null
}
},
data() {
return {
scheduled: null,
innerLayout: { ...this.layout },
options: { ...this.config }
};
return {
scheduled: null,
innerLayout: {
...this.layout
}
}
},
mounted() {
Plotly.newPlot(this.$el, this.data, this.innerLayout, this.options);
events.forEach(evt => {
this.$el.on(evt.completeName, evt.handler(this));
});
Plotly.newPlot(this.$el, this.data, this.innerLayout, this.config),
events.forEach((e=>{
this.$el.on(e.completeName, e.handler(this))
}
))
},
watch: {
data: {
handler() {
console.log('watching');
this.schedule({ replot: true });
data: {
handler() {
this.schedule({
replot: !0
})
},
deep: !0
},
deep: true
},
layout(layout) {
this.innerLayout = { ...layout };
this.schedule({ replot: false });
},
config(config) {
this.options = { ...config };
this.schedule({ replot: false });
}
options: {
handler(e, t) {
JSON.stringify(e) !== JSON.stringify(t) && this.schedule({
replot: !0
})
},
deep: !0
},
layout(e) {
this.innerLayout = {
...e
},
this.schedule({
replot: !1
})
}
},
computed: {
options() {
return {
responsive: !1,
...Object.keys(this.$attrs).reduce(((e,t)=>(e[camelize(t)] = this.$attrs[t],
e)), {})
}
}
},
beforeDestroy() {
events.forEach(event => this.$el.removeAllListeners(event.completeName));
Plotly.purge(this.$el);
events.forEach((e=>this.$el.removeAllListeners(e.completeName))),
Plotly.purge(this.$el)
},
methods: {
...methods,
onResize() {
Plotly.Plots.resize(this.$el);
},
schedule(context) {
const { scheduled } = this;
if (scheduled) {
scheduled.replot = scheduled.replot || context.replot;
return;
...methods,
onResize() {
Plotly.Plots.resize(this.$el)
},
schedule(e) {
const {scheduled: t} = this;
t ? t.replot = t.replot || e.replot : (this.scheduled = e,
this.$nextTick((()=>{
const {scheduled: {replot: e}} = this;
this.scheduled = null,
e ? this.react() : this.relayout(this.innerLayout)
}
)))
},
toImage(e) {
const t = Object.assign(this.getPrintOptions(), e);
return Plotly.toImage(this.$el, t)
},
downloadImage(e) {
const t = `plot--${(new Date).toISOString()}`
, i = Object.assign(this.getPrintOptions(), {
filename: t
}, e);
return Plotly.downloadImage(this.$el, i)
},
getPrintOptions() {
const {$el: e} = this;
return {
format: "png",
width: e.clientWidth,
height: e.clientHeight
}
},
react() {
Plotly.react(this.$el, this.data, this.innerLayout, this.config)
}
this.scheduled = context;
this.$nextTick(() => {
const {
scheduled: { replot }
} = this;
this.scheduled = null;
if (replot) {
this.react();
return;
}
this.relayout(this.innerLayout);
});
},
toImage(options) {
const allOptions = Object.assign(this.getPrintOptions(), options);
return Plotly.toImage(this.$el, allOptions);
},
downloadImage(options) {
const filename = `plot--${new Date().toISOString()}`;
const allOptions = Object.assign(this.getPrintOptions(), { filename }, options);
return Plotly.downloadImage(this.$el, allOptions);
},
getPrintOptions() {
const { $el } = this;
return {
format: "png",
width: $el.clientWidth,
height: $el.clientHeight
};
},
react() {
Plotly.react(this.$el, this.data, this.innerLayout, this.options);
}
}
})
});
2 changes: 1 addition & 1 deletion assets/js/vueplotly.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 54e3879

Please sign in to comment.