Skip to content

Commit 1ba80d5

Browse files
committedDec 20, 2023
VueUiTiremarks create component
1 parent 5dd3e61 commit 1ba80d5

10 files changed

+507
-8
lines changed
 

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Available components:
4040
- [VueUiThermometer](https://vue-data-ui.graphieros.com/docs#vue-ui-thermometer)
4141
- [VueUiRings](https://vue-data-ui.graphieros.com/docs#vue-ui-rings)
4242
- [VueUiWheel](https://vue-data-ui.graphieros.com/docs#vue-ui-wheel)
43+
- [VueUiTiremarks](https://vue-data-ui.graphieros.com/docs#vue-ui-tiremarks)
4344

4445

4546
## Mini charts

‎package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vue-data-ui",
33
"private": false,
4-
"version": "1.9.30",
4+
"version": "1.9.31",
55
"type": "module",
66
"description": "A user-empowering data visualization Vue components library",
77
"keywords": [
@@ -32,7 +32,8 @@
3232
"relationship circle",
3333
"thermometer",
3434
"rings",
35-
"wheel"
35+
"wheel",
36+
"tiremarks"
3637
],
3738
"author": "Alec Lloyd Probert",
3839
"repository": {

‎src/App.vue

+23-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import StackTest from "./components/vue-ui-sparkstackbar.vue";
4040
import HistoTest from "./components/vue-ui-sparkhistogram.vue";
4141
import RingsTest from "./components/vue-ui-rings.vue";
4242
import WheelTest from "./components/vue-ui-wheel.vue";
43+
import TireTest from "./components/vue-ui-tiremarks.vue";
4344
4445
const dataset = ref([
4546
{
@@ -3522,6 +3523,10 @@ const skeletonOptions = ref([
35223523
35233524
const skeletonChoice = ref('wheel')
35243525
3526+
const tiremarksDataset = ref({
3527+
percentage: 75
3528+
})
3529+
35253530
</script>
35263531

35273532
<template>
@@ -3535,7 +3540,24 @@ const skeletonChoice = ref('wheel')
35353540
<h4 style="color: #5f8bee">Manual testing arena</h4>
35363541
</div>
35373542

3538-
<Box @copy="copyConfig(PROD_CONFIG.vue_ui_skeleton)" open>
3543+
<Box @copy="copyConfig(PROD_CONFIG.vue_ui_tiremarks)" open>
3544+
<template #title>VueUiTiremarks</template>
3545+
<template #dev>
3546+
<TireTest
3547+
:dataset="tiremarksDataset"
3548+
/>
3549+
</template>
3550+
<template #prod>
3551+
<VueUiTiremarks
3552+
:dataset="tiremarksDataset"
3553+
/>
3554+
</template>
3555+
<template #config>
3556+
{{ PROD_CONFIG.vue_ui_tiremarks }}
3557+
</template>
3558+
</Box>
3559+
3560+
<Box @copy="copyConfig(PROD_CONFIG.vue_ui_skeleton)">
35393561
<template #general>
35403562
<select v-model="skeletonChoice">
35413563
<option v-for="(opt) in skeletonOptions">{{ opt }}</option>

‎src/components/vue-ui-tiremarks.vue

+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
<script setup>
2+
import { ref, computed, nextTick, onMounted } from "vue";
3+
import pdf from "../pdf";
4+
import img from "../img";
5+
import { useNestedProp } from "../useNestedProp";
6+
import mainConfig from "../default_configs.json";
7+
import { shiftHue } from "../lib";
8+
import Title from "../atoms/Title.vue";
9+
import UserOptions from "../atoms/UserOptions.vue";
10+
11+
const props = defineProps({
12+
config: {
13+
type: Object,
14+
default() {
15+
return {}
16+
}
17+
},
18+
dataset: {
19+
type: Object,
20+
default() {
21+
return {}
22+
}
23+
},
24+
});
25+
26+
const uid = ref(`vue-ui-tiremarks-${Math.random()}`);
27+
28+
const defaultConfig = ref(mainConfig.vue_ui_tiremarks);
29+
30+
const isPrinting = ref(false);
31+
const isImaging = ref(false);
32+
const tiremarksChart = ref(null);
33+
34+
const tiremarksConfig = computed(() => {
35+
return useNestedProp({
36+
userConfig: props.config,
37+
defaultConfig: defaultConfig.value
38+
});
39+
});
40+
41+
const activeValue = ref(tiremarksConfig.value.style.chart.animation.use ? 0 : props.dataset.percentage);
42+
43+
onMounted(() => {
44+
let acceleration = 0;
45+
let speed = tiremarksConfig.value.style.chart.animation.speed;
46+
let incr = (0.005) * tiremarksConfig.value.style.chart.animation.acceleration;
47+
function animate() {
48+
activeValue.value += speed + acceleration;
49+
acceleration += incr;
50+
if (activeValue.value < props.dataset.percentage) {
51+
requestAnimationFrame(animate);
52+
} else {
53+
activeValue.value = props.dataset.percentage;
54+
}
55+
}
56+
57+
if(tiremarksConfig.value.style.chart.animation.use) {
58+
activeValue.value = 0;
59+
animate();
60+
}
61+
});
62+
63+
const isVertical = computed(() => {
64+
return tiremarksConfig.value.style.chart.layout.display === 'vertical';
65+
});
66+
67+
const hasGradient = computed(() => {
68+
return tiremarksConfig.value.style.chart.layout.ticks.gradient.show;
69+
});
70+
71+
const padding = computed(() => {
72+
73+
const paddingRef = {
74+
top: 48,
75+
left: 64,
76+
right: 64,
77+
bottom: 48
78+
}
79+
80+
if(isVertical.value) {
81+
return {
82+
top: tiremarksConfig.value.style.chart.percentage.verticalPosition === 'top' ? paddingRef.top : 3,
83+
left: 3,
84+
right: 3,
85+
bottom: tiremarksConfig.value.style.chart.percentage.verticalPosition === 'bottom' ? paddingRef.bottom : 3
86+
}
87+
} else {
88+
return {
89+
top: 0,
90+
bottom: 0,
91+
left: tiremarksConfig.value.style.chart.percentage.horizontalPosition === 'left' ? paddingRef.left : 3,
92+
right: tiremarksConfig.value.style.chart.percentage.horizontalPosition === 'right' ? paddingRef.right : 10,
93+
}
94+
}
95+
})
96+
97+
98+
// This should return a total for x and another for y
99+
const totalPadding = computed(() => {
100+
return Object.values(padding.value).reduce((a, b) => a + b, 0)
101+
})
102+
103+
const svg = computed(() => {
104+
const small = 56;
105+
const big = 312;
106+
return {
107+
height: isVertical.value ? big : small,
108+
width: isVertical.value ? small : big,
109+
}
110+
})
111+
112+
const max = ref(100);
113+
114+
const proportion = computed(() => {
115+
return props.dataset.percentage / max.value
116+
});
117+
118+
const tickSize = computed(() => {
119+
if (isVertical.value) {
120+
return {
121+
mark: ((svg.value.height - totalPadding.value) / 100) * 0.5,
122+
space: ((svg.value.height - totalPadding.value) / 100) * 0.5
123+
}
124+
} else {
125+
return {
126+
mark: ((svg.value.width - totalPadding.value) / 100) * 0.5,
127+
space: ((svg.value.width - totalPadding.value) / 100) * 0.5
128+
}
129+
}
130+
})
131+
132+
const ticks = computed(() => {
133+
const arr = [];
134+
const marks = 100;
135+
for (let i = 0; i < marks; i += 1) {
136+
const color = tiremarksConfig.value.style.chart.layout.ticks.gradient.show ? shiftHue(tiremarksConfig.value.style.chart.layout.activeColor, i / marks * (tiremarksConfig.value.style.chart.layout.ticks.gradient.shiftHueIntensity / 100)) : tiremarksConfig.value.style.chart.layout.activeColor;
137+
if(isVertical.value) {
138+
const verticalCrescendo = tiremarksConfig.value.style.chart.layout.crescendo ? ((marks - i) * (svg.value.width - padding.value.left - padding.value.right) / marks / 3) : 0;
139+
const v_x1 = padding.value.left + 4 + verticalCrescendo;
140+
const v_x2 = svg.value.width - padding.value.right - 4 - verticalCrescendo;
141+
const v_y1 = svg.value.height - padding.value.bottom - (i * tickSize.value.mark) - (i * tickSize.value.space) - tickSize.value.mark;
142+
const v_y2 = svg.value.height - padding.value.bottom - (i * tickSize.value.mark) - (i * tickSize.value.space) - tickSize.value.mark;
143+
const v_space_x = (v_x2 - v_x1 ) / tiremarksConfig.value.style.chart.layout.curveAngleX;
144+
const v_space_y = tiremarksConfig.value.style.chart.layout.curveAngleY * ((1 + i) / marks);
145+
arr.push({
146+
x1: v_x1,
147+
x2: v_x2,
148+
y1: v_y1,
149+
y2: v_y2,
150+
curve: `M ${v_x1} ${v_y1} C ${v_x1 + v_space_x} ${v_y1 - v_space_y}, ${v_x2 - v_space_x} ${v_y2 - v_space_y}, ${v_x2} ${v_y2}`,
151+
color
152+
})
153+
} else {
154+
const horizontalCrescendo = tiremarksConfig.value.style.chart.layout.crescendo ? ((marks - i) * (svg.value.height - padding.value.top - padding.value.bottom) / marks / 3) : 0;
155+
const h_x1 = padding.value.left + (i * tickSize.value.mark) + (i * tickSize.value.space) - tickSize.value.mark;
156+
const h_x2 = h_x1;
157+
const h_y1 = padding.value.top + 4 + horizontalCrescendo;
158+
const h_y2 = svg.value.height - padding.value.bottom - 4 - horizontalCrescendo;
159+
const h_space_x = tiremarksConfig.value.style.chart.layout.curveAngleY * ((1 + i) / marks);
160+
const h_space_y = (h_y2 - h_y1 ) / tiremarksConfig.value.style.chart.layout.curveAngleX;
161+
arr.push({
162+
x1: h_x1,
163+
x2: h_x2,
164+
y1: h_y1,
165+
y2: h_y2,
166+
curve: `M ${h_x1} ${h_y1} C ${h_x1 + h_space_x} ${h_y1 + h_space_y}, ${h_x2 + h_space_x} ${h_y2 - h_space_y}, ${h_x2} ${h_y2}`,
167+
color
168+
})
169+
}
170+
}
171+
return arr;
172+
});
173+
174+
const dataLabel = computed(() => {
175+
let x,y,textAnchor,fontSize;
176+
const fontSizeOffset = tiremarksConfig.value.style.chart.percentage.fontSize / 3;
177+
178+
if(isVertical.value) {
179+
if(tiremarksConfig.value.style.chart.percentage.verticalPosition === 'top') {
180+
x = svg.value.width / 2;
181+
y = padding.value.top / 2;
182+
textAnchor = 'middle';
183+
} else if(tiremarksConfig.value.style.chart.percentage.verticalPosition === 'bottom') {
184+
x = svg.value.width / 2;
185+
y = svg.value.height - (padding.value.bottom / 2) + fontSizeOffset;
186+
textAnchor = 'middle';
187+
}
188+
} else {
189+
if(tiremarksConfig.value.style.chart.percentage.horizontalPosition === 'left') {
190+
x = 4;
191+
y = (svg.value.height / 2) + fontSizeOffset;
192+
textAnchor = 'start';
193+
} else if(tiremarksConfig.value.style.chart.percentage.horizontalPosition === 'right') {
194+
x = svg.value.width - padding.value.right + 8;
195+
y = (svg.value.height / 2) + fontSizeOffset;
196+
textAnchor = 'start';
197+
}
198+
}
199+
200+
return {
201+
x,
202+
y,
203+
textAnchor,
204+
bold: tiremarksConfig.value.style.chart.percentage.bold,
205+
fontSize: tiremarksConfig.value.style.chart.percentage.fontSize,
206+
fill: tiremarksConfig.value.style.chart.percentage.color
207+
}
208+
})
209+
210+
const __to__ = ref(null);
211+
212+
function showSpinnerPdf() {
213+
isPrinting.value = true;
214+
}
215+
216+
function generatePdf(){
217+
showSpinnerPdf();
218+
clearTimeout(__to__.value);
219+
__to__.value = setTimeout(() => {
220+
pdf({
221+
domElement: document.getElementById(`${uid.value}`),
222+
fileName: tiremarksConfig.value.style.chart.title.text || 'vue-ui-tiremarks'
223+
}).finally(() => {
224+
isPrinting.value = false;
225+
})
226+
}, 100)
227+
}
228+
229+
function showSpinnerImage() {
230+
isImaging.value = true;
231+
}
232+
233+
function generateImage() {
234+
showSpinnerImage();
235+
clearTimeout(__to__.value);
236+
__to__.value = setTimeout(() => {
237+
img({
238+
domElement: document.getElementById(`${uid.value}`),
239+
fileName: tiremarksConfig.value.style.chart.title.text || 'vue-ui-tiremarks',
240+
format: 'png'
241+
}).finally(() => {
242+
isImaging.value = false;
243+
})
244+
}, 100)
245+
}
246+
247+
defineExpose({
248+
generatePdf,
249+
generateImage
250+
});
251+
252+
</script>
253+
254+
<template>
255+
<div :ref="`tiremarksChart`" :class="`vue-ui-tiremarks ${tiremarksConfig.useCssAnimation ? '' : 'vue-ui-dna'}`" :style="`font-family:${tiremarksConfig.style.fontFamily};width:100%; text-align:center;${(tiremarksConfig.userOptions.show && !isImaging) ? 'padding-top:36px' : ''}`" :id="uid">
256+
257+
<div v-if="tiremarksConfig.style.chart.title.text" :style="`width:100%;background:${tiremarksConfig.style.chart.backgroundColor};padding-bottom:12px`">
258+
<Title
259+
:config="{
260+
title: {
261+
cy: 'wheel-title',
262+
text: tiremarksConfig.style.chart.title.text,
263+
color: tiremarksConfig.style.chart.title.color,
264+
fontSize: tiremarksConfig.style.chart.title.fontSize,
265+
bold: tiremarksConfig.style.chart.title.bold
266+
},
267+
subtitle: {
268+
cy: 'wheel-subtitle',
269+
text: tiremarksConfig.style.chart.title.subtitle.text,
270+
color: tiremarksConfig.style.chart.title.subtitle.color,
271+
fontSize: tiremarksConfig.style.chart.title.subtitle.fontSize,
272+
bold: tiremarksConfig.style.chart.title.subtitle.bold
273+
},
274+
}"
275+
/>
276+
</div>
277+
278+
<UserOptions
279+
ref="details"
280+
v-if="tiremarksConfig.userOptions.show"
281+
:backgroundColor="tiremarksConfig.style.chart.backgroundColor"
282+
:color="tiremarksConfig.style.chart.color"
283+
:isPrinting="isPrinting"
284+
:isImaging="isImaging"
285+
:title="tiremarksConfig.userOptions.title"
286+
:uid="uid"
287+
:hasImg="true"
288+
:hasXls="false"
289+
@generatePdf="generatePdf"
290+
@generateImage="generateImage"
291+
/>
292+
293+
<svg :viewBox="`0 0 ${svg.width} ${svg.height}`" :style="`max-width:100%; overflow: visible; background:${tiremarksConfig.style.chart.backgroundColor};color:${tiremarksConfig.style.chart.color}`">
294+
<g v-if="tiremarksConfig.style.chart.layout.curved">
295+
<path
296+
v-for="(tick, i) in ticks"
297+
:d="tick.curve"
298+
:stroke-width="tickSize.mark"
299+
:stroke="activeValue >= i ? tick.color : tiremarksConfig.style.chart.layout.inactiveColor"
300+
stroke-linecap="round"
301+
fill="none"
302+
:class="{ 'vue-ui-tick-animated': tiremarksConfig.style.chart.animation.use && i <= activeValue }"
303+
/>
304+
</g>
305+
<g v-else>
306+
<line
307+
v-for="(tick, i) in ticks"
308+
:x1="tick.x1"
309+
:y1="tick.y1"
310+
:x2="tick.x2"
311+
:y2="tick.y2"
312+
:stroke-width="tickSize.mark"
313+
:stroke="activeValue >= i ? tick.color : tiremarksConfig.style.chart.layout.inactiveColor"
314+
stroke-linecap="round"
315+
/>
316+
</g>
317+
<text
318+
v-if="tiremarksConfig.style.chart.percentage.show"
319+
:x="dataLabel.x"
320+
:y="dataLabel.y"
321+
:font-size="dataLabel.fontSize"
322+
:fill="tiremarksConfig.style.chart.layout.ticks.gradient.show && tiremarksConfig.style.chart.percentage.useGradientColor ? shiftHue(tiremarksConfig.style.chart.layout.activeColor, activeValue / 100 * (tiremarksConfig.style.chart.layout.ticks.gradient.shiftHueIntensity / 100)) : tiremarksConfig.style.chart.percentage.color"
323+
:font-weight="dataLabel.bold ? 'bold': 'normal'"
324+
:text-anchor="dataLabel.textAnchor"
325+
>
326+
{{ activeValue.toFixed(tiremarksConfig.style.chart.percentage.rounding) + '%' }}
327+
</text>
328+
</svg>
329+
</div>
330+
</template>
331+
332+
<style scoped>
333+
.vue-ui-tiremarks * {
334+
transition: unset;
335+
}
336+
.vue-ui-tiremarks {
337+
user-select: none;
338+
position: relative;
339+
}
340+
.vue-ui-tick-animated {
341+
animation: animate-tick 0.3s ease-in;
342+
transform-origin: center;
343+
}
344+
345+
@keyframes animate-tick {
346+
0% {
347+
stroke-width: 2;
348+
transform: scale(1,1.1);
349+
}
350+
100% {
351+
stroke-width: initial;
352+
transform: scale(1,1);
353+
}
354+
}
355+
</style>

‎src/components/vue-ui-wheel.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ defineExpose({
146146
class="vue-ui-wheel"
147147
ref="wheelChart"
148148
:id="uid"
149-
:style="`font-family:${wheelConfig.style.fontFamily};width:100%; text-align:center;${wheelConfig.userOptions.show ? 'padding-top:36px' : ''}`"
149+
:style="`font-family:${wheelConfig.style.fontFamily};width:100%; text-align:center;${(wheelConfig.userOptions.show && !isImaging) ? 'padding-top:36px' : ''}`"
150150
>
151151

152152
<div v-if="wheelConfig.style.chart.title.text" :style="`width:100%;background:${wheelConfig.style.chart.backgroundColor};padding-bottom:12px`">

‎src/default_configs.json

+55
Original file line numberDiff line numberDiff line change
@@ -2561,5 +2561,60 @@
25612561
"show": true,
25622562
"title": "options"
25632563
}
2564+
},
2565+
"vue_ui_tiremarks": {
2566+
"style": {
2567+
"fontFamily":"inherit",
2568+
"chart": {
2569+
"backgroundColor":"#FFFFFF",
2570+
"color":"#2D353C",
2571+
"animation": {
2572+
"use":true,
2573+
"speed": 0.5,
2574+
"acceleration": 1
2575+
},
2576+
"layout": {
2577+
"display": "horizontal",
2578+
"crescendo": true,
2579+
"curved": true,
2580+
"curveAngleX": 10,
2581+
"curveAngleY": 10,
2582+
"activeColor":"#5f8bee",
2583+
"inactiveColor":"#e1e5e8",
2584+
"ticks": {
2585+
"gradient": {
2586+
"show": true,
2587+
"shiftHueIntensity": 100
2588+
}
2589+
}
2590+
},
2591+
"percentage": {
2592+
"show": true,
2593+
"useGradientColor": true,
2594+
"color": "#5f8bee",
2595+
"fontSize": 16,
2596+
"bold": true,
2597+
"rounding": 1,
2598+
"verticalPosition": "bottom",
2599+
"horizontalPosition": "left"
2600+
},
2601+
"title": {
2602+
"text": "Title",
2603+
"color": "#2D353C",
2604+
"fontSize": 20,
2605+
"bold": true,
2606+
"subtitle": {
2607+
"color": "#A1A1A1",
2608+
"text": "Subtitle",
2609+
"fontSize": 16,
2610+
"bold": false
2611+
}
2612+
}
2613+
}
2614+
},
2615+
"userOptions": {
2616+
"show": true,
2617+
"title": "options"
2618+
}
25642619
}
25652620
}

‎src/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import VueUiSparkStackbar from "./components/vue-ui-sparkstackbar.vue";
2626
import VueUiSparkHistogram from "./components/vue-ui-sparkhistogram.vue";
2727
import VueUiRings from "./components/vue-ui-rings.vue";
2828
import VueUiWheel from "./components/vue-ui-wheel.vue";
29+
import VueUiTiremarks from "./components/vue-ui-tiremarks.vue";
2930

3031
export {
3132
VueUiXy,
@@ -55,5 +56,6 @@ export {
5556
VueUiSparkStackbar,
5657
VueUiSparkHistogram,
5758
VueUiRings,
58-
VueUiWheel
59+
VueUiWheel,
60+
VueUiTiremarks
5961
};

‎src/main.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
VueUiSparkStackbar,
3030
VueUiSparkHistogram,
3131
VueUiRings,
32-
VueUiWheel
32+
VueUiWheel,
33+
VueUiTiremarks,
3334
} from 'vue-data-ui';
3435
import 'vue-data-ui/style.css';
3536

@@ -62,4 +63,5 @@ app.component("VueUiSparkStackbar", VueUiSparkStackbar);
6263
app.component("VueUiSparkHistogram", VueUiSparkHistogram);
6364
app.component("VueUiRings", VueUiRings);
6465
app.component("VueUiWheel", VueUiWheel);
66+
app.component("VueUiTiremarks", VueUiTiremarks);
6567
app.mount('#app');

‎types/vue-data-ui.d.ts

+61
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,67 @@ declare module 'vue-data-ui' {
55
[key: string]: unknown;
66
}
77

8+
export type VueUiTiremarksConfig = {
9+
style: {
10+
fontFamily: string;
11+
chart: {
12+
backgroundColor: string;
13+
color: string;
14+
animation: {
15+
use: boolean;
16+
speed: number;
17+
acceleration: number;
18+
};
19+
layout: {
20+
display: "horizontal" | "vertical";
21+
crescendo: boolean;
22+
curved: boolean;
23+
curveAngleX: number;
24+
curveAngleY: number;
25+
activeColor: string;
26+
inactiveColor: string;
27+
ticks: {
28+
gradient: {
29+
show: boolean;
30+
shiftHueIntensity: number;
31+
};
32+
};
33+
};
34+
percentage: {
35+
show: boolean;
36+
useGradientColor: boolean;
37+
color: string;
38+
fontSize: number;
39+
bold: boolean;
40+
rounding: 1;
41+
verticalPosition: "bottom" | "top";
42+
horizontalPosition: "left" | "right";
43+
};
44+
title: {
45+
text: string;
46+
color: string;
47+
fontSize: number;
48+
bold: boolean;
49+
subtitle: {
50+
color: string;
51+
text: string;
52+
fontSize: number;
53+
bold: boolean;
54+
};
55+
};
56+
};
57+
};
58+
};
59+
60+
export type VueUiTiremarksDataset = {
61+
percentage: number;
62+
}
63+
64+
export const VueUiTiremarks: DefineComponent<{
65+
config?: VueUiTiremarksConfig;
66+
dataset: VueUiTiremarksDataset;
67+
}>;
68+
869
export type VueUiWheelConfig = {
970
style: {
1071
fontFamily: string;

0 commit comments

Comments
 (0)
Please sign in to comment.