shinetek-legend
v1.2.0
Published
图例组件。用于绘制各种专题图图例或色标卡等
Readme
ShinetekLegend
图例组件,用于绘制各类专题图图例与色标卡。
支持 block(色块图例)、linear(线性渐变图例)、like-linear(类线性图例)、scale(比例尺)等多种图例类型,并提供 Canvas 与 HTML 两种渲染模式。
安装
npm install shinetek-legend快速开始
<div id="legend-container"></div>import ShinetekLegend from 'shinetek-legend'
const element = document.getElementById('legend-container')
const legendDoc = [ /* 图例数据,见下文 legendDoc 说明 */ ]
const options = { model: 'canvas' }
const legend = new ShinetekLegend(element, legendDoc, options)
legend.render()API
new ShinetekLegend(element, legendDoc, options)
创建图例实例。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| element | HTMLElement | 是 | 图例渲染的目标容器,渲染结果会作为子节点插入该容器。建议使用 div 元素 |
| legendDoc | Array<LegendSchema> | 是 | 图例配置数组,每个元素代表一组图例,结构见下文「legendDoc」 |
| options | Object | 否 | 全局渲染选项,未指定字段会使用默认值,结构见下文「Options」 |
返回 ShinetekLegend 实例,实例上暴露 render() 方法。
构造行为说明:
- 构造函数仅保存引用并合并
options与默认配置,不会立即渲染。 legendDoc与options在构造后仍被实例持有,外部修改会影响后续render()结果。- 支持在同一页面创建多个
ShinetekLegend实例,各自使用独立的容器与配置。
legend.render()
根据当前实例上的 legendDoc 与 options 重新渲染图例。
使用注意:渲染管线为纯函数式设计,不会修改传入的 legendDoc 与 options(派生尺寸通过内部 LayoutContext 传递),同一组数据可安全地多次 render() 或跨实例复用。
渲染行为:
- 重复
render()幂等:每次渲染前会自动清空容器内旧图例(mount执行innerHTML = ''),不会重复叠加。 - 根据
options.model选择 Canvas 或 HTML 渲染模式。 - 根据
options.itemAlign选择水平或垂直布局;垂直布局支持 block / linear 组(HTML 模式已验证,scale / like-linear 组会告警跳过,详见下文「Options 字段说明」)。 - 渲染完成后,图例元素会作为子节点追加到构造时传入的
element中。
const legend = new ShinetekLegend(container, legendDoc, options)
legend.render()
// 数据更新后再次调用即可,旧图例会被自动替换
legend.render()异常与边界:
legendDoc中不支持的type会抛出Error:`${group.type}: is undefined`。linear/like-linear类型要求value、color、label均为数组,且长度满足各自约束;不满足时会抛出Error。scale类型要求value为有效分辨率数值,否则比例尺长度可能为 0。- 若
options.width设为'100%',实际画布尺寸可能受父容器宽度影响,建议父容器有明确宽度。
legendDoc
legendDoc 为数组,每个元素描述一组图例,通过 type 字段区分图例类型。
组图例结构(LegendSchema)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| title | string | 否 | 该组图例的标题 |
| type | string | 是 | 图例类型:block、linear、like-linear、scale |
| list | Array<ElementSchema> | 是 | 该组图例包含的图例项 |
通用规则:
title为空字符串或缺失时,不绘制标题。list至少包含一个元素,否则该组不渲染。- 不同类型的
list元素结构不同,详见下方各类型说明。 type大小写敏感,仅支持全小写形式。
block — 色块图例
用于展示离散类别或符号,每个图例项为一块颜色或一条线。
const blockDoc = [
{
title: '行政区划',
type: 'block',
list: [
{ label: '省界', color: '#ffc107', style: 'line' },
{ label: '市界', color: '#ee1122', style: 'line' },
{ label: '陆地', color: '#11ff22', style: 'plane' },
{ label: '水体', color: '#1122ff', style: 'plane' }
]
}
]ElementSchema 字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| label | string | 是 | 图例文字 |
| color | string | 是 | 色块或线条颜色,支持 #rrggbb、rgb() 等 CSS 颜色 |
| style | string | 否 | 图例样式:line(线)、plane(面)、point(点)。默认行为取决于渲染器实现 |
说明:
style为'line'时,在色块中部绘制一条横线。style为'plane'时,在色块内部填充颜色。style为'point'时,在色块中心绘制圆点。- 当
options.showItemLabel为false时,label不绘制,但仍建议提供以保证 tooltip 行为。
linear — 线性渐变图例
用于展示连续渐变的数据范围,如温度、高程等。
const linearDoc = [
{
title: '温度',
type: 'linear',
list: [
{
value: [0, 5, 10, 15, 20, 25, 30],
label: ['0', '5', '10', '15', '20', '25', '30'],
color: ['#FFFCE8', '#004AFF', '#00E8FF', '#00FF38', '#B2FF00', '#FF9C00', '#FF1300']
}
]
}
]ElementSchema 字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| value | Array<number> | 是 | 渐变节点数值,数组长度与 label、color 保持一致 |
| label | Array<string> | 是 | 每个节点对应的文字标签 |
| color | Array<string> | 是 | 每个节点对应的颜色,按顺序形成渐变 |
约束:
value、color、label必须为数组,否则抛出Error。value.length必须等于color.length,否则抛出Error。value与label长度建议一致;不一致时仅按value长度绘制颜色,标签可能缺失。- 渐变方向固定为从左到右(
value[0]到value[last])。
like-linear — 类线性图例
数据结构同 linear,用于实现分段色标等效果。渲染时按色段分隔显示,而非平滑渐变。
const likeLinearDoc = [
{
title: '降雨量',
type: 'like-linear',
list: [
{
value: [0, 10, 25, 50, 100],
label: ['0', '10', '25', '50', '100'],
color: ['#E0F7FA', '#81D4FA', '#29B6F6', '#0288D1', '#01579B']
}
]
}
]约束:
value、color、label必须为数组,否则抛出Error。value.length必须大于color.length,否则抛出Error。value[i]与value[i+1]定义第i个色段的范围,color[i]为该色段颜色。- 每个色段使用独立边框绘制,视觉上呈现分段效果。
- 中间标签在空间不足时会被自动跳过,仅保证头尾标签显示。
scale — 比例尺
用于展示地图比例尺,根据分辨率计算并绘制对应长度的线段与标签。
const scaleDoc = [
{
title: '比例尺',
type: 'scale',
list: [{ value: 0.0025, unit: 'km' }]
}
]ElementSchema 字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| value | number | 是 | 地图分辨率(单位:度/像素),用于计算比例尺实际长度 |
| unit | string | 否 | 单位:km 或 m,默认为 km |
说明:
- 比例尺按 4 段黑白交替绘制,末段通常用于容纳单位文字。
km模式下步长会取整到0.05km的倍数,使刻度值更易读。m模式下步长直接按分辨率换算并取整。- 若容器宽度过小,比例尺会自动缩短以保证刻度值可读。
Options
全局渲染选项及其默认值:
const options = {
width: '100%', // 容器宽度
model: 'canvas', // 渲染模式
groupSpacing: 8, // 组图例之间的间距
itemAlign: 'horizontal', // 单项布局方向
itemWidth: 55, // 色块宽度(px)
itemHeight: 15, // 色块高度(px;垂直模式为色标条高度,未显式设置时默认 160)
itemSpacing: 5, // 色块间距(px)
itemFontSize: '12px', // 图例标签字号
itemFontColor: '#000000',// 图例标签颜色
itemFontFamily: 'Arial', // 图例标签字体
itemFontWeight: 'normal',// 图例标签字重
itemFontStyle: 'normal', // 图例标签字体样式
itemBorderWidth: 1, // 色块边框宽度(px)
itemBorderColor: '#000000', // 色块边框颜色
itemTextAlign: 'center', // 图例标签文字对齐
itemWordWrap: true, // 图例标签是否自动换行(仅 block 有效)
itemMaxRows: 0, // 图例标签最大行数,0 表示不限制;超出显示省略号
itemLineHeight: 1.2, // 图例标签行高(相对字号倍数)
titleFontSize: '14px', // 标题字号
titleFontColor: '#000000',// 标题颜色
titleFontFamily: 'Arial',// 标题字体
titleFontWeight: 'normal',// 标题字重
titleFontStyle: 'normal',// 标题字体样式
titleTextAlign: 'start', // 标题文字对齐
rowSpacing: 6, // 图例行间距(px)
textSpacing: 3, // 文字与色块间距(px)
titleSpacing: 5, // 标题与内容间距(px)
textRowSpacing: 1, // 文字行间距(已弃用,由 itemLineHeight 替代)
showItemLabel: true // 是否显示图例标签
}Options 字段说明
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| width | string \| number | '100%' | 容器宽度。水平模式可传 'auto' 或具体数值;垂直模式仅接受数值(px),'100%'/'auto' 回退为内容自适应宽度并告警;数值宽度小于内容宽时溢出(HTML)/裁剪(Canvas)并告警 |
| model | string | 'canvas' | 渲染模式:'canvas' 或 'html' |
| groupSpacing | number | 8 | 多组图例之间的间距(px;水平模式为垂直间距,垂直模式为水平间距) |
| itemAlign | string | 'horizontal' | 单项布局方向:'horizontal'(水平)或 'vertical'(垂直)。垂直模式:色标竖排、多分组从左到右、label 在色标右侧、数值自下而上递增;支持 block / linear 组,scale / like-linear 组告警跳过(scale 请创建独立横向实例);非法值抛错 |
| itemWidth | number | 55 | 色块宽度(px;垂直模式下为色标条宽度) |
| itemHeight | number | 15 | 色块高度(px)。垂直模式下为色标条高度,未显式设置时默认 160 |
| itemSpacing | number | 5 | 相邻图例项之间的间距(px) |
| itemFontSize | string | '12px' | 图例标签字号 |
| itemFontColor | string | '#000000' | 图例标签颜色 |
| itemFontFamily | string | 'Arial' | 图例标签字体 |
| itemFontWeight | string | 'normal' | 图例标签字重,如 'normal' / 'bold' |
| itemFontStyle | string | 'normal' | 图例标签字体样式,如 'normal' / 'italic' |
| itemBorderWidth | number | 1 | 色块边框宽度(px) |
| itemBorderColor | string | '#000000' | 色块边框颜色 |
| itemTextAlign | string | 'center' | 图例标签文字对齐:'start'、'center'、'end' |
| itemWordWrap | boolean | true | 图例标签是否自动换行(仅 block 组有效) |
| itemMaxRows | number | 0 | 图例标签最大行数,0 表示不限制;超出时末行显示省略号 |
| itemLineHeight | number | 1.2 | 图例标签行高,相对字号倍数 |
| labelWidth | number | undefined | 垂直布局 block 专用:标签换行宽度(px);未设置时跟随 itemWidth,可显式设置以解耦色标条宽度与标签宽度 |
| titleFontSize | string | '14px' | 图例标题字号 |
| titleFontColor | string | '#000000' | 图例标题颜色 |
| titleFontFamily | string | 'Arial' | 图例标题字体 |
| titleFontWeight | string | 'normal' | 标题字重,如 'normal' / 'bold' |
| titleFontStyle | string | 'normal' | 标题字体样式,如 'normal' / 'italic' |
| titleTextAlign | string | 'start' | 标题文字对齐:'start'、'center'、'end' |
| rowSpacing | number | 6 | 图例内容行间距(px) |
| textSpacing | number | 3 | 文字与色块之间的间距(px) |
| titleSpacing | number | 5 | 标题与内容之间的间距(px) |
| textRowSpacing | number | 1 | 多行文字时的行间距(已弃用,由 itemLineHeight 替代,保留仅用于向后兼容) |
| showItemLabel | boolean | true | 是否显示图例标签。支持 block、linear、like-linear、scale |
垂直布局(itemAlign: 'vertical')
专题图国际通行版式(图幅右侧竖色标):色标竖排、多分组从左到右、label 在色标右侧、数值自下而上递增。
// 色标列(竖):一个实例
new ShinetekLegend(container, legendDoc, {
model: 'html',
itemAlign: 'vertical',
itemHeight: 160, // 色标条高度(默认值,可省略)
width: 120 // 垂直模式建议显式数值 px
}).render()
// 比例尺:独立横向实例(垂直模式不支持 scale 组),放在图幅左下角
new ShinetekLegend(scaleContainer, scaleDoc, {
model: 'html',
width: 200
}).render()行为要点:
- 支持
block(分段/单值色标卡,含 line/point/plane 样式)与linear(渐变色标卡)组;scale、like-linear组渲染时告警并跳过。 - 色标条尺寸:宽度由
itemWidth控制,高度由itemHeight控制(未显式设置时默认 160);block 组每块高 = max(字高 + 2, itemHeight ÷ 类别数),类别多时总高可超过itemHeight。 - block 标签宽度:默认与
itemWidth一致;可通过labelWidth独立设置标签换行宽度,实现色标条宽度与标签宽度解耦。 width仅接受数值 px;'100%'/'auto'回退为内容自适应宽度并告警。- 导出链路已验证:HTML 模式 + html2canvas(1.4.1)栅格化无失真,验收页
demo/vertical.html。
渲染模式对比
model 选项决定图例最终如何呈现。两种模式在视觉上一致,但在 DOM 结构、样式能力、交互能力与性能特征上有明显差异。
| 特性 | Canvas 模式 | HTML 模式 |
|---|---|---|
| 输出形式 | 生成单个 <canvas> 元素 | 生成多个绝对定位的 div / span 元素 |
| DOM 节点数 | 1 个 | 与图例项、标签数量成正比 |
| 适用场景 | 截图、导出为图片、静态展示、大数据量 | 需要交互、样式覆盖、响应式调整、无障碍 |
| 样式覆盖 | 不易通过 CSS 修改 | 可通过 CSS 直接修改子元素样式 |
| 交互能力 | 无原生 hover / click,需自行实现 hit-test | 可直接为子元素绑定 hover / click / title |
| 无障碍 | 文本不可被屏幕阅读器读取 | 文本为真实 DOM 节点,可访问性较好 |
| 性能 | 渲染后无需维护 DOM 节点,重绘代价低 | 节点较多时可能触发重排,内存占用略高 |
| 文字测量 | 使用 Canvas measureText | 使用内部临时 Canvas 测量,结果与 Canvas 模式一致 |
| 尺寸自适应 | 画布位图可能因 CSS 缩放而模糊 | 文本与边框为矢量 DOM,缩放清晰 |
选择建议
- 优先选择 Canvas 模式:
- 需要将图例导出为图片或截图。
- 图例项数量较多,对渲染性能敏感。
- 不需要与图例进行交互,仅作为静态图例展示。
- 优先选择 HTML 模式:
- 需要对图例项添加 hover 提示、click 事件或右键菜单。
- 需要通过 CSS 主题或暗色模式动态调整图例样式。
- 需要屏幕阅读器可访问,或需复制图例中的文本。
- 容器尺寸会频繁变化,要求图例矢量清晰缩放。
已知限制
- 垂直模式(
itemAlign: 'vertical')支持 block / linear 组,HTML 模式已验证(含 html2canvas 导出,见demo/vertical.html);Canvas 垂直路径未专项验证(渲染时会告警提示)。scale / like-linear 组在垂直模式下告警跳过:比例尺请创建独立横向实例,like-linear 垂直化计划在下个迭代支持。 - Canvas 模式下若父容器宽度变化后需要重新渲染,需手动调用
render()并确保画布尺寸已更新。 - HTML 模式下生成的子元素使用绝对定位,父容器需设置
position: relative(组件已自动设置)。
TypeScript
类型声明文件位于 src/ts/,包含:
src/ts/legend.d.ts:ShinetekLegend.LegendSchema与ShinetekLegend.ElementSchemasrc/ts/options.d.ts:ShinetekLegend.options
说明:当前声明文件使用全局命名空间
ShinetekLegend,未随 npm 包自动注册为模块类型。建议在 TypeScript 项目中自行扩展或补全模块声明,示例见下文。
使用项目内置声明
将 src/ts/ 下的声明文件复制到你的项目类型目录,或在 tsconfig.json 中包含它们:
{
"include": [
"src/**/*",
"node_modules/shinetek-legend/src/ts/**/*.d.ts"
]
}然后使用全局命名空间类型:
const doc: ShinetekLegend.LegendSchema[] = [
{
title: '温度',
type: 'linear',
list: [
{
value: [0, 10, 20],
label: ['0', '10', '20'],
color: ['#0000ff', '#00ff00', '#ff0000']
}
]
}
]
const options: ShinetekLegend.options = {
model: 'canvas',
itemWidth: 60,
showItemLabel: true
}推荐:补全模块声明(更完整的类型支持)
在项目类型目录中新建 shinetek-legend.d.ts,扩展模块声明以获得完整的类型检查:
declare module 'shinetek-legend' {
namespace ShinetekLegend {
type LegendType = 'block' | 'linear' | 'like-linear' | 'scale'
type BlockStyle = 'line' | 'plane' | 'point'
type RenderModel = 'canvas' | 'html'
type ItemAlign = 'horizontal' | 'vertical'
type TextAlign = 'start' | 'center' | 'end'
type ScaleUnit = 'km' | 'm'
interface BaseElementSchema {
label: string | string[]
color: string | string[]
}
interface BlockElementSchema extends BaseElementSchema {
label: string
color: string
style: BlockStyle
}
interface LinearElementSchema extends BaseElementSchema {
value: number[]
label: string[]
color: string[]
}
interface ScaleElementSchema {
value: number
unit?: ScaleUnit
}
type ElementSchema = BlockElementSchema | LinearElementSchema | ScaleElementSchema
interface LegendSchema {
title?: string
type: LegendType
list: ElementSchema[]
}
interface options {
width?: string | number
model?: RenderModel
groupSpacing?: number
itemAlign?: ItemAlign
itemWidth?: number
itemHeight?: number
itemSpacing?: number
itemFontSize?: string
itemFontColor?: string
itemFontFamily?: string
itemBorderWidth?: number
itemBorderColor?: string
itemTextAlign?: TextAlign
titleFontSize?: string
titleFontColor?: string
titleFontFamily?: string
titleTextAlign?: TextAlign
rowSpacing?: number
textSpacing?: number
titleSpacing?: number
textRowSpacing?: number
showItemLabel?: boolean
}
class ShinetekLegend {
constructor(
containerElement: HTMLElement,
doc: LegendSchema[],
options?: options
)
render(): void
}
}
export = ShinetekLegend
}使用示例:
import ShinetekLegend from 'shinetek-legend'
const container = document.getElementById('legend-container')
if (!container) {
throw new Error('legend-container not found')
}
const blockDoc: ShinetekLegend.LegendSchema = {
title: '行政区划',
type: 'block',
list: [
{ label: '省界', color: '#ffc107', style: 'line' },
{ label: '市界', color: '#ee1122', style: 'line' },
{ label: '陆地', color: '#11ff22', style: 'plane' }
]
}
const linearDoc: ShinetekLegend.LegendSchema = {
title: '温度',
type: 'linear',
list: [
{
value: [0, 10, 20, 30],
label: ['0', '10', '20', '30'],
color: ['#FFFCE8', '#00E8FF', '#FF9C00', '#FF1300']
}
]
}
const scaleDoc: ShinetekLegend.LegendSchema = {
title: '比例尺',
type: 'scale',
list: [{ value: 0.0025, unit: 'km' }]
}
const options: ShinetekLegend.options = {
model: 'html',
itemWidth: 60,
itemHeight: 16,
itemSpacing: 6,
itemFontSize: '13px',
showItemLabel: true
}
const legend = new ShinetekLegend(container, [blockDoc, linearDoc, scaleDoc], options)
legend.render()注意事项
- 官方类型声明尚未完善,部分字段类型较宽松(如
string而非字面量联合类型),建议参考上方推荐示例自行扩展。 - 若不希望引入全局命名空间,可直接使用
as const推断字面量类型,或将上述模块声明改为纯接口定义。 LegendSchema.list当前声明为单元素元组[ElementSchema],实际支持多个元素,建议在使用时按ElementSchema[]处理。
本地构建
npm install
npm run build构建产物由 webpack.config.js 生成。
项目文档
- PROJECT-GUIDE.md — 项目导读
- WORKING-STATE.md — 当前开发状态与待办
- DEVELOPLOGS.md — 设计决策历史
许可证
ISC © FanTaSyLin
