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
20 changes: 20 additions & 0 deletions examples/cdn/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "cdn",
"private": true,
"version": "5.3.0",
"description": "A bundler-free example app which loads `@perspective-dev/viewer` from `<script type=\"module\">` tags.",
"type": "module",
"scripts": {
"start": "node server.mjs"
},
"keywords": [],
"license": "Apache-2.0",
"dependencies": {
"@perspective-dev/client": "workspace:",
"@perspective-dev/server": "workspace:",
"@perspective-dev/viewer": "workspace:",
"@perspective-dev/viewer-charts": "workspace:",
"@perspective-dev/viewer-datagrid": "workspace:",
"superstore-arrow": "catalog:"
}
}
19 changes: 19 additions & 0 deletions examples/cdn/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import { WebSocketServer } from "@perspective-dev/client";
import { fileURLToPath } from "url";
import { dirname } from "path";

const __dirname = dirname(fileURLToPath(import.meta.url));

new WebSocketServer({ assets: [__dirname, "dist", "../../"] });
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
border-radius: 3px;
padding: 3px;
overflow-y: auto;
white-space: pre;
z-index: 10;
line-height: 16px;
}
9 changes: 2 additions & 7 deletions packages/viewer-charts/src/ts/axis/bar-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import type { Canvas2D, Context2D } from "../charts/canvas-types";
import { PlotLayout } from "../layout/plot-layout";
import { formatTickValue, formatDateTickValue } from "../layout/ticks";
import { stepTickFormatter } from "../layout/ticks";
import { initCanvas } from "./canvas";
import {
renderCategoricalXTicks,
Expand All @@ -37,12 +37,7 @@ function tickFormatter(
return override;
}

if (!domain.isDate) {
return formatTickValue;
}

const step = ticks.length > 1 ? ticks[1] - ticks[0] : 0;
return (v: number) => formatDateTickValue(v, step);
return stepTickFormatter(domain.isDate, ticks);
}

