前端项目结构调整

This commit is contained in:
JIANG
2026-03-10 11:04:30 +08:00
parent 7f25bd34d5
commit 520e1cb3f1
52 changed files with 242 additions and 345 deletions
-181
View File
@@ -1,181 +0,0 @@
/**
* 优雅分段分类 - 类似QGIS的Pretty Breaks
* 生成"好看"、易读的断点数值
* @param {number[]} data - 数据数组
* @param {number} n_classes - 分类数量
* @returns {number[]} 断点数组
*/
function prettyBreaksClassification(data, n_classes) {
if (data.length === 0) return [];
// const min_val = Math.min(...data);
// const max_val = Math.max(...data);
// const min_val = data.reduce((min, val) => Math.min(min, val), Infinity);
// 保证最小值不小于0
const min_val = Math.max(data.reduce((min, val) => Math.min(min, val), Infinity), 0);
const max_val = data.reduce((max, val) => Math.max(max, val), -Infinity);
const data_range = max_val - min_val;
// 计算基础间隔
const raw_interval = data_range / n_classes;
// 寻找"优雅"的间隔
const magnitude = 10 ** Math.floor(Math.log10(raw_interval));
const normalized = raw_interval / magnitude;
// 选择最接近的优雅数字
let nice_interval;
if (normalized <= 1) {
nice_interval = magnitude;
} else if (normalized <= 2) {
nice_interval = 2 * magnitude;
} else if (normalized <= 5) {
nice_interval = 5 * magnitude;
} else {
nice_interval = 10 * magnitude;
}
// 计算优雅的起始点
const nice_min = Math.floor(min_val / nice_interval) * nice_interval;
const nice_max = Math.ceil(max_val / nice_interval) * nice_interval;
// 生成断点
const breaks = [];
let current = nice_min;
while (current <= nice_max && breaks.length < n_classes + 1) {
breaks.push(current);
current += nice_interval;
}
// 确保包含最大值
if (breaks.length === 0 || breaks[breaks.length - 1] < max_val) {
breaks.push(nice_max);
}
// 调整为n_classes个区间
if (breaks.length > n_classes + 1) {
breaks.splice(n_classes + 1);
}
return breaks;
}
/**
* 计算类内方差
* @param {number[]} data - 排序后的数据数组
* @param {number} start - 起始索引
* @param {number} end - 结束索引
* @returns {number} 类内方差
*/
function variance(data, start, end) {
if (start >= end) return 0;
const mean = data.slice(start, end + 1).reduce((a, b) => a + b, 0) / (end - start + 1);
return data.slice(start, end + 1).reduce((sum, val) => sum + (val - mean) ** 2, 0);
}
/**
* Jenks自然断点分类算法
* @param {number[]} data - 数据数组
* @param {number} n_classes - 分类数量
* @returns {number[]} 断点数组
*/
function jenks_breaks_jenkspy(data, n_classes) {
if (data.length === 0) return [];
if (n_classes >= data.length) return data.slice().sort((a, b) => a - b);
const sortedData = data.slice().sort((a, b) => a - b);
const n = sortedData.length;
const k = n_classes;
// 初始化矩阵
const lowerClassLimits = Array.from({ length: n + 1 }, () => Array(k + 1).fill(0));
const varianceCombinations = Array.from({ length: n + 1 }, () => Array(k + 1).fill(0));
for (let i = 1; i <= n; i++) {
lowerClassLimits[i][1] = 1;
varianceCombinations[i][1] = variance(sortedData, 0, i - 1);
for (let j = 2; j <= k; j++) {
varianceCombinations[i][j] = Infinity;
}
}
// 动态规划
for (let l = 2; l <= k; l++) {
for (let m = l; m <= n; m++) {
for (let i = l - 1; i < m; i++) {
const v = varianceCombinations[i][l - 1] + variance(sortedData, i, m - 1);
if (v < varianceCombinations[m][l]) {
varianceCombinations[m][l] = v;
lowerClassLimits[m][l] = i;
}
}
}
}
// 回溯找到断点
const breaks = [];
let current = n;
for (let j = k; j >= 1; j--) {
breaks.unshift(sortedData[lowerClassLimits[current][j] - 1] || sortedData[0]);
current = lowerClassLimits[current][j];
}
breaks.push(sortedData[n - 1]);
return breaks;
}
/**
* 使用分层采样优化的Jenks算法
* 确保采样数据能代表原数据的分布
* @param {number[]} data - 数据数组
* @param {number} n_classes - 分类数量
* @param {number} sample_size - 采样大小,默认10000
* @returns {number[]} 断点数组
*/
function jenks_with_stratified_sampling(data, n_classes, sample_size = 10000) {
if (data.length <= sample_size) {
return jenks_breaks_jenkspy(data, n_classes);
}
// 对数据排序
const sorted_data = data.slice().sort((a, b) => a - b);
// 计算采样间隔
const interval = sorted_data.length / sample_size;
// 分层采样
const sampled_data = [];
for (let i = 0; i < sample_size; i++) {
const index = Math.floor(i * interval);
if (index < sorted_data.length) {
sampled_data.push(sorted_data[index]);
}
}
return jenks_breaks_jenkspy(sampled_data, n_classes);
}
/**
* 根据指定的方法计算数据的分类断点。
* @param {Array<number>} data - 要分类的数值数据数组。
* @param {number} segments - 要创建的段数或类别数。
* @param {string} classificationMethod - 要使用的分类方法。支持的值:"pretty_breaks" 或 "jenks_optimized"。
* @returns {Array<number>} 分类的断点数组。如果数据为空或无效,则返回空数组。
*/
function calculateClassification(
data,
segments,
classificationMethod
) {
if (!data || data.length === 0) {
return [];
}
if (classificationMethod === "pretty_breaks") {
return prettyBreaksClassification(data, segments);
}
if (classificationMethod === "jenks_optimized") {
return jenks_with_stratified_sampling(data, segments);
}
}
module.exports = { prettyBreaksClassification, jenks_breaks_jenkspy, jenks_with_stratified_sampling, calculateClassification };
+136
View File
@@ -0,0 +1,136 @@
export function prettyBreaksClassification(
data: number[],
nClasses: number
): number[] {
if (data.length === 0) return [];
const minVal = Math.max(data.reduce((min, val) => Math.min(min, val), Infinity), 0);
const maxVal = data.reduce((max, val) => Math.max(max, val), -Infinity);
const dataRange = maxVal - minVal;
const rawInterval = dataRange / nClasses;
const magnitude = 10 ** Math.floor(Math.log10(rawInterval));
const normalized = rawInterval / magnitude;
let niceInterval: number;
if (normalized <= 1) {
niceInterval = magnitude;
} else if (normalized <= 2) {
niceInterval = 2 * magnitude;
} else if (normalized <= 5) {
niceInterval = 5 * magnitude;
} else {
niceInterval = 10 * magnitude;
}
const niceMin = Math.floor(minVal / niceInterval) * niceInterval;
const niceMax = Math.ceil(maxVal / niceInterval) * niceInterval;
const breaks: number[] = [];
let current = niceMin;
while (current <= niceMax && breaks.length < nClasses + 1) {
breaks.push(current);
current += niceInterval;
}
if (breaks.length === 0 || breaks[breaks.length - 1] < maxVal) {
breaks.push(niceMax);
}
if (breaks.length > nClasses + 1) {
breaks.splice(nClasses + 1);
}
return breaks;
}
function variance(data: number[], start: number, end: number): number {
if (start >= end) return 0;
const mean = data.slice(start, end + 1).reduce((a, b) => a + b, 0) / (end - start + 1);
return data.slice(start, end + 1).reduce((sum, val) => sum + (val - mean) ** 2, 0);
}
export function jenks_breaks_jenkspy(data: number[], nClasses: number): number[] {
if (data.length === 0) return [];
if (nClasses >= data.length) return data.slice().sort((a, b) => a - b);
const sortedData = data.slice().sort((a, b) => a - b);
const n = sortedData.length;
const k = nClasses;
const lowerClassLimits = Array.from({ length: n + 1 }, () => Array(k + 1).fill(0));
const varianceCombinations = Array.from({ length: n + 1 }, () => Array(k + 1).fill(0));
for (let i = 1; i <= n; i++) {
lowerClassLimits[i][1] = 1;
varianceCombinations[i][1] = variance(sortedData, 0, i - 1);
for (let j = 2; j <= k; j++) {
varianceCombinations[i][j] = Infinity;
}
}
for (let l = 2; l <= k; l++) {
for (let m = l; m <= n; m++) {
for (let i = l - 1; i < m; i++) {
const v = varianceCombinations[i][l - 1] + variance(sortedData, i, m - 1);
if (v < varianceCombinations[m][l]) {
varianceCombinations[m][l] = v;
lowerClassLimits[m][l] = i;
}
}
}
}
const breaks: number[] = [];
let current = n;
for (let j = k; j >= 1; j--) {
breaks.unshift(sortedData[lowerClassLimits[current][j] - 1] || sortedData[0]);
current = lowerClassLimits[current][j];
}
breaks.push(sortedData[n - 1]);
return breaks;
}
export function jenks_with_stratified_sampling(
data: number[],
nClasses: number,
sampleSize = 10000
): number[] {
if (data.length <= sampleSize) {
return jenks_breaks_jenkspy(data, nClasses);
}
const sortedData = data.slice().sort((a, b) => a - b);
const interval = sortedData.length / sampleSize;
const sampledData: number[] = [];
for (let i = 0; i < sampleSize; i++) {
const index = Math.floor(i * interval);
if (index < sortedData.length) {
sampledData.push(sortedData[index]);
}
}
return jenks_breaks_jenkspy(sampledData, nClasses);
}
export function calculateClassification(
data: number[],
segments: number,
classificationMethod: string
): number[] {
if (!data || data.length === 0) {
return [];
}
if (classificationMethod === "pretty_breaks") {
return prettyBreaksClassification(data, segments);
}
if (classificationMethod === "jenks_optimized") {
return jenks_with_stratified_sampling(data, segments);
}
return [];
}
-35
View File
@@ -1,35 +0,0 @@
/**
* 将颜色字符串解析为包含红色、绿色、蓝色和 alpha 分量的对象。
* 支持 rgba、rgb 和十六进制颜色格式。对于 rgba 和 rgb,提取 r、g、b 和 a(如果未提供,则默认为 1)。
* 对于十六进制(例如 #RRGGBB),提取 r、g、b。(e.g., "rgba(255, 0, 0, 0.5)", "rgb(255, 0, 0)", or "#FF0000").
* @param {string} color - 要解析的颜色字符串(例如 "rgba(255, 0, 0, 0.5)"、"rgb(255, 0, 0)" 或 "#FF0000")。
* @returns {{r: number, g: number, b: number, a?: number}} 包含颜色分量的对象:
* - r: 红色分量 (0-255)
* - g: 绿色分量 (0-255)
* - b: 蓝色分量 (0-255)
* - a: Alpha 分量 (0-1),如果未指定则默认为 1
**/
function parseColor(color) {
// 解析 rgba 格式的颜色
const match = color.match(
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
);
if (match) {
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
// 如果没有 alpha 值,默认为 1
a: match[4] ? parseFloat(match[4]) : 1,
};
}
// 如果还是十六进制格式,保持原来的解析方式
const hex = color.replace("#", "");
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
};
}
module.exports = { parseColor };
-21
View File
@@ -1,21 +0,0 @@
const { parseColor } = require('./parseColor');
describe('parseColor', () => {
it('should parse hex color', () => {
expect(parseColor('#FF0000')).toEqual({ r: 255, g: 0, b: 0 });
expect(parseColor('00FF00')).toEqual({ r: 0, g: 255, b: 0 });
});
it('should parse rgb color', () => {
expect(parseColor('rgb(0, 0, 255)')).toEqual({ r: 0, g: 0, b: 255, a: 1 });
});
it('should parse rgba color', () => {
expect(parseColor('rgba(0, 0, 255, 0.5)')).toEqual({ r: 0, g: 0, b: 255, a: 0.5 });
});
it('should default alpha to 1 if not provided in rgba-like pattern', () => {
// The regex supports optional alpha
expect(parseColor('rgba(0, 0, 255)')).toEqual({ r: 0, g: 0, b: 255, a: 1 });
});
});
+25
View File
@@ -0,0 +1,25 @@
import { parseColor } from "./parseColor";
describe("parseColor", () => {
it("should parse hex color", () => {
expect(parseColor("#FF0000")).toEqual({ r: 255, g: 0, b: 0 });
expect(parseColor("00FF00")).toEqual({ r: 0, g: 255, b: 0 });
});
it("should parse rgb color", () => {
expect(parseColor("rgb(0, 0, 255)")).toEqual({ r: 0, g: 0, b: 255, a: 1 });
});
it("should parse rgba color", () => {
expect(parseColor("rgba(0, 0, 255, 0.5)")).toEqual({
r: 0,
g: 0,
b: 255,
a: 0.5,
});
});
it("should default alpha to 1 if not provided in rgba-like pattern", () => {
expect(parseColor("rgba(0, 0, 255)")).toEqual({ r: 0, g: 0, b: 255, a: 1 });
});
});
+31
View File
@@ -0,0 +1,31 @@
export interface ParsedColor {
r: number;
g: number;
b: number;
a?: number;
}
/**
* 将颜色字符串解析为包含红色、绿色、蓝色和 alpha 分量的对象。
*/
export function parseColor(color: string): ParsedColor {
const match = color.match(
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
);
if (match) {
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
a: match[4] ? parseFloat(match[4]) : 1,
};
}
const hex = color.replace("#", "");
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
};
}