@trebco/treb 28.7.0 → 28.10.5

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.
Files changed (37) hide show
  1. package/dist/treb-spreadsheet-light.mjs +14 -14
  2. package/dist/treb-spreadsheet.mjs +12 -12
  3. package/dist/treb.d.ts +39 -6
  4. package/notes/connected-elements.md +37 -0
  5. package/package.json +1 -1
  6. package/treb-base-types/src/gradient.ts +1 -1
  7. package/treb-base-types/src/localization.ts +6 -0
  8. package/treb-calculator/src/calculator.ts +72 -30
  9. package/treb-calculator/src/dag/calculation_leaf_vertex.ts +7 -0
  10. package/treb-calculator/src/dag/graph.ts +8 -0
  11. package/treb-calculator/src/functions/base-functions.ts +30 -1
  12. package/treb-calculator/src/index.ts +1 -1
  13. package/treb-charts/src/chart-functions.ts +14 -0
  14. package/treb-charts/src/chart-types.ts +25 -1
  15. package/treb-charts/src/chart-utils.ts +195 -9
  16. package/treb-charts/src/chart.ts +4 -0
  17. package/treb-charts/src/default-chart-renderer.ts +17 -1
  18. package/treb-charts/src/renderer.ts +182 -9
  19. package/treb-charts/style/charts.scss +39 -0
  20. package/treb-embed/markup/toolbar.html +35 -34
  21. package/treb-embed/src/custom-element/treb-global.ts +10 -2
  22. package/treb-embed/src/embedded-spreadsheet.ts +209 -106
  23. package/treb-embed/src/options.ts +7 -0
  24. package/treb-embed/style/layout.scss +4 -0
  25. package/treb-embed/style/toolbar.scss +37 -0
  26. package/treb-grid/src/index.ts +1 -1
  27. package/treb-grid/src/types/conditional_format.ts +1 -1
  28. package/treb-grid/src/types/data_model.ts +32 -0
  29. package/treb-grid/src/types/grid.ts +11 -1
  30. package/treb-grid/src/types/grid_base.ts +161 -5
  31. package/treb-grid/src/types/grid_command.ts +32 -0
  32. package/treb-grid/src/types/grid_events.ts +7 -0
  33. package/treb-grid/src/types/grid_options.ts +8 -0
  34. package/treb-grid/src/types/sheet.ts +0 -56
  35. package/treb-grid/src/types/update_flags.ts +1 -0
  36. package/treb-parser/src/parser-types.ts +6 -0
  37. package/treb-parser/src/parser.ts +48 -1
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { type UnionValue, ValueType, type ArrayUnion } from 'treb-base-types';
3
3
  import { LegendStyle } from './chart-types';
4
- import type { SubSeries, SeriesType, BarData, ChartDataBaseType, ChartData, ScatterData2, LineData, DonutSlice } from './chart-types';
4
+ import type { SubSeries, SeriesType, BarData, ChartDataBaseType, ChartData, ScatterData2, LineData, DonutSlice, BubbleChartData } from './chart-types';
5
5
  import { NumberFormatCache } from 'treb-format';
6
6
  import { Util } from './util';
7
7
 
@@ -17,17 +17,33 @@ const DEFAULT_FORMAT = '#,##0.00'; // why not use "general", or whatever the usu
17
17
 
