mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Color fires from ember to white-hot instead of blue to red
About 90% of detections are under 20 MW, so the old blue and teal stops painted most of the map in cold colors that do not read as fire. The ramp now runs ember, red, flame, orange, gold and white-hot at the same thresholds, each stop lighter than the one below, so the brightest marks are the strongest fires. The heatmap uses the same colors.
This commit is contained in:
1 parent
02953665de
commit
18d55f58b9
12 files changed
+115
-87
No files matched your search
@@ -1,9 +1,9 @@
|
||||
- Color every Active Fires detection by fire radiative power on a six-stop
|
||||
cold → hot ramp (blue · teal · lime · amber · orange · red at 0 · 5 · 20 ·
|
||||
50 · 100 · 300 MW), and add a log-scale minimum-FRP slider whose track is the
|
||||
ramp, an All / Nominal+ / High confidence switch and an "N of M shown"
|
||||
counter to the layer row. Far zoom shows a Snap Map–style heatmap instead
|
||||
of squares, empty where nothing burns. The filter applies once before
|
||||
rendering, so markers, cards, the heatmap, context and the top-fires query agree; share
|
||||
ember → white-hot ramp (ember · red · flame · orange · gold · white-hot at
|
||||
0 · 5 · 20 · 50 · 100 · 300 MW, brighter as power rises), and add a
|
||||
log-scale minimum-FRP slider whose track is the ramp, an All / Nominal+ /
|
||||
High confidence switch and an "N of M shown" counter to the layer row. Far
|
||||
zoom shows a Snap Map–style heatmap in the same colors instead of squares,
|
||||
empty where nothing burns. The filter applies once before rendering, so markers, cards, the heatmap, context and the top-fires query agree; share
|
||||
links keep it (`w` token: `f`, `c`), and `get_active_fires` takes matching
|
||||
`min_frp_mw` and `confidence` arguments. Default 0 MW / All (gekh, #1426).
|
||||
+10
-7
@@ -2175,19 +2175,22 @@ accessible name remains "Don't show this again". Control behavior is unchanged.
|
||||
|
||||
## Active Fires intensity filter and color ramp
|
||||
|
||||
Every FIRMS detection is colored by fire radiative power on a six-stop cold →
|
||||
hot ramp: blue from 0 MW, teal from 5, lime from 20, amber from 50, orange from
|
||||
100 and red from 300 MW (`FRP_RAMP` in `src/data/firmsIntensity.js`). Glow
|
||||
sprites and fire-card accents read that ramp; confidence and brightness do
|
||||
not affect the color.
|
||||
Every FIRMS detection is colored by fire radiative power on a six-stop ember →
|
||||
white-hot ramp: dark ember from 0 MW, red from 5, flame from 20, orange from
|
||||
50, gold from 100 and pale white-hot from 300 MW (`FRP_RAMP` in
|
||||
`src/data/firmsIntensity.js`). Each stop is lighter than the one below and
|
||||
none is a cold hue, so the brightest marks are the strongest fires; ~90% of
|
||||
detections are under 20 MW and stay dim red. Glow sprites and fire-card
|
||||
accents read that ramp; confidence and brightness do not affect the color.
|
||||
|
||||
At far zoom (camera above ~3,000 km) the layer drapes a Snap Map–style
|
||||
heatmap instead of individual markers: the shown detections are binned on a
|
||||
global 2048 × 1024 grid, each weighted by its FRP in megawatts (so heat is
|
||||
radiated power, and raising the FRP slider does not dim areas where strong fires
|
||||
burn), Gaussian-blurred,
|
||||
normalized on a log scale against the 99.5th percentile and mapped from blue
|
||||
through cyan, green, yellow and orange to red. Nothing is painted where
|
||||
normalized on a log scale against the 99.5th percentile and mapped through the
|
||||
same colors, from a faint ember edge through red, orange and gold to a pale
|
||||
white-hot core. Nothing is painted where
|
||||
nothing burns (`src/data/firmsHeatRaster.js`). It is one full-globe imagery
|
||||
tile on the active imagery host (`src/layers/firms/heatImagery.js`), rebuilt
|
||||
on each data refresh or filter change (the first paint at once, later ones
|
||||
|
||||
@@ -45,7 +45,7 @@ test('buildFireCard: title carries FRP, detail carries conf/age/satellite', () =
|
||||
assert.equal(card.details.length, 1);
|
||||
assert.equal(card.details[0], 'high · 2h · N20');
|
||||
assert.equal(card.selected, false);
|
||||
assert.equal(card.accent, accentForSeverity('red'), 'FRP 1520 is red-hot');
|
||||
assert.equal(card.accent, accentForSeverity('white-hot'), 'FRP 1520 is white-hot');
|
||||
assert.deepEqual(card.position, { x: 1, y: 2, z: 3 }, 'uses the candidate position untouched');
|
||||
});
|
||||
|
||||
@@ -69,26 +69,26 @@ test('buildFireCard: power that rounds to 10 MW drops the decimal', () => {
|
||||
assert.equal(selected.title, 'FIRE · 10 MW');
|
||||
});
|
||||
|
||||
test('buildFireCard: SNPP satellite code renders as SNPP, weak fire is blue', () => {
|
||||
test('buildFireCard: SNPP satellite code renders as SNPP, weak fire is an ember', () => {
|
||||
const card = buildFireCard({ fire: fire({ satellite: 'N', frp: 0.8, confidence: 0.3 }), position: {} }, NOW);
|
||||
assert.match(card.details[0], /SNPP$/);
|
||||
assert.equal(card.title, '▲ 0.8 MW');
|
||||
assert.equal(card.accent, accentForSeverity('blue'));
|
||||
assert.equal(card.accent, accentForSeverity('ember'));
|
||||
});
|
||||
|
||||
test('buildFireCard: accent steps through the FRP ramp at 5 · 20 · 50 · 100 · 300 MW', () => {
|
||||
const accentAt = (frp) =>
|
||||
buildFireCard({ fire: fire({ frp }), position: {} }, NOW).accent;
|
||||
assert.equal(accentAt(4.9), accentForSeverity('blue'));
|
||||
assert.equal(accentAt(5), accentForSeverity('teal'));
|
||||
assert.equal(accentAt(20), accentForSeverity('lime'));
|
||||
assert.equal(accentAt(50), accentForSeverity('amber'));
|
||||
assert.equal(accentAt(100), accentForSeverity('orange'));
|
||||
assert.equal(accentAt(300), accentForSeverity('red'));
|
||||
assert.equal(accentAt(4.9), accentForSeverity('ember'));
|
||||
assert.equal(accentAt(5), accentForSeverity('red'));
|
||||
assert.equal(accentAt(20), accentForSeverity('flame'));
|
||||
assert.equal(accentAt(50), accentForSeverity('orange'));
|
||||
assert.equal(accentAt(100), accentForSeverity('gold'));
|
||||
assert.equal(accentAt(300), accentForSeverity('white-hot'));
|
||||
// Confidence never moves the color: a low-confidence 864 MW fire stays red.
|
||||
assert.equal(
|
||||
buildFireCard({ fire: fire({ frp: 864, confidence: 0.3 }), position: {} }, NOW).accent,
|
||||
accentForSeverity('red'),
|
||||
accentForSeverity('white-hot'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -109,12 +109,12 @@ test('buildCellCard: plural noun, max FRP and newest age, accent passthrough', (
|
||||
const candidate = {
|
||||
cell: { count: 14, maxFrp: 210.4, newestAcqMs: NOW - 3 * H, night: 2 },
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
accent: accentForSeverity('orange'),
|
||||
accent: accentForSeverity('gold'),
|
||||
};
|
||||
const card = buildCellCard(candidate, NOW);
|
||||
assert.equal(card.title, '14 FIRES');
|
||||
assert.equal(card.details[0], 'max 210 MW · new 3h');
|
||||
assert.equal(card.accent, accentForSeverity('orange'));
|
||||
assert.equal(card.accent, accentForSeverity('gold'));
|
||||
});
|
||||
|
||||
test('buildCellCard: the count picks the plural form of the language', () => {
|
||||
@@ -166,7 +166,7 @@ test('buildCellCard: singular noun and missing-age omission', () => {
|
||||
const card = buildCellCard(candidate, NOW);
|
||||
assert.equal(card.title, '1 FIRE');
|
||||
assert.equal(card.details[0], 'max 9.9 MW');
|
||||
assert.equal(card.accent, accentForSeverity('blue'), 'missing accent defaults to the coldest stop');
|
||||
assert.equal(card.accent, accentForSeverity('ember'), 'missing accent defaults to the weakest stop');
|
||||
});
|
||||
|
||||
test('FIRMS host policy keeps ambient cards bounded and selected cards protected', () => {
|
||||
|
||||
@@ -8,14 +8,18 @@
|
||||
/** Below this normalized heat nothing is painted: empty land stays empty. */
|
||||
export const HEAT_FLOOR = 0.03;
|
||||
|
||||
/**
|
||||
* The FRP marker ramp as a continuous glow: faint ember at the edges, a
|
||||
* pale white-hot core where the most power burns.
|
||||
*/
|
||||
export const HEAT_PALETTE = Object.freeze([
|
||||
{ at: 0.0, rgb: [43, 108, 255], alpha: 0 },
|
||||
{ at: 0.08, rgb: [43, 108, 255], alpha: 0.35 },
|
||||
{ at: 0.25, rgb: [25, 195, 255], alpha: 0.55 },
|
||||
{ at: 0.45, rgb: [47, 224, 122], alpha: 0.65 },
|
||||
{ at: 0.65, rgb: [245, 230, 66], alpha: 0.75 },
|
||||
{ at: 0.82, rgb: [255, 138, 31], alpha: 0.82 },
|
||||
{ at: 1.0, rgb: [255, 42, 26], alpha: 0.88 },
|
||||
{ at: 0.0, rgb: [158, 42, 27], alpha: 0 },
|
||||
{ at: 0.08, rgb: [158, 42, 27], alpha: 0.35 },
|
||||
{ at: 0.25, rgb: [210, 64, 31], alpha: 0.55 },
|
||||
{ at: 0.45, rgb: [240, 106, 40], alpha: 0.65 },
|
||||
{ at: 0.65, rgb: [251, 154, 46], alpha: 0.75 },
|
||||
{ at: 0.82, rgb: [253, 200, 74], alpha: 0.82 },
|
||||
{ at: 1.0, rgb: [255, 243, 176], alpha: 0.9 },
|
||||
]);
|
||||
|
||||
export function heatColorAt(t) {
|
||||
|
||||
@@ -11,16 +11,20 @@ import {
|
||||
|
||||
const fire = (lat, lon, frp = 10) => ({ lat, lon, frp });
|
||||
|
||||
test('palette runs cold to hot and starts fully transparent', () => {
|
||||
test('palette runs ember to white-hot and starts fully transparent', () => {
|
||||
assert.equal(HEAT_PALETTE[0].at, 0);
|
||||
assert.equal(HEAT_PALETTE.at(-1).at, 1);
|
||||
assert.deepEqual(heatColorAt(0), [0, 0, 0, 0]);
|
||||
assert.equal(heatColorAt(0.01)[3], 0, 'below the floor nothing is painted');
|
||||
const low = heatColorAt(0.15);
|
||||
const high = heatColorAt(1);
|
||||
assert.ok(low[2] > low[0], 'low heat is blue');
|
||||
assert.ok(high[0] > high[2], 'the core is red');
|
||||
assert.ok(low[0] > low[1] && low[1] > low[2], 'low heat is an ember red');
|
||||
assert.ok(high[1] > low[1] && high[2] > low[2], 'the core is paler');
|
||||
assert.ok(high[3] > low[3], 'hotter is more opaque');
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const [r, , b] = heatColorAt(i / 100);
|
||||
assert.ok(r >= b, `no cold hue at ${i}%`);
|
||||
}
|
||||
});
|
||||
|
||||
test('binFires drops each detection into its equirectangular pixel', () => {
|
||||
|
||||
+15
-12
@@ -1,28 +1,31 @@
|
||||
/**
|
||||
* @module firmsIntensity
|
||||
* @description Fire intensity policy shared by the Active Fires layer and the
|
||||
* agent fire tool: the six-stop cold → hot FRP color ramp, the log-scale
|
||||
* agent fire tool: the six-stop ember → white-hot FRP color ramp, the log-scale
|
||||
* slider that doubles as its legend, and the one filter every consumer
|
||||
* (markers, cards, heat cells, the top-fires query, `get_active_fires`) runs
|
||||
* before anything is rendered or selected. Pure — no Cesium, no DOM.
|
||||
*
|
||||
* Hue order follows the Portolan FIRMS explorer (`FRP_RAMP`, stepped, not
|
||||
* interpolated). The thresholds are GEV's own, set on a real week's
|
||||
* percentiles so every color appears: Portolan's 0/10/50/200/500/1000 MW
|
||||
* leaves one red fire a week (HAL-675).
|
||||
* The ramp (`FRP_RAMP`, stepped, not interpolated) runs like a fire cooling
|
||||
* down: a dim ember for weak fires through red, orange and gold to a pale
|
||||
* white-hot core, lightness rising with power, so the brightest marks are the
|
||||
* strongest fires. No cold hues: ~90% of detections are under 20 MW and a
|
||||
* blue start painted most of the map blue. The thresholds are GEV's own, set
|
||||
* on a real week's percentiles so every color appears: Portolan's
|
||||
* 0/10/50/200/500/1000 MW leaves one hottest fire a week (HAL-675).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Stepped FRP ramp, coldest first. A detection takes the LAST stop whose
|
||||
* Stepped FRP ramp, weakest first. A detection takes the LAST stop whose
|
||||
* `minFrp` it reaches. Names key the glow-sprite cache and the card accents.
|
||||
*/
|
||||
export const FRP_RAMP = Object.freeze([
|
||||
Object.freeze({ name: 'blue', minFrp: 0, hex: '#4a5bd4' }),
|
||||
Object.freeze({ name: 'teal', minFrp: 5, hex: '#39a0a8' }),
|
||||
Object.freeze({ name: 'lime', minFrp: 20, hex: '#c9cf4a' }),
|
||||
Object.freeze({ name: 'amber', minFrp: 50, hex: '#f2b134' }),
|
||||
Object.freeze({ name: 'orange', minFrp: 100, hex: '#e8722c' }),
|
||||
Object.freeze({ name: 'red', minFrp: 300, hex: '#d1382a' }),
|
||||
Object.freeze({ name: 'ember', minFrp: 0, hex: '#9e2a1b' }),
|
||||
Object.freeze({ name: 'red', minFrp: 5, hex: '#d2401f' }),
|
||||
Object.freeze({ name: 'flame', minFrp: 20, hex: '#f06a28' }),
|
||||
Object.freeze({ name: 'orange', minFrp: 50, hex: '#fb9a2e' }),
|
||||
Object.freeze({ name: 'gold', minFrp: 100, hex: '#fdc84a' }),
|
||||
Object.freeze({ name: 'white-hot', minFrp: 300, hex: '#fff3b0' }),
|
||||
]);
|
||||
|
||||
/** Slider ticks: the ramp thresholds, so slider, legend and markers agree. */
|
||||
|
||||
@@ -18,40 +18,54 @@ import {
|
||||
sliderPositionForFrp,
|
||||
} from './firmsIntensity.js';
|
||||
|
||||
test('the ramp has six cold → hot stops at 0 · 5 · 20 · 50 · 100 · 300 MW in Portolan hue order', () => {
|
||||
test('the ramp has six ember → white-hot stops at 0 · 5 · 20 · 50 · 100 · 300 MW', () => {
|
||||
assert.deepEqual(
|
||||
FRP_RAMP.map((stop) => [stop.name, stop.minFrp, stop.hex]),
|
||||
[
|
||||
['blue', 0, '#4a5bd4'],
|
||||
['teal', 5, '#39a0a8'],
|
||||
['lime', 20, '#c9cf4a'],
|
||||
['amber', 50, '#f2b134'],
|
||||
['orange', 100, '#e8722c'],
|
||||
['red', 300, '#d1382a'],
|
||||
['ember', 0, '#9e2a1b'],
|
||||
['red', 5, '#d2401f'],
|
||||
['flame', 20, '#f06a28'],
|
||||
['orange', 50, '#fb9a2e'],
|
||||
['gold', 100, '#fdc84a'],
|
||||
['white-hot', 300, '#fff3b0'],
|
||||
],
|
||||
);
|
||||
assert.deepEqual([...FRP_SLIDER_TICKS], [0, 5, 20, 50, 100, 300]);
|
||||
assert.ok(Object.isFrozen(FRP_RAMP));
|
||||
});
|
||||
|
||||
test('every stop is warm and each is lighter than the one below', () => {
|
||||
const luma = ([r, g, b]) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
let previous = -1;
|
||||
for (const stop of FRP_RAMP) {
|
||||
const rgb = hexToRgb(stop.hex);
|
||||
assert.ok(rgb[0] >= rgb[2], `${stop.name} has no cold hue`);
|
||||
assert.ok(
|
||||
luma(rgb) > previous,
|
||||
`${stop.name} is lighter than the stop below`,
|
||||
);
|
||||
previous = luma(rgb);
|
||||
}
|
||||
});
|
||||
|
||||
test('a detection takes the last stop it reaches; garbage reads as 0 MW', () => {
|
||||
assert.equal(frpRampStop(0).name, 'blue');
|
||||
assert.equal(frpRampStop(4.99).name, 'blue');
|
||||
assert.equal(frpRampStop(5).name, 'teal');
|
||||
assert.equal(frpRampStop(19.9).name, 'teal');
|
||||
assert.equal(frpRampStop(20).name, 'lime');
|
||||
assert.equal(frpRampStop(50).name, 'amber');
|
||||
assert.equal(frpRampStop(100).name, 'orange');
|
||||
assert.equal(frpRampStop(299.9).name, 'orange');
|
||||
assert.equal(frpRampStop(300).name, 'red');
|
||||
assert.equal(frpRampStop(1246).name, 'red');
|
||||
assert.equal(frpRampStop(NaN).name, 'blue');
|
||||
assert.equal(frpRampStop(-3).name, 'blue');
|
||||
assert.equal(frpRampStop(undefined).name, 'blue');
|
||||
assert.equal(frpRampStop(0).name, 'ember');
|
||||
assert.equal(frpRampStop(4.99).name, 'ember');
|
||||
assert.equal(frpRampStop(5).name, 'red');
|
||||
assert.equal(frpRampStop(19.9).name, 'red');
|
||||
assert.equal(frpRampStop(20).name, 'flame');
|
||||
assert.equal(frpRampStop(50).name, 'orange');
|
||||
assert.equal(frpRampStop(100).name, 'gold');
|
||||
assert.equal(frpRampStop(299.9).name, 'gold');
|
||||
assert.equal(frpRampStop(300).name, 'white-hot');
|
||||
assert.equal(frpRampStop(1246).name, 'white-hot');
|
||||
assert.equal(frpRampStop(NaN).name, 'ember');
|
||||
assert.equal(frpRampStop(-3).name, 'ember');
|
||||
assert.equal(frpRampStop(undefined).name, 'ember');
|
||||
});
|
||||
|
||||
test('hex stops convert to the "r, g, b" accents the cards paint', () => {
|
||||
assert.deepEqual(hexToRgb('#4a5bd4'), [74, 91, 212]);
|
||||
assert.deepEqual(hexToRgb('#9e2a1b'), [158, 42, 27]);
|
||||
assert.deepEqual(hexToRgb('d1382a'), [209, 56, 42]);
|
||||
});
|
||||
|
||||
@@ -84,9 +98,9 @@ test('the log slider pins 0 MW at the left and runs 1 → 1000 MW; values round-
|
||||
test('the track gradient is the stepped ramp on the log axis, so it doubles as the legend', () => {
|
||||
const gradient = rampTrackGradient();
|
||||
assert.match(gradient, /^linear-gradient\(90deg, /);
|
||||
assert.match(gradient, /#4a5bd4 0\.00% 23\.3\d%/);
|
||||
assert.match(gradient, /#39a0a8 23\.3\d% 43\.3\d%/);
|
||||
assert.match(gradient, /#d1382a 82\.5\d% 100\.00%\)$/);
|
||||
assert.match(gradient, /#9e2a1b 0\.00% 23\.3\d%/);
|
||||
assert.match(gradient, /#d2401f 23\.3\d% 43\.3\d%/);
|
||||
assert.match(gradient, /#fff3b0 82\.5\d% 100\.00%\)$/);
|
||||
assert.equal(gradient.match(/#/g).length, FRP_RAMP.length);
|
||||
});
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ const ACCENT_RGB = Object.freeze(
|
||||
);
|
||||
|
||||
/**
|
||||
* Severity stop name → "r, g, b" accent string (defaults to the coldest stop).
|
||||
* @param {string} stopName - A FRP_RAMP stop name ('blue' … 'red').
|
||||
* Severity stop name → "r, g, b" accent string (defaults to the weakest stop).
|
||||
* @param {string} stopName - A FRP_RAMP stop name ('ember' … 'white-hot').
|
||||
* @returns {string}
|
||||
*/
|
||||
export function accentForSeverity(stopName) {
|
||||
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
formatFrpMw,
|
||||
} from './firmsLabels.js';
|
||||
|
||||
test('FIRMS card accents are the six FRP ramp stops, coldest as the fallback', () => {
|
||||
// Portolan hue order; blue #4a5bd4 … red #d1382a (HAL-675).
|
||||
assert.equal(accentForSeverity('blue'), '74, 91, 212');
|
||||
assert.equal(accentForSeverity('teal'), '57, 160, 168');
|
||||
assert.equal(accentForSeverity('lime'), '201, 207, 74');
|
||||
assert.equal(accentForSeverity('amber'), '242, 177, 52');
|
||||
assert.equal(accentForSeverity('orange'), '232, 114, 44');
|
||||
assert.equal(accentForSeverity('red'), '209, 56, 42');
|
||||
assert.equal(accentForSeverity('yellow'), accentForSeverity('blue'));
|
||||
assert.equal(accentForSeverity('chartreuse'), accentForSeverity('blue'));
|
||||
test('FIRMS card accents are the six FRP ramp stops, weakest as the fallback', () => {
|
||||
// Ember #9e2a1b … white-hot #fff3b0, lightness rising with power.
|
||||
assert.equal(accentForSeverity('ember'), '158, 42, 27');
|
||||
assert.equal(accentForSeverity('red'), '210, 64, 31');
|
||||
assert.equal(accentForSeverity('flame'), '240, 106, 40');
|
||||
assert.equal(accentForSeverity('orange'), '251, 154, 46');
|
||||
assert.equal(accentForSeverity('gold'), '253, 200, 74');
|
||||
assert.equal(accentForSeverity('white-hot'), '255, 243, 176');
|
||||
assert.equal(accentForSeverity('blue'), accentForSeverity('ember'));
|
||||
assert.equal(accentForSeverity('chartreuse'), accentForSeverity('ember'));
|
||||
});
|
||||
|
||||
test('FIRMS power that rounds to 10 MW drops the decimal', () => {
|
||||
|
||||
@@ -121,7 +121,7 @@ export function createModel({
|
||||
}
|
||||
|
||||
/**
|
||||
* The ramp stop for a fire radiative power (stepped, coldest first).
|
||||
* The ramp stop for a fire radiative power (stepped, weakest first).
|
||||
* Intensity alone drives the color: confidence feeds the card text and the
|
||||
* switch, brightness is unreliable (saturates at 367 K) — see HAL-675.
|
||||
* @param {number} frp - Megawatts.
|
||||
|
||||
@@ -111,7 +111,7 @@ export const CULL_LIFT_M = 12;
|
||||
|
||||
/**
|
||||
* Color stops for detection glow sprites and card accents: the
|
||||
* six-stop cold → hot FRP ramp (firmsIntensity.js), coldest first, as Cesium
|
||||
* six-stop ember → white-hot FRP ramp (firmsIntensity.js), weakest first, as Cesium
|
||||
* colors. Stop names key the glow-sprite cache and the card accents.
|
||||
*/
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const fireRow = (value, active = 'all') => ({
|
||||
max: 1000,
|
||||
step: 1,
|
||||
value,
|
||||
track: 'linear-gradient(90deg, #4a5bd4, #d1382a)',
|
||||
track: 'linear-gradient(90deg, #9e2a1b, #fff3b0)',
|
||||
ticks: [
|
||||
{ at: 0, label: '0' },
|
||||
{ at: 0.5, label: '30' },
|
||||
|
||||
Reference in new issue
Block a user