webwriter-chart
Version:
ww-chart is an interactive data visualization widget for the WebWriter tool that implements various charts and diagrams for static and exploratory data visualization.
300 lines (258 loc) • 8.64 kB
text/typescript
import { ChartData, ChartDataSet, ChartDataSets } from "./interfaces";
import randomWords from "random-words";
export const converter = {
fromAttribute: (value: string | null, type: unknown): any => {
if (!value) return null;
if (type === Number || type === "number") return parseInt(value);
if (type === Boolean || type === "boolean") return value === "true";
if (type === Object || type === Array) return JSON.parse(value);
if (type === "color")
return isColorHex(value) ? convertHexToRGB(value) : value;
if (String(type).startsWith("Object<")) {
const obj = JSON.parse(value.toString());
const objType = String(type).replace("Object<", "").replace(">", "");
const newObj: { [key: string]: string } = {};
for (const key in obj) {
newObj[key] = converter.fromAttribute(obj[key], objType);
}
return newObj;
}
if (String(type).startsWith("Array<")) {
const arr = JSON.parse(value.toString());
const arrType = String(type).replace("Array<", "").replace(">", "");
const newArr: string[] = [];
for (const item of arr) {
newArr.push(converter.fromAttribute(item, arrType));
}
return newArr;
}
if (String(type) === "ChartData") {
//TODO: add option for simple data-only object
let result: ChartData = {};
const obj: ChartData = JSON.parse(value.toString());
for (const key in obj) {
result[key] = {
value: obj[key].value,
//TODO: add option for auto color
color: converter.fromAttribute(obj[key].color || "#000000", "color"),
label: obj[key].label || key,
};
}
return result;
}
return value;
},
toAttribute: (value: unknown, type: unknown): string | null => {
if (!value) return null;
if (type === Number) return value.toString();
if (type === Boolean) return value.toString();
if (type === Object || type === Array) return JSON.stringify(value);
if (type === "color") convertRGBToHex(value.toString());
if (String(type).startsWith("Object<"))
return JSON.stringify(value.toString());
if (String(type) === "ChartData") return JSON.stringify(value.toString());
return value.toString();
},
};
export function attribute(value: string): string;
export function attribute(value: number): string;
export function attribute(value: boolean): string;
export function attribute(value: object): string;
export function attribute(value: Array<unknown>): string;
export function attribute(
value: string | number | boolean | object | Array<unknown>
): string {
if (Array.isArray(value)) return JSON.stringify(value);
if (typeof value === "number") return value.toString();
if (typeof value === "boolean") return value.toString();
if (typeof value === "object") return JSON.stringify(value);
return value;
}
export function isColorHex(color: string): boolean {
return /^#[0-9A-F]{6}$/i.test(color);
}
export function convertHexToRGB(color: string): string {
if (!color) return "rgb(0,0,0)";
if (!isColorHex(color)) return color;
const r = parseInt(color.substring(1, 3), 16),
g = parseInt(color.substring(3, 5), 16),
b = parseInt(color.substring(5, 7), 16);
return `rgb(${r},${g},${b})`;
}
export function convertRGBToHex(color: string): string {
if (!color) return "#000000";
if (!color.startsWith("rgb")) return color;
const rgb = color
.replace(/^rgb\(|\s+|\)$/g, "")
.split(",")
.map(Number);
return (
"#" +
rgb
.map((x) => {
const hex = x.toString(16);
return hex.length === 1 ? "0" + hex : hex;
})
.join("")
);
}
export function addAlphaToRGB(color: string, alpha: number): string {
if (!color) return "rgba(0,0,0,0)";
if (!color.startsWith("rgb")) color = convertHexToRGB(color);
return color.replace("rgb", "rgba").replace(")", `, ${alpha})`);
}
export function removeAlphaFromRGB(color: string): string {
if (!color) return "rgb(0,0,0)";
return color.replace(/,.*\)/, ")");
}
export function randomNumber(min?: number, max?: number): number {
if (min && max) return Math.floor(Math.random() * (max - min) + min);
return Math.floor(Math.random() * 100);
}
export function randomColor(): string {
const h = randomNumber(0, 360);
const l = randomNumber(30, 60);
return hslToHex(h, 100, l);
}
export function generateColorWheel(numColors: number): string[] {
const colors = [];
const saturation = 100; // Full saturation for vibrant colors
const lightnessBase = 50; // Midpoint lightness for a balanced color
// Exclude red by spacing hues and avoiding the red spectrum
const step = Math.floor((360 - 60) / numColors); // Total degrees excluding red range
for (let i = 0; i < numColors; i++) {
let hue = step * i;
// Adjust hue to skip the red range (0-30° and 330-360°)
if (hue >= 0 && hue < 30) {
hue += 20; // Skip the first red range
} else if (hue >= 330) {
hue -= 20; // Skip the second red range
}
// Introduce a slight variation in lightness for more contrast
const lightness = lightnessBase + (i % 2 === 0 ? 10 : -10);
colors.push(hslToHex(hue, saturation, lightness));
}
return colors;
}
export function randomString(): string {
return randomWords(1)[0];
}
function hslToHex(h: number, s: number, l: number) {
l /= 100;
const a = (s * Math.min(l, 1 - l)) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
.toString(16)
.padStart(2, "0"); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}`;
}
export function chartOption<T extends keyof TypeMapping>(
type: T | "array",
defaultValue: TypeMapping[T],
name: string
): OptionDef<T>;
export function chartOption(
type: "number",
defaultValue: number,
name: string,
min?: number,
max?: number
): OptionDef<"number">;
export function chartOption<T extends keyof TypeMapping>(
type: T | "number",
defaultValue: TypeMapping[T] | number,
name: string,
min?: number,
max?: number
): OptionDef<T> {
if (type === "number")
return {
//@ts-ignore
type: "number",
default: defaultValue,
name,
min: min ?? Number.MIN_SAFE_INTEGER,
max: max ?? Number.MAX_SAFE_INTEGER,
};
else
return {
type,
default: defaultValue,
name,
} as any;
}
export function fillArray<T>(length: number, value: T): T[] {
return Array(length).fill(value);
}
export async function readFileInput(e: any): Promise<string> {
return new Promise((resolve, reject) => {
let fileContent = "";
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function (event) {
if (!event.target) return;
fileContent += event.target.result + "\n";
};
reader.readAsText(file);
reader.onloadend = () => resolve(fileContent);
reader.onerror = () => reject();
});
}
export function csvToDataset(fileContent: string): {
datasets: ChartDataSets;
title: string | undefined;
} {
const sets: ChartDataSet[] = [];
const lines = fileContent.replace(/\r\n/g, "\n").split("\n");
const wwcFormat = lines[0].startsWith("#wwc#");
let numberOfDataColumns = lines[0].split(",").indexOf("#");
let title = undefined;
let firstElementIsNumber = true;
lines.slice(1).forEach((line) => {
if (!line) return;
const values = line.trim().split(",");
let color = values[numberOfDataColumns + 1];
let colors = values.slice(numberOfDataColumns + 2);
if (values.length < numberOfDataColumns) return;
if (!wwcFormat) {
color = randomColor();
colors = values.map(() => randomColor());
}
const data =
numberOfDataColumns === -1
? values.map((x) => Number(x))
: values.slice(0, numberOfDataColumns).map((x) => Number(x));
let set = {
name: randomString(),
data,
color,
colors,
};
if (Number.isNaN(Number(values[0]))) {
firstElementIsNumber = false;
set.name = values[0];
set.data = set.data.slice(1);
}
sets.push(set);
});
let labels =
numberOfDataColumns === -1
? lines[0].split(",")
: lines[0].split(",").slice(0, numberOfDataColumns);
if (wwcFormat) labels[0] = labels[0].slice(5);
if (!firstElementIsNumber) {
title = labels[0];
labels = labels.slice(1);
debugger;
}
return {
datasets: {
labels: labels,
sets: sets,
},
title,
};
}