18
18
  export const ReadSeries = (data: Array<any>): SeriesType => {
19
19
 
20
+ // series type is (now)
21
+ //
22
+ // [0] label, string
23
+ // [1] X, array, metadata [* could be single value?]
24
+ // [2] Y, array, metadata [* could be single value?]
25
+ // [3] Z, array, metadata [* could be single value?]
26
+ // [4] index, number
27
+ // [5] subtype, string
28
+ //
29
+
20
30
  // in this case it's (label, X, Y)
21
31
  const series: SeriesType = {
22
32
  x: { data: [] },
23
33
  y: { data: [] },
24
34
  };
25
35
 
26
- if (data[3] && typeof data[3] === 'number') {
27
- series.index = data[3];
36
+ if (data[4] && typeof data[4] === 'number') {
37
+ series.index = data[4];
28
38
  }
29
- if (data[4]) {
30
- series.subtype = data[4].toString();
39
+
40
+ if (data[5]) {
41
+ series.subtype = data[5].toString();
42
+ }
43
+
44
+ if (data[6]) {
45
+ const labels = Util.Flatten(Array.isArray(data[6]) ? data[6] : [data[6]]);
46
+ series.labels = labels.map(value => (typeof value === 'undefined') ? '' : value.toString());
31
47
  }
32
48
 
33
49
  if (data[0]) {
@@ -47,6 +63,17 @@ export const ReadSeries = (data: Array<any>): SeriesType => {
47
63
  }
48
64
  }
49
65
 
66
+ // convert single value series to arrays so we can just use the old routine
67
+
68
+ for (let i = 1; i < 4; i++) {
69
+ if (data[i] && typeof data[i] === 'object' && data[i].key === 'metadata') {
70
+ data[i] = {
71
+ type: ValueType.array,
72
+ value: [data[i]],
73
+ }
74
+ }
75
+ }
76
+
50
77
  // read [2] first, so we can default for [1] if necessary
51
78
 
52
79
  if (!!data[2] && (typeof data[2] === 'object') && data[2].type === ValueType.array) {
@@ -67,7 +94,21 @@ export const ReadSeries = (data: Array<any>): SeriesType => {
67
94
  }
68
95
  }
69
96
 
70
- for (const subseries of [series.x, series.y]) {
97
+ const entries = [series.x, series.y]
98
+
99
+ // try reading [3]
100
+
101
+ if (!!data[3] && (typeof data[3] === 'object') && data[3].type === ValueType.array) {
102
+ const flat = Util.Flatten(data[3].value);
103
+ series.z = { data: [] };
104
+ series.z.data = flat.map(item => typeof item.value.value === 'number' ? item.value.value : undefined);
105
+ if (flat[0].value.format) {
106
+ series.z.format = flat[0].value.format;
107
+ }
108
+ }
109
+
110
+
111
+ for (const subseries of entries) {
71
112
 
72
113
  // in case of no values
73
114
  if (subseries.data.length) {
@@ -169,10 +210,13 @@ export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValu
169
210
  if (raw_data.type === ValueType.object) {
170
211
  if (raw_data.key === 'group') {
171
212
  if (Array.isArray(raw_data.value)) {
172
- for (const entry of raw_data.value) {
213
+ for (const [series_index, entry] of raw_data.value.entries()) {
173
214
  if (!!entry && (typeof entry === 'object')) {
174
215
  if (entry.key === 'series') {
175
216
  const series = ReadSeries(entry.value);
217
+ if (typeof series.index === 'undefined') {
218
+ series.index = series_index + 1;
219
+ }
176
220
  list.push(series);
177
221
  }
178
222
  else if (entry.type === ValueType.array) {
@@ -285,13 +329,40 @@ export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: n
285
329
  }
286
330
 
287
331
  const x = series.filter(test => test.x.range);
288
- const x_min = Math.min.apply(0, x.map(test => test.x.range?.min || 0));
289
- const x_max = Math.max.apply(0, x.map(test => test.x.range?.max || 0));
332
+ let x_min = Math.min.apply(0, x.map(test => test.x.range?.min || 0));
333
+ let x_max = Math.max.apply(0, x.map(test => test.x.range?.max || 0));
290
334
 
291
335
  const y = series.filter(test => test.y.range);
292
336
  let y_min = Math.min.apply(0, x.map(test => test.y.range?.min || 0));
293
337
  let y_max = Math.max.apply(0, x.map(test => test.y.range?.max || 0));
294
338
 
339
+ // if there's z data (used for bubble size), adjust x/y min/max to
340
+ // account for the z size so bubbles are contained within the grid
341
+
342
+ for (const subseries of series) {
343
+ if (subseries.z) {
344
+ for (const [index, z] of subseries.z.data.entries()) {
345
+ if (typeof z !== 'undefined') {
346
+ const x = subseries.x.data[index];
347
+
348
+ const half = Math.max(0, z/2); // accounting for negative values (which we don't use)
349
+
350
+ if (typeof x !== 'undefined') {
351
+ x_min = Math.min(x_min, x - half);
352
+ x_max = Math.max(x_max, x + half);
353
+ }
354
+
355
+ const y = subseries.y.data[index];
356
+ if (typeof y !== 'undefined') {
357
+ y_min = Math.min(y_min, y - half);
358
+ y_max = Math.max(y_max, y + half);
359
+ }
360
+
361
+ }
362
+ }
363
+ }
364
+ }
365
+
295
366
  if (typeof y_floor !== 'undefined') {
296
367
  y_min = Math.min(y_min, y_floor);
297
368
  }
@@ -364,6 +435,121 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
364
435
 
365
436
  //------------------------------------------------------------------------------
366
437
 
438
+ export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
439
+
440
+ const series: SeriesType[] = TransformSeriesData(args[0]);
441
+ const common = CommonData(series);
442
+ const title = args[1]?.toString() || undefined;
443
+ const options = args[2]?.toString() || undefined;
444
+
445
+ // console.info({ series, common, title, options });
446
+
447
+ const chart_data: BubbleChartData = {
448
+
449
+ legend: common.legend,
450
+ legend_style: LegendStyle.bubble,
451
+ type: 'bubble',
452
+ series,
453
+ title,
454
+
455
+ x_scale: common.x.scale,
456
+ x_labels: common.x.labels,
457
+
458
+ y_scale: common.y.scale,
459
+ y_labels: common.y.labels,
460
+
461
+ };
462
+
463
+ return chart_data;
464
+
465
+ /*
466
+ const [x, y, z] = [0,1,2].map(index => {
467
+ const arg = args[index];
468
+ if (arg.type === ValueType.array) {
469
+ return ArrayToSeries(arg).y;
470
+ }
471
+ return undefined;
472
+ });
473
+
474
+ let c: string[]|undefined = undefined;
475
+ if (Array.isArray(args[3])) {
476
+ c = Util.Flatten(args[3]).map(value => (value||'').toString());
477
+ }
478
+
479
+ const title = args[4]?.toString() || undefined;
480
+
481
+ // FIXME: need to pad out the axes by the values at the edges,
482
+ // so the whole circle is included in the chart area.
483
+
484
+ const [x_scale, y_scale] = [x, y].map(subseries => {
485
+
486
+ let series_min = 0;
487
+ let series_max = 1;
488
+ let first = false;
489
+
490
+ if (subseries?.data) {
491
+
492
+ for (const [index, value] of subseries.data.entries()) {
493
+ if (typeof value === 'number') {
494
+
495
+ if (!first) {
496
+ first = true;
497
+ series_min = value;
498
+ series_max = value;
499
+ }
500
+
501
+ const size = (z?.data?.[index]) || 0;
502
+ series_min = Math.min(series_min, value - size / 2);
503
+ series_max = Math.max(series_max, value + size / 2);
504
+ }
505
+ }
506
+ }
507
+
508
+ return Util.Scale(series_min, series_max, 7);
509
+
510
+ });
511
+
512
+ let x_labels: string[] | undefined;
513
+ let y_labels: string[] | undefined;
514
+
515
+ if (x?.format) {
516
+ x_labels = [];
517
+ const format = NumberFormatCache.Get(x.format);
518
+ for (let i = 0; i <= x_scale.count; i++) {
519
+ x_labels.push(format.Format(x_scale.min + i * x_scale.step));
520
+ }
521
+ }
522
+
523
+ if (y?.format) {
524
+ y_labels = [];
525
+ const format = NumberFormatCache.Get(y.format);
526
+ for (let i = 0; i <= y_scale.count; i++) {
527
+ y_labels.push(format.Format(y_scale.min + i * y_scale.step));
528
+ }
529
+ }
530
+
531
+ return {
532
+
533
+ type: 'bubble',
534
+
535
+ title,
536
+
537
+ x,
538
+ y,
539
+ z,
540
+ c,
541
+
542
+ x_scale,
543
+ y_scale,
544
+
545
+ x_labels,
546
+ y_labels,
547
+
548
+ };
549
+
550
+ */
551
+
552
+ };
367
553
 
368
554
  /**
369
555
  * args is [data, title, options]
@@ -64,6 +64,10 @@ export class Chart {
64
64
  this.chart_data = ChartUtils.CreateScatterChart(args, 'line');
65
65
  break;
66
66
 
67
+ case 'bubble.chart':
68
+ this.chart_data = ChartUtils.CreateBubbleChart(args);
69
+ break;
70
+
67
71
  default:
68
72
  this.Clear();
69
73
  break;
@@ -95,6 +95,7 @@ export class DefaultChartRenderer implements ChartRendererType {
95
95
  || chart_data.type === 'histogram2'
96
96
  || chart_data.type === 'bar'
97
97
  || chart_data.type === 'scatter2'
98
+ || chart_data.type === 'bubble'
98
99
  ) {
99
100
 
100
101
  // we need to measure first, then lay out the other axis, then we
@@ -122,7 +123,7 @@ export class DefaultChartRenderer implements ChartRendererType {
122
123
  let max_width = 0;
123
124
  let max_height = 0;
124
125
 
125
- const scale = (chart_data.type === 'scatter2') ? chart_data.y_scale : chart_data.scale;
126
+ const scale = (chart_data.type === 'scatter2' || chart_data.type === 'bubble') ? chart_data.y_scale : chart_data.scale;
126
127
 
127
128
  const count = (chart_data.type === 'bar') ?
128
129
  chart_data.y_labels.length :
@@ -163,6 +164,7 @@ export class DefaultChartRenderer implements ChartRendererType {
163
164
  chart_data.type !== 'area' &&
164
165
  chart_data.type !== 'bar' &&
165
166
  chart_data.type !== 'scatter2' &&
167
+ chart_data.type !== 'bubble' &&
166
168
  chart_data.type !== 'histogram2'
167
169
  );
168
170
 
@@ -199,6 +201,20 @@ export class DefaultChartRenderer implements ChartRendererType {
199
201
  this.renderer.RenderPoints(area, chart_data.x, chart_data.y, 'mc mc-correlation series-1');
200
202
  break;
201
203
 
204
+ case 'bubble':
205
+
206
+ this.renderer.RenderGrid(area,
207
+ chart_data.y_scale.count,
208
+ chart_data.x_scale.count + 1, // (sigh)
209
+ 'chart-grid');
210
+
211
+ for (const [index, series] of chart_data.series.entries()) {
212
+ const series_index = (typeof series.index === 'number') ? series.index : index + 1;
213
+ this.renderer.RenderBubbleSeries(area, series, chart_data.x_scale, chart_data.y_scale, `bubble-chart series-${series_index}`);
214
+ }
215
+
216
+ break;
217
+
202
218
  case 'scatter2':
203
219
 
204
220
  this.renderer.RenderGrid(area,
@@ -21,7 +21,7 @@
21
21
 
22
22
  import type { Size, Point } from './rectangle';
23
23
  import { Area } from './rectangle';
24
- import type { DonutSlice, LegendOptions} from './chart-types';
24
+ import type { DonutSlice, LegendOptions, SeriesType} from './chart-types';
25
25
  import { LegendLayout, LegendPosition, LegendStyle } from './chart-types';
26
26
  import type { RangeScale } from 'treb-utils';
27
27
 
@@ -196,14 +196,22 @@ export class ChartRenderer {
196
196
  group.appendChild(SVGNode('text', {
197
197
  'dominant-baseline': 'middle', x: x + marker_width, y, dy: (trident ? '.3em' : undefined) }, label.label));
198
198
 
199
- if (options.style === LegendStyle.marker) {
200
- group.appendChild(SVGNode('rect', {
201
- class: `series-${color}`, x, y: marker_y - 4, width: 8, height: 8 }));
202
- }
203
- else {
204
- group.appendChild(SVGNode('rect', {
205
- class: `series-${color}`, x, y: marker_y - 1, width: marker_width - 3, height: 2}));
206
- }
199
+ switch (options.style) {
200
+ case LegendStyle.marker:
201
+ group.appendChild(SVGNode('rect', {
202
+ class: `series-${color}`, x, y: marker_y - 4, width: 8, height: 8 }));
203
+ break;
204
+
205
+ case LegendStyle.bubble:
206
+ group.appendChild(SVGNode('circle', {
207
+ class: `series-${color}`, cx: x + marker_width - 11, cy: marker_y - 4 + 3, /* r: '0.25em' */ }));
208
+ break;
209
+
210
+ default:
211
+ group.appendChild(SVGNode('rect', {
212
+ class: `series-${color}`, x, y: marker_y - 1, width: marker_width - 3, height: 2}));
213
+ break;
214
+ }
207
215
 
208
216
  h = Math.max(h, text_metrrics.height);
209
217
  x += text_metrrics.width + marker_width + padding;
@@ -1193,6 +1201,171 @@ export class ChartRenderer {
1193
1201
 
1194
1202
  }
1195
1203
 
1204
+ public RenderBubbleSeries(
1205
+ area: Area,
1206
+ series: SeriesType,
1207
+ x_scale: RangeScale,
1208
+ y_scale: RangeScale,
1209
+ classes?: string | string[]): void {
1210
+
1211
+ const xrange = (x_scale.max - x_scale.min) || 1;
1212
+ const yrange = (y_scale.max - y_scale.min) || 1;
1213
+
1214
+ // const marker_elements: string[] = [];
1215
+ const points: Array<{x: number, y: number, z: number} | undefined> = [];
1216
+ const labels: Array<{
1217
+ x: number,
1218
+ y: number,
1219
+ text: string,
1220
+ offset: number,
1221
+ }> = [];
1222
+
1223
+ const d: string[] = [];
1224
+ const areas: string[] = [];
1225
+
1226
+ const group = SVGNode('g', {class: classes});
1227
+
1228
+ // if (title) node.setAttribute('title', title);
1229
+ this.group.appendChild(group);
1230
+
1231
+ if (series.z) {
1232
+ for (const [index, z] of series.z.data.entries()) {
1233
+
1234
+ const x = series.x.data[index];
1235
+ const y = series.y.data[index];
1236
+
1237
+ if (typeof x !== 'undefined' && typeof y !== 'undefined' && typeof z !== 'undefined' && z > 0) {
1238
+
1239
+ const size_x = z / xrange * area.width;
1240
+ const size_y = z / yrange * area.height;
1241
+ const size = Math.max(size_x, size_y);
1242
+
1243
+ const point: Point & { z: number } = {
1244
+ x: area.left + ((x - x_scale.min) / xrange) * area.width,
1245
+ y: area.bottom - ((y - y_scale.min) / yrange) * area.height,
1246
+ z: size,
1247
+ };
1248
+
1249
+ points.push(point);
1250
+
1251
+ if (series.labels?.[index]) {
1252
+ const r = point.z/2;
1253
+ labels.push({
1254
+ x: point.x, // + Math.cos(Math.PI/4) * r,
1255
+ y: point.y, // + Math.sin(Math.PI/4) * r,
1256
+ text: series.labels?.[index] || '',
1257
+ offset: Math.cos(Math.PI/4) * r,
1258
+ });
1259
+ }
1260
+
1261
+ }
1262
+
1263
+ }
1264
+ }
1265
+
1266
+ /*
1267
+
1268
+ let z_min = z[0] || 0;
1269
+ let z_max = z[0] || 0;
1270
+
1271
+ const map: Map<string, number> = new Map();
1272
+
1273
+ for (let i = 0; i < count; i++) {
1274
+
1275
+ const a = x[i];
1276
+ const b = y[i];
1277
+
1278
+ if (typeof a === 'undefined' || typeof b === 'undefined') {
1279
+ points.push(undefined);
1280
+ }
1281
+ else {
1282
+
1283
+ const series_key = c[i] || '';
1284
+ let series = map.get(series_key);
1285
+
1286
+ if (typeof series === 'undefined') {
1287
+
1288
+ series = map.size + 1;
1289
+
1290
+ map.set(series_key, series);
1291
+ }
1292
+
1293
+ let size = z[i] || 0;
1294
+ if (size) {
1295
+ const size_x = size / xrange * area.width;
1296
+ const size_y = size / yrange * area.height;
1297
+ size = Math.min(size_x, size_y);
1298
+ }
1299
+
1300
+ points.push({
1301
+ x: area.left + ((a - x_scale.min) / xrange) * area.width,
1302
+ y: area.bottom - ((b - y_scale.min) / yrange) * area.height,
1303
+ z: size,
1304
+ series,
1305
+ });
1306
+
1307
+ }
1308
+
1309
+ }
1310
+
1311
+ */
1312
+
1313
+ for (const point of points) {
1314
+ if (point) {
1315
+ group.appendChild(SVGNode('circle', {
1316
+ cx: point.x,
1317
+ cy: point.y,
1318
+ r: point.z / 2,
1319
+ class: `point`,
1320
+ }));
1321
+ }
1322
+ }
1323
+
1324
+ if (labels.length) {
1325
+ const container = this.label_group.getBoundingClientRect();
1326
+
1327
+ for (const entry of labels) {
1328
+ if (entry.text) {
1329
+
1330
+ const group = this.label_group.appendChild(SVGNode('g', {
1331
+ class: 'bubble-label',
1332
+ }));
1333
+
1334
+ const rect = group.appendChild(SVGNode('rect', {
1335
+ x: entry.x, // + entry.offset,
1336
+ y: entry.y, // + entry.offset,
1337
+ // rx: `3px`,
1338
+ // fill: 'Canvas',
1339
+ // 'fill-opacity': '60%',
1340
+ // stroke: `none`,
1341
+ // 'style': `--translate-offset: ${Math.round(entry.offset)}px`,
1342
+ class: 'label-background'
1343
+ }));
1344
+
1345
+ const label = group.appendChild(SVGNode('text', {
1346
+ x: entry.x, // + entry.offset,
1347
+ y: entry.y, // + entry.offset,
1348
+ offset: entry.offset,
1349
+ class: 'label-text',
1350
+ 'text-anchor': 'middle',
1351
+ 'alignment-baseline': 'middle',
1352
+ 'style': `--translate-offset: ${Math.round(entry.offset)}px`,
1353
+ }, entry.text));
1354
+
1355
+ const bounds = label.getBoundingClientRect();
1356
+
1357
+ rect.setAttribute('x', (bounds.left - container.left - 2).toString());
1358
+ rect.setAttribute('y', (bounds.top - container.top - 1).toString());
1359
+ rect.style.height = (bounds.height + 2) + `px`;
1360
+ rect.style.width = (bounds.width + 4) + `px`;
1361
+
1362
+ }
1363
+ }
1364
+ }
1365
+
1366
+
1367
+ }
1368
+
1196
1369
  public RenderScatterSeries(area: Area,
1197
1370
  x: Array<number | undefined>,
1198
1371
  y: Array<number | undefined>,
@@ -92,6 +92,13 @@
92
92
  rect {
93
93
  fill: currentColor;
94
94
  }
95
+ circle {
96
+ fill: currentColor;
97
+ fill-opacity: .5;
98
+ stroke: currentColor;
99
+ stroke-width: 2px;
100
+ r: .25em;
101
+ }
95
102
  }
96
103
 
97
104
  /* grid */
@@ -161,6 +168,38 @@
161
168
  }
162
169
  }
163
170
 
171
+ .bubble-chart {
172
+
173
+ stroke-width: 3;
174
+ fill: color-mix(in srgb, currentColor 75%, transparent);
175
+ stroke: currentColor;
176
+ }
177
+
178
+ .bubble-label {
179
+
180
+ .label-background {
181
+ stroke: none;
182
+ fill: none;
183
+
184
+ /*
185
+ fill: Canvas;
186
+ fill-opacity: .5;
187
+ rx: 2px;
188
+ */
189
+ }
190
+
191
+ .label-text {
192
+
193
+ /**
194
+ * default translate to lower-right. you can calc() to switch
195
+ * to a different position.
196
+ */
197
+ transform: translate(var(--translate-offset), var(--translate-offset));
198
+
199
+ }
200
+
201
+ }
202
+
164
203
  /* scatter plot line (and marker -- change that class name) */
165
204
  .scatter-plot {
166
205
 
@@ -65,6 +65,27 @@
65
65
  </div>
66
66
  </div>
67
67
 
68
+ <div composite>
69
+ <button data-command="border-bottom" data-target="border" title="Bottom border"></button>
70
+ <div class="treb-menu">
71
+ <button dropdown title="Border options"></button>
72
+ <div class="treb-icon-buttons" data-replace="border">
73
+ <button data-command="border-top" title="Top border"></button>
74
+ <button data-command="border-left" title="Left border"></button>
75
+ <button data-command="border-right" title="Right border"></button>
76
+ <button data-command="border-bottom" title="Bottom border"></button>
77
+ <button data-command="border-double-bottom" title="Double bottom border"></button>
78
+ <button data-command="border-outside" title="Outside borders"></button>
79
+ <button data-command="border-all" title="All borders"></button>
80
+ <button data-command="border-none" title="Clear borders"></button>
81
+ <div separator></div>
82
+ <div class="treb-menu treb-color-menu treb-submenu" data-color-command="border-color" data-replace-color="border" title="Border color" data-default-color-text="Default border color">
83
+ <button data-icon="palette" data-color-bar="border" data-color="{}"></button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
68
89
  <div composite>
69
90
  <button data-command="fill-color" data-color-bar="fill" data-color="{}" title="Fill color"></button>
70
91
  <div class="treb-menu treb-color-menu" data-color-command="fill-color" data-replace-color="fill" data-default-color-text="No fill">
@@ -94,28 +115,8 @@
94
115
  </div>
95
116
  </div>
96
117
 
97
- <div composite>
98
- <button data-command="border-bottom" data-target="border" title="Bottom border"></button>
99
- <div class="treb-menu">
100
- <button dropdown title="Border options"></button>
101
- <div class="treb-icon-buttons" data-replace="border">
102
- <button data-command="border-top" title="Top border"></button>
103
- <button data-command="border-left" title="Left border"></button>
104
- <button data-command="border-right" title="Right border"></button>
105
- <button data-command="border-bottom" title="Bottom border"></button>
106
- <button data-command="border-double-bottom" title="Double bottom border"></button>
107
- <button data-command="border-outside" title="Outside borders"></button>
108
- <button data-command="border-all" title="All borders"></button>
109
- <button data-command="border-none" title="Clear borders"></button>
110
- <div separator></div>
111
- <div class="treb-menu treb-color-menu treb-submenu" data-color-command="border-color" data-replace-color="border" title="Border color" data-default-color-text="Default border color">
112
- <button data-icon="palette" data-color-bar="border" data-color="{}"></button>
113
- </div>
114
- </div>
115
- </div>
116
- </div>
117
-
118
118
  <div composite font-scale>
119
+ <div class="treb-font-scale-icon"></div>
119
120
  <input class="treb-font-scale" title="Font scale">
120
121
  <div class="treb-menu">
121
122
  <button dropdown title="Font scale options"></button>
@@ -131,19 +132,6 @@
131
132
  </div>
132
133
  </div>
133
134
 
134
- <div class="treb-menu">
135
- <button data-icon="layout" title="Rows & columns"></button>
136
- <div>
137
- <button data-command="insert-row">Insert row</button>
138
- <button data-command="insert-column">Insert column</button>
139
- <button data-command="delete-row">Delete row</button>
140
- <button data-command="delete-column">Delete column</button>
141
- <div separator add-remove-sheet></div>
142
- <button data-command="insert-sheet" add-remove-sheet>Add sheet</button>
143
- <button data-command="delete-sheet" add-remove-sheet>Delete sheet</button>
144
- </div>
145
- </div>
146
-
147
135
  <div composite>
148
136
  <input class="treb-number-format" title="Number format">
149
137
  <div class="treb-menu">
@@ -157,6 +145,19 @@
157
145
  <button data-command="increase-precision" title="Increase precision"></button>
158
146
  </div>
159
147
 
148
+ <div class="treb-menu">
149
+ <button data-icon="layout" title="Rows & columns"></button>
150
+ <div>
151
+ <button data-command="insert-row">Insert row</button>
152
+ <button data-command="insert-column">Insert column</button>
153
+ <button data-command="delete-row">Delete row</button>
154
+ <button data-command="delete-column">Delete column</button>
155
+ <div separator add-remove-sheet></div>
156
+ <button data-command="insert-sheet" add-remove-sheet>Add sheet</button>
157
+ <button data-command="delete-sheet" add-remove-sheet>Delete sheet</button>
158
+ </div>
159
+ </div>
160
+
160
161
  <div composite chart-menu>
161
162
  <button data-command="insert-column-chart" data-target="annotation" title="Insert column chart"></button>
162
163
  <div class="treb-menu">