I want to display dynamic data via charts,graphs which is retrieved from database. There are various gems available like highcharts, google graphs, gruff library, etc. Can anyone please site an example how to retrieve data from database and use it to display charts,graphs using anyone of these gems/libraries ? Any help will useful.
Thanks
Okay here's a perfect example. In an application I recently completed I wanted to get the statistics for all employees and their overtime hours. I also then got the summed total for all employees. I decided to use highcharts. I believe that the best thing going to represent charts and graphs. Has really good documentation to back it up as well.
Controller
class StatisticsController < ApplicationController
def index
#users = User.all
#shifts = Shift.scoped
end
end
index.html.erb
<div id="container" style="min-width: 400px; height: 400px; margin: 0 auto"></div>
<script>
$(function () {
var chart;
$(document).ready(function () {
chart = new Highcharts.Chart({
chart:{
renderTo:'container',
type:'column'
},
title:{
text:'<%= Date.today.strftime("%B")%> Overtime'
},
xAxis:{
categories:[
'<%= Date.today.strftime("%B")%>'
]
},
yAxis:{
min:0,
title:{
text:'Hours'
}
},
legend:{
layout:'vertical',
backgroundColor:'#FFFFFF',
align:'left',
verticalAlign:'top',
x:100,
y:70,
floating:true,
shadow:true
},
tooltip:{
formatter:function () {
return '' +
'Hours' + ': ' + this.y;
},
credits:{
text:'SomeText.co.uk',
hreft:'http://wwww.putyourlinkhere.co.uk'
}
},
plotOptions:{
column:{
pointPadding:0.4,
borderWidth:0
}
},
series:[
<%#users.each do |user|%>
{
name:'<%= user.name%>',
data:[<%= user.shift.sum(:overtime)%>]
},
<% end %>
{
name:'Total',
data: [<%= #shifts.to_a.sum(&:overtime)%>],
color:'#FE9D00'
}
]
});
});
});
</script>
From this example you can see that the series is what is going to represent your data that you want to display. I loop through all users and output their name along with summing their shift. Further down I then grab the total by getting all shifts and putting it into an array and summing the overtime.
Oh and also you will need to download highcharts and put all relevant files in your assets.
This should be a good enough example to get you going.
Related
Keeping the table as basic as possible to figure this out. I am struggling to learn how to create server side sorting/pagination to function in vuetify. When I add the :options or :server-items-length bind the table no longer sorts or paginates.
Without either of those, I get a default listing of 10 items per row - and the sorting all works perfectly fine as well the pagination. However, parameters in the api require a page item count thus forcing a hardcoded number or using the :options bind. If i just place a hard coded number things work fine, but when I bind I get proper items per page but no sorting and pagination.
Very simple data table:
<v-data-table
:items="ItemResults.items"
:headers="TableData.TableHeaders"
:loading="TableData.isLoading"
>
</v-data-table>
//Base data returns, with headers and options as well the array that items are stored in.
data() {
return {
ItemResults:[],
TableData: {
isLoading: true,
TableHeaders: [
{ value: "title", text: "Title" },
{ value: 'artist', text: 'Artist' },
{ value: 'upc', text: 'UPC' },
{ value: "retailPrice", text: "Price/Quantity"},
],
},
options:
{
page: 1,
itemsPerPage: 15
},
}
},
//Then last, my async method to grab the data from the api, and place it in the itemresults array.
async getProducts(){
this.TableData.isLoading = true;
const { page, itemsPerPage } = this.options;
var temp = await this.$axios.get(`Inventory/InventoryListing_inStock/1/${page}/${itemsPerPage}`);
this.ItemResults = temp.data;
this.TableData.isLoading = false;
return this.ItemResults;
},
I have tried following Vuetify Pagination and sort serverside guide, but I'm not sure where they are recommending to make the axios call.
The lead backend dev is working on setting a sorting function up in the api for me to call paramaters to as well - Im not sure how that will function along side.
but I dont know how to have this controlled by vuetify eithier, or the best practice here.
EDIT:
I've synced the following:
:options.sync="options"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
but i think I dont need to sync the last two. My options:
options:
{
page: 1,
itemsPerPage: 15,
sortBy: ['title'],
sortDesc: [false]
},
and in my data I put the array for sort by and sort desc
sortBy: [
'title', 'artist', 'upc', 'retailPrice'
],
sortDesc:[true, false],
pagination is now working, and sort ascending is now working, but when I click to descend the header I get an error that the last two params are empty on update to / / instead of /sortBy/sortDesc result. So its not listing the values on changes.
When your component mounts, you need to fetch the total number of items available on the server and the first page of data, and set these as :items and :server-items-length. The latter will (as you observed) disable paging and sorting, as your server will take care of it, and it is used to calculate the total number of pages given a selected page size.
Now you need to know when to reload data. For this, you either bind options to the v-data-table with two-way binding (.sync) and set a watcher on it, or you listen to the update:options event. And whenever options change, you fetch the selected data from the server.
That's basically all it takes.
In your template:
<v-data-table
:items="items"
:headers="headers"
:loading="true"
:server-items-length="numberOfItemsOnServer"
#update:options="options => loadPage(options)"
/>
In your component:
methods: {
async loadPage(options){
this.items = [] // if you want to show DataTable's loading-text
this.items = await fetch('yourUrl' + new URLSearchParams({
// build url params from the options object
offset: (options.page - 1) * options.itemsPerPage,
limit: options.itemsPerPage,
orderBy: options.sortBy.map((sb,ix) => [sb, options.sortDesc[ix] ? 'desc' : 'asc'])
}))
}
}
I’ve just recently started using Vue and so far so good, but I’ve ran into a bit of an issue that I can’t figure out a good solution to.
I have a photo gallery with a few different sections. I have an overall gallery component, a gallery section component and an image component. Essentially, I’m using a photos array for each section to store the photos data for that section. Within the sections I’m using v-for to display the photos. The gallery is infinitely scrolling so when you scroll to the bottom, more images load and the photos array for that section is updated.
Here’s my problem, currently the photos arrays are stored on the data of the overall gallery component, so when I update one of the photos arrays it seems to cause the entire gallery to rerender. The more images on the screen, the worse effect this has on the performance and the less responsive the page becomes.
I’m aware I could move the photos array to the data of the individual sections, but as far as I can tell this would still rerender that entire section.
I don’t really know if there’s any good solution that’ll do what I’m trying to do, having a certain amount of reactivity but only updating the things that changed. I don’t know if something like that is possible.
I’ve tried messing around with computed data, props, methods etc. but I can’t work out a better solution.
Here’s the code I’ve been working with in the overall gallery component:
<template>
<div class="photo-gallery">
<gallery-section v-for="(section, index) in sections" v-bind:section="section" class="photo-gallery__section" v-bind:key="index"></gallery-section>
</div>
</template>
<script>
import * as qb from "../queryBuilder";
let layout = [
{
title: "Highlighted Photos",
request: {
filters: qb.group([
qb.filter("rating", ">=", 4),
]),
options: {
offset: 0,
limit: 2,
order: ["rand()"],
size: 740
}
},
total: 2,
photoClass: "photo--highlighted",
loading: false,
photos: []
},
{
title: "More photos",
request: {
filters: qb.group([
qb.filter("rating", ">=", 2),
]),
options: {
offset: 0,
limit: 40,
order: ["rand()"]
}
},
total: Infinity,
loading: false,
photos: []
}
];
export default {
data() {
return {
sections: layout,
currentSection: 0
}
},
mounted() {
this.getPhotos(this.sections[0]);
this.getPhotos(this.sections[1]);
},
methods: {
getPhotos(section) {
section.loading = true;
let currentSection = this.currentSection;
fetch(`api/photos/search/filter/${JSON.stringify(section.request.filters)}/${JSON.stringify(section.request.options)}`)
.then(response => response.json())
.then(response => {
section.photos.push(...response.images);
// Set offset for next request
this.sections[this.currentSection].request.options.offset = section.photos.length;
// Check if current section is complete or if less than the requested amount of photos was returned
if (
this.sections[this.currentSection].total === section.photos.length ||
response.images.length < this.sections[this.currentSection].request.options.limit
) {
if (this.sections.length -1 != this.currentSection) {
// Move onto next section
this.currentSection++;
} else {
// Set currentSection to null if all sections have been fully loaded
this.currentSection = null;
}
}
})
.finally(() => {
section.loading = false;
});
},
scrollHandler() {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500) {
if (this.currentSection != null && !this.sections[this.currentSection].loading) {
this.getPhotos(this.sections[this.currentSection]);
}
}
}
},
created() {
window.addEventListener("scroll", this.scrollHandler);
},
destroyed() {
window.removeEventListener("scroll", this.scrollHandler);
}
}
</script>
One thing I've also noticed is that whenever more photos are loaded, the mount function for every photo component on the page runs.
Would anyone be able to point me in the right direction? Any advise would be very much appreciated.
Thank you, Jason.
The issue was the the way I was generating the keys for my photos component instances, which cannot be seen in the code I included above. I figured out that the random number being generated as the key meant Vue could not keep track of the element as the key would keep changing. I'm now generating unique keys on the server side and using them instead. It works as expected now.
I have a highmaps 'chart' and the only thing that I want is to redraw the whole map inside an external function. Let me explain better. The map draws itself immediatly when the page loads up but I fetch some data from an external service and set it to a variable. Then I would like to just redraw the chart so that the new data appears in the map itself. Below is my code.
<template>
<div>
<highmaps :options="chartOptions"></highmaps>
</div>
</template>
<script>
import axios from 'axios';
import HighCharts from 'vue-highcharts';
import json from '../map.json'
let regions = [];
export default {
data: function () {
return {
chartOptions: {
chart: {
map: json, // The map data is taken from the .json file imported above
},
map: {
/* hc-a2 is the specific code used, you can find all codes in the map.json file */
joinBy: ['hc-key', 'code'],
allAreas: false,
tooltip: {
headerFormat: '',
pointFormat: '{point.name}: <b>{series.name}</b>'
},
series: [
{
borderColor: '#a0451c',
cursor: 'pointer',
name: 'ERROR',
color: "red",
data: regions.map(function (code) {
return {code: code};
}),
}
],
}
},
created: function(){
let app = this;
/* Ajax call to get all parameters from database */
axios.get('http://localhost:8080/devices')
.then(function (response) {
region.push(response.parameter)
/* I would like to redraw the chart right here */
}).catch(function (error){
console.error("Download Devices ERROR: " + error);
})
}
}
</script>
As you can see I import my map and the regions variable is set to an empty array. Doing this results in the map having only the borders and no region is colored in red. After that there is the created:function() function that is used to make the ajax call and retrieve data. After that I just save the data pushing it into the array and then obviously nothing happens but I would like to redraw the map so that the newly imported data will be shown. Down here is the image of what I would like to create.
If you have any idea on how to implement a thing like this or just want to suggest a better way of handling the problem, please comment.
Thanks in advance for the help. Cheers!
After a few days without any answer I found some marginal help online and came to a pretty satisfying conclusion on this problem so I hope it can help someone else.
So the first thing I did was to understand how created and mounted were different in Vue.js. I used the keyword created at first when working on this project. Because of that, inside this function, I placed my ajax call that gave me data which I then loaded inside the 'chart' by using the .addSeries method of the chart itself.
To reference the chart itself I used this: let chart: this.$refs.highcharts.chart. This searches for the field refs in any of your components/html elements and links it to the variable. So in the html there was something like this:
<template>
<div>
<highmaps :options="chartOptions" ref="highcharts"></highmaps>
</div>
</template>
The real problem was that the chart didn't even start rendering while all this process was going on so I changed the created keyword with mounted which means that it executes all the code when all of the components are correctly mounted and so my chart would be already rendered.
To give you (maybe) a better idea of what I am talking about I will post some code down below
mounted: function(){
let errorRegions = [];
let chart = this.$refs.highcharts.chart;
axios.get('localhost:8080/foo').then(function(response)
{
/* Code to work on data */
response.forEach(function(device){
errorRegions.push(device);
}
chart.addSeries({
name: "ERROR",
color: "red",
data: errorRegions
}
/* ...Some more code... */
})
}
And this is the result (have been adding some more series in the same exact manner)
Really hoping I have been of help to someone else. Cheers!
I want to build a app shows up the follow board.
So I create a custom html apps, the source code is listed here.
<!DOCTYPE html>
<html>
<head>
<title>StoryMap</title>
<script type="text/javascript" src="/apps/2.0/sdk-debug.js"></script>
<script type="text/javascript">
Rally.onReady(function () {
Ext.define('mycardcolumnheader', {
extend: Rally.ui.cardboard.ColumnHeader,
alias: 'widget.mycardcolumnheader',
});
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
//Fetch tree of items
// a) starting from item type in picker
// b) subject to a filter
//The number of columns is related to the number of lowest level PI type items that are found
//The header must show the top level (from the picker) and all the children
//The user stories are shown in a vertical line below
//Might want to introduce the concept of timebox in the vertical direction (for user stories)
//The cards for the PIs should show the progress bars
var ch = Ext.create( 'Ext.Container', {
});
var dTab = Ext.create('Ext.Container', {
items: [{
xtype: 'rallycardboard',
types: ['HierarchicalRequirement'],
// columnConfig: { xtype: 'mycardcolumn', displayField: 'feat', valueField: 'fest' },
// attribute: 'ScheduleState'
attribute: 'Feature',
rowConfig: { field: 'Release', sortDirection: 'ASC' },
enableDragAndDrop: true
}]
});
this.add(dTab);
}
});
Rally.launchApp('CustomApp', {
name:"StoryMap",
parentRepos:""
});
});
</script>
<style type="text/css">
.app {
/* Add app styles here */
}
</style>
</head>
<body>
</body>
</html>
It's very similar to what I want, but still have a small bug, I do not know how to fix it. the bug is if I choose the parent project, the board will show up the same release into multiple swimlanes. Here is a example.
this is probably because each Rally Project has its own Release and the App is not smart enough to recognize they are all logically the same Release.
If create Release 1 at the Parent Project level and cascade the Release to the child projects, Rally actually creates 3 Releases ... 1 for each project. Our application happens to know that if the Release name, start date, and end dates match, they should be treated as a single Release. It appears the App does not have this logic included in it.
But how to fix it? anyone could take a look?
This is sort of expected behavior, mostly due to the fact that we never built the ability for the board to "bucket" the like releases into its rows or columns.
You'll probably need to override the isMatchingRecord method on Rally.ui.cardboard.row.Row to be able to bucket the rows:
//add this at the top of your app
Ext.define('Rally.ui.cardboard.row.RowFix', {
override: 'Rally.ui.cardboard.row.Row',
isMatchingRecord: function(record) {
return this.callParent(arguments) ||
this.getValue().Name === (record.get('Release') && record.get('Release').Name);
}
});
I have what I think is a simple scenario where I have to generate multiple textareas with RTE capability. I am using TinyMce which works marvelously if I only have one textarea, but the others don't. I have created a simple example MVC 4 app to try to get it all working before migrating my new knowledge to the real app. There are other items on this page that are all editable so it appears that the problem might stem from the html helper. Or from the fact that the resultant html shows that all three textareas have the same id. However, since the code doesn't obviously reference the id I didn't think I would matter. Anyone know for sure?
I have a simple model:
TextModel text = new TextModel();
text.P1 = "This is an editable element.";
I have included TinyMce in my BundleConfig file, then in my _Layout. Then I have a strongly typed view.
#model TinyMCE_lite.Models.TextModel
And a script section to expand my textareas on focus:
<script>
$(window).load(function () {
$('textarea.expand').focus(function () {
$(this).addClass("expanding")
$(this).animate({
height: "10em"
}, 200);
});
$('textarea.expand').blur(function () {
$(this).animate({
height: "28px"
}, 100);
$(this).removeClass("expanding")
});
});
Then I crank out three in a loop:
#using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
<fieldset>
<h1 class="editable">Editable header</h1>
#for (int count = 0; count < 3; count++ )
{
int index = count + 1;
<h3>#index</h3>
<p class="editable">#Html.DisplayFor(model => model.P1)</p>
#Html.TextAreaFor(model => model.P2, 0,0, new { #class = "expand" })
}
<div style="margin-top: 25px;"><input type="submit" value="Save" /></div>
</fieldset>
}
The first one acts as expected, showing the editor elements, but not the others. All three expand as expect. Hopefully I have overlooked something simple. Any ideas?
Wow! Turns out it was the identical ids. All I had to do was create unique ids and it works nicely.
I changed this:
#Html.TextAreaFor(model => model.P2, 0,0, new { #class = "expand" })
to this:
#Html.TextAreaFor(model => model.P2, 0,0, new { #class = "expand", id = #count })
Hopefully this will prevent someone else from hours of agony trying to figure this out.