@svgrid/grid 3.0.2 → 3.0.4
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 +2 -1
- package/dist/GridMenus.svelte +44 -2
- package/dist/SvChartMenu.svelte +69 -0
- package/dist/SvChartMenu.svelte.d.ts +11 -0
- package/dist/SvChartPanes.svelte +113 -0
- package/dist/SvChartPanes.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.d.ts +110 -39
- package/dist/SvGrid.controller.svelte.js +655 -200
- package/dist/SvGrid.css +105 -2
- package/dist/SvGrid.svelte +386 -514
- package/dist/SvGrid.types.d.ts +210 -9
- package/dist/SvGridCellEditor.svelte +17 -0
- package/dist/SvGridChart.svelte +2257 -329
- package/dist/SvGridChart.svelte.d.ts +6 -106
- package/dist/SvGridChart.types.d.ts +338 -0
- package/dist/SvGridChart.types.js +1 -0
- package/dist/SvGridChartBuilder.svelte +323 -0
- package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
- package/dist/SvGridChartPanel.svelte +221 -275
- package/dist/SvGridChartPickers.svelte +417 -0
- package/dist/SvGridChartPickers.svelte.d.ts +70 -0
- package/dist/SvGridChartView.svelte +13 -14
- package/dist/SvModal.svelte +7 -1
- package/dist/SvModal.svelte.d.ts +5 -0
- package/dist/ai.d.ts +40 -8
- package/dist/ai.js +135 -24
- package/dist/aria.d.ts +11 -0
- package/dist/build-api.js +108 -22
- package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
- package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
- package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
- package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
- package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
- package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
- package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
- package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
- package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
- package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
- package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
- package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
- package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
- package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
- package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
- package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
- package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
- package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
- package/dist/cdn/SvModal-CIZWCcad.js +397 -0
- package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
- package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
- package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
- package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
- package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
- package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
- package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
- package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
- package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
- package/dist/cdn/dismissable-DAHetSNk.js +44 -0
- package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
- package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
- package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
- package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
- package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
- package/dist/cdn/svgrid.js +26 -15
- package/dist/cdn/svgrid.svelte-external.js +26 -15
- package/dist/chart-axes.d.ts +53 -0
- package/dist/chart-axes.js +351 -0
- package/dist/chart-cartesian.d.ts +88 -0
- package/dist/chart-cartesian.js +1862 -0
- package/dist/chart-decimate.d.ts +51 -0
- package/dist/chart-decimate.js +199 -0
- package/dist/chart-export-pdf.d.ts +47 -0
- package/dist/chart-export-pdf.js +187 -0
- package/dist/chart-export.d.ts +2 -1
- package/dist/chart-export.js +18 -3
- package/dist/chart-financial.d.ts +91 -0
- package/dist/chart-financial.js +175 -0
- package/dist/chart-flow.d.ts +10 -0
- package/dist/chart-flow.js +298 -0
- package/dist/chart-format.d.ts +18 -0
- package/dist/chart-format.js +65 -0
- package/dist/chart-grid.d.ts +8 -0
- package/dist/chart-grid.js +190 -0
- package/dist/chart-hierarchy.d.ts +17 -0
- package/dist/chart-hierarchy.js +146 -0
- package/dist/chart-indicators.d.ts +115 -0
- package/dist/chart-indicators.js +389 -0
- package/dist/chart-messages.d.ts +95 -0
- package/dist/chart-messages.js +92 -0
- package/dist/chart-motion.d.ts +16 -0
- package/dist/chart-motion.js +141 -0
- package/dist/chart-panel-messages.d.ts +265 -0
- package/dist/chart-panel-messages.js +285 -0
- package/dist/chart-pivot.d.ts +87 -0
- package/dist/chart-pivot.js +156 -0
- package/dist/chart-polar.d.ts +25 -0
- package/dist/chart-polar.js +700 -0
- package/dist/chart-samples.d.ts +16 -0
- package/dist/chart-samples.js +153 -0
- package/dist/chart-scale.d.ts +113 -0
- package/dist/chart-scale.js +405 -0
- package/dist/chart-stats.d.ts +111 -0
- package/dist/chart-stats.js +419 -0
- package/dist/chart-stream.d.ts +45 -0
- package/dist/chart-stream.js +91 -0
- package/dist/chart-summary.d.ts +27 -0
- package/dist/chart-summary.js +205 -0
- package/dist/chart-sync.svelte.d.ts +32 -0
- package/dist/chart-sync.svelte.js +23 -0
- package/dist/chart-table.d.ts +35 -0
- package/dist/chart-table.js +165 -0
- package/dist/chart-trend.d.ts +19 -0
- package/dist/chart-trend.js +61 -0
- package/dist/chart-types.d.ts +1456 -0
- package/dist/chart-types.js +6 -0
- package/dist/chart-validate.d.ts +43 -0
- package/dist/chart-validate.js +272 -0
- package/dist/chart-zoom.d.ts +40 -0
- package/dist/chart-zoom.js +145 -0
- package/dist/chart.d.ts +186 -865
- package/dist/chart.js +460 -2204
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +137 -18
- package/dist/column-resize.d.ts +3 -0
- package/dist/column-resize.js +4 -1
- package/dist/columns.js +3 -0
- package/dist/command-context.d.ts +19 -0
- package/dist/command-context.js +116 -0
- package/dist/core.d.ts +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.d.ts +2 -8
- package/dist/editing.js +262 -33
- package/dist/fill-patterns.d.ts +0 -5
- package/dist/fill-patterns.js +56 -2
- package/dist/grid-messages.d.ts +10 -3
- package/dist/grid-messages.js +5 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.js +20 -2
- package/dist/keyboard-handlers.js +88 -31
- package/dist/keyboard.d.ts +55 -0
- package/dist/keyboard.js +132 -0
- package/dist/menus.d.ts +3 -1
- package/dist/menus.js +7 -2
- package/dist/merges.d.ts +97 -0
- package/dist/merges.js +147 -0
- package/dist/row-resize.d.ts +3 -0
- package/dist/row-resize.js +14 -0
- package/dist/scroll-sync.d.ts +1 -0
- package/dist/scroll-sync.js +24 -0
- package/dist/selection.d.ts +16 -0
- package/dist/selection.js +108 -16
- package/dist/shortcut-registry.d.ts +145 -0
- package/dist/shortcut-registry.js +44 -0
- package/dist/svgrid-wrapper.types.d.ts +213 -22
- package/dist/virtualization/virtualizer.js +38 -17
- package/package.json +6 -1
- package/src/GridMenus.svelte +44 -2
- package/src/SvChartMenu.svelte +69 -0
- package/src/SvChartPanes.svelte +113 -0
- package/src/SvChartPanes.test.ts +66 -0
- package/src/SvGrid.controller.svelte.ts +629 -172
- package/src/SvGrid.css +105 -2
- package/src/SvGrid.svelte +386 -514
- package/src/SvGrid.types.ts +188 -8
- package/src/SvGridCellEditor.svelte +17 -0
- package/src/SvGridChart.svelte +2257 -329
- package/src/SvGridChart.test.ts +1419 -1
- package/src/SvGridChart.types.ts +336 -0
- package/src/SvGridChartBuilder.svelte +323 -0
- package/src/SvGridChartPanel.svelte +221 -275
- package/src/SvGridChartPickers.svelte +417 -0
- package/src/SvGridChartView.svelte +13 -14
- package/src/SvModal.svelte +7 -1
- package/src/SvModal.test.ts +9 -0
- package/src/ai.test.ts +57 -2
- package/src/ai.ts +166 -22
- package/src/aria.d.ts +11 -0
- package/src/build-api.coverage.test.ts +184 -0
- package/src/build-api.ts +107 -18
- package/src/chart-axes.test.ts +573 -0
- package/src/chart-axes.ts +342 -0
- package/src/chart-cartesian.ts +1863 -0
- package/src/chart-decimate.test.ts +249 -0
- package/src/chart-decimate.ts +186 -0
- package/src/chart-export-pdf.test.ts +91 -0
- package/src/chart-export-pdf.ts +207 -0
- package/src/chart-export.test.ts +14 -0
- package/src/chart-export.ts +13 -3
- package/src/chart-financial.ts +208 -0
- package/src/chart-flow.ts +286 -0
- package/src/chart-format.ts +67 -0
- package/src/chart-grid.ts +179 -0
- package/src/chart-hierarchy.ts +135 -0
- package/src/chart-indicators.test.ts +443 -0
- package/src/chart-indicators.ts +384 -0
- package/src/chart-messages.test.ts +21 -0
- package/src/chart-messages.ts +184 -0
- package/src/chart-motion.test.ts +71 -0
- package/src/chart-motion.ts +148 -0
- package/src/chart-panel-messages.test.ts +29 -0
- package/src/chart-panel-messages.ts +550 -0
- package/src/chart-pivot.test.ts +54 -0
- package/src/chart-pivot.ts +225 -0
- package/src/chart-polar.ts +674 -0
- package/src/chart-samples.ts +106 -0
- package/src/chart-scale.ts +394 -0
- package/src/chart-series-types.test.ts +714 -0
- package/src/chart-stats.ts +409 -0
- package/src/chart-stream.test.ts +75 -0
- package/src/chart-stream.ts +114 -0
- package/src/chart-summary.test.ts +90 -0
- package/src/chart-summary.ts +197 -0
- package/src/chart-sync.svelte.ts +41 -0
- package/src/chart-table.test.ts +76 -0
- package/src/chart-table.ts +178 -0
- package/src/chart-trend.ts +47 -0
- package/src/chart-types.ts +1309 -0
- package/src/chart-validate.test.ts +100 -0
- package/src/chart-validate.ts +247 -0
- package/src/chart-zoom.test.ts +137 -0
- package/src/chart-zoom.ts +124 -0
- package/src/chart.coverage.test.ts +81 -0
- package/src/chart.test.ts +56 -3
- package/src/chart.ts +557 -2784
- package/src/clipboard.test.ts +151 -0
- package/src/clipboard.ts +145 -16
- package/src/column-resize.ts +7 -1
- package/src/columns.ts +2 -0
- package/src/command-context.test.ts +200 -0
- package/src/command-context.ts +137 -0
- package/src/core.ts +7 -0
- package/src/editing.test.ts +281 -3
- package/src/editing.ts +256 -38
- package/src/fill-patterns.test.ts +35 -0
- package/src/fill-patterns.ts +56 -2
- package/src/grid-messages.ts +16 -3
- package/src/history.test.ts +196 -0
- package/src/history.ts +162 -0
- package/src/icon-seam.test.ts +3 -2
- package/src/index.ts +150 -2
- package/src/keyboard-handlers.coverage.test.ts +126 -6
- package/src/keyboard-handlers.ts +91 -30
- package/src/keyboard-shortcuts.seam.test.ts +200 -0
- package/src/keyboard.test.ts +124 -1
- package/src/keyboard.ts +142 -0
- package/src/menus.test.ts +26 -0
- package/src/menus.ts +10 -3
- package/src/merges.test.ts +112 -0
- package/src/merges.ts +168 -0
- package/src/pivot.grid.test.ts +40 -5
- package/src/row-resize.test.ts +44 -0
- package/src/row-resize.ts +17 -0
- package/src/scroll-sync.test.ts +42 -1
- package/src/scroll-sync.ts +25 -0
- package/src/selection.multi-range.test.ts +34 -0
- package/src/selection.test.ts +30 -2
- package/src/selection.ts +111 -19
- package/src/shortcut-registry.test.ts +97 -0
- package/src/shortcut-registry.ts +181 -0
- package/src/svgrid-wrapper.types.ts +184 -13
- package/src/svgrid.behavior.test.ts +135 -0
- package/src/svgrid.charting.test.ts +513 -3
- package/src/svgrid.context-menu.test.ts +31 -0
- package/src/svgrid.new-features.wrapper.test.ts +10 -3
- package/src/virtualization/virtualizer.ts +39 -17
- package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
- package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
- package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
- package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
- package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
- package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
- package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
- package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
- package/dist/cdn/chart-CKJRCPOk.js +0 -1652
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF and print for a rendered chart, with no PDF library.
|
|
3
|
+
*
|
|
4
|
+
* A one-page PDF is a short text file with one binary blob in it, so the
|
|
5
|
+
* writer here is the whole format we need: a catalog, a page, a Helvetica
|
|
6
|
+
* font, one image XObject and a content stream that places the text and the
|
|
7
|
+
* image. The chart is rasterised through the existing PNG path and re-encoded
|
|
8
|
+
* as JPEG, which the PDF embeds as-is under `/DCTDecode`; nothing needs zlib.
|
|
9
|
+
* JPEG has no alpha, so the chart background is always painted.
|
|
10
|
+
*
|
|
11
|
+
* Print opens a window with the standalone SVG and calls `print()`.
|
|
12
|
+
*/
|
|
13
|
+
import { chartToPngBlob, chartToSvgString, type ChartExportOptions } from './chart-export'
|
|
14
|
+
|
|
15
|
+
/** Options for the PDF: page, orientation, the text around the chart. */
|
|
16
|
+
export type ChartPdfOptions = ChartExportOptions & {
|
|
17
|
+
/** Paper size. Default A4. */
|
|
18
|
+
page?: 'A4' | 'Letter'
|
|
19
|
+
/** Default: whichever fits the chart's aspect ratio better. */
|
|
20
|
+
orientation?: 'portrait' | 'landscape'
|
|
21
|
+
title?: string
|
|
22
|
+
subtitle?: string
|
|
23
|
+
caption?: string
|
|
24
|
+
/** JPEG quality, 0..1. Default 0.92. */
|
|
25
|
+
quality?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PAGES: Record<'A4' | 'Letter', [number, number]> = { A4: [595.28, 841.89], Letter: [612, 792] }
|
|
29
|
+
|
|
30
|
+
/** Latin-1 bytes of a string; anything outside it becomes `?`. */
|
|
31
|
+
function latin1(s: string): Uint8Array {
|
|
32
|
+
const out = new Uint8Array(s.length)
|
|
33
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
34
|
+
const c = s.charCodeAt(i)
|
|
35
|
+
out[i] = c < 256 ? c : 63
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A PDF string literal: parentheses and backslashes escaped. */
|
|
41
|
+
function pdfText(s: string): string {
|
|
42
|
+
return '(' + s.replace(/[\\()]/g, (m) => '\\' + m).replace(/[\r\n]+/g, ' ') + ')'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Roughly how wide Helvetica text is at a size, for fitting a line. */
|
|
46
|
+
function textWidth(s: string, size: number): number {
|
|
47
|
+
return s.length * size * 0.5
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Re-encode a PNG blob as JPEG on a painted background. */
|
|
51
|
+
function pngToJpeg(png: Blob, background: string, quality: number): Promise<{ bytes: Uint8Array; width: number; height: number }> {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const url = URL.createObjectURL(png)
|
|
54
|
+
const img = new Image()
|
|
55
|
+
img.onload = () => {
|
|
56
|
+
const canvas = document.createElement('canvas')
|
|
57
|
+
canvas.width = img.naturalWidth || img.width
|
|
58
|
+
canvas.height = img.naturalHeight || img.height
|
|
59
|
+
const ctx = canvas.getContext('2d')
|
|
60
|
+
if (!ctx) { URL.revokeObjectURL(url); reject(new Error('canvas 2d context unavailable')); return }
|
|
61
|
+
ctx.fillStyle = background
|
|
62
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
63
|
+
ctx.drawImage(img, 0, 0)
|
|
64
|
+
URL.revokeObjectURL(url)
|
|
65
|
+
canvas.toBlob(
|
|
66
|
+
(blob) => {
|
|
67
|
+
if (!blob) { reject(new Error('toBlob failed')); return }
|
|
68
|
+
blob.arrayBuffer().then((buf) => resolve({ bytes: new Uint8Array(buf), width: canvas.width, height: canvas.height }), reject)
|
|
69
|
+
},
|
|
70
|
+
'image/jpeg',
|
|
71
|
+
quality,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('failed to decode chart PNG')) }
|
|
75
|
+
img.src = url
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Assemble a one-page PDF from a JPEG and the text around it. Exported for
|
|
81
|
+
* tests and for callers that already have a JPEG; `chartToPdfBlob` is the
|
|
82
|
+
* one that starts from a chart element.
|
|
83
|
+
*/
|
|
84
|
+
export function buildChartPdf(
|
|
85
|
+
jpeg: { bytes: Uint8Array; width: number; height: number },
|
|
86
|
+
opts: Pick<ChartPdfOptions, 'page' | 'orientation' | 'title' | 'subtitle' | 'caption'> = {},
|
|
87
|
+
): Uint8Array {
|
|
88
|
+
const [pw0, ph0] = PAGES[opts.page ?? 'A4']
|
|
89
|
+
const landscape = opts.orientation ? opts.orientation === 'landscape' : jpeg.width / jpeg.height > pw0 / ph0
|
|
90
|
+
const pw = landscape ? ph0 : pw0
|
|
91
|
+
const ph = landscape ? pw0 : ph0
|
|
92
|
+
const margin = 40
|
|
93
|
+
const title = (opts.title ?? '').trim()
|
|
94
|
+
const subtitle = (opts.subtitle ?? '').trim()
|
|
95
|
+
const caption = (opts.caption ?? '').trim()
|
|
96
|
+
|
|
97
|
+
// Text block at the top, image below it, caption at the foot.
|
|
98
|
+
let y = ph - margin
|
|
99
|
+
const content: string[] = []
|
|
100
|
+
const line = (text: string, size: number, gray: number) => {
|
|
101
|
+
y -= size + 4
|
|
102
|
+
content.push(`BT /F1 ${size} Tf ${gray} g ${margin} ${y.toFixed(2)} Td ${pdfText(text)} Tj ET`)
|
|
103
|
+
}
|
|
104
|
+
if (title) line(title, 16, 0)
|
|
105
|
+
if (subtitle) line(subtitle, 11, 0.4)
|
|
106
|
+
if (title || subtitle) y -= 8
|
|
107
|
+
const footer = caption ? 20 : 0
|
|
108
|
+
const availW = pw - margin * 2
|
|
109
|
+
const availH = y - margin - footer
|
|
110
|
+
const scale = Math.min(availW / jpeg.width, availH / jpeg.height)
|
|
111
|
+
const iw = jpeg.width * scale
|
|
112
|
+
const ih = jpeg.height * scale
|
|
113
|
+
const ix = margin + (availW - iw) / 2
|
|
114
|
+
const iy = y - ih
|
|
115
|
+
content.push(`q ${iw.toFixed(2)} 0 0 ${ih.toFixed(2)} ${ix.toFixed(2)} ${iy.toFixed(2)} cm /Im1 Do Q`)
|
|
116
|
+
if (caption) {
|
|
117
|
+
const size = 9
|
|
118
|
+
const text = textWidth(caption, size) > availW ? caption.slice(0, Math.floor(availW / (size * 0.5)) - 3) + '...' : caption
|
|
119
|
+
content.push(`BT /F1 ${size} Tf 0.4 g ${margin} ${(margin - 4).toFixed(2)} Td ${pdfText(text)} Tj ET`)
|
|
120
|
+
}
|
|
121
|
+
const stream = content.join('\n')
|
|
122
|
+
|
|
123
|
+
const objects: Array<Uint8Array | string> = []
|
|
124
|
+
const offsets: number[] = []
|
|
125
|
+
let length = 0
|
|
126
|
+
const push = (part: Uint8Array | string) => {
|
|
127
|
+
objects.push(part)
|
|
128
|
+
length += typeof part === 'string' ? part.length : part.length
|
|
129
|
+
}
|
|
130
|
+
push('%PDF-1.4\n%âãÏÓ\n')
|
|
131
|
+
const obj = (n: number, body: string, bin?: Uint8Array) => {
|
|
132
|
+
offsets[n] = length
|
|
133
|
+
push(`${n} 0 obj\n${body}\n`)
|
|
134
|
+
if (bin) {
|
|
135
|
+
push('stream\n')
|
|
136
|
+
push(bin)
|
|
137
|
+
push('\nendstream\n')
|
|
138
|
+
}
|
|
139
|
+
push('endobj\n')
|
|
140
|
+
}
|
|
141
|
+
obj(1, '<< /Type /Catalog /Pages 2 0 R >>')
|
|
142
|
+
obj(2, '<< /Type /Pages /Kids [3 0 R] /Count 1 >>')
|
|
143
|
+
obj(3, `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pw.toFixed(2)} ${ph.toFixed(2)}] /Resources << /XObject << /Im1 4 0 R >> /Font << /F1 5 0 R >> >> /Contents 6 0 R >>`)
|
|
144
|
+
obj(4, `<< /Type /XObject /Subtype /Image /Width ${jpeg.width} /Height ${jpeg.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${jpeg.bytes.length} >>`, jpeg.bytes)
|
|
145
|
+
obj(5, '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>')
|
|
146
|
+
obj(6, `<< /Length ${stream.length} >>`, latin1(stream))
|
|
147
|
+
const xref = length
|
|
148
|
+
push(`xref\n0 7\n0000000000 65535 f \n`)
|
|
149
|
+
for (let n = 1; n <= 6; n += 1) push(`${String(offsets[n]).padStart(10, '0')} 00000 n \n`)
|
|
150
|
+
push(`trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`)
|
|
151
|
+
|
|
152
|
+
const out = new Uint8Array(new ArrayBuffer(length))
|
|
153
|
+
let at = 0
|
|
154
|
+
for (const part of objects) {
|
|
155
|
+
const bytes = typeof part === 'string' ? latin1(part) : part
|
|
156
|
+
out.set(bytes, at)
|
|
157
|
+
at += bytes.length
|
|
158
|
+
}
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The chart as a one-page PDF `Blob`. */
|
|
163
|
+
export async function chartToPdfBlob(source: SVGSVGElement | HTMLElement, options: ChartPdfOptions = {}): Promise<Blob> {
|
|
164
|
+
const svg = source instanceof SVGSVGElement ? source : (source.querySelector('svg.sv-grid-chart-svg') ?? source.querySelector('svg'))
|
|
165
|
+
const background = options.background ?? (svg ? getComputedStyle(svg).getPropertyValue('--sg-bg').trim() || '#ffffff' : '#ffffff')
|
|
166
|
+
const png = await chartToPngBlob(source, { ...options, background, scale: options.scale ?? 2 })
|
|
167
|
+
const jpeg = await pngToJpeg(png, background, options.quality ?? 0.92)
|
|
168
|
+
const bytes = buildChartPdf(jpeg, options)
|
|
169
|
+
return new Blob([bytes.buffer as ArrayBuffer], { type: 'application/pdf' })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Download the chart as a `.pdf` file. */
|
|
173
|
+
export async function downloadChartPdf(source: SVGSVGElement | HTMLElement, filename = 'chart.pdf', options: ChartPdfOptions = {}): Promise<void> {
|
|
174
|
+
const blob = await chartToPdfBlob(source, options)
|
|
175
|
+
const url = URL.createObjectURL(blob)
|
|
176
|
+
const a = document.createElement('a')
|
|
177
|
+
a.href = url
|
|
178
|
+
a.download = filename
|
|
179
|
+
document.body.appendChild(a)
|
|
180
|
+
a.click()
|
|
181
|
+
a.remove()
|
|
182
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Print the chart: a new window with the standalone SVG (and the title
|
|
187
|
+
* above it), sized to the page, that opens the print dialog once it has
|
|
188
|
+
* drawn. Returns false when the window was blocked.
|
|
189
|
+
*/
|
|
190
|
+
export function printChart(source: SVGSVGElement | HTMLElement, options: ChartExportOptions & { title?: string } = {}): boolean {
|
|
191
|
+
const svg = chartToSvgString(source, options)
|
|
192
|
+
const win = window.open('', '_blank', 'width=900,height=700')
|
|
193
|
+
if (!win) return false
|
|
194
|
+
const esc = (s: string) => s.replace(/[&<>]/g, (m) => ({ '&': '&', '<': '<', '>': '>' })[m]!)
|
|
195
|
+
win.document.write(
|
|
196
|
+
`<!doctype html><html><head><meta charset="utf-8"><title>${esc(options.title ?? 'Chart')}</title>` +
|
|
197
|
+
`<style>body{margin:24px;font-family:system-ui,sans-serif}h1{font-size:16px;margin:0 0 12px}svg{max-width:100%;height:auto}@page{margin:12mm}</style></head><body>` +
|
|
198
|
+
(options.title ? `<h1>${esc(options.title)}</h1>` : '') +
|
|
199
|
+
svg +
|
|
200
|
+
`</body></html>`,
|
|
201
|
+
)
|
|
202
|
+
win.document.close()
|
|
203
|
+
const go = () => { win.focus(); win.print() }
|
|
204
|
+
if (win.document.readyState === 'complete') setTimeout(go, 50)
|
|
205
|
+
else win.addEventListener('load', () => setTimeout(go, 50))
|
|
206
|
+
return true
|
|
207
|
+
}
|
package/src/chart-export.test.ts
CHANGED
|
@@ -372,6 +372,20 @@ describe('downloadChartPng', () => {
|
|
|
372
372
|
})
|
|
373
373
|
})
|
|
374
374
|
|
|
375
|
+
describe('chartSpecToCsv: gaps', () => {
|
|
376
|
+
it('writes a NaN or null value as an empty cell, not the word NaN', () => {
|
|
377
|
+
const csv = chartSpecToCsv({
|
|
378
|
+
type: 'line',
|
|
379
|
+
categories: ['Jan', 'Feb', 'Mar'],
|
|
380
|
+
series: [
|
|
381
|
+
{ label: 'Actual', values: [10, Number.NaN, 12] },
|
|
382
|
+
{ label: 'Forecast', values: [null as unknown as number, 11, Number.NaN] },
|
|
383
|
+
],
|
|
384
|
+
})
|
|
385
|
+
expect(csv.split('\n')).toEqual(['Category,Actual,Forecast', 'Jan,10,', 'Feb,,11', 'Mar,12,'])
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
375
389
|
describe('chartSpecToCsv: series that carry more than one number', () => {
|
|
376
390
|
it('keeps a plain series byte-identical', () => {
|
|
377
391
|
const csv = chartSpecToCsv({
|
package/src/chart-export.ts
CHANGED
|
@@ -170,7 +170,9 @@ export async function downloadChartPng(
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
const csvCell = (v: unknown): string => {
|
|
173
|
-
|
|
173
|
+
// A gap is an empty cell: `null` already was, and a NaN gap (the way a
|
|
174
|
+
// forecast series marks the months before it starts) wrote the word "NaN".
|
|
175
|
+
const s = v == null || (typeof v === 'number' && !Number.isFinite(v)) ? '' : String(v)
|
|
174
176
|
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -180,7 +182,8 @@ const csvCell = (v: unknown): string => {
|
|
|
180
182
|
*
|
|
181
183
|
* A series that carries more than one number per category widens instead of
|
|
182
184
|
* losing them. `ohlc` becomes four columns, `boxes` five plus its outliers,
|
|
183
|
-
* `errors` two
|
|
185
|
+
* `errors` two, `lowValues` a low / high pair, `targets` a value / target
|
|
186
|
+
* pair. This used to write `values` and nothing else, which quietly
|
|
184
187
|
* made "Export CSV" on a candlestick chart hand back the closing prices only -
|
|
185
188
|
* the screen-reader table was given the four real numbers on purpose and the
|
|
186
189
|
* export, which is what people actually take away, was not.
|
|
@@ -213,9 +216,11 @@ export function chartSpecToCsv(spec: ChartSpec): string {
|
|
|
213
216
|
// bare label, so an existing export is byte-identical.
|
|
214
217
|
const header: string[] = ['Category']
|
|
215
218
|
for (const s of series) {
|
|
216
|
-
if (s.ohlc) header.push(`${s.label} Open`, `${s.label} High`, `${s.label} Low`, `${s.label} Close`)
|
|
219
|
+
if (s.ohlc) header.push(`${s.label} Open`, `${s.label} High`, `${s.label} Low`, `${s.label} Close`, ...(s.volumes ? [`${s.label} Volume`] : []))
|
|
217
220
|
else if (s.boxes) header.push(`${s.label} Min`, `${s.label} Q1`, `${s.label} Median`, `${s.label} Q3`, `${s.label} Max`, `${s.label} Outliers`)
|
|
218
221
|
else if (s.errors) header.push(s.label, `${s.label} Low`, `${s.label} High`)
|
|
222
|
+
else if (s.lowValues) header.push(`${s.label} low`, `${s.label} high`)
|
|
223
|
+
else if (s.targets) header.push(s.label, `${s.label} target`)
|
|
219
224
|
else header.push(s.label)
|
|
220
225
|
}
|
|
221
226
|
|
|
@@ -225,6 +230,7 @@ export function chartSpecToCsv(spec: ChartSpec): string {
|
|
|
225
230
|
if (s.ohlc) {
|
|
226
231
|
const k = s.ohlc[i]
|
|
227
232
|
cells.push(k?.o ?? '', k?.h ?? '', k?.l ?? '', k?.c ?? '')
|
|
233
|
+
if (s.volumes) cells.push(s.volumes[i] ?? '')
|
|
228
234
|
} else if (s.boxes) {
|
|
229
235
|
const b = s.boxes[i]
|
|
230
236
|
// Outliers are a list inside one cell; csvCell quotes it.
|
|
@@ -235,6 +241,10 @@ export function chartSpecToCsv(spec: ChartSpec): string {
|
|
|
235
241
|
const lo = e == null ? '' : typeof e === 'number' ? (v ?? 0) - Math.abs(e) : Math.min(e.lo, e.hi)
|
|
236
242
|
const hi = e == null ? '' : typeof e === 'number' ? (v ?? 0) + Math.abs(e) : Math.max(e.lo, e.hi)
|
|
237
243
|
cells.push(v ?? '', lo, hi)
|
|
244
|
+
} else if (s.lowValues) {
|
|
245
|
+
cells.push(s.lowValues[i] ?? '', s.values[i] ?? '')
|
|
246
|
+
} else if (s.targets) {
|
|
247
|
+
cells.push(s.values[i] ?? '', s.targets[i] ?? '')
|
|
238
248
|
} else {
|
|
239
249
|
cells.push(s.values[i] ?? '')
|
|
240
250
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price-chart helpers: candle transforms, calendar resampling and the
|
|
3
|
+
* row-to-candlestick adapter. Indicators live in chart-indicators. DOM-free.
|
|
4
|
+
*/
|
|
5
|
+
import type { ChartSeries, ChartSpec, OhlcBar, SeriesOverlay } from './chart-types'
|
|
6
|
+
import { bucketStart, type ChartTimeBucket } from './chart-stats'
|
|
7
|
+
import type { ChartIndicatorSpec } from './chart-indicators'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Heikin-Ashi ("average bar") candles from ordinary OHLC bars. Each close is
|
|
11
|
+
* the mean of the four prices, each open the midpoint of the previous
|
|
12
|
+
* Heikin-Ashi body, and the high / low stretch to include both. Runs of one
|
|
13
|
+
* colour then read as trends, at the cost of the true open and close.
|
|
14
|
+
* `null` bars (gaps) stay null and reset the chain.
|
|
15
|
+
*/
|
|
16
|
+
export function heikinAshi(bars: ReadonlyArray<OhlcBar | null>): Array<OhlcBar | null> {
|
|
17
|
+
const out: Array<OhlcBar | null> = new Array(bars.length)
|
|
18
|
+
let prev: OhlcBar | null = null
|
|
19
|
+
for (let i = 0; i < bars.length; i += 1) {
|
|
20
|
+
const b = bars[i]
|
|
21
|
+
if (!b || ![b.o, b.h, b.l, b.c].every(Number.isFinite)) {
|
|
22
|
+
out[i] = null
|
|
23
|
+
prev = null
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
const c = (b.o + b.h + b.l + b.c) / 4
|
|
27
|
+
const o = prev ? (prev.o + prev.c) / 2 : (b.o + b.c) / 2
|
|
28
|
+
const ha: OhlcBar = { o, c, h: Math.max(b.h, o, c), l: Math.min(b.l, o, c) }
|
|
29
|
+
out[i] = ha
|
|
30
|
+
prev = ha
|
|
31
|
+
}
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Roll daily (or finer) bars up to a coarser calendar bucket: the first
|
|
37
|
+
* open, the highest high, the lowest low, the last close, the summed volume.
|
|
38
|
+
* Categories are ISO dates and come back as the bucket starts (`bucketStart`),
|
|
39
|
+
* sorted; bars whose date does not parse are dropped. Pass `volumes` to get
|
|
40
|
+
* them summed alongside.
|
|
41
|
+
*/
|
|
42
|
+
export function resampleOhlc(
|
|
43
|
+
categories: ReadonlyArray<string>,
|
|
44
|
+
ohlc: ReadonlyArray<OhlcBar | null>,
|
|
45
|
+
opts: { bucket: ChartTimeBucket; volumes?: ReadonlyArray<number> },
|
|
46
|
+
): { categories: string[]; ohlc: Array<OhlcBar | null>; volumes?: number[] } {
|
|
47
|
+
const groups = new Map<string, { bar: OhlcBar | null; vol: number; first: number; last: number }>()
|
|
48
|
+
const order: Array<[string, number]> = []
|
|
49
|
+
for (let i = 0; i < categories.length; i += 1) {
|
|
50
|
+
const t = Date.parse(categories[i]!)
|
|
51
|
+
if (!Number.isFinite(t)) continue
|
|
52
|
+
const key = bucketStart(t, opts.bucket)
|
|
53
|
+
if (!key) continue
|
|
54
|
+
let g = groups.get(key)
|
|
55
|
+
if (!g) {
|
|
56
|
+
g = { bar: null, vol: 0, first: Infinity, last: -Infinity }
|
|
57
|
+
groups.set(key, g)
|
|
58
|
+
order.push([key, Date.parse(key)])
|
|
59
|
+
}
|
|
60
|
+
const b = ohlc[i]
|
|
61
|
+
if (b) {
|
|
62
|
+
// Rows may arrive out of order: the open is the EARLIEST bar's and the
|
|
63
|
+
// close the LATEST bar's, by date, not by position.
|
|
64
|
+
if (!g.bar) g.bar = { ...b }
|
|
65
|
+
else {
|
|
66
|
+
g.bar.h = Math.max(g.bar.h, b.h)
|
|
67
|
+
g.bar.l = Math.min(g.bar.l, b.l)
|
|
68
|
+
if (t >= g.last) g.bar.c = b.c
|
|
69
|
+
if (t <= g.first) g.bar.o = b.o
|
|
70
|
+
}
|
|
71
|
+
if (t > g.last) g.last = t
|
|
72
|
+
if (t < g.first) g.first = t
|
|
73
|
+
}
|
|
74
|
+
const v = opts.volumes?.[i]
|
|
75
|
+
if (Number.isFinite(v)) g.vol += v!
|
|
76
|
+
}
|
|
77
|
+
order.sort((a, b) => a[1] - b[1])
|
|
78
|
+
const out = {
|
|
79
|
+
categories: order.map(([k]) => k),
|
|
80
|
+
ohlc: order.map(([k]) => groups.get(k)!.bar),
|
|
81
|
+
...(opts.volumes ? { volumes: order.map(([k]) => groups.get(k)!.vol) } : {}),
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What the grid's chart panel stores for a price chart: the column ids. */
|
|
87
|
+
export type OhlcColumns = { open: string | null; high: string | null; low: string | null; close: string | null; volume: string | null }
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Guess the price columns from their names: the first numeric column called
|
|
91
|
+
* open / high / low / close (or last, price) / volume. What the chart panel
|
|
92
|
+
* shows before the reader picks anything; a picked column always wins.
|
|
93
|
+
*/
|
|
94
|
+
export function guessOhlcColumns(
|
|
95
|
+
measures: ReadonlyArray<{ id: string; field: string; label: string }>,
|
|
96
|
+
picked?: Partial<OhlcColumns> | null,
|
|
97
|
+
): OhlcColumns {
|
|
98
|
+
const guess = (re: RegExp) => measures.find((m) => re.test(m.field) || re.test(m.label))?.id ?? null
|
|
99
|
+
return {
|
|
100
|
+
open: picked?.open ?? guess(/open/i),
|
|
101
|
+
high: picked?.high ?? guess(/high/i),
|
|
102
|
+
low: picked?.low ?? guess(/low/i),
|
|
103
|
+
close: picked?.close ?? guess(/close|last|price/i),
|
|
104
|
+
volume: picked?.volume ?? guess(/vol/i),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The `rowsToDirectSpec` options for the grid panel's price chart, or null
|
|
110
|
+
* while a price column is still unpicked. `fieldOf` maps a column id to its
|
|
111
|
+
* field name; the date column is the category.
|
|
112
|
+
*/
|
|
113
|
+
export function ohlcDirectOptions(
|
|
114
|
+
fieldOf: (id: string | null) => string | null | undefined,
|
|
115
|
+
dateId: string | null,
|
|
116
|
+
cols: OhlcColumns,
|
|
117
|
+
extra: { bucket?: ChartTimeBucket | null; candleStyle?: ChartSpec['candleStyle'] | null },
|
|
118
|
+
): { category: string; ohlc: NonNullable<Parameters<typeof import('./chart').rowsToDirectSpec>[2]['ohlc']> } | null {
|
|
119
|
+
const date = fieldOf(dateId)
|
|
120
|
+
const open = fieldOf(cols.open)
|
|
121
|
+
const high = fieldOf(cols.high)
|
|
122
|
+
const low = fieldOf(cols.low)
|
|
123
|
+
const close = fieldOf(cols.close)
|
|
124
|
+
if (!date || !open || !high || !low || !close) return null
|
|
125
|
+
const volume = fieldOf(cols.volume)
|
|
126
|
+
return {
|
|
127
|
+
category: date,
|
|
128
|
+
ohlc: {
|
|
129
|
+
open, high, low, close,
|
|
130
|
+
...(volume ? { volume } : {}),
|
|
131
|
+
...(extra.bucket ? { bucket: extra.bucket } : {}),
|
|
132
|
+
...(extra.candleStyle ? { candleStyle: extra.candleStyle } : {}),
|
|
133
|
+
lastPriceLine: true,
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The indicator names the grid's chart panel offers. */
|
|
139
|
+
export type ChartPanelIndicator = 'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap'
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Split the panel's indicator list into the panes that go under the price
|
|
143
|
+
* (in order) and the one overlay that rides on it (a series carries a single
|
|
144
|
+
* `overlay`, so the first overlay kind wins).
|
|
145
|
+
*/
|
|
146
|
+
export function splitPanelIndicators(kinds: ReadonlyArray<ChartPanelIndicator>): { panes: ChartIndicatorSpec[]; overlay: SeriesOverlay | null } {
|
|
147
|
+
const OVERLAY: Partial<Record<ChartPanelIndicator, SeriesOverlay>> = { sma: 'sma:20', ema: 'ema:20', bb: 'bb:20:2', vwap: 'vwap' }
|
|
148
|
+
const panes: ChartIndicatorSpec[] = []
|
|
149
|
+
let overlay: SeriesOverlay | null = null
|
|
150
|
+
for (const k of kinds) {
|
|
151
|
+
const o = OVERLAY[k]
|
|
152
|
+
if (o) { overlay ??= o; continue }
|
|
153
|
+
if (k === 'volume' || k === 'rsi' || k === 'macd' || k === 'stochastic' || k === 'atr' || k === 'obv') panes.push({ kind: k })
|
|
154
|
+
}
|
|
155
|
+
return { panes, overlay }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A candlestick spec straight from rows: one bar per date (sorted), with the
|
|
160
|
+
* volume alongside when a field is named, optionally rolled up to a calendar
|
|
161
|
+
* bucket first. The x axis is ordinal-time, so weekends and holidays leave no
|
|
162
|
+
* gaps.
|
|
163
|
+
*/
|
|
164
|
+
export function rowsToOhlcSpec<T extends Record<string, unknown>>(
|
|
165
|
+
rows: ReadonlyArray<T>,
|
|
166
|
+
opts: {
|
|
167
|
+
date: keyof T & string
|
|
168
|
+
open: keyof T & string
|
|
169
|
+
high: keyof T & string
|
|
170
|
+
low: keyof T & string
|
|
171
|
+
close: keyof T & string
|
|
172
|
+
volume?: keyof T & string
|
|
173
|
+
bucket?: ChartTimeBucket
|
|
174
|
+
/** The series label. Default "Price". */
|
|
175
|
+
label?: string
|
|
176
|
+
type?: 'candlestick' | 'ohlc'
|
|
177
|
+
candleStyle?: ChartSpec['candleStyle']
|
|
178
|
+
lastPriceLine?: boolean
|
|
179
|
+
},
|
|
180
|
+
): ChartSpec {
|
|
181
|
+
const num = (v: unknown): number => (typeof v === 'number' ? v : Number(v))
|
|
182
|
+
const sorted = rows
|
|
183
|
+
.map((r) => ({ r, t: Date.parse(String(r[opts.date])) }))
|
|
184
|
+
.filter((x) => Number.isFinite(x.t))
|
|
185
|
+
.sort((a, b) => a.t - b.t)
|
|
186
|
+
let categories = sorted.map((x) => new Date(x.t).toISOString().slice(0, 10))
|
|
187
|
+
let ohlc: Array<OhlcBar | null> = sorted.map(({ r }) => {
|
|
188
|
+
const b = { o: num(r[opts.open]), h: num(r[opts.high]), l: num(r[opts.low]), c: num(r[opts.close]) }
|
|
189
|
+
return [b.o, b.h, b.l, b.c].every(Number.isFinite) ? b : null
|
|
190
|
+
})
|
|
191
|
+
let volumes = opts.volume ? sorted.map(({ r }) => num(r[opts.volume!])) : undefined
|
|
192
|
+
if (opts.bucket) {
|
|
193
|
+
const rolled = resampleOhlc(categories, ohlc, { bucket: opts.bucket, volumes })
|
|
194
|
+
categories = rolled.categories
|
|
195
|
+
ohlc = rolled.ohlc
|
|
196
|
+
volumes = rolled.volumes
|
|
197
|
+
}
|
|
198
|
+
const label = opts.label ?? 'Price'
|
|
199
|
+
const series: ChartSeries = { label, values: ohlc.map((b) => (b ? b.c : Number.NaN)), ohlc, ...(volumes ? { volumes } : {}) }
|
|
200
|
+
return {
|
|
201
|
+
type: opts.type ?? 'candlestick',
|
|
202
|
+
categories,
|
|
203
|
+
series: [series],
|
|
204
|
+
xType: 'ordinal-time',
|
|
205
|
+
...(opts.candleStyle ? { candleStyle: opts.candleStyle } : {}),
|
|
206
|
+
...(opts.lastPriceLine ? { lastPriceLine: true } : {}),
|
|
207
|
+
}
|
|
208
|
+
}
|