/**
Expand Down
11 changes: 2 additions & 9 deletions packages/viewer-charts/src/ts/axis/numeric-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@

import type { Canvas2D, Context2D } from "../charts/canvas-types";
import { PlotLayout, type PlotRect } from "../layout/plot-layout";
import {
computeNiceTicks,
formatTickValue,
formatDateTickValue,
} from "../layout/ticks";
import { computeNiceTicks, stepTickFormatter } from "../layout/ticks";
import { getScaledContext } from "./canvas";
import {
drawGridlinesX,
Expand Down Expand Up @@ -81,10 +77,7 @@ function tickFmt(
return override;
}

const step = ticks.length > 1 ? ticks[1] - ticks[0] : 0;
return domain.isDate
? (v: number) => formatDateTickValue(v, step)
: formatTickValue;
return stepTickFormatter(domain.isDate, ticks);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import type { CandlestickChart } from "./candlestick";
import { renderCandlestickChromeOverlay } from "./candlestick-render";
import { tooltipStyleOf } from "../../interaction/tooltip-grid";

/**
* Pixels of horizontal slack around the wick centerline so narrow
Expand Down Expand Up @@ -163,24 +164,24 @@ export function showCandlestickPinnedTooltip(
return;
}

const lines = buildCandlestickTooltipLines(chart, idx);
if (lines.length === 0) {
const grid = buildCandlestickTooltipLines(chart, idx);
if (grid.length === 0) {
return;
}

const xCenter = candles.xCenter[idx];
const yMid = (candles.high[idx] + candles.low[idx]) / 2;
const pos = chart._lastLayout.dataToPixel(xCenter, yMid);

// CSS bounds come from the chart's own layout, which is populated
// by the render path regardless of where the chart runs.
const cssWidth = chart._lastLayout.cssWidth;
const cssHeight = chart._lastLayout.cssHeight;

chart._tooltip.pin(lines, pos, { cssWidth, cssHeight });
chart._tooltip.pin(
grid,
pos,
{ cssWidth, cssHeight },
tooltipStyleOf(chart._pluginConfig),
);

// Pinning hides the inline hover tooltip but does not change the
// WebGL pass — only the chrome overlay needs to redraw.
chart._hoveredIdx = -1;
renderCandlestickChromeOverlay(chart);
}
Expand All @@ -190,19 +191,14 @@ export function dismissCandlestickPinnedTooltip(chart: CandlestickChart): void {
chart._pinnedIdx = -1;
}

/**
* Build tooltip lines for candle at index `idx` in the columnar
* storage. Indexed access avoids materializing a `CandleRecord` POJO
* on the hot tooltip path.
*/
export function buildCandlestickTooltipLines(
chart: CandlestickChart,
idx: number,
): string[] {
const lines: string[] = [];
): string[][] {
const grid: string[][] = [];
const candles = chart._candles;
if (idx < 0 || idx >= candles.count) {
return lines;
return grid;
}

const catIdx = candles.catIdx[idx];
Expand All @@ -212,15 +208,45 @@ export function buildCandlestickTooltipLines(
const high = candles.high[idx];
const low = candles.low[idx];

const categoryText = candleCategoryText(chart, catIdx);
if (categoryText !== "") {
grid.push([categoryText]);
}

if (splitIdx >= 0 && chart._splitPrefixes.length > 1) {
const prefix = chart._splitPrefixes[splitIdx];
if (prefix) {
grid.push([prefix]);
}
}

const openFmt = chart.getColumnFormatter(chart._columnSlots[0], "value");
const closeFmt = chart.getColumnFormatter(chart._columnSlots[1], "value");
const highFmt = chart.getColumnFormatter(chart._columnSlots[2], "value");
const lowFmt = chart.getColumnFormatter(chart._columnSlots[3], "value");
grid.push(["Open", openFmt(open)]);
grid.push(["Close", closeFmt(close)]);
grid.push(["High", highFmt(high)]);
grid.push(["Low", lowFmt(low)]);

return grid;
}

export function candleCategoryText(
chart: CandlestickChart,
catIdx: number,
): string {
if (
chart._categoryAxisMode === "numeric" &&
chart._numericCategoryDomain &&
chart._categoryPositions
) {
const v = chart._categoryPositions[catIdx];
const xColumn = chart._groupBy[0];
lines.push(chart.getColumnFormatter(xColumn, "value")(v));
} else if (chart._rowPaths.length > 0) {
return chart.getColumnFormatter(xColumn, "value")(v);
}

if (chart._rowPaths.length > 0) {
const parts: string[] = [];
for (const rp of chart._rowPaths) {
const s = rp.labels[catIdx] ?? "";
Expand All @@ -229,28 +255,8 @@ export function buildCandlestickTooltipLines(
}
}

if (parts.length > 0) {
lines.push(parts.join(" › "));
}
} else {
lines.push(`Row ${catIdx + chart._rowOffset}`);
}

if (splitIdx >= 0 && chart._splitPrefixes.length > 1) {
const prefix = chart._splitPrefixes[splitIdx];
if (prefix) {
lines.push(prefix);
}
return parts.join(" › ");
}

const openFmt = chart.getColumnFormatter(chart._columnSlots[0], "value");
const closeFmt = chart.getColumnFormatter(chart._columnSlots[1], "value");
const highFmt = chart.getColumnFormatter(chart._columnSlots[2], "value");
const lowFmt = chart.getColumnFormatter(chart._columnSlots[3], "value");
lines.push(`Open: ${openFmt(open)}`);
lines.push(`Close: ${closeFmt(close)}`);
lines.push(`High: ${highFmt(high)}`);
lines.push(`Low: ${lowFmt(low)}`);

return lines;
return `Row ${catIdx + chart._rowOffset}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ import { PlotLayout } from "../../layout/plot-layout";
import { sampleGradient } from "../../theme/gradient";
import { renderInPlotFrame } from "../../webgl/plot-frame";
import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
import { computeNiceTicks } from "../../layout/ticks";
import { tooltipStyleOf } from "../../interaction/tooltip-grid";
import {
renderAxisHoverIndicators,
type AxisIndicator,
} from "../../interaction/axis-indicators";
import { computeNiceTicks, stepTickFormatter } from "../../layout/ticks";
import { type AxisDomain } from "../../axis/numeric-axis";
import {
renderBarAxesChrome,
Expand All @@ -27,7 +32,10 @@ import {
measureCategoricalAxisHeight,
type CategoricalDomain,
} from "../../axis/categorical-axis";
import { buildCandlestickTooltipLines } from "./candlestick-interact";
import {
buildCandlestickTooltipLines,
candleCategoryText,
} from "./candlestick-interact";
import {
computeVisibleExtent,
type VisibleExtent,
Expand Down Expand Up @@ -321,18 +329,73 @@ function renderCandlestickTooltip(chart: CandlestickChart): void {
const pos = layout.dataToPixel(xCenter, yMid);
const lines = buildCandlestickTooltipLines(chart, i);
const theme = chart._resolveTheme();
renderCanvasTooltip(
const dpr = chart._glManager?.dpr ?? 1;

let valueColumn = chart._columnSlots[1];
let value = candles.close[i];
if (valueColumn == null || !Number.isFinite(value)) {
valueColumn =
chart._columnSlots[0] ??
chart._columnSlots[2] ??
chart._columnSlots[3];
value = candles.open[i];
}

const datumPos = layout.dataToPixel(xCenter, value);
const indicators: AxisIndicator[] = [];

let categoryText: string;
if (
chart._categoryAxisMode === "numeric" &&
chart._numericCategoryDomain &&
chart._categoryPositions
) {
const catFmt =
chart.getColumnFormatter(chart._groupBy[0], "tick") ??
stepTickFormatter(
chart._numericCategoryDomain.isDate,
chart._lastCatTicks,
);
categoryText = catFmt(chart._categoryPositions[candles.catIdx[i]]);
} else {
categoryText = candleCategoryText(chart, candles.catIdx[i]);
}

if (categoryText !== "") {
indicators.push({
side: "bottom",
px: datumPos.px,
py: datumPos.py,
text: categoryText,
});
}

if (Number.isFinite(value)) {
const valueFmt =
chart.getColumnFormatter(valueColumn, "tick") ??
stepTickFormatter(chart._lastYDomain?.isDate, chart._lastYTicks);
indicators.push({
side: "left",
px: datumPos.px,
py: datumPos.py,
text: valueFmt(value),
});
}

renderAxisHoverIndicators(
chart._chromeCanvas,
pos,
lines,
layout,
theme,
chart._glManager?.dpr ?? 1,
{
crosshair: false,
highlightRadius: 0,
},
dpr,
indicators,
chart._pluginConfig.tooltip_opacity,
);

renderCanvasTooltip(chart._chromeCanvas, pos, lines, layout, theme, dpr, {
...tooltipStyleOf(chart._pluginConfig),
crosshair: false,
highlightRadius: 0,
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import type { CartesianChart } from "./cartesian";
import { renderCartesianChromeOverlay } from "./cartesian-render";
import { tooltipStyleOf } from "../../interaction/tooltip-grid";

const TOOLTIP_RADIUS_PX = 24;

Expand Down Expand Up @@ -311,7 +312,7 @@ export function showCartesianPinnedTooltip(
);

const serial = chart._lazyTooltip.beginPin();
chart.glyph.buildTooltipLines(chart, pointIdx).then((lines) => {
chart.glyph.buildTooltipLines(chart, pointIdx).then((grid) => {
// Abandon the pin if the user moved on (another pin/dismiss
// between click and resolve) or the underlying view changed.
if (!chart._lazyTooltip.isPinFresh(serial)) {
Expand All @@ -322,11 +323,16 @@ export function showCartesianPinnedTooltip(
return;
}

if (lines.length === 0) {
if (grid.length === 0) {
return;
}

chart._tooltip.pin(lines, pos, layout);
chart._tooltip.pin(
grid,
pos,
layout,
tooltipStyleOf(chart._pluginConfig),
);
});

chart._hoveredIndex = -1;
Expand Down
Loading
Loading