diff --git a/CHANGELOG.md b/CHANGELOG.md index 985da5c9..8db8faac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented here. +## [3.6.3] - 2024-10-30 + +- Reconnect closed apis when tab made visible again (& ios pwa) +- Fetch range based on indicator length +- Autocomplete local variable in script +- Zoomable indicator preview +- Markdown editor for indicators description +- Indicator scaleWith dropdown + ## [3.6.2] - 2024-09-29 - Fix Dockerfile and startup instructions diff --git a/package-lock.json b/package-lock.json index 7ac3cd18..2124f8ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aggr", - "version": "3.6.2", + "version": "3.6.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aggr", - "version": "3.6.1", + "version": "3.6.3", "dependencies": { "@mdi/font": "^5.6.55", "@vue/eslint-config-typescript": "^11.0.3", diff --git a/package.json b/package.json index 5d41dbb7..01dd93de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aggr", - "version": "3.6.2", + "version": "3.6.3", "private": true, "type": "module", "scripts": { diff --git a/src/assets/sass/dialog.scss b/src/assets/sass/dialog.scss index 90fde653..848d72b7 100644 --- a/src/assets/sass/dialog.scss +++ b/src/assets/sass/dialog.scss @@ -124,9 +124,12 @@ border-radius: 0.75rem 0.75rem 0 0; cursor: grab; + &-wrapper { + position: relative; + } + #{$self}--contrasted & { border-bottom: 0 !important; - padding-bottom: 0; background-color: var(--theme-base-o25); } @@ -153,10 +156,11 @@ } &__subheader { + position: relative; + z-index: 1; #{$self}--contrasted & { background-color: var(--theme-base-o25); - padding-top: 0.5rem; } } @@ -239,13 +243,7 @@ .dialog__close { text-align: center; - padding: 0.75rem; - margin: -0.75rem -0.75rem -0.75rem 0.75rem; - display: flex; - align-items: center; - justify-content: center; - align-self: stretch; - border-radius: 0 $border-radius-base 0 0; + margin: 0 -0.5rem 0 0.5rem; color: var(--theme-color-100); &:hover { diff --git a/src/assets/sass/editor.scss b/src/assets/sass/editor.scss new file mode 100644 index 00000000..3e718dbc --- /dev/null +++ b/src/assets/sass/editor.scss @@ -0,0 +1,30 @@ +.editor { + height: 100%; + min-height: 50px; + + .monaco-editor { + #app.-light & { + --vscode-editor-background: var(--theme-background-100); + --vscode-editorStickyScroll-background: var(--theme-background-100); + --vscode-editorStickyScrollHover-background: var(--theme-background-100); + --vscode-editorGutter-background: var(--theme-background-100); + } + + .minimap-shadow-visible { + box-shadow: rgb(0 0 0 / 10%) -6px 0 6px -6px inset; + } + + .scroll-decoration { + box-shadow: rgb(0 0 0 / 10%) 0 6px 6px -6px inset; + } + + .view-overlays .current-line { + border-color: var(--theme-background-100); + } + + .minimap { + left: auto !important; + right: 0 !important; + } + } +} \ No newline at end of file diff --git a/src/components/chart/Chart.vue b/src/components/chart/Chart.vue index fb1ed793..3bca3926 100644 --- a/src/components/chart/Chart.vue +++ b/src/components/chart/Chart.vue @@ -36,7 +36,7 @@ @click="toggleTimeframeDropdown($event, $refs.timeframeButton)" class="-arrow -cases pane-header__highlight pane-chart__timeframe-selector" > - {{ !isKnownTimeframe ? timeframeForHuman : '' }} + {{ !isKnownTimeframe ? timeframeForHuman : '' }}
diff --git a/src/components/chart/IndicatorsOverlay.vue b/src/components/chart/IndicatorsOverlay.vue index 8d6c571a..ceb2bca7 100644 --- a/src/components/chart/IndicatorsOverlay.vue +++ b/src/components/chart/IndicatorsOverlay.vue @@ -1,69 +1,11 @@ + diff --git a/src/components/framework/editor/completion.ts b/src/components/framework/editor/completion.ts new file mode 100644 index 00000000..5ca936a6 --- /dev/null +++ b/src/components/framework/editor/completion.ts @@ -0,0 +1,124 @@ +import { languages, Range } from 'monaco-editor/esm/vs/editor/editor.api' +import { loadMd, showReference, TOKENS } from './references' + +import AGGR_SUGGESTIONS from './suggestions' + +const COMPLETION_THRESHOLD = 10 +const TIME_THRESHOLD = 5000 + +let persistentVariablesCache = [] +let completionCount = 0 +let lastCacheUpdate = Date.now() + +function updatePersistentVariablesCache(model) { + const text = model.getValue() + persistentVariablesCache = [ + ...new Set( + [ + ...text.matchAll( + /(? match[1]) + ) + ] + + lastCacheUpdate = Date.now() + completionCount = 0 + return persistentVariablesCache.length +} + +export function provideCompletionItems(model, position) { + const word = model.getWordUntilPosition(position) + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn + } + + const queryFilter = new RegExp(`${word.word}`, 'i') + + const now = Date.now() + if ( + completionCount >= COMPLETION_THRESHOLD || + now - lastCacheUpdate > TIME_THRESHOLD + ) { + updatePersistentVariablesCache(model) + } + completionCount++ + + const suggestions = AGGR_SUGGESTIONS.filter( + a => queryFilter.test(a.label) || queryFilter.test(a.detail) + ).map(s => ({ + ...s, + kind: languages.CompletionItemKind.Function, + range + })) + + persistentVariablesCache.forEach(variable => { + suggestions.push({ + label: variable, + kind: languages.CompletionItemKind.Variable, + range, + detail: 'Local variable', + insertText: variable + }) + }) + + return { suggestions } +} + +export async function provideHover(model, position) { + const word = model.getWordAtPosition(position) + + if (!word || !word.word) { + return + } + + const token = word.word.replace(/^plot/, '') + + if (TOKENS.indexOf(token) !== -1) { + const md = await loadMd(token) + let contents + if (md) { + contents = md + .split(/\n\n/) + .slice(0, 2) + .map(row => ({ + value: row + })) + .concat({ + value: `[Learn more](${token})` + }) + + setTimeout(() => { + const linkElement = document.querySelector(`a[data-href="${token}"]`) + + if (!linkElement) { + return + } + + linkElement.addEventListener('click', (event: MouseEvent) => { + event.preventDefault() + + showReference(token, md, { + x: event.clientX, + y: event.clientY + }) + }) + }, 500) + } else { + contents = [{ value: 'no definition found' }] + } + + return { + range: new Range( + position.lineNumber, + word.startColumn, + position.lineNumber, + word.endColumn + ), + contents + } + } +} diff --git a/src/components/framework/editor/editor.ts b/src/components/framework/editor/editor.ts index 3a0d59e2..c26e4655 100644 --- a/src/components/framework/editor/editor.ts +++ b/src/components/framework/editor/editor.ts @@ -2,13 +2,10 @@ import 'monaco-editor/esm/vs/editor/editor.all.js' import 'monaco-editor/esm/vs/language/typescript/monaco.contribution' import 'monaco-editor/esm/vs/basic-languages/monaco.contribution' -import { - editor, - languages, - Range -} from 'monaco-editor/esm/vs/editor/editor.api' -import AGGR_SUGGESTIONS from './suggestions' -import { loadMd, showReference, TOKENS } from './references' +import store from '@/store' +import { editor, languages } from 'monaco-editor/esm/vs/editor/editor.api' +import { provideCompletionItems, provideHover } from './completion' +import { rgbToHex, splitColorCode } from '@/utils/colors' languages.typescript.javascriptDefaults.setCompilerOptions({ noLib: true, @@ -22,86 +19,48 @@ languages.typescript.javascriptDefaults.setDiagnosticsOptions({ }) languages.registerCompletionItemProvider('javascript', { - provideCompletionItems: function (model, position) { - const word = model.getWordUntilPosition(position) - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn - } - - const queryFilter = new RegExp(`${word}`, 'i') - - return { - suggestions: AGGR_SUGGESTIONS.filter( - a => queryFilter.test(a.label) || queryFilter.test(a.detail) - ).map(s => ({ - ...s, - kind: languages.CompletionItemKind.Function, - range - })) - } - } + provideCompletionItems }) languages.registerHoverProvider('javascript', { - provideHover: async function (model, position) { - // Get the word at the current position - const word = model.getWordAtPosition(position) + provideHover +}) - if (!word || !word.word) { - return +const lsLight = store.state.settings.theme === 'light' +const style = getComputedStyle(document.documentElement) +const backgroundColor = splitColorCode( + style.getPropertyValue('--theme-background-base') +) +const backgroundColor100 = splitColorCode( + style.getPropertyValue('--theme-background-100') +) +const backgroundColor150 = splitColorCode( + style.getPropertyValue('--theme-background-150') +) + +if (lsLight) { + editor.defineTheme('aggr', { + base: 'vs', + inherit: true, + rules: [], + colors: { + 'minimap.background': rgbToHex(backgroundColor150), + 'editor.background': rgbToHex(backgroundColor100), + 'editor.lineHighlightBackground': rgbToHex(backgroundColor), + 'editor.lineHighlightBorder': rgbToHex(backgroundColor) } - - const token = word.word.replace(/^plot/, '') - - // Check if the word is one of the specific tokens - if (TOKENS.indexOf(token) !== -1) { - const md = await loadMd(token) - let contents - if (md) { - contents = md - .split(/\n\n/) - .slice(0, 2) - .map(row => ({ - value: row - })) - .concat({ - value: `[Learn more](${token})` - }) - - setTimeout(() => { - const linkElement = document.querySelector(`a[data-href="${token}"]`) - - if (!linkElement) { - return - } - - linkElement.addEventListener('click', (event: MouseEvent) => { - event.preventDefault() - - showReference(token, md, { - x: event.clientX, - y: event.clientY - }) - }) - }, 500) - } else { - contents = [{ value: 'no definition found' }] - } - - return { - range: new Range( - position.lineNumber, - word.startColumn, - position.lineNumber, - word.endColumn - ), - contents - } + }) +} else { + editor.defineTheme('aggr', { + base: 'vs-dark', + inherit: true, + rules: [], + colors: { + 'editor.background': rgbToHex(backgroundColor100), + 'editor.lineHighlightBackground': rgbToHex(backgroundColor), + 'editor.lineHighlightBorder': rgbToHex(backgroundColor) } - } -}) + }) +} export default editor diff --git a/src/components/framework/editor/references/README.md b/src/components/framework/editor/references/README.md deleted file mode 100644 index 8344d6b5..00000000 --- a/src/components/framework/editor/references/README.md +++ /dev/null @@ -1,29 +0,0 @@ -```ts -area( - value: number | { - value: number, - time: number - }, - [options] -) -``` - -Renders an area series for the current bar using the specified value and optional styling parameters. This function adapts the `addAreaSeries` method from Lightweight Charts for individual bar rendering. - -## Parameters - -- `value`: The value for the current bar, either a number or an object with `value` and `time` properties. -- `options` (optional): A set of styling options for the area series. Options include: - - `lineColor`: The color of the line in the area series (e.g., `lineColor=yellow`). - - `topColor`: The color at the top of the area series (e.g., `topColor='#2962FF'`). - - `bottomColor`: The color at the bottom of the area series (e.g., `bottomColor='rgba(41, 98, 255, 0.28)'`). - -## Returns - -- This function does not return a value. It renders the area series on the chart for the current bar. - -## Summary - -The `area` function is designed for real-time, bar-by-bar rendering of area series in charts. It accepts a value for the current bar and a series of optional styling parameters, allowing for customization of the series appearance. The parameters follow a key=value format, making it flexible and intuitive to specify various options. This function is ideal for applications requiring dynamic and visually distinct data visualization, particularly in financial or data-intensive contexts. - -*Note: All options are optional. The function can be used with just the `value` parameter (e.g., `area(1)`), in which case default styles will be applied. The styling parameters must be valid CSS color values for the function to render the series correctly.* diff --git a/src/components/framework/editor/references/brokenarea.md b/src/components/framework/editor/references/brokenarea.md new file mode 100644 index 00000000..5303d882 --- /dev/null +++ b/src/components/framework/editor/references/brokenarea.md @@ -0,0 +1,171 @@ + + +```ts +brokenarea(cell: BrokenAreaCell, [options]) +``` + +Draws a "broken cloud," or a shape with both a top and bottom value, capable of stopping and reappearing elsewhere without continuity. The `brokenarea` function is flexible enough to create rectangles, background fills, and even horizontal lines, enabling advanced plotting capabilities. When applied correctly, it can generate complex visualizations like heatmaps. + +## Parameters + +- **`cell`** — Defines the primary characteristics of the broken area. +- **`options`** *(optional)* — Allows additional customization, such as color and stroke properties. + +## `BrokenAreaCell` Type + +```ts +export interface BrokenAreaCell { + time: number; // Unix timestamp + lowerValue: number; + higherValue: number; + extendRight?: boolean; + infinite?: boolean; // Draw beyond viewport if set to true + color?: string; + label?: string; + id?: string; +} +``` + +## `BrokenAreaOptions` Type + +```ts +export interface BrokenAreaOptions { + color?: string; + strokeColor?: string; + strokeWidth?: number; +} +``` + +## Example Usage + +### Basic Example (Suboptimal Usage) + +``` +brokenarea({ + time: time, + lowerValue: 10, + higherValue: 20, + extendRight: true, + color: 'yellow' +}) +``` + +This example draws a rectangle extending infinitely to the right, which can function as a test but lacks practical application. + +### Advanced Example (Dynamic Slot-Based Drawing) + +The `brokenarea` function is most effective when used to dynamically allocate slots for drawing lines or rectangles on the chart. By defining multiple `brokenarea` slots programmatically, you gain greater control over redrawing and updating visual elements based on script logic. + +```ts +// top of the script +if (!boundaries) { + // check if boundaries isn't defined = initial run of the script + + // define some persistent script variables + pendingRedraws = [] // number[] + slots = [] // {index: number, redrawAt: number}[] + + // indicator related, but usefull to underestand the point + cells = [] // {[normalizedCellPrice: number]: {strength: number, index: number, top: number, bottom: number, count: number} + + // here we make use of the series global variable + for (var i = 0; i < series.length; i++) { + if (series[i].seriesType() !== 'BrokenArea') { + continue + } + + // register each available series as a slot + slots.push({ + index: i, + redrawAt: 0 + }) + + // boundaries: { [cellId: string]: *bar index* } + series[i].setExtensionsBoundaries(boundaries) + } +} + +// define the slots at whatever part of the script +brokenarea() +brokenarea() +brokenarea() +brokenarea() +brokenarea() +brokenarea() +brokenarea() +brokenarea() +brokenarea() +brokenarea() +brokenarea() +``` + +This binds the `boundaries` object to each series slot, allowing control over where each cell extension halts. + +```ts +// at the bottom of the script, you would have something like this +// pendingRedraws here is an array of the cells that needs to be redrawn, each redraw use another object `cells`, a store for all the cells +if (pendingRedraws.length) { + for (var i = 0; i < pendingRedraws.length; i++) { + var cell = cells[pendingRedraws[i]] + + if (!cell) { + // maybe cell doesn't exist anymore + pendingRedraws.splice(i--, 1) + continue + } + + var slot = slots.find(slot => slot.redrawAt < bar.length) + + if (slot) { + // lock that slot to NOT be used until next bar + slot.redrawAt = bar.length + 1 + + if (cell.id) { + // this is the interesting part. it tells the cell previously drawn by that slot (with extendRight: true) to stop extending at the bar index `bar.length - 2` + boundaries[cell.id] = bar.length - 2 + } + + // register a new cell id + cell.id = Math.random().toString() + + // indicator related stuff based on our cell + var ratio = Math.max(0.01, Math.min(1, cell.strength * cell.count * (options.strength / 100))) + var color = interpolate(ratio, color0, color1, color2, color3) + + // this is the second interesting part. it just tells the selected brokenarea() slot (bar.series[series[slot.index].id]) to draw the rectangle + bar.series[series[slot.index].id] ={ + id: cell.id, + time: time - bar.timeframe, + lowerValue: cell.top, + higherValue: cell.bottom, + extendRight: true, + color: color + } + + pendingRedraws.splice(i--, 1) + } else { + break; + } + } +} +``` + +This setup provides efficient control over drawing, only updating cells that have changed. With proper configuration, this approach forms the basis for heatmap visualizations by selectively redrawing areas as needed. + +### Horizontal lines + +The `brokenarea` function can also create horizontal lines, offering programmable and efficient rendering. Setting `lowerValue` and `higherValue` to the same value draws a line instead of a rectangle. + +Example: Drawing a horizontal threshold line above a histogram series. + +```ts +threshold = option(default=1000000,type=range,step=10000,min=1000,max=10000000) +brokenarea(infinite=true,strokeWidth=0.5,strokeColor=options.upColor,id=threshold) + +if (bar.length === 1) { + // draw once + bar.series.threshold = { time: time, lowerValue: options.threshold, higherValue: options.threshold, extendRight: true } +} +``` + +With `brokenarea`, precise control over plot boundaries and appearance is possible, enabling a high degree of customization across various charting applications. diff --git a/src/components/framework/editor/references/cloudarea.md b/src/components/framework/editor/references/cloudarea.md new file mode 100644 index 00000000..33d17ec8 --- /dev/null +++ b/src/components/framework/editor/references/cloudarea.md @@ -0,0 +1,36 @@ +```ts +cloudarea( + lowerValue: number, + higherValue: number, + [options] +) +``` + +Exemple: + +```ts +cloudarea($price.low, $price.high, positiveColor=green, negativeColor=red) +``` + +Draw continuous cloud (shape with a top and a bottom value) + +```ts +export interface CloudAreaStyleOptions { + positiveColor: string; + negativeColor: string; + higherLineColor: string; + higherLineStyle: LineStyle; + higherLineWidth: LineWidth; + higherLineType: LineType; + lowerLineColor: string; + lowerLineStyle: LineStyle; + lowerLineWidth: LineWidth; + lowerLineType: LineType; + crosshairMarkerVisible: boolean; + crosshairMarkerRadius: number; + crosshairMarkerBorderColor: string; + crosshairMarkerBackgroundColor: string; +} +``` + +***Note: all of these options are static, aka remains the same for the whole series*** \ No newline at end of file diff --git a/src/components/framework/editor/references/line.md b/src/components/framework/editor/references/line.md new file mode 100644 index 00000000..312b4cc2 --- /dev/null +++ b/src/components/framework/editor/references/line.md @@ -0,0 +1,31 @@ +```ts +line(data: number | { + value: number, + [time: number] +}, [options]) +``` + +Renders a line series. + +## Parameters + +- **`data`**: An object representing the line series data, containing **`value`** (the y-axis value). Optionally, **`time`** can also be included to specify the x-axis position. +- **`options`** (optional): A set of styling options for the line series. Options include: + - **`color`**: The color of the line (e.g., `color='#2196f3'`). + - **`lineWidth`**: The width of the line in pixels (e.g., `lineWidth=2`). + - **`lineStyle`**: The style of the line, such as solid or dashed (e.g., `lineStyle=3`). + +## Returns + +- This function does not return a value. It renders the line series on the chart for the current bar. + +## Summary + +The `line` function integrates with Lightweight Charts' `addLineSeries` to display data as a line chart. Each point on the chart represents a `value` at a specific `time`, and the points are connected by straight lines to visualize trends over time. This function is essential for displaying continuous data, making it suitable for various applications like financial analysis, scientific data representation, and monitoring system metrics. + +*Note: All options are optional. The function can be used with just the `data` parameter, applying default styles if options are not specified. The options must be valid CSS color values or appropriate numerical values for the function to render the series correctly.* + + + + + diff --git a/src/components/framework/editor/references/option.md b/src/components/framework/editor/references/option.md index 600b20b8..41ffe7d7 100644 --- a/src/components/framework/editor/references/option.md +++ b/src/components/framework/editor/references/option.md @@ -30,7 +30,7 @@ export enum ALLOWED_OPTION_TYPES { ```ts // number input with `min`, `max` and `step` attribute -threshold = option((type = number), (min = 0), (max = 10), (step = 0.1)) +threshold = option(type = number, min = 0, max = 10, step = 0.1) ``` ```ts @@ -58,19 +58,19 @@ console.log(MyText) // "" ```ts smallrange = option( - (type = range), - (label = 'Small range'), - (min = 0), - (max = 1) +type = range, +label = 'Small range', +min = 0, +max = 1 ) bigrange = option( - (type = range), - (label = 'Big range'), - (gradient = ['red', 'limegreen']), // colorize slider - (min = 0), - (max = 1000000), - (log = true) // slider will ajust displayed value logarithmic scale +type = range, +label = 'Big range', +gradient = ['red', 'limegreen'], // colorize slider +min = 0, +max = 1000000, +log = true // slider will ajust displayed value logarithmic scale ) ``` diff --git a/src/components/indicators/IndicatorDetail.vue b/src/components/indicators/IndicatorDetail.vue index 7bbbbff0..a0f01ca2 100644 --- a/src/components/indicators/IndicatorDetail.vue +++ b/src/components/indicators/IndicatorDetail.vue @@ -2,15 +2,14 @@
-
- - - - - #{{ indicator.id }} - - -
+
@@ -43,10 +42,9 @@

- {{ indicator.description || 'Add description' }} -

+ />
  • - + diff --git a/src/components/indicators/IndicatorLibraryDialog.vue b/src/components/indicators/IndicatorLibraryDialog.vue index 9af053aa..aaaaa055 100644 --- a/src/components/indicators/IndicatorLibraryDialog.vue +++ b/src/components/indicators/IndicatorLibraryDialog.vue @@ -15,10 +15,7 @@ diff --git a/src/components/indicators/IndicatorPreview.vue b/src/components/indicators/IndicatorPreview.vue new file mode 100644 index 00000000..994631cc --- /dev/null +++ b/src/components/indicators/IndicatorPreview.vue @@ -0,0 +1,228 @@ + + + + diff --git a/src/components/indicators/PriceScaleButton.vue b/src/components/indicators/PriceScaleButton.vue new file mode 100644 index 00000000..74adfd13 --- /dev/null +++ b/src/components/indicators/PriceScaleButton.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/components/indicators/PriceScaleDropdown.vue b/src/components/indicators/PriceScaleDropdown.vue new file mode 100644 index 00000000..76f270e5 --- /dev/null +++ b/src/components/indicators/PriceScaleDropdown.vue @@ -0,0 +1,71 @@ + + + + diff --git a/src/components/library/EditResourceDialog.vue b/src/components/library/EditResourceDialog.vue index e45e4b27..c9daedff 100644 --- a/src/components/library/EditResourceDialog.vue +++ b/src/components/library/EditResourceDialog.vue @@ -5,12 +5,13 @@ v-if="dialogOpened" class="edit-resource-dialog" @clickOutside="hide" + @resize="resizeEditor" >