x4js 2.3.10 → 2.3.12
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/README.md +13 -1
- package/aicli.md +194 -0
- package/aicontext.md +75 -3
- package/aiguide.md +67 -0
- package/cli/main.mjs +3 -1
- package/package.json +2 -1
- package/src/components/autocomplete/autocomplete.module.scss +28 -0
- package/src/components/autocomplete/autocomplete.ts +361 -0
- package/src/components/boxes/boxes.ts +66 -0
- package/src/components/button/button.ts +1 -1
- package/src/components/chart/chart.module.scss +126 -0
- package/src/components/chart/chart.ts +758 -0
- package/src/components/combobox/combobox.ts +5 -5
- package/src/components/components.ts +2 -0
- package/src/components/header/header.ts +38 -3
- package/src/components/listbox/listbox.module.scss +4 -5
- package/src/components/listbox/listbox.ts +1 -1
- package/src/components/select/select.module.scss +26 -17
- package/src/components/select/select.ts +60 -10
- package/src/components/spreadsheet/spreadsheet.module.scss +31 -0
- package/src/components/spreadsheet/spreadsheet.ts +546 -11
- package/src/components/textedit/textedit.ts +4 -4
- package/src/core/core_router.ts +15 -3
- package/src/core/core_svg.ts +2 -2
- package/readme.npm.md +0 -248
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file autocomplete.ts
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
import { Component, ComponentEvent, ComponentEvents, EvChange } from '../../core/component';
|
|
18
|
+
import { EventCallback } from '../../core/core_events';
|
|
19
|
+
import { class_ns, kbNav } from '../../core/core_tools';
|
|
20
|
+
|
|
21
|
+
import { HBox } from '../boxes/boxes';
|
|
22
|
+
import { DropdownList } from '../combobox/combobox';
|
|
23
|
+
import { TextInputProps } from '../input/input';
|
|
24
|
+
import { SimpleText } from '../label/label';
|
|
25
|
+
import { ListboxID, ListItem } from '../listbox/listbox';
|
|
26
|
+
import { TextEdit, TextEditBase } from '../textedit/textedit';
|
|
27
|
+
|
|
28
|
+
import "./autocomplete.module.scss"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* fired when suggestions are needed for a text
|
|
32
|
+
* setItems can be called later (async), late answers are ignored
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export interface EvSearch extends ComponentEvent {
|
|
36
|
+
text: string;
|
|
37
|
+
setItems: ( items: ListItem[] ) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* fired when the value is validated
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export interface EvAutoCompleteChange extends EvChange {
|
|
45
|
+
item: ListItem; // picked item, null for a free text
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AutoCompleteEvents extends ComponentEvents {
|
|
49
|
+
search: EvSearch;
|
|
50
|
+
change: EvAutoCompleteChange;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AutoCompleteProps extends Omit<TextInputProps,"type"|"change">, TextEditBase {
|
|
54
|
+
search: EventCallback<EvSearch>;
|
|
55
|
+
change?: EventCallback<EvAutoCompleteChange>;
|
|
56
|
+
|
|
57
|
+
/** only a picked item (or nothing) is accepted, default false: free text is kept */
|
|
58
|
+
strict?: boolean;
|
|
59
|
+
|
|
60
|
+
/** what is written in the field when an item is picked (default "text") */
|
|
61
|
+
itemValue?: "text" | "id";
|
|
62
|
+
|
|
63
|
+
/** delay between the last key and the search, in ms (default 250) */
|
|
64
|
+
delay?: number;
|
|
65
|
+
|
|
66
|
+
/** number of chars needed to start a search (default 1) */
|
|
67
|
+
minChars?: number;
|
|
68
|
+
|
|
69
|
+
/** custom renderer for the suggestions */
|
|
70
|
+
renderer?: ( item: ListItem ) => Component;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// suggestion as: id + text
|
|
74
|
+
const render_id_item = ( item: ListItem ) => {
|
|
75
|
+
return new HBox( {
|
|
76
|
+
cls: item.cls,
|
|
77
|
+
content: [
|
|
78
|
+
new SimpleText( { cls: "id", text: item.id+"" } ),
|
|
79
|
+
new SimpleText( { cls: "text", text: item.text } ),
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Text field with suggestions asked on demand (search event).
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* new AutoComplete( {
|
|
89
|
+
* label: "Customer",
|
|
90
|
+
* strict: true,
|
|
91
|
+
* search: async ( ev ) => {
|
|
92
|
+
* const rows = await api.searchCustomers( ev.text );
|
|
93
|
+
* ev.setItems( rows.map( r => ( { id: r.id, text: r.name } ) ) );
|
|
94
|
+
* },
|
|
95
|
+
* change: ( ev ) => console.log( ev.value, ev.item ),
|
|
96
|
+
* })
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
@class_ns( "x4" )
|
|
100
|
+
export class AutoComplete extends TextEdit<AutoCompleteEvents> {
|
|
101
|
+
|
|
102
|
+
private _popup: DropdownList;
|
|
103
|
+
|
|
104
|
+
private _strict: boolean;
|
|
105
|
+
private _by_id: boolean;
|
|
106
|
+
private _delay: number;
|
|
107
|
+
private _min_chars: number;
|
|
108
|
+
|
|
109
|
+
private _text: string; // last validated text
|
|
110
|
+
private _item: ListItem; // last picked item (null: free text)
|
|
111
|
+
private _seq: number; // search sequence, to ignore late answers
|
|
112
|
+
private _count: number; // number of suggestions
|
|
113
|
+
|
|
114
|
+
constructor( props: AutoCompleteProps ) {
|
|
115
|
+
// the change of the input (each key) starts the search, our change event is fired on validation
|
|
116
|
+
super( { ...props, type: "text", change: ( ) => this._on_input( ) } );
|
|
117
|
+
|
|
118
|
+
this._strict = !!props.strict;
|
|
119
|
+
this._by_id = props.itemValue=="id";
|
|
120
|
+
this._delay = props.delay ?? 250;
|
|
121
|
+
this._min_chars = props.minChars ?? 1;
|
|
122
|
+
|
|
123
|
+
this._text = this.getInput( ).getValue( );
|
|
124
|
+
this._item = null;
|
|
125
|
+
this._seq = 0;
|
|
126
|
+
this._count = 0;
|
|
127
|
+
|
|
128
|
+
this.mapPropEvents( props, 'change', 'search' );
|
|
129
|
+
|
|
130
|
+
this._popup = new DropdownList( {
|
|
131
|
+
cls: "autocomplete",
|
|
132
|
+
items: [],
|
|
133
|
+
renderer: props.renderer ?? ( this._by_id ? render_id_item : undefined )
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
this._popup.on( "click", ( ev ) => {
|
|
137
|
+
const item = this._popup.getList( ).getItem( ev.context as ListboxID );
|
|
138
|
+
if( item ) {
|
|
139
|
+
this._pick( item );
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.getInput( ).addDOMEvent( "keydown", ( ev ) => this._on_key( ev ) );
|
|
144
|
+
|
|
145
|
+
this.addDOMEvent( "focusout", ( ev ) => this._on_focusout( ev ) );
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
*
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
private _on_input( ) {
|
|
153
|
+
const text = this.getInput( ).getValue( );
|
|
154
|
+
|
|
155
|
+
// pending answers are obsolete
|
|
156
|
+
this._seq++;
|
|
157
|
+
|
|
158
|
+
if( text.length<this._min_chars ) {
|
|
159
|
+
this.clearTimeout( "search" );
|
|
160
|
+
this._popup.show( false );
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.setTimeout( "search", this._delay, ( ) => this._search( text ) );
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
*
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
private _search( text: string ) {
|
|
172
|
+
const seq = ++this._seq;
|
|
173
|
+
|
|
174
|
+
const setItems = ( items: ListItem[] ) => {
|
|
175
|
+
// a newer search was started (or the field was left)
|
|
176
|
+
if( seq!=this._seq ) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this._setItems( items ?? [] );
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.fire( "search", { text, setItems } );
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private _setItems( items: ListItem[] ) {
|
|
187
|
+
const list = this._popup.getList( );
|
|
188
|
+
list.setItems( items );
|
|
189
|
+
this._count = items.length;
|
|
190
|
+
|
|
191
|
+
if( !items.length ) {
|
|
192
|
+
this._popup.show( false );
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.showDropDown( );
|
|
197
|
+
|
|
198
|
+
// strict: Enter picks the first suggestion
|
|
199
|
+
if( this._strict ) {
|
|
200
|
+
list.navigate( kbNav.first );
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
*
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
private _on_key( ev: KeyboardEvent ) {
|
|
209
|
+
const list = this._popup.getList( );
|
|
210
|
+
const open = this._popup.isOpen( );
|
|
211
|
+
|
|
212
|
+
switch( ev.key ) {
|
|
213
|
+
case "ArrowUp":
|
|
214
|
+
case "ArrowDown": {
|
|
215
|
+
if( open ) {
|
|
216
|
+
list.navigate( ev.key=="ArrowDown" ? kbNav.next : kbNav.prev );
|
|
217
|
+
}
|
|
218
|
+
else if( this._count ) {
|
|
219
|
+
this.showDropDown( );
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.clearTimeout( "search" );
|
|
223
|
+
this._search( this.getInput( ).getValue( ) );
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case "Enter": {
|
|
230
|
+
const [sel] = open ? list.getSelection( ) : [];
|
|
231
|
+
const item = sel!==undefined ? list.getItem( sel ) : null;
|
|
232
|
+
|
|
233
|
+
if( !item ) {
|
|
234
|
+
// nothing to pick: the key is not for us
|
|
235
|
+
this._popup.show( false );
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this._pick( item );
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case "Escape": {
|
|
244
|
+
if( !open ) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._popup.show( false );
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
default: {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
ev.preventDefault( );
|
|
258
|
+
ev.stopPropagation( );
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
*
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
private _on_focusout( ev: FocusEvent ) {
|
|
266
|
+
// focus is moving inside the control
|
|
267
|
+
const to = ev.relatedTarget as Node;
|
|
268
|
+
if( to && this.dom.contains( to ) ) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.clearTimeout( "search" );
|
|
273
|
+
this._seq++;
|
|
274
|
+
this._popup.show( false );
|
|
275
|
+
|
|
276
|
+
const input = this.getInput( );
|
|
277
|
+
const text = input.getValue( );
|
|
278
|
+
if( text===this._text ) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// strict: back to the last validated value (but the field can be emptied)
|
|
283
|
+
if( this._strict && text!=="" ) {
|
|
284
|
+
input.setValue( this._text );
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this._commit( text, null );
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* an item was choosen in the suggestions
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
private _pick( item: ListItem ) {
|
|
296
|
+
const text = ( this._by_id ? item.id : item.text ) + "";
|
|
297
|
+
|
|
298
|
+
this.clearTimeout( "search" );
|
|
299
|
+
this._seq++;
|
|
300
|
+
|
|
301
|
+
this.getInput( ).setValue( text );
|
|
302
|
+
this._popup.show( false );
|
|
303
|
+
|
|
304
|
+
this._commit( text, item );
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private _commit( text: string, item: ListItem ) {
|
|
308
|
+
const changed = text!==this._text || item?.id!==this._item?.id;
|
|
309
|
+
|
|
310
|
+
this._text = text;
|
|
311
|
+
this._item = item;
|
|
312
|
+
|
|
313
|
+
if( changed ) {
|
|
314
|
+
this.fire( "change", { value: text, item } );
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
*
|
|
320
|
+
*/
|
|
321
|
+
|
|
322
|
+
showDropDown( ) {
|
|
323
|
+
if( this.isDisabled() || this._popup.isOpen( ) ) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const rc = ( this.query( ":scope > #edit" ) ?? this ).getBoundingRect( );
|
|
328
|
+
this._popup.setStyleValue( "minWidth", rc.width+"px" );
|
|
329
|
+
this._popup.displayNear( rc, "top left", "bottom left", {x:0,y:6} );
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* change the text of the field (no item is selected, no event)
|
|
334
|
+
*/
|
|
335
|
+
|
|
336
|
+
override setValue( value: string ) {
|
|
337
|
+
super.setValue( value );
|
|
338
|
+
|
|
339
|
+
this._text = this.getInput( ).getValue( );
|
|
340
|
+
this._item = null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* select an item as if it was picked in the suggestions (no event)
|
|
345
|
+
*/
|
|
346
|
+
|
|
347
|
+
setSelection( item: ListItem ) {
|
|
348
|
+
super.setValue( ( this._by_id ? item.id : item.text ) + "" );
|
|
349
|
+
|
|
350
|
+
this._text = this.getInput( ).getValue( );
|
|
351
|
+
this._item = item;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @returns the id of the picked item, undefined if the text does not come from the suggestions
|
|
356
|
+
*/
|
|
357
|
+
|
|
358
|
+
getSelection( ): ListboxID {
|
|
359
|
+
return this.getInput( ).getValue( )===this._text ? this._item?.id : undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { asap, class_ns, isArray, isNumber } from '../../core/core_tools';
|
|
18
18
|
import { Component, ComponentEvents, ComponentProps, EvSelectionChange } from "../../core/component"
|
|
19
19
|
import { EventCallback } from '../../core/core_events';
|
|
20
|
+
import { HSizer, VSizer } from '../sizers/sizer';
|
|
20
21
|
|
|
21
22
|
import "./boxes.module.scss";
|
|
22
23
|
|
|
@@ -91,6 +92,71 @@ export class VBox<P extends BoxProps=BoxProps,E extends ComponentEvents=Componen
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
|
|
95
|
+
// :: SPLITBOX ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
96
|
+
|
|
97
|
+
export interface SplitBoxProps extends Omit<BoxProps,"content"> {
|
|
98
|
+
/** Panels to lay out, a sizer is inserted between each of them. */
|
|
99
|
+
content: Component[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* insert a sizer between each panel.
|
|
104
|
+
* the sizer resizes the panel that is not flex (the flex one takes the remaining space),
|
|
105
|
+
* when both are of the same kind, the previous panel is resized.
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
function split_content( panels: Component[], horz: boolean ) {
|
|
109
|
+
const els = panels?.filter( x => !!x ) ?? [];
|
|
110
|
+
const res: Component[] = [];
|
|
111
|
+
|
|
112
|
+
els.forEach( ( p, i ) => {
|
|
113
|
+
if( i>0 ) {
|
|
114
|
+
const prev = els[i-1];
|
|
115
|
+
const next = prev.hasClass( "x4flex" ) && !p.hasClass( "x4flex" );
|
|
116
|
+
res.push( horz ? new HSizer( next ) : new VSizer( next ) );
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
res.push( p );
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return res;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A horizontal box where panels are separated by sizers: panels are side by side
|
|
127
|
+
* (like in a {@link HBox}) and the user can change their width.
|
|
128
|
+
* The CSS class for this component is automatically generated as `x4hsplitbox`.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* new HSplitBox( {
|
|
132
|
+
* content: [
|
|
133
|
+
* new Treeview( { width: 250, ... } ), // resized by the sizer
|
|
134
|
+
* new Panel( { flex: 1, ... } ), // takes the remaining space
|
|
135
|
+
* ]
|
|
136
|
+
* })
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
@class_ns( "x4" )
|
|
140
|
+
export class HSplitBox<P extends SplitBoxProps=SplitBoxProps,E extends ComponentEvents=ComponentEvents> extends HBox<P,E> {
|
|
141
|
+
constructor( props: P ) {
|
|
142
|
+
super( { ...props, content: split_content( props.content, true ) } );
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A vertical box where panels are separated by sizers: panels are stacked
|
|
148
|
+
* (like in a {@link VBox}) and the user can change their height.
|
|
149
|
+
* The CSS class for this component is automatically generated as `x4vsplitbox`.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
@class_ns( "x4" )
|
|
153
|
+
export class VSplitBox<P extends SplitBoxProps=SplitBoxProps,E extends ComponentEvents=ComponentEvents> extends VBox<P,E> {
|
|
154
|
+
constructor( props: P ) {
|
|
155
|
+
super( { ...props, content: split_content( props.content, false ) } );
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
94
160
|
type ContentBuilder = ( ) => Component;
|
|
95
161
|
|
|
96
162
|
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
15
|
**/
|
|
16
16
|
|
|
17
|
-
import { Menu, MenuElement, MenuProps } from 'x4js';
|
|
18
17
|
import { Component, ComponentEvents, ComponentProps, EvClick } from "../../core/component"
|
|
19
18
|
import { EventCallback } from '../../core/core_events';
|
|
20
19
|
import { class_ns, isFunction, UnsafeHtml } from '../../core/core_tools';
|
|
21
20
|
|
|
22
21
|
import { Icon } from "../icon/icon"
|
|
22
|
+
import { Menu, MenuElement } from '../menu/menu';
|
|
23
23
|
|
|
24
24
|
import "./button.module.scss";
|
|
25
25
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file chart.module.scss
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--chart-background: white;
|
|
19
|
+
--chart-text: #52514e;
|
|
20
|
+
--chart-grid: #e6e5e0;
|
|
21
|
+
--chart-axis: #b5b4ae;
|
|
22
|
+
|
|
23
|
+
// series colors, used in this order
|
|
24
|
+
--chart-color-1: #2a78d6;
|
|
25
|
+
--chart-color-2: #eb6834;
|
|
26
|
+
--chart-color-3: #1baf7a;
|
|
27
|
+
--chart-color-4: #eda100;
|
|
28
|
+
--chart-color-5: #e87ba4;
|
|
29
|
+
--chart-color-6: #008300;
|
|
30
|
+
--chart-color-7: #4a3aa7;
|
|
31
|
+
--chart-color-8: #e34948;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.x4chart {
|
|
35
|
+
display: block;
|
|
36
|
+
background-color: var( --chart-background );
|
|
37
|
+
|
|
38
|
+
text {
|
|
39
|
+
font-size: 10px;
|
|
40
|
+
fill: var( --chart-text );
|
|
41
|
+
user-select: none;
|
|
42
|
+
|
|
43
|
+
&.name {
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.grid,
|
|
49
|
+
.base {
|
|
50
|
+
fill: none;
|
|
51
|
+
stroke-width: 1px;
|
|
52
|
+
shape-rendering: crispEdges;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.grid {
|
|
56
|
+
stroke: var( --chart-grid );
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.base {
|
|
60
|
+
stroke: var( --chart-axis );
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --chart-c is the color of the element (palette slot, or serie.color)
|
|
64
|
+
@for $i from 1 through 8 {
|
|
65
|
+
.c#{$i} {
|
|
66
|
+
--chart-c: var( --chart-color-#{$i} );
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.mark,
|
|
71
|
+
.bar,
|
|
72
|
+
.slice {
|
|
73
|
+
fill: var( --chart-c );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.slice {
|
|
77
|
+
stroke: var( --chart-background );
|
|
78
|
+
stroke-width: 2px;
|
|
79
|
+
stroke-linejoin: round;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ring {
|
|
83
|
+
fill: none;
|
|
84
|
+
stroke: var( --chart-c );
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.line {
|
|
88
|
+
fill: none;
|
|
89
|
+
stroke: var( --chart-c );
|
|
90
|
+
stroke-width: 2px;
|
|
91
|
+
stroke-linejoin: round;
|
|
92
|
+
stroke-linecap: round;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.area {
|
|
96
|
+
fill: var( --chart-c );
|
|
97
|
+
fill-opacity: 0.15;
|
|
98
|
+
stroke: none;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.dot {
|
|
102
|
+
fill: var( --chart-c );
|
|
103
|
+
stroke: var( --chart-background );
|
|
104
|
+
stroke-width: 2px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.bar,
|
|
108
|
+
.slice,
|
|
109
|
+
.ring,
|
|
110
|
+
.dot {
|
|
111
|
+
transition: opacity 0.15s ease;
|
|
112
|
+
|
|
113
|
+
&:hover {
|
|
114
|
+
opacity: 0.75;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// hover point of a line without dots
|
|
119
|
+
.dot.ghost {
|
|
120
|
+
opacity: 0;
|
|
121
|
+
|
|
122
|
+
&:hover {
|
|
123
|
+
opacity: 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|