@opendata-ai/openchart-vanilla 2.3.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +72 -50
- package/dist/index.js.map +1 -1
- package/dist/styles.css +757 -0
- package/package.json +7 -4
- package/src/__tests__/svg-renderer.test.ts +19 -22
- package/src/svg-renderer.ts +97 -63
package/dist/index.js
CHANGED
|
@@ -2116,6 +2116,23 @@ import { compileChart } from "@opendata-ai/openchart-engine";
|
|
|
2116
2116
|
// src/svg-renderer.ts
|
|
2117
2117
|
import { estimateTextWidth } from "@opendata-ai/openchart-core";
|
|
2118
2118
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
2119
|
+
function computeXAxisExtent(layout) {
|
|
2120
|
+
const xAxis = layout.axes.x;
|
|
2121
|
+
if (!xAxis) return 0;
|
|
2122
|
+
if (xAxis.tickAngle && Math.abs(xAxis.tickAngle) > 10) {
|
|
2123
|
+
const fontSize = xAxis.tickLabelStyle.fontSize;
|
|
2124
|
+
const fontWeight = xAxis.tickLabelStyle.fontWeight;
|
|
2125
|
+
const angleRad = Math.abs(xAxis.tickAngle) * (Math.PI / 180);
|
|
2126
|
+
let maxLabelWidth = 40;
|
|
2127
|
+
for (const tick of xAxis.ticks) {
|
|
2128
|
+
const w = estimateTextWidth(tick.label, fontSize, fontWeight);
|
|
2129
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
2130
|
+
}
|
|
2131
|
+
const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
|
|
2132
|
+
return xAxis.label ? rotatedHeight + 20 : rotatedHeight;
|
|
2133
|
+
}
|
|
2134
|
+
return xAxis.label ? 48 : 26;
|
|
2135
|
+
}
|
|
2119
2136
|
function createSVGElement(tag) {
|
|
2120
2137
|
return document.createElementNS(SVG_NS, tag);
|
|
2121
2138
|
}
|
|
@@ -2160,7 +2177,7 @@ function renderChrome(parent, layout) {
|
|
|
2160
2177
|
if (chrome.subtitle) {
|
|
2161
2178
|
renderChromeElement(g, chrome.subtitle, "viz-subtitle", "subtitle");
|
|
2162
2179
|
}
|
|
2163
|
-
const xAxisExtent = layout
|
|
2180
|
+
const xAxisExtent = computeXAxisExtent(layout);
|
|
2164
2181
|
const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
|
|
2165
2182
|
if (chrome.source) {
|
|
2166
2183
|
renderChromeElement(
|
|
@@ -2209,11 +2226,23 @@ function renderAxis(parent, axis, orientation, layout) {
|
|
|
2209
2226
|
if (orientation === "x") {
|
|
2210
2227
|
const label = createSVGElement("text");
|
|
2211
2228
|
label.setAttribute("class", "viz-axis-tick");
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2229
|
+
if (axis.tickAngle && Math.abs(axis.tickAngle) > 10) {
|
|
2230
|
+
const labelX = tick.position;
|
|
2231
|
+
const labelY = area.y + area.height + 6;
|
|
2232
|
+
setAttrs(label, {
|
|
2233
|
+
x: labelX,
|
|
2234
|
+
y: labelY,
|
|
2235
|
+
"text-anchor": axis.tickAngle < 0 ? "end" : "start",
|
|
2236
|
+
"dominant-baseline": "central",
|
|
2237
|
+
transform: `rotate(${axis.tickAngle}, ${labelX}, ${labelY})`
|
|
2238
|
+
});
|
|
2239
|
+
} else {
|
|
2240
|
+
setAttrs(label, {
|
|
2241
|
+
x: tick.position,
|
|
2242
|
+
y: area.y + area.height + 14,
|
|
2243
|
+
"text-anchor": "middle"
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2217
2246
|
applyTextStyle(label, axis.tickLabelStyle);
|
|
2218
2247
|
label.textContent = tick.label;
|
|
2219
2248
|
g.appendChild(label);
|
|
@@ -2263,9 +2292,24 @@ function renderAxis(parent, axis, orientation, layout) {
|
|
|
2263
2292
|
applyTextStyle(axisLabel, axis.labelStyle);
|
|
2264
2293
|
axisLabel.textContent = axis.label;
|
|
2265
2294
|
if (orientation === "x") {
|
|
2295
|
+
let titleY = area.y + area.height + 35;
|
|
2296
|
+
if (axis.tickAngle && Math.abs(axis.tickAngle) > 10) {
|
|
2297
|
+
const angleRad = Math.abs(axis.tickAngle) * (Math.PI / 180);
|
|
2298
|
+
let maxLabelWidth = 40;
|
|
2299
|
+
for (const tick of axis.ticks) {
|
|
2300
|
+
const w = estimateTextWidth(
|
|
2301
|
+
tick.label,
|
|
2302
|
+
axis.tickLabelStyle.fontSize,
|
|
2303
|
+
axis.tickLabelStyle.fontWeight
|
|
2304
|
+
);
|
|
2305
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
2306
|
+
}
|
|
2307
|
+
const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
|
|
2308
|
+
titleY = area.y + area.height + rotatedHeight + 14;
|
|
2309
|
+
}
|
|
2266
2310
|
setAttrs(axisLabel, {
|
|
2267
2311
|
x: area.x + area.width / 2,
|
|
2268
|
-
y:
|
|
2312
|
+
y: titleY,
|
|
2269
2313
|
"text-anchor": "middle"
|
|
2270
2314
|
});
|
|
2271
2315
|
} else {
|
|
@@ -2308,6 +2352,9 @@ function renderLineMark(mark, index) {
|
|
|
2308
2352
|
if (mark.strokeDasharray) {
|
|
2309
2353
|
path.setAttribute("stroke-dasharray", mark.strokeDasharray);
|
|
2310
2354
|
}
|
|
2355
|
+
if (mark.opacity != null) {
|
|
2356
|
+
path.setAttribute("opacity", String(mark.opacity));
|
|
2357
|
+
}
|
|
2311
2358
|
g.appendChild(path);
|
|
2312
2359
|
}
|
|
2313
2360
|
if (mark.label?.visible) {
|
|
@@ -2740,52 +2787,21 @@ function renderLegend(parent, legend) {
|
|
|
2740
2787
|
}
|
|
2741
2788
|
parent.appendChild(g);
|
|
2742
2789
|
}
|
|
2743
|
-
var BRAND_FONT_SIZE =
|
|
2790
|
+
var BRAND_FONT_SIZE = 11;
|
|
2744
2791
|
var BRAND_MIN_WIDTH2 = 120;
|
|
2745
2792
|
var BRAND_URL = "https://tryopendata.ai";
|
|
2746
2793
|
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
2747
|
-
function
|
|
2794
|
+
function renderBrand(parent, layout) {
|
|
2795
|
+
if (layout.dimensions.width < BRAND_MIN_WIDTH2) return;
|
|
2748
2796
|
const { width } = layout.dimensions;
|
|
2749
2797
|
const padding = layout.theme.spacing.padding;
|
|
2750
2798
|
const rightEdge = width - padding;
|
|
2799
|
+
const fill = layout.theme.colors.axis;
|
|
2751
2800
|
const { chrome } = layout;
|
|
2752
|
-
const xAxisExtent = layout
|
|
2801
|
+
const xAxisExtent = computeXAxisExtent(layout);
|
|
2753
2802
|
const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
|
|
2754
2803
|
const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
|
|
2755
2804
|
const chromeY = firstBottom ? bottomOffset + firstBottom.y : bottomOffset + layout.theme.spacing.chartToFooter;
|
|
2756
|
-
const y = chromeY + BRAND_FONT_SIZE;
|
|
2757
|
-
const dataWidth = estimateTextWidth("Data", BRAND_FONT_SIZE, 600);
|
|
2758
|
-
const dataX = rightEdge - dataWidth;
|
|
2759
|
-
const openX = dataX;
|
|
2760
|
-
return { openX, dataX, y, fill: layout.theme.colors.axis };
|
|
2761
|
-
}
|
|
2762
|
-
function renderBrandOpen(parent, layout) {
|
|
2763
|
-
if (layout.dimensions.width < BRAND_MIN_WIDTH2) return;
|
|
2764
|
-
const { openX, y, fill } = brandPosition(layout);
|
|
2765
|
-
const a = createSVGElement("a");
|
|
2766
|
-
a.setAttribute("href", BRAND_URL);
|
|
2767
|
-
a.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
|
|
2768
|
-
a.setAttribute("target", "_blank");
|
|
2769
|
-
a.setAttribute("rel", "noopener");
|
|
2770
|
-
a.setAttribute("class", "viz-axis-ref");
|
|
2771
|
-
const text = createSVGElement("text");
|
|
2772
|
-
setAttrs(text, {
|
|
2773
|
-
x: openX,
|
|
2774
|
-
y,
|
|
2775
|
-
"font-family": layout.theme.fonts.family,
|
|
2776
|
-
"font-size": BRAND_FONT_SIZE,
|
|
2777
|
-
"font-weight": 500,
|
|
2778
|
-
"text-anchor": "end",
|
|
2779
|
-
"fill-opacity": 0.55
|
|
2780
|
-
});
|
|
2781
|
-
text.style.setProperty("fill", fill);
|
|
2782
|
-
text.textContent = "Open";
|
|
2783
|
-
a.appendChild(text);
|
|
2784
|
-
parent.appendChild(a);
|
|
2785
|
-
}
|
|
2786
|
-
function renderBrandData(parent, layout) {
|
|
2787
|
-
if (layout.dimensions.width < BRAND_MIN_WIDTH2) return;
|
|
2788
|
-
const { dataX, y, fill } = brandPosition(layout);
|
|
2789
2805
|
const a = createSVGElement("a");
|
|
2790
2806
|
a.setAttribute("href", BRAND_URL);
|
|
2791
2807
|
a.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
|
|
@@ -2794,16 +2810,23 @@ function renderBrandData(parent, layout) {
|
|
|
2794
2810
|
a.setAttribute("class", "viz-chrome-ref");
|
|
2795
2811
|
const text = createSVGElement("text");
|
|
2796
2812
|
setAttrs(text, {
|
|
2797
|
-
x:
|
|
2798
|
-
y,
|
|
2813
|
+
x: rightEdge,
|
|
2814
|
+
y: chromeY,
|
|
2799
2815
|
"font-family": layout.theme.fonts.family,
|
|
2800
2816
|
"font-size": BRAND_FONT_SIZE,
|
|
2801
|
-
"
|
|
2802
|
-
"
|
|
2817
|
+
"text-anchor": "end",
|
|
2818
|
+
"dominant-baseline": "hanging",
|
|
2803
2819
|
"fill-opacity": 0.55
|
|
2804
2820
|
});
|
|
2805
2821
|
text.style.setProperty("fill", fill);
|
|
2806
|
-
|
|
2822
|
+
const openSpan = createSVGElement("tspan");
|
|
2823
|
+
openSpan.setAttribute("font-weight", "500");
|
|
2824
|
+
openSpan.textContent = "Open";
|
|
2825
|
+
text.appendChild(openSpan);
|
|
2826
|
+
const dataSpan = createSVGElement("tspan");
|
|
2827
|
+
dataSpan.setAttribute("font-weight", "600");
|
|
2828
|
+
dataSpan.textContent = "Data";
|
|
2829
|
+
text.appendChild(dataSpan);
|
|
2807
2830
|
a.appendChild(text);
|
|
2808
2831
|
parent.appendChild(a);
|
|
2809
2832
|
}
|
|
@@ -2847,9 +2870,8 @@ function renderChartSVG(layout, container) {
|
|
|
2847
2870
|
svg.appendChild(clippedGroup);
|
|
2848
2871
|
renderAnnotations(svg, layout);
|
|
2849
2872
|
renderLegend(svg, layout.legend);
|
|
2850
|
-
renderBrandOpen(svg, layout);
|
|
2851
2873
|
renderChrome(svg, layout);
|
|
2852
|
-
|
|
2874
|
+
renderBrand(svg, layout);
|
|
2853
2875
|
container.appendChild(svg);
|
|
2854
2876
|
return svg;
|
|
2855
2877
|
}
|