

本教程将从零开始构建一个真正可用的图表:安装、数据、坐标轴、交互、一条实时更新的曲线,以及一个响应数据点击的事件处理程序。大约需要十分钟,如果您不想引入构建工具链,也完全不必。
如果您已经在桌面端用过 ProEssentials,可以快速略读。 JavaScript 的属性名就是 WinForms 的属性名,事件名同样如此。第 10 步把 C# 与 JavaScript 并排放在一起,您可以看到改动有多小。
更想直接读成品代码?打开在线示例浏览器 — 每个示例都在图表旁显示自身的源码。
有两种入门方式,而您所写的页面在两种方式下完全相同,不同的只是路径。
克隆 starter 仓库 — 让图表出现在屏幕上的最短路径。库就提交在页面旁边,因此没有任何东西需要解析,也没有任何东西需要安装:
git clone https://github.com/GigasoftInc/proessentials-js-starter
npm start
这就是全部步骤。该包没有依赖,它定义的那一个脚本会启动一个小型静态服务器,原因见下文。
或者从 npm 安装,在您自己的项目中:
npm install proessentials
无论哪种方式,页面都是两个脚本标签。 第一个是引擎,一个传统脚本。第二个是您自己的代码,以模块方式加载。
<script src="proessentials.iife.js"></script>
<script type="module" src="app.js"></script>
在 app.js 中,您导入所需图表的属性接口。这是在您自己的 JavaScript 中的一次 import,而不是又一个 include:
import { attachApi, Enums as E } from './pe-api-graph.js';
它无法合并为一个标签,这并不是打包上的缺陷:属性接口是真正的 ES 模块,而传统脚本无法容纳模块。同时提供两种形式的图表库,也都在同一处分开。从 npm 安装时,这两个路径改为指向 node_modules/proessentials/dist/ — 请保留开头的 ./,因为开头的 / 也是合法 URL,在浏览器中运行完全正常,只是编辑器的补全会悄无声息地全部失效。
此外无需任何配置。 不需要打包器,不需要 import map,不需要 jsconfig.json,也不需要复制资源。库会相对于自身的脚本 URL 而非页面来解析它自己的 WebAssembly 二进制文件和内置位图,因此无论库放在哪里都能找到。编辑器补全能工作也是同样的道理:TypeScript 定义就放在它所描述的模块旁边。
请通过服务器提供页面,不要从磁盘直接打开。 file:// 形式的 URL 没有源(origin),而浏览器不会为没有源的页面获取 WebAssembly 模块 — 于是页面保持空白,但页面本身并没有任何问题。任何静态服务器都可以,上面的 npm start 就是其中之一。这一点常让从「可以直接打开」的库转过来的人受阻:没有模块、也没有任何东西需要获取的普通脚本,自然可以顺利打开。而这个需要获取一个 2.9 MB 的引擎,如果您尝试,页面会明确告诉您。
运行以上任何内容都不需要许可密钥。 没有激活,没有域名注册,也没有回传。
一个元素即可。图表会自动适配它的尺寸。
<div id="chart" style="width:800px; height:520px"></div>
引擎是 WebAssembly,因此只加载一次,且是异步的。之后的一切都是同步的属性设置。
// 1. The engine. Once per page, however many charts you go on to make //
const m = await ProEssentials();
// 2. The control. This is the WinForms designer step: dropping a Pego
// on the form. autoResize is the web Dock = Fill //
const ctl = new PeControl(document.getElementById('chart'), {
module: m,
kind: 'graph',
autoResize: true,
});
// 3. The property tree //
const Pego1 = ctl.attach(attachApi);
三个对象,各司其职。 m 是引擎,之后您不会再碰它。ctl 是控件,画布、菜单、滚动条和事件都归它所有。Pego1 是属性接口,本教程余下的内容都在这里进行。
为什么叫 Pego1? 因为在每个平台上的每一个 ProEssentials 示例里,这个对象三十年来一直叫这个名字。您可以随意命名,但如果保留这个名字,我们的示例和 AI 助手的输出都能直接粘贴进您的项目。
这是唯一值得读两遍的一步。 ProEssentials 要求按特定顺序设置属性:
先 Subsets,再 Points,再数据,再其他全部,最后 render()。
原因在于,每一次数据写入都会对 Subsets × Points 做边界检查。如果在告诉图表尺寸之前就设置数据,写入会被拒绝 — 而图表仍会用默认数据绘制出来,看上去完全说得通。这是所有平台上最常见的一个错误,一旦知道就很容易避开。
// 1. Shape first. Subsets = rows, Points = columns //
Pego1.PeData.Subsets = 2;
Pego1.PeData.Points = 6;
// 2. Then the data //
Pego1.PeData.Y[0][0] = 10; Pego1.PeData.Y[0][1] = 30;
Pego1.PeData.Y[0][2] = 20; Pego1.PeData.Y[0][3] = 40;
Pego1.PeData.Y[0][4] = 30; Pego1.PeData.Y[0][5] = 50;
Pego1.PeData.Y[1][0] = 15; Pego1.PeData.Y[1][1] = 63;
Pego1.PeData.Y[1][2] = 74; Pego1.PeData.Y[1][3] = 54;
Pego1.PeData.Y[1][4] = 25; Pego1.PeData.Y[1][5] = 34;
注意下标写法。 C# 写作 PeData.Y[s, p];JavaScript 没有二维下标,因此写作 PeData.Y[s][p]。这是整套 API 中唯一形式上的差别。
Pego1.PeString.MainTitle = "Units Sold per Month";
Pego1.PeString.SubTitle = "";
Pego1.PeString.YAxisLabel = "Units Sold";
Pego1.PeString.PointLabels[0] = "Jan";
Pego1.PeString.PointLabels[1] = "Feb";
Pego1.PeString.PointLabels[2] = "Mar";
Pego1.PeString.PointLabels[3] = "Apr";
Pego1.PeString.PointLabels[4] = "May";
Pego1.PeString.PointLabels[5] = "Jun";
Pego1.PeString.SubsetLabels[0] = "Texas";
Pego1.PeString.SubsetLabels[1] = "Florida";
坐标轴默认自动缩放。当您需要固定它时 — 在实时图表上几乎总是需要,因为自动缩放的坐标轴每一帧都会重新适配,曲线看上去像在呼吸 — 请切换为手动控制:
Pego1.PeGrid.Configure.ManualScaleControlY = E.ManualScaleControl.MinMax;
Pego1.PeGrid.Configure.ManualMinY = 0;
Pego1.PeGrid.Configure.ManualMaxY = 100;
Graph 对象只缩放 Y 轴。 它把数据点从左到右等间距绘制,因此没有需要固定的 X 刻度,也没有 ManualMinX。如果您的 X 值本身带有含义,例如时间戳或不规则的采样间隔,那属于 Scientific Graph,它以相同的名称提供完整的 X 一组属性。整套 API 始终一致;两者都可以在示例浏览器中查看。
枚举位于 E 之下,名称与 .NET 枚举完全相同。
Pego1.PePlot.Method = E.GraphPlottingMethod.Bar;
Pego1.PeGrid.LineControl = E.GridLineControl.Both;
Pego1.PeGrid.Style = E.GridStyle.Dot;
Pego1.PePlot.Option.BarGlassEffect = true;
Pego1.PePlot.DataShadows = E.DataShadows.Shadows;
缩放、滚动条、右键菜单和跟踪光标全部内置。它们是属性,不是您需要编写的代码。
// Left-drag draws a zoom box; z or the popup menu undoes it //
Pego1.PeUserInterface.Allow.Zooming = E.AllowZooming.HorzAndVert;
// Follows the data under the pointer and shows the value //
Pego1.PeUserInterface.Cursor.PromptTracking = true;
// Middle-button drag to pan //
Pego1.PeUserInterface.Scrollbar.MouseDraggingX = true;
Pego1.PeUserInterface.Scrollbar.MouseDraggingY = true;
// This enables data hot spots. Step 9 writes the handler //
Pego1.PeUserInterface.HotSpot.Data = true;
一次调用,放在最后,在所有属性都设置完之后。
ctl.render(); // last, always //
实时更新有两种形态,值得先弄清您需要哪一种。
追加 — 向不断增长的曲线添加新采样点。开销低,也是走纸式图表的通常选择。
替换 — 每一帧都交给图表一份全新的数据集。示波器每次扫描就是这样做的,任何在绘制前对数据做过滤或重算的应用程序也是如此。这是更难的情形,也正是这个引擎为之而生的情形:下面的示例在每一帧替换四个各 100,000 点的 Subset — 合计 400,000 点。
// requestAnimationFrame, not setInterval -- ask for what the display
// will take and never more, or the callbacks queue up //
const tick = () => {
Pego1.PeData.Y.load(block, 4, 100000); // values, subsets, points //
ctl.render();
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
有两个值得养成的习惯。 固定坐标轴,不要让它自动缩放,否则图表每一帧都会重新适配,曲线看起来像在呼吸。以及用 load() 把整个类型化数组一次性交给引擎,而不是逐点赋值 — 这样只跨越一次边界。
在 X 序列真实存在的 Scientific Graph 上还有第三种:当只有 Y 发生变化时设置 PeData.ReuseDataX = true,引擎就不会重建一条并未变动的 X 序列。Graph 对象没有 X 序列,因此这里不适用。
第 7 步启用了数据热点。下面就是处理程序。
ctl.PeDataHotSpot.add((sender, ev) => {
alert("Subset " + ev.subset + ", Point " + ev.point +
" with a value of " + Pego1.PeData.Y[ev.subset][ev.point]);
});
点击一根柱子。引擎会在鼠标按下时做命中测试,填好热点记录,并触发对应的事件,其中 ev.subset 和 ev.point 都已经解析完毕。您不需要挂接点击监听器,也不需要自行解析类型码。
.add 就是 C# 的 +=,.remove 就是 -=。 JavaScript 没有可以重载的运算符,因此事件以方法的形式提供。它们和 .NET 原版一样是多播的:第二次 .add 会再添加一个处理程序,而不是替换第一个。每个句柄还带有 .count 和 .clear()。
一个 Graph 带有 37 个事件,拼写与 .NET 完全一致 — PeSubsetHotSpot、PeGraphHotSpot、PeMainTitle、PeZoomIn、PeCursorMoved 等等。某个控件无法触发的事件是直接不存在,而不是留一个失效的入口:没有可订阅的句柄,因此一个永远不会触发的订阅会在写下它的那一行抛出 TypeError,而不是在运行时悄无声息。
注意接收方。 属性在 Pego1 上,事件在 ctl(也就是控件本身)上。在 WinForms 中两者都在 pego1 上。Web 端把事件留在控件上,这样即使页面从未绑定属性接口,也依然能收到事件 — 一个您只用来点击的图表,完全不需要调用 attach()。
同一个图表,写两遍。这就是这套 API 的全部主张,展示比解释更省事。
C# — WinForms、WPF 或 WinUI:
pego1.PeData.Subsets = 2;
pego1.PeData.Points = 6;
pego1.PeData.Y[0, 0] = 10;
pego1.PeString.MainTitle = "Units Sold per Month";
pego1.PePlot.Method = GraphPlottingMethod.Bar;
pego1.PeUserInterface.HotSpot.Data = true;
pego1.PeDataHotSpot += OnDataHotSpot;
pego1.PeFunction.ReinitializeResetImage();
JavaScript — 浏览器:
Pego1.PeData.Subsets = 2;
Pego1.PeData.Points = 6;
Pego1.PeData.Y[0][0] = 10;
Pego1.PeString.MainTitle = "Units Sold per Month";
Pego1.PePlot.Method = E.GraphPlottingMethod.Bar;
Pego1.PeUserInterface.HotSpot.Data = true;
ctl.PeDataHotSpot.add(onDataHotSpot);
ctl.render();
相同的属性名,相同的顺序,底层相同的引擎。差别有五处,就这么多:二维下标、枚举前缀、用 .add 代替 +=、事件的接收方,以及最后那次调用。您对 ProEssentials 的了解全部可以迁移,我们的 AI 助手所掌握的同样如此 — 因为它是基于同一套属性模型作答的。
正在迁移已有的桌面代码? 最后一行可以原样运行:Pego1.PeFunction.ReinitializeResetImage() 在 JavaScript 控件上同样存在,Reinitialize、Reset、GetRectGraph 以及 PeFunction 的其余部分也都在。它与 render() 是同一次调用,后者是我们在新的浏览器代码中使用的较短写法。