<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ZingSoft Demo</title>
<script nonce="undefined" src="https://cdn.zingchart.com/zingchart.min.js"></script>
<script nonce="undefined" src="https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.2/marked.min.js"></script>
<style>
/* main.css */
html,
body {
margin: 0;
padding: 0;
}
html,
body,
#zingChart {
height: 100%;
width: 100%;
}
body {
background-color: rgb(118, 118, 118);
color: white;
display: grid;
font-family: sans-serif;
grid-template-areas:
'blank1'
'title'
'chart'
'blank2';
grid-template-rows: 1fr auto auto 1fr;
justify-content: center;
row-gap: 1rem;
padding: 0;
}
header {
grid-area: title;
text-align: center;
h1 {
filter: drop-shadow(2px 2px 3px rgba(0, 0, 0, 0.6));
}
}
main {
align-items: center;
column-gap: 2rem;
display: grid;
grid-area: chart;
grid-template-columns: 2fr 1fr;
height: 100%;
}
section {
height: fit-content;
width: 100%;
}
#sectionChart {
align-items: center;
background-color: #dbdbdb;
border: 1rem solid #dbdbdb;
border-radius: 10px;
box-sizing: border-box;
display: grid;
filter: drop-shadow(2px 2px 8px rgba(0, 0, 0, 0.25));
#zingChart {
border-radius: 5px;
justify-self: flex-end;
min-height: 43.5rem;
}
}
#sectionOutput {
background-color: #dbdbdb;
border: 1rem solid #dbdbdb;
border-radius: 10px;
box-sizing: border-box;
color: black;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
filter: drop-shadow(2px 2px 8px rgba(0, 0, 0, 0.25));
height: 45.5rem;
justify-items: center;
justify-self: flex-start;
row-gap: 2ch;
width: 52ch;
output {
background-color: white;
border-radius: 5px;
box-sizing: border-box;
display: block;
overflow-y: scroll;
padding: 1ch;
width: 100%;
&> :first-child {
margin-block-start: 0;
}
}
output.error {
color: red;
display: grid;
font-size: 1.5rem;
place-items: center;
text-align: center;
}
output:has(.loader.active) {
display: grid;
place-items: center;
}
button {
background-color: white;
border: none;
border-radius: 5px;
cursor: pointer;
filter: drop-shadow(2px 2px 3px rgba(0, 0, 0, 0.35));
padding: 1ch 3ch;
width: fit-content;
}
button:hover {
filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.2));
}
button:active {
filter: none;
}
.wrapper {
display: grid;
justify-items: center;
row-gap: 1rem;
width: fit-content;
}
}
@media (max-width: 1500px) {
body {
grid-template-columns: 1fr;
padding: 0;
width: 100%;
}
main {
grid-template-columns: 1fr;
grid-template-rows: auto auto;
justify-items: center;
row-gap: 2rem;
}
main #sectionChart {
width: 90%;
}
main #sectionChart #zingChart {
height: auto;
min-height: 35rem;
width: 100%;
}
main #sectionOutput {
margin-bottom: 2rem;
justify-self: unset;
width: 90%;
}
}
@media (max-width: 1000px) {
main #sectionChart #zingChart {
min-height: 25rem;
}
}
/*****************************************/
/*** Loading Animation ***/
/*** https://css-loaders.com/colorful/ ***/
/*****************************************/
/* HTML: <div class="loader"></div> */
.loader {
display: none;
}
.loader.active {
width: 40px;
aspect-ratio: 1;
border-radius: 50%;
margin-top: -30px;
display: flex;
justify-content: center;
overflow: hidden;
transform-origin: 50% 116.5%;
animation: l17 2s infinite linear;
}
.loader.active:before {
content: '';
min-width: 233%;
height: 233%;
background: radial-gradient(farthest-side, #6fde99 90%, #0000) top,
radial-gradient(farthest-side, #61e9d8 90%, #0000) left,
radial-gradient(farthest-side, #ff9c85 90%, #0000) bottom,
radial-gradient(farthest-side, #ffe295 90%, #0000) right;
background-size: 43% 43%;
background-repeat: no-repeat;
animation: inherit;
animation-direction: reverse;
}
@keyframes l17 {
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<header>
<h1>Analyze Your ZingChart with AI</h1>
</header>
<main>
<section id="sectionChart">
<div id="zingChart"></div>
</section>
<section id="sectionOutput">
<output>
<div class="loader"></div>
</output>
<button>Analyze Chart</button>
</section>
</main>
<command-bar label="Ask AI a Question About the Chart"></command-bar>
<script>
ZC.LICENSE = ["569d52cefae586f634c54f86dc99e6a9", "b55b025e438fa8a98e32482b5f768ff5"]; // main.js
/*******************************************/
/* HELPER FUNCTIONS FOR THIS DEMO - IGNORE */
/*******************************************/
var __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function(resolve) {
resolve(value);
});
}
return new(P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/*****************/
/* PROGRAM START */
/*****************/
const SERVER_URL = 'https://app-pmfi2cavkq-uc.a.run.app';
const ANALYZE_CHART_API = '/analyze-chart';
const ANALYZE_CHART_QUERY_API = '/analyze-chart-query';
const CHART_ID = 'zingChart';
let chartData = {
warrants: [3, 3, 4, 6, 4, 3, 3, 5, 6, 7, 9, 10],
orders: [3, 3, 4, 7, 4, 3, 3, 7, 8, 9, 11, 12],
subpoenas: [3, 3, 4, 6, 4, 3, 3, 9, 10, 12, 13, 14],
usRequests: [3, 3, 4, 6, 4, 3, 3, 10, 11, 13, 14, 15],
governmentRequests: [3, 3, 4, 6, 4, 3, 3, 11, 12, 14, 15, 20],
};
let chartConfig = {
type: 'area',
globals: {
fontColor: '#333',
fontFamily: 'sans-serif',
},
title: {
text: 'Government Requests by Volume',
align: 'center',
padding: '5px',
},
subtitle: {
text: 'This area graph shows the total number of requests received over time, broken down by request type',
align: 'center',
fontColor: '#505050',
padding: '10px',
},
legend: {
align: 'center',
border: 'none',
item: {
fontSize: '18px',
},
layout: '3x2',
marker: {
type: 'circle',
},
verticalAlign: 'bottom',
},
plot: {
tooltip: {
text: '%t:<br>%v Requests received between<br>July 2020 & June 2021',
backgroundColor: '#F7F9FA',
borderRadius: '4px',
callout: true,
fontColor: '#505050',
fontSize: '18px',
padding: '20px 35px',
},
aspect: 'spline',
},
scaleX: {
labels: [
'Jul 20',
'Aug 20',
'Sep 20',
'Oct 20',
'Nov 20',
'Dec 20',
'Jan 21',
'Feb 21',
'Mar 21',
'Apr 21',
'May 21',
'Jun 21',
],
},
scaleY: {
values: '0:16:2',
},
series: [{
text: 'Arrest Warrants',
values: chartData.warrants,
alphaArea: 1,
backgroundColor: '#62E9D8',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
lineWidth: '3px',
marker: {
size: '0px',
},
zIndex: 10,
},
{
text: ' Court Orders',
values: chartData.orders,
alphaArea: 1,
backgroundColor: '#70DE99',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 9,
},
{
text: 'Subpoenas',
values: chartData.subpoenas,
alphaArea: 1,
backgroundColor: '#B2DC93',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 8,
},
{
text: 'US Requests',
values: chartData.usRequests,
alphaArea: 1,
backgroundColor: '#FFE295',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 7,
},
{
text: 'Government Requests',
values: chartData.governmentRequests,
alphaArea: 1,
backgroundColor: '#FF9C85',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 6,
},
],
gui: {
contextMenu: {
item: {
fontColor: 'white',
},
customItems: [{
text: 'Analyze Chart with AI',
function: 'analyzeWithAI()',
id: 'analyzeWithAI',
}, ],
},
},
mediaRules: [{
maxWidth: 600,
chart: {
paddingTop: 30,
},
},
{
maxWidth: 600,
subtitle: {
text: 'This area graph shows the total number of requests\nreceived over time, broken down by request type',
paddingBottom: 10,
},
},
{
maxWidth: 500,
legend: {
layout: '6x1',
},
},
{
maxWidth: 400,
title: {
text: 'Government Requests\nby Volume',
paddingBottom: 20,
},
},
],
};
/**
* Exports a given chart to a jpg image
* @param {string} chartId The ID of the chart to export the image from
* @return {string} The base64 encoded image data as a JPG
*/
function exportChartImage(chartId) {
return __awaiter(this, void 0, void 0, function*() {
return new Promise((resolve, reject) => {
try {
zingchart.exec(chartId, 'getimagedata', {
filetype: 'jpg',
callback: (imagedata) => resolve(imagedata),
});
} catch (err) {
reject(err);
}
});
});
}
/**
* @param {string} chartId The ID of the chart to export the image from
* @param {string} [query] The query to send along with the chart
* @return {string} The AI's response of the image
*/
function analyzeChart(chartId, query) {
return __awaiter(this, void 0, void 0, function*() {
// Create the URL and Request Body for the AI request
let url = SERVER_URL;
let reqBody = {
chartImage: yield exportChartImage(chartId)
};
// Determine what type of request it is
if (query) {
url += ANALYZE_CHART_QUERY_API;
reqBody.query = query;
} else {
url += ANALYZE_CHART_API;
}
try {
let response = yield fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(reqBody),
});
return yield response.json();
} catch (err) {
console.error('Trouble reaching endpoint', err);
}
});
}
/**
* Analyze the current chart with AI and then output that analysis to the page.
* Function exposed to window so it's available to ZingChart.
* @param {string} [query] Query to send along with chart
*/
window.analyzeWithAI = function(query) {
let output = document.querySelector('output');
output.innerHTML = /* HTML */ `<div class="loader"></div>`;
output.classList.remove('error');
let loader = document.querySelector('output > .loader');
// Create simple loader counter for UX
loader.classList.add('active');
// Sends chart off to analyze
analyzeChart(CHART_ID, query)
.then((aiAnalysis) => {
var _a, _b;
loader.classList.remove('active');
aiAnalysis = (_b = (_a = aiAnalysis.choices[0]) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.content;
if (!aiAnalysis) {
output.innerHTML = 'Error: Trouble reaching AI server';
output.classList.add('error');
} else {
let renderedMarkdown = marked.parse(aiAnalysis);
output.innerHTML = renderedMarkdown;
}
})
.catch((err) => {
output.innerHTML = 'Error: Trouble reaching AI server';
output.classList.add('error');
});
};
function init() {
// Render ZingChart
zingchart.render({
id: CHART_ID,
data: chartConfig,
height: '100%',
// height: '400',
width: '100%',
});
// Attach event listener to analyze chart on button click
let analyzeChartBtn = document.querySelector('#sectionOutput button');
analyzeChartBtn.addEventListener('click', () => analyzeWithAI());
}
init();
</script>
</body>
</html>
/* main.css */
html,
body {
margin: 0;
padding: 0;
}
html,
body,
#zingChart {
height: 100%;
width: 100%;
}
body {
background-color: rgb(118, 118, 118);
color: white;
display: grid;
font-family: sans-serif;
grid-template-areas:
'blank1'
'title'
'chart'
'blank2';
grid-template-rows: 1fr auto auto 1fr;
justify-content: center;
row-gap: 1rem;
padding: 0;
}
header {
grid-area: title;
text-align: center;
h1 {
filter: drop-shadow(2px 2px 3px rgba(0, 0, 0, 0.6));
}
}
main {
align-items: center;
column-gap: 2rem;
display: grid;
grid-area: chart;
grid-template-columns: 2fr 1fr;
height: 100%;
}
section {
height: fit-content;
width: 100%;
}
#sectionChart {
align-items: center;
background-color: #dbdbdb;
border: 1rem solid #dbdbdb;
border-radius: 10px;
box-sizing: border-box;
display: grid;
filter: drop-shadow(2px 2px 8px rgba(0, 0, 0, 0.25));
#zingChart {
border-radius: 5px;
justify-self: flex-end;
min-height: 43.5rem;
}
}
#sectionOutput {
background-color: #dbdbdb;
border: 1rem solid #dbdbdb;
border-radius: 10px;
box-sizing: border-box;
color: black;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
filter: drop-shadow(2px 2px 8px rgba(0, 0, 0, 0.25));
height: 45.5rem;
justify-items: center;
justify-self: flex-start;
row-gap: 2ch;
width: 52ch;
output {
background-color: white;
border-radius: 5px;
box-sizing: border-box;
display: block;
overflow-y: scroll;
padding: 1ch;
width: 100%;
&> :first-child {
margin-block-start: 0;
}
}
output.error {
color: red;
display: grid;
font-size: 1.5rem;
place-items: center;
text-align: center;
}
output:has(.loader.active) {
display: grid;
place-items: center;
}
button {
background-color: white;
border: none;
border-radius: 5px;
cursor: pointer;
filter: drop-shadow(2px 2px 3px rgba(0, 0, 0, 0.35));
padding: 1ch 3ch;
width: fit-content;
}
button:hover {
filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.2));
}
button:active {
filter: none;
}
.wrapper {
display: grid;
justify-items: center;
row-gap: 1rem;
width: fit-content;
}
}
@media (max-width: 1500px) {
body {
grid-template-columns: 1fr;
padding: 0;
width: 100%;
}
main {
grid-template-columns: 1fr;
grid-template-rows: auto auto;
justify-items: center;
row-gap: 2rem;
}
main #sectionChart {
width: 90%;
}
main #sectionChart #zingChart {
height: auto;
min-height: 35rem;
width: 100%;
}
main #sectionOutput {
margin-bottom: 2rem;
justify-self: unset;
width: 90%;
}
}
@media (max-width: 1000px) {
main #sectionChart #zingChart {
min-height: 25rem;
}
}
/*****************************************/
/*** Loading Animation ***/
/*** https://css-loaders.com/colorful/ ***/
/*****************************************/
/* HTML: <div class="loader"></div> */
.loader {
display: none;
}
.loader.active {
width: 40px;
aspect-ratio: 1;
border-radius: 50%;
margin-top: -30px;
display: flex;
justify-content: center;
overflow: hidden;
transform-origin: 50% 116.5%;
animation: l17 2s infinite linear;
}
.loader.active:before {
content: '';
min-width: 233%;
height: 233%;
background: radial-gradient(farthest-side, #6fde99 90%, #0000) top,
radial-gradient(farthest-side, #61e9d8 90%, #0000) left,
radial-gradient(farthest-side, #ff9c85 90%, #0000) bottom,
radial-gradient(farthest-side, #ffe295 90%, #0000) right;
background-size: 43% 43%;
background-repeat: no-repeat;
animation: inherit;
animation-direction: reverse;
}
@keyframes l17 {
100% {
transform: rotate(360deg);
}
}
// main.js
/*******************************************/
/* HELPER FUNCTIONS FOR THIS DEMO - IGNORE */
/*******************************************/
var __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function(resolve) {
resolve(value);
});
}
return new(P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/*****************/
/* PROGRAM START */
/*****************/
const SERVER_URL = 'https://app-pmfi2cavkq-uc.a.run.app';
const ANALYZE_CHART_API = '/analyze-chart';
const ANALYZE_CHART_QUERY_API = '/analyze-chart-query';
const CHART_ID = 'zingChart';
let chartData = {
warrants: [3, 3, 4, 6, 4, 3, 3, 5, 6, 7, 9, 10],
orders: [3, 3, 4, 7, 4, 3, 3, 7, 8, 9, 11, 12],
subpoenas: [3, 3, 4, 6, 4, 3, 3, 9, 10, 12, 13, 14],
usRequests: [3, 3, 4, 6, 4, 3, 3, 10, 11, 13, 14, 15],
governmentRequests: [3, 3, 4, 6, 4, 3, 3, 11, 12, 14, 15, 20],
};
let chartConfig = {
type: 'area',
globals: {
fontColor: '#333',
fontFamily: 'sans-serif',
},
title: {
text: 'Government Requests by Volume',
align: 'center',
padding: '5px',
},
subtitle: {
text: 'This area graph shows the total number of requests received over time, broken down by request type',
align: 'center',
fontColor: '#505050',
padding: '10px',
},
legend: {
align: 'center',
border: 'none',
item: {
fontSize: '18px',
},
layout: '3x2',
marker: {
type: 'circle',
},
verticalAlign: 'bottom',
},
plot: {
tooltip: {
text: '%t:<br>%v Requests received between<br>July 2020 & June 2021',
backgroundColor: '#F7F9FA',
borderRadius: '4px',
callout: true,
fontColor: '#505050',
fontSize: '18px',
padding: '20px 35px',
},
aspect: 'spline',
},
scaleX: {
labels: [
'Jul 20',
'Aug 20',
'Sep 20',
'Oct 20',
'Nov 20',
'Dec 20',
'Jan 21',
'Feb 21',
'Mar 21',
'Apr 21',
'May 21',
'Jun 21',
],
},
scaleY: {
values: '0:16:2',
},
series: [{
text: 'Arrest Warrants',
values: chartData.warrants,
alphaArea: 1,
backgroundColor: '#62E9D8',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
lineWidth: '3px',
marker: {
size: '0px',
},
zIndex: 10,
},
{
text: ' Court Orders',
values: chartData.orders,
alphaArea: 1,
backgroundColor: '#70DE99',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 9,
},
{
text: 'Subpoenas',
values: chartData.subpoenas,
alphaArea: 1,
backgroundColor: '#B2DC93',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 8,
},
{
text: 'US Requests',
values: chartData.usRequests,
alphaArea: 1,
backgroundColor: '#FFE295',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 7,
},
{
text: 'Government Requests',
values: chartData.governmentRequests,
alphaArea: 1,
backgroundColor: '#FF9C85',
hoverMarker: {
backgroundColor: '#fff',
borderColor: '#eee',
size: '10px',
},
lineColor: '#ffffff',
marker: {
size: '0px',
},
zIndex: 6,
},
],
gui: {
contextMenu: {
item: {
fontColor: 'white',
},
customItems: [{
text: 'Analyze Chart with AI',
function: 'analyzeWithAI()',
id: 'analyzeWithAI',
}, ],
},
},
mediaRules: [{
maxWidth: 600,
chart: {
paddingTop: 30,
},
},
{
maxWidth: 600,
subtitle: {
text: 'This area graph shows the total number of requests\nreceived over time, broken down by request type',
paddingBottom: 10,
},
},
{
maxWidth: 500,
legend: {
layout: '6x1',
},
},
{
maxWidth: 400,
title: {
text: 'Government Requests\nby Volume',
paddingBottom: 20,
},
},
],
};
/**
* Exports a given chart to a jpg image
* @param {string} chartId The ID of the chart to export the image from
* @return {string} The base64 encoded image data as a JPG
*/
function exportChartImage(chartId) {
return __awaiter(this, void 0, void 0, function*() {
return new Promise((resolve, reject) => {
try {
zingchart.exec(chartId, 'getimagedata', {
filetype: 'jpg',
callback: (imagedata) => resolve(imagedata),
});
} catch (err) {
reject(err);
}
});
});
}
/**
* @param {string} chartId The ID of the chart to export the image from
* @param {string} [query] The query to send along with the chart
* @return {string} The AI's response of the image
*/
function analyzeChart(chartId, query) {
return __awaiter(this, void 0, void 0, function*() {
// Create the URL and Request Body for the AI request
let url = SERVER_URL;
let reqBody = {
chartImage: yield exportChartImage(chartId)
};
// Determine what type of request it is
if (query) {
url += ANALYZE_CHART_QUERY_API;
reqBody.query = query;
} else {
url += ANALYZE_CHART_API;
}
try {
let response = yield fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(reqBody),
});
return yield response.json();
} catch (err) {
console.error('Trouble reaching endpoint', err);
}
});
}
/**
* Analyze the current chart with AI and then output that analysis to the page.
* Function exposed to window so it's available to ZingChart.
* @param {string} [query] Query to send along with chart
*/
window.analyzeWithAI = function(query) {
let output = document.querySelector('output');
output.innerHTML = /* HTML */ `<div class="loader"></div>`;
output.classList.remove('error');
let loader = document.querySelector('output > .loader');
// Create simple loader counter for UX
loader.classList.add('active');
// Sends chart off to analyze
analyzeChart(CHART_ID, query)
.then((aiAnalysis) => {
var _a, _b;
loader.classList.remove('active');
aiAnalysis = (_b = (_a = aiAnalysis.choices[0]) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.content;
if (!aiAnalysis) {
output.innerHTML = 'Error: Trouble reaching AI server';
output.classList.add('error');
} else {
let renderedMarkdown = marked.parse(aiAnalysis);
output.innerHTML = renderedMarkdown;
}
})
.catch((err) => {
output.innerHTML = 'Error: Trouble reaching AI server';
output.classList.add('error');
});
};
function init() {
// Render ZingChart
zingchart.render({
id: CHART_ID,
data: chartConfig,
height: '100%',
// height: '400',
width: '100%',
});
// Attach event listener to analyze chart on button click
let analyzeChartBtn = document.querySelector('#sectionOutput button');
analyzeChartBtn.addEventListener('click', () => analyzeWithAI());
}
init();