Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/webserver/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"watch": "webpack --watch --config webpack/dev.js",
"analyze": "webpack --config webpack/analyze.js",
"build": "npm exec tsc && webpack --config webpack/prod.js --progress --color",
"build_dev": "npm exec tsc && webpack --config webpack/dev.js --progress --color",
"lint": "npm exec tsc && npm exec -- eslint src --ext .js --ext .ts",
"check-types": "npm exec tsc"
},
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/site/src/html/bodybuilder.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
{{end}}

{{define "bottom"}}
<script src="/js/entry.js?v=LVFHWeVTq"></script>
<script src="/js/entry.js?v=LFgBaLTq"></script>
</body>
</html>
{{end}}
26 changes: 7 additions & 19 deletions client/webserver/site/src/js/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export class DepthChart extends Chart {
book: OrderBook
zoomLevel: number
lotSize: number
rateStep: number
conventionalRateStep: number
lines: DepthLine[]
markers: Record<string, DepthMarker[]>
zoomInBttn: Region
Expand Down Expand Up @@ -454,11 +454,10 @@ export class DepthChart extends Chart {
}

// set sets the current data set and draws.
set (book: OrderBook, lotSize: number, rateStep: number, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) {
set (book: OrderBook, lotSize: number, rateStepEnc: number, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) {
this.book = book
this.lotSize = lotSize / baseUnitInfo.conventional.conversionFactor
const [qFactor, bFactor] = [quoteUnitInfo.conventional.conversionFactor, baseUnitInfo.conventional.conversionFactor]
this.rateStep = rateStep / RateEncodingFactor * qFactor / bFactor
this.conventionalRateStep = Doc.conventionalRateStep(rateStepEnc, baseUnitInfo, quoteUnitInfo)
this.baseUnit = baseUnitInfo.conventional.unit
this.quoteUnit = quoteUnitInfo.conventional.unit
if (!this.zoomLevel) {
Expand Down Expand Up @@ -588,7 +587,7 @@ export class DepthChart extends Chart {

// Print the x labels
const xLabels = makeLabels(ctx, this.plotRegion.width(), dataExtents.x.min,
dataExtents.x.max, 100, this.rateStep, '')
dataExtents.x.max, 100, this.conventionalRateStep, '')

this.plotXLabels(xLabels, low, high, [`${this.quoteUnit}/`, this.baseUnit])

Expand All @@ -611,7 +610,7 @@ export class DepthChart extends Chart {
ctx.textBaseline = 'middle'
ctx.fillStyle = this.theme.value
const y = 0.5 * dataExtents.y.max
ctx.fillText(formatLabelValue(midGap), tools.x(midGap), tools.y(y))
ctx.fillText(Doc.formatFourSigFigs(midGap), tools.x(midGap), tools.y(y))
ctx.font = '12px \'sans\', sans-serif'
// ctx.fillText('mid-market price', tools.x(midGap), tools.y(y) + 24)
ctx.fillText(`${(gapWidth / midGap * 100).toFixed(2)}% spread`,
Expand Down Expand Up @@ -928,7 +927,7 @@ export class CandleChart extends Chart {

// Apply labels.
const rFactor = this.rateConversionFactor
this.doYLabels(this.candleRegion, rateStep, this.market.quotesymbol, v => formatLabelValue(v / rFactor))
this.doYLabels(this.candleRegion, rateStep, this.market.quotesymbol, v => Doc.formatFourSigFigs(v / rFactor))
this.candleRegion.extents.x.min = this.yRegion.extents.x.max
this.volumeRegion.extents.x.min = this.yRegion.extents.x.max

Expand Down Expand Up @@ -1325,7 +1324,7 @@ function makeLabels (
unit: string,
valFmt?: (v: number) => string
): LabelSet {
valFmt = valFmt || formatLabelValue
valFmt = valFmt || Doc.formatFourSigFigs
const n = screenW / spacingGuess
const diff = max - min
if (n < 1 || diff <= 0) return { lbls: [] }
Expand Down Expand Up @@ -1437,17 +1436,6 @@ function clamp (v: number, min: number, max: number): number {
return v
}

/* labelSpecs is specifications for axis tick labels. */
const labelSpecs = {
minimumSignificantDigits: 4,
maximumSignificantDigits: 5
}

/* formatLabelValue formats the provided value using the labelSpecs format. */
function formatLabelValue (x: number) {
return x.toLocaleString('en-us', labelSpecs)
}
Comment thread
norwnd marked this conversation as resolved.

/* floatCompare compares two floats to within a tolerance of 1e-8. */
function floatCompare (a: number, b: number) {
return withinTolerance(a, b, 1e-8)
Expand Down
144 changes: 125 additions & 19 deletions client/webserver/site/src/js/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
WalletState,
PageElement
} from './registry'
import { RateEncodingFactor } from './orderutil'

const parser = new window.DOMParser()

Expand All @@ -27,20 +28,22 @@ const BipIDs: Record<number, string> = {

const BipSymbols = Object.values(BipIDs)

const log10RateEncodingFactor = Math.round(Math.log10(RateEncodingFactor))

const intFormatter = new Intl.NumberFormat((navigator.languages as string[]))

const threeSigFigs = new Intl.NumberFormat((navigator.languages as string[]), {
minimumSignificantDigits: 3,
maximumSignificantDigits: 3
const fourSigFigs = new Intl.NumberFormat((navigator.languages as string[]), {
minimumSignificantDigits: 4,
maximumSignificantDigits: 4
})

const fiveSigFigs = new Intl.NumberFormat((navigator.languages as string[]), {
minimumSignificantDigits: 5,
maximumSignificantDigits: 5
const oneFractionalDigit = new Intl.NumberFormat((navigator.languages as string[]), {
minimumFractionDigits: 1,
maximumFractionDigits: 1
})

/* A cache for formatters used for Doc.formatCoinValue. */
const decimalFormatters = {}
const decimalFormatters: Record<number, Intl.NumberFormat> = {}

/*
* decimalFormatter gets the formatCoinValue formatter for the specified decimal
Expand All @@ -51,25 +54,25 @@ function decimalFormatter (prec: number) {
}

/* A cache for formatters used for Doc.formatFullPrecision. */
const fullPrecisionFormatters = {}
const fullPrecisionFormatters: Record<number, Intl.NumberFormat> = {}

/*
* fullPrecisionFormatter gets the formatFullPrecision formatter for the
* specified decimal precision.
*/
function fullPrecisionFormatter (prec: number) {
return formatter(fullPrecisionFormatters, prec, prec)
function fullPrecisionFormatter (prec: number, locales?: string | string[]) {
return formatter(fullPrecisionFormatters, prec, prec, locales)
}

/*
* formatter gets the formatter from the supplied cache if it already exists,
* else creates it.
*/
function formatter (formatters: Record<string, Intl.NumberFormat>, min: number, max: number): Intl.NumberFormat {
function formatter (formatters: Record<string, Intl.NumberFormat>, min: number, max: number, locales?: string | string[]): Intl.NumberFormat {
const k = `${min}-${max}`
let fmt = formatters[k]
if (!fmt) {
fmt = new Intl.NumberFormat((navigator.languages as string[]), {
fmt = new Intl.NumberFormat(locales || navigator.languages as string[], {
minimumFractionDigits: min,
maximumFractionDigits: max
})
Expand Down Expand Up @@ -259,15 +262,20 @@ export default class Doc {
return decimalFormatter(prec).format(v)
}

static formatThreeSigFigs (v: number): string {
if (v >= 1000) return intFormatter.format(Math.round(v))
return threeSigFigs.format(v)
/*
* formatRateFullPrecision formats rate to represent it exactly at rate step
* precision, trimming non-effectual zeros if there are any.
*/
static formatRateFullPrecision (encRate: number, bui: UnitInfo, qui: UnitInfo, rateStepEnc: number) {
const r = bui.conventional.conversionFactor / qui.conventional.conversionFactor
const convRate = encRate * r / RateEncodingFactor
const rateStepDigits = log10RateEncodingFactor - Math.floor(Math.log10(rateStepEnc)) -
Math.floor(Math.log10(bui.conventional.conversionFactor) - Math.log10(qui.conventional.conversionFactor))
return fullPrecisionFormatter(rateStepDigits).format(convRate)
}

static formatFiveSigFigs (v: number, prec?: number): string {
if (v >= 10000) return intFormatter.format(Math.round(v))
else if (v < 1e5) return fullPrecisionFormatter(prec ?? 8 /* rate encoding factor */).format(v)
return fiveSigFigs.format(v)
static formatFourSigFigs (n: number): string {
return formatSigFigsWithFormatters(oneFractionalDigit, fourSigFigs, n)
}

/*
Expand All @@ -292,6 +300,11 @@ export default class Doc {
return fullPrecisionFormatter(prec).format(value)
}

static conventionalRateStep (rateStepEnc: number, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) {
const [qFactor, bFactor] = [quoteUnitInfo.conventional.conversionFactor, baseUnitInfo.conventional.conversionFactor]
return rateStepEnc / RateEncodingFactor * (bFactor / qFactor)
}

/*
* logoPath creates a path to a png logo for the specified ticker symbol. If
* the symbol is not a supported asset, the generic letter logo will be
Expand Down Expand Up @@ -626,3 +639,96 @@ function timeMod (t: number, dur: number) {
const n = Math.floor(t / dur)
return [n, t - n * dur]
}

function formatSigFigsWithFormatters (intFormatter: Intl.NumberFormat, sigFigFormatter: Intl.NumberFormat, n: number, maxDecimals?: number, locales?: string | string[]): string {
if (n >= 1000) return intFormatter.format(n)
const s = sigFigFormatter.format(n)
if (typeof maxDecimals !== 'number') return s
const fractional = sigFigFormatter.formatToParts(n).filter((part: Intl.NumberFormatPart) => part.type === 'fraction')[0].value
if (fractional.length <= maxDecimals) return s
return fullPrecisionFormatter(maxDecimals, locales).format(n)
}

if (process.env.NODE_ENV === 'development') {
// Code will only appear in dev build.
// https://webpack.js.org/guides/production/
window.testFormatFourSigFigs = () => {
const tests: [string, string, number | undefined, string][] = [
['en-US', '1.234567', undefined, '1.235'], // sigFigFormatter
['en-US', '1.234567', 2, '1.23'], // decimalFormatter
['en-US', '1234', undefined, '1,234.0'], // oneFractionalDigit
['en-US', '12', undefined, '12.00'], // sigFigFormatter
['fr-FR', '123.45678', undefined, '123,5'], // oneFractionalDigit
['fr-FR', '1234.5', undefined, '1 234,5'], // U+202F for thousands separator
// For Arabic, https://www.saitak.com/number is useful, but seems to use
// slightly different unicode points and no thousands separator. I think
// the Arabic decimal separator is supposed to be more like a point, not
// a comma, but Google Chrome uses U+066B (Arabic Decimal Separator),
// which looks like a comma to me. ¯\_(ツ)_/¯
['ar-EG', '123.45678', undefined, '١٢٣٫٥'],
['ar-EG', '1234', undefined, '١٬٢٣٤٫٠'],
['ar-EG', '0.12345', 3, '٠٫١٢٣']

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is failing because you're not passing the "preciseFormatter" and fullPrecisionFormatter uses navigator.languages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it is test-specific issue (and using navigator.languages is what we want users to use in practice), right ? We can use something like this to accommodate this test (it passes now): 2577606

The reason I wanted to remove preciseFormatter from there is because it kinda duplicates maxDecimals param (according to my understanding) and it isn't clear what happens if preciseFormatter has precision set to anything different from maxDecimals value.

image

]

// Reproduce the NumberFormats with ONLY our desired language.
for (const [code, unformatted, maxDecimals, expected] of tests) {
const intFormatter = new Intl.NumberFormat(code, { // oneFractionalDigit
minimumFractionDigits: 1,
maximumFractionDigits: 1
})
const sigFigFormatter = new Intl.NumberFormat(code, {
minimumSignificantDigits: 4,
maximumSignificantDigits: 4
})
for (const k in decimalFormatters) delete decimalFormatters[k] // cleanup
for (const k in fullPrecisionFormatters) delete fullPrecisionFormatters[k] // cleanup
const s = formatSigFigsWithFormatters(intFormatter, sigFigFormatter, parseFloat(unformatted), maxDecimals, code)
if (s !== expected) console.log(`TEST FAILED: f('${code}', ${unformatted}, ${maxDecimals}) => '${s}' != '${expected}'}`)
else console.log(`✔️ f('${code}', ${unformatted}, ${maxDecimals}) => ${s} ✔️`)
}
}

window.testFormatRateFullPrecision = () => {
const tests: [number, number, number, number, string][] = [
// Two utxo assets with a conventional rate of 0.15. Conventional rate
// step is 100 / 1e8 = 1e-6, so there should be 6 decimal digits.
[1.5e7, 100, 1e8, 1e8, '0.150000'],
// USDC quote -> utxo base with a rate of $10 / 1 XYZ. USDC has an
// conversion factor of 1e6, so $10 encodes to 1e7, 1 XYZ encodes to 1e8,
// encoded rate is 1e7 / 1e8 * 1e8 = 1e7, bFactor / qFactor is 1e2.
// The conventional rate step is 200 / 1e8 * 1e2 = 2e-4, so using
// rateStepDigits, we should get 4 decimal digits.
[1e7, 200, 1e6, 1e8, '10.0000'],
// Set a rate of 1 atom USDC for 0.01 BTC. That atomic rate will be 1 /
// 1e6 = 1e-6. The encoded rate will be 1e-6 * 1e8 = 1e2. As long as our
// rate step divides evenly into 100, this should work. The conventional
// rate is 1e-6 / 1e-2 = 1e-4, so expect 4 decimal digits.
[1e2, 100, 1e6, 1e8, '0.0001'],
// DCR-ETH, expect 6 decimals.
[1.5e7, 1000, 1e9, 1e8, '0.015000'],
[1e6, 1000, 1e9, 1e8, '0.001000'],
[1e3, 1000, 1e9, 1e8, '0.000001'],
[100001000, 1000, 1e9, 1e8, '0.100001'],
[1000001000, 1000, 1e9, 1e8, '1.000001'],
// DCR-USDC, expect 3 decimals.
[1.5e7, 1000, 1e6, 1e8, '15.000'],
[1e6, 1000, 1e6, 1e8, '1.000'],
[1e3, 1000, 1e6, 1e8, '0.001'],
[101000, 1000, 1e6, 1e8, '0.101'],
[1001000, 1000, 1e6, 1e8, '1.001'],
// UTXO assets but with a rate step that's not a perfect power of 10.
// For a rate step of 500, a min rate would be e.g. rate step = 500.
// 5e2 / 1e8 = 5e-6 = 0.000005
[5e2, 500, 1e8, 1e8, '0.000005']
]

for (const [encRate, rateStep, qFactor, bFactor, expEncoding] of tests) {
for (const k in fullPrecisionFormatters) delete fullPrecisionFormatters[k] // cleanup

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to clean up fullPrecisionFormatters because we might have wrong locales there due to other tests running previously, which breaks tests depending on run order

const bui = { conventional: { conversionFactor: bFactor } } as any as UnitInfo
const qui = { conventional: { conversionFactor: qFactor } } as any as UnitInfo
const enc = Doc.formatRateFullPrecision(encRate, bui, qui, rateStep)
if (enc !== expEncoding) console.log(`TEST FAILED: f(${encRate}, ${bFactor}, ${qFactor}, ${rateStep}) => ${enc} != ${expEncoding}`)
else console.log(`✔️ f(${encRate}, ${bFactor}, ${qFactor}, ${rateStep}) => ${enc} ✔️`)
}
}
}
Loading