abstract-chart 7.5.1 → 8.0.1

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/src/chart.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import * as AI from "abstract-image";
2
3
  import * as Axis from "./axis";
3
4
  import { exhaustiveCheck } from "ts-exhaustive-check";
@@ -8,16 +9,22 @@ export type Partial<T> = { [P in keyof T]?: T[P] };
8
9
 
9
10
  export type LabelLayout = "original" | "end" | "center";
10
11
 
12
+ const axisLabelPosFactor = 0.65;
13
+
11
14
  export interface Chart {
12
15
  readonly width: number;
13
16
  readonly height: number;
14
17
  readonly chartPoints: Array<ChartPoint>;
15
18
  readonly chartLines: Array<ChartLine>;
16
19
  readonly chartStack: ChartStack;
17
- readonly xAxisBottom: Axis.Axis | undefined;
18
- readonly xAxisTop: Axis.Axis | undefined;
19
- readonly yAxisLeft: Axis.Axis | undefined;
20
- readonly yAxisRight: Axis.Axis | undefined;
20
+ readonly chartDataAxisesBottom: Array<ChartDataAxis>;
21
+ readonly chartDataAxisesTop: Array<ChartDataAxis>;
22
+ readonly chartDataAxisesLeft: Array<ChartDataAxis>;
23
+ readonly chartDataAxisesRight: Array<ChartDataAxis>;
24
+ readonly xAxisesBottom: ReadonlyArray<Axis.Axis>;
25
+ readonly xAxisesTop: ReadonlyArray<Axis.Axis>;
26
+ readonly yAxisesLeft: ReadonlyArray<Axis.Axis>;
27
+ readonly yAxisesRight: ReadonlyArray<Axis.Axis>;
21
28
  readonly backgroundColor: AI.Color;
22
29
  readonly xGrid: ChartGrid;
23
30
  readonly yGrid: ChartGrid;
@@ -27,6 +34,7 @@ export interface Chart {
27
34
  readonly textOutlineColor: AI.Color;
28
35
  readonly labelLayout: LabelLayout;
29
36
  readonly padding: Padding;
37
+ readonly axisWidth: number;
30
38
  }
31
39
 
32
40
  export type ChartGrid = { readonly color: AI.Color; readonly thickness: number };
@@ -40,10 +48,14 @@ export function createChart(props: ChartProps): Chart {
40
48
  chartPoints: props.chartPoints ?? [],
41
49
  chartLines: props.chartLines ?? [],
42
50
  chartStack: props.chartStack ?? createChartStack({}),
43
- xAxisBottom: props.xAxisBottom ?? Axis.createLinearAxis(0, 100, ""),
44
- xAxisTop: props.xAxisTop ?? undefined,
45
- yAxisLeft: props.yAxisLeft ?? Axis.createLinearAxis(0, 100, ""),
46
- yAxisRight: props.yAxisRight ?? undefined,
51
+ chartDataAxisesBottom: props.chartDataAxisesBottom ?? [],
52
+ chartDataAxisesTop: props.chartDataAxisesTop ?? [],
53
+ chartDataAxisesLeft: props.chartDataAxisesLeft ?? [],
54
+ chartDataAxisesRight: props.chartDataAxisesRight ?? [],
55
+ xAxisesBottom: props.xAxisesBottom ?? [],
56
+ xAxisesTop: props.xAxisesTop ?? [],
57
+ yAxisesLeft: props.yAxisesLeft ?? [],
58
+ yAxisesRight: props.yAxisesRight ?? [],
47
59
  backgroundColor: props.backgroundColor ?? AI.white,
48
60
  font: props.font ?? "Arial",
49
61
  fontSize: props.fontSize ?? 12,
@@ -51,11 +63,12 @@ export function createChart(props: ChartProps): Chart {
51
63
  textOutlineColor: props.textOutlineColor ?? AI.transparent,
52
64
  labelLayout: props.labelLayout ?? "original",
53
65
  padding: {
54
- top: props.padding?.top ?? (props.xAxisTop !== undefined ? 45 : 10),
55
- right: props.padding?.right ?? (props.yAxisRight !== undefined ? 45 : 10),
56
- bottom: props.padding?.bottom ?? (props.xAxisBottom === undefined ? 10 : 45),
57
- left: props.padding?.left ?? (!props.yAxisLeft === undefined ? 10 : 45),
66
+ top: props.padding?.top ?? 10,
67
+ right: props.padding?.right ?? 10,
68
+ bottom: props.padding?.bottom ?? 10,
69
+ left: props.padding?.left ?? 10,
58
70
  },
71
+ axisWidth: props.axisWidth ?? 80,
59
72
  xGrid: { color: props.xGrid?.color ?? AI.gray, thickness: props.xGrid?.thickness ?? 1 },
60
73
  yGrid: { color: props.yGrid?.color ?? AI.gray, thickness: props.yGrid?.thickness ?? 1 },
61
74
  };
@@ -182,49 +195,140 @@ export function createChartStack(props: ChartStackProps): ChartStack {
182
195
  return { points, xAxis, yAxis, config };
183
196
  }
184
197
 
185
- export function inverseTransformPoint(
186
- point: AI.Point,
187
- chart: Chart,
188
- xAxis: XAxis,
189
- yAxis: YAxis,
190
- padding: Padding
191
- ): AI.Point | undefined {
198
+ export type ChartDataAxis = Axis.AxisBase & {
199
+ readonly points: Array<AI.Point>;
200
+ };
201
+
202
+ export function createChartDataAxis(
203
+ points: Array<AI.Point>,
204
+ label: string,
205
+ labelRotation?: number,
206
+ tickLabelDisp?: number,
207
+ labelColor?: AI.Color,
208
+ tickLabelColor?: AI.Color,
209
+ thickness?: number,
210
+ axisColor?: AI.Color,
211
+ tickFontSize?: number,
212
+ axisFontSize?: number,
213
+ id?: string
214
+ ): ChartDataAxis {
215
+ return {
216
+ points,
217
+ label,
218
+ labelRotation,
219
+ tickLabelDisp,
220
+ labelColor,
221
+ tickLabelColor,
222
+ thickness,
223
+ axisColor,
224
+ tickFontSize,
225
+ axisFontSize,
226
+ id,
227
+ };
228
+ }
229
+
230
+ export function inverseTransformPoint(point: AI.Point, chart: Chart, xAxis: XAxis, yAxis: YAxis): AI.Point | undefined {
231
+ const padding = finalPadding(chart);
192
232
  const xMin = padding.left;
193
233
  const xMax = chart.width - padding.right;
194
234
  const yMin = chart.height - padding.bottom;
195
235
  const yMax = padding.top;
196
- const x = Axis.inverseTransformValue(point.x, xMin, xMax, xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom);
197
- const y = Axis.inverseTransformValue(point.y, yMin, yMax, yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft);
236
+ const x = Axis.inverseTransformValue(
237
+ point.x,
238
+ xMin,
239
+ xMax,
240
+ xAxis === "top" ? chart.xAxisesTop[0] : chart.xAxisesBottom[0]
241
+ );
242
+ const y = Axis.inverseTransformValue(
243
+ point.y,
244
+ yMin,
245
+ yMax,
246
+ yAxis === "right" ? chart.yAxisesRight[0] : chart.yAxisesLeft[0]
247
+ );
198
248
  if (x === undefined || y === undefined) {
199
249
  return undefined;
200
250
  }
201
251
  return AI.createPoint(x, y);
202
252
  }
203
253
 
254
+ function finalPadding(chart: Chart): Padding {
255
+ return {
256
+ bottom: chart.padding.bottom + (chart.xAxisesBottom.length + chart.chartDataAxisesBottom.length) * chart.axisWidth,
257
+ top: chart.padding.top + (chart.xAxisesTop.length + chart.chartDataAxisesTop.length) * chart.axisWidth,
258
+ left: chart.padding.left + (chart.yAxisesLeft.length + chart.chartDataAxisesLeft.length) * chart.axisWidth,
259
+ right: chart.padding.right + (chart.yAxisesRight.length + chart.chartDataAxisesRight.length) * chart.axisWidth,
260
+ };
261
+ }
262
+
204
263
  export function renderChart(chart: Chart): AI.AbstractImage {
205
- const { width, height, xAxisBottom, xAxisTop, yAxisLeft, yAxisRight, padding } = chart;
264
+ const { width, height, xAxisesBottom, xAxisesTop, yAxisesLeft, yAxisesRight } = chart;
265
+
266
+ const padding = finalPadding(chart);
206
267
 
207
268
  const gridWidth = width - padding.left - padding.right;
208
269
  const gridHeight = height - padding.bottom - padding.top;
209
270
 
210
271
  const xMin = padding.left;
211
- const xMax = width - padding.right;
212
- const yMin = height - padding.bottom;
272
+ const xMax = chart.width - padding.right;
273
+ const yMin = chart.height - padding.bottom;
213
274
  const yMax = padding.top;
214
275
 
215
276
  const renderedBackground = generateBackground(xMin, xMax, yMin, yMax, chart);
216
277
 
217
278
  const xNumTicks = gridWidth / 40;
218
- const renderedXAxisBottom = generateXAxisBottom(xNumTicks, xAxisBottom, xMin, xMax, yMin, yMax, chart);
219
- const renderedXAxisTop = generateXAxisTop(xNumTicks, xAxisTop, xMin, xMax, yMax, chart);
279
+ const renderedXAxisBottom = generateXAxises("bottom", xNumTicks, xAxisesBottom, xMin, xMax, yMin, yMax, chart);
280
+ const renderedXAxisTop = generateXAxises("top", xNumTicks, xAxisesTop, xMin, xMax, yMin, yMax, chart);
220
281
 
221
282
  const yNumTicks = gridHeight / 40;
222
- const renderedYAxisLeft = generateYAxisLeft(yNumTicks, yAxisLeft, xMin, xMax, yMin, yMax, chart);
223
- const renderedYAxisRight = generateYAxisRight(yNumTicks, yAxisRight, xMax, yMin, yMax, chart);
283
+ const renderedYAxisLeft = generateYAxises("left", yNumTicks, yAxisesLeft, xMin, xMax, yMin, yMax, chart);
284
+ const renderedYAxisRight = generateYAxises("right", yNumTicks, yAxisesRight, xMin, xMax, yMin, yMax, chart);
224
285
 
225
286
  const renderedPoints = generatePoints(xMin, xMax, yMin, yMax, chart);
226
287
  const renderedLines = generateLines(xMin, xMax, yMin, yMax, chart);
227
288
  const renderedStack = generateStack(xMin, xMax, yMin, yMax, chart);
289
+ const dataNumTicksX = gridWidth / 70;
290
+ const renderedDataAxisesBottom = generateDataAxisesX(
291
+ "bottom",
292
+ chart.chartDataAxisesBottom,
293
+ dataNumTicksX,
294
+ xMin,
295
+ xMax,
296
+ yMin,
297
+ yMax,
298
+ chart
299
+ );
300
+ const renderedDataAxisesTop = generateDataAxisesX(
301
+ "top",
302
+ chart.chartDataAxisesTop,
303
+ dataNumTicksX,
304
+ xMin,
305
+ xMax,
306
+ yMin,
307
+ yMax,
308
+ chart
309
+ );
310
+
311
+ const dataNumTicksY = gridHeight / 70;
312
+ const renderedDataAxisesLeft = generateDataAxisesY(
313
+ "left",
314
+ chart.chartDataAxisesLeft,
315
+ dataNumTicksY,
316
+ xMin,
317
+ xMax,
318
+ yMin,
319
+ yMax,
320
+ chart
321
+ );
322
+ const renderedDataAxisesRight = generateDataAxisesY(
323
+ "right",
324
+ chart.chartDataAxisesRight,
325
+ dataNumTicksY,
326
+ xMin,
327
+ xMax,
328
+ yMin,
329
+ yMax,
330
+ chart
331
+ );
228
332
 
229
333
  const components = [
230
334
  renderedBackground,
@@ -235,6 +339,10 @@ export function renderChart(chart: Chart): AI.AbstractImage {
235
339
  renderedStack,
236
340
  renderedLines,
237
341
  renderedPoints,
342
+ renderedDataAxisesBottom,
343
+ renderedDataAxisesTop,
344
+ renderedDataAxisesLeft,
345
+ renderedDataAxisesRight,
238
346
  ];
239
347
  const topLeft = AI.createPoint(0, 0);
240
348
  const size = AI.createSize(width, height);
@@ -251,9 +359,10 @@ export function generateBackground(xMin: number, xMax: number, yMin: number, yMa
251
359
  );
252
360
  }
253
361
 
254
- export function generateXAxisBottom(
362
+ export function generateXAxises(
363
+ xAxis: XAxis,
255
364
  xNumTicks: number,
256
- axis: Axis.Axis | undefined,
365
+ axises: ReadonlyArray<Axis.Axis>,
257
366
  xMin: number,
258
367
  xMax: number,
259
368
  yMin: number,
@@ -261,96 +370,105 @@ export function generateXAxisBottom(
261
370
  chart: Chart
262
371
  ): AI.Component {
263
372
  const components = Array<AI.Component>();
264
- if (!axis) {
265
- return AI.createGroup("XAxisBottom", components);
266
- }
267
- const axisLabelPosY = yMin + chart.padding.bottom - (axis.axisFontSize ?? chart.fontSize);
268
- const xTicks = Axis.getTicks(xNumTicks, axis);
269
- if (chart.xGrid) {
270
- components.push(generateXAxisGridLines(xMin, xMax, yMin + 10, yMax, xTicks, axis, chart.xGrid));
271
- }
272
- components.push(
273
- AI.createLine({ x: xMin, y: yMin }, { x: xMax, y: yMin }, axis.axisColor ?? AI.gray, axis.thickness ?? 1),
274
- generateXAxisLabels(xMin, xMax, yMin + (axis.tickLabelDisp ?? 10), "down", xTicks, axis, chart)
275
- );
276
-
277
- switch (chart.labelLayout) {
278
- case "original":
373
+ let lineY = xAxis === "bottom" ? yMin : yMax;
374
+ const dirFactor = xAxis == "bottom" ? 1 : -1;
375
+ for (const [ix, axis] of axises.entries()) {
376
+ const fullGrid = ix === 0 && xAxis === "bottom";
377
+ const xTicks = Axis.getTicks(xNumTicks, axis);
378
+ if (chart.xGrid) {
279
379
  components.push(
280
- generateXAxisLabel(
281
- xMax + chart.padding.right,
282
- yMin + (axis.tickLabelDisp ?? 10),
283
- "uniform",
284
- "down",
285
- axis,
286
- chart
287
- )
380
+ generateXAxisGridLines(xMin, xMax, lineY + dirFactor * 10, fullGrid ? yMax : lineY, xTicks, axis, chart.xGrid)
288
381
  );
289
- break;
290
- case "end":
291
- components.push(generateXAxisLabel(xMax, axisLabelPosY, "left", "down", axis, chart));
292
- break;
293
- case "center":
294
- components.push(generateXAxisLabel((xMin + xMax) / 2, axisLabelPosY, "uniform", "down", axis, chart));
295
- break;
296
- default:
297
- return exhaustiveCheck(chart.labelLayout);
382
+ }
383
+ components.push(
384
+ AI.createLine({ x: xMin, y: lineY }, { x: xMax, y: lineY }, axis.axisColor ?? AI.gray, axis.thickness ?? 1),
385
+ generateXAxisLabels(xMin, xMax, lineY + dirFactor * 12, xAxis === "bottom" ? "down" : "up", xTicks, axis, chart)
386
+ );
387
+
388
+ const axisLabelPosY = lineY + dirFactor * chart.axisWidth * axisLabelPosFactor;
389
+ switch (chart.labelLayout) {
390
+ case "original":
391
+ components.push(
392
+ generateXAxisLabel(
393
+ xMax + chart.padding.right,
394
+ lineY + (axis.tickLabelDisp ?? 10),
395
+ "uniform",
396
+ "down",
397
+ axis,
398
+ chart
399
+ )
400
+ );
401
+ break;
402
+ case "end":
403
+ components.push(generateXAxisLabel(xMax, axisLabelPosY, "left", "uniform", axis, chart));
404
+ break;
405
+ case "center":
406
+ components.push(generateXAxisLabel((xMin + xMax) / 2, axisLabelPosY, "uniform", "uniform", axis, chart));
407
+ break;
408
+ default:
409
+ return exhaustiveCheck(chart.labelLayout);
410
+ }
411
+ lineY += dirFactor * chart.axisWidth;
298
412
  }
299
413
 
300
- return AI.createGroup("XAxisBottom", components);
414
+ return AI.createGroup(xAxis + "XAxis", components);
301
415
  }
302
416
 
303
- export function generateXAxisTop(
304
- xNumTicks: number,
305
- axis: Axis.Axis | undefined,
417
+ export function generateYAxises(
418
+ yAxis: YAxis,
419
+ yNumTicks: number,
420
+ axises: ReadonlyArray<Axis.Axis>,
306
421
  xMin: number,
307
422
  xMax: number,
423
+ yMin: number,
308
424
  yMax: number,
309
425
  chart: Chart
310
426
  ): AI.Component {
311
427
  const components = Array<AI.Component>();
312
- if (!axis) {
313
- return AI.createGroup("XAxisTop", components);
314
- }
315
- const axisLabelPosY = yMax - chart.padding.top + (axis.axisFontSize ?? chart.fontSize);
316
- const xTicks = Axis.getTicks(xNumTicks, axis);
317
- if (chart.xGrid) {
318
- components.push(generateXAxisGridLines(xMin, xMax, yMax - 10, yMax, xTicks, axis, chart.xGrid));
319
- }
320
- components.push(
321
- AI.createLine({ x: xMin, y: yMax }, { x: xMax, y: yMax }, axis.axisColor ?? AI.gray, axis.thickness ?? 1),
322
- generateXAxisLabels(xMin, xMax, yMax - (axis.tickLabelDisp ?? 13), "up", xTicks, axis, chart)
323
- );
324
-
325
- switch (chart.labelLayout) {
326
- case "original":
428
+ let lineX = yAxis === "left" ? xMin : xMax;
429
+ const dirFactor = yAxis == "left" ? -1 : 1;
430
+ for (const [ix, axis] of axises.entries()) {
431
+ const fullGrid = ix === 0 && yAxis === "left";
432
+ const yTicks = Axis.getTicks(yNumTicks, axis);
433
+ if (chart.yGrid) {
327
434
  components.push(
328
- generateXAxisLabel(
329
- xMax + 0.5 * chart.padding.right,
330
- yMax - (axis.tickLabelDisp ?? 13),
331
- "uniform",
332
- "up",
333
- axis,
334
- chart
335
- )
435
+ generateYAxisLines(lineX + dirFactor * 10, fullGrid ? xMax : lineX, yMin, yMax, yTicks, axis, chart.yGrid)
336
436
  );
337
- break;
338
- case "end":
339
- components.push(generateXAxisLabel(xMax, axisLabelPosY, "left", "up", axis, chart));
340
- break;
341
- case "center":
342
- components.push(generateXAxisLabel((xMin + xMax) / 2, axisLabelPosY, "uniform", "up", axis, chart));
343
- break;
344
- default:
345
- return exhaustiveCheck(chart.labelLayout);
437
+ }
438
+ components.push(
439
+ AI.createLine({ x: lineX, y: yMin }, { x: lineX, y: yMax }, axis.axisColor ?? AI.gray, axis.thickness ?? 1),
440
+ generateYAxisLabels(lineX + dirFactor * 12, yMin, yMax, yAxis, yTicks, axis, chart)
441
+ );
442
+
443
+ const axisLabelPosX = lineX + dirFactor * chart.axisWidth * axisLabelPosFactor;
444
+ const rotation = yAxis === "left" ? -90 : 90;
445
+ switch (chart.labelLayout) {
446
+ case "original":
447
+ components.push(
448
+ generateYAxisLabel(axisLabelPosX, yMax + 0.5 * chart.padding.bottom, rotation, "uniform", "up", axis, chart)
449
+ );
450
+ break;
451
+ case "end":
452
+ components.push(generateYAxisLabel(axisLabelPosX, yMax, rotation, yAxis, "uniform", axis, chart));
453
+ break;
454
+ case "center":
455
+ components.push(
456
+ generateYAxisLabel(axisLabelPosX, (yMin + yMax) / 2, rotation, "uniform", "uniform", axis, chart)
457
+ );
458
+ break;
459
+ default:
460
+ return exhaustiveCheck(chart.labelLayout);
461
+ }
462
+ lineX += dirFactor * chart.axisWidth;
346
463
  }
347
464
 
348
- return AI.createGroup("XAxisTop", components);
465
+ return AI.createGroup("YAxisLeft", components);
349
466
  }
350
467
 
351
- export function generateYAxisLeft(
352
- yNumTicks: number,
353
- axis: Axis.Axis | undefined,
468
+ export function generateDataAxisesX(
469
+ xAxis: XAxis,
470
+ axises: Array<ChartDataAxis>,
471
+ numTicks: number,
354
472
  xMin: number,
355
473
  xMax: number,
356
474
  yMin: number,
@@ -358,79 +476,216 @@ export function generateYAxisLeft(
358
476
  chart: Chart
359
477
  ): AI.Component {
360
478
  const components = Array<AI.Component>();
361
- if (!axis) {
362
- return AI.createGroup("YAxisLeft", components);
363
- }
364
- const axisLabelPosX = xMin - chart.padding.left + (axis.axisFontSize ?? chart.fontSize);
479
+ let lineY =
480
+ xAxis === "bottom"
481
+ ? yMin + chart.xAxisesBottom.length * chart.axisWidth
482
+ : yMax - chart.xAxisesTop.length * chart.axisWidth;
483
+ const dirFactor = xAxis === "bottom" ? 1 : -1;
484
+ for (const axis of axises) {
485
+ const min = Math.min(...axis.points.map((p) => p.y));
486
+ const max = Math.max(...axis.points.map((p) => p.y));
487
+ const linear = Axis.createLinearAxis(
488
+ min,
489
+ max,
490
+ axis.label,
491
+ axis.labelColor,
492
+ axis.labelRotation,
493
+ axis.tickLabelDisp,
494
+ axis.thickness,
495
+ axis.axisColor,
496
+ axis.id
497
+ );
498
+ const findX = (y: number): number => {
499
+ for (let i = 0; i < axis.points.length; ++i) {
500
+ const p0 = i > 0 ? axis.points[i - 1] : undefined;
501
+ const p1 = axis.points[i];
502
+ if (!p1) {
503
+ continue;
504
+ }
505
+ if (p0 && p0.y <= y && p1.y >= y) {
506
+ const k = (p1.x - p0.x) / (p1.y - p0.y);
507
+ const x = p0.x + k * (y - p0.y);
508
+ return x;
509
+ }
510
+ if (!p0 && p1.y >= y) {
511
+ return p1.x;
512
+ }
513
+ }
514
+ return axis.points[axis.points.length - 1]?.x ?? 0;
515
+ };
516
+ const yValues = Axis.getTicks(numTicks, linear).map((t) => t.value);
517
+ const lineY2 = lineY;
518
+ components.push(
519
+ ...yValues.flatMap((y) => {
520
+ const tickX = findX(y);
521
+ const x = Axis.transformValue(tickX, xMin, xMax, chart.xAxisesBottom[0]);
522
+ const start = AI.createPoint(x, lineY2);
523
+ const end = AI.createPoint(x, lineY2 + dirFactor * 10);
524
+ const textPos = AI.createPoint(x, lineY2 + dirFactor * 12);
525
+ return [
526
+ AI.createLine(start, end, chart.xGrid.color, chart.xGrid.thickness),
527
+ AI.createText(
528
+ textPos,
529
+ formatNumber(y),
530
+ chart.font,
531
+ axis.tickFontSize ?? chart.fontSize,
532
+ axis.labelColor ?? AI.black,
533
+ "normal",
534
+ axis.labelRotation ?? 0,
535
+ "center",
536
+ "uniform",
537
+ xAxis === "bottom" ? "down" : "up",
538
+ 0,
539
+ axis.labelColor ?? AI.black,
540
+ false
541
+ ),
542
+ ];
543
+ })
544
+ );
365
545
 
366
- const yTicks = Axis.getTicks(yNumTicks, axis);
367
- if (chart.yGrid) {
368
- components.push(generateYAxisLines(xMin - 5, xMax, yMin, yMax, yTicks, axis, chart.yGrid));
369
- }
370
- components.push(
371
- AI.createLine({ x: xMin, y: yMin }, { x: xMin, y: yMax }, axis.axisColor ?? AI.gray, axis.thickness ?? 1),
372
- generateYAxisLabels(xMin - (axis.tickLabelDisp ?? 7), yMin, yMax, "left", yTicks, axis, chart)
373
- );
546
+ components.push(
547
+ AI.createLine({ x: xMin, y: lineY }, { x: xMax, y: lineY }, axis.axisColor ?? AI.gray, axis.thickness ?? 1)
548
+ );
374
549
 
375
- switch (chart.labelLayout) {
376
- case "original":
377
- components.push(
378
- generateYAxisLabel(axisLabelPosX, yMax + 0.5 * chart.padding.bottom, "uniform", "up", axis, chart)
379
- );
380
- break;
381
- case "end":
382
- components.push(generateYAxisLabel(axisLabelPosX, yMax, "left", "up", axis, chart));
383
- break;
384
- case "center":
385
- components.push(generateYAxisLabel(axisLabelPosX, (yMin + yMax) / 2, "uniform", "up", axis, chart));
386
- break;
387
- default:
388
- return exhaustiveCheck(chart.labelLayout);
550
+ const axisLabelPosY = lineY + dirFactor * chart.axisWidth * axisLabelPosFactor;
551
+ switch (chart.labelLayout) {
552
+ case "original":
553
+ components.push(
554
+ generateXAxisLabel(
555
+ xMax + chart.padding.right,
556
+ yMin + (axis.tickLabelDisp ?? 10),
557
+ "uniform",
558
+ "up",
559
+ linear,
560
+ chart
561
+ )
562
+ );
563
+ break;
564
+ case "end":
565
+ components.push(generateXAxisLabel(xMax, axisLabelPosY, "left", "uniform", linear, chart));
566
+ break;
567
+ case "center":
568
+ components.push(generateXAxisLabel((xMin + xMax) / 2, axisLabelPosY, "uniform", "uniform", linear, chart));
569
+ break;
570
+ default:
571
+ return exhaustiveCheck(chart.labelLayout);
572
+ }
573
+ lineY += dirFactor * chart.axisWidth;
389
574
  }
390
-
391
- return AI.createGroup("YAxisLeft", components);
575
+ return AI.createGroup(xAxis + "XDataAxis", components);
392
576
  }
393
577
 
394
- export function generateYAxisRight(
395
- yNumTicks: number,
396
- axis: Axis.Axis | undefined,
578
+ export function generateDataAxisesY(
579
+ yAxis: YAxis,
580
+ axises: Array<ChartDataAxis>,
581
+ numTicks: number,
582
+ xMin: number,
397
583
  xMax: number,
398
584
  yMin: number,
399
585
  yMax: number,
400
586
  chart: Chart
401
587
  ): AI.Component {
402
588
  const components = Array<AI.Component>();
403
- if (!axis) {
404
- return AI.createGroup("YAxisRight", components);
405
- }
406
- const axisLabelPosX = xMax + chart.padding.right - (axis.axisFontSize ?? chart.fontSize);
407
-
408
- const yTicks = Axis.getTicks(yNumTicks, axis);
409
- if (chart.yGrid) {
410
- components.push(generateYAxisLines(xMax - 5, xMax + 5, yMin, yMax, yTicks, axis, chart.yGrid));
411
- }
412
- components.push(
413
- AI.createLine({ x: xMax, y: yMin }, { x: xMax, y: yMax }, axis.axisColor ?? AI.gray, axis.thickness ?? 1),
414
- generateYAxisLabels(xMax + (axis.tickLabelDisp ?? 7), yMin, yMax, "right", yTicks, axis, chart)
415
- );
589
+ let lineX =
590
+ yAxis === "left"
591
+ ? xMin - chart.yAxisesLeft.length * chart.axisWidth
592
+ : xMax + chart.yAxisesRight.length * chart.axisWidth;
593
+ const dirFactor = yAxis === "left" ? -1 : 1;
594
+ for (const axis of axises) {
595
+ const min = Math.min(...axis.points.map((p) => p.y));
596
+ const max = Math.max(...axis.points.map((p) => p.y));
597
+ const linear = Axis.createLinearAxis(
598
+ min,
599
+ max,
600
+ axis.label,
601
+ axis.labelColor,
602
+ axis.labelRotation,
603
+ axis.tickLabelDisp,
604
+ axis.thickness,
605
+ axis.axisColor,
606
+ axis.id
607
+ );
608
+ const findX = (y: number): number => {
609
+ for (let i = 0; i < axis.points.length; ++i) {
610
+ const p0 = i > 0 ? axis.points[i - 1] : undefined;
611
+ const p1 = axis.points[i];
612
+ if (!p1) {
613
+ continue;
614
+ }
615
+ if (p0 && p0.y <= y && p1.y >= y) {
616
+ const k = (p1.x - p0.x) / (p1.y - p0.y);
617
+ const x = p0.x + k * (y - p0.y);
618
+ return x;
619
+ }
620
+ if (!p0 && p1.y >= y) {
621
+ return p1.x;
622
+ }
623
+ }
624
+ return axis.points[axis.points.length - 1]?.x ?? 0;
625
+ };
626
+ const yValues = Axis.getTicks(numTicks, linear).map((t) => t.value);
627
+ const lineX2 = lineX;
628
+ components.push(
629
+ ...yValues.flatMap((y) => {
630
+ const tickY = findX(y);
631
+ const yPx = Axis.transformValue(tickY, yMin, yMax, chart.yAxisesLeft[0]);
632
+ const start = AI.createPoint(lineX2, yPx);
633
+ const end = AI.createPoint(lineX2 + dirFactor * 10, yPx);
634
+ const textPos = AI.createPoint(lineX2 + dirFactor * 12, yPx);
635
+ return [
636
+ AI.createLine(start, end, chart.xGrid.color, chart.xGrid.thickness),
637
+ AI.createText(
638
+ textPos,
639
+ formatNumber(y),
640
+ chart.font,
641
+ axis.tickFontSize ?? chart.fontSize,
642
+ axis.labelColor ?? AI.black,
643
+ "normal",
644
+ 0,
645
+ "center",
646
+ yAxis,
647
+ "uniform",
648
+ 0,
649
+ axis.labelColor ?? AI.black,
650
+ false
651
+ ),
652
+ ];
653
+ })
654
+ );
416
655
 
417
- switch (chart.labelLayout) {
418
- case "original":
419
- components.push(
420
- generateYAxisLabel(axisLabelPosX, yMax + 0.5 * chart.padding.bottom, "uniform", "up", axis, chart)
421
- );
422
- break;
423
- case "end":
424
- components.push(generateYAxisLabel(axisLabelPosX, yMax, "left", "up", axis, chart));
425
- break;
426
- case "center":
427
- components.push(generateYAxisLabel(axisLabelPosX, (yMin + yMax) / 2, "uniform", "up", axis, chart));
428
- break;
429
- default:
430
- return exhaustiveCheck(chart.labelLayout);
656
+ components.push(
657
+ AI.createLine({ x: lineX, y: yMin }, { x: lineX, y: yMax }, axis.axisColor ?? AI.gray, axis.thickness ?? 1)
658
+ );
659
+ const rotation = yAxis === "left" ? -90 : 90;
660
+ const axisLabelPosX = lineX + dirFactor * chart.axisWidth * axisLabelPosFactor;
661
+ switch (chart.labelLayout) {
662
+ case "original":
663
+ components.push(
664
+ generateYAxisLabel(
665
+ xMax + chart.padding.right,
666
+ yMin + (axis.tickLabelDisp ?? 10),
667
+ rotation,
668
+ "uniform",
669
+ "up",
670
+ linear,
671
+ chart
672
+ )
673
+ );
674
+ break;
675
+ case "end":
676
+ components.push(generateYAxisLabel(axisLabelPosX, yMax, rotation, "left", "uniform", linear, chart));
677
+ break;
678
+ case "center":
679
+ components.push(
680
+ generateYAxisLabel(axisLabelPosX, (yMin + yMax) / 2, rotation, "uniform", "uniform", linear, chart)
681
+ );
682
+ break;
683
+ default:
684
+ return exhaustiveCheck(chart.labelLayout);
685
+ }
686
+ lineX += dirFactor * chart.axisWidth;
431
687
  }
432
-
433
- return AI.createGroup("YAxisRight", components);
688
+ return AI.createGroup(yAxis + "YDataAxis", components);
434
689
  }
435
690
 
436
691
  export function generateStack(xMin: number, xMax: number, yMin: number, yMax: number, chart: Chart): AI.Component {
@@ -462,8 +717,8 @@ function generateUnsignedStack(xMin: number, xMax: number, yMin: number, yMax: n
462
717
  return AI.createGroup("stack", []);
463
718
  }
464
719
 
465
- const xAxis = chart.chartStack.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
466
- const yAxis = chart.chartStack.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
720
+ const xAxis = chart.chartStack.xAxis === "top" ? chart.xAxisesTop[0] : chart.xAxisesBottom[0];
721
+ const yAxis = chart.chartStack.yAxis === "right" ? chart.yAxisesRight[0] : chart.yAxisesLeft[0];
467
722
 
468
723
  const xPoints = chart.chartStack.points.map((stackPoints) => {
469
724
  let sumY = 0;
@@ -506,8 +761,8 @@ export function generateLines(xMin: number, xMax: number, yMin: number, yMax: nu
506
761
  if (l.points.length < 2) {
507
762
  return AI.createGroup(l.label, []);
508
763
  }
509
- const xAxis = l.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
510
- const yAxis = l.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
764
+ const xAxis = l.xAxis === "top" ? chart.xAxisesTop[0] : chart.xAxisesBottom[0];
765
+ const yAxis = l.yAxis === "right" ? chart.yAxisesRight[0] : chart.yAxisesLeft[0];
511
766
  const points = l.points.map((p) => Axis.transformPoint(p, xMin, xMax, yMin, yMax, xAxis, yAxis));
512
767
  const segments = getLineSegmentsInsideChart(xMin, xMax, yMin, yMax, points);
513
768
  const components = [];
@@ -623,8 +878,8 @@ function lineLine(a0: AI.Point, a1: AI.Point, b0: AI.Point, b1: AI.Point): AI.Po
623
878
 
624
879
  export function generatePoints(xMin: number, xMax: number, yMin: number, yMax: number, chart: Chart): AI.Component {
625
880
  const points = chart.chartPoints.map((p) => {
626
- const xAxis = p.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
627
- const yAxis = p.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
881
+ const xAxis = p.xAxis === "top" ? chart.xAxisesTop[0] : chart.xAxisesBottom[0];
882
+ const yAxis = p.yAxis === "right" ? chart.yAxisesRight[0] : chart.yAxisesLeft[0];
628
883
  const position = Axis.transformPoint(p.position, xMin, xMax, yMin, yMax, xAxis, yAxis);
629
884
  const outlineColor = p.textOutlineColor ?? chart.textOutlineColor;
630
885
  const components = [
@@ -722,6 +977,18 @@ export function generateXAxisLabels(
722
977
  axis: Axis.Axis,
723
978
  chart: Chart
724
979
  ): AI.Component {
980
+ const rotation = axis.labelRotation ?? 0;
981
+
982
+ const horizontalGrowth: AI.GrowthDirection = (() => {
983
+ if (rotation === 0) {
984
+ return "uniform";
985
+ }
986
+ if (growVertical === "down") {
987
+ return rotation < 0 ? "left" : "right";
988
+ } else {
989
+ return rotation < 0 ? "right" : "left";
990
+ }
991
+ })();
725
992
  const xLabels = ticks.map((l) => {
726
993
  const position = AI.createPoint(Axis.transformValue(l.value, xMin, xMax, axis), y);
727
994
  return AI.createText(
@@ -731,9 +998,9 @@ export function generateXAxisLabels(
731
998
  axis.tickFontSize ?? chart.fontSize,
732
999
  axis.labelColor ?? AI.black,
733
1000
  "normal",
734
- axis.labelRotation ?? 0,
1001
+ rotation,
735
1002
  "center",
736
- "uniform",
1003
+ horizontalGrowth,
737
1004
  growVertical,
738
1005
  0,
739
1006
  axis.labelColor ?? AI.black,
@@ -797,6 +1064,18 @@ export function generateYAxisLabels(
797
1064
  yAxis: Axis.Axis,
798
1065
  chart: Chart
799
1066
  ): AI.Component {
1067
+ const rotation = yAxis.labelRotation ?? 0;
1068
+ const growVertical: AI.GrowthDirection = (() => {
1069
+ if (rotation === 0) {
1070
+ return "uniform";
1071
+ }
1072
+ if (growHorizontal === "left") {
1073
+ return rotation < 0 ? "up" : "down";
1074
+ } else {
1075
+ return rotation < 0 ? "down" : "up";
1076
+ }
1077
+ })();
1078
+
800
1079
  const yLabels = yTicks.map((l) => {
801
1080
  const position = AI.createPoint(x, Axis.transformValue(l.value, yMin, yMax, yAxis));
802
1081
  return AI.createText(
@@ -806,10 +1085,10 @@ export function generateYAxisLabels(
806
1085
  yAxis.tickFontSize ?? chart.fontSize,
807
1086
  yAxis.labelColor ?? AI.black,
808
1087
  "normal",
809
- yAxis.labelRotation ?? 0,
1088
+ rotation,
810
1089
  "center",
811
1090
  growHorizontal,
812
- "uniform",
1091
+ growVertical,
813
1092
  0,
814
1093
  yAxis.labelColor ?? AI.black,
815
1094
  false
@@ -821,6 +1100,7 @@ export function generateYAxisLabels(
821
1100
  export function generateYAxisLabel(
822
1101
  x: number,
823
1102
  y: number,
1103
+ rotation: number,
824
1104
  horizontalGrowthDirection: AI.GrowthDirection,
825
1105
  verticalGrowthDirection: AI.GrowthDirection,
826
1106
  axis: Axis.Axis,
@@ -834,7 +1114,7 @@ export function generateYAxisLabel(
834
1114
  axis.axisFontSize ?? chart.fontSize,
835
1115
  axis.labelColor ?? AI.black,
836
1116
  "normal",
837
- -90,
1117
+ rotation,
838
1118
  "center",
839
1119
  horizontalGrowthDirection,
840
1120
  verticalGrowthDirection,