directus-extension-chart-interface 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.js +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aymeric Schaeffer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Directus JSON Apex Chart Interface
|
|
2
|
+
|
|
3
|
+
Directus v12 interface extension for a `json` field. It renders JSON point data with ApexCharts and includes a manual JSON editor that emits Directus `input` events only when the text is valid JSON.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Copy or mount the built extension according to your Directus extension setup. The package declares one app extension:
|
|
13
|
+
|
|
14
|
+
- Type: `interface`
|
|
15
|
+
- Interface ID: `json-apex-chart`
|
|
16
|
+
- Supported field type: `json`
|
|
17
|
+
|
|
18
|
+
## Recommended Directus Field
|
|
19
|
+
|
|
20
|
+
- Database type: `json`
|
|
21
|
+
- Interface: `JSON Apex Chart`
|
|
22
|
+
- No relation
|
|
23
|
+
- Stored directly on the item
|
|
24
|
+
|
|
25
|
+
## JSON Format
|
|
26
|
+
|
|
27
|
+
Recommended format:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"version": 1,
|
|
32
|
+
"name": "Curve name",
|
|
33
|
+
"x_axis": {
|
|
34
|
+
"label": "X axis",
|
|
35
|
+
"unit": "s"
|
|
36
|
+
},
|
|
37
|
+
"y_axis": {
|
|
38
|
+
"label": "Y axis",
|
|
39
|
+
"unit": "V"
|
|
40
|
+
},
|
|
41
|
+
"points": [
|
|
42
|
+
[0, 0],
|
|
43
|
+
[1, 2.5]
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The interface also accepts point objects when reading:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"points": [
|
|
53
|
+
{ "x": 0, "y": 0 },
|
|
54
|
+
{ "x": 1, "y": 2.5 }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The chart view does not rewrite the stored JSON. The editor emits the parsed object only after valid JSON changes.
|
|
60
|
+
|
|
61
|
+
## Interface Options
|
|
62
|
+
|
|
63
|
+
- `chartType`: `line` or `scatter`, default `line`
|
|
64
|
+
- `showMarkers`: default `false`
|
|
65
|
+
- `height`: default `320`
|
|
66
|
+
- `smoothCurve`: default `true`
|
|
67
|
+
- `showTitle`: default `true`
|
|
68
|
+
- `showGrid`: default `true`
|
|
69
|
+
- `maxRenderedPoints`: default `5000`
|
|
70
|
+
|
|
71
|
+
If the point count exceeds `maxRenderedPoints`, the interface downsamples the rendered chart only. The stored JSON is not changed.
|
|
72
|
+
|
|
73
|
+
## Limitations
|
|
74
|
+
|
|
75
|
+
- Validation is intentionally minimal and client-side.
|
|
76
|
+
- Points must contain finite numeric `x` and `y` values.
|
|
77
|
+
- `name`, axis labels, and units are optional strings.
|
|
78
|
+
- No backend endpoint, module, layout, panel, flow, or external storage is included.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineInterface as e}from"@directus/extensions-sdk";import{defineComponent as t,ref as a,shallowRef as n,computed as r,watch as o,onMounted as i,onActivated as l,onBeforeUnmount as s,resolveComponent as u,createElementBlock as d,openBlock as c,normalizeClass as h,createElementVNode as v,createVNode as p,withCtx as f,createTextVNode as m,createBlock as b,createCommentVNode as y,toDisplayString as x,Fragment as g,normalizeStyle as w,nextTick as k}from"vue";function j(e,t){return e&&t?`${e} (${t})`:e??t}function S(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function _(e){return void 0===e||"string"==typeof e}function C(e,t,a){void 0!==e&&(S(e)?(_(e.label)||a.push(`${t}.label must be a string.`),_(e.unit)||a.push(`${t}.unit must be a string.`)):a.push(`${t} must be an object.`))}function A(e){return"number"==typeof e&&Number.isFinite(e)}function N(e){return{label:e?.label,unit:e?.unit}}function O(e){if(Array.isArray(e)){const[t,a]=e;return A(t)&&A(a)?{x:t,y:a}:null}return"object"==typeof e&&null!==e&&A(e.x)&&A(e.y)?{x:e.x,y:e.y}:null}function T(e){const t=function(e){const t=[];if(!S(e))return{valid:!1,errors:["Value must be a JSON object."]};const a=e;return Array.isArray(a.points)||t.push("points must be an array."),_(a.name)||t.push("name must be a string."),C(a.x_axis,"x_axis",t),C(a.y_axis,"y_axis",t),{valid:0===t.length,errors:t}}(e);if(!t.valid)return{...t,data:null};const a=e,n=a.points??[],r=[];let o=0;for(const e of n){const t=O(e);t?r.push(t):o+=1}const i=[...t.errors];return 0===r.length&&i.push("points must contain at least one valid [x, y] pair."),{valid:0===i.length,errors:i,data:0===i.length?{version:a.version,name:a.name,xAxis:N(a.x_axis),yAxis:N(a.y_axis),points:r,skippedPoints:o}:null}}const M={class:"json-chart-toolbar"},E={key:0,class:"json-chart-panel"},R={key:0,class:"json-chart-state"},J={key:1,class:"json-chart-state is-error"},B={key:0,class:"json-chart-note"},P={key:0},z={key:1},G={key:1,class:"json-chart-editor"},L=["disabled","value"],$={key:0,class:"json-chart-note is-error"};var F=t({__name:"interface",props:{value:{},disabled:{type:Boolean,default:!1},nonEditable:{type:Boolean,default:!1},chartType:{default:"line"},showMarkers:{type:Boolean,default:!1},height:{default:320},smoothCurve:{type:Boolean,default:!0},showTitle:{type:Boolean,default:!0},showGrid:{type:Boolean,default:!0},maxRenderedPoints:{default:5e3}},emits:["input"],setup(e,{emit:t}){const S=e,_=t,C=a("chart"),A=a(null),N=n(null),O=a(""),F=a("");let I=null,V=null,q=null,H=null,W=0;const X=r(()=>S.disabled||S.nonEditable),Y=r(()=>null===S.value||void 0===S.value||""===S.value),D=r(()=>({chartType:S.chartType,showMarkers:S.showMarkers,height:Math.max(160,Number(S.height)||320),smoothCurve:S.smoothCurve,showTitle:S.showTitle,showGrid:S.showGrid,maxRenderedPoints:Math.max(2,Number(S.maxRenderedPoints)||5e3)})),U=r(()=>Y.value?null:T(S.value)),K=r(()=>U.value?.data?function(e,t){const a=Math.max(2,Math.floor(t));if(e.length<=a)return e;const n=(e.length-1)/(a-1),r=[];for(let t=0;t<a;t+=1)r.push(e[Math.round(t*n)]);return r}(U.value.data.points,D.value.maxRenderedPoints):[]),Q=r(()=>{const e=U.value?.data;return Boolean(e&&K.value.length<e.points.length)}),Z=r(()=>{const e=U.value?.data;if(!e)return"empty";const t=K.value.reduce((e,t,a)=>(e+(a+1)*(31*Math.round(1e6*t.x)+17*Math.round(1e6*t.y)))%1000000007,0);return JSON.stringify({name:e.name,total:e.points.length,rendered:K.value.length,pointHash:t,options:D.value})});function ee(e){return null==e||""===e?JSON.stringify({version:1,name:"Curve name",x_axis:{label:"X axis",unit:"s"},y_axis:{label:"Y axis",unit:"V"},points:[[0,0],[1,2.5]]},null,2):JSON.stringify(e,null,2)}async function te(){N.value&&(N.value.destroy(),N.value=null)}function ae(){if(!A.value||!A.value.isConnected)return!1;const e=A.value.getBoundingClientRect();return e.width>0&&e.height>0&&A.value.getClientRects().length>0}function ne(){null!==q&&window.cancelAnimationFrame(q),q=window.requestAnimationFrame(()=>{q=null,ie()})}function re(){null!==H&&(window.clearInterval(H),H=null)}function oe(){re();let e=0;H=window.setInterval(()=>{e+=1,"chart"===C.value&&U.value?.valid&&U.value.data&&!function(){const e=A.value?.querySelector(".apexcharts-svg");if(!(e instanceof SVGElement))return!1;const t=e.getBoundingClientRect();return t.width>0&&t.height>0}()?(ae()&&(N.value?te().then(()=>ie()):ne()),e>=80&&re()):re()},150)}async function ie(e=0){const t=++W;if("chart"!==C.value||!A.value||!U.value?.valid||!U.value.data)return re(),void await te();if(await k(),!ae())return oe(),void(e<80&&window.setTimeout(()=>{ie(e+1)},150));const a=function(e,t,a){const n=e.points.length;return{chart:{type:a.chartType,height:a.height,redrawOnParentResize:!0,redrawOnWindowResize:!0,animations:{enabled:n<=1e3},toolbar:{show:!0},zoom:{enabled:!0}},series:[{name:e.name||"Series",data:t.map(e=>({x:e.x,y:e.y}))}],title:{text:a.showTitle?e.name:void 0,align:"left",style:{fontSize:"14px",fontWeight:600}},stroke:{curve:a.smoothCurve?"smooth":"straight",width:"scatter"===a.chartType?0:2},markers:{size:a.showMarkers||"scatter"===a.chartType?4:0},grid:{show:a.showGrid},xaxis:{type:"numeric",title:{text:j(e.xAxis.label,e.xAxis.unit)}},yaxis:{title:{text:j(e.yAxis.label,e.yAxis.unit)}},noData:{text:"No chart data"},tooltip:{shared:!1,x:{formatter:e=>String(e)}}}}(U.value.data,K.value,D.value);if(I??(I=(await import("apexcharts")).default),t===W){if(!N.value)return N.value=new I(A.value,a),await N.value.render(),void oe();await N.value.updateOptions(a,!1,!0,!1),await N.value.updateSeries(a.series??[],!1),oe()}}function le(e){"chart"===e&&F.value||(C.value=e,"json"===e&&(O.value=ee(S.value),F.value=""))}function se(e){const t=e.target;O.value=t.value;try{const e=JSON.parse(t.value);F.value="",_("input",e)}catch(e){F.value=e instanceof Error?e.message:"Invalid JSON."}}function ue(){const e={version:1,name:"Curve name",x_axis:{label:"X axis",unit:"s"},y_axis:{label:"Y axis",unit:"V"},points:[[0,0],[1,2.5]]};O.value=JSON.stringify(e,null,2),F.value="",_("input",e),C.value="chart"}function de(){V?.disconnect(),V=null,A.value&&(V=new ResizeObserver(()=>{"chart"===C.value&&ne()}),V.observe(A.value))}function ce(){document.hidden||(ne(),oe())}return o(()=>S.value,e=>{"json"!==C.value||F.value||(O.value=ee(e))},{immediate:!0}),o([C,Z],()=>{ne(),oe()},{immediate:!0,flush:"post"}),o(A,()=>{de(),ne()},{flush:"post"}),i(()=>{de(),document.addEventListener("visibilitychange",ce),window.addEventListener("focus",ne),ne(),oe()}),l(()=>{ne(),oe()}),s(()=>{document.removeEventListener("visibilitychange",ce),window.removeEventListener("focus",ne),V?.disconnect(),V=null,null!==q&&(window.cancelAnimationFrame(q),q=null),re(),te()}),(e,t)=>{const a=u("v-button");return c(),d("div",{class:h(["json-chart-interface",{"is-readonly":X.value}])},[v("div",M,[p(a,{small:"",secondary:"",disabled:"chart"===C.value||Boolean(F.value),onClick:t[0]||(t[0]=e=>le("chart"))},{default:f(()=>[...t[3]||(t[3]=[m(" Chart ",-1)])]),_:1},8,["disabled"]),p(a,{small:"",secondary:"",disabled:"json"===C.value,onClick:t[1]||(t[1]=e=>le("json"))},{default:f(()=>[...t[4]||(t[4]=[m(" JSON ",-1)])]),_:1},8,["disabled"])]),"chart"===C.value?(c(),d("div",E,[Y.value?(c(),d("div",R,[t[6]||(t[6]=v("p",null,"No chart JSON yet.",-1)),X.value?y("v-if",!0):(c(),b(a,{key:0,small:"",onClick:ue},{default:f(()=>[...t[5]||(t[5]=[m("Use Example JSON",-1)])]),_:1}))])):U.value&&!U.value.valid?(c(),d("div",J,[v("p",null,x(U.value.errors[0]),1),p(a,{small:"",secondary:"",onClick:t[2]||(t[2]=e=>le("json"))},{default:f(()=>[...t[7]||(t[7]=[m("Edit JSON",-1)])]),_:1})])):(c(),d(g,{key:2},[v("div",{ref_key:"chartElement",ref:A,class:"json-chart-canvas",style:w({minHeight:`${D.value.height}px`})},null,4),Q.value||U.value?.data?.skippedPoints?(c(),d("div",B,[Q.value?(c(),d("span",P," Showing "+x(K.value.length)+" of "+x(U.value?.data?.points.length)+" points. ",1)):y("v-if",!0),U.value?.data?.skippedPoints?(c(),d("span",z," Skipped "+x(U.value.data.skippedPoints)+" invalid points. ",1)):y("v-if",!0)])):y("v-if",!0)],64))])):(c(),d("div",G,[v("textarea",{class:"json-chart-textarea",spellcheck:"false",disabled:X.value,value:O.value,onInput:se},null,40,L),F.value?(c(),d("div",$,x(F.value),1)):y("v-if",!0)]))],2)}}}),I=[],V=[];!function(e,t){if(e&&"undefined"!=typeof document){var a,n=!0===t.prepend?"prepend":"append",r=!0===t.singleTag,o="string"==typeof t.container?document.querySelector(t.container):document.getElementsByTagName("head")[0];if(r){var i=I.indexOf(o);-1===i&&(i=I.push(o)-1,V[i]={}),a=V[i]&&V[i][n]?V[i][n]:V[i][n]=l()}else a=l();65279===e.charCodeAt(0)&&(e=e.substring(1)),a.styleSheet?a.styleSheet.cssText+=e:a.appendChild(document.createTextNode(e))}function l(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),t.attributes)for(var a=Object.keys(t.attributes),r=0;r<a.length;r++)e.setAttribute(a[r],t.attributes[a[r]]);var i="prepend"===n?"afterbegin":"beforeend";return o.insertAdjacentElement(i,e),e}}('\n.json-chart-interface[data-v-ad6be0ec] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n width: 100%;\n}\n.json-chart-toolbar[data-v-ad6be0ec] {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.json-chart-panel[data-v-ad6be0ec],\n.json-chart-editor[data-v-ad6be0ec] {\n width: 100%;\n}\n.json-chart-canvas[data-v-ad6be0ec] {\n width: 100%;\n}\n.json-chart-state[data-v-ad6be0ec] {\n display: flex;\n min-height: 160px;\n flex-direction: column;\n gap: 12px;\n align-items: flex-start;\n justify-content: center;\n padding: 20px;\n color: var(--theme--foreground-subdued);\n border: var(--theme--border-width) solid var(--theme--border-color);\n border-radius: var(--theme--border-radius);\n background: var(--theme--background-subdued);\n}\n.json-chart-state p[data-v-ad6be0ec] {\n margin: 0;\n}\n.json-chart-textarea[data-v-ad6be0ec] {\n width: 100%;\n min-height: 320px;\n padding: 12px;\n color: var(--theme--foreground);\n font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;\n resize: vertical;\n border: var(--theme--border-width) solid var(--theme--border-color);\n border-radius: var(--theme--border-radius);\n background: var(--theme--background);\n}\n.json-chart-textarea[data-v-ad6be0ec]:disabled {\n color: var(--theme--foreground-subdued);\n background: var(--theme--background-subdued);\n cursor: not-allowed;\n}\n.json-chart-note[data-v-ad6be0ec] {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 8px;\n color: var(--theme--foreground-subdued);\n font-size: 12px;\n}\n.is-error[data-v-ad6be0ec] {\n color: var(--theme--danger);\n}\n.is-readonly .json-chart-toolbar[data-v-ad6be0ec] {\n opacity: 0.85;\n}\n',{});var q=e({id:"json-apex-chart",name:"JSON Apex Chart",icon:"show_chart",description:"Render JSON points as an ApexCharts chart.",component:((e,t)=>{const a=e.__vccOpts||e;for(const[e,n]of t)a[e]=n;return a})(F,[["__scopeId","data-v-ad6be0ec"]]),types:["json"],group:"standard",options:[{field:"chartType",name:"Chart Type",type:"string",meta:{width:"half",interface:"select-dropdown",options:{choices:[{text:"Line",value:"line"},{text:"Scatter",value:"scatter"}]}},schema:{default_value:"line"}},{field:"showMarkers",name:"Show Markers",type:"boolean",meta:{width:"half",interface:"boolean"},schema:{default_value:!1}},{field:"height",name:"Height",type:"integer",meta:{width:"half",interface:"input",options:{min:160,step:20}},schema:{default_value:320}},{field:"smoothCurve",name:"Smooth Curve",type:"boolean",meta:{width:"half",interface:"boolean"},schema:{default_value:!0}},{field:"showTitle",name:"Show Title",type:"boolean",meta:{width:"half",interface:"boolean"},schema:{default_value:!0}},{field:"showGrid",name:"Show Grid",type:"boolean",meta:{width:"half",interface:"boolean"},schema:{default_value:!0}},{field:"maxRenderedPoints",name:"Max Rendered Points",type:"integer",meta:{width:"half",interface:"input",options:{min:2,step:100}},schema:{default_value:5e3}}]});export{q as default};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "directus-extension-chart-interface",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Directus interface for rendering editable JSON data as ApexCharts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "directus-extension build -w",
|
|
11
|
+
"build": "directus-extension build",
|
|
12
|
+
"typecheck": "vue-tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"apexcharts": "^4.7.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@directus/extensions-sdk": "^15.0.0",
|
|
19
|
+
"@vitejs/plugin-vue": "^5.2.4",
|
|
20
|
+
"typescript": "^5.8.3",
|
|
21
|
+
"vue": "^3.5.17",
|
|
22
|
+
"vue-tsc": "^2.2.12"
|
|
23
|
+
},
|
|
24
|
+
"directus:extension": {
|
|
25
|
+
"type": "interface",
|
|
26
|
+
"path": "dist/index.js",
|
|
27
|
+
"source": "src/index.ts",
|
|
28
|
+
"host": "^12.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"directus",
|
|
32
|
+
"directus-extension",
|
|
33
|
+
"interface",
|
|
34
|
+
"chart",
|
|
35
|
+
"apexcharts"